M1+M2: RAG-Pipeline mit verbindlichem Grounding

agent/-Paket: Ingest (601 Layer-2-Eintraege -> 3005 Chunks, FTS5-BM25 +
Vektoren-Cache), Hybrid-Retrieval (RRF, Stand-Boost, cross_ref-Erweiterung),
Ollama-Client (embed/chat, think-Flag-Fallback, kurzes Connect-Budget),
Systemprompt mit Zitierpflicht, Post-Validierung (zitierte IDs gemaess
Retrieved-Set, 1x Regenerierung, dann Verweigerung), FastAPI (/ask, /health,
/reindex), CLI, Goldset (31 Fragen, IDs gegen kb.json verifiziert, inkl.
ATZ-Konfliktfall + 4 Verweigerungsfaelle), Eval-Suite, Test-Chat.

41 Offline-Tests gruen. Baseline BM25-only: Hit-Rate 0,871 / Recall@8 0,855 /
MRR 0,476. Hybrid-Messung, Antwortmodus-Eval und Modell-Bake-off (M3) auf
dem Host ausstaendig (Ollama aus der Zed-Sandbox nicht erreichbar).

MEMORY.md und planung.md Umsetzungsstand aktualisiert.
This commit is contained in:
2026-09-14 16:53:04 +02:00
parent bf8191b013
commit 2cba72aeb0
26 changed files with 2574 additions and 1 deletions
+99
View File
@@ -0,0 +1,99 @@
"""Tests: Index-Bau (Chunking, FTS, Metadaten, Schema-Gates)."""
import sqlite3
import pytest
from agent.ingest import SCHEMA, build_index
from agent.kb import KbValidationError
def test_build_index_chunks_and_fts(mini_index):
con = sqlite3.connect(mini_index.db_path)
try:
n_chunks = con.execute("SELECT COUNT(*) FROM chunks").fetchone()[0]
n_fts = con.execute("SELECT COUNT(*) FROM chunks_fts").fetchone()[0]
n_entries = con.execute(
"SELECT COUNT(DISTINCT entry_id) FROM chunks"
).fetchone()[0]
assert n_entries == 3
assert n_chunks == n_fts and n_chunks >= 7
row = con.execute(
"SELECT entry_id, section, norm FROM chunks WHERE entry_id='lb-min-01' "
"AND section LIKE 'Kernwerte%'"
).fetchone()
assert row is not None
# Umlaut-Folding im FTS-Text: "Lohnausgleich" normalisiert auffindbar
assert "lohnausgleich" in row[2]
meta = dict(con.execute("SELECT key, value FROM meta").fetchall())
assert meta["n_entries"] == "3"
assert meta["embed_model"] == "" # embed_off=True
finally:
con.close()
def test_norm_contains_tags_and_legal_bases(mini_index):
con = sqlite3.connect(mini_index.db_path)
try:
norm = con.execute(
"SELECT norm FROM chunks WHERE entry_id='lb-min-01' "
"AND section='Zusammenfassung'"
).fetchone()[0]
assert "alvg" in norm # legal_bases im FTS-Text
assert "azg" in norm and "19e" in norm
finally:
con.close()
def test_rebuild_is_idempotent(mini_index):
stats = build_index(mini_index)
assert stats.n_entries == 3
con = sqlite3.connect(mini_index.db_path)
try:
assert con.execute("SELECT COUNT(*) FROM chunks").fetchone()[0] == stats.n_chunks
finally:
con.close()
def test_build_index_aborts_on_gate_error(mini_cfg):
"""Gate-Fehler (Registry kaputt) bricht den Ingest ab — kein halber Index."""
from pathlib import Path
(Path(mini_cfg.kb_dir) / "kb.json").write_text(
'{"n_entries": 0, "entries": []}', encoding="utf-8"
)
with pytest.raises(KbValidationError):
build_index(mini_cfg)
def test_vectors_table_cached_across_rebuilds(mini_index):
"""Die Vektoren-Tabelle bleibt beim Rebuild erhalten (Cache-Garantie)."""
con = sqlite3.connect(mini_index.db_path)
try:
con.execute(
"INSERT INTO vectors(content_hash, model, dim, vec) "
"VALUES ('deadbeef', 'bge-m3', 2, x'000000003f800000')"
) # 0.0, 1.0
con.commit()
finally:
con.close()
build_index(mini_index)
con = sqlite3.connect(mini_index.db_path)
try:
assert con.execute(
"SELECT COUNT(*) FROM vectors WHERE content_hash='deadbeef'"
).fetchone()[0] == 1
finally:
con.close()
def test_schema_creates_fts5(tmp_path):
con = sqlite3.connect(tmp_path / "s.db")
try:
con.executescript(SCHEMA)
con.execute("INSERT INTO chunks_fts(rowid, norm) VALUES (1, 'testtext')")
hits = con.execute(
"SELECT rowid FROM chunks_fts WHERE chunks_fts MATCH '\"testtext\"'"
).fetchall()
assert hits == [(1,)]
finally:
con.close()