UNPKG

@hashgraphonline/conversational-agent

Version:

Hashgraph Online conversational AI agent implementing HCS-10 communication, HCS-2 registries, and content inscription on Hedera. https://hol.org

366 lines (319 loc) 9.63 kB
import type { ContentType, ContentSource } from '../types/content-reference'; import type { ContentStorage } from '../memory/content-storage'; import { Logger } from '@hashgraphonline/standards-sdk'; export interface MCPResponseContent { content: unknown; type: 'text' | 'image' | 'resource' | 'text[]' | 'image[]'; sizeBytes: number; mimeType?: string; } export interface ProcessedResponse { content: unknown; wasProcessed: boolean; referenceCreated?: boolean; referenceId?: string; originalSize?: number; errors?: string[]; } /** * Content reference interface */ interface ContentReference { referenceId: string; preview?: string; metadata: { sizeBytes: number; contentType?: string; [key: string]: unknown; }; } export interface ContentAnalysis { shouldProcess: boolean; contents: MCPResponseContent[]; totalSize: number; largestContentSize: number; } export class MCPContentProcessor { private contentStorage: ContentStorage; private logger: Logger; constructor(contentStorage: ContentStorage, logger: Logger) { this.contentStorage = contentStorage; this.logger = logger; } analyzeResponse(response: unknown): ContentAnalysis { const contents: MCPResponseContent[] = []; let totalSize = 0; this.extractContentFromResponse(response, contents); totalSize = contents.reduce((sum, content) => sum + content.sizeBytes, 0); const largestContentSize = contents.reduce( (max, content) => Math.max(max, content.sizeBytes), 0 ); const shouldProcess = contents.some((content) => this.contentStorage.shouldUseReference( typeof content.content === 'string' ? content.content : JSON.stringify(content.content) ) ); return { shouldProcess, contents, totalSize, largestContentSize, }; } async processResponse( response: unknown, serverName: string, toolName: string ): Promise<ProcessedResponse> { try { const analysis = this.analyzeResponse(response); if (!analysis.shouldProcess) { return { content: response, wasProcessed: false, }; } const processedResponse = await this.createReferencedResponse( response, analysis, serverName, toolName ); return processedResponse; } catch (error) { this.logger.error('Error processing MCP response:', error); return { content: response, wasProcessed: false, errors: [ error instanceof Error ? error.message : 'Unknown processing error', ], }; } } private extractContentFromResponse( obj: unknown, contents: MCPResponseContent[] ): void { if (obj === null || obj === undefined) { return; } if (Array.isArray(obj)) { obj.forEach((item) => this.extractContentFromResponse(item, contents)); return; } if (typeof obj === 'object') { const record = obj as Record<string, unknown>; if (record.type === 'text' && typeof record.text === 'string') { contents.push({ content: record.text, type: 'text', sizeBytes: Buffer.byteLength(record.text, 'utf8'), mimeType: this.detectMimeType(record.text as string), }); return; } if (record.type === 'image' && typeof record.data === 'string') { contents.push({ content: record.data, type: 'image', sizeBytes: Math.ceil(record.data.length * 0.75), mimeType: (record.mimeType as string) || 'image/jpeg', }); return; } if (record.type === 'resource' && record.resource) { const resourceStr = JSON.stringify(record.resource); contents.push({ content: resourceStr, type: 'resource', sizeBytes: Buffer.byteLength(resourceStr, 'utf8'), mimeType: 'application/json', }); return; } Object.values(record).forEach((value) => this.extractContentFromResponse(value, contents) ); return; } if (typeof obj === 'string') { if (obj.length > 1000) { contents.push({ content: obj, type: 'text', sizeBytes: Buffer.byteLength(obj, 'utf8'), mimeType: this.detectMimeType(obj), }); } } } private async createReferencedResponse( originalResponse: unknown, analysis: ContentAnalysis, serverName: string, toolName: string ): Promise<ProcessedResponse> { const processedResponse = this.deepClone(originalResponse); const errors: string[] = []; let referenceCreated = false; let totalReferenceSize = 0; for (const contentInfo of analysis.contents) { if ( this.contentStorage.shouldUseReference( typeof contentInfo.content === 'string' ? contentInfo.content : JSON.stringify(contentInfo.content) ) ) { try { const contentBuffer = Buffer.from( typeof contentInfo.content === 'string' ? contentInfo.content : JSON.stringify(contentInfo.content), 'utf8' ); const contentType = this.mapMimeTypeToContentType( contentInfo.mimeType ); const metadata: Parameters< typeof this.contentStorage.storeContentIfLarge >[1] = { contentType, source: 'mcp_tool' as ContentSource, mcpToolName: `${serverName}::${toolName}`, tags: ['mcp_response', serverName, toolName], }; if (contentInfo.mimeType !== undefined) { metadata.mimeType = contentInfo.mimeType; } const reference = await this.contentStorage.storeContentIfLarge( contentBuffer, metadata ); if (reference) { this.replaceContentInResponse( processedResponse, contentInfo.content, this.createLightweightReference(reference) ); referenceCreated = true; totalReferenceSize += contentBuffer.length; } } catch (error) { errors.push( `Failed to create reference: ${ error instanceof Error ? error.message : 'Unknown error' }` ); } } } const result: ProcessedResponse = { content: processedResponse, wasProcessed: true, referenceCreated, originalSize: totalReferenceSize, }; if (errors.length > 0) { result.errors = errors; } return result; } private createLightweightReference( reference: ContentReference ): Record<string, unknown> { return { type: 'content_reference', referenceId: reference.referenceId, preview: reference.preview, size: reference.metadata.sizeBytes, contentType: reference.metadata.contentType, format: 'ref://{id}', _isReference: true, }; } private replaceContentInResponse( obj: unknown, oldContent: unknown, newContent: unknown ): void { if (obj === null || obj === undefined) { return; } if (Array.isArray(obj)) { for (let i = 0; i < obj.length; i++) { if (obj[i] === oldContent) { obj[i] = newContent; } else { this.replaceContentInResponse(obj[i], oldContent, newContent); } } return; } if (typeof obj === 'object') { const record = obj as Record<string, unknown>; if (record.type === 'text' && record.text === oldContent) { for (const key of Object.keys(record)) { delete record[key]; } if (typeof newContent === 'object' && newContent !== null) { const newContentRecord = newContent as Record<string, unknown>; for (const key of Object.keys(newContentRecord)) { record[key] = newContentRecord[key]; } } return; } for (const key in record) { if (record[key] === oldContent) { record[key] = newContent; } else { this.replaceContentInResponse(record[key], oldContent, newContent); } } } } private detectMimeType(content: string): string { if (content.trim().startsWith('{') || content.trim().startsWith('[')) { return 'application/json'; } if (content.includes('<html>') || content.includes('<!DOCTYPE')) { return 'text/html'; } if (content.includes('# ') || content.includes('## ')) { return 'text/markdown'; } return 'text/plain'; } private mapMimeTypeToContentType(mimeType?: string): ContentType { if (!mimeType) return 'text'; if (mimeType.startsWith('text/plain')) return 'text'; if (mimeType === 'application/json') return 'json'; if (mimeType === 'text/html') return 'html'; if (mimeType === 'text/markdown') return 'markdown'; if (mimeType.startsWith('text/')) return 'text'; return 'binary'; } private deepClone<T>(obj: T): T { if (obj === null || typeof obj !== 'object') { return obj; } if (obj instanceof Date) { return new Date(obj.getTime()) as T; } if (Array.isArray(obj)) { return obj.map((item) => this.deepClone(item)) as T; } const cloned = {} as T; for (const key in obj) { if (obj.hasOwnProperty(key)) { cloned[key] = this.deepClone(obj[key]); } } return cloned; } }