feat: add flowing templates and PDF import pipeline
CI / build-and-test (push) Canceled after 0s

This commit is contained in:
2026-08-31 19:56:25 +02:00
parent 95e3f677e4
commit 3e5f197bdb
19 changed files with 920 additions and 44 deletions
+81
View File
@@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/db.php';
header('Content-Type: application/json');
$config = require __DIR__ . '/config.php';
$pdo = ai_backend_db($config);
$user = ai_backend_authenticate($pdo);
if ((float) $user['balance_usd'] <= 0) {
ai_backend_fail(402, 'Kein Guthaben mehr vorhanden.');
}
$body = json_decode(file_get_contents('php://input'), true);
if (!is_array($body) || !isset($body['document']['pages']) || !is_array($body['candidates'] ?? null)) {
ai_backend_fail(400, 'Dokument oder Kandidaten fehlen.');
}
if (count($body['candidates']) > 500) {
ai_backend_fail(413, 'Zu viele Textkandidaten in einer Anfrage.');
}
$systemPrompt = <<<'PROMPT'
Du klassifizierst Textblöcke aus deutschen Schul- und Verwaltungsdokumenten für einen
Vorlageneditor. Die Geometrie wurde deterministisch aus einem PDF extrahiert und darf von dir
niemals geändert oder ergänzt werden. Du erhältst Seiten, Textblöcke und eine Liste von Kandidaten.
Für jeden Kandidaten:
- vergib einen kurzen stabilen deutschen Placeholder-Namen nur aus Buchstaben und Ziffern,
- wähle type ausschließlich aus text, multiline, date, number,
- wähle confidence ausschließlich aus high, medium, low,
- darfst du benachbarte, logisch zusammengehörige Zeilen gruppieren. Dann enthält blockIds alle
Original-IDs der Gruppe. Jede übernommene ID muss exakt aus der Eingabe stammen.
- typische Namen sind Anrede, Empfaenger, Adresse, PlzOrt, Datum, Betreff, Aktenzeichen, Brieftext.
Antworte AUSSCHLIESSLICH mit gültigem JSON in diesem Schema:
{
"classifications": [
{
"id": "<id eines Kandidaten>",
"blockIds": ["<unveränderte Original-ID>"],
"name": "<PlaceholderName>",
"type": "text|multiline|date|number",
"confidence": "high|medium|low"
}
]
}
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);
if (!is_array($parsed) || !is_array($parsed['classifications'] ?? null)) {
ai_backend_fail(502, 'Die KI hat kein gültiges Klassifikations-JSON geliefert.');
}
$knownIds = [];
foreach ($body['document']['pages'] as $page) {
foreach (($page['textBlocks'] ?? []) as $block) {
if (isset($block['id'])) $knownIds[(string) $block['id']] = true;
}
}
$candidateIds = array_fill_keys(array_map(fn($x) => (string) ($x['id'] ?? ''), $body['candidates']), true);
$allowedTypes = ['text', 'multiline', 'date', 'number'];
$allowedConfidence = ['high', 'medium', 'low'];
foreach ($parsed['classifications'] as $classification) {
if (!isset($candidateIds[(string) ($classification['id'] ?? '')])
|| !in_array($classification['type'] ?? '', $allowedTypes, true)
|| !in_array($classification['confidence'] ?? '', $allowedConfidence, true)
|| !preg_match('/^[\pL\pN]+$/u', (string) ($classification['name'] ?? ''))
|| !is_array($classification['blockIds'] ?? null)) {
ai_backend_fail(502, 'Die KI-Klassifikation enthält ungültige Werte.');
}
foreach ($classification['blockIds'] as $blockId) {
if (!isset($knownIds[(string) $blockId])) {
ai_backend_fail(502, 'Die KI-Klassifikation referenziert einen unbekannten Textblock.');
}
}
}
echo json_encode(['classifications' => $parsed['classifications']], JSON_UNESCAPED_UNICODE);
+15
View File
@@ -13,6 +13,21 @@ class FakeProvider implements ProviderInterface
{
public function sendMessage(string $systemPrompt, string $userContent, int $maxTokens): array
{
if (str_contains($systemPrompt, 'Placeholder-Namen')) {
$input = json_decode($userContent, true);
$classifications = array_map(static fn(array $candidate): array => [
'id' => $candidate['id'],
'blockIds' => $candidate['blockIds'] ?? [$candidate['id']],
'name' => $candidate['suggestedName'] ?? 'Feld',
'type' => $candidate['type'] ?? 'text',
'confidence' => $candidate['confidence'] ?? 'low',
], $input['candidates'] ?? []);
return [
'content' => json_encode(['classifications' => $classifications]),
'inputTokens' => 42, 'outputTokens' => 17,
'cacheCreationInputTokens' => 0, 'cacheReadInputTokens' => 0,
];
}
return [
'content' => json_encode([
'lessons' => [