diff --git a/RETRIEX_SHOP_ADVISORY_INTENT_FIX_README.md b/RETRIEX_SHOP_ADVISORY_INTENT_FIX_README.md new file mode 100644 index 0000000..c17d6ef --- /dev/null +++ b/RETRIEX_SHOP_ADVISORY_INTENT_FIX_README.md @@ -0,0 +1,64 @@ +# RetrieX shop advisory intent fix + +## Problem + +The prompt + +```text +welcher pockettester ist für Redox messung gut +``` + +was classified as a purely technical RAG question because the factual guard matched +question wording such as `welcher` plus technical wording such as `Messung`. +As a result no Shopware search was executed. The later follow-up + +```text +suche im shop +``` + +then had to recover a shop query from history/client context, which remained fragile. + +## Fix + +This patch keeps the factual RAG guard for technical questions, but lets clear product +advisory prompts pass into commerce detection when both conditions are true: + +1. The prompt contains an advisory term such as `gut`, `geeignet`, `empfehl...`. +2. The prompt contains a product/device class signal such as `pockettester`, + `messgerät`, `handmessgerät`, etc. + +This means: + +- `Was ist der niedrigste Grenzwert ... Testomat ... Wasserhärte?` stays RAG-only. +- `mit welchem indikator wird der wert gemessen` stays RAG-only. +- `was kostet der indikator` stays commerce/product search. +- `welcher pockettester ist für Redox messung gut` now triggers commerce search directly. + +## Changed files + +```text +src/Intent/CommerceIntentLite.php +src/Config/CommerceIntentConfig.php +config/retriex/intent.yaml +config/retriex/agent.yaml +``` + +## Validation performed + +```bash +php -n -l src/Intent/CommerceIntentLite.php +php -n -l src/Config/CommerceIntentConfig.php +``` + +Additional local intent check: + +```text +Was ist der niedrigste Grenzwert ... => none +mit welchem indikator ... => none +was kostet der indikator => product_search +welcher pockettester ist für Redox messung gut => product_search +suche im shop => product_search +suche im shop nach redox messung => product_search +welcher Grenzwert kann mit dem Testomat 808 gemessen werden => none +welches Messgerät ist für Redox Messung geeignet => product_search +``` diff --git a/config/retriex/agent.yaml b/config/retriex/agent.yaml index 6fbbd49..df9d875 100644 --- a/config/retriex/agent.yaml +++ b/config/retriex/agent.yaml @@ -61,7 +61,7 @@ parameters: - '- Separate terms using spaces only.' - '- If a relevant product name is present, it must be placed at the beginning of the final search query.' - '- Try to always identify all products mentioned in the user input text, even in long prompts.' - - '- Look for terms such as Testomat, Horiba, Tritromat, or words like indicator/Indikator.' + - '- Look for terms such as Testomat, Horiba, Tritromat, Pockettester, Redox, ORP, or words like indicator/Indikator.' - '- If the current user input is vague or referential, use the recent conversation context only as support.' - '- Do not output words that only describe conversation flow, such as "same", "again", "also", or "like above".' conversation_context_rules: diff --git a/config/retriex/intent.yaml b/config/retriex/intent.yaml index 27e653f..c4742f4 100644 --- a/config/retriex/intent.yaml +++ b/config/retriex/intent.yaml @@ -21,6 +21,10 @@ parameters: - analysegeraet - messgerät - messgeraet + - pockettester + - pocket tester + - handmessgerät + - handmessgeraet - analysator - analyzer - puffer @@ -40,6 +44,7 @@ parameters: - eignet - besser - besten + - gut - gut für - gut fuer - passend für diff --git a/public/assets/styles/base.css b/public/assets/styles/base.css index 642da3c..9606f4e 100644 --- a/public/assets/styles/base.css +++ b/public/assets/styles/base.css @@ -393,7 +393,7 @@ span.think { background-position: 220% 0; -webkit-background-clip: text; background-clip: text; - animation: thinkScan 2.2s linear infinite; + animation: thinkScan 1.4s linear infinite; } @keyframes thinkScan { diff --git a/rag-inprogress-shop-advisory-intent-fix-patch-only.zip b/rag-inprogress-shop-advisory-intent-fix-patch-only.zip new file mode 100644 index 0000000..f9711a3 Binary files /dev/null and b/rag-inprogress-shop-advisory-intent-fix-patch-only.zip differ diff --git a/src/Config/CommerceIntentConfig.php b/src/Config/CommerceIntentConfig.php index 03d8f64..b529689 100644 --- a/src/Config/CommerceIntentConfig.php +++ b/src/Config/CommerceIntentConfig.php @@ -25,6 +25,10 @@ final class CommerceIntentConfig 'analysegeraet', 'messgerät', 'messgeraet', + 'pockettester', + 'pocket tester', + 'handmessgerät', + 'handmessgeraet', 'analysator', 'analyzer', 'puffer', @@ -46,6 +50,7 @@ final class CommerceIntentConfig 'eignet', 'besser', 'besten', + 'gut', 'gut für', 'gut fuer', 'passend für', diff --git a/src/Intent/CommerceIntentLite.php b/src/Intent/CommerceIntentLite.php index 87820bc..2c92719 100644 --- a/src/Intent/CommerceIntentLite.php +++ b/src/Intent/CommerceIntentLite.php @@ -40,7 +40,11 @@ final class CommerceIntentLite ); } - if ($this->isTechnicalFactualKnowledgeQuery($prompt) && !$this->hasExplicitCommerceIntent($prompt)) { + if ( + $this->isTechnicalFactualKnowledgeQuery($prompt) + && !$this->hasExplicitCommerceIntent($prompt) + && !$this->hasAdvisoryProductSelectionIntent($prompt) + ) { return $this->buildDetectionResult( intent: self::NONE, score: 0, @@ -95,6 +99,81 @@ final class CommerceIntentLite return $this->matchesAnyPattern($prompt, $this->config->getExplicitCommerceIntentPatterns()); } + /** + * Product advisory prompts can contain technical words such as "Messung". + * + * They must not be swallowed by the factual RAG guard when the user is + * clearly asking for a suitable shop product or device class. + */ + private function hasAdvisoryProductSelectionIntent(string $prompt): bool + { + if (!$this->containsConfiguredAdvisorySignal($prompt)) { + return false; + } + + if (preg_match($this->config->getModelLikeProductPattern(), $prompt) === 1) { + return true; + } + + foreach ($this->config->getStrongSignalsList() as $signal) { + $signal = mb_strtolower(trim($signal)); + + if ($signal === '' || $this->isNonProductCommerceSignal($signal)) { + continue; + } + + if (str_contains($prompt, $signal)) { + return true; + } + } + + return false; + } + + private function containsConfiguredAdvisorySignal(string $prompt): bool + { + foreach ($this->config->getAdvisorySignals() as $signal) { + if ($this->containsConfiguredPhraseOrToken($prompt, $signal)) { + return true; + } + } + + return false; + } + + private function containsConfiguredPhraseOrToken(string $prompt, string $signal): bool + { + $signal = mb_strtolower(trim($signal)); + + if ($signal === '') { + return false; + } + + if (preg_match('/\s/u', $signal) === 1) { + return str_contains($prompt, $signal); + } + + $pattern = '/(?config->getAdvisorySignals() as $signal) { - if (str_contains($prompt, mb_strtolower($signal))) { + if ($this->containsConfiguredPhraseOrToken($prompt, $signal)) { $score += $this->config->getAdvisorySignalScore(); $signals[] = $this->config->getAdvisorySignalPrefix() . $signal; }