p38
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace App\Agent;
|
||||
|
||||
use App\Commerce\Dto\ShopProductResult;
|
||||
use App\Config\LanguageCleanupConfig;
|
||||
use App\Config\PromptBuilderConfig;
|
||||
use App\Context\ContextService;
|
||||
use App\Repository\SystemPromptRepository;
|
||||
@@ -19,6 +20,7 @@ final readonly class PromptBuilder
|
||||
private SystemPromptRepository $systemPromptRepository,
|
||||
private ModelGenerationConfigProvider $modelGenerationConfigProvider,
|
||||
private PromptBuilderConfig $config,
|
||||
private LanguageCleanupConfig $languageCleanupConfig,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -335,7 +337,7 @@ final readonly class PromptBuilder
|
||||
}
|
||||
|
||||
if ($hasShopResults && !$commerceSearchAttempted) {
|
||||
$rules[] = '- Treat shop results as provided context only; do not imply that a live shop check was performed in this run.';
|
||||
$rules[] = $this->config->getFallbackEscalationProvidedShopResultsContextRule();
|
||||
}
|
||||
|
||||
if ($rules === []) {
|
||||
@@ -609,7 +611,7 @@ final readonly class PromptBuilder
|
||||
$positiveContextTerms = $this->extractMeasurementGuardStringList($guard, 'positive_context_terms');
|
||||
$negativeContextTerms = $this->extractMeasurementGuardStringList($guard, 'negative_context_terms');
|
||||
$nonEquivalentTerms = $this->extractMeasurementGuardStringList($guard, 'non_equivalent_terms');
|
||||
$label = $this->normalizeBlockText((string) ($guard['label'] ?? 'requested measurement parameter'));
|
||||
$label = $this->normalizeBlockText((string) ($guard['label'] ?? $this->config->getMeasurementEvidenceRuleTemplate('default_requested_parameter_label')));
|
||||
$strictNoEvidence = (bool) ($guard['strict_no_evidence'] ?? true);
|
||||
$resolvedRequestedRole = $requestedRole ?? $this->resolveRequestedProductRole($prompt);
|
||||
$safeNoEvidenceAnswer = $this->normalizeBlockText((string) (
|
||||
@@ -634,58 +636,59 @@ final readonly class PromptBuilder
|
||||
|
||||
if ($hasEvidence) {
|
||||
$shopHasEvidence = true;
|
||||
$shopEvidenceLines[] = sprintf(
|
||||
'- Shop record %d (%s): explicit positive evidence for %s is present in this same record.',
|
||||
$index + 1,
|
||||
$productName !== '' ? $productName : 'unnamed product',
|
||||
$label
|
||||
);
|
||||
$shopEvidenceLines[] = $this->renderPromptTemplate($this->config->getMeasurementEvidenceRuleTemplate('shop_positive_evidence'), [
|
||||
'index' => (string) ($index + 1),
|
||||
'product' => $productName !== '' ? $productName : $this->config->getMeasurementEvidenceRuleTemplate('unnamed_product'),
|
||||
'label' => $label,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
if ($shopEvidenceLines === []) {
|
||||
$shopEvidenceLines[] = sprintf(
|
||||
'- No shop product record shown to the model contains explicit positive evidence for %s in the same record.',
|
||||
$label
|
||||
);
|
||||
$shopEvidenceLines[] = $this->renderPromptTemplate($this->config->getMeasurementEvidenceRuleTemplate('shop_no_evidence'), [
|
||||
'label' => $label,
|
||||
]);
|
||||
}
|
||||
|
||||
$rules = $this->config->getMeasurementEvidenceIntroRules();
|
||||
$rules = array_merge($rules, $this->config->getMeasurementEvidenceProductSpecificRules());
|
||||
$rules[] = '- User requested measurement parameter: ' . $label . '.';
|
||||
$rules[] = '- Positive parameter terms for this request: ' . implode(', ', $positiveTerms) . '.';
|
||||
$rules[] = $this->renderPromptTemplate($this->config->getMeasurementEvidenceRuleTemplate('requested_parameter'), ['label' => $label]);
|
||||
$rules[] = $this->renderPromptTemplate($this->config->getMeasurementEvidenceRuleTemplate('positive_terms'), ['terms' => implode(', ', $positiveTerms)]);
|
||||
if ($positiveContextTerms !== []) {
|
||||
$rules[] = '- These parameter terms count as suitability evidence only in a measurement-purpose context such as: ' . implode(', ', $positiveContextTerms) . '.';
|
||||
$rules[] = $this->renderPromptTemplate($this->config->getMeasurementEvidenceRuleTemplate('positive_context_terms'), ['terms' => implode(', ', $positiveContextTerms)]);
|
||||
}
|
||||
if ($negativeContextTerms !== []) {
|
||||
$rules[] = '- These contexts are not suitability evidence by themselves: ' . implode(', ', $negativeContextTerms) . '.';
|
||||
$rules[] = $this->renderPromptTemplate($this->config->getMeasurementEvidenceRuleTemplate('negative_context_terms'), ['terms' => implode(', ', $negativeContextTerms)]);
|
||||
}
|
||||
|
||||
if ($nonEquivalentTerms !== []) {
|
||||
$rules[] = '- Terms that must NOT be treated as equivalent positive evidence: ' . implode(', ', $nonEquivalentTerms) . '.';
|
||||
$rules[] = $this->renderPromptTemplate($this->config->getMeasurementEvidenceRuleTemplate('non_equivalent_terms'), ['terms' => implode(', ', $nonEquivalentTerms)]);
|
||||
}
|
||||
|
||||
$rules[] = '- RAG/URL evidence scan for this exact parameter: ' . ($knowledgeHasEvidence ? 'explicit positive evidence found.' : 'no explicit positive evidence found.');
|
||||
$rules[] = $this->renderPromptTemplate($this->config->getMeasurementEvidenceRuleTemplate('rag_url_evidence_scan'), [
|
||||
'state' => $knowledgeHasEvidence
|
||||
? $this->config->getMeasurementEvidenceRuleTemplate('rag_url_evidence_found')
|
||||
: $this->config->getMeasurementEvidenceRuleTemplate('rag_url_evidence_missing'),
|
||||
]);
|
||||
$rules = array_merge($rules, $shopEvidenceLines);
|
||||
|
||||
if (!$strictNoEvidence && !$knowledgeHasEvidence && !$shopHasEvidence) {
|
||||
$rules[] = '- The deterministic exact-term scan did not find product-specific evidence. The answer may still use a clearly equivalent named measurement parameter from the same source record, but must not infer suitability from generic categories, document titles, tags, search terms, neighbouring products, or broad umbrella-topic wording.';
|
||||
$rules[] = $this->config->getMeasurementEvidenceRuleTemplate('deterministic_scan_no_product_specific_evidence');
|
||||
}
|
||||
|
||||
if ($strictNoEvidence && !$knowledgeHasEvidence && !$shopHasEvidence) {
|
||||
$rules[] = '- Mandatory answer behavior: do not recommend a product as suitable for this measurement parameter.';
|
||||
$rules[] = $this->config->getMeasurementEvidenceRuleTemplate('mandatory_no_recommendation');
|
||||
if ($safeNoEvidenceAnswer !== '') {
|
||||
$rules[] = '- Start the answer with this meaning in the user language: ' . $safeNoEvidenceAnswer;
|
||||
$rules[] = $this->renderPromptTemplate($this->config->getMeasurementEvidenceRuleTemplate('start_answer_meaning'), ['answer' => $safeNoEvidenceAnswer]);
|
||||
}
|
||||
if ($resolvedRequestedRole === 'accessory_or_consumable') {
|
||||
$rules[] = '- Do not recommend accessories for a different measurement parameter just because they are accessories. If only accessories for other parameters are present, say that only non-matching accessory hits were found.';
|
||||
$rules[] = $this->config->getMeasurementEvidenceRuleTemplate('accessory_mismatch');
|
||||
} else {
|
||||
$rules[] = '- You may list exact shop hits only as commercial/search hits under a heading such as "Shop-Treffer (technische Eignung nicht sicher belegt)".';
|
||||
$rules[] = $this->config->getMeasurementEvidenceRuleTemplate('commercial_hits_only');
|
||||
}
|
||||
}
|
||||
|
||||
$rules[] = '- Do not output measurement ranges, methods, application areas, advantages, or alternative suitable models unless the same source record contains explicit positive evidence for the requested measurement parameter.';
|
||||
$rules[] = '- The generated shop search query, search intent, ranking position, and user question are not factual evidence for product suitability.';
|
||||
$rules = array_merge($rules, $this->config->getMeasurementEvidenceFinalRules());
|
||||
|
||||
return $this->buildRuleBlock(
|
||||
$this->config->getMeasurementEvidenceSectionLabel(),
|
||||
@@ -693,6 +696,19 @@ final readonly class PromptBuilder
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param array<string, string> $values
|
||||
*/
|
||||
private function renderPromptTemplate(string $template, array $values): string
|
||||
{
|
||||
foreach ($values as $key => $value) {
|
||||
$template = str_replace('{' . $key . '}', $value, $template);
|
||||
}
|
||||
|
||||
return $template;
|
||||
}
|
||||
|
||||
private function buildShopMeasurementEvidenceLine(ShopProductResult $product, ?array $guard): string
|
||||
{
|
||||
if ($guard === null) {
|
||||
@@ -702,23 +718,21 @@ final readonly class PromptBuilder
|
||||
$positiveTerms = $this->extractMeasurementGuardStringList($guard, 'positive_terms');
|
||||
$positiveContextTerms = $this->extractMeasurementGuardStringList($guard, 'positive_context_terms');
|
||||
$negativeContextTerms = $this->extractMeasurementGuardStringList($guard, 'negative_context_terms');
|
||||
$label = $this->normalizeBlockText((string) ($guard['label'] ?? 'requested measurement parameter'));
|
||||
$label = $this->normalizeBlockText((string) ($guard['label'] ?? $this->config->getMeasurementEvidenceRuleTemplate('default_requested_parameter_label')));
|
||||
|
||||
if ($positiveTerms === []) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if ($this->shopProductHasMeasurementEvidence($product, $positiveTerms, $positiveContextTerms, $negativeContextTerms)) {
|
||||
return sprintf(
|
||||
'Requested measurement evidence: explicit positive evidence for %s is present in this same SHOP PRODUCT RECORD.',
|
||||
$label
|
||||
);
|
||||
return $this->renderPromptTemplate($this->config->getMeasurementEvidenceRuleTemplate('shop_record_positive_evidence_line'), [
|
||||
'label' => $label,
|
||||
]);
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
'Requested measurement evidence: no explicit positive evidence for %s is present in this SHOP PRODUCT RECORD. Do not present this record as technically suitable for that measurement parameter.',
|
||||
$label
|
||||
);
|
||||
return $this->renderPromptTemplate($this->config->getMeasurementEvidenceRuleTemplate('shop_record_no_evidence_line'), [
|
||||
'label' => $label,
|
||||
]);
|
||||
}
|
||||
|
||||
private function resolveRequestedMeasurementGuard(string $prompt): ?array
|
||||
@@ -815,11 +829,11 @@ final readonly class PromptBuilder
|
||||
return;
|
||||
}
|
||||
|
||||
$parts = preg_split('/\s*(?:,|;|\/|\boder\b|\bund\b|\bor\b|\band\b)\s*/iu', $value) ?: [$value];
|
||||
$parts = preg_split($this->config->getParameterParsingSplitPattern(), $value) ?: [$value];
|
||||
|
||||
foreach ($parts as $part) {
|
||||
$part = $this->normalizeBlockText((string) $part);
|
||||
$part = trim($part, " \t\n\r\0\x0B-–—:()[]{}\"'`“”„");
|
||||
$part = trim($part, $this->config->getParameterParsingTrimCharacters());
|
||||
|
||||
if ($part === '' || preg_match('/[\p{L}\p{N}]/u', $part) !== 1) {
|
||||
continue;
|
||||
@@ -835,7 +849,7 @@ final readonly class PromptBuilder
|
||||
|
||||
private function renderMeasurementEvidenceTemplate(string $template, string $label): string
|
||||
{
|
||||
return strtr($template, ['{label}' => $label]);
|
||||
return $this->renderPromptTemplate($template, ['label' => $label]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -992,7 +1006,7 @@ final readonly class PromptBuilder
|
||||
private function normalizeForMeasurementMatching(string $value): string
|
||||
{
|
||||
$value = mb_strtolower($this->normalizeBlockText($value), 'UTF-8');
|
||||
$value = str_replace(['‐', '‑', '‒', '–', '—'], '-', $value);
|
||||
$value = $this->languageCleanupConfig->normalizeDashEquivalents($value);
|
||||
$value = preg_replace('/<[^>]+>/u', ' ', $value) ?? $value;
|
||||
$value = preg_replace('/\s+/u', ' ', $value) ?? $value;
|
||||
|
||||
|
||||
@@ -135,6 +135,22 @@ final class AgentRunnerConfig
|
||||
return $this->getRequiredString('follow_up_context.reference_anchor.hardness_value_pattern');
|
||||
}
|
||||
|
||||
|
||||
public function getFollowUpContextPreviousUserQuestionTemplate(): string
|
||||
{
|
||||
return $this->getRequiredString('follow_up_context.context_labels.previous_user_question_template');
|
||||
}
|
||||
|
||||
public function getFollowUpContextPreviousReferenceAnchorsTemplate(): string
|
||||
{
|
||||
return $this->getRequiredString('follow_up_context.context_labels.previous_reference_anchors_template');
|
||||
}
|
||||
|
||||
public function getFollowUpContextCurrentQuestionTemplate(): string
|
||||
{
|
||||
return $this->getRequiredString('follow_up_context.context_labels.current_follow_up_question_template');
|
||||
}
|
||||
|
||||
public function isInputNormalizationEnabled(): bool
|
||||
{
|
||||
return $this->getRequiredBool('input_normalization.enabled');
|
||||
@@ -170,6 +186,14 @@ final class AgentRunnerConfig
|
||||
return $this->getRequiredString('input_normalization.output_prefix_pattern');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getInputNormalizationPlaceholderOutputs(): array
|
||||
{
|
||||
return $this->getRequiredStringList('input_normalization.placeholder_outputs');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
@@ -396,6 +420,45 @@ final class AgentRunnerConfig
|
||||
return $out;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return array<int, array{label:string, prompt:string}>
|
||||
*/
|
||||
private function getRequiredActionList(string $key): array
|
||||
{
|
||||
$value = $this->requiredValue($key);
|
||||
|
||||
if (!is_array($value)) {
|
||||
throw new \InvalidArgumentException(sprintf('RetrieX agent config key "%s" must be a list of action definitions.', $key));
|
||||
}
|
||||
|
||||
$out = [];
|
||||
|
||||
foreach ($value as $item) {
|
||||
if (!is_array($item)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$label = isset($item['label']) && is_scalar($item['label']) ? trim((string) $item['label']) : '';
|
||||
$prompt = isset($item['prompt']) && is_scalar($item['prompt']) ? trim((string) $item['prompt']) : '';
|
||||
|
||||
if ($label === '' || $prompt === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$out[] = [
|
||||
'label' => $label,
|
||||
'prompt' => $prompt,
|
||||
];
|
||||
}
|
||||
|
||||
if ($out === []) {
|
||||
throw new \InvalidArgumentException(sprintf('RetrieX agent config key "%s" must contain at least one valid action definition.', $key));
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string[]>
|
||||
*/
|
||||
@@ -637,6 +700,55 @@ final class AgentRunnerConfig
|
||||
return $this->getRequiredString('no_llm_fallback.messages.no_data');
|
||||
}
|
||||
|
||||
|
||||
public function getShopRepairCheckMessage(): string
|
||||
{
|
||||
return $this->getRequiredString('messages.shop_repair_check');
|
||||
}
|
||||
|
||||
public function getShopQueryOptimizationHeartbeatMessage(): string
|
||||
{
|
||||
return $this->getRequiredString('messages.shop_query_optimization_heartbeat');
|
||||
}
|
||||
|
||||
public function getProductionUiStageLabel(string $key): string
|
||||
{
|
||||
return $this->getRequiredString('production_ui.stage_labels.' . $key);
|
||||
}
|
||||
|
||||
public function getProductionUiConfidenceLabel(string $key): string
|
||||
{
|
||||
return $this->getRequiredString('production_ui.confidence_labels.' . $key);
|
||||
}
|
||||
|
||||
public function getProductionUiText(string $key): string
|
||||
{
|
||||
return $this->getRequiredString('production_ui.text.' . $key);
|
||||
}
|
||||
|
||||
public function getProductionUiTemplate(string $key): string
|
||||
{
|
||||
return $this->getRequiredString('production_ui.templates.' . $key);
|
||||
}
|
||||
|
||||
public function getProductionUiShopResultsMaxCards(): int
|
||||
{
|
||||
return $this->getRequiredInt('production_ui.shop_results.max_cards');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array{label:string, prompt:string}>
|
||||
*/
|
||||
public function getProductionUiFollowUpActions(string $group): array
|
||||
{
|
||||
return $this->getRequiredActionList('production_ui.follow_up_actions.' . $group);
|
||||
}
|
||||
|
||||
public function getNoLlmProductField(string $key): string
|
||||
{
|
||||
return $this->getRequiredString('no_llm_fallback.product_fields.' . $key);
|
||||
}
|
||||
|
||||
public function getNoLlmFallbackNoShopResultsWithKnowledgeMessage(): string
|
||||
{
|
||||
return $this->getRequiredString('no_llm_fallback.messages.no_shop_results_with_knowledge');
|
||||
|
||||
@@ -65,6 +65,29 @@ final class LanguageCleanupConfig
|
||||
return strtr($value, $map);
|
||||
}
|
||||
|
||||
|
||||
/** @return string[] */
|
||||
public function getWordSeparatorCharacters(): array
|
||||
{
|
||||
return $this->getNormalizationStringList('word_separator_chars');
|
||||
}
|
||||
|
||||
/** @return string[] */
|
||||
public function getDashEquivalents(): array
|
||||
{
|
||||
return $this->getNormalizationStringList('dash_equivalents');
|
||||
}
|
||||
|
||||
public function replaceWordSeparatorsWithSpace(string $value): string
|
||||
{
|
||||
return str_replace($this->getWordSeparatorCharacters(), ' ', $value);
|
||||
}
|
||||
|
||||
public function normalizeDashEquivalents(string $value): string
|
||||
{
|
||||
return str_replace($this->getDashEquivalents(), '-', $value);
|
||||
}
|
||||
|
||||
/** @return string[] */
|
||||
public function getCleanupProfileNames(): array
|
||||
{
|
||||
|
||||
@@ -306,6 +306,11 @@ final class PromptBuilderConfig
|
||||
return $this->getRequiredString('fallback_escalation.state_line_template');
|
||||
}
|
||||
|
||||
public function getFallbackEscalationProvidedShopResultsContextRule(): string
|
||||
{
|
||||
return $this->getRequiredString('fallback_escalation.provided_shop_results_context_rule');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
@@ -623,6 +628,29 @@ final class PromptBuilderConfig
|
||||
return $this->getRequiredString('measurement_evidence_guard.generic_safe_no_accessory_evidence_answer_template_de');
|
||||
}
|
||||
|
||||
public function getMeasurementEvidenceRuleTemplate(string $key): string
|
||||
{
|
||||
return $this->getRequiredString('measurement_evidence_guard.rule_templates.' . $key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getMeasurementEvidenceFinalRules(): array
|
||||
{
|
||||
return $this->getRequiredStringList('measurement_evidence_guard.final_rules');
|
||||
}
|
||||
|
||||
public function getParameterParsingSplitPattern(): string
|
||||
{
|
||||
return $this->getRequiredString('parameter_parsing.split_pattern');
|
||||
}
|
||||
|
||||
public function getParameterParsingTrimCharacters(): string
|
||||
{
|
||||
return $this->getRequiredString('parameter_parsing.trim_characters');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Knowledge\Retrieval;
|
||||
|
||||
use App\Config\LanguageCleanupConfig;
|
||||
use App\Config\NdjsonHybridRetrieverConfig;
|
||||
use App\Knowledge\ChunkManager;
|
||||
|
||||
@@ -11,7 +12,8 @@ final readonly class NdjsonChunkLookup
|
||||
{
|
||||
public function __construct(
|
||||
private ChunkManager $chunkManager,
|
||||
private NdjsonHybridRetrieverConfig $retrieverConfig
|
||||
private NdjsonHybridRetrieverConfig $retrieverConfig,
|
||||
private LanguageCleanupConfig $languageCleanupConfig
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -416,7 +418,7 @@ final readonly class NdjsonChunkLookup
|
||||
private function normalizeText(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;
|
||||
|
||||
|
||||
@@ -1689,7 +1689,7 @@ final readonly class NdjsonHybridRetriever implements RetrieverInterface
|
||||
private function normalizeText(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;
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Knowledge\Retrieval;
|
||||
|
||||
use App\Config\LanguageCleanupConfig;
|
||||
use App\Knowledge\StopWords;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use SQLite3;
|
||||
@@ -18,6 +19,7 @@ final readonly class NdjsonKeywordRetriever
|
||||
private string $projectDir,
|
||||
private LoggerInterface $agentLogger,
|
||||
private StopWords $stopWords,
|
||||
private LanguageCleanupConfig $languageCleanupConfig,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -177,7 +179,7 @@ final readonly class NdjsonKeywordRetriever
|
||||
private function normalizeText(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;
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Knowledge\Retrieval;
|
||||
|
||||
use App\Config\LanguageCleanupConfig;
|
||||
use App\Knowledge\StopWords;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use SQLite3;
|
||||
@@ -23,6 +24,7 @@ final readonly class NdjsonLexicalIndexBuilder
|
||||
private string $projectDir,
|
||||
private LoggerInterface $agentLogger,
|
||||
private StopWords $stopWords,
|
||||
private LanguageCleanupConfig $languageCleanupConfig,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -350,7 +352,7 @@ final readonly class NdjsonLexicalIndexBuilder
|
||||
private function normalizeText(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;
|
||||
|
||||
|
||||
@@ -4,12 +4,14 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Knowledge\Retrieval;
|
||||
|
||||
use App\Config\LanguageCleanupConfig;
|
||||
use App\Knowledge\StopWords;
|
||||
|
||||
final readonly class QueryCleaner
|
||||
{
|
||||
public function __construct(
|
||||
private StopWords $stopWords
|
||||
private StopWords $stopWords,
|
||||
private LanguageCleanupConfig $languageCleanupConfig
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -33,7 +35,7 @@ final readonly class QueryCleaner
|
||||
$query = mb_strtolower($query, 'UTF-8');
|
||||
|
||||
// 2. Treat hyphens and slashes as word separators
|
||||
$query = str_replace(['-', '/'], ' ', $query);
|
||||
$query = $this->languageCleanupConfig->replaceWordSeparatorsWithSpace($query);
|
||||
|
||||
// 3. Remove special characters, but keep:
|
||||
// - letters
|
||||
|
||||
Reference in New Issue
Block a user