From 8ece67b4619f3f1a14ea1535694357f411e15662 Mon Sep 17 00:00:00 2001 From: team2 Date: Wed, 29 Apr 2026 21:27:24 +0200 Subject: [PATCH] third step --- ...TENT_LIGHT_SALES_YAML_ONLY_PATCH_README.md | 91 +++++++++++ config/retriex/intent.yaml | 6 +- src/Config/IntentLightConfig.php | 103 ++++++------ src/Config/RetriexEffectiveConfigProvider.php | 6 +- src/Config/SalesIntentConfig.php | 148 ++++++++---------- src/Intent/IntentLite.php | 2 +- src/Intent/SalesIntentLite.php | 4 +- 7 files changed, 220 insertions(+), 140 deletions(-) create mode 100644 RETRIEX_INTENT_LIGHT_SALES_YAML_ONLY_PATCH_README.md diff --git a/RETRIEX_INTENT_LIGHT_SALES_YAML_ONLY_PATCH_README.md b/RETRIEX_INTENT_LIGHT_SALES_YAML_ONLY_PATCH_README.md new file mode 100644 index 0000000..d287a8f --- /dev/null +++ b/RETRIEX_INTENT_LIGHT_SALES_YAML_ONLY_PATCH_README.md @@ -0,0 +1,91 @@ +# RetrieX Patch 3: IntentLight + SalesIntent YAML-only + +This patch continues the incremental YAML-source-of-truth migration. + +## Scope + +Only these intent areas are changed: + +- `IntentLightConfig` +- `SalesIntentConfig` +- direct usages of their former PHP constants +- the effective-config dump for these values +- `config/retriex/intent.yaml` + +No retrieval, prompt, shop, commerce-parser, SSE, or answer-generation logic is changed. + +## What changed + +### IntentLightConfig + +Removed PHP defaults: + +- `LIST_THRESHOLD` +- `QUANTITY_WORDS` +- `STRONG_PATTERNS` + +Added required YAML-backed accessors: + +- `getListThreshold()` +- `getQuantityWords()` +- `getStrongPatterns()` + +The previous values now live in: + +```yaml +retriex.intent.light.config: + list_threshold: 4 + quantity_words: [...] + strong_patterns: [...] +``` + +### SalesIntentConfig + +Removed PHP defaults: + +- `DOMINANCE_DELTA` +- `MIN_SCORE_THRESHOLD` +- `SALES_SIGNALS` +- `COMPARISON_SIGNALS` +- `OBJECTION_SIGNALS` +- `IMPLEMENTATION_SIGNALS` +- `ROI_SIGNALS` + +Added required YAML-backed accessors: + +- `getDominanceDelta()` +- `getMinScoreThreshold()` +- `getSalesSignals()` +- `getComparisonSignals()` +- `getObjectionSignals()` +- `getImplementationSignals()` +- `getRoiSignals()` + +The previous values now live in: + +```yaml +retriex.intent.sales.config: + dominance_delta: 2 + min_score_threshold: 3 + sales_signals: [...] + comparison_signals: [...] + objection_signals: [...] + implementation_signals: [...] + roi_signals: [...] +``` + +## Validation performed + +Static validation in the patch workspace: + +```bash +php -l src/Config/IntentLightConfig.php +php -l src/Config/SalesIntentConfig.php +php -l src/Intent/IntentLite.php +php -l src/Intent/SalesIntentLite.php +php -l src/Config/RetriexEffectiveConfigProvider.php +``` + +YAML was parsed successfully with the new keys. + +The full Symfony regression commands were not executed in the extracted ZIP workspace because `vendor/` is not part of the uploaded archive. diff --git a/config/retriex/intent.yaml b/config/retriex/intent.yaml index cea9474..7da0584 100644 --- a/config/retriex/intent.yaml +++ b/config/retriex/intent.yaml @@ -1,5 +1,6 @@ # Intent vocabulary and pattern configuration. -# Lists mirror the previous PHP defaults exactly; PHP defaults remain as fallback. +# Lists and thresholds mirror the previous PHP defaults exactly. +# Migrated config areas are YAML-only; remaining areas are migrated incrementally. parameters: retriex.intent.commerce.config: strong_signals: @@ -141,6 +142,7 @@ parameters: max_allowed_score: 1.0 retriex.intent.light.config: + list_threshold: 4 quantity_words: - alle - sämtliche @@ -175,6 +177,8 @@ parameters: - '/\btop\s*\d+\b/u' retriex.intent.sales.config: + dominance_delta: 2 + min_score_threshold: 3 sales_signals: - preis - preise diff --git a/src/Config/IntentLightConfig.php b/src/Config/IntentLightConfig.php index dbaf323..920e6e1 100644 --- a/src/Config/IntentLightConfig.php +++ b/src/Config/IntentLightConfig.php @@ -4,71 +4,72 @@ declare(strict_types=1); namespace App\Config; -class IntentLightConfig +/** + * YAML-backed deterministic list-intent configuration. + * + * This class intentionally has no PHP fallback values. Missing or invalid + * configuration must be fixed in config/retriex/intent.yaml. + */ +final class IntentLightConfig { - public const LIST_THRESHOLD = 4; - - private const QUANTITY_WORDS = [ - 'alle', - 'sämtliche', - 'saemtliche', - 'mehrere', - 'verschiedene', - 'einige', - 'viele', - 'optionen', - 'möglichkeiten', - 'moeglichkeiten', - 'varianten', - 'arten', - 'modelle', - 'funktionen', - 'punkte', - 'schritte', - 'kategorien', - 'übersicht', - 'uebersicht', - ]; - - private const STRONG_PATTERNS = [ - '/\bliste(n)?\b/u', - '/\bauflisten\b/u', - '/\baufz(a|ä)hl(en)?\b/u', - '/\bnenn(e)?\b/u', - '/\bzeig(e)?\b/u', - '/\bwelche\s+sind\b/u', - '/\bwelche\s+gibt\s+es\b/u', - '/\bwas\s+sind\b/u', - '/\bwie\s+viele\b/u', - '/\branking\b/u', - '/\btop\s*\d+\b/u', - ]; - /** * @param array $config */ - public function __construct(private readonly array $config = []) + public function __construct(private readonly array $config) { } + public function getListThreshold(): int + { + return $this->requiredPositiveInt('list_threshold'); + } + /** @return string[] */ public function getQuantityWords(): array { - return $this->stringList('quantity_words', self::QUANTITY_WORDS); + return $this->requiredStringList('quantity_words'); } /** @return string[] */ public function getStrongPatterns(): array { - return $this->stringList('strong_patterns', self::STRONG_PATTERNS); + return $this->requiredStringList('strong_patterns'); + } + + private function requiredPositiveInt(string $key): int + { + if (!array_key_exists($key, $this->config)) { + throw new \InvalidArgumentException(sprintf('Missing required RetrieX light intent config key "%s".', $key)); + } + + $value = $this->config[$key]; + + if (is_int($value)) { + $intValue = $value; + } elseif (is_string($value) && preg_match('/^-?\d+$/', trim($value)) === 1) { + $intValue = (int) trim($value); + } else { + throw new \InvalidArgumentException(sprintf('RetrieX light intent config key "%s" must be an integer.', $key)); + } + + if ($intValue <= 0) { + throw new \InvalidArgumentException(sprintf('RetrieX light intent config key "%s" must be greater than 0.', $key)); + } + + return $intValue; } /** @return string[] */ - private function stringList(string $key, array $default): array + private function requiredStringList(string $key): array { - $value = $this->config[$key] ?? $default; + if (!array_key_exists($key, $this->config)) { + throw new \InvalidArgumentException(sprintf('Missing required RetrieX light intent config key "%s".', $key)); + } + + $value = $this->config[$key]; + if (!is_array($value)) { - return $default; + throw new \InvalidArgumentException(sprintf('RetrieX light intent config key "%s" must be a list.', $key)); } $out = []; @@ -78,13 +79,19 @@ class IntentLightConfig } $item = trim((string) $item); - if ($item === '' || in_array($item, $out, true)) { + if ($item === '') { continue; } - $out[] = $item; + if (!in_array($item, $out, true)) { + $out[] = $item; + } } - return $out !== [] ? $out : $default; + if ($out === []) { + throw new \InvalidArgumentException(sprintf('RetrieX light intent config key "%s" must not be empty.', $key)); + } + + return $out; } } diff --git a/src/Config/RetriexEffectiveConfigProvider.php b/src/Config/RetriexEffectiveConfigProvider.php index 2bfff5d..99601eb 100644 --- a/src/Config/RetriexEffectiveConfigProvider.php +++ b/src/Config/RetriexEffectiveConfigProvider.php @@ -658,13 +658,13 @@ final readonly class RetriexEffectiveConfigProvider ], ], 'light' => [ - 'list_threshold' => IntentLightConfig::LIST_THRESHOLD, + 'list_threshold' => $this->intentLightConfig->getListThreshold(), 'quantity_words' => $this->intentLightConfig->getQuantityWords(), 'strong_patterns' => $this->intentLightConfig->getStrongPatterns(), ], 'sales' => [ - 'dominance_delta' => SalesIntentConfig::DOMINANCE_DELTA, - 'min_score_threshold' => SalesIntentConfig::MIN_SCORE_THRESHOLD, + 'dominance_delta' => $this->salesIntentConfig->getDominanceDelta(), + 'min_score_threshold' => $this->salesIntentConfig->getMinScoreThreshold(), 'sales_signals' => $this->salesIntentConfig->getSalesSignals(), 'comparison_signals' => $this->salesIntentConfig->getComparisonSignals(), 'objection_signals' => $this->salesIntentConfig->getObjectionSignals(), diff --git a/src/Config/SalesIntentConfig.php b/src/Config/SalesIntentConfig.php index bda11e3..62d2d2f 100644 --- a/src/Config/SalesIntentConfig.php +++ b/src/Config/SalesIntentConfig.php @@ -4,123 +4,95 @@ declare(strict_types=1); namespace App\Config; -class SalesIntentConfig +/** + * YAML-backed deterministic sales-intent configuration. + * + * This class intentionally has no PHP fallback values. Missing or invalid + * configuration must be fixed in config/retriex/intent.yaml. + */ +final class SalesIntentConfig { - // Minimum gap between Top 1 and Top 2 so that an intent is truly dominant. - public const DOMINANCE_DELTA = 2; - - // Minimum score required for any non-discovery intent to be accepted. - public const MIN_SCORE_THRESHOLD = 3; - - private const SALES_SIGNALS = [ - 'preis', - 'preise', - 'kosten', - 'lizenz', - 'lizenzmodell', - 'tarif', - 'tarife', - 'gebuehr', - 'gebühr', - 'monatlich', - 'jaehrlich', - 'jährlich', - 'abo', - 'subscription', - ]; - - private const COMPARISON_SIGNALS = [ - '/\bvergleich(en)?\b/u', - '/\bvs\b/u', - '/\bgegenueber\b/u', - '/\balternative(n)?\b/u', - '/\bunterschied(e)?\b/u', - '/\bbesser\b/u', - ]; - - private const OBJECTION_SIGNALS = [ - 'problem', - 'risiko', - 'nachteil', - 'datenschutz', - 'dsgvo', - 'sicherheit', - 'compliance', - 'kritik', - 'zweifel', - 'unsicher', - ]; - - private const IMPLEMENTATION_SIGNALS = [ - 'implementierung', - 'implementieren', - 'integration', - 'integrieren', - 'einführung', - 'einfuehrung', - 'aufwand', - 'setup', - 'rollout', - 'migration', - 'installation', - 'api', - 'schnittstelle', - ]; - - private const ROI_SIGNALS = [ - 'roi', - 'rentabilitaet', - 'rentabilität', - 'business case', - 'einsparung', - 'kosten senken', - 'umsatz steigern', - 'effizienz steigern', - ]; - /** * @param array $config */ - public function __construct(private readonly array $config = []) + public function __construct(private readonly array $config) { } + public function getDominanceDelta(): int + { + return $this->requiredNonNegativeInt('dominance_delta'); + } + + public function getMinScoreThreshold(): int + { + return $this->requiredNonNegativeInt('min_score_threshold'); + } + /** @return string[] */ public function getSalesSignals(): array { - return $this->stringList('sales_signals', self::SALES_SIGNALS); + return $this->requiredStringList('sales_signals'); } /** @return string[] */ public function getComparisonSignals(): array { - return $this->stringList('comparison_signals', self::COMPARISON_SIGNALS); + return $this->requiredStringList('comparison_signals'); } /** @return string[] */ public function getObjectionSignals(): array { - return $this->stringList('objection_signals', self::OBJECTION_SIGNALS); + return $this->requiredStringList('objection_signals'); } /** @return string[] */ public function getImplementationSignals(): array { - return $this->stringList('implementation_signals', self::IMPLEMENTATION_SIGNALS); + return $this->requiredStringList('implementation_signals'); } /** @return string[] */ public function getRoiSignals(): array { - return $this->stringList('roi_signals', self::ROI_SIGNALS); + return $this->requiredStringList('roi_signals'); + } + + private function requiredNonNegativeInt(string $key): int + { + if (!array_key_exists($key, $this->config)) { + throw new \InvalidArgumentException(sprintf('Missing required RetrieX sales intent config key "%s".', $key)); + } + + $value = $this->config[$key]; + + if (is_int($value)) { + $intValue = $value; + } elseif (is_string($value) && preg_match('/^-?\d+$/', trim($value)) === 1) { + $intValue = (int) trim($value); + } else { + throw new \InvalidArgumentException(sprintf('RetrieX sales intent config key "%s" must be an integer.', $key)); + } + + if ($intValue < 0) { + throw new \InvalidArgumentException(sprintf('RetrieX sales intent config key "%s" must be greater than or equal to 0.', $key)); + } + + return $intValue; } /** @return string[] */ - private function stringList(string $key, array $default): array + private function requiredStringList(string $key): array { - $value = $this->config[$key] ?? $default; + if (!array_key_exists($key, $this->config)) { + throw new \InvalidArgumentException(sprintf('Missing required RetrieX sales intent config key "%s".', $key)); + } + + $value = $this->config[$key]; + if (!is_array($value)) { - return $default; + throw new \InvalidArgumentException(sprintf('RetrieX sales intent config key "%s" must be a list.', $key)); } $out = []; @@ -130,13 +102,19 @@ class SalesIntentConfig } $item = trim((string) $item); - if ($item === '' || in_array($item, $out, true)) { + if ($item === '') { continue; } - $out[] = $item; + if (!in_array($item, $out, true)) { + $out[] = $item; + } } - return $out !== [] ? $out : $default; + if ($out === []) { + throw new \InvalidArgumentException(sprintf('RetrieX sales intent config key "%s" must not be empty.', $key)); + } + + return $out; } } diff --git a/src/Intent/IntentLite.php b/src/Intent/IntentLite.php index a696979..8bd21da 100644 --- a/src/Intent/IntentLite.php +++ b/src/Intent/IntentLite.php @@ -78,7 +78,7 @@ final readonly class IntentLite // -------------------------------------------------------- // Entscheidung // -------------------------------------------------------- - $isList = $score >= IntentLightConfig::LIST_THRESHOLD; + $isList = $score >= $this->config->getListThreshold(); return [ 'is_list' => $isList, diff --git a/src/Intent/SalesIntentLite.php b/src/Intent/SalesIntentLite.php index c9f582f..3566886 100644 --- a/src/Intent/SalesIntentLite.php +++ b/src/Intent/SalesIntentLite.php @@ -98,7 +98,7 @@ final class SalesIntentLite $secondScore = $values[1] ?? 0; // Kein relevanter Score → Discovery - if ($topScore < SalesIntentConfig::MIN_SCORE_THRESHOLD) { + if ($topScore < $this->config->getMinScoreThreshold()) { return [ 'intent' => self::DISCOVERY, 'score' => 0, @@ -106,7 +106,7 @@ final class SalesIntentLite } // Keine klare Dominanz → Discovery - if (($topScore - $secondScore) < SalesIntentConfig::DOMINANCE_DELTA) { + if (($topScore - $secondScore) < $this->config->getDominanceDelta()) { return [ 'intent' => self::DISCOVERY, 'score' => $topScore,