@hashgraphonline/conversational-agent
Version:
Hashgraph Online conversational AI agent implementing HCS-10 communication, HCS-2 registries, and content inscription on Hedera
651 lines (650 loc) • 22.5 kB
JavaScript
import { ReferenceIdGenerator } from "./index24.js";
import { DEFAULT_CONTENT_REFERENCE_CONFIG, ContentReferenceError } from "./index25.js";
const _ContentStorage = class _ContentStorage {
constructor(maxStorage = _ContentStorage.DEFAULT_MAX_STORAGE, referenceConfig) {
this.messages = [];
this.idCounter = 0;
this.contentStore = /* @__PURE__ */ new Map();
this.maxStorage = maxStorage;
this.referenceConfig = { ...DEFAULT_CONTENT_REFERENCE_CONFIG, ...referenceConfig };
this.referenceStats = {
activeReferences: 0,
totalStorageBytes: 0,
recentlyCleanedUp: 0,
totalResolutions: 0,
failedResolutions: 0,
averageContentSize: 0,
storageUtilization: 0,
performanceMetrics: {
averageCreationTimeMs: 0,
averageResolutionTimeMs: 0,
averageCleanupTimeMs: 0,
creationTimes: [],
resolutionTimes: [],
cleanupTimes: []
}
};
if (this.referenceConfig.enableAutoCleanup) {
this.startReferenceCleanupTimer();
}
}
/**
* Store messages in the content storage
* Automatically drops oldest messages if storage limit is exceeded
* @param messages - Messages to store
* @returns Result indicating how many messages were stored and dropped
*/
storeMessages(messages) {
if (messages.length === 0) {
return { stored: 0, dropped: 0 };
}
const now = /* @__PURE__ */ new Date();
let dropped = 0;
const storedMessages = messages.map((message) => ({
message,
storedAt: now,
id: this.generateId()
}));
this.messages.push(...storedMessages);
while (this.messages.length > this.maxStorage) {
this.messages.shift();
dropped++;
}
return {
stored: storedMessages.length,
dropped
};
}
/**
* Get the most recent messages from storage
* @param count - Number of recent messages to retrieve
* @returns Array of recent messages in chronological order
*/
getRecentMessages(count) {
if (count <= 0 || this.messages.length === 0) {
return [];
}
const startIndex = Math.max(0, this.messages.length - count);
return this.messages.slice(startIndex).map((stored) => stored.message);
}
/**
* Search for messages containing specific text or patterns
* @param query - Search term or regex pattern
* @param options - Search configuration options
* @returns Array of matching messages
*/
searchMessages(query, options = {}) {
if (!query || this.messages.length === 0) {
return [];
}
const {
caseSensitive = false,
limit,
useRegex = false
} = options;
let matches = [];
if (useRegex) {
try {
const regex = new RegExp(query, caseSensitive ? "g" : "gi");
matches = this.messages.filter((stored) => regex.test(stored.message.content)).map((stored) => stored.message);
} catch (error) {
return [];
}
} else {
const searchTerm = caseSensitive ? query : query.toLowerCase();
matches = this.messages.filter((stored) => {
const content = stored.message.content;
const searchContent = caseSensitive ? content : content.toLowerCase();
return searchContent.includes(searchTerm);
}).map((stored) => stored.message);
}
return limit ? matches.slice(0, limit) : matches;
}
/**
* Get messages from a specific time range
* @param startTime - Start of time range (inclusive)
* @param endTime - End of time range (inclusive)
* @returns Array of messages within the time range
*/
getMessagesFromTimeRange(startTime, endTime) {
if (startTime > endTime || this.messages.length === 0) {
return [];
}
return this.messages.filter(
(stored) => stored.storedAt >= startTime && stored.storedAt <= endTime
).map((stored) => stored.message);
}
/**
* Get storage statistics and usage information
* @returns Current storage statistics
*/
getStorageStats() {
const totalMessages = this.messages.length;
const usagePercentage = totalMessages > 0 ? Math.round(totalMessages / this.maxStorage * 100) : 0;
let oldestMessageTime;
let newestMessageTime;
if (totalMessages > 0) {
oldestMessageTime = this.messages[0].storedAt;
newestMessageTime = this.messages[totalMessages - 1].storedAt;
}
return {
totalMessages,
maxStorageLimit: this.maxStorage,
usagePercentage,
oldestMessageTime,
newestMessageTime
};
}
/**
* Clear all stored messages
*/
clear() {
this.messages = [];
this.idCounter = 0;
}
/**
* Get total number of stored messages
* @returns Number of messages currently in storage
*/
getTotalStoredMessages() {
return this.messages.length;
}
/**
* Update the maximum storage limit
* @param newLimit - New maximum storage limit
*/
updateStorageLimit(newLimit) {
if (newLimit <= 0) {
throw new Error("Storage limit must be greater than 0");
}
this.maxStorage = newLimit;
while (this.messages.length > this.maxStorage) {
this.messages.shift();
}
}
/**
* Get messages by message type
* @param messageType - Type of messages to retrieve ('human', 'ai', 'system', etc.)
* @param limit - Maximum number of messages to return
* @returns Array of messages of the specified type
*/
getMessagesByType(messageType, limit) {
const filtered = this.messages.filter((stored) => stored.message._getType() === messageType).map((stored) => stored.message);
return limit ? filtered.slice(0, limit) : filtered;
}
/**
* Get the current storage configuration
* @returns Storage configuration object
*/
getConfig() {
return {
maxStorage: this.maxStorage,
currentUsage: this.messages.length,
utilizationPercentage: this.messages.length / this.maxStorage * 100
};
}
/**
* Generate a unique ID for stored messages
* @returns Unique string identifier
*/
generateId() {
return `msg_${++this.idCounter}_${Date.now()}`;
}
/**
* Get messages stored within the last N minutes
* @param minutes - Number of minutes to look back
* @returns Array of messages from the last N minutes
*/
getRecentMessagesByTime(minutes) {
if (minutes <= 0 || this.messages.length === 0) {
return [];
}
const cutoffTime = new Date(Date.now() - minutes * 60 * 1e3);
return this.messages.filter((stored) => stored.storedAt >= cutoffTime).map((stored) => stored.message);
}
/**
* Export messages to a JSON-serializable format
* @returns Serializable representation of stored messages
*/
exportMessages() {
return this.messages.map((stored) => ({
content: stored.message.content,
type: stored.message._getType(),
storedAt: stored.storedAt.toISOString(),
id: stored.id
}));
}
/**
* Determine if content should be stored as a reference based on size
*/
shouldUseReference(content) {
const size = Buffer.isBuffer(content) ? content.length : Buffer.byteLength(content, "utf8");
return size > this.referenceConfig.sizeThresholdBytes;
}
/**
* Store content and return a reference if it exceeds the size threshold
* Otherwise returns null to indicate direct content should be used
*/
async storeContentIfLarge(content, metadata) {
const buffer = Buffer.isBuffer(content) ? content : Buffer.from(content, "utf8");
if (!this.shouldUseReference(buffer)) {
return null;
}
const storeMetadata = {
contentType: metadata.contentType || this.detectContentType(buffer, metadata.mimeType),
sizeBytes: buffer.length,
source: metadata.source,
tags: []
};
if (metadata.mimeType !== void 0) {
storeMetadata.mimeType = metadata.mimeType;
}
if (metadata.mcpToolName !== void 0) {
storeMetadata.mcpToolName = metadata.mcpToolName;
}
if (metadata.fileName !== void 0) {
storeMetadata.fileName = metadata.fileName;
}
if (metadata.tags !== void 0) {
storeMetadata.tags = metadata.tags;
}
if (metadata.customMetadata !== void 0) {
storeMetadata.customMetadata = metadata.customMetadata;
}
return await this.storeContent(buffer, storeMetadata);
}
/**
* Store content and return a reference (implements ContentReferenceStore)
*/
async storeContent(content, metadata) {
const startTime = Date.now();
try {
const now = /* @__PURE__ */ new Date();
const referenceId = ReferenceIdGenerator.generateId(content);
const fullMetadata = {
...metadata,
createdAt: now,
lastAccessedAt: now,
accessCount: 0
};
const storedContent = {
content,
metadata: fullMetadata,
state: "active"
};
const expirationTime = this.calculateExpirationTime(metadata.source);
if (expirationTime !== void 0) {
storedContent.expiresAt = expirationTime;
}
this.contentStore.set(referenceId, storedContent);
this.updateStatsAfterStore(content.length);
await this.enforceReferenceStorageLimits();
const preview = this.createContentPreview(content, fullMetadata.contentType);
const referenceMetadata = {
contentType: fullMetadata.contentType,
sizeBytes: fullMetadata.sizeBytes,
source: fullMetadata.source
};
if (fullMetadata.fileName !== void 0) {
referenceMetadata.fileName = fullMetadata.fileName;
}
if (fullMetadata.mimeType !== void 0) {
referenceMetadata.mimeType = fullMetadata.mimeType;
}
const reference = {
referenceId,
state: "active",
preview,
metadata: referenceMetadata,
createdAt: now,
format: "ref://{id}"
};
const duration = Date.now() - startTime;
this.recordPerformanceMetric("creation", duration);
return reference;
} catch (error) {
const duration = Date.now() - startTime;
this.recordPerformanceMetric("creation", duration);
throw new ContentReferenceError(
`Failed to store content: ${error instanceof Error ? error.message : "Unknown error"}`,
"system_error",
void 0,
["Try again", "Check storage limits", "Contact administrator"]
);
}
}
/**
* Resolve a reference to its content (implements ContentReferenceStore)
*/
async resolveReference(referenceId) {
const startTime = Date.now();
try {
if (!ReferenceIdGenerator.isValidReferenceId(referenceId)) {
this.referenceStats.failedResolutions++;
return {
success: false,
error: "Invalid reference ID format",
errorType: "not_found",
suggestedActions: ["Check the reference ID format", "Ensure the reference ID is complete"]
};
}
const storedContent = this.contentStore.get(referenceId);
if (!storedContent) {
this.referenceStats.failedResolutions++;
return {
success: false,
error: "Reference not found",
errorType: "not_found",
suggestedActions: ["Verify the reference ID", "Check if the content has expired", "Request fresh content"]
};
}
if (storedContent.expiresAt && storedContent.expiresAt < /* @__PURE__ */ new Date()) {
storedContent.state = "expired";
this.referenceStats.failedResolutions++;
return {
success: false,
error: "Reference has expired",
errorType: "expired",
suggestedActions: ["Request fresh content", "Use alternative content source"]
};
}
if (storedContent.state !== "active") {
this.referenceStats.failedResolutions++;
return {
success: false,
error: `Reference is ${storedContent.state}`,
errorType: storedContent.state === "expired" ? "expired" : "corrupted",
suggestedActions: ["Request fresh content", "Check reference validity"]
};
}
storedContent.metadata.lastAccessedAt = /* @__PURE__ */ new Date();
storedContent.metadata.accessCount++;
this.referenceStats.totalResolutions++;
const duration = Date.now() - startTime;
this.recordPerformanceMetric("resolution", duration);
return {
success: true,
content: storedContent.content,
metadata: storedContent.metadata
};
} catch (error) {
const duration = Date.now() - startTime;
this.recordPerformanceMetric("resolution", duration);
this.referenceStats.failedResolutions++;
return {
success: false,
error: `System error resolving reference: ${error instanceof Error ? error.message : "Unknown error"}`,
errorType: "system_error",
suggestedActions: ["Try again", "Contact administrator"]
};
}
}
/**
* Check if a reference exists and is valid
*/
async hasReference(referenceId) {
if (!ReferenceIdGenerator.isValidReferenceId(referenceId)) {
return false;
}
const storedContent = this.contentStore.get(referenceId);
if (!storedContent) {
return false;
}
if (storedContent.expiresAt && storedContent.expiresAt < /* @__PURE__ */ new Date()) {
storedContent.state = "expired";
return false;
}
return storedContent.state === "active";
}
/**
* Mark a reference for cleanup
*/
async cleanupReference(referenceId) {
const storedContent = this.contentStore.get(referenceId);
if (!storedContent) {
return false;
}
this.referenceStats.totalStorageBytes -= storedContent.content.length;
this.referenceStats.activeReferences--;
this.referenceStats.recentlyCleanedUp++;
this.contentStore.delete(referenceId);
return true;
}
/**
* Get current reference storage statistics (implements ContentReferenceStore)
*/
async getStats() {
this.updateReferenceStorageStats();
return {
...this.referenceStats,
performanceMetrics: {
averageCreationTimeMs: this.calculateAverage(this.referenceStats.performanceMetrics.creationTimes),
averageResolutionTimeMs: this.calculateAverage(this.referenceStats.performanceMetrics.resolutionTimes),
averageCleanupTimeMs: this.calculateAverage(this.referenceStats.performanceMetrics.cleanupTimes)
}
};
}
/**
* Update reference configuration
*/
async updateConfig(config) {
this.referenceConfig = { ...this.referenceConfig, ...config };
if (this.cleanupTimer) {
clearInterval(this.cleanupTimer);
delete this.cleanupTimer;
}
if (this.referenceConfig.enableAutoCleanup) {
this.startReferenceCleanupTimer();
}
}
/**
* Perform cleanup based on current policies (implements ContentReferenceStore)
*/
async performCleanup() {
const startTime = Date.now();
const errors = [];
let cleanedUp = 0;
try {
const now = /* @__PURE__ */ new Date();
const toCleanup = [];
for (const [referenceId, storedContent] of this.contentStore.entries()) {
let shouldCleanup = false;
if (storedContent.expiresAt && storedContent.expiresAt < now) {
shouldCleanup = true;
storedContent.state = "expired";
}
const ageMs = now.getTime() - storedContent.metadata.createdAt.getTime();
const policy = this.getCleanupPolicy(storedContent.metadata.source);
if (ageMs > policy.maxAgeMs) {
shouldCleanup = true;
}
if (storedContent.state === "cleanup_pending") {
shouldCleanup = true;
}
if (shouldCleanup) {
toCleanup.push(referenceId);
}
}
toCleanup.sort((a, b) => {
const aContent = this.contentStore.get(a);
const bContent = this.contentStore.get(b);
const aPriority = this.getCleanupPolicy(aContent.metadata.source).priority;
const bPriority = this.getCleanupPolicy(bContent.metadata.source).priority;
return bPriority - aPriority;
});
for (const referenceId of toCleanup) {
try {
const success = await this.cleanupReference(referenceId);
if (success) {
cleanedUp++;
}
} catch (error) {
errors.push(`Failed to cleanup ${referenceId}: ${error instanceof Error ? error.message : "Unknown error"}`);
}
}
if (this.contentStore.size > this.referenceConfig.maxReferences) {
const sortedByAge = Array.from(this.contentStore.entries()).sort(([, a], [, b]) => a.metadata.lastAccessedAt.getTime() - b.metadata.lastAccessedAt.getTime());
const excessCount = this.contentStore.size - this.referenceConfig.maxReferences;
for (let i = 0; i < excessCount && i < sortedByAge.length; i++) {
const [referenceId] = sortedByAge[i];
try {
const success = await this.cleanupReference(referenceId);
if (success) {
cleanedUp++;
}
} catch (error) {
errors.push(`Failed to cleanup excess reference ${referenceId}: ${error instanceof Error ? error.message : "Unknown error"}`);
}
}
}
const duration = Date.now() - startTime;
this.recordPerformanceMetric("cleanup", duration);
return { cleanedUp, errors };
} catch (error) {
const duration = Date.now() - startTime;
this.recordPerformanceMetric("cleanup", duration);
const errorMessage = `Cleanup process failed: ${error instanceof Error ? error.message : "Unknown error"}`;
errors.push(errorMessage);
return { cleanedUp, errors };
}
}
/**
* Get reference configuration for debugging
*/
getReferenceConfig() {
return { ...this.referenceConfig };
}
async enforceReferenceStorageLimits() {
if (this.contentStore.size >= this.referenceConfig.maxReferences) {
await this.performCleanup();
}
if (this.referenceStats.totalStorageBytes >= this.referenceConfig.maxTotalStorageBytes) {
await this.performCleanup();
}
}
calculateExpirationTime(source) {
const policy = this.getCleanupPolicy(source);
return new Date(Date.now() + policy.maxAgeMs);
}
getCleanupPolicy(source) {
switch (source) {
case "mcp_tool":
return this.referenceConfig.cleanupPolicies.recent;
case "user_upload":
return this.referenceConfig.cleanupPolicies.userContent;
case "agent_generated":
return this.referenceConfig.cleanupPolicies.agentGenerated;
default:
return this.referenceConfig.cleanupPolicies.default;
}
}
detectContentType(content, mimeType) {
if (mimeType) {
if (mimeType === "text/html") return "html";
if (mimeType === "text/markdown") return "markdown";
if (mimeType === "application/json") return "json";
if (mimeType.startsWith("text/")) return "text";
return "binary";
}
const contentStr = content.toString("utf8", 0, Math.min(content.length, 1e3));
if (contentStr.startsWith("{") || contentStr.startsWith("[")) return "json";
if (contentStr.includes("<html>") || contentStr.includes("<!DOCTYPE")) return "html";
if (contentStr.includes("#") && contentStr.includes("\n")) return "markdown";
return "text";
}
createContentPreview(content, contentType) {
const maxLength = 200;
let preview = content.toString("utf8", 0, Math.min(content.length, maxLength * 2));
if (contentType === "html") {
preview = preview.replace(/<[^>]*>/g, "").replace(/\s+/g, " ").trim();
} else if (contentType === "json") {
try {
const parsed = JSON.parse(preview);
preview = JSON.stringify(parsed, null, 0);
} catch {
}
}
preview = preview.trim();
if (preview.length > maxLength) {
preview = preview.substring(0, maxLength) + "...";
}
return preview || "[Binary content]";
}
updateStatsAfterStore(sizeBytes) {
this.referenceStats.activeReferences++;
this.referenceStats.totalStorageBytes += sizeBytes;
this.updateReferenceStorageStats();
}
updateReferenceStorageStats() {
if (this.referenceStats.activeReferences > 0) {
this.referenceStats.averageContentSize = this.referenceStats.totalStorageBytes / this.referenceStats.activeReferences;
}
this.referenceStats.storageUtilization = this.referenceStats.totalStorageBytes / this.referenceConfig.maxTotalStorageBytes * 100;
let mostAccessedId;
let maxAccess = 0;
for (const [referenceId, storedContent] of this.contentStore.entries()) {
if (storedContent.metadata.accessCount > maxAccess) {
maxAccess = storedContent.metadata.accessCount;
mostAccessedId = referenceId;
}
}
if (mostAccessedId !== void 0) {
this.referenceStats.mostAccessedReferenceId = mostAccessedId;
} else {
delete this.referenceStats.mostAccessedReferenceId;
}
}
recordPerformanceMetric(type, timeMs) {
const metrics = this.referenceStats.performanceMetrics;
const maxRecords = 100;
switch (type) {
case "creation":
metrics.creationTimes.push(timeMs);
if (metrics.creationTimes.length > maxRecords) {
metrics.creationTimes.shift();
}
break;
case "resolution":
metrics.resolutionTimes.push(timeMs);
if (metrics.resolutionTimes.length > maxRecords) {
metrics.resolutionTimes.shift();
}
break;
case "cleanup":
metrics.cleanupTimes.push(timeMs);
if (metrics.cleanupTimes.length > maxRecords) {
metrics.cleanupTimes.shift();
}
break;
}
}
calculateAverage(times) {
if (times.length === 0) return 0;
return times.reduce((sum, time) => sum + time, 0) / times.length;
}
startReferenceCleanupTimer() {
this.cleanupTimer = setInterval(async () => {
try {
await this.performCleanup();
} catch (error) {
}
}, this.referenceConfig.cleanupIntervalMs);
}
/**
* Clean up resources (enhanced to include reference cleanup)
*/
async dispose() {
if (this.cleanupTimer) {
clearInterval(this.cleanupTimer);
delete this.cleanupTimer;
}
this.contentStore.clear();
this.clear();
}
};
_ContentStorage.DEFAULT_MAX_STORAGE = 1e3;
let ContentStorage = _ContentStorage;
export {
ContentStorage
};
//# sourceMappingURL=index14.js.map