Log the 2026-08-24 session: F3 fix live, audit filled, corpus re-ingest scoped
This commit is contained in:
@@ -203,6 +203,18 @@ class QueryFrame:
|
||||
# ordinary clarifying question in the API response and trace — this lets
|
||||
# `RagAgent._route` surface the real cause instead.
|
||||
system_error: str | None = None
|
||||
# The part of the turn the Dược thư cannot answer AT ALL, as the model
|
||||
# named it — a property the book does not record (giá, nơi bán, bảo hiểm)
|
||||
# or a comparative judgement it never makes ("hãng nào tốt nhất"). Set
|
||||
# means refuse, and `_parse` forces `turn_type` to "out_of_scope" on it.
|
||||
#
|
||||
# Why a field and not another phrase list: `attribute` is validated against
|
||||
# SECTION_KEYS and anything unrecognised collapses to None, which made
|
||||
# "user asked for a section but did not say which" and "user asked for
|
||||
# something the book has no section for" indistinguishable — both became
|
||||
# `attribute=None` and both clarified. Found live 2026-08-24: "Paracetamol
|
||||
# giá bao nhiêu?" answered "Bạn muốn hỏi liều cho người lớn hay trẻ em?".
|
||||
unsupported_request: str | None = None
|
||||
raw: dict = field(default_factory=dict, compare=False)
|
||||
|
||||
|
||||
@@ -213,6 +225,19 @@ FRAME_SCHEMA = {
|
||||
"drugs": ["drug_id exactly as it appears in the provided catalog list"],
|
||||
"unknown_drugs": ["a drug name the user mentioned that is NOT in the catalog"],
|
||||
"attribute": "one of the section keys provided, or null",
|
||||
"unsupported_request": (
|
||||
"Null in the ordinary case. Set it ONLY when the turn asks for "
|
||||
"something the Dược thư does not contain at all, and name that thing "
|
||||
"briefly in Vietnamese. Two kinds qualify: (a) a commercial or "
|
||||
"administrative property the book never records — giá/giá tiền, nơi "
|
||||
"bán/mua ở đâu, bảo hiểm chi trả, hạn dùng của một hộp cụ thể; (b) a "
|
||||
"comparative or evaluative judgement the book never makes — 'hãng nào "
|
||||
"tốt nhất', 'thuốc nào hay hơn', 'nên chọn loại nào'. "
|
||||
"IMPORTANT — do NOT set it for trade names: the monograph HAS a "
|
||||
"'Tên thương mại' section, so 'Paracetamol của hãng nào', 'biệt dược "
|
||||
"của X' are ordinary in-scope lookups (attribute=ten_thuong_mai). "
|
||||
"Only RANKING trade names is unsupported, not listing them."
|
||||
),
|
||||
"population": "tre_em | tre_so_sinh | nguoi_lon | nguoi_cao_tuoi | phu_nu_co_thai | phu_nu_cho_con_bu | suy_than | suy_gan | null",
|
||||
"weight_kg": (
|
||||
"number if a body weight is given, else null. Vietnamese casual speech "
|
||||
@@ -412,6 +437,10 @@ Quy tắc bắt buộc:
|
||||
thực sự muốn hỏi điều gì (vd "Anh/chị muốn hỏi đường dùng nào ạ?"), không tự
|
||||
suy đoán lại giá trị cũ.
|
||||
- Chào hỏi/vu vơ -> "smalltalk". Ngoài phạm vi chuyên luận thuốc -> "out_of_scope".
|
||||
- Nếu câu hỏi nhắm vào thứ Dược thư không ghi (giá tiền, nơi mua, bảo hiểm) hoặc
|
||||
đòi xếp hạng hơn kém ("hãng nào tốt nhất", "thuốc nào hay hơn"), đặt
|
||||
"unsupported_request" nêu ngắn gọn thứ đó. Tên biệt dược CÓ trong sách (mục
|
||||
"Tên thương mại"), nên hỏi biệt dược là hợp lệ — không đặt unsupported_request.
|
||||
- Khi needs_clarify=true, kèm "quick_replies": 2-4 phương án NGẮN cho câu hỏi lại
|
||||
đó, CHỈ khi nó thực sự có vài lựa chọn rời rạc tự nhiên (vd đối tượng: "Người
|
||||
lớn"/"Trẻ em"). Để mảng rỗng nếu cần một giá trị cụ thể không có lựa chọn ngắn
|
||||
@@ -658,6 +687,24 @@ class LlmQueryUnderstander:
|
||||
if turn_type not in TURN_TYPES:
|
||||
turn_type = "drug_attribute" if drugs else "out_of_scope"
|
||||
needs_clarify = data.get("needs_clarify") is True
|
||||
# The scope gate, applied deterministically rather than trusted to the
|
||||
# model's own `turn_type`. When the turn asks for something the book
|
||||
# does not contain, refusing is the only correct outcome — Feature-List
|
||||
# F3 requires 100% of out-of-scope turns to be refused, and the failure
|
||||
# this fixes was precisely the model saying `drug_attribute` while
|
||||
# leaving `attribute` null, which downstream read as "which section did
|
||||
# you mean?" and asked the user a question the book cannot answer.
|
||||
#
|
||||
# Deliberately unconditional: it fires even when a valid `attribute`
|
||||
# was also parsed. A turn mixing an answerable section with an
|
||||
# unanswerable property ("giá bao nhiêu và liều người lớn?") is refused
|
||||
# whole rather than half-answered. Over-refusing is the safe direction
|
||||
# for a safety threshold; the 90-case suite is the guard against
|
||||
# over-refusing in practice.
|
||||
unsupported_request = _clean_str(data.get("unsupported_request"))
|
||||
if unsupported_request:
|
||||
turn_type = "out_of_scope"
|
||||
needs_clarify = False
|
||||
clarify_reason = _clean_str(data.get("clarify_reason"))
|
||||
quick_replies = (
|
||||
_clean_quick_replies(data.get("quick_replies"))
|
||||
@@ -700,6 +747,7 @@ class LlmQueryUnderstander:
|
||||
needs_clarify=needs_clarify,
|
||||
clarify_reason=clarify_reason,
|
||||
quick_replies=quick_replies,
|
||||
unsupported_request=unsupported_request,
|
||||
raw=data if isinstance(data, dict) else {},
|
||||
)
|
||||
|
||||
@@ -750,6 +798,17 @@ def _apply_reverse_relation_cues(frame: QueryFrame, turn: str) -> QueryFrame:
|
||||
)
|
||||
|
||||
|
||||
# Shared with `_apply_condition_candidate_cue` below: a turn naming these
|
||||
# signals is describing one patient's own combined profile ("BN X kèm Y"),
|
||||
# not asking the model to pick between unrelated conditions.
|
||||
_PATIENT_CONTEXT_CUES = (
|
||||
" bn ", " benh nhan ", " nguoi benh ", " kem ", " di ung ",
|
||||
" dang dung ", " mang thai ", " cho con bu ", " tuoi ", " kg ",
|
||||
" ckd ", " suy than ", " suy gan ", " child pugh ", " egfr ",
|
||||
" creatinin ", " ast ", " alt ",
|
||||
)
|
||||
|
||||
|
||||
def _apply_condition_candidate_cue(
|
||||
frame: QueryFrame, turn: str, normalizer: ConditionNormalizer
|
||||
) -> QueryFrame:
|
||||
@@ -774,6 +833,23 @@ def _apply_condition_candidate_cue(
|
||||
)
|
||||
if not any(cue in text for cue in candidate_cues):
|
||||
return frame
|
||||
if condition.ambiguous and any(cue in text for cue in _PATIENT_CONTEXT_CUES):
|
||||
# Found live 2026-08-20 (eval case P08): "BN tăng huyết áp kèm xơ gan
|
||||
# Child-Pugh B dùng thuốc nào cần lưu ý?" reliably clarified instead
|
||||
# of answering, 4/4 reproductions. The raw understanding call reads
|
||||
# a comorbidity ("kèm xơ gan...") as a FORK in what the question
|
||||
# means ("thuốc nào cần lưu ý" vs "thuốc nào gây tăng huyết áp") and
|
||||
# marks the condition ambiguous with its own clarify_question — but
|
||||
# this turn already told us which drug lane it wants (a candidate
|
||||
# cue matched, e.g. "thuốc nào cần"), so the fork the model raised
|
||||
# is not genuine: `_apply_general_condition_scope` already treats
|
||||
# this same cue set as "this is one patient's profile, not a choice
|
||||
# between diseases", and `frame.patient_context` (hepatic/renal/etc,
|
||||
# parsed separately and left untouched here) is exactly what lets
|
||||
# `_condition_to_drug`'s `assess_patient_candidates` answer safely
|
||||
# instead — clearing the stale ambiguity is what lets a turn reach
|
||||
# that path instead of dead-ending in a clarify loop.
|
||||
condition = replace(condition, ambiguous=False, clarify_question=None)
|
||||
return replace(
|
||||
frame,
|
||||
turn_type="condition_to_drug",
|
||||
@@ -799,13 +875,7 @@ def _apply_general_condition_scope(frame: QueryFrame, turn: str) -> QueryFrame:
|
||||
if frame.turn_type not in {"condition_to_drug", "symptom_to_drug"}:
|
||||
return frame
|
||||
text = f" {normalize_name(turn)} "
|
||||
patient_cues = (
|
||||
" bn ", " benh nhan ", " nguoi benh ", " kem ", " di ung ",
|
||||
" dang dung ", " mang thai ", " cho con bu ", " tuoi ", " kg ",
|
||||
" ckd ", " suy than ", " suy gan ", " child pugh ", " egfr ",
|
||||
" creatinin ", " ast ", " alt ",
|
||||
)
|
||||
if any(cue in text for cue in patient_cues):
|
||||
if any(cue in text for cue in _PATIENT_CONTEXT_CUES):
|
||||
return frame
|
||||
primary = (
|
||||
frame.condition.normalized_condition
|
||||
|
||||
Reference in New Issue
Block a user