backend: dedupe shared logic (indicator_summary_job, CHAT_REFERENCE_LINE, call_openrouter alias)

- indicator_summary_job.py imported its own copies of _month_spend and
  _latest_quotes_by_group; _market_context.py already exposes these.
  Switched to the canonical imports. Also fixed _market_context's
  latest_quotes_by_group to actually filter null prices (it claimed to
  in its docstring but lacked the WHERE clause).
- api.py duplicated REFERENCE_LINE as CHAT_REFERENCE_LINE — same string,
  two sources of truth. Now imports REFERENCE_LINE.
- Chat endpoint used the deprecated `call_openrouter` alias and passed
  an explicit `model=` that bypassed the provider chain. Switched to
  `call_llm` with default model selection, then removed the alias.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Giorgio Gilestro 2026-05-27 19:30:11 +02:00
parent a2bcb2c053
commit b47c45e218
4 changed files with 10 additions and 48 deletions

View file

@ -42,6 +42,7 @@ async def latest_quotes_by_group(session) -> dict[str, list[dict]]:
& (Quote.symbol == sub.c.symbol) & (Quote.symbol == sub.c.symbol)
& (Quote.fetched_at == sub.c.mx), & (Quote.fetched_at == sub.c.mx),
) )
.where(Quote.price.is_not(None))
.order_by(Quote.group_name, Quote.symbol) .order_by(Quote.group_name, Quote.symbol)
) )
rows = (await session.execute(stmt)).scalars().all() rows = (await session.execute(stmt)).scalars().all()

View file

