From b47c45e2183e5ac7743f1602bd72df816f62532d Mon Sep 17 00:00:00 2001 From: Giorgio Gilestro Date: Wed, 27 May 2026 19:30:11 +0200 Subject: [PATCH] backend: dedupe shared logic (indicator_summary_job, CHAT_REFERENCE_LINE, call_openrouter alias) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- app/jobs/_market_context.py | 1 + app/jobs/indicator_summary_job.py | 42 +++---------------------------- app/routers/api.py | 12 ++++----- app/services/openrouter.py | 3 --- 4 files changed, 10 insertions(+), 48 deletions(-) diff --git a/app/jobs/_market_context.py b/app/jobs/_market_context.py index 5dd591f..d55d695 100644 --- a/app/jobs/_market_context.py +++ b/app/jobs/_market_context.py @@ -42,6 +42,7 @@ async def latest_quotes_by_group(session) -> dict[str, list[dict]]: & (Quote.symbol == sub.c.symbol) & (Quote.fetched_at == sub.c.mx), ) + .where(Quote.price.is_not(None)) .order_by(Quote.group_name, Quote.symbol) ) rows = (await session.execute(stmt)).scalars().all() diff --git a/app/jobs/indicator_summary_job.py b/app/jobs/indicator_summary_job.py index d96b309..5513a5c 100644 --- a/app/jobs/indicator_summary_job.py +++ b/app/jobs/indicator_summary_job.py @@ -5,7 +5,6 @@ from __future__ import annotations import asyncio import re -from collections import defaultdict import httpx 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.db import utcnow 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.openrouter import ( PROMPT_VERSION, @@ -136,40 +136,6 @@ def clean_summary(text: str) -> str: 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( session, client: httpx.AsyncClient, group: str, quotes: list[dict], @@ -254,13 +220,13 @@ async def run() -> None: jr.error = reason return - spent = await _month_spend(session) + spent = await month_spend(session) if spent >= s.OPENROUTER_MONTHLY_CAP_USD: jr.status = "skipped" jr.error = f"monthly cap reached (${spent:.2f})" return - groups = await _latest_quotes_by_group(session) + groups = await latest_quotes_by_group(session) # Only summarise groups currently configured in TOML — drops stale # group names (e.g. an old "pie" before T212 sourcing) that still have # quotes in the table but no UI presence. diff --git a/app/routers/api.py b/app/routers/api.py index 5edd6f3..ee8eda1 100644 --- a/app/routers/api.py +++ b/app/routers/api.py @@ -24,10 +24,11 @@ from app.auth import require_token, maybe_current_user, CurrentUser from app.services.i18n import ACTIVE_LANGUAGES from app.config import get_settings from app.db import get_session, utcnow +from app.jobs._market_context import REFERENCE_LINE from app.services.openrouter import ( PROMPT_VERSION, build_chat_system_prompt, - call_openrouter, + call_llm, month_start, ) from app.templates_env import templates @@ -710,10 +711,7 @@ class ChatRequest(BaseModel): 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 = [ "hormuz", "iran", "opec", "brent", "wti", "crude", "oil", "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, quotes_by_group=quotes, headlines=headlines, - reference_line=CHAT_REFERENCE_LINE, + reference_line=REFERENCE_LINE, ) msgs = [{"role": "system", "content": system_prompt}] @@ -831,7 +829,7 @@ async def chat( try: 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: session.add(AICall( model=s.OPENROUTER_MODEL, status="error", error=str(e)[:500], diff --git a/app/services/openrouter.py b/app/services/openrouter.py index a542b98..ff3215e 100644 --- a/app/services/openrouter.py +++ b/app/services/openrouter.py @@ -775,9 +775,6 @@ async def call_llm( raise last_exc -# Back-compat alias for any straggling import sites. -call_openrouter = call_llm - def month_window() -> tuple[datetime, datetime]: """[start, now] in UTC for the current calendar month."""