feat(evidence): contribute to semantic stages (#42)

This commit is contained in:
2026-08-24 22:05:30 +02:00
parent 3420c57c8b
commit f1a9b567ba
17 changed files with 619 additions and 23 deletions
+132 -1
View File
@@ -8,6 +8,7 @@ import pytest
from tht.decisions import DecisionRecord
from tht.evidence import (
EvidenceSearchContext,
acquire,
active_searcher,
build_preprocessing_pipeline,
@@ -16,6 +17,7 @@ from tht.evidence import (
discover,
project_session,
resolve_citation,
search_evidence,
)
from tht.evidence.contracts import (
AcquiredDocument,
@@ -25,6 +27,7 @@ from tht.evidence.contracts import (
)
from tht.evidence.corpus.models import CanonicalDocument, CorpusManifest
from tht.evidence.corpus.store import CorpusStore
from tht.ports.vector import VectorReadUnavailable
from tht.session.models import Candidate, SchemaLinking
@@ -186,13 +189,141 @@ def test_retrieval_entries_preserve_hit_order_and_existing_projection_shape():
SimpleNamespace(label="Second", status="reviewed", content="abcdefgh"),
SimpleNamespace(label="First", status=None, content="12345678"),
]
assert build_retrieval_entries(hits, excerpt_chars=5) == [
{"title": "Second", "status": "reviewed", "excerpt": "abcde"},
{"title": "First", "status": None, "excerpt": "12345"},
]
def test_typed_search_renders_one_stable_query_and_groups_fragments_by_evidence_unit():
"""Removing context rendering, hard filters, or grouping changes this public result."""
class Searcher:
vector_generation = "gen:" + "a" * 32
def __init__(self):
self.calls = []
def search(self, embedding, **kwargs):
self.calls.append((embedding, kwargs))
return [
SimpleNamespace(
id="fragment:second", similarity=0.7, content="second excerpt",
title="Pediatric range", metadata={
"evidence_id": "evidence:pediatric-range", "evidence_kind": "formula",
"document_id": "doc:range", "ordinal": 1,
"source_uri": "file:///curated/pediatric-range.md",
"provenance": {"source_file": "source/range.md"},
},
),
SimpleNamespace(
id="fragment:first", similarity=0.9, content="first excerpt",
title="Pediatric range", metadata={
"evidence_id": "evidence:pediatric-range", "evidence_kind": "formula",
"document_id": "doc:range", "ordinal": 0,
"source_uri": "file:///curated/pediatric-range.md",
"provenance": {"source_file": "source/range.md"},
},
),
]
class Embedder:
def __init__(self):
self.queries = []
def embed_query(self, query):
self.queries.append(query)
return [0.25]
searcher = Searcher()
embedder = Embedder()
outcome = search_evidence(
" Pazienti \"Età" + "\r\n" + " pediatrica ",
"schema_linking",
EvidenceSearchContext(
concepts=("pediatrica", "pediatrica", " Età "),
tables=("clinical.patient",),
columns=("clinical.patient.Age",),
required_kinds=("formula",),
required_concepts=("Età",),
required_tables=("clinical.patient",),
required_columns=("clinical.patient.Age",),
),
searcher=searcher,
embedder=embedder,
)
rendered = (
"Domanda: Pazienti \"Età\n pediatrica\n"
"Concetti: Età, pediatrica\n"
"Tabelle: clinical.patient\n"
"Colonne: clinical.patient.Age"
)
assert embedder.queries == [rendered]
assert searcher.calls == [([0.25], {
"top_n": 10,
"kinds": ["evidence"],
"query_text": rendered,
"metadata_filter": {
"purpose": "schema_linking",
"required_kinds": ["formula"],
"required_concepts": ["Età"],
"required_tables": ["clinical.patient"],
"required_columns": ["clinical.patient.Age"],
},
})]
assert outcome.status == "available"
assert outcome.vector_generation == "gen:" + "a" * 32
assert [(item.evidence_id, item.excerpts, item.provenance, item.citation) for item in outcome.results] == [
("evidence:pediatric-range", ("first excerpt", "second excerpt"),
{"source_file": "source/range.md"}, "file:///curated/pediatric-range.md"),
]
def test_typed_search_reports_vector_errors_as_unavailable_not_empty_results():
class UnavailableSearcher:
vector_generation = "gen:" + "a" * 32
def search(self, _embedding, **_kwargs):
raise VectorReadUnavailable("reader unavailable")
outcome = search_evidence(
"question", "rewriting", EvidenceSearchContext(),
searcher=UnavailableSearcher(), embedder=SimpleNamespace(embed_query=lambda _query: [0.25]),
)
assert outcome.status == "unavailable"
assert outcome.code == "vector_unavailable"
assert outcome.results == ()
def test_typed_search_without_an_active_generation_is_unavailable_not_an_empty_search():
outcome = search_evidence(
"question", "rewriting", EvidenceSearchContext(),
searcher=SimpleNamespace(), embedder=SimpleNamespace(embed_query=lambda _query: [0.25]),
)
assert outcome.status == "unavailable"
assert outcome.code == "active_corpus_unavailable"
def test_typed_search_reports_a_malformed_fragment_payload_as_unavailable():
class Searcher:
vector_generation = "gen:" + "a" * 32
def search(self, _embedding, **_kwargs):
return [SimpleNamespace(
id="fragment:bad", similarity=0.5, title="Bad", content="bad",
metadata={"evidence_id": "evidence:bad"},
)]
outcome = search_evidence(
"question", "rewriting", EvidenceSearchContext(),
searcher=Searcher(), embedder=SimpleNamespace(embed_query=lambda _query: [0.25]),
)
assert outcome.status == "unavailable"
assert outcome.code == "evidence_search_unavailable"
def _canonical_store(root, evidence_id):
content = f"# {evidence_id}\n"
digest = hashlib.sha256(content.encode()).hexdigest()