@hashgraphonline/conversational-agent
Version:
Hashgraph Online conversational AI agent implementing HCS-10 communication, HCS-2 registries, and content inscription on Hedera. https://hol.org
513 lines (512 loc) • 17.7 kB
JavaScript
import { SystemMessage } from "@langchain/core/messages";
import { Logger } from "@hashgraphonline/standards-sdk";
import { MemoryWindow } from "./index21.js";
import { ContentStorage } from "./index22.js";
import { TokenCounter } from "./index20.js";
const IS_ENTITY_ASSOCIATION_FLAG = '"isEntityAssociation":true';
const _SmartMemoryManager = class _SmartMemoryManager {
constructor(config = {}) {
this.config = { ..._SmartMemoryManager.DEFAULT_CONFIG, ...config };
this.logger = new Logger({ module: "SmartMemoryManager" });
this.tokenCounter = new TokenCounter(this.config.modelName);
this._contentStorage = new ContentStorage(this.config.storageLimit);
this.memoryWindow = new MemoryWindow(
this.config.maxTokens,
this.config.reserveTokens,
this.tokenCounter
);
}
/**
* Get the content storage instance for file/content reference operations
* @returns ContentStorage instance
*/
get contentStorage() {
return this._contentStorage;
}
/**
* Add a message to the active memory window
* Automatically handles pruning and storage of displaced messages
* @param message - Message to add
*/
addMessage(message) {
const result = this.memoryWindow.addMessage(message);
if (result.prunedMessages.length > 0) {
this._contentStorage.storeMessages(result.prunedMessages);
}
}
/**
* Get all active messages from the memory window
* @returns Array of active messages in chronological order
*/
getMessages() {
return this.memoryWindow.getMessages();
}
/**
* Clear active memory window
* @param clearStorage - Whether to also clear the content storage (default: false)
*/
clear(clearStorage = false) {
this.memoryWindow.clear();
if (clearStorage) {
this._contentStorage.clear();
}
}
/**
* Set the system prompt for the memory window
* @param systemPrompt - System prompt text
*/
setSystemPrompt(systemPrompt) {
this.memoryWindow.setSystemPrompt(systemPrompt);
}
/**
* Get the current system prompt
* @returns Current system prompt text
*/
getSystemPrompt() {
return this.memoryWindow.getSystemPrompt();
}
/**
* Search through stored message history
* @param query - Search term or pattern
* @param options - Search configuration
* @returns Array of matching messages from history
*/
searchHistory(query, options = {}) {
return this._contentStorage.searchMessages(query, options);
}
/**
* Get recent messages from storage history
* @param count - Number of recent messages to retrieve
* @returns Array of recent messages from storage
*/
getRecentHistory(count) {
return this._contentStorage.getRecentMessages(count);
}
/**
* Check if a message can be added without exceeding limits
* @param message - Message to test
* @returns True if message can be added
*/
canAddMessage(message) {
return this.memoryWindow.canAddMessage(message);
}
/**
* Get statistics about the active memory window
* @returns Memory usage statistics
*/
getMemoryStats() {
const windowStats = this.memoryWindow.getStats();
return {
totalActiveMessages: windowStats.totalMessages,
currentTokenCount: windowStats.currentTokens,
maxTokens: windowStats.maxTokens,
remainingCapacity: windowStats.remainingCapacity,
systemPromptTokens: windowStats.systemPromptTokens,
usagePercentage: windowStats.usagePercentage
};
}
/**
* Get statistics about the content storage
* @returns Storage usage statistics
*/
getStorageStats() {
return this._contentStorage.getStorageStats();
}
/**
* Get combined statistics for both active memory and storage
* @returns Combined memory and storage statistics
*/
getOverallStats() {
const memoryStats = this.getMemoryStats();
const storageStats = this.getStorageStats();
return {
activeMemory: memoryStats,
storage: storageStats,
totalMessagesManaged: memoryStats.totalActiveMessages + storageStats.totalMessages,
activeMemoryUtilization: memoryStats.usagePercentage,
storageUtilization: storageStats.usagePercentage
};
}
/**
* Update the configuration and apply changes
* @param newConfig - New configuration options
*/
updateConfig(newConfig) {
this.config = { ...this.config, ...newConfig };
if (newConfig.maxTokens !== void 0 || newConfig.reserveTokens !== void 0) {
this.memoryWindow.updateLimits(
this.config.maxTokens,
this.config.reserveTokens
);
}
if (newConfig.storageLimit !== void 0) {
this._contentStorage.updateStorageLimit(this.config.storageLimit);
}
}
/**
* Get current configuration
* @returns Current configuration settings
*/
getConfig() {
return { ...this.config };
}
/**
* Get messages from storage within a time range
* @param startTime - Start of time range
* @param endTime - End of time range
* @returns Messages within the specified time range
*/
getHistoryFromTimeRange(startTime, endTime) {
return this._contentStorage.getMessagesFromTimeRange(startTime, endTime);
}
/**
* Get messages from storage by message type
* @param messageType - Type of messages to retrieve ('human', 'ai', 'system', etc.)
* @param limit - Maximum number of messages to return
* @returns Messages of the specified type
*/
getHistoryByType(messageType, limit) {
return this._contentStorage.getMessagesByType(messageType, limit);
}
/**
* Get recent messages from storage within the last N minutes
* @param minutes - Number of minutes to look back
* @returns Messages from the last N minutes
*/
getRecentHistoryByTime(minutes) {
return this._contentStorage.getRecentMessagesByTime(minutes);
}
/**
* Export the current state for persistence or analysis
* @returns Serializable representation of memory state
*/
exportState() {
return {
config: this.config,
activeMessages: this.memoryWindow.getMessages().map((msg) => ({
content: msg.content,
type: msg._getType()
})),
systemPrompt: this.memoryWindow.getSystemPrompt(),
memoryStats: this.getMemoryStats(),
storageStats: this.getStorageStats(),
storedMessages: this._contentStorage.exportMessages()
};
}
/**
* Get a summary of conversation context for external use
* Useful for providing context to other systems or for logging
* @param includeStoredContext - Whether to include recent stored messages
* @returns Context summary object
*/
getContextSummary(includeStoredContext = false) {
const activeMessages = this.getMessages();
const summary = {
activeMessageCount: activeMessages.length,
systemPrompt: this.getSystemPrompt(),
recentMessages: activeMessages.slice(-5),
memoryUtilization: this.getMemoryStats().usagePercentage,
hasStoredHistory: this.getStorageStats().totalMessages > 0
};
if (includeStoredContext) {
return {
...summary,
recentStoredMessages: this.getRecentHistory(10),
storageStats: this.getStorageStats()
};
}
return summary;
}
/**
* Perform maintenance operations
* Optimizes storage and cleans up resources
*/
performMaintenance() {
}
/**
* Store an entity association for later resolution
* @param entityId - The blockchain entity ID
* @param entityName - User-provided or derived friendly name
* @param entityType - Type of entity (token, account, topic, etc.)
* @param transactionId - Optional transaction ID that created this entity
*/
storeEntityAssociation(entityId, entityName, entityType, transactionId, sessionId) {
try {
if (!entityId || typeof entityId !== "string" || entityId.trim().length === 0) {
return;
}
if (!entityName || typeof entityName !== "string" || entityName.trim().length === 0) {
return;
}
if (!entityType || typeof entityType !== "string" || entityType.trim().length === 0) {
return;
}
const sanitizedEntityId = entityId.trim();
const sanitizedEntityName = entityName.trim().substring(0, 100);
const sanitizedEntityType = this.normalizeEntityType(entityType);
let usageHint = "";
if (sanitizedEntityType === "tokenid") {
usageHint = "Use this as tokenId for HTS operations";
} else if (sanitizedEntityType === "topicid") {
usageHint = "Can be used for HCS operations, HRLs for minting with the format hcs://1/<topicId>, etc.";
} else if (sanitizedEntityType === "accountid") {
usageHint = "Can be used for account based operations";
}
const association = {
entityId: sanitizedEntityId,
entityName: sanitizedEntityName,
entityType: sanitizedEntityType,
createdAt: /* @__PURE__ */ new Date(),
isEntityAssociation: true,
...usageHint ? { usage: usageHint } : {},
...sanitizedEntityType === "topicId" ? { hrl: `hcs://1/${sanitizedEntityId}` } : {},
...transactionId !== void 0 && transactionId !== null && transactionId.trim() !== "" ? { transactionId: transactionId.trim() } : {},
...sessionId && sessionId.trim() !== "" ? { sessionId: sessionId.trim() } : {}
};
const content = JSON.stringify(association);
const entityMessage = {
_getType: () => "system",
content,
id: `entity_${sanitizedEntityId}_${Date.now()}`,
name: "entity_association",
additional_kwargs: {
entityId: sanitizedEntityId,
entityName: sanitizedEntityName,
entityType: sanitizedEntityType,
isEntityAssociation: true,
...sessionId && sessionId.trim() !== "" ? { sessionId: sessionId.trim() } : {}
}
};
try {
this.memoryWindow.addMessage(new SystemMessage(content));
} catch {
}
this._contentStorage.storeMessages([entityMessage]);
} catch (error) {
this.logger.error("Failed to store entity association", {
entityId,
entityName,
entityType,
error: error instanceof Error ? error.message : String(error)
});
}
}
/**
* Normalize various type aliases to canonical EntityFormat strings using a registry.
*/
normalizeEntityType(input) {
const raw = (input || "").trim();
if (raw.length === 0) {
return "";
}
const key = raw.replace(/[^a-z]/gi, "").toLowerCase();
const REGISTRY = {
topic: "topicId",
topicid: "topicId",
token: "tokenId",
tokenid: "tokenId",
account: "accountId",
accountid: "accountId",
contract: "contractId",
contractid: "contractId",
file: "fileId",
fileid: "fileId",
schedule: "scheduleId",
scheduleid: "scheduleId"
};
if (Object.prototype.hasOwnProperty.call(REGISTRY, key)) {
return REGISTRY[key];
}
if (/^[a-z]+Id$/.test(raw)) {
return raw;
}
return raw;
}
/**
* Resolve entity references from natural language queries
* @param query - Search query (entity name or natural language reference)
* @param options - Resolution options for filtering and fuzzy matching
* @returns Array of matching entity associations
*/
resolveEntityReference(query, options = {}) {
try {
if (!query || typeof query !== "string") {
return [];
}
const sanitizedQuery = query.trim();
if (sanitizedQuery.length === 0) {
return [];
}
if (sanitizedQuery.length > 200) {
}
const { entityType, limit = 10, fuzzyMatch = true } = options;
const safeLimit = Math.max(1, Math.min(limit || 10, 100));
const isEntityIdQuery = /^0\.0\.\d+$/.test(sanitizedQuery);
const searchResults = this._contentStorage.searchMessages(
sanitizedQuery.substring(0, 200),
{
caseSensitive: false,
limit: safeLimit * 2
}
);
const associations = [];
for (const message of searchResults) {
try {
const content = message.content;
if (content.includes(IS_ENTITY_ASSOCIATION_FLAG) || content.includes("entityId")) {
const parsed = JSON.parse(content);
if (parsed.entityId && parsed.entityName && parsed.entityType) {
if (entityType && parsed.entityType !== entityType) {
continue;
}
if (isEntityIdQuery) {
if (parsed.entityId !== sanitizedQuery) {
continue;
}
}
associations.push(parsed);
}
}
} catch {
continue;
}
}
if (fuzzyMatch && associations.length === 0 && !isEntityIdQuery) {
const fuzzyQueries = [
query.toLowerCase(),
`token`,
`account`,
entityType || ""
].filter(Boolean);
for (const fuzzyQuery of fuzzyQueries) {
if (fuzzyQuery === query.toLowerCase()) continue;
const fuzzyResults = this._contentStorage.searchMessages(fuzzyQuery, {
caseSensitive: false,
limit
});
for (const message of fuzzyResults) {
try {
const content = message.content;
if (content.includes(IS_ENTITY_ASSOCIATION_FLAG)) {
const parsed = JSON.parse(content);
if (parsed.entityId && parsed.entityName && parsed.entityType) {
if (entityType && parsed.entityType !== entityType) {
continue;
}
associations.push(parsed);
}
}
} catch {
continue;
}
}
}
}
const uniqueAssociations = associations.filter(
(assoc, index, arr) => arr.findIndex((a) => a.entityId === assoc.entityId) === index
).sort(
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
);
const results = uniqueAssociations.slice(0, safeLimit);
return results;
} catch (error) {
this.logger.error("Failed to resolve entity reference", {
query,
options,
error: error instanceof Error ? error.message : String(error)
});
return [];
}
}
/**
* Get all entity associations, optionally filtered by type
* @param entityType - Optional filter by entity type
* @returns Array of entity associations
*/
getEntityAssociations(entityType) {
try {
const rawFilter = entityType ? entityType.trim() : void 0;
const filterCanonical = rawFilter ? this.normalizeEntityType(rawFilter) : void 0;
if (entityType && (!rawFilter || rawFilter.length === 0)) {
return [];
}
const SEARCH_ANY_ENTITY = "entityId";
const searchQuery = filterCanonical || SEARCH_ANY_ENTITY;
const searchResults = this._contentStorage.searchMessages(searchQuery, {
caseSensitive: false,
limit: 100
});
const associations = [];
for (const message of searchResults) {
try {
const content = message.content;
if (content.includes(IS_ENTITY_ASSOCIATION_FLAG)) {
const parsed = JSON.parse(content);
if (parsed.entityId && parsed.entityName && parsed.entityType) {
if (filterCanonical && parsed.entityType !== filterCanonical) {
continue;
}
if (parsed.createdAt && typeof parsed.createdAt === "string") {
parsed.createdAt = new Date(parsed.createdAt);
}
associations.push(parsed);
}
}
} catch (parseError) {
this.logger.warn("Failed to parse entity association from message", {
messageContent: typeof message.content === "string" ? message.content.substring(0, 100) : "non-string",
error: parseError instanceof Error ? parseError.message : String(parseError)
});
continue;
}
}
const mergedById = /* @__PURE__ */ new Map();
const getTime = (d) => d instanceof Date ? d.getTime() : new Date(d).getTime();
for (const assoc of associations) {
const existing = mergedById.get(assoc.entityId);
if (!existing) {
mergedById.set(assoc.entityId, assoc);
continue;
}
const existingTime = getTime(existing.createdAt);
const currentTime = getTime(assoc.createdAt);
const preferCurrent = currentTime > existingTime || !!assoc.transactionId && !existing.transactionId;
if (preferCurrent) {
mergedById.set(assoc.entityId, {
...existing,
...assoc
});
}
}
const results = Array.from(mergedById.values()).sort(
(a, b) => getTime(b.createdAt) - getTime(a.createdAt)
);
return results;
} catch (error) {
this.logger.error("Failed to get entity associations", {
entityType,
error: error instanceof Error ? error.message : String(error)
});
return [];
}
}
/**
* Clean up resources and dispose of components
*/
dispose() {
this.memoryWindow.dispose();
this._contentStorage.dispose();
this.tokenCounter.dispose();
}
};
_SmartMemoryManager.DEFAULT_CONFIG = {
maxTokens: 8e3,
reserveTokens: 1e3,
modelName: "gpt-4o",
storageLimit: 1e3
};
let SmartMemoryManager = _SmartMemoryManager;
export {
SmartMemoryManager
};
//# sourceMappingURL=index19.js.map