4.1 KiB
Claude ownership claim — 2026-08-11
Read CLAUDE_HANDOFF_2026-08-10.md first. Two items in it have since moved
on (checked against the code and against live production on 2026-08-11):
- The structured-claims refactor it describes as in progress is finished and
shipped (
dfdbf52, then9c3acd0).pytest -q --ignore=tests/test_api.py --ignore=tests/test_live_datastores.py= 219 passed at the start of this session. - Entailment majority vote (2-of-3) is no longer in the code.
df55af4introduced it;9c3acd0replaced it with a single pass (_ENTAILMENT_MAX_ATTEMPTS = 1), with the reasoning in_verify_entailment's docstring: repeating an identical temperature-0 prompt is a correlated retry rather than an independent vote.
What this session is changing, and why
All five items come from driving production (https://realvuxbaro.me) by
hand plus direct /api/chat probes — measured, not inferred from docs.
rag/answer.py— availability failures during the entailment pass are currently recorded and shown asunsupported_claim, so a timing or outage problem reaches the user as a content failure. The failure taxonomy indocs/current-rag-pipeline-audit.md§4 keeps these separate. Reusing the reason codes that already exist and are already mapped inapps/web/app/api/chat/route.ts(request_budget_exhausted,provider_unavailable,malformed_output) — no new reason code, so the frontend mapping needs no change. Fail-closed behaviour is unchanged; only the label changes.rag/answer.py—_verify_entailment'sfor _ in range(_ENTAILMENT_MAX_ATTEMPTS)loop always returns on its first iteration, so raising that constant silently does nothing, and the unreachablereturn Falseafter it returns aboolwhere every caller reads.supported. Making the single-pass intent explicit.rag/agent.py— the pediatric dosing gate asks "Bé bao nhiêu tuổi và cân nặng bao nhiêu kg?" even when the user just gave one of the two. Reproduced 5/5 live ("18 ký", "18 cân", explicit "18 kg", "Trẻ 5 tuổi"). The gate itself is NOT being loosened — both fields stay required, which is clinically right here (the correct answer uses both an age band and a mg/kg rule). Only the question text becomes specific to what is actually missing.apps/web/app/_components/ChatPanel.tsx— a hard 25s client abort against a backend whose own budget is 40s (config.py:60). Measured n=8 sequential: 2/8 exceeded 25s, and one of those was a correctanswerable/grounded/2-citation reply at 25.1s that the user never saw. Caddy (reverse_proxy web:3000, no timeout) and the BFF (signal: request.signal, no own timeout) do not cap this, so the client constant is the only binding limit.packages/ui/src/ChatBubble.tsx— chips dedupe bychunkIdbut the label is only drug+section+page, so several distinct chunks render as identical-looking chips. Not collapsing them by label: the click handler maps to a specific citation index, so collapsing would make real evidence blocks unreachable from the prose, and provenance is a hard guardrail.
Files claimed
apps/ai-service/rag/answer.py, apps/ai-service/rag/agent.py,
apps/ai-service/tests/test_grounded_generation.py,
apps/ai-service/tests/test_agent.py,
apps/web/app/_components/ChatPanel.tsx,
packages/ui/src/ChatBubble.tsx, and this file.
Not touching ingestion/, retrieval adapters, rag/service.py,
rag/understanding.py, or anything sparse/BM25 related — task #4 (real
BM25 via Qdrant native sparse vectors) is still not started.
Test-coverage context for review
These areas start with little automated cover: no tests currently touch
missing_pediatric_age_or_weight / missing_population, and the repo has no
frontend test setup (no test script in apps/web/package.json, no
ChatPanel/ChatBubble tests). A passing pytest run therefore is not on its
own sufficient evidence here. Backend tests are being added alongside the
changes, and the two frontend changes are verified by driving the real site.