UNPKG

@juspay/neurolink

Version:

Universal AI Development Platform with working MCP integration, multi-provider support, and professional CLI. Built-in tools operational, 58+ external MCP servers discoverable. Connect to filesystem, GitHub, database operations, and more. Build, test, and

495 lines (494 loc) 23.3 kB
import { logger } from "../utils/logger.js"; import { SYSTEM_LIMITS } from "../core/constants.js"; import { directAgentTools } from "../agent/directTools.js"; /** * Validates if a result contains a valid toolsObject structure * @param result - The result object to validate * @returns true if the result contains a valid toolsObject, false otherwise */ function isValidToolsObject(result) { return (result !== null && typeof result === "object" && "toolsObject" in result && result.toolsObject !== null && typeof result.toolsObject === "object" && Object.keys(result.toolsObject).length > 0); } /** * Abstract base class for all AI providers * Tools are integrated as first-class citizens - always available by default */ export class BaseProvider { modelName; providerName; defaultTimeout = 30000; // 30 seconds // Tools are ALWAYS part of the provider - no flags, no conditions directTools = directAgentTools; mcpTools; // MCP tools loaded dynamically when available sessionId; userId; sdk; // Reference to NeuroLink SDK instance for custom tools constructor(modelName, providerName, sdk) { this.modelName = modelName || this.getDefaultModel(); this.providerName = providerName || this.getProviderName(); this.sdk = sdk; } /** * Check if this provider supports tool/function calling * Override in subclasses to disable tools for specific providers or models * @returns true by default, providers can override to return false */ supportsTools() { return true; } // =================== // PUBLIC API METHODS // =================== /** * Primary streaming method - implements AIProvider interface * When tools are involved, falls back to generate() with synthetic streaming */ async stream(optionsOrPrompt, analysisSchema) { const options = this.normalizeStreamOptions(optionsOrPrompt); // CRITICAL FIX: Always prefer real streaming over fake streaming // Try real streaming first, use fake streaming only as fallback try { const realStreamResult = await this.executeStream(options, analysisSchema); // If real streaming succeeds, return it (with tools support via Vercel AI SDK) return realStreamResult; } catch (realStreamError) { logger.warn(`Real streaming failed for ${this.providerName}, falling back to fake streaming:`, realStreamError); // Fallback to fake streaming only if real streaming fails AND tools are enabled if (!options.disableTools && this.supportsTools()) { try { // Convert stream options to text generation options const textOptions = { prompt: options.input?.text || "", systemPrompt: options.systemPrompt, temperature: options.temperature, maxTokens: options.maxTokens, disableTools: false, maxSteps: options.maxSteps || 5, provider: options.provider, model: options.model, // 🔧 FIX: Include analytics and evaluation options from stream options enableAnalytics: options.enableAnalytics, enableEvaluation: options.enableEvaluation, evaluationDomain: options.evaluationDomain, toolUsageContext: options.toolUsageContext, context: options.context, }; const result = await this.generate(textOptions, analysisSchema); // Create a synthetic stream from the generate result that simulates progressive delivery return { stream: (async function* () { if (result?.content) { // Split content into words for more natural streaming const words = result.content.split(/(\s+)/); // Keep whitespace let buffer = ""; for (let i = 0; i < words.length; i++) { buffer += words[i]; // Yield chunks of roughly 5-10 words or at punctuation const shouldYield = i === words.length - 1 || // Last word buffer.length > 50 || // Buffer getting long /[.!?;,]\s*$/.test(buffer); // End of sentence/clause if (shouldYield && buffer.trim()) { yield { content: buffer }; buffer = ""; // Small delay to simulate streaming (1-10ms) await new Promise((resolve) => setTimeout(resolve, Math.random() * 9 + 1)); } } // Yield any remaining content if (buffer.trim()) { yield { content: buffer }; } } })(), usage: result?.usage, provider: result?.provider, model: result?.model, toolCalls: result?.toolCalls?.map((call) => ({ toolName: call.toolName, parameters: call.args, id: call.toolCallId, })), toolResults: result?.toolResults ? result.toolResults.map((tr) => ({ toolName: tr.toolName || "unknown", status: (tr.status === "error" ? "failure" : "success"), result: tr.result, error: tr.error, })) : undefined, // 🔧 FIX: Include analytics and evaluation from generate result analytics: result?.analytics, evaluation: result?.evaluation, }; } catch (error) { logger.error(`Fake streaming fallback failed for ${this.providerName}:`, error); throw this.handleProviderError(error); } } else { // If real streaming failed and no tools are enabled, re-throw the original error logger.error(`Real streaming failed for ${this.providerName}:`, realStreamError); throw this.handleProviderError(realStreamError); } } } /** * Text generation method - implements AIProvider interface * Tools are always available unless explicitly disabled */ async generate(optionsOrPrompt, analysisSchema) { const options = this.normalizeTextOptions(optionsOrPrompt); const startTime = Date.now(); try { // Import generateText dynamically to avoid circular dependencies const { generateText } = await import("ai"); // Get ALL available tools (direct + MCP when available) const shouldUseTools = !options.disableTools && this.supportsTools(); const tools = shouldUseTools ? await this.getAllTools() : {}; logger.debug(`[BaseProvider.generate] Tools for ${this.providerName}: ${Object.keys(tools).join(", ")}`); // EVERY provider uses Vercel AI SDK - no exceptions const model = await this.getAISDKModel(); // This method is now REQUIRED const result = await generateText({ model, prompt: options.prompt || options.input?.text || "", system: options.systemPrompt, tools, maxSteps: options.maxSteps || 5, toolChoice: shouldUseTools ? "auto" : "none", temperature: options.temperature, maxTokens: options.maxTokens || 8192, }); // Extract tool names from tool calls for tracking // AI SDK puts tool calls in steps array for multi-step generation const toolsUsed = []; // First check direct tool calls (fallback) if (result.toolCalls && result.toolCalls.length > 0) { toolsUsed.push(...result.toolCalls.map((tc) => { return (tc.toolName || tc.name || "unknown"); })); } // Then check steps for tool calls (primary source for multi-step) if (result.steps && Array.isArray(result.steps)) { for (const step of result.steps || []) { if (step?.toolCalls && Array.isArray(step.toolCalls)) { toolsUsed.push(...step.toolCalls.map((tc) => { return tc.toolName || tc.name || "unknown"; })); } } } // Remove duplicates const uniqueToolsUsed = [...new Set(toolsUsed)]; // Format the result with tool executions included const enhancedResult = { content: result.text, usage: { inputTokens: result.usage?.promptTokens || 0, outputTokens: result.usage?.completionTokens || 0, totalTokens: result.usage?.totalTokens || 0, }, provider: this.providerName, model: this.modelName, toolCalls: result.toolCalls ? result.toolCalls.map((tc) => ({ toolCallId: tc.toolCallId || tc.id || "unknown", toolName: tc.toolName || tc.name || "unknown", args: tc.args || tc.parameters || {}, })) : [], toolResults: result.toolResults, toolsUsed: uniqueToolsUsed, }; // Enhanced result with analytics and evaluation return await this.enhanceResult(enhancedResult, options, startTime); } catch (error) { logger.error(`Generate failed for ${this.providerName}:`, error); throw this.handleProviderError(error); } } /** * Alias for generate method - implements AIProvider interface */ async gen(optionsOrPrompt, analysisSchema) { return this.generate(optionsOrPrompt, analysisSchema); } // =================== // TOOL MANAGEMENT // =================== /** * Get all available tools - direct tools are ALWAYS available * MCP tools are added when available (without blocking) */ async getAllTools() { const tools = { ...this.directTools, // Always include direct tools }; logger.debug(`[BaseProvider] getAllTools called, SDK available: ${!!this.sdk}, type: ${typeof this.sdk}`); logger.debug(`[BaseProvider] Direct tools: ${Object.keys(this.directTools).join(", ")}`); // Add custom tools from SDK if available logger.debug(`[BaseProvider] Checking SDK: ${!!this.sdk}, has getInMemoryServers: ${this.sdk && typeof this.sdk.getInMemoryServers}`); if (this.sdk && typeof this.sdk.getInMemoryServers === "function") { logger.debug(`[BaseProvider] SDK check passed, loading custom tools`); try { const inMemoryServers = this.sdk.getInMemoryServers(); logger.debug(`[BaseProvider] Got servers:`, inMemoryServers.size); logger.debug(`[BaseProvider] Loading custom tools from SDK, found ${inMemoryServers.size} servers`); if (inMemoryServers && inMemoryServers.size > 0) { // Convert in-memory server tools to AI SDK format for (const [serverId, serverConfig] of inMemoryServers) { const server = serverConfig.server; if (server && server.tools) { // Handle both Map and object formats const toolEntries = server.tools instanceof Map ? Array.from(server.tools.entries()) : Object.entries(server.tools || {}); for (const [toolName, toolInfo] of toolEntries) { if (toolInfo && typeof toolInfo.execute === "function") { logger.debug(`[BaseProvider] Converting custom tool: ${toolName}`); // Convert to AI SDK tool format const { tool: createAISDKTool } = await import("ai"); const { z } = await import("zod"); tools[toolName] = createAISDKTool({ description: toolInfo.description || `Tool ${toolName}`, parameters: toolInfo.inputSchema || toolInfo.parameters || z.object({}), execute: async (args) => { const result = await toolInfo.execute(args); // Handle MCP-style results if (result && typeof result === "object" && "success" in result) { if (result.success) { return result.data; } else { const errorMsg = typeof result.error === "string" ? result.error : "Tool execution failed"; throw new Error(errorMsg); } } return result; }, }); } } } } } } catch (error) { logger.debug(`Failed to load custom tools for ${this.providerName}:`, error); // Not an error - custom tools are optional } } // MCP tools loading simplified - removed functionCalling dependency if (!this.mcpTools) { // Set empty tools object - MCP tools are handled at a higher level this.mcpTools = {}; } // Add MCP tools if available if (this.mcpTools) { Object.assign(tools, this.mcpTools); } logger.debug(`[BaseProvider] getAllTools returning tools: ${Object.keys(tools).join(", ")}`); return tools; } /** * Set session context for MCP tools */ setSessionContext(sessionId, userId) { this.sessionId = sessionId; this.userId = userId; } // =================== // TEMPLATE METHODS - COMMON FUNCTIONALITY // =================== normalizeTextOptions(optionsOrPrompt) { if (typeof optionsOrPrompt === "string") { return { prompt: optionsOrPrompt, provider: this.providerName, model: this.modelName, }; } // Handle both prompt and input.text formats const prompt = optionsOrPrompt.prompt || optionsOrPrompt.input?.text || ""; return { ...optionsOrPrompt, prompt, provider: optionsOrPrompt.provider || this.providerName, model: optionsOrPrompt.model || this.modelName, }; } normalizeStreamOptions(optionsOrPrompt) { if (typeof optionsOrPrompt === "string") { return { input: { text: optionsOrPrompt }, provider: this.providerName, model: this.modelName, }; } return { ...optionsOrPrompt, provider: optionsOrPrompt.provider || this.providerName, model: optionsOrPrompt.model || this.modelName, }; } async enhanceResult(result, options, startTime) { const responseTime = Date.now() - startTime; let enhancedResult = { ...result }; if (options.enableAnalytics) { try { logger.debug(`Creating analytics for ${this.providerName}...`); const analytics = await this.createAnalytics(result, responseTime, options); logger.debug(`Analytics created:`, analytics); enhancedResult = { ...enhancedResult, analytics }; } catch (error) { logger.warn(`Analytics creation failed for ${this.providerName}:`, error); } } if (options.enableEvaluation) { try { const evaluation = await this.createEvaluation(result, options); enhancedResult = { ...enhancedResult, evaluation }; } catch (error) { logger.warn(`Evaluation creation failed for ${this.providerName}:`, error); } } return enhancedResult; } async createAnalytics(result, responseTime, options) { const { createAnalytics } = await import("./analytics.js"); return createAnalytics(this.providerName, this.modelName, result, responseTime, options.context); } async createEvaluation(result, options) { const { evaluateResponse } = await import("../core/evaluation.js"); const evaluation = await evaluateResponse(result.content, options.prompt); return evaluation; } validateOptions(options) { // 🔧 EDGE CASE: Basic prompt validation if (!options.prompt || options.prompt.trim().length === 0) { throw new Error("Prompt is required and cannot be empty"); } // 🔧 EDGE CASE: Handle very large prompts (>1M characters) if (options.prompt.length > SYSTEM_LIMITS.MAX_PROMPT_LENGTH) { throw new Error(`Prompt too large: ${options.prompt.length} characters (max: ${SYSTEM_LIMITS.MAX_PROMPT_LENGTH}). Consider breaking into smaller chunks. Use BaseProvider.chunkPrompt(prompt, maxSize, overlap) static method for chunking.`); } // 🔧 EDGE CASE: Validate token limits if (options.maxTokens && options.maxTokens > 200000) { throw new Error(`Max tokens too high: ${options.maxTokens} (recommended max: 200,000). This may cause timeouts or API errors.`); } if (options.maxTokens && options.maxTokens < 1) { throw new Error("Max tokens must be at least 1"); } // 🔧 EDGE CASE: Validate temperature range if (options.temperature !== undefined) { if (options.temperature < 0 || options.temperature > 2) { throw new Error(`Temperature must be between 0 and 2, got: ${options.temperature}`); } } // 🔧 EDGE CASE: Validate timeout values if (options.timeout !== undefined) { const timeoutMs = typeof options.timeout === "string" ? parseInt(options.timeout, 10) : options.timeout; if (isNaN(timeoutMs) || timeoutMs < 1000) { throw new Error(`Timeout must be at least 1000ms (1 second), got: ${options.timeout}`); } if (timeoutMs > SYSTEM_LIMITS.LONG_TIMEOUT_WARNING) { logger.warn(`⚠️ Very long timeout: ${timeoutMs}ms. This may cause the CLI to hang.`); } } // 🔧 EDGE CASE: Validate maxSteps for tool execution if (options.maxSteps !== undefined && options.maxSteps > 20) { throw new Error(`Max steps too high: ${options.maxSteps} (recommended max: 20). This may cause long execution times.`); } } getProviderInfo() { return { provider: this.providerName, model: this.modelName, }; } /** * Get timeout value in milliseconds */ getTimeout(options) { if (!options.timeout) { return this.defaultTimeout; } if (typeof options.timeout === "number") { return options.timeout; } // Parse string timeout (e.g., '30s', '2m', '1h') const timeoutStr = options.timeout.toLowerCase(); const value = parseInt(timeoutStr); if (timeoutStr.includes("h")) { return value * 60 * 60 * 1000; } else if (timeoutStr.includes("m")) { return value * 60 * 1000; } else if (timeoutStr.includes("s")) { return value * 1000; } return this.defaultTimeout; } /** * Utility method to chunk large prompts into smaller pieces * @param prompt The prompt to chunk * @param maxChunkSize Maximum size per chunk (default: 900,000 characters) * @param overlap Overlap between chunks to maintain context (default: 100 characters) * @returns Array of prompt chunks */ static chunkPrompt(prompt, maxChunkSize = 900000, overlap = 100) { if (prompt.length <= maxChunkSize) { return [prompt]; } const chunks = []; let start = 0; while (start < prompt.length) { const end = Math.min(start + maxChunkSize, prompt.length); chunks.push(prompt.slice(start, end)); // Break if we've reached the end if (end >= prompt.length) { break; } // Move start forward, accounting for overlap const nextStart = end - overlap; // Ensure we make progress (avoid infinite loops) if (nextStart <= start) { start = end; } else { start = Math.max(nextStart, 0); } } return chunks; } }