UNPKG

@codai/memorai-core

Version:

Core memory engine with vector operations for AI agents

352 lines 13.6 kB
import { nanoid } from 'nanoid'; import { MemoryError } from '../types/index.js'; import { EmbeddingService } from '../embedding/EmbeddingService.js'; import { MemoryVectorStore, QdrantVectorStore } from '../vector/VectorStore.js'; import { MemoryConfigManager } from '../config/MemoryConfig.js'; export class MemoryEngine { config; embedding; vectorStore; isInitialized = false; constructor(config) { this.config = new MemoryConfigManager(config); this.embedding = new EmbeddingService(this.config.getEmbedding()); const vectorConfig = this.config.getVectorDB(); const qdrantStore = new QdrantVectorStore(vectorConfig.url, vectorConfig.collection, vectorConfig.dimension, vectorConfig.api_key); this.vectorStore = new MemoryVectorStore(qdrantStore); } async initialize() { if (this.isInitialized) { return; } try { await this.vectorStore.initialize(); this.isInitialized = true; } catch (error) { if (error instanceof Error) { throw new MemoryError(`Failed to initialize memory engine: ${error.message}`, 'INIT_ERROR'); } throw new MemoryError('Unknown initialization error', 'INIT_ERROR'); } } /** * Natural language interface for agents: remember new information */ async remember(content, tenantId, agentId, options = {}) { await this.ensureInitialized(); if (!content || content.trim().length === 0) { throw new MemoryError('Content cannot be empty', 'INVALID_CONTENT'); } try { // Generate embedding const embeddingResult = await this.embedding.embed(content); // Create memory metadata const memory = { id: nanoid(), type: options.type ?? this.classifyMemoryType(content), content: content.trim(), embedding: embeddingResult.embedding, confidence: 1.0, // New memories start with full confidence createdAt: new Date(), updatedAt: new Date(), lastAccessedAt: new Date(), accessCount: 0, importance: options.importance ?? this.calculateImportance(content), emotional_weight: options.emotional_weight, tags: options.tags ?? [], context: options.context, tenant_id: tenantId, agent_id: agentId, ttl: options.ttl, }; // Store in vector database await this.vectorStore.storeMemory(memory, embeddingResult.embedding); return memory.id; } catch (error) { if (error instanceof Error) { throw new MemoryError(`Failed to remember: ${error.message}`, 'REMEMBER_ERROR'); } throw new MemoryError('Unknown remember error', 'REMEMBER_ERROR'); } } /** * Natural language interface for agents: recall relevant memories */ async recall(query, tenantId, agentId, options = {}) { await this.ensureInitialized(); if (!query || query.trim().length === 0) { throw new MemoryError('Query cannot be empty', 'INVALID_QUERY'); } try { // Generate query embedding const embeddingResult = await this.embedding.embed(query); // Build memory query const memoryQuery = { query: query.trim(), type: options.type, limit: options.limit ?? 10, threshold: options.threshold ?? 0.7, tenant_id: tenantId, agent_id: agentId, include_context: options.include_context ?? true, time_decay: options.time_decay ?? true, }; // Search memories const results = await this.vectorStore.searchMemories(embeddingResult.embedding, memoryQuery); // Apply time decay if enabled if (options.time_decay) { return this.applyTimeDecay(results); } return results; } catch (error) { if (error instanceof Error) { throw new MemoryError(`Failed to recall: ${error.message}`, 'RECALL_ERROR'); } throw new MemoryError('Unknown recall error', 'RECALL_ERROR'); } } /** * Natural language interface for agents: forget specific memories */ async forget(query, tenantId, agentId, confirmThreshold = 0.9) { await this.ensureInitialized(); try { // Find memories to forget const memories = await this.recall(query, tenantId, agentId, { threshold: confirmThreshold, limit: 100, }); if (memories.length === 0) { return 0; } // Delete memories const ids = memories.map(m => m.memory.id); await this.vectorStore.deleteMemories(ids); return ids.length; } catch (error) { if (error instanceof Error) { throw new MemoryError(`Failed to forget: ${error.message}`, 'FORGET_ERROR'); } throw new MemoryError('Unknown forget error', 'FORGET_ERROR'); } } /** * Natural language interface for agents: get contextual information */ async context(request) { await this.ensureInitialized(); try { let memories = []; if (request.topic) { memories = await this.recall(request.topic, request.tenant_id, request.agent_id, { limit: request.max_memories, threshold: 0.6, }); } else { // Get recent memories if no topic specified const recentQuery = { query: 'recent context', tenant_id: request.tenant_id, agent_id: request.agent_id, limit: request.max_memories, threshold: 0.5, include_context: true, time_decay: true, }; // This would need a different approach for recent memories // For now, we'll return empty results } // Filter by memory types if specified if (request.memory_types && request.memory_types.length > 0) { memories = memories.filter(m => request.memory_types.includes(m.memory.type)); } // Generate context summary const summary = this.generateContextSummary(memories); const contextText = this.generateContextText(memories); return { context: contextText, memories, summary, confidence: this.calculateContextConfidence(memories), generated_at: new Date(), }; } catch (error) { if (error instanceof Error) { throw new MemoryError(`Failed to generate context: ${error.message}`, 'CONTEXT_ERROR'); } throw new MemoryError('Unknown context error', 'CONTEXT_ERROR'); } } /** * Health check for the memory system */ async healthCheck() { const components = { vector_store: false, embedding: false, }; try { // Check vector store components.vector_store = await this.vectorStore.healthCheck(); // Check embedding service try { await this.embedding.embed('health check'); components.embedding = true; } catch { components.embedding = false; } const allHealthy = Object.values(components).every(Boolean); const result = { status: allHealthy ? 'healthy' : 'unhealthy', components, }; if (components.vector_store) { const memoryCount = await this.vectorStore.getMemoryCount('health-check'); return { ...result, memory_count: memoryCount }; } return result; } catch { return { status: 'unhealthy', components, }; } } /** * Get system health status */ async getHealth() { try { const checks = {}; // Check embedding service try { await this.embedding.embed('health check'); checks.embedding = true; } catch { checks.embedding = false; } // Check vector store try { // Basic vector store test - this might need adjustment based on VectorStore interface checks.vectorStore = this.isInitialized; } catch { checks.vectorStore = false; } const allHealthy = Object.values(checks).every(check => check); const anyHealthy = Object.values(checks).some(check => check); return { status: allHealthy ? 'healthy' : anyHealthy ? 'degraded' : 'unhealthy', checks, timestamp: new Date() }; } catch (error) { return { status: 'unhealthy', checks: { error: false }, timestamp: new Date() }; } } /** * Close connections and clean up resources */ async close() { // Close vector store connections if needed // This might need implementation based on the actual VectorStore interface this.isInitialized = false; } async ensureInitialized() { if (!this.isInitialized) { await this.initialize(); } } classifyMemoryType(content) { const lowerContent = content.toLowerCase(); // Simple classification based on keywords if (lowerContent.includes('prefer') || lowerContent.includes('like') || lowerContent.includes('dislike')) { return 'preference'; } if (lowerContent.includes('how to') || lowerContent.includes('step') || lowerContent.includes('process')) { return 'procedure'; } if (lowerContent.includes('is') || lowerContent.includes('are') || lowerContent.includes('fact')) { return 'fact'; } if (lowerContent.includes('personality') || lowerContent.includes('behavior') || lowerContent.includes('style')) { return 'personality'; } return 'thread'; // Default to thread for conversational content } calculateImportance(content) { // Simple importance calculation let importance = 0.5; // Base importance const lowerContent = content.toLowerCase(); // Keywords that increase importance const importantKeywords = ['critical', 'important', 'remember', 'always', 'never', 'error', 'bug']; for (const keyword of importantKeywords) { if (lowerContent.includes(keyword)) { importance += 0.1; } } // Length affects importance (longer content might be more detailed) if (content.length > 200) { importance += 0.1; } return Math.min(importance, 1.0); } applyTimeDecay(results) { const now = new Date(); return results.map(result => { const ageInDays = (now.getTime() - result.memory.lastAccessedAt.getTime()) / (1000 * 60 * 60 * 24); // Apply exponential decay (memories lose relevance over time) const decayFactor = Math.exp(-ageInDays / 30); // 30-day half-life const adjustedScore = result.score * decayFactor; return { ...result, score: Math.max(adjustedScore, 0.1), // Minimum score to avoid complete elimination }; }).sort((a, b) => b.score - a.score); } generateContextSummary(memories) { if (memories.length === 0) { return 'No relevant memories found.'; } const typeCount = memories.reduce((acc, m) => { acc[m.memory.type] = (acc[m.memory.type] || 0) + 1; return acc; }, {}); const typeSummary = Object.entries(typeCount) .map(([type, count]) => `${count} ${type}${count > 1 ? 's' : ''}`) .join(', '); return `Found ${memories.length} relevant memories: ${typeSummary}`; } generateContextText(memories) { if (memories.length === 0) { return ''; } return memories .slice(0, 10) // Limit to top 10 memories .map(m => `[${m.memory.type}] ${m.memory.content}`) .join('\n\n'); } calculateContextConfidence(memories) { if (memories.length === 0) { return 0; } const avgScore = memories.reduce((sum, m) => sum + m.score, 0) / memories.length; const avgConfidence = memories.reduce((sum, m) => sum + m.memory.confidence, 0) / memories.length; return (avgScore + avgConfidence) / 2; } } //# sourceMappingURL=MemoryEngine.js.map