# Agent-Memory — pv-agent Rollender Übergabe-Log für agent-Threads. Workflow: `.agents/SKILL.md` (agent-memory-Skill). Ergänzen, nicht überschreiben. ## Current focus D24 validiert (500-Euro-Fragen verifiziert, think-Fix, Offline-Eval Recall@8 0,95). D25-Planung steht (planung.md Abschnitt 14) und **M4.2 ist umgesetzt**: Agent `mode=review` mit schema-gebundenem Odoo-Kontext, Beweisklassen, Injection- Abgrenzung und `plausibility`-Verdict; Feature-Flag `PV_REVIEW_MODE` (default aus). KV-Varianten-Mapping gebaut (`tools/catalogs/kv_variant_map.json`, 439/614 abgedeckt, Seeds SI-2203/SI-2748 getestet). 106 Tests grün. Als Nächstes: M4.1 — Odoo-Modul `l10n_at_payroll_agent` (Client, Kontext-Builder, Pilot-Workflow am Draft-Payslip). ## Completed (2026-09-16, Modul-Review/D25-Planung) - **`.oddo-module/` verlinkt** (Symlink → ../odoo-at-payroll/addons, in `.gitignore` aufgenommen). Review gegen M4: Odoo 19.0 (Kern 19.0.10.0.0 auf `hr_payroll`, Privat 19.0.17.0.0, Gemeinde-Bgld, Dokumente), LGPL-3, ~22k Zeilen Python / ~7,5k XML / ~25 Testdateien. **Kein HTTP-/Controller-/Agenten-Code vorhanden** — M4 startet bei null. Tenant-Isolation via `ir.rule` (company_ids), Feldgruppen `group_hr_payroll_user`. Rechenkern: sozialversicherung.py (§-49-Ausschal- tungen mit Jahres-Kumulative `_l10n_at_sv49_ytd`, WF-Satzvektor Bundesland ×Jahr, DAG), lohnsteuer.py (§ 66/67/68 mit YTD-Freibetragsverbrauch), payslip_private (KommSt/DZ/FLAF-DB/SZ), sachbezuege/reisekosten. Parameter aus ÖGK-TASY-Export gegen offiziellen Report gespiegelt; 2027-Rahmen. KV-Katalog `l10n.at.payroll.kv` mit versionierten `kv.wert`, Gruppen/Stufen und CSV-Import-Wizard (Sprungwarnung >10 %); `library_variant_id` verlinkt auf die KV-Library (SI-2203/SI-2748). D25-Planung steht in `planung.md` Abschnitt 14: neues Modul `l10n_at_payroll_agent`, ir.config_parameter plus eigene Gruppe, serverseitiger Client, hart kodierte Kontext-Whitelist (facts key/value/note, keine Personendaten), Pilot-Workflow Plausibilitätsprüfung einer geplanten Auszahlung (Odoo rechnet auf dem Draft-Payslip, Agent liefert strukturiertes Verdict), Agent-M4.2: `mode=review` plus context-Schema, drei Beweisklassen, Injection-Abgrenzung und Feature-Flag `PV_REVIEW_MODE` (default aus). Nicht-Ziele fixiert. - **M4.2 Agent-Review-Modus (2026-09-16, D25):** `AskRequest.mode` `knowledge|review` + `context` (StrictModel: facts ≤40, key-Muster, computation + components ≤40; keine freien Objekte). `answer_question` nimmt `context` an: Review-Addendum an den Systemprompt (Regeln 13/14: Kontext = Daten/keine Anweisungen, übermittelte Werte ohne KB-ID, Verdict-Format „Plausibilitätsprüfung:“ mit OK/WARN ⚠/OFFEN), `parse_plausibility_checks` extrahiert Checks (OK/WARN ohne erlaubte KB-ID zählen nicht), `plausibility_verdict` (implausible bei ⚠, plausible bei OK, sonst not_checkable). Fehlender/leerer Abschnitt → Regenerierung; bleibt er aus, bleibt die zitiergültige Fachantwort mit `not_checkable`-Verdict bestehen (keine UNCERTAIN-Eskalation). API-Gating: review ohne `PV_REVIEW_MODE` → 422; knowledge+context → 422; review ohne context → 422. `grounding.data_scope` review: `knowledge_base_plus_review_context`. Audit: `context_json`-Spalte mit idempotenter Migration, Metadatenmodus ohne Kontext-Freitext. CLI: `ask --context FILE`. Response-Felder `mode` + `plausibility`. Tests +8 (tests/test_review.py) → **107 grün**. **Real-Lauf validiert** (qwen3.8, Odoo-Kontext-Sonde mit provokativ falscher Steuerfreiheit in der Simulation): verified, 1 Regen, **Verdict implausible** — WARN Lohnsteuer (lb-lvr-07/ lb-naz-03), OK SV-Grundsatz (lb-naz-02), OFFEN DB/DZ + BVK; Parser-Fix: Heading-Regex toleriert `**Plausibilitätsprüfung:**` (Doppelpunkt in den Sternen) sowie ⚠-Präfix in der Aspect-Extraktion. - **KV-Varianten-Mapping (Odoo ↔ KB, 2026-09-16):** Die KV-Library führt bereits `wko/match-report.json` (wko_slug → oegb_variant_id; 407 matched / 32 low / 175 unmatched). `tools/build_kv_variant_map.py` erzeugt daraus `tools/catalogs/kv_variant_map.json`: 105 Varianten (98 matched, 7 low), 439/614 KB-Einträge abgedeckt; je Variante `docs` mit kv_kvt_id + slug + doctype als Paare, Low-Confidence markiert, Rest in `unmatched_kb_entries` (WKO-aktuelle Dokumente ohne ÖGB-Gegenstück, z. B. KV-Abschluss-News). Odoo kann pro `library_variant_id` die zugehörigen KB-Einträge auflösen; Seed-Abdeckung SI-2203/SI-2748 testet `tests/test_kv_variant_map.py`. Tests gesamt **98 grün**. ## Completed (2026-09-14) - **Planung** (`planung.md`): Architektur, Grounding-Regeln, Modellfeld, Meilensteine M1–M4; **Skill** `.agents/skills/pv-rag-agent/SKILL.md`. - **Commits**: `25eb285` (Wissensbasis-Import), `bf8191b` (Planung + Skills), `2cba72a` (M1+M2-Implementierung, 41 Tests). - **Implementierung M1+M2** (`agent/`): kb/ingest/retrieve/generate/ ollama_client/api/cli/eval, Goldset 31 Fragen, Test-Chat. - **Ollama-Anbindung verifiziert** (Ziel-Instanz `http://100.103.83.12:11435`, Ollama 0.32.13): `bge-m3` per API gepullt; `qwen3.8:27b` war bereits installiert. Hybrid-Index: 3.005 Chunks, alle eingebettet (144 s). - **M1-Akzeptanz erreicht**: Retrieval Hybrid Hit-Rate 0,968 · Recall@8 **0,952** (>0,9 ✓) · MRR 0,690 (BM25-only: 0,871/0,855/0,476). - **M2-Antwortmodus-Eval** (qwen3.8:27b, Thinking aus): Zitier-Präzision **100 %** (4 Regenerierungen, alle geheilt) · Verweigerung korrekt 94,3 % · erwartete Quelle zitiert 80,6 % · Latenz mean 32 s / p95 53 s. Report: `data/eval-qwen38.json`. - Der ATZ-Konfliktfall (28,5 vs. 27,5 %) wird korrekt mit ⚠ und beiden IDs beantwortet; harte Verweigerung (UStVA, Retrieval nicht leer) funktioniert. - **Topologie geklärt**: `100.103.83.12` ist die **Remote-GPU-Maschine** (Entwicklungsumgebung, R9700, Tailscale); die Dev-Maschine selbst hat einen eigenen, fast leeren Ollama auf localhost:11434 (dorthin ging der erste bge-m3-Pull — auf die GPU-Maschine neu gepullt). Ziel-Instanz ist ausschließlich `http://100.103.83.12:11435`. - **Bake-off M3 — abgeschlossen (D7)**: alle 6 Kandidaten gemessen (Tabelle in `planung.md` Abschnitt 13). **`qwen3.8:27b` gewinnt** (100 % Zitier-Präzision, 94,3 % Verweigerung korrekt, 80,6 % erwartete Quelle, 32 s mean). `gemma4:26b` = dokumentierter Latenz-Kandidat (7,9 s mean, 100 % Zitier-Präzision, aber 67,7 % Quellentreue, 3 Fehlverweigerungen). mistral-small3.1: 71,4 % Verweigerungskorrektheit — Deutsch-Hypothese praktisch widerlegt. **muse-glimmer:latest** (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. qwen3.6:27B: 2 dauerhafte Zitierverletzungen, 10 Regenerierungen. gemma4:12B: 2 Verletzungen. Reports: `data/eval-*.json`. q-008/q-031 verweigern alle Top-Kandidaten — Prompt-/Retrieval-Tuning (nicht modellspezifisch). - **Remote-GPU-Maschine**: nach `systemctl restart ollama` (User-Aktion) liefen alle Läufe stabil; zwischen Kandidaten wurde per `keep_alive: 0` entladen (Crash-Ursache war der Modell-Swap-Stress beim gemma4-Lauf). ## Completed (2026-09-15) - **KB-Erweiterung um WKO-Kollektivverträge + RIS-Gesetze** (User-Auftrag): `.firecrawl/kv-portal/wko-kv/docs/` (614 KV-Dokumente: Lohn-/Gehaltsordnungen mit Lohntabellen, KV-Abschlüsse, Erläuterungen, Zusatz-KVs) und `.firecrawl/ris/gesetze/` (59 Gesetze, nur die im Lexis360-Bestand zitierten §-Ausschnitte, Stand 2026-09-13) → **1 274 Layer-2-Einträge** (`kv-kvt-001…614`, `ris--nn`), 14 984 Chunks, Reindex 8,5 min (11 947 neue Embeddings, bge-m3 auf :11435). - **D9 (2026-09-15):** kv/ris-Einträge sind **quellentreu generiert** (keine Eigene-Worte-Kuratierung — Gesetze sind amtliche Werke, KV- Lohntabellen zahlenexakt; Konvention 2 der Wissensbasis gilt nur für die lizenzierten Quellen lb/wk). Tool-Output: `tools/ingest_sources.py` + `tools/build_registry.py` (Registry in diesem Repo neu gebaut — das Lexis-Tool `build_lexis_kb.py` lebt im Schwesterprojekt), eingefrorene Kataloge `tools/catalogs/*.json` (nur Metadaten, versioniert). - **ID-Räume:** `kv-kvt-` (ein Cluster `kollektivvertraege`, Branche als Tag) und `ris--` auf der bestehenden Cluster-Map + 7 neue Cluster (kvt/zvr/avr/agg/lst/abo/nso); `LAW_MAP` in `tools/kb_common.py` dokumentiert die 59 Zuordnungen. - **D10 (Retrieval-Kalibrierung, Goldset-Sweep 2026-09-15):** Korpus- verdopplung drückte Recall@8 auf 0,851 — kalibriert auf `dense_weight=2.0` (Dense-Liste relativ zu BM25 gewichtet; BM25 ist durch KV-§-Titel-Matches inflationiert), `rrf_k=20`, `candidate_pool=150` → **Recall@8 0,923 (>0,9 ✓)**, Hit-Rate 0,973 (36/37), MRR 0,667. ENV: `PV_DENSE_WEIGHT`/`PV_RRF_K`/ `PV_CANDIDATE_POOL`. Entry-RRF-Variante getestet: kein Mehrwert. - **Antwortmodus-Eval (qwen3.8:27b, 42 Fragen):** Zitier-Präzision 95,2 % (2 Verletzungen), Verweigerung korrekt 90,5 % (4 Fehlver- weigerungen, u.a. q-022/q-029), erwartete Quelle zitiert 83,8 %, Latenz mean 31 s / p95 53 s, 9 Regenerierungen — **unter den M3- Gates** (100 % / 94,3 %): Korpusverdopplung macht Grounding härter; das laufende Fehlverweigerungs-Tuning (uncommittete Änderungen in generate.py/retrieve.py/goldset.yaml, unangetastet) ist weiteres Thema. Report: `data/eval-qwen38-kvris.json`. - **Goldset:** +7 Fragen (q-101–106 KV-Preise/§-Fragen, r-005 Branchen- Refusal) — append-only. q-021 rekalibriert: erwartete ID jetzt `lb-kar-04` („Karenz - Anspruch, Beginn und Dauer“, Top-1, deckt Beginn/Dauer vollständig; lb-kar-01 nur Nachbarschaft — dokumentiert im Goldset-Note). - **Tests:** 41 → 49, alle grün (neue ID-Räume, html-Source, Konverter, LAW_MAP-Vollabdeckung, Korpus-Integrationszahl 1 274). - **D15 (Rechtsprechungs-Intake, 2026-09-15):** `.rechtsprechung/` → neuer ID-Raum `rj-rjs-*`, gemeinsamer Cluster `rechtsprechung`: 320 RIS-OGD- Entscheidungen/Rechtssätze, 23 zitierte RIS-Normauszüge und 5 als nichtamtlich markierte EuGH-lexetius-Textwiedergaben. Parser: `tools/ingest_sources.py --source rj`; lange Volltexte werden an Absatzgrenzen in H2-Chunks geteilt. `rj_catalog.json` friert die IDs ein; Registry/Index und Agent-Zitatvalidierung kennen `rj-*`. Korpus 1.274 → **1.622** Einträge / 77 Cluster; Reindex: 16.480 Chunks, 1.496 neue Embeddings in 75 s. Retrieval-Goldset erweitert um q-120–123: Hit-Rate 0,956 · Recall@8 **0,922** (Gate ✓) · MRR 0,661, alle vier neuen Fälle gefunden. Gezielte Antwortläufe q-120–123: alle zitiergültig, nicht verweigert, keine Regenerierung; Systemprompt verlangt für als nicht amtlich markierte Quellen die gleiche Einschränkung in der Antwort (q-123 verifiziert). Voller Antwortmodus-Eval (50 Fragen): **100 % Zitier-Präzision**, **100 % Verweigerung korrekt**, erwartete Quelle 93,3 %, mean 39,4 s / p95 82,2 s, 5 Regenerierungen; Report `data/eval-qwen38-rj.json` (lokal/unversioniert). Offline-Tests: 65 grün. **Nicht als generelle Commit-Freigabe lesen:** Die gelieferten OGH/VwGH/VfGH-Dateien nennen überwiegend RIS-OGD, der User sprach jedoch von Lexis/opendataloader; die Publikations-/Lizenzfreigabe der quellentreuen Volltexte ist vor Commit ausdrücklich zu prüfen. EuGH-Wiedergaben sind als nichtamtlich markiert und verweisen auf EUR-Lex. ## Completed (2026-09-16) - **D16 (Gestaltungsfragen / Retrieval-Tuning):** Goldset um q-124–126 erweitert (500-Euro-Zusatzzahlung, Alternativen, Mitarbeiterprämie 2026). `agent/query_planner.py` erkennt Decision-Support deterministisch und zerlegt in drei gesetzlich gescopte Queries: Mitarbeiterprämie samt §-49-Konflikt, Zukunftssicherung sowie Sach-/zweckgebundene Leistungen. Decision-Support läuft als `specific`, nicht als Survey/Map-Reduce. `Retriever.search()` nutzt denselben Plan im Offline-Eval; `search_multi()` behält Scope/Jahr auch bei einer einzelnen Query. Ergebnis (53 Goldsetfragen / 48 Retrieval-Fälle): q-124/q-125/q-126 jeweils Recall@8 **1,00**, gesamt Recall@8 **0,9271** (Gate >0,9), Hit-Rate 0,9583, MRR 0,6158. Bekannte Altfehler q-015/q-113 bleiben unverändert. - **Decision-Support-Grounding:** Systemprompt trennt Barzahlung von zweckgebundenen Leistungen, verbietet pauschale Sieger ohne Kontext, verlangt vergleichbare Abgabendimensionen und genau eine Rückfrage. Der bekannte Konflikt Mitarbeiterprämie 2026 wird offen dargestellt: `wk-akt-04` nennt SV/BV/DB/DZ/KommSt-Pflicht, `lb-sva-03` ordnet die Prämie systematisch in den taxativen Katalog beitragsfreier Bezüge ein. Zusätzlich erzwingt ein semantischer Post-Validation-Gate ⚠ + beide IDs + korrekte Rollen und verhindert interne Regelverweise bzw. Quellenpriorisierung. Gezielte Real-Läufe: q-124/q-125 nicht verweigert, zitiergültig; q-126 war bereits sauber. Tests **72 grün**; `git diff --check` sauber. - **D16 Voll-Eval (2026-09-16):** 53 Fragen mit `qwen3.8:27b`; Report `data/eval-qwen38-decision-support.json` (lokal/gitignored). Retrieval: Hit-Rate 0,9583, Recall@8 **0,9271**, MRR 0,6266. Antworten: Zitier-Präzision **98,11 %**, Verweigerung korrekt **98,11 %**, erwartete Quelle 91,67 %, mean 40,3 s / p95 98,4 s, 4 Regenerierungen. Einziger Gate-Fehler: q-125 eskalierte nach zwei semantisch unzureichenden Antworten zu `UNCERTAIN` (`verified=false`). Direkte Wiederholung von q-125 war mit einer Regenerierung vollständig, zitiergültig und stellte den Konflikt korrekt dar — verbleibend ist stochastische Gate-/Regenerierungs-Flakiness, kein Retrieval-Fehler. q-124 und q-126 bestanden im Voll-Eval. - **D16 Stabilisierung/Bestätigung (2026-09-16):** Bei vollständig ausgelassenem, aber retrieved Mitarbeiterprämien-Konflikt ergänzt die Pipeline deterministisch eine feste, ausschließlich aus `lb-sva-03` und `wk-akt-04` formulierte ⚠-Notiz. Eigene Konfliktversuche bleiben ungeändert und werden weiter auf Rollenfehler/Priorisierung geprüft. Der semantische Gate akzeptiert gleichwertige Verben (`ordnet`, `listet`, `führt`) statt fragiler exakter Wortwahl. Sonden: q-124 3/3 ohne Regenerierung, q-125 3/3 verifiziert, q-126 verifiziert. Voller Bestätigungslauf (53 Fragen), Report `data/eval-qwen38-decision-support-confirm.json`: Zitier-Präzision **100 %**, erwartete Quelle 93,75 %, mean 40,3 s / p95 69,6 s, 3 Regenerierungen; q-124–126 alle bestanden. Verweigerungskorrektheit 98,11 % nur wegen der bekannten stochastischen Survey-Fehlverweigerung q-029; direkte q-029- Wiederholung antwortete verifiziert (aber sehr langsam, 280 s). - **q-015 fachlich rekalibriert:** Eine allgemeine Tagesgeldfrage mischt zwei Ebenen: Der KV regelt vorrangig den arbeitsrechtlichen Anspruch, das EStG den steuerfreien Satz. Branchen-KV-Treffer sind deshalb korrekt; q-015 hat keine künstliche Einzel-Pflicht-ID mehr. Neue q-127 prüft ausdrücklich nur die steuerliche Höhe und findet `lb-rei-09`. Goldset 54 Fragen; Offline-Retrieval nun Hit-Rate 0,9792, Recall@8 **0,9479**, MRR 0,6653. Tests **75 grün**. - **llama.cpp-LLM-Eval (2026-09-16):** Lokaler/privater OpenAI-kompatibler Server aus `.env` getestet (Zugangsdaten/URLs nicht dokumentiert oder versioniert); nur das LLM wurde gewechselt, Retrieval und bge-m3-Index blieben unverändert. llama.cpp benötigt `chat_template_kwargs.enable_thinking=false`; Chatmodell meldet sich als `qwen3.8`. 54-Fragen-Report: `data/eval-llamacpp-qwen38.json` (lokal/gitignored). Zitier-Präzision **100 %**, Verweigerung korrekt 96,3 %, erwartete Quelle 91,67 %, mean 54,9 s / p95 104,8 s, 2 Regenerierungen. q-124–126 bestanden alle ohne Regenerierung. Fehlverweigerungen: q-029 (bekannte Survey-Flakiness, 357 s) und q-102. q-102 ist kein LLM-Defizit: Mit Planner lieferte der KV-Scope viele ähnlich benannte KVs und nur einen irrelevanten Abschnitt von kv-kvt-197; ohne Planner beantwortete dasselbe llama.cpp-Modell die Frage in 14,8 s korrekt aus kv-kvt-056. Schluss: llama.cpp ist grounding-stabil, aber langsamer als die Ollama-Basislinie; q-102 erfordert später Planner-/Retrieval-Tuning, nicht Modell-Tuning. - **llama.cpp-Rerun nach Server-Tuning (2026-09-16):** Report `data/eval-llamacpp-qwen38-rerun.json` (lokal/gitignored), gleicher Aufbau wie zuvor: nur LLM über llama.cpp, Retrieval/bge-m3 unverändert. Ergebnis: Zitier-Präzision **100 %**, Verweigerung korrekt **98,15 %**, erwartete Quelle **95,83 %**, mean 53,5 s / p95 105,4 s, 6 Regenerierungen. Gegenüber dem ersten Lauf: q-029 geheilt, erwartete Quelle +4,16 Prozentpunkte, mean ca. 1,3 s schneller; q-102 bleibt als einzige Fehlverweigerung reproduzierbar (Planner-/Kontextproblem). q-124–126 bestehen weiterhin; q-125/q-126 benötigten diesmal je eine Regenerierung. Das Server-Tuning ist damit qualitativ besser, erhöht aber die Regenerierungen (2 → 6); Latenz bleibt deutlich über der Ollama-Basislinie. - **llama.cpp Full-Stack (Chat + Embeddings, 2026-09-16):** llama.cpp- Embeddings sind trotz gleicher Dimension 1024 inkompatibel zu Ollama `bge-m3` (Same-Text-Cosinus nur ca. -0,004 bis 0,017); deshalb separater, lokaler Index `data/index-llamacpp.db`. Vollständig 16.480 Chunks; der Embedding-Server lehnt einzelne Texte > ca. 9.000 Zeichen ab, daher wurden nur für diesen Test längere Embedding-Inputs deterministisch auf 9.000 Zeichen gekürzt (KB und Produktionsindex unverändert). Indexbau ca. 17,4 min. Retrieval ohne Neukalibrierung: Hit-Rate 0,9583, Recall@8 **0,9323**, MRR 0,5977 (Gate >0,9 erfüllt). Vollreport `data/eval-llamacpp-fullstack.json` (lokal/gitignored): Zitier-Präzision **100 %**, Verweigerung korrekt 94,44 %, erwartete Quelle 87,5 %, mean **44,6 s**, p95 130,8 s, 3 Regenerierungen. Full-Stack ist schneller als llama.cpp-LLM + bge-m3 (53,5 s), aber fachlich schwächer: Fehlverweigerungen q-029, q-102, q-111. q-102: korrektes Dokument kv-kvt-056, aber falscher repräsentativer Abschnitt; q-111: Embedding-Retrieval verfehlt kv-kvt-001 komplett und liefert fremde Lehrlingstabellen. Decision-Support q-124–126 besteht weiter. Fazit: Full-Stack ist testfähig und erfüllt das Retrieval- Recall-Gate, ersetzt bge-m3 aber noch nicht; zuerst eigene RRF-/Dense- Kalibrierung und KV-Metadaten-/Representative-Section-Tuning nötig. - **llama.cpp Full-Stack Rerun (2026-09-16):** identischer separater Index und unveränderte Parameter; Report `data/eval-llamacpp-fullstack-rerun.json`. Zitier-Präzision **100 %**, Verweigerung korrekt **98,15 %**, erwartete Quelle **91,67 %**, mean 49,5 s / p95 101,2 s, nur 1 Regenerierung. Gegenüber erstem Full-Stack-Lauf: q-029 und q-111 geheilt, Fehlverweigerungen 3 → 1, erwartete Quelle +4,17 Prozentpunkte, Regenerierungen 3 → 1; Latenz dagegen 44,6 → 49,5 s. Einziger reproduzierbarer Fehler bleibt q-102 (korrektes Dokument, falscher Representative-Section-Kontext). q-124–126 alle ohne Regenerierung bestanden. Full-Stack erreicht damit dieselbe Verweigerungs- korrektheit wie der aktualisierte llama.cpp-LLM+bge-m3-Lauf, bleibt aber bei erwarteter Quelle darunter (91,67 % vs. 95,83 %). - **D17 (Runtime-Entscheidung, 2026-09-16):** Nach den llama.cpp-LLM- und Full-Stack-Vergleichen bleibt der Agent bei **Ollama** mit `qwen3.8:27b` + `bge-m3`. Gründe: beste bzw. stabilere Gesamtbalance aus erwarteter Quellentreue, Verweigerungskorrektheit und Latenz; llama.cpp- Full-Stack zeigte embedding-spezifische KV-/Representative-Section-Fehler und höhere Flakiness. Der letzte angeforderte Full-Stack-Rerun wurde nach drei Fragen vom User abgebrochen und nicht gewertet. Lokale llama.cpp- Indizes/Reports und `.env` bleiben unversioniert; keine Integration in die Produktionskonfiguration. - **Finaler Ollama-54-Eval (2026-09-16):** Nach Rückkehr zum Ziel-Stack `qwen3.8:27b` + `bge-m3`, Report `data/eval-qwen38-final-54.json` (lokal/gitignored). Retrieval: Hit-Rate 0,9792, Recall@8 **0,9479**, MRR 0,6378. Antworten: Zitier-Präzision **100 %**, Verweigerung korrekt **100 %**, erwartete Quelle 95,83 %, mean **38,8 s**, p95 67,2 s, 2 Regenerierungen. q-029 sowie q-124–127 bestanden; damit beide Haupt-Gates erstmals im finalen 54er-Set vollständig erfüllt. Zwei Quellentreue- Abweichungen ohne Grounding-Verstoß: q-127 zitiert alternative Reisekosten- Quellen (`lb-rei-01/03`, `wk-rei-01` plus KVs) statt erwarteter `lb-rei-09`; q-025 zitiert Betriebsübergangs-/Insolvenzquellen statt der bisherigen Expected-ID. Optional später Goldset-Expected-IDs fachlich rekalibrieren, aber kein Release-Blocker. - **D18 (Rechtsprechungsnutzung, 2026-09-16, User-Freigabe):** Auf gerichtliche Entscheidungen darf in Antworten verwiesen werden. Ihr Inhalt darf in eigenen Worten wiedergegeben und stellenweise zitiert werden. Die Agentenantwort muss weiterhin die retrieved `rj-*`-/RIS-KB-ID nennen, nichtamtliche Wiedergaben ausdrücklich kennzeichnen und darf keine nicht retrieved Entscheidung ergänzen. Diese Freigabe umfasst Referenz, Zusammenfassung und auszugsweise Zitate; sie ist keine pauschale Freigabe zur Veröffentlichung vollständiger quellentreuer Entscheidungstexte. ## Open issues / blockers - **Push**: Remote localhost:3003 aus der Sandbox nicht erreichbar — User pusht vom Host. - **Fehlverweigerungen — Stand nach Prompt v2:** q-008 und q-031 sind **behoben** (Section-Priorität + Regel 8). Verbleibend: q-029 („Welche Neuerungen behandelt WIKU Personal aktuell 2026?“) — breite Survey-Frage über 12 Hefte; Kontext enthält echte Zusammenfassungen, Modell verweigert trotzdem (sicherer Fehlermodus). Mögliche spätere Hebel: Survey-Rule im Prompt, mehr Kontextblöcke oder Map-Reduce-artige Zusammenfassung. Weitere Iterationen erst mit neuen Evaluationsdaten. - **Latenz**: mean 32 s/ Antwort ist hoch (Dense 27B + große Prompts). Hebel: weniger Kontextblöcke (aktuell 8+6), schnellere Kandidaten. - **Cloud-Modelle** (`*:cloud` auf der Ollama-Instanz) sind für Antworten tabu (Anforderung: lokal). Nicht versehentlich konfigurieren. - **Bake-off M3 — erledigt** (siehe Completed; Entscheidung D7 in `planung.md`). Nach Tuning von Prompt/Retrieval: erneuter kurzer Bestätigungslauf nur mit dem Sieger. - **M4 Odoo**: native LLM-Module des konkreten Odoo-19-Stands verifizieren (keine API-Annahmen); Option A (dünnes Custom-Modul + Service-API) ist Default. - **Nach KV/RIS-Erweiterung (2026-09-15)**: Antwortmodus vor dem Tuning (D10-Follow-up) unter den Gates (Zitier-Präzision 95,2 % — 2 Verlet- zungen; Verweigerung 90,5 % — 4 Fehlverweigerungen, u.a. q-022/q-029; erwartete Quelle 83,8 %) — Report `data/eval-qwen38-kvris.json`. **D11-Fix behoben die 4 Fehlverweigerungen**: Antwortmodus jetzt Zitier-Präzision 97,6 %, Verweigerung **97,6 %** (>94,3 %-Gate ✓), erwartete Quelle 91,9 %, Retrieval Recall@8 0,946 — Report `data/eval-qwen38-kvris-tuned.json`. Verbleibend: q-015 (Branchen- Noise) und q-024 als transiente Flakiness (leerer Draft — Think-Ghost- Verdacht bei 32k-Kontext; beobachten). - **q-015 (Tagesgelder Dienstreisen)** — Branchen-Noise: 8 Branchen-KV- Einträge verdrängen lb-rei-01/09 (EStG-Taggeld-Abrechnung) aus Top-8; generische Branchen-Frage ist mehrdeutig geworden. Hebel: Frage präzisieren (Branche/Kontext) oder KV-§-Titel-Matches dämpfen. ## Decisions & conventions - **D1 (Planung):** Schlanke Eigen-Pipeline statt LangChain/LlamaIndex — Grounding-Kontrolle schlägt Framework-Komfort bei 601 Dokumenten. - **D2:** Retrieval-Korpus ist **nur Layer 2**; Layer 1 bleibt aus Prompts (Lizenz); Antworten zitieren `[kb-id]` + `(Stand YYYY-MM)`. - **D3:** Umlaut-Folding für FTS (NFKD, ß→ss) — gilt konsistent für Index und Query; ASCII-Slug-Konvention der Wissensbasis bleibt davon unberührt. - **D4:** Post-Validierung strikt: zitierte IDs ⊆ Retrieved-Set (Block-Kopf- IDs); Fließtext-Verweis-IDs sind KEINE Belege (Systemprompt-Regel 2) — Verstoß → 1× Regenerierung → Verweigerung (UNCERTAIN_MESSAGE). - **D5:** Vektoren-Tabelle ist Cache (Content-Hash × Modell), Rebuild löscht sie nicht; `--no-embed` setzt `embed_off` im Config-Copy. - **D6:** Leeres Retrieval → deterministische Verweigerung ohne LLM-Call. - **D7 (Bake-off 2026-09-14):** `qwen3.8:27b` ist das fixierte Antwortmodell (100 % Zitier-Präzision, 94,3 % Verweigerung korrekt). `gemma4:26b` als dokumentierter Latenz-Kandidat; ein Modellwechsel läuft nur erneut über das dokumentierte Protokoll (Skill). - **D8 (Prompt-Tuning 2026-09-14):** Prompt v2 + Kontext-Section- Priorität. (a) Regel 4 erlaubt Teilantworten bei unvollständiger Deckung; Regel 8 verlangt Prämisse-Korrektur statt Verweigerung (Muster-Beispiel „Mindestlohngesetz“). (b) `_representative_chunk` wählt pro Eintrag die beste **Inhaltssektion** (Zusammenfassung > Kernwerte > Rechtsgrundlagen > Payroll > sonstige; „Verweise“ nur als letzter Rückgriff — BM25 rangiert die dünnen Navigations-Chunks bevorzugt, q-008-Ursache). Ergebnis v1→v2: Zitier-Präzision 100 % gehalten; q-008 + q-031 behoben; erwartete Quelle 80,6 → 83,9 %; Verweigerung 94,3 % (Fehler getauscht: neu q-029 — breite Survey-Frage über 12 Hefte, sicherer Fehlermodus). Report `data/eval-qwen38-v2.json`. - **D9 (KV/RIS-Erweiterung, 2026-09-15):** kv-/ris-Einträge sind **quellentreu generiert** (keine Eigene-Worte-Kuratierung — Gesetze sind amtliche Werke, KV-Lohntabellen zahlenexakt; Konvention 2 der Wissensbasis gilt nur für die lizenzierten Quellen lb/wk). Tool-Output: `tools/ingest_sources.py` + `tools/build_registry.py` (Registry in diesem Repo neu gebaut — das Lexis-Tool `build_lexis_kb.py` lebt im Schwesterprojekt), eingefrorene Kataloge `tools/catalogs/*.json` (nur Metadaten, versioniert). IDs: `kv-kvt-` (ein Cluster `kollektivvertraege`, Branche als Tag), `ris--` auf der bestehenden Cluster-Map + 7 neue Cluster; `LAW_MAP` in `tools/kb_common.py` (59 Gesetze). - **D10 (Retrieval-Kalibrierung, 2026-09-15):** nach KV/RIS-Erweiterung (3.005 → 14 984 Chunks) dense_weight=2.0 (Dense relativ zu BM25 gewichtet — BM25 durch KV-§-Titel-Matches inflationiert), rrf_k=20, candidate_pool=150 → Recall@8 0,851 → 0,923 (>0,9 ✓), Hit-Rate 0,973, MRR 0,667. ENV: PV_DENSE_WEIGHT / PV_RRF_K / PV_CANDIDATE_POOL. Entry-RRF-Variante getestet: kein Mehrwert. - **D11 (Prompt-Budget + Kontext-Abdeckung, 2026-09-15):** lange KV-Chunks ueberlieferten num_ctx=16384 (q-022: 62,7 KB Prompt ≈ 18k Tokens) — Ollama trunciert den Systemprompt vorn, das Modell verliert die Zitierregeln („Block 8“-Zitate statt IDs → Regenerierung → Verweigerung). Fix: num_ctx 32 768, `trim_results` (max_context_chars=90 000, Tail- Bloecke ganz weg statt trunciert, Mindestbestand 6) und cross_ref_expand 3 → 6 (die Top-3 sind oft Branchen-KV-Bloecke mit leeren cross_refs — kuratierte Nachbarn kamen sonst nie nach). Ergebnis: alle 4 Fehlverweigerungs-Faelle (q-001/q-019/q-022/q-029) geheilt, 10/10 Bestaetigungslaeufe OK. Eval: Zitier-Praezision 97,6 %, Verweigerung 97,6 % (> 94,3 %-Gate ✓), erwartete Quelle 91,9 % (vorher 83,8 %), Retrieval Recall@8 **0,946** (cross_ref-Extras bringen Expected-IDs nach), MRR 0,671. Latenz mean 33 s. Report `data/eval-qwen38-kvris-tuned.json`. Restrisiko: q-024 flaky (1/3 Laeufe — transienter leerer Draft, Think-Ghost-Verdacht); q-015 Branchen-Noise bleibt offen. - **D12 (Komplexe-Fragen-Stufe 1 / M6, 2026-09-15):** Query-Planer vor dem Retrieval: Heuristik-Gate (Jahreszahl, Vergleichs-/Aggregationsmarker, Laenge) entscheidet, ob ein kleiner LLM-Call die Frage in 1-3 Sub-Queries zerlegt (JSON, temp 0, num_predict 220; Fehler -> Original als Einzel- Query). Multi-Query-Retrieval: je Sub-Query BM25+Dense, Beitraege summieren; **Per-Query-Slots** (2 je Sub-Query) sichern jeden Frage- aspekt im Kontext — ohne sie dominieren Eintraege, die in mehreren Sub-Queries mittelgut matchen (q-113: 5 KV-Urlaubs-SS schlugen lb-url-05 (Rang 6) und ris-url-01 (>Top-12) ueber beide Sub-Queries). **Scope-Filter** je Sub-Query: Planer markiert "gesetz" (nur Lexis/WIKU/ RIS-Chunks) bzw. "kv" (nur Branchen-KV) — q-113 geheilt (ris-url-01/ lb-url-05 + KV-Ss im selben Kontext). Scope-Fallback: gefilterte Suche leer -> unscoped. Temporal-Intent: stand_year je Sub-Query, kv-Eintraege im Geltungsjahr erhalten temporal_boost (default 0 — FTS-Tag-Signal `jahr-YYYY` reichte in der Messung). Grounding unveraendert: ein Kontext aus der Union, Post-Validierung ueber die gesamte Retrieved- Menge, Verweigerungspflicht unveraendert. Eval (46 Fragen): Zitier-Praezision 97,8 %, Verweigerung 97,8 % (Gate > 94,3 % erfuellt), erwartete Quelle 90,2 %, Latenz mean 33,5 s. Temporal-Fragen laufen sogar ohne Planer gut (Jahr-Toknen matchen `jahr-YYYY`-Tags). Neue komplexe Fragen im Goldset: q-110 (Temporal 2023, nach Korrektur — eine 2024er-Friseur-Lohnordnung existiert im Korpus NICHT: kv-kvt-161 ist der Mantelvertrag ohne Lohntabelle; korrekt verweigert), q-111 (Temporal 2025), q-112 (Abfertigung Vergleich), q-113 (Gesetz+KV Multi-Source). Tests 50 -> 57. Offen: q-029 Survey (Stufe-2-Hebel: Map-Reduce), q-113 Planer- Scope flaky (1/2 Laeufe ohne gesetz-Marker). - **D13 (Antworttyp-Routing / Stufe 2, 2026-09-15):** (a) Planer liefert jetzt `type: survey|specific`; Survey-Fragen („Welche Neuerungen …“) laufen ueber Map-Reduce: breiteres Retrieval (`survey_blocks`=16, PV_SURVEY_BLOCKS), ein Map-Call destilliert JE Block als Stichpunkte mit seiner KB-ID (MAP_SYSTEM_PROMPT), ein Reduce-Call synthetisiert daraus die Antwort mit dem normalen Grounding-Prompt; Zitier- Validierung weiterhin strikt ueber die Retrieved-Union, leerer Map-Output -> Fallback Einzelantwort. (b) Systemprompt-Regel 9: bei wesentlicher Kontextabhaengigkeit (Branche, Bundesland, Zeitraum) belegte allgemeine Aussage + EINE Rueckfrage statt Verweigerung (API-first; Odoo-Chat kann die Rueckfrage als Follow-up nutzen). Ergebnis: **q-029 geheilt** (vorher jahrelange Fehlverweigerung; jetzt Teilantwort mit 5 belegten Heften, 95 s — Map-Reduce-Latenz nur bei Survey-Fragen), **q-015** antwortet mit expliziter KV-Abhaengigkeit + Rueckfrage statt Branchen-Noise. Eval (46 Fragen): Zitier-Praezision 97,8 %, Verweigerung 97,8 % (Gate erfuellt), erwartete Quelle 90,2 %, Latenz mean 34,2 s. Tests 57 -> 59. Report `data/eval-qwen38-stage2.json`. Verbleibend: q-024 transiente Flakiness (Think-Ghost-Verdacht, 2/3 Laeufe sauber). - **D14 (Output-Budget + length-Retry, 2026-09-15):** q-024-Flakiness Ursache gefunden — NICHT Thinking (Hypothese widerlegt: thinking-Feld leer, keine Tags), sondern `num_predict=1024`: lange belegte Antworten wurden bei `done_reason=length` abgschnitten (q-024: 3/3 Sondenlaeufe) → unvollstaendige Zitationen → CITE_RE matcht partielle IDs → Verletzung → Regen-Eskalation → UNCERTAIN. Fix: (a) num_predict 1024 → 2048; (b) `OllamaClient.chat_full()` liefert (content, done_reason); (c) `chat_with_length_retry`: ein technischer Retry mit 2× num_predict bei length (zaehlt nicht als Regel-Regenerierung, laeuft auch im Map- und im Regenerierungspfad). **Voll-Eval: Zitier-Praezision 100 %, Verweigerung korrekt 100 %** (46/46 — alle M3-Gates erfuellt, erstmals ueber Basislinie), erwartete Quelle 92,7 %, Latenz mean 39,6 s / p95 78 s (+5 s: vollstaendige statt abgeschnittener Antworten). q-024: 4/4 stabil. Tests 59 → 60. Report `data/eval-qwen38-lengthfix.json`. - **D19 (API-first-Vertrag, 2026-09-16):** `/v1/ask`, `/v1/health` und `/v1/reindex` sind die stabilen Endpunkte; alte Pfade bleiben deprecated. Der Ask-Response enthält Status, Quellen, belegte Konflikte, extrahierte Rückfrage, Planungs- und Grounding-Metadaten sowie eine Request-ID. Optionaler Bearer-Schutz über `PV_API_KEY`; Reindex kann mit `PV_ADMIN_API_KEY` getrennt werden. Leere Keys sind nur lokaler Entwicklungsmodus. Health und öffentliche Fehler nennen keine internen Hosts/Exceptions. v1 ist zustandslos, `knowledge_base_only`, lehnt unbekannte JSON-Felder ab und akzeptiert keine Payroll-/Mitarbeiterobjekte. Odoo-Lohndaten erfordern später einen separaten tenant-autorisierten Vertrag mit Datensatzregeln, Datenminimierung, Audit und Cross-Tenant-Tests. Vertrag: `docs/API.md`. Der gemeinsame SQLite-Retriever ist durch `check_same_thread=False` plus `RLock` worker-thread-sicher. Neue Nicht-lokaler CLI-Bind ohne `PV_API_KEY` wird fail-closed abgelehnt. Neue API-/Concurrency-Tests erhöhen die Offline-Suite von 75 auf **82 grüne Tests**. - **D20 (Test-Frontend, 2026-09-16):** FastAPI liefert unter `/` ein dependency-freies, responsives Frontend aus `web/` aus. Es nutzt same-origin `/v1/ask` und `/v1/health`, zeigt Status, Quellen, Konflikte, Rückfragen, Suchplan und Grounding-Metadaten und hält den Verlauf nur im DOM. Ein optional gemerkter Service-Key liegt ausschließlich im `sessionStorage` des Tabs; kein Secret wird in die Assets injiziert. Modelltext wird ohne `innerHTML` gerendert. CSP gilt gezielt für UI/Assets, damit `/docs` nutzbar bleibt; globale Header setzen `nosniff`, `DENY` und `no-referrer`. Betrieb auf dem Tailscale-Interface: `python -m agent.cli serve --host 100.103.83.12` mit gesetztem `PV_API_KEY`; UI auf Port 8080. Tests 82 → 84. - **D21 (Docker-Compose-Deployment, 2026-09-16):** `compose.yaml` baut einen gemeinsamen UI-/API-Container, bindet Port 8080 nur an `100.103.83.12` und trat zunächst dem externen Netz `ollama-default` bei; D22 korrigiert den tatsächlichen Namen auf `ollama_default`. Ollama wird nicht dupliziert; `OLLAMA_URL` ist konfigurierbar (Beispiel `http://ollama:11434`, Alias auf Zielhost noch zu verifizieren). `data/` ist schreibbar, `wissensbasis/` read-only; Root-FS read-only, Capabilities entfernt, no-new-privileges, unprivilegierter `PUID:PGID`, verpflichtender `PV_API_KEY`. `.dockerignore` hält `.env`, Index, Layer-1-Korpora und KB aus dem Image; Runtime-Image nutzt `requirements-runtime.txt`. Compose `config --quiet`, Image-Build und read-only Import-/Asset-Smoke-Test bestanden. Deployment: `docs/DOCKER.md`. Statische Deploymenttests erhöhen die Suite von 84 auf **87 Tests**. - **D22 (Bootstrap, Audit und Bewertungen, 2026-09-16):** Das Container-CMD führt vor FastAPI `agent.bootstrap` aus. Ein Index gilt nur bei vorhandenen Chunks und vollständiger Vektorabdeckung für `PV_EMBED_MODEL` als bereit; andernfalls wird er im externen Netz `ollama_default` automatisch aufgebaut. Embedding-Fehler stoppen den Container statt BM25-only in Produktion zu starten. `agent.audit` protokolliert Interaktionen und Bewertungen in `data/audit.db` mit konfigurierbarer 30-Tage-Aufbewahrung und optionalen `AUDIT`-JSON-Zeilen nach stdout. `PV_AUDIT_LOG_CONTENT=false` entfernt alle Freitexte inklusive indirekter Inhalte in Suchplan/Quellen/Konflikten. Die authentisierte API `/v1/ratings` speichert `up|down` plus optionalen Kommentar; das Frontend zeigt die Funktion nur bei aktiviertem Audit. Keys/Header werden nie geloggt. Docker-JSON-Logs rotieren bei 50 MB × 5. Letzte Einträge: `python -m agent.cli audit --limit 20`. Offline-Suite **93 Tests**; Compose- Validierung, Image-Rebuild und unprivilegierter read-only Bootstrap-Smoke-Test gegen den bestehenden vollständigen Index bestanden. - **D23 (Antwortkommentare, 2026-09-16):** Zusätzlich zu `up|down` besitzt jede protokollierte Antwort eine immer sichtbare Kommentarbox. Der authentisierte Endpunkt `/v1/comments` speichert mehrere, jeweils datierte Kommentare pro Request-ID in der separaten Audit-Tabelle `comments`; leere Texte werden abgewiesen, Maximallänge 2.000 Zeichen. `agent.cli audit` gibt Kommentare gemeinsam mit Interaktion und Bewertung aus. Bei deaktivierter Inhaltsprotokollierung bleiben Kommentarereignis und ID sichtbar, der Text wird weder in SQLite noch stdout gespeichert. - **D24 (Kostenaufstellung + think-Fix, 2026-09-16):** (a) `is_decision_support` matcht auf `normalize_text(question)`; die Regex deckt neben NFKD-Folding (gunstig/losung) auch ue-Schreibweisen (guenstigste loesung) ab — vorher liefen solche Fragen in den generischen LLM-Planer und wurden verweigert. (b) Neue Trigger: "einmalig ... auszah", "bar auszah", "wieviel/wie viel ... kostet", "kostet mich", "mitarbeiterpra(e)m". (c) COST_INTENT: deterministischer Plan ergänzt `cost_tax` (einmalige Bezüge, Jahressechstel 6 %, 620 — trifft lb-son-04) und `cost_lnk` (Beitragssätze DN + Dienstgeberanteil, DB/DZ, KommSt — trifft lb-sva-06, lb-lnk-02/07); bei Kostennnabsicht fällt Zukunftssicherung weg (Slots), Cap 4 Queries. (d) Regel 11: konkrete Eckdaten → Arbeitgeberkosten Schritt für Schritt aus belegten Sätzen rechnen, Annahmen nennen, Rückfrage nur bei wesentlichem Fehlen. (e) generate.chat(): OllamaError bei think=true (leerer content, Antwort nur im thinking-Feld) → einmaliger Retry ohne thinking. (f) Goldset 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: Q1 (günstigste) verified, 1 Regen; Q2 (Kosten) verified mit AG-SV-Rechnung; PV_THINK=true verified (281 s). - **Bake-off-Protokoll** (Skill): Modellwechsel nur über dokumentierten Goldset-Vergleich; Kriterium: Zitier-Präzision > Verweigerungs- korrektheit > Latenz. ## Files that matter right now - `planung.md` — Plan + Entscheidungspunkte (Abschnitt 12) + Stand. - `.agents/skills/pv-rag-agent/SKILL.md` — verbindliche Regeln. - `agent/README.md` — Betrieb, Konfiguration, Host-Schritte. - `.oddo-module/` (Symlink) — Odoo-19-Addon-Bestand; `planung.md` Abschnitt 14 — M4/D25-Planung; `l10n_at_hr_payroll_private/models/kollektivvertrag.py` — KV-Katalog mit `library_variant_id` (Brücke zu KB). - `docs/API.md` — v1-Vertrag, Authentisierung, Privacy-Grenze und Odoo-Aufruf. - `agent/api.py` — versionierte Service-Oberfläche, UI-Auslieferung und Sicherheitsgrenzen. - `web/` — dependency-freies Test-Frontend. - `compose.yaml`, `Dockerfile`, `.env.example`, `.dockerignore` — Zielhost-Stack und automatischer Bootstrap. - `agent/bootstrap.py`, `agent/audit.py` — Indexbereitschaft, Q&A-Log und Bewertungen. - `docs/DOCKER.md` — Deployment, Indexaufbau und Smoke-Tests. - `agent/eval/goldset.yaml` — Goldset (IDs gegen kb.json verifiziert). - `agent/generate.py` — Grounding-Kern (Prompt, Post-Validierung). - `wissensbasis/README.md` — Layer-2-Schema + Rechtsprechungs-Provenance. - `tools/ingest_sources.py` / `tools/catalogs/rj_catalog.json` — rj-Intake und gefrorene IDs. - `agent/eval/goldset.yaml` — q-120–123; als Nächstes Antwortmodus-Eval.