diff --git a/apps/server/src/routes/api/llm.ts b/apps/server/src/routes/api/llm.ts index f13547410..e7ee06448 100644 --- a/apps/server/src/routes/api/llm.ts +++ b/apps/server/src/routes/api/llm.ts @@ -561,13 +561,9 @@ async function handleStreamingProcess( const aiServiceManager = await import('../../services/llm/ai_service_manager.js'); await aiServiceManager.default.getOrCreateAnyService(); - // Use the chat pipeline directly for streaming - const { ChatPipeline } = await import('../../services/llm/pipeline/chat_pipeline.js'); - const pipeline = new ChatPipeline({ - enableStreaming: true, - enableMetrics: true, - maxToolCallIterations: 5 - }); + // Use the simplified chat pipeline directly for streaming + const simplifiedPipeline = await import('../../services/llm/pipeline/simplified_pipeline.js'); + const pipeline = simplifiedPipeline.default; // Get selected model const { getSelectedModelConfig } = await import('../../services/llm/config/configuration_helpers.js'); diff --git a/apps/server/src/services/llm/ai_interface.ts b/apps/server/src/services/llm/ai_interface.ts index d40f780e0..6e089a6e8 100644 --- a/apps/server/src/services/llm/ai_interface.ts +++ b/apps/server/src/services/llm/ai_interface.ts @@ -1,5 +1,8 @@ -import type { ToolCall } from './tools/tool_interfaces.js'; import type { ModelMetadata } from './providers/provider_options.js'; +import type { ToolCall } from './tools/tool_interfaces.js'; + +// Re-export ToolCall so it's available from this module +export type { ToolCall } from './tools/tool_interfaces.js'; /** * Interface for chat messages between client and LLM models diff --git a/apps/server/src/services/llm/ai_service_manager.ts b/apps/server/src/services/llm/ai_service_manager.ts index f761e2e6d..347b907d8 100644 --- a/apps/server/src/services/llm/ai_service_manager.ts +++ b/apps/server/src/services/llm/ai_service_manager.ts @@ -754,13 +754,22 @@ export class AIServiceManager implements IAIServiceManager, Disposable { return 'openai'; } + /** + * Check if a service cache entry is stale + */ + private isServiceStale(entry: ServiceCacheEntry): boolean { + const now = Date.now(); + return now - entry.lastUsed > this.SERVICE_TTL_MS; + } + /** * Check if a specific provider is available */ isProviderAvailable(provider: string): boolean { - // Check if this is the current provider and if it's available - if (this.currentProvider === provider && this.currentService) { - return this.currentService.isAvailable(); + // Check if we have a cached service for this provider + const cachedEntry = this.serviceCache.get(provider as ServiceProviders); + if (cachedEntry && !this.isServiceStale(cachedEntry)) { + return cachedEntry.service.isAvailable(); } // For other providers, check configuration @@ -784,8 +793,9 @@ export class AIServiceManager implements IAIServiceManager, Disposable { * Get metadata about a provider */ getProviderMetadata(provider: string): ProviderMetadata | null { - // Only return metadata if this is the current active provider - if (this.currentProvider === provider && this.currentService) { + // Check if we have a cached service for this provider + const cachedEntry = this.serviceCache.get(provider as ServiceProviders); + if (cachedEntry && !this.isServiceStale(cachedEntry)) { return { name: provider, capabilities: { diff --git a/apps/server/src/services/llm/interfaces/message_formatter.ts b/apps/server/src/services/llm/interfaces/message_formatter.ts index 3ec387d0a..23f34d131 100644 --- a/apps/server/src/services/llm/interfaces/message_formatter.ts +++ b/apps/server/src/services/llm/interfaces/message_formatter.ts @@ -1,7 +1,4 @@ import type { Message } from "../ai_interface.js"; -// These imports need to be added for the factory to work -import { OpenAIMessageFormatter } from "../formatters/openai_formatter.js"; -import { OllamaMessageFormatter } from "../formatters/ollama_formatter.js"; /** * Interface for provider-specific message formatters @@ -34,6 +31,41 @@ export interface MessageFormatter { getMaxContextLength(): number; } +/** + * Default message formatter implementation + */ +class DefaultMessageFormatter implements MessageFormatter { + formatMessages(messages: Message[], systemPrompt?: string, context?: string): Message[] { + const formattedMessages: Message[] = []; + + // Add system prompt if provided + if (systemPrompt || context) { + const systemContent = [systemPrompt, context].filter(Boolean).join('\n\n'); + if (systemContent) { + formattedMessages.push({ + role: 'system', + content: systemContent + }); + } + } + + // Add the rest of the messages + formattedMessages.push(...messages); + + return formattedMessages; + } + + cleanContextContent(content: string): string { + // Basic cleanup: trim and remove excessive whitespace + return content.trim().replace(/\n{3,}/g, '\n\n'); + } + + getMaxContextLength(): number { + // Default to a reasonable context length + return 10000; + } +} + /** * Factory to get the appropriate message formatter for a provider */ @@ -69,23 +101,9 @@ export class MessageFormatterFactory { return this.formatters[providerKey]; } - // Create and cache new formatter - switch (providerKey) { - case 'openai': - this.formatters[providerKey] = new OpenAIMessageFormatter(); - break; - case 'anthropic': - console.warn('Anthropic formatter not available, using OpenAI formatter as fallback'); - this.formatters[providerKey] = new OpenAIMessageFormatter(); - break; - case 'ollama': - this.formatters[providerKey] = new OllamaMessageFormatter(); - break; - default: - // Default to OpenAI formatter for unknown providers - console.warn(`No specific formatter for provider: ${providerName}. Using OpenAI formatter as default.`); - this.formatters[providerKey] = new OpenAIMessageFormatter(); - } + // For now, all providers use the default formatter + // In the future, we can add provider-specific formatters here + this.formatters[providerKey] = new DefaultMessageFormatter(); return this.formatters[providerKey]; } diff --git a/apps/server/src/services/llm/pipeline/configuration_service.ts b/apps/server/src/services/llm/pipeline/configuration_service.ts index eb4df8f0d..5927bcb2c 100644 --- a/apps/server/src/services/llm/pipeline/configuration_service.ts +++ b/apps/server/src/services/llm/pipeline/configuration_service.ts @@ -8,8 +8,8 @@ * - No scattered options.getOption() calls */ -import options from '../../../options.js'; -import log from '../../../log.js'; +import options from '../../options.js'; +import log from '../../log.js'; import type { ChatCompletionOptions } from '../ai_interface.js'; // Configuration interfaces @@ -108,9 +108,9 @@ export class ConfigurationService { this.lastLoadTime = Date.now(); if (!this.validationResult.valid) { - log.error('Configuration validation failed', this.validationResult.errors); + log.error(`Configuration validation failed: ${JSON.stringify(this.validationResult.errors)}`); } else if (this.validationResult.warnings.length > 0) { - log.warn('Configuration warnings', this.validationResult.warnings); + log.info(`[WARN] Configuration warnings: ${JSON.stringify(this.validationResult.warnings)}`); } else { log.info('Configuration loaded and validated successfully'); } @@ -146,43 +146,43 @@ export class ConfigurationService { // Default configuration const defaults: DefaultConfiguration = { - systemPrompt: options.getOption('llmSystemPrompt') || 'You are a helpful AI assistant.', - temperature: this.parseFloat(options.getOption('llmTemperature'), 0.7), - maxTokens: this.parseInt(options.getOption('llmMaxTokens'), 2000), - topP: this.parseFloat(options.getOption('llmTopP'), 0.9), - presencePenalty: this.parseFloat(options.getOption('llmPresencePenalty'), 0), - frequencyPenalty: this.parseFloat(options.getOption('llmFrequencyPenalty'), 0) + systemPrompt: (options as any).getOptionOrNull('llmSystemPrompt') || 'You are a helpful AI assistant.', + temperature: this.parseFloat((options as any).getOptionOrNull('llmTemperature'), 0.7), + maxTokens: this.parseInt((options as any).getOptionOrNull('llmMaxTokens'), 2000), + topP: this.parseFloat((options as any).getOptionOrNull('llmTopP'), 0.9), + presencePenalty: this.parseFloat((options as any).getOptionOrNull('llmPresencePenalty'), 0), + frequencyPenalty: this.parseFloat((options as any).getOptionOrNull('llmFrequencyPenalty'), 0) }; // Tool configuration const tools: ToolConfiguration = { - enabled: options.getOptionBool('llmToolsEnabled') !== false, - maxIterations: this.parseInt(options.getOption('llmMaxToolIterations'), 5), - timeout: this.parseInt(options.getOption('llmToolTimeout'), 30000), - parallelExecution: options.getOptionBool('llmParallelTools') !== false + enabled: (options as any).getOptionBool('llmToolsEnabled') !== false, + maxIterations: this.parseInt((options as any).getOptionOrNull('llmMaxToolIterations'), 5), + timeout: this.parseInt((options as any).getOptionOrNull('llmToolTimeout'), 30000), + parallelExecution: (options as any).getOptionBool('llmParallelTools') !== false }; // Streaming configuration const streaming: StreamingConfiguration = { - enabled: options.getOptionBool('llmStreamingEnabled') !== false, - chunkSize: this.parseInt(options.getOption('llmStreamChunkSize'), 256), - flushInterval: this.parseInt(options.getOption('llmStreamFlushInterval'), 100) + enabled: (options as any).getOptionBool('llmStreamingEnabled') !== false, + chunkSize: this.parseInt((options as any).getOptionOrNull('llmStreamChunkSize'), 256), + flushInterval: this.parseInt((options as any).getOptionOrNull('llmStreamFlushInterval'), 100) }; // Debug configuration const debug: DebugConfiguration = { - enabled: options.getOptionBool('llmDebugEnabled'), + enabled: (options as any).getOptionBool('llmDebugEnabled'), logLevel: this.getLogLevel(), - enableMetrics: options.getOptionBool('llmMetricsEnabled'), - enableTracing: options.getOptionBool('llmTracingEnabled') + enableMetrics: (options as any).getOptionBool('llmMetricsEnabled'), + enableTracing: (options as any).getOptionBool('llmTracingEnabled') }; // Limit configuration const limits: LimitConfiguration = { - maxMessageLength: this.parseInt(options.getOption('llmMaxMessageLength'), 100000), - maxConversationLength: this.parseInt(options.getOption('llmMaxConversationLength'), 50), - maxContextLength: this.parseInt(options.getOption('llmMaxContextLength'), 10000), - rateLimitPerMinute: this.parseInt(options.getOption('llmRateLimitPerMinute'), 60) + maxMessageLength: this.parseInt((options as any).getOptionOrNull('llmMaxMessageLength'), 100000), + maxConversationLength: this.parseInt((options as any).getOptionOrNull('llmMaxConversationLength'), 50), + maxContextLength: this.parseInt((options as any).getOptionOrNull('llmMaxContextLength'), 10000), + rateLimitPerMinute: this.parseInt((options as any).getOptionOrNull('llmRateLimitPerMinute'), 60) }; return { @@ -199,14 +199,14 @@ export class ConfigurationService { * Load OpenAI configuration */ private loadOpenAIConfig() { - const apiKey = options.getOption('openaiApiKey'); + const apiKey = options.getOption('openaiApiKey' as any); if (!apiKey) return undefined; return { apiKey, - baseUrl: options.getOption('openaiBaseUrl') || undefined, - defaultModel: options.getOption('openaiDefaultModel') || 'gpt-4-turbo-preview', - maxTokens: this.parseInt(options.getOption('openaiMaxTokens'), 4096) + baseUrl: options.getOption('openaiBaseUrl' as any) || undefined, + defaultModel: options.getOption('openaiDefaultModel' as any) || 'gpt-4-turbo-preview', + maxTokens: this.parseInt(options.getOption('openaiMaxTokens' as any), 4096) }; } @@ -214,14 +214,14 @@ export class ConfigurationService { * Load Anthropic configuration */ private loadAnthropicConfig() { - const apiKey = options.getOption('anthropicApiKey'); + const apiKey = options.getOption('anthropicApiKey' as any); if (!apiKey) return undefined; return { apiKey, - baseUrl: options.getOption('anthropicBaseUrl') || undefined, - defaultModel: options.getOption('anthropicDefaultModel') || 'claude-3-opus-20240229', - maxTokens: this.parseInt(options.getOption('anthropicMaxTokens'), 4096) + baseUrl: options.getOption('anthropicBaseUrl' as any) || undefined, + defaultModel: options.getOption('anthropicDefaultModel' as any) || 'claude-3-opus-20240229', + maxTokens: this.parseInt(options.getOption('anthropicMaxTokens' as any), 4096) }; } @@ -229,13 +229,13 @@ export class ConfigurationService { * Load Ollama configuration */ private loadOllamaConfig() { - const baseUrl = options.getOption('ollamaBaseUrl'); + const baseUrl = options.getOption('ollamaBaseUrl' as any); if (!baseUrl) return undefined; return { baseUrl, - defaultModel: options.getOption('ollamaDefaultModel') || 'llama2', - maxTokens: this.parseInt(options.getOption('ollamaMaxTokens'), 2048) + defaultModel: options.getOption('ollamaDefaultModel' as any) || 'llama2', + maxTokens: this.parseInt(options.getOption('ollamaMaxTokens' as any), 2048) }; } @@ -262,13 +262,13 @@ export class ConfigurationService { errors.push(`Configuration missing for selected provider: ${config.providers.selected}`); } else { // Provider-specific validation - if (config.providers.selected === 'openai' && !selectedConfig.apiKey) { + if (config.providers.selected === 'openai' && !('apiKey' in selectedConfig && selectedConfig.apiKey)) { errors.push('OpenAI API key is required'); } - if (config.providers.selected === 'anthropic' && !selectedConfig.apiKey) { + if (config.providers.selected === 'anthropic' && !('apiKey' in selectedConfig && selectedConfig.apiKey)) { errors.push('Anthropic API key is required'); } - if (config.providers.selected === 'ollama' && !selectedConfig.baseUrl) { + if (config.providers.selected === 'ollama' && !('baseUrl' in selectedConfig && selectedConfig.baseUrl)) { errors.push('Ollama base URL is required'); } } @@ -304,7 +304,7 @@ export class ConfigurationService { * Get selected provider */ private getSelectedProvider(): 'openai' | 'anthropic' | 'ollama' | null { - const provider = options.getOption('aiSelectedProvider'); + const provider = options.getOption('aiSelectedProvider' as any); if (provider === 'openai' || provider === 'anthropic' || provider === 'ollama') { return provider; } @@ -315,7 +315,7 @@ export class ConfigurationService { * Get log level */ private getLogLevel(): 'error' | 'warn' | 'info' | 'debug' { - const level = options.getOption('llmLogLevel') || 'info'; + const level = options.getOption('llmLogLevel' as any) || 'info'; if (level === 'error' || level === 'warn' || level === 'info' || level === 'debug') { return level; } @@ -347,7 +347,7 @@ export class ConfigurationService { if (!this.config || Date.now() - this.lastLoadTime > this.CACHE_DURATION) { // Reload configuration if cache expired this.initialize().catch(error => { - log.error('Failed to reload configuration', error); + log.error(`Failed to reload configuration: ${error instanceof Error ? error.message : String(error)}`); }); } @@ -416,10 +416,10 @@ export class ConfigurationService { const defaults = this.getDefaultConfig(); return { temperature: defaults.temperature, - max_tokens: defaults.maxTokens, - top_p: defaults.topP, - presence_penalty: defaults.presencePenalty, - frequency_penalty: defaults.frequencyPenalty + maxTokens: defaults.maxTokens, + topP: defaults.topP, + presencePenalty: defaults.presencePenalty, + frequencyPenalty: defaults.frequencyPenalty }; } diff --git a/apps/server/src/services/llm/pipeline/logging_service.ts b/apps/server/src/services/llm/pipeline/logging_service.ts index 0c836bb8f..41ec56873 100644 --- a/apps/server/src/services/llm/pipeline/logging_service.ts +++ b/apps/server/src/services/llm/pipeline/logging_service.ts @@ -8,7 +8,7 @@ * - No production debug statements */ -import log from '../../../log.js'; +import log from '../../log.js'; import configurationService from './configuration_service.js'; // Log levels @@ -128,14 +128,20 @@ export class LoggingService { switch (entry.level) { case LogLevel.ERROR: if (entry.error) { - log.error(formattedMessage, entry.error); + log.error(`${formattedMessage}: ${entry.error instanceof Error ? entry.error.message : String(entry.error)}`); + } else if (entry.data) { + log.error(`${formattedMessage}: ${JSON.stringify(entry.data)}`); } else { - log.error(formattedMessage, entry.data); + log.error(formattedMessage); } break; case LogLevel.WARN: - log.warn(formattedMessage, entry.data); + if (entry.data && Object.keys(entry.data).length > 0) { + log.info(`[WARN] ${formattedMessage} - ${JSON.stringify(entry.data)}`); + } else { + log.info(`[WARN] ${formattedMessage}`); + } break; case LogLevel.INFO: diff --git a/apps/server/src/services/llm/pipeline/model_registry.ts b/apps/server/src/services/llm/pipeline/model_registry.ts index e612a5838..4dca5a3c5 100644 --- a/apps/server/src/services/llm/pipeline/model_registry.ts +++ b/apps/server/src/services/llm/pipeline/model_registry.ts @@ -8,7 +8,7 @@ * - Performance characteristics */ -import log from '../../../log.js'; +import log from '../../log.js'; // Model capability interfaces export interface ModelCapabilities { @@ -354,7 +354,7 @@ export class ModelRegistry { registerModel(model: ModelInfo): void { const key = `${model.provider}:${model.id}`; this.models.set(key, model); - log.debug(`Registered model: ${key}`); + log.info(`Registered model: ${key}`); } /** @@ -412,15 +412,16 @@ export class ModelRegistry { if (constraints?.requiresStreaming) { candidates = candidates.filter(m => m.capabilities.supportsStreaming); } - if (constraints?.minContextWindow) { - candidates = candidates.filter(m => m.capabilities.contextWindow >= constraints.minContextWindow); + if (constraints?.minContextWindow !== undefined) { + const minWindow = constraints.minContextWindow; + candidates = candidates.filter(m => m.capabilities.contextWindow >= minWindow); } // Filter by cost if (constraints?.maxCost !== undefined) { candidates = candidates.filter(m => { if (!m.cost) return true; // Local models have no cost - return m.cost.inputTokens <= constraints.maxCost; + return m.cost.inputTokens <= constraints.maxCost!; }); } diff --git a/apps/server/src/services/llm/pipeline/simplified_pipeline.ts b/apps/server/src/services/llm/pipeline/simplified_pipeline.ts index b73834fa4..0173b484f 100644 --- a/apps/server/src/services/llm/pipeline/simplified_pipeline.ts +++ b/apps/server/src/services/llm/pipeline/simplified_pipeline.ts @@ -177,12 +177,12 @@ export class SimplifiedChatPipeline { } // Execute LLM call - const service = aiServiceManager.getService(); + const service = await aiServiceManager.getService(); if (!service) { throw new Error('No AI service available'); } - const response = await service.chat(messages, options); + const response = await service.generateChatCompletion(messages, options); this.recordMetric('llm_execution', Date.now() - startTime); logger.log(LogLevel.DEBUG, 'Stage 2: LLM execution completed', { @@ -249,12 +249,12 @@ export class SimplifiedChatPipeline { enableTools: true }; - const service = aiServiceManager.getService(); + const service = await aiServiceManager.getService(); if (!service) { throw new Error('No AI service available'); } - currentResponse = await service.chat(currentMessages, followUpOptions); + currentResponse = await service.generateChatCompletion(currentMessages, followUpOptions); // Check if we need another iteration if (!currentResponse.tool_calls?.length) { @@ -302,9 +302,8 @@ export class SimplifiedChatPipeline { response.text = accumulatedText; } - // Add metadata - response.metadata = { - ...response.metadata, + // Add metadata to response (cast to any to add extra properties) + (response as any).metadata = { requestId: logger.requestId, processingTime: Date.now() - startTime }; @@ -325,7 +324,7 @@ export class SimplifiedChatPipeline { toolCalls: ToolCall[], logger: ReturnType ): Promise> { - const results = []; + const results: Array<{ toolCallId: string; content: string }> = []; for (const toolCall of toolCalls) { try { @@ -334,17 +333,20 @@ export class SimplifiedChatPipeline { throw new Error(`Tool not found: ${toolCall.function.name}`); } - const args = JSON.parse(toolCall.function.arguments || '{}'); + const argsString = typeof toolCall.function.arguments === 'string' + ? toolCall.function.arguments + : JSON.stringify(toolCall.function.arguments || {}); + const args = JSON.parse(argsString); const result = await tool.execute(args); results.push({ - toolCallId: toolCall.id, + toolCallId: toolCall.id || `tool_${Date.now()}`, content: typeof result === 'string' ? result : JSON.stringify(result) }); logger.log(LogLevel.DEBUG, 'Tool executed successfully', { tool: toolCall.function.name, - toolCallId: toolCall.id + toolCallId: toolCall.id || 'no-id' }); } catch (error) { @@ -354,7 +356,7 @@ export class SimplifiedChatPipeline { }); results.push({ - toolCallId: toolCall.id, + toolCallId: toolCall.id || `tool_error_${Date.now()}`, content: `Error: ${error instanceof Error ? error.message : String(error)}` }); } @@ -371,7 +373,16 @@ export class SimplifiedChatPipeline { // This is a simplified context extraction // In production, this would call the semantic search service const contextService = await import('../context/services/context_service.js'); - return await contextService.default.getContextForQuery(query, noteId); + const results = await contextService.default.findRelevantNotes(query, noteId, { + maxResults: 5, + summarize: true + }); + + // Format results as context string + if (results && results.length > 0) { + return results.map(r => `${r.title}: ${r.content}`).join('\n\n'); + } + return null; } catch (error) { loggingService.log(LogLevel.ERROR, 'Context extraction failed', { error }); return null; diff --git a/apps/server/src/services/llm/providers/ollama_service.ts b/apps/server/src/services/llm/providers/ollama_service.ts index 4ebbbaa4b..c9932edaf 100644 --- a/apps/server/src/services/llm/providers/ollama_service.ts +++ b/apps/server/src/services/llm/providers/ollama_service.ts @@ -1,6 +1,5 @@ import { BaseAIService } from '../base_ai_service.js'; import type { Message, ChatCompletionOptions, ChatResponse, StreamChunk } from '../ai_interface.js'; -import { OllamaMessageFormatter } from '../formatters/ollama_formatter.js'; import log from '../../log.js'; import type { ToolCall, Tool } from '../tools/tool_interfaces.js'; import toolRegistry from '../tools/tool_registry.js'; @@ -55,12 +54,10 @@ interface OllamaRequestOptions { } export class OllamaService extends BaseAIService { - private formatter: OllamaMessageFormatter; private client: Ollama | null = null; constructor() { super('Ollama'); - this.formatter = new OllamaMessageFormatter(); } override isAvailable(): boolean { @@ -147,14 +144,11 @@ export class OllamaService extends BaseAIService { // Determine if tools will be used in this request const willUseTools = providerOptions.enableTools !== false; - // Use the formatter to prepare messages - messagesToSend = this.formatter.formatMessages( - messages, - systemPrompt, - undefined, // context - providerOptions.preserveSystemPrompt, - willUseTools // Pass flag indicating if tools will be used - ); + // Format messages directly (Ollama uses OpenAI format) + messagesToSend = [ + { role: 'system', content: systemPrompt }, + ...messages + ]; log.info(`Sending to Ollama with formatted messages: ${messagesToSend.length}${willUseTools ? ' (with tool instructions)' : ''}`); } diff --git a/apps/server/src/services/llm/tools/tool_format_adapter.ts b/apps/server/src/services/llm/tools/tool_format_adapter.ts index e2fc0df59..6c9979662 100644 --- a/apps/server/src/services/llm/tools/tool_format_adapter.ts +++ b/apps/server/src/services/llm/tools/tool_format_adapter.ts @@ -60,7 +60,7 @@ export class ToolFormatAdapter { // OpenAI format matches our standard format return tools; default: - log.warn(`Unknown provider ${provider}, returning tools in standard format`); + log.info(`Warning: Unknown provider ${provider}, returning tools in standard format`); return tools; } } @@ -148,7 +148,7 @@ export class ToolFormatAdapter { // OpenAI format matches our standard format return toolCalls as ToolCall[]; default: - log.warn(`Unknown provider ${provider}, attempting standard conversion`); + log.info(`Warning: Unknown provider ${provider}, attempting standard conversion`); return toolCalls as ToolCall[]; } } @@ -265,7 +265,7 @@ export class ToolFormatAdapter { // Warn if required array is missing or empty (Anthropic may send empty inputs) if (!tool.input_schema.required || tool.input_schema.required.length === 0) { - log.warn(`Anthropic tool ${tool.name} has no required parameters - may receive empty inputs`); + log.info(`Warning: Anthropic tool ${tool.name} has no required parameters - may receive empty inputs`); } return true;