Commit graph

8 commits

Author SHA1 Message Date
f9534f7ad6 review: gate strategic-log, portfolio, chat, and digest on reviewer
Extends the reviewer agent — previously only protecting indicator
summaries — to every AI-generated surface that reaches a user. The
reviewer's prompt already rejects scratchpad, truncation,
meta-commentary, and (since a6e476b) financial advice; wiring it in
turns those rules from prompt-level "asks" into structural gates.

Four call sites updated:

- ai_log_job.run() : after each tone/analysis variant is generated,
  pass through review_read. On reject, log the reason and skip the
  StrategicLog insert; the API's existing "latest StrategicLog" lookup
  falls back to the previous clean log.

- services/portfolio_analysis.analyse() : on reject, raise a clean
  RuntimeError that the /api/analyze router already maps to HTTP 502
  with a retry-able message. Portfolio analysis isn't cached server-
  side, so the user retries; the reviewer's verdict reason goes into
  the AICall ledger as the leaked-status row's error column.

- routers/chat.chat() : on reject, instead of returning the raw
  assistant content we return a short refusal explaining the limit
  and inviting a rephrase. Adds ~1-2 s of latency per turn (one extra
  LLM call to Haiku) — the only user-facing latency tax.

- jobs/email_digest_job._generate_variants() : on reject, the variant
  is dropped for the cycle. Recipients on the rejected tone get no
  digest email this run, which is better than delivering inbox copy
  that drifts into advice (emails are unrecallable once sent).

In every case the AICall ledger row records the reviewer cost so
month_spend stays accurate across all paths.

The reviewer system prompt is slightly generalised to cover both the
indicator-summary case and the longer-form log/digest/chat case:
- removes "short interpretive read" framing
- softens the "any question" rule so genuine rhetorical structure in
  a long-form log doesn't trigger a reject

tests/conftest.py grows an autouse fixture that stubs review_read to
clean=True in every consumer module. Tests that mock the generator
shouldn't have to also mock the safety gate behind it; tests that
specifically want the reject branch can override with their own
monkeypatch. test_output_review.py is unaffected — it imports
review_read directly.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 14:40:04 +02:00
b055eea1c2 email: split digest renderer to digest_email.py
email_service.py was 428 lines covering three different concerns:
SMTP transport, OTP/welcome rendering (tightly coupled — same brand
template + theme), and digest rendering (a totally different shape
of email, different layout, different copy cadence). The two halves
changed at different cadences and made the file noisy to navigate.

Extracted render_digest_email + _DIGEST_HTML_TEMPLATE +
_strip_html_to_text to app/services/digest_email.py. SMTP transport
and the OTP/welcome surface stay in email_service.py.

Import sites updated: email_digest_job and test_email_render now
import render_digest_email from digest_email. The OTP/welcome
import sites (auth router, branding tests, test_email_service) are
untouched.

No behaviour change — pure relocation. Templates byte-identical.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 21:33:06 +02:00
4adc8dfe82 openrouter: split into llm_prompts (prompt engineering) + transport
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>
2026-05-27 21:27:23 +02:00
924f37548b digest: translate variants once per active non-en language
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 17:07:18 +02:00
82e529b6b2 jobs: extract shared market-context helpers from ai_log_job
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 23:18:27 +02:00
ce4b19dbb8 digest: record AICall rows so digest LLM spend counts toward monthly cap
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 23:17:14 +02:00
1fdf7d6a23 digest: fall back to any successful tone variant, not just INTERMEDIATE 2026-05-25 23:14:46 +02:00
2462882006 digest: daily/weekly job w/ EmailSend idempotency
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 23:13:24 +02:00