@codai/memorai-mcp
Version:
MemorAI CBD-based MCP Server - High-Performance Vector Memory System
467 lines (465 loc) • 22.9 kB
JavaScript
/**
* MemorAI MCP v9.1.0 - Memory Recommendation Engine
*
* Provides intelligent recommendations for memory management optimization,
* including review suggestions, new memory creation, relationship formation,
* and cleanup actions.
*
* Part of Phase 2: Enterprise Features Implementation
*/
/**
* Advanced recommendation engine for intelligent memory management
*/
export class MemoryRecommendationEngine {
openaiClient;
memories;
recommendationCache;
constructor(openaiClient, memories) {
this.openaiClient = openaiClient;
this.memories = memories || new Map();
this.recommendationCache = new Map();
}
/**
* Recommend memories to review based on usage patterns and importance
*/
async recommendReview(agentId) {
const agentMemories = Array.from(this.memories.values())
.filter(memory => memory.metadata.agentId === agentId);
const recommendations = [];
// 1. Recommend reviewing isolated memories (no relationships)
const isolatedMemories = agentMemories.filter(memory => memory.relationships.length === 0);
for (const memory of isolatedMemories.slice(0, 5)) {
recommendations.push({
type: 'review',
memoryKey: memory.structuredKey,
priority: 'medium',
reasoning: 'This memory has no relationships with other memories, which may indicate it needs better integration into your knowledge graph.',
actionSuggestion: 'Review this memory and consider adding relationships or additional context to improve its connectivity.',
confidence: 0.8,
estimatedImpact: 'Improved knowledge connectivity and easier discovery of related information'
});
}
// 2. Recommend reviewing high-importance, old memories
const oldImportantMemories = agentMemories
.filter(memory => {
const daysSinceCreation = (Date.now() - new Date(memory.metadata.timestamp).getTime()) / (1000 * 60 * 60 * 24);
return memory.metadata.importance > 0.7 && daysSinceCreation > 30;
})
.sort((a, b) => b.metadata.importance - a.metadata.importance);
for (const memory of oldImportantMemories.slice(0, 3)) {
const daysSince = Math.floor((Date.now() - new Date(memory.metadata.timestamp).getTime()) / (1000 * 60 * 60 * 24));
recommendations.push({
type: 'review',
memoryKey: memory.structuredKey,
priority: 'high',
reasoning: `This high-importance memory (${(memory.metadata.importance * 100).toFixed(0)}%) was created ${daysSince} days ago and may need updating.`,
actionSuggestion: 'Review this memory for accuracy and consider updating it with new information or insights.',
confidence: 0.9,
estimatedImpact: 'Ensures important knowledge remains current and accurate'
});
}
// 3. Recommend reviewing memories with weak relationships
const weaklyConnectedMemories = agentMemories
.filter(memory => {
const avgStrength = memory.relationships.length > 0
? memory.relationships.reduce((sum, rel) => sum + rel.strength, 0) / memory.relationships.length
: 0;
return memory.relationships.length > 0 && avgStrength < 0.4;
});
for (const memory of weaklyConnectedMemories.slice(0, 2)) {
recommendations.push({
type: 'review',
memoryKey: memory.structuredKey,
priority: 'low',
reasoning: 'This memory has relationships, but they are generally weak, suggesting the connections might need strengthening or reevaluation.',
actionSuggestion: 'Review the relationships of this memory and consider strengthening relevant connections or removing inappropriate ones.',
confidence: 0.7,
estimatedImpact: 'Better relationship quality and more meaningful knowledge connections'
});
}
return recommendations;
}
/**
* Suggest new memories to create based on knowledge gaps
*/
async suggestNewMemories(agentId) {
const agentMemories = Array.from(this.memories.values())
.filter(memory => memory.metadata.agentId === agentId);
if (agentMemories.length === 0) {
return [];
}
const suggestions = [];
// Analyze existing content to identify patterns and gaps
const contentAnalysis = await this.analyzeContentForGaps(agentMemories);
// 1. Suggest memories for underrepresented topics
for (const topic of contentAnalysis.underrepresentedTopics) {
suggestions.push({
suggestedContent: `Consider creating a comprehensive overview of ${topic}, including key concepts, best practices, and common challenges.`,
category: 'knowledge_expansion',
priority: 'medium',
reasoning: `The topic "${topic}" appears frequently in your existing memories but lacks a comprehensive overview or guide.`,
relatedMemories: agentMemories
.filter(memory => memory.content.toLowerCase().includes(topic.toLowerCase()))
.slice(0, 3)
.map(memory => memory.structuredKey)
});
}
// 2. Suggest follow-up memories for incomplete topics
for (const incompleteArea of contentAnalysis.incompleteAreas) {
suggestions.push({
suggestedContent: `Create a detailed exploration of ${incompleteArea.topic}, focusing on ${incompleteArea.missingAspects.join(', ')}.`,
category: 'completion',
priority: 'high',
reasoning: `Your knowledge of ${incompleteArea.topic} seems incomplete. Adding information about ${incompleteArea.missingAspects.join(', ')} would provide better coverage.`,
relatedMemories: incompleteArea.relatedMemories
});
}
// 3. Suggest summary memories for complex topics
const complexTopics = await this.identifyComplexTopics(agentMemories);
for (const topic of complexTopics.slice(0, 2)) {
suggestions.push({
suggestedContent: `Create a summary or synthesis of your knowledge about ${topic.name}, connecting the key insights from your existing memories.`,
category: 'synthesis',
priority: 'medium',
reasoning: `You have multiple memories about ${topic.name} (${topic.memoryCount} memories) but no summary that ties them together.`,
relatedMemories: topic.memoryKeys
});
}
// 4. Suggest practical application memories
const theoreticalConcepts = agentMemories.filter(memory => memory.metadata.entityType === 'concept' &&
!agentMemories.some(m => m.metadata.entityType === 'example' &&
this.areMemoriesRelated(memory, m)));
for (const concept of theoreticalConcepts.slice(0, 2)) {
suggestions.push({
suggestedContent: `Create practical examples or use cases demonstrating how to apply the concepts from: "${concept.content.substring(0, 100)}..."`,
category: 'practical_application',
priority: 'medium',
reasoning: 'This theoretical concept could benefit from practical examples to make it more actionable.',
relatedMemories: [concept.structuredKey]
});
}
return suggestions.slice(0, 8); // Return top 8 suggestions
}
/**
* Recommend memory relationships to create
*/
async recommendRelationships(agentId) {
const agentMemories = Array.from(this.memories.values())
.filter(memory => memory.metadata.agentId === agentId);
const suggestions = [];
// 1. Find memories with similar content that aren't connected
for (const memory of agentMemories) {
const similarMemories = await this.findSimilarUnconnectedMemories(memory, agentMemories);
for (const similar of similarMemories.slice(0, 2)) {
suggestions.push({
sourceMemoryKey: memory.structuredKey,
targetMemoryKey: similar.memory.structuredKey,
relationshipType: 'related',
confidence: similar.similarity,
reasoning: `These memories share similar content (${(similar.similarity * 100).toFixed(1)}% similarity) and would benefit from being connected.`
});
}
}
// 2. Find sequential or dependency relationships
const sequentialRelationships = await this.findSequentialRelationships(agentMemories);
suggestions.push(...sequentialRelationships);
// 3. Find contradictory memories that should be explicitly linked
const contradictoryRelationships = await this.findContradictoryRelationships(agentMemories);
suggestions.push(...contradictoryRelationships);
// 4. Find explanatory relationships
const explanatoryRelationships = await this.findExplanatoryRelationships(agentMemories);
suggestions.push(...explanatoryRelationships);
// Sort by confidence and return top suggestions
return suggestions
.sort((a, b) => b.confidence - a.confidence)
.slice(0, 10);
}
/**
* Suggest memory cleanup actions
*/
async suggestCleanup(agentId) {
const agentMemories = Array.from(this.memories.values())
.filter(memory => memory.metadata.agentId === agentId);
const suggestions = [];
// 1. Identify duplicate memories
const duplicateGroups = await this.findDuplicateMemories(agentMemories);
for (const group of duplicateGroups) {
suggestions.push({
type: 'merge',
memoryKeys: group.memoryKeys,
reasoning: `These memories contain very similar content (${(group.similarity * 100).toFixed(1)}% similarity) and could be merged to reduce redundancy.`,
riskLevel: group.similarity > 0.9 ? 'low' : 'medium'
});
}
// 2. Identify outdated memories
const outdatedMemories = agentMemories.filter(memory => {
const daysSinceCreation = (Date.now() - new Date(memory.metadata.timestamp).getTime()) / (1000 * 60 * 60 * 24);
return daysSinceCreation > 365 && memory.metadata.importance < 0.3 && memory.relationships.length === 0;
});
if (outdatedMemories.length > 0) {
suggestions.push({
type: 'archive',
memoryKeys: outdatedMemories.slice(0, 10).map(m => m.structuredKey),
reasoning: 'These memories are over a year old, have low importance scores, and no relationships. Consider archiving them.',
riskLevel: 'low'
});
}
// 3. Identify very short or low-quality memories
const lowQualityMemories = agentMemories.filter(memory => {
return memory.content.length < 20 ||
memory.content.split(' ').length < 5 ||
memory.metadata.importance === 0;
});
if (lowQualityMemories.length > 0) {
suggestions.push({
type: 'update',
memoryKeys: lowQualityMemories.slice(0, 5).map(m => m.structuredKey),
reasoning: 'These memories are very short or lack sufficient detail. Consider expanding them or removing if no longer relevant.',
riskLevel: 'medium'
});
}
// 4. Identify broken relationships
const brokenRelationships = await this.findBrokenRelationships(agentMemories);
if (brokenRelationships.length > 0) {
suggestions.push({
type: 'update',
memoryKeys: brokenRelationships,
reasoning: 'These memories have relationships pointing to non-existent memories. The relationships should be cleaned up.',
riskLevel: 'low'
});
}
return suggestions;
}
// Private helper methods
async analyzeContentForGaps(memories) {
// Extract topics from existing memories
const topicFrequency = {};
const contentSample = memories.slice(0, 20).map(m => m.content).join('\n\n');
if (!this.openaiClient) {
// Fallback analysis without AI
return {
underrepresentedTopics: [],
incompleteAreas: []
};
}
try {
const response = await this.openaiClient.chat.completions.create({
model: 'gpt-4',
messages: [{
role: 'user',
content: `Analyze this memory collection and identify:
1. Topics that are mentioned but underrepresented (need more coverage)
2. Areas where knowledge seems incomplete
Content sample: ${contentSample.substring(0, 3000)}
Respond in JSON format:
{
"underrepresentedTopics": ["topic1", "topic2"],
"incompleteAreas": [
{
"topic": "topic_name",
"missingAspects": ["aspect1", "aspect2"]
}
]
}`
}],
max_tokens: 500
});
const analysis = JSON.parse(response.choices[0]?.message?.content || '{}');
// Add related memories for incomplete areas
const incompleteAreas = analysis.incompleteAreas?.map((area) => ({
...area,
relatedMemories: memories
.filter(m => m.content.toLowerCase().includes(area.topic.toLowerCase()))
.slice(0, 3)
.map(m => m.structuredKey)
})) || [];
return {
underrepresentedTopics: analysis.underrepresentedTopics || [],
incompleteAreas
};
}
catch (error) {
console.error('Error analyzing content gaps:', error);
return {
underrepresentedTopics: [],
incompleteAreas: []
};
}
}
async identifyComplexTopics(memories) {
const topicGroups = {};
// Group memories by project and tags
for (const memory of memories) {
const topics = [
memory.metadata.project,
...(memory.metadata.tags || []),
memory.metadata.entityType
].filter(Boolean);
for (const topic of topics) {
if (!topicGroups[topic]) {
topicGroups[topic] = [];
}
topicGroups[topic].push(memory.structuredKey);
}
}
// Find topics with multiple memories but no summary
return Object.entries(topicGroups)
.filter(([topic, keys]) => keys.length >= 3)
.map(([topic, keys]) => ({
name: topic,
memoryCount: keys.length,
memoryKeys: keys.slice(0, 5) // Limit to 5 for readability
}))
.sort((a, b) => b.memoryCount - a.memoryCount);
}
areMemoriesRelated(memory1, memory2) {
return memory1.relationships.some((rel) => rel.targetKey === memory2.structuredKey) ||
memory2.relationships.some((rel) => rel.targetKey === memory1.structuredKey);
}
async findSimilarUnconnectedMemories(targetMemory, allMemories) {
const similar = [];
for (const memory of allMemories) {
if (memory.id === targetMemory.id)
continue;
// Skip if already connected
if (this.areMemoriesRelated(targetMemory, memory))
continue;
const similarity = await this.calculateContentSimilarity(targetMemory.content, memory.content);
if (similarity > 0.6) {
similar.push({ memory, similarity });
}
}
return similar.sort((a, b) => b.similarity - a.similarity);
}
async calculateContentSimilarity(content1, content2) {
// Simple Jaccard similarity
const words1 = new Set(content1.toLowerCase().split(/\s+/));
const words2 = new Set(content2.toLowerCase().split(/\s+/));
const intersection = new Set([...words1].filter(x => words2.has(x)));
const union = new Set([...words1, ...words2]);
return intersection.size / union.size;
}
async findSequentialRelationships(memories) {
const suggestions = [];
// Look for memories that might have sequential relationships
const sortedMemories = memories.sort((a, b) => new Date(a.metadata.timestamp).getTime() - new Date(b.metadata.timestamp).getTime());
for (let i = 0; i < sortedMemories.length - 1; i++) {
const current = sortedMemories[i];
const next = sortedMemories[i + 1];
// Ensure both memories exist
if (!current || !next)
continue;
// Check if they're in the same project and close in time
if (current.metadata?.project === next.metadata?.project) {
const currentTime = current.metadata?.timestamp ? new Date(current.metadata.timestamp).getTime() : 0;
const nextTime = next.metadata?.timestamp ? new Date(next.metadata.timestamp).getTime() : 0;
const timeDiff = nextTime - currentTime;
const daysDiff = timeDiff / (1000 * 60 * 60 * 24);
if (daysDiff <= 7 && !this.areMemoriesRelated(current, next)) {
suggestions.push({
sourceMemoryKey: current.structuredKey,
targetMemoryKey: next.structuredKey,
relationshipType: 'follows',
confidence: 0.7,
reasoning: `These memories were created ${daysDiff.toFixed(1)} days apart in the same project, suggesting a sequential relationship.`
});
}
}
}
return suggestions.slice(0, 5);
}
async findContradictoryRelationships(memories) {
const suggestions = [];
// Use AI to identify potentially contradictory content
for (const memory1 of memories.slice(0, 10)) { // Limit for performance
for (const memory2 of memories) {
if (memory1.id >= memory2.id)
continue;
if (this.areMemoriesRelated(memory1, memory2))
continue;
// Simple heuristic: look for opposing words
const opposingWords = ['not', 'never', 'don\'t', 'can\'t', 'shouldn\'t', 'avoid', 'wrong', 'incorrect'];
const content1Lower = memory1.content.toLowerCase();
const content2Lower = memory2.content.toLowerCase();
const hasOpposingWords = opposingWords.some(word => (content1Lower.includes(word) && !content2Lower.includes(word)) ||
(!content1Lower.includes(word) && content2Lower.includes(word)));
const topicSimilarity = await this.calculateContentSimilarity(memory1.content, memory2.content);
if (hasOpposingWords && topicSimilarity > 0.3) {
suggestions.push({
sourceMemoryKey: memory1.structuredKey,
targetMemoryKey: memory2.structuredKey,
relationshipType: 'contradicts',
confidence: 0.6,
reasoning: 'These memories seem to discuss similar topics but with potentially contradictory viewpoints.'
});
}
}
}
return suggestions.slice(0, 3);
}
async findExplanatoryRelationships(memories) {
const suggestions = [];
// Find concept-example pairs
const concepts = memories.filter(m => m.metadata.entityType === 'concept');
const examples = memories.filter(m => m.metadata.entityType === 'example');
for (const concept of concepts) {
for (const example of examples) {
if (this.areMemoriesRelated(concept, example))
continue;
const similarity = await this.calculateContentSimilarity(concept.content, example.content);
if (similarity > 0.4) {
suggestions.push({
sourceMemoryKey: example.structuredKey,
targetMemoryKey: concept.structuredKey,
relationshipType: 'exemplifies',
confidence: similarity,
reasoning: `This example appears to demonstrate the concept described in the target memory.`
});
}
}
}
return suggestions.slice(0, 5);
}
async findDuplicateMemories(memories) {
const duplicateGroups = [];
const processed = new Set();
for (const memory of memories) {
if (processed.has(memory.id))
continue;
const duplicates = [memory];
let totalSimilarity = 0;
let comparisons = 0;
for (const other of memories) {
if (memory.id === other.id || processed.has(other.id))
continue;
const similarity = await this.calculateContentSimilarity(memory.content, other.content);
if (similarity > 0.8) {
duplicates.push(other);
totalSimilarity += similarity;
comparisons++;
processed.add(other.id);
}
}
if (duplicates.length > 1) {
duplicateGroups.push({
memoryKeys: duplicates.map(m => m.structuredKey),
similarity: comparisons > 0 ? totalSimilarity / comparisons : 0
});
}
processed.add(memory.id);
}
return duplicateGroups;
}
async findBrokenRelationships(memories) {
const brokenMemories = [];
const memoryKeys = new Set(memories.map(m => m.structuredKey));
for (const memory of memories) {
const hasBrokenRelationships = memory.relationships.some((rel) => !memoryKeys.has(rel.targetKey));
if (hasBrokenRelationships) {
brokenMemories.push(memory.structuredKey);
}
}
return brokenMemories;
}
}
//# sourceMappingURL=recommendation-engine.js.map