Log the 2026-08-24 session: F3 fix live, audit filled, corpus re-ingest scoped
This commit is contained in:
@@ -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())
|
||||
Reference in New Issue
Block a user