| | import { useState, useEffect, useCallback } from 'react'; |
| | import { MemorySystemState, MemoryItem } from '../types'; |
| |
|
| | interface MemoryConfig { |
| | compressionRatio?: number; |
| | retentionThreshold?: number; |
| | cleanupInterval?: number; |
| | } |
| |
|
| | interface MemoryRetrievalOptions { |
| | limit?: number; |
| | threshold?: number; |
| | includeArchived?: boolean; |
| | sortBy?: 'relevance' | 'recency' | 'importance'; |
| | } |
| |
|
| | export const useMemorySystem = (config: MemoryConfig = {}) => { |
| | const [memorySystem, setMemorySystem] = useState<MemorySystemState>({ |
| | shortTerm: [], |
| | longTerm: [], |
| | archive: [], |
| | compressionRatio: config.compressionRatio || 0.7, |
| | retentionScore: config.retentionThreshold || 0.8, |
| | cyclicCleanup: 0 |
| | }); |
| |
|
| | |
| | const storeMemory = useCallback(async (item: MemoryItem) => { |
| | setMemorySystem(prev => { |
| | const newState = { ...prev }; |
| | |
| | |
| | const isDuplicate = prev.shortTerm.some(existing => |
| | calculateSimilarity(existing.content, item.content) > 0.9 |
| | ); |
| | |
| | if (!isDuplicate) { |
| | |
| | newState.shortTerm = [...prev.shortTerm, item]; |
| | |
| | |
| | if (newState.shortTerm.length > 100) { |
| | compressMemoriesInternal(newState); |
| | } |
| | } |
| | |
| | return newState; |
| | }); |
| | }, []); |
| |
|
| | |
| | const retrieveMemories = useCallback(async ( |
| | query: string, |
| | options: MemoryRetrievalOptions = {} |
| | ): Promise<MemoryItem[]> => { |
| | const { limit = 10, threshold = 0.7, includeArchived = false, sortBy = 'relevance' } = options; |
| | |
| | const allMemories = [ |
| | ...memorySystem.shortTerm, |
| | ...memorySystem.longTerm, |
| | ...(includeArchived ? memorySystem.archive : []) |
| | ]; |
| |
|
| | |
| | const scoredMemories = allMemories.map(memory => ({ |
| | ...memory, |
| | relevanceScore: calculateRelevance(query, memory) |
| | })).filter(memory => memory.relevanceScore >= threshold); |
| |
|
| | |
| | scoredMemories.sort((a, b) => { |
| | switch (sortBy) { |
| | case 'recency': |
| | return new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime(); |
| | case 'importance': |
| | return b.importance - a.importance; |
| | case 'relevance': |
| | default: |
| | return b.relevanceScore - a.relevanceScore; |
| | } |
| | }); |
| |
|
| | |
| | const retrievedIds = scoredMemories.slice(0, limit).map(m => m.id); |
| | setMemorySystem(prev => ({ |
| | ...prev, |
| | shortTerm: prev.shortTerm.map(m => |
| | retrievedIds.includes(m.id) ? { ...m, accessCount: m.accessCount + 1 } : m |
| | ), |
| | longTerm: prev.longTerm.map(m => |
| | retrievedIds.includes(m.id) ? { ...m, accessCount: m.accessCount + 1 } : m |
| | ) |
| | })); |
| |
|
| | return scoredMemories.slice(0, limit); |
| | }, [memorySystem]); |
| |
|
| | |
| | const compressMemories = useCallback(async () => { |
| | setMemorySystem(prev => { |
| | const newState = { ...prev }; |
| | compressMemoriesInternal(newState); |
| | return newState; |
| | }); |
| | }, []); |
| |
|
| | |
| | const compressMemoriesInternal = (state: MemorySystemState) => { |
| | const now = new Date(); |
| | const compressionThreshold = 50; |
| | |
| | if (state.shortTerm.length > compressionThreshold) { |
| | |
| | const scoredMemories = state.shortTerm.map(memory => ({ |
| | ...memory, |
| | retentionScore: calculateRetentionScore(memory, now) |
| | })); |
| |
|
| | |
| | scoredMemories.sort((a, b) => b.retentionScore - a.retentionScore); |
| |
|
| | |
| | const keepInShortTerm = Math.floor(compressionThreshold * 0.7); |
| | state.shortTerm = scoredMemories.slice(0, keepInShortTerm); |
| |
|
| | |
| | const moveToLongTerm = scoredMemories.slice(keepInShortTerm, keepInShortTerm + 20); |
| | state.longTerm = [...state.longTerm, ...moveToLongTerm]; |
| |
|
| | |
| | const toArchive = scoredMemories.slice(keepInShortTerm + 20); |
| | const archiveWorthy = toArchive.filter(m => m.retentionScore > 0.3); |
| | state.archive = [...state.archive, ...archiveWorthy]; |
| |
|
| | |
| | const totalOriginal = scoredMemories.length; |
| | const totalKept = state.shortTerm.length + moveToLongTerm.length + archiveWorthy.length; |
| | state.compressionRatio = totalKept / totalOriginal; |
| | } |
| |
|
| | |
| | const archiveRetentionDays = 30; |
| | const cutoffDate = new Date(now.getTime() - archiveRetentionDays * 24 * 60 * 60 * 1000); |
| | state.archive = state.archive.filter(memory => |
| | new Date(memory.timestamp) > cutoffDate || memory.importance > 0.8 |
| | ); |
| |
|
| | state.cyclicCleanup++; |
| | }; |
| |
|
| | |
| | const calculateSimilarity = (content1: any, content2: any): number => { |
| | const str1 = JSON.stringify(content1).toLowerCase(); |
| | const str2 = JSON.stringify(content2).toLowerCase(); |
| | |
| | if (str1 === str2) return 1.0; |
| | |
| | |
| | const words1 = new Set(str1.split(/\s+/)); |
| | const words2 = new Set(str2.split(/\s+/)); |
| | const intersection = new Set([...words1].filter(x => words2.has(x))); |
| | const union = new Set([...words1, ...words2]); |
| | |
| | return intersection.size / union.size; |
| | }; |
| |
|
| | |
| | const calculateRelevance = (query: string, memory: MemoryItem): number => { |
| | const queryLower = query.toLowerCase(); |
| | const contentStr = JSON.stringify(memory.content).toLowerCase(); |
| | const tagsStr = memory.tags.join(' ').toLowerCase(); |
| | |
| | let score = 0; |
| | |
| | |
| | if (contentStr.includes(queryLower)) { |
| | score += 0.8; |
| | } |
| | |
| | |
| | if (tagsStr.includes(queryLower)) { |
| | score += 0.6; |
| | } |
| | |
| | |
| | const queryWords = queryLower.split(/\s+/); |
| | const contentWords = contentStr.split(/\s+/); |
| | const overlap = queryWords.filter(word => contentWords.includes(word)).length; |
| | score += (overlap / queryWords.length) * 0.4; |
| | |
| | |
| | score *= (1 + memory.importance * 0.2); |
| | score *= (1 + Math.log(memory.accessCount + 1) * 0.1); |
| | |
| | |
| | const daysSinceCreation = (Date.now() - new Date(memory.timestamp).getTime()) / (1000 * 60 * 60 * 24); |
| | score *= Math.max(0.5, 1 - daysSinceCreation * 0.01); |
| | |
| | return Math.min(1.0, score); |
| | }; |
| |
|
| | |
| | const calculateRetentionScore = (memory: MemoryItem, currentTime: Date): number => { |
| | const ageInDays = (currentTime.getTime() - new Date(memory.timestamp).getTime()) / (1000 * 60 * 60 * 24); |
| | |
| | |
| | let score = memory.importance; |
| | |
| | |
| | score += Math.min(0.3, memory.accessCount * 0.05); |
| | |
| | |
| | score *= Math.exp(-ageInDays * 0.1); |
| | |
| | |
| | const importantTags = ['critical', 'important', 'user-preference', 'system-config']; |
| | const hasImportantTags = memory.tags.some(tag => importantTags.includes(tag)); |
| | if (hasImportantTags) { |
| | score *= 1.5; |
| | } |
| | |
| | return Math.min(1.0, score); |
| | }; |
| |
|
| | |
| | const getMemoryStats = useCallback(() => { |
| | const totalMemories = memorySystem.shortTerm.length + memorySystem.longTerm.length + memorySystem.archive.length; |
| | const averageImportance = totalMemories > 0 |
| | ? [...memorySystem.shortTerm, ...memorySystem.longTerm, ...memorySystem.archive] |
| | .reduce((sum, m) => sum + m.importance, 0) / totalMemories |
| | : 0; |
| | |
| | return { |
| | totalMemories, |
| | shortTermCount: memorySystem.shortTerm.length, |
| | longTermCount: memorySystem.longTerm.length, |
| | archiveCount: memorySystem.archive.length, |
| | averageImportance, |
| | compressionRatio: memorySystem.compressionRatio, |
| | retentionScore: memorySystem.retentionScore, |
| | cleanupCycles: memorySystem.cyclicCleanup |
| | }; |
| | }, [memorySystem]); |
| |
|
| | |
| | useEffect(() => { |
| | const cleanupInterval = setInterval(() => { |
| | compressMemories(); |
| | }, config.cleanupInterval || 300000); |
| |
|
| | return () => clearInterval(cleanupInterval); |
| | }, [compressMemories, config.cleanupInterval]); |
| |
|
| | return { |
| | memorySystem, |
| | storeMemory, |
| | retrieveMemories, |
| | compressMemories, |
| | getMemoryStats |
| | }; |
| | }; |
| |
|
| |
|