p63
This commit is contained in:
32
config/retriex/chat-messages.yaml
Normal file
32
config/retriex/chat-messages.yaml
Normal file
@@ -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.'
|
||||
@@ -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%'
|
||||
|
||||
@@ -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.
|
||||
265
src/Config/ChatMessagesConfig.php
Normal file
265
src/Config/ChatMessagesConfig.php
Normal file
@@ -0,0 +1,265 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Config;
|
||||
|
||||
final class ChatMessagesConfig
|
||||
{
|
||||
/**
|
||||
* @param array<string, mixed> $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<string, mixed>
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return $this->config;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{status:string, errors:list<string>}
|
||||
*/
|
||||
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<string>
|
||||
*/
|
||||
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<string, scalar|null> $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;
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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<string, mixed> */
|
||||
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<string, mixed> */
|
||||
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',
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user