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:
@@ -0,0 +1,74 @@
|
||||
"""Tests: Layer-2-Parsing, Sektionen, Cross-Ref-Integrität, kb.json-Gate."""
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from agent.kb import KbValidationError, load_entry, load_kb, parse_frontmatter, split_sections
|
||||
from tests.conftest import DOC_ATZ, MIN_IDS, write_mini_kb
|
||||
|
||||
|
||||
def test_parse_frontmatter_and_sections():
|
||||
meta, body = parse_frontmatter(DOC_ATZ)
|
||||
assert meta["id"] == "lb-min-01"
|
||||
assert meta["stand"] == "2026-01"
|
||||
sections = split_sections(body)
|
||||
titles = [s.title for s in sections]
|
||||
assert "Zusammenfassung" in titles
|
||||
assert titles[1].startswith("Kernwerte & Fristen")
|
||||
assert all(s.text for s in sections)
|
||||
|
||||
|
||||
def test_split_sections_ignores_h1_and_intro():
|
||||
sections = split_sections("# Titel\n\n*Quellzeile*\n\n## A\n\nText A\n\n## B\n\nText B")
|
||||
assert [s.title for s in sections] == ["A", "B"]
|
||||
assert sections[0].text == "Text A"
|
||||
|
||||
|
||||
def test_load_entry_minimal_doc(tmp_path):
|
||||
p = tmp_path / "doc.md"
|
||||
p.write_text(DOC_ATZ, encoding="utf-8")
|
||||
e = load_entry(p)
|
||||
assert e.id == "lb-min-01"
|
||||
assert e.tags == ["altersteilzeit", "lohnausgleich"]
|
||||
assert e.cross_refs == ["lb-min-02"]
|
||||
assert len(e.sections) >= 3
|
||||
|
||||
|
||||
def test_gate_rejects_out_of_sync_registry(tmp_path):
|
||||
root = write_mini_kb(tmp_path / "kb")
|
||||
kb = json.loads((root / "kb.json").read_text(encoding="utf-8"))
|
||||
kb["entries"].append({"id": "lb-min-99"})
|
||||
(root / "kb.json").write_text(json.dumps(kb), encoding="utf-8")
|
||||
with pytest.raises(KbValidationError, match="out of sync"):
|
||||
load_kb(root)
|
||||
|
||||
|
||||
def test_gate_rejects_dangling_cross_refs(tmp_path):
|
||||
root = write_mini_kb(tmp_path / "kb")
|
||||
doc = root / "dokumente" / "altersteilzeit_uberblick.md"
|
||||
doc.write_text(
|
||||
DOC_ATZ.replace('cross_refs: ["lb-min-02"]', 'cross_refs: ["lb-min-42"]'),
|
||||
encoding="utf-8",
|
||||
)
|
||||
with pytest.raises(KbValidationError, match="dangling cross_refs"):
|
||||
load_kb(root)
|
||||
|
||||
|
||||
def test_gate_rejects_invalid_stand(tmp_path):
|
||||
root = write_mini_kb(tmp_path / "kb")
|
||||
doc = root / "dokumente" / "altersteilzeit_uberblick.md"
|
||||
doc.write_text(DOC_ATZ.replace("stand: 2026-01", "stand: Jänner 2026"), encoding="utf-8")
|
||||
with pytest.raises(KbValidationError, match="not YYYY-MM"):
|
||||
load_kb(root)
|
||||
|
||||
|
||||
def test_real_corpus_loads_and_matches_registry():
|
||||
"""Integrationstest gegen die echte Wissensbasis (Gate inklusive)."""
|
||||
entries = load_kb("wissensbasis", verify_registry=True)
|
||||
ids = {e.id for e in entries}
|
||||
assert len(entries) == 601
|
||||
assert "lb-atz-07" in ids and "wk-akt-01" in ids
|
||||
atz = [e for e in entries if e.id == "lb-atz-07"][0]
|
||||
assert atz.topic == "altersteilzeit"
|
||||
assert any(s.title.startswith("Kernwerte") for s in atz.sections)
|
||||
Reference in New Issue
Block a user