UNPKG

@codai/memorai-core

Version:

Simplified advanced memory engine - no tiers, just powerful semantic search with persistence

225 lines (224 loc) 9.32 kB
import { logger } from '../utils/logger.js'; export class MemoryRelationshipManager { constructor() { this.relationships = new Map(); // Logger is already instantiated as singleton } async createRelationship(relationshipData) { const relationship = { ...relationshipData, strength: relationshipData.strength ?? 1.0, isActive: relationshipData.isActive ?? true, id: `rel_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, createdAt: new Date(), updatedAt: new Date(), }; // Store relationship for both source and target memories const sourceKey = `${relationship.sourceMemoryId}_${relationship.tenantId}`; const targetKey = `${relationship.targetMemoryId}_${relationship.tenantId}`; if (!this.relationships.has(sourceKey)) { this.relationships.set(sourceKey, []); } if (!this.relationships.has(targetKey)) { this.relationships.set(targetKey, []); } this.relationships.get(sourceKey).push(relationship); this.relationships.get(targetKey).push(relationship); logger.info('Created memory relationship', { relationshipId: relationship.id, type: relationship.relationshipType, sourceMemoryId: relationship.sourceMemoryId, targetMemoryId: relationship.targetMemoryId, }); return relationship; } async getRelationships(memoryId, tenantId) { const key = `${memoryId}_${tenantId}`; return this.relationships.get(key) || []; } async deleteRelationship(relationshipId, tenantId) { for (const [key, relationships] of this.relationships.entries()) { const index = relationships.findIndex(r => r.id === relationshipId && r.tenantId === tenantId); if (index !== -1) { relationships.splice(index, 1); logger.info('Deleted memory relationship', { relationshipId }); return true; } } return false; } async findRelatedMemories(memoryId, relationshipTypes, tenantId) { const relationships = await this.getRelationships(memoryId, tenantId); return relationships .filter(r => relationshipTypes.includes(r.relationshipType) && r.isActive) .map(r => r.sourceMemoryId === memoryId ? r.targetMemoryId : r.sourceMemoryId); } async buildMemoryGraph(query) { const visited = new Set(); const nodes = []; const edges = []; const edgeIds = new Set(); // Track unique edges const paths = []; await this.traverseGraph(query.startMemoryId, query.tenantId, visited, nodes, edges, edgeIds, paths, 0, query.maxDepth, query.relationshipTypes, query.includeInactive, [query.startMemoryId]); const statistics = { totalNodes: nodes.length, totalEdges: edges.length, maxDepth: Math.max(...paths.map(p => p.length - 1), 0), averageConnectivity: nodes.length > 0 ? edges.length / nodes.length : 0, }; return { nodes, edges, paths, statistics, }; } async traverseGraph(memoryId, tenantId, visited, nodes, edges, edgeIds, paths, currentDepth, maxDepth, relationshipTypes, includeInactive = false, currentPath = []) { if (currentDepth >= maxDepth || visited.has(memoryId)) { if (currentPath.length > 1) { paths.push([...currentPath]); } return; } visited.add(memoryId); // Get relationships for current memory const relationships = await this.getRelationships(memoryId, tenantId); const filteredRelationships = relationships.filter(r => { if (!includeInactive && !r.isActive) return false; if (relationshipTypes && !relationshipTypes.includes(r.relationshipType)) return false; return true; }); // Add unique relationships to edges for (const relationship of filteredRelationships) { if (!edgeIds.has(relationship.id)) { edges.push(relationship); edgeIds.add(relationship.id); } } // Traverse connected memories for (const relationship of filteredRelationships) { const nextMemoryId = relationship.sourceMemoryId === memoryId ? relationship.targetMemoryId : relationship.sourceMemoryId; await this.traverseGraph(nextMemoryId, tenantId, visited, nodes, edges, edgeIds, paths, currentDepth + 1, maxDepth, relationshipTypes, includeInactive, [...currentPath, nextMemoryId]); } } async suggestRelationships(memoryId, tenantId) { // This would typically use AI/ML to suggest relationships // For now, return empty array as placeholder logger.info('Generating relationship suggestions', { memoryId, tenantId }); // TODO: Implement AI-powered relationship suggestions // - Analyze memory content similarity // - Detect temporal relationships // - Identify complementary or conflicting information // - Suggest context-based relationships return []; } /** * Create hierarchical parent-child relationship */ async createParentChildRelationship(parentId, childId, tenantId) { const parentRel = await this.createRelationship({ sourceMemoryId: parentId, targetMemoryId: childId, relationshipType: 'parent', tenantId, strength: 1.0, isActive: true, }); const childRel = await this.createRelationship({ sourceMemoryId: childId, targetMemoryId: parentId, relationshipType: 'child', tenantId, strength: 1.0, isActive: true, }); return [parentRel, childRel]; } /** * Create sibling relationship between memories */ async createSiblingRelationship(memoryId1, memoryId2, tenantId) { const rel1 = await this.createRelationship({ sourceMemoryId: memoryId1, targetMemoryId: memoryId2, relationshipType: 'sibling', tenantId, strength: 1.0, isActive: true, }); const rel2 = await this.createRelationship({ sourceMemoryId: memoryId2, targetMemoryId: memoryId1, relationshipType: 'sibling', tenantId, strength: 1.0, isActive: true, }); return [rel1, rel2]; } /** * Get memory hierarchy (parents, children, siblings) */ async getMemoryHierarchy(memoryId, tenantId) { const relationships = await this.getRelationships(memoryId, tenantId); const parents = relationships .filter(r => r.relationshipType === 'child' && r.sourceMemoryId === memoryId) .map(r => r.targetMemoryId); const children = relationships .filter(r => r.relationshipType === 'parent' && r.sourceMemoryId === memoryId) .map(r => r.targetMemoryId); const siblings = relationships .filter(r => r.relationshipType === 'sibling' && r.sourceMemoryId === memoryId) .map(r => r.targetMemoryId); return { parents, children, siblings }; } /** * Find memory conflicts (memories that contradict each other) */ async findMemoryConflicts(tenantId) { const conflicts = []; const seen = new Set(); for (const relationships of this.relationships.values()) { for (const relationship of relationships) { if (relationship.relationshipType === 'conflicts' && relationship.tenantId === tenantId && relationship.isActive && !seen.has(relationship.id)) { conflicts.push(relationship); seen.add(relationship.id); } } } return conflicts; } /** * Clean up orphaned relationships */ async cleanupOrphanedRelationships(validMemoryIds, tenantId) { let cleanupCount = 0; const deletedRelationshipIds = new Set(); for (const [key, relationships] of this.relationships.entries()) { const updatedRelationships = relationships.filter(r => { if (r.tenantId !== tenantId) return true; // Keep other tenant's relationships const isValid = validMemoryIds.includes(r.sourceMemoryId) && validMemoryIds.includes(r.targetMemoryId); if (!isValid && !deletedRelationshipIds.has(r.id)) { cleanupCount++; deletedRelationshipIds.add(r.id); } return isValid; }); this.relationships.set(key, updatedRelationships); } logger.info('Cleaned up orphaned relationships', { count: cleanupCount, tenantId, }); return cleanupCount; } }