@hashgraphonline/conversational-agent
Version:
Hashgraph Online conversational AI agent implementing HCS-10 communication, HCS-2 registries, and content inscription on Hedera
427 lines (426 loc) • 15.4 kB
JavaScript
import { createOpenAIToolsAgent } from "langchain/agents";
import { ContentAwareAgentExecutor } from "./index21.js";
import { ChatPromptTemplate, MessagesPlaceholder } from "@langchain/core/prompts";
import { ChatOpenAI } from "@langchain/openai";
import { TokenUsageCallbackHandler, calculateTokenCostSync, getAllHederaCorePlugins, HederaAgentKit } from "hedera-agent-kit";
import { BaseAgent } from "./index7.js";
import { MCPClientManager } from "./index22.js";
import { convertMCPToolToLangChain } from "./index23.js";
import { SmartMemoryManager } from "./index15.js";
class LangChainAgent extends BaseAgent {
constructor() {
super(...arguments);
this.systemMessage = "";
this.mcpConnectionStatus = /* @__PURE__ */ new Map();
}
async boot() {
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";
this.tokenTracker = new TokenUsageCallbackHandler(modelName);
const allTools = this.agentKit.getAggregatedLangChainTools();
this.tools = this.filterTools(allTools);
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.systemMessage = this.buildSystemPrompt();
this.smartMemory.setSystemPrompt(this.systemMessage);
await this.createExecutor();
this.initialized = true;
this.logger.info("LangChain Hedera agent initialized");
} 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 {
this.logger.info("LangChainAgent.chat called with:", {
message,
contextLength: context?.messages?.length || 0
});
if (context?.messages && context.messages.length > 0) {
this.smartMemory.clear();
for (const msg of context.messages) {
this.smartMemory.addMessage(msg);
}
}
const { HumanMessage } = await import("@langchain/core/messages");
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 result = await this.executor.invoke({
input: message,
chat_history: this.smartMemory.getMessages()
});
this.logger.info("LangChainAgent executor result:", result);
let response = {
output: result.output || "",
message: result.output || "",
notes: [],
intermediateSteps: result.intermediateSteps
};
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;
}
}
const parsedSteps = result?.intermediateSteps?.[0]?.observation;
if (parsedSteps && typeof parsedSteps === "string" && this.isJSON(parsedSteps)) {
try {
const parsed = JSON.parse(parsedSteps);
response = { ...response, ...parsed };
} catch (error) {
this.logger.error("Error parsing intermediate steps:", error);
}
}
if (!response.output || response.output.trim() === "") {
response.output = "Agent action complete.";
}
if (response.output) {
const { AIMessage } = await import("@langchain/core/messages");
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;
} 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;
}
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);
}
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() {
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 agent = await createOpenAIToolsAgent({
llm,
tools: langchainTools,
prompt
});
this.executor = new ContentAwareAgentExecutor({
agent,
tools: langchainTools,
verbose: this.config.debug?.verbose ?? false,
returnIntermediateSteps: true
});
}
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 = "Too many requests. Please wait a moment and try again.";
userFriendlyOutput = "I'm receiving too many requests right now. Please wait a moment and try again.";
}
} 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.tools.push(langchainTool);
}
} 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.tools.push(langchainTool);
}
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);
}
/**
* 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=index8.js.map