Wire the guarded conversational RAG answer layer end-to-end
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
"""Follow-ups must inherit context, and must never inherit it silently.
|
||||
|
||||
The cases here are the ones the owner named on 2026-08-05: "còn trẻ em thì
|
||||
sao?", "giải thích kỹ hơn", and not making the user repeat themselves. The
|
||||
adversarial cases are the ones that make inheritance dangerous in a formulary
|
||||
— a stale drug, and an explicit mention being overridden by context.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from rag.conversation import (
|
||||
FOCUS_TTL_TURNS,
|
||||
ConversationState,
|
||||
Focus,
|
||||
Turn,
|
||||
detect_population,
|
||||
detect_verbosity,
|
||||
looks_like_followup,
|
||||
resolve_against,
|
||||
update_focus,
|
||||
)
|
||||
|
||||
|
||||
def _state(turn_count: int = 1, **focus_fields) -> ConversationState:
|
||||
focus = Focus()
|
||||
for name, value in focus_fields.items():
|
||||
focus = focus.with_field(name, value, turn_count - 1)
|
||||
return ConversationState("c1", focus=focus, turn_count=turn_count)
|
||||
|
||||
|
||||
# --- the follow-ups the owner asked for --------------------------------------
|
||||
|
||||
|
||||
def test_con_tre_em_thi_sao_inherits_drug_and_section():
|
||||
state = _state(drug_id="metformin", section_key="lieu_luong_va_cach_dung")
|
||||
|
||||
resolved = resolve_against(state, "còn trẻ em thì sao?", None, None)
|
||||
|
||||
assert resolved.drug_id == "metformin"
|
||||
assert resolved.section_key == "lieu_luong_va_cach_dung"
|
||||
assert resolved.population == "tre_em"
|
||||
assert resolved.inherited_drug is True
|
||||
|
||||
|
||||
def test_giai_thich_ky_hon_sets_verbosity_and_keeps_the_topic():
|
||||
state = _state(drug_id="warfarin", section_key="tuong_tac_thuoc")
|
||||
|
||||
resolved = resolve_against(state, "giải thích kỹ hơn", None, None)
|
||||
|
||||
assert resolved.drug_id == "warfarin"
|
||||
assert resolved.verbosity == "detailed"
|
||||
|
||||
|
||||
def test_the_user_is_not_made_to_repeat_the_drug():
|
||||
state = _state(drug_id="metformin")
|
||||
|
||||
resolved = resolve_against(state, "chống chỉ định", None, "chong_chi_dinh")
|
||||
|
||||
assert resolved.drug_id == "metformin"
|
||||
assert resolved.section_key == "chong_chi_dinh"
|
||||
|
||||
|
||||
# --- what makes inheritance safe ---------------------------------------------
|
||||
|
||||
|
||||
def test_an_explicit_drug_always_beats_context():
|
||||
"""Naming a drug must override whatever the conversation was about, or a
|
||||
deliberate topic change silently answers about the previous medicine."""
|
||||
state = _state(drug_id="metformin", section_key="lieu_luong_va_cach_dung")
|
||||
|
||||
resolved = resolve_against(state, "liều dùng warfarin", "warfarin", None)
|
||||
|
||||
assert resolved.drug_id == "warfarin"
|
||||
assert resolved.inherited_drug is False
|
||||
|
||||
|
||||
def test_a_stale_drug_is_dropped_rather_than_inherited():
|
||||
"""Beyond the TTL the drug is not context, it is a hazard."""
|
||||
state = _state(turn_count=FOCUS_TTL_TURNS + 3, drug_id="metformin")
|
||||
# `_state` stamps at turn_count - 1, so age is 1; age it past the TTL.
|
||||
aged = ConversationState(
|
||||
"c1",
|
||||
focus=Focus(drug_id="metformin", set_at_turn={"drug_id": 0}),
|
||||
turn_count=FOCUS_TTL_TURNS + 2,
|
||||
)
|
||||
|
||||
assert state.inherited("drug_id") == "metformin"
|
||||
assert aged.inherited("drug_id") is None
|
||||
|
||||
resolved = resolve_against(aged, "còn trẻ em thì sao?", None, None)
|
||||
assert resolved.drug_id is None
|
||||
|
||||
|
||||
def test_an_inherited_drug_must_be_named_in_the_answer():
|
||||
state = _state(drug_id="metformin")
|
||||
|
||||
inherited = resolve_against(state, "còn trẻ em thì sao?", None, None)
|
||||
explicit = resolve_against(state, "liều warfarin", "warfarin", None)
|
||||
|
||||
assert inherited.needs_carry_over_notice is True
|
||||
assert explicit.needs_carry_over_notice is False
|
||||
|
||||
|
||||
# --- phrase detection ---------------------------------------------------------
|
||||
|
||||
|
||||
def test_longest_population_phrase_wins():
|
||||
"""`phụ nữ cho con bú` must not be read as `phụ nữ`, and `trẻ sơ sinh`
|
||||
must not be read as `trẻ em` — the same rule `sections.py` relies on."""
|
||||
assert detect_population("phụ nữ cho con bú") == "phu_nu_cho_con_bu"
|
||||
assert detect_population("trẻ sơ sinh dùng sao") == "tre_so_sinh"
|
||||
assert detect_population("bà bầu uống được không") == "phu_nu_co_thai"
|
||||
assert detect_population("liều cho người lớn") == "nguoi_lon"
|
||||
|
||||
|
||||
def test_no_population_named_is_none_not_a_guess():
|
||||
assert detect_population("liều dùng paracetamol") is None
|
||||
assert detect_verbosity("liều dùng paracetamol") is None
|
||||
|
||||
|
||||
def test_followup_markers():
|
||||
assert looks_like_followup("còn trẻ em thì sao?") is True
|
||||
assert looks_like_followup("so với metformin thì sao") is True
|
||||
assert looks_like_followup("liều dùng paracetamol") is False
|
||||
|
||||
|
||||
# --- window and focus update --------------------------------------------------
|
||||
|
||||
|
||||
def test_recent_window_evicts_oldest():
|
||||
state = ConversationState("c1")
|
||||
for index in range(8):
|
||||
state = state.append(Turn("user", f"q{index}", "2026-08-05"), window=6)
|
||||
|
||||
assert len(state.recent) == 6
|
||||
assert state.recent[0].text == "q2"
|
||||
assert state.turn_count == 8
|
||||
|
||||
|
||||
def test_focus_update_stamps_the_current_turn():
|
||||
state = _state(turn_count=3)
|
||||
|
||||
resolved = resolve_against(state, "liều dùng metformin", "metformin", "lieu_luong_va_cach_dung")
|
||||
focus = update_focus(state, resolved)
|
||||
|
||||
assert focus.drug_id == "metformin"
|
||||
assert focus.set_at_turn["drug_id"] == 3
|
||||
Reference in New Issue
Block a user