| 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 |
| }; |
| }; |
|
|
|
|