Add explain analysis workflow and UI
This commit is contained in:
174
backend/tests/test_news_enricher.py
Normal file
174
backend/tests/test_news_enricher.py
Normal file
@@ -0,0 +1,174 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from backend.enrich import news_enricher
|
||||
|
||||
|
||||
def test_classify_news_row_falls_back_to_local_rules(monkeypatch):
|
||||
monkeypatch.setattr(news_enricher, "analyze_news_row_with_llm", lambda row: None)
|
||||
result = news_enricher.classify_news_row(
|
||||
{
|
||||
"title": "Apple shares drop after weak guidance",
|
||||
"summary": "Investors reacted negatively to softer-than-expected outlook.",
|
||||
}
|
||||
)
|
||||
assert result["analysis_source"] == "local"
|
||||
assert result["sentiment"] == "negative"
|
||||
assert result["summary"]
|
||||
|
||||
|
||||
def test_classify_news_row_prefers_llm_when_available(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
news_enricher,
|
||||
"analyze_news_row_with_llm",
|
||||
lambda row: {
|
||||
"relevance": "high",
|
||||
"sentiment": "positive",
|
||||
"key_discussion": "Demand resilience",
|
||||
"summary": "LLM summary",
|
||||
"reason_growth": "Orders remain strong",
|
||||
"reason_decrease": "",
|
||||
"raw_json": {"provider": "llm"},
|
||||
},
|
||||
)
|
||||
result = news_enricher.classify_news_row(
|
||||
{
|
||||
"title": "Apple expands AI features",
|
||||
"summary": "New devices and software updates were announced.",
|
||||
}
|
||||
)
|
||||
assert result["analysis_source"] == "llm"
|
||||
assert result["sentiment"] == "positive"
|
||||
assert result["summary"] == "LLM summary"
|
||||
|
||||
|
||||
def test_build_analysis_rows_prefers_batch_llm_and_dedupes(monkeypatch):
|
||||
monkeypatch.setattr(news_enricher, "llm_enrichment_enabled", lambda: True)
|
||||
monkeypatch.setattr(news_enricher, "get_env_int", lambda key, default=0: 8)
|
||||
monkeypatch.setattr(
|
||||
news_enricher,
|
||||
"analyze_news_rows_with_llm",
|
||||
lambda rows: {
|
||||
"news-1": {
|
||||
"relevance": "high",
|
||||
"sentiment": "positive",
|
||||
"key_discussion": "Batch result",
|
||||
"summary": "Batch summary",
|
||||
"reason_growth": "Growth",
|
||||
"reason_decrease": "",
|
||||
"raw_json": {"provider": "batch"},
|
||||
}
|
||||
},
|
||||
)
|
||||
monkeypatch.setattr(news_enricher, "analyze_news_row_with_llm", lambda row: None)
|
||||
rows = news_enricher.build_analysis_rows(
|
||||
symbol="AAPL",
|
||||
news_rows=[
|
||||
{"id": "news-1", "trade_date": "2026-03-10", "title": "Same title", "summary": "Same summary"},
|
||||
{"id": "news-2", "trade_date": "2026-03-10", "title": "Same title", "summary": "Same summary"},
|
||||
],
|
||||
ohlc_rows=[],
|
||||
)
|
||||
rows, stats = rows
|
||||
assert len(rows) == 1
|
||||
assert rows[0]["analysis_source"] == "llm"
|
||||
assert rows[0]["summary"] == "Batch summary"
|
||||
assert stats["deduped_count"] == 1
|
||||
assert stats["llm_count"] == 1
|
||||
|
||||
|
||||
def test_enrich_news_for_symbol_skips_existing(monkeypatch):
|
||||
class DummyStore:
|
||||
def get_news_items(self, symbol, start_date=None, end_date=None, limit=200):
|
||||
return [
|
||||
{"id": "news-1", "trade_date": "2026-03-10", "title": "One", "summary": "One"},
|
||||
{"id": "news-2", "trade_date": "2026-03-11", "title": "Two", "summary": "Two"},
|
||||
]
|
||||
|
||||
def get_analyzed_news_ids(self, symbol, start_date=None, end_date=None):
|
||||
return {"news-1"}
|
||||
|
||||
def get_ohlc(self, symbol, start_date, end_date):
|
||||
return []
|
||||
|
||||
def upsert_news_analysis(self, symbol, rows, analysis_source="local"):
|
||||
self.rows = rows
|
||||
return len(rows)
|
||||
|
||||
monkeypatch.setattr(
|
||||
news_enricher,
|
||||
"build_analysis_rows",
|
||||
lambda symbol, news_rows, ohlc_rows: (
|
||||
[
|
||||
{
|
||||
"news_id": row["id"],
|
||||
"trade_date": row["trade_date"],
|
||||
"summary": row["summary"],
|
||||
"analysis_source": "local",
|
||||
}
|
||||
for row in news_rows
|
||||
],
|
||||
{"deduped_count": 0, "llm_count": 0, "local_count": len(news_rows)},
|
||||
),
|
||||
)
|
||||
store = DummyStore()
|
||||
result = news_enricher.enrich_news_for_symbol(store, "AAPL")
|
||||
assert result["news_count"] == 2
|
||||
assert result["queued_count"] == 1
|
||||
assert result["skipped_existing_count"] == 1
|
||||
assert len(store.rows) == 1
|
||||
assert store.rows[0]["news_id"] == "news-2"
|
||||
|
||||
|
||||
def test_enrich_news_for_symbol_only_reanalyzes_local(monkeypatch):
|
||||
class DummyStore:
|
||||
def get_news_items(self, symbol, start_date=None, end_date=None, limit=200):
|
||||
return [
|
||||
{"id": "news-1", "trade_date": "2026-03-10", "title": "One", "summary": "One"},
|
||||
{"id": "news-2", "trade_date": "2026-03-11", "title": "Two", "summary": "Two"},
|
||||
{"id": "news-3", "trade_date": "2026-03-12", "title": "Three", "summary": "Three"},
|
||||
]
|
||||
|
||||
def get_analyzed_news_sources(self, symbol, start_date=None, end_date=None):
|
||||
return {"news-1": "local", "news-2": "llm"}
|
||||
|
||||
def get_ohlc(self, symbol, start_date, end_date):
|
||||
return []
|
||||
|
||||
def upsert_news_analysis(self, symbol, rows, analysis_source="local"):
|
||||
self.rows = rows
|
||||
return len(rows)
|
||||
|
||||
monkeypatch.setattr(
|
||||
news_enricher,
|
||||
"build_analysis_rows",
|
||||
lambda symbol, news_rows, ohlc_rows: (
|
||||
[
|
||||
{
|
||||
"news_id": row["id"],
|
||||
"trade_date": row["trade_date"],
|
||||
"summary": row["summary"],
|
||||
"analysis_source": "llm" if row["id"] == "news-1" else "local",
|
||||
}
|
||||
for row in news_rows
|
||||
],
|
||||
{"deduped_count": 0, "llm_count": 1, "local_count": 0},
|
||||
),
|
||||
)
|
||||
|
||||
store = DummyStore()
|
||||
result = news_enricher.enrich_news_for_symbol(
|
||||
store,
|
||||
"AAPL",
|
||||
only_reanalyze_local=True,
|
||||
)
|
||||
|
||||
assert result["news_count"] == 3
|
||||
assert result["queued_count"] == 1
|
||||
assert result["skipped_existing_count"] == 2
|
||||
assert result["only_reanalyze_local"] is True
|
||||
assert result["upgraded_local_to_llm_count"] == 1
|
||||
assert result["execution_summary"]["upgraded_dates"] == ["2026-03-10"]
|
||||
assert result["execution_summary"]["remaining_local_titles"] == []
|
||||
assert result["execution_summary"]["skipped_missing_analysis_count"] == 1
|
||||
assert result["execution_summary"]["skipped_non_local_count"] == 1
|
||||
assert [row["news_id"] for row in store.rows] == ["news-1"]
|
||||
Reference in New Issue
Block a user