KI-gestützte Planungsunterstützung (4.5.9) + Kompetenzkatalog-Import (8.1.2)

KI-Unterstützung: neuer Einstellungen-Tab (Anmeldung, Guthaben) und Button im
Planungs-Tab, der Einheiten+Stunden als JSON an ein neues PHP-Backend (ai-backend/)
sendet und die Antwort als prüfbare Vorschlagsliste zurückbringt. Provider-Aufruf,
Guthabenverwaltung und Abrechnung nach echten Token-Kosten laufen serverseitig, der
Desktop-Client sieht nie einen LLM-API-Key. Zentral abgesichert: eine von der KI
zurückgegebene Stunden-Id, die zu keiner echten Lesson der Einheit passt, wird nie
als Update übernommen, sondern immer als neue Stunde behandelt.

Kompetenzkatalog-Import (8.1.2): JSON-Export/Import für Kompetenzkataloge.
This commit is contained in:
2026-08-16 01:31:14 +02:00
parent 2b4fda7bb3
commit 8495e1b8d0
40 changed files with 2326 additions and 51 deletions
@@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/ProviderInterface.php';
/** Ruft die Anthropic Messages API direkt per curl auf — bewusst ohne SDK-Abhängigkeit. */
class AnthropicProvider implements ProviderInterface
{
public function __construct(private string $apiKey, private string $model) {}
public function sendMessage(string $systemPrompt, string $userContent, int $maxTokens): array
{
$ch = curl_init('https://api.anthropic.com/v1/messages');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'content-type: application/json',
'x-api-key: ' . $this->apiKey,
'anthropic-version: 2023-06-01',
],
CURLOPT_POSTFIELDS => json_encode([
'model' => $this->model,
'max_tokens' => $maxTokens,
'system' => $systemPrompt,
'messages' => [['role' => 'user', 'content' => $userContent]],
]),
CURLOPT_TIMEOUT => 90,
]);
$raw = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlError = curl_error($ch);
curl_close($ch);
if ($raw === false) {
throw new RuntimeException("Anthropic-Anfrage fehlgeschlagen: $curlError");
}
if ($httpCode >= 400) {
throw new RuntimeException("Anthropic-API-Fehler (HTTP $httpCode): " . substr((string) $raw, 0, 500));
}
$data = json_decode((string) $raw, true);
if (!is_array($data)) {
throw new RuntimeException('Anthropic-Antwort konnte nicht als JSON gelesen werden.');
}
if (($data['stop_reason'] ?? null) === 'refusal') {
throw new RuntimeException('Die KI hat die Anfrage abgelehnt.');
}
$text = '';
foreach (($data['content'] ?? []) as $block) {
if (($block['type'] ?? null) === 'text') {
$text .= $block['text'];
}
}
return [
'content' => $text,
'inputTokens' => (int) ($data['usage']['input_tokens'] ?? 0),
'outputTokens' => (int) ($data['usage']['output_tokens'] ?? 0),
];
}
}
+36
View File
@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/ProviderInterface.php';
/**
* Für lokale Smoke-Tests ohne echten API-Key (siehe README.md) — liefert eine feste,
* valide AiPlanningResponse-JSON zurück statt einen echten LLM-Aufruf zu machen. Niemals als
* Standard-Provider in config.php eintragen, nur über eine explizite lokale Umgebungsvariable
* (siehe plan.php) für Entwicklungszwecke aktivieren.
*/
class FakeProvider implements ProviderInterface
{
public function sendMessage(string $systemPrompt, string $userContent, int $maxTokens): array
{
return [
'content' => json_encode([
'lessons' => [
[
'id' => null,
'date' => null,
'lessonNumber' => null,
'topic' => 'Fake-Vorschlag zum Testen',
'startTime' => null,
'phases' => [],
'homework' => null,
'reflection' => null,
],
],
'summary' => 'Antwort des FakeProvider (kein echter KI-Aufruf).',
]),
'inputTokens' => 42,
'outputTokens' => 17,
];
}
}
@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
interface ProviderInterface
{
/**
* @return array{content: string, inputTokens: int, outputTokens: int}
*/
public function sendMessage(string $systemPrompt, string $userContent, int $maxTokens): array;
}