Fix ai-service Dockerfile: bake in drug_entities.json, override its path
This commit is contained in:
@@ -16,6 +16,7 @@ from rag.models import (
|
||||
SourceRef,
|
||||
SubjectScope,
|
||||
)
|
||||
from rag.ports import AnswerGenerationUnavailable
|
||||
from rag.prompt import build_request
|
||||
|
||||
|
||||
@@ -41,22 +42,31 @@ class _Routing:
|
||||
|
||||
|
||||
class _Generator:
|
||||
def __init__(self, payload: dict, entailment_payload: dict | None = None) -> None:
|
||||
def __init__(
|
||||
self, payload: dict, entailment_payload: dict | None = None,
|
||||
sufficiency_payload: dict | None = None,
|
||||
) -> None:
|
||||
self._payload = payload
|
||||
self._entailment_payload = entailment_payload or {
|
||||
"entailed": True,
|
||||
"unsupported": [],
|
||||
}
|
||||
self._sufficiency_payload = sufficiency_payload
|
||||
|
||||
def generate(self, system: str, user: str, schema: dict) -> str: # noqa: ARG002
|
||||
# `_generate` also runs a post-generation entailment check; tell the
|
||||
# two request shapes apart by schema so callers here only need to
|
||||
# fake the main answer, not both.
|
||||
payload = (
|
||||
self._entailment_payload
|
||||
if "entailed" in schema.get("properties", {})
|
||||
else self._payload
|
||||
)
|
||||
# `_generate` also runs a post-generation entailment check, and
|
||||
# `_check_sufficiency` runs its own separate call before that — tell
|
||||
# the three request shapes apart by schema so a test asserting on one
|
||||
# doesn't have to also shape a payload for the others.
|
||||
props = schema.get("properties", {})
|
||||
if "entailed" in props:
|
||||
payload = self._entailment_payload
|
||||
elif "sufficient" in props and self._sufficiency_payload is not None:
|
||||
payload = self._sufficiency_payload
|
||||
else:
|
||||
payload = self._payload
|
||||
if isinstance(payload, BaseException):
|
||||
raise payload
|
||||
return json.dumps(payload, ensure_ascii=False)
|
||||
|
||||
|
||||
@@ -122,6 +132,42 @@ def test_underspecified_dose_asks_instead_of_dumping():
|
||||
assert "tuổi" in g.clarification
|
||||
assert g.answer == g.clarification
|
||||
assert g.generated is False
|
||||
assert g.quick_replies == ()
|
||||
|
||||
|
||||
def test_underspecified_dose_carries_quick_replies_when_the_model_offers_them():
|
||||
result = _answerable(_evidence(0, 100), _evidence(1, 200))
|
||||
gen = _Generator(
|
||||
{"sufficient": False,
|
||||
"clarifying_question": "Người lớn hay trẻ em?",
|
||||
"quick_replies": ["Người lớn", "Trẻ em"]}
|
||||
)
|
||||
service = GroundedAnswerService(_Routing(result), gen)
|
||||
|
||||
g = service.answer("paracetamol", SubjectScope.HUMAN, QueryIntent.FACT_LOOKUP)
|
||||
|
||||
assert g.quick_replies == ("Người lớn", "Trẻ em")
|
||||
|
||||
|
||||
def test_sufficiency_check_outage_fails_open_to_generation_not_abstain():
|
||||
"""F-10: `_check_sufficiency` documents (and this pins) a deliberate
|
||||
fail-OPEN on provider outage — unlike every other failure mode in this
|
||||
service, a sufficiency-check outage does not abstain, it just skips the
|
||||
clarify heuristic and lets grounding/entailment (tested elsewhere) be
|
||||
the real safety net on whatever gets generated next."""
|
||||
result = _answerable(_evidence(0, 100), _evidence(1, 200))
|
||||
gen = _Generator(
|
||||
{"answer": "Đoạn bằng chứng 0 [1].", "evidence_sufficient": True,
|
||||
"clarifying_question": None},
|
||||
sufficiency_payload=AnswerGenerationUnavailable("Bedrock unreachable"),
|
||||
)
|
||||
service = GroundedAnswerService(_Routing(result), gen)
|
||||
|
||||
g = service.answer("liều người lớn", SubjectScope.HUMAN, QueryIntent.FACT_LOOKUP)
|
||||
|
||||
assert g.clarification is None
|
||||
assert g.result.decision == EvidenceDecision.ANSWERABLE
|
||||
assert g.generated is True
|
||||
|
||||
|
||||
def test_sufficient_query_is_not_turned_into_a_clarification():
|
||||
@@ -144,3 +190,52 @@ def test_bare_name_builds_an_intro_prompt():
|
||||
normal = build_request("Liều?", ("đoạn A",), intro=False)
|
||||
assert "CÂU HỎI:" in normal.user
|
||||
assert "GIỚI THIỆU" not in normal.user
|
||||
|
||||
|
||||
def test_list_mode_prompt_instructs_enumerating_every_drug_not_ranking():
|
||||
"""2026-08-07 finding: without an explicit instruction, the model picked
|
||||
one drug out of 8 real symptom_to_drug matches and silently dropped the
|
||||
rest — verified live. `list_mode` closes that."""
|
||||
request = build_request("thuốc gì trị sốt", ("chỉ định A", "chỉ định B"), list_mode=True)
|
||||
assert "LIỆT KÊ TẤT CẢ" in request.user
|
||||
assert "không xếp hạng" in request.user or "KHÔNG" in request.user
|
||||
assert "CÂU HỎI:" in request.user
|
||||
|
||||
|
||||
def test_list_mode_skips_the_sufficiency_clarify():
|
||||
"""If the sufficiency call were NOT actually skipped, it would read
|
||||
`sufficiency_payload` (`sufficient=False`) and clarify. `list_mode=True`
|
||||
must never call it at all, so only the real answer payload is ever read."""
|
||||
result = _answerable(_evidence(0, 100), _evidence(1, 200))
|
||||
gen = _Generator(
|
||||
{"answer": "Đoạn bằng chứng 0 [1]. Đoạn bằng chứng 1 [2].",
|
||||
"evidence_sufficient": True, "clarifying_question": None},
|
||||
sufficiency_payload={
|
||||
"sufficient": False, "clarifying_question": "Loại nào?", "quick_replies": [],
|
||||
},
|
||||
)
|
||||
service = GroundedAnswerService(_Routing(result), gen)
|
||||
|
||||
g = service.answer_from_result("thuốc gì trị sốt", result, list_mode=True)
|
||||
|
||||
assert g.clarification is None
|
||||
assert g.answer is not None
|
||||
assert g.generated is True
|
||||
|
||||
|
||||
def test_without_list_mode_the_same_evidence_does_ask_for_clarification():
|
||||
"""Control for the test above: the same sufficiency payload, without
|
||||
`list_mode`, must actually clarify — proving the previous test's "not
|
||||
skipped" branch is reachable and would have failed loudly."""
|
||||
result = _answerable(_evidence(0, 100), _evidence(1, 200))
|
||||
gen = _Generator(
|
||||
{"answer": "unused", "evidence_sufficient": True, "clarifying_question": None},
|
||||
sufficiency_payload={
|
||||
"sufficient": False, "clarifying_question": "Loại nào?", "quick_replies": [],
|
||||
},
|
||||
)
|
||||
service = GroundedAnswerService(_Routing(result), gen)
|
||||
|
||||
g = service.answer_from_result("thuốc gì trị sốt", result, list_mode=False)
|
||||
|
||||
assert g.clarification == "Loại nào?"
|
||||
|
||||
Reference in New Issue
Block a user