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