Files
LehrerApp/ai-backend/README.md
adminandClaude Sonnet 5 7b883555bf feat: Chemikalien-Recherche über lokale RiSU-Stoffliste (kein KI-Aufruf mehr)
Ersetzt den ursprünglichen KI-Websuche-Ansatz (GESTIS scheiterte sowohl über
Websuche als auch über direkten API-Zugriff an fehlenden schulspezifischen
Tätigkeitsbeschränkungen) durch einen reinen lokalen Datenbank-Lookup gegen die
offizielle RiSU-Stoffliste (1764 Stoffe inkl. Tätigkeitsbeschränkungs-Codes,
Legende zur Abfragezeit übersetzt statt in die Datenbank gebacken). Keine
KI-Kosten, keine Drittanbieter-Abhängigkeit mehr für diese Funktion.

HazardSubstance.AgeRestriction zu ActivityRestriction umbenannt, da die
Tätigkeitsbeschränkung mehr abdeckt als reine Altersgrenzen. Details und
verworfene Zwischenstände in TODO.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 23:43:56 +02:00

228 lines
14 KiB
Markdown
Raw Permalink 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.
# KI-Backend (TODO 4.5.9)
Kleines PHP-Zwischenelement, das die KI-gestützte Planungsunterstützung des Desktop-Clients
absichert: der LLM-API-Key liegt nur hier auf dem Server, der Desktop-Client bekommt nur ein
eigenes Bearer-Token gegen dieses Backend und ein pro Nutzer geführtes Guthaben. Deployment
macht der Nutzer selbst — dieses README beschreibt die nötigen Schritte.
## Voraussetzungen
- PHP 8.1 oder neuer (nutzt `never`-Rückgabetypen, `match`, First-Class-Callable-Syntax nicht,
aber typisierte Properties/Enums nicht zwingend — 8.1 ist die sichere Untergrenze).
- PHP-Erweiterungen: `pdo_mysql`, `curl`, `json` (bei den meisten Hosting-Paketen bereits dabei).
- Eine MySQL- oder MariaDB-Datenbank.
- Ein API-Key für Anthropic (`https://console.anthropic.com`).
## Einrichtung
1. Datenbank anlegen und `schema.sql` importieren:
```bash
mysql -u <user> -p <datenbankname> < schema.sql
```
2. `config.example.php` nach `config.php` kopieren und ausfüllen (DB-Zugang, Anthropic-API-Key,
ggf. die Preistabelle gegen die aktuelle Anthropic-Preisseite prüfen — Preise ändern sich).
`config.php` ist in `.gitignore` und darf nie committet werden.
3. Falls du die Web-Variante aus Schritt 5 nutzen willst (kein PHP-CLI/SSH auf dem Hosting):
`setup_secret` in `config.php` auf einen langen Zufallswert setzen (z.B. per
`openssl rand -hex 32` erzeugt, oder ein beliebiger Passwortgenerator).
4. Den kompletten `ai-backend/`-Ordner auf den PHP-Server hochladen. **Empfehlung:** `config.php`,
`db.php`, `schema.sql`, `providers/` und `scripts/` außerhalb des öffentlichen Webroots ablegen,
falls das Hosting das erlaubt (Pfade in den `require`-Aufrufen entsprechend anpassen) — robuster
als sich allein auf die mitgelieferte `.htaccess` zu verlassen, die nur bei Apache mit
aktiviertem `mod_rewrite`/erlaubten `.htaccess`-Overrides greift.
5. Ersten Nutzer anlegen (weitere Lehrer später genauso) — zwei Varianten, je nachdem was das
Hosting hergibt:
**Mit PHP-CLI/SSH-Zugriff** (`scripts/create-user.php`):
```bash
php scripts/create-user.php sebastian "einStarkesPasswort" 10.00
```
Der dritte Parameter ist das Startguthaben in USD, optional (Standard 0).
**Ohne CLI/SSH** (übliches Shared Hosting, nur FTP/Webspace) — der Server läuft nach Schritt 4
bereits, also einmalig von deinem eigenen Rechner aus aufrufen:
```bash
curl -X POST https://DEINE-DOMAIN/setup-user.php \
-d '{"secret":"<setup_secret aus config.php>","username":"sebastian","password":"einStarkesPasswort","balanceUsd":10.00}'
```
**Direkt danach `setup-user.php` per FTP wieder vom Server löschen** — sonst bleibt ein
Endpunkt erreichbar, der (mit dem Geheimnis) beliebige Nutzer anlegen kann. Ein erneuter
Aufruf mit demselben Benutzernamen aktualisiert Passwort/Guthaben, falls z.B. das Guthaben
beim ersten Versuch vergessen wurde — dafür braucht es die Datei aber kein zweites Mal auf
dem Server, einfach vor dem Löschen alle nötigen Nutzer in einer Sitzung anlegen.
6. In `LehrerApp.Desktop/AppBootstrapper.cs` die Konstante `AiBackendUrl` auf die tatsächlich
deployte Domain setzen und die App neu bauen.
7. In der App unter Einstellungen → KI-Unterstützung aktivieren und mit dem angelegten Nutzer
anmelden.
## Update für bereits deployte Installationen (Prompt-Caching-Nachtrag)
Falls `ai-backend/` schon einmal deployt wurde (schema.sql lief bereits, `config.php` existiert
schon): zwei Schritte nötig, damit Abrechnung und `plan.php` mit dem neuen Prompt Caching
funktionieren.
1. Migration einmalig einspielen (ergänzt zwei neue, nicht-destruktive Spalten):
```bash
mysql -u <user> -p <datenbankname> < migrations/2026-08-add-cache-tokens.sql
```
2. In der bestehenden `config.php` bei jedem Preistabellen-Eintrag `cache_write`/`cache_read`
ergänzen (siehe `config.example.php` für aktuelle Richtwerte) — ohne diese Ergänzung wird der
Cache-Anteil einfach mit 0 USD abgerechnet (kein Fehler, aber auch keine Kostenersparnis).
Danach alle geänderten Dateien (`plan.php`, `db.php`, `providers/`, `.htaccess`, falls noch nicht
aktuell) erneut hochladen.
## Update für bereits deployte Installationen ("Schattenfeld"-Nachtrag, `explain.php`)
Kein neues DB-Schema nötig (nutzt dieselben `users`/`tokens`/`transactions`-Tabellen wie `plan.php`).
Einfach die neue Datei `explain.php` sowie die aktualisierten `db.php` und `plan.php` hochladen
(die Abrechnungslogik wurde aus `plan.php` in eine gemeinsame Funktion `ai_backend_call_and_charge`
in `db.php` verschoben, damit `explain.php` sie mitverwenden kann, ohne sie zu duplizieren).
## Update für bereits deployte Installationen (Gefährdungsbeurteilungs-Entwurf, `gbu.php`)
Kein neues DB-Schema nötig (nutzt dieselben `users`/`tokens`/`transactions`-Tabellen wie
`plan.php`/`explain.php`). Einfach die neue Datei `gbu.php` hochladen.
## Update für bereits deployte Installationen (Chemikalien-Recherche, `substance.php`)
Neues Feature — reiner lokaler Datenbank-Lookup, **kein KI-Aufruf, keine Kosten, keine
Drittanbieter-Abhängigkeit** (Nutzerwunsch: "wir brauchen die KI gar nicht mehr im Backend").
Vorgeschichte in Kurzform (Details siehe TODO.md, Nachtrag zu 4.2): zunächst wurde versucht, die
Daten per KI-Websuche bei GESTIS zu recherchieren — das scheiterte, weil `gestis.dguv.de` eine
reine JavaScript-Anwendung ohne durchsuchbaren/abrufbaren Inhalt ist. Ein direkter Abruf über
GESTIS' eigene (inoffizielle) API funktionierte zwar technisch, hatte aber keine strukturierte,
schulspezifische Tätigkeitsbeschränkung (Freigabe/Verbot/Altersgrenzen) — genau die für den
Unterricht wichtigste Angabe. Der Nutzer stellte stattdessen die offizielle **RiSU-Stoffliste**
("Richtlinie Sicheres Experimentieren im Unterricht") als PDF bereit — ein für den Schuleinsatz
zusammengestelltes Verzeichnis mit exakt den benötigten Spalten (Name, CAS-Nr., GHS-Piktogramme,
Signalwort, H-/P-Sätze UND Tätigkeitsbeschränkung), aus dem `data/stoffliste.json` (~1800 Stoffe)
erzeugt wurde. `substance.php` ist damit auf einen reinen, kostenlosen Datenbank-Lookup reduziert.
**Deployment:** neue Dateien `substance.php` und `stoffliste.php` hochladen, sowie
`data/stoffliste.json` (~760 KB, nur lesend benötigt). Kein Migrations-Schritt, keine
`config.php`-Änderung nötig — der Endpunkt prüft weiterhin das Bearer-Token wie jeder andere
(`ai_backend_authenticate`), braucht aber keinerlei KI-/Abrechnungs-Infrastruktur mehr.
**Legende der Tätigkeitsbeschränkungs-Codes** (steht in keiner Form im PDF selbst, vom Nutzer
bestätigt — Übersetzung passiert zur Abfragezeit in `stoffliste.php`
(`STOFFLISTE_RESTRICTION_LEGEND`), bewusst NICHT in `data/stoffliste.json` gespeichert, damit eine
künftige Korrektur nur eine Codeänderung statt einer neuen Datenbank-Generierung braucht — genau
das war hier schon einmal nötig, die erste Fassung dieser Legende war falsch):
| Code | Bedeutung |
| --- | --- |
| `+` | Generelle Erlaubnis |
| `X` | Generelles Tätigkeitsverbot an Schulen |
| `L+` | Tätigkeitsverbot für Lehrkräfte, Ausnahme siehe RiSU (I 3.5) |
| `S` | Tätigkeitsverbot für Schülerinnen und Schüler |
| `S4K` | Tätigkeitsverbot für Schülerinnen und Schüler bis einschließlich Jahrgangsstufe 4 |
| `S9K` | Tätigkeitsverbot für Schülerinnen und Schüler bis einschließlich Jahrgangsstufe 9 |
| `W` | Tätigkeitsverbot für werdende oder stillende Mütter |
| `ESP` | Gesonderte Ersatzstoffprüfung notwendig |
**Bekannte Lücken:**
- Diese Stoffliste enthält keine Entsorgungshinweise (anders als der frühere GESTIS-Ansatz) — das
`disposal`-Feld im Wizard bleibt bei einem Datenbank-Treffer leer und muss weiterhin von Hand
ausgefüllt werden.
- Für ca. 40 der ca. 1800 Stoffe im PDF (~2 %) konnte die Tabellenerkennung die Spalten nicht
zuverlässig auseinanderhalten (ungewöhnlich lange Zellinhalte lassen `pdfplumber`s
Spaltenerkennung für einzelne Zeilen kollabieren) — betroffene Zeilen wurden NICHT mit
geratenen/möglicherweise falschen Werten aufgenommen, sondern bewusst ausgelassen (ein falscher
Stoffname bei sonst korrekten Gefahrendaten wäre gefährlicher als ein fehlender Eintrag). Bei
Bedarf lässt sich die Liste der fehlenden Stoffe aus dem PDF neu ermitteln.
**Aktualisieren der Datenbank:** wenn die RiSU-Stoffliste künftig überarbeitet wird, das PDF neu
herunterladen und `data/stoffliste.json` neu erzeugen (Parsing-Logik ist nicht Teil des Repos,
da einmalig genutzt — bei Bedarf erneut über `pdfplumber` extrahieren, Spaltenreihenfolge siehe
`stoffliste.php`-Kommentar). Die Tätigkeitsbeschränkungs-**Codes** werden weiterhin mit
gespeichert, nur ihre Übersetzung lebt separat in `STOFFLISTE_RESTRICTION_LEGEND`.
## Prompt Caching
Der Systemprompt in `plan.php` ist vollständig statisch (identisch bei jeder Anfrage, jedes
Nutzers). `AnthropicProvider.php` markiert ihn deshalb als cacheable — wiederholte Anfragen
innerhalb der Anthropic-Cache-TTL (Standard: 5 Minuten) zahlen für diesen Anteil nur den stark
reduzierten "Cache-Read"-Preis statt des vollen Input-Preises; der erste Aufruf nach TTL-Ablauf
zahlt einen kleinen Aufpreis fürs Neuschreiben des Caches. Das passiert automatisch, ohne
Zutun — nichts an der Bedienung der App ändert sich dadurch. Beobachtbar ist der Effekt in der
`transactions`-Tabelle (`cache_creation_input_tokens` vs. `cache_read_input_tokens`).
## Smoke-Test ohne echten API-Key
`plan.php` liest die Umgebungsvariable `AI_BACKEND_FAKE_PROVIDER` — bei `1` wird statt eines
echten Anthropic-Aufrufs `providers/FakeProvider.php` verwendet (liefert eine feste,
schema-valide Test-Antwort). Damit lassen sich Login, Guthabenprüfung und die 402-Pfade lokal
prüfen, ohne echte Kosten zu verursachen:
```bash
AI_BACKEND_FAKE_PROVIDER=1 php -S localhost:8000 -t .
```
Dann z.B.:
```bash
curl -X POST http://localhost:8000/login.php \
-d '{"username":"sebastian","password":"einStarkesPasswort"}'
curl http://localhost:8000/status.php -H "Authorization: Bearer <token aus login.php>"
```
**Wichtig:** `AI_BACKEND_FAKE_PROVIDER` niemals auf dem produktiven Server setzen — sonst bekommt
die App nur die feste Test-Antwort statt echter KI-Vorschläge.
## Fehlerbehebung: "Anmeldung abgelaufen" direkt nach dem Anmelden
Login funktioniert, aber jede Anfrage danach (Guthaben, KI-Anfrage) meldet sofort eine
abgelaufene/ungültige Anmeldung? Typische Ursache: viele Apache/PHP-FPM-Hosting-Setups reichen
den `Authorization`-Header standardmäßig gar nicht erst an PHP durch — `login.php` braucht ihn
nicht, aber `status.php`/`plan.php` schon, und der kommt dort leer an.
Dagegen sind bereits zwei Absicherungen eingebaut:
- `.htaccess` erzwingt die Weiterleitung des Headers (`RewriteRule .* - [E=HTTP_AUTHORIZATION:%1]`).
- `db.php` liest zusätzlich `REDIRECT_HTTP_AUTHORIZATION` und `getallheaders()` als Fallback.
Falls es trotzdem weiter auftritt: `.htaccess`-Overrides sind auf manchem Hosting serverseitig
deaktiviert (`AllowOverride None`), oder es läuft nginx statt Apache (dort gilt `.htaccess`
grundsätzlich nicht) — dann hilft nur eine serverseitige Konfiguration durch den Hoster/Support
(z.B. bei nginx ein `fastcgi_param HTTP_AUTHORIZATION $http_authorization;`).
## Was hiermit NICHT geprüft ist
- Ob Anthropic zuverlässig valides JSON im erwarteten Schema liefert (reine Prompt-Qualitätsfrage,
nur mit dem echten API-Key zu beurteilen).
- Ob die berechneten Kosten exakt mit der tatsächlichen Anthropic-Abrechnung übereinstimmen.
- TLS/`.htaccess`-Wirksamkeit und PHP-Version/Erweiterungen auf dem tatsächlichen Hosting.
- Die komplette Kette Desktop → dieses Backend → Anthropic unter echten Netzwerkbedingungen.
- Ob Prompt Caching tatsächlich greift (`cache_read_input_tokens` > 0 bei einer zweiten Anfrage
innerhalb von 5 Minuten) — der `FakeProvider` simuliert kein Caching, das lässt sich nur gegen
die echte Anthropic-API beobachten (z.B. per Blick in die `transactions`-Tabelle).
- **`substance.php`/`stoffliste.php` (Chemikalien-Recherche):** braucht keinen echten API-Key und
keine Netzwerkverbindung mehr, ist also grundsätzlich einfacher zu verifizieren als der Rest
dieser Liste — trotzdem NICHT in einer echten PHP-Umgebung gelaufen (kein PHP auf der
Implementierungs-Maschine verfügbar). Die Lookup-/Normalisierungslogik in `stoffliste.php` sowie
der PDF→JSON-Umwandlungsschritt für `data/stoffliste.json` wurden in Python nachgebaut und
gegen Stichproben verifiziert (u.a. Natrium, Kaliumdichromat, Zinn-Verbindungen), nicht aber der
tatsächlich ausgeführte PHP-Code selbst — vor dem produktiven Einsatz einmal mit ein paar
bekannten Stoffnamen durchtesten.
**Nachtrag (Nutzer-Testlauf):** eine harte `allowed_domains`-Einschränkung auf GESTIS lieferte
für tatsächlich vorhandene Stoffe trotzdem "nicht sicher auffindbar" — vermutlich liefert die
Websuche für GESTIS' dynamisch aufgebaute Seiten zu wenige brauchbar indexierte Treffer.
`allowed_domains` wurde daraufhin entfernt; die Quellenrangfolge (GESTIS zuerst, sonst andere
anerkannte Quellen) steht jetzt nur noch im Systemprompt, mit Pflichtangabe der tatsächlich
genutzten Quelle im `source`-Feld. Ob GESTIS damit in der Praxis öfter tatsächlich als Quelle
gefunden wird, ist weiterhin nur mit echten Testläufen zu beurteilen.
## Guthaben aufladen
Für den aktuellen Umfang (ein bis wenige Nutzer) reicht ein manueller SQL-Befehl:
```sql
UPDATE users SET balance_usd = balance_usd + 10.00 WHERE username = 'sebastian';
INSERT INTO transactions (user_id, type, cost_usd, balance_after)
SELECT id, 'topup', -10.00, balance_usd FROM users WHERE username = 'sebastian';
```
(Kein Admin-UI in dieser Ausbaustufe — bei Bedarf später ergänzbar.)