/** * Core Agent Implementation for autonoma Ecosystem * * Provides standardized agent creation and management with LangGraph + AgentKit integration. */ // Node.js process global declare const process: { env: Record; memoryUsage(): { heapUsed: number; [key: string]: number }; }; declare const fetch: typeof globalThis.fetch; import { AgentConfig, autonomaAgent, AgentMessage, AgentResponse, AgentStatus, AgentMetrics, LangGraphAgent, MessageService, // RAGService, // Not yet implemented Tool, ToolCall } from './types.js'; import { ChatOpenAI } from '@langchain/openai'; import { createReactAgent } from '@langchain/langgraph/prebuilt'; import { createUnifiedMCPTools, UnifiedMCPTools } from '@autonomaai/mcp-client'; import { executeMCPTool } from '@autonomaai/mcp-client'; import { streamingService, StreamingEventType } from './streaming.js'; // Structured logging interface interface Logger { info(message: string, meta?: Record): void; warn(message: string, meta?: Record): void; error(message: string, meta?: Record): void; debug(message: string, meta?: Record): void; } type McpServiceName = 'hummingbot' | 'rag' | 'dexscreener' | 'apy'; // Simple structured logger implementation class StructuredLogger implements Logger { constructor(private agentName: string, private logLevel: string = 'info') {} private log(level: string, message: string, meta?: Record): void { if (!this.shouldLog(level)) return; const timestamp = new Date().toISOString(); const logEntry = { timestamp, level, agent: this.agentName, message, ...(meta && { meta }) }; // Use structured JSON logging for production, readable format for development if (process.env.NODE_ENV === 'production') { console.log(JSON.stringify(logEntry)); } else { console.log(`[${timestamp}] ${level.toUpperCase()} [${this.agentName}] ${message}${meta ? ` ${JSON.stringify(meta)}` : ''}`); } } private shouldLog(level: string): boolean { const levels = ['debug', 'info', 'warn', 'error']; const currentLevel = levels.indexOf(this.logLevel); const messageLevel = levels.indexOf(level); return messageLevel >= currentLevel; } info(message: string, meta?: Record): void { this.log('info', message, meta); } warn(message: string, meta?: Record): void { this.log('warn', message, meta); } error(message: string, meta?: Record): void { this.log('error', message, meta); } debug(message: string, meta?: Record): void { this.log('debug', message, meta); } } /** * Core agent implementation that standardizes LangGraph + AgentKit patterns */ export class StandardAgent implements autonomaAgent { public readonly id: string; public readonly name: string; public readonly description?: string; public capabilities: string[] = []; public tools: Tool[] = []; public config: AgentConfig; private langGraphAgent?: LangGraphAgent; private openAiConfig?: { apiKey: string; model: string; temperature: number; maxTokens: number; }; private lastTokenUsage: number = 0; private messageService?: MessageService; // private ragService?: RAGService; // Not yet implemented private unifiedMcpTools?: UnifiedMCPTools; private status: AgentStatus['status'] = 'idle'; private startTime: number = Date.now(); private metrics: AgentMetrics; private logger: Logger; constructor(config: AgentConfig) { this.id = `agent_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; this.name = config.name; this.description = config.description; this.config = config; // Initialize structured logger const logLevel = process.env.LOG_LEVEL || (process.env.NODE_ENV === 'production' ? 'info' : 'debug'); this.logger = new StructuredLogger(this.name, logLevel); this.metrics = { totalMessages: 0, totalToolCalls: 0, averageResponseTime: 0, successRate: 100, topTools: [], errorCount: 0, uptime: 0 }; } async start(): Promise { try { this.status = 'processing'; // Initialize the underlying LangGraph agent await this.initializeLangGraphAgent(); // Initialize services if configured await this.initializeServices(); this.status = 'idle'; this.logger.info('Agent started successfully', { agentId: this.id, capabilities: this.capabilities, toolCount: this.tools.length }); } catch (error) { this.status = 'error'; this.metrics.errorCount++; throw new Error(`Failed to start agent: ${error instanceof Error ? error.message : String(error)}`); } } async stop(): Promise { this.status = 'stopped'; this.logger.info('Agent stopped', { agentId: this.id, uptime: Date.now() - this.startTime, totalMessages: this.metrics.totalMessages }); } async process(message: AgentMessage): Promise { const startTime = Date.now(); this.status = 'processing'; try { if (!this.langGraphAgent) { throw new Error('Agent not initialized. Call start() first.'); } // Save user message if message service is available if (this.messageService) { await this.messageService.saveMessage(message); } // Process with LangGraph agent const response = await this.processWithLangGraph(message); // Update metrics this.updateMetrics(startTime, true); this.status = 'idle'; return response; } catch (error) { this.updateMetrics(startTime, false); this.status = 'error'; this.metrics.errorCount++; const errorMessage = error instanceof Error ? error.message : String(error); return { content: `I encountered an error: ${errorMessage}`, timestamp: new Date().toISOString(), error: errorMessage }; } } async updateConfig(config: Partial): Promise { this.config = { ...this.config, ...config }; // Reinitialize if agent is running if (this.langGraphAgent) { await this.initializeLangGraphAgent(); } } async addTool(tool: Tool): Promise { this.tools.push(tool); this.capabilities.push(tool.name || 'unknown_tool'); // Reinitialize agent with new tools if (this.langGraphAgent) { await this.initializeLangGraphAgent(); } } async removeTool(toolName: string): Promise { this.tools = this.tools.filter(tool => tool.name !== toolName); this.capabilities = this.capabilities.filter(cap => cap !== toolName); // Reinitialize agent without the tool if (this.langGraphAgent) { await this.initializeLangGraphAgent(); } } getStatus(): AgentStatus { return { status: this.status, uptime: Date.now() - this.startTime, lastActivity: new Date().toISOString(), activeTools: this.tools.map(tool => tool.name || 'unknown'), memoryUsage: { conversationMessages: this.metrics.totalMessages, totalMemoryMB: process.memoryUsage().heapUsed / 1024 / 1024 } }; } getMetrics(): AgentMetrics { return { ...this.metrics, uptime: Date.now() - this.startTime }; } async clearMemory(): Promise { if (this.messageService) { await this.messageService.clearHistory(); } // Reset conversation-related metrics this.metrics.totalMessages = 0; } async getConversationHistory(limit: number = 50): Promise { if (this.messageService) { return await this.messageService.getRecentMessages(limit); } return []; } private buildSystemPrompt(): string { if (this.config.prompt?.systemMessage) { return this.config.prompt.systemMessage; } return 'You are a helpful AI assistant providing insights to the autonoma platform.'; } private async buildConversationMessages(message: AgentMessage): Promise> { const messages: Array<{ role: string; content: string }> = []; const systemPrompt = this.buildSystemPrompt(); messages.push({ role: 'system', content: systemPrompt }); if (this.config.prompt?.context) { messages.push({ role: 'system', content: this.config.prompt.context }); } const conversationHistory = await this.getConversationHistory(10); for (const entry of conversationHistory.reverse()) { messages.push({ role: entry.role, content: entry.content }); } messages.push({ role: message.role, content: message.content }); return messages; } private getSessionId(message: AgentMessage): string { return message.metadata?.sessionId || 'default'; } private emitStreamingEvent( sessionId: string, type: StreamingEventType, content: string, metadata?: Record ): void { streamingService.publish(sessionId, { type, content, metadata }); } private publishStreamingChunks(sessionId: string, text: string, type: StreamingEventType = 'progress'): void { const chunkSize = 120; for (let start = 0; start < text.length; start += chunkSize) { const chunk = text.slice(start, start + chunkSize); if (chunk.trim()) { this.emitStreamingEvent(sessionId, type, chunk, { chunk_index: Math.floor(start / chunkSize), chunk_size: chunkSize }); } } } private async requestOpenAICompletion(messages: Array<{ role: string; content: string }>): Promise { const config = this.openAiConfig; if (!config) { throw new Error('LLM configuration is not initialized'); } const response = await fetch('https://api.openai.com/v1/chat/completions', { method: 'POST', headers: { authorization: `Bearer ${config.apiKey}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ model: config.model, messages, temperature: config.temperature, max_tokens: config.maxTokens, stream: false }) }); const payload = await response.json(); if (!response.ok) { const errorMessage = payload?.error?.message || response.statusText; throw new Error(`OpenAI API error (${response.status}): ${errorMessage}`); } const choice = payload?.choices?.[0]?.message?.content; if (!choice) { throw new Error('OpenAI response is missing content'); } this.lastTokenUsage = payload?.usage?.total_tokens || this.lastTokenUsage; return choice; } private async createLangGraphTools(): Promise { const tools: Tool[] = []; // Include any custom tools provided by the agent configuration const customTools = this.config.tools?.customTools; if (Array.isArray(customTools)) { tools.push(...customTools); } if (this.config.tools?.enableMCP) { const hummingbotUrl = this.config.tools.mcpServerUrl || process.env.HUMMINGBOT_MCP_URL || 'http://localhost:8000'; const ragUrl = this.config.tools.ragServerUrl || process.env.RAG_MCP_URL || 'http://localhost:3002'; const dexscreenerUrl = process.env.DEXSCREENER_MCP_URL || 'http://localhost:3000'; const apyUrl = process.env.APY_STRATEGY_MCP_URL || 'http://localhost:8003'; this.unifiedMcpTools = createUnifiedMCPTools({ hummingbotUrl, ragServerUrl: ragUrl, dexscreenerUrl, apyStrategyUrl: apyUrl }); tools.push(this.createLangGraphMcpTool()); } return tools; } private createLangGraphMcpTool(): Tool { return { name: 'call_mcp_tool', description: 'Execute an MCP tool by service name (hummingbot, rag, dexscreener, apy). Provide tool arguments as needed.', func: async (params: { service: McpServiceName; tool: string; arguments?: Record; }) => { const client = this.getMcpClientForService(params.service); if (!client) { throw new Error(`MCP service ${params.service} is not configured for this agent`); } const result = await executeMCPTool(client, params.tool, params.arguments); if (!result.success) { throw new Error(result.error || `MCP tool ${params.tool} failed without providing an error`); } return result.response; } }; } private getMcpClientForService(service: McpServiceName): any { if (!this.unifiedMcpTools) return null; switch (service) { case 'hummingbot': return this.unifiedMcpTools.hummingbotTools; case 'rag': return this.unifiedMcpTools.ragTools; case 'dexscreener': return this.unifiedMcpTools.dexscreenerTools; case 'apy': return this.unifiedMcpTools.apyStrategyTools; default: return null; } } // ============================================================================= // Private Implementation Methods // ============================================================================= private async initializeLangGraphAgent(): Promise { const llmConfig = this.config.llm || {}; const apiKey = llmConfig.openAIApiKey || process.env.OPENAI_API_KEY; if (!apiKey) { throw new Error('OpenAI API key is required to initialize the agent'); } this.openAiConfig = { apiKey, model: llmConfig.model || process.env.OPENAI_MODEL || 'gpt-4o-mini', temperature: llmConfig.temperature ?? 0.3, maxTokens: llmConfig.maxTokens ?? parseInt(process.env.OPENAI_MAX_TOKENS || '2048', 10) }; const llm = new ChatOpenAI({ openAIApiKey: this.openAiConfig.apiKey, modelName: this.openAiConfig.model, temperature: this.openAiConfig.temperature, maxTokens: this.openAiConfig.maxTokens }); const tools = await this.createLangGraphTools(); this.tools = tools; this.langGraphAgent = await createReactAgent({ llm, tools, messageModifier: this.buildSystemPrompt() }); } private async initializeServices(): Promise { if (this.config.services?.messageService) { // Support passing a concrete MessageService instance if (typeof this.config.services.messageService !== 'boolean') { this.messageService = this.config.services.messageService; } this.logger.info('Message service enabled', { service: 'messageService', config: this.config.services.messageService }); } if (this.config.services?.ragService) { // Initialize RAG service - implementation will be injected // if (typeof this.config.services.ragService !== 'boolean') { // this.ragService = this.config.services.ragService; // } this.logger.info('RAG service enabled (not yet implemented)', { service: 'ragService', config: this.config.services.ragService }); } } private async processWithLangGraph(message: AgentMessage): Promise { if (!this.langGraphAgent) { throw new Error('LangGraph agent is not initialized'); } const sessionId = this.getSessionId(message); const conversation = await this.buildConversationMessages(message); this.emitStreamingEvent(sessionId, 'start', 'Processing your request with LangGraph'); const agentInput = conversation.map(entry => `${entry.role.toUpperCase()}: ${entry.content}`).join('\n\n'); const startTime = Date.now(); try { const runResult = await (this.langGraphAgent as any).run(agentInput); const { content, toolCalls, tokensUsed } = this.normalizeLangGraphResult(runResult); const processingTime = Date.now() - startTime; const timestamp = new Date().toISOString(); if (this.messageService) { await this.messageService.saveMessage({ role: 'assistant', content, timestamp }); } this.metrics.totalToolCalls += toolCalls.length; const response: AgentResponse = { content, timestamp, toolCalls, metrics: { processingTime, tokensUsed, toolsUsed: toolCalls.map(call => call.tool) } }; this.publishStreamingChunks(sessionId, content); this.emitStreamingEvent(sessionId, 'done', content, { toolCalls: toolCalls.map(call => call.tool) }); return response; } catch (error) { this.logger.warn('LangGraph agent failed, falling back to direct OpenAI completion', { error: error instanceof Error ? error.message : String(error) }); const fallbackContent = await this.requestOpenAICompletion(conversation); const processingTime = Date.now() - startTime; const timestamp = new Date().toISOString(); if (this.messageService) { await this.messageService.saveMessage({ role: 'assistant', content: fallbackContent, timestamp }); } this.emitStreamingEvent(sessionId, 'error', 'LangGraph agent fallback triggered', { error: error instanceof Error ? error.message : String(error) }); this.publishStreamingChunks(sessionId, fallbackContent); this.emitStreamingEvent(sessionId, 'done', fallbackContent); return { content: fallbackContent, timestamp, metrics: { processingTime, tokensUsed: this.lastTokenUsage, toolsUsed: [] } }; } } private normalizeLangGraphResult(result: any): { content: string; toolCalls: ToolCall[]; tokensUsed: number; } { let content = ''; if (typeof result === 'string') { content = result; } else if (typeof result?.content === 'string') { content = result.content; } else if (typeof result?.response === 'string') { content = result.response; } else if (Array.isArray(result?.output)) { content = result.output .map((segment: any) => (typeof segment === 'string' ? segment : segment?.text || segment?.content || '')) .filter(Boolean) .join('\n'); } else if (typeof result?.output === 'string') { content = result.output; } const toolCalls: ToolCall[] = Array.isArray(result?.toolCalls) ? result.toolCalls.map((toolCall: any) => ({ tool: toolCall.tool || toolCall.name || 'unknown', input: toolCall.input || toolCall.arguments, output: toolCall.output || toolCall.result, duration: toolCall.duration || 0, success: toolCall.success ?? !toolCall.error, error: toolCall.error })) : []; const tokensUsed = result?.usage?.total_tokens || this.lastTokenUsage; this.lastTokenUsage = tokensUsed; return { content, toolCalls, tokensUsed }; } private updateMetrics(startTime: number, success: boolean): void { const processingTime = Date.now() - startTime; this.metrics.totalMessages++; this.metrics.averageResponseTime = (this.metrics.averageResponseTime * (this.metrics.totalMessages - 1) + processingTime) / this.metrics.totalMessages; if (!success) { this.metrics.errorCount++; } this.metrics.successRate = ((this.metrics.totalMessages - this.metrics.errorCount) / this.metrics.totalMessages) * 100; } // ============================================================================= // Static Factory Methods // ============================================================================= static createTradingAgent(config: AgentConfig): StandardAgent { const tradingConfig = { ...config, capabilities: [ 'trading_controller_management', 'exchange_connectivity', 'market_data_analysis', 'risk_management', 'portfolio_optimization', ...(config.prompt?.capabilities || []) ] }; return new StandardAgent(tradingConfig); } static createDataAnalysisAgent(config: AgentConfig): StandardAgent { const analysisConfig = { ...config, capabilities: [ 'data_collection', 'statistical_analysis', 'pattern_recognition', 'report_generation', 'visualization', ...(config.prompt?.capabilities || []) ] }; return new StandardAgent(analysisConfig); } static createCustomerServiceAgent(config: AgentConfig): StandardAgent { const serviceConfig = { ...config, capabilities: [ 'question_answering', 'knowledge_base_search', 'issue_escalation', 'conversation_management', 'sentiment_analysis', ...(config.prompt?.capabilities || []) ] }; return new StandardAgent(serviceConfig); } } /** * Agent builder for fluent configuration */ export class AgentBuilder { private config: AgentConfig = { name: 'unnamed_agent' }; setName(name: string): AgentBuilder { this.config.name = name; return this; } setDescription(description: string): AgentBuilder { this.config.description = description; return this; } setLLM(llmConfig: AgentConfig['llm']): AgentBuilder { this.config.llm = llmConfig; return this; } setAgentKit(agentKitConfig: AgentConfig['agentKit']): AgentBuilder { this.config.agentKit = agentKitConfig; return this; } setTools(toolsConfig: AgentConfig['tools']): AgentBuilder { this.config.tools = toolsConfig; return this; } setPrompt(promptConfig: AgentConfig['prompt']): AgentBuilder { this.config.prompt = promptConfig; return this; } setMemory(memoryConfig: AgentConfig['memory']): AgentBuilder { this.config.memory = memoryConfig; return this; } setServices(servicesConfig: AgentConfig['services']): AgentBuilder { this.config.services = servicesConfig; return this; } addCustomTool(tool: Tool): AgentBuilder { if (!this.config.tools) { this.config.tools = {}; } if (!this.config.tools.customTools) { this.config.tools.customTools = []; } this.config.tools.customTools.push(tool); return this; } async build(): Promise { return new StandardAgent(this.config); } } /** * Utility functions for agent management */ export class AgentUtils { static validateConfig(config: AgentConfig): { valid: boolean; errors: string[] } { const errors: string[] = []; if (!config.name || config.name.trim() === '') { errors.push('Agent name is required'); } if (config.tools?.enableAgentKit) { if (!config.agentKit?.cdpApiKeyId || !config.agentKit?.cdpApiKeySecret) { errors.push('AgentKit requires CDP API credentials'); } } if (config.tools?.enableMCP && !config.tools?.mcpServerUrl) { errors.push('MCP tools require MCP server URL'); } return { valid: errors.length === 0, errors }; } static getEnvironmentConfig(): AgentConfig { return { name: process.env.AGENT_NAME || 'environment_agent', llm: { model: process.env.LLM_MODEL || 'gpt-4o-mini', openAIApiKey: process.env.OPENAI_API_KEY }, agentKit: { networkId: process.env.NETWORK_ID || 'base-sepolia', cdpApiKeyId: process.env.CDP_API_KEY_ID, cdpApiKeySecret: process.env.CDP_API_KEY_SECRET, walletDataFile: process.env.WALLET_DATA_FILE || 'wallet_data.txt' }, tools: { enableAgentKit: !!process.env.CDP_API_KEY_ID, enableMCP: !!process.env.MCP_SERVER_URL, enableRAG: !!process.env.RAG_SERVER_URL, mcpServerUrl: process.env.HUMMINGBOT_MCP_URL || 'http://localhost:8000', ragServerUrl: process.env.RAG_MCP_URL || 'http://localhost:3002' }, memory: { enabled: true, persistentThreadId: true, maxMessages: parseInt(process.env.MAX_MESSAGES || '100') }, services: { messageService: true, ragService: !!process.env.RAG_SERVER_URL, loggingEnabled: process.env.NODE_ENV !== 'production' } }; } }