feat(agent): harden API-first service

This commit is contained in:
2026-09-16 21:12:47 +02:00
parent 3f7e033097
commit aa0bee340f
12 changed files with 839 additions and 162 deletions
+168
View File
@@ -0,0 +1,168 @@
# PV RAG Agent — HTTP API v1
## Zweck und Grenze
Die API ist der eigenständige Zugriffspunkt für CLI-, Web- und spätere
Odoo-Clients. Version 1 ist ein **zustandsloser Wissensdienst**:
- verarbeitet ausschließlich `question`, optionale Retrieval-Tiefe `top_k`
und den festen Modus `knowledge`;
- antwortet ausschließlich aus der Layer-2-Wissensbasis;
- nimmt keine Mandanten-, Mitarbeiter- oder Abrechnungsobjekte entgegen;
- speichert keinen Gesprächsverlauf;
- nutzt keine Websuche und keine externen Tools.
Personenbezogene Lohndaten dürfen nicht in den Fragetext eingebettet werden.
Die spätere Odoo-Lohndatenintegration bekommt einen getrennten Vertrag mit
Mandantenbindung, Rollenprüfung, Datenminimierung, Auditierung und definierter
Aufbewahrung. Ein allgemeiner API-Key allein reicht dafür nicht aus.
## Endpunkte
| Methode | Pfad | Auth | Zweck |
|---|---|---|---|
| `POST` | `/v1/ask` | Service-Key | belegte Wissensantwort |
| `GET` | `/v1/health` | öffentlich | Readiness ohne interne Hostdetails |
| `POST` | `/v1/reindex` | Admin-Key | Index nach KB-Änderung neu aufbauen |
Die bisherigen Pfade `/ask`, `/health` und `/reindex` bleiben vorläufig als
`deprecated` Kompatibilitätsrouten erhalten und liefern denselben Vertrag.
## Authentisierung
```bash
export PV_API_KEY='service-key-aus-secret-store'
export PV_ADMIN_API_KEY='separater-admin-key-aus-secret-store'
python -m agent.cli serve --host 127.0.0.1
```
Requests senden den Key als Bearer-Token:
```http
Authorization: Bearer <key>
```
- Ist `PV_ADMIN_API_KEY` leer, verwendet `/v1/reindex` den normalen
`PV_API_KEY`.
- Sind beide Variablen leer, läuft der Dienst zur lokalen Entwicklung ohne
Authentisierung. Der CLI-Start lehnt in diesem Zustand nicht-lokale Bind-
Adressen wie `0.0.0.0` ab. Ein exponiertes Deployment muss mindestens
`PV_API_KEY` setzen.
- Keys gehören in einen Secret Store bzw. eine nicht versionierte
Umgebungsdatei; sie dürfen nicht in Odoo-Quellcode oder Git liegen.
- TLS wird am Reverse Proxy bzw. Service Mesh terminiert. Bearer-Tokens dürfen
nicht unverschlüsselt über fremde Netze übertragen werden.
## `POST /v1/ask`
Request:
```json
{
"question": "Wie hoch ist der steuerfreie Tagesgeldsatz?",
"top_k": 8,
"mode": "knowledge"
}
```
Unbekannte Felder werden mit HTTP `422` abgewiesen. Das ist insbesondere die
technische Vertragsgrenze gegen ad-hoc-Felder wie `employee_data` oder
`payroll_context`.
Response (gekürzt):
```json
{
"api_version": "v1",
"request_id": "odoo-request-123",
"status": "answered",
"question": "Wie hoch ist der steuerfreie Tagesgeldsatz?",
"answer": "... [lb-rei-09] ...",
"refused": false,
"verified": true,
"citations": ["lb-rei-09"],
"sources": [
{
"id": "lb-rei-09",
"title": "...",
"section": "Kernwerte & Fristen",
"stand": "2026-01",
"work": "..."
}
],
"conflicts": [],
"assumptions": [],
"clarification_question": null,
"alternatives": [],
"answer_type": "specific",
"planned": false,
"planned_queries": [],
"grounding": {
"data_scope": "knowledge_base_only",
"citations_verified": true,
"context_count": 8,
"regenerations": 0
},
"n_context": 8,
"model": "qwen3.8:27b",
"latency_ms": 38800,
"regenerations": 0
}
```
### Semantik
- `status=answered`: beantwortet und zitierseitig verifiziert.
- `status=refused`: Wissensbasis deckt die Frage nicht; sichere Verweigerung.
- `status=uncertain`: Regenerierung konnte die Grounding-Regeln nicht erfüllen.
- `sources` enthält nur tatsächlich zitierte Quellen, nicht sämtliche
Retrieval-Treffer.
- `conflicts` enthält mit `⚠` markierte, bereits in der belegten Antwort
vorkommende Konfliktpassagen und deren Quellen-IDs.
- `clarification_question` wird aus einer abschließenden Rückfrage übernommen.
- `assumptions` und `alternatives` sind bereits stabile Vertragsfelder, bleiben
in v1 aber leer. Der Dienst errät diese Strukturen nicht aus Freitext; ihre
spätere Befüllung benötigt einen eigenen belegbaren Generierungsvertrag.
- `grounding.data_scope=knowledge_base_only` ist in v1 unveränderlich.
Der Client darf `verified=false` nicht als normale Fachantwort darstellen.
Empfohlen ist ein sichtbarer Warnzustand ohne automatische Folgeverarbeitung.
## Request-Korrelation
Ein Client kann einen technisch neutralen Header mitsenden:
```http
X-Request-ID: odoo-request-123
```
Erlaubt sind 1128 Zeichen aus `A-Z`, `a-z`, `0-9`, `.`, `_`, `:`, `-`.
Ungültige oder fehlende IDs werden durch eine zufällige ID ersetzt. Die ID
wird im Response-Header und Response-Body zurückgegeben. Keine Namen,
Personalnummern oder sonstigen personenbezogenen Daten als Request-ID nutzen.
## Fehlerverhalten
- `401`: fehlender oder falscher Bearer-Key;
- `422`: ungültiger Request bzw. nicht erlaubte Felder;
- `503`: Index, Modellserver oder Reindex vorübergehend nicht verfügbar.
Öffentliche Fehlerantworten enthalten keine internen Hosts, Dateipfade oder
Exception-Texte. Der Server loggt technische Fehler mit `request_id`.
## Odoo-Anbindung der Wissens-API
Odoo soll `/v1/ask` serverseitig aufrufen, nicht direkt aus dem Browser:
1. Service-URL und Key verschlüsselt bzw. als Deployment-Secret verwalten;
2. Benutzerzugriff in Odoo über eine eigene Gruppe steuern;
3. nur die Frage und eine nicht personenbezogene `X-Request-ID` senden;
4. `answer`, `sources`, `conflicts` und `clarification_question` rendern;
5. `status` und `verified` zwingend auswerten;
6. keine eigene Retrieval- oder Grounding-Logik in Odoo duplizieren.
Die Payroll-Datenintegration wird nicht durch zusätzliche freie JSON-Felder an
`/v1/ask` umgesetzt. Sie benötigt mindestens: Odoo-seitige Datensatzregeln,
service-seitige Tenant-Bindung, erlaubte Datenprojektionen statt Rohobjekten,
Zweckbindung, Audit-Events, kurze Aufbewahrung und Tests gegen Cross-Tenant-
Datenabfluss.