feat(rag): hybrid FTS+vector retrieval and multi-format ingestion — name-your-tool questions find the right document

This commit is contained in:
2026-08-22 01:27:02 -04:00
parent 2f738a7f19
commit 7e8d14702e
36 changed files with 2018 additions and 290 deletions
+43 -4
View File
@@ -94,7 +94,7 @@ def seeded_kb(db) -> Iterator[FakeRagLLM]:
db.commit()
llm = FakeRagLLM()
summary = asyncio.run(import_sources([FIXTURES], llm, session=db))
assert summary.added == 3
assert summary.added == 8 # A9 formats; .hidden/ skipped
yield llm
db.execute(text("TRUNCATE chunks, documents, query_log"))
db.commit()
@@ -163,12 +163,19 @@ def test_chat_writes_query_log_row(client, db, seeded_kb: FakeRagLLM) -> None:
assert row.question == QUESTION
assert row.deflected is False
total_chunks = db.scalar(select(func.count()).select_from(Chunk))
assert row.chunk_hits == min(get_settings().top_k_chunks, total_chunks)
# chunk_hits is the fused candidate set (cosine top-N ∪ FTS top-N).
assert 1 <= row.chunk_hits <= total_chunks
assert row.top_score > 0.0 # genuine token-overlap cosine, best hit
assert row.top_score >= get_settings().relevance_threshold # why the gate answered
assert row.top_score <= 1.0
assert "docs/homelab/kubernetes.md" in row.sources
assert row.latency_ms >= 0
# Why the gate answered (A8 revised): cosine over the threshold OR a
# lexical hit. The mock-calibrated threshold (0.30, see tests/conftest.py)
# makes the cosine branch true here; the FTS branch is covered too —
# "kubernetes" / "cluster" match the doc's tsvector.
thr = get_settings().relevance_threshold
assert row.top_score >= thr or (row.fts_hits or 0) > 0
assert (row.fts_hits or 0) >= 1 # the lexical branch really fired
def test_off_topic_question_deflects_honestly(client, db, seeded_kb: FakeRagLLM) -> None:
@@ -202,14 +209,46 @@ def test_off_topic_question_deflects_honestly(client, db, seeded_kb: FakeRagLLM)
assert "Talos Linux" not in system["content"] # full doc content never sent
assert "<documents>" not in system["content"]
# Durable record: deflected=true + the weak top_score.
# Durable record: deflected=true + the weak top_score. Deflection is
# only reached when the cosine is under the threshold AND no chunk
# FTS-matches the question — so fts_hits must be zero here.
row = db.scalars(select(QueryLog)).one()
assert row.question == OFF_TOPIC
assert row.deflected is True
assert 0.0 < row.top_score < get_settings().relevance_threshold
assert row.fts_hits == 0
assert row.chunk_hits >= 1
def test_keyword_question_grounded_by_lexical_hit_despite_weak_cosine(
client, db, seeded_kb: FakeRagLLM
) -> None:
"""Phase 09: a name-your-tool question the vector model barely ranks
("kafkabridge" only appears in static-dns.json) must still be grounded
via the FTS branch — LOW only fires at weak cosine AND zero hits."""
fastapi_app.dependency_overrides[chat_api.get_llm] = lambda: seeded_kb
try:
_, _, frames = _stream_chat(client, "How does kafkabridge work?")
finally:
fastapi_app.dependency_overrides.clear()
done = frames[-1]
assert done["type"] == "done"
assert done["deflected"] is False # weak cosine, but a lexical hit
assert done["suggestions"] == []
sources = done["sources"]
assert sources and sources[0]["path"] == "homelab/networking/static-dns.json"
(system, _user) = seeded_kb.seen_messages[0][0], seeded_kb.seen_messages[0][1]
assert "<relevance>HIGH</relevance>" in system["content"] # grounded prompt
row = db.scalars(select(QueryLog)).one()
assert row.deflected is False
assert row.top_score < get_settings().relevance_threshold # weak vector score
assert (row.fts_hits or 0) >= 1 # …and it is the FTS hit that grounds it
assert "docs/homelab/networking/static-dns.json" in row.sources
def test_chat_empty_kb_streams_empty_sources(client, db) -> None:
db.execute(text("TRUNCATE chunks, documents, query_log"))
db.commit()