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:
408
backend/tests/services/test_importance_scorer.py
Normal file
408
backend/tests/services/test_importance_scorer.py
Normal file
@@ -0,0 +1,408 @@
|
||||
"""
|
||||
Tests for Importance Scoring System (M.1)
|
||||
|
||||
Tests: frequency tracking, emotion analysis, impact evaluation, and composite importance scoring.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from app.services.memory.frequency_tracker import FrequencyTracker
|
||||
from app.services.memory.emotion_analyzer import EmotionAnalyzer
|
||||
from app.services.memory.impact_evaluator import ImpactEvaluator
|
||||
from app.services.memory.importance_scorer import ImportanceScorer, ImportanceLevel
|
||||
|
||||
|
||||
def create_mock_memory(
|
||||
frequency_count: int = 0,
|
||||
last_recalled_at=None,
|
||||
content: str = "",
|
||||
emotion_tags: list = None,
|
||||
associated_topics: list = None,
|
||||
last_accessed_at=None,
|
||||
):
|
||||
"""Create a mock UserMemory for testing"""
|
||||
memory = MagicMock()
|
||||
memory.frequency_count = frequency_count
|
||||
memory.last_recalled_at = last_recalled_at
|
||||
memory.content = content
|
||||
memory.emotion_tags = emotion_tags or []
|
||||
memory.associated_topics = associated_topics or []
|
||||
memory.last_accessed_at = last_accessed_at
|
||||
memory.importance_score = 0.5
|
||||
memory.importance_level = "medium"
|
||||
return memory
|
||||
|
||||
|
||||
class TestFrequencyTracker:
|
||||
"""Test frequency tracking"""
|
||||
|
||||
def test_increment(self):
|
||||
tracker = FrequencyTracker()
|
||||
memory = create_mock_memory(frequency_count=5)
|
||||
|
||||
result = tracker.increment(memory)
|
||||
|
||||
assert result.frequency_count == 6
|
||||
assert result.last_recalled_at is not None
|
||||
|
||||
def test_increment_from_zero(self):
|
||||
tracker = FrequencyTracker()
|
||||
memory = create_mock_memory(frequency_count=0)
|
||||
|
||||
result = tracker.increment(memory)
|
||||
|
||||
assert result.frequency_count == 1
|
||||
|
||||
def test_get_frequency_score_zero(self):
|
||||
tracker = FrequencyTracker()
|
||||
memory = create_mock_memory(frequency_count=0)
|
||||
|
||||
score = tracker.get_frequency_score(memory)
|
||||
|
||||
assert score == 0.0
|
||||
|
||||
def test_get_frequency_score_normal(self):
|
||||
tracker = FrequencyTracker()
|
||||
memory = create_mock_memory(frequency_count=5)
|
||||
|
||||
score = tracker.get_frequency_score(memory)
|
||||
|
||||
# log(1+5) / log(1+10) ≈ log(6)/log(11) ≈ 0.778 / 1.041 ≈ 0.747
|
||||
assert 0.7 < score < 0.8
|
||||
|
||||
def test_get_frequency_score_capped(self):
|
||||
tracker = FrequencyTracker()
|
||||
memory = create_mock_memory(frequency_count=100)
|
||||
|
||||
score = tracker.get_frequency_score(memory)
|
||||
|
||||
# Should be capped at 1.0
|
||||
assert score <= 1.0
|
||||
|
||||
def test_get_recency_score_recent(self):
|
||||
tracker = FrequencyTracker()
|
||||
memory = create_mock_memory(last_recalled_at=datetime.now(UTC))
|
||||
|
||||
score = tracker.get_recency_score(memory)
|
||||
|
||||
assert score > 0.9
|
||||
|
||||
def test_get_recency_score_old(self):
|
||||
tracker = FrequencyTracker()
|
||||
old_date = datetime.now(UTC) - timedelta(days=60)
|
||||
memory = create_mock_memory(last_recalled_at=old_date)
|
||||
|
||||
score = tracker.get_recency_score(memory)
|
||||
|
||||
# ~60 days old with 30-day half-life should be ~0.25
|
||||
assert score < 0.3
|
||||
|
||||
def test_get_recency_score_never_recalled(self):
|
||||
tracker = FrequencyTracker()
|
||||
memory = create_mock_memory(last_recalled_at=None)
|
||||
|
||||
score = tracker.get_recency_score(memory)
|
||||
|
||||
assert score == 0.0
|
||||
|
||||
def test_get_time_decay(self):
|
||||
tracker = FrequencyTracker()
|
||||
recent = datetime.now(UTC) - timedelta(days=7)
|
||||
memory = create_mock_memory(last_accessed_at=recent)
|
||||
|
||||
decay = tracker.get_time_decay(memory)
|
||||
|
||||
# ~7 days with 30-day half-life: exp(-7/30) ≈ 0.79
|
||||
assert 0.7 < decay < 0.9
|
||||
|
||||
|
||||
class TestEmotionAnalyzer:
|
||||
"""Test emotion analysis"""
|
||||
|
||||
def test_extract_high_intensity(self):
|
||||
analyzer = EmotionAnalyzer()
|
||||
text = "这件事很重要,我急需解决!"
|
||||
|
||||
matched = analyzer.extract(text)
|
||||
|
||||
assert "很重要" in matched or "急" in matched
|
||||
|
||||
def test_extract_worry(self):
|
||||
analyzer = EmotionAnalyzer()
|
||||
text = "我很担心这个问题"
|
||||
|
||||
matched = analyzer.extract(text)
|
||||
|
||||
assert "担心" in matched
|
||||
|
||||
def test_extract_no_emotion(self):
|
||||
analyzer = EmotionAnalyzer()
|
||||
text = "今天天气不错"
|
||||
|
||||
matched = analyzer.extract(text)
|
||||
|
||||
assert len(matched) == 0
|
||||
|
||||
def test_extract_urgency_pattern(self):
|
||||
analyzer = EmotionAnalyzer()
|
||||
text = "马上要迟到了"
|
||||
|
||||
matched = analyzer.extract(text)
|
||||
|
||||
assert any("URGENCY" in m for m in matched)
|
||||
|
||||
def test_calculate_score_high(self):
|
||||
analyzer = EmotionAnalyzer()
|
||||
memory = create_mock_memory(content="这件事非常重要,急需解决!")
|
||||
|
||||
score = analyzer.calculate_score(memory)
|
||||
|
||||
assert score >= 0.9
|
||||
|
||||
def test_calculate_score_neutral(self):
|
||||
analyzer = EmotionAnalyzer()
|
||||
memory = create_mock_memory(content="今天吃了苹果")
|
||||
|
||||
score = analyzer.calculate_score(memory)
|
||||
|
||||
assert score == 0.0
|
||||
|
||||
def test_calculate_score_from_emotion_tags(self):
|
||||
analyzer = EmotionAnalyzer()
|
||||
memory = create_mock_memory(emotion_tags=["急", "很重要"])
|
||||
|
||||
score = analyzer.calculate_score(memory)
|
||||
|
||||
assert score >= 0.9
|
||||
|
||||
def test_get_emotion_profile(self):
|
||||
analyzer = EmotionAnalyzer()
|
||||
text = "我很担心这个问题,必须马上解决"
|
||||
|
||||
profile = analyzer.get_emotion_profile(text)
|
||||
|
||||
assert "matched_keywords" in profile
|
||||
assert "max_weight" in profile
|
||||
assert profile["max_weight"] > 0
|
||||
|
||||
|
||||
class TestImpactEvaluator:
|
||||
"""Test impact evaluation"""
|
||||
|
||||
def test_evaluate_no_topics(self):
|
||||
evaluator = ImpactEvaluator()
|
||||
memory = create_mock_memory(associated_topics=[])
|
||||
|
||||
score = evaluator.evaluate(memory)
|
||||
|
||||
assert score == 0.0
|
||||
|
||||
def test_evaluate_single_topic(self):
|
||||
evaluator = ImpactEvaluator()
|
||||
memory = create_mock_memory(associated_topics=["工作"])
|
||||
|
||||
score = evaluator.evaluate(memory)
|
||||
|
||||
# 1 topic / 5 threshold = 0.2
|
||||
assert score == 0.2
|
||||
|
||||
def test_evaluate_full_topics(self):
|
||||
evaluator = ImpactEvaluator()
|
||||
memory = create_mock_memory(associated_topics=["工作", "健康", "家庭", "财务", "爱好"])
|
||||
|
||||
score = evaluator.evaluate(memory)
|
||||
|
||||
# 5 topics / 5 threshold = 1.0
|
||||
assert score == 1.0
|
||||
|
||||
def test_evaluate_over_threshold(self):
|
||||
evaluator = ImpactEvaluator()
|
||||
memory = create_mock_memory(associated_topics=["a", "b", "c", "d", "e", "f", "g"])
|
||||
|
||||
score = evaluator.evaluate(memory)
|
||||
|
||||
# Capped at 1.0
|
||||
assert score == 1.0
|
||||
|
||||
def test_get_topic_overlap(self):
|
||||
evaluator = ImpactEvaluator()
|
||||
memory_a = create_mock_memory(associated_topics=["工作", "健康", "家庭"])
|
||||
memory_b = create_mock_memory(associated_topics=["工作", "健康", "爱好"])
|
||||
|
||||
overlap = evaluator.get_topic_overlap(memory_a, memory_b)
|
||||
|
||||
# Intersection: {工作, 健康} = 2, Union: {工作, 健康, 家庭, 爱好} = 4
|
||||
# 2/4 = 0.5
|
||||
assert overlap == 0.5
|
||||
|
||||
def test_rank_by_impact(self):
|
||||
evaluator = ImpactEvaluator()
|
||||
memory_low = create_mock_memory(associated_topics=["a"])
|
||||
memory_high = create_mock_memory(associated_topics=["a", "b", "c", "d", "e"])
|
||||
|
||||
ranked = evaluator.rank_by_impact([memory_low, memory_high])
|
||||
|
||||
assert ranked[0] == memory_high
|
||||
assert ranked[1] == memory_low
|
||||
|
||||
|
||||
class TestImportanceScorer:
|
||||
"""Test composite importance scoring"""
|
||||
|
||||
def test_calculate_score_fresh_memory(self):
|
||||
scorer = ImportanceScorer()
|
||||
memory = create_mock_memory(
|
||||
frequency_count=0,
|
||||
last_recalled_at=None,
|
||||
content="今天吃了苹果",
|
||||
associated_topics=[],
|
||||
)
|
||||
|
||||
score = scorer.calculate_score(memory)
|
||||
|
||||
# All zeros, should be ~0
|
||||
assert score < 0.1
|
||||
|
||||
def test_calculate_score_high_frequency(self):
|
||||
scorer = ImportanceScorer()
|
||||
memory = create_mock_memory(
|
||||
frequency_count=10,
|
||||
last_recalled_at=datetime.now(UTC),
|
||||
content="工作相关",
|
||||
associated_topics=["工作"],
|
||||
)
|
||||
|
||||
score = scorer.calculate_score(memory)
|
||||
|
||||
# High frequency (log(11)/log(11) ≈ 1.0) * 0.35 + recency * 0.20 + emotion * 0.25 + impact * 0.20
|
||||
# ≈ 0.35 + 0.20 + 0 + 0.04 = 0.59
|
||||
assert score > 0.5
|
||||
|
||||
def test_calculate_score_with_emotion(self):
|
||||
scorer = ImportanceScorer()
|
||||
memory = create_mock_memory(
|
||||
frequency_count=1,
|
||||
content="这件事很重要,我急需解决!",
|
||||
associated_topics=["工作"],
|
||||
)
|
||||
|
||||
score = scorer.calculate_score(memory)
|
||||
|
||||
# Emotion score ~0.9 * 0.25 = 0.225
|
||||
assert score > 0.2
|
||||
|
||||
def test_calculate_score_high_all_factors(self):
|
||||
scorer = ImportanceScorer()
|
||||
memory = create_mock_memory(
|
||||
frequency_count=10,
|
||||
last_recalled_at=datetime.now(UTC),
|
||||
content="这个问题非常紧急,必须马上处理!",
|
||||
associated_topics=["工作", "健康", "家庭", "财务", "爱好"],
|
||||
)
|
||||
|
||||
score = scorer.calculate_score(memory)
|
||||
|
||||
# All factors high
|
||||
assert score > 0.7
|
||||
|
||||
def test_get_importance_level_high(self):
|
||||
scorer = ImportanceScorer()
|
||||
|
||||
level = scorer.get_importance_level(0.85)
|
||||
|
||||
assert level == ImportanceLevel.HIGH
|
||||
|
||||
def test_get_importance_level_medium(self):
|
||||
scorer = ImportanceScorer()
|
||||
|
||||
level = scorer.get_importance_level(0.6)
|
||||
|
||||
assert level == ImportanceLevel.MEDIUM
|
||||
|
||||
def test_get_importance_level_low(self):
|
||||
scorer = ImportanceScorer()
|
||||
|
||||
level = scorer.get_importance_level(0.3)
|
||||
|
||||
assert level == ImportanceLevel.LOW
|
||||
|
||||
def test_should_escalate_by_score(self):
|
||||
scorer = ImportanceScorer()
|
||||
memory = create_mock_memory(
|
||||
frequency_count=10,
|
||||
last_recalled_at=datetime.now(UTC),
|
||||
content="紧急!非常重要!",
|
||||
associated_topics=["a", "b", "c", "d", "e"],
|
||||
)
|
||||
|
||||
result = scorer.should_escalate(memory)
|
||||
|
||||
# With high freq (0.35) + recent (0.20) + emotion (0.25) + many topics (0.20) = 1.0
|
||||
assert result is True
|
||||
|
||||
def test_should_escalate_by_emotion(self):
|
||||
scorer = ImportanceScorer()
|
||||
memory = create_mock_memory(
|
||||
frequency_count=1,
|
||||
content="紧急!非常重要!",
|
||||
associated_topics=[],
|
||||
)
|
||||
|
||||
result = scorer.should_escalate(memory)
|
||||
|
||||
# Emotion intensity alone triggers escalation
|
||||
assert result is True
|
||||
|
||||
def test_should_not_escalate(self):
|
||||
scorer = ImportanceScorer()
|
||||
memory = create_mock_memory(
|
||||
frequency_count=0,
|
||||
content="今天天气不错",
|
||||
associated_topics=[],
|
||||
)
|
||||
|
||||
result = scorer.should_escalate(memory)
|
||||
|
||||
assert result is False
|
||||
|
||||
def test_score_and_classify(self):
|
||||
scorer = ImportanceScorer()
|
||||
memory = create_mock_memory(frequency_count=10, associated_topics=["a", "b", "c", "d", "e"])
|
||||
|
||||
score, level = scorer.score_and_classify(memory)
|
||||
|
||||
assert isinstance(score, float)
|
||||
assert 0.0 <= score <= 1.0
|
||||
assert level in ImportanceLevel
|
||||
|
||||
def test_update_memory_importance(self):
|
||||
scorer = ImportanceScorer()
|
||||
memory = create_mock_memory(
|
||||
frequency_count=10,
|
||||
last_recalled_at=datetime.now(UTC),
|
||||
content="这个问题非常重要!",
|
||||
associated_topics=["工作", "健康"],
|
||||
)
|
||||
|
||||
result = scorer.update_memory_importance(memory)
|
||||
|
||||
assert result.importance_score is not None
|
||||
assert result.importance_level in ["high", "medium", "low"]
|
||||
|
||||
|
||||
class TestImportanceLevel:
|
||||
"""Test ImportanceLevel enum"""
|
||||
|
||||
def test_level_values(self):
|
||||
assert ImportanceLevel.HIGH.value == "high"
|
||||
assert ImportanceLevel.MEDIUM.value == "medium"
|
||||
assert ImportanceLevel.LOW.value == "low"
|
||||
|
||||
def test_is_string_enum(self):
|
||||
assert isinstance(ImportanceLevel.HIGH, str)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
Reference in New Issue
Block a user