Files
admin 4b4a746e17
CI / build-and-test (push) Canceled after 0s
Update im KI-Backend für pdf-import
2026-08-31 23:20:19 +02:00
..
2026-08-31 23:20:19 +02:00
2026-08-31 23:20:19 +02:00
2026-08-31 23:20:19 +02:00

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:

    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):

    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:

    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):
    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 pdfplumbers 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:

AI_BACKEND_FAKE_PROVIDER=1 php -S localhost:8000 -t .

Dann z.B.:

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:

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.)