@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
text/typescript
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;
}
}