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

848 lines (847 loc) 28.8 kB
import { ServerSigner, getAllHederaCorePlugins } from "hedera-agent-kit"; import { Logger } from "@hashgraphonline/standards-sdk"; import { createAgent } from "./index9.js"; import { BrowserSigner } from "./index40.js"; import { LangChainProvider } from "./index10.js"; import { ChatOpenAI } from "@langchain/openai"; import { ChatAnthropic } from "@langchain/anthropic"; import { SystemMessage, HumanMessage, AIMessage } from "@langchain/core/messages"; import { HCS10Plugin } from "./index2.js"; import { HCS2Plugin } from "./index3.js"; import { InscribePlugin } from "./index4.js"; import { getWalletBridgeProvider } from "./index37.js"; import { OpenConvaiState, InscriberBuilder, SignerProviderRegistry } from "@hashgraphonline/standards-agent-kit"; import { HbarPlugin } from "./index5.js"; import { WebBrowserPlugin } from "./index41.js"; import { getSystemMessage } from "./index42.js"; import { ContentStoreManager } from "./index25.js"; import { SmartMemoryManager } from "./index19.js"; import "./index20.js"; import "./index21.js"; import "./index22.js"; import "crypto"; import { createEntityTools } from "./index35.js"; import { ParameterService } from "./index30.js"; import { FormatConverterRegistry } from "./index26.js"; import { TopicIdToHrlConverter } from "./index28.js"; import { StringNormalizationConverter } from "./index29.js"; const DEFAULT_MODEL_NAME = "gpt-4o"; const DEFAULT_OPENAI_MODEL = "gpt-4o-mini"; const DEFAULT_OPENROUTER_MODEL = "openai/gpt-4o-mini"; const DEFAULT_CLAUDE_MODEL = "claude-3-7-sonnet-latest"; const DEFAULT_TEMPERATURE = 0.1; const DEFAULT_NETWORK = "testnet"; const DEFAULT_OPERATIONAL_MODE = "autonomous"; const _ConversationalAgent = class _ConversationalAgent { constructor(options) { this.options = options; this.stateManager = options.stateManager || new OpenConvaiState(); this.hcs10Plugin = new HCS10Plugin(); this.hcs2Plugin = new HCS2Plugin(); this.inscribePlugin = new InscribePlugin(); this.hbarPlugin = new HbarPlugin(); this.webBrowserPlugin = new WebBrowserPlugin(); this.logger = new Logger({ module: "ConversationalAgent", silent: options.disableLogging || false }); if (this.options.entityMemoryEnabled !== false) { if (!options.openAIApiKey) { throw new Error( "OpenAI/Anthropic API key is required when entity memory is enabled" ); } this.memoryManager = new SmartMemoryManager( this.options.entityMemoryConfig ); this.logger.info("Entity memory initialized"); const provider = options.entityMemoryProvider || options.llmProvider || "openai"; let modelName = options.entityMemoryModelName; if (!modelName) { if (provider === "anthropic") { modelName = DEFAULT_CLAUDE_MODEL; } else if (provider === "openrouter") { modelName = DEFAULT_OPENROUTER_MODEL; } else { modelName = DEFAULT_OPENAI_MODEL; } } let resolverLLM; if (provider === "anthropic") { resolverLLM = new ChatAnthropic({ apiKey: options.openAIApiKey, model: modelName, temperature: 0 }); } else if (provider === "openrouter") { const baseURL = options.openRouterBaseURL || "https://openrouter.ai/api/v1"; const apiKey = options.openRouterApiKey || options.openAIApiKey; resolverLLM = new ChatOpenAI({ apiKey, model: modelName, temperature: 0, configuration: { baseURL, defaultHeaders: { "HTTP-Referer": process.env.OPENROUTER_REFERRER || "https://hashgraphonline.com", "X-Title": process.env.OPENROUTER_TITLE || "Hashgraph Online Conversational Agent" } } }); } else { resolverLLM = new ChatOpenAI({ apiKey: options.openAIApiKey, model: modelName, temperature: 0 }); } this.entityTools = createEntityTools(resolverLLM); this.logger.info("LLM-based entity resolver tools initialized"); } } /** * Initialize the conversational agent with Hedera Hashgraph connection and AI configuration * @throws {Error} If account ID or private key is missing * @throws {Error} If initialization fails */ async initialize() { const { accountId, privateKey, network = DEFAULT_NETWORK, openAIApiKey, openAIModelName = DEFAULT_MODEL_NAME, llmProvider = "openai" } = this.options; this.validateOptions(accountId, privateKey); try { const opMode = this.options.operationalMode || DEFAULT_OPERATIONAL_MODE; const bytesMode = opMode !== "autonomous"; let signer; try { const IB = InscriberBuilder; if (typeof IB.setPreferWalletOnly === "function") { IB.setPreferWalletOnly(false); } } catch (e) { this.logger.warn("Failed to set wallet-only preference", e); } if (!bytesMode) { signer = new ServerSigner( accountId, privateKey, network ); } else { const chain = String(network || "testnet") === "mainnet" ? "mainnet" : "testnet"; const effectiveAccount = this.options.userAccountId || accountId; signer = new BrowserSigner( effectiveAccount, chain, this.options.walletExecutor ); } this.logger.info("Signer configured", { operationalMode: opMode, bytesMode, signerClass: Object.getPrototypeOf(signer)?.constructor?.name || "unknown" }); try { const bridge = getWalletBridgeProvider(); if (bridge) { const IB = InscriberBuilder; if (typeof IB.setWalletInfoResolver === "function") { IB.setWalletInfoResolver(async () => { const status = await bridge.status(); if (status.connected && status.accountId && status.network) { return { accountId: status.accountId, network: status.network }; } return null; }); } if (typeof IB.setWalletExecutor === "function") { IB.setWalletExecutor( async (base64, network2) => { return await bridge.executeBytes(base64, network2); } ); } if (typeof IB.setStartInscriptionDelegate === "function" && bridge.startInscription) { IB.setStartInscriptionDelegate( async (request, network2) => { return await bridge.startInscription(request, network2); } ); } try { const status = await bridge.status(); const enforceWallet = !!(bytesMode && status.connected); SignerProviderRegistry.setWalletInfoResolver(async () => { const s = await bridge.status(); if (s.connected && s.accountId && s.network) { return { accountId: s.accountId, network: s.network }; } return null; }); SignerProviderRegistry.setWalletExecutor( async (base64, network2) => { return await bridge.executeBytes(base64, network2); } ); const extended = bridge; if (typeof extended?.startHCS === "function") { SignerProviderRegistry.setStartHCSDelegate( async (op, request, network2) => { return await extended.startHCS( op, request, network2 ); } ); } else { SignerProviderRegistry.setStartHCSDelegate(null); } SignerProviderRegistry.setPreferWalletOnly(enforceWallet); const IB2 = InscriberBuilder; if (typeof IB2.setPreferWalletOnly === "function") { IB2.setPreferWalletOnly(enforceWallet); } } catch (sakWireErr) { this.logger.warn( "Failed to wire SAK SignerProviderRegistry wallet delegates", sakWireErr ); } } } catch (e) { this.logger.warn( "Failed to register wallet bridge providers", e ); } let llm; let providerInfo = { provider: llmProvider }; if (llmProvider === "anthropic") { llm = new ChatAnthropic({ apiKey: openAIApiKey, model: openAIModelName || DEFAULT_CLAUDE_MODEL, temperature: DEFAULT_TEMPERATURE }); providerInfo = { ...providerInfo, model: openAIModelName || DEFAULT_CLAUDE_MODEL, keyPresent: !!openAIApiKey }; } else if (llmProvider === "openrouter") { const baseURL = this.options.openRouterBaseURL || "https://openrouter.ai/api/v1"; const apiKey = this.options.openRouterApiKey || openAIApiKey; const modelName = openAIModelName || "anthropic/claude-3-haiku-20240307"; llm = new ChatOpenAI({ apiKey, model: modelName, temperature: DEFAULT_TEMPERATURE, configuration: { baseURL, defaultHeaders: { "HTTP-Referer": process.env.OPENROUTER_REFERRER || "https://hashgraphonline.com", "X-Title": process.env.OPENROUTER_TITLE || "Hashgraph Online Conversational Agent" } } }); providerInfo = { ...providerInfo, model: modelName, baseURL, keyPresent: !!apiKey }; } else { const modelName2 = openAIModelName || DEFAULT_OPENAI_MODEL; const isGPT5Model = modelName2.toLowerCase().includes("gpt-5") || modelName2.toLowerCase().includes("gpt5"); llm = new ChatOpenAI({ apiKey: openAIApiKey, model: modelName2, ...isGPT5Model ? { temperature: 1 } : { temperature: DEFAULT_TEMPERATURE } }); providerInfo = { ...providerInfo, model: modelName2, keyPresent: !!openAIApiKey }; } this.logger.info("AI provider configured", providerInfo); this.logger.info("Preparing plugins..."); const allPlugins = this.preparePlugins(); this.logger.info("Creating agent config..."); const agentConfig = this.createAgentConfig( signer, llm, allPlugins ); this.logger.info("Creating agent..."); this.agent = createAgent(agentConfig); this.logger.info("Agent created"); this.logger.info("Configuring HCS10 plugin..."); this.configureHCS10Plugin(allPlugins); this.logger.info("HCS10 plugin configured"); this.contentStoreManager = new ContentStoreManager(); await this.contentStoreManager.initialize(); this.logger.info( "ContentStoreManager initialized for content reference support" ); this.logger.info("About to call agent.boot()"); this.logger.info("🔥 About to call agent.boot()"); await this.agent.boot(); this.logger.info("agent.boot() completed"); this.logger.info("🔥 agent.boot() completed"); if (this.agent) { try { const registry = new FormatConverterRegistry(); registry.register(new TopicIdToHrlConverter()); registry.register(new StringNormalizationConverter()); const paramService = new ParameterService( registry, this.options.network || "testnet" ); paramService.attachToAgent(this.agent, { getEntities: async () => this.memoryManager?.getEntityAssociations() || [] }); this.logger.info( "Parameter preprocessing callback attached (internal)" ); } catch (e) { this.logger.warn( "Failed to attach internal parameter preprocessing callback", e ); } const cfg = agentConfig; cfg.filtering = cfg.filtering || {}; const originalPredicate = cfg.filtering.toolPredicate; const userPredicate = this.options.toolFilter; cfg.filtering.toolPredicate = (tool) => { if (tool && tool.name === "hedera-account-transfer-hbar") { return false; } if (tool && tool.name === "hedera-hts-airdrop-token") { return false; } if (originalPredicate && !originalPredicate(tool)) { return false; } if (userPredicate && !userPredicate(tool)) { return false; } return true; }; } if (this.options.mcpServers && this.options.mcpServers.length > 0) { this.connectMCP(); } } catch (error) { this.logger.error("Failed to initialize ConversationalAgent:", error); throw error; } } /** * Get the HCS-10 plugin instance * @returns {HCS10Plugin} The HCS-10 plugin instance */ getPlugin() { return this.hcs10Plugin; } /** * Get the state manager instance * @returns {IStateManager} The state manager instance */ getStateManager() { return this.stateManager; } /** * Get the underlying agent instance * @returns {ReturnType<typeof createAgent>} The agent instance * @throws {Error} If agent is not initialized */ getAgent() { if (!this.agent) { throw new Error(_ConversationalAgent.NOT_INITIALIZED_ERROR); } return this.agent; } /** * Get the conversational agent instance (alias for getAgent) * @returns {ReturnType<typeof createAgent>} The agent instance * @throws {Error} If agent is not initialized */ getConversationalAgent() { return this.getAgent(); } /** * Process a message through the conversational agent * @param {string} message - The message to process * @param {Array<{type: 'human' | 'ai'; content: string}>} chatHistory - Previous chat history * @returns {Promise<ChatResponse>} The agent's response * @throws {Error} If agent is not initialized */ async processMessage(message, chatHistory = []) { if (!this.agent) { throw new Error("Agent not initialized. Call initialize() first."); } try { const resolvedMessage = message; const messages = chatHistory.map((msg) => { const content = msg.content; if (msg.type === "system") { return new SystemMessage(content); } return msg.type === "human" ? new HumanMessage(content) : new AIMessage(content); }); const context = { messages }; const response = await this.agent.chat(resolvedMessage, context); if (this.memoryManager && this.options.operationalMode !== "returnBytes") { await this.extractAndStoreEntities(response, message); } this.logger.info("Message processed successfully"); return response; } catch (error) { this.logger.error("Error processing message:", error); throw error; } } /** * Process form submission through the conversational agent * @param {FormSubmission} submission - The form submission data * @returns {Promise<ChatResponse>} The agent's response after processing the form * @throws {Error} If agent is not initialized or doesn't support form processing */ async processFormSubmission(submission) { if (!this.agent) { throw new Error(_ConversationalAgent.NOT_INITIALIZED_ERROR); } try { this.logger.info("Processing form submission:", { formId: submission.formId, toolName: submission.toolName, parameterKeys: Object.keys(submission.parameters || {}), hasContext: !!submission.context }); const response = await this.agent.processFormSubmission(submission); this.logger.info("Form submission processed successfully"); return response; } catch (error) { this.logger.error("Error processing form submission:", error); throw error; } } /** * Validates initialization options and throws if required fields are missing. * * @param accountId - The Hedera account ID * @param privateKey - The private key for the account * @throws {Error} If required fields are missing */ validateOptions(accountId, privateKey) { const opMode = this.options.operationalMode || DEFAULT_OPERATIONAL_MODE; const bytesMode = opMode !== "autonomous"; if (!accountId) { throw new Error("Account ID is required"); } if (!privateKey && !bytesMode) { throw new Error("Private key is required in autonomous mode"); } if (typeof accountId !== "string") { throw new Error( `Account ID must be a string, received ${typeof accountId}` ); } if (!bytesMode && typeof privateKey !== "string") { throw new Error( `Private key must be a string, received ${typeof privateKey}: ${JSON.stringify( privateKey )}` ); } if (!bytesMode && typeof privateKey === "string" && privateKey.length < 10) { throw new Error("Private key appears to be invalid (too short)"); } } /** * Prepares the list of plugins to use based on configuration. * * @returns Array of plugins to initialize with the agent */ preparePlugins() { const { additionalPlugins = [], enabledPlugins, disabledPlugins } = this.options; const standardPlugins = [ this.hcs10Plugin, this.hcs2Plugin, this.inscribePlugin, this.hbarPlugin ]; standardPlugins.push(this.webBrowserPlugin); const corePlugins = getAllHederaCorePlugins(); let pluginPool = [...standardPlugins, ...corePlugins]; if (enabledPlugins) { const enabledSet = new Set(enabledPlugins); pluginPool = pluginPool.filter((plugin) => enabledSet.has(plugin.id)); } if (disabledPlugins && disabledPlugins.length > 0) { const disabledSet = new Set(disabledPlugins); pluginPool = pluginPool.filter((plugin) => !disabledSet.has(plugin.id)); } const additional = disabledPlugins && disabledPlugins.length > 0 ? additionalPlugins.filter((plugin) => !disabledPlugins.includes(plugin.id)) : additionalPlugins; return [...pluginPool, ...additional]; } /** * Creates the agent configuration object. * * @param signer - The signer instance * @param llm - The language model instance * @param allPlugins - Array of plugins to use * @returns Configuration object for creating the agent */ createAgentConfig(signer, llm, allPlugins) { const { operationalMode = DEFAULT_OPERATIONAL_MODE, userAccountId, scheduleUserTransactionsInBytesMode, customSystemMessagePreamble, customSystemMessagePostamble, verbose = false, mirrorNodeConfig, disableLogging, accountId = "" } = this.options; return { framework: "langchain", signer, execution: { mode: operationalMode === "autonomous" ? "direct" : "bytes", operationalMode, ...userAccountId && { userAccountId }, ...scheduleUserTransactionsInBytesMode !== void 0 && { scheduleUserTransactionsInBytesMode, scheduleUserTransactions: scheduleUserTransactionsInBytesMode } }, ai: { provider: new LangChainProvider(llm), temperature: DEFAULT_TEMPERATURE }, filtering: { toolPredicate: (tool) => { if (tool.name === "hedera-account-transfer-hbar") return false; if (this.options.toolFilter && !this.options.toolFilter(tool)) { return false; } return true; } }, messaging: { systemPreamble: customSystemMessagePreamble || getSystemMessage(accountId), ...customSystemMessagePostamble && { systemPostamble: customSystemMessagePostamble }, conciseMode: true }, extensions: { plugins: allPlugins, ...mirrorNodeConfig && { mirrorConfig: mirrorNodeConfig } }, ...this.options.mcpServers && { mcp: { servers: this.options.mcpServers, autoConnect: false } }, debug: { verbose, silent: disableLogging ?? false } }; } /** * Configures the HCS-10 plugin with the state manager. * * @param allPlugins - Array of all plugins */ configureHCS10Plugin(allPlugins) { const hcs10 = allPlugins.find((p) => p.id === "hcs-10"); if (hcs10) { hcs10.appConfig = { stateManager: this.stateManager }; } } /** * Create a ConversationalAgent with specific plugins enabled */ static withPlugins(options, plugins) { return new _ConversationalAgent({ ...options, enabledPlugins: plugins }); } /** * Create a ConversationalAgent with only HTS (Hedera Token Service) tools enabled */ static withHTS(options) { return this.withPlugins(options, ["hts-token"]); } /** * Create a ConversationalAgent with only HCS-2 tools enabled */ static withHCS2(options) { return this.withPlugins(options, ["hcs-2"]); } /** * Create a ConversationalAgent with only HCS-10 tools enabled */ static withHCS10(options) { return this.withPlugins(options, ["hcs-10"]); } /** * Create a ConversationalAgent with only inscription tools enabled */ static withInscribe(options) { return this.withPlugins(options, ["inscribe"]); } /** * Create a ConversationalAgent with only account management tools enabled */ static withAccount(options) { return this.withPlugins(options, ["account"]); } /** * Create a ConversationalAgent with only file service tools enabled */ static withFileService(options) { return this.withPlugins(options, ["file-service"]); } /** * Create a ConversationalAgent with only consensus service tools enabled */ static withConsensusService(options) { return this.withPlugins(options, ["consensus-service"]); } /** * Create a ConversationalAgent with only smart contract tools enabled */ static withSmartContract(options) { return this.withPlugins(options, ["smart-contract"]); } /** * Create a ConversationalAgent with all HCS standards plugins */ static withAllStandards(options) { return this.withPlugins(options, ["hcs-10", "hcs-2", "inscribe"]); } /** * Create a ConversationalAgent with minimal Hedera tools (no HCS standards) */ static minimal(options) { return this.withPlugins(options, []); } /** * Create a ConversationalAgent with MCP servers configured */ static withMCP(options, mcpServers) { return new _ConversationalAgent({ ...options, mcpServers }); } /** * Extract and store entities from agent responses * @param response - Agent response containing potential entity information * @param originalMessage - Original user message for context */ async extractAndStoreEntities(response, originalMessage) { if (!this.memoryManager || !this.entityTools) { return; } try { this.logger.info("Starting LLM-based entity extraction"); const responseText = this.extractResponseText(response); const entitiesJson = await this.entityTools.extractEntities.call({ response: responseText, userMessage: originalMessage }); try { const entities = JSON.parse(entitiesJson); for (const entity of entities) { if (entity && typeof entity === "object" && "name" in entity && "type" in entity && "id" in entity) { this.logger.info( `Storing entity: ${entity.name} (${entity.type}) -> ${entity.id}` ); const transactionId = this.extractTransactionId(response); const idStr = String(entity.id); const isHederaId = /^0\.0\.[0-9]+$/.test(idStr); if (!isHederaId) { this.logger.warn("Skipping non-ID entity from extraction", { id: idStr, name: String(entity.name), type: String(entity.type) }); } else { this.memoryManager.storeEntityAssociation( idStr, String(entity.name), String(entity.type), transactionId ); } } } if (entities.length > 0) { this.logger.info( `Stored ${entities.length} entities via LLM extraction` ); } else { this.logger.info("No entities found in response via LLM extraction"); } } catch (parseError) { this.logger.error( "Failed to parse extracted entities JSON:", parseError ); throw parseError; } } catch (error) { this.logger.error("Entity extraction failed:", error); throw error; } } /** * Extract transaction ID from response if available * @param response - Transaction response * @returns Transaction ID or undefined */ extractTransactionId(response) { try { if (typeof response === "object" && response && "transactionId" in response) { const responseWithTxId = response; return typeof responseWithTxId.transactionId === "string" ? responseWithTxId.transactionId : void 0; } if (typeof response === "string") { const match = response.match( /transaction[\s\w]*ID[\s:"]*([0-9a-fA-F@._-]+)/i ); return match ? match[1] : void 0; } return void 0; } catch { return void 0; } } /** * Connect to MCP servers asynchronously * @private */ connectMCP() { if (!this.agent || !this.options.mcpServers) { return; } this.agent.connectMCPServers().catch((e) => { this.logger.error("Failed to connect MCP servers:", e); }).then(() => { this.logger.info("MCP servers connected successfully"); }); } /** * Get MCP connection status for all servers * @returns {Map<string, MCPConnectionStatus>} Connection status map */ getMCPConnectionStatus() { if (this.agent) { return this.agent.getMCPConnectionStatus(); } return /* @__PURE__ */ new Map(); } /** * Check if a specific MCP server is connected * @param {string} serverName - Name of the server to check * @returns {boolean} True if connected, false otherwise */ isMCPServerConnected(serverName) { if (this.agent) { const statusMap = this.agent.getMCPConnectionStatus(); const status = statusMap.get(serverName); return status?.connected ?? false; } return false; } /** * Clean up resources */ async cleanup() { try { this.logger.info("Cleaning up ConversationalAgent..."); if (this.memoryManager) { try { this.memoryManager.dispose(); this.logger.info("Memory manager cleaned up successfully"); } catch (error) { this.logger.warn("Error cleaning up memory manager:", error); } this.memoryManager = void 0; } if (this.contentStoreManager) { await this.contentStoreManager.dispose(); this.logger.info("ContentStoreManager cleaned up"); } this.logger.info("ConversationalAgent cleanup completed"); } catch (error) { this.logger.error("Error during cleanup:", error); } } /** * Switch operational mode */ switchMode(mode) { if (this.agent?.switchMode) { this.agent.switchMode(mode || "autonomous"); } } /** * Get usage statistics */ getUsageStats() { return this.agent?.getUsageStats?.() ?? {}; } /** * Clear usage statistics */ clearUsageStats() { if (this.agent?.clearUsageStats) { this.agent.clearUsageStats(); } } /** * Shutdown the agent */ shutdown() { return this.agent?.shutdown?.() ?? Promise.resolve(); } extractResponseText(response) { if (typeof response === "string") { return response; } if (response && typeof response === "object" && "output" in response) { const responseWithOutput = response; return String(responseWithOutput.output); } return JSON.stringify(response); } }; _ConversationalAgent.NOT_INITIALIZED_ERROR = "Agent not initialized. Call initialize() first."; let ConversationalAgent = _ConversationalAgent; export { ConversationalAgent }; //# sourceMappingURL=index7.js.map