Fix live multi-turn: pass the resolved drug, stop did-you-mean garbage

This commit is contained in:
2026-08-05 16:54:35 +07:00
parent ef08b4929e
commit 1e8cbdb586
29 changed files with 2013 additions and 83 deletions
@@ -0,0 +1,147 @@
"""The Converse adapter must translate provider failure into the domain error,
isolate the JSON envelope the API cannot enforce, and never leak an SDK type.
No network: a stub client stands in for `bedrock-runtime`. The grounding check
that actually guards correctness lives in `rag.grounding` and is tested there;
here we prove only the adapter's envelope and failure contract.
"""
from __future__ import annotations
import json
import pytest
from adapters.bedrock_converse import (
BedrockConverseAnswerGenerator,
BedrockCohereReranker,
RerankUnavailable,
_extract_json,
)
from rag.ports import AnswerGenerationUnavailable
def _converse_reply(text: str, stop_reason: str = "end_turn") -> dict:
return {
"stopReason": stop_reason,
"output": {"message": {"content": [{"text": text}]}},
"usage": {"inputTokens": 10, "outputTokens": 5},
}
class _Client:
def __init__(self, reply: dict) -> None:
self._reply = reply
self.calls: list[dict] = []
def converse(self, **kwargs):
self.calls.append(kwargs)
return self._reply
def _payload(answer: str = "Liều 500 mg [1]", sufficient: bool = True) -> str:
return json.dumps({"answer": answer, "evidence_sufficient": sufficient}, ensure_ascii=False)
def test_returns_the_models_json_on_success():
client = _Client(_converse_reply(_payload()))
gen = BedrockConverseAnswerGenerator(client=client, model_id="deepseek.v3.2")
raw = gen.generate("system", "user", {"type": "object"})
parsed = json.loads(raw)
assert parsed["answer"] == "Liều 500 mg [1]"
assert parsed["evidence_sufficient"] is True
# The prompt carries the schema and a JSON-only directive to the model.
sent = client.calls[0]["messages"][0]["content"][0]["text"]
assert "JSON" in sent
def test_strips_a_markdown_fence_the_api_cannot_forbid():
fenced = f"```json\n{_payload()}\n```"
gen = BedrockConverseAnswerGenerator(client=_Client(_converse_reply(fenced)))
parsed = json.loads(gen.generate("s", "u", {}))
assert parsed["answer"] == "Liều 500 mg [1]"
def test_extracts_json_when_the_model_adds_prose_around_it():
noisy = f"Đây là câu trả lời: {_payload()} Hết."
assert json.loads(_extract_json(noisy))["evidence_sufficient"] is True
def test_a_filtered_stop_reason_is_an_outage_not_an_answer():
client = _Client(_converse_reply("", stop_reason="content_filtered"))
gen = BedrockConverseAnswerGenerator(client=client)
with pytest.raises(AnswerGenerationUnavailable):
gen.generate("s", "u", {})
def test_empty_text_raises_rather_than_returning_blank():
gen = BedrockConverseAnswerGenerator(client=_Client(_converse_reply(" ")))
with pytest.raises(AnswerGenerationUnavailable):
gen.generate("s", "u", {})
def test_real_botocore_client_error_becomes_the_domain_error():
botocore_exceptions = pytest.importorskip("botocore.exceptions")
class _Refusing:
def converse(self, **kwargs):
raise botocore_exceptions.ClientError(
{"Error": {"Code": "AccessDeniedException", "Message": "denied"}},
"Converse",
)
gen = BedrockConverseAnswerGenerator(client=_Refusing())
with pytest.raises(AnswerGenerationUnavailable):
gen.generate("s", "u", {})
# --- reranker -----------------------------------------------------------------
class _RerankBody:
def __init__(self, payload: dict) -> None:
self._data = json.dumps(payload).encode("utf-8")
def read(self) -> bytes:
return self._data
class _RerankClient:
def __init__(self, payload: dict) -> None:
self._payload = payload
def invoke_model(self, **kwargs):
return {"body": _RerankBody(self._payload)}
def test_rerank_returns_indices_most_relevant_first():
client = _RerankClient({"results": [{"index": 2}, {"index": 0}, {"index": 1}]})
reranker = BedrockCohereReranker(client=client)
order = reranker.rerank("chống chỉ định", ["a", "b", "c"])
assert order == [2, 0, 1]
def test_rerank_raises_so_the_caller_keeps_original_order():
botocore_exceptions = pytest.importorskip("botocore.exceptions")
class _Refusing:
def invoke_model(self, **kwargs):
raise botocore_exceptions.ClientError(
{"Error": {"Code": "AccessDeniedException", "Message": "denied"}},
"InvokeModel",
)
with pytest.raises(RerankUnavailable):
BedrockCohereReranker(client=_Refusing()).rerank("q", ["a", "b"])
def test_rerank_of_nothing_is_empty():
assert BedrockCohereReranker(client=_RerankClient({})).rerank("q", []) == []