import { v4 as uuidv4 } from "uuid"; import { createHash } from "crypto"; import { MemoryConfig, MemoryConfigSchema, MemoryItem, Message, SearchFilters, SearchResult, ChatResult, MemoryItemWithContext, } from "../types"; import { EmbedderFactory, LLMFactory, VectorStoreFactory, HistoryManagerFactory, } from "../utils/factory"; import { getFactRetrievalMessages, getUpdateMemoryMessages, parseMessages, removeCodeBlocks, } from "../prompts"; import { DummyHistoryManager } from "../storage/DummyHistoryManager"; import { Embedder } from "../embeddings/base"; import { LLM } from "../llms/base"; import { VectorStore } from "../vector_stores/base"; import { ConfigManager } from "../config/manager"; import { AddMemoryOptions, SearchMemoryOptions, ChatMemoryOptions, DeleteAllMemoryOptions, GetAllMemoryOptions, InternalSearchOptions, } from "./memory.types"; import { parse_vision_messages } from "../utils/memory"; import { HistoryManager } from "../storage/base"; import { SearchAgents, SearchAgentOptions } from "../agents/search"; export class Memory { private config: MemoryConfig; private customPrompt: string | undefined; private embedder: Embedder; private vectorStore: VectorStore; private llm: LLM; private db: HistoryManager; constructor(config: Partial = {}) { // Merge and validate config this.config = ConfigManager.mergeConfig(config); this.customPrompt = this.config.customPrompt; this.embedder = EmbedderFactory.create( this.config.embedder.provider, this.config.embedder.config, ); this.vectorStore = VectorStoreFactory.create( this.config.vectorStore.provider, this.config.vectorStore.config, ); this.llm = LLMFactory.create( this.config.llm.provider, this.config.llm.config, ); if (this.config.disableHistory) { this.db = new DummyHistoryManager(); } else { const defaultConfig = { provider: "sqlite", config: { historyDbPath: this.config.historyDbPath || ":memory:", }, }; this.db = this.config.historyStore && !this.config.disableHistory ? HistoryManagerFactory.create( this.config.historyStore.provider, this.config.historyStore, ) : HistoryManagerFactory.create("sqlite", defaultConfig); } } static fromConfig(configDict: Record): Memory { try { const config = MemoryConfigSchema.parse(configDict); return new Memory(config); } catch (e) { console.error("Configuration validation error:", e); throw e; } } async add( messages: string | Message[], config: AddMemoryOptions, ): Promise { const { userId, agentId, runId, metadata = {}, filters = {}, infer = true, batchSize = 5, batchStride = 2, enableBatching = true, } = config; // Auto-generate a unique messageId for this add operation const messageId = uuidv4(); if (userId) filters.userId = metadata.userId = userId; if (agentId) filters.agentId = metadata.agentId = agentId; if (runId) filters.runId = metadata.runId = runId; metadata.messageId = messageId; if (!filters.userId && !filters.agentId && !filters.runId) { throw new Error( "One of the filters: userId, agentId or runId is required!", ); } const parsedMessages = Array.isArray(messages) ? (messages as Message[]) : [{ role: "user", content: messages }]; const final_parsedMessages = await parse_vision_messages(parsedMessages); // STORE ALL RAW MESSAGES FIRST - regardless of whether they become memories console.log(`📝 Storing ${final_parsedMessages.length} raw messages for future reference`); await this.db.storeRawMessages(final_parsedMessages, { userId: metadata.userId, runId: metadata.runId, messageId, batchInfo: enableBatching && final_parsedMessages.length > batchSize ? { totalMessages: final_parsedMessages.length, batchSize, batchStride, } : undefined }); // Check if batching is needed if (enableBatching && final_parsedMessages.length > batchSize) { console.log(`Processing ${final_parsedMessages.length} messages in batches (size: ${batchSize}, stride: ${batchStride})`); return this.addInBatches(final_parsedMessages, metadata, filters, infer, batchSize, batchStride); } // Add to vector store normally for smaller message sets const vectorStoreResult = await this.addToVectorStore( final_parsedMessages, metadata, filters, infer, ); return { results: vectorStoreResult }; } private async addInBatches( messages: Message[], metadata: Record, filters: SearchFilters, infer: boolean, batchSize: number, batchStride: number, ): Promise { const allResults: MemoryItem[] = []; const totalMessages = messages.length; let batchIndex = 0; for (let i = 0; i < totalMessages; i += batchStride) { // Get the current batch const batchEnd = Math.min(i + batchSize, totalMessages); const batch = messages.slice(i, batchEnd); if (batch.length === 0) break; batchIndex++; console.log(`Processing batch ${batchIndex}: messages ${i + 1}-${batchEnd} (${batch.length} messages)`); // Create batch-specific metadata const batchMetadata = { ...metadata, batchIndex, batchStart: i, batchEnd, totalBatches: Math.ceil((totalMessages - batchSize) / batchStride) + 1, originalMessageCount: totalMessages, messageId: uuidv4(), // Each batch gets its own message ID for tracking }; try { // Store raw messages for this batch (in addition to the main storage) await this.db.storeRawMessages(batch, { userId: metadata.userId, runId: metadata.runId, messageId: batchMetadata.messageId, batchInfo: { batchIndex, batchStart: i, batchEnd, parentMessageId: metadata.messageId, // Link to original message ID } }); // Process this batch const batchResult = await this.addToVectorStore( batch, batchMetadata, filters, infer, ); // Add batch results to the overall results allResults.push(...batchResult); // Add a small delay between batches to avoid overwhelming the LLM if (i + batchStride < totalMessages) { await new Promise(resolve => setTimeout(resolve, 500)); // 500ms delay } } catch (error) { console.error(`Error processing batch ${batchIndex}:`, error); // Continue with next batch even if one fails } } console.log(`✅ Completed batch processing: ${allResults.length} memories created from ${totalMessages} messages`); return { results: allResults }; } private async addToVectorStore( messages: Message[], metadata: Record, filters: SearchFilters, infer: boolean, ): Promise { if (!infer) { const returnedMemories: MemoryItem[] = []; for (const message of messages) { if (message.content === "system") { continue; } const memoryId = await this.createMemory( message.content as string, {}, metadata, messages, ); returnedMemories.push({ id: memoryId, memory: message.content as string, metadata: { event: "ADD" }, messages, }); } return returnedMemories; } const parsedMessages = messages.map((m) => m.content).join("\n"); // Get existing entities to provide context const existingEntities = await this.db.getAllEntities({ userId: filters.userId, runId: filters.runId, }); const [systemPrompt, userPrompt] = this.customPrompt ? [this.customPrompt, `Input:\n${parsedMessages}`] : getFactRetrievalMessages(parsedMessages, existingEntities); const response = await this.llm.generateResponse( [ { role: "system", content: systemPrompt }, { role: "user", content: userPrompt }, ], { type: "json_object" }, ); const cleanResponse = removeCodeBlocks(response as string); let facts: string[] = []; let extractedEntities: Array<{ entity_id: string; label: string; type: string }> = []; try { const parsed = JSON.parse(cleanResponse); facts = parsed.facts || []; extractedEntities = parsed.entities || []; } catch (e) { console.error( "Failed to parse facts and entities from LLM response:", cleanResponse, e, ); facts = []; extractedEntities = []; } // Get embeddings for new facts const newMessageEmbeddings: Record = {}; const retrievedOldMemory: Array<{ id: string; text: string; entity_ids?: string[] }> = []; // Create embeddings and search for similar memories for (const fact of facts) { const embedding = await this.embedder.embed(fact); newMessageEmbeddings[fact] = embedding; const existingMemories = await this.vectorStore.search( embedding, 5, filters, ); for (const mem of existingMemories) { const entityIds = await this.db.getMemoryEntities(mem.id); retrievedOldMemory.push({ id: mem.id, text: mem.payload.data, entity_ids: entityIds, }); } } // Remove duplicates from old memories const uniqueOldMemories = retrievedOldMemory.filter( (mem, index) => retrievedOldMemory.findIndex((m) => m.id === mem.id) === index, ); // Create UUID mapping for handling UUID hallucinations const tempUuidMapping: Record = {}; uniqueOldMemories.forEach((item, idx) => { tempUuidMapping[String(idx)] = item.id; uniqueOldMemories[idx].id = String(idx); }); // Get memory update decisions with entities const updatePrompt = getUpdateMemoryMessages( uniqueOldMemories, facts, extractedEntities, existingEntities ); const updateResponse = await this.llm.generateResponse( [{ role: "user", content: updatePrompt }], { type: "json_object" }, ); const cleanUpdateResponse = removeCodeBlocks(updateResponse as string); let memoryActions: any[] = []; let entityActions: any[] = []; try { const parsed = JSON.parse(cleanUpdateResponse); memoryActions = Array.isArray(parsed.memory) ? parsed.memory : []; entityActions = Array.isArray(parsed.entities) ? parsed.entities : []; } catch (e) { console.error( "Failed to parse memory and entity actions from LLM response:", cleanUpdateResponse, e, ); memoryActions = []; entityActions = []; } // Ensure entityActions is always an array if (!Array.isArray(entityActions)) { console.warn("entityActions is not an array, defaulting to empty array:", entityActions); entityActions = []; } // Process entity actions first for (const entityAction of entityActions) { try { switch (entityAction.event) { case "ADD": await this.db.addEntity( entityAction.entity_id, entityAction.label, entityAction.type, filters.userId, filters.runId, ); break; case "UPDATE": await this.db.updateEntity( entityAction.entity_id, entityAction.label, entityAction.type, filters.userId, filters.runId, ); break; case "DELETE": await this.db.deleteEntity(entityAction.entity_id); break; } } catch (error) { console.error(`Error processing entity action: ${error}`); } } // Process memory actions const results: MemoryItem[] = []; for (const action of memoryActions) { try { switch (action.event) { case "ADD": { const memoryId = await this.createMemory( action.text, newMessageEmbeddings, metadata, messages, ); // Associate entities with memory if (action.entity_ids && action.entity_ids.length > 0) { await this.db.associateMemoryWithEntities(memoryId, action.entity_ids); } results.push({ id: memoryId, memory: action.text, metadata: { event: action.event }, entityIds: action.entity_ids || [], messages, }); break; } case "UPDATE": { const realMemoryId = tempUuidMapping[action.id]; await this.updateMemory( realMemoryId, action.text, newMessageEmbeddings, metadata, messages, ); // Update entity associations if (action.entity_ids) { await this.db.associateMemoryWithEntities(realMemoryId, action.entity_ids); } results.push({ id: realMemoryId, memory: action.text, metadata: { event: action.event, previousMemory: action.old_memory, }, entityIds: action.entity_ids || [], messages, }); break; } case "DELETE": { const realMemoryId = tempUuidMapping[action.id]; await this.deleteMemory(realMemoryId); results.push({ id: realMemoryId, memory: action.text, metadata: { event: action.event }, messages, }); break; } } } catch (error) { console.error(`Error processing memory action: ${error}`); } } return results; } async get(memoryId: string): Promise { const memory = await this.vectorStore.get(memoryId); if (!memory) return null; const filters = { ...(memory.payload.userId && { userId: memory.payload.userId }), ...(memory.payload.agentId && { agentId: memory.payload.agentId }), ...(memory.payload.runId && { runId: memory.payload.runId }), }; // Get associated entities const entityIds = await this.db.getMemoryEntities(memoryId); // Parse original messages if available let originalMessages: Message[] | undefined; if (memory.payload.originalMessages) { try { originalMessages = JSON.parse(memory.payload.originalMessages); } catch (e) { console.warn("Failed to parse original messages:", e); } } const memoryItem: MemoryItem = { id: memory.id, memory: memory.payload.data, hash: memory.payload.hash, createdAt: memory.payload.createdAt, updatedAt: memory.payload.updatedAt, metadata: {}, entityIds, ...(originalMessages && { messages: originalMessages }), }; // Add additional metadata const excludedKeys = new Set([ "userId", "agentId", "runId", "hash", "data", "createdAt", "updatedAt", "originalMessages", // Exclude this as we handle it separately ]); for (const [key, value] of Object.entries(memory.payload)) { if (!excludedKeys.has(key)) { memoryItem.metadata![key] = value; } } return { ...memoryItem, ...filters }; } async search( query: string, config: SearchMemoryOptions, ): Promise { // IMPORTANT: This method should NEVER receive customInstructions // Search operations must remain objective and unbiased const { userId, agentId, runId, limit = 100, filters = {}, includeContext = true, contextWindowSize = 10, useSearchAgents = false, maxAgentSteps = 3, enableMultihop = false, maxHops = 3, includeFullMessages = false, // Default to false for efficiency reasoningCustomInstructions } = config; if (userId) filters.userId = userId; if (agentId) filters.agentId = agentId; if (runId) filters.runId = runId; if (!filters.userId && !filters.agentId && !filters.runId) { throw new Error( "One of the filters: userId, agentId or runId is required!", ); } // Search vector store const queryEmbedding = await this.embedder.embed(query); const memories = await this.vectorStore.search( queryEmbedding, limit, filters, ); const excludedKeys = new Set([ "userId", "agentId", "runId", "hash", "data", "createdAt", "updatedAt", "originalMessages", // Exclude this as we handle it separately ]); // Process each memory and add context if enabled const processedResults = await Promise.all(memories.map(async (mem) => { // Get associated entities const entityIds = await this.db.getMemoryEntities(mem.id); // Parse original messages if available let originalMessages: Message[] | undefined; if (mem.payload.originalMessages) { try { originalMessages = JSON.parse(mem.payload.originalMessages); } catch (e) { console.warn("Failed to parse original messages:", e); } } // Create basic memory item const memoryItem = { id: mem.id, memory: mem.payload.data, hash: mem.payload.hash, createdAt: mem.payload.createdAt, updatedAt: mem.payload.updatedAt, score: mem.score, entityIds, // Only include messages if explicitly requested ...(includeFullMessages && originalMessages && { messages: originalMessages }), metadata: Object.entries(mem.payload) .filter(([key]) => !excludedKeys.has(key)) .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}), ...(mem.payload.userId && { userId: mem.payload.userId }), ...(mem.payload.agentId && { agentId: mem.payload.agentId }), ...(mem.payload.runId && { runId: mem.payload.runId }), }; // Add context messages if enabled, full messages requested, and messageId exists if (includeContext && includeFullMessages && mem.payload.messageId) { try { const contextMessages = await this.db.getMessageContext( mem.payload.messageId, { beforeCount: contextWindowSize, afterCount: contextWindowSize, userId: mem.payload.userId, runId: mem.payload.runId } ); if (contextMessages.length > 0) { // Transform history entries to context messages const context = contextMessages.map(msg => ({ id: msg.id.toString(), // Convert to string to match ContextMessage type content: msg.new_value || msg.previous_value || '', timestamp: msg.happened_at || msg.created_at, messageId: msg.message_id, userId: msg.user_id, runId: msg.run_id, type: msg.type })); return { ...memoryItem, context }; } } catch (error) { console.error(`Error fetching context messages for memory ${mem.id}:`, error); } } return memoryItem; })); // Filter context before agent analysis to prevent overwhelming const filteredResults = processedResults.length > 50 ? await this.filterRelevantContext(query, processedResults, 50, 'search') : processedResults; const initialResults: SearchResult = { results: filteredResults, }; // If search agents are enabled, analyze and potentially enhance results if (useSearchAgents) { try { // Create a search function for the research agent that binds to this Memory instance const searchFunction = async (query: string, options?: any) => { return this.search(query, { ...options, useSearchAgents: false, // Prevent recursive calls }); }; const searchAgents = new SearchAgents(this.llm, this.db, searchFunction); const agentOptions: SearchAgentOptions = { userId, agentId, runId, maxSteps: maxAgentSteps, enableMultihop, maxHops, reasoningCustomInstructions }; // Brief status log if (filteredResults.length === 0) { console.log(`🔍 No memories found, searching raw messages...`); } else { console.log(`🤖 Enhancing ${filteredResults.length} focused memory results with agents...`); } // Use multihop analysis if enabled if (enableMultihop) { const multihopResult = await searchAgents.executeMultihopAnalysis( query, initialResults, agentOptions ); return { ...multihopResult.finalResult, agentAnalysis: { multihopResults: multihopResult.hopResults, isMultihop: true } }; } else { // Single hop analysis (original behavior) const decision = await searchAgents.analyzeSearchNeed( query, initialResults, agentOptions ); // Execute additional steps if needed if (decision.needsAdditionalSteps) { const { additionalResults, stepResults } = await searchAgents.executeSteps( decision, agentOptions, true, // Log summary for single-hop scenarios query // Pass query for context filtering ); // Enhance the search result with agent data return { ...initialResults, results: [...initialResults.results, ...additionalResults], agentAnalysis: { decision, stepResults, additionalResults, isMultihop: false } }; } else { // Still need to log summary for single-hop with no additional steps searchAgents.finalizeSingleHopAnalysis(); } // Include analysis even if no additional steps return { ...initialResults, agentAnalysis: { decision, stepResults: [], additionalResults: [], isMultihop: false } }; } } catch (error) { console.error("Search agents failed:", error); // Return original results if agents fail return initialResults; } } return initialResults; } async chat( query: string, config: ChatMemoryOptions, ): Promise { const startTime = Date.now(); const { userId, agentId, runId, limit = 10, // Smaller default for chat filters = {}, includeContext = true, contextWindowSize = 5, // Smaller context window for chat useSearchAgents = true, // Enable agents by default for chat maxAgentSteps = 2, enableMultihop = true, // Enable multihop by default for chat maxHops = 2, // Smaller default for chat temperature = 0.7, maxTokens = 500, responseCustomInstructions, reasoningCustomInstructions, customInstructions, // Legacy support } = config; // Handle legacy customInstructions for backward compatibility const finalResponseInstructions = responseCustomInstructions || customInstructions; // PHASE 1: SEARCH AND CONTEXT GATHERING // Note: customInstructions are NOT used in this phase to avoid bias in information retrieval const searchOptions = this.extractSearchOptionsFromChatOptions(config); // For chat, we want more detailed results including full messages for better context searchOptions.includeFullMessages = true; const searchResult = await this.search(query, searchOptions); // PHASE 2: FINAL RESPONSE GENERATION // Filter context one final time for response generation to ensure focus const preResponseMemories = searchResult.results.length > 8 ? await this.filterRelevantContext(query, searchResult.results, 8, 'response') : searchResult.results; // Note: customInstructions are ONLY applied here for the final answer const relevantMemories = preResponseMemories; const contextInfo = this.prepareChatContext(query, relevantMemories, includeContext); // Generate final response with custom instructions applied const chatPrompt = this.buildChatPrompt( query, contextInfo, searchResult.agentAnalysis, finalResponseInstructions // Use the correct response instructions ); try { const response = await this.llm.generateResponse( [{ role: "user", content: chatPrompt }], { type: "text" as const, }, ); const processingTime = Date.now() - startTime; return { response: response as string, sources: relevantMemories, agentAnalysis: searchResult.agentAnalysis, metadata: { searchQuery: query, sourcesCount: relevantMemories.length, responseTokens: (response as string).split(' ').length, // Rough token estimate processingTimeMs: processingTime, responseCustomInstructions: finalResponseInstructions || null, reasoningCustomInstructions: reasoningCustomInstructions || null, customInstructions: customInstructions || null, // Legacy field multihopEnabled: enableMultihop, hopsUsed: searchResult.agentAnalysis?.multihopResults?.length || 0, }, }; } catch (error) { console.error("Error generating chat response:", error); // Fallback response const processingTime = Date.now() - startTime; return { response: "I'm sorry, I encountered an error while processing your request. Please try again.", sources: relevantMemories, agentAnalysis: searchResult.agentAnalysis, metadata: { searchQuery: query, sourcesCount: relevantMemories.length, processingTimeMs: processingTime, responseCustomInstructions: finalResponseInstructions || null, reasoningCustomInstructions: reasoningCustomInstructions || null, customInstructions: customInstructions || null, // Legacy field multihopEnabled: enableMultihop, hopsUsed: 0, }, }; } } private prepareChatContext(query: string, memories: MemoryItemWithContext[], includeContext: boolean): string { if (memories.length === 0) { return "No relevant information found in memory."; } let context = "## Relevant Information from Memory:\n\n"; memories.forEach((memory, index) => { context += `### Memory ${index + 1} (Score: ${memory.score?.toFixed(3) || 'N/A'})\n`; context += `**Content:** ${memory.memory}\n`; if (memory.entityIds && memory.entityIds.length > 0) { context += `**Entities:** ${memory.entityIds.join(', ')}\n`; } if (memory.messages && memory.messages.length > 0) { context += `**Original Messages:**\n`; memory.messages.forEach((msg: Message, msgIdx: number) => { const content = typeof msg.content === 'string' ? msg.content : '[multimodal content]'; context += ` ${msgIdx + 1}. [${msg.role}]: ${content}\n`; }); } else if (memory.metadata?.messageId && !memory.messages) { context += `**Note:** Full message details available via memory ID ${memory.id}\n`; } if (includeContext && memory.context && memory.context.length > 0) { context += `**Context Messages:**\n`; memory.context.forEach((ctxMsg: any) => { const typeSymbol = ctxMsg.type === 'before' ? '↑' : ctxMsg.type === 'after' ? '↓' : '→'; context += ` ${typeSymbol} ${ctxMsg.content}\n`; }); } if (memory.metadata && Object.keys(memory.metadata).length > 0) { context += `**Metadata:** ${JSON.stringify(memory.metadata)}\n`; } context += "\n"; }); return context; } private buildChatPrompt(query: string, contextInfo: string, agentAnalysis?: any, customInstructions?: string): string { let prompt = `You are an AI assistant with access to a user's memory system. Your mission is to provide SPECIFIC, OBJECTIVE, and COMPLETE answers by extracting precise details from the available information. ## User Query (PRIORITY TARGET) ${query} ## Available Context (Enhanced for Specificity) ${contextInfo}`; if (agentAnalysis) { if (agentAnalysis.isMultihop && agentAnalysis.multihopResults) { prompt += `\n## Search Analysis Summary The system performed ${agentAnalysis.multihopResults.length} comprehensive analysis step(s) to gather specific details for your query.`; } else if (agentAnalysis.decision && agentAnalysis.decision.needsAdditionalSteps) { prompt += `\n## Search Analysis Summary Enhanced information gathering was performed to provide specific, objective details for your query.`; } } // FINAL RESPONSE CUSTOMIZATION: Apply custom instructions only to the final answer if (customInstructions) { prompt += `\n## Response Instructions ${customInstructions}`; } prompt += `\n\n## ENHANCED RESPONSE OBJECTIVES Your mission is to provide the most SPECIFIC and OBJECTIVE answer possible to: "${query}" ### SPECIFICITY REQUIREMENTS: 1. **CONVERT VAGUE TO SPECIFIC**: - "last year" → exact year (e.g., "2023") - "home country" → actual country name (e.g., "India") - "that activity" → specific activity name (e.g., "playing chess") - "the company" → actual company name (e.g., "Google") - "recently" → specific timeframe (e.g., "November 2024") 2. **PROVIDE CONCRETE DETAILS**: - Use exact dates, not relative terms - Use proper names, not pronouns or generic references - Use specific locations, not vague directionals - Use precise quantities, not approximations 3. **RESOLVE AMBIGUITY**: - Cross-reference multiple memories to clarify unclear references - Use conversation context to identify what "that" or "it" refers to - Connect entities across different memories for complete understanding 4. **EVIDENCE-BASED CLAIMS**: - Only state facts that are directly supported by the memory content - When making inferences, clearly indicate they are inferences - Distinguish between explicit information and contextual deductions ### RESPONSE STRATEGY: - **Lead with the MOST SPECIFIC answer possible** - **Support with concrete evidence from memories** - **Explain HOW you derived specific details from vague references** - **Acknowledge when information cannot be made more specific** ### QUALITY CHECKLIST: ✅ Does my answer use specific names, dates, and details? ✅ Have I converted vague terms to concrete information where possible? ✅ Is my answer objective and fact-based? ✅ Have I provided the most complete answer possible from available context? Answer the query: "${query}" Response:`; return prompt; } async update(memoryId: string, data: string): Promise<{ message: string }> { const embedding = await this.embedder.embed(data); await this.updateMemory(memoryId, data, { [data]: embedding }); return { message: "Memory updated successfully!" }; } async delete(memoryId: string): Promise<{ message: string }> { await this.deleteMemory(memoryId); return { message: "Memory deleted successfully!" }; } async deleteAll( config: DeleteAllMemoryOptions, ): Promise<{ message: string }> { const { userId, agentId, runId } = config; const filters: SearchFilters = {}; if (userId) filters.userId = userId; if (agentId) filters.agentId = agentId; if (runId) filters.runId = runId; if (!Object.keys(filters).length) { throw new Error( "At least one filter is required to delete all memories. If you want to delete all memories, use the `reset()` method.", ); } const [memories] = await this.vectorStore.list(filters); for (const memory of memories) { await this.deleteMemory(memory.id); } return { message: "Memories deleted successfully!" }; } async history(memoryId: string, options?: { userId?: string, runId?: string }): Promise { return this.db.getHistory(memoryId, options); } async getAllMessages(options?: { userId?: string, runId?: string, limit?: number }): Promise { return this.db.getAllMessages(options); } async reset(): Promise { await this.db.reset(); // Check provider before attempting deleteCol if (this.config.vectorStore.provider.toLowerCase() !== "langchain") { try { await this.vectorStore.deleteCol(); } catch (e) { console.error( `Failed to delete collection for provider '${this.config.vectorStore.provider}':`, e, ); // Decide if you want to re-throw or just log } } else { console.warn( "Memory.reset(): Skipping vector store collection deletion as 'langchain' provider is used. Underlying Langchain vector store data is not cleared by this operation.", ); } // Re-initialize factories/clients based on the original config this.embedder = EmbedderFactory.create( this.config.embedder.provider, this.config.embedder.config, ); // Re-create vector store instance - crucial for Langchain to reset wrapper state if needed this.vectorStore = VectorStoreFactory.create( this.config.vectorStore.provider, this.config.vectorStore.config, // This will pass the original client instance back ); this.llm = LLMFactory.create( this.config.llm.provider, this.config.llm.config, ); // Re-init DB if needed (though db.reset() likely handles its state) // Re-init Graph if needed } async getAll(config: GetAllMemoryOptions): Promise { const { userId, agentId, runId, limit = 100 } = config; const filters: SearchFilters = {}; if (userId) filters.userId = userId; if (agentId) filters.agentId = agentId; if (runId) filters.runId = runId; const [memories] = await this.vectorStore.list(filters, limit); const excludedKeys = new Set([ "userId", "agentId", "runId", "hash", "data", "createdAt", "updatedAt", "originalMessages", // Exclude this as we handle it separately ]); const results = await Promise.all(memories.map(async (mem) => { // Get associated entities const entityIds = await this.db.getMemoryEntities(mem.id); // Parse original messages if available let originalMessages: Message[] | undefined; if (mem.payload.originalMessages) { try { originalMessages = JSON.parse(mem.payload.originalMessages); } catch (e) { console.warn("Failed to parse original messages:", e); } } return { id: mem.id, memory: mem.payload.data, hash: mem.payload.hash, createdAt: mem.payload.createdAt, updatedAt: mem.payload.updatedAt, entityIds, ...(originalMessages && { messages: originalMessages }), metadata: Object.entries(mem.payload) .filter(([key]) => !excludedKeys.has(key)) .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}), ...(mem.payload.userId && { userId: mem.payload.userId }), ...(mem.payload.agentId && { agentId: mem.payload.agentId }), ...(mem.payload.runId && { runId: mem.payload.runId }), }; })); return { results }; } private async createMemory( data: string, existingEmbeddings: Record, metadata: Record, messages?: Message[], ): Promise { const memoryId = uuidv4(); const embedding = existingEmbeddings[data] || (await this.embedder.embed(data)); const memoryMetadata = { ...metadata, data, hash: createHash("md5").update(data).digest("hex"), createdAt: new Date().toISOString(), ...(messages && { originalMessages: JSON.stringify(messages) }), }; await this.vectorStore.insert([embedding], [memoryId], [memoryMetadata]); await this.db.addHistory( memoryId, null, data, "ADD", memoryMetadata.createdAt, undefined, 0, metadata.userId, metadata.runId, metadata.messageId, memoryMetadata.createdAt, ); return memoryId; } private async updateMemory( memoryId: string, data: string, existingEmbeddings: Record, metadata: Record = {}, messages?: Message[], ): Promise { const existingMemory = await this.vectorStore.get(memoryId); if (!existingMemory) { throw new Error(`Memory with ID ${memoryId} not found`); } const prevValue = existingMemory.payload.data; const embedding = existingEmbeddings[data] || (await this.embedder.embed(data)); const newMetadata = { ...metadata, data, hash: createHash("md5").update(data).digest("hex"), createdAt: existingMemory.payload.createdAt, updatedAt: new Date().toISOString(), ...(existingMemory.payload.userId && { userId: existingMemory.payload.userId, }), ...(existingMemory.payload.agentId && { agentId: existingMemory.payload.agentId, }), ...(existingMemory.payload.runId && { runId: existingMemory.payload.runId, }), ...(messages && { originalMessages: JSON.stringify(messages) }), }; await this.vectorStore.update(memoryId, embedding, newMetadata); await this.db.addHistory( memoryId, prevValue, data, "UPDATE", newMetadata.createdAt, newMetadata.updatedAt, 0, metadata.userId || existingMemory.payload.userId, metadata.runId || existingMemory.payload.runId, metadata.messageId, newMetadata.updatedAt, ); return memoryId; } private async deleteMemory(memoryId: string): Promise { const existingMemory = await this.vectorStore.get(memoryId); if (!existingMemory) { throw new Error(`Memory with ID ${memoryId} not found`); } const prevValue = existingMemory.payload.data; await this.vectorStore.delete(memoryId); const currentTime = new Date().toISOString(); await this.db.addHistory( memoryId, prevValue, null, "DELETE", existingMemory.payload.createdAt, currentTime, 1, existingMemory.payload.userId, existingMemory.payload.runId, existingMemory.payload.messageId, currentTime, ); return memoryId; } // Entity management methods async addEntity( entityId: string, label: string, type: string, userId?: string, runId?: string, ): Promise { return this.db.addEntity(entityId, label, type, userId, runId); } async updateEntity( entityId: string, label: string, type: string, userId?: string, runId?: string, ): Promise { return this.db.updateEntity(entityId, label, type, userId, runId); } async deleteEntity(entityId: string): Promise { return this.db.deleteEntity(entityId); } async getEntity(entityId: string): Promise { return this.db.getEntity(entityId); } async getAllEntities(options?: { userId?: string, runId?: string }): Promise { return this.db.getAllEntities(options); } async associateMemoryWithEntities(memoryId: string, entityIds: string[]): Promise { return this.db.associateMemoryWithEntities(memoryId, entityIds); } async getMemoryEntities(memoryId: string): Promise { return this.db.getMemoryEntities(memoryId); } // Utility method to safely convert chat options to search options // This ensures response-specific options are excluded while allowing reasoningCustomInstructions private extractSearchOptionsFromChatOptions(chatOptions: ChatMemoryOptions): SearchMemoryOptions { const { responseCustomInstructions, // Deliberately excluded - only for final response customInstructions, // Deliberately excluded - legacy response field temperature, // Deliberately excluded maxTokens, // Deliberately excluded ...searchOptions } = chatOptions; return searchOptions; } // Context filtering to reduce overwhelming information and improve query focus private async filterRelevantContext( query: string, results: MemoryItemWithContext[], maxResults: number = 50, // Increased from 40 to 50 focusMode: 'search' | 'response' = 'search' ): Promise { if (results.length <= maxResults) { return results; } // Separate high-value items that should be preserved const highValueItems: MemoryItemWithContext[] = []; const regularItems: MemoryItemWithContext[] = []; // Define score thresholds for high-value content const highScoreThreshold = 0.3; // High semantic similarity const maxScoreFound = Math.max(...results.map(r => r.score || 0)); const dynamicThreshold = Math.max(highScoreThreshold, maxScoreFound * 0.5); // Use 90% of max score if higher than 0.85 results.forEach(item => { const isRawMessage = item.type === 'raw_message'; const isPatternMessage = item.type === 'message_pattern_search'; const hasHighScore = (item.score || 0) >= dynamicThreshold; // Preserve high-value items automatically // Raw messages and pattern search results with high scores are especially valuable // as they contain specific dates, context, and conversation flow if ((isRawMessage && hasHighScore) || (isPatternMessage && hasHighScore) || (item.score || 0) >= 0.5) { highValueItems.push(item); } else { regularItems.push(item); } }); // If we have too many high-value items, just return the top ones if (highValueItems.length >= maxResults) { console.log(`🔍 ${highValueItems.length} high-value items found, using top ${maxResults} (preserving raw messages, pattern searches & high scores)`); return highValueItems .sort((a, b) => (b.score || 0) - (a.score || 0)) .slice(0, maxResults); } // Calculate how many regular items we can include after high-value items const remainingSlots = maxResults - highValueItems.length; if (remainingSlots <= 0) { return highValueItems; } console.log(`🔍 Filtering context: ${results.length} → ${maxResults} results (${highValueItems.length} high-value + ${remainingSlots} filtered, ${focusMode} mode)`); // Use LLM to intelligently filter remaining results based on query relevance const filterPrompt = `You are an intelligent context filter for a memory search system. Your task is to preserve the MOST VALUABLE memories while removing only truly irrelevant ones. ## Critical Query: "${query}" ## HIGH-VALUE ITEMS ALREADY PRESERVED (${highValueItems.length}): ${highValueItems.map((item, idx) => { const memory = item.memory || '[No content]'; const truncated = memory.substring(0, 150); const typeInfo = item.type ? ` [Type: ${item.type}]` : ''; return `✓ ${idx + 1}. [Score: ${item.score?.toFixed(3) || 'N/A'}] ${truncated}${memory.length > 150 ? '...' : ''}${typeInfo}`; }).join('\n')} ## REGULAR ITEMS TO FILTER (${regularItems.length} total, select ${remainingSlots}): ${regularItems.slice(0, 40).map((item, idx) => { const memory = item.memory || '[No content]'; const truncated = memory.substring(0, 200); // Increased length for better context const entityInfo = item.entityIds && item.entityIds.length > 0 ? ` [Entities: ${item.entityIds.join(', ')}]` : ''; const messageInfo = item.messages && item.messages.length > 0 ? ` [${item.messages.length} msgs]` : ''; const typeInfo = item.type ? ` [Type: ${item.type}]` : ''; return `${idx + 1}. [Score: ${item.score?.toFixed(3) || 'N/A'}] ${truncated}${memory.length > 200 ? '...' : ''}${entityInfo}${messageInfo}${typeInfo}`; }).join('\n')} ${regularItems.length > 40 ? `\n... and ${regularItems.length - 40} more items` : ''} ## ENHANCED FILTERING INSTRUCTIONS: **NOTE: High-value items (raw messages with high scores, pattern search results, very high scores ≥0.5) are ALREADY PRESERVED above. You only need to select the most valuable ${remainingSlots} items from the regular items below.** ${focusMode === 'search' ? ` **Search Agent Mode - Preserve Rich Context:** ✅ ALWAYS KEEP: - Memories with SPECIFIC DETAILS (dates, names, numbers, locations) - Memories with entity associations that could provide context - Memories that contain the EXACT answer or clues to the answer - Memories with vague terms that could be clarified (e.g., "last year", "home country", "that thing") - Memories with original messages that might contain additional specifics - Memories that could help convert vague references to concrete facts ❌ ONLY REMOVE: - Truly off-topic conversations - Duplicate information without additional context - Generic small talk that adds no factual value - Completely unrelated discussions **PRIORITY: Keep memories that might help find OBJECTIVE, SPECIFIC answers** ` : ` **Response Generation Mode - Focus on Answer Completion:** ✅ ALWAYS KEEP: - Memories that DIRECTLY answer the query with specifics - Memories with concrete facts, exact dates, specific names - Memories that provide context to convert vague terms to specific ones - Memories with detailed information that answers "what", "when", "where", "who" - Memories that help resolve ambiguity in the query ❌ ONLY REMOVE: - Background information that doesn't add to the specific answer - Redundant details already covered by higher-priority memories - General discussions that don't provide concrete answers **PRIORITY: Keep memories needed to provide the most COMPLETE, SPECIFIC answer** `} ## Special Focus Areas: - If query asks about TIME: Keep ALL memories with ANY temporal references - If query asks about IDENTITY/SPECIFICS: Keep memories that could clarify vague terms - If query asks about RELATIONSHIPS: Keep memories showing connections between entities - If query asks about EVENTS: Keep memories with context before/during/after events ## Enhanced Selection Criteria: 1. **Information Density**: Memories with more specific details rank higher 2. **Contextual Value**: Memories that could help clarify vague references 3. **Entity Richness**: Memories with entity associations for cross-referencing 4. **Message Completeness**: Memories with full conversation context 5. **Answer Completeness**: Memories that contribute to a complete, specific answer ## Task: Select the ${remainingSlots} most VALUABLE memory indices from the REGULAR ITEMS list above (1-${Math.min(regularItems.length, 40)}) that will help provide the most COMPLETE and SPECIFIC answer to the query. REMEMBER: - High-value items are already preserved automatically - You only need to select from the regular items list - It's better to keep a potentially useful memory than to remove one that might contain the key detail needed for a complete answer Respond with only the selected indices as a JSON array of numbers, ordered by value (most valuable first). Example: [3, 7, 1, 12, 5]`; try { const response = await this.llm.generateResponse( [{ role: "user", content: filterPrompt }], { type: "text" as const } ); // Parse the response to get selected indices const cleanResponse = (response as string).replace(/```json|```/g, '').trim(); let selectedIndices: number[]; try { selectedIndices = JSON.parse(cleanResponse); } catch (e) { // Fallback: extract numbers from response const numbers = cleanResponse.match(/\d+/g); selectedIndices = numbers ? numbers.map(n => parseInt(n)).slice(0, maxResults) : []; } // Validate and filter indices for regular items only const validIndices = selectedIndices .filter(idx => idx >= 1 && idx <= Math.min(regularItems.length, 40)) .slice(0, remainingSlots); if (validIndices.length > 0) { const selectedRegularItems = validIndices.map(idx => regularItems[idx - 1]).filter(Boolean); const finalResults = [...highValueItems, ...selectedRegularItems]; console.log(` ✅ Combined ${highValueItems.length} high-value + ${selectedRegularItems.length} filtered = ${finalResults.length} total memories`); return finalResults; } } catch (error) { console.error("Context filtering failed, using fallback:", error); } // Fallback: use top scored results console.log(` ⚠️ Using fallback: top ${maxResults} by score`); return results .sort((a, b) => (b.score || 0) - (a.score || 0)) .slice(0, maxResults); } async naiveSearch( query: string, config: { userId?: string; agentId?: string; runId?: string; limit?: number; filters?: SearchFilters; }, ): Promise { const { userId, agentId, runId, limit = 100, filters = {}, } = config; if (userId) filters.userId = userId; if (agentId) filters.agentId = agentId; if (runId) filters.runId = runId; if (!filters.userId && !filters.agentId && !filters.runId) { throw new Error( "One of the filters: userId, agentId or runId is required!", ); } // Simple semantic search on vector store only const queryEmbedding = await this.embedder.embed(query); const memories = await this.vectorStore.search( queryEmbedding, limit, filters, ); const excludedKeys = new Set([ "userId", "agentId", "runId", "hash", "data", "createdAt", "updatedAt", "originalMessages", ]); // Process memories into basic result format const results = await Promise.all(memories.map(async (mem) => { // Get associated entities const entityIds = await this.db.getMemoryEntities(mem.id); // Parse original messages if available let originalMessages: Message[] | undefined; if (mem.payload.originalMessages) { try { originalMessages = JSON.parse(mem.payload.originalMessages); } catch (e) { console.warn("Failed to parse original messages:", e); } } return { id: mem.id, memory: mem.payload.data, hash: mem.payload.hash, createdAt: mem.payload.createdAt, updatedAt: mem.payload.updatedAt, score: mem.score, entityIds, ...(originalMessages && { messages: originalMessages }), metadata: Object.entries(mem.payload) .filter(([key]) => !excludedKeys.has(key)) .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}), ...(mem.payload.userId && { userId: mem.payload.userId }), ...(mem.payload.agentId && { agentId: mem.payload.agentId }), ...(mem.payload.runId && { runId: mem.payload.runId }), }; })); return { results }; } async naiveChat( query: string, config: { userId?: string; agentId?: string; runId?: string; limit?: number; filters?: SearchFilters; temperature?: number; maxTokens?: number; }, ): Promise { const startTime = Date.now(); const { userId, agentId, runId, limit = 10, filters = {}, temperature = 0.7, maxTokens = 500, } = config; // Simple search without agents const searchResult = await this.naiveSearch(query, { userId, agentId, runId, limit, filters }); // Prepare simple context from search results const contextInfo = this.prepareNaiveChatContext(query, searchResult.results); // Generate simple response const chatPrompt = this.buildNaiveChatPrompt(query, contextInfo); try { const response = await this.llm.generateResponse( [{ role: "user", content: chatPrompt }], { type: "text" as const, }, ); const processingTime = Date.now() - startTime; return { response: response as string, sources: searchResult.results, metadata: { searchQuery: query, sourcesCount: searchResult.results.length, responseTokens: (response as string).split(' ').length, processingTimeMs: processingTime, isNaiveMode: true, }, }; } catch (error) { console.error("Error generating naive chat response:", error); const processingTime = Date.now() - startTime; return { response: "I'm sorry, I encountered an error while processing your request. Please try again.", sources: searchResult.results, metadata: { searchQuery: query, sourcesCount: searchResult.results.length, processingTimeMs: processingTime, isNaiveMode: true, }, }; } } private prepareNaiveChatContext(query: string, memories: any[]): string { if (memories.length === 0) { return "No relevant information found in memory."; } let context = "## Relevant Information from Memory:\n\n"; memories.forEach((memory, index) => { context += `### Memory ${index + 1} (Score: ${memory.score?.toFixed(3) || 'N/A'})\n`; context += `**Content:** ${memory.memory}\n`; if (memory.entityIds && memory.entityIds.length > 0) { context += `**Entities:** ${memory.entityIds.join(', ')}\n`; } if (memory.messages && memory.messages.length > 0) { context += `**Original Messages:**\n`; memory.messages.forEach((msg: Message, msgIdx: number) => { const content = typeof msg.content === 'string' ? msg.content : '[multimodal content]'; context += ` ${msgIdx + 1}. [${msg.role}]: ${content}\n`; }); } context += "\n"; }); return context; } private buildNaiveChatPrompt(query: string, contextInfo: string): string { return `You are an AI assistant with access to a user's memory system. Answer the user's question based on the available information. ## User Query ${query} ## Available Context ${contextInfo} ## Instructions - Provide a helpful and accurate response based on the available memory information - If no relevant information is found, acknowledge this and provide a general response - Be concise but informative - Use the specific details from the memories when available Response:`; } }