@codai/memorai-core
Version:
Simplified advanced memory engine - no tiers, just powerful semantic search with persistence
557 lines (556 loc) • 20.9 kB
JavaScript
/**
* Conversation Context Reconstruction Engine
* Advanced system for reconstructing and maintaining conversation context across sessions
*/
import { PatternRecognitionEngine, } from './PatternRecognition.js';
import { RelationshipExtractor, } from './RelationshipExtractor.js';
export class ConversationContextReconstructor {
constructor(config = {}) {
this.activeThreads = new Map();
this.threadHistory = [];
this.config = {
maxThreadAge: 24,
maxMemoriesPerThread: 100,
similarityThreshold: 0.7,
continuityThreshold: 0.6,
enableCrossSessionContext: true,
enableEmotionalContext: true,
enableIntentTracking: true,
...config,
};
this.relationshipExtractor = new RelationshipExtractor();
this.patternEngine = new PatternRecognitionEngine();
}
/**
* Reconstruct conversation context from memory fragments
*/
async reconstructContext(agentId, memories, userId) {
console.log(`🔄 Reconstructing conversation context for agent ${agentId}...`);
// Filter memories for this agent
const agentMemories = memories.filter(m => m.agent_id === agentId);
// Group memories into conversation threads
const threads = await this.groupMemoriesIntoThreads(agentMemories, agentId, userId);
// Analyze relationships within each thread
for (const thread of threads) {
thread.relationships =
await this.relationshipExtractor.extractRelationships(thread.memories);
thread.patterns = await this.patternEngine.analyzePatterns(thread.memories);
}
// Reconstruct context for each thread
for (const thread of threads) {
thread.context = await this.buildConversationContext(thread);
}
// Link threads across sessions if enabled
if (this.config.enableCrossSessionContext) {
await this.linkCrossSessionThreads(threads);
}
// Update active threads
this.updateActiveThreads(threads);
console.log(`✅ Reconstructed ${threads.length} conversation threads`);
return threads;
}
/**
* Group memories into logical conversation threads
*/
async groupMemoriesIntoThreads(memories, agentId, userId) {
const threads = [];
const sortedMemories = [...memories].sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
let currentThread = null;
const maxGapHours = 2; // Maximum gap between memories in same thread
for (const memory of sortedMemories) {
const shouldStartNewThread = !currentThread ||
this.shouldStartNewThread(currentThread, memory, maxGapHours);
if (shouldStartNewThread) {
// Save current thread
if (currentThread) {
threads.push(currentThread);
}
// Start new thread
currentThread = {
id: `thread_${agentId}_${memory.createdAt.getTime()}`,
agentId,
userId,
startTime: memory.createdAt,
endTime: memory.createdAt,
isActive: false,
context: this.createEmptyContext(),
memories: [memory],
relationships: [],
patterns: [],
metadata: {
complexity: 0,
continuity: 0,
tags: [...memory.tags],
},
};
}
else {
// Add to current thread
currentThread.memories.push(memory);
currentThread.endTime = memory.createdAt;
currentThread.metadata.tags = [
...new Set([...currentThread.metadata.tags, ...memory.tags]),
];
}
}
// Save final thread
if (currentThread) {
threads.push(currentThread);
}
// Determine which threads are still active
const now = new Date();
const activeThreshold = this.config.maxThreadAge * 60 * 60 * 1000;
for (const thread of threads) {
thread.isActive =
now.getTime() - thread.endTime.getTime() < activeThreshold;
}
return threads;
}
/**
* Determine if a new thread should start
*/
shouldStartNewThread(currentThread, memory, maxGapHours) {
// Check time gap
const timeDiff = memory.createdAt.getTime() - currentThread.endTime.getTime();
const hoursDiff = timeDiff / (1000 * 60 * 60);
if (hoursDiff > maxGapHours) {
return true;
}
// Check topic similarity
const topicSimilarity = this.calculateTopicSimilarity(currentThread, memory);
if (topicSimilarity < this.config.similarityThreshold) {
return true;
}
// Check thread size
if (currentThread.memories.length >= this.config.maxMemoriesPerThread) {
return true;
}
return false;
}
/**
* Calculate topic similarity between thread and new memory
*/
calculateTopicSimilarity(thread, memory) {
const threadTags = new Set(thread.metadata.tags);
const memoryTags = new Set(memory.tags);
const intersection = [...threadTags].filter(tag => memoryTags.has(tag));
const union = [...new Set([...threadTags, ...memoryTags])];
return union.length > 0 ? intersection.length / union.length : 0;
}
/**
* Build conversation context for a thread
*/
async buildConversationContext(thread) {
const context = {
currentTopic: '',
topicHistory: [],
entities: [],
intentions: [],
emotionalContext: {
currentMood: 'neutral',
moodHistory: [],
},
preferences: {},
previousSessions: [],
};
// Extract current topic from most recent memories
const recentMemories = thread.memories.slice(-5);
context.currentTopic = this.extractDominantTopic(recentMemories);
// Build topic history
context.topicHistory = await this.buildTopicHistory(thread.memories);
// Extract entities
context.entities = await this.extractEntities(thread.memories);
// Track intentions if enabled
if (this.config.enableIntentTracking) {
context.intentions = await this.extractIntentions(thread.memories);
}
// Build emotional context if enabled
if (this.config.enableEmotionalContext) {
context.emotionalContext = await this.buildEmotionalContext(thread.memories);
}
// Extract preferences
context.preferences = await this.extractPreferences(thread.memories);
return context;
}
/**
* Extract dominant topic from memories
*/
extractDominantTopic(memories) {
const topicCounts = new Map();
for (const memory of memories) {
for (const tag of memory.tags) {
topicCounts.set(tag, (topicCounts.get(tag) || 0) + 1);
}
// Also extract topic from content (simplified)
const words = memory.content.toLowerCase().split(/\W+/);
for (const word of words) {
if (word.length > 4) {
topicCounts.set(word, (topicCounts.get(word) || 0) + 0.5);
}
}
}
const sortedTopics = Array.from(topicCounts.entries()).sort((a, b) => b[1] - a[1]);
return sortedTopics[0]?.[0] || 'general';
}
/**
* Build topic history with confidence scores
*/
async buildTopicHistory(memories) {
const topicHistory = [];
// Group memories by time windows (e.g., 10-minute intervals)
const timeWindows = this.groupMemoriesByTimeWindows(memories, 10);
for (const window of timeWindows) {
const topic = this.extractDominantTopic(window.memories);
const confidence = this.calculateTopicConfidence(window.memories, topic);
topicHistory.push({
topic,
timestamp: window.startTime,
confidence,
});
}
return topicHistory;
}
/**
* Group memories by time windows
*/
groupMemoriesByTimeWindows(memories, windowMinutes) {
const windows = [];
const sortedMemories = [...memories].sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
if (sortedMemories.length === 0)
return windows;
const windowMs = windowMinutes * 60 * 1000;
let currentWindow = {
startTime: sortedMemories[0].createdAt,
endTime: new Date(sortedMemories[0].createdAt.getTime() + windowMs),
memories: [],
};
for (const memory of sortedMemories) {
if (memory.createdAt >= currentWindow.endTime) {
// Start new window
if (currentWindow.memories.length > 0) {
windows.push(currentWindow);
}
currentWindow = {
startTime: memory.createdAt,
endTime: new Date(memory.createdAt.getTime() + windowMs),
memories: [memory],
};
}
else {
currentWindow.memories.push(memory);
}
}
// Add final window
if (currentWindow.memories.length > 0) {
windows.push(currentWindow);
}
return windows;
}
/**
* Calculate confidence score for a topic
*/
calculateTopicConfidence(memories, topic) {
let relevantMemories = 0;
for (const memory of memories) {
if (memory.tags.includes(topic) ||
memory.content.toLowerCase().includes(topic.toLowerCase())) {
relevantMemories++;
}
}
return memories.length > 0 ? relevantMemories / memories.length : 0;
}
/**
* Extract entities from memories
*/
async extractEntities(memories) {
const entities = new Map();
for (const memory of memories) {
// Extract entities from tags
for (const tag of memory.tags) {
const key = tag.toLowerCase();
if (entities.has(key)) {
const entity = entities.get(key);
entity.mentions++;
entity.lastMention = memory.createdAt;
entity.importance = Math.min(1, entity.importance + memory.importance * 0.1);
}
else {
entities.set(key, {
name: tag,
type: this.classifyEntityType(tag),
mentions: 1,
lastMention: memory.createdAt,
importance: memory.importance,
});
}
}
// Simple entity extraction from content (in real implementation, would use NLP)
const words = memory.content.split(/\W+/);
for (const word of words) {
if (word.length > 2 && /^[A-Z][a-z]+/.test(word)) {
const key = word.toLowerCase();
if (entities.has(key)) {
entities.get(key).mentions++;
}
else {
entities.set(key, {
name: word,
type: 'unknown',
mentions: 1,
lastMention: memory.createdAt,
importance: 0.3,
});
}
}
}
}
return Array.from(entities.values())
.filter(entity => entity.mentions > 1)
.sort((a, b) => b.importance - a.importance);
}
/**
* Classify entity type (simplified)
*/
classifyEntityType(entity) {
const lowerEntity = entity.toLowerCase();
if (['person', 'user', 'agent', 'assistant'].some(type => lowerEntity.includes(type))) {
return 'person';
}
if (['project', 'task', 'work', 'job'].some(type => lowerEntity.includes(type))) {
return 'project';
}
if (['tech', 'code', 'programming', 'software'].some(type => lowerEntity.includes(type))) {
return 'technology';
}
return 'concept';
}
/**
* Extract intentions from memories
*/
async extractIntentions(memories) {
const intentions = [];
for (const memory of memories) {
// Simple intent extraction based on keywords
const intentKeywords = {
learn: ['learn', 'understand', 'explain', 'teach', 'show'],
create: ['create', 'build', 'make', 'generate', 'develop'],
solve: ['solve', 'fix', 'debug', 'resolve', 'troubleshoot'],
find: ['find', 'search', 'locate', 'discover', 'retrieve'],
plan: ['plan', 'schedule', 'organize', 'prepare', 'arrange'],
};
const content = memory.content.toLowerCase();
for (const [intent, keywords] of Object.entries(intentKeywords)) {
const matches = keywords.filter(keyword => content.includes(keyword));
if (matches.length > 0) {
const confidence = Math.min(1, matches.length / keywords.length + 0.3);
intentions.push({
intent,
confidence,
fulfilled: this.isIntentFulfilled(intent, memory, memories),
timestamp: memory.createdAt,
});
}
}
}
return intentions;
}
/**
* Determine if an intent was fulfilled
*/
isIntentFulfilled(intent, memory, allMemories) {
// Simple heuristic: check if subsequent memories contain fulfillment indicators
const laterMemories = allMemories.filter(m => m.createdAt > memory.createdAt);
const fulfillmentWords = [
'done',
'completed',
'finished',
'resolved',
'achieved',
];
return laterMemories.some(m => fulfillmentWords.some(word => m.content.toLowerCase().includes(word)));
}
/**
* Build emotional context
*/
async buildEmotionalContext(memories) {
const moodHistory = [];
for (const memory of memories) {
const mood = this.detectMoodFromMemory(memory);
if (mood) {
moodHistory.push(mood);
}
}
const currentMood = moodHistory.length > 0
? moodHistory[moodHistory.length - 1].mood
: 'neutral';
return {
currentMood,
moodHistory,
};
}
/**
* Detect mood from memory content
*/
detectMoodFromMemory(memory) {
const content = memory.content.toLowerCase();
const moodIndicators = {
positive: [
'happy',
'excited',
'great',
'awesome',
'excellent',
'love',
'wonderful',
],
negative: [
'sad',
'angry',
'frustrated',
'upset',
'disappointed',
'terrible',
'hate',
],
curious: ['wonder', 'interesting', 'curious', 'explore', 'discover'],
focused: ['concentrate', 'focus', 'work', 'task', 'goal', 'objective'],
confused: ['confused', 'unclear', "don't understand", 'puzzled', 'lost'],
};
for (const [mood, indicators] of Object.entries(moodIndicators)) {
const matches = indicators.filter(indicator => content.includes(indicator));
if (matches.length > 0) {
return {
mood,
timestamp: memory.createdAt,
intensity: Math.min(1, matches.length / indicators.length + 0.5),
};
}
}
return null;
}
/**
* Extract preferences from memories
*/
async extractPreferences(memories) {
const preferences = {};
for (const memory of memories) {
if (memory.type === 'preference') {
// Extract preference from content
const prefMatch = memory.content.match(/prefer\s+(.+)/i);
if (prefMatch) {
const key = memory.tags[0] || 'general';
preferences[key] = prefMatch[1];
}
}
}
return preferences;
}
/**
* Link threads across sessions
*/
async linkCrossSessionThreads(threads) {
for (const thread of threads) {
const relatedThreads = this.findRelatedThreads(thread, threads);
thread.context.previousSessions = relatedThreads.map(t => t.id);
}
}
/**
* Find related threads based on similarity
*/
findRelatedThreads(thread, allThreads) {
const related = [];
for (const otherThread of allThreads) {
if (otherThread.id === thread.id)
continue;
const similarity = this.calculateThreadSimilarity(thread, otherThread);
if (similarity > this.config.similarityThreshold) {
related.push(otherThread);
}
}
return related.sort((a, b) => b.endTime.getTime() - a.endTime.getTime());
}
/**
* Calculate similarity between threads
*/
calculateThreadSimilarity(thread1, thread2) {
const tags1 = new Set(thread1.metadata.tags);
const tags2 = new Set(thread2.metadata.tags);
const intersection = [...tags1].filter(tag => tags2.has(tag));
const union = [...new Set([...tags1, ...tags2])];
const tagSimilarity = union.length > 0 ? intersection.length / union.length : 0;
// Factor in agent similarity
const agentSimilarity = thread1.agentId === thread2.agentId ? 1 : 0;
return tagSimilarity * 0.7 + agentSimilarity * 0.3;
}
/**
* Create empty conversation context
*/
createEmptyContext() {
return {
currentTopic: '',
topicHistory: [],
entities: [],
intentions: [],
emotionalContext: {
currentMood: 'neutral',
moodHistory: [],
},
preferences: {},
previousSessions: [],
};
}
/**
* Update active threads cache
*/
updateActiveThreads(threads) {
// Clear inactive threads
for (const [threadId, thread] of this.activeThreads.entries()) {
if (!thread.isActive) {
this.threadHistory.push(thread);
this.activeThreads.delete(threadId);
}
}
// Add new active threads
for (const thread of threads) {
if (thread.isActive) {
this.activeThreads.set(thread.id, thread);
}
}
}
/**
* Get active conversation threads
*/
getActiveThreads() {
return Array.from(this.activeThreads.values());
}
/**
* Get conversation thread by ID
*/
getThread(threadId) {
return (this.activeThreads.get(threadId) ||
this.threadHistory.find(t => t.id === threadId) ||
null);
}
/**
* Get conversation summary for a thread
*/
async getConversationSummary(threadId) {
const thread = this.getThread(threadId);
if (!thread)
return null;
const duration = thread.endTime.getTime() - thread.startTime.getTime();
const keyTopics = thread.context.topicHistory
.sort((a, b) => b.confidence - a.confidence)
.slice(0, 5)
.map(t => t.topic);
const summary = `Conversation thread with ${thread.memories.length} memories over ${Math.round(duration / (1000 * 60))} minutes. ` +
`Main topics: ${keyTopics.join(', ')}. Current mood: ${thread.context.emotionalContext.currentMood}.`;
return {
summary,
keyTopics,
duration,
memoryCount: thread.memories.length,
continuityScore: thread.metadata.continuity,
};
}
}