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>
228 lines
14 KiB
Markdown
228 lines
14 KiB
Markdown
# 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.)
|