feat(briefing): add rss_classifier service for LLM-based topic tagging
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -40,3 +40,91 @@ def test_extract_item_prefers_content_over_summary():
|
||||
entry.published_parsed = None
|
||||
item = extract_item(entry)
|
||||
assert item["content"] == "Full content here"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_classify_items_batch_returns_topic_map():
|
||||
"""classify_items_batch should return a dict mapping item_id to topic list."""
|
||||
from unittest.mock import AsyncMock
|
||||
from fabledassistant.services.rss_classifier import classify_items_batch
|
||||
|
||||
fake_response = '{"1": ["technology", "ai"], "2": ["politics"]}'
|
||||
|
||||
with patch(
|
||||
"fabledassistant.services.rss_classifier._llm_classify",
|
||||
new_callable=AsyncMock,
|
||||
return_value=fake_response,
|
||||
):
|
||||
items = [
|
||||
{"id": 1, "title": "OpenAI releases GPT-5", "content": "..."},
|
||||
{"id": 2, "title": "EU passes new law", "content": "..."},
|
||||
]
|
||||
result = await classify_items_batch(items, user_include_topics=[])
|
||||
assert result[1] == ["technology", "ai"]
|
||||
assert result[2] == ["politics"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_classify_items_batch_handles_llm_failure():
|
||||
"""classify_items_batch should return empty dict on LLM error."""
|
||||
from unittest.mock import AsyncMock
|
||||
from fabledassistant.services.rss_classifier import classify_items_batch
|
||||
|
||||
with patch(
|
||||
"fabledassistant.services.rss_classifier._llm_classify",
|
||||
new_callable=AsyncMock,
|
||||
side_effect=Exception("LLM unavailable"),
|
||||
):
|
||||
items = [{"id": 5, "title": "Some news", "content": ""}]
|
||||
result = await classify_items_batch(items, user_include_topics=[])
|
||||
assert result == {}
|
||||
|
||||
|
||||
def test_score_rss_items_excludes_blacklisted_topics():
|
||||
"""Items with excluded topics should be removed."""
|
||||
from fabledassistant.services.briefing_preferences import score_and_filter_items
|
||||
|
||||
items = [
|
||||
{"id": 1, "title": "Tech news", "topics": ["technology"], "published_at": "2026-03-25T08:00:00"},
|
||||
{"id": 2, "title": "Sports score", "topics": ["sports"], "published_at": "2026-03-25T08:00:00"},
|
||||
]
|
||||
result = score_and_filter_items(
|
||||
items,
|
||||
include_topics=["technology"],
|
||||
exclude_topics=["sports"],
|
||||
topic_scores={},
|
||||
max_items=10,
|
||||
)
|
||||
ids = [r["id"] for r in result]
|
||||
assert 1 in ids
|
||||
assert 2 not in ids
|
||||
|
||||
|
||||
def test_score_rss_items_boosts_included_topics():
|
||||
"""Items matching include_topics should rank higher than neutral items."""
|
||||
from fabledassistant.services.briefing_preferences import score_and_filter_items
|
||||
|
||||
items = [
|
||||
{"id": 1, "title": "Random news", "topics": ["other"], "published_at": "2026-03-25T07:00:00"},
|
||||
{"id": 2, "title": "Tech news", "topics": ["technology"], "published_at": "2026-03-25T06:00:00"},
|
||||
]
|
||||
result = score_and_filter_items(
|
||||
items,
|
||||
include_topics=["technology"],
|
||||
exclude_topics=[],
|
||||
topic_scores={},
|
||||
max_items=10,
|
||||
)
|
||||
assert result[0]["id"] == 2
|
||||
|
||||
|
||||
def test_score_rss_items_no_preferences_returns_all():
|
||||
"""With no preferences, all items should be returned sorted by recency."""
|
||||
from fabledassistant.services.briefing_preferences import score_and_filter_items
|
||||
|
||||
items = [
|
||||
{"id": 1, "title": "A", "topics": [], "published_at": "2026-03-24T10:00:00"},
|
||||
{"id": 2, "title": "B", "topics": [], "published_at": "2026-03-25T10:00:00"},
|
||||
]
|
||||
result = score_and_filter_items(items, [], [], {}, max_items=10)
|
||||
assert result[0]["id"] == 2 # Newer first
|
||||
|
||||
Reference in New Issue
Block a user