diff --git a/ai-backend/db.php b/ai-backend/db.php index ce813e8..8ad81c9 100644 --- a/ai-backend/db.php +++ b/ai-backend/db.php @@ -77,6 +77,42 @@ function ai_backend_fail(int $httpStatus, string $message): never exit; } +/** + * Dekodiert eine JSON-Antwort des LLM. Modelle setzen trotz entsprechender Anweisung gelegentlich + * Markdown-Codezäune oder einen kurzen Begleittext um das eigentliche JSON. Diese rein + * syntaktischen Zusätze sollen eine ansonsten gültige Antwort nicht unbrauchbar machen. + */ +function ai_backend_decode_json_response(string $content): ?array +{ + $content = trim($content, "\xEF\xBB\xBF \t\n\r\0\x0B"); + $candidates = [$content]; + + if (preg_match('/```(?:json)?\s*([\s\S]*?)\s*```/i', $content, $match) === 1) { + $candidates[] = trim($match[1]); + } + + $firstBrace = strpos($content, '{'); + $lastBrace = strrpos($content, '}'); + if ($firstBrace !== false && $lastBrace !== false && $lastBrace >= $firstBrace) { + $candidates[] = substr($content, $firstBrace, $lastBrace - $firstBrace + 1); + } + + foreach (array_unique($candidates) as $candidate) { + $decoded = json_decode($candidate, true); + if (is_array($decoded) && json_last_error() === JSON_ERROR_NONE) { + return $decoded; + } + } + + error_log(sprintf( + 'LLM JSON parse failed: %s; length=%d; sha256=%s', + json_last_error_msg(), + strlen($content), + hash('sha256', $content) + )); + return null; +} + /** * Ruft das konfigurierte LLM (oder den FakeProvider für lokale Tests, siehe README.md) auf und * verrechnet die echten Token-Kosten gegen das Guthaben des Nutzers. Gemeinsame Logik für jeden diff --git a/ai-backend/explain.php b/ai-backend/explain.php index 805dfdc..33e5dc2 100644 --- a/ai-backend/explain.php +++ b/ai-backend/explain.php @@ -60,7 +60,7 @@ PROMPT; $userContent = json_encode($body); $result = ai_backend_call_and_charge($pdo, $config, $user, $systemPrompt, $userContent); -$parsed = json_decode($result['content'], true); +$parsed = ai_backend_decode_json_response($result['content']); if (!is_array($parsed) || !isset($parsed['explanation'])) { ai_backend_fail(502, 'Die KI hat kein gültiges JSON im erwarteten Schema zurückgegeben.'); } diff --git a/ai-backend/gbu.php b/ai-backend/gbu.php index e40c056..f0da369 100644 --- a/ai-backend/gbu.php +++ b/ai-backend/gbu.php @@ -82,7 +82,7 @@ PROMPT; $userContent = json_encode($body); $result = ai_backend_call_and_charge($pdo, $config, $user, $systemPrompt, $userContent); -$parsed = json_decode($result['content'], true); +$parsed = ai_backend_decode_json_response($result['content']); if (!is_array($parsed) || !isset($parsed['procedure'])) { ai_backend_fail(502, 'Die KI hat kein gültiges JSON im erwarteten Schema zurückgegeben.'); } diff --git a/ai-backend/pdf-template.php b/ai-backend/pdf-template.php index d8e7fe0..639b9c3 100644 --- a/ai-backend/pdf-template.php +++ b/ai-backend/pdf-template.php @@ -48,7 +48,7 @@ PROMPT; $result = ai_backend_call_and_charge($pdo, $config, $user, $systemPrompt, json_encode($body, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); -$parsed = json_decode($result['content'], true); +$parsed = ai_backend_decode_json_response($result['content']); if (!is_array($parsed) || !is_array($parsed['classifications'] ?? null)) { ai_backend_fail(502, 'Die KI hat kein gültiges Klassifikations-JSON geliefert.'); } @@ -78,4 +78,3 @@ foreach ($parsed['classifications'] as $classification) { } echo json_encode(['classifications' => $parsed['classifications']], JSON_UNESCAPED_UNICODE); - diff --git a/ai-backend/plan.php b/ai-backend/plan.php index 182c2c8..acd423e 100644 --- a/ai-backend/plan.php +++ b/ai-backend/plan.php @@ -161,7 +161,7 @@ $result = ai_backend_call_and_charge($pdo, $config, $user, $systemPrompt, $userC // Erst NACH der Abrechnung validieren: die Token wurden real verbraucht, das wird auch dann // verrechnet, wenn die KI kein valides JSON geliefert hat (siehe Planungsdokument). -$parsed = json_decode($result['content'], true); +$parsed = ai_backend_decode_json_response($result['content']); if (!is_array($parsed) || !isset($parsed['lessons'])) { ai_backend_fail(502, 'Die KI hat kein gültiges JSON im erwarteten Schema zurückgegeben.'); }