Log the 2026-08-24 session: F3 fix live, audit filled, corpus re-ingest scoped

This commit is contained in:
2026-08-24 15:07:30 +07:00
parent f3eaab0948
commit 33b16c885b
7 changed files with 575 additions and 11 deletions
+139
View File
@@ -0,0 +1,139 @@
"""Ask the same clinical question several ways; the answer must not change.
The 90-case suite pins one exact wording per case, and `rag/understanding.py`
routes on hardcoded Vietnamese phrase lists (`candidate_cues`, `patient_cues`).
Together those make it possible to pass eval while a user who phrases the same
question differently gets a different outcome -- the system would be memorising
the test, not understanding the request. Nothing in the existing harness can
detect that, because every case is a single phrasing.
This probe closes that hole. Each group below is ONE clinical intent written
several ways by hand (not model-generated: a model asked to paraphrase tends to
preserve the distinctive words that drive the routing, which is exactly what
must vary). A group is CONSISTENT when every phrasing lands on the same
decision. Which decision is right is a separate question -- this measures
stability, not correctness, and instability is a defect regardless of which
answer is the good one.
Usage:
python scripts/paraphrase_probe.py --base-url https://realvuxbaro.me
"""
from __future__ import annotations
import argparse
import json
import time
import urllib.request
from collections import Counter
# Each group: (id, intent, [phrasings]).
GROUPS = [
(
"comorbidity",
"Patient with a comorbidity asks which drugs need caution (the P08 shape)",
[
"BN tăng huyết áp kèm xơ gan Child-Pugh B dùng thuốc nào cần lưu ý?",
"Bệnh nhân bị cao huyết áp và xơ gan thì cần thận trọng với thuốc nào?",
"Người bệnh xơ gan Child-Pugh B, huyết áp cao, nên lưu ý những thuốc gì?",
"Có xơ gan mà bị tăng huyết áp thì thuốc nào phải cẩn thận?",
],
),
(
"contraindication",
"Straight contraindication lookup for one named drug",
[
"Chống chỉ định của Ibuprofen là gì?",
"Ibuprofen chống chỉ định với ai?",
"Những trường hợp nào không được dùng Ibuprofen?",
"Ai không nên uống Ibuprofen?",
],
),
(
"pediatric_dose",
"Paediatric dose, which the service must clarify on (age/weight required)",
[
"Liều Paracetamol cho trẻ em là bao nhiêu?",
"Trẻ con uống Paracetamol liều thế nào?",
"Cho bé dùng Paracetamol bao nhiêu mg?",
"Paracetamol dùng cho trẻ nhỏ liều ra sao?",
],
),
(
"out_of_scope",
"Out of scope -- must refuse every time, this is the safety threshold",
[
"Thuốc Paracetamol giá bao nhiêu tiền?",
"Mua Paracetamol ở đâu rẻ nhất?",
"Paracetamol hãng nào tốt nhất hiện nay?",
"Giá một hộp Paracetamol là bao nhiêu?",
],
),
]
def ask(endpoint: str, question: str, conversation_id: str, timeout: float):
payload = {"content": question, "conversationId": conversation_id}
request = urllib.request.Request(
endpoint,
data=json.dumps(payload, ensure_ascii=False).encode("utf-8"),
headers={"Content-Type": "application/json; charset=utf-8"},
method="POST",
)
with urllib.request.urlopen(request, timeout=timeout) as response:
body = json.loads(response.read().decode("utf-8"))
message = body.get("message") or {}
return {
"decision": message.get("decision"),
"reason": message.get("reason"),
"drug": message.get("resolvedDrugId"),
"citations": len(message.get("citations") or []),
"otel_trace_id": response.headers.get("X-Trace-ID"),
"answer": (message.get("content") or "")[:160],
}
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--base-url", required=True)
parser.add_argument("--timeout", type=float, default=120.0)
parser.add_argument("--out", default="")
args = parser.parse_args()
endpoint = args.base_url.rstrip("/") + "/api/chat"
run_id = str(int(time.time()))
results, unstable = [], 0
for group_id, intent, phrasings in GROUPS:
print(f"\n=== {group_id}{intent}")
decisions = []
for index, question in enumerate(phrasings, start=1):
# A fresh conversation per phrasing: shared history would let an
# earlier turn answer a later one and hide the instability.
conversation_id = f"para-{group_id}-{index}-{run_id}"
try:
row = ask(endpoint, question, conversation_id, args.timeout)
except Exception as exc: # noqa: BLE001 - recorded, not swallowed
row = {"decision": "ERROR", "reason": repr(exc)[:80], "citations": 0}
row.update({"group": group_id, "phrasing": question})
results.append(row)
decisions.append(row["decision"])
print(
f" [{index}] {row['decision']:<11} cit={row['citations']} "
f"drug={row.get('drug')} :: {question[:52]}"
)
counts = Counter(decisions)
stable = len(counts) == 1
unstable += 0 if stable else 1
print(f" -> {'CONSISTENT' if stable else 'INCONSISTENT'} {dict(counts)}")
print(f"\n=== {len(GROUPS) - unstable}/{len(GROUPS)} intents answered consistently ===")
if args.out:
with open(args.out, "w", encoding="utf-8") as handle:
for row in results:
handle.write(json.dumps(row, ensure_ascii=False) + "\n")
return 0
if __name__ == "__main__":
raise SystemExit(main())