feat(memory): Day M.1 complete - importance scoring system
- Add FrequencyTracker: increment(), get_frequency_score(), get_recency_score(), get_time_decay() - Add EmotionAnalyzer: EMOTION_KEYWORDS dict, extract(), calculate_score(), get_emotion_profile() - Add ImpactEvaluator: evaluate(), get_topic_overlap(), rank_by_impact() - Add ImportanceScorer: composite scoring (freq 35% + recency 20% + emotion 25% + impact 20%) - Update UserMemory model: frequency_count, emotion_tags, importance_score, importance_level, associated_topics - Integrate ImportanceScorer into memory_service.py (recall + importance update) - Add 37 tests for all memory scoring components - Fix urgency patterns: remove overly broad '今天' that matched neutral text - Update memory-update checklist: mark all M.1 tasks complete
This commit is contained in:
84
backend/app/services/memory/frequency_tracker.py
Normal file
84
backend/app/services/memory/frequency_tracker.py
Normal file
@@ -0,0 +1,84 @@
|
||||
"""
|
||||
FrequencyTracker
|
||||
|
||||
Tracks how often a memory is recalled and calculates frequency/recency scores.
|
||||
"""
|
||||
|
||||
from datetime import UTC, datetime
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from app.models.memory import UserMemory
|
||||
|
||||
|
||||
class FrequencyTracker:
|
||||
"""Track and score memory recall frequency"""
|
||||
|
||||
# Score weights
|
||||
MAX_FREQUENCY = 10 # Cap frequency count for scoring
|
||||
RECENCY_DECAY_DAYS = 30 # After 30 days, recency score drops significantly
|
||||
|
||||
def increment(self, memory: "UserMemory") -> "UserMemory":
|
||||
"""Increment recall count and update last recalled timestamp"""
|
||||
memory.frequency_count = (memory.frequency_count or 0) + 1
|
||||
memory.last_recalled_at = datetime.now(UTC)
|
||||
return memory
|
||||
|
||||
def get_frequency_score(self, memory: "UserMemory") -> float:
|
||||
"""Calculate normalized frequency score (0.0 - 1.0)
|
||||
|
||||
Uses logarithmic scaling to prevent high-frequency memories
|
||||
from dominating completely.
|
||||
"""
|
||||
count = memory.frequency_count or 0
|
||||
if count == 0:
|
||||
return 0.0
|
||||
# Logarithmic scaling: more recalls have diminishing returns
|
||||
# log(1+x) / log(1+MAX) gives 0-1 range
|
||||
import math
|
||||
|
||||
score = math.log(1 + count) / math.log(1 + self.MAX_FREQUENCY)
|
||||
return min(1.0, max(0.0, score))
|
||||
|
||||
def get_recency_score(self, memory: "UserMemory") -> float:
|
||||
"""Calculate recency score (0.0 - 1.0)
|
||||
|
||||
Memory recalled recently scores higher. Uses exponential decay.
|
||||
"""
|
||||
last_recalled = memory.last_recalled_at
|
||||
if last_recalled is None:
|
||||
return 0.0
|
||||
|
||||
now = datetime.now(UTC)
|
||||
if isinstance(last_recalled, datetime):
|
||||
if last_recalled.tzinfo is None:
|
||||
last_recalled = last_recalled.replace(tzinfo=UTC)
|
||||
days_since = (now - last_recalled).total_seconds() / 86400
|
||||
else:
|
||||
days_since = self.RECENCY_DECAY_DAYS
|
||||
|
||||
# Exponential decay: half-life of RECENCY_DECAY_DAYS
|
||||
import math
|
||||
|
||||
decay = math.exp(-days_since / self.RECENCY_DECAY_DAYS)
|
||||
return min(1.0, max(0.0, decay))
|
||||
|
||||
def get_time_decay(self, memory: "UserMemory") -> float:
|
||||
"""Calculate time-based decay factor for forgetting curve"""
|
||||
last_accessed = getattr(memory, "last_accessed_at", None)
|
||||
if last_accessed is None:
|
||||
last_accessed = memory.last_recalled_at
|
||||
if last_accessed is None:
|
||||
return 1.0
|
||||
|
||||
now = datetime.now(UTC)
|
||||
if isinstance(last_accessed, datetime):
|
||||
if last_accessed.tzinfo is None:
|
||||
last_accessed = last_accessed.replace(tzinfo=UTC)
|
||||
days_since = (now - last_accessed).total_seconds() / 86400
|
||||
else:
|
||||
days_since = 0
|
||||
|
||||
import math
|
||||
|
||||
return math.exp(-days_since / self.RECENCY_DECAY_DAYS)
|
||||
Reference in New Issue
Block a user