Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion bioscancast/stages/insight/text_extraction/chunk_extractor.py
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,20 @@ def extract_facts_from_chunk(
# and content-insertion hallucinations. See ``_quote_matches`` for
# the rationale and the layers.
canonical_quote = _quote_matches(raw_quote, chunk.text)
source_chunk_id = chunk.chunk_id
if canonical_quote is None:
# Minimal fallback for chunk-routing misses: if retrieval sends a
# chunk adjacent to the one the model quoted, accept only when the
# quote appears verbatim in some other chunk of the same document.
for alt_chunk in document.chunks:
if alt_chunk.chunk_id == chunk.chunk_id:
continue
alt_quote = _quote_matches(raw_quote, alt_chunk.text)
if alt_quote is not None:
canonical_quote = alt_quote
source_chunk_id = alt_chunk.chunk_id
break

if canonical_quote is None:
logger.warning(
"Hallucination guard: dropping fact with non-matching quote. "
Expand Down Expand Up @@ -456,7 +470,7 @@ def extract_facts_from_chunk(
sources=[
ChunkReference(
document_id=document.id,
chunk_id=chunk.chunk_id,
chunk_id=source_chunk_id,
source_url=document.source_url,
quote=canonical_quote[:200],
),
Expand Down
39 changes: 39 additions & 0 deletions bioscancast/tests/test_insight_chunk_extractor.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,45 @@ def test_hallucination_guard_passes_valid_quote():
assert len(records) == 2


# ---------------------------------------------------------------------------
# Cross-chunk quote routing (retrieval-miss fallback)
# ---------------------------------------------------------------------------


def test_cross_chunk_quote_routing_recovers_fact_and_corrects_chunk_id():
"""If retrieval routes a chunk adjacent to the one the model actually
quoted, the fact should still be accepted — the quote is verbatim in a
sibling chunk of the same document — and its provenance chunk_id should
point at the chunk that truly contains the quote, not the routed one."""
routed_chunk = DOC_WHO_SUDAN.chunks[0] # heading chunk — no case count
quoted_chunk = DOC_WHO_SUDAN.chunks[1] # prose chunk the quote lives in

# Precondition: the prose quote is absent from the routed heading chunk
# but present in its sibling prose chunk.
quote_text = "9 confirmed cases including 3 deaths have been reported"
assert _normalize_whitespace(quote_text) not in _normalize_whitespace(
routed_chunk.text
)
assert _normalize_whitespace(quote_text) in _normalize_whitespace(
quoted_chunk.text
)

client = FakeLLMClient([SUDAN_PROSE_RESPONSE])
records, _ = extract_facts_from_chunk(
routed_chunk, DOC_WHO_SUDAN, QUESTION_SUDAN, client, model="gpt-4o-mini"
)

# The fact survives the hallucination guard via the sibling-chunk fallback.
assert len(records) == 2
# Provenance is corrected to the chunk that actually contains the quote.
assert records[0].sources[0].chunk_id == quoted_chunk.chunk_id
assert records[0].sources[0].chunk_id != routed_chunk.chunk_id
# The recorded canonical quote is still a verbatim substring of that chunk.
assert _normalize_whitespace(
records[0].sources[0].quote
) in _normalize_whitespace(quoted_chunk.text)


# ---------------------------------------------------------------------------
# Country code resolution
# ---------------------------------------------------------------------------
Expand Down