File size: 1,803 Bytes
f873f92
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Traceability endpoint — user-facing per-turn provenance (KM-691).

`GET /api/v1/traceability?analysis_id=&message_id=` returns the provenance record for
one assistant turn: planning / tool calls (with real inputs + outputs) / data sources
(with the executed query). One JSONB row per assistant `message_id`, written by the
chat pipeline right before the `done` SSE event; the FE fires this GET on `done`.

Renamed from the contracted `/api/v1/observability` (team decision 2026-07-06) so it is
never confused with the Langfuse *observability* stack (engineering-only, PII-masked).
No auth — Go fronts Python.
"""

from fastapi import APIRouter, HTTPException, Query

from src.middlewares.logging import get_logger, log_execution
from src.traceability import PostgresTraceabilityStore, TraceabilityPayload

logger = get_logger("traceability_api")

router = APIRouter(prefix="/api/v1", tags=["Traceability"])

# Warm, process-shared store (mirrors the chat endpoints' module-level instances).
_store = PostgresTraceabilityStore()


@router.get("/traceability", response_model=TraceabilityPayload)
@log_execution(logger)
async def get_traceability(
    analysis_id: str = Query(..., description="Analysis/session id"),
    message_id: str = Query(
        ..., description="Assistant turn id, taken from the `done` SSE event"
    ),
) -> TraceabilityPayload:
    """Fetch one turn's provenance record. 404 while the turn is still running or if
    the id is unknown (the FE never gets a `message_id` for error turns, so 404 is
    the correct answer there)."""
    payload = await _store.get(analysis_id, message_id)
    if payload is None:
        raise HTTPException(
            status_code=404,
            detail=f"No traceability for message '{message_id}' yet.",
        )
    return payload