@hashgraphonline/conversational-agent
Version:
Hashgraph Online conversational AI agent implementing HCS-10 communication, HCS-2 registries, and content inscription on Hedera. https://hol.org
341 lines (327 loc) • 11 kB
JavaScript
import { ChatOpenAI } from "@langchain/openai";
import { Logger } from "@hashgraphonline/standards-sdk";
import { ENTITY_PATTERNS } from "./index43.js";
import "hedera-agent-kit";
import "@hashgraphonline/standards-agent-kit";
import "./index14.js";
import "./index38.js";
import "@langchain/core/tools";
import "zod";
import "@ethersphere/bee-js";
import "./index48.js";
import "./index49.js";
import "./index50.js";
import "./index51.js";
import "./index52.js";
import "./index53.js";
import "./index54.js";
import "./index55.js";
import "./index56.js";
import "./index57.js";
import "./index58.js";
import "./index59.js";
import "./index7.js";
import "langchain/agents";
import "zod-to-json-schema";
import "./index13.js";
import "@langchain/core/prompts";
import "@modelcontextprotocol/sdk/client/index.js";
import "@modelcontextprotocol/sdk/client/stdio.js";
import "./index19.js";
import "@langchain/core/messages";
import "./index20.js";
import "./index21.js";
import "./index22.js";
import "crypto";
import { EntityFormat } from "./index27.js";
import "./index29.js";
import "./index35.js";
class EntityResolver {
constructor(config) {
this.llm = new ChatOpenAI({
apiKey: config.apiKey,
modelName: config.modelName || "gpt-4o-mini",
temperature: 0
});
this.logger = new Logger({ module: "EntityResolver" });
}
/**
* Resolve entity references using LLM instead of regex
*/
async resolveReferences(message, entities) {
if (!entities || entities.length === 0) {
return message;
}
const byType = entities.reduce((acc, e) => {
if (!acc[e.entityType]) acc[e.entityType] = [];
acc[e.entityType].push(e);
return acc;
}, {});
try {
const stats = Object.fromEntries(
Object.entries(byType).map(([type, list]) => [
type,
{
count: list.length,
mostRecent: list[0]?.entityId
}
])
);
this.logger.info("resolveReferences: input summary", {
messagePreview: message.substring(0, 200),
entityStats: stats
});
} catch {
}
let context = "Available entities in memory:\n";
for (const [type, list] of Object.entries(byType)) {
const recent = list[0];
context += `Most recent ${type}: "${recent.entityName}" = ${recent.entityId}
`;
if (list.length > 1) {
context += ` (${list.length - 1} other ${type}s in memory)
`;
}
}
const prompt = `Task: Replace entity references with their IDs from memory. STRICT TYPE RULES:
- For phrases referring to "${ENTITY_PATTERNS.TOKEN_REFERENCE}" or actions that clearly require a token (create/mint/airdrop/associate/etc.), resolve to the most recent TOKEN entity ID (never a topic or account).
- For phrases referring to "${ENTITY_PATTERNS.TOPIC_REFERENCE}" or actions that clearly require a topic (inscribe/publish/consensus/etc.), resolve to the most recent TOPIC entity ID (never a token or account).
- Do not infer or invent entity IDs. Only use those present in the provided context.
${context}
User message: "${message}"
Instructions:
- If the user says "${ENTITY_PATTERNS.TOPIC_REFERENCE}" or "that topic" → replace with the most recent topic ID
- If the user says "${ENTITY_PATTERNS.TOKEN_REFERENCE}" or "that token" → replace with the most recent token ID (never a topic)
- If the user says "it" or "that" after an action verb → replace with the most recent entity ID
- Examples:
* "submit on ${ENTITY_PATTERNS.TOPIC_REFERENCE}" → "submit on 0.0.6543472"
* "airdrop ${ENTITY_PATTERNS.TOKEN_REFERENCE}" → "airdrop 0.0.123456"
* "send a message to it" → "send a message to 0.0.6543472"
Return ONLY the message with replacements made. Do not add any explanations.
Resolved message:`;
try {
const response = await this.llm.invoke(prompt);
const resolved = response.content.trim();
const changed = resolved !== message;
try {
this.logger.info("resolveReferences: resolution result", {
changed,
hasEntityId: /\b0\.0\.\d+\b/.test(resolved),
resolvedPreview: resolved.substring(0, 200)
});
} catch {
}
if (changed && resolved.includes("0.0.")) {
return resolved;
}
return message;
} catch {
return message;
}
}
/**
* Extract entities from agent response using receipt data (preferred) or LLM fallback
*/
async extractEntities(response, userMessage) {
const receiptEntities = this.extractFromReceipt(response, userMessage);
if (receiptEntities.length > 0) {
return receiptEntities;
}
return this.extractWithLLM(response, userMessage);
}
/**
* Extract entities from transaction receipt data (primary method)
*/
extractFromReceipt(response, userMessage) {
const entities = [];
let parsedResponse;
try {
parsedResponse = typeof response === "string" ? JSON.parse(response) : response;
} catch {
parsedResponse = response;
}
if (!parsedResponse || parsedResponse.success === false) {
return entities;
}
const receipt = parsedResponse.receipt || parsedResponse.result?.receipt || parsedResponse.data?.receipt;
if (!receipt) {
return entities;
}
const entityName = this.extractNameFromMessage(userMessage);
const transactionId = parsedResponse.transactionId || parsedResponse.result?.transactionId || void 0;
const extractEntityId = (entityId) => {
if (typeof entityId === "string") {
return entityId;
}
if (entityId && typeof entityId === "object" && typeof entityId.toString === "function") {
if (entityId.toString !== Object.prototype.toString) {
return entityId.toString();
}
}
return String(entityId);
};
if (receipt.tokenId) {
const entity = {
id: extractEntityId(receipt.tokenId),
name: entityName,
type: EntityFormat.TOKEN_ID
};
if (transactionId) {
entity.transactionId = transactionId;
}
entities.push(entity);
}
if (receipt.topicId) {
const entity = {
id: extractEntityId(receipt.topicId),
name: entityName,
type: EntityFormat.TOPIC_ID
};
if (transactionId) {
entity.transactionId = transactionId;
}
entities.push(entity);
}
if (receipt.accountId) {
const entity = {
id: extractEntityId(receipt.accountId),
name: entityName,
type: EntityFormat.ACCOUNT_ID
};
if (transactionId) {
entity.transactionId = transactionId;
}
entities.push(entity);
}
if (receipt.contractId) {
const entity = {
id: extractEntityId(receipt.contractId),
name: entityName,
type: EntityFormat.CONTRACT_ID
};
if (transactionId) {
entity.transactionId = transactionId;
}
entities.push(entity);
}
if (receipt.fileId) {
const entity = {
id: extractEntityId(receipt.fileId),
name: entityName,
type: EntityFormat.FILE_ID
};
if (transactionId) {
entity.transactionId = transactionId;
}
entities.push(entity);
}
if (receipt.scheduleId) {
const entity = {
id: extractEntityId(receipt.scheduleId),
name: entityName,
type: EntityFormat.SCHEDULE_ID
};
if (transactionId) {
entity.transactionId = transactionId;
}
entities.push(entity);
}
return entities;
}
/**
* Extract entity name from user message
*/
extractNameFromMessage(message) {
const quotedMatch = message.match(/"([^"]+)"/);
if (quotedMatch) return quotedMatch[1];
const calledMatch = message.match(/called\s+([A-Za-z0-9#\s_-]+?)(?:\s|$)/i);
if (calledMatch) return calledMatch[1].trim();
const forMatch = message.match(/for\s+([A-Za-z0-9#\s_-]+)/i);
if (forMatch) return forMatch[1].trim();
const namedMatch = message.match(
/(?:token|topic|account|contract)\s+([A-Za-z0-9#_-]+)/i
);
if (namedMatch) return namedMatch[1].trim();
if (message.includes("new account")) return "new account";
return "unnamed_entity";
}
/**
* Extract entities using LLM (fallback method)
*/
async extractWithLLM(response, userMessage) {
const text = typeof response === "string" ? response : JSON.stringify(response);
const prompt = `Analyze this agent response and extract ONLY newly created entities.
User asked: "${userMessage.substring(0, 200)}"
Agent response: ${text.substring(0, 3e3)}
CRITICAL: Only extract Hedera entity IDs in the format 0.0.XXXXX (shard.realm.number).
DO NOT extract:
- Token symbols (e.g., "FOREV", "USDC", "HBAR")
- Token names (e.g., "Forever", "My Token")
- Transaction IDs (format: 0.0.XXX@timestamp)
- Account aliases or mnemonics
Look for:
1. Success messages with entity IDs (e.g., "Successfully created topic 0.0.6543472")
2. Transaction confirmations that created new entities
3. Entity IDs that appear after words like "created", "new", "successfully"
DO NOT include:
- Token symbols or names (these are NOT entity IDs)
- Account IDs that already existed (like sender/receiver accounts)
- Entity IDs that were parameters to the operation
- Failed operations
- Anything that doesn't match the 0.0.XXXXX format
Return a JSON array of newly created entities:
[{"id": "0.0.XXX", "name": "descriptive_name", "type": "topic|token|account"}]
If no entities were created, return: []
JSON:`;
try {
const response2 = await this.llm.invoke(prompt);
const content = response2.content;
const match = content.match(/\[[\s\S]*?\]/);
if (match) {
const entities = JSON.parse(match[0]);
return entities;
}
} catch {
}
return [];
}
/**
* Validate that an entity matches the expected type
*/
validateEntityType(entityId, expectedType, entities) {
const stored = entities.find((e) => e.entityId === entityId);
return !!stored && stored.entityType === expectedType;
}
/**
* Resolve entity references with type validation
*/
async resolveWithTypeValidation(query, entities, expectedType) {
await this.resolveReferences(query, entities);
if (!expectedType) {
return entities;
}
return entities.filter((entity) => entity.entityType === expectedType);
}
/**
* Get entities filtered by type
*/
getEntitiesByType(entities, entityType) {
return entities.filter((entity) => entity.entityType === entityType);
}
/**
* Find the most recent entity of a specific type
*/
getMostRecentEntityByType(entities, entityType) {
const filtered = entities.filter(
(entity) => entity.entityType === entityType
);
if (filtered.length === 0) return null;
return filtered.reduce(
(most, current) => current.createdAt > most.createdAt ? current : most
);
}
}
export {
EntityResolver
};
//# sourceMappingURL=index24.js.map