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
+200
View File
@@ -0,0 +1,200 @@
"""Fixtures: Mini-Wissensbasis (3 Einträge), Index, Fake-Ollama-Client.
Alle Tests laufen offline — kein Ollama, kein Netz. Die Mini-KB folgt dem
verbindlichen Layer-2-Schema (Frontmatter + H2-Sektionen + kb.json-Gate).
"""
from __future__ import annotations
import json
from pathlib import Path
import pytest
from agent.config import Config
DOC_ATZ = """---
id: lb-min-01
batch: 1
title: "Altersteilzeit - Überblick"
work: "Lexis Briefings Personalrecht"
chapter: "Beschäftigungsverhältnisse"
topic: altersteilzeit
author: "Marek"
stand: 2026-01
source:
pdf: ".lexis360/Lexis360_test_atz.pdf"
text: ".lexis360/md/test_atz.md"
legal_bases: ["AlVG", "AZG § 19e"]
tags: [altersteilzeit, lohnausgleich]
cross_refs: ["lb-min-02"]
---
# Altersteilzeit Überblick
*Lexis Briefings Personalrecht, Marek, Stand Jänner 2026 (lb-min-01).*
## Zusammenfassung
Altersteilzeit ist eine Teilzeit, bei der der Arbeitnehmer zusätzlich zum
Teilzeitentgelt einen Lohnausgleich erhält; das AMS ersetzt dem Arbeitgeber
einen Teil der Zusatzkosten (Altersteilzeitgeld).
## Kernwerte & Fristen (Stand 2026-01)
| Wert / Regel | Detail |
|---|---|
| Ersatzquote | 28,5 % der ersetzbaren Kosten (Stand 2026-01) |
## Rechtsgrundlagen
- AZG § 19e (Kündigungszuschlag)
## Verweise
- lb-min-02
"""
DOC_URL = """---
id: lb-min-02
batch: 1
title: "Urlaubsanspruch und Verbrauch"
work: "Lexis Briefings Personalrecht"
chapter: "Urlaub & Karenzierung"
topic: urlaub
author: "Marek"
stand: 2026-07
source:
pdf: ".lexis360/Lexis360_test_urlaub.pdf"
text: ".lexis360/md/test_urlaub.md"
legal_bases: ["UrlG"]
tags: [urlaub, urlaubsentgelt]
cross_refs: ["lb-min-01"]
---
# Urlaubsanspruch und Verbrauch
*Lexis Briefings Personalrecht, Marek, Stand Juli 2026 (lb-min-02).*
## Zusammenfassung
Der gesetzliche Urlaubsanspruch beträgt fünf Werktage Wochenurlaub je
Dienstjahr; das Urlaubsentgelt ist wie Entgeltfortzahlung zu berechnen.
## Kernwerte & Fristen (Stand 2026-07)
| Wert / Regel | Detail |
|---|---|
| Mindesturlaub | 5 Wochen je Dienstjahr (Stand 2026-07) |
## Verweise
- lb-min-01
"""
DOC_WIKU = """---
id: wk-min-01
batch: 1
title: "WIKU Praxisfall: Lohnverrechnung"
work: "WIKU Fachbroschüre"
chapter: "WIKU Fachbroschüre"
topic: lohnverrechnung
author: "Wilhelm Kurzböck"
stand: 2026-03
source:
pdf: ".wiku/test_lohnverrechnung.pdf"
text: ".wiku/md/test_lohnverrechnung.md"
legal_bases: ["EStG § 25"]
tags: [lohnverrechnung, praxisfall]
cross_refs: ["lb-min-01"]
---
# WIKU Praxisfall: Lohnverrechnung
*WIKU Fachbroschüre, Wilhelm Kurzböck, Stand März 2026 (wk-min-01).*
## Zusammenfassung
Praxisfall zur laufenden Lohnverrechnung: Abrechnungsperiode und
Beitragsgrundlagen sind monatlich festzulegen.
## Verweise
- lb-min-01
"""
MIN_DOCS = {
"altersteilzeit_uberblick.md": DOC_ATZ,
"urlaubsanspruch.md": DOC_URL,
"wiku_lohnverrechnung.md": DOC_WIKU,
}
MIN_IDS = ["lb-min-01", "lb-min-02", "wk-min-01"]
def write_mini_kb(root: Path) -> Path:
doc = root / "dokumente"
doc.mkdir(parents=True, exist_ok=True)
for name, content in MIN_DOCS.items():
(doc / name).write_text(content, encoding="utf-8")
kb = {
"n_entries": len(MIN_IDS),
"entries": [{"id": i, "title": i} for i in MIN_IDS],
}
(root / "kb.json").write_text(
json.dumps(kb, ensure_ascii=False, indent=1), encoding="utf-8"
)
return root
class FakeOllama:
"""Skriptbarer Chat-Client; embed() schlägt fehl (Dense bleibt ungetestet)."""
def __init__(self, answers: list[str] | None = None):
self.answers = list(answers or [])
self.calls = 0
self.last_messages: list | None = None
def chat(self, model, messages, **kwargs):
self.calls += 1
self.last_messages = messages
if not self.answers:
raise AssertionError("FakeOllama: keine skriptierte Antwort mehr")
return self.answers.pop(0)
def embed(self, model, texts):
raise RuntimeError("embed nicht verfügbar (offline Test)")
def is_up(self):
return False
def close(self):
pass
@pytest.fixture
def mini_kb(tmp_path):
return write_mini_kb(tmp_path / "kb")
@pytest.fixture
def mini_cfg(tmp_path, mini_kb) -> Config:
return Config(
kb_dir=str(mini_kb),
db_path=str(tmp_path / "index.db"),
embed_off=True,
)
@pytest.fixture
def mini_index(mini_cfg) -> Config:
from agent.ingest import build_index
stats = build_index(mini_cfg)
assert stats.n_entries == 3
assert stats.n_chunks >= 7
assert stats.embed_error is None
return mini_cfg
@pytest.fixture
def fake_ollama():
return FakeOllama