062e010d7b
- Query-Planer (agent/query_planner.py): Heuristik-Gate (Jahr, Vergleichs-/ Aggregationsmarker, Laenge) entscheidet ueber einen kleinen LLM-Call, der komplexe Fragen in 1-3 Sub-Queries zerlegt (JSON, temp 0; Fehler -> Original als Einzel-Query). Je Sub-Query optional stand_year und scope. - Multi-Query-Retrieval: je Sub-Query BM25+Dense mit RRF-Summe; Per-Query-Slots (2 je Sub-Query) sichern jeden Frageaspekt im Kontext (sonst dominieren Eintraege, die in mehreren Sub-Queries mittelgut matchen — q-113-Befund). Scope-Filter: 'gesetz' (nur Lexis/WIKU/RIS) und 'kv' (nur Branchen-KV) mit Fallback auf unscoped bei leerem Ergebnis. Temporal-Intent: stand_year + temporal_boost (default 0, FTS-jahr-Tag-Signal reichte). - Grounding unveraendert: eine Retrieved-Menge (Union), eine Antwort, Post-Validierung ueber die Union, Verweigerungspflicht unveraendert. - Goldset 42 -> 46: q-110 (Temporal 2023; 2024er-Lohnordnung existiert im Korpus nicht - Mantelvertrag ohne Lohntabelle, korrekt verweigert), q-111 (2025), q-112 (Abfertigung-Vergleich), q-113 (Gesetz+KV). - Eval (46 Fragen): Zitier-Praezision 97,8 %, Verweigerung 97,8 % (Gate >94,3 % erfuellt), erwartete Quelle 90,2 %, Latenz mean 33,5 s. - Tests 50 -> 57. Reports lokal: data/eval-qwen38-stage1*.json. API-first festgehalten (D13-Vorbereitung): Odoo bleibt duenner Client; Lohndaten-Zugriff in M4 erfordert Privacy-Neubewertung.
202 lines
4.7 KiB
Python
202 lines
4.7 KiB
Python
"""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,
|
||
planner_enabled=False, # Tests skripten Chat-Antworten; Planer
|
||
# separat testbar (siehe test_generate/test_query_planner)
|
||
)
|
||
|
||
|
||
@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 |