121 lines
4.2 KiB
PHP
121 lines
4.2 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Controller;
|
|
|
|
use App\Agent\AgentRunner;
|
|
use App\Http\ClientIdResolver;
|
|
use Symfony\Component\HttpFoundation\Request;
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
use Symfony\Component\HttpFoundation\StreamedResponse;
|
|
use Symfony\Component\Routing\Annotation\Route;
|
|
|
|
final readonly class AskSseController
|
|
{
|
|
public function __construct(
|
|
private AgentRunner $agentRunner,
|
|
private ClientIdResolver $clientIdResolver,
|
|
) {}
|
|
|
|
#[Route('/ask-sse', name: 'ask_sse', methods: ['POST'])]
|
|
public function stream(Request $request): StreamedResponse
|
|
{
|
|
$data = json_decode($request->getContent(), true);
|
|
$prompt = trim((string) ($data['prompt'] ?? ''));
|
|
|
|
$cookieResponse = new Response();
|
|
$clientId = $this->clientIdResolver->resolve($request, $cookieResponse);
|
|
|
|
return new StreamedResponse(
|
|
function () use ($prompt, $clientId, $cookieResponse): void {
|
|
|
|
// ---------------------------------------------------------
|
|
// Disable all PHP output buffering
|
|
// ---------------------------------------------------------
|
|
while (ob_get_level() > 0) {
|
|
ob_end_flush();
|
|
}
|
|
|
|
// ---------------------------------------------------------
|
|
// Forward cookies
|
|
// ---------------------------------------------------------
|
|
foreach ($cookieResponse->headers->getCookies() as $cookie) {
|
|
header('Set-Cookie: ' . $cookie, false);
|
|
}
|
|
|
|
// ---------------------------------------------------------
|
|
// SSE prelude
|
|
// ---------------------------------------------------------
|
|
echo "retry: 3000\n\n";
|
|
flush();
|
|
|
|
if ($prompt === '') {
|
|
$this->sendEvent('error', 'Empty prompt');
|
|
$this->sendEvent('done', '[DONE]');
|
|
return;
|
|
}
|
|
|
|
// ---------------------------------------------------------
|
|
// 🔥 FIXED: Sende Chunks direkt (behält \n!)
|
|
// ---------------------------------------------------------
|
|
try {
|
|
foreach ($this->agentRunner->run($prompt, $clientId, true) as $chunk) {
|
|
$chunk = str_replace(["\r\n", "\r"], "\n", $chunk);
|
|
$this->sendData($chunk);
|
|
}
|
|
} catch (\Throwable $e) {
|
|
$this->sendData(
|
|
'<span class="text-danger">❌ Stream abgebrochen: '
|
|
. htmlspecialchars($e->getMessage(), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')
|
|
. '</span>'
|
|
);
|
|
}
|
|
|
|
// ---------------------------------------------------------
|
|
// Signal completion
|
|
// ---------------------------------------------------------
|
|
$this->sendEvent('done', '[DONE]');
|
|
},
|
|
200,
|
|
[
|
|
'Content-Type' => 'text/event-stream; charset=utf-8',
|
|
'Cache-Control' => 'no-cache, no-store, must-revalidate',
|
|
'Connection' => 'keep-alive',
|
|
'X-Accel-Buffering' => 'no',
|
|
]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* FIXED: Behält Markdown-Struktur (\n) bei
|
|
*
|
|
* SSE erlaubt mehrere "data:"-Zeilen pro Event.
|
|
* Jede Zeile wird als separate data-Zeile gesendet.
|
|
*/
|
|
private function sendData(string $data): void
|
|
{
|
|
// Split by \n und sende jede Zeile einzeln
|
|
$lines = explode("\n", $data);
|
|
|
|
foreach ($lines as $line) {
|
|
echo 'data: ' . $line . "\n";
|
|
}
|
|
|
|
// Leere Zeile = Ende der SSE-Message
|
|
echo "\n\n";
|
|
flush();
|
|
}
|
|
|
|
/**
|
|
* Sends a named SSE event.
|
|
*/
|
|
private function sendEvent(string $event, string $data): void
|
|
{
|
|
$safe = str_replace(["\r", "\n"], ' ', $data);
|
|
|
|
echo "event: {$event}\n";
|
|
echo "data: {$safe}\n\n";
|
|
flush();
|
|
}
|
|
} |