UNPKG

@robota-sdk/core

Version:

Core functionality for building AI agents with TypeScript - conversation management, function calling, and multi-provider support for OpenAI, Anthropic, and Google AI

1,645 lines (1,631 loc) 53.5 kB
'use strict'; var tiktoken = require('@dqbd/tiktoken'); // src/conversation-history.ts function isUserMessage(message) { return message.role === "user"; } function isAssistantMessage(message) { return message.role === "assistant"; } function isSystemMessage(message) { return message.role === "system"; } function isToolMessage(message) { return message.role === "tool"; } function createUserMessage(content, options) { return { role: "user", content, name: options?.name, timestamp: /* @__PURE__ */ new Date(), metadata: options?.metadata }; } function createAssistantMessage(content, options) { return { role: "assistant", content, toolCalls: options?.toolCalls, timestamp: /* @__PURE__ */ new Date(), metadata: options?.metadata }; } function createSystemMessage(content, options) { return { role: "system", content, name: options?.name, timestamp: /* @__PURE__ */ new Date(), metadata: options?.metadata }; } function createToolMessage(content, options) { return { role: "tool", content, toolCallId: options?.toolCallId, name: options?.name, timestamp: /* @__PURE__ */ new Date(), metadata: options?.metadata }; } var BaseConversationHistory = class { /** Maximum number of messages to store (0 = unlimited) */ maxMessages; constructor(options) { this.maxMessages = options?.maxMessages || 0; } // Common convenience methods with shared implementation addUserMessage(content, metadata) { const message = createUserMessage(content, { metadata }); this.addMessage(message); } addAssistantMessage(content, toolCalls, metadata) { const message = createAssistantMessage(content, { toolCalls, metadata }); this.addMessage(message); } addSystemMessage(content, metadata) { const message = createSystemMessage(content, { metadata }); this.addMessage(message); } addToolMessageWithId(content, toolCallId, toolName, metadata) { const message = createToolMessage(content, { toolCallId, name: toolName, metadata }); this.addMessage(message); } getMessagesByRole(role) { return this.getMessages().filter((message) => message.role === role); } getRecentMessages(count) { const messages = this.getMessages(); return messages.slice(-count); } /** * Apply message limit by removing oldest messages while preserving system messages * @internal */ applyMessageLimit(messages) { if (this.maxMessages > 0 && messages.length > this.maxMessages) { const systemMessages = messages.filter(isSystemMessage); const nonSystemMessages = messages.filter((msg) => !isSystemMessage(msg)); const availableSlots = Math.max(0, this.maxMessages - systemMessages.length); const limitedNonSystemMessages = nonSystemMessages.slice(-availableSlots); return [...systemMessages, ...limitedNonSystemMessages]; } return messages; } }; var SimpleConversationHistory = class extends BaseConversationHistory { /** @internal Array storing all messages */ messages = []; /** * Create a new SimpleConversationHistory instance * * @param options - Configuration options * @param options.maxMessages - Maximum number of messages to keep (0 = unlimited) */ constructor(options) { super(options); } /** * Add a message to conversation history * * Appends the message to the history and applies message count limits if configured. * System messages are always preserved when applying limits. * * @param message - Universal message to add */ addMessage(message) { this.messages.push(message); this._applyMessageLimit(); } /** * Get all messages in chronological order * * @returns Defensive copy of all messages */ getMessages() { return [...this.messages]; } /** * Get total message count * * @returns Number of messages currently stored */ getMessageCount() { return this.messages.length; } /** * Clear all conversation history * * Removes all messages from the history. */ clear() { this.messages = []; } /** * Apply message count limits while preserving system messages * * When maxMessages is set and exceeded, this method removes older messages * while always preserving system messages which are important for context. * * @internal */ _applyMessageLimit() { this.messages = this.applyMessageLimit(this.messages); } }; var PersistentSystemConversationHistory = class extends BaseConversationHistory { /** @internal Underlying conversation history */ history; /** @internal Current system prompt */ systemPrompt; /** * Create a new PersistentSystemConversationHistory instance * * @param systemPrompt - Initial system prompt to set * @param options - Configuration options passed to underlying SimpleConversationHistory */ constructor(systemPrompt, options) { super(options); this.history = new SimpleConversationHistory(options); this.systemPrompt = systemPrompt; this.history.addSystemMessage(this.systemPrompt); } /** * Add a message to conversation history (delegates to underlying history) * * @param message - Universal message to add */ addMessage(message) { this.history.addMessage(message); } /** * Get all messages (delegates to underlying history) * * @returns Array of all messages including system messages */ getMessages() { return this.history.getMessages(); } /** * Get total message count (delegates to underlying history) * * @returns Total number of messages including system messages */ getMessageCount() { return this.history.getMessageCount(); } /** * Clear conversation history but preserve system prompt * * Clears all messages and re-adds the current system prompt. */ clear() { this.history.clear(); this.history.addSystemMessage(this.systemPrompt); } /** * Update the system prompt and refresh system messages * * Removes old system messages, updates the system prompt, and adds * the new system prompt as a system message. * * @param systemPrompt - New system prompt to set */ updateSystemPrompt(systemPrompt) { this.systemPrompt = systemPrompt; const messages = this.history.getMessages(); const nonSystemMessages = messages.filter((msg) => !isSystemMessage(msg)); this.history.clear(); this.history.addSystemMessage(this.systemPrompt); nonSystemMessages.forEach((message) => this.history.addMessage(message)); } /** * Get the current system prompt * * @returns Current system prompt string */ getSystemPrompt() { return this.systemPrompt; } }; // src/managers/ai-provider-manager.ts var AIProviderManager = class { aiProviders = {}; currentProvider; currentModel; /** * Add an AI provider * * @param name - Provider name * @param aiProvider - AI provider instance */ addProvider(name, aiProvider) { this.aiProviders[name] = aiProvider; } /** * Set current AI provider and model * * @param providerName - Provider name * @param model - Model name */ setCurrentAI(providerName, model) { if (!this.aiProviders[providerName]) { throw new Error(`AI provider '${providerName}' not found.`); } this.currentProvider = providerName; this.currentModel = model; } /** * Get currently configured AI provider and model */ getCurrentAI() { return { provider: this.currentProvider, model: this.currentModel }; } /** * Get current AI provider instance */ getCurrentProvider() { if (!this.currentProvider) { return null; } return this.aiProviders[this.currentProvider] || null; } /** * Get current model name */ getCurrentModel() { return this.currentModel || null; } /** * Check if AI provider is configured */ isConfigured() { return !!(this.currentProvider && this.currentModel); } /** * Release resources of all AI providers */ async close() { for (const [_name, aiProvider] of Object.entries(this.aiProviders)) { if (aiProvider.close && typeof aiProvider.close === "function") { await aiProvider.close(); } } } }; // src/managers/tool-provider-manager.ts var ToolProviderManager = class { toolProviders = []; allowedFunctions; logger; constructor(logger2, allowedFunctions) { this.logger = logger2; this.allowedFunctions = allowedFunctions; } /** * Add a Tool Provider * * @param toolProvider - Tool provider instance */ addProvider(toolProvider) { this.toolProviders.push(toolProvider); } /** * Add multiple Tool Providers * * @param toolProviders - Array of tool providers */ addProviders(toolProviders) { this.toolProviders.push(...toolProviders); } /** * Set allowed function list * * @param allowedFunctions - Array of allowed function names */ setAllowedFunctions(allowedFunctions) { this.allowedFunctions = allowedFunctions; } /** * Call a tool * * @param toolName - Name of the tool to call * @param parameters - Parameters to pass to the tool * @returns Tool call result */ async callTool(toolName, parameters) { if (this.toolProviders.length === 0) { throw new Error("Tool providers are not configured."); } if (this.allowedFunctions && !this.allowedFunctions.includes(toolName)) { throw new Error(`Tool '${toolName}' is not allowed.`); } for (const toolProvider of this.toolProviders) { if (toolProvider.functions?.some((fn) => fn.name === toolName)) { try { const result = await toolProvider.callTool(toolName, parameters); return result; } catch (error) { this.logger.error(`Error calling tool '${toolName}':`, error); throw new Error(`Tool call failed: ${error instanceof Error ? error.message : String(error)}`); } } } throw new Error(`Tool '${toolName}' not found.`); } /** * Get list of available tools * * @returns List of tool schemas */ getAvailableTools() { return this.toolProviders.reduce((tools, toolProvider) => { if (toolProvider.functions) { tools.push(...toolProvider.functions); } return tools; }, []); } /** * Get the number of registered Tool Providers */ getProviderCount() { return this.toolProviders.length; } /** * Check if Tool Providers are registered */ hasProviders() { return this.toolProviders.length > 0; } /** * Check if a specific tool is available * * @param toolName - Name of the tool to check */ hasTool(toolName) { return this.toolProviders.some( (toolProvider) => toolProvider.functions?.some((fn) => fn.name === toolName) ); } /** * Clean up resources for all tool providers */ async close() { for (const toolProvider of this.toolProviders) { const provider = toolProvider; if (provider.close && typeof provider.close === "function") { try { await provider.close(); } catch (error) { this.logger.error("Error closing tool provider:", error); } } } } }; // src/managers/system-message-manager.ts var SystemMessageManager = class { systemPrompt; systemMessages; /** * Set a single system prompt * * @param prompt - System prompt content */ setSystemPrompt(prompt) { this.systemPrompt = prompt; this.systemMessages = void 0; } /** * Set multiple system messages * * @param messages - Array of system messages */ setSystemMessages(messages) { this.systemPrompt = void 0; this.systemMessages = messages; } /** * Add a new system message to existing system messages * * @param content - Content of the system message to add */ addSystemMessage(content) { if (this.systemPrompt) { if (!this.systemMessages || this.systemMessages.length === 1 && this.systemMessages[0].role === "system" && this.systemMessages[0].content === this.systemPrompt) { this.systemMessages = [ { role: "system", content: this.systemPrompt }, { role: "system", content } ]; } else { this.systemMessages.push({ role: "system", content }); } this.systemPrompt = void 0; } else { if (!this.systemMessages) { this.systemMessages = []; } this.systemMessages.push({ role: "system", content }); } } /** * Get the current system prompt */ getSystemPrompt() { return this.systemPrompt; } /** * Get the current system messages */ getSystemMessages() { return this.systemMessages; } /** * Check if system messages are configured */ hasSystemMessages() { return !!(this.systemPrompt || this.systemMessages && this.systemMessages.length > 0); } /** * Clear system messages */ clear() { this.systemPrompt = void 0; this.systemMessages = void 0; } }; // src/managers/function-call-manager.ts var FunctionCallManager = class { config; constructor(initialConfig) { this.config = { defaultMode: initialConfig?.defaultMode || "auto", maxCalls: initialConfig?.maxCalls || 10, timeout: initialConfig?.timeout || 3e4, allowedFunctions: initialConfig?.allowedFunctions }; } /** * Set function call mode * * @param mode - Function call mode ('auto', 'force', 'disabled') */ setFunctionCallMode(mode) { this.config.defaultMode = mode; } /** * Configure function call settings * * @param config - Function call configuration options */ configure(config) { if (config.mode) { this.config.defaultMode = config.mode; } if (config.maxCalls !== void 0) { this.config.maxCalls = config.maxCalls; } if (config.timeout !== void 0) { this.config.timeout = config.timeout; } if (config.allowedFunctions) { this.config.allowedFunctions = config.allowedFunctions; } } /** * Get current function call mode */ getDefaultMode() { return this.config.defaultMode; } /** * Get maximum call count */ getMaxCalls() { return this.config.maxCalls; } /** * Get timeout setting */ getTimeout() { return this.config.timeout; } /** * Get allowed functions list */ getAllowedFunctions() { return this.config.allowedFunctions; } /** * Get complete configuration */ getConfig() { return { ...this.config }; } /** * Check if a specific function is allowed * * @param functionName - Function name to check */ isFunctionAllowed(functionName) { if (!this.config.allowedFunctions) { return true; } return this.config.allowedFunctions.includes(functionName); } }; // src/managers/analytics-manager.ts var AnalyticsManager = class { requestCount = 0; totalTokensUsed = 0; tokenUsageHistory = []; /** * Record a new request and token usage * @param tokensUsed - Number of tokens used in this request * @param provider - AI provider name * @param model - Model name */ recordRequest(tokensUsed, provider, model) { this.requestCount++; this.totalTokensUsed += tokensUsed; this.tokenUsageHistory.push({ timestamp: /* @__PURE__ */ new Date(), tokens: tokensUsed, provider, model }); } /** * Get total number of requests made */ getRequestCount() { return this.requestCount; } /** * Get total number of tokens used */ getTotalTokensUsed() { return this.totalTokensUsed; } /** * Get detailed analytics data */ getAnalytics() { const averageTokensPerRequest = this.requestCount > 0 ? this.totalTokensUsed / this.requestCount : 0; return { requestCount: this.requestCount, totalTokensUsed: this.totalTokensUsed, averageTokensPerRequest: Math.round(averageTokensPerRequest * 100) / 100, tokenUsageHistory: [...this.tokenUsageHistory] }; } /** * Reset all analytics data */ reset() { this.requestCount = 0; this.totalTokensUsed = 0; this.tokenUsageHistory = []; } /** * Get token usage for a specific time period * @param startDate - Start date for the period * @param endDate - End date for the period (optional, defaults to now) */ getTokenUsageByPeriod(startDate, endDate) { const end = endDate || /* @__PURE__ */ new Date(); const filteredHistory = this.tokenUsageHistory.filter( (entry) => entry.timestamp >= startDate && entry.timestamp <= end ); const totalTokens = filteredHistory.reduce((sum, entry) => sum + entry.tokens, 0); return { totalTokens, requestCount: filteredHistory.length, usageHistory: filteredHistory }; } }; // src/managers/request-limit-manager.ts var RequestLimitManager = class { maxTokens; maxRequests; currentTokensUsed = 0; currentRequestCount = 0; constructor(maxTokens = 4096, maxRequests = 25) { this.maxTokens = maxTokens; this.maxRequests = maxRequests; } /** * Set maximum token limit (0 = unlimited) */ setMaxTokens(limit) { if (limit < 0) { throw new Error("Max token limit cannot be negative"); } this.maxTokens = limit; } /** * Set maximum request limit (0 = unlimited) */ setMaxRequests(limit) { if (limit < 0) { throw new Error("Max request limit cannot be negative"); } this.maxRequests = limit; } /** * Get current maximum token limit */ getMaxTokens() { return this.maxTokens; } /** * Get current maximum request limit */ getMaxRequests() { return this.maxRequests; } /** * Check if adding estimated tokens would exceed the limit * @param estimatedTokens - Estimated number of tokens to add * @throws Error if token limit would be exceeded */ checkEstimatedTokenLimit(estimatedTokens) { if (this.maxTokens === 0) return; if (this.currentTokensUsed + estimatedTokens > this.maxTokens) { throw new Error( `Estimated token limit would be exceeded. Current usage: ${this.currentTokensUsed}, estimated additional tokens: ${estimatedTokens}, limit: ${this.maxTokens}. Request aborted to prevent unnecessary costs.` ); } } /** * Check if adding new tokens would exceed the limit * @param tokensToAdd - Number of tokens to add * @throws Error if token limit would be exceeded */ checkTokenLimit(tokensToAdd) { if (this.maxTokens === 0) return; if (this.currentTokensUsed + tokensToAdd > this.maxTokens) { throw new Error( `Token limit exceeded. Current usage: ${this.currentTokensUsed}, attempting to add: ${tokensToAdd}, limit: ${this.maxTokens}` ); } } /** * Check if adding a new request would exceed the request limit * @throws Error if request limit would be exceeded */ checkRequestLimit() { if (this.maxRequests === 0) return; if (this.currentRequestCount + 1 > this.maxRequests) { throw new Error( `Request limit exceeded. Current requests: ${this.currentRequestCount}, limit: ${this.maxRequests}` ); } } /** * Record a successful request with token usage * @param tokensUsed - Number of tokens used in this request */ recordRequest(tokensUsed) { this.checkRequestLimit(); this.checkTokenLimit(tokensUsed); this.currentRequestCount++; this.currentTokensUsed += tokensUsed; } /** * Get current token usage */ getCurrentTokensUsed() { return this.currentTokensUsed; } /** * Get current request count */ getCurrentRequestCount() { return this.currentRequestCount; } /** * Get remaining tokens (undefined if unlimited) */ getRemainingTokens() { if (this.maxTokens === 0) return void 0; return Math.max(0, this.maxTokens - this.currentTokensUsed); } /** * Get remaining requests (undefined if unlimited) */ getRemainingRequests() { if (this.maxRequests === 0) return void 0; return Math.max(0, this.maxRequests - this.currentRequestCount); } /** * Check if tokens are unlimited */ isTokensUnlimited() { return this.maxTokens === 0; } /** * Check if requests are unlimited */ isRequestsUnlimited() { return this.maxRequests === 0; } /** * Reset usage counters (but keep limits) */ reset() { this.currentTokensUsed = 0; this.currentRequestCount = 0; } /** * Get comprehensive limit information */ getLimitInfo() { return { maxTokens: this.maxTokens, maxRequests: this.maxRequests, currentTokensUsed: this.currentTokensUsed, currentRequestCount: this.currentRequestCount, remainingTokens: this.getRemainingTokens(), remainingRequests: this.getRemainingRequests(), isTokensUnlimited: this.isTokensUnlimited(), isRequestsUnlimited: this.isRequestsUnlimited() }; } }; // src/utils.ts function removeUndefined(obj) { const result = { ...obj }; for (const key in result) { if (result[key] === void 0) { delete result[key]; } else if (typeof result[key] === "object" && result[key] !== null) { result[key] = removeUndefined(result[key]); } } return result; } var logger = { info: (...args) => { if (process.env.NODE_ENV !== "production") { console.log("[INFO]", ...args); } }, warn: (...args) => { if (process.env.NODE_ENV !== "production") { console.warn("[WARN]", ...args); } }, error: (...args) => { if (process.env.NODE_ENV !== "production") { console.error("[ERROR]", ...args); } } }; function convertUniversalToBaseMessage(universalMessage) { const baseMessage = { role: universalMessage.role === "tool" ? "function" : universalMessage.role, content: universalMessage.content || "" }; if (isUserMessage(universalMessage) && universalMessage.name) { baseMessage.name = universalMessage.name; } if (isAssistantMessage(universalMessage) && universalMessage.toolCalls) { baseMessage.toolCalls = universalMessage.toolCalls; } if (isToolMessage(universalMessage) && universalMessage.toolCallId) { baseMessage.toolCallId = universalMessage.toolCallId; baseMessage.name = universalMessage.name; } return baseMessage; } function convertUniversalToBaseMessages(universalMessages) { return universalMessages.map(convertUniversalToBaseMessage); } // src/analyzers/token-analyzer.ts var TokenAnalyzer = class { /** * Calculate tokens for a given text using tiktoken * @param text - Text to calculate tokens for * @param model - Model name for encoding selection * @returns Number of tokens */ calculateTokens(text, model) { if (!text) return 0; try { let encoding; if (model) { const modelMapping = { "gpt-4": "gpt-4", "gpt-4-0613": "gpt-4-0613", "gpt-4-32k": "gpt-4-32k", "gpt-4-32k-0613": "gpt-4-32k-0613", "gpt-4-1106-preview": "gpt-4-1106-preview", "gpt-4-0125-preview": "gpt-4-0125-preview", "gpt-4-turbo-preview": "gpt-4-0125-preview", "gpt-3.5-turbo": "gpt-3.5-turbo", "gpt-3.5-turbo-0613": "gpt-3.5-turbo-0613", "gpt-3.5-turbo-16k": "gpt-3.5-turbo-16k", "gpt-3.5-turbo-16k-0613": "gpt-3.5-turbo-16k-0613", "gpt-3.5-turbo-1106": "gpt-3.5-turbo-1106", "gpt-3.5-turbo-0125": "gpt-3.5-turbo-0125" }; const mappedModel = modelMapping[model] || model; try { encoding = tiktoken.encoding_for_model(mappedModel); } catch (error) { encoding = tiktoken.get_encoding("cl100k_base"); } } else { encoding = tiktoken.get_encoding("cl100k_base"); } const tokens = encoding.encode(text); const tokenCount = tokens.length; encoding.free(); return tokenCount; } catch (error) { logger.warn("Failed to calculate tokens with tiktoken, using estimation:", error); return Math.ceil(text.length / 4); } } /** * Calculate tokens for an array of messages * @param messages - Array of messages to calculate tokens for * @param model - Model name for encoding selection * @returns Number of tokens */ calculateMessagesTokens(messages, model) { if (!messages || messages.length === 0) return 0; try { let totalTokens = 0; for (const message of messages) { if (message.content) { totalTokens += this.calculateTokens(message.content, model); } totalTokens += 3; if (message.name) { totalTokens += this.calculateTokens(message.name, model); } } totalTokens += 3; return totalTokens; } catch (error) { logger.warn("Failed to calculate message tokens, using estimation:", error); const totalText = messages.map((m) => (m.content || "") + (m.name || "")).join(" "); return Math.ceil(totalText.length / 4); } } }; // src/services/conversation-service.ts var ConversationService = class { temperature; maxTokens; logger; debug; constructor(temperature, maxTokens, logger2 = console, debug = false) { this.temperature = temperature; this.maxTokens = maxTokens; this.logger = logger2; this.debug = debug; } /** * Prepare context * * @param conversationHistory - ConversationHistory instance * @param systemPrompt - Optional system prompt * @param systemMessages - System messages * @param options - Run options */ prepareContext(conversationHistory, systemPrompt, systemMessages, options = {}) { const universalMessages = conversationHistory.getMessages(); const context = { messages: universalMessages }; if (options.systemPrompt) { context.systemPrompt = options.systemPrompt; } else if (systemMessages && systemMessages.length > 0) { context.systemMessages = systemMessages; } else if (systemPrompt) { context.systemPrompt = systemPrompt; } return context; } /** * Generate response * * @param aiProvider - AI provider * @param model - Model name * @param context - Conversation context * @param options - Run options * @param availableTools - Available tools * @param onToolCall - Tool call function * @param conversationHistory - Conversation history to store messages sequentially */ async generateResponse(aiProvider, model, context, options = {}, availableTools = [], onToolCall, conversationHistory) { try { const response = await aiProvider.chat(model, context, { ...options, temperature: options.temperature ?? this.temperature, maxTokens: options.maxTokens ?? this.maxTokens, tools: availableTools, functionCallMode: options.functionCallMode, forcedFunction: options.forcedFunction, forcedArguments: options.forcedArguments }); if (onToolCall && response.toolCalls && response.toolCalls.length > 0) { return await this.handleToolCalls( response, context, aiProvider, model, options, availableTools, onToolCall, conversationHistory ); } return response; } catch (error) { logger.error("Error occurred during AI client call:", error); throw new Error(`Error during AI client call: ${error instanceof Error ? error.message : String(error)}`); } } /** * Handle function call (simplified - only tool_calls format) */ async handleFunctionCall(response, context, aiProvider, model, options, availableTools, onToolCall, conversationHistory) { if (response.toolCalls && response.toolCalls.length > 0) { return await this.handleToolCalls( response, context, aiProvider, model, options, availableTools, onToolCall, conversationHistory ); } return response; } /** * Handle new tool_calls format (OpenAI tool calling) */ async handleToolCalls(response, context, aiProvider, model, options, availableTools, onToolCall, conversationHistory) { const toolCalls = response.toolCalls; const assistantMessage = { role: "assistant", content: response.content || null, timestamp: /* @__PURE__ */ new Date(), toolCalls: toolCalls.map((tc) => ({ id: tc.id, type: tc.type, function: tc.function })) }; if (this.debug) { this.logger.info("\u{1F50D} [Debug] Assistant message created:", { role: assistantMessage.role, content: assistantMessage.content, hasToolCalls: !!assistantMessage.toolCalls, toolCallsCount: assistantMessage.toolCalls?.length || 0, toolCallIds: assistantMessage.toolCalls?.map((tc) => tc.id) || [] }); } if (conversationHistory) { if (this.debug) { this.logger.info("\u{1F4DD} [ConversationService] Storing assistant message with tool calls"); } conversationHistory.addAssistantMessage( assistantMessage.content || "", assistantMessage.toolCalls ); } const toolResultMessages = []; for (const toolCall of toolCalls) { const { name, arguments: args } = toolCall.function; const parsedArgs = typeof args === "string" ? JSON.parse(args) : args; try { if (this.debug) { this.logger.info(`\u{1F527} [Tool Call] ${name} (ID: ${toolCall.id})`, parsedArgs); } const toolResult = await onToolCall(name, parsedArgs); if (this.debug) { this.logger.info(`\u2705 [Tool Result] ${name} (ID: ${toolCall.id})`, toolResult); } const toolResponseMessage = { role: "tool", content: typeof toolResult === "string" ? toolResult : JSON.stringify(toolResult), timestamp: /* @__PURE__ */ new Date(), toolCallId: toolCall.id, // This is crucial for OpenAI name }; toolResultMessages.push(toolResponseMessage); if (conversationHistory) { if (this.debug) { this.logger.info(`\u{1F4DD} [ConversationService] Storing tool result message (ID: ${toolCall.id})`); } conversationHistory.addToolMessageWithId( toolResponseMessage.content, toolCall.id, name ); } } catch (toolError) { logger.error("Error during tool call:", toolError); const errorMessage = { role: "tool", content: JSON.stringify({ error: toolError instanceof Error ? toolError.message : String(toolError) }), timestamp: /* @__PURE__ */ new Date(), toolCallId: toolCall.id, name }; toolResultMessages.push(errorMessage); if (conversationHistory) { if (this.debug) { this.logger.info(`\u{1F4DD} [ConversationService] Storing tool error message (ID: ${toolCall.id})`); } conversationHistory.addToolMessageWithId( errorMessage.content, toolCall.id, name ); } } } const newContext = { ...context, messages: [ ...context.messages, assistantMessage, // Assistant message with tool_calls ...toolResultMessages // Tool result messages with toolCallId ] }; if (this.debug) { this.logger.info("\u{1F50D} [Debug] Messages in new context:", newContext.messages.length); newContext.messages.slice(-10).forEach((msg, idx) => { const displayIdx = newContext.messages.length - 10 + idx + 1; if (msg.role === "tool") { this.logger.info(` ${displayIdx}. ${msg.role}: [tool_call_id: ${msg.toolCallId}]`); } else if (msg.role === "assistant" && msg.toolCalls) { this.logger.info(` ${displayIdx}. ${msg.role}: [has ${msg.toolCalls.length} tool calls]`); } else { this.logger.info(` ${displayIdx}. ${msg.role}: ${msg.content?.substring(0, 50) || ""}...`); } }); } const finalResponse = await aiProvider.chat(model, newContext, { ...options, temperature: options.temperature ?? this.temperature, maxTokens: options.maxTokens ?? this.maxTokens, tools: availableTools, functionCallMode: "disabled" // Prevent recursive tool calls }); return finalResponse; } /** * Generate streaming response */ async generateStream(aiProvider, model, context, options = {}, availableTools = []) { if (!aiProvider.chatStream) { throw new Error(`AI provider does not support streaming.`); } try { return aiProvider.chatStream(model, context, { ...options, temperature: options.temperature ?? this.temperature, maxTokens: options.maxTokens ?? this.maxTokens, tools: availableTools }); } catch (error) { this.logger.error("Error occurred during streaming API call:", error); throw new Error(`Error during streaming API call: ${error instanceof Error ? error.message : String(error)}`); } } }; // src/services/execution-service.ts var ExecutionService = class { constructor(aiProviderManager, toolProviderManager, requestLimitManager, analyticsManager, tokenAnalyzer, conversationService, logger2, debug, onToolCall) { this.aiProviderManager = aiProviderManager; this.toolProviderManager = toolProviderManager; this.requestLimitManager = requestLimitManager; this.analyticsManager = analyticsManager; this.tokenAnalyzer = tokenAnalyzer; this.conversationService = conversationService; this.logger = logger2; this.debug = debug; this.onToolCall = onToolCall; } /** * Execute a text prompt and return response */ async executePrompt(prompt, context) { context.conversationHistory.addUserMessage(prompt); const conversationContext = this.conversationService.prepareContext( context.conversationHistory, context.systemMessageManager.getSystemPrompt(), context.systemMessageManager.getSystemMessages(), context.options ); const response = await this.executeWithLimitsAndAnalytics(conversationContext, context.options, context.conversationHistory); response.toolCalls && response.toolCalls.length > 0; if (response.content) { if (this.debug) { this.logger.info(`\u{1F4DD} [ExecutionService] Adding assistant response to history: ${response.content.substring(0, 100)}...`); } context.conversationHistory.addAssistantMessage(response.content); } if (this.debug) { const hasToolCalls2 = response.toolCalls && response.toolCalls.length > 0; if (hasToolCalls2) { this.logger.info(`\u{1F50D} [ExecutionService] Tool calling completed. Final response: ${response.content?.substring(0, 100) || "No content"}...`); } else { this.logger.info(`\u{1F50D} [ExecutionService] Regular conversation completed. Response: ${response.content?.substring(0, 100) || "No content"}...`); } this.logger.info(`\u{1F4CA} [ExecutionService] Total messages in history: ${context.conversationHistory.getMessageCount()}`); } return response.content || ""; } /** * Execute a streaming prompt and return response stream */ async executeStream(prompt, context) { context.conversationHistory.addUserMessage(prompt); const conversationContext = this.conversationService.prepareContext( context.conversationHistory, context.systemMessageManager.getSystemPrompt(), context.systemMessageManager.getSystemMessages(), context.options ); return this.generateStream(conversationContext, context.options); } /** * Execute with request limits and analytics tracking */ async executeWithLimitsAndAnalytics(conversationContext, options, conversationHistory) { this.requestLimitManager.checkRequestLimit(); const currentAI = this.aiProviderManager.getCurrentAI(); const currentModel = currentAI.model || "unknown"; await this.checkTokenLimits(conversationContext.messages, currentModel); const response = await this.generateResponse(conversationContext, options, conversationHistory); this.recordUsageAnalytics(response, currentAI.provider || "unknown", currentModel); return response; } /** * Check token limits before making API call */ async checkTokenLimits(messages, currentModel) { if (!this.requestLimitManager.isTokensUnlimited()) { try { const estimatedTokens = this.tokenAnalyzer.calculateMessagesTokens( messages, currentModel ); if (this.debug) { this.logger.info(`\u{1F50D} [Token Estimation] Model: ${currentModel}, Estimated tokens: ${estimatedTokens}`); } this.requestLimitManager.checkEstimatedTokenLimit(estimatedTokens); } catch (error) { this.logger.error("Token limit check failed:", error); throw error; } } } /** * Record usage in analytics and limits */ recordUsageAnalytics(response, provider, model) { if (response.usage?.totalTokens) { this.requestLimitManager.recordRequest(response.usage.totalTokens); this.analyticsManager.recordRequest( response.usage.totalTokens, provider, model ); } } /** * Generate response (internal use) */ async generateResponse(context, options = {}, conversationHistory) { if (!this.aiProviderManager.isConfigured()) { throw new Error("Current AI provider and model are not configured. Use setCurrentAI() method to configure."); } const currentAiProvider = this.aiProviderManager.getCurrentProvider(); const currentModel = this.aiProviderManager.getCurrentModel(); return this.conversationService.generateResponse( currentAiProvider, currentModel, context, options, this.toolProviderManager.getAvailableTools(), async (toolName, params) => { const result = await this.toolProviderManager.callTool(toolName, params); if (this.onToolCall) { this.onToolCall(toolName, params, result); } return result; }, conversationHistory ); } /** * Generate streaming response (internal use) */ async generateStream(context, options = {}) { if (!this.aiProviderManager.isConfigured()) { throw new Error("Current AI provider and model are not configured. Use setCurrentAI() method to configure."); } const currentAiProvider = this.aiProviderManager.getCurrentProvider(); const currentModel = this.aiProviderManager.getCurrentModel(); return this.conversationService.generateStream( currentAiProvider, currentModel, context, options, this.toolProviderManager.getAvailableTools() ); } }; // src/managers/robota-config-manager.ts var RobotaConfigManager = class { config; constructor(input) { this.config = this.createConfiguration(input); this.validateConfiguration(); } /** * Create configuration with defaults applied */ createConfiguration(input) { return { aiProviders: input.aiProviders || {}, currentProvider: input.currentProvider, currentModel: input.currentModel, temperature: input.temperature, maxTokens: input.maxTokens, toolProviders: input.toolProviders || [], functionCallConfig: input.functionCallConfig, onToolCall: input.onToolCall, systemPrompt: input.systemPrompt, systemMessages: input.systemMessages, conversationHistory: input.conversationHistory || new SimpleConversationHistory(), logger: input.logger || console, debug: input.debug || false, maxTokenLimit: input.maxTokenLimit ?? 4096, maxRequestLimit: input.maxRequestLimit ?? 25 }; } /** * Validate configuration consistency */ validateConfiguration() { if (this.config.currentProvider && !this.config.aiProviders[this.config.currentProvider]) { throw new Error(`Current AI provider '${this.config.currentProvider}' is not registered`); } if (this.config.maxTokenLimit < 0) { throw new Error("Maximum token limit cannot be negative"); } if (this.config.maxRequestLimit < 0) { throw new Error("Maximum request limit cannot be negative"); } } /** * Get the complete configuration */ getConfiguration() { return { ...this.config }; } /** * Update AI provider configuration */ updateAIConfig(updates) { if (updates.currentProvider !== void 0) { if (updates.currentProvider && !this.config.aiProviders[updates.currentProvider]) { throw new Error(`AI provider '${updates.currentProvider}' is not registered`); } this.config.currentProvider = updates.currentProvider; } if (updates.currentModel !== void 0) { this.config.currentModel = updates.currentModel; } if (updates.temperature !== void 0) { this.config.temperature = updates.temperature; } if (updates.maxTokens !== void 0) { this.config.maxTokens = updates.maxTokens; } } /** * Update system configuration */ updateSystemConfig(updates) { if (updates.systemPrompt !== void 0) { this.config.systemPrompt = updates.systemPrompt; } if (updates.systemMessages !== void 0) { this.config.systemMessages = updates.systemMessages; } if (updates.debug !== void 0) { this.config.debug = updates.debug; } } /** * Update limit configuration */ updateLimits(updates) { if (updates.maxTokenLimit !== void 0) { if (updates.maxTokenLimit < 0) { throw new Error("Maximum token limit cannot be negative"); } this.config.maxTokenLimit = updates.maxTokenLimit; } if (updates.maxRequestLimit !== void 0) { if (updates.maxRequestLimit < 0) { throw new Error("Maximum request limit cannot be negative"); } this.config.maxRequestLimit = updates.maxRequestLimit; } } /** * Add AI provider */ addAIProvider(name, provider) { this.config.aiProviders[name] = provider; } /** * Remove AI provider */ removeAIProvider(name) { if (this.config.currentProvider === name) { this.config.currentProvider = void 0; this.config.currentModel = void 0; } delete this.config.aiProviders[name]; } /** * Add tool provider */ addToolProvider(provider) { this.config.toolProviders.push(provider); } /** * Remove tool provider */ removeToolProvider(provider) { const index = this.config.toolProviders.indexOf(provider); if (index !== -1) { this.config.toolProviders.splice(index, 1); } } /** * Set tool call callback */ setToolCallCallback(callback) { this.config.onToolCall = callback; } /** * Check if AI is properly configured */ isAIConfigured() { return !!(this.config.currentProvider && this.config.currentModel && this.config.aiProviders[this.config.currentProvider]); } }; // src/robota.ts var Robota = class { /** @internal Configuration manager handling all settings */ configManager; /** @internal Service handling execution logic */ executionService; /** @internal Conversation service for high-level operations */ conversationService; /** @internal Token analysis utilities */ tokenAnalyzer; /** @internal Conversation history storage */ conversationHistory; /** @internal Optional tool call callback */ onToolCall; /** @internal Logger instance */ logger; /** @internal Debug mode flag */ debug; // === FACADE PATTERN: Exposed Managers === /** * AI provider management - register providers, set current provider/model, configure parameters * * @see {@link AIProviderManager} */ ai; /** * System message management - set prompts, manage system instructions * * @see {@link SystemMessageManager} */ system; /** * Function/tool call management - configure modes, timeouts, allowed functions * * @see {@link FunctionCallManager} */ functions; /** * Analytics and metrics - track usage, performance, costs * * @see {@link AnalyticsManager} */ analytics; /** * Tool provider management - register and manage tool providers * * @see {@link ToolProviderManager} */ tools; /** * Request and token limits - control usage and costs * * @see {@link RequestLimitManager} */ limits; /** * Conversation history - direct access to conversation management * * @see {@link ConversationHistory} */ conversation; constructor(options = {}) { this.logger = options.logger || console; this.debug = options.debug || false; this.onToolCall = options.onToolCall; this.conversationHistory = options.conversationHistory || new SimpleConversationHistory(); this.conversation = this.conversationHistory; this.ai = new AIProviderManager(); this.system = new SystemMessageManager(); this.functions = new FunctionCallManager(); this.analytics = new AnalyticsManager(); this.tools = new ToolProviderManager(this.logger); this.limits = new RequestLimitManager(); this.tokenAnalyzer = new TokenAnalyzer(); this.conversationService = new ConversationService( options.temperature, options.maxTokens, this.logger, this.debug ); this.applyConfiguration(options); this.configManager = new RobotaConfigManager({ aiProviders: options.aiProviders || {}, currentProvider: options.currentProvider, currentModel: options.currentModel, temperature: options.temperature, maxTokens: options.maxTokens, debug: this.debug, maxTokenLimit: options.maxTokenLimit || 4096, maxRequestLimit: options.maxRequestLimit || 25 }); this.executionService = new ExecutionService( this.ai, this.tools, this.limits, this.analytics, this.tokenAnalyzer, this.conversationService, this.logger, this.debug, this.onToolCall ); } /** * Apply configuration from options * @internal */ applyConfiguration(options) { if (options.aiProviders) { for (const [name, provider] of Object.entries(options.aiProviders)) { this.ai.addProvider(name, provider); } } if (options.currentProvider && options.currentModel) { this.ai.setCurrentAI(options.currentProvider, options.currentModel); } if (options.toolProviders) { for (const provider of options.toolProviders) { this.tools.addProvider(provider); } } if (options.provider) { this.tools.addProvider(options.provider); } if (options.systemPrompt) { this.system.setSystemPrompt(options.systemPrompt); this.conversationHistory.addSystemMessage(options.systemPrompt); } if (options.systemMessages) { this.system.setSystemMessages(options.systemMessages); for (const msg of options.systemMessages) { if (msg.role === "system") { this.conversationHistory.addSystemMessage(msg.content); } } } if (options.functionCallConfig) { this.functions.configure(options.functionCallConfig); } if (options.maxTokenLimit) { this.limits.setMaxTokens(options.maxTokenLimit); } if (options.maxRequestLimit) { this.limits.setMaxRequests(options.maxRequestLimit); } } // ============================================================ // Core Execution Methods // ============================================================ /** * Execute a prompt and get a text response * * Main method for running AI conversations. Handles the full pipeline: * - Context preparation with conversation history * - AI provider execution with tool support * - Response processing and history updates * - Analytics and limit tracking * * @param prompt - The user prompt to process * @param options - Optional configuration for this specific request * @returns Promise resolving to the AI's text response * * @throws {Error} When no AI provider is configured * @throws {Error} When rate limits are exceeded * @throws {Error} When AI provider fails * * @see {@link ../../../apps/examples/01-basic/01-simple-conversation.ts | Basic Usage} */ async run(prompt, options = {}) { return this.executionService.executePrompt(prompt, { conversationHistory: this.conversationHistory, systemMessageManager: this.system, options }); } /** * Execute a prompt and get a streaming response * * Similar to run() but returns an async iterator for streaming responses. * Useful for real-time display of AI responses. * * @param prompt - The user prompt to process * @param options - Optional configuration for this specific request * @returns Promise resolving to an async iterator of response chunks * * @throws {Error} When no AI provider is configured * @throws {Error} When rate limits are exceeded * @throws {Error} When AI provider fails * * @see {@link ../../../apps/examples/01-basic/01-simple-conversation.ts | Streaming Example} */ async runStream(prompt, options = {}) { return this.executionService.executeStream(prompt, { conversationHistory: this.conversationHistory, systemMessageManager: this.system, options }); } /** * Call a specific tool directly by name * * Bypasses AI provider and calls a tool directly with provided parameters. * Useful for testing tools or direct tool execution.