UNPKG

mongodb-memory-bank-mcp

Version:

FIXED: MongoDB Memory Bank MCP with bulletproof error handling, smart operations, and session state management. Eliminates [object Object] errors and user confusion.

554 lines (553 loc) 22.7 kB
import { MongoDBConnection } from '../connection/mongodb-connection.js'; import { getCollectionNames, mongoConfig } from '../../../main/config/mongodb-config.js'; import { VoyageEmbeddingService } from '../../ai/voyage-embedding-service.js'; import { ContentRoutingService } from '../../../shared/services/content-routing-service.js'; // Removed unused template imports export class MongoDBMemoryRepository { db; collection; embeddingService; constructor() { // Initialize lazily to avoid connection issues this.embeddingService = new VoyageEmbeddingService(); } async ensureConnection() { if (!this.db) { this.db = await MongoDBConnection.getInstance().getDatabase(); this.collection = this.db.collection(getCollectionNames().memories); } } async store(memory) { await this.ensureConnection(); // Generate embedding for Atlas deployments let contentVector = memory.contentVector; console.log('🔍 Vector Storage Debug:', { embeddingServiceAvailable: this.embeddingService.isAvailable(), hasExistingVector: !!contentVector, contentLength: memory.content.length, fileName: memory.fileName }); if (this.embeddingService.isAvailable() && !contentVector) { console.log('🚀 Generating embedding for:', memory.fileName); const embeddingResult = await this.embeddingService.generateEmbedding(memory.content); if (embeddingResult) { contentVector = embeddingResult.embedding; console.log('✅ Embedding generated successfully:', { dimensions: contentVector.length, tokens: embeddingResult.tokens, fileName: memory.fileName }); } else { console.log('❌ Embedding generation failed for:', memory.fileName); } } else { console.log('⏭️ Skipping embedding generation:', { isAvailable: this.embeddingService.isAvailable(), hasExisting: !!contentVector }); } const doc = { projectName: memory.projectName, fileName: memory.fileName, content: memory.content, tags: memory.tags, lastModified: new Date(), wordCount: this.countWords(memory.content), contentVector, // This should now be populated if everything works summary: memory.summary }; console.log('💾 Storing memory with vector:', { fileName: memory.fileName, hasVector: !!doc.contentVector, vectorDimensions: doc.contentVector?.length || 0 }); const result = await this.collection.replaceOne({ projectName: memory.projectName, fileName: memory.fileName }, doc, { upsert: true }); // Get the inserted/updated document with ID const savedDoc = await this.collection.findOne({ projectName: memory.projectName, fileName: memory.fileName }); return this.documentToMemory(savedDoc); } async load(projectName, fileName) { await this.ensureConnection(); const doc = await this.collection.findOne({ projectName, fileName }); return doc ? this.documentToMemory(doc) : null; } async update(projectName, fileName, content) { await this.ensureConnection(); // 🎯 CONTENT ROUTING: Implement intelligent content routing to maintain 6-file structure const existingFiles = await this.listFiles(projectName); const existingFileNames = existingFiles.map(f => f.fileName); // Analyze content and determine routing const routingResult = ContentRoutingService.routeContent(fileName, content, existingFileNames); console.log(`[CONTENT-ROUTING] ${fileName}${routingResult.targetFile} (${routingResult.confidence}% confidence: ${routingResult.reasoning})`); // If routing to a different file, merge content with target file if (routingResult.targetFile !== fileName && routingResult.shouldMerge) { const targetMemory = await this.load(projectName, routingResult.targetFile); if (targetMemory) { const mergedContent = ContentRoutingService.mergeContent(targetMemory.content, content, routingResult.mergeStrategy); // Update the target file with merged content const updateDoc = { $set: { content: mergedContent, lastModified: new Date(), wordCount: this.countWords(mergedContent), tags: [...(targetMemory.tags || []), 'auto-merged', 'content-routed'] } }; const result = await this.collection.findOneAndUpdate({ projectName, fileName: routingResult.targetFile }, updateDoc, { returnDocument: 'after' }); console.log(`[CONTENT-ROUTING] Successfully merged content into ${routingResult.targetFile}`); return result ? this.documentToMemory(result) : null; } } // Standard update for direct file updates const updateDoc = { $set: { content, lastModified: new Date(), wordCount: this.countWords(content) } }; const result = await this.collection.findOneAndUpdate({ projectName, fileName }, updateDoc, { returnDocument: 'after' }); return result ? this.documentToMemory(result) : null; } async delete(projectName, fileName) { await this.ensureConnection(); const result = await this.collection.deleteOne({ projectName, fileName }); return result.deletedCount > 0; } async listByProject(projectName) { await this.ensureConnection(); const docs = await this.collection .find({ projectName }) .sort({ lastModified: -1 }) .toArray(); return docs.map(doc => this.documentToMemory(doc)); } async listFiles(projectName) { await this.ensureConnection(); const docs = await this.collection .find({ projectName }) .sort({ lastModified: -1 }) .toArray(); return docs.map(doc => this.documentToMemory(doc)); } async listAll() { await this.ensureConnection(); const docs = await this.collection .find({}) .sort({ lastModified: -1 }) .toArray(); return docs.map(doc => this.documentToMemory(doc)); } async findByFileName(projectName, fileName) { await this.ensureConnection(); const doc = await this.collection.findOne({ projectName, fileName }); return doc ? this.documentToMemory(doc) : null; } async search(params) { await this.ensureConnection(); const { query, projectName, tags, limit = 10, useSemanticSearch = false } = params; // Use semantic search for Atlas, text search for Community if (mongoConfig.isAtlas && useSemanticSearch && mongoConfig.enableVectorSearch) { return this.semanticSearch(params); } else { return this.textSearch(params); } } async findRelated(projectName, fileName, limit = 5) { const memory = await this.load(projectName, fileName); if (!memory) return []; // Use tags and content similarity to find related memories const pipeline = [ { $match: { $and: [ { projectName }, { fileName: { $ne: fileName } }, { tags: { $in: memory.tags } } ] } }, { $addFields: { score: { $size: { $setIntersection: ['$tags', memory.tags] } } } }, { $sort: { score: -1, lastModified: -1 } }, { $limit: limit } ]; const docs = await this.collection.aggregate(pipeline).toArray(); return docs.map(doc => ({ ...this.documentToMemory(doc), score: doc.score, relevance: 'tag-similarity' })); } async getProjectStats(projectName) { await this.ensureConnection(); const pipeline = [ { $match: { projectName } }, { $group: { _id: null, totalMemories: { $sum: 1 }, totalWords: { $sum: '$wordCount' }, allTags: { $push: '$tags' }, lastActivity: { $max: '$lastModified' } } }, { $project: { totalMemories: 1, totalWords: 1, lastActivity: 1, commonTags: { $reduce: { input: '$allTags', initialValue: [], in: { $setUnion: ['$$value', '$$this'] } } } } } ]; const result = await this.collection.aggregate(pipeline).toArray(); const stats = result[0]; return { totalMemories: stats?.totalMemories || 0, totalWords: stats?.totalWords || 0, commonTags: stats?.commonTags || [], lastActivity: stats?.lastActivity || new Date() }; } async textSearch(params) { const { query, projectName, tags, limit = 10 } = params; const searchQuery = { $text: { $search: query } }; if (projectName) { searchQuery.projectName = projectName; } if (tags && tags.length > 0) { searchQuery.tags = { $in: tags }; } const docs = await this.collection .find(searchQuery) .sort({ score: { $meta: 'textScore' } }) .limit(limit) .toArray(); return docs.map(doc => ({ ...this.documentToMemory(doc), score: 1.0, // MongoDB text search score not easily accessible in this context relevance: 'text-match' })); } async semanticSearch(params) { const { query, projectName, tags, limit = 10 } = params; console.log(`[🔥 GOLDEN FEATURE] Starting MongoDB $rankFusion hybrid search for: "${query}"`); // Generate query embedding const queryVector = await this.embeddingService.generateQueryEmbedding(query); if (!queryVector) { console.log(`[⚠️ EMBEDDING FAILED] Falling back to text search`); return this.textSearch(params); } // 🚀 REVOLUTIONARY: Check if MongoDB supports $rankFusion (8.1+) const supportsRankFusion = await this.checkRankFusionSupport(); if (supportsRankFusion) { console.log(`[🎯 $RANKFUSION] Using MongoDB's revolutionary hybrid search with reciprocal rank fusion`); return this.hybridRankFusionSearch(query, queryVector, projectName, tags, limit); } else { console.log(`[⚠️ FALLBACK] MongoDB version doesn't support $rankFusion, using vector search only`); return this.vectorOnlySearch(query, queryVector, projectName, tags, limit); } } /** * 🔥 GOLDEN FEATURE: MongoDB's Revolutionary $rankFusion Hybrid Search * This is the crown jewel - combining vector + text search with reciprocal rank fusion */ async hybridRankFusionSearch(query, queryVector, projectName, tags, limit = 10) { try { // 🎯 Build the revolutionary $rankFusion pipeline const pipeline = [ { $rankFusion: { input: { pipelines: { // 🎯 Vector Search Pipeline - Semantic Understanding vectorPipeline: [ { $vectorSearch: { index: 'vector_index', path: 'contentVector', queryVector: queryVector, numCandidates: Math.min(limit * 10, 100), limit: limit * 2 } }, ...(projectName ? [{ $match: { projectName } }] : []), ...(tags && tags.length > 0 ? [{ $match: { tags: { $in: tags } } }] : []) ], // 🎯 Text Search Pipeline - Exact Keyword Matching textPipeline: [ { $search: { index: 'default', text: { query: query, path: ['content', 'fileName', 'tags'] } } }, ...(projectName ? [{ $match: { projectName } }] : []), ...(tags && tags.length > 0 ? [{ $match: { tags: { $in: tags } } }] : []), { $limit: limit * 2 } ] } }, // 🎯 Weighted Reciprocal Rank Fusion - The Magic Formula combination: { weights: { vectorPipeline: 0.6, // Favor semantic understanding textPipeline: 0.4 // But include exact matches } }, scoreDetails: true } }, { $addFields: { score: { $meta: "scoreDetails" }, relevance: "hybrid-rankfusion" } }, { $limit: limit } ]; console.log(`[🎉 $RANKFUSION] Executing hybrid search with weighted reciprocal rank fusion`); const docs = await this.collection.aggregate(pipeline).toArray(); console.log(`[📊 HYBRID POWER] Found ${docs.length} results using MongoDB's unique $rankFusion algorithm`); return docs.map(doc => ({ ...this.documentToMemory(doc), score: doc.score || 1.0, relevance: 'hybrid-rankfusion' })); } catch (error) { console.error(`[❌ $RANKFUSION ERROR] Hybrid search failed:`, error); // Fallback to vector-only search return this.vectorOnlySearch(query, queryVector, projectName, tags, limit); } } /** * 🎯 Vector-Only Search - Fallback for older MongoDB versions */ async vectorOnlySearch(query, queryVector, projectName, tags, limit = 10) { try { const pipeline = [ { $vectorSearch: { index: 'vector_index', path: 'contentVector', queryVector: queryVector, numCandidates: Math.min(limit * 10, 100), limit: limit } } ]; // Add filters const matchStage = {}; if (projectName) { matchStage.projectName = projectName; } if (tags && tags.length > 0) { matchStage.tags = { $in: tags }; } if (Object.keys(matchStage).length > 0) { pipeline.push({ $match: matchStage }); } pipeline.push({ $limit: limit }); const docs = await this.collection.aggregate(pipeline).toArray(); console.log(`[✅ VECTOR SUCCESS] Found ${docs.length} vector-only results`); return docs.map(doc => ({ ...this.documentToMemory(doc), score: doc.score || 1.0, relevance: 'vector-match' })); } catch (error) { console.error(`[❌ VECTOR ERROR] Vector search failed:`, error); // Final fallback to text search return this.textSearch({ query, projectName, tags, limit }); } } /** * 🔍 Check if MongoDB supports $rankFusion (requires 8.1+) */ async checkRankFusionSupport() { try { await this.ensureConnection(); const adminDb = this.db.admin(); const buildInfo = await adminDb.buildInfo(); // Parse version string (e.g., "8.1.0" or "8.1.0-rc1") const versionMatch = buildInfo.version.match(/^(\d+)\.(\d+)/); if (!versionMatch) { console.log(`[⚠️ VERSION] Could not parse MongoDB version: ${buildInfo.version}`); return false; } const major = parseInt(versionMatch[1]); const minor = parseInt(versionMatch[2]); // $rankFusion requires MongoDB 8.1+ const supportsRankFusion = major > 8 || (major === 8 && minor >= 1); console.log(`[📊 VERSION CHECK] MongoDB ${buildInfo.version} - $rankFusion support: ${supportsRankFusion}`); return supportsRankFusion; } catch (error) { console.log(`[⚠️ VERSION CHECK] Failed to check MongoDB version, assuming no $rankFusion support:`, error instanceof Error ? error.message : String(error)); return false; } } documentToMemory(doc) { return { id: doc._id?.toString(), projectName: doc.projectName, fileName: doc.fileName, content: doc.content, tags: doc.tags, lastModified: doc.lastModified, wordCount: doc.wordCount, contentVector: doc.contentVector, summary: doc.summary }; } countWords(content) { return content.trim().split(/\s+/).length; } // Clean, AI-optimized repository - no unused template code /** * Search memories by type with structure awareness */ async searchByType(memoryType, projectName, limit = 10) { await this.ensureConnection(); const docs = await this.collection .find({ projectName, memoryType }) .sort({ lastModified: -1 }) .limit(limit) .toArray(); return docs.map(doc => this.documentToMemory(doc)); } /** * 🎯 AI-OPTIMIZED: Search memories by tags for better context discovery */ async searchByTags(tags, projectName, limit = 10) { await this.ensureConnection(); const filter = { tags: { $in: tags } }; if (projectName) { filter.projectName = projectName; } const documents = await this.collection .find(filter) .limit(limit) .sort({ lastModified: -1 }) .toArray(); return documents.map(doc => this.documentToMemory(doc)); } /** * Get related memories based on relationships */ async getRelatedMemories(fileName, projectName, limit = 5) { await this.ensureConnection(); const memory = await this.load(projectName, fileName); if (!memory || !memory.relationships) { return this.findRelated(projectName, fileName, limit); } // Get memories based on relationships const relatedFileNames = [ ...memory.relationships.dependsOn, ...memory.relationships.influences, ...memory.relationships.relatedTo ]; if (relatedFileNames.length === 0) { return this.findRelated(projectName, fileName, limit); } const docs = await this.collection .find({ projectName, fileName: { $in: relatedFileNames } }) .sort({ lastModified: -1 }) .limit(limit) .toArray(); return docs.map(doc => ({ ...this.documentToMemory(doc), score: 1.0, relevance: 'relationship-based' })); } // Removed duplicate template methods /** * Get template usage statistics for a project */ async getTemplateStats(projectName) { await this.ensureConnection(); const pipeline = [ { $match: { projectName } }, { $group: { _id: '$memoryType', count: { $sum: 1 } } } ]; const results = await this.collection.aggregate(pipeline).toArray(); const stats = {}; results.forEach(result => { if (result._id) { stats[result._id] = result.count; } }); return stats; } // 🔄 BACKWARD COMPATIBILITY: Minimal implementations for existing code async storeStructured(memory) { // Simple implementation - just store as regular memory return this.store(memory); } async validateTemplate(content, memoryType, fileName) { // Always return valid - no template validation needed return { isValid: true, errors: [], warnings: [], suggestions: [] }; } generateTemplateContent(memoryType, projectName) { // Return empty template - not used in practice return `# ${memoryType}\n\nContent for ${projectName || 'project'}`; } async getMemoriesByHierarchy(projectName, level) { // Simple implementation - return memories by type return this.listByProject(projectName); } }