Files
MtoRagSystem/src/Controller/AskSseController.php
2026-04-25 12:19:20 +02:00

135 lines
3.9 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'] ?? ''));
/**
* Default:
* - Browser chat uses budgeted recent context
* - Full context must be explicitly requested
*/
$includeFullContext = filter_var(
$data['fullContext'] ?? false,
FILTER_VALIDATE_BOOL
);
$cookieResponse = new Response();
$clientId = $this->clientIdResolver->resolve($request, $cookieResponse);
return new StreamedResponse(
function () use ($prompt, $clientId, $cookieResponse, $includeFullContext): void {
@set_time_limit(0);
@ini_set('output_buffering', 'off');
@ini_set('zlib.output_compression', '0');
while (ob_get_level() > 0) {
ob_end_flush();
}
foreach ($cookieResponse->headers->getCookies() as $cookie) {
header('Set-Cookie: ' . $cookie, false);
}
echo "retry: 3000\n\n";
$this->sendComment('stream-open');
if ($prompt === '') {
$this->sendEvent('error', 'Empty prompt');
$this->sendEvent('done', '[DONE]');
return;
}
try {
foreach ($this->agentRunner->run($prompt, $clientId, $includeFullContext) as $chunk) {
if (connection_aborted() === 1) {
return;
}
$chunk = str_replace(["\r\n", "\r"], "\n", $chunk);
$this->sendData($chunk);
}
} catch (\Throwable $e) {
$this->sendEvent(
'error',
'❌ Stream abgebrochen: ' . $e->getMessage()
);
}
$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',
'X-Content-Type-Options' => 'nosniff',
]
);
}
private function sendData(string $data): void
{
if ($data === '') {
$this->sendComment('keepalive');
return;
}
$lines = explode("\n", $data);
foreach ($lines as $line) {
echo 'data: ' . $line . "\n";
}
echo "\n\n";
$this->flushOutput();
}
private function sendEvent(string $event, string $data): void
{
$safe = str_replace(["\r", "\n"], ' ', $data);
echo "event: {$event}\n";
echo "data: {$safe}\n\n";
$this->flushOutput();
}
private function sendComment(string $comment): void
{
$safe = str_replace(["\r", "\n"], ' ', $comment);
echo ': ' . $safe . "\n\n";
$this->flushOutput();
}
private function flushOutput(): void
{
if (function_exists('ob_flush')) {
@ob_flush();
}
@flush();
}
}