This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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.');
|
||||
}
|
||||
|
||||
+1
-1
@@ -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.');
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
+1
-1
@@ -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.');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user