UNPKG

@hashgraphonline/conversational-agent

Version:

Hashgraph Online conversational AI agent implementing HCS-10 communication, HCS-2 registries, and content inscription on Hedera

350 lines (299 loc) • 10.5 kB
import type { ContentReference, ReferenceId } from '../types/content-reference'; import type { ContentStorage } from '../memory/ContentStorage'; import { Logger } from '@hashgraphonline/standards-sdk'; export interface ReferenceContext { reference: ContentReference; displayedAt: Date; lastAccessedAt: Date; contextId: string; conversationTurn: number; } export interface ReferenceDisplayOptions { maxPreviewLength?: number; showMetadata?: boolean; showSize?: boolean; includeActions?: boolean; format?: 'inline' | 'card' | 'compact'; } export interface DisplayResult { displayText: string; hasValidReference: boolean; contextId?: string; suggestedActions?: string[]; } /** * Manages content references within agent conversation context * Tracks reference usage, provides display formatting, and handles reference validation */ export class ReferenceContextManager { private activeReferences: Map<ReferenceId, ReferenceContext> = new Map(); private contentStorage: ContentStorage; private logger: Logger; private conversationTurn = 0; constructor(contentStorage: ContentStorage, logger: Logger) { this.contentStorage = contentStorage; this.logger = logger; } /** * Add a reference to the current conversation context */ addReference(reference: ContentReference): string { this.conversationTurn++; const contextId = `ctx_${Date.now()}_${reference.referenceId.substring(0, 8)}`; const context: ReferenceContext = { reference, displayedAt: new Date(), lastAccessedAt: new Date(), contextId, conversationTurn: this.conversationTurn }; this.activeReferences.set(reference.referenceId, context); this.logger.debug(`Added reference to conversation context: ${reference.referenceId} (${contextId})`); return contextId; } /** * Generate display text for a content reference */ async displayReference( reference: ContentReference, options: ReferenceDisplayOptions = {} ): Promise<DisplayResult> { const { maxPreviewLength = 150, showMetadata = true, showSize = true, includeActions = true, format = 'card' } = options; try { const isValid = await this.contentStorage.hasReference(reference.referenceId); if (!isValid) { return { displayText: this.formatInvalidReference(reference, includeActions), hasValidReference: false, suggestedActions: ['Request fresh content', 'Use alternative content source'] }; } const contextId = this.addReference(reference); let displayText = ''; switch (format) { case 'inline': displayText = this.formatInlineReference(reference, maxPreviewLength); break; case 'compact': displayText = this.formatCompactReference(reference, showSize); break; case 'card': default: displayText = this.formatCardReference(reference, { maxPreviewLength, showMetadata, showSize, includeActions, contextId }); break; } return { displayText, hasValidReference: true, contextId }; } catch (error) { this.logger.error('Error displaying reference:', error); return { displayText: this.formatErrorReference(reference, error), hasValidReference: false, suggestedActions: ['Check reference validity', 'Try again', 'Contact administrator'] }; } } /** * Get the most recent reference for "inscribe it" commands */ getMostRecentReference(): ContentReference | null { if (this.activeReferences.size === 0) { return null; } let mostRecent: ReferenceContext | null = null; for (const context of this.activeReferences.values()) { if (!mostRecent || context.displayedAt > mostRecent.displayedAt) { mostRecent = context; } } if (mostRecent) { mostRecent.lastAccessedAt = new Date(); return mostRecent.reference; } return null; } /** * Get reference by context ID */ getReferenceByContextId(contextId: string): ContentReference | null { for (const context of this.activeReferences.values()) { if (context.contextId === contextId) { context.lastAccessedAt = new Date(); return context.reference; } } return null; } /** * Validate all active references and remove invalid ones */ async validateReferences(): Promise<{ valid: number; invalid: number; removed: ReferenceId[] }> { const removed: ReferenceId[] = []; let valid = 0; let invalid = 0; for (const [referenceId, _context] of this.activeReferences.entries()) { try { const isValid = await this.contentStorage.hasReference(referenceId); if (isValid) { valid++; } else { invalid++; removed.push(referenceId); this.activeReferences.delete(referenceId); this.logger.debug(`Removed invalid reference from context: ${referenceId}`); } } catch (error) { invalid++; removed.push(referenceId); this.activeReferences.delete(referenceId); this.logger.warn(`Error validating reference ${referenceId}:`, error); } } return { valid, invalid, removed }; } /** * Clear old references based on age and usage */ cleanupOldReferences(maxAgeMs: number = 30 * 60 * 1000): number { const cutoffTime = new Date(Date.now() - maxAgeMs); const toRemove: ReferenceId[] = []; for (const [referenceId, context] of this.activeReferences.entries()) { if (context.lastAccessedAt < cutoffTime) { toRemove.push(referenceId); } } toRemove.forEach(referenceId => { this.activeReferences.delete(referenceId); }); if (toRemove.length > 0) { this.logger.debug(`Cleaned up ${toRemove.length} old references from context`); } return toRemove.length; } /** * Get current context statistics */ getContextStats(): { activeReferences: number; conversationTurn: number; oldestReference: number | null; mostRecentReference: number | null; } { return { activeReferences: this.activeReferences.size, conversationTurn: this.conversationTurn, oldestReference: this.getOldestReferenceAge(), mostRecentReference: this.getMostRecentReferenceAge() }; } /** * Clear all references from context */ clear(): void { this.activeReferences.clear(); this.conversationTurn = 0; this.logger.debug('Cleared all references from context'); } private formatCardReference( reference: ContentReference, options: { maxPreviewLength: number; showMetadata: boolean; showSize: boolean; includeActions: boolean; contextId: string; } ): string { const { maxPreviewLength, showMetadata, showSize, includeActions, contextId } = options; let display = `šŸ“„ **Large Content Reference**\n`; display += `**Preview:** ${this.truncateText(reference.preview, maxPreviewLength)}\n`; if (showSize) { const sizeKB = Math.round(reference.metadata.sizeBytes / 1024); display += `**Size:** ${sizeKB}KB`; if (reference.metadata.contentType !== 'binary') { display += ` (${reference.metadata.contentType})`; } display += '\n'; } if (showMetadata) { if (reference.metadata.fileName) { display += `**File:** ${reference.metadata.fileName}\n`; } if (reference.metadata.source) { display += `**Source:** ${reference.metadata.source}\n`; } } if (includeActions) { display += `\nšŸ’” You can say "inscribe it" to inscribe this content to the Hedera Hashgraph.`; } display += `\n\n*Reference ID: ${reference.referenceId.substring(0, 12)}...*`; display += `\n*Context: ${contextId}*`; return display; } private formatInlineReference(reference: ContentReference, maxPreviewLength: number): string { const sizeKB = Math.round(reference.metadata.sizeBytes / 1024); return `šŸ“„ [${sizeKB}KB ${reference.metadata.contentType}] ${this.truncateText(reference.preview, maxPreviewLength)}`; } private formatCompactReference(reference: ContentReference, showSize: boolean): string { const sizeKB = Math.round(reference.metadata.sizeBytes / 1024); const sizeText = showSize ? ` (${sizeKB}KB)` : ''; return `šŸ“„ Referenced content${sizeText}: ${reference.metadata.fileName || 'large content'}`; } private formatInvalidReference(reference: ContentReference, includeActions: boolean): string { let display = `āŒ **Content Reference Expired**\n`; display += `The referenced content is no longer available.\n`; display += `**Original:** ${this.truncateText(reference.preview, 100)}\n`; if (includeActions) { display += `\nšŸ’” Please request fresh content from the original source.`; } return display; } private formatErrorReference(reference: ContentReference, error: unknown): string { const errorMsg = error instanceof Error ? error.message : 'Unknown error'; let display = `āš ļø **Reference Error**\n`; display += `Error accessing referenced content: ${errorMsg}\n`; display += `**Reference:** ${this.truncateText(reference.preview, 100)}\n`; return display; } private truncateText(text: string, maxLength: number): string { if (text.length <= maxLength) { return text; } return text.substring(0, maxLength) + '...'; } private getOldestReferenceAge(): number | null { if (this.activeReferences.size === 0) return null; let oldest: Date | null = null; for (const context of this.activeReferences.values()) { if (!oldest || context.displayedAt < oldest) { oldest = context.displayedAt; } } return oldest ? Date.now() - oldest.getTime() : null; } private getMostRecentReferenceAge(): number | null { if (this.activeReferences.size === 0) return null; let newest: Date | null = null; for (const context of this.activeReferences.values()) { if (!newest || context.displayedAt > newest) { newest = context.displayedAt; } } return newest ? Date.now() - newest.getTime() : null; } }