515 lines
22 KiB
Python
515 lines
22 KiB
Python
"""`rag/understanding.py::LlmQueryUnderstander` — zero coverage before this
|
|
(Codex's 2026-08-06 review, F-03/F-10), despite being the entry point for
|
|
every live turn once F-03 wired it in.
|
|
|
|
Also covers F-04 (bounded candidates): the resolver decides which drug_ids
|
|
are even plausible for a turn *before* the model runs, and the model's pick
|
|
is validated against that bound, not the full catalog.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
|
|
from rag.ports import AnswerGenerationUnavailable
|
|
from rag.understanding import (
|
|
SECTION_KEY_HINTS,
|
|
SECTION_KEYS,
|
|
LlmQueryUnderstander,
|
|
QueryFrame,
|
|
)
|
|
|
|
CATALOG = {
|
|
"paracetamol_acetaminophen": "paracetamol acetaminophen, PARACETAMOL",
|
|
"metformin": "metformin, METFORMIN",
|
|
}
|
|
|
|
|
|
class _FixedLlm:
|
|
def __init__(self, payload) -> None:
|
|
self._payload = payload
|
|
|
|
def generate(self, system: str, user: str, schema: dict) -> str:
|
|
if isinstance(self._payload, BaseException):
|
|
raise self._payload
|
|
if isinstance(self._payload, str):
|
|
return self._payload
|
|
return json.dumps(self._payload, ensure_ascii=False)
|
|
|
|
|
|
class _Resolution:
|
|
def __init__(self, status="not_found", drug_id=None, candidate_drug_ids=()) -> None:
|
|
self.status = status
|
|
self.drug_id = drug_id
|
|
self.candidate_drug_ids = candidate_drug_ids
|
|
|
|
|
|
class _FakeResolver:
|
|
"""Deterministic stand-in for `CatalogDrugResolver`: resolves a line to
|
|
a drug_id if one of `known`'s substrings appears in it (case-insensitive),
|
|
with no fuzzy suggestions unless `suggestions` is given."""
|
|
|
|
def __init__(self, known: dict[str, str], suggestions: dict[str, str] | None = None) -> None:
|
|
self._known = known
|
|
self._suggestions = suggestions or {}
|
|
self.suggest_calls = 0
|
|
|
|
def resolve(self, query: str) -> _Resolution:
|
|
low = query.lower()
|
|
for needle, drug_id in self._known.items():
|
|
if needle in low:
|
|
return _Resolution(status="resolved", drug_id=drug_id)
|
|
return _Resolution()
|
|
|
|
def suggest(self, query: str, k: int = 3, min_score: float = 0.5):
|
|
self.suggest_calls += 1
|
|
low = query.lower()
|
|
return [
|
|
(drug_id, 0.9) for needle, drug_id in self._suggestions.items() if needle in low
|
|
][:k]
|
|
|
|
|
|
RESOLVER = _FakeResolver({
|
|
"metformin": "metformin",
|
|
"paracetamol": "paracetamol_acetaminophen",
|
|
})
|
|
|
|
|
|
def test_drug_id_in_exact_underscore_form_resolves():
|
|
understander = LlmQueryUnderstander(_FixedLlm({
|
|
"turn_type": "drug_attribute", "drugs": ["metformin"],
|
|
"unknown_drugs": [], "attribute": None, "population": None,
|
|
"weight_kg": None, "age_text": None, "indication": None,
|
|
"needs_clarify": False, "clarify_reason": None,
|
|
}), CATALOG, RESOLVER)
|
|
frame = understander.understand("liều metformin")
|
|
assert frame.drugs == ("metformin",)
|
|
assert frame.unknown_drugs == ()
|
|
|
|
|
|
def test_exact_candidate_does_not_repeat_the_catalog_wide_fuzzy_scan():
|
|
resolver = _FakeResolver({"metformin": "metformin"})
|
|
understander = LlmQueryUnderstander(_FixedLlm({
|
|
"turn_type": "drug_attribute", "drugs": ["metformin"],
|
|
"unknown_drugs": [], "attribute": "chong_chi_dinh",
|
|
"population": None, "weight_kg": None, "age_text": None,
|
|
"indication": None, "needs_clarify": False, "clarify_reason": None,
|
|
}), CATALOG, resolver)
|
|
|
|
frame = understander.understand("chống chỉ định metformin")
|
|
|
|
assert frame.drugs == ("metformin",)
|
|
assert resolver.suggest_calls == 0
|
|
|
|
|
|
def test_drug_id_echoed_with_spaces_instead_of_underscores_still_resolves():
|
|
"""Reproduces the live 2026-08-06 bug on a genuine multi-turn shape: the
|
|
drug is named in an earlier turn (in history), the current turn is just
|
|
"30 cân", and the model echoed the spaced display name instead of the
|
|
underscored id. The old strict-equality check demoted a correctly
|
|
identified drug to unknown_drugs — producing "Không tìm thấy
|
|
paracetamol trong Dược thư" for a drug that plainly is in it."""
|
|
understander = LlmQueryUnderstander(_FixedLlm({
|
|
"turn_type": "dosing_calc", "drugs": ["paracetamol acetaminophen"],
|
|
"unknown_drugs": [], "attribute": None, "population": "tre_em",
|
|
"weight_kg": 30, "age_text": "7 tuổi", "indication": None,
|
|
"needs_clarify": False, "clarify_reason": None,
|
|
}), CATALOG, RESOLVER)
|
|
frame = understander.understand(
|
|
"30 cân",
|
|
history=("Người dùng: Liều paracetamol cho trẻ em", "Trợ lý: Bé mấy tuổi?"),
|
|
)
|
|
assert frame.drugs == ("paracetamol_acetaminophen",)
|
|
assert frame.unknown_drugs == ()
|
|
|
|
|
|
def test_a_genuinely_invented_name_is_unknown_not_substituted():
|
|
understander = LlmQueryUnderstander(_FixedLlm({
|
|
"turn_type": "drug_attribute", "drugs": [],
|
|
"unknown_drugs": ["aspirinol"], "attribute": None, "population": None,
|
|
"weight_kg": None, "age_text": None, "indication": None,
|
|
"needs_clarify": False, "clarify_reason": None,
|
|
}), CATALOG, RESOLVER)
|
|
frame = understander.understand("liều aspirinol")
|
|
assert frame.drugs == ()
|
|
assert frame.unknown_drugs == ("aspirinol",)
|
|
|
|
|
|
def test_a_name_with_no_deterministic_candidate_is_unknown_even_if_the_model_names_a_real_id():
|
|
"""F-04's actual guarantee: the model naming a *real* catalog id is not
|
|
enough — that id must also be among the turn's deterministic candidates.
|
|
Nothing in "liều aspirinol" fuzzy/exact-matches any real drug (per
|
|
RESOLVER), so even if the model output a real id here, it must be
|
|
rejected: the candidate bound, not just catalog membership, is what's
|
|
trusted."""
|
|
understander = LlmQueryUnderstander(_FixedLlm({
|
|
"turn_type": "drug_attribute", "drugs": ["metformin"],
|
|
"unknown_drugs": [], "attribute": None, "population": None,
|
|
"weight_kg": None, "age_text": None, "indication": None,
|
|
"needs_clarify": False, "clarify_reason": None,
|
|
}), CATALOG, RESOLVER)
|
|
frame = understander.understand("liều aspirinol")
|
|
assert frame.drugs == ()
|
|
assert "metformin" in frame.unknown_drugs
|
|
|
|
|
|
def test_fuzzy_suggestion_bounds_a_typo_into_the_candidate_set():
|
|
resolver = _FakeResolver({}, suggestions={"metfomin": "metformin"})
|
|
understander = LlmQueryUnderstander(_FixedLlm({
|
|
"turn_type": "drug_attribute", "drugs": ["metformin"],
|
|
"unknown_drugs": [], "attribute": None, "population": None,
|
|
"weight_kg": None, "age_text": None, "indication": None,
|
|
"needs_clarify": False, "clarify_reason": None,
|
|
}), CATALOG, resolver)
|
|
frame = understander.understand("liều metfomin")
|
|
assert frame.drugs == ("metformin",)
|
|
|
|
|
|
# --- F-10: a small battery of invented near-alias shapes, beyond the single
|
|
# "aspirinol" case above — each simulates a different way a name could be
|
|
# crafted to *look* like it should fuzzy-match a real drug ---------------
|
|
|
|
|
|
def test_a_real_drug_name_with_a_brand_like_suffix_is_not_substituted():
|
|
resolver = _FakeResolver({}) # nothing in this turn resolves or suggests
|
|
understander = LlmQueryUnderstander(_FixedLlm({
|
|
"turn_type": "drug_attribute", "drugs": [],
|
|
"unknown_drugs": ["metforminex"], "attribute": None, "population": None,
|
|
"weight_kg": None, "age_text": None, "indication": None,
|
|
"needs_clarify": False, "clarify_reason": None,
|
|
}), CATALOG, resolver)
|
|
frame = understander.understand("liều metforminex")
|
|
assert frame.drugs == ()
|
|
assert frame.unknown_drugs == ("metforminex",)
|
|
|
|
|
|
def test_a_name_blending_two_real_drugs_is_not_substituted_for_either():
|
|
resolver = _FakeResolver({})
|
|
understander = LlmQueryUnderstander(_FixedLlm({
|
|
"turn_type": "drug_attribute", "drugs": ["metformin", "paracetamol_acetaminophen"],
|
|
"unknown_drugs": [], "attribute": None, "population": None,
|
|
"weight_kg": None, "age_text": None, "indication": None,
|
|
"needs_clarify": False, "clarify_reason": None,
|
|
}), CATALOG, resolver)
|
|
frame = understander.understand("liều metformacetamol")
|
|
# Neither real id has deterministic candidate support for this turn —
|
|
# F-04's bound must reject both, not accept the ones that happen to be
|
|
# real catalog members.
|
|
assert frame.drugs == ()
|
|
assert "metformin" in frame.unknown_drugs
|
|
assert "paracetamol_acetaminophen" in frame.unknown_drugs
|
|
|
|
|
|
def test_malformed_json_fails_closed_to_a_clarify():
|
|
understander = LlmQueryUnderstander(_FixedLlm("not json"), CATALOG, RESOLVER)
|
|
frame = understander.understand("gì đó")
|
|
assert frame.turn_type == "out_of_scope"
|
|
assert frame.needs_clarify is True
|
|
assert frame.quick_replies == ()
|
|
|
|
|
|
def test_quick_replies_are_parsed_when_the_model_offers_them():
|
|
understander = LlmQueryUnderstander(_FixedLlm({
|
|
"turn_type": "dosing_calc", "drugs": ["paracetamol_acetaminophen"],
|
|
"unknown_drugs": [], "attribute": None, "population": None,
|
|
"weight_kg": None, "age_text": None, "indication": None,
|
|
"needs_clarify": True, "clarify_reason": "Người lớn hay trẻ em?",
|
|
"quick_replies": ["Người lớn", "Trẻ em"],
|
|
}), CATALOG, RESOLVER)
|
|
frame = understander.understand("liều paracetamol")
|
|
assert frame.quick_replies == ("Người lớn", "Trẻ em")
|
|
|
|
|
|
def test_quick_replies_are_dynamic_but_bounded_before_becoming_ui_chips():
|
|
understander = LlmQueryUnderstander(_FixedLlm({
|
|
"turn_type": "dosing_calc", "drugs": ["paracetamol_acetaminophen"],
|
|
"unknown_drugs": [], "attribute": None, "population": None,
|
|
"weight_kg": None, "age_text": None, "indication": None,
|
|
"needs_clarify": True, "clarify_reason": "Chọn nhóm phù hợp?",
|
|
"quick_replies": [
|
|
" Người lớn ", "người lớn", "Trẻ em", 12,
|
|
"Phụ nữ có thai", "Người cao tuổi", "Lựa chọn thứ năm",
|
|
],
|
|
}), CATALOG, RESOLVER)
|
|
|
|
frame = understander.understand("liều paracetamol")
|
|
|
|
assert frame.quick_replies == (
|
|
"Người lớn", "Trẻ em", "Phụ nữ có thai", "Người cao tuổi"
|
|
)
|
|
|
|
|
|
def test_string_false_does_not_turn_into_a_clarification():
|
|
understander = LlmQueryUnderstander(_FixedLlm({
|
|
"turn_type": "drug_attribute", "drugs": ["paracetamol_acetaminophen"],
|
|
"unknown_drugs": [], "attribute": "chong_chi_dinh", "population": None,
|
|
"weight_kg": None, "age_text": None, "indication": None,
|
|
"needs_clarify": "false", "clarify_reason": "Không được hiển thị",
|
|
"quick_replies": ["Có", "Không"],
|
|
}), CATALOG, RESOLVER)
|
|
|
|
frame = understander.understand("chống chỉ định paracetamol")
|
|
|
|
assert frame.needs_clarify is False
|
|
assert frame.quick_replies == ()
|
|
|
|
|
|
def test_missing_quick_replies_key_defaults_to_empty_not_a_crash():
|
|
"""The model is asked for `quick_replies` but structured-output providers
|
|
aren't guaranteed to include every optional key — a clarify without it
|
|
must still parse, just with no chips."""
|
|
understander = LlmQueryUnderstander(_FixedLlm({
|
|
"turn_type": "dosing_calc", "drugs": [],
|
|
"unknown_drugs": [], "attribute": None, "population": None,
|
|
"weight_kg": None, "age_text": None, "indication": None,
|
|
"needs_clarify": True, "clarify_reason": "Cân nặng bao nhiêu kg?",
|
|
}), CATALOG, RESOLVER)
|
|
frame = understander.understand("liều cho bé")
|
|
assert frame.quick_replies == ()
|
|
|
|
|
|
def test_route_is_parsed_when_the_model_resolves_it():
|
|
"""The 2026-08-07 bug: a bare reply like 'Uống' answering the model's own
|
|
prior route question had nowhere to be recorded (QueryFrame had no route
|
|
field), so the model could only repeat its clarify_reason verbatim."""
|
|
understander = LlmQueryUnderstander(_FixedLlm({
|
|
"turn_type": "dosing_calc", "drugs": ["paracetamol_acetaminophen"],
|
|
"unknown_drugs": [], "attribute": "lieu_luong_va_cach_dung",
|
|
"population": "nguoi_lon", "weight_kg": None, "age_text": None,
|
|
"indication": None, "route": "uong",
|
|
"needs_clarify": False, "clarify_reason": None,
|
|
}), CATALOG, RESOLVER)
|
|
frame = understander.understand(
|
|
"Uống",
|
|
history=(
|
|
"Người dùng: Liều paracetamol hạ sốt là bao nhiêu?",
|
|
"Trợ lý: Người lớn hay trẻ em? Uống hay đặt trực tràng?",
|
|
"Người dùng: Người lớn",
|
|
"Trợ lý: Uống hay đặt trực tràng?",
|
|
),
|
|
)
|
|
assert frame.route == "uong"
|
|
assert frame.needs_clarify is False
|
|
|
|
|
|
def test_follow_up_exposes_a_first_class_standalone_query():
|
|
understander = LlmQueryUnderstander(_FixedLlm({
|
|
"turn_type": "drug_attribute", "drugs": ["metformin"],
|
|
"unknown_drugs": [], "attribute": "chong_chi_dinh",
|
|
"population": None, "weight_kg": None, "age_text": None,
|
|
"indication": None, "route": None,
|
|
"standalone_query": "Chống chỉ định của metformin",
|
|
"depends_on_previous_turn": True,
|
|
"needs_clarify": False, "clarify_reason": None,
|
|
}), CATALOG, RESOLVER)
|
|
|
|
frame = understander.understand(
|
|
"thế còn chống chỉ định?",
|
|
history=("Người dùng: Metformin dùng để làm gì?",),
|
|
)
|
|
|
|
assert frame.standalone_query == "Chống chỉ định của metformin"
|
|
assert frame.depends_on_previous_turn is True
|
|
|
|
|
|
def test_missing_route_key_defaults_to_none_not_a_crash():
|
|
understander = LlmQueryUnderstander(_FixedLlm({
|
|
"turn_type": "dosing_calc", "drugs": [],
|
|
"unknown_drugs": [], "attribute": None, "population": None,
|
|
"weight_kg": None, "age_text": None, "indication": None,
|
|
"needs_clarify": False, "clarify_reason": None,
|
|
}), CATALOG, RESOLVER)
|
|
frame = understander.understand("liều metformin")
|
|
assert frame.route is None
|
|
|
|
|
|
def test_unrecognised_turn_type_falls_back_based_on_whether_a_drug_resolved():
|
|
understander = LlmQueryUnderstander(_FixedLlm({
|
|
"turn_type": "not_a_real_type", "drugs": ["metformin"],
|
|
"unknown_drugs": [], "attribute": None, "population": None,
|
|
"weight_kg": None, "age_text": None, "indication": None,
|
|
"needs_clarify": False, "clarify_reason": None,
|
|
}), CATALOG, RESOLVER)
|
|
frame = understander.understand("metformin")
|
|
assert frame.turn_type == "drug_attribute"
|
|
|
|
|
|
def test_every_section_key_has_a_hint():
|
|
# A key with no gloss shown to the model is exactly the bug this fixed —
|
|
# never let a new SECTION_KEYS entry silently ship without one.
|
|
assert set(SECTION_KEYS) == set(SECTION_KEY_HINTS)
|
|
|
|
|
|
def test_prompt_disambiguates_than_trong_from_chong_chi_dinh():
|
|
"""Reproduces the live 2026-08-06 golden-eval finding: a bare section
|
|
key list gave the model nothing to tell "thận trọng" (precautions) apart
|
|
from "chống chỉ định" (contraindications) — 9/9 live calls for "X cần
|
|
thận trọng gì?" picked chong_chi_dinh, silently answering from the wrong
|
|
section and dropping safety content the precautions section actually
|
|
has (metformin's lactic-acidosis warning, gentamicin's oto/nephrotoxicity).
|
|
Fixed with an inline gloss; this pins the gloss's presence in the actual
|
|
request sent, not just its existence in the hints dict."""
|
|
captured = {}
|
|
|
|
class _CapturingLlm:
|
|
def generate(self, system, user, schema):
|
|
captured["user"] = user
|
|
return json.dumps({
|
|
"turn_type": "drug_attribute", "drugs": ["metformin"],
|
|
"unknown_drugs": [], "attribute": "than_trong", "population": None,
|
|
"weight_kg": None, "age_text": None, "indication": None,
|
|
"needs_clarify": False, "clarify_reason": None,
|
|
})
|
|
|
|
understander = LlmQueryUnderstander(_CapturingLlm(), CATALOG, RESOLVER)
|
|
understander.understand("metformin cần thận trọng gì?")
|
|
|
|
assert "KHÁC chống chỉ định" in captured["user"]
|
|
assert "nhiễm toan lactic" in captured["user"]
|
|
|
|
|
|
def test_invalid_attribute_is_dropped_not_passed_through():
|
|
understander = LlmQueryUnderstander(_FixedLlm({
|
|
"turn_type": "drug_attribute", "drugs": ["metformin"],
|
|
"unknown_drugs": [], "attribute": "not_a_real_section", "population": None,
|
|
"weight_kg": None, "age_text": None, "indication": None,
|
|
"needs_clarify": False, "clarify_reason": None,
|
|
}), CATALOG, RESOLVER)
|
|
frame = understander.understand("metformin")
|
|
assert frame.attribute is None
|
|
|
|
|
|
# --- F-10: provider outage during the ONE call site that had no error
|
|
# handling at all ------------------------------------------------------------
|
|
|
|
|
|
# --- F-11: prior-frame merge — the code-level backstop for the model
|
|
# dropping an already-established slot mid clarify-chain. Found live
|
|
# 2026-08-07 (50-question hand-typed browser audit): reproduced 3 times
|
|
# independently as either a non-terminating re-ask of the same clarify
|
|
# question, or a stale drug bleeding into an unrelated new topic. --------
|
|
|
|
|
|
def test_prior_frame_known_fields_survive_a_short_reply_the_model_drops():
|
|
"""The Insulin/Azithromycin shape: the new call's own JSON comes back
|
|
with the just-answered field null (a real, observed failure — the model
|
|
is asked to restate it and sometimes doesn't), but since this turn named
|
|
no drug of its own (a short reply like "20kg" never does), the previously
|
|
established fields must survive via the merge, not be silently lost."""
|
|
understander = LlmQueryUnderstander(_FixedLlm({
|
|
"turn_type": "dosing_calc", "drugs": [],
|
|
"unknown_drugs": [], "attribute": None, "population": None,
|
|
"weight_kg": 20, "age_text": None, "indication": None,
|
|
"needs_clarify": False, "clarify_reason": None,
|
|
}), CATALOG, RESOLVER)
|
|
prior = QueryFrame(
|
|
turn_type="dosing_calc", drugs=("paracetamol_acetaminophen",),
|
|
attribute="lieu_luong_va_cach_dung", needs_clarify=True,
|
|
clarify_reason="Bé nặng bao nhiêu kg?",
|
|
)
|
|
frame = understander.understand("bé nặng 20 cân", prior_frame=prior)
|
|
assert frame.drugs == ("paracetamol_acetaminophen",)
|
|
assert frame.attribute == "lieu_luong_va_cach_dung"
|
|
assert frame.weight_kg == 20
|
|
|
|
|
|
def test_prior_frame_is_not_merged_when_the_turn_resolves_a_different_drug():
|
|
"""The headache/OMEPRAZOL bleed this guards against: a turn that itself
|
|
names a real, different drug is a genuine topic change and must not
|
|
inherit the old drug's population/weight/route — merging here would
|
|
reproduce the exact bug being fixed."""
|
|
understander = LlmQueryUnderstander(_FixedLlm({
|
|
"turn_type": "drug_attribute", "drugs": ["metformin"],
|
|
"unknown_drugs": [], "attribute": "chi_dinh", "population": None,
|
|
"weight_kg": None, "age_text": None, "indication": None,
|
|
"needs_clarify": False, "clarify_reason": None,
|
|
}), CATALOG, RESOLVER)
|
|
prior = QueryFrame(
|
|
turn_type="drug_attribute", drugs=("paracetamol_acetaminophen",),
|
|
population="tre_em", weight_kg=20, needs_clarify=True,
|
|
clarify_reason="Bé nặng bao nhiêu kg?",
|
|
)
|
|
frame = understander.understand("chỉ định của metformin là gì", prior_frame=prior)
|
|
assert frame.drugs == ("metformin",)
|
|
assert frame.population is None
|
|
assert frame.weight_kg is None
|
|
|
|
|
|
def test_prior_frame_that_was_already_resolved_is_not_merged():
|
|
"""A prior turn that already answered (needs_clarify=False) has nothing
|
|
to continue — merging it into a brand-new turn would leak stale state
|
|
into an unrelated question that happens to follow it."""
|
|
understander = LlmQueryUnderstander(_FixedLlm({
|
|
"turn_type": "smalltalk", "drugs": [],
|
|
"unknown_drugs": [], "attribute": None, "population": None,
|
|
"weight_kg": None, "age_text": None, "indication": None,
|
|
"needs_clarify": False, "clarify_reason": None,
|
|
}), CATALOG, RESOLVER)
|
|
prior = QueryFrame(
|
|
turn_type="drug_attribute", drugs=("metformin",),
|
|
population="nguoi_lon", needs_clarify=False,
|
|
)
|
|
frame = understander.understand("cảm ơn bạn", prior_frame=prior)
|
|
assert frame.drugs == ()
|
|
assert frame.population is None
|
|
|
|
|
|
def test_known_facts_block_is_sent_to_the_model_on_a_clarify_continuation():
|
|
captured = {}
|
|
|
|
class _CapturingLlm:
|
|
def generate(self, system, user, schema):
|
|
captured["user"] = user
|
|
return json.dumps({
|
|
"turn_type": "dosing_calc", "drugs": ["paracetamol_acetaminophen"],
|
|
"unknown_drugs": [], "attribute": None, "population": None,
|
|
"weight_kg": 20, "age_text": None, "indication": None,
|
|
"needs_clarify": False, "clarify_reason": None,
|
|
})
|
|
|
|
understander = LlmQueryUnderstander(_CapturingLlm(), CATALOG, RESOLVER)
|
|
prior = QueryFrame(
|
|
turn_type="dosing_calc", drugs=("paracetamol_acetaminophen",),
|
|
population="tre_em", needs_clarify=True,
|
|
clarify_reason="Bé nặng bao nhiêu kg?",
|
|
)
|
|
understander.understand("20 cân", prior_frame=prior)
|
|
|
|
assert "THÔNG TIN ĐÃ XÁC ĐỊNH" in captured["user"]
|
|
assert "paracetamol_acetaminophen" in captured["user"]
|
|
|
|
|
|
def test_no_known_facts_block_when_there_is_no_prior_clarify():
|
|
captured = {}
|
|
|
|
class _CapturingLlm:
|
|
def generate(self, system, user, schema):
|
|
captured["user"] = user
|
|
return json.dumps({
|
|
"turn_type": "smalltalk", "drugs": [],
|
|
"unknown_drugs": [], "attribute": None, "population": None,
|
|
"weight_kg": None, "age_text": None, "indication": None,
|
|
"needs_clarify": False, "clarify_reason": None,
|
|
})
|
|
|
|
understander = LlmQueryUnderstander(_CapturingLlm(), CATALOG, RESOLVER)
|
|
understander.understand("chào bạn")
|
|
assert "THÔNG TIN ĐÃ XÁC ĐỊNH" not in captured["user"]
|
|
|
|
|
|
def test_provider_outage_fails_closed_to_a_clarify_not_an_unhandled_crash():
|
|
"""Found live 2026-08-07: unlike every other LLM call site in this
|
|
product, `understand()` had no try/except around its call at all — a
|
|
Bedrock outage here propagated straight through `RagAgent.handle()`
|
|
into an unhandled 500 (`routers/rag.py` only wraps the trace-save call,
|
|
not `agent.handle()`), instead of the graceful abstain every other
|
|
failure mode already gets."""
|
|
understander = LlmQueryUnderstander(
|
|
_FixedLlm(AnswerGenerationUnavailable("Bedrock unreachable")),
|
|
CATALOG, RESOLVER,
|
|
)
|
|
frame = understander.understand("liều paracetamol cho người lớn")
|
|
assert frame.needs_clarify is True
|
|
assert frame.clarify_reason is not None
|
|
assert frame.drugs == ()
|
|
assert frame.quick_replies == ()
|