@ -5,7 +5,6 @@ from __future__ import annotations
import asyncio import asyncio
import re import re
from collections import defaultdict
import httpx import httpx
from sqlalchemy import desc, func, select from sqlalchemy import desc, func, select
@ -13,7 +12,8 @@ from sqlalchemy import desc, func, select
from app.config import get_settings, load_groups from app.config import get_settings, load_groups
from app.db import utcnow from app.db import utcnow
from app.jobs._helpers import job_lifecycle, log from app.jobs._helpers import job_lifecycle, log
from app.models import AICall, IndicatorSummary, JobRun, Quote from app.jobs._market_context import latest_quotes_by_group, month_spend
from app.models import AICall, IndicatorSummary, JobRun
from app.services.cadence import DEFAULT_POLICY from app.services.cadence import DEFAULT_POLICY
from app.services.openrouter import ( from app.services.openrouter import (
PROMPT_VERSION, PROMPT_VERSION,
@ -136,40 +136,6 @@ def clean_summary(text: str) -> str:
return out return out
async def _latest_quotes_by_group(session) -> dict[str, list[dict]]:
"""Latest non-null quote per (group, symbol). Drops error rows."""
sub = (
select(Quote.group_name, Quote.symbol,
func.max(Quote.fetched_at).label("mx"))
.group_by(Quote.group_name, Quote.symbol)
.subquery()
)
rows = (await session.execute(
select(Quote).join(
sub,
(Quote.group_name == sub.c.group_name)
& (Quote.symbol == sub.c.symbol)
& (Quote.fetched_at == sub.c.mx),
).where(Quote.price.is_not(None))
.order_by(Quote.group_name, Quote.symbol)
)).scalars().all()
by_group: dict[str, list[dict]] = defaultdict(list)
for q in rows:
by_group[q.group_name].append({
"symbol": q.symbol, "label": q.label,
"price": q.price, "currency": q.currency,
"as_of": q.as_of, "changes": q.changes,
})
return by_group
async def _month_spend(session) -> float:
total = (await session.execute(
select(func.coalesce(func.sum(AICall.cost_usd), 0.0))
.where(AICall.called_at >= month_start())
)).scalar()
return float(total or 0.0)
async def _generate_one( async def _generate_one(
session, client: httpx.AsyncClient, group: str, quotes: list[dict], session, client: httpx.AsyncClient, group: str, quotes: list[dict],
@ -254,13 +220,13 @@ async def run() -> None:
jr.error = reason jr.error = reason
return return
spent = await _month_spend(session) spent = await month_spend(session)
if spent >= s.OPENROUTER_MONTHLY_CAP_USD: if spent >= s.OPENROUTER_MONTHLY_CAP_USD:
jr.status = "skipped" jr.status = "skipped"
jr.error = f"monthly cap reached (${spent:.2f})" jr.error = f"monthly cap reached (${spent:.2f})"
return return
groups = await _latest_quotes_by_group(session) groups = await latest_quotes_by_group(session)
# Only summarise groups currently configured in TOML — drops stale # Only summarise groups currently configured in TOML — drops stale
# group names (e.g. an old "pie" before T212 sourcing) that still have # group names (e.g. an old "pie" before T212 sourcing) that still have
# quotes in the table but no UI presence. # quotes in the table but no UI presence.

View file

@ -24,10 +24,11 @@ from app.auth import require_token, maybe_current_user, CurrentUser
from app.services.i18n import ACTIVE_LANGUAGES from app.services.i18n import ACTIVE_LANGUAGES
from app.config import get_settings from app.config import get_settings
from app.db import get_session, utcnow from app.db import get_session, utcnow
from app.jobs._market_context import REFERENCE_LINE
from app.services.openrouter import ( from app.services.openrouter import (
PROMPT_VERSION, PROMPT_VERSION,
build_chat_system_prompt, build_chat_system_prompt,
call_openrouter, call_llm,
month_start, month_start,
) )
from app.templates_env import templates from app.templates_env import templates
@ -710,10 +711,7 @@ class ChatRequest(BaseModel):
messages: list[ChatMessage] messages: list[ChatMessage]
CHAT_REFERENCE_LINE = (
"S&P 7,501 (ATH) · VIX 18.0 · US 10y 4.45% · HY OAS 279bps · "
"Brent $109/bbl · Gold $4,651/oz · CPI 3.8% YoY"
)
THESIS_KEYWORDS_FALLBACK = [ THESIS_KEYWORDS_FALLBACK = [
"hormuz", "iran", "opec", "brent", "wti", "crude", "oil", "hormuz", "iran", "opec", "brent", "wti", "crude", "oil",
"china", "taiwan", "yuan", "fed", "inflation", "cpi", "yield", "china", "taiwan", "yuan", "fed", "inflation", "cpi", "yield",
@ -822,7 +820,7 @@ async def chat(
log_generated_at=log_row.generated_at if log_row else None, log_generated_at=log_row.generated_at if log_row else None,
quotes_by_group=quotes, quotes_by_group=quotes,
headlines=headlines, headlines=headlines,
reference_line=CHAT_REFERENCE_LINE, reference_line=REFERENCE_LINE,
) )
msgs = [{"role": "system", "content": system_prompt}] msgs = [{"role": "system", "content": system_prompt}]
@ -831,7 +829,7 @@ async def chat(
try: try:
async with httpx.AsyncClient(follow_redirects=True) as client: async with httpx.AsyncClient(follow_redirects=True) as client:
result = await call_openrouter(client, msgs, model=s.OPENROUTER_MODEL) result = await call_llm(client, msgs)
except Exception as e: except Exception as e:
session.add(AICall( session.add(AICall(
model=s.OPENROUTER_MODEL, status="error", error=str(e)[:500], model=s.OPENROUTER_MODEL, status="error", error=str(e)[:500],

View file

@ -775,9 +775,6 @@ async def call_llm(
raise last_exc raise last_exc
# Back-compat alias for any straggling import sites.
call_openrouter = call_llm
def month_window() -> tuple[datetime, datetime]: def month_window() -> tuple[datetime, datetime]:
"""[start, now] in UTC for the current calendar month.""" """[start, now] in UTC for the current calendar month."""