Files
pv-agent/tests/test_retrieve.py
fegger 2cba72aeb0 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.
2026-09-14 16:53:04 +02:00

82 lines
2.9 KiB
Python

"""Tests: Hybrid-Retrieval (BM25-only offline): Fusion, Entry-Dedup,
cross_ref-Erweiterung, leeres Retrieval."""
import pytest
from agent.retrieve import Retriever
@pytest.fixture
def retriever(mini_index):
r = Retriever(mini_index)
yield r
r.close()
def test_search_finds_expected_entry(retriever):
results = retriever.search("Was ist Altersteilzeit und Lohnausgleich?")
assert results, "Retrieval sollte Treffer liefern"
assert results[0].entry_id == "lb-min-01"
assert results[0].stand == "2026-01"
# Beim Section-Schnitt entscheidet BM25-Längennormalisierung; hier zählt
# der richtige Eintrag, nicht der konkrete Abschnitt.
def test_search_cross_ref_expansion(retriever):
"""Top-Treffer lb-min-02 → cross_ref lb-min-01 wird als Erweiterung ergänzt."""
results = retriever.search("Urlaubsanspruch fünf Werktage")
main = [r for r in results if r.source != "cross_ref"]
extra = [r for r in results if r.source == "cross_ref"]
assert main and main[0].entry_id == "lb-min-02"
assert any(r.entry_id == "lb-min-01" for r in extra)
def test_search_no_match_returns_empty(retriever):
results = retriever.search("kanadische quellensteuer bermuda")
assert results == []
def test_search_dedups_entries(mini_index):
"""Pro Eintrag höchstens ein Haupt-Chunk im Kontext (Entry-Level-Dedup)."""
r = Retriever(mini_index)
try:
results = r.search("Lohnausgleich Urlaubsentgelt Lohnverrechnung",
n_entries=2)
main_ids = [x.entry_id for x in results if x.source != "cross_ref"]
assert len(main_ids) == len(set(main_ids))
assert len(main_ids) <= 2
finally:
r.close()
def test_recency_boost_prefers_newer_stand(retriever):
"""Milde Aktualitätsgewichtung: bei Gleichstand gewinnt der neuere Stand.
'Urlaubsanspruch' (2026-07) sollte vor 'Altersteilzeit' (2026-01)
landen, wenn beide im Kontext sind und der Query beide trifft.
"""
results = retriever.search("Urlaubsanspruch Altersteilzeit")
main = [r for r in results if r.source != "cross_ref"]
if {r.entry_id for r in main} >= {"lb-min-01", "lb-min-02"}:
# Beide im Kontext -> Reihenfolge prüfen ist nur bei Score-Nähe sinnvoll;
# hier reicht die Existenz-Annahme, der Boost ist bewusst minimal.
assert main[0].entry_id in {"lb-min-01", "lb-min-02"}
def test_stats_report(mini_index):
r = Retriever(mini_index)
try:
s = r.stats()
assert s["n_entries"] == 3
assert s["dense_available"] is False # embed_off=True
assert s["stand_min"] == "202601"
assert s["stand_max"] == "202607"
finally:
r.close()
def test_retriever_requires_index(tmp_path):
from agent.config import Config
cfg = Config(kb_dir="wissensbasis", db_path=str(tmp_path / "missing.db"))
with pytest.raises(RuntimeError, match="ingest"):
Retriever(cfg)