UNPKG

@hashgraphonline/conversational-agent

Version:

Hashgraph Online conversational AI agent implementing HCS-10 communication, HCS-2 registries, and content inscription on Hedera. https://hol.org

1,236 lines (1,235 loc) 44.5 kB
import { createOpenAIToolsAgent } from "langchain/agents"; import { FormAwareAgentExecutor } from "./index32.js"; import { ChatPromptTemplate, MessagesPlaceholder } from "@langchain/core/prompts"; import { ChatOpenAI } from "@langchain/openai"; import { calculateTokenCostSync, TokenUsageCallbackHandler, getAllHederaCorePlugins, HederaAgentKit } from "hedera-agent-kit"; import { BaseAgent } from "./index8.js"; import { MCPClientManager } from "./index16.js"; import { convertMCPToolToLangChain } from "./index18.js"; import { SmartMemoryManager } from "./index19.js"; import { ResponseFormatter } from "./index36.js"; import { ERROR_MESSAGES } from "./index45.js"; import "./index43.js"; import { SystemMessage, AIMessage, HumanMessage } from "@langchain/core/messages"; import { ToolRegistry } from "./index46.js"; import { ExecutionPipeline } from "./index47.js"; import { FormEngine } from "./index12.js"; function hasHashLinkBlock(metadata) { if (!metadata || typeof metadata !== "object") { return false; } const meta = metadata; if (!("hashLinkBlock" in meta) || !meta.hashLinkBlock || typeof meta.hashLinkBlock !== "object") { return false; } const block = meta.hashLinkBlock; return "blockId" in block && "hashLink" in block && "template" in block && "attributes" in block && typeof block.blockId === "string" && typeof block.hashLink === "string" && typeof block.template === "string" && typeof block.attributes === "object"; } class LangChainAgent extends BaseAgent { constructor() { super(...arguments); this.systemMessage = ""; this.mcpConnectionStatus = /* @__PURE__ */ new Map(); } addToolRawToMemory(name, payload) { try { const content = `[tool-raw:${name}] ${payload}`; this.smartMemory.addMessage(new SystemMessage(content)); } catch { } } persistToolRaw(toolName, output) { try { let payload = ""; if (typeof output === "string") { payload = this.isJSON(output) ? output : JSON.stringify({ output }); } else if (output !== void 0) { try { payload = JSON.stringify(output); } catch { payload = String(output); } } else { payload = JSON.stringify({ observation: null }); } this.addToolRawToMemory(toolName, payload); } catch { } } persistIntermediateSteps(steps) { if (!steps || !Array.isArray(steps)) { return; } try { for (const step of steps) { const name = step?.action?.tool || "unknown"; const obs = step?.observation; this.persistToolRaw(name, obs); } } catch { } } /** * Get inscription tool by capability instead of hardcoded name */ getInscriptionTool() { const criticalTools = this.toolRegistry.getToolsByCapability( "priority", "critical" ); for (const entry of criticalTools) { const tool = entry.tool; const name = tool.name.toLowerCase(); const desc = tool.description?.toLowerCase() || ""; if (name.includes("inscribe") || name.includes("hashinal") || desc.includes("inscribe") || desc.includes("hashinal")) { return tool; } } const allTools = this.toolRegistry.getAllRegistryEntries(); for (const entry of allTools) { const tool = entry.tool; const name = tool.name.toLowerCase(); const desc = tool.description?.toLowerCase() || ""; if (name.includes("inscribe") || name.includes("hashinal") || desc.includes("inscribe") || desc.includes("hashinal")) { return tool; } } return null; } /** * Execute a tool directly with parameters, optionally using ExecutionPipeline */ async executeToolDirect(toolName, parameters, useExecutionPipeline = false) { if (useExecutionPipeline && this.executionPipeline && this.smartMemory) { const sessionContext = { sessionId: `session-${Date.now()}`, timestamp: Date.now() }; const result = await this.executionPipeline.execute( toolName, parameters, sessionContext ); if (!result.success) { throw new Error(result.error || "Pipeline execution failed"); } return result.output; } const entry = this.toolRegistry.getTool(toolName); if (!entry) { throw new Error(`Tool not found: ${toolName}`); } let processedParameters = { ...parameters }; if (this.pendingParameterPreprocessingCallback) { this.logger.info( "Applying parameter preprocessing in executeToolDirect", { toolName, hasCallback: true, parameterKeys: Object.keys(parameters) } ); try { processedParameters = await this.pendingParameterPreprocessingCallback( toolName, parameters ); if (JSON.stringify(processedParameters) !== JSON.stringify(parameters)) { this.logger.info("Parameters preprocessed successfully", { toolName, originalKeys: Object.keys(parameters), processedKeys: Object.keys(processedParameters), changes: Object.keys(processedParameters).filter( (key) => processedParameters[key] !== parameters[key] ) }); } } catch (error) { this.logger.warn( "Parameter preprocessing failed, using original parameters", { toolName, error: error instanceof Error ? error.message : "Unknown error" } ); processedParameters = parameters; } } const mergedArgs = { ...processedParameters, renderForm: false }; if (entry.wrapper) { const maybeWrapper = entry.tool; if (maybeWrapper.originalTool?.call) { return await maybeWrapper.originalTool.call(mergedArgs); } } return await entry.tool.call(mergedArgs); } /** * Create a standard ChatResponse from tool output */ createToolResponse(toolOutput) { return { output: toolOutput, message: toolOutput, notes: [] }; } /** * Handle TOOL_EXECUTION format messages */ async handleToolExecution(message, context) { let isToolExecution = false; let toolExecutionData = null; try { if (message.includes("TOOL_EXECUTION")) { const parsed = JSON.parse(message); if (parsed.type === "TOOL_EXECUTION") { isToolExecution = true; toolExecutionData = parsed; } } } catch { } if (!isToolExecution || !toolExecutionData?.formId) { return null; } try { const params = toolExecutionData.parameters || {}; const toolName = toolExecutionData.toolName; if (toolName) { const toolOutput = await this.executeToolDirect(toolName, params); try { const payload = this.isJSON(toolOutput) ? toolOutput : JSON.stringify({ output: toolOutput }); this.addToolRawToMemory(toolName, payload); } catch { } return this.createToolResponse(toolOutput); } } catch { } const formSubmission = { formId: toolExecutionData.formId, toolName: toolExecutionData.toolName || "", parameters: toolExecutionData.parameters || {}, timestamp: Date.now() }; if (this.executor && "processFormSubmission" in this.executor && typeof this.executor.processFormSubmission === "function") { return this.processFormSubmission(formSubmission, context); } return null; } /** * Handle direct tool execution commands */ async handleDirectToolExecution(message) { if (typeof message !== "string" || !message.includes("Please execute the following tool:")) { return null; } try { const toolLineMatch = message.match(/Tool:\s*(.+)/); const argsLineIndex = message.indexOf("Arguments:"); if (toolLineMatch && argsLineIndex !== -1) { const toolName = toolLineMatch[1].trim(); const argsText = message.slice(argsLineIndex + "Arguments:".length).trim(); let args = {}; try { args = JSON.parse(argsText); } catch { } const toolOutput = await this.executeToolDirect(toolName, args); try { const payload = this.isJSON(toolOutput) ? toolOutput : JSON.stringify({ output: toolOutput }); this.addToolRawToMemory(toolName, payload); } catch { } return this.createToolResponse(toolOutput); } } catch { } return null; } /** * Handle JSON format tool calls and form submissions */ async handleJsonToolCalls(message, context) { if (typeof message !== "string") { return null; } try { const trimmed = message.trim(); if (!(trimmed.startsWith("{") && trimmed.endsWith("}")) && !(trimmed.startsWith("[") && trimmed.endsWith("]"))) { return null; } const obj = JSON.parse(trimmed); const formId = obj["formId"]; const toolName = obj["toolName"] || ""; const parameters = obj["parameters"] || {}; if (formId && this.executor && "processFormSubmission" in this.executor && typeof this.executor.processFormSubmission === "function") { return this.processFormSubmission( { formId, toolName, parameters, timestamp: Date.now() }, context ); } if (toolName) { const toolOutput = await this.executeToolDirect(toolName, parameters); try { const payload = this.isJSON(toolOutput) ? toolOutput : JSON.stringify({ output: toolOutput }); this.addToolRawToMemory(toolName, payload); } catch { } return this.createToolResponse(toolOutput); } } catch { } return null; } /** * Handle content-ref messages for inscription tools */ async handleContentRefMessages(message) { if (typeof message !== "string" || !message.includes("content-ref:")) { return null; } try { const tool = this.getInscriptionTool(); if (!tool) { return null; } const idMatch = message.match(/content-ref:([A-Za-z0-9_\-]+)/i) || message.match(/content-ref:([^\s)]+)/i); const contentRef = idMatch && idMatch[1] ? `content-ref:${idMatch[1]}` : message.match(/content-ref:[^\s)]+/i)?.[0] || void 0; const args = contentRef ? { contentRef, renderForm: true, withHashLinkBlocks: true } : { renderForm: true, withHashLinkBlocks: true }; const toolOutput = await tool.call(args); let parsed; try { parsed = typeof toolOutput === "string" ? JSON.parse(toolOutput) : toolOutput; } catch { } if (parsed && parsed["requiresForm"] && parsed["formMessage"]) { const pending = /* @__PURE__ */ new Map(); const originalInput = { input: message, chat_history: this.smartMemory.getMessages() }; const formMessage = parsed["formMessage"]; pending.set(formMessage.id, { toolName: tool.name, originalInput, originalToolInput: args, schema: null }); const maybeRestore = this.executor; if (typeof maybeRestore.restorePendingForms === "function") { maybeRestore.restorePendingForms(pending); } const outputMsg = parsed["message"] || "Please complete the form to continue."; return { output: outputMsg, message: outputMsg, notes: [], requiresForm: true, formMessage }; } } catch { } return null; } /** * Process executor result and format response */ async processExecutorResult(result) { let outputStr = ""; if (typeof result.output === "string") { outputStr = result.output; } else if (result.output) { try { outputStr = JSON.stringify(result.output); } catch { outputStr = String(result.output); } } let response = { output: outputStr, message: outputStr, notes: [], intermediateSteps: result.intermediateSteps }; if (result.requiresForm && result.formMessage) { response.formMessage = result.formMessage; response.requiresForm = true; } 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; } this.persistIntermediateSteps( result.intermediateSteps ); } const steps = result?.intermediateSteps || []; const lastJsonObservation = [...steps].reverse().find( (s) => typeof s?.observation === "string" && this.isJSON(s.observation) )?.observation; if (lastJsonObservation) { try { const parsed = JSON.parse(lastJsonObservation); if (ResponseFormatter.isInscriptionResponse(parsed)) { const formattedMessage = ResponseFormatter.formatInscriptionResponse(parsed); response.output = formattedMessage; response.message = formattedMessage; if (parsed.inscription) { response.inscription = parsed.inscription; } if (parsed.metadata) { response.metadata = { ...response.metadata, ...parsed.metadata }; } } else { if (typeof parsed.message === "string" && parsed.message.trim().length > 0) { response.message = parsed.message; response.output = parsed.message; } if (parsed.success === true) { delete response.error; } if (typeof parsed.transactionBytes === "string") { response.metadata = { ...response.metadata, transactionBytes: parsed.transactionBytes }; } if (typeof parsed.scheduleId === "string") { response.scheduleId = parsed.scheduleId; } } const blockMetadata = this.processHashLinkBlocks(parsed); if (blockMetadata.hashLinkBlock) { response.metadata = { ...response.metadata, ...blockMetadata }; } } catch (error) { this.logger.error("Error parsing intermediate steps:", error); } } if (!response.output || response.output.trim() === "") { response.output = "Agent action complete."; } if (response.output) { 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; } /** * Normalize context messages into LangChain message instances and load into memory */ /** * Loads context messages into memory, merging with existing messages */ loadContextMessages(context) { if (!this.smartMemory || !context?.messages || context.messages.length === 0) { return; } const existingMessages = this.smartMemory.getMessages(); const existingContent = new Set( existingMessages.map((m) => `${m.constructor.name}:${m.content}`) ); for (const msg of context.messages) { let messageClass; let content; if (msg instanceof HumanMessage || msg instanceof AIMessage || msg instanceof SystemMessage) { messageClass = msg.constructor; content = msg.content; } else if (msg && typeof msg === "object" && "content" in msg && "type" in msg) { content = String(msg.content); const type = String(msg.type); if (type === "human") messageClass = HumanMessage; else if (type === "ai") messageClass = AIMessage; else if (type === "system") messageClass = SystemMessage; else continue; } else { continue; } const key = `${messageClass.name}:${content}`; if (!existingContent.has(key)) { this.smartMemory.addMessage(new messageClass(content)); existingContent.add(key); } } } async boot() { this.logger.info("🚨🚨🚨 LANGCHAIN AGENT BOOT METHOD CALLED 🚨🚨🚨"); 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"; try { if (typeof TokenUsageCallbackHandler === "function") { this.tokenTracker = new TokenUsageCallbackHandler(modelName); } else { this.logger.warn("TokenUsageCallbackHandler unavailable or not a constructor; skipping token tracking"); } } catch { this.logger.warn("TokenUsageCallbackHandler threw; skipping token tracking"); } this.toolRegistry = new ToolRegistry(this.logger); const allTools = this.agentKit.getAggregatedLangChainTools(); this.logger.info("=== TOOL REGISTRATION START ==="); this.logger.info( "All tools from agentKit:", allTools.map((t) => t.name) ); const filteredTools = this.filterTools(allTools); this.logger.info( "Filtered tools for registration:", filteredTools.map((t) => t.name) ); for (const tool of filteredTools) { this.logger.info(`🔧 Registering tool: ${tool.name}`); const options = {}; const name = tool.name.toLowerCase(); const desc = tool.description?.toLowerCase() || ""; if (tool.name === "hedera-hts-mint-nft") { const originalCall = tool.call.bind(tool); tool.call = async (args) => { if (args.metaOptions && typeof args.metaOptions === "object") { const metaOptions = args.metaOptions; if (metaOptions.transactionMemo) { this.logger.warn( "🚨 WORKAROUND: Stripping transactionMemo from hedera-hts-mint-nft to avoid bug", { originalMemo: metaOptions.transactionMemo } ); delete metaOptions.transactionMemo; } } return originalCall(args); }; } if (name.includes("inscribe") || name.includes("hashinal") || desc.includes("inscribe") || desc.includes("hashinal")) { options.forceWrapper = true; options.metadata = { category: "core", version: "1.0.0", dependencies: [] }; this.logger.info(`🎯 CRITICAL TOOL DEBUG - ${tool.name} schema:`, { hasSchema: !!tool.schema, schemaType: tool.schema?.constructor?.name, hasRenderConfig: !!tool.schema?._renderConfig, renderConfig: tool.schema?._renderConfig }); } this.toolRegistry.registerTool(tool, options); } this.tools = this.toolRegistry.getAllTools(); this.logger.info(`🚀 TOOLS REGISTERED: ${this.tools.length} tools`); const stats = this.toolRegistry.getStatistics(); this.logger.info("📊 Tool Registry Statistics:", { total: stats.totalTools, wrapped: stats.wrappedTools, unwrapped: stats.unwrappedTools, categories: stats.categoryCounts, priorities: stats.priorityCounts }); const inscriptionTool = this.getInscriptionTool(); if (inscriptionTool) { const entry = this.toolRegistry.getTool(inscriptionTool.name); if (entry) { this.logger.info( `✅ Inscription tool registered: ${inscriptionTool.name}` ); } } const toolNames = this.toolRegistry.getToolNames(); const uniqueNames = new Set(toolNames); if (toolNames.length !== uniqueNames.size) { this.logger.error("DUPLICATE TOOL NAMES DETECTED in registry!"); const duplicates = toolNames.filter( (name, index) => toolNames.indexOf(name) !== index ); throw new Error( `Duplicate tool names detected: ${duplicates.join(", ")}` ); } 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.formEngine = new FormEngine(this.logger); this.executionPipeline = new ExecutionPipeline( this.toolRegistry, this.formEngine, this.smartMemory, this.logger ); this.systemMessage = this.buildSystemPrompt(); this.smartMemory.setSystemPrompt(this.systemMessage); await this.createExecutor(); this.initialized = true; this.logger.info("LangChain Hedera agent initialized with ToolRegistry"); } 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 { const toolExecutionResult = await this.handleToolExecution( message, context ); if (toolExecutionResult) { return toolExecutionResult; } const directToolResult = await this.handleDirectToolExecution(message); if (directToolResult) { return directToolResult; } const jsonToolResult = await this.handleJsonToolCalls(message, context); if (jsonToolResult) { return jsonToolResult; } const contentRefResult = await this.handleContentRefMessages(message); if (contentRefResult) { return contentRefResult; } this.logger.info("LangChainAgent.chat called with:", { message, contextLength: context?.messages?.length || 0 }); this.loadContextMessages(context); 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 currentMessages = this.smartMemory.getMessages(); this.logger.info("Current messages in memory:", { count: currentMessages.length }); try { const instr = currentMessages.map((m) => String(m.content || "")).filter( (c) => typeof c === "string" && (c.includes("[instruction:") || c.includes("[tool-next-steps:")) ); if (instr.length > 0) { this.logger.info("Instruction/next-steps messages in memory:", { messages: instr }); } } catch { } const result = await this.executor.invoke({ input: message, chat_history: currentMessages }); this.logger.info("LangChainAgent executor result:", result); return this.processExecutorResult(result); } 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; } if (this.toolRegistry) { this.toolRegistry.clear(); } 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); } /** * Processes form submission and continues with tool execution */ async processFormSubmission(submission, context) { if (!this.initialized || !this.executor || !this.smartMemory) { throw new Error("Agent not initialized. Call boot() first."); } try { if (!submission.parameters || typeof submission.parameters !== "object") { this.logger.error("Invalid form submission parameters:", { parameters: submission.parameters, type: typeof submission.parameters }); const errorInfo = JSON.stringify(submission, null, 2); return this.handleError( new Error(`Invalid form submission parameters: ${errorInfo}`) ); } this.loadContextMessages(context); const safeSubmission = { ...submission, parameters: submission.parameters || {} }; const result = await this.executor.processFormSubmission(safeSubmission); const preservedMetadata = result?.metadata ? { ...result.metadata } : {}; try { const maybeRaw = result.rawToolOutput; const toolName = result.toolName || "unknown"; if (typeof maybeRaw === "string" && maybeRaw.trim().length > 0) { const payload = this.isJSON(maybeRaw) ? maybeRaw : JSON.stringify({ output: maybeRaw }); this.addToolRawToMemory(toolName, payload); } } catch { } let outputMessage = "Form processed successfully."; if (typeof result.output === "string") { outputMessage = result.output; } else if (result.output) { try { outputMessage = JSON.stringify(result.output); } catch { outputMessage = String(result.output); } } let response = { output: outputMessage, message: outputMessage, notes: [], intermediateSteps: result.intermediateSteps }; if (result.metadata) { response.metadata = { ...response.metadata, ...result.metadata }; this.logger.info("🔍 DEBUG: Metadata after merge from result:", { hasMetadata: !!response.metadata, metadataKeys: response.metadata ? Object.keys(response.metadata) : [], hasHashLinkBlock: hasHashLinkBlock(response.metadata), hashLinkBlockContent: hasHashLinkBlock(response.metadata) ? response.metadata.hashLinkBlock : void 0 }); } if (result.requiresForm && result.formMessage) { response.formMessage = result.formMessage; response.requiresForm = true; } if (result.intermediateSteps && Array.isArray(result.intermediateSteps)) { const toolCalls = result.intermediateSteps.map( (step, index) => { const name = step?.action?.tool || "unknown"; const args = step?.action?.toolInput || {}; const obs = step?.observation; let output = ""; if (typeof obs === "string") { output = obs; } else if (obs && typeof obs === "object") { try { output = JSON.stringify(obs); } catch { output = String(obs); } } else if (obs !== void 0) { output = String(obs); } return { id: `call_${index}`, name, args, output }; } ); if (toolCalls.length > 0) { response.tool_calls = toolCalls; } this.persistIntermediateSteps( result.intermediateSteps ); } const parsedSteps = result?.intermediateSteps?.[0]?.observation; if (parsedSteps && typeof parsedSteps === "string" && this.isJSON(parsedSteps)) { try { const parsed = JSON.parse(parsedSteps); response = { ...response, ...parsed }; const blockMetadata = this.processHashLinkBlocks(parsed); if (blockMetadata.hashLinkBlock) { response.metadata = { ...response.metadata, ...blockMetadata }; } } catch (error) { this.logger.error("Error parsing intermediate steps:", error); } } if (response.output) { 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(); this.logger.info("🔍 DEBUG: Metadata before memoryStats merge:", { hasMetadata: !!response.metadata, metadataKeys: response.metadata ? Object.keys(response.metadata) : [], hasHashLinkBlock: hasHashLinkBlock(response.metadata) }); response.metadata = { ...preservedMetadata, ...response.metadata, memoryStats: { activeMessages: finalMemoryStats.totalActiveMessages, tokenUsage: finalMemoryStats.currentTokenCount, maxTokens: finalMemoryStats.maxTokens, usagePercentage: finalMemoryStats.usagePercentage } }; this.logger.info("🔍 DEBUG: Final response metadata before return:", { hasMetadata: !!response.metadata, metadataKeys: response.metadata ? Object.keys(response.metadata) : [], hasHashLinkBlock: hasHashLinkBlock(response.metadata), fullMetadata: response.metadata }); if (hasHashLinkBlock(preservedMetadata) && !hasHashLinkBlock(response.metadata)) { this.logger.error( "❌ CRITICAL: HashLink metadata was lost during processing!" ); this.logger.error( "Original metadata had hashLinkBlock:", preservedMetadata.hashLinkBlock ); this.logger.error("Final metadata missing hashLinkBlock"); } return response; } catch (error) { this.logger.error("Form submission processing error:", error); return this.handleError(error); } } /** * Check if the agent has pending forms that need to be completed */ hasPendingForms() { return this.executor ? this.executor.hasPendingForms() : false; } /** * Get information about pending forms */ getPendingFormsInfo() { return this.executor ? this.executor.getPendingFormsInfo() : []; } 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() { const existingPendingForms = this.executor?.getPendingForms() || /* @__PURE__ */ new Map(); 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 inscriptionTool = this.getInscriptionTool(); if (inscriptionTool) { const entry = this.toolRegistry.getTool(inscriptionTool.name); if (entry) { this.logger.info( `✅ Inscription tool registered: ${inscriptionTool.name}` ); } } const stats = this.toolRegistry.getStatistics(); this.logger.info("🛡️ TOOL SECURITY REPORT:", { totalTools: stats.totalTools, wrappedTools: stats.wrappedTools, unwrappedTools: stats.unwrappedTools, categories: stats.categoryCounts, priorities: stats.priorityCounts }); this.logger.info( `📊 Tool Security Summary: ${stats.wrappedTools} wrapped, ${stats.unwrappedTools} unwrapped` ); const agent = await createOpenAIToolsAgent({ llm, tools: langchainTools, prompt }); this.executor = new FormAwareAgentExecutor({ agent, tools: langchainTools, verbose: this.config.debug?.verbose ?? false, returnIntermediateSteps: true }); if (this.pendingParameterPreprocessingCallback) { this.executor.setParameterPreprocessingCallback( this.pendingParameterPreprocessingCallback ); this.logger.info( "Parameter preprocessing callback re-applied to new executor", { hasCallback: true } ); } if (existingPendingForms.size > 0) { this.logger.info( `Restoring ${existingPendingForms.size} pending forms to new executor` ); this.executor.restorePendingForms(existingPendingForms); } this.logger.info("FormAwareAgentExecutor initialization complete"); } /** * Set parameter preprocessing callback for tool parameter format conversion */ setParameterPreprocessingCallback(callback) { this.pendingParameterPreprocessingCallback = callback; if (this.executor) { this.executor.setParameterPreprocessingCallback(callback); this.logger.info("Parameter preprocessing callback configured", { hasCallback: !!callback }); } else { this.logger.warn( "Cannot set parameter preprocessing callback: executor not initialized" ); } } 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 = ERROR_MESSAGES.TOO_MANY_REQUESTS; userFriendlyOutput = ERROR_MESSAGES.RATE_LIMITED; } } 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.toolRegistry.registerTool(langchainTool, { metadata: { category: "mcp", version: "1.0.0", dependencies: [serverConfig.name] } }); } this.tools = this.toolRegistry.getAllTools(); } 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.toolRegistry.registerTool(langchainTool, { metadata: { category: "mcp", version: "1.0.0", dependencies: [serverConfig.name] } }); } this.tools = this.toolRegistry.getAllTools(); 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); } /** * Detects and processes HashLink blocks from tool responses * @param parsedResponse - The parsed JSON response from a tool * @returns Metadata object containing hashLinkBlock if detected */ processHashLinkBlocks(parsedResponse) { try { const responseRecord = parsedResponse; if (parsedResponse && typeof parsedResponse === "object" && responseRecord.hashLinkBlock && typeof responseRecord.hashLinkBlock === "object") { const block = responseRecord.hashLinkBlock; if (block.blockId && block.hashLink && block.template && block.attributes && typeof block.blockId === "string" && typeof block.hashLink === "string" && typeof block.template === "string" && typeof block.attributes === "object") { this.logger.info("HashLink block detected:", { blockId: block.blockId, hashLink: block.hashLink, template: block.template, attributeKeys: Object.keys(block.attributes) }); return { hashLinkBlock: { blockId: block.blockId, hashLink: block.hashLink, template: block.template, attributes: block.attributes } }; } else { this.logger.warn("Invalid HashLink block structure detected:", block); } } } catch (error) { this.logger.error("Error processing HashLink blocks:", error); } return {}; } /** * 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=index34.js.map