diff --git a/RETRIEX_PATCH_8_COMMERCE_INTENT_YAML_ONLY_README.md b/RETRIEX_PATCH_8_COMMERCE_INTENT_YAML_ONLY_README.md new file mode 100644 index 0000000..fc8325c --- /dev/null +++ b/RETRIEX_PATCH_8_COMMERCE_INTENT_YAML_ONLY_README.md @@ -0,0 +1,23 @@ +# RetrieX Patch 8 - CommerceIntentConfig YAML-only + +Scope: +- Move `CommerceIntentConfig` lists, regex patterns, labels, and score thresholds to YAML-only access. +- Keep `CommerceQueryParser`, `ShopService`, `PromptBuilder`, Retrieval, SSE, and frontend unchanged. + +Changed files: +- `config/retriex/intent.yaml` +- `src/Config/CommerceIntentConfig.php` +- `src/Config/RetriexEffectiveConfigProvider.php` + +After applying, run: + +```bash +php bin/console cache:clear +php bin/console mto:agent:config:validate +php bin/console mto:agent:config:audit-source --details +php bin/console mto:agent:regression:test +``` + +Expected: +- `CommerceIntentConfig` fallback accessors disappear from the source audit. +- Regression baseline remains green. diff --git a/config/retriex/intent.yaml b/config/retriex/intent.yaml index 7da0584..3ac1c1b 100644 --- a/config/retriex/intent.yaml +++ b/config/retriex/intent.yaml @@ -132,6 +132,36 @@ parameters: - '/\bzubehör\b/u' - '/\bzubehoer\b/u' - '/\bersatzteil(?:e)?\b/u' + patterns: + sku_like: '/\b\d{4,10}\b/u' + price_value_template: '/\b\d+(?:[.,]\d+)?\s*(?:{price_pattern})\b/u' + size_extraction_template: '/\b(?:{size_pattern})\s*([a-z0-9.-]+)\b/u' + size_value_template: '/\b(?:{size_pattern})\s*[a-z0-9.-]+\b/u' + size_token_value_template: '/\b(?:{size_token_pattern})\b/u' + color_value_template: '/\b(?:{color_pattern})\b/u' + model_like_product: '/\b[a-zäöüß][a-zäöüß®\-]*(?:\s+[a-zäöüß][a-zäöüß®\-]*){0,2}\s+\d{2,5}[a-z0-9\-]*\b/u' + labels: + support_or_diagnostic_signal: support_or_diagnostic + sku_signal: sku + price_signal: price + size_signal: size + size_token_signal: size_token + color_signal: color + advisory_signal_prefix: 'advisory:' + advisory_product_selection_signal: advisory_product_selection + model_like_product_signal: model_like_product + scores: + product_search_min_score: 3 + advisory_product_search_min_score: 2 + strong_signal_score: 3 + sku_signal_score: 2 + price_signal_score: 2 + size_signal_score: 2 + size_token_signal_score: 1 + color_signal_score: 1 + advisory_signal_score: 1 + advisory_product_selection_signal_score: 3 + model_like_product_signal_score: 3 retriex.intent.catalog.config: min_score: 0.72 diff --git a/src/Config/CommerceIntentConfig.php b/src/Config/CommerceIntentConfig.php index 95a5165..a222fd6 100644 --- a/src/Config/CommerceIntentConfig.php +++ b/src/Config/CommerceIntentConfig.php @@ -6,153 +6,6 @@ namespace App\Config; final class CommerceIntentConfig { - private const STRONG_SIGNALS = [ - 'shop', - 'alle', - 'preis', - 'kunde', - 'online', - 'produkt', - 'artikel', - 'sku', - 'kaufen', - 'kostet', - 'suche', - 'such', - 'finde', - 'finden', - 'analysegerät', - 'analysegeraet', - 'messgerät', - 'messgeraet', - 'pockettester', - 'pocket tester', - 'handmessgerät', - 'handmessgeraet', - 'analysator', - 'analyzer', - 'puffer', - 'kalibrierpuffer', - 'kalibrierlösung', - 'kalibrierloesung', - 'kalibrierung', - 'chemie', - 'reagenz', - 'reagenzien', - 'verbrauchsmaterial', - 'zubehör', - 'zubehoer', - 'ersatzteil', - ]; - - private const ADVISORY_SIGNALS = [ - 'passt', - 'eignet', - 'besser', - 'besten', - 'gut', - 'gut für', - 'gut fuer', - 'passend für', - 'passend fuer', - 'geeignet', - 'geeigent', - 'empfiehl', - 'empfehl', - ]; - - private const ADVISORY_PRODUCT_SELECTION_PATTERNS = [ - '/\bmit\s+welche(?:m|n|r|s)?\s+(?:testomat(?:en)?|pockettester|pocket\s+tester|analysegerät|analysegeraet|messgerät|messgeraet|analysator|analyzer)\b.*\b(?:messen|messung|überwach(?:en|ung)?|ueberwach(?:en|ung)?)\b/u', - '/\bwelche(?:r|s|n|m)?\s+(?:testomat(?:en)?|pockettester|pocket\s+tester|analysegerät|analysegeraet|messgerät|messgeraet|analysator|analyzer)\b.*\b(?:kann|können|koennen|misst|messen|überwacht|ueberwacht|eignet|geeignet|passt|gut|empfehl)\b.*\b(?:messen|messung|überwach(?:en|ung)?|ueberwach(?:en|ung)?)\b/u', - '/\b(?:testomat(?:en)?|pockettester|pocket\s+tester|analysegerät|analysegeraet|messgerät|messgeraet|analysator|analyzer)\b.*\b(?:für|fuer)\b.*\b(?:messung|messen|überwachung|ueberwachung)\b/u', - ]; - - private const PRICE_TERMS = [ - 'euro', - '€', - 'eur', - 'teuer', - 'preis', - 'kosten', - 'kostet', - ]; - - private const COLOR_TERMS = [ - 'schwarz', - 'weiß', - 'weis', - 'blau', - 'grau', - 'beige', - 'rosa', - 'pink', - 'gruen', - 'orange', - 'braun', - ]; - - private const SIZE_TOKEN_TERMS = [ - 'xs', - 's', - 'm', - 'l', - 'xl', - 'xxl', - 'xxxxl', - ]; - - private const SIZE_TERMS = [ - 'größe', - 'groesse', - 'grösse', - ]; - - private const SUPPORT_DIAGNOSTIC_PATTERNS = [ - '/\bfehler\b/u', - '/\bfehlercode\b/u', - '/\berror\b/u', - '/\bstörung\b/u', - '/\bstoerung\b/u', - '/\balarm\b/u', - '/\bstörungsmeldung\b/u', - '/\bstoerungsmeldung\b/u', - '/\bmeldung\b/u', - '/\bwarnung\b/u', - '/\bwarncode\b/u', - '/\bcode\b/u', - '/\bwas bedeutet\b/u', - '/\bwarum\b/u', - '/\bblinkt\b/u', - '/\bzeigt\b/u', - '/\bzeigt an\b/u', - '/\bursache\b/u', - '/\bdiagnose\b/u', - '/\bservicefall\b/u', - '/\bproblem\b/u', - '/\bstörung beheben\b/u', - '/\bstoerung beheben\b/u', - '/\be\d{1,3}\b/u', - ]; - - private const EXPLICIT_COMMERCE_INTENT_PATTERNS = [ - '/\bshop\b/u', - '/\bpreis\b/u', - '/\bkosten\b/u', - '/\bkostet\b/u', - '/\bkaufen\b/u', - '/\bbestellen\b/u', - '/\bprodukt\b/u', - '/\bartikel\b/u', - '/\bsku\b/u', - '/\bonline\b/u', - '/\bchemie\b/u', - '/\breagenz(?:ien)?\b/u', - '/\bverbrauchsmaterial(?:ien)?\b/u', - '/\bzubehör\b/u', - '/\bzubehoer\b/u', - '/\bersatzteil(?:e)?\b/u', - ]; - /** * @param array $config */ @@ -163,25 +16,25 @@ final class CommerceIntentConfig /** @return string[] */ public function getStrongSignalsList(): array { - return $this->stringList('strong_signals', self::STRONG_SIGNALS); + return $this->requiredStringList('strong_signals'); } /** @return string[] */ public function getAdvisorySignals(): array { - return $this->stringList('advisory_signals', self::ADVISORY_SIGNALS); + return $this->requiredStringList('advisory_signals'); } /** @return string[] */ public function getAdvisoryProductSelectionPatterns(): array { - return $this->stringList('advisory_product_selection_patterns', self::ADVISORY_PRODUCT_SELECTION_PATTERNS); + return $this->requiredStringList('advisory_product_selection_patterns'); } /** @return string[] */ public function getPriceTerms(): array { - return $this->stringList('price_terms', self::PRICE_TERMS); + return $this->requiredStringList('price_terms'); } public function getPricePattern(): string @@ -192,7 +45,7 @@ final class CommerceIntentConfig /** @return string[] */ public function getColorTerms(): array { - return $this->stringList('color_terms', self::COLOR_TERMS); + return $this->requiredStringList('color_terms'); } public function getColorPattern(): string @@ -203,7 +56,7 @@ final class CommerceIntentConfig /** @return string[] */ public function getSizeTokenTerms(): array { - return $this->stringList('size_token_terms', self::SIZE_TOKEN_TERMS); + return $this->requiredStringList('size_token_terms'); } public function getSizeTokenPattern(): string @@ -214,7 +67,7 @@ final class CommerceIntentConfig /** @return string[] */ public function getSizeTerms(): array { - return $this->stringList('size_terms', self::SIZE_TERMS); + return $this->requiredStringList('size_terms'); } public function getSizePattern(): string @@ -224,157 +77,192 @@ final class CommerceIntentConfig public function getSizeExtractionPattern(): string { - return '/\b(?:' . $this->getSizePattern() . ')\s*([a-z0-9.-]+)\b/u'; + return $this->renderPatternTemplate('patterns.size_extraction_template', [ + 'size_pattern' => $this->getSizePattern(), + ]); } /** @return string[] */ public function getSupportDiagnosticPatterns(): array { - return $this->stringList('support_diagnostic_patterns', self::SUPPORT_DIAGNOSTIC_PATTERNS); + return $this->requiredStringList('support_diagnostic_patterns'); } /** @return string[] */ public function getExplicitCommerceIntentPatterns(): array { - return $this->stringList('explicit_commerce_intent_patterns', self::EXPLICIT_COMMERCE_INTENT_PATTERNS); + return $this->requiredStringList('explicit_commerce_intent_patterns'); } public function getSkuLikePattern(): string { - return '/\b\d{4,10}\b/u'; + return $this->requiredString('patterns.sku_like'); } public function getPriceValuePattern(): string { - return '/\b\d+(?:[.,]\d+)?\s*(?:' . $this->getPricePattern() . ')\b/u'; + return $this->renderPatternTemplate('patterns.price_value_template', [ + 'price_pattern' => $this->getPricePattern(), + ]); } public function getSizeValuePattern(): string { - return '/\b(?:' . $this->getSizePattern() . ')\s*[a-z0-9.-]+\b/u'; + return $this->renderPatternTemplate('patterns.size_value_template', [ + 'size_pattern' => $this->getSizePattern(), + ]); } public function getSizeTokenValuePattern(): string { - return '/\b(?:' . $this->getSizeTokenPattern() . ')\b/u'; + return $this->renderPatternTemplate('patterns.size_token_value_template', [ + 'size_token_pattern' => $this->getSizeTokenPattern(), + ]); } public function getColorValuePattern(): string { - return '/\b(?:' . $this->getColorPattern() . ')\b/u'; + return $this->renderPatternTemplate('patterns.color_value_template', [ + 'color_pattern' => $this->getColorPattern(), + ]); } public function getSupportOrDiagnosticSignalLabel(): string { - return 'support_or_diagnostic'; + return $this->requiredString('labels.support_or_diagnostic_signal'); } public function getSkuSignalLabel(): string { - return 'sku'; + return $this->requiredString('labels.sku_signal'); } public function getPriceSignalLabel(): string { - return 'price'; + return $this->requiredString('labels.price_signal'); } public function getSizeSignalLabel(): string { - return 'size'; + return $this->requiredString('labels.size_signal'); } public function getSizeTokenSignalLabel(): string { - return 'size_token'; + return $this->requiredString('labels.size_token_signal'); } public function getColorSignalLabel(): string { - return 'color'; + return $this->requiredString('labels.color_signal'); } public function getAdvisorySignalPrefix(): string { - return 'advisory:'; + return $this->requiredString('labels.advisory_signal_prefix'); } public function getAdvisoryProductSelectionSignalLabel(): string { - return 'advisory_product_selection'; + return $this->requiredString('labels.advisory_product_selection_signal'); } public function getProductSearchMinScore(): int { - return 3; + return $this->requiredInt('scores.product_search_min_score'); } public function getAdvisoryProductSearchMinScore(): int { - return 2; + return $this->requiredInt('scores.advisory_product_search_min_score'); } public function getStrongSignalScore(): int { - return 3; + return $this->requiredInt('scores.strong_signal_score'); } public function getSkuSignalScore(): int { - return 2; + return $this->requiredInt('scores.sku_signal_score'); } public function getPriceSignalScore(): int { - return 2; + return $this->requiredInt('scores.price_signal_score'); } public function getSizeSignalScore(): int { - return 2; + return $this->requiredInt('scores.size_signal_score'); } public function getSizeTokenSignalScore(): int { - return 1; + return $this->requiredInt('scores.size_token_signal_score'); } public function getColorSignalScore(): int { - return 1; + return $this->requiredInt('scores.color_signal_score'); } public function getAdvisorySignalScore(): int { - return 1; + return $this->requiredInt('scores.advisory_signal_score'); } public function getAdvisoryProductSelectionSignalScore(): int { - return 3; + return $this->requiredInt('scores.advisory_product_selection_signal_score'); } public function getModelLikeProductPattern(): string { - return '/\b[a-zäöüß][a-zäöüß®\-]*(?:\s+[a-zäöüß][a-zäöüß®\-]*){0,2}\s+\d{2,5}[a-z0-9\-]*\b/u'; + return $this->requiredString('patterns.model_like_product'); } public function getModelLikeProductSignalLabel(): string { - return 'model_like_product'; + return $this->requiredString('labels.model_like_product_signal'); } public function getModelLikeProductSignalScore(): int { - return 3; + return $this->requiredInt('scores.model_like_product_signal_score'); + } + + private function requiredInt(string $key): int + { + $value = $this->getRequiredValue($key); + if (!is_int($value)) { + throw new \InvalidArgumentException(sprintf('RetrieX commerce intent config key "%s" must be an integer.', $key)); + } + + return $value; + } + + private function requiredString(string $key): string + { + $value = $this->getRequiredValue($key); + if (!is_scalar($value)) { + throw new \InvalidArgumentException(sprintf('RetrieX commerce intent config key "%s" must be a string.', $key)); + } + + $value = trim((string) $value); + if ($value === '') { + throw new \InvalidArgumentException(sprintf('RetrieX commerce intent config key "%s" must not be empty.', $key)); + } + + return $value; } /** @return string[] */ - private function stringList(string $key, array $default): array + private function requiredStringList(string $key): array { - $value = $this->config[$key] ?? $default; + $value = $this->getRequiredValue($key); if (!is_array($value)) { - return $default; + throw new \InvalidArgumentException(sprintf('RetrieX commerce intent config key "%s" must be a list.', $key)); } $out = []; @@ -391,6 +279,43 @@ final class CommerceIntentConfig $out[] = $item; } - return $out !== [] ? $out : $default; + if ($out === []) { + throw new \InvalidArgumentException(sprintf('RetrieX commerce intent config key "%s" must contain at least one value.', $key)); + } + + return $out; + } + + /** + * @param array $replacements + */ + private function renderPatternTemplate(string $key, array $replacements): string + { + $template = $this->requiredString($key); + $replace = []; + foreach ($replacements as $placeholder => $value) { + $replace['{' . $placeholder . '}'] = $value; + } + + $pattern = strtr($template, $replace); + if (preg_match('/\{[A-Za-z_][A-Za-z0-9_]*\}/', $pattern) === 1) { + throw new \InvalidArgumentException(sprintf('RetrieX commerce intent pattern template "%s" contains unresolved placeholders.', $key)); + } + + return $pattern; + } + + private function getRequiredValue(string $key): mixed + { + $parts = explode('.', $key); + $value = $this->config; + foreach ($parts as $part) { + if (!is_array($value) || !array_key_exists($part, $value)) { + throw new \InvalidArgumentException(sprintf('Missing required RetrieX commerce intent config key "%s".', $key)); + } + $value = $value[$part]; + } + + return $value; } } diff --git a/src/Config/RetriexEffectiveConfigProvider.php b/src/Config/RetriexEffectiveConfigProvider.php index 8160c1c..0923775 100644 --- a/src/Config/RetriexEffectiveConfigProvider.php +++ b/src/Config/RetriexEffectiveConfigProvider.php @@ -645,12 +645,33 @@ final readonly class RetriexEffectiveConfigProvider 'commerce' => [ 'strong_signals' => $this->commerceIntentConfig->getStrongSignalsList(), 'advisory_signals' => $this->commerceIntentConfig->getAdvisorySignals(), + 'advisory_product_selection_patterns' => $this->commerceIntentConfig->getAdvisoryProductSelectionPatterns(), 'price_terms' => $this->commerceIntentConfig->getPriceTerms(), 'color_terms' => $this->commerceIntentConfig->getColorTerms(), 'size_token_terms' => $this->commerceIntentConfig->getSizeTokenTerms(), 'size_terms' => $this->commerceIntentConfig->getSizeTerms(), 'support_diagnostic_patterns' => $this->commerceIntentConfig->getSupportDiagnosticPatterns(), 'explicit_commerce_intent_patterns' => $this->commerceIntentConfig->getExplicitCommerceIntentPatterns(), + 'patterns' => [ + 'sku_like' => $this->commerceIntentConfig->getSkuLikePattern(), + 'price_value' => $this->commerceIntentConfig->getPriceValuePattern(), + 'size_extraction' => $this->commerceIntentConfig->getSizeExtractionPattern(), + 'size_value' => $this->commerceIntentConfig->getSizeValuePattern(), + 'size_token_value' => $this->commerceIntentConfig->getSizeTokenValuePattern(), + 'color_value' => $this->commerceIntentConfig->getColorValuePattern(), + 'model_like_product' => $this->commerceIntentConfig->getModelLikeProductPattern(), + ], + 'labels' => [ + 'support_or_diagnostic_signal' => $this->commerceIntentConfig->getSupportOrDiagnosticSignalLabel(), + 'sku_signal' => $this->commerceIntentConfig->getSkuSignalLabel(), + 'price_signal' => $this->commerceIntentConfig->getPriceSignalLabel(), + 'size_signal' => $this->commerceIntentConfig->getSizeSignalLabel(), + 'size_token_signal' => $this->commerceIntentConfig->getSizeTokenSignalLabel(), + 'color_signal' => $this->commerceIntentConfig->getColorSignalLabel(), + 'advisory_signal_prefix' => $this->commerceIntentConfig->getAdvisorySignalPrefix(), + 'advisory_product_selection_signal' => $this->commerceIntentConfig->getAdvisoryProductSelectionSignalLabel(), + 'model_like_product_signal' => $this->commerceIntentConfig->getModelLikeProductSignalLabel(), + ], 'thresholds' => [ 'product_search_min_score' => $this->commerceIntentConfig->getProductSearchMinScore(), 'advisory_product_search_min_score' => $this->commerceIntentConfig->getAdvisoryProductSearchMinScore(), @@ -661,6 +682,7 @@ final readonly class RetriexEffectiveConfigProvider 'size_token_signal_score' => $this->commerceIntentConfig->getSizeTokenSignalScore(), 'color_signal_score' => $this->commerceIntentConfig->getColorSignalScore(), 'advisory_signal_score' => $this->commerceIntentConfig->getAdvisorySignalScore(), + 'advisory_product_selection_signal_score' => $this->commerceIntentConfig->getAdvisoryProductSelectionSignalScore(), 'model_like_product_signal_score' => $this->commerceIntentConfig->getModelLikeProductSignalScore(), ], ], @@ -681,7 +703,6 @@ final readonly class RetriexEffectiveConfigProvider ]; } - /** @return array */ private function languageConfig(): array { return ['stopwords' => $this->stopWordsConfig->getStopWords()];