UNPKG

@hashgraphonline/conversational-agent

Version:

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

597 lines (596 loc) 19.1 kB
import { ServerSigner, getAllHederaCorePlugins } from "hedera-agent-kit"; import { Logger, HederaMirrorNode } from "@hashgraphonline/standards-sdk"; import { createAgent } from "./index9.js"; import { LangChainProvider } from "./index10.js"; import { ChatOpenAI } from "@langchain/openai"; import { ChatAnthropic } from "@langchain/anthropic"; import { HumanMessage, AIMessage } from "@langchain/core/messages"; import { HCS10Plugin } from "./index2.js"; import { HCS2Plugin } from "./index3.js"; import { InscribePlugin } from "./index4.js"; import { HbarPlugin } from "./index5.js"; import { OpenConvaiState } from "@hashgraphonline/standards-agent-kit"; import { PrivateKey } from "@hashgraph/sdk"; import { getSystemMessage } from "./index20.js"; import { ContentStoreManager } from "./index17.js"; import "./index12.js"; import "./index13.js"; import "./index14.js"; import { SmartMemoryManager } from "./index15.js"; import { createEntityTools } from "./index16.js"; const DEFAULT_MODEL_NAME = "gpt-4o"; const DEFAULT_TEMPERATURE = 0.1; const DEFAULT_NETWORK = "testnet"; const DEFAULT_OPERATIONAL_MODE = "autonomous"; 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.logger = new Logger({ module: "ConversationalAgent", silent: options.disableLogging || false }); if (this.options.entityMemoryEnabled !== false) { if (!options.openAIApiKey) { throw new Error( "OpenAI API key is required when entity memory is enabled" ); } this.memoryManager = new SmartMemoryManager( this.options.entityMemoryConfig ); this.logger.info("Entity memory initialized"); this.entityTools = createEntityTools(options.openAIApiKey, "gpt-4o-mini"); 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 privateKeyInstance = await this.detectPrivateKeyType( accountId, privateKey, network ); const serverSigner = new ServerSigner( accountId, privateKeyInstance, network ); let llm; if (llmProvider === "anthropic") { llm = new ChatAnthropic({ apiKey: openAIApiKey, modelName: openAIModelName || "claude-3-5-sonnet-20241022", temperature: DEFAULT_TEMPERATURE }); } else { const modelName = openAIModelName || "gpt-4o-mini"; const isGPT5Model = modelName.toLowerCase().includes("gpt-5") || modelName.toLowerCase().includes("gpt5"); llm = new ChatOpenAI({ apiKey: openAIApiKey, modelName: openAIModelName, ...isGPT5Model ? { temperature: 1 } : { temperature: DEFAULT_TEMPERATURE } }); } const allPlugins = this.preparePlugins(); const agentConfig = this.createAgentConfig(serverSigner, llm, allPlugins); this.agent = createAgent(agentConfig); this.configureHCS10Plugin(allPlugins); this.contentStoreManager = new ContentStoreManager(); await this.contentStoreManager.initialize(); this.logger.info( "ContentStoreManager initialized for content reference support" ); await this.agent.boot(); if (this.agent) { 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("Agent not initialized. Call initialize() first."); } 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 = this.memoryManager ? await this.resolveEntitiesInMessage(message) : message; const messages = chatHistory.map((msg) => { if (msg.type === "human") { return new HumanMessage(msg.content); } else { return new AIMessage(msg.content); } }); const context = { messages }; const response = await this.agent.chat(resolvedMessage, context); if (this.memoryManager) { await this.extractAndStoreEntities(response, message); } this.logger.info("Message processed successfully"); return response; } catch (error) { this.logger.error("Error processing message:", 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) { if (!accountId || !privateKey) { throw new Error("Account ID and private key are required"); } } /** * Prepares the list of plugins to use based on configuration. * * @returns Array of plugins to initialize with the agent */ preparePlugins() { const { additionalPlugins = [], enabledPlugins } = this.options; const standardPlugins = [ this.hcs10Plugin, this.hcs2Plugin, this.inscribePlugin, this.hbarPlugin ]; const corePlugins = getAllHederaCorePlugins(); if (enabledPlugins) { const enabledSet = new Set(enabledPlugins); const filteredPlugins = [...standardPlugins, ...corePlugins].filter( (plugin) => enabledSet.has(plugin.id) ); return [...filteredPlugins, ...additionalPlugins]; } return [...standardPlugins, ...corePlugins, ...additionalPlugins]; } /** * Creates the agent configuration object. * * @param serverSigner - The server signer instance * @param llm - The language model instance * @param allPlugins - Array of plugins to use * @returns Configuration object for creating the agent */ createAgentConfig(serverSigner, llm, allPlugins) { const { operationalMode = DEFAULT_OPERATIONAL_MODE, userAccountId, scheduleUserTransactionsInBytesMode, customSystemMessagePreamble, customSystemMessagePostamble, verbose = false, mirrorNodeConfig, disableLogging, accountId = "" } = this.options; return { framework: "langchain", signer: serverSigner, 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 }); } /** * Detect the private key type by querying the account info from mirror node * @param {string} accountId - The Hedera account ID * @param {string} privateKey - The private key string * @param {NetworkType} network - The Hedera Hashgraph * @returns {Promise<PrivateKey>} The appropriate PrivateKey instance */ async detectPrivateKeyType(accountId, privateKey, network) { const mirrorNode = new HederaMirrorNode(network); const accountInfo = await mirrorNode.requestAccount(accountId); const keyType = accountInfo?.key?._type || ""; if (keyType?.toLowerCase()?.includes("ecdsa")) { return PrivateKey.fromStringECDSA(privateKey); } else { return PrivateKey.fromStringED25519(privateKey); } } /** * Resolve entity references using LLM-based resolver * @param content - Message content to resolve * @returns Resolved message content with entity IDs replaced */ async resolveEntitiesInMessage(content) { if (!this.memoryManager || !this.entityTools) { return content; } try { const entities = this.memoryManager.getEntityAssociations(); if (entities.length === 0) { this.logger.info("No entities in memory, skipping resolution"); return content; } this.logger.info( `Starting LLM-based entity resolution for: "${content.substring( 0, 100 )}..."` ); const resolvedContent = await this.entityTools.resolveEntities.call({ message: content, entities: entities.map((e) => ({ entityId: e.entityId, entityName: e.entityName, entityType: e.entityType })) }); if (resolvedContent !== content) { this.logger.info( `Entity resolution completed. Original: "${content}" -> Resolved: "${resolvedContent}"` ); } return resolvedContent; } catch (error) { this.logger.error("Entity resolution failed:", error); throw error; } } /** * 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) { this.logger.info( `Storing entity: ${entity.name} (${entity.type}) -> ${entity.id}` ); const transactionId = this.extractTransactionId(response); this.memoryManager.storeEntityAssociation( entity.id, entity.name, 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) { return response.transactionId; } 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); } } extractResponseText(response) { if (typeof response === "string") { return response; } if (response && typeof response === "object" && "output" in response) { return String(response.output); } return JSON.stringify(response); } } export { ConversationalAgent }; //# sourceMappingURL=index6.js.map