From 424aef257598c3aa74b60906702d76da3074582f Mon Sep 17 00:00:00 2001 From: team 1 Date: Sat, 9 May 2026 11:10:29 +0200 Subject: [PATCH] p63 --- config/retriex/chat-messages.yaml | 32 +++ config/services.yaml | 5 + ...EX_PATCH_63_CHAT_MESSAGES_CONFIG_README.md | 82 ++++++ src/Config/ChatMessagesConfig.php | 265 ++++++++++++++++++ src/Config/ConfigSourceAuditProvider.php | 1 + src/Config/GenreSourceOfTruthGuard.php | 1 + src/Config/RetriexEffectiveConfigProvider.php | 19 ++ src/Controller/AskSseController.php | 62 ++-- 8 files changed, 437 insertions(+), 30 deletions(-) create mode 100644 config/retriex/chat-messages.yaml create mode 100644 patch_history/RETRIEX_PATCH_63_CHAT_MESSAGES_CONFIG_README.md create mode 100644 src/Config/ChatMessagesConfig.php diff --git a/config/retriex/chat-messages.yaml b/config/retriex/chat-messages.yaml new file mode 100644 index 0000000..2d7a88a --- /dev/null +++ b/config/retriex/chat-messages.yaml @@ -0,0 +1,32 @@ +# User-visible chat, SSE and stream lifecycle messages. +# Protocol tokens, status enum values and debug comments intentionally stay in code. +parameters: + retriex.chat_messages.config: + sse: + empty_prompt: 'Bitte gib eine Frage ein.' + job_create_failed: 'Der Antwort-Job konnte nicht erstellt werden: {error}' + job_missing: 'Der Antwort-Job wurde nicht gefunden.' + stream_interrupted: 'Die Verbindung zum Antwort-Stream wurde unterbrochen.' + stream_aborted: 'Stream abgebrochen: {error}' + stream_aborted_event: '❌ {message}' + unknown_stream_error: 'Unbekannter Streamfehler.' + history_failure: 'Systemhinweis: Antwort konnte nicht abgeschlossen werden. Ursache: {message}' + fatal_server_error: '❌ Fataler Serverfehler: {message} in {file}:{line}' + fatal_unknown_error: 'unknown error' + fatal_unknown_file: 'unknown file' + fatal_unknown_line: '?' + stream_failed_with_message: 'Der Antwort-Stream ist fehlgeschlagen: {message}' + stream_failed_retry: 'Der Antwort-Stream ist fehlgeschlagen. Bitte sende die Anfrage erneut.' + stream_interrupted_retry: 'Der Antwort-Stream wurde durch einen Verbindungsabbruch unterbrochen. Bitte sende die Anfrage erneut, falls die Antwort unvollständig ist.' + job_stale: 'Der Antwort-Job liefert seit längerer Zeit keine neuen Daten. Der Stream wurde beendet, damit die Oberfläche nicht hängen bleibt.' + claim: + expired: 'Der Antwort-Job ist abgelaufen. Bitte sende die Anfrage erneut.' + invalid: 'Der Antwort-Job ist ungültig. Bitte sende die Anfrage erneut.' + lock_failed: 'Der Antwort-Job ist gerade gesperrt. Bitte sende die Anfrage erneut, falls keine Antwort erscheint.' + running: 'Der Antwort-Stream läuft bereits oder wurde nach einem Verbindungsabbruch erneut geöffnet. Bitte sende die Anfrage erneut, falls die Antwort unvollständig ist.' + interrupted: 'Der Antwort-Stream wurde durch einen Verbindungsabbruch unterbrochen. Bitte sende die Anfrage erneut, falls die Antwort unvollständig ist.' + completed: 'Der Antwort-Stream wurde bereits abgeschlossen. Bitte sende eine neue Anfrage, wenn du eine weitere Antwort brauchst.' + missing: 'Der Antwort-Job wurde nicht gefunden. Falls deine Verbindung kurz unterbrochen war, sende die Anfrage bitte erneut.' + storage: + directory_create_failed: 'Stream job directory could not be created.' + write_failed: 'Stream job could not be written.' diff --git a/config/services.yaml b/config/services.yaml index 369f907..384addf 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -7,6 +7,7 @@ imports: - { resource: 'retriex/model.yaml' } - { resource: 'retriex/prompt.yaml' } - { resource: 'retriex/agent.yaml' } + - { resource: 'retriex/chat-messages.yaml' } - { resource: 'retriex/retrieval.yaml' } - { resource: 'retriex/language.yaml' } - { resource: 'retriex/query_enrichment.yaml' } @@ -146,6 +147,10 @@ services: $vocabulary: '@App\Config\DomainVocabularyConfig' $genreConfig: '@App\Config\GenreConfig' + App\Config\ChatMessagesConfig: + arguments: + $config: '%retriex.chat_messages.config%' + App\Config\NdjsonHybridRetrieverConfig: arguments: $config: '%retriex.retrieval.config%' diff --git a/patch_history/RETRIEX_PATCH_63_CHAT_MESSAGES_CONFIG_README.md b/patch_history/RETRIEX_PATCH_63_CHAT_MESSAGES_CONFIG_README.md new file mode 100644 index 0000000..630523a --- /dev/null +++ b/patch_history/RETRIEX_PATCH_63_CHAT_MESSAGES_CONFIG_README.md @@ -0,0 +1,82 @@ +# RetrieX Patch 63 - Chat Messages Config + +## Ziel + +Patch 63 führt eine zentrale YAML-Konfiguration für chat-sichtbare SSE-/Job-Meldungen ein. Der erste sichere Schnitt fokussiert bewusst den `AskSseController`, weil dort bislang viele nutzerlesbare Stream-, Job- und Fehlertexte direkt im PHP-Code standen. + +## Geänderte Dateien + +- `config/retriex/chat-messages.yaml` + - Neue zentrale Konfiguration `retriex.chat_messages.config`. + - Enthält nutzerlesbare SSE-/Job-/Stream-Meldungen inklusive Claim-, Stale-, Fatal- und Storage-Fehlertexten. +- `src/Config/ChatMessagesConfig.php` + - Neue Config-Fassade für Pflichttexte, Template-Rendering mit `{placeholder}` und Validierung der erforderlichen Message-Pfade. +- `src/Controller/AskSseController.php` + - Nutzerlesbare SSE-/Job-Meldungen werden über `ChatMessagesConfig` gelesen. + - Technische Protokollwerte bleiben im Code: SSE-Eventnamen, Job-Status-Enums, `[DONE]`, Retry-/Keepalive-Kommentare und Header. +- `config/services.yaml` + - Import von `retriex/chat-messages.yaml`. + - Service-Argument für `ChatMessagesConfig`. +- `src/Config/RetriexEffectiveConfigProvider.php` + - `chat_messages` wird im Effective-Config-Dump ausgegeben. + - Required-Message-Pfade werden in `mto:agent:config:validate` mitvalidiert. +- `src/Config/ConfigSourceAuditProvider.php` + - `ChatMessagesConfig` wird dem YAML-Parameter `retriex.chat_messages.config` zugeordnet. +- `src/Config/GenreSourceOfTruthGuard.php` + - Raw-Config-Root für `retriex.chat_messages.config` wird konsistent als `chat_messages` gemappt. + +## Bewusste Abgrenzung + +Dieser Patch migriert noch nicht alle bereits YAML-konfigurierten Agent-/Prompt-/Shop-UI-Texte aus `agent.yaml` nach `chat-messages.yaml`. Das wäre ein eigener zweiter Schritt, weil dort viele bestehende Getter und Regressionen betroffen sind. + +Ebenfalls noch nicht migriert sind statische Frontend-Texte in: + +- `public/assets/js/base.js` +- `public/index.html` + +Für diese Texte sollte ein separater Frontend-/Template-Schnitt folgen, z. B. über Twig-Rendering oder einen kleinen `/chat-messages` JSON-Endpunkt. + +## Leitlinien + +- Keine neuen PHP-only User-Messages. +- Keine fachliche Runtime-Änderung. +- Keine Änderungen an Retrieval, Ranking, Shopquery, Prompting oder Produktlogik. +- Keine Migration technischer Protokolltokens nach YAML. + +## Lokale Prüfungen + +Durchgeführt: + +```bash +php -l src/Config/ChatMessagesConfig.php +php -l src/Controller/AskSseController.php +php -l src/Config/RetriexEffectiveConfigProvider.php +php -l src/Config/ConfigSourceAuditProvider.php +php -l src/Config/GenreSourceOfTruthGuard.php +python3 -c "import yaml; yaml.safe_load(open('config/retriex/chat-messages.yaml')); yaml.safe_load(open('config/services.yaml'))" +``` + +Außerdem wurde `ChatMessagesConfig` mit einem kleinen PHP-Smoke-Test instanziiert und das Template-Rendering plus `validate()` geprüft. + +Nicht ausführbar in der Patch-Umgebung: + +```bash +php bin/console mto:agent:config:validate +``` + +Grund: Im entpackten ZIP fehlt `vendor/`; `bin/console` bricht mit `Dependencies are missing. Try running "composer install".` ab. + +## Empfohlene Prüfungen nach Einspielen + +```bash +bin/console mto:agent:config:validate +bin/console mto:agent:regression:test +bin/console mto:agent:config:audit-source --details +bin/console mto:agent:config:audit-patterns --details +``` + +Zusätzlich manuell prüfen: + +- Leere Frage an `/ask-jobs` oder `/ask-sse` liefert die YAML-Meldung aus `chat-messages.yaml`. +- Ungültiger/fehlender Job liefert die YAML-Meldung `sse.job_missing` bzw. `sse.claim.missing`. +- Running-/Interrupted-/Failed-Reconnects zeigen weiterhin sinnvolle, aber YAML-konfigurierte Meldungen. diff --git a/src/Config/ChatMessagesConfig.php b/src/Config/ChatMessagesConfig.php new file mode 100644 index 0000000..a1a85db --- /dev/null +++ b/src/Config/ChatMessagesConfig.php @@ -0,0 +1,265 @@ + $config + */ + public function __construct(private readonly array $config = []) + { + } + + public function getSseEmptyPrompt(): string + { + return $this->string('sse.empty_prompt'); + } + + public function getSseJobCreateFailed(string $error): string + { + return $this->render('sse.job_create_failed', ['error' => $error]); + } + + public function getSseJobMissing(): string + { + return $this->string('sse.job_missing'); + } + + public function getSseStreamInterrupted(): string + { + return $this->string('sse.stream_interrupted'); + } + + public function getSseStreamAborted(string $error): string + { + return $this->render('sse.stream_aborted', ['error' => $error]); + } + + public function getSseStreamAbortedEvent(string $message): string + { + return $this->render('sse.stream_aborted_event', ['message' => $message]); + } + + public function getSseUnknownStreamError(): string + { + return $this->string('sse.unknown_stream_error'); + } + + public function getSseHistoryFailure(string $message): string + { + return $this->render('sse.history_failure', ['message' => $message]); + } + + public function getSseFatalServerError(string $message, string $file, string $line): string + { + return $this->render('sse.fatal_server_error', [ + 'message' => $message, + 'file' => $file, + 'line' => $line, + ]); + } + + public function getSseFatalUnknownError(): string + { + return $this->string('sse.fatal_unknown_error'); + } + + public function getSseFatalUnknownFile(): string + { + return $this->string('sse.fatal_unknown_file'); + } + + public function getSseFatalUnknownLine(): string + { + return $this->string('sse.fatal_unknown_line'); + } + + public function getSseStreamFailedWithMessage(string $message): string + { + return $this->render('sse.stream_failed_with_message', ['message' => $message]); + } + + public function getSseStreamFailedRetry(): string + { + return $this->string('sse.stream_failed_retry'); + } + + public function getSseStreamInterruptedRetry(): string + { + return $this->string('sse.stream_interrupted_retry'); + } + + public function getSseJobStale(): string + { + return $this->string('sse.job_stale'); + } + + public function getSseClaimExpired(): string + { + return $this->string('sse.claim.expired'); + } + + public function getSseClaimInvalid(): string + { + return $this->string('sse.claim.invalid'); + } + + public function getSseClaimLockFailed(): string + { + return $this->string('sse.claim.lock_failed'); + } + + public function getSseClaimRunning(): string + { + return $this->string('sse.claim.running'); + } + + public function getSseClaimInterrupted(): string + { + return $this->string('sse.claim.interrupted'); + } + + public function getSseClaimCompleted(): string + { + return $this->string('sse.claim.completed'); + } + + public function getSseClaimMissing(): string + { + return $this->string('sse.claim.missing'); + } + + public function getSseStorageDirectoryCreateFailed(): string + { + return $this->string('sse.storage.directory_create_failed'); + } + + public function getSseStorageWriteFailed(): string + { + return $this->string('sse.storage.write_failed'); + } + + /** + * @return array + */ + public function toArray(): array + { + return $this->config; + } + + /** + * @return array{status:string, errors:list} + */ + public function validate(): array + { + $errors = []; + + foreach ($this->requiredMessagePaths() as $path) { + try { + $this->string($path); + } catch (\InvalidArgumentException $e) { + $errors[] = $e->getMessage(); + } + } + + return [ + 'status' => $errors === [] ? 'OK' : 'ERROR', + 'errors' => $errors, + ]; + } + + /** + * @return list + */ + private function requiredMessagePaths(): array + { + return [ + 'sse.empty_prompt', + 'sse.job_create_failed', + 'sse.job_missing', + 'sse.stream_interrupted', + 'sse.stream_aborted', + 'sse.stream_aborted_event', + 'sse.unknown_stream_error', + 'sse.history_failure', + 'sse.fatal_server_error', + 'sse.fatal_unknown_error', + 'sse.fatal_unknown_file', + 'sse.fatal_unknown_line', + 'sse.stream_failed_with_message', + 'sse.stream_failed_retry', + 'sse.stream_interrupted_retry', + 'sse.job_stale', + 'sse.claim.expired', + 'sse.claim.invalid', + 'sse.claim.lock_failed', + 'sse.claim.running', + 'sse.claim.interrupted', + 'sse.claim.completed', + 'sse.claim.missing', + 'sse.storage.directory_create_failed', + 'sse.storage.write_failed', + ]; + } + + /** + * @param array $parameters + */ + private function render(string $path, array $parameters): string + { + $template = $this->string($path); + $replacements = []; + + foreach ($parameters as $key => $value) { + $replacements['{' . $key . '}'] = $this->normalizePlaceholderValue($value); + } + + return strtr($template, $replacements); + } + + private function normalizePlaceholderValue(mixed $value): string + { + if ($value === null) { + return ''; + } + + if (is_bool($value)) { + return $value ? 'true' : 'false'; + } + + if (is_scalar($value)) { + return trim(preg_replace('/\s+/u', ' ', (string) $value) ?? (string) $value); + } + + return ''; + } + + private function string(string $path): string + { + $value = $this->value($path); + + if (is_string($value) && trim($value) !== '') { + return $value; + } + + throw new \InvalidArgumentException(sprintf('RetrieX chat messages config key "%s" must be a non-empty string.', $path)); + } + + private function value(string $path): mixed + { + $current = $this->config; + + foreach (explode('.', $path) as $segment) { + if (!is_array($current) || !array_key_exists($segment, $current)) { + throw new \InvalidArgumentException(sprintf('RetrieX chat messages config key "%s" is required.', $path)); + } + + $current = $current[$segment]; + } + + return $current; + } +} diff --git a/src/Config/ConfigSourceAuditProvider.php b/src/Config/ConfigSourceAuditProvider.php index 0d1765d..50b8714 100644 --- a/src/Config/ConfigSourceAuditProvider.php +++ b/src/Config/ConfigSourceAuditProvider.php @@ -14,6 +14,7 @@ final readonly class ConfigSourceAuditProvider 'CommerceQueryParserConfig' => 'retriex.commerce_query.config', 'ContextServiceConfig' => 'retriex.context.config', 'CatalogIntentConfig' => 'retriex.intent.catalog.config', + 'ChatMessagesConfig' => 'retriex.chat_messages.config', 'DomainVocabularyConfig' => 'retriex.vocabulary.config', 'IntentLightConfig' => 'retriex.intent.light.config', 'LanguageCleanupConfig' => 'retriex.stopwords.config', diff --git a/src/Config/GenreSourceOfTruthGuard.php b/src/Config/GenreSourceOfTruthGuard.php index aaa4829..2ba4e88 100644 --- a/src/Config/GenreSourceOfTruthGuard.php +++ b/src/Config/GenreSourceOfTruthGuard.php @@ -546,6 +546,7 @@ final readonly class GenreSourceOfTruthGuard $config = []; $parameterRoots = [ 'retriex.agent.config' => 'agent', + 'retriex.chat_messages.config' => 'chat_messages', 'retriex.commerce_query.config' => 'commerce_query', 'retriex.governance.config' => 'governance', 'retriex.intent.commerce.config' => 'intent.commerce', diff --git a/src/Config/RetriexEffectiveConfigProvider.php b/src/Config/RetriexEffectiveConfigProvider.php index 5e99eb9..1a1217f 100644 --- a/src/Config/RetriexEffectiveConfigProvider.php +++ b/src/Config/RetriexEffectiveConfigProvider.php @@ -18,6 +18,7 @@ final readonly class RetriexEffectiveConfigProvider private NdjsonHybridRetrieverConfig $retrieverConfig, private DomainVocabularyConfig $domainVocabularyConfig, private AgentRunnerConfig $agentRunnerConfig, + private ChatMessagesConfig $chatMessagesConfig, private SearchRepairConfig $searchRepairConfig, private CommerceIntentConfig $commerceIntentConfig, private CommerceQueryParserConfig $commerceQueryParserConfig, @@ -52,6 +53,7 @@ final readonly class RetriexEffectiveConfigProvider 'retrieval' => $this->retrievalConfig(), 'prompt' => $this->promptConfig(), 'agent' => $this->agentConfig(), + 'chat_messages' => $this->chatMessagesConfig(), 'vector' => $this->vectorConfig(), 'commerce' => $this->commerceConfig(), 'commerce_query' => $this->commerceQueryConfig(), @@ -86,6 +88,7 @@ final readonly class RetriexEffectiveConfigProvider $this->validateRetrieval($config['retrieval'], $errors, $warnings); $this->validatePrompt($config['prompt'], $errors, $warnings); $this->validateAgent($config['agent'], $errors, $warnings); + $this->validateChatMessages($errors); $this->validateVector($config['vector'], $errors, $warnings); $this->validateCommerce($config['commerce'], $errors, $warnings); $this->validateCommerceQuery($config['commerce_query'], $errors, $warnings); @@ -602,6 +605,21 @@ final readonly class RetriexEffectiveConfigProvider ]; } + /** @return array */ + private function chatMessagesConfig(): array + { + return $this->chatMessagesConfig->toArray(); + } + + private function validateChatMessages(array &$errors): void + { + $validation = $this->chatMessagesConfig->validate(); + + foreach ($validation['errors'] as $error) { + $errors[] = $error; + } + } + /** @return array */ private function agentConfig(): array { @@ -1429,6 +1447,7 @@ final readonly class RetriexEffectiveConfigProvider { $configRoots = [ 'retriex.agent.config' => 'agent', + 'retriex.chat_messages.config' => 'chat_messages', 'retriex.commerce_query.config' => 'commerce_query', 'retriex.governance.config' => 'governance', 'retriex.intent.commerce.config' => 'intent.commerce', diff --git a/src/Controller/AskSseController.php b/src/Controller/AskSseController.php index 3c30b6d..b019e9c 100644 --- a/src/Controller/AskSseController.php +++ b/src/Controller/AskSseController.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace App\Controller; use App\Agent\AgentRunner; +use App\Config\ChatMessagesConfig; use App\Context\ContextService; use App\Http\ClientIdResolver; use Symfony\Component\HttpFoundation\JsonResponse; @@ -27,6 +28,7 @@ final readonly class AskSseController public function __construct( private AgentRunner $agentRunner, + private ChatMessagesConfig $chatMessages, private ClientIdResolver $clientIdResolver, private ContextService $contextService, private string $projectDir, @@ -40,7 +42,7 @@ final readonly class AskSseController $prompt = trim((string) ($data['prompt'] ?? '')); if ($prompt === '') { - return new JsonResponse(['error' => 'Empty prompt'], Response::HTTP_BAD_REQUEST); + return new JsonResponse(['error' => $this->chatMessages->getSseEmptyPrompt()], Response::HTTP_BAD_REQUEST); } /** @@ -73,7 +75,7 @@ final readonly class AskSseController ]); } catch (Throwable $e) { return new JsonResponse( - ['error' => 'Stream job could not be created: ' . $this->formatThrowableForClient($e)], + ['error' => $this->chatMessages->getSseJobCreateFailed($this->formatThrowableForClient($e))], Response::HTTP_INTERNAL_SERVER_ERROR ); } @@ -95,7 +97,7 @@ final readonly class AskSseController if (!is_array($job)) { return new JsonResponse([ 'status' => 'missing', - 'message' => 'Der Antwort-Job wurde nicht gefunden.', + 'message' => $this->chatMessages->getSseJobMissing(), 'lastEventId' => 0, ], Response::HTTP_NOT_FOUND); } @@ -213,8 +215,8 @@ final readonly class AskSseController $this->sendComment('stream-open'); if ($prompt === '') { - $this->markJobStatus($jobId, self::JOB_STATUS_FAILED, 'Empty prompt'); - $this->sendEvent('error', 'Empty prompt'); + $this->markJobStatus($jobId, self::JOB_STATUS_FAILED, $this->chatMessages->getSseEmptyPrompt()); + $this->sendEvent('error', $this->chatMessages->getSseEmptyPrompt()); $this->sendDoneEvent(); return; } @@ -225,7 +227,7 @@ final readonly class AskSseController $this->markJobStatus( $jobId, self::JOB_STATUS_INTERRUPTED, - 'Die Verbindung zum Antwort-Stream wurde unterbrochen.' + $this->chatMessages->getSseStreamInterrupted() ); return; } @@ -235,11 +237,11 @@ final readonly class AskSseController $this->sendData($chunk, $eventId); } } catch (Throwable $e) { - $message = 'Stream abgebrochen: ' . $this->formatThrowableForClient($e); + $message = $this->chatMessages->getSseStreamAborted($this->formatThrowableForClient($e)); $this->markJobStatus($jobId, self::JOB_STATUS_FAILED, $message); $this->sendEvent( 'error', - '❌ ' . $message + $this->chatMessages->getSseStreamAbortedEvent($message) ); if ($prompt !== '' && $clientId !== '') { @@ -260,13 +262,13 @@ final readonly class AskSseController $message = trim(preg_replace('/\s+/u', ' ', $message) ?? $message); if ($message === '') { - $message = 'Unbekannter Streamfehler.'; + $message = $this->chatMessages->getSseUnknownStreamError(); } $this->contextService->appendHistory( $clientId, $prompt, - 'Systemhinweis: Antwort konnte nicht abgeschlossen werden. Ursache: ' . $message + $this->chatMessages->getSseHistoryFailure($message) ); } catch (Throwable) { // History persistence must never break the SSE error response. @@ -288,11 +290,10 @@ final readonly class AskSseController return; } - $message = sprintf( - '❌ Fataler Serverfehler: %s in %s:%s', - (string) ($error['message'] ?? 'unknown error'), - (string) ($error['file'] ?? 'unknown file'), - (string) ($error['line'] ?? '?') + $message = $this->chatMessages->getSseFatalServerError( + (string) ($error['message'] ?? $this->chatMessages->getSseFatalUnknownError()), + (string) ($error['file'] ?? $this->chatMessages->getSseFatalUnknownFile()), + (string) ($error['line'] ?? $this->chatMessages->getSseFatalUnknownLine()) ); $this->markJobStatus($jobId, self::JOB_STATUS_FAILED, $message); @@ -420,13 +421,13 @@ final readonly class AskSseController $directory = $this->jobDirectory(); if (!is_dir($directory) && !mkdir($directory, 0775, true) && !is_dir($directory)) { - throw new \RuntimeException('Stream job directory could not be created.'); + throw new \RuntimeException($this->chatMessages->getSseStorageDirectoryCreateFailed()); } $json = json_encode($payload, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); if (file_put_contents($this->jobPath($jobId), $json, LOCK_EX) === false) { - throw new \RuntimeException('Stream job could not be written.'); + throw new \RuntimeException($this->chatMessages->getSseStorageWriteFailed()); } } @@ -664,8 +665,8 @@ final readonly class AskSseController $this->sendEvent( 'error', $message !== '' - ? 'Der Antwort-Stream ist fehlgeschlagen: ' . $message - : 'Der Antwort-Stream ist fehlgeschlagen. Bitte sende die Anfrage erneut.' + ? $this->chatMessages->getSseStreamFailedWithMessage($message) + : $this->chatMessages->getSseStreamFailedRetry() ); $this->sendDoneEvent(); return; @@ -676,7 +677,7 @@ final readonly class AskSseController 'error', $message !== '' ? $message - : 'Der Antwort-Stream wurde durch einen Verbindungsabbruch unterbrochen. Bitte sende die Anfrage erneut, falls die Antwort unvollständig ist.' + : $this->chatMessages->getSseStreamInterruptedRetry() ); $this->sendDoneEvent(); return; @@ -857,7 +858,7 @@ final readonly class AskSseController return $job; } - $message = 'Der Antwort-Job liefert seit längerer Zeit keine neuen Daten. Der Stream wurde beendet, damit die Oberfläche nicht hängen bleibt.'; + $message = $this->chatMessages->getSseJobStale(); $this->markJobStatus($jobId, self::JOB_STATUS_FAILED, $message); $freshJob = $this->readJob($jobId); @@ -875,39 +876,40 @@ final readonly class AskSseController $message = is_string($claim['message'] ?? null) ? trim((string) $claim['message']) : ''; if ($reason === 'expired') { - return 'Der Antwort-Job ist abgelaufen. Bitte sende die Anfrage erneut.'; + return $this->chatMessages->getSseClaimExpired(); } if ($reason === 'invalid') { - return 'Der Antwort-Job ist ungültig. Bitte sende die Anfrage erneut.'; + return $this->chatMessages->getSseClaimInvalid(); } if ($reason === 'lock_failed') { - return 'Der Antwort-Job ist gerade gesperrt. Bitte sende die Anfrage erneut, falls keine Antwort erscheint.'; + return $this->chatMessages->getSseClaimLockFailed(); } if ($reason === 'not_pending') { if ($status === self::JOB_STATUS_RUNNING) { - return 'Der Antwort-Stream läuft bereits oder wurde nach einem Verbindungsabbruch erneut geöffnet. Bitte sende die Anfrage erneut, falls die Antwort unvollständig ist.'; + return $this->chatMessages->getSseClaimRunning(); } if ($status === self::JOB_STATUS_INTERRUPTED) { - return 'Der Antwort-Stream wurde durch einen Verbindungsabbruch unterbrochen. Bitte sende die Anfrage erneut, falls die Antwort unvollständig ist.'; + return $this->chatMessages->getSseClaimInterrupted(); } if ($status === self::JOB_STATUS_COMPLETED) { - return 'Der Antwort-Stream wurde bereits abgeschlossen. Bitte sende eine neue Anfrage, wenn du eine weitere Antwort brauchst.'; + return $this->chatMessages->getSseClaimCompleted(); } if ($status === self::JOB_STATUS_FAILED) { return $message !== '' - ? 'Der Antwort-Stream ist fehlgeschlagen: ' . $message - : 'Der Antwort-Stream ist fehlgeschlagen. Bitte sende die Anfrage erneut.'; + ? $this->chatMessages->getSseStreamFailedWithMessage($message) + : $this->chatMessages->getSseStreamFailedRetry(); } } - return 'Der Antwort-Job wurde nicht gefunden. Falls deine Verbindung kurz unterbrochen war, sende die Anfrage bitte erneut.'; + return $this->chatMessages->getSseClaimMissing(); } + private function cleanupExpiredJobs(): void { $directory = $this->jobDirectory();