This commit is contained in:
team 1
2026-04-30 16:07:20 +02:00
parent 12577be636
commit b3b294bfc2
4 changed files with 187 additions and 188 deletions

View File

@@ -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.

View File

@@ -132,6 +132,36 @@ parameters:
- '/\bzubehör\b/u' - '/\bzubehör\b/u'
- '/\bzubehoer\b/u' - '/\bzubehoer\b/u'
- '/\bersatzteil(?:e)?\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: retriex.intent.catalog.config:
min_score: 0.72 min_score: 0.72

View File

@@ -6,153 +6,6 @@ namespace App\Config;
final class CommerceIntentConfig 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<string, mixed> $config * @param array<string, mixed> $config
*/ */
@@ -163,25 +16,25 @@ final class CommerceIntentConfig
/** @return string[] */ /** @return string[] */
public function getStrongSignalsList(): array public function getStrongSignalsList(): array
{ {
return $this->stringList('strong_signals', self::STRONG_SIGNALS); return $this->requiredStringList('strong_signals');
} }
/** @return string[] */ /** @return string[] */
public function getAdvisorySignals(): array public function getAdvisorySignals(): array
{ {
return $this->stringList('advisory_signals', self::ADVISORY_SIGNALS); return $this->requiredStringList('advisory_signals');
} }
/** @return string[] */ /** @return string[] */
public function getAdvisoryProductSelectionPatterns(): array 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[] */ /** @return string[] */
public function getPriceTerms(): array public function getPriceTerms(): array
{ {
return $this->stringList('price_terms', self::PRICE_TERMS); return $this->requiredStringList('price_terms');
} }
public function getPricePattern(): string public function getPricePattern(): string
@@ -192,7 +45,7 @@ final class CommerceIntentConfig
/** @return string[] */ /** @return string[] */
public function getColorTerms(): array public function getColorTerms(): array
{ {
return $this->stringList('color_terms', self::COLOR_TERMS); return $this->requiredStringList('color_terms');
} }
public function getColorPattern(): string public function getColorPattern(): string
@@ -203,7 +56,7 @@ final class CommerceIntentConfig
/** @return string[] */ /** @return string[] */
public function getSizeTokenTerms(): array public function getSizeTokenTerms(): array
{ {
return $this->stringList('size_token_terms', self::SIZE_TOKEN_TERMS); return $this->requiredStringList('size_token_terms');
} }
public function getSizeTokenPattern(): string public function getSizeTokenPattern(): string
@@ -214,7 +67,7 @@ final class CommerceIntentConfig
/** @return string[] */ /** @return string[] */
public function getSizeTerms(): array public function getSizeTerms(): array
{ {
return $this->stringList('size_terms', self::SIZE_TERMS); return $this->requiredStringList('size_terms');
} }
public function getSizePattern(): string public function getSizePattern(): string
@@ -224,157 +77,192 @@ final class CommerceIntentConfig
public function getSizeExtractionPattern(): string 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[] */ /** @return string[] */
public function getSupportDiagnosticPatterns(): array public function getSupportDiagnosticPatterns(): array
{ {
return $this->stringList('support_diagnostic_patterns', self::SUPPORT_DIAGNOSTIC_PATTERNS); return $this->requiredStringList('support_diagnostic_patterns');
} }
/** @return string[] */ /** @return string[] */
public function getExplicitCommerceIntentPatterns(): array 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 public function getSkuLikePattern(): string
{ {
return '/\b\d{4,10}\b/u'; return $this->requiredString('patterns.sku_like');
} }
public function getPriceValuePattern(): string 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 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 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 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 public function getSupportOrDiagnosticSignalLabel(): string
{ {
return 'support_or_diagnostic'; return $this->requiredString('labels.support_or_diagnostic_signal');
} }
public function getSkuSignalLabel(): string public function getSkuSignalLabel(): string
{ {
return 'sku'; return $this->requiredString('labels.sku_signal');
} }
public function getPriceSignalLabel(): string public function getPriceSignalLabel(): string
{ {
return 'price'; return $this->requiredString('labels.price_signal');
} }
public function getSizeSignalLabel(): string public function getSizeSignalLabel(): string
{ {
return 'size'; return $this->requiredString('labels.size_signal');
} }
public function getSizeTokenSignalLabel(): string public function getSizeTokenSignalLabel(): string
{ {
return 'size_token'; return $this->requiredString('labels.size_token_signal');
} }
public function getColorSignalLabel(): string public function getColorSignalLabel(): string
{ {
return 'color'; return $this->requiredString('labels.color_signal');
} }
public function getAdvisorySignalPrefix(): string public function getAdvisorySignalPrefix(): string
{ {
return 'advisory:'; return $this->requiredString('labels.advisory_signal_prefix');
} }
public function getAdvisoryProductSelectionSignalLabel(): string public function getAdvisoryProductSelectionSignalLabel(): string
{ {
return 'advisory_product_selection'; return $this->requiredString('labels.advisory_product_selection_signal');
} }
public function getProductSearchMinScore(): int public function getProductSearchMinScore(): int
{ {
return 3; return $this->requiredInt('scores.product_search_min_score');
} }
public function getAdvisoryProductSearchMinScore(): int public function getAdvisoryProductSearchMinScore(): int
{ {
return 2; return $this->requiredInt('scores.advisory_product_search_min_score');
} }
public function getStrongSignalScore(): int public function getStrongSignalScore(): int
{ {
return 3; return $this->requiredInt('scores.strong_signal_score');
} }
public function getSkuSignalScore(): int public function getSkuSignalScore(): int
{ {
return 2; return $this->requiredInt('scores.sku_signal_score');
} }
public function getPriceSignalScore(): int public function getPriceSignalScore(): int
{ {
return 2; return $this->requiredInt('scores.price_signal_score');
} }
public function getSizeSignalScore(): int public function getSizeSignalScore(): int
{ {
return 2; return $this->requiredInt('scores.size_signal_score');
} }
public function getSizeTokenSignalScore(): int public function getSizeTokenSignalScore(): int
{ {
return 1; return $this->requiredInt('scores.size_token_signal_score');
} }
public function getColorSignalScore(): int public function getColorSignalScore(): int
{ {
return 1; return $this->requiredInt('scores.color_signal_score');
} }
public function getAdvisorySignalScore(): int public function getAdvisorySignalScore(): int
{ {
return 1; return $this->requiredInt('scores.advisory_signal_score');
} }
public function getAdvisoryProductSelectionSignalScore(): int public function getAdvisoryProductSelectionSignalScore(): int
{ {
return 3; return $this->requiredInt('scores.advisory_product_selection_signal_score');
} }
public function getModelLikeProductPattern(): string 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 public function getModelLikeProductSignalLabel(): string
{ {
return 'model_like_product'; return $this->requiredString('labels.model_like_product_signal');
} }
public function getModelLikeProductSignalScore(): int 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[] */ /** @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)) { if (!is_array($value)) {
return $default; throw new \InvalidArgumentException(sprintf('RetrieX commerce intent config key "%s" must be a list.', $key));
} }
$out = []; $out = [];
@@ -391,6 +279,43 @@ final class CommerceIntentConfig
$out[] = $item; $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<string, string> $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;
} }
} }

View File

@@ -645,12 +645,33 @@ final readonly class RetriexEffectiveConfigProvider
'commerce' => [ 'commerce' => [
'strong_signals' => $this->commerceIntentConfig->getStrongSignalsList(), 'strong_signals' => $this->commerceIntentConfig->getStrongSignalsList(),
'advisory_signals' => $this->commerceIntentConfig->getAdvisorySignals(), 'advisory_signals' => $this->commerceIntentConfig->getAdvisorySignals(),
'advisory_product_selection_patterns' => $this->commerceIntentConfig->getAdvisoryProductSelectionPatterns(),
'price_terms' => $this->commerceIntentConfig->getPriceTerms(), 'price_terms' => $this->commerceIntentConfig->getPriceTerms(),
'color_terms' => $this->commerceIntentConfig->getColorTerms(), 'color_terms' => $this->commerceIntentConfig->getColorTerms(),
'size_token_terms' => $this->commerceIntentConfig->getSizeTokenTerms(), 'size_token_terms' => $this->commerceIntentConfig->getSizeTokenTerms(),
'size_terms' => $this->commerceIntentConfig->getSizeTerms(), 'size_terms' => $this->commerceIntentConfig->getSizeTerms(),
'support_diagnostic_patterns' => $this->commerceIntentConfig->getSupportDiagnosticPatterns(), 'support_diagnostic_patterns' => $this->commerceIntentConfig->getSupportDiagnosticPatterns(),
'explicit_commerce_intent_patterns' => $this->commerceIntentConfig->getExplicitCommerceIntentPatterns(), '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' => [ 'thresholds' => [
'product_search_min_score' => $this->commerceIntentConfig->getProductSearchMinScore(), 'product_search_min_score' => $this->commerceIntentConfig->getProductSearchMinScore(),
'advisory_product_search_min_score' => $this->commerceIntentConfig->getAdvisoryProductSearchMinScore(), 'advisory_product_search_min_score' => $this->commerceIntentConfig->getAdvisoryProductSearchMinScore(),
@@ -661,6 +682,7 @@ final readonly class RetriexEffectiveConfigProvider
'size_token_signal_score' => $this->commerceIntentConfig->getSizeTokenSignalScore(), 'size_token_signal_score' => $this->commerceIntentConfig->getSizeTokenSignalScore(),
'color_signal_score' => $this->commerceIntentConfig->getColorSignalScore(), 'color_signal_score' => $this->commerceIntentConfig->getColorSignalScore(),
'advisory_signal_score' => $this->commerceIntentConfig->getAdvisorySignalScore(), 'advisory_signal_score' => $this->commerceIntentConfig->getAdvisorySignalScore(),
'advisory_product_selection_signal_score' => $this->commerceIntentConfig->getAdvisoryProductSelectionSignalScore(),
'model_like_product_signal_score' => $this->commerceIntentConfig->getModelLikeProductSignalScore(), 'model_like_product_signal_score' => $this->commerceIntentConfig->getModelLikeProductSignalScore(),
], ],
], ],
@@ -681,7 +703,6 @@ final readonly class RetriexEffectiveConfigProvider
]; ];
} }
/** @return array<string, mixed> */
private function languageConfig(): array private function languageConfig(): array
{ {
return ['stopwords' => $this->stopWordsConfig->getStopWords()]; return ['stopwords' => $this->stopWordsConfig->getStopWords()];