@codai/memorai-core
Version:
Simplified advanced memory engine - no tiers, just powerful semantic search with persistence
281 lines (280 loc) • 10.2 kB
JavaScript
export class ContextEngine {
/**
* Generate a comprehensive context summary from memories
*/
generateContextSummary(memories, options = {}) {
if (memories.length === 0) {
return 'No relevant context available.';
}
const { maxLength = 2000, includeScore = false, includeTimestamp = false, } = options;
let summary = '';
// Group memories by type
const groupedMemories = this.groupMemoriesByType(memories);
for (const [type, typeMemories] of Object.entries(groupedMemories)) {
summary += `\n## ${this.formatMemoryType(type)}\n`;
for (const memory of typeMemories.slice(0, 5)) {
// Limit per type
const prefix = includeScore ? `[${memory.score.toFixed(2)}] ` : '';
const timestamp = includeTimestamp
? ` (${memory.memory.createdAt.toLocaleDateString()})`
: '';
summary += `- ${prefix}${memory.memory.content}${timestamp}\n`;
if (summary.length > maxLength) {
summary =
summary.substring(0, maxLength) + '...\n[Context truncated]';
break;
}
}
}
return summary.trim();
}
/**
* Generate structured context for agent consumption
*/ generateAgentContext(memories) {
const contextText = this.generateContextText(memories);
const summary = this.generateContextSummary(memories);
const confidence = this.calculateContextConfidence(memories);
return {
context: contextText,
memories,
summary,
confidence,
generated_at: new Date(),
total_count: memories.length,
context_summary: summary,
};
}
/**
* Extract key themes from memories
*/
extractThemes(memories) {
const themes = new Map();
for (const memory of memories) {
const keywords = this.extractKeywords(memory.memory.content);
for (const keyword of keywords) {
const existing = themes.get(keyword) || { frequency: 0, importance: 0 };
themes.set(keyword, {
frequency: existing.frequency + 1,
importance: Math.max(existing.importance, memory.memory.importance),
});
}
}
return Array.from(themes.entries())
.map(([theme, data]) => ({
theme,
frequency: data.frequency,
importance: data.importance,
}))
.sort((a, b) => b.frequency * b.importance - a.frequency * a.importance)
.slice(0, 10); // Top 10 themes
}
/**
* Detect emotional context from memories
*/
analyzeEmotionalContext(memories) {
const emotionalWeights = memories
.map(m => m.memory.emotional_weight)
.filter((weight) => weight !== undefined);
if (emotionalWeights.length === 0) {
return {
overall_sentiment: 'neutral',
emotional_weight: 0,
emotional_distribution: {},
};
}
const avgWeight = emotionalWeights.reduce((sum, w) => sum + w, 0) / emotionalWeights.length;
let sentiment;
if (avgWeight > 0.2) {
sentiment = 'positive';
}
else if (avgWeight < -0.2) {
sentiment = 'negative';
}
else {
sentiment = 'neutral';
}
// Simple emotional distribution
const distribution = {
positive: emotionalWeights.filter(w => w > 0.2).length,
neutral: emotionalWeights.filter(w => w >= -0.2 && w <= 0.2).length,
negative: emotionalWeights.filter(w => w < -0.2).length,
};
return {
overall_sentiment: sentiment,
emotional_weight: avgWeight,
emotional_distribution: distribution,
};
}
/**
* Generate time-based context analysis
*/
analyzeTemporalContext(memories) {
const now = new Date();
const timeline = new Map();
for (const memory of memories) {
const age = now.getTime() - memory.memory.createdAt.getTime();
const period = this.categorizeTimePeriod(age);
const existing = timeline.get(period) || { count: 0, importance: 0 };
timeline.set(period, {
count: existing.count + 1,
importance: existing.importance + memory.memory.importance,
});
}
const timelineArray = Array.from(timeline.entries()).map(([period, data]) => ({
period,
count: data.count,
avg_importance: data.count > 0 ? data.importance / data.count : 0,
}));
const recencyDistribution = {};
for (const item of timelineArray) {
recencyDistribution[item.period] = item.count;
}
return {
timeline: timelineArray,
recency_distribution: recencyDistribution,
};
}
/**
* Smart context filtering based on relevance and importance
*/
filterContextualMemories(memories, maxMemories = 20, importanceThreshold = 0.3) {
// Filter by importance threshold
let filtered = memories.filter(m => m.memory.importance >= importanceThreshold);
// Sort by composite score (similarity + importance + recency)
filtered = filtered.sort((a, b) => {
const scoreA = this.calculateCompositeScore(a);
const scoreB = this.calculateCompositeScore(b);
return scoreB - scoreA;
});
// Ensure diversity of memory types
const diverseMemories = [];
const typeCount = new Map();
const maxPerType = Math.max(1, Math.floor(maxMemories / 5)); // Max per type
for (const memory of filtered) {
const currentCount = typeCount.get(memory.memory.type) || 0;
if (currentCount < maxPerType && diverseMemories.length < maxMemories) {
diverseMemories.push(memory);
typeCount.set(memory.memory.type, currentCount + 1);
}
}
// Fill remaining slots with any high-scoring memories
for (const memory of filtered) {
if (diverseMemories.length >= maxMemories)
break;
if (!diverseMemories.includes(memory)) {
diverseMemories.push(memory);
}
}
return diverseMemories.slice(0, maxMemories);
}
groupMemoriesByType(memories) {
return memories.reduce((groups, memory) => {
const type = memory.memory.type;
if (!groups[type]) {
groups[type] = [];
}
groups[type].push(memory);
return groups;
}, {});
}
formatMemoryType(type) {
const formatted = type.charAt(0).toUpperCase() + type.slice(1);
return formatted.replace(/_/g, ' ');
}
generateContextText(memories) {
if (memories.length === 0) {
return '';
}
const filtered = this.filterContextualMemories(memories, 15);
return filtered
.map(m => {
const typeLabel = m.memory.type.toUpperCase();
const confidence = (m.score * 100).toFixed(0);
return `[${typeLabel}:${confidence}%] ${m.memory.content}`;
})
.join('\n\n');
}
calculateContextConfidence(memories) {
if (memories.length === 0) {
return 0;
}
// Weighted average of memory scores and confidence
const weightedSum = memories.reduce((sum, m) => {
const weight = m.memory.importance;
return sum + m.score * m.memory.confidence * weight;
}, 0);
const totalWeight = memories.reduce((sum, m) => sum + m.memory.importance, 0);
return totalWeight > 0 ? weightedSum / totalWeight : 0;
}
extractKeywords(content) {
// Simple keyword extraction
const words = content
.toLowerCase()
.replace(/[^\w\s]/g, '')
.split(/\s+/)
.filter(word => word.length > 3);
// Filter out common stop words
const stopWords = new Set([
'this',
'that',
'with',
'have',
'will',
'from',
'they',
'know',
'want',
'been',
'good',
'much',
'some',
'time',
'very',
'when',
'come',
'here',
'just',
'like',
'long',
'make',
'many',
'over',
'such',
'take',
'than',
'them',
'well',
'were',
]);
return words.filter(word => !stopWords.has(word)).slice(0, 5); // Top 5 keywords
}
categorizeTimePeriod(ageMs) {
const minutes = ageMs / (1000 * 60);
const hours = minutes / 60;
const days = hours / 24;
const weeks = days / 7;
if (minutes < 60)
return 'last_hour';
if (hours < 24)
return 'today';
if (days < 7)
return 'this_week';
if (weeks < 4)
return 'this_month';
if (days < 365)
return 'this_year';
return 'older';
}
calculateCompositeScore(memory) {
const now = new Date();
const ageInDays = (now.getTime() - memory.memory.lastAccessedAt.getTime()) /
(1000 * 60 * 60 * 24);
// Recency factor (more recent = higher score)
const recencyFactor = Math.exp(-ageInDays / 30); // 30-day decay
// Composite score combining similarity, importance, and recency
return (memory.score * 0.5 + // Semantic similarity
memory.memory.importance * 0.3 + // Content importance
recencyFactor * 0.2 // Recency bonus
);
}
}