timeout($timeout) ->acceptJson() ->post($config['url'] ?? 'https://api.openai.com/v1/chat/completions', [ 'model' => $model, 'response_format' => ['type' => 'json_object'], 'messages' => [ ['role' => 'system', 'content' => $this->systemPrompt()], ['role' => 'user', 'content' => $this->userPrompt($pressRelease)], ], ]); if ($response->failed()) { throw new RuntimeException("OpenAI-Klassifikation fehlgeschlagen (HTTP {$response->status()})."); } $payload = $response->json(); $content = data_get($payload, 'choices.0.message.content'); if (! is_string($content) || trim($content) === '') { throw new RuntimeException('OpenAI-Antwort enthielt keinen Inhalt.'); } $parsed = json_decode($content, true); if (! is_array($parsed) || ! isset($parsed['classification'])) { throw new RuntimeException('OpenAI-Antwort war kein gültiges Klassifikations-JSON.'); } $classification = PressReleaseClassification::tryFrom((string) $parsed['classification']); if ($classification === null) { throw new RuntimeException('OpenAI lieferte einen unbekannten Klassifikationswert.'); } $reasons = array_values(array_filter(array_map( static fn ($reason): string => (string) $reason, is_array($parsed['reasons'] ?? null) ? $parsed['reasons'] : [], ))); return new ClassificationResult( classification: $classification, reasons: $reasons, provider: 'openai', model: $model, rawResponse: is_array($payload) ? $payload : [], ); } private function systemPrompt(): string { return <<<'PROMPT' Du bist ein redaktioneller Prüf-Assistent für ein deutsches Presseportal. Bewerte, ob eine eingereichte Pressemitteilung veröffentlicht werden darf. Klassifiziere genau eine der drei Stufen: - "green": unauffällig, kann veröffentlicht werden. - "yellow": grenzwertig/unklar, sollte manuell geprüft werden. - "red": unzulässig, darf nicht veröffentlicht werden. Prüfe insbesondere diese Faktoren (Red Flags): - reine Werbung statt journalistischer Pressemitteilung - beleidigende, diskriminierende oder hetzerische Inhalte - rechtlich heikle Aussagen (z. B. unbelegte Heil-/Gewinnversprechen, Verleumdung, Aufruf zu Straftaten) - Spam-Muster (sinnlose Keyword-Wiederholung, irreführende Links) - unseriöse oder offensichtlich falsche Versprechen Antworte AUSSCHLIESSLICH als JSON-Objekt in genau diesem Schema: {"classification": "green|yellow|red", "reasons": ["kurze Begründung", ...]} Bei "green" darf "reasons" leer sein. Schreibe Begründungen auf Deutsch. PROMPT; } private function userPrompt(PressRelease $pressRelease): string { $title = (string) $pressRelease->title; $text = trim(strip_tags((string) $pressRelease->text)); return "Titel:\n{$title}\n\nText:\n{$text}"; } }