UNPKG

semem

Version:

Semantic Memory for Intelligent Agents

383 lines (330 loc) 13 kB
import BaseAPI from '../common/BaseAPI.js'; import { v4 as uuidv4 } from 'uuid'; import { EventEmitter } from 'events'; /** * Chat API handler for generating chat responses with memory context */ export default class ChatAPI extends BaseAPI { constructor(config = {}) { super(config); this.memoryManager = null; this.llmHandler = null; this.conversationCache = new Map(); this.similarityThreshold = config.similarityThreshold || 0.7; this.contextWindow = config.contextWindow || 5; } async initialize() { await super.initialize(); // Get dependencies from registry const registry = this.config.registry; if (!registry) { throw new Error('Registry is required for ChatAPI'); } try { this.memoryManager = registry.get('memory'); this.llmHandler = registry.get('llm'); this.logger.info('ChatAPI initialized successfully'); } catch (error) { this.logger.error('Failed to initialize ChatAPI:', error); throw error; } } /** * Execute a chat operation */ async executeOperation(operation, params) { this._validateParams(params); const start = Date.now(); try { let result; switch (operation) { case 'chat': result = await this.generateChatResponse(params); break; case 'stream': result = await this.streamChatResponse(params); break; case 'completion': result = await this.generateCompletion(params); break; default: throw new Error(`Unknown operation: ${operation}`); } const duration = Date.now() - start; this._emitMetric(`chat.${operation}.duration`, duration); this._emitMetric(`chat.${operation}.count`, 1); return result; } catch (error) { this._emitMetric(`chat.${operation}.errors`, 1); throw error; } } /** * Generate a chat response with memory context */ async generateChatResponse({ prompt, conversationId, useMemory = true, temperature = 0.7, model }) { if (!prompt) { throw new Error('Prompt is required'); } try { // Get or create conversation const conversation = this._getConversation(conversationId); // Get relevant memories if needed let relevantMemories = []; if (useMemory) { relevantMemories = await this.memoryManager.retrieveRelevantInteractions( prompt, this.similarityThreshold ); } // Create context from conversation history and relevant memories const context = this._buildContext(conversation, relevantMemories); // Generate response // Generate response with the specified model or use default const response = await this.llmHandler.generateResponse( prompt, context, { temperature, model: model // Will fall back to LLMHandler's default if not provided } ); this.logger.log(`Generated response using model: ${model || 'default'}`); // Update conversation history conversation.history.push({ role: 'user', content: prompt }); conversation.history.push({ role: 'assistant', content: response }); // Trim conversation if needed if (conversation.history.length > this.contextWindow * 2) { conversation.history = conversation.history.slice(-this.contextWindow * 2); } // Store in memory if enabled if (useMemory) { const embedding = await this.memoryManager.generateEmbedding( `${prompt} ${response}` ); const concepts = await this.memoryManager.extractConcepts( `${prompt} ${response}` ); await this.memoryManager.addInteraction( prompt, response, embedding, concepts, { conversationId: conversation.id, timestamp: Date.now() } ); } this._emitMetric('chat.generate.count', 1); return { response, conversationId: conversation.id, memoryIds: relevantMemories.map(m => m.interaction.id || '') .filter(id => id !== '') }; } catch (error) { this._emitMetric('chat.generate.errors', 1); throw error; } } /** * Stream a chat response with memory context */ async streamChatResponse({ prompt, conversationId, useMemory = true, temperature = 0.7 }) { if (!prompt) { throw new Error('Prompt is required'); } try { // Create a stream const stream = new EventEmitter(); // Process in background (async () => { try { // Get or create conversation const conversation = this._getConversation(conversationId); // Get relevant memories if needed let relevantMemories = []; if (useMemory) { relevantMemories = await this.memoryManager.retrieveRelevantInteractions( prompt, this.similarityThreshold ); } // Create context from conversation history and relevant memories const context = this._buildContext(conversation, relevantMemories); // Generate streaming response let responseText = ''; const responseStream = await this.llmHandler.generateStreamingResponse( prompt, context, { temperature } ); responseStream.on('data', (chunk) => { responseText += chunk; stream.emit('data', { chunk }); }); responseStream.on('end', async () => { // Update conversation history conversation.history.push({ role: 'user', content: prompt }); conversation.history.push({ role: 'assistant', content: responseText }); // Trim conversation if needed if (conversation.history.length > this.contextWindow * 2) { conversation.history = conversation.history.slice(-this.contextWindow * 2); } // Store in memory if enabled if (useMemory) { const embedding = await this.memoryManager.generateEmbedding( `${prompt} ${responseText}` ); const concepts = await this.memoryManager.extractConcepts( `${prompt} ${responseText}` ); await this.memoryManager.addInteraction( prompt, responseText, embedding, concepts, { conversationId: conversation.id, timestamp: Date.now() } ); } // End stream stream.emit('end'); }); responseStream.on('error', (error) => { stream.emit('error', error); }); } catch (error) { stream.emit('error', error); } })(); this._emitMetric('chat.stream.count', 1); return stream; } catch (error) { this._emitMetric('chat.stream.errors', 1); throw error; } } /** * Generate a text completion with memory context */ async generateCompletion({ prompt, max_tokens = 100, temperature = 0.7 }) { if (!prompt) { throw new Error('Prompt is required'); } try { // Get relevant memories const relevantMemories = await this.memoryManager.retrieveRelevantInteractions( prompt, this.similarityThreshold ); // Create context from relevant memories const context = relevantMemories.map(m => `${m.interaction.prompt} ${m.interaction.output}` ).join('\n\n'); // Generate completion const completion = await this.llmHandler.generateCompletion( prompt, context, { max_tokens, temperature } ); this._emitMetric('chat.completion.count', 1); return { completion, memoryIds: relevantMemories.map(m => m.interaction.id || '') .filter(id => id !== '') }; } catch (error) { this._emitMetric('chat.completion.errors', 1); throw error; } } /** * Get or create a conversation */ _getConversation(conversationId) { if (!conversationId) { return this._createConversation(); } const conversation = this.conversationCache.get(conversationId); if (!conversation) { // Create a new conversation with the provided ID return this._createConversation(conversationId); } return conversation; } /** * Create a new conversation */ _createConversation(id = null) { const conversationId = id || uuidv4(); const conversation = { id: conversationId, created: Date.now(), lastAccessed: Date.now(), history: [] }; this.conversationCache.set(conversationId, conversation); return conversation; } /** * Build context for the LLM from conversation history and relevant memories */ _buildContext(conversation, relevantMemories) { const context = { conversation: conversation.history, relevantMemories: relevantMemories.map(memory => ({ prompt: memory.interaction.prompt, response: memory.interaction.output, similarity: memory.similarity })) }; return context; } /** * Get chat API metrics */ async getMetrics() { const baseMetrics = await super.getMetrics(); return { ...baseMetrics, conversations: { count: this.conversationCache.size, active: this._getActiveConversationCount() }, operations: { chat: { count: await this._getMetricValue('chat.chat.count'), errors: await this._getMetricValue('chat.chat.errors'), duration: await this._getMetricValue('chat.chat.duration') }, stream: { count: await this._getMetricValue('chat.stream.count'), errors: await this._getMetricValue('chat.stream.errors') }, completion: { count: await this._getMetricValue('chat.completion.count'), errors: await this._getMetricValue('chat.completion.errors') } } }; } _getActiveConversationCount() { const now = Date.now(); const activeThreshold = 30 * 60 * 1000; // 30 minutes let count = 0; for (const conversation of this.conversationCache.values()) { if (now - conversation.lastAccessed < activeThreshold) { count++; } } return count; } async _getMetricValue(metricName) { // In a real implementation, this would fetch from a metrics store return 0; } }