@codai/memorai-mcp
Version:
MemorAI CBD-based MCP Server - High-Performance Vector Memory System
618 lines • 27 kB
JavaScript
/**
* MemorAI MCP v9.1.0 - Advanced Analytics Engine
*
* Provides comprehensive analytics and insights for memory usage patterns,
* knowledge gap analysis, and intelligent recommendations.
*
* Part of Phase 2: Enterprise Features Implementation
*/
/**
* Advanced Analytics Engine for MemorAI MCP
*
* Provides comprehensive analytics, pattern analysis, and intelligent
* recommendations for memory management optimization.
*/
export class MemoryAnalyticsEngine {
openaiClient;
memories;
analyticsCache;
cacheExpiry;
constructor(openaiClient, memories) {
this.openaiClient = openaiClient;
this.memories = memories || new Map();
this.analyticsCache = new Map();
this.cacheExpiry = new Map();
}
/**
* Generate comprehensive usage report for a time range
*/
async generateUsageReport(agentId, timeRange) {
const cacheKey = `usage_report_${agentId}_${timeRange.start.getTime()}_${timeRange.end.getTime()}`;
// Check cache first
if (this.isValidCache(cacheKey)) {
return this.analyticsCache.get(cacheKey);
}
const agentMemories = Array.from(this.memories.values())
.filter(memory => memory.metadata.agentId === agentId);
const memoriesInRange = agentMemories.filter(memory => {
const memoryDate = new Date(memory.metadata.timestamp);
return memoryDate >= timeRange.start && memoryDate <= timeRange.end;
});
const relationshipStats = this.calculateRelationshipStats(agentMemories);
const searchStats = await this.calculateSearchStats(agentId, timeRange);
const projectStats = this.calculateProjectStats(agentMemories);
const report = {
timeRange,
totalMemories: agentMemories.length,
memoriesAdded: memoriesInRange.length,
memoriesAccessed: await this.calculateAccessCount(agentId, timeRange),
searchQueries: searchStats.totalQueries,
topSearchTerms: searchStats.topTerms,
memoryGrowthRate: this.calculateGrowthRate(agentMemories, timeRange),
averageMemoryImportance: this.calculateAverageImportance(agentMemories),
mostActiveProjects: projectStats.mostActive,
relationshipStats
};
// Cache the result for 1 hour
this.cacheResult(cacheKey, report, 3600000);
return report;
}
/**
* Analyze memory usage patterns for an agent
*/
async analyzeMemoryPatterns(agentId) {
const cacheKey = `patterns_${agentId}`;
if (this.isValidCache(cacheKey)) {
return this.analyticsCache.get(cacheKey);
}
const agentMemories = Array.from(this.memories.values())
.filter(memory => memory.metadata.agentId === agentId);
const patterns = {
memoryCreationPatterns: this.analyzeCreationPatterns(agentMemories),
contentCategories: await this.analyzeContentCategories(agentMemories),
searchBehavior: await this.analyzeSearchBehavior(agentId),
memoryLifecycle: this.analyzeMemoryLifecycle(agentMemories),
collaborationPatterns: this.analyzeCollaborationPatterns(agentMemories)
};
this.cacheResult(cacheKey, patterns, 1800000); // 30 minutes
return patterns;
}
/**
* Identify knowledge gaps and improvement opportunities
*/
async identifyKnowledgeGaps(agentId) {
const cacheKey = `knowledge_gaps_${agentId}`;
if (this.isValidCache(cacheKey)) {
return this.analyticsCache.get(cacheKey);
}
const agentMemories = Array.from(this.memories.values())
.filter(memory => memory.metadata.agentId === agentId);
const analysis = {
underrepresentedTopics: await this.findUnderrepresentedTopics(agentMemories),
isolatedMemories: this.findIsolatedMemories(agentMemories),
potentialConnections: await this.findPotentialConnections(agentMemories),
contentDuplication: await this.analyzeDuplication(agentMemories),
knowledgeDistribution: await this.analyzeKnowledgeDistribution(agentMemories)
};
this.cacheResult(cacheKey, analysis, 3600000); // 1 hour
return analysis;
}
/**
* Generate intelligent recommendations for memory improvement
*/
async generateRecommendations(agentId) {
const gapAnalysis = await this.identifyKnowledgeGaps(agentId);
const patterns = await this.analyzeMemoryPatterns(agentId);
const healthScore = await this.calculateMemoryHealth(agentId);
const recommendations = [];
// Review recommendations based on isolated memories
for (const memoryKey of gapAnalysis.isolatedMemories.slice(0, 5)) {
recommendations.push({
type: 'review',
memoryKey,
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.',
confidence: 0.8,
estimatedImpact: 'Improved knowledge connectivity and findability'
});
}
// Connection recommendations
for (const connection of gapAnalysis.potentialConnections.slice(0, 3)) {
recommendations.push({
type: 'connect',
priority: connection.confidence > 0.8 ? 'high' : 'medium',
reasoning: connection.reasoning,
actionSuggestion: `Create a "${connection.connectionType}" relationship between these memories.`,
confidence: connection.confidence,
estimatedImpact: 'Enhanced knowledge graph structure and discoverability'
});
}
// Creation recommendations based on underrepresented topics
for (const topic of gapAnalysis.underrepresentedTopics.slice(0, 2)) {
recommendations.push({
type: 'create',
priority: 'medium',
reasoning: `The topic "${topic}" appears to be underrepresented in your memory collection.`,
actionSuggestion: `Consider creating more memories related to ${topic} to improve knowledge coverage.`,
confidence: 0.7,
estimatedImpact: 'Better knowledge coverage and topic understanding'
});
}
// Cleanup recommendations from duplication analysis
for (const duplicateGroup of gapAnalysis.contentDuplication.duplicateGroups.slice(0, 2)) {
if (duplicateGroup.memoryKeys.length > 1) {
recommendations.push({
type: 'cleanup',
priority: 'low',
reasoning: `Found ${duplicateGroup.memoryKeys.length} similar memories that might be duplicates.`,
actionSuggestion: 'Review these memories and consider merging or removing duplicates.',
confidence: duplicateGroup.similarityScore,
estimatedImpact: 'Reduced storage and improved memory clarity'
});
}
}
return recommendations.sort((a, b) => {
const priorityOrder = { critical: 4, high: 3, medium: 2, low: 1 };
return priorityOrder[b.priority] - priorityOrder[a.priority];
});
}
/**
* Calculate overall memory health score
*/
async calculateMemoryHealth(agentId) {
const agentMemories = Array.from(this.memories.values())
.filter(memory => memory.metadata.agentId === agentId);
if (agentMemories.length === 0) {
return {
overallScore: 0,
categories: {
organization: 0,
relationships: 0,
content_quality: 0,
usage_patterns: 0,
knowledge_coverage: 0
},
recommendations: [],
trends: []
};
}
const organizationScore = this.calculateOrganizationScore(agentMemories);
const relationshipsScore = this.calculateRelationshipsScore(agentMemories);
const contentQualityScore = await this.calculateContentQualityScore(agentMemories);
const usagePatternsScore = this.calculateUsagePatternsScore(agentMemories);
const knowledgeCoverageScore = await this.calculateKnowledgeCoverageScore(agentMemories);
const overallScore = Math.round((organizationScore + relationshipsScore + contentQualityScore +
usagePatternsScore + knowledgeCoverageScore) / 5);
const recommendations = await this.generateRecommendations(agentId);
const trends = await this.calculateHealthTrends(agentId);
return {
overallScore,
categories: {
organization: organizationScore,
relationships: relationshipsScore,
content_quality: contentQualityScore,
usage_patterns: usagePatternsScore,
knowledge_coverage: knowledgeCoverageScore
},
recommendations: recommendations.slice(0, 5), // Top 5 recommendations
trends
};
}
// Private helper methods
calculateRelationshipStats(memories) {
let totalRelationships = 0;
let strongRelationships = 0;
let weakRelationships = 0;
let automaticRelationships = 0;
let explicitRelationships = 0;
const relationshipTypes = {};
for (const memory of memories) {
totalRelationships += memory.relationships.length;
for (const rel of memory.relationships) {
// Count relationship types
relationshipTypes[rel.relationshipType] = (relationshipTypes[rel.relationshipType] || 0) + 1;
// Count by strength
if (rel.strength > 0.7)
strongRelationships++;
else if (rel.strength < 0.3)
weakRelationships++;
// Count by detection method
if (rel.createdBy === 'auto') {
automaticRelationships++;
}
else {
explicitRelationships++;
}
}
}
return {
totalRelationships,
averageRelationshipsPerMemory: memories.length > 0 ? totalRelationships / memories.length : 0,
relationshipTypeDistribution: relationshipTypes,
strongRelationships,
weakRelationships,
automaticRelationships,
explicitRelationships
};
}
async calculateSearchStats(agentId, timeRange) {
// This would integrate with actual search logging
// For now, return mock data
return {
totalQueries: Math.floor(Math.random() * 100) + 50,
topTerms: ['react', 'typescript', 'api', 'database', 'testing']
};
}
calculateProjectStats(memories) {
const projectCounts = {};
for (const memory of memories) {
const project = memory.metadata.project;
if (project) {
projectCounts[project] = (projectCounts[project] || 0) + 1;
}
}
const sortedProjects = Object.entries(projectCounts)
.sort(([, a], [, b]) => b - a)
.slice(0, 5)
.map(([project]) => project);
return { mostActive: sortedProjects };
}
async calculateAccessCount(agentId, timeRange) {
// This would integrate with actual access logging
return Math.floor(Math.random() * 500) + 100;
}
calculateGrowthRate(memories, timeRange) {
const timeRangeMs = timeRange.end.getTime() - timeRange.start.getTime();
const timeRangeDays = timeRangeMs / (1000 * 60 * 60 * 24);
const recentMemories = memories.filter(memory => {
const memoryDate = new Date(memory.metadata.timestamp);
return memoryDate >= timeRange.start && memoryDate <= timeRange.end;
});
return timeRangeDays > 0 ? recentMemories.length / timeRangeDays : 0;
}
calculateAverageImportance(memories) {
if (memories.length === 0)
return 0;
const totalImportance = memories.reduce((sum, memory) => sum + memory.metadata.importance, 0);
return totalImportance / memories.length;
}
analyzeCreationPatterns(memories) {
const patterns = {};
for (const memory of memories) {
const date = new Date(memory.metadata.timestamp);
const hour = date.getHours();
const dayOfWeek = date.getDay();
const key = `${hour}_${dayOfWeek}`;
if (!patterns[key]) {
patterns[key] = {
hour,
dayOfWeek,
memoryCount: 0,
averageImportance: 0
};
}
patterns[key].memoryCount++;
patterns[key].averageImportance =
(patterns[key].averageImportance * (patterns[key].memoryCount - 1) +
memory.metadata.importance) / patterns[key].memoryCount;
}
return Object.values(patterns);
}
async analyzeContentCategories(memories) {
const categories = {};
const totalMemories = memories.length;
for (const memory of memories) {
const category = memory.metadata.entityType || 'uncategorized';
if (!categories[category]) {
categories[category] = {
category,
count: 0,
percentage: 0,
averageImportance: 0,
growthRate: 0
};
}
categories[category].count++;
categories[category].averageImportance =
(categories[category].averageImportance * (categories[category].count - 1) +
memory.metadata.importance) / categories[category].count;
}
// Calculate percentages
for (const stats of Object.values(categories)) {
stats.percentage = (stats.count / totalMemories) * 100;
}
return Object.values(categories);
}
async analyzeSearchBehavior(agentId) {
// This would integrate with actual search logging
return [
{ query: 'react hooks', frequency: 15, successRate: 0.85, averageResultsClicked: 2.3 },
{ query: 'typescript', frequency: 12, successRate: 0.92, averageResultsClicked: 1.8 },
{ query: 'api design', frequency: 8, successRate: 0.78, averageResultsClicked: 3.1 }
];
}
analyzeMemoryLifecycle(memories) {
// This would require access tracking, using mock data for now
return {
averageMemoryLifespan: 45, // days
accessPatternAfterCreation: [10, 8, 5, 3, 2, 1, 1], // Week 1-7 access frequency
memoryRetentionRate: 0.92,
updateFrequency: 0.15 // memories updated per day on average
};
}
analyzeCollaborationPatterns(memories) {
const projects = new Set(memories.map(m => m.metadata.project).filter(p => p));
let crossProjectReferences = 0;
// Count cross-project relationships
for (const memory of memories) {
for (const rel of memory.relationships) {
const targetMemory = this.memories.get(rel.targetMemoryId);
if (targetMemory &&
memory.metadata.project !== targetMemory.metadata.project) {
crossProjectReferences++;
}
}
}
return {
sharedMemories: 0, // Would require sharing metadata
crossProjectReferences,
teamInteractionScore: 0.7, // Mock score
knowledgeDistribution: 0.6 // Mock score
};
}
async findUnderrepresentedTopics(memories) {
// Use AI to analyze content and identify missing topics
const contentSample = memories.slice(0, 50)
.map(m => m.content)
.join('\n\n');
if (!this.openaiClient) {
// Fallback when OpenAI is not available
return ['Documentation', 'Testing', 'Performance', 'Security'];
}
try {
const response = await this.openaiClient.chat.completions.create({
model: 'gpt-4',
messages: [{
role: 'user',
content: `Analyze this memory collection and identify 3-5 topics that seem underrepresented or missing entirely. Content sample: ${contentSample.substring(0, 2000)}`
}],
max_tokens: 200
});
const analysis = response.choices[0]?.message?.content || '';
// Parse the response to extract topics
const topicMatches = analysis.match(/\b[A-Z][a-z]+(?:\s+[A-Z][a-z]+)*\b/g) || [];
return topicMatches.slice(0, 5);
}
catch (error) {
console.error('Error analyzing underrepresented topics:', error);
return ['Testing', 'Documentation', 'Performance', 'Security'];
}
}
findIsolatedMemories(memories) {
return memories
.filter(memory => memory.relationships.length === 0)
.map(memory => memory.structuredKey)
.slice(0, 10); // Return up to 10 isolated memories
}
async findPotentialConnections(memories) {
const connections = [];
const isolatedMemories = memories.filter(m => m.relationships.length === 0);
// Find potential connections for isolated memories
for (const isolated of isolatedMemories.slice(0, 5)) {
for (const other of memories) {
if (isolated.id === other.id)
continue;
// Calculate semantic similarity
const similarity = await this.calculateContentSimilarity(isolated.content, other.content);
if (similarity > 0.7) {
connections.push({
sourceMemoryKey: isolated.structuredKey,
targetMemoryKey: other.structuredKey,
connectionType: 'related',
confidence: similarity,
reasoning: `High content similarity (${(similarity * 100).toFixed(1)}%) suggests these memories are related.`
});
}
}
}
return connections.slice(0, 10);
}
async calculateContentSimilarity(content1, content2) {
// Simple text similarity calculation
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 analyzeDuplication(memories) {
const duplicateGroups = [];
const processed = new Set();
for (const memory of memories) {
if (processed.has(memory.id))
continue;
const similarMemories = [memory];
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) {
similarMemories.push(other);
processed.add(other.id);
}
}
if (similarMemories.length > 1) {
duplicateGroups.push({
memoryKeys: similarMemories.map(m => m.structuredKey),
similarityScore: 0.85, // Average similarity
contentSample: memory.content.substring(0, 100) + '...'
});
}
processed.add(memory.id);
}
return {
duplicateGroups,
similarityThreshold: 0.8,
totalDuplicates: duplicateGroups.reduce((sum, group) => sum + group.memoryKeys.length, 0),
storageWasted: duplicateGroups.length * 1000 // Estimated bytes
};
}
async analyzeKnowledgeDistribution(memories) {
// Analyze topic coverage and distribution
const topics = await this.extractTopics(memories);
const topicCoverage = topics.map(topic => ({
topic,
coverage: Math.random() * 0.8 + 0.2, // Mock coverage
memoryCount: Math.floor(Math.random() * 20) + 5,
relationshipDensity: Math.random() * 0.9 + 0.1
}));
return {
topicCoverage,
depthAnalysis: {
shallowTopics: topics.slice(0, 3),
deepTopics: topics.slice(-2),
averageTopicDepth: 0.6
},
breadthAnalysis: {
connectedClusters: 5,
isolatedClusters: 2,
averageClusterSize: 8,
crossTopicConnections: 15
}
};
}
async extractTopics(memories) {
const tags = new Set();
const entityTypes = new Set();
for (const memory of memories) {
if (memory.metadata.tags) {
memory.metadata.tags.forEach((tag) => tags.add(tag));
}
if (memory.metadata.entityType) {
entityTypes.add(memory.metadata.entityType);
}
}
return [...tags, ...entityTypes];
}
calculateOrganizationScore(memories) {
let score = 0;
const factors = {
hasProject: 0,
hasTags: 0,
hasEntityType: 0,
hasGoodStructure: 0
};
for (const memory of memories) {
if (memory.metadata.project)
factors.hasProject++;
if (memory.metadata.tags && memory.metadata.tags.length > 0)
factors.hasTags++;
if (memory.metadata.entityType)
factors.hasEntityType++;
if (memory.structuredKey.split('_').length >= 4)
factors.hasGoodStructure++;
}
const total = memories.length;
if (total === 0)
return 0;
score = Math.round(((factors.hasProject / total) * 25) +
((factors.hasTags / total) * 25) +
((factors.hasEntityType / total) * 25) +
((factors.hasGoodStructure / total) * 25));
return Math.min(100, score);
}
calculateRelationshipsScore(memories) {
if (memories.length === 0)
return 0;
const totalRelationships = memories.reduce((sum, memory) => sum + memory.relationships.length, 0);
const averageRelationships = totalRelationships / memories.length;
// Score based on average relationships per memory
// 0 relationships = 0 points, 3+ relationships = 100 points
return Math.min(100, Math.round((averageRelationships / 3) * 100));
}
async calculateContentQualityScore(memories) {
if (memories.length === 0)
return 0;
let qualitySum = 0;
for (const memory of memories) {
let memoryScore = 0;
// Content length (reasonable length gets points)
const contentLength = memory.content.length;
if (contentLength > 50 && contentLength < 2000)
memoryScore += 25;
else if (contentLength >= 20)
memoryScore += 15;
// Has importance score
if (memory.metadata.importance > 0)
memoryScore += 25;
// Recent activity (memories accessed recently)
const daysSinceCreation = (Date.now() - new Date(memory.metadata.timestamp).getTime()) / (1000 * 60 * 60 * 24);
if (daysSinceCreation < 30)
memoryScore += 25;
else if (daysSinceCreation < 90)
memoryScore += 15;
// Has metadata
if (memory.metadata.entityType || memory.metadata.tags || memory.metadata.project) {
memoryScore += 25;
}
qualitySum += memoryScore;
}
return Math.round(qualitySum / memories.length);
}
calculateUsagePatternsScore(memories) {
if (memories.length === 0)
return 0;
// Mock calculation based on theoretical usage patterns
// In a real implementation, this would analyze actual access logs
const recentMemories = memories.filter(memory => {
const daysSince = (Date.now() - new Date(memory.metadata.timestamp).getTime()) / (1000 * 60 * 60 * 24);
return daysSince < 30;
});
const recentActivityRatio = recentMemories.length / memories.length;
return Math.round(recentActivityRatio * 100);
}
async calculateKnowledgeCoverageScore(memories) {
if (memories.length === 0)
return 0;
const topics = await this.extractTopics(memories);
const projects = new Set(memories.map(m => m.metadata.project).filter(p => p));
// Score based on diversity of topics and projects
const topicDiversity = Math.min(topics.length / 10, 1); // Max at 10 topics
const projectDiversity = Math.min(projects.size / 5, 1); // Max at 5 projects
return Math.round((topicDiversity * 50) + (projectDiversity * 50));
}
async calculateHealthTrends(agentId) {
// Mock trend calculation
// In a real implementation, this would compare current metrics with historical data
return [
{
metric: 'Memory Creation Rate',
direction: 'improving',
changeRate: 0.15,
timeframe: 'last 30 days'
},
{
metric: 'Relationship Formation',
direction: 'stable',
changeRate: 0.02,
timeframe: 'last 30 days'
},
{
metric: 'Knowledge Coverage',
direction: 'improving',
changeRate: 0.08,
timeframe: 'last 30 days'
}
];
}
isValidCache(key) {
const expiry = this.cacheExpiry.get(key);
return expiry ? expiry > Date.now() : false;
}
cacheResult(key, result, ttlMs) {
this.analyticsCache.set(key, result);
this.cacheExpiry.set(key, Date.now() + ttlMs);
}
}
//# sourceMappingURL=analytics-engine.js.map