186 lines
5.8 KiB
Python
186 lines
5.8 KiB
Python
import pytest
|
|
|
|
|
|
def _fixture(path):
|
|
path.write_text(
|
|
"""
|
|
schema_version: 1
|
|
queries:
|
|
- id: lexical-code
|
|
query: Qual è il codice ICD-10?
|
|
profile: lexical
|
|
purpose: schema_linking
|
|
expected: [evidence:icd]
|
|
- id: semantic-age
|
|
query: Come distinguo i pazienti pediatrici?
|
|
profile: semantic
|
|
purpose: sql_generation
|
|
expected: [evidence:fascia-pediatrica]
|
|
- id: mixed-formula
|
|
query: Formula per patient.birth_date?
|
|
profile: mixed
|
|
purpose: sql_generation
|
|
expected: [evidence:fascia-pediatrica]
|
|
""".strip(),
|
|
encoding="utf-8",
|
|
)
|
|
|
|
|
|
class _Embedder:
|
|
def embed_query(self, query):
|
|
return [float(len(query))]
|
|
|
|
|
|
class _Hit:
|
|
def __init__(self, evidence_id, kind, score):
|
|
self.id = evidence_id + ":fragment"
|
|
self.similarity = score
|
|
self.metadata = {"evidence_id": evidence_id, "evidence_kind": kind}
|
|
|
|
|
|
class _Searcher:
|
|
def __init__(self):
|
|
self.calls = []
|
|
|
|
def search(self, collections, embedding, **kwargs):
|
|
self.calls.append((collections, embedding, kwargs))
|
|
mode = kwargs["retrieval_mode"]
|
|
if mode == "dense":
|
|
return [_Hit("evidence:fascia-pediatrica", "formula", 0.9)]
|
|
if mode == "bm25":
|
|
return [_Hit("evidence:icd", "enum", 0.8)]
|
|
return [
|
|
_Hit("evidence:fascia-pediatrica", "formula", 0.95),
|
|
_Hit("evidence:icd", "enum", 0.8),
|
|
]
|
|
|
|
|
|
def test_evaluation_reports_branch_and_fused_ranks_without_turning_diagnostics_into_gates(tmp_path):
|
|
from tht.evidence.evaluation import evaluate_retrieval, load_evaluation_fixture
|
|
|
|
fixture_path = tmp_path / "evaluation.yaml"
|
|
_fixture(fixture_path)
|
|
searcher = _Searcher()
|
|
|
|
report = evaluate_retrieval(
|
|
load_evaluation_fixture(fixture_path),
|
|
workspace_revision="a" * 40,
|
|
document_generations={"doc:one": "gen:" + "1" * 32},
|
|
workspace_id="psd-clinical",
|
|
language="italian",
|
|
searcher=searcher,
|
|
embedder=_Embedder(),
|
|
)
|
|
|
|
assert report.passed is True
|
|
assert report.workspace_revision == "a" * 40
|
|
assert report.vector_generation == "gen:" + "1" * 32
|
|
assert report.rrf == {"algorithm": "rrf", "k": 60, "prefetch_limit_multiplier": 2}
|
|
assert report.queries[0].hit_at_5 is True
|
|
assert report.queries[0].hit_at_10 is True
|
|
assert report.queries[0].missing_expected == ()
|
|
assert report.queries[0].expected[0].dense_rank is None
|
|
assert report.queries[0].expected[0].bm25_rank == 1
|
|
assert report.queries[0].expected[0].fused_rank == 2
|
|
assert report.counts_by_expected_kind == {"enum": 1, "formula": 2}
|
|
assert len(searcher.calls) == 9
|
|
assert {call[2]["retrieval_mode"] for call in searcher.calls} == {"dense", "bm25", "fused"}
|
|
assert all(call[2]["metadata_filter"] == {
|
|
"workspace_id": "psd-clinical",
|
|
"vector_generation": "gen:" + "1" * 32,
|
|
"document_ids": ["doc:one"],
|
|
"purpose": call[2]["metadata_filter"]["purpose"],
|
|
"required_kinds": [],
|
|
"required_concepts": [],
|
|
"required_tables": [],
|
|
"required_columns": [],
|
|
} for call in searcher.calls)
|
|
|
|
|
|
def test_evaluation_fails_only_when_a_query_has_no_expected_fused_hit_in_top_ten(tmp_path):
|
|
from tht.evidence.evaluation import evaluate_retrieval, load_evaluation_fixture
|
|
|
|
fixture_path = tmp_path / "evaluation.yaml"
|
|
_fixture(fixture_path)
|
|
|
|
class MissingExpectedSearcher(_Searcher):
|
|
def search(self, collections, embedding, **kwargs):
|
|
self.calls.append((collections, embedding, kwargs))
|
|
return [_Hit("evidence:other", "domain", 1.0)]
|
|
|
|
report = evaluate_retrieval(
|
|
load_evaluation_fixture(fixture_path),
|
|
workspace_revision="a" * 40,
|
|
document_generations={"doc:one": "gen:" + "1" * 32},
|
|
workspace_id="psd-clinical",
|
|
language="italian",
|
|
searcher=MissingExpectedSearcher(),
|
|
embedder=_Embedder(),
|
|
expected_kinds={"evidence:icd": "enum", "evidence:fascia-pediatrica": "formula"},
|
|
)
|
|
|
|
assert report.passed is False
|
|
assert all(query.hit_at_5 is False and query.hit_at_10 is False for query in report.queries)
|
|
assert all(query.empty_result is False for query in report.queries)
|
|
assert report.queries[0].missing_expected == ("evidence:icd",)
|
|
assert report.counts_by_expected_kind == {"enum": 1, "formula": 2}
|
|
|
|
|
|
def test_evaluation_fixture_requires_all_retrieval_profiles(tmp_path):
|
|
from tht.evidence.evaluation import EvaluationFixtureError, load_evaluation_fixture
|
|
|
|
path = tmp_path / "evaluation.yaml"
|
|
path.write_text(
|
|
"""
|
|
schema_version: 1
|
|
queries:
|
|
- id: lexical-code
|
|
query: Qual è il codice ICD-10?
|
|
profile: lexical
|
|
purpose: schema_linking
|
|
expected: [evidence:icd]
|
|
- id: semantic-age
|
|
query: Come distinguo i pazienti pediatrici?
|
|
profile: semantic
|
|
purpose: sql_generation
|
|
expected: [evidence:fascia-pediatrica]
|
|
""".strip(),
|
|
encoding="utf-8",
|
|
)
|
|
|
|
with pytest.raises(EvaluationFixtureError, match="mixed"):
|
|
load_evaluation_fixture(path)
|
|
|
|
|
|
def test_evaluation_fixture_rejects_duplicate_ids_empty_expectations_and_private_purposes(tmp_path):
|
|
from tht.evidence.evaluation import EvaluationFixtureError, load_evaluation_fixture
|
|
|
|
path = tmp_path / "evaluation.yaml"
|
|
path.write_text(
|
|
"""
|
|
schema_version: 1
|
|
queries:
|
|
- id: duplicate
|
|
query: a
|
|
profile: lexical
|
|
purpose: private
|
|
expected: []
|
|
- id: duplicate
|
|
query: b
|
|
profile: semantic
|
|
purpose: sql_generation
|
|
expected: [evidence:b]
|
|
- id: mixed
|
|
query: c
|
|
profile: mixed
|
|
purpose: rewriting
|
|
expected: [evidence:c]
|
|
""".strip(),
|
|
encoding="utf-8",
|
|
)
|
|
|
|
with pytest.raises(EvaluationFixtureError) as failure:
|
|
load_evaluation_fixture(path)
|
|
|
|
assert {"duplicate", "expected", "purpose"} <= set(str(failure.value).split())
|