digest: daily + weekly prompt builders (NOVICE/INTERMEDIATE)
This commit is contained in:
parent
51efccd3b1
commit
ca6b174b51
2 changed files with 143 additions and 1 deletions
|
|
@ -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)."""
|
||||||
|
|
|
||||||
48
tests/test_digest_prompts.py
Normal file
48
tests/test_digest_prompts.py
Normal 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
|
||||||
Loading…
Add table
Add a link
Reference in a new issue