Files
pv-agent/planung.md
T

490 lines
29 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## User
bitte plane das folgende:
Eine Wissensdatenbank und ein Agent der Fragen zu Oesterreichischer Personalverrechnung mit dem Wissen aus der Wissensdatenbank beantwortet.
Entwicklungsumgebung-GPU: AMD Radeon Ai Pro r9700 - 32GB vram
Ollama-Server: http://100.183.83.12:11435
Agent: lokales Modell (ollama): empfehle ein Modell. der Agent darf nur wissen aus der wissensbasis verwenden, kein trainingswissen, kein web search
Wissensdatenbank: bitte um einen Vorschlag fuer die Umsetzung. bereits vorhanden: [@wissensbasis](file:///home/fegger/Projects/odoo-projects/pv-agent/wissensbasis/)
in weiterer Folge soll der Agent in Odoo-Enterprise verwendet werden.
---
# Plan: Wissensbasis-Agent Österreichische Personalverrechnung
*Stand: 2026-09-14 — Planung, noch keine Implementierung.*
## 1. Ist-Zustand (verifiziert)
- **Wissensbasis** (`wissensbasis/`): **601 kuratierte Layer-2-Einträge**
(571 Lexis Briefings Personalrecht, 30 WIKU), verteilt über 69 Cluster
(`INDEX.md`). Median ~7,3 KB, max ~31 KB, gesamt ~4,8 MB — ideale
Größe für vollständige Indizierung.
- **Struktur retrieval-gerecht:** verbindliches Frontmatter-Schema
(`id`, `stand`, `topic`, `tags`, `legal_bases`, `cross_refs`, …),
`kb.json` als generierte/validierte Registry, fixer Dokumentaufbau
(`## Zusammenfassung``## Kernwerte & Fristen``## Rechtsgrundlagen`
`## Payroll-Relevanz (Odoo)``## Verweise`).
- **Layer-1-Volltexte:** `.lexis360/md/` (572 Dateien) lokal vorhanden,
lizenzbeschränkt + unversioniert. `.wiku/` fehlt in diesem Checkout —
für Phase A irrelevant (Retrieval läuft auf Layer 2).
- **Ollama-Server** `http://100.103.83.12:11435` (Ziel-Instanz mit Modell-Zoo,
Custom-Port; der Host betreibt zusätzlich eine fast leere Instanz auf 11434):
aus der Zed-Sandbox erreichbar (2026-09-14, nach URL-Korrektur) —
`bge-m3` wurde per API gepullt, `qwen3.8:27b` war bereits installiert.
- **GPU:** Radeon AI Pro R9700, 32 GB (Strix Halo / RDNA 5) — budgetiert
Modellwahl auf ~2628 GB nutzbar.
## 2. Zielbild
Ein RAG-Agent, der Fragen zur österreichischen Personalverrechnung
**ausschließlich** aus den Layer-2-Einträgen beantwortet:
- jede fachliche Aussage mit KB-ID und `stand` belegt,
- **kein** Trainingswissen, **kein** Web, **keine** Tools,
- ehrliche Verweigerung, wenn die Wissensbasis nichts hergibt,
- Korpuskonflikte (z. B. ATZ-Ersatzquote 28,5 % vs. 27,5 %, lb-atz-07/09/12)
werden **beidseitig mit ⚠** referenziert — nie still aufgelöst,
- später Chat-Oberfläche in Odoo Enterprise.
## 3. Architektur (Phase A — eigener schlanker RAG-Service)
```mermaid
flowchart TD
KB[wissensbasis/dokumente/*.md + kb.json] -->|Ingest: Frontmatter + H2-Sektionen| IDX[(data/index.db - SQLite: FTS5 BM25 + Vektoren + Metadaten)]
IDX -->|Hybrid-Retrieval: BM25 + Dense, Metadaten-Filter, cross_refs| CTX[Kontextblöcke 8-12]
CTX -->|Systemprompt: nur Kontext, Zitierpflicht| LLM[Ollama qwen3:32b]
LLM -->|Antwort-Entwurf| CHECK{Post-Validierung: zitierte IDs ⊆ retrieved IDs?}
CHECK -->|ja| A[Antwort + Quellenblock]
CHECK -->|nein, 1x| LLM
CHECK -->|nein, 2x| R[Antwort als unsicher markiert / verweigert]
A --> API[FastAPI: POST /ask, GET /health, POST /reindex]
R --> API
API --> CLI[CLI + Mini-Web-UI zum Testen]
```
**Warum kein LangChain/LlamaIndex:** 601 Dokumente, sauberes Schema —
die Pipeline ist als eigenständiger Python-Code (~400600 Zeilen)
überschaubar und gibt volle Kontrolle über die Grounding- und
Zitier-Regeln, die hier der kritische Teil sind. Frameworks würden
Abhängigkeiten einführen, ohne das Kernproblem (Grounding) abzunehmen.
**Warum Layer 2 als Retrieval-Korpus:** kuratiert, eigene Worte
(lizenzkonform), versioniert, Metadaten-annotiert. Layer-1-Volltexte
bleiben außen vor (offener Lizenz-/Provisioning-Punkt laut README);
eine spätere Erweiterung für Tiefenzitate ist möglich, ohne die
Architektur zu ändern.
## 4. Modell-Empfehlung (Ollama)
| Modell | Rolle | Größe (Q4) | Begründung |
|---|---|---|---|
| **`qwen3.8:27b`** | **primärer Bake-off-Kandidat** | ~18 GB · 256k ctx | neueste Qwen-Generation (Ollama-Library, verifiziert 2026-09); Thinking per Request abschaltbar; Vision vorhanden, hier ungenutzt |
| `qwen3:32b` | bekannte Größe / Fallback | ~20 GB | bestes Deutsch + Instruction-Following im ≤32B-Bereich; Zitierdisziplin entscheidender als Weltwissen (das wir unterdrücken) |
| `gemma3:27b` | Alternative (Bake-off) | ~17 GB | sehr gutes Deutsch, 128k Kontext |
| `mistral-small3.2:24b` | Bake-off (Deutsch-Kandidat) | ~15 GB · 128k ctx | europäischer Anbieter, Europasprachen-Fokus → starke Deutsch-Hypothese (im Bake-off verifizieren); starkes Instruction-Following + wenige Wiederholungsfehler; kein Thinking-Modus |
| `qwen3:30b-a3b` | Latenz-Alternative | ~18 GB | MoE (3,3B aktiv) → deutlich schneller, etwas schwächer |
| `qwen3:14b` | Dev/Bake-off | ~9 GB | falls Zitierqualität reicht → halbe Latenz |
| **`bge-m3`** | **Embeddings** | ~1,2 GB | multilingual (Deutsch stark), 8k Kontext, über Ollama `/api/embed` |
| `bge-reranker-v2-m3` | optionales Reranking | ~1,1 GB | Rerank-API der installierten Ollama-Version vorher verifizieren; Fallback: Hybrid-Score ohne Reranker |
**VRAM-Budget:** Antwortmodell (1520 GB) + bge-m3 (~1,5 GB) + KV-Cache für
RAG-Prompts (48k Tokens Kontext, ~16k Kontextfenster ≈ 34 GB) ⇒
~2026 GB von 32 GB — passt mit Reserve. Thinking-Modus abschalten
(Latenz; bei `qwen3.8:27b` ist Thinking Default **an** → per Request
deaktivieren, API-Option lt. Library: `reasoning_effort`,
`preserve_thinking` — beim Implementieren verifizieren).
**Empfehlung:** Bake-off-Feld (M3): `qwen3.8:27b` und `qwen3:32b`
(Front-Runner) sowie `gemma3:27b` und `mistral-small3.2:24b` als
Deutsch-Kandidaten — Deutsch-/Zitierqualität ist jeweils unverifiziert.
Verbindliche Entscheidung erst nach Bake-off auf dem Goldset — Kriterium
bleibt Zitier-Präzision vor Latenz; liegt `qwen3:14b` bei gleicher
Zitierqualität auf, gewinnt Latenz.
> **Bake-off-Ergebnis (2026-09-14, Abschnitt 13):** `qwen3.8:27b` hat
> gewonnen und ist als Antwortmodell fixiert.
## 5. Grounding-Konzept (der kritische Teil)
1. **Systemprompt (deutsch):** antworte ausschließlich aus den
nummerierten Kontextblöcken; jede fachliche Aussage mit
`[kb-id]`-Beleg; Werte **immer mit** `(Stand YYYY-MM)`; fehlt etwas →
„Dazu enthält die Wissensbasis keine Aussage“ + ggf. verwandte
Cluster nennen; §-Zitate nur wenn die Quelle sie nennt; Korpus-
konflikte beidseitig mit ⚠ darstellen; keine Ergänzungen aus
Trainingswissen.
2. **Kontextblöcke** mit Metadatenkopf (ID · Titel · Stand · topic ·
Quellenwerk) — das Modell sieht nur, was im Retrieval war.
3. **Post-Validierung:** jede zitierte ID muss in der Retrieved-Menge
stehen; Verstoß → eine Regenerierung mit härterem Hinweis, dann
Verweigern/„unsicher“. Temperatur ~0,1.
4. **Kein Ausweg nach außen:** keine Tools, kein Browsing, kein
Web-Search-Hook — architektonisch gibt es nur Wissensbasis → Prompt.
## 6. Wissensdatenbank-Umsetzung (Vorschlag)
Die bestehende Wissensbasis ist bereits retrieval-gerecht — **kein
Umbau nötig**, nur ein Ingest-Index:
- **Chunking:** H2-Sektionen je Eintrag als Retrieval-Einheit (Parent-
Child: Treffer auf Sektion, Kontext = ganze Sektion + Metadatenkopf);
`Kernwerte & Fristen`-Tabellen als eigene Chunks (Zahlenfragen!);
Frontmatter im Index (topic, tags, legal_bases, stand).
- **Hybrid-Retrieval:** SQLite FTS5 (BM25; deutsche Normalisierung:
Umlaut-Folding beim Indexing) + Dense-Embeddings (bge-m3) + optional
Reranker; Reciprocal-Rank-Fusion; `cross_refs` der Top-Treffer als
kontrollierte Kontext-Erweiterung.
- **Metadaten-Filter:** `topic`-Vorfokus aus der Frage, Aktualitäts-
gewichtung über `stand`.
- **Quelle der Ingestion:** Layer-2-Frontmatter direkt (Single Source of
Truth); `kb.json` zusätzlich als Konsistenz-Gate (Anzahl/IDs müssen
matchen).
- **Update-Zyklus:** nach jedem neuen Batch einmal `POST /reindex`
(vollständiger Rebuild dauert bei 601 Einträgen Sekunden; Embeddings
gecacht, nur neue Einträge einbetten).
- **Speicherung:** eine SQLite-Datei `data/index.db` (gitignored) —
keine externe Vektor-DB nötig; Skalierungsreserve bis ~10.000
Einträge ohne Architekturwechsel.
## 7. Neue Dateien (Phase A)
```
agent/
config.py # Ollama-URL, Modellnamen, Ports (ENV-override)
ingest.py # Layer-2 → index.db (FTS5 + Vektoren via /api/embed)
retrieve.py # Hybrid-Suche + Filter + cross_refs (+ optional Rerank)
generate.py # Prompt-Bau, Ollama-Chat, Post-Validierung, Antwortformat
api.py # FastAPI: /ask, /health, /reindex
cli.py # Frage im Terminal (Dev-Loop)
eval/goldset.yaml # 3050 Fragen → Soll-IDs (inkl. Konfliktfälle)
eval/evaluate.py # Recall@k, Zitier-Präzision, Verweigerungsraten, Latenz
web/index.html # minimalistischer Test-Chat
data/ # index.db (gitignored)
tests/ # pytest: Ingest-, Retrieval-, Grounding-Unit-Tests
```
## 8. Validierung
- **Goldset:** 3050 Fragen mit Soll-IDs je Cluster, inkl. 35
Outside-KB-Fragen (Verweigerung!) und Konfliktfragen (ATZ-Quoten).
- **Metriken:** Retrieval-Recall@8 (Ziel >0,9), Zitier-Präzision
(100 % zitierte IDs ∈ retrieved), Verweigerungskorrektheit,
End-zu-End-Latenz.
- **Modell-Bake-off (M3):** qwen3.8:27b vs. qwen3:32b vs. gemma3:27b vs.
mistral-small3.2:24b (plus qwen3:14b als Latenz-Untergrenze) auf dem
Goldset; Entscheidung dokumentieren (analog D1/D2-Stil des Projekts).
- **Unit-Tests:** Ingest-Schema, Umlaut-Normalisierung, Post-Validierung
(Halluzinations-ID → Regenerierung), Konflikt-Darstellung.
## 9. Phase B: Odoo-Enterprise-Integration (später)
- **Option A (empfohlen):** dünnes Custom-Modul mit OWL-Chat-Panel,
`ir.config_parameter` für die Service-URL, rollenbasierter Zugriff.
Der RAG-Service bleibt Single Source of Truth für Grounding und
Zitate; Ollama bleibt extern. Unabhängig von Odoo-Version-Features.
- **Option B (zu prüfen):** nativer Odoo-LLM-Stack (in Odoo 19 neu
eingeführte `llm`/Agent-/Knowledge-Module) mit Ollama als
OpenAI-kompatiblem Provider. **Muss zuerst gegen Euren konkreten
Odoo-19-Quellstand verifiziert werden** (Modulnamen/APIs sind hier
nicht im Projekt und werden nicht aus Trainingswissen behauptet).
Striktes Grounding + Zitierdisziplin wären dort nachzubauen.
- Entscheidung erst nach Verifikation; Phase A läuft davon unabhängig
und wird von beiden Optionen unverändert genutzt.
- **Datenschutz-Bonus:** der Agent ist reiner Wissensassistent — es
fließen keine Mitarbeiter-/Abrechnungsdaten ins Modell, nur die
Frage und Layer-2-Fachtexte.
## 10. Risiken & offene Punkte
1. **Ollama-Erreichbarkeit** gegeben (Ziel-Instanz 11435; Sandbox kann
verbinden). Achtung Doppel-Instanz auf 11434 — URL nicht "korrigieren".
Cloud-Modelle (`*:cloud`) nicht für Antworten verwenden (Anforderung:
lokal).
2. **Ollama-Version:** `/api/embed` + allfällige Rerank-Unterstützung
prüfen; Fallback ohne Reranker ist unkritisch.
3. **Strix Halo (gfx-1151):** Ollama/ROCm muss die Karte unterstützen
(Server läuft offenbar bereits — im Bake-off Performance messen).
4. **Halluzination trotz allem:** Prompt + Post-Validierung reduzieren,
aber nicht eliminieren → Eval-Suite als Dauerschutz; Antworten
führen immer Quellen-IDs (Nachprüfbarkeit durch den Nutzer).
5. **WIKU-Layer-1** fehlt lokal — nur relevant, falls später Layer-1-
Tiefenzitate gewünscht (Lizenzpunkt aus README bleibt offen).
6. **Odoo-19-LLM-Module** unverifiziert → Phase B separat planen.
## 11. Meilensteine
| # | Inhalt | Ergebnis |
|---|---|---|
| M1 | Ingest + Index + Hybrid-Retrieval (ohne LLM) | Recall@8 auf Goldset messbar |
| M2 | Ollama-Anbindung (Embed + Generate), Grounding-Regeln, `/ask`-API, CLI | nutzbarer Agent im Terminal |
| M3 | Eval-Suite + Modell-Bake-off | dokumentierte Modell-Entscheidung |
| M4 | Odoo-Integration | separater Plan nach Odoo-19-Verifikation |
## 12. Entscheidungspunkte (an Dich)
1. **Modell:** `qwen3.8:27b` als primärer Bake-off-Kandidat ok — oder
gleich als Startmodell festlegen (und `qwen3:32b` nur als Fallback)?
2. **Bake-off:** vergleichst Du die drei Antwortmodelle auf dem Goldset
(empfohlen) oder legen wir qwen3:32b direkt fest?
3. **Layer 1:** bewusst außen vor in Phase A — einverstanden?
4. **Odoo-Version für Phase B:** Odoo 19 Enterprise (passend zu
`l10n_at_hr_payroll*`) — bitte bestätigen.
## 13. Umsetzungsstand (2026-09-14)
- **M1 erledigt und akzeptiert:** Hybrid-Index (601 Einträge → 3.005 Chunks,
FTS5-BM25 + bge-m3-Dense, Ollama :11435), Goldset 31 Fragen. **Recall@8
0,952 > 0,9** (Hit-Rate 0,968, MRR 0,690; BM25-only-Vergleich:
0,855 — die vier Komposita-Fehltreffer behebt die Dense-Suche alle).
41 Unit-Tests grün.
- **M2 erledigt und gegen das echte Modell validiert** (qwen3.8:27b,
Thinking aus): Zitier-Präzision **100 %** (4 Verletzungen → Post-
Validierung → Regenerierung, alle geheilt), Verweigerung korrekt 94,3 %,
Latenz mean 32 s / p95 53 s. ATZ-Konfliktfall wird korrekt beidseitig
mit ⚠ beantwortet; harte Verweigerungsfälle (UStVA) funktionieren.
- **M3 erledigt — Bake-off (2026-09-14, Goldset 35 Fragen, Thinking aus):**
| Kandidat | Zitier-Präzision | Verweigerung korrekt | Erw. Quelle | mean/p95 | Regen |
|---|---|---|---|---|---|
| **qwen3.8:27b** ✅ | **100 %** | **94,3 %** | **80,6 %** | 32 s / 53 s | 4 |
| gemma4:26b | 100 % | 91,4 % | 67,7 % | 7,9 s / 12 s | 4 |
| gemma4:12B | 94,3 % | 91,4 % | 77,4 % | 21 s / 40 s | 8 |
| qwen3.6:27B | 94,3 % | 85,7 % | 80,6 % | 43 s / 89 s | 10 |
| muse-glimmer:latest | 100 % | 77,1 % | 74,2 % | 37,5 s / 51 s | 1 |
| mistral-small3.1:24b | 100 % | 71,4 % | 58,1 % | 20 s / 43 s | 2 |
**Entscheidung (D7):** `qwen3.8:27b` ist das Antwortmodell (Protokoll:
Zitier-Präzision → Verweigerungskorrektheit → Latenz). `gemma4:26b`
wird als dokumentierter Latenz-Kandidat für späteres interaktives Tuning
geführt (4× schneller bei 100 % Zitier-Präzision, aber schwächere
Quellentreue und 3 statt 2 Fehlverweigerungen). mistral-small3.1
(Deutsch-Hypothese) ist praktisch widerlegt: 71,4 % Verweigerungs-
korrektheit. **muse-glimmer** (2026-09-14 nachgereicht): 100 %
Zitier-Präzision bei nur 1 Regenerierung (diszipliniertestes Modell),
aber 8/35 falsche Verweigerungen (Überverweigerung teils trotz
vorhandener Zitate) und 37,5 s mean — Rang 5 von 6, schlägt qwen3.8
in keiner Kennzahl. q-008 und q-031 verweigern alle Top-Kandidaten —
Prompt-/Retrieval-Tuning-Thema, kein Modellthema.
**Prompt v2 + Kontext-Tuning (2026-09-14):** (a) Regel 4 erlaubt
Teilantworten, Regel 8 verlangt Prämisse-Korrektur mit Muster-Beispiel;
(b) Kontextblöcke pro Eintrag = beste Inhaltssektion statt bester
Rangfolge-Chunk („Verweise“-Sektionen zuletzt — BM25-Längennormalisierung
rangiert die dünnen Navigations-Chips bevorzugt, Ursache q-008).
Bestätigungslauf qwen3.8:27b (35 Fragen): Zitier-Präzision **100 %**,
Verweigerung korrekt 94,3 % (q-008/q-031 behoben; neuer bekannter Fall
q-029 — breite Survey-Frage, sicherer Fehlermodus), erwartete Quelle
**83,9 %** (v1: 80,6 %), mean 34 s. Report `data/eval-qwen38-v2.json`.
- **M4 offen:** Odoo-Integration (separater Plan nach Verifikation der
Odoo-19-LLM-Module).
- **KV/RIS-Erweiterung (2026-09-15, D9/D10):** Korpus 601 → 1 274 Einträge
(614 WKO-KV-Dokumente `kv-kvt-…`, 59 RIS-Gesetze `ris-…`; quelltreu
generiert — D9), 14 984 Chunks. Retrieval kalibriert (D10:
dense_weight 2.0, rrf_k 20, Pool 150) → Recall@8 0,923 (>0,9 ✓).
Antwortmodus: Zitier-Präzision 95,2 %, Verweigerung 90,5 % —
Fehlverweigerungs-Tuning (offen, laufender Arbeitsstand) fortsetzen.
Intake: `tools/ingest_sources.py`, Registry: `tools/build_registry.py`.
- **Tuning abgeschlossen (2026-09-15, D11):** Ursache der Fehlverweige-
rungen war Prompt-Overflow (lange KV-Chunks > 16k — Systemprompt
trunciert weg). Fix: num_ctx 32 768, max_context_chars=90 000
(`trim_results`), cross_ref_expand 6 → alle 4 Fälle geheilt (10/10
Bestätigungsläufe). Retrieval Recall@8 0,946 · Antworten: Zitier-
Präzision 97,6 %, Verweigerung 97,6 % (Gate ✓), erwartete Quelle
91,9 %, Latenz mean 33 s. Offen: q-015 Branchen-Noise, q-024
transiente Flakiness (leerer Draft).
- **Komplexe-Fragen Stufe 1 (2026-09-15, M6/D12):** Query-Planer
(Heuristik-Gate → 1-3 Sub-Queries mit Stand-Jahr und Scope), Multi-
Query-Retrieval mit Per-Query-Slots und Scope-Filtern („gesetz“/„kv“).
Eval (46 Fragen): Zitier-Präzision 97,8 %, Verweigerung 97,8 %
(Gate ✓), erwartete Quelle 90,2 %, Latenz mean 33,5 s. Goldset 42 →
46 Fragen (q-110113). Offen für Stufe 2: Aggregation → Map-Reduce
(q-029), Rückfragen statt Verweigerung (API-first), danach
Rechtsprechung-Intake (`rj-*`, Lexis-md/json).
- **Antworttyp-Routing Stufe 2 (2026-09-15, M6/D13):** Planer-Typ
`survey` → Map-Reduce (survey_blocks=16; Map destilliert je Block
mit KB-ID, Reduce synthetisiert; Validierung unverändert über die
Union). Systemprompt-Regel 9: Kontext-Abhängigkeit → belegte allgemeine
Aussage + eine Rückfrage (API-first). q-029 geheilt (vorher hart-
näckigste Fehlverweigerung), q-015 mit KV-Abhängigkeits-Hinweis.
Eval: 97,8 % / 97,8 % / 90,2 %, Latenz mean 34,2 s (Map-Reduce nur
bei Survey-Fragen, ~95 s). Nächster Schritt: Rechtsprechung-Intake
(`rj-*`), dann M4 (Odoo; Privacy-Neubewertung für Lohndaten-Zugriff).
- **Output-Budget + length-Retry (2026-09-15, M6/D14):** q-024-Flakiness
war num_predict=1024 (done_reason=length, abgeschnittene Zitationen),
nicht Thinking. Fix: num_predict=2048, chat_full() mit done_reason,
technischer 2×-Budget-Retry bei length. **Voll-Eval erstmals mit allen
Gates erfüllt: Zitier-Präzision 100 %, Verweigerung 100 %, erwartete
Quelle 92,7 %** (46/46), Latenz mean 39,6 s. M6 damit abgeschlossen;
nächster Schritt: Rechtsprechung-Intake (`rj-*`), dann M4 (Odoo;
Privacy-Neubewertung für Lohndaten-Zugriff).
- **Rechtsprechungs-Intake (2026-09-15, D15):** `.rechtsprechung/` wurde
über `tools/ingest_sources.py --source rj` in den neuen ID-Raum
`rj-rjs-*` übernommen: 320 RIS-OGD-Entscheidungen/Rechtssätze, 23
zitierte RIS-Normauszüge sowie 5 als nichtamtliche lexetius-
Textwiedergaben markierte EuGH-Urteile. Korpus: 1.274 → **1.622**
Einträge, 77 Cluster; lange Volltexte werden an Absatzgrenzen in H2-
Chunks geteilt. Reindex: 16.480 Chunks, 1.496 neue bge-m3-Embeddings,
75 s. Erweiterter Retrieval-Eval (50 Fragen, inkl. q-120123): Hit-Rate
0,956 · Recall@8 **0,922** (Gate >0,9 ✓) · MRR 0,661; alle neuen Fälle
gefunden. Die gezielten Antwortläufe q-120123 sind alle zitiergültig,
nicht verweigert und ohne Regenerierung; EuGH q-123 weist nach einer
Prompt-Regel explizit auf die nichtamtliche Wiedergabe hin. Voller
Antwortmodus-Eval (50 Fragen): **100 % Zitier-Präzision**, **100 %
Verweigerung korrekt**, erwartete Quelle 93,3 %, Latenz mean 39,4 s /
p95 82,2 s, fünf Regenerierungen; Report `data/eval-qwen38-rj.json`
(lokal, unversioniert). Vor einem Commit der
quellentreuen Volltexte ist die Publikations-/Lizenzfreigabe bewusst zu
bestätigen; sie folgt nicht automatisch aus der Regel zu amtlichen
Gesetzestexten.
- **API-first-Festigung (2026-09-16, D19):** Der eigenständige Service hat
einen versionierten, strikt validierten Vertrag unter `/v1/ask`,
`/v1/health` und `/v1/reindex`; die alten Pfade bleiben deprecated. Antworten
liefern Status, Quellen, extrahierte belegte Konflikte, Rückfrage,
Planungsmetadaten und einen expliziten Grounding-Block. Request-IDs erlauben
technische Korrelation ohne Personenkennzeichen. Optionaler Bearer-Schutz
trennt Service- und Admin-Key; ein nicht-lokaler Bind ohne Service-Key wird
fail-closed abgelehnt. Health und Fehlerantworten geben keine interne
Ollama-URL bzw. Exception-Details mehr aus. Der gemeinsam genutzte SQLite-
Retriever ist für FastAPI-Worker-Threads serialisiert. **Privacy-Grenze:** v1
akzeptiert ausschließlich `mode=knowledge` und keine freien Payroll-/
Mitarbeiterdatenfelder. Die spätere Odoo-Lohndatenintegration erhält einen
getrennten tenant-autorisierten Vertrag; sie wird nicht durch Anhängen von
Rohdaten an `/v1/ask` umgesetzt. Vertrag und Odoo-Clientregeln:
`docs/API.md`.
- **Test-Frontend (2026-09-16, D20):** FastAPI liefert ein responsives,
dependency-freies same-origin UI unter `/` aus. Es rendert Antworttext
XSS-sicher, zeigt Health, Quellen, Konflikte, Rückfragen sowie Grounding-
Metadaten und speichert weder Fragen noch Chatverlauf. Ein Service-Key wird
nur vom Benutzer eingegeben und optional im `sessionStorage` des Tabs
gehalten. Zielbetrieb ist `http://100.103.83.12:8080/` mit Bind an das
Tailscale-Interface und gesetztem `PV_API_KEY`; CSP und weitere Security-
Header schützen die UI, ohne FastAPI `/docs` zu blockieren.
- **Docker-Deployment (2026-09-16, D21):** Ein gehärteter Compose-Service
betreibt UI und API gemeinsam auf `100.103.83.12:8080` und nutzt das externe
Netz `ollama_default`. Ollama bleibt ein separater bestehender Container;
sein Netzwerk-DNS-Alias wird über `OLLAMA_URL` konfiguriert. Index und KB
bleiben Host-Bind-Mounts, Secrets und lokale Korpora außerhalb des Images.
Der Stack verlangt einen Service-Key, läuft als konfigurierbare unprivilegierte
UID/GID mit read-only Root-FS und besitzt einen Readiness-Healthcheck.
Deployment und initialer Indexaufbau: `docs/DOCKER.md`.
- **Bootstrap, Audit und Bewertungen (2026-09-16, D22):** Vor jedem API-Start
prüft der Container, ob Chunks und Embeddings im persistenten Index vollständig
sind, und baut einen fehlenden/unvollständigen Index automatisch über Ollama
auf; bei Embedding-Fehlern startet die API nicht. Interaktionen werden
strukturiert in `data/audit.db` und optional als JSON nach stdout geloggt;
Inhaltsprotokoll und 30-Tage-Aufbewahrung sind konfigurierbar. Eine
authentisierte `/v1/ratings`-API sowie die UI erfassen Daumen hoch/runter und
optionales Feedback pro Request-ID. Im Metadatenmodus werden auch indirekte
Freitexte aus Quellen, Konflikten und Suchplan entfernt.
- **Antwortkommentare (2026-09-16, D23):** Unabhängig von `up|down` können
Nutzer über eine dauerhaft sichtbare Kommentarbox mehrere Kommentare pro
Antwort erfassen. `/v1/comments` bindet jeden Kommentar an die verifizierte
Request-ID; Audit-CLI und SQLite-Log führen die datierten Kommentare mit der
ursprünglichen Frage/Antwort zusammen. Im Metadatenmodus bleibt der
Kommentartext aus Persistenz und stdout entfernt.
- **Kostenaufstellung für Gestaltungsfragen (2026-09-16, D24):** Der
Decision-Support-Trigger matched jetzt auf normalisierter Frage (NFKD-Folding
plus ue-Varianten) — Fragen wie "guenstigste loesung" ohne Umlaute liefen
vorher in den generischen LLM-Planer und verweigerten. Bei expliziter
Kostennnabsicht ("wieviel kostet mich das", "einmalig ... bar auszahlen")
ergänzt der deterministische Plan zwei gesetzlich gescopte Queries: Lohnsteuer
einmaliger Bezüge (lb-son-04) und Arbeitgeberbelastung (lb-sva-06, lb-lnk);
die Zukunftssicherungs-Query fällt dann zugunsten der Slots weg. Systemprompt
Regel 11 verlangt nun die Anwendung auf den konkreten Fall: Arbeitgeberkosten
Schritt für Schritt aus belegten Sätzen, Annahmen explizit, Rückfrage nur bei
wesentlichem Fehlen. think=true: Leerer Content (Antwort nur im thinking-Feld)
führt zu einem einmaligen Retry ohne Thinking statt HTTP 503. Goldset +2
(q-128/q-129, beide recall=1,00); Offline-Eval 56 Fragen: Hit-Rate 0,98 ·
Recall@8 **0,95** · MRR 0,67. Real-Läufe beider User-Fragen: verifiziert,
mit belegter Rechnung (AG-SV auf 500 €, Lohnsteuer, Prämienvergleich).
- Betrieb: `agent/README.md`.
## 14. Phase B / M4 — Odoo-Integration (D25-Planung, Stand 2026-09-16)
**Vorentscheidung (mit User):** Odoo orchestriert und rechnet (System of
Record); der Agent konsumiert nur das vorgegebene Ergebnis und prüft
Plausibilität gegen die KB. Kein Rückpfad Odoo → Agent-Tools im Agenten;
die Aufweichung der Privacy-Regel 8 passiert bewusst erst hier und wird
im Agenten per Feature-Flag (`PV_REVIEW_MODE`, default aus) freigeschaltet.
### 14.1 Modul-Review (verlinkt als `.oddo-module/` → ../odoo-at-payroll/addons)
- Vier Module, Odoo **19.0** (`l10n_at_hr_payroll` 19.0.10.0.0 auf der
echten `hr_payroll`-Engine; `l10n_at_hr_payroll_private` 19.0.17.0.0;
`l10n_at_gemeinde_payroll` Bgld./GemBG; `l10n_at_payroll_dokumente`),
LGPL-3, ~22k Zeilen Python + ~7,5k XML, ~25 Testdateien.
- **Kein Agenten-/HTTP-Code vorhanden** (keine Controller, requests,
ir.config_parameter) — M4 startet bei null, nichts ist zurückzubauen.
- **Tenant-Isolation nativ:** `ir.rule`-Company-Regeln (GP9/AP9-Muster),
Felder am Vertrag mit `group_hr_payroll_user`-Gruppen.
- **Rechenkern komplett:** `sozialversicherung.py` (SVDN/SVDG, §-49-
Ausschaltungen **mit Jahres-Kumulative** `_l10n_at_sv49_ytd`, WF-Satzvektor
Bundesland×Jahr, DAG), `lohnsteuer.py` (§ 66 kumulativ, § 67 Sechstel/
Fünftel, § 68-Freibeträge mit YTD-Verbrauch `_l10n_at_st_frei_ytd`),
`payslip_private.py` (KommSt § 9, DZ §§ 122/126 WKG, FLAF-DB § 41 FLAG,
SZ-Basis/Dienstzeitfaktor), `sachbezuege`/`reisekosten` (km-YTD-Split).
Genau die Jahres-Salden, die im KB-Chat nur Annahmen sind, sind hier real.
- **Parametersystem:** `hr.rule.parameter`-Seeds 2026 (SV-Werte aus ÖGK-
TASY-Export, gegen offiziellen Report gespiegelt; LST 2026; 2027-Rahmen),
NSchAB/Wien als Company-Flags.
- **KV-Katalog in Odoo:** `l10n.at.payroll.kv` (+ versionierte `kv.wert`,
Gruppen/Stufen, Import-Wizard per CSV-Paste mit Sprungwarnung >10 %).
`library_variant_id` verlinkt auf die KV-Library-Variante (z. B. SI-2203,
Seed SI-2203/SI-2748). **Brücke gebaut (2026-09-16):** die KV-Library führt
bereits einen WKO-Match-Report (`wko/match-report.json`: wko_slug →
oegb_variant_id, 407 matched / 32 low / 175 unmatched); daraus erzeugt
`tools/build_kv_variant_map.py` die versionierte Map
`tools/catalogs/kv_variant_map.json` (105 Varianten, 439/614 KB-Einträge
abgedeckt; Docs je Variante mit kv_kvt_id+slug+doctype; Low-Confidence
markiert). Odoo kann pro `library_variant_id` die zugehörigen KB-Einträge
auflösen; die 175 unmatched sind WKO-aktuelle Dokumente ohne ÖGB-
Gegenstück (z. B. KV-Abschluss-News). Tests: `tests/test_kv_variant_map.py`
(inkl. Seed-Abdeckung SI-2203/SI-2748).
- Vertragsfelder für Kontext-Whitelist vorhanden: `hr.version` (KV, Gruppe,
Erfahrungsstufe, Überzahlung, Vordienstzeiten), Company (Bundesland,
KommSt-Gemeinde, NSchAB).
### 14.2 Architektur M4 (geplant)
1. **Neues fünftes Modul** `l10n_at_payroll_agent` (statt Einbau in die vier
bestehenden): Service-Client, Kontext-Builder, Verdict-UI. Depends:
`l10n_at_hr_payroll_private` (+ `hr_payroll`).
2. **Konfiguration:** Service-URL + API-Key über `ir.config_parameter`, nur
lesbar für eine eigene Gruppe `pv_agent_user`; Key nie in Views/Logs.
3. **Serverseitiger Client** (`pv.agent.client`, Odoo-`requests`, Timeout,
neutrale Fehler): Aufruf `/v1/ask` mit `mode=review` und schematisiertem
Kontext — **Whitelist hart kodiert** (facts: key/value/note; keine Namen,
SVNR, Geburtsdaten; KV-Name/Code, Bundesland/Gemeinde, Beträge, Jahres-
salden, Berechnungsergebnis mit Basis). `X-Request-ID` aus Odoo.
4. **Workflow „Plausibilitätsprüfung einer geplanten Auszahlung“** (Pilot):
Wizard/Dialog am Payslip/Kontext → Odoo rechnet mit den bestehenden
`_l10n_at_*`-Methoden auf einem Draft → `computation`-Objekt (Komponenten,
Bemessung, Ergebnis) → Agent prüft gegen KB → strukturiertes `plausibility`-
Verdict (verdict plausible/implausible/not-checkable + checks mit
erwartet/erhalten/⚠/source_id) → Anzeige im Dialog; keine automatische
Korrektur, Odoo bleibt autoritativ.
5. **Agent-seitig (M4.2, umgesetzt 2026-09-16):** v1.x-Contract `mode=review`
+ `context`-Feld (extra=forbid, Whitelist-Schema: facts ≤40 mit key-Muster
`[a-z0-9_.-]`/value ≤200, computation mit components ≤40). Prompt-Addendum
mit drei Beweisklassen (KB-Beleg vs. übermittelter Wert vs. Odoo-Berechnung),
Injection-Abgrenzung (Kontext ist Daten, keine Anweisungen) und
Verdict-Format (Abschnitt „Plausibilitätsprüfung:“ mit OK/WARN ⚠/OFFEN-
Zeilen; OK/WARN brauchen KB-Beleg, sonst Regenerierung → bleibt der
Abschnitt aus, fällt das Verdict ehrlich auf `not_checkable` statt die
zitiergültige Fachantwort zu verwerfen). Feature-Flag `PV_REVIEW_MODE`
(default aus — Test-Agent bleibt knowledge-only). `grounding.data_scope`
im Review: `knowledge_base_plus_review_context`. Audit speichert den
Kontext in `context_json` (Metadatenmodus: ohne Freitext). Tests
`tests/test_review.py` (8 Fälle, offline).
6. **Audit:** Odoo protokolliert gesendete facts/Ergebnis + request_id;
agentseitig deckt sich `data/audit.db` über dieselbe Request-ID.
7. **Nicht-Ziele Phase B:** keine Lohnart-Erstellung durch den Agenten, keine
automatischen Buchungen, kein direkter Mitarbeiterzugriff im Chat
(Mandanten-/Rollengrenze bleibt in Odoo).