UNPKG

@hashgraphonline/conversational-agent

Version:

Hashgraph Online conversational AI agent implementing HCS-10 communication, HCS-2 registries, and content inscription on Hedera

427 lines (426 loc) 15.4 kB
import { createOpenAIToolsAgent } from "langchain/agents"; import { ContentAwareAgentExecutor } from "./index21.js"; import { ChatPromptTemplate, MessagesPlaceholder } from "@langchain/core/prompts"; import { ChatOpenAI } from "@langchain/openai"; import { TokenUsageCallbackHandler, calculateTokenCostSync, getAllHederaCorePlugins, HederaAgentKit } from "hedera-agent-kit"; import { BaseAgent } from "./index7.js"; import { MCPClientManager } from "./index22.js"; import { convertMCPToolToLangChain } from "./index23.js"; import { SmartMemoryManager } from "./index15.js"; class LangChainAgent extends BaseAgent { constructor() { super(...arguments); this.systemMessage = ""; this.mcpConnectionStatus = /* @__PURE__ */ new Map(); } async boot() { if (this.initialized) { this.logger.warn("Agent already initialized"); return; } try { this.agentKit = await this.createAgentKit(); await this.agentKit.initialize(); const modelName = this.config.ai?.modelName || process.env.OPENAI_MODEL_NAME || "gpt-4o-mini"; this.tokenTracker = new TokenUsageCallbackHandler(modelName); const allTools = this.agentKit.getAggregatedLangChainTools(); this.tools = this.filterTools(allTools); if (this.config.mcp?.servers && this.config.mcp.servers.length > 0) { if (this.config.mcp.autoConnect !== false) { await this.initializeMCP(); } else { this.logger.info( "MCP servers configured but autoConnect=false, skipping synchronous connection" ); this.mcpManager = new MCPClientManager(this.logger); } } this.smartMemory = new SmartMemoryManager({ modelName, maxTokens: 9e4, reserveTokens: 1e4, storageLimit: 1e3 }); this.logger.info("SmartMemoryManager initialized:", { modelName, toolsCount: this.tools.length, maxTokens: 9e4, reserveTokens: 1e4 }); this.systemMessage = this.buildSystemPrompt(); this.smartMemory.setSystemPrompt(this.systemMessage); await this.createExecutor(); this.initialized = true; this.logger.info("LangChain Hedera agent initialized"); } catch (error) { this.logger.error("Failed to initialize agent:", error); throw error; } } async chat(message, context) { if (!this.initialized || !this.executor || !this.smartMemory) { throw new Error("Agent not initialized. Call boot() first."); } try { this.logger.info("LangChainAgent.chat called with:", { message, contextLength: context?.messages?.length || 0 }); if (context?.messages && context.messages.length > 0) { this.smartMemory.clear(); for (const msg of context.messages) { this.smartMemory.addMessage(msg); } } const { HumanMessage } = await import("@langchain/core/messages"); this.smartMemory.addMessage(new HumanMessage(message)); const memoryStats = this.smartMemory.getMemoryStats(); this.logger.info("Memory stats before execution:", { totalMessages: memoryStats.totalActiveMessages, currentTokens: memoryStats.currentTokenCount, maxTokens: memoryStats.maxTokens, usagePercentage: memoryStats.usagePercentage, toolsCount: this.tools.length }); const result = await this.executor.invoke({ input: message, chat_history: this.smartMemory.getMessages() }); this.logger.info("LangChainAgent executor result:", result); let response = { output: result.output || "", message: result.output || "", notes: [], intermediateSteps: result.intermediateSteps }; if (result.intermediateSteps && Array.isArray(result.intermediateSteps)) { const toolCalls = result.intermediateSteps.map( (step, index) => ({ id: `call_${index}`, name: step.action?.tool || "unknown", args: step.action?.toolInput || {}, output: typeof step.observation === "string" ? step.observation : JSON.stringify(step.observation) }) ); if (toolCalls.length > 0) { response.tool_calls = toolCalls; } } const parsedSteps = result?.intermediateSteps?.[0]?.observation; if (parsedSteps && typeof parsedSteps === "string" && this.isJSON(parsedSteps)) { try { const parsed = JSON.parse(parsedSteps); response = { ...response, ...parsed }; } catch (error) { this.logger.error("Error parsing intermediate steps:", error); } } if (!response.output || response.output.trim() === "") { response.output = "Agent action complete."; } if (response.output) { const { AIMessage } = await import("@langchain/core/messages"); this.smartMemory.addMessage(new AIMessage(response.output)); } if (this.tokenTracker) { const tokenUsage = this.tokenTracker.getLatestTokenUsage(); if (tokenUsage) { response.tokenUsage = tokenUsage; response.cost = calculateTokenCostSync(tokenUsage); } } const finalMemoryStats = this.smartMemory.getMemoryStats(); response.metadata = { ...response.metadata, memoryStats: { activeMessages: finalMemoryStats.totalActiveMessages, tokenUsage: finalMemoryStats.currentTokenCount, maxTokens: finalMemoryStats.maxTokens, usagePercentage: finalMemoryStats.usagePercentage } }; this.logger.info("LangChainAgent.chat returning response:", response); return response; } catch (error) { this.logger.error("LangChainAgent.chat error:", error); return this.handleError(error); } } async shutdown() { if (this.mcpManager) { await this.mcpManager.disconnectAll(); } if (this.smartMemory) { this.smartMemory.dispose(); this.smartMemory = void 0; } this.executor = void 0; this.agentKit = void 0; this.tools = []; this.initialized = false; this.logger.info("Agent cleaned up"); } switchMode(mode) { if (this.config.execution) { this.config.execution.operationalMode = mode; } else { this.config.execution = { operationalMode: mode }; } if (this.agentKit) { this.agentKit.operationalMode = mode; } this.systemMessage = this.buildSystemPrompt(); this.logger.info(`Operational mode switched to: ${mode}`); } getUsageStats() { if (!this.tokenTracker) { return { promptTokens: 0, completionTokens: 0, totalTokens: 0, cost: { totalCost: 0 } }; } const usage = this.tokenTracker.getTotalTokenUsage(); const cost = calculateTokenCostSync(usage); return { ...usage, cost }; } getUsageLog() { if (!this.tokenTracker) { return []; } return this.tokenTracker.getTokenUsageHistory().map((usage) => ({ ...usage, cost: calculateTokenCostSync(usage) })); } clearUsageStats() { if (this.tokenTracker) { this.tokenTracker.reset(); this.logger.info("Usage statistics cleared"); } } getMCPConnectionStatus() { return new Map(this.mcpConnectionStatus); } async createAgentKit() { const corePlugins = getAllHederaCorePlugins(); const extensionPlugins = this.config.extensions?.plugins || []; const plugins = [...corePlugins, ...extensionPlugins]; const operationalMode = this.config.execution?.operationalMode || "returnBytes"; const modelName = this.config.ai?.modelName || "gpt-4o"; return new HederaAgentKit( this.config.signer, { plugins }, operationalMode, this.config.execution?.userAccountId, this.config.execution?.scheduleUserTransactionsInBytesMode ?? false, void 0, modelName, this.config.extensions?.mirrorConfig, this.config.debug?.silent ?? false ); } async createExecutor() { let llm; if (this.config.ai?.provider && this.config.ai.provider.getModel) { llm = this.config.ai.provider.getModel(); } else if (this.config.ai?.llm) { llm = this.config.ai.llm; } else { const apiKey = this.config.ai?.apiKey || process.env.OPENAI_API_KEY; if (!apiKey) { throw new Error("OpenAI API key required"); } const modelName = this.config.ai?.modelName || "gpt-4o-mini"; const isGPT5Model = modelName.toLowerCase().includes("gpt-5") || modelName.toLowerCase().includes("gpt5"); llm = new ChatOpenAI({ apiKey, modelName, callbacks: this.tokenTracker ? [this.tokenTracker] : [], ...isGPT5Model ? { temperature: 1 } : {} }); } const prompt = ChatPromptTemplate.fromMessages([ ["system", this.systemMessage], new MessagesPlaceholder("chat_history"), ["human", "{input}"], new MessagesPlaceholder("agent_scratchpad") ]); const langchainTools = this.tools; const agent = await createOpenAIToolsAgent({ llm, tools: langchainTools, prompt }); this.executor = new ContentAwareAgentExecutor({ agent, tools: langchainTools, verbose: this.config.debug?.verbose ?? false, returnIntermediateSteps: true }); } handleError(error) { const errorMessage = error instanceof Error ? error.message : "Unknown error"; this.logger.error("Chat error:", error); let tokenUsage; let cost; if (this.tokenTracker) { tokenUsage = this.tokenTracker.getLatestTokenUsage(); if (tokenUsage) { cost = calculateTokenCostSync(tokenUsage); } } let userFriendlyMessage = errorMessage; let userFriendlyOutput = errorMessage; if (errorMessage.includes("429")) { if (errorMessage.includes("quota")) { userFriendlyMessage = "API quota exceeded. Please check your OpenAI billing and usage limits."; userFriendlyOutput = "I'm currently unable to respond because the API quota has been exceeded. Please check your OpenAI account billing and usage limits, then try again."; } else { userFriendlyMessage = "Too many requests. Please wait a moment and try again."; userFriendlyOutput = "I'm receiving too many requests right now. Please wait a moment and try again."; } } else if (errorMessage.includes("401") || errorMessage.includes("unauthorized")) { userFriendlyMessage = "API authentication failed. Please check your API key configuration."; userFriendlyOutput = "There's an issue with the API authentication. Please check your OpenAI API key configuration in settings."; } else if (errorMessage.includes("timeout")) { userFriendlyMessage = "Request timed out. Please try again."; userFriendlyOutput = "The request took too long to process. Please try again."; } else if (errorMessage.includes("network") || errorMessage.includes("fetch")) { userFriendlyMessage = "Network error. Please check your internet connection and try again."; userFriendlyOutput = "There was a network error. Please check your internet connection and try again."; } else if (errorMessage.includes("400")) { userFriendlyMessage = errorMessage; userFriendlyOutput = errorMessage; } const errorResponse = { output: userFriendlyOutput, message: userFriendlyMessage, error: errorMessage, notes: [] }; if (tokenUsage) { errorResponse.tokenUsage = tokenUsage; } if (cost) { errorResponse.cost = cost; } return errorResponse; } async initializeMCP() { this.mcpManager = new MCPClientManager(this.logger); for (const serverConfig of this.config.mcp.servers) { if (serverConfig.autoConnect === false) { this.logger.info( `Skipping MCP server ${serverConfig.name} (autoConnect=false)` ); continue; } const status = await this.mcpManager.connectServer(serverConfig); if (status.connected) { this.logger.info( `Connected to MCP server ${status.serverName} with ${status.tools.length} tools` ); for (const mcpTool of status.tools) { const langchainTool = convertMCPToolToLangChain( mcpTool, this.mcpManager, serverConfig ); this.tools.push(langchainTool); } } else { this.logger.error( `Failed to connect to MCP server ${status.serverName}: ${status.error}` ); } } } /** * Connect to MCP servers asynchronously after agent boot with background timeout pattern */ async connectMCPServers() { if (!this.config.mcp?.servers || this.config.mcp.servers.length === 0) { return; } if (!this.mcpManager) { this.mcpManager = new MCPClientManager(this.logger); } this.logger.info( `Starting background MCP server connections for ${this.config.mcp.servers.length} servers...` ); this.config.mcp.servers.forEach((serverConfig) => { this.connectServerInBackground(serverConfig); }); this.logger.info("MCP server connections initiated in background"); } /** * Connect to a single MCP server in background with timeout */ connectServerInBackground(serverConfig) { const serverName = serverConfig.name; setTimeout(async () => { try { this.logger.info(`Background connecting to MCP server: ${serverName}`); const status = await this.mcpManager.connectServer(serverConfig); this.mcpConnectionStatus.set(serverName, status); if (status.connected) { this.logger.info( `Successfully connected to MCP server ${status.serverName} with ${status.tools.length} tools` ); for (const mcpTool of status.tools) { const langchainTool = convertMCPToolToLangChain( mcpTool, this.mcpManager, serverConfig ); this.tools.push(langchainTool); } if (this.initialized && this.executor) { this.logger.info( `Recreating executor with ${this.tools.length} total tools` ); await this.createExecutor(); } } else { this.logger.error( `Failed to connect to MCP server ${status.serverName}: ${status.error}` ); } } catch (error) { this.logger.error( `Background connection failed for MCP server ${serverName}:`, error ); this.mcpConnectionStatus.set(serverName, { connected: false, serverName, tools: [], error: error instanceof Error ? error.message : "Connection failed" }); } }, 1e3); } /** * Check if a string is valid JSON */ isJSON(str) { if (typeof str !== "string") return false; const trimmed = str.trim(); if (!trimmed) return false; if (!(trimmed.startsWith("{") && trimmed.endsWith("}")) && !(trimmed.startsWith("[") && trimmed.endsWith("]"))) { return false; } try { JSON.parse(trimmed); return true; } catch { return false; } } } export { LangChainAgent }; //# sourceMappingURL=index8.js.map