third step
This commit is contained in:
91
RETRIEX_INTENT_LIGHT_SALES_YAML_ONLY_PATCH_README.md
Normal file
91
RETRIEX_INTENT_LIGHT_SALES_YAML_ONLY_PATCH_README.md
Normal file
@@ -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.
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
# Intent vocabulary and pattern configuration.
|
# 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:
|
parameters:
|
||||||
retriex.intent.commerce.config:
|
retriex.intent.commerce.config:
|
||||||
strong_signals:
|
strong_signals:
|
||||||
@@ -141,6 +142,7 @@ parameters:
|
|||||||
max_allowed_score: 1.0
|
max_allowed_score: 1.0
|
||||||
|
|
||||||
retriex.intent.light.config:
|
retriex.intent.light.config:
|
||||||
|
list_threshold: 4
|
||||||
quantity_words:
|
quantity_words:
|
||||||
- alle
|
- alle
|
||||||
- sämtliche
|
- sämtliche
|
||||||
@@ -175,6 +177,8 @@ parameters:
|
|||||||
- '/\btop\s*\d+\b/u'
|
- '/\btop\s*\d+\b/u'
|
||||||
|
|
||||||
retriex.intent.sales.config:
|
retriex.intent.sales.config:
|
||||||
|
dominance_delta: 2
|
||||||
|
min_score_threshold: 3
|
||||||
sales_signals:
|
sales_signals:
|
||||||
- preis
|
- preis
|
||||||
- preise
|
- preise
|
||||||
|
|||||||
@@ -4,71 +4,72 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Config;
|
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<string, mixed> $config
|
* @param array<string, mixed> $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[] */
|
/** @return string[] */
|
||||||
public function getQuantityWords(): array
|
public function getQuantityWords(): array
|
||||||
{
|
{
|
||||||
return $this->stringList('quantity_words', self::QUANTITY_WORDS);
|
return $this->requiredStringList('quantity_words');
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return string[] */
|
/** @return string[] */
|
||||||
public function getStrongPatterns(): array
|
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[] */
|
/** @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)) {
|
if (!is_array($value)) {
|
||||||
return $default;
|
throw new \InvalidArgumentException(sprintf('RetrieX light intent config key "%s" must be a list.', $key));
|
||||||
}
|
}
|
||||||
|
|
||||||
$out = [];
|
$out = [];
|
||||||
@@ -78,13 +79,19 @@ class IntentLightConfig
|
|||||||
}
|
}
|
||||||
|
|
||||||
$item = trim((string) $item);
|
$item = trim((string) $item);
|
||||||
if ($item === '' || in_array($item, $out, true)) {
|
if ($item === '') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!in_array($item, $out, true)) {
|
||||||
$out[] = $item;
|
$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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -658,13 +658,13 @@ final readonly class RetriexEffectiveConfigProvider
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
'light' => [
|
'light' => [
|
||||||
'list_threshold' => IntentLightConfig::LIST_THRESHOLD,
|
'list_threshold' => $this->intentLightConfig->getListThreshold(),
|
||||||
'quantity_words' => $this->intentLightConfig->getQuantityWords(),
|
'quantity_words' => $this->intentLightConfig->getQuantityWords(),
|
||||||
'strong_patterns' => $this->intentLightConfig->getStrongPatterns(),
|
'strong_patterns' => $this->intentLightConfig->getStrongPatterns(),
|
||||||
],
|
],
|
||||||
'sales' => [
|
'sales' => [
|
||||||
'dominance_delta' => SalesIntentConfig::DOMINANCE_DELTA,
|
'dominance_delta' => $this->salesIntentConfig->getDominanceDelta(),
|
||||||
'min_score_threshold' => SalesIntentConfig::MIN_SCORE_THRESHOLD,
|
'min_score_threshold' => $this->salesIntentConfig->getMinScoreThreshold(),
|
||||||
'sales_signals' => $this->salesIntentConfig->getSalesSignals(),
|
'sales_signals' => $this->salesIntentConfig->getSalesSignals(),
|
||||||
'comparison_signals' => $this->salesIntentConfig->getComparisonSignals(),
|
'comparison_signals' => $this->salesIntentConfig->getComparisonSignals(),
|
||||||
'objection_signals' => $this->salesIntentConfig->getObjectionSignals(),
|
'objection_signals' => $this->salesIntentConfig->getObjectionSignals(),
|
||||||
|
|||||||
@@ -4,123 +4,95 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Config;
|
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<string, mixed> $config
|
* @param array<string, mixed> $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[] */
|
/** @return string[] */
|
||||||
public function getSalesSignals(): array
|
public function getSalesSignals(): array
|
||||||
{
|
{
|
||||||
return $this->stringList('sales_signals', self::SALES_SIGNALS);
|
return $this->requiredStringList('sales_signals');
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return string[] */
|
/** @return string[] */
|
||||||
public function getComparisonSignals(): array
|
public function getComparisonSignals(): array
|
||||||
{
|
{
|
||||||
return $this->stringList('comparison_signals', self::COMPARISON_SIGNALS);
|
return $this->requiredStringList('comparison_signals');
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return string[] */
|
/** @return string[] */
|
||||||
public function getObjectionSignals(): array
|
public function getObjectionSignals(): array
|
||||||
{
|
{
|
||||||
return $this->stringList('objection_signals', self::OBJECTION_SIGNALS);
|
return $this->requiredStringList('objection_signals');
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return string[] */
|
/** @return string[] */
|
||||||
public function getImplementationSignals(): array
|
public function getImplementationSignals(): array
|
||||||
{
|
{
|
||||||
return $this->stringList('implementation_signals', self::IMPLEMENTATION_SIGNALS);
|
return $this->requiredStringList('implementation_signals');
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return string[] */
|
/** @return string[] */
|
||||||
public function getRoiSignals(): array
|
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[] */
|
/** @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)) {
|
if (!is_array($value)) {
|
||||||
return $default;
|
throw new \InvalidArgumentException(sprintf('RetrieX sales intent config key "%s" must be a list.', $key));
|
||||||
}
|
}
|
||||||
|
|
||||||
$out = [];
|
$out = [];
|
||||||
@@ -130,13 +102,19 @@ class SalesIntentConfig
|
|||||||
}
|
}
|
||||||
|
|
||||||
$item = trim((string) $item);
|
$item = trim((string) $item);
|
||||||
if ($item === '' || in_array($item, $out, true)) {
|
if ($item === '') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!in_array($item, $out, true)) {
|
||||||
$out[] = $item;
|
$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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ final readonly class IntentLite
|
|||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
// Entscheidung
|
// Entscheidung
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
$isList = $score >= IntentLightConfig::LIST_THRESHOLD;
|
$isList = $score >= $this->config->getListThreshold();
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'is_list' => $isList,
|
'is_list' => $isList,
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ final class SalesIntentLite
|
|||||||
$secondScore = $values[1] ?? 0;
|
$secondScore = $values[1] ?? 0;
|
||||||
|
|
||||||
// Kein relevanter Score → Discovery
|
// Kein relevanter Score → Discovery
|
||||||
if ($topScore < SalesIntentConfig::MIN_SCORE_THRESHOLD) {
|
if ($topScore < $this->config->getMinScoreThreshold()) {
|
||||||
return [
|
return [
|
||||||
'intent' => self::DISCOVERY,
|
'intent' => self::DISCOVERY,
|
||||||
'score' => 0,
|
'score' => 0,
|
||||||
@@ -106,7 +106,7 @@ final class SalesIntentLite
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Keine klare Dominanz → Discovery
|
// Keine klare Dominanz → Discovery
|
||||||
if (($topScore - $secondScore) < SalesIntentConfig::DOMINANCE_DELTA) {
|
if (($topScore - $secondScore) < $this->config->getDominanceDelta()) {
|
||||||
return [
|
return [
|
||||||
'intent' => self::DISCOVERY,
|
'intent' => self::DISCOVERY,
|
||||||
'score' => $topScore,
|
'score' => $topScore,
|
||||||
|
|||||||
Reference in New Issue
Block a user