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