@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
JavaScript
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