Verify exact production traces and record rollout

This commit is contained in:
2026-08-11 11:29:59 +07:00
parent 6b8f7584ed
commit 59e6ad2d0d
46 changed files with 2795 additions and 290 deletions
+58 -30
View File
@@ -7,9 +7,15 @@ from fastapi import APIRouter, Depends, HTTPException, Request
from pydantic import BaseModel, Field
from rag.answer import GroundedAnswerService
from rag.metrics import TRACE_WRITE_FAILED, Metrics, NullMetrics
from rag.metrics import DECISION, TRACE_WRITE_FAILED, Metrics, NullMetrics
from rag.models import QueryIntent, SubjectScope
from rag.policy import resolve_subject_scope
from rag.telemetry import (
annotate_current_span,
current_correlation_id,
current_trace_id,
stage,
)
class TraceWriter(Protocol):
@@ -61,6 +67,8 @@ class AnswerPlanResponse(BaseModel):
class RagQueryResponse(BaseModel):
trace_id: str
correlation_id: str
otel_trace_id: str | None = None
decision: str
reason: str
answer: str | None
@@ -166,7 +174,10 @@ def query_rag(
traces: Annotated[TraceWriter, Depends(_trace_writer)],
metrics: Annotated[Metrics, Depends(_metrics)],
) -> RagQueryResponse:
agent = getattr(request.app.state, "conversational", None)
with stage("receive"):
agent = getattr(request.app.state, "conversational", None)
subject_scope = resolve_subject_scope(payload.query, payload.subject_scope)
intent = payload.intent
# `payload.subject_scope`/`payload.intent` are what the CALLER claims —
# logged below for audit, but the RagAgent path does not take them as an
@@ -175,9 +186,6 @@ def query_rag(
# own LLM understanding call, and does not gate on intent at all (this
# product is for doctors/pharmacists; a client label must not be, and
# here structurally cannot be, the safety decision — F-02).
subject_scope = resolve_subject_scope(payload.query, payload.subject_scope)
intent = payload.intent
if agent is not None:
# The live path (F-03): one LLM call understands the turn (drug
# identity against the real catalog, turn type, population/weight),
@@ -199,7 +207,8 @@ def query_rag(
# No generator configured (ANSWER_PROVIDER=disabled): there is no LLM
# to understand a turn with, so this is retrieval-only, single-turn,
# unchanged from before F-03.
grounded = answers.answer(payload.query, subject_scope, intent)
with stage("routing"):
grounded = answers.answer(payload.query, subject_scope, intent)
if grounded.clarification is not None:
decision, reason = "clarify", "needs_more_info"
answer = grounded.clarification
@@ -229,32 +238,51 @@ def query_rag(
# with whether the answer was safe. `trace_id` degrades to a local,
# unpersisted uuid — still a valid response field, just not one `GET
# /v1/rag/trace/{id}` (if it existed) could later look up.
correlation_id = current_correlation_id()
otel_trace_id = current_trace_id()
try:
trace_id = traces.save(
query=payload.query,
# The resolved (server-derived) values, not the caller's claim —
# this is what actually gated the answer, so it's what the trace
# must show.
subject_scope=subject_scope.value,
intent=intent.value,
decision=decision,
reason=reason,
resolved_drug_id=resolved_drug_id,
citations=tuple(item.model_dump() for item in citations),
)
with stage("persistence"):
trace_id = traces.save(
query=payload.query,
# The resolved (server-derived) values, not the caller's claim —
# this is what actually gated the answer, so it's what the trace
# must show.
subject_scope=subject_scope.value,
intent=intent.value,
decision=decision,
reason=reason,
resolved_drug_id=resolved_drug_id,
citations=tuple(item.model_dump() for item in citations),
correlation_id=correlation_id,
otel_trace_id=otel_trace_id,
)
except Exception:
metrics.increment(TRACE_WRITE_FAILED)
trace_id = str(uuid.uuid4())
return RagQueryResponse(
trace_id=trace_id,
decision=decision,
reason=reason,
answer=answer,
resolved_drug_id=resolved_drug_id,
citations=citations,
generated=generated,
quick_replies=quick_replies,
blocks=blocks,
answer_mode=answer_mode,
answer_plan=answer_plan,
metrics.increment(DECISION, decision=decision, reason=reason)
annotate_current_span(
**{
"duocthu.decision": decision,
"duocthu.reason": reason,
"duocthu.citation_count": len(citations),
"duocthu.generated": generated,
"duocthu.persisted_trace_id": trace_id,
}
)
with stage("response"):
response = RagQueryResponse(
trace_id=trace_id,
correlation_id=correlation_id,
otel_trace_id=otel_trace_id,
decision=decision,
reason=reason,
answer=answer,
resolved_drug_id=resolved_drug_id,
citations=citations,
generated=generated,
quick_replies=quick_replies,
blocks=blocks,
answer_mode=answer_mode,
answer_plan=answer_plan,
)
return response