"""Charts endpoint — user-facing chart retrieval for one turn (S2 visualization, SPINE_V2_PLAN §4.4/§4.5). `GET /api/v1/charts?message_id=` returns every chart a `render_chart` tool call produced during one assistant turn, as `dataeyond.chart.v1` envelopes (SPINE_V2_PLAN §4.2). Rows are written by the chat pipeline right before the `done` SSE event; the FE fires this GET on `done` with the `message_id` from that event. Lookup is by `message_id` alone (Python-minted UUID4 — lead decision 2026-07-13). Every response is HTTP 200 with an explicit `status` marker (lead ask 2026-07-13): - `success` — ≥1 chart; render them. - `empty` — the turn completed but produced no charts (the common case). - `not_found` — no completed turn is known for this message_id (mistyped/stale id, or an error turn — those never write a row). Distinguished from `empty` via the turn's traceability row. No auth — Go fronts Python. """ from typing import Literal from fastapi import APIRouter, Query from pydantic import BaseModel from src.charts import ChartRecord, PostgresChartStore from src.middlewares.logging import get_logger, log_execution logger = get_logger("charts_api") router = APIRouter(prefix="/api/v1", tags=["Charts"]) # Warm, process-shared store (mirrors the chat endpoints' module-level instances). _store = PostgresChartStore() class ChartsResponse(BaseModel): status: Literal["success", "empty", "not_found"] message: str count: int charts: list[ChartRecord] @router.get("/charts", response_model=ChartsResponse) @log_execution(logger) async def get_charts( message_id: str = Query( ..., description="Assistant turn id, taken from the `done` SSE event" ), ) -> ChartsResponse: """Fetch every chart for one turn. Always 200 — the `status` field carries the outcome, so the FE can call this unconditionally on every `done`.""" charts = await _store.list_for_message(message_id) if charts: return ChartsResponse( status="success", message=f"{len(charts)} chart(s) for this message.", count=len(charts), charts=charts, ) if await _store.turn_exists(message_id): return ChartsResponse( status="empty", message="This message completed without producing charts.", count=0, charts=[], ) return ChartsResponse( status="not_found", message="No completed turn is known for this message_id.", count=0, charts=[], )