This commit is contained in:
team 1
2026-05-04 18:46:26 +02:00
parent 90ced0352a
commit ebd71ba748
15 changed files with 739 additions and 182 deletions

View File

@@ -83,7 +83,7 @@ final readonly class AgentRunner
yield $this->systemMsg(
$this->buildProductionUiMetaMessage(
stageLabel: 'Antwort wird vorbereitet',
stageLabel: $this->agentRunnerConfig->getProductionUiStageLabel('preparing_answer'),
ragCount: null,
shopCount: null,
shopCountMode: $this->resolveShopCountModeForMeta(
@@ -93,7 +93,7 @@ final readonly class AgentRunner
shopSearchSkippedBecauseNoQuery: $shopSearchSkippedBecauseNoQuery
),
sourceLabels: $sources,
confidenceLabel: 'Beleglage wird geprüft'
confidenceLabel: $this->agentRunnerConfig->getProductionUiConfidenceLabel('checking_evidence')
),
'meta'
);
@@ -141,7 +141,7 @@ final readonly class AgentRunner
if ($this->isCommerceIntent($commerceIntent)) {
yield $this->systemMsg(
$this->buildProductionUiMetaMessage(
stageLabel: 'Shop-Routing erkannt',
stageLabel: $this->agentRunnerConfig->getProductionUiStageLabel('shop_routing_detected'),
ragCount: null,
shopCount: null,
shopCountMode: $this->resolveShopCountModeForMeta(
@@ -151,7 +151,7 @@ final readonly class AgentRunner
shopSearchSkippedBecauseNoQuery: $shopSearchSkippedBecauseNoQuery
),
sourceLabels: $sources,
confidenceLabel: 'Shopdaten werden geprüft'
confidenceLabel: $this->agentRunnerConfig->getProductionUiConfidenceLabel('checking_shop_data')
),
'meta'
);
@@ -174,7 +174,7 @@ final readonly class AgentRunner
yield $this->systemMsg(
$this->buildProductionUiMetaMessage(
stageLabel: 'RAG-Wissen wurde durchsucht',
stageLabel: $this->agentRunnerConfig->getProductionUiStageLabel('rag_searched'),
ragCount: count($knowledgeChunks),
shopCount: null,
shopCountMode: $this->resolveShopCountModeForMeta(
@@ -202,7 +202,7 @@ final readonly class AgentRunner
if ($this->isCommerceIntent($commerceIntent)) {
yield $this->systemMsg(
$this->buildProductionUiMetaMessage(
stageLabel: 'Shop-Suche wird vorbereitet',
stageLabel: $this->agentRunnerConfig->getProductionUiStageLabel('shop_search_preparing'),
ragCount: count($knowledgeChunks),
shopCount: null,
shopCountMode: 'loading',
@@ -308,7 +308,7 @@ final readonly class AgentRunner
yield $this->systemMsg(
$this->buildProductionUiMetaMessage(
stageLabel: 'Mehr Kontext nötig',
stageLabel: $this->agentRunnerConfig->getProductionUiStageLabel('more_context_needed'),
ragCount: count($knowledgeChunks),
shopCount: null,
shopCountMode: $this->resolveShopCountModeForMeta(
@@ -318,7 +318,7 @@ final readonly class AgentRunner
shopSearchSkippedBecauseNoQuery: $shopSearchSkippedBecauseNoQuery
),
sourceLabels: $sources,
confidenceLabel: 'mehr Kontext nötig',
confidenceLabel: $this->agentRunnerConfig->getProductionUiConfidenceLabel('more_context_needed'),
completed: true
),
'meta'
@@ -370,7 +370,7 @@ final readonly class AgentRunner
yield $this->systemMsg(
$this->buildProductionUiMetaMessage(
stageLabel: 'Shop wird durchsucht',
stageLabel: $this->agentRunnerConfig->getProductionUiStageLabel('shop_search_running'),
ragCount: count($knowledgeChunks),
shopCount: null,
shopCountMode: 'loading',
@@ -420,7 +420,7 @@ final readonly class AgentRunner
'meta'
);
$historyNotices[] = $this->buildHistoryNotice(
'Shopdaten konnten nicht geladen werden',
$this->agentRunnerConfig->getProductionUiText('history_notice_shop_unavailable_title'),
$primaryShopSearchFailureReason
);
@@ -431,7 +431,7 @@ final readonly class AgentRunner
'repairQueries' => [],
];
} else {
yield $this->systemMsg('Erweiterte Shopsuche wird geprüft…', 'think');
yield $this->systemMsg($this->agentRunnerConfig->getShopRepairCheckMessage(), 'think');
$repairPayload = $this->repairShopResults(
prompt: $prompt,
@@ -476,7 +476,7 @@ final readonly class AgentRunner
yield $this->systemMsg(
$this->buildProductionUiMetaMessage(
stageLabel: $primaryShopSearchHadSystemFailure ? 'Shopdaten nicht verfügbar' : 'Shop-Suche abgeschlossen',
stageLabel: $primaryShopSearchHadSystemFailure ? $this->agentRunnerConfig->getProductionUiStageLabel('shop_unavailable') : $this->agentRunnerConfig->getProductionUiStageLabel('shop_completed'),
ragCount: count($knowledgeChunks),
shopCount: $primaryShopSearchHadSystemFailure ? null : count($shopResults),
shopCountMode: $primaryShopSearchHadSystemFailure ? 'unavailable' : 'count',
@@ -542,7 +542,7 @@ final readonly class AgentRunner
yield $this->systemMsg(
$this->buildProductionUiMetaMessage(
stageLabel: 'Antwort wird generiert',
stageLabel: $this->agentRunnerConfig->getProductionUiStageLabel('answer_generating'),
ragCount: count($knowledgeChunks),
shopCount: $primaryShopSearchHadSystemFailure ? null : ($shopSearchAttempted ? count($shopResults) : null),
shopCountMode: $this->resolveShopCountModeForMeta(
@@ -587,7 +587,7 @@ final readonly class AgentRunner
yield $this->systemMsg(
$this->buildProductionUiMetaMessage(
stageLabel: 'Abgeschlossen',
stageLabel: $this->agentRunnerConfig->getProductionUiStageLabel('completed'),
ragCount: count($knowledgeChunks),
shopCount: $primaryShopSearchHadSystemFailure ? null : ($shopSearchAttempted ? count($shopResults) : null),
shopCountMode: $this->resolveShopCountModeForMeta(
@@ -667,7 +667,7 @@ final readonly class AgentRunner
$userErrorMessage = $this->buildUserErrorMessage($e);
yield $this->systemMsg(
$this->buildProductionUiMetaMessage(
stageLabel: 'Antwort wurde unterbrochen',
stageLabel: $this->agentRunnerConfig->getProductionUiStageLabel('interrupted'),
ragCount: count($knowledgeChunks),
shopCount: $primaryShopSearchHadSystemFailure ? null : ($shopSearchAttempted ? count($shopResults) : null),
shopCountMode: $this->resolveShopCountModeForMeta(
@@ -677,7 +677,7 @@ final readonly class AgentRunner
shopSearchSkippedBecauseNoQuery: $shopSearchSkippedBecauseNoQuery
),
sourceLabels: $sources,
confidenceLabel: 'nicht abgeschlossen',
confidenceLabel: $this->agentRunnerConfig->getProductionUiConfidenceLabel('interrupted'),
completed: true
),
'meta'
@@ -686,7 +686,7 @@ final readonly class AgentRunner
$historyResponse = $this->buildHistoryResponse('', array_merge(
$historyNotices,
[$this->buildHistoryNotice('Antwort konnte nicht abgeschlossen werden', $e->getMessage())]
[$this->buildHistoryNotice($this->agentRunnerConfig->getProductionUiText('history_notice_answer_incomplete_title'), $e->getMessage())]
));
if ($historyResponse !== '') {
@@ -985,12 +985,7 @@ final readonly class AgentRunner
private function normalizeFuzzyRoutingToken(string $token): string
{
$token = mb_strtolower(trim($token), 'UTF-8');
$token = strtr($token, [
'ä' => 'ae',
'ö' => 'oe',
'ü' => 'ue',
'ß' => 'ss',
]);
$token = $this->languageCleanupConfig->transliterateToAscii($token);
$token = preg_replace('/[^a-z0-9]+/u', '', $token) ?? $token;
return trim($token);
@@ -1028,13 +1023,10 @@ final readonly class AgentRunner
{
$normalized = $this->normalizeRoutingComparisonText($candidate);
return in_array($normalized, [
'normalized user input',
'corrected user input',
'user input',
'normalisierte nutzereingabe',
'korrigierte nutzereingabe',
], true);
return in_array($normalized, array_map(
fn (string $placeholder): string => $this->normalizeRoutingComparisonText($placeholder),
$this->agentRunnerConfig->getInputNormalizationPlaceholderOutputs()
), true);
}
private function normalizeRoutingComparisonText(string $value): string
@@ -1132,7 +1124,7 @@ final readonly class AgentRunner
$lines = [];
foreach ($previousQuestions as $question) {
$lines[] = 'Vorherige Nutzerfrage: ' . $question;
$lines[] = $this->renderAgentTemplate($this->agentRunnerConfig->getFollowUpContextPreviousUserQuestionTemplate(), ['question' => $question]);
}
if ($referenceAnchors !== []) {
@@ -1140,7 +1132,7 @@ final readonly class AgentRunner
. implode(' ', $referenceAnchors);
}
$lines[] = 'Aktuelle Folgefrage: ' . $prompt;
$lines[] = $this->renderAgentTemplate($this->agentRunnerConfig->getFollowUpContextCurrentQuestionTemplate(), ['question' => $prompt]);
return implode("\n", $lines);
}
@@ -1354,7 +1346,7 @@ final readonly class AgentRunner
private function normalizeFollowUpText(string $value): string
{
$value = mb_strtolower(trim($value), 'UTF-8');
$value = str_replace(['-', '/', '_'], ' ', $value);
$value = $this->languageCleanupConfig->replaceWordSeparatorsWithSpace($value);
$value = preg_replace('/[^\p{L}\p{N}\s]+/u', ' ', $value) ?? $value;
$value = preg_replace('/\s+/u', ' ', $value) ?? $value;
@@ -1389,7 +1381,7 @@ final readonly class AgentRunner
}
if (time() - $lastHeartbeatAt >= 2) {
yield $this->systemMsg('Shop-Suchanfrage wird optimiert…', 'think');
yield $this->systemMsg($this->agentRunnerConfig->getShopQueryOptimizationHeartbeatMessage(), 'think');
$lastHeartbeatAt = time();
}
@@ -2027,7 +2019,7 @@ final readonly class AgentRunner
private function tokenizeShopQueryCandidate(string $value): array
{
$value = mb_strtolower(trim($value), 'UTF-8');
$value = str_replace(['-', '/', '_'], ' ', $value);
$value = $this->languageCleanupConfig->replaceWordSeparatorsWithSpace($value);
if (preg_match_all('/\d+(?:[,.]\d+)?|[\p{L}\p{N}]+/u', $value, $matches) !== 1) {
return [];
@@ -2077,7 +2069,7 @@ final readonly class AgentRunner
private function tokenizeMetaGuardText(string $value): array
{
$value = mb_strtolower(trim($value), 'UTF-8');
$value = str_replace(['-', '/', '_'], ' ', $value);
$value = $this->languageCleanupConfig->replaceWordSeparatorsWithSpace($value);
$value = preg_replace('/[^\p{L}\p{N}]+/u', ' ', $value) ?? $value;
$value = preg_replace('/\s+/u', ' ', $value) ?? $value;
$value = trim($value);
@@ -2232,7 +2224,10 @@ final readonly class AgentRunner
}
$template = $this->agentRunnerConfig->getShopQueryContextAnchorEnrichmentTemplate();
$enriched = str_replace(['{anchor}', '{query}'], [$anchor, $query], $template);
$enriched = $this->renderAgentTemplate($template, [
'anchor' => $anchor,
'query' => $query,
]);
$enriched = preg_replace('/\s+/u', ' ', $enriched) ?? $enriched;
return trim($enriched) !== '' ? trim($enriched) : $query;
@@ -2505,7 +2500,10 @@ final readonly class AgentRunner
: $this->agentRunnerConfig->getNoLlmFallbackShopUnavailableNoKnowledgeMessage();
if ($reason !== '') {
$message .= ' Ursache: ' . $reason;
$message = $this->renderAgentTemplate($this->agentRunnerConfig->getNoLlmProductField('unavailable_reason_template'), [
'message' => $message,
'reason' => $reason,
]);
}
return trim($message);
@@ -2542,7 +2540,7 @@ final readonly class AgentRunner
}
if ($lines === []) {
return ['- Es wurden Shop-Treffer übergeben, aber keine lesbaren Produktdaten gefunden.'];
return [$this->agentRunnerConfig->getNoLlmProductField('unreadable_results_message')];
}
return $lines;
@@ -2554,33 +2552,36 @@ final readonly class AgentRunner
$productRole = $this->resolveNoLlmShopProductRole($product);
$name = $this->normalizeOneLine($product->name);
$parts[] = $name !== '' ? $name : 'Unbenanntes Shop-Produkt';
$parts[] = $name !== '' ? $name : $this->agentRunnerConfig->getNoLlmProductField('unnamed_product');
if ($product->productNumber !== null && trim($product->productNumber) !== '') {
$parts[] = 'Art.-Nr. ' . $this->normalizeOneLine($product->productNumber);
$parts[] = $this->renderAgentTemplate($this->agentRunnerConfig->getNoLlmProductField('product_number_template'), ['value' => $this->normalizeOneLine($product->productNumber)]);
}
if ($product->manufacturer !== null && trim($product->manufacturer) !== '') {
$parts[] = 'Hersteller: ' . $this->normalizeOneLine($product->manufacturer);
$parts[] = $this->renderAgentTemplate($this->agentRunnerConfig->getNoLlmProductField('manufacturer_template'), ['value' => $this->normalizeOneLine($product->manufacturer)]);
}
if ($product->price !== null && trim($product->price) !== '') {
$parts[] = 'Preis: ' . $this->normalizeOneLine($product->price);
$parts[] = $this->renderAgentTemplate($this->agentRunnerConfig->getNoLlmProductField('price_template'), ['value' => $this->normalizeOneLine($product->price)]);
}
if ($product->available !== null) {
$parts[] = 'Verfügbar: ' . ($product->available ? 'ja' : 'nein');
$parts[] = $this->renderAgentTemplate($this->agentRunnerConfig->getNoLlmProductField('availability_template'), ['value' => $product->available ? $this->agentRunnerConfig->getNoLlmProductField('availability_yes') : $this->agentRunnerConfig->getNoLlmProductField('availability_no')]);
}
if ($product->url !== null && trim($product->url) !== '') {
$parts[] = 'URL: ' . $this->normalizeOneLine($product->url);
$parts[] = $this->renderAgentTemplate($this->agentRunnerConfig->getNoLlmProductField('url_template'), ['value' => $this->normalizeOneLine($product->url)]);
}
if ($requestedProductRole === 'main_device_or_system' && $productRole === 'accessory_or_consumable') {
$parts[] = 'Hinweis: Zubehör/Verbrauchsartikel; nicht als Messanlage/Gerät bestätigt';
$parts[] = $this->agentRunnerConfig->getNoLlmProductField('incompatible_role_note');
}
return sprintf('%d. %s', $index, implode(' | ', $parts));
return $this->renderAgentTemplate($this->agentRunnerConfig->getNoLlmProductField('line_template'), [
'index' => (string) $index,
'parts' => implode($this->agentRunnerConfig->getNoLlmProductField('separator'), $parts),
]);
}
/**
@@ -2722,20 +2723,20 @@ final readonly class AgentRunner
private function resolveRagEvidenceConfidenceLabel(string $knowledgeEvidenceState): string
{
return match ($knowledgeEvidenceState) {
'direct' => 'fachlich belegt',
'aggregate_missing' => 'geprüfte Quellen, keine passende Zählinformation',
'weak' => 'RAG-Näherungstreffer, kein direkter Fachbeleg',
default => 'noch keine belastbaren Treffer',
'direct' => $this->agentRunnerConfig->getProductionUiConfidenceLabel('direct'),
'aggregate_missing' => $this->agentRunnerConfig->getProductionUiConfidenceLabel('aggregate_missing'),
'weak' => $this->agentRunnerConfig->getProductionUiConfidenceLabel('weak'),
default => $this->agentRunnerConfig->getProductionUiConfidenceLabel('default'),
};
}
private function resolveRagEvidenceShopCheckConfidenceLabel(string $knowledgeEvidenceState): string
{
return match ($knowledgeEvidenceState) {
'direct' => 'fachlich belegt; Shopdaten werden geprüft',
'aggregate_missing' => 'geprüfte Quellen ohne Zählinformation; Shopdaten werden geprüft',
'weak' => 'RAG-Näherungstreffer; Shopdaten werden geprüft',
default => 'Shopdaten werden geprüft',
'direct' => $this->agentRunnerConfig->getProductionUiConfidenceLabel('direct_shop_check'),
'aggregate_missing' => $this->agentRunnerConfig->getProductionUiConfidenceLabel('aggregate_missing_shop_check'),
'weak' => $this->agentRunnerConfig->getProductionUiConfidenceLabel('weak_shop_check'),
default => $this->agentRunnerConfig->getProductionUiConfidenceLabel('default_shop_check'),
};
}
@@ -2902,13 +2903,8 @@ final readonly class AgentRunner
{
$value = html_entity_decode(strip_tags($value), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
$value = mb_strtolower($value, 'UTF-8');
$value = str_replace(['', '', '', '', '—'], '-', $value);
$value = strtr($value, [
'ä' => 'ae',
'ö' => 'oe',
'ü' => 'ue',
'ß' => 'ss',
]);
$value = $this->languageCleanupConfig->normalizeDashEquivalents($value);
$value = $this->languageCleanupConfig->transliterateToAscii($value);
$value = preg_replace('/\s+/u', ' ', $value) ?? $value;
return trim($value);
@@ -2957,7 +2953,7 @@ final readonly class AgentRunner
$noLlmMessage = $this->plainTextFromHtml($this->agentRunnerConfig->getNoLlmDataReceivedMessage());
if ($noLlmMessage === '') {
$noLlmMessage = 'Es wurden keine Daten vom LLM empfangen.';
$noLlmMessage = $this->agentRunnerConfig->getProductionUiText('no_llm_history_default');
}
$parts[] = 'Systemhinweis: ' . $noLlmMessage;
@@ -2972,18 +2968,21 @@ final readonly class AgentRunner
$detail = $this->normalizeOneLine($this->plainTextFromHtml((string) $detail));
if ($title === '') {
$title = 'Systemhinweis';
$title = $this->agentRunnerConfig->getProductionUiText('history_notice_default_title');
}
if ($detail === '') {
return 'Systemhinweis: ' . $title . '.';
return $this->renderAgentTemplate($this->agentRunnerConfig->getProductionUiTemplate('history_notice_without_detail'), ['title' => $title]);
}
if (mb_strlen($detail, 'UTF-8') > 500) {
$detail = rtrim(mb_substr($detail, 0, 497, 'UTF-8')) . '...';
}
return 'Systemhinweis: ' . $title . '. Ursache: ' . $detail;
return $this->renderAgentTemplate($this->agentRunnerConfig->getProductionUiTemplate('history_notice_with_detail'), [
'title' => $title,
'detail' => $detail,
]);
}
private function plainTextFromHtml(string $value): string
@@ -3033,32 +3032,34 @@ final readonly class AgentRunner
): string {
$state = $completed ? 'completed' : 'running';
$ragLabel = $ragCount === null
? 'RAG-Treffer: wird geprüft'
: 'RAG-Treffer: ' . max(0, $ragCount);
? $this->agentRunnerConfig->getProductionUiText('rag_hits_checking')
: $this->renderAgentTemplate($this->agentRunnerConfig->getProductionUiTemplate('rag_hits_count'), ['count' => (string) max(0, $ragCount)]);
$shopLabel = match ($shopCountMode) {
'count' => 'Shop-Treffer: ' . max(0, (int) $shopCount),
'loading' => 'Shop-Treffer: wird geladen',
'unavailable' => 'Shop-Treffer: nicht verfügbar',
'not_resolved' => 'Shop-Treffer: keine Suchquery',
default => 'Shop-Treffer: nicht angefragt',
'count' => $this->renderAgentTemplate($this->agentRunnerConfig->getProductionUiTemplate('shop_hits_count'), ['count' => (string) max(0, (int) $shopCount)]),
'loading' => $this->agentRunnerConfig->getProductionUiText('shop_hits_loading'),
'unavailable' => $this->agentRunnerConfig->getProductionUiText('shop_hits_unavailable'),
'not_resolved' => $this->agentRunnerConfig->getProductionUiText('shop_hits_no_query'),
default => $this->agentRunnerConfig->getProductionUiText('shop_hits_not_requested'),
};
$statusLabel = $completed ? 'Status: abgeschlossen' : 'Status: läuft';
$statusLabel = $completed
? $this->agentRunnerConfig->getProductionUiText('status_completed')
: $this->agentRunnerConfig->getProductionUiText('status_running');
$sources = $this->formatProductionUiSourceLabels($sourceLabels);
$html = '<div class="retriex-meta-card retriex-run-meta" data-retriex-meta-id="run-status" data-retriex-meta-state="'
. htmlspecialchars($state, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')
. '">'
. '<div class="retriex-meta-card__eyebrow">RetrieX-Status</div>'
. '<div class="retriex-meta-card__eyebrow">' . htmlspecialchars($this->agentRunnerConfig->getProductionUiText('run_status_eyebrow'), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</div>'
. '<div class="retriex-meta-card__title">' . htmlspecialchars($stageLabel, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</div>'
. '<div class="retriex-meta-card__body">'
. '<span class="retriex-meta-pill retriex-meta-pill--status">' . htmlspecialchars($statusLabel, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</span>'
. '<span class="retriex-meta-pill retriex-meta-pill--result">' . htmlspecialchars($ragLabel, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</span>'
. '<span class="retriex-meta-pill retriex-meta-pill--result">' . htmlspecialchars($shopLabel, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</span>'
. '<span class="retriex-meta-pill retriex-meta-pill--confidence">Beleglage: ' . htmlspecialchars($confidenceLabel, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</span>'
. '<span class="retriex-meta-pill retriex-meta-pill--confidence">' . htmlspecialchars($this->agentRunnerConfig->getProductionUiText('evidence_prefix'), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '' . htmlspecialchars($confidenceLabel, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</span>'
. '</div>';
if ($sources !== []) {
$html .= '<div class="retriex-source-overview"><span>Datenbasis</span><div class="retriex-source-overview__badges">';
$html .= '<div class="retriex-source-overview"><span>' . htmlspecialchars($this->agentRunnerConfig->getProductionUiText('data_basis_label'), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</span><div class="retriex-source-overview__badges">';
foreach ($sources as $source) {
$html .= '<span class="retriex-source-chip">' . htmlspecialchars($source, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</span>';
@@ -3066,8 +3067,10 @@ final readonly class AgentRunner
$html .= '</div></div>';
} else {
$emptySourceLabel = $completed ? 'keine belastbare Datenbasis' : 'wird geprüft';
$html .= '<div class="retriex-source-overview"><span>Datenbasis</span><div class="retriex-source-overview__empty">'
$emptySourceLabel = $completed
? $this->agentRunnerConfig->getProductionUiText('data_basis_empty_completed')
: $this->agentRunnerConfig->getProductionUiText('data_basis_empty_running');
$html .= '<div class="retriex-source-overview"><span>' . htmlspecialchars($this->agentRunnerConfig->getProductionUiText('data_basis_label'), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</span><div class="retriex-source-overview__empty">'
. htmlspecialchars($emptySourceLabel, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')
. '</div></div>';
}
@@ -3087,35 +3090,37 @@ final readonly class AgentRunner
): string {
if ($knowledgeEvidenceState === 'aggregate_missing' && !$hasShopResults) {
return $shopSearchHadSystemFailure
? 'geprüfte Quellen ohne Zählinformation; Shopdaten nicht verfügbar'
: 'geprüfte Quellen, keine passende Zählinformation';
? $this->agentRunnerConfig->getProductionUiConfidenceLabel('aggregate_missing_shop_unavailable')
: $this->agentRunnerConfig->getProductionUiConfidenceLabel('aggregate_missing_no_count');
}
if ($shopSearchHadSystemFailure) {
return $hasKnowledge ? 'fachlich belegt; Shopdaten nicht verfügbar' : 'Shopdaten nicht verfügbar';
return $hasKnowledge
? $this->agentRunnerConfig->getProductionUiConfidenceLabel('shop_unavailable_with_knowledge')
: $this->agentRunnerConfig->getProductionUiConfidenceLabel('shop_unavailable');
}
if ($hasKnowledge && $hasShopResults) {
return 'RAG + Shopdaten';
return $this->agentRunnerConfig->getProductionUiConfidenceLabel('rag_and_shop');
}
if (!$hasKnowledge && $hasShopResults) {
return 'nur Shopdaten';
return $this->agentRunnerConfig->getProductionUiConfidenceLabel('shop_only');
}
if ($hasKnowledge && $shopSearchAttempted) {
return 'RAG-Wissen, keine Shop-Treffer';
return $this->agentRunnerConfig->getProductionUiConfidenceLabel('rag_no_shop_hits');
}
if ($hasKnowledge) {
return 'fachlich belegt';
return $this->agentRunnerConfig->getProductionUiConfidenceLabel('direct');
}
if ($isCommerceIntent || $shopSearchAttempted) {
return 'keine belastbaren Daten';
return $this->agentRunnerConfig->getProductionUiConfidenceLabel('no_reliable_data');
}
return 'noch keine belastbaren Treffer';
return $this->agentRunnerConfig->getProductionUiConfidenceLabel('no_reliable_hits');
}
/**
@@ -3137,7 +3142,7 @@ final readonly class AgentRunner
}
if ($label === $this->plainTextFromHtml($this->agentRunnerConfig->getShopSystemSourceLabel())) {
$label = 'Live-Shopdaten';
$label = $this->agentRunnerConfig->getProductionUiText('live_shop_source_plain_label');
}
if (!in_array($label, $labels, true)) {
@@ -3153,27 +3158,27 @@ final readonly class AgentRunner
*/
private function buildShopProductCardsMessage(array $shopResults, string $query, bool $usedRepair): string
{
$maxCards = 5;
$maxCards = max(1, $this->agentRunnerConfig->getProductionUiShopResultsMaxCards());
$visibleResults = array_slice($shopResults, 0, $maxCards);
$totalCount = count($shopResults);
$query = $this->normalizeOneLine($query);
$summary = $totalCount . ' Shop-Treffer ausgewertet';
$summary = $this->renderAgentTemplate($this->agentRunnerConfig->getProductionUiTemplate('shop_results_summary'), ['count' => (string) $totalCount]);
if ($totalCount > $maxCards) {
$summary .= ' · Top ' . $maxCards . ' angezeigt';
$summary .= $this->renderAgentTemplate($this->agentRunnerConfig->getProductionUiTemplate('shop_results_top_displayed_suffix'), ['max' => (string) $maxCards]);
}
if ($usedRepair) {
$summary .= ' · erweiterte Shopsuche genutzt';
$summary .= $this->agentRunnerConfig->getProductionUiTemplate('shop_results_repair_suffix');
}
$html = '<div class="retriex-meta-card retriex-product-results" data-retriex-meta-id="shop-results" data-retriex-meta-state="completed">'
. '<div class="retriex-meta-card__eyebrow">Shop-Ergebnisse</div>'
. '<div class="retriex-meta-card__title">Shop-Ergebnisse</div>'
. '<div class="retriex-meta-card__eyebrow">' . htmlspecialchars($this->agentRunnerConfig->getProductionUiText('shop_results_eyebrow'), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</div>'
. '<div class="retriex-meta-card__title">' . htmlspecialchars($this->agentRunnerConfig->getProductionUiText('shop_results_title'), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</div>'
. '<div class="retriex-product-results__summary">' . htmlspecialchars($summary, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</div>';
if ($query !== '') {
$html .= '<div class="retriex-meta-query retriex-meta-query--compact"><span>Ausgewertete Suchquery</span><code>'
$html .= '<div class="retriex-meta-query retriex-meta-query--compact"><span>' . htmlspecialchars($this->agentRunnerConfig->getProductionUiText('evaluated_query_label'), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</span><code>'
. htmlspecialchars($query, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')
. '</code></div>';
}
@@ -3195,7 +3200,7 @@ final readonly class AgentRunner
private function buildShopProductCard(ShopProductResult $product, string $query): string
{
$name = $this->normalizeOneLine($product->name) ?: 'Unbenanntes Produkt';
$name = $this->normalizeOneLine($product->name) ?: $this->agentRunnerConfig->getProductionUiText('unnamed_product');
$productNumber = $this->normalizeOneLine((string) $product->productNumber);
$manufacturer = $this->normalizeOneLine((string) $product->manufacturer);
$price = $this->normalizeOneLine((string) $product->price);
@@ -3215,16 +3220,16 @@ final readonly class AgentRunner
}
$html .= '</div><dl class="retriex-product-card__facts">';
$html .= '<div><dt>Artikelnummer</dt><dd>' . htmlspecialchars($productNumber !== '' ? $productNumber : 'nicht übermittelt', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</dd></div>';
$html .= '<div><dt>Preis</dt><dd>' . htmlspecialchars($price !== '' ? $price : 'nicht übermittelt', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</dd></div>';
$html .= '<div><dt>Verfügbarkeit</dt><dd>' . htmlspecialchars($availability, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</dd></div>';
$html .= '<div><dt>' . htmlspecialchars($this->agentRunnerConfig->getProductionUiText('product_number_label'), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</dt><dd>' . htmlspecialchars($productNumber !== '' ? $productNumber : $this->agentRunnerConfig->getProductionUiText('field_not_provided'), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</dd></div>';
$html .= '<div><dt>' . htmlspecialchars($this->agentRunnerConfig->getProductionUiText('price_label'), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</dt><dd>' . htmlspecialchars($price !== '' ? $price : $this->agentRunnerConfig->getProductionUiText('field_not_provided'), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</dd></div>';
$html .= '<div><dt>' . htmlspecialchars($this->agentRunnerConfig->getProductionUiText('availability_label'), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</dt><dd>' . htmlspecialchars($availability, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</dd></div>';
if ($manufacturer !== '') {
$html .= '<div><dt>Hersteller</dt><dd>' . htmlspecialchars($manufacturer, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</dd></div>';
$html .= '<div><dt>' . htmlspecialchars($this->agentRunnerConfig->getProductionUiText('manufacturer_label'), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</dt><dd>' . htmlspecialchars($manufacturer, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</dd></div>';
}
$html .= '</dl>'
. '<div class="retriex-product-card__relevance"><span>Relevanz</span>'
. '<div class="retriex-product-card__relevance"><span>' . htmlspecialchars($this->agentRunnerConfig->getProductionUiText('relevance_label'), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</span>'
. htmlspecialchars($relevance, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')
. '</div>'
. '</article>';
@@ -3235,9 +3240,9 @@ final readonly class AgentRunner
private function formatProductAvailability(?bool $available): string
{
return match ($available) {
true => 'verfügbar',
false => 'nicht verfügbar',
default => 'Shopstatus nicht übermittelt',
true => $this->agentRunnerConfig->getProductionUiText('availability_yes'),
false => $this->agentRunnerConfig->getProductionUiText('availability_no'),
default => $this->agentRunnerConfig->getProductionUiText('availability_unknown'),
};
}
@@ -3254,28 +3259,32 @@ final readonly class AgentRunner
}
if ($matchedQueries !== []) {
return 'Gefunden über: ' . implode(', ', array_slice($matchedQueries, 0, 3));
return $this->renderAgentTemplate($this->agentRunnerConfig->getProductionUiTemplate('relevance_matched_queries'), [
'queries' => implode(', ', array_slice($matchedQueries, 0, 3)),
]);
}
foreach ($product->highlights as $highlight) {
$highlight = $this->normalizeOneLine($this->plainTextFromHtml((string) $highlight));
if ($highlight !== '') {
return 'Passender Shop-Hinweis: ' . mb_substr($highlight, 0, 140, 'UTF-8');
return $this->renderAgentTemplate($this->agentRunnerConfig->getProductionUiTemplate('relevance_highlight'), [
'highlight' => mb_substr($highlight, 0, 140, 'UTF-8'),
]);
}
}
$matchSource = $this->normalizeOneLine((string) $product->matchSource);
if ($matchSource !== '') {
return 'Trefferquelle: ' . $matchSource;
return $this->renderAgentTemplate($this->agentRunnerConfig->getProductionUiTemplate('relevance_match_source'), ['source' => $matchSource]);
}
if ($query !== '') {
return 'Passend zur Suchquery: ' . $query;
return $this->renderAgentTemplate($this->agentRunnerConfig->getProductionUiTemplate('relevance_query'), ['query' => $query]);
}
return 'Aus den Live-Shopdaten übernommen';
return $this->agentRunnerConfig->getProductionUiTemplate('relevance_default');
}
private function buildFollowUpActionsMessage(bool $isCommerceIntent, bool $hasShopResults, bool $hasKnowledge): string
@@ -3287,14 +3296,11 @@ final readonly class AgentRunner
$actions = [];
if ($isCommerceIntent || $hasShopResults) {
$actions[] = ['Im Shop suchen', 'Suche die aktuelle Produktauswahl im Shop.'];
$actions[] = ['Nur Zubehör anzeigen', 'Zeige aus der aktuellen Produktauswahl nur Zubehör.'];
$actions[] = ['Nur Geräte anzeigen', 'Zeige aus der aktuellen Produktauswahl nur Geräte.'];
$actions[] = ['Preis anzeigen', 'Zeige mir die Preise der aktuell relevanten Produkte.'];
$actions = array_merge($actions, $this->agentRunnerConfig->getProductionUiFollowUpActions('commerce'));
}
if ($hasKnowledge || $hasShopResults) {
$actions[] = ['Technische Details anzeigen', 'Zeige technische Details zur aktuellen Antwort.'];
$actions = array_merge($actions, $this->agentRunnerConfig->getProductionUiFollowUpActions('knowledge'));
}
if ($actions === []) {
@@ -3302,11 +3308,16 @@ final readonly class AgentRunner
}
$html = '<div class="retriex-meta-card retriex-followup-actions" data-retriex-meta-id="followup-actions" data-retriex-meta-state="completed">'
. '<div class="retriex-meta-card__eyebrow">Folgeaktionen</div>'
. '<div class="retriex-meta-card__title">Was möchtest du als Nächstes tun?</div>'
. '<div class="retriex-meta-card__eyebrow">' . htmlspecialchars($this->agentRunnerConfig->getProductionUiText('followup_eyebrow'), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</div>'
. '<div class="retriex-meta-card__title">' . htmlspecialchars($this->agentRunnerConfig->getProductionUiText('followup_title'), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</div>'
. '<div class="retriex-action-chip-row">';
foreach ($actions as [$label, $actionPrompt]) {
foreach ($actions as $action) {
$label = (string) ($action['label'] ?? '');
$actionPrompt = (string) ($action['prompt'] ?? '');
if ($label === '' || $actionPrompt === '') {
continue;
}
$html .= '<button type="button" class="retriex-action-chip" data-retriex-action-prompt="'
. htmlspecialchars($actionPrompt, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')
. '">'
@@ -3334,26 +3345,32 @@ final readonly class AgentRunner
$originalQuery = $this->normalizeOneLine($originalQuery);
if ($query === '') {
$query = $originalQuery !== '' ? $originalQuery : 'keine Suchquery ermittelt';
$query = $originalQuery !== '' ? $originalQuery : $this->agentRunnerConfig->getProductionUiText('shop_meta_fallback_query');
}
$queryModeLabel = $usedOptimizedQuery ? 'optimiert' : 'direkt';
$intentLabel = $commerceIntent !== '' ? $commerceIntent : 'commerce';
$title = $unavailable ? 'Shopdaten nicht verfügbar' : ($completed ? 'Shop-Suche abgeschlossen' : 'Shop-Suche wird ausgeführt');
$statusLabel = $completed ? 'Status: abgeschlossen' : 'Status: läuft';
$queryModeLabel = $usedOptimizedQuery ? $this->agentRunnerConfig->getProductionUiText('shop_meta_query_mode_optimized') : $this->agentRunnerConfig->getProductionUiText('shop_meta_query_mode_direct');
$intentLabel = $commerceIntent !== '' ? $commerceIntent : $this->agentRunnerConfig->getProductionUiText('shop_meta_default_intent');
$title = $unavailable
? $this->agentRunnerConfig->getProductionUiText('shop_meta_title_unavailable')
: ($completed
? $this->agentRunnerConfig->getProductionUiText('shop_meta_title_completed')
: $this->agentRunnerConfig->getProductionUiText('shop_meta_title_running'));
$statusLabel = $completed
? $this->agentRunnerConfig->getProductionUiText('shop_meta_status_completed')
: $this->agentRunnerConfig->getProductionUiText('shop_meta_status_running');
$resultLabel = $unavailable
? 'Shoptreffer: nicht verfügbar'
? $this->agentRunnerConfig->getProductionUiText('shop_meta_result_unavailable')
: ($resultCount === null
? 'Shoptreffer: wird geladen'
: 'Shoptreffer: ' . max(0, $resultCount));
? $this->agentRunnerConfig->getProductionUiText('shop_meta_result_loading')
: $this->renderAgentTemplate($this->agentRunnerConfig->getProductionUiTemplate('shop_meta_result_count'), ['count' => (string) max(0, $resultCount)]));
$state = $completed ? 'completed' : 'running';
$resultCountAttribute = $resultCount === null ? '' : (string) max(0, $resultCount);
$repairLabel = '';
if ($usedRepair) {
$repairLabel = 'Erweiterte Suche: genutzt';
$repairLabel = $this->agentRunnerConfig->getProductionUiText('shop_meta_repair_used');
} elseif ($attemptedRepair) {
$repairLabel = 'Erweiterte Suche: geprüft';
$repairLabel = $this->agentRunnerConfig->getProductionUiText('shop_meta_repair_checked');
}
$html = '<div class="retriex-meta-card retriex-shop-meta" data-retriex-meta-id="shop-search" data-retriex-meta-state="'
@@ -3361,20 +3378,20 @@ final readonly class AgentRunner
. '" data-retriex-shop-result-count="'
. htmlspecialchars($resultCountAttribute, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')
. '">'
. '<div class="retriex-meta-card__eyebrow">Shop-Suche</div>'
. '<div class="retriex-meta-card__eyebrow">' . htmlspecialchars($this->agentRunnerConfig->getProductionUiText('shop_meta_eyebrow'), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</div>'
. '<div class="retriex-meta-card__title">' . htmlspecialchars($title, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</div>'
. '<div class="retriex-meta-card__body">'
. '<span class="retriex-meta-pill retriex-meta-pill--result">' . htmlspecialchars($resultLabel, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</span>'
. '<span class="retriex-meta-pill">' . htmlspecialchars($statusLabel, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</span>'
. '<span class="retriex-meta-pill">Query: ' . htmlspecialchars($queryModeLabel, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</span>'
. '<span class="retriex-meta-pill">Intent: ' . htmlspecialchars($intentLabel, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</span>';
. '<span class="retriex-meta-pill">' . htmlspecialchars($this->agentRunnerConfig->getProductionUiText('shop_meta_query_prefix'), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . htmlspecialchars($queryModeLabel, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</span>'
. '<span class="retriex-meta-pill">' . htmlspecialchars($this->agentRunnerConfig->getProductionUiText('shop_meta_intent_prefix'), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . htmlspecialchars($intentLabel, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</span>';
if ($repairLabel !== '') {
$html .= '<span class="retriex-meta-pill">' . htmlspecialchars($repairLabel, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</span>';
}
$html .= '</div>'
. '<div class="retriex-meta-query"><span>Gesendete Suchquery</span><code>'
. '<div class="retriex-meta-query"><span>' . htmlspecialchars($this->agentRunnerConfig->getProductionUiText('shop_meta_query_label'), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</span><code>'
. htmlspecialchars($query, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')
. '</code></div>'
. '</div>';
@@ -3387,7 +3404,7 @@ final readonly class AgentRunner
$reason = $this->normalizeOneLine((string) $reason);
if ($reason === '') {
$reason = 'Keine Detailmeldung vom Shopware-Server.';
$reason = $this->agentRunnerConfig->getProductionUiText('shop_unavailable_default_reason');
}
if (mb_strlen($reason, 'UTF-8') > 320) {
@@ -3397,14 +3414,27 @@ final readonly class AgentRunner
return '<div class="retriex-alert retriex-alert--warning">'
. '<div class="retriex-alert__icon">⚠️</div>'
. '<div class="retriex-alert__content">'
. '<div class="retriex-alert__title">Shopdaten konnten nicht geladen werden</div>'
. '<div class="retriex-alert__text">RetrieX antwortet ohne Live-Shopdaten weiter. Ursache: '
. '<div class="retriex-alert__title">' . htmlspecialchars($this->agentRunnerConfig->getProductionUiText('shop_unavailable_title'), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</div>'
. '<div class="retriex-alert__text">' . htmlspecialchars($this->agentRunnerConfig->getProductionUiText('shop_unavailable_text_prefix'), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . ''
. htmlspecialchars($reason, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')
. '</div>'
. '</div>'
. '</div>';
}
/**
* @param array<string, string> $values
*/
private function renderAgentTemplate(string $template, array $values): string
{
foreach ($values as $key => $value) {
$template = str_replace('{' . $key . '}', $value, $template);
}
return $template;
}
private function normalizeOneLine(string $value): string
{
$value = trim($value);