david / evidence /retrieve.py
SNAPKITTYWEST's picture
October 2026 main drop: mirror from GitHub
f109bf8 verified
Raw History Blame Contribute Delete
11.8 kB
"""Hybrid evidence retrieval — dense + lexical + metadata + provenance.
In-memory stand-in for sql/001_agent_and_evidence_indexes.sql.
Production SQL uses vector(1536); tests use fixed dim-8 vectors (no live DB).
Merge / rerank rule (deterministic):
score = dense_cosine + lexical_overlap + source_authority - penalties
sort by (-score, evidence_id)
penalties: missing required parent_evidence when policy requires it
Hybrid note: a chunk with weak dense but strong lexical can still rank
if lexical_overlap compensates (no hard dense floor).
"""
from __future__ import annotations
import hashlib
import math
import re
from dataclasses import dataclass, field
from typing import Any, Iterable, Sequence
# Production pgvector column is vector(1536); test corpus uses tiny fixed vectors.
EMBEDDING_DIM_TEST = 8
EMBEDDING_DIM_PRODUCTION = 1536
@dataclass
class EvidenceRecord:
"""Fields mirror the SQL ``evidence`` table."""
evidence_id: str
source_id: str
source_type: str
document_id: str | None
location: str | None
content: str
content_hash: str
embedding: list[float] | None
source_authority: float = 0.5
parent_evidence: tuple[str, ...] = ()
created_at: str | None = None
def _content_hash(text: str) -> str:
return hashlib.sha256(text.encode("utf-8")).hexdigest()[:16]
def _tokenize(text: str) -> set[str]:
return {t for t in re.findall(r"[a-z0-9]+", text.lower()) if len(t) > 1}
def cosine_similarity(a: Sequence[float], b: Sequence[float]) -> float:
if not a or not b or len(a) != len(b):
return 0.0
dot = sum(x * y for x, y in zip(a, b))
na = math.sqrt(sum(x * x for x in a))
nb = math.sqrt(sum(y * y for y in b))
if na == 0.0 or nb == 0.0:
return 0.0
return dot / (na * nb)
def lexical_score(query: str, content: str) -> float:
"""Token overlap stand-in for ``content_tsv @@ plainto_tsquery``."""
q = _tokenize(query)
if not q:
return 0.0
c = _tokenize(content)
if not c:
return 0.0
return len(q & c) / len(q)
def empty_bundle() -> dict[str, Any]:
"""EvidenceBundle-shaped dict with no hits (contracts.md)."""
return {
"facts": [],
"sources": [],
"provenance": [],
"retrieval_metadata": [],
}
def seed_cobol_corpus() -> list[EvidenceRecord]:
"""Fixed COBOL-modernization corpus (dim-8 embeddings for tests).
Axes (approx): [cobol, balance, payment, schema, pricing, vol, greek, junk]
"""
# Strong COBOL / balance / payment cluster
copybook = EvidenceRecord(
evidence_id="ev_copybook_01",
source_id="src_copybook_acct",
source_type="copybook",
document_id="ACCTREC.cpy",
location="01-ACCOUNT-BALANCE",
content=(
"01 ACCOUNT-RECORD. "
"05 ACCOUNT-BALANCE PIC S9(9)V99. "
"05 PAYMENT-AMOUNT PIC S9(7)V99. "
"05 LAST-PAYMENT-DATE PIC X(8)."
),
content_hash="",
embedding=[0.95, 0.90, 0.85, 0.20, 0.05, 0.0, 0.0, 0.0],
source_authority=0.95,
parent_evidence=("ev_provenance_root",),
created_at="2026-01-01T00:00:00Z",
)
cobol_src = EvidenceRecord(
evidence_id="ev_cobol_src_01",
source_id="src_cobol_pay",
source_type="cobol_source",
document_id="PAYROLL.cbl",
location="PROCEDURE DIVISION / CALC-BALANCE",
content=(
"COMPUTE ACCOUNT-BALANCE = ACCOUNT-BALANCE - PAYMENT-AMOUNT. "
"IF ACCOUNT-BALANCE < ZERO THEN PERFORM OVERDRAFT-CHECK. "
"MOVE PAYMENT-AMOUNT TO WS-LAST-PAYMENT."
),
content_hash="",
embedding=[0.92, 0.88, 0.90, 0.15, 0.05, 0.0, 0.0, 0.0],
source_authority=0.90,
parent_evidence=("ev_provenance_root",),
created_at="2026-01-01T00:00:00Z",
)
db2 = EvidenceRecord(
evidence_id="ev_db2_schema_01",
source_id="src_db2_acct",
source_type="db2_schema",
document_id="ACCT.DDL",
location="TABLE ACCOUNT_BALANCES",
content=(
"CREATE TABLE ACCOUNT_BALANCES ("
"ACCT_ID CHAR(12), BALANCE DECIMAL(11,2), "
"LAST_PAYMENT DECIMAL(9,2), UPDATED_TS TIMESTAMP)."
),
content_hash="",
embedding=[0.70, 0.80, 0.55, 0.95, 0.05, 0.0, 0.0, 0.0],
source_authority=0.85,
parent_evidence=("ev_provenance_root",),
created_at="2026-01-01T00:00:00Z",
)
# Should NOT win on a COBOL balance query (pricing / quant cluster)
quant = EvidenceRecord(
evidence_id="ev_quant_pricing_01",
source_id="src_quant_report",
source_type="quant_report",
document_id="PRICING_Q3.pdf",
location="section:monte_carlo",
content=(
"Monte Carlo pricing of exotic options. "
"Implied volatility surface and greek sensitivities. "
"No COBOL or account balance fields."
),
content_hash="",
embedding=[0.05, 0.05, 0.05, 0.05, 0.95, 0.90, 0.85, 0.10],
source_authority=0.80,
parent_evidence=("ev_provenance_root",),
created_at="2026-01-01T00:00:00Z",
)
# Low-authority junk for provenance filter tests
junk = EvidenceRecord(
evidence_id="ev_junk_lowauth_01",
source_id="src_web_scrape",
source_type="web_scrape",
document_id="random-blog.html",
location="body",
content=(
"Someone said COBOL payment balance might work like a spreadsheet. "
"Unverified forum post."
),
content_hash="",
embedding=[0.60, 0.55, 0.50, 0.10, 0.10, 0.0, 0.0, 0.9],
source_authority=0.15,
parent_evidence=(),
created_at="2026-01-01T00:00:00Z",
)
# Weak dense (far from COBOL cluster) but strong lexical on balance/payment/cobol
# — hybrid: lexical can lift it into results when dense alone would not.
lexical_lift = EvidenceRecord(
evidence_id="ev_lexical_lift_01",
source_id="src_glossary",
source_type="glossary",
document_id="LEGACY-GLOSSARY.txt",
location="entry:PAYMENT-BALANCE",
content=(
"cobol payment balance glossary entry: "
"PAYMENT and BALANCE fields on ACCOUNT records in legacy batch."
),
content_hash="",
# Oriented away from COBOL cluster → weak cosine vs COBOL query
embedding=[0.10, 0.15, 0.10, 0.05, 0.40, 0.35, 0.30, 0.20],
source_authority=0.70,
parent_evidence=("ev_provenance_root",),
created_at="2026-01-01T00:00:00Z",
)
records = [copybook, cobol_src, db2, quant, junk, lexical_lift]
for r in records:
r.content_hash = _content_hash(r.content)
return records
# Canonical COBOL payment/balance query embedding (dim 8)
COBOL_BALANCE_QUERY = "cobol account payment balance CALC-BALANCE"
COBOL_BALANCE_QUERY_VEC = [0.93, 0.91, 0.88, 0.25, 0.05, 0.0, 0.0, 0.0]
@dataclass
class EvidenceIndex:
"""In-memory evidence index (no Postgres required)."""
records: list[EvidenceRecord] = field(default_factory=list)
def __post_init__(self) -> None:
if not self.records:
self.records = seed_cobol_corpus()
self._by_id = {r.evidence_id: r for r in self.records}
def add(self, record: EvidenceRecord) -> None:
if not record.content_hash:
record.content_hash = _content_hash(record.content)
self.records.append(record)
self._by_id[record.evidence_id] = record
def retrieve(
query: str,
*,
query_embedding: Sequence[float] | None = None,
index: EvidenceIndex | None = None,
source_types: Iterable[str] | None = None,
document_ids: Iterable[str] | None = None,
min_authority: float = 0.0,
require_parent_evidence: bool = False,
top_k: int = 10,
) -> dict[str, Any]:
"""Hybrid retrieve → EvidenceBundle-shaped dict (facts/sources/provenance/…).
Agents never receive bare text without provenance fields on each source.
"""
if not query or not query.strip():
return empty_bundle()
idx = index or EvidenceIndex()
st_filter = set(source_types) if source_types is not None else None
doc_filter = set(document_ids) if document_ids is not None else None
q_emb = list(query_embedding) if query_embedding is not None else None
scored: list[tuple[float, EvidenceRecord, dict[str, float]]] = []
for rec in idx.records:
# Metadata filters
if st_filter is not None and rec.source_type not in st_filter:
continue
if doc_filter is not None and rec.document_id not in doc_filter:
continue
# Provenance / authority
if rec.source_authority < min_authority:
continue
if require_parent_evidence and not rec.parent_evidence:
continue
dense = 0.0
if q_emb is not None and rec.embedding is not None:
dense = cosine_similarity(q_emb, rec.embedding)
lex = lexical_score(query, rec.content)
authority = float(rec.source_authority)
penalty = 0.0
if require_parent_evidence and not rec.parent_evidence:
penalty += 1.0 # defensive; already filtered above
score = dense + lex + authority - penalty
parts = {
"dense": dense,
"lexical": lex,
"source_authority": authority,
"penalty": penalty,
"score": score,
}
scored.append((score, rec, parts))
# Deterministic rerank: score desc, then evidence_id asc
scored.sort(key=lambda t: (-t[0], t[1].evidence_id))
top = scored[:top_k]
if not top:
return empty_bundle()
facts: list[dict[str, Any]] = []
sources: list[dict[str, Any]] = []
provenance: list[dict[str, Any]] = []
retrieval_metadata: list[dict[str, Any]] = []
for score, rec, parts in top:
facts.append(
{
"evidence_id": rec.evidence_id,
"summary": rec.content[:160],
"source_type": rec.source_type,
}
)
sources.append(
{
"evidence_id": rec.evidence_id,
"source_id": rec.source_id,
"source_type": rec.source_type,
"document_id": rec.document_id,
"location": rec.location,
"content_hash": rec.content_hash,
"source_authority": rec.source_authority,
"parent_evidence": list(rec.parent_evidence),
"content": rec.content,
"retrieval_score": score,
}
)
provenance.append(
{
"evidence_id": rec.evidence_id,
"source_id": rec.source_id,
"content_hash": rec.content_hash,
"parent_evidence": list(rec.parent_evidence),
"source_authority": rec.source_authority,
}
)
retrieval_metadata.append(
{
"evidence_id": rec.evidence_id,
"dense": parts["dense"],
"lexical": parts["lexical"],
"source_authority": parts["source_authority"],
"penalty": parts["penalty"],
"score": parts["score"],
"embedding_dim_note": (
f"test vectors dim={EMBEDDING_DIM_TEST}; "
f"production SQL uses vector({EMBEDDING_DIM_PRODUCTION})"
),
}
)
return {
"facts": facts,
"sources": sources,
"provenance": provenance,
"retrieval_metadata": retrieval_metadata,
}