openrouter.py was 790 lines mixing two orthogonal concerns: - Prompt engineering (build_system_prompt, build_summary_*, build_chat_*, build_daily_digest_*, etc.) — ~400 lines, changes weekly as PROMPT_VERSION bumps - LLM transport (call_llm, _provider_chain, _call_provider, retry + fallback machinery) — ~250 lines, rarely changes Extracted the prompt-engineering surface to app/services/llm_prompts.py. Transport stays in openrouter.py (consistent with the filename — the OpenRouter URL is the transport's anchor). All import sites (jobs, routers, services, tests) split their multi-import lines into two: prompt-things from llm_prompts, transport from openrouter. PROMPT_VERSION constant, _TONE_ALIASES, _resolve_tone, and SYSTEM_PROMPT moved with the prompt functions. No behaviour change — pure relocation. Function signatures, body, and naming all preserved. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
64 lines
2.4 KiB
Python
64 lines
2.4 KiB
Python
"""Unit tests for the daily / weekly digest prompt builders."""
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime, timezone
|
|
|
|
from app.services.llm_prompts import (
|
|
build_daily_digest_prompt,
|
|
build_weekly_digest_prompt,
|
|
)
|
|
|
|
|
|
def _ctx():
|
|
return dict(
|
|
today=datetime(2026, 5, 25, 6, 30, tzinfo=timezone.utc),
|
|
quotes_by_group={"equities": [{"symbol": "SPX", "price": 7500.0,
|
|
"label": "S&P 500", "currency": "USD",
|
|
"source": "test", "note": "",
|
|
"as_of": None, "changes": {}}]},
|
|
headlines_by_bucket={"general": [{"when": "2026-05-25T05:00:00+00:00",
|
|
"source": "FT", "title": "Brent slides"}]},
|
|
reference_line="S&P 7,501 (ATH) · VIX 18.0 · US 10y 4.45%",
|
|
)
|
|
|
|
|
|
def test_daily_prompt_tone_intermediate():
|
|
sys_, usr = build_daily_digest_prompt(tone="INTERMEDIATE", **_ctx())
|
|
assert "INTERMEDIATE" in sys_.upper() or "intermediate" in sys_.lower()
|
|
assert "Brent slides" in usr
|
|
assert "daily" in sys_.lower()
|
|
|
|
|
|
def test_daily_prompt_tone_novice_differs():
|
|
sys_int, _ = build_daily_digest_prompt(tone="INTERMEDIATE", **_ctx())
|
|
sys_nov, _ = build_daily_digest_prompt(tone="NOVICE", **_ctx())
|
|
assert sys_int != sys_nov
|
|
|
|
|
|
def test_weekly_prompt_mentions_week():
|
|
sys_, usr = build_weekly_digest_prompt(tone="INTERMEDIATE", **_ctx())
|
|
assert "week" in sys_.lower() or "weekly" in sys_.lower()
|
|
assert "Brent slides" in usr
|
|
|
|
|
|
def test_prompts_return_strings():
|
|
for fn in (build_daily_digest_prompt, build_weekly_digest_prompt):
|
|
sys_, usr = fn(tone="INTERMEDIATE", **_ctx())
|
|
assert isinstance(sys_, str) and isinstance(usr, str)
|
|
assert len(sys_) > 50 and len(usr) > 50
|
|
|
|
|
|
def test_prompts_tolerate_empty_data():
|
|
"""No quotes, no headlines — builders must still produce non-empty
|
|
prompts without raising. Guards the `if headlines_by_bucket` and
|
|
`if quotes_by_group` branches in _digest_user_prompt."""
|
|
empty_ctx = dict(
|
|
today=datetime(2026, 5, 25, 6, 30, tzinfo=timezone.utc),
|
|
quotes_by_group={},
|
|
headlines_by_bucket={},
|
|
reference_line="S&P 7,501 (ATH)",
|
|
)
|
|
for fn in (build_daily_digest_prompt, build_weekly_digest_prompt):
|
|
sys_, usr = fn(tone="INTERMEDIATE", **empty_ctx)
|
|
assert "S&P 7,501" in usr
|
|
assert len(sys_) > 50
|