# Benutzerhandbuch — Fundus ## 1. Quellen anlegen Fundus liest aus zwei Quellenarten: **Watchfolder** (Dateien) und **DB-View** (Read-only-Datenbank-Query). In Phase 1 gibt es dafuer noch keine Formular-Oberflaeche im Admin-Frontend — Quellen werden per SQL-Insert in die Tabelle `sources` angelegt. Die Admin-Oberflaeche (`https://fundus.c3po42.de`) zeigt bestehende Quellen unter der Karte „Quellen" nur an (Name, Art, Status). ### Watchfolder-Quelle ```sql INSERT INTO sources (name, kind, config, policy_profile, active) VALUES ('Handbuecher', 'watchfolder', '{"path": "/data/watch/handbuecher"}', 'klinik', true); ``` `config.path` ist der Ordner **innerhalb des Containers** (Volume-Mount `./data:/data` in `docker-compose.prod.yml`) bzw. unter `FUNDUS_WATCH_DIR` lokal. Der Watchfolder wird rekursiv gescannt; neue oder inhaltlich geaenderte Dateien (Content-Hash-Vergleich) werden beim naechsten Ingest-Zyklus automatisch aufgenommen, unveraenderte uebersprungen. ### DB-View-Quelle ```sql INSERT INTO sources (name, kind, config, policy_profile, active) VALUES ('MVA-Tickets', 'dbview', '{ "dsn": "postgresql+asyncpg://fundus_reader:***@mva-db:5432/mva", "query": "SELECT id, betreff, beschreibung, erstellt_am FROM tickets", "cursor_field": "id", "title": "MVA-Support-Tickets" }', 'klinik', true); ``` **Pflicht:** der User im `dsn` MUSS Read-only sein (siehe README.md, Abschnitt „Pflicht-Warnung"). `cursor_field` muss monoton steigend sein (ID, Timestamp) — Fundus liest bei jedem Zyklus nur Zeilen mit `cursor_field > last_cursor` und schreibt `last_cursor` danach zurueck in `sources.config`. Erstlauf liest die komplette Tabelle (`last_cursor` startet bei 0). ### Quelle deaktivieren ```sql UPDATE sources SET active = false WHERE name = 'Handbuecher'; ``` Deaktivierte Quellen werden im Ingestion-Zyklus uebersprungen, bleiben aber mitsamt bereits eingelesener Dokumente in der DB erhalten. ## 2. Watchfolder befuellen Dateien einfach in den konfigurierten Ordner legen (lokal unter `FUNDUS_WATCH_DIR`, auf dem VPS unter `/opt/fundus/data/watch//`, siehe `sources.config.path` je Quelle). Unterstuetzte Formate laufen ueber Docling (PDF, DOCX, HTML, ...) oder den Direktpfad fuer `.md`/`.txt`. Der naechste Ingest-Zyklus (Default alle 5 Minuten, `FUNDUS_SCAN_INTERVAL`) nimmt neue Dateien automatisch auf. Sofort auf einen Zyklus warten wollt ihr nicht? `POST /api/ingest/run` (mit Admin-JWT) stoesst einen Zyklus manuell an. Scheitert die Konvertierung oder die Anonymisierung einer Datei, landet sie in der Tabelle `quarantine` (Spalten `origin`, `reason`) statt in `documents` — die Karte „Bestand" im Admin-Frontend zeigt die Quarantaene-Anzahl. Ursache pruefen (`SELECT * FROM quarantine ORDER BY created_at DESC`), Datei korrigieren oder aus dem Watchfolder entfernen. ## 3. Policy anpassen (`policy.json`) ```json { "profile": "klinik", "entity_types": { "PERSON": "reversibel", "PHONE_NUMBER": "reversibel", "EMAIL_ADDRESS": "reversibel", "ADDRESS": "reversibel", "KVNR": "irreversibel", "IBAN": "irreversibel", "DATE_OF_BIRTH": "irreversibel", "DIAGNOSE": "irreversibel" }, "default": "irreversibel" } ``` Drei moegliche Werte je Entitaetstyp: - **`reversibel`** — Original wird Fernet-verschluesselt im Vault gespeichert, mit gueltigem `deanonymize`-JWT spaeter wieder auffloesbar. Fuer Entitaeten, bei denen ein autorisierter Nutzer den Klartext gelegentlich braucht (z.B. Ansprechpartner-Name). - **`irreversibel`** — nur ein HMAC-Lookup-Hash wird gespeichert, kein Klartext, in keiner Form. Fuer alles, was nie wieder im Klartext auftauchen darf (medizinische Diagnosen, Versichertennummern, IBAN, Geburtsdatum). - **`aus`** — Entitaetstyp wird gar nicht ersetzt (z.B. wenn ein Pattern zu viele False Positives erzeugt und der Typ fuer diese Quelle irrelevant ist). `default` greift fuer alle Entitaetstypen, die hier nicht explizit gelistet sind — bewusst konservativ auf `irreversibel` gesetzt. Nach einer Aenderung an `policy.json` muss der Fundus-Prozess neu gestartet werden (`Policy.load()` liest die Datei nur beim Ingest-Zyklus-Start neu ein); bereits anonymisierte Bestandsdokumente werden **nicht** rueckwirkend angepasst — nur neu eingelesene bzw. inhaltlich geaenderte Dokumente greifen die neue Policy. ## 4. Golden Questions pflegen (`golden_questions.yml`) ```yaml - frage: Wie wird ein Projekt auf den VPS deployt? erwartete_quelle: CLAUDE.md - frage: Welchen Port nutzt Fundus? erwartete_quelle: CLAUDE.md ``` Nach jedem Ingest-Zyklus mit mindestens einem neuen/geaenderten Dokument laeuft `run_golden_questions()`: jede Frage wird per Hybrid-Suche gestellt, und geprueft, ob `erwartete_quelle` unter den Top-5-Treffern (`origin`-Feld) ist. Ergebnis (`passed`/`total`) landet in `GOLDEN_STATE` (sichtbar unter `GET /api/health`) und wird als Audit-Eintrag `golden_run` protokolliert. Golden Questions sind ein Regressionsnetz: fuegt eine neue Kern-Quelle (Handbuch, wichtige Doku) hinzu und ergaenzt eine Frage dazu, damit ein spaeterer Chunking-/Embedding-Aenderung sofort auffaellt, wenn die Antwort nicht mehr gefunden wird. ## 5. De-Anonymisierungs-Berechtigung (JWT-Claim) Nur JWTs mit dem Claim `deanonymize: true` duerfen reversible Pseudonyme in Antworten aufgeloest bekommen. Der einzige aktuelle Weg an so ein Token: `POST /api/login` mit `admin`/`FUNDUS_ADMIN_PASSWORD` — `create_token("admin", deanonymize=True)` setzt den Claim fest. `X-Api-Key`-Requests bekommen **nie** de-anonymisierte Antworten, unabhaengig vom Endpoint — das ist im Code (`auth.check_request_auth()`) hart verdrahtet, nicht konfigurierbar. Wer weiteren Personen De-Anonymisierung erlauben will, ohne das Admin-Passwort zu teilen, braucht ein eigenes Login/Rollenmodell — in Phase 1 gibt es nur den einen Admin-Account. ## 6. Modellwechsel-Prozedur (Re-Embedding) Das Embedding-Modell (`config.EMBED_MODEL`, aktuell `nomic-ai/nomic-embed-text-v1.5`, 768 Dimensionen) ist Teil des Vektor-Spaltentyps in `chunks.embedding`. Ein Wechsel auf ein anderes Modell (andere Dimension oder einfach andere Vektorraum-Semantik) macht **alle bestehenden Embeddings unvergleichbar** mit neuen — die Hybrid-Suche wuerde sonst Aepfel mit Birnen per RRF fusionieren und stillschweigend schlechte Treffer liefern. Prozedur: 1. `config.EMBED_MODEL` (und ggf. `EMBED_DIM`) auf das neue Modell setzen. 2. Falls sich `EMBED_DIM` aendert: Schema-Migration fuer die Spalte `chunks.embedding` (neue Vector-Dimension, `ALTER TABLE` oder Neuanlage — pgvector erlaubt keine In-Place-Dimensionsaenderung). 3. **Alle** bestehenden Chunks neu embedden — nicht nur neue Dokumente. Kein automatischer Trigger dafuer in Phase 1; Vorgehen: Chunks-Tabelle leeren und alle Dokumente erneut durch `pipeline.ingest_text()`-Pfad laufen lassen (z.B. per einmaligem Re-Ingest-Skript, das `documents.markdown` liest, `chunk_markdown()` + `embed_documents()` erneut aufruft und `chunks` neu befuellt — der anonymisierte Text in `documents` bleibt dabei unveraendert, nur Chunking/Embedding wird wiederholt). 4. Danach `POST /api/ingest/run` bzw. einen normalen Zyklus abwarten und `GET /api/health` auf den Golden-Questions-Status pruefen — signifikanter Einbruch in `passed/total` ist ein Warnsignal, dass das neue Modell auf den Kern-Dokumenten schlechter performt als erwartet. 5. Downtime/Uebergangsphase einplanen: waehrend des Re-Embeddings liefert die Suche eine Mischung aus altem und neuem Vektorraum — fuer eine Kern-Wissensbasis am besten in einem Wartungsfenster fahren statt live.