digest: daily + weekly prompt builders (NOVICE/INTERMEDIATE)

This commit is contained in:
Giorgio Gilestro 2026-05-25 22:57:29 +02:00
parent 51efccd3b1
commit ca6b174b51
2 changed files with 143 additions and 1 deletions

View file

@ -30,7 +30,8 @@ OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions"
# v7 (2026-05-18): Forbid "(Updated HH:MM UTC)" clauses in the date header — # v7 (2026-05-18): Forbid "(Updated HH:MM UTC)" clauses in the date header —
# the model was hallucinating future times. The user prompt now carries the # the model was hallucinating future times. The user prompt now carries the
# actual current UTC time so the model has accurate temporal context. # actual current UTC time so the model has accurate temporal context.
PROMPT_VERSION = 8 # v9 (2026-05-25): Adds daily + weekly digest prompt builders for email.
PROMPT_VERSION = 9
# --- Core: invariant across tone/analysis settings ---------------------------- # --- Core: invariant across tone/analysis settings ----------------------------
@ -507,6 +508,99 @@ def build_user_prompt(
return "\n".join(parts) return "\n".join(parts)
def build_daily_digest_prompt(
*,
tone: str,
today,
quotes_by_group: dict,
headlines_by_bucket: dict,
reference_line: str,
) -> tuple[str, str]:
"""System + user prompt for the once-a-day editorial digest.
Different from the hourly log: the daily digest reflects on the past
24h and looks forward to the upcoming session. Longer, less
'live-blogging,' more contextual. Target ~600 words."""
tone_clause = (
"Use plain English. Define any jargon on first use."
if tone.upper() == "NOVICE"
else "Write for a reader who already speaks markets fluently."
)
system = (
"You write the daily editorial digest for Read the Markets. "
f"Audience tone: {tone.upper()}. {tone_clause} "
"Cover: (1) what mattered yesterday, (2) what to watch in today's "
"EU and US sessions, (3) one cross-asset thread connecting them. "
"No predictions of price level, no buy/sell language. Target ~600 "
"words. Output HTML using only <p>, <h3>, <ul>, <li>, <strong>, "
"<em> — no <html>, <head>, or <body> wrapper, no inline styles."
)
user = _digest_user_prompt(today, quotes_by_group, headlines_by_bucket, reference_line)
return system, user
def build_weekly_digest_prompt(
*,
tone: str,
today,
quotes_by_group: dict,
headlines_by_bucket: dict,
reference_line: str,
) -> tuple[str, str]:
"""System + user prompt for the Sunday weekly recap + look-ahead.
Sent to ALL opt-in users (free and paid). Target ~900 words."""
tone_clause = (
"Use plain English. Define any jargon on first use."
if tone.upper() == "NOVICE"
else "Write for a reader who already speaks markets fluently."
)
system = (
"You write the Sunday weekly digest for Read the Markets. "
f"Audience tone: {tone.upper()}. {tone_clause} "
"Cover: (1) the week behind — what moved and why, "
"(2) the week ahead — releases, earnings, central-bank meetings, "
"(3) the cross-asset story to keep in mind. "
"No predictions of price level, no buy/sell language. Target ~900 "
"words. Output HTML using only <p>, <h3>, <ul>, <li>, <strong>, "
"<em> — no <html>, <head>, or <body> wrapper, no inline styles."
)
user = _digest_user_prompt(today, quotes_by_group, headlines_by_bucket, reference_line)
return system, user
def _digest_user_prompt(today, quotes_by_group, headlines_by_bucket, reference_line):
"""Shared user-message body used by both digest prompts. Same data
shape as the hourly user prompt; reformatted for the digest context."""
today_str = today.strftime("%A %d %B %Y") if hasattr(today, "strftime") else str(today)
lines = [f"TODAY (UTC): {today_str}", "", f"REFERENCE: {reference_line}", ""]
if headlines_by_bucket:
lines.append("HEADLINES BY CATEGORY")
for cat, items in headlines_by_bucket.items():
lines.append(f" [{cat}]")
for h in items[:30]:
when = h.get("when", "")
src = h.get("source", "")
title = h.get("title", "")
lines.append(f" {when} · {src} · {title}")
lines.append("")
if quotes_by_group:
lines.append("LATEST QUOTES BY GROUP")
for grp, items in quotes_by_group.items():
lines.append(f" [{grp}]")
for q in items[:30]:
sym = q.get("symbol", "")
price = q.get("price", "")
lbl = q.get("label", "")
ccy = q.get("currency", "")
lines.append(f" {sym} ({lbl}) — {price} {ccy}")
lines.append("")
return "\n".join(lines)
def _provider_chain() -> list[str]: def _provider_chain() -> list[str]:
"""Ordered list of providers to try: primary, then fallback (unless """Ordered list of providers to try: primary, then fallback (unless
the fallback is unset, the same as primary, or has no API key).""" the fallback is unset, the same as primary, or has no API key)."""

View file

@ -0,0 +1,48 @@
"""Unit tests for the daily / weekly digest prompt builders."""
from __future__ import annotations
from datetime import datetime, timezone
from app.services.openrouter 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