From ced1431a350957df0b4f0eeb6dd3e30cf6c57ade Mon Sep 17 00:00:00 2001 From: team 1 Date: Wed, 6 May 2026 13:51:22 +0200 Subject: [PATCH] fix p51 --- config/retriex/agent.yaml | 5 + config/retriex/vocabulary.yaml | 16 +- ...T_PRODUCT_PRIMARY_IDENTITY_GUARD_README.md | 122 +++++++++++++++ src/Agent/AgentRunner.php | 144 +++++++++++++++++- src/Config/AgentRunnerConfig.php | 5 + 5 files changed, 281 insertions(+), 11 deletions(-) create mode 100644 patch_history/RETRIEX_PATCH_51_DIRECT_PRODUCT_PRIMARY_IDENTITY_GUARD_README.md diff --git a/config/retriex/agent.yaml b/config/retriex/agent.yaml index 4579f8a..5b226f1 100644 --- a/config/retriex/agent.yaml +++ b/config/retriex/agent.yaml @@ -500,6 +500,11 @@ parameters: direct_result_guard: enabled: true + # Direct product-list answers should only list products whose primary + # identity (name/URL) matches the requested product type. This prevents + # devices from being listed as a requested consumable merely because the + # description mentions such consumables as accessories. + prefer_primary_identity_matches: true compound_prefix_match: enabled: true # Some Shopware product names combine the requested product type with diff --git a/config/retriex/vocabulary.yaml b/config/retriex/vocabulary.yaml index f785374..9f1034e 100644 --- a/config/retriex/vocabulary.yaml +++ b/config/retriex/vocabulary.yaml @@ -329,12 +329,6 @@ parameters: - lösung - loesung - solution - - puffer - - pufferlösung - - pufferloesung - - kalibrierpuffer - - kalibrierlösung - - kalibrierloesung - teststreifen - test strip - filter @@ -343,6 +337,12 @@ parameters: - service set - serviceset - service-set + - puffer + - pufferlösung + - pufferloesung + - kalibrierpuffer + - kalibrierlösung + - kalibrierloesung device_product: add: - analysegerät @@ -629,6 +629,10 @@ parameters: - sensor - puffer - kalibrierpuffer + - pufferlösung + - pufferloesung + - kalibrierlösung + - kalibrierloesung direct_product_attribute_stop_terms: include: - direct_product_attribute_stop_terms diff --git a/patch_history/RETRIEX_PATCH_51_DIRECT_PRODUCT_PRIMARY_IDENTITY_GUARD_README.md b/patch_history/RETRIEX_PATCH_51_DIRECT_PRODUCT_PRIMARY_IDENTITY_GUARD_README.md new file mode 100644 index 0000000..046f7ff --- /dev/null +++ b/patch_history/RETRIEX_PATCH_51_DIRECT_PRODUCT_PRIMARY_IDENTITY_GUARD_README.md @@ -0,0 +1,122 @@ +# RetrieX Patch p51 - Direct Product Primary Identity Guard + +## Ziel + +Behebt die verbleibende Regression bei direkten Shop-Produktlisten, bei der eine Anfrage nach Pufferlösungen Geräte/Koffer ausgeben konnte. + +Beispiel: + +```text +welche neomeris puffer gibt es für Kalibrierung von pH-Messgeräten +``` + +Die Shopquery ist korrekt und soll unverändert bleiben: + +```text +neomeris puffer kalibrierung ph messgeräten +``` + +Trotzdem wurden zuletzt nur pH-Messgeräte/Messkoffer ausgegeben, obwohl der Shop Pufferlösungen liefert. + +## Ursache + +Der direkte Produkt-Guard aus p45/p49/p50 hat Produktmatches über den gesamten Produkt-Corpus zugelassen: + +- Produktname +- Beschreibung +- Highlights +- Custom Fields +- URL + +Dadurch konnten Hauptgeräte oder Koffer als Treffer für `puffer` gelten, wenn ihre Beschreibung oder ihr Zubehörtext Pufferlösungen erwähnt. Für eine direkte Produktlistenfrage nach einer Produktart ist das zu breit: Ein Gerät, das Pufferlösungen im Zubehörtext erwähnt, ist selbst keine Pufferlösung. + +## Änderung + +Für direkte Produktlisten mit erkanntem Produkttyp gilt nun: + +1. Es werden zuerst nur Produkte behalten, deren primäre Identität den angefragten Produkttyp matcht. + - primäre Identität = Produktname + Produkt-URL +2. Corpus-Matches über Beschreibung/Custom Fields werden nicht mehr als Ersatzliste ausgegeben, wenn der direkte Guard aktiv ist. +3. Compound-Matches bleiben erhalten: + - `puffer` matcht `pufferlösung` + - `kalibrierpuffer` matcht zusammengesetzte Kalibrierpuffer-Begriffe +4. Puffer-/Kalibrierbegriffe sind weiterhin YAML-konfigurierbare Zubehör-/Verbrauchsmaterialbegriffe. + +Damit werden Produkte wie diese akzeptiert: + +```text +Neomeris pH-Pufferlösung pH 7.00 +Neomeris Redox-Pufferlösung 475 mV +``` + +und Produkte wie diese nicht mehr als Pufferliste ausgegeben: + +```text +Professional pH/mV/Redox/Temperatur Handmessgerät ... +Wassermeister Messkoffer +``` + +## Wichtig + +- Keine Änderung an der Shopquery. +- Keine Änderung an Shopware-Kriterien. +- Keine Neomeris-/pH-/Redox-Sonderlogik im PHP-Core. +- Keine neuen fachlichen Tokenlisten im Core; die Begriffe bleiben in YAML. +- Der Patch enthält die p50-Compound-/Puffer-Konfiguration vollständig, damit er auf dem aktuellen Arbeitsstand sicher anwendbar ist. +- Der p48-Referential-Anchor-Fallback-Guard bleibt enthalten, damit referenzielle Shop-Preisfragen nicht regressieren. + +## Geänderte Dateien + +```text +config/retriex/agent.yaml +config/retriex/vocabulary.yaml +src/Agent/AgentRunner.php +src/Config/AgentRunnerConfig.php +patch_history/RETRIEX_PATCH_51_DIRECT_PRODUCT_PRIMARY_IDENTITY_GUARD_README.md +``` + +## Erwartetes Verhalten + +```text +welche neomeris puffer gibt es für Kalibrierung von pH-Messgeräten +``` + +soll Pufferlösungen ausgeben, sofern sie in den Shopdaten enthalten sind, z. B.: + +```text +Neomeris pH-Pufferlösung pH 4.01 +Neomeris pH-Pufferlösung pH 7.00 +Neomeris pH-Pufferlösung pH 9.21 +Neomeris pH-Pufferlösung pH 10.01 +Neomeris Redox-Pufferlösung 200 mV / 475 mV / 650 mV +``` + +Falls die Shopdaten tatsächlich keine primären Pufferprodukte enthalten, soll RetrieX keine Geräte/Koffer als Ersatzprodukte listen, sondern sauber "keine passenden Shopdaten" ausgeben. + +## Lokale Checks + +Ausgeführt: + +```bash +php -l src/Agent/AgentRunner.php +php -l src/Config/AgentRunnerConfig.php +python3 - <<'PY' +import yaml +from pathlib import Path +for f in ['config/retriex/agent.yaml', 'config/retriex/vocabulary.yaml']: + yaml.safe_load(Path(f).read_text()) +print('YAML ok') +PY +``` + +`bin/console` wurde lokal nicht ausgeführt, weil `vendor/` im ZIP nicht enthalten ist. + +## Nach dem Einspielen prüfen + +```bash +bin/console cache:clear +bin/console mto:agent:config:validate +bin/console mto:agent:regression:test +bin/console mto:agent:config:audit-source --details +bin/console mto:agent:config:audit-patterns --details +``` diff --git a/src/Agent/AgentRunner.php b/src/Agent/AgentRunner.php index acff5b3..107961e 100644 --- a/src/Agent/AgentRunner.php +++ b/src/Agent/AgentRunner.php @@ -292,6 +292,26 @@ final readonly class AgentRunner $optimizedShopQuery = ''; } + $referentialAnchoredShopSearchQuery = $this->guardReferentialShopQueryFallbackWithHistoryAnchor( + prompt: $originalPrompt, + shopSearchQuery: $shopSearchQuery, + commerceHistoryContext: $shopQueryHistoryContext + ); + + if ($referentialAnchoredShopSearchQuery !== $shopSearchQuery) { + $this->agentLogger->info('Enriched referential shop fallback query with history anchor', [ + 'userId' => $userId, + 'prompt' => $prompt, + 'routingPrompt' => $routingPrompt, + 'optimizedShopQuery' => $optimizedShopQuery, + 'shopSearchQuery' => $shopSearchQuery, + 'referentialAnchoredShopSearchQuery' => $referentialAnchoredShopSearchQuery, + ]); + + $shopSearchQuery = $referentialAnchoredShopSearchQuery; + $optimizedShopQuery = ''; + } + $ragAnchoredShopSearchQuery = $this->enrichShopSearchQueryWithRagAnchor( prompt: $originalPrompt, shopSearchQuery: $shopSearchQuery, @@ -2581,6 +2601,83 @@ final readonly class AgentRunner return trim($query); } + private function guardReferentialShopQueryFallbackWithHistoryAnchor( + string $prompt, + string $shopSearchQuery, + string $commerceHistoryContext + ): string { + if (!$this->agentRunnerConfig->isShopQueryContextAnchorEnrichmentEnabled()) { + return $shopSearchQuery; + } + + if (trim($commerceHistoryContext) === '') { + return $shopSearchQuery; + } + + if (!$this->shouldUseCommerceHistoryForShopQuery($prompt)) { + return $shopSearchQuery; + } + + $combined = trim($shopSearchQuery . ' ' . $prompt); + if (!$this->containsConfiguredShopQueryAnchorTrigger($combined)) { + return $shopSearchQuery; + } + + $anchor = $this->normalizeShopQueryAnchor( + $this->extractLatestConfiguredShopQueryContextAnchor($commerceHistoryContext) + ); + + if ($anchor === '' || $this->queryAlreadyContainsAllAnchorTokens($shopSearchQuery, $anchor)) { + return $shopSearchQuery; + } + + $referentialQuery = $this->extractReferentialShopQueryTriggerTerms($combined); + if ($referentialQuery === '') { + return $shopSearchQuery; + } + + $template = $this->agentRunnerConfig->getShopQueryContextAnchorEnrichmentTemplate(); + $enriched = $this->renderAgentTemplate($template, [ + 'anchor' => $anchor, + 'query' => $referentialQuery, + ]); + $enriched = preg_replace('/\s+/u', ' ', $enriched) ?? $enriched; + $enriched = trim($enriched); + + return $enriched !== '' ? $enriched : $shopSearchQuery; + } + + private function extractReferentialShopQueryTriggerTerms(string $text): string + { + $tokens = $this->tokenizeShopQueryCandidate($text); + + if ($tokens === []) { + return ''; + } + + $triggerTokens = []; + foreach ($this->agentRunnerConfig->getShopQueryContextAnchorEnrichmentTriggerTerms() as $term) { + foreach ($this->tokenizeShopQueryCandidate($term) as $termToken) { + $triggerTokens[$termToken] = true; + } + } + + if ($triggerTokens === []) { + return ''; + } + + $out = []; + foreach ($tokens as $token) { + if (!isset($triggerTokens[$token]) || isset($out[$token])) { + continue; + } + + $out[$token] = $token; + } + + return implode(' ', array_values($out)); + } + private function enrichReferentialShopQueryFromHistory( string $query, string $sourcePrompt, @@ -2959,18 +3056,29 @@ final readonly class AgentRunner return $shopResults; } - $filtered = []; + $primaryMatches = []; + $corpusMatches = []; + foreach ($shopResults as $product) { if (!$product instanceof ShopProductResult) { continue; } + if ($this->shopProductPrimaryIdentityMatchesAnyDirectProductTerm($product, $requestedTerms)) { + $primaryMatches[] = $product; + continue; + } + if ($this->shopProductMatchesAnyDirectProductTerm($product, $requestedTerms)) { - $filtered[] = $product; + $corpusMatches[] = $product; } } - return $filtered; + if ($this->agentRunnerConfig->shouldPreferDirectShopResultGuardPrimaryIdentityMatches()) { + return $primaryMatches; + } + + return array_values(array_merge($primaryMatches, $corpusMatches)); } /** @@ -3226,6 +3334,19 @@ final readonly class AgentRunner return true; } + /** + * @param string[] $requestedTerms + */ + private function shopProductPrimaryIdentityMatchesAnyDirectProductTerm(ShopProductResult $product, array $requestedTerms): bool + { + $primaryText = trim(implode(' ', array_filter([ + $product->name, + $product->url, + ]))); + + return $this->textMatchesAnyDirectProductTerm($primaryText, $requestedTerms); + } + /** * @param string[] $requestedTerms */ @@ -3236,14 +3357,27 @@ final readonly class AgentRunner $product->description, implode(' ', $product->highlights), $product->customFields, + $product->url, ]))); + return $this->textMatchesAnyDirectProductTerm($productText, $requestedTerms); + } + + /** + * @param string[] $requestedTerms + */ + private function textMatchesAnyDirectProductTerm(string $text, array $requestedTerms): bool + { + if (trim($text) === '') { + return false; + } + foreach ($requestedTerms as $term) { - if ($this->containsAllShopQueryTokens($productText, $term)) { + if ($this->containsAllShopQueryTokens($text, $term)) { return true; } - if ($this->containsAllShopQueryTokensWithCompoundPrefixes($productText, $term)) { + if ($this->containsAllShopQueryTokensWithCompoundPrefixes($text, $term)) { return true; } } diff --git a/src/Config/AgentRunnerConfig.php b/src/Config/AgentRunnerConfig.php index 9693032..663657e 100644 --- a/src/Config/AgentRunnerConfig.php +++ b/src/Config/AgentRunnerConfig.php @@ -1140,6 +1140,11 @@ final class AgentRunnerConfig return $this->getRequiredBool('shop_prompt.direct_result_guard.enabled'); } + public function shouldPreferDirectShopResultGuardPrimaryIdentityMatches(): bool + { + return $this->getOptionalBool('shop_prompt.direct_result_guard.prefer_primary_identity_matches', true); + } + public function isDirectShopResultGuardCompoundPrefixMatchEnabled(): bool { return $this->getOptionalBool('shop_prompt.direct_result_guard.compound_prefix_match.enabled', false);