Add Langfuse as a self-hosted eval and trace viewer
This commit is contained in:
@@ -232,6 +232,87 @@ def list_history(
|
||||
)
|
||||
|
||||
|
||||
class TranscriptMessage(BaseModel):
|
||||
role: Literal["user", "assistant"]
|
||||
trace_id: str
|
||||
content: str | None = None
|
||||
decision: str | None = None
|
||||
reason: str | None = None
|
||||
resolved_drug_id: str | None = None
|
||||
citations: list[dict[str, Any]] = []
|
||||
generated: bool = False
|
||||
quick_replies: list[str] = []
|
||||
blocks: list[dict[str, Any]] = []
|
||||
answer_mode: str | None = None
|
||||
answer_plan: dict[str, Any] | None = None
|
||||
candidate_assessments: list[dict[str, Any]] = []
|
||||
disclaimer: str | None = None
|
||||
created_at: str
|
||||
|
||||
|
||||
class TranscriptResponse(BaseModel):
|
||||
messages: list[TranscriptMessage]
|
||||
|
||||
|
||||
@router.get("/transcript", response_model=TranscriptResponse)
|
||||
def get_transcript(
|
||||
conversation_id: Annotated[str, Query(max_length=128)],
|
||||
traces: Annotated[TraceWriter, Depends(_trace_writer)],
|
||||
) -> TranscriptResponse:
|
||||
"""The actual conversation for one session — both the question AND the
|
||||
answer for each turn, oldest first, so a resumed session can be redrawn
|
||||
and continued rather than starting blank (the gap `/history` deliberately
|
||||
leaves open, see its docstring). Same no-auth scoping as `/history`: an
|
||||
empty/missing `conversation_id` returns nothing.
|
||||
|
||||
A row saved before `response_payload` existed (or where persistence
|
||||
raced a DB outage) has no replayable answer — its turn contributes only
|
||||
the user message, not a silently-wrong assistant one.
|
||||
"""
|
||||
trimmed = conversation_id.strip()
|
||||
if not trimmed:
|
||||
return TranscriptResponse(messages=[])
|
||||
try:
|
||||
rows = traces.list_by_conversation(trimmed, limit=_HISTORY_LIMIT)
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=503, detail="trace_store_unavailable") from exc
|
||||
|
||||
messages: list[TranscriptMessage] = []
|
||||
for row in reversed(rows): # list_by_conversation is newest-first
|
||||
created_at = row.created_at.isoformat() if row.created_at else ""
|
||||
messages.append(
|
||||
TranscriptMessage(
|
||||
role="user",
|
||||
trace_id=row.trace_id,
|
||||
content=row.query,
|
||||
created_at=created_at,
|
||||
)
|
||||
)
|
||||
payload = row.response_payload
|
||||
if payload is None:
|
||||
continue
|
||||
messages.append(
|
||||
TranscriptMessage(
|
||||
role="assistant",
|
||||
trace_id=row.trace_id,
|
||||
content=payload.get("answer"),
|
||||
decision=row.decision,
|
||||
reason=row.reason,
|
||||
resolved_drug_id=payload.get("resolved_drug_id"),
|
||||
citations=payload.get("citations") or [],
|
||||
generated=bool(payload.get("generated", False)),
|
||||
quick_replies=payload.get("quick_replies") or [],
|
||||
blocks=payload.get("blocks") or [],
|
||||
answer_mode=payload.get("answer_mode"),
|
||||
answer_plan=payload.get("answer_plan"),
|
||||
candidate_assessments=payload.get("candidate_assessments") or [],
|
||||
disclaimer=payload.get("disclaimer"),
|
||||
created_at=created_at,
|
||||
)
|
||||
)
|
||||
return TranscriptResponse(messages=messages)
|
||||
|
||||
|
||||
class SuggestResponse(BaseModel):
|
||||
suggestions: list[str]
|
||||
|
||||
@@ -488,6 +569,22 @@ def query_rag(
|
||||
# /v1/rag/trace/{id}` (if it existed) could later look up.
|
||||
correlation_id = current_correlation_id()
|
||||
otel_trace_id = current_trace_id()
|
||||
# The full assistant turn, persisted alongside the trace so a later
|
||||
# `/transcript` read can redraw this exact answer without re-running the
|
||||
# query — the shape mirrors `RagQueryResponse` minus the ids (trace_id is
|
||||
# the row's own primary key; correlation/otel ids are separate columns).
|
||||
response_payload = {
|
||||
"answer": answer,
|
||||
"resolved_drug_id": resolved_drug_id,
|
||||
"citations": [item.model_dump() for item in citations],
|
||||
"generated": generated,
|
||||
"quick_replies": quick_replies,
|
||||
"blocks": [item.model_dump() for item in blocks],
|
||||
"answer_mode": answer_mode,
|
||||
"answer_plan": answer_plan.model_dump() if answer_plan else None,
|
||||
"candidate_assessments": [item.model_dump() for item in candidate_assessments],
|
||||
"disclaimer": DISCLAIMER,
|
||||
}
|
||||
try:
|
||||
with stage("persistence"):
|
||||
trace_id = traces.save(
|
||||
@@ -504,6 +601,7 @@ def query_rag(
|
||||
correlation_id=correlation_id,
|
||||
otel_trace_id=otel_trace_id,
|
||||
conversation_id=payload.conversation_id,
|
||||
response_payload=response_payload,
|
||||
)
|
||||
except Exception:
|
||||
metrics.increment(TRACE_WRITE_FAILED)
|
||||
|
||||
Reference in New Issue
Block a user