File size: 2,609 Bytes
5a60e93 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | """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=[],
)
|