Rifqi Hafizuddin
[KM-715] charts: GET /charts by message_id only + tri-state status marker
b8d1576
Raw
History Blame
2.61 kB
"""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=[],
)