third step

This commit is contained in:
team2
2026-04-29 21:27:24 +02:00
parent b71adf33f0
commit 8ece67b461
7 changed files with 220 additions and 140 deletions

View File

@@ -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<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[] */
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;
}
}

View File

@@ -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(),

View File

@@ -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<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[] */
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;
}
}

View File

@@ -78,7 +78,7 @@ final readonly class IntentLite
// --------------------------------------------------------
// Entscheidung
// --------------------------------------------------------
$isList = $score >= IntentLightConfig::LIST_THRESHOLD;
$isList = $score >= $this->config->getListThreshold();
return [
'is_list' => $isList,

View File

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