From ee7930ce16dd455a3f544ab5e1b3163faf903b79 Mon Sep 17 00:00:00 2001 From: team 1 Date: Sun, 10 May 2026 11:17:24 +0200 Subject: [PATCH] p84 --- ..._DEVICE_REFERENTIAL_PRICE_ANCHOR_README.md | 44 +++++ src/Agent/AgentRunner.php | 163 ++++++++++++++++++ 2 files changed, 207 insertions(+) create mode 100644 patch_history/RETRIEX_PATCH_84_MAIN_DEVICE_REFERENTIAL_PRICE_ANCHOR_README.md diff --git a/patch_history/RETRIEX_PATCH_84_MAIN_DEVICE_REFERENTIAL_PRICE_ANCHOR_README.md b/patch_history/RETRIEX_PATCH_84_MAIN_DEVICE_REFERENTIAL_PRICE_ANCHOR_README.md new file mode 100644 index 0000000..ca901b1 --- /dev/null +++ b/patch_history/RETRIEX_PATCH_84_MAIN_DEVICE_REFERENTIAL_PRICE_ANCHOR_README.md @@ -0,0 +1,44 @@ +# RetrieX Patch p84 - Main Device Referential Price Anchor + +## Ziel + +Referenzielle Gerätepreis-Nachfragen nach einem Zubehör-/Indikator-Flow sollen den zuletzt belegten Hauptgeräteanker behalten. + +Beispiel: + +1. `zu welchem gerät gehört der indikator 300` +2. `Preis anzeigen` +3. `und was kostet das gerät selber` + +Die dritte Anfrage darf nicht mehr auf die generische Shopquery `gerät` reduziert werden, sondern soll den zuletzt belegten Gerätemodellanker verwenden, z. B. `testomat 808`. + +## Änderung + +- Ergänzt in `AgentRunner` einen engen Guard für referenzielle Hauptgeräte-Shopqueries. +- Der Guard greift nur, wenn: + - die aktuelle Query noch keinen Modellanker enthält, + - die aktuelle Frage einen Hauptgerätebezug enthält, + - die aktuelle Frage keinen Zubehör-/Indikator-/Reagenzbezug enthält, + - die bereinigte Query nur generische Geräte-/Preis-/Referenz-/Stopword-Tokens enthält, + - im Verlauf ein Produktmodellanker vorhanden ist. +- Bei Treffer wird die generische Query durch den neuesten Modellanker aus dem Verlauf ersetzt. + +## Regressionsschutz + +Die bestehende Zubehör-/Indikator-Ankerlogik bleibt unverändert und läuft weiterhin vor diesem neuen Guard. Der neue Guard blockiert sich selbst, wenn die aktuelle Frage Zubehör-/Indikator-/Reagenz-Tokens enthält. Dadurch sollen Flows wie `was kostet der indikator` weiterhin über die bestehende Zubehörlogik laufen. + +## Nicht geändert + +- Kein Retrieval-Scoring. +- Kein Shop-Ranking. +- Kein Shop-Matching. +- Keine PromptBuilder-Änderung. +- Keine neuen harten Produkt- oder Fachlisten im PHP-Core; die Entscheidung nutzt bestehende YAML-konfigurierbare Tokenlisten. + +## Lokale Checks + +- `php -l src/Agent/AgentRunner.php` +- YAML-Parsing der RetrieX-Konfiguration +- Logische Guard-Simulation: + - `gerät` + Verlauf mit `Testomat 808 Indikator 300` + Prompt `und was kostet das gerät selber` -> `testomat 808` + - `indikator`-/`zubehör`-Prompts bleiben vom neuen Hauptgeräte-Guard ausgeschlossen diff --git a/src/Agent/AgentRunner.php b/src/Agent/AgentRunner.php index 6792bdd..2c06622 100644 --- a/src/Agent/AgentRunner.php +++ b/src/Agent/AgentRunner.php @@ -315,6 +315,26 @@ final readonly class AgentRunner $optimizedShopQuery = ''; } + $mainDeviceAnchoredShopSearchQuery = $this->guardMainDeviceReferentialShopQueryWithHistoryModelAnchor( + prompt: $originalPrompt, + shopSearchQuery: $shopSearchQuery, + commerceHistoryContext: $shopQueryHistoryContext + ); + + if ($mainDeviceAnchoredShopSearchQuery !== $shopSearchQuery) { + $this->agentLogger->info('Enriched referential main-device shop query with history model anchor', [ + 'userId' => $userId, + 'prompt' => $prompt, + 'routingPrompt' => $routingPrompt, + 'optimizedShopQuery' => $optimizedShopQuery, + 'shopSearchQuery' => $shopSearchQuery, + 'mainDeviceAnchoredShopSearchQuery' => $mainDeviceAnchoredShopSearchQuery, + ]); + + $shopSearchQuery = $mainDeviceAnchoredShopSearchQuery; + $optimizedShopQuery = ''; + } + $ragAnchoredShopSearchQuery = $this->enrichShopSearchQueryWithRagAnchor( prompt: $originalPrompt, shopSearchQuery: $shopSearchQuery, @@ -2907,6 +2927,149 @@ final readonly class AgentRunner return $enriched !== '' ? $enriched : $shopSearchQuery; } + private function guardMainDeviceReferentialShopQueryWithHistoryModelAnchor( + string $prompt, + string $shopSearchQuery, + string $commerceHistoryContext + ): string { + $shopSearchQuery = trim($shopSearchQuery); + + if ( + $shopSearchQuery === '' + || trim($commerceHistoryContext) === '' + || $this->referenceAnchorExtractor->extractFirstProductModelAnchor($prompt) !== '' + || $this->referenceAnchorExtractor->extractFirstProductModelAnchor($shopSearchQuery) !== '' + ) { + return $shopSearchQuery; + } + + if (!$this->isMainDeviceReferentialShopQueryPrompt($prompt)) { + return $shopSearchQuery; + } + + if (!$this->isGenericMainDeviceReferentialShopQuery($shopSearchQuery)) { + return $shopSearchQuery; + } + + $modelAnchor = $this->normalizeShopQueryAnchor( + $this->extractLatestHistoryProductModelAnchor($commerceHistoryContext) + ); + + if ($modelAnchor === '') { + return $shopSearchQuery; + } + + return $this->queryAlreadyContainsAllAnchorTokens($shopSearchQuery, $modelAnchor) + ? $shopSearchQuery + : $modelAnchor; + } + + private function isMainDeviceReferentialShopQueryPrompt(string $prompt): bool + { + $tokens = $this->tokenizeShopQueryCandidate($prompt); + if ($tokens === []) { + return false; + } + + $tokenSet = array_fill_keys($tokens, true); + $mainDeviceTokens = $this->buildShopQueryTokenSet( + $this->agentRunnerConfig->getNoLlmMainDeviceRequestRoleKeywords() + ); + + if (!$this->tokenSetIntersects($tokenSet, $mainDeviceTokens)) { + return false; + } + + $accessoryTokens = $this->buildShopQueryTokenSet($this->mergeUniqueStrings( + $this->agentRunnerConfig->getNoLlmAccessoryProductRoleKeywords(), + $this->agentRunnerConfig->getRequestedAccessoryCodeTerms() + )); + + if ($this->tokenSetIntersects($tokenSet, $accessoryTokens)) { + return false; + } + + $referenceTokens = $this->buildShopQueryTokenSet($this->mergeUniqueStrings( + $this->agentRunnerConfig->getShopQueryContextUsageReferentialTerms(), + $this->agentRunnerConfig->getShopQueryMetaOnlyTerms() + )); + + return $this->tokenSetIntersects($tokenSet, $referenceTokens); + } + + private function isGenericMainDeviceReferentialShopQuery(string $shopSearchQuery): bool + { + $tokens = $this->tokenizeShopQueryCandidate($shopSearchQuery); + if ($tokens === []) { + return false; + } + + foreach ($tokens as $token) { + if (preg_match('/\d/u', $token) === 1) { + return false; + } + } + + $genericTerms = $this->mergeUniqueStrings( + $this->agentRunnerConfig->getNoLlmMainDeviceRequestRoleKeywords(), + $this->agentRunnerConfig->getShopQueryContextUsageReferentialTerms() + ); + $genericTerms = $this->mergeUniqueStrings($genericTerms, $this->agentRunnerConfig->getShopQueryMetaOnlyTerms()); + $genericTerms = $this->mergeUniqueStrings($genericTerms, $this->agentRunnerConfig->getShopQueryContextFallbackFilterTerms()); + $genericTerms = $this->mergeUniqueStrings($genericTerms, $this->agentRunnerConfig->getShopQueryStopwordCleanupTerms()); + + $genericTokens = $this->buildShopQueryTokenSet($genericTerms); + + if ($genericTokens === []) { + return false; + } + + $hasMainDeviceToken = false; + $mainDeviceTokens = $this->buildShopQueryTokenSet( + $this->agentRunnerConfig->getNoLlmMainDeviceRequestRoleKeywords() + ); + + foreach ($tokens as $token) { + if (!isset($genericTokens[$token])) { + return false; + } + + if (isset($mainDeviceTokens[$token])) { + $hasMainDeviceToken = true; + } + } + + return $hasMainDeviceToken; + } + + private function extractLatestHistoryProductModelAnchor(string $commerceHistoryContext): string + { + foreach ($this->extractHistoryTurnsNewestFirst($commerceHistoryContext) as $turn) { + $modelAnchor = $this->referenceAnchorExtractor->extractFirstProductModelAnchor($turn); + + if ($modelAnchor !== '') { + return $modelAnchor; + } + } + + return ''; + } + + /** + * @param array $left + * @param array $right + */ + private function tokenSetIntersects(array $left, array $right): bool + { + foreach ($left as $token => $_) { + if (isset($right[$token])) { + return true; + } + } + + return false; + } + private function extractReferentialShopQueryTriggerTerms(string $text): string { $tokens = $this->tokenizeShopQueryCandidate($text);