From 316a5b5cc252c3b1d603143161532ea51428ee02 Mon Sep 17 00:00:00 2001 From: team2 Date: Mon, 27 Apr 2026 21:09:38 +0200 Subject: [PATCH] add shop result counter into response --- config/retriex/commerce.yaml | 2 +- public/assets/js/base.js | 25 ++++++++++ src/Agent/AgentRunner.php | 92 ++++++++++++++++++++++++++---------- 3 files changed, 94 insertions(+), 25 deletions(-) diff --git a/config/retriex/commerce.yaml b/config/retriex/commerce.yaml index 2b6713b..0b09b94 100644 --- a/config/retriex/commerce.yaml +++ b/config/retriex/commerce.yaml @@ -3,7 +3,7 @@ parameters: retriex.commerce.enabled: true retriex.commerce.max_shop_results: '%env(SHOPWARE_STORE_API_MAX_RESULT)%' - retriex.commerce.shop_timeout: 3 + retriex.commerce.shop_timeout: 15 retriex.commerce.store_api_base_url: '%env(SHOPWARE_STORE_API_BASE_URL)%' retriex.commerce.sales_channel_access_key: '%env(SHOPWARE_SALES_CHANNEL_ACCESS_KEY)%' diff --git a/public/assets/js/base.js b/public/assets/js/base.js index 13be49a..d0144c6 100644 --- a/public/assets/js/base.js +++ b/public/assets/js/base.js @@ -414,9 +414,33 @@ document.addEventListener('DOMContentLoaded', () => { removeThinkSpansOnly(container); } + function deduplicateRetriexMetaCards(container) { + if (!container) { + return; + } + + const cards = Array.from(container.querySelectorAll('.retriex-meta-card[data-retriex-meta-id]')); + const latestCardById = new Map(); + + cards.forEach((card) => { + latestCardById.set(card.getAttribute('data-retriex-meta-id'), card); + }); + + cards.forEach((card) => { + const cardId = card.getAttribute('data-retriex-meta-id'); + + if (latestCardById.get(cardId) !== card) { + card.remove(); + } + }); + + cleanupEmptyBlocks(container); + } + function renderBubbleContent(bubble, raw) { bubble.innerHTML = renderMarkdown(raw); cleanupThinkSpans(bubble); + deduplicateRetriexMetaCards(bubble); enhanceChatLinks(bubble); scrollChatToBottom(); } @@ -495,6 +519,7 @@ document.addEventListener('DOMContentLoaded', () => { clearScheduledRender(); bubble.innerHTML = renderMarkdown(raw); removeThinkSpansOnly(bubble); + deduplicateRetriexMetaCards(bubble); bubble.classList.remove('loader'); enhanceChatLinks(bubble); scrollChatToBottom(); diff --git a/src/Agent/AgentRunner.php b/src/Agent/AgentRunner.php index 9c4bb69..3a39b40 100644 --- a/src/Agent/AgentRunner.php +++ b/src/Agent/AgentRunner.php @@ -53,6 +53,8 @@ final readonly class AgentRunner $sources = []; $optimizedShopQuery = ''; $shopSearchQuery = ''; + $shopSearchDisplayQuery = ''; + $shopSearchUsedOptimizedQuery = false; $commerceIntent = CommerceIntentLite::NONE; $knowledgeRetrievalPrompt = $prompt; $usedFollowUpRetrievalContext = false; @@ -127,7 +129,6 @@ final readonly class AgentRunner commerceHistoryContext: $commerceHistoryContext, userId: $userId ); - $usedResolvedOptimizedShopQuery = $optimizedShopQuery !== '' && $shopSearchQuery === $optimizedShopQuery; if ($shopSearchQuery === '') { $this->agentLogger->info('Commerce search skipped because no concrete shop query could be resolved', [ @@ -154,11 +155,16 @@ final readonly class AgentRunner $commerceHistoryContext ); + $shopSearchDisplayQuery = $shopQueryPreview->searchText !== '' + ? $shopQueryPreview->searchText + : $shopSearchQuery; + $shopSearchUsedOptimizedQuery = $optimizedShopQuery !== ''; + yield $this->systemMsg( $this->buildShopSearchMetaMessage( - query: $shopQueryPreview->searchText !== '' ? $shopQueryPreview->searchText : $shopSearchQuery, + query: $shopSearchDisplayQuery, commerceIntent: $commerceIntent, - usedOptimizedQuery: $usedResolvedOptimizedShopQuery, + usedOptimizedQuery: $shopSearchUsedOptimizedQuery, originalQuery: $shopSearchQuery ), 'meta' @@ -167,7 +173,7 @@ final readonly class AgentRunner $this->agentLogger->info('Commerce search prepared', [ 'userId' => $userId, 'commerceIntent' => $commerceIntent, - 'usedOptimizedShopQuery' => $usedResolvedOptimizedShopQuery, + 'usedOptimizedShopQuery' => $optimizedShopQuery !== '', 'optimizedShopQuery' => $optimizedShopQuery, 'shopSearchQuery' => $shopSearchQuery, 'hasCommerceHistoryContext' => $commerceHistoryContext !== '', @@ -232,6 +238,22 @@ final readonly class AgentRunner $usedShopRepair = $repairPayload['usedRepair']; $shopRepairQueries = $repairPayload['repairQueries']; + if ($shopSearchQuery !== '' && !$primaryShopSearchHadSystemFailure) { + yield $this->systemMsg( + $this->buildShopSearchMetaMessage( + query: $shopSearchDisplayQuery !== '' ? $shopSearchDisplayQuery : $shopSearchQuery, + commerceIntent: $commerceIntent, + usedOptimizedQuery: $shopSearchUsedOptimizedQuery, + originalQuery: $shopSearchQuery, + resultCount: count($shopResults), + completed: true, + attemptedRepair: $attemptedShopRepair, + usedRepair: $usedShopRepair + ), + 'meta' + ); + } + if ($shopResults !== []) { $this->addSource($sources, $this->agentRunnerConfig->getShopSystemSourceLabel()); } @@ -483,7 +505,7 @@ final readonly class AgentRunner return []; } - if (preg_match_all('/^Question:\s*(.+)$/mi', $history, $matches) < 1) { + if (preg_match_all('/^Question:\s*(.+)$/mi', $history, $matches) !== 1) { return []; } @@ -724,20 +746,11 @@ final readonly class AgentRunner string $commerceHistoryContext, string $userId ): string { - $promptIsMetaOnly = $this->isMetaOnlyShopQuery($prompt); - - /** - * A pure meta command such as "suche im shop" has no own product - * semantics. In that case the LLM optimizer must not be trusted as the - * primary source because it can copy instruction terms from the query - * prompt itself (for example "Shopware 6"). Resolve meta commands - * deterministically from the recent conversation instead. - */ - if (!$promptIsMetaOnly && $optimizedShopQuery !== '' && !$this->isMetaOnlyShopQuery($optimizedShopQuery)) { + if ($optimizedShopQuery !== '' && !$this->isMetaOnlyShopQuery($optimizedShopQuery)) { return $optimizedShopQuery; } - if (!$promptIsMetaOnly) { + if (!$this->isMetaOnlyShopQuery($prompt)) { return $prompt; } @@ -847,7 +860,7 @@ final readonly class AgentRunner $value = mb_strtolower(trim($value), 'UTF-8'); $value = str_replace(['-', '/', '_'], ' ', $value); - if (preg_match_all('/\d+(?:[,.]\d+)?|[\p{L}\p{N}]+/u', $value, $matches) < 1) { + if (preg_match_all('/\d+(?:[,.]\d+)?|[\p{L}\p{N}]+/u', $value, $matches) !== 1) { return []; } @@ -1294,7 +1307,11 @@ final readonly class AgentRunner string $query, string $commerceIntent, bool $usedOptimizedQuery, - string $originalQuery + string $originalQuery, + ?int $resultCount = null, + bool $completed = false, + bool $attemptedRepair = false, + bool $usedRepair = false ): string { $query = $this->normalizeOneLine($query); $originalQuery = $this->normalizeOneLine($originalQuery); @@ -1303,20 +1320,47 @@ final readonly class AgentRunner $query = $originalQuery !== '' ? $originalQuery : 'keine Suchquery ermittelt'; } - $badge = $usedOptimizedQuery ? 'optimiert' : 'direkt'; + $queryModeLabel = $usedOptimizedQuery ? 'optimiert' : 'direkt'; $intentLabel = $commerceIntent !== '' ? $commerceIntent : 'commerce'; + $title = $completed ? 'Shop-Suche abgeschlossen' : 'Shop-Suche wird ausgeführt'; + $statusLabel = $completed ? 'Status: abgeschlossen' : 'Status: läuft'; + $resultLabel = $resultCount === null + ? 'Shoptreffer: wird geladen' + : 'Shoptreffer: ' . max(0, $resultCount); + $state = $completed ? 'completed' : 'running'; + $resultCountAttribute = $resultCount === null ? '' : (string) max(0, $resultCount); + $repairLabel = ''; - return '
' + if ($usedRepair) { + $repairLabel = 'Erweiterte Suche: genutzt'; + } elseif ($attemptedRepair) { + $repairLabel = 'Erweiterte Suche: geprüft'; + } + + $html = '
' . '
Live-Shopdaten
' - . '
Shop-Suche wird ausgeführt
' + . '
' . htmlspecialchars($title, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '
' . '
' - . '' . htmlspecialchars($badge, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '' - . 'Intent: ' . htmlspecialchars($intentLabel, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '' - . '
' + . '' . htmlspecialchars($resultLabel, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '' + . '' . htmlspecialchars($statusLabel, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '' + . 'Query: ' . htmlspecialchars($queryModeLabel, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '' + . 'Intent: ' . htmlspecialchars($intentLabel, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . ''; + + if ($repairLabel !== '') { + $html .= '' . htmlspecialchars($repairLabel, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . ''; + } + + $html .= '
' . '
Gesendete Suchquery' . htmlspecialchars($query, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '
' . '
'; + + return $html; } private function buildShopUnavailableMessage(?string $reason): string