add new configs
This commit is contained in:
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Intent;
|
||||
|
||||
use App\Config\CatalogIntentConfig;
|
||||
use App\Knowledge\Retrieval\QueryCleaner;
|
||||
use App\Tag\TagVectorSearchClient;
|
||||
use App\Tag\TagTypes;
|
||||
@@ -24,23 +25,12 @@ use App\Tag\TagTypes;
|
||||
* - SalesIntent
|
||||
* - Routing
|
||||
*/
|
||||
final class CatalogIntentLite
|
||||
final readonly class CatalogIntentLite
|
||||
{
|
||||
/**
|
||||
* Minimaler Similarity-Score.
|
||||
* Verhindert Rauschen.
|
||||
*/
|
||||
private const MIN_SCORE = 0.72;
|
||||
|
||||
/**
|
||||
* Differenz zwischen Top1 und Top2,
|
||||
* damit kein unsicherer Treffer akzeptiert wird.
|
||||
*/
|
||||
private const AMBIGUITY_DELTA = 0.02;
|
||||
|
||||
public function __construct(
|
||||
private readonly TagVectorSearchClient $tagVectorClient,
|
||||
private readonly QueryCleaner $queryCleaner,
|
||||
private TagVectorSearchClient $tagVectorClient,
|
||||
private QueryCleaner $queryCleaner
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -67,7 +57,7 @@ final class CatalogIntentLite
|
||||
$bestScore = (float)($best['score'] ?? 0.0);
|
||||
|
||||
// 2) Score-Tags
|
||||
if ($bestScore < self::MIN_SCORE) {
|
||||
if ($bestScore < CatalogIntentConfig::MIN_SCORE) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -75,7 +65,7 @@ final class CatalogIntentLite
|
||||
if (isset($hits[1])) {
|
||||
$secondScore = (float)($hits[1]['score'] ?? 0.0);
|
||||
|
||||
if (abs($bestScore - $secondScore) < self::AMBIGUITY_DELTA) {
|
||||
if (abs($bestScore - $secondScore) < CatalogIntentConfig::AMBIGUITY_DELTA) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,12 +4,21 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Intent;
|
||||
|
||||
use App\Config\CommerceIntentConfig;
|
||||
|
||||
final class CommerceIntentLite
|
||||
{
|
||||
public const NONE = 'none';
|
||||
public const PRODUCT_SEARCH = 'product_search';
|
||||
public const ADVISORY_PRODUCT_SEARCH = 'advisory_product_search';
|
||||
|
||||
public function __construct(
|
||||
private readonly CommerceIntentConfig $config
|
||||
)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{intent:string, score:int, signals:string[]}
|
||||
*/
|
||||
@@ -28,28 +37,7 @@ final class CommerceIntentLite
|
||||
$score = 0;
|
||||
$signals = [];
|
||||
|
||||
$strongSignals = [
|
||||
'suche',
|
||||
'habt',
|
||||
'gibt',
|
||||
'zeig',
|
||||
'welche',
|
||||
'vergleich',
|
||||
'alternativ',
|
||||
'find',
|
||||
'shop',
|
||||
'store',
|
||||
'sku',
|
||||
'Artikel',
|
||||
'Gerät',
|
||||
'testomat',
|
||||
'indikator',
|
||||
'Titromat',
|
||||
'Seminar',
|
||||
'Schulung',
|
||||
'Sensor',
|
||||
'liste'
|
||||
];
|
||||
$strongSignals = $this->config->getStrongSignalsList();
|
||||
|
||||
foreach ($strongSignals as $signal) {
|
||||
if (str_contains($p, strtolower($signal))) {
|
||||
@@ -58,40 +46,36 @@ final class CommerceIntentLite
|
||||
}
|
||||
}
|
||||
|
||||
if(preg_match('#\d{3,10}#', $p)){
|
||||
if (preg_match('#\d{3,10}#', $p)) {
|
||||
$score += 2;
|
||||
$signals[] = 'sku';
|
||||
}
|
||||
|
||||
if (preg_match('/\b\d+(?:[.,]\d+)?\s*(euro|€|eur|teuer|preis|kosten)\b/u', $p) === 1) {
|
||||
$pricePattern = $this->config->getPricePattern();
|
||||
if (preg_match('/\b\d+(?:[.,]\d+)?\s*(' . $pricePattern . ')\b/u', $p) === 1) {
|
||||
$score += 2;
|
||||
$signals[] = 'price';
|
||||
}
|
||||
|
||||
if (preg_match('/\b(größe|groesse|grösse)\s*[a-z0-9.-]+\b/u', $p) === 1) {
|
||||
$sizePattern = $this->config->getSizePattern();
|
||||
if (preg_match('/\b(' . $sizePattern . ')\s*[a-z0-9.-]+\b/u', $p) === 1) {
|
||||
$score += 2;
|
||||
$signals[] = 'size';
|
||||
}
|
||||
|
||||
if (preg_match('/\b(xs|s|m|l|xl|xxl|xxxl)\b/u', $p) === 1) {
|
||||
$sizeTokenPattern = $this->config->getSizeTokenPattern();
|
||||
if (preg_match('/\b(' . $sizeTokenPattern . ')\b/u', $p) === 1) {
|
||||
$score += 1;
|
||||
$signals[] = 'size_token';
|
||||
}
|
||||
|
||||
if (preg_match('/\b(schwarz|weiß|weiss|rot|blau|grün|gruen|gelb|grau|beige|rosa|pink|orange|braun)\b/u', $p) === 1) {
|
||||
$colorPattern = $this->config->getColorPattern();
|
||||
if (preg_match('/\b(' . $colorPattern . ')\b/u', $p) === 1) {
|
||||
$score += 1;
|
||||
$signals[] = 'color';
|
||||
}
|
||||
|
||||
$advisorySignals = [
|
||||
'passt',
|
||||
'eignet',
|
||||
'besser',
|
||||
'besten',
|
||||
'geeignet',
|
||||
'empfiehl',
|
||||
'empfehl',
|
||||
];
|
||||
$advisorySignals = $this->config->getAdvisorySignals();
|
||||
|
||||
foreach ($advisorySignals as $signal) {
|
||||
if (str_contains($p, $signal)) {
|
||||
|
||||
@@ -4,19 +4,27 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Intent;
|
||||
|
||||
use App\Config\IntentLightConfig;
|
||||
|
||||
/**
|
||||
* IntentLite
|
||||
*
|
||||
* Deterministische, LLM-agnostische Intent-Erkennung.
|
||||
* Fokus: LIST-Intent für Retrieval-Steuerung.
|
||||
* Deterministic, LLM-agnostic intent detection.
|
||||
* Focus: LIST intent for retrieval control.
|
||||
*
|
||||
* WICHTIG:
|
||||
* - Immer mit dem ORIGINAL-Prompt aufrufen.
|
||||
* - Nicht mit dem QueryCleaner-Ergebnis.
|
||||
* IMPORTANT:
|
||||
* - Always call it with the ORIGINAL prompt.
|
||||
* - Not with the QueryCleaner result.
|
||||
*/
|
||||
final class IntentLite
|
||||
final readonly class IntentLite
|
||||
{
|
||||
private const LIST_THRESHOLD = 4;
|
||||
|
||||
public function __construct(
|
||||
private IntentLightConfig $config
|
||||
)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public function detectList(string $originalPrompt): array
|
||||
{
|
||||
@@ -28,19 +36,7 @@ final class IntentLite
|
||||
// --------------------------------------------------------
|
||||
// 1. Starke explizite Listen-Trigger (hohes Gewicht)
|
||||
// --------------------------------------------------------
|
||||
$strongPatterns = [
|
||||
'/\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',
|
||||
];
|
||||
$strongPatterns = $this->config->getStrongPatterns();
|
||||
|
||||
foreach ($strongPatterns as $pattern) {
|
||||
if (preg_match($pattern, $p) === 1) {
|
||||
@@ -52,27 +48,7 @@ final class IntentLite
|
||||
// --------------------------------------------------------
|
||||
// 2. Mengen- / Mehrzahl-Indikatoren
|
||||
// --------------------------------------------------------
|
||||
$quantityWords = [
|
||||
'alle',
|
||||
'sämtliche',
|
||||
'saemtliche',
|
||||
'mehrere',
|
||||
'verschiedene',
|
||||
'einige',
|
||||
'viele',
|
||||
'optionen',
|
||||
'möglichkeiten',
|
||||
'moeglichkeiten',
|
||||
'varianten',
|
||||
'arten',
|
||||
'modelle',
|
||||
'funktionen',
|
||||
'punkte',
|
||||
'schritte',
|
||||
'kategorien',
|
||||
'übersicht',
|
||||
'uebersicht',
|
||||
];
|
||||
$quantityWords = $this->config->getQuantityWords();
|
||||
|
||||
foreach ($quantityWords as $word) {
|
||||
if (preg_match('/\b' . preg_quote($word, '/') . '\b/u', $p) === 1) {
|
||||
@@ -102,11 +78,11 @@ final class IntentLite
|
||||
// --------------------------------------------------------
|
||||
// Entscheidung
|
||||
// --------------------------------------------------------
|
||||
$isList = $score >= self::LIST_THRESHOLD;
|
||||
$isList = $score >= IntentLightConfig::LIST_THRESHOLD;
|
||||
|
||||
return [
|
||||
'is_list' => $isList,
|
||||
'score' => $score,
|
||||
'score' => $score,
|
||||
'signals' => $signals,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -4,26 +4,23 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Intent;
|
||||
|
||||
use App\Config\SalesIntentConfig;
|
||||
|
||||
final class SalesIntentLite
|
||||
{
|
||||
public const DISCOVERY = 'discovery';
|
||||
public const PRICING = 'pricing';
|
||||
public const COMPARISON = 'comparison';
|
||||
public const OBJECTION = 'objection';
|
||||
public const DISCOVERY = 'discovery';
|
||||
public const PRICING = 'pricing';
|
||||
public const COMPARISON = 'comparison';
|
||||
public const OBJECTION = 'objection';
|
||||
public const IMPLEMENTATION = 'implementation';
|
||||
public const ROI = 'roi';
|
||||
public const ROI = 'roi';
|
||||
|
||||
/**
|
||||
* Mindestabstand zwischen Top1 und Top2,
|
||||
* damit ein Intent wirklich dominant ist.
|
||||
*/
|
||||
private const DOMINANCE_DELTA = 2;
|
||||
public function __construct(
|
||||
private readonly SalesIntentConfig $config
|
||||
)
|
||||
{
|
||||
|
||||
/**
|
||||
* Mindestscore, damit überhaupt ein Nicht-Discovery-Intent
|
||||
* akzeptiert wird.
|
||||
*/
|
||||
private const MIN_SCORE_THRESHOLD = 3;
|
||||
}
|
||||
|
||||
public function detect(string $originalPrompt): array
|
||||
{
|
||||
@@ -32,27 +29,23 @@ final class SalesIntentLite
|
||||
if ($p === '') {
|
||||
return [
|
||||
'intent' => self::DISCOVERY,
|
||||
'score' => 0,
|
||||
'score' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
$scores = [
|
||||
self::PRICING => 0,
|
||||
self::COMPARISON => 0,
|
||||
self::OBJECTION => 0,
|
||||
self::PRICING => 0,
|
||||
self::COMPARISON => 0,
|
||||
self::OBJECTION => 0,
|
||||
self::IMPLEMENTATION => 0,
|
||||
self::ROI => 0,
|
||||
self::ROI => 0,
|
||||
];
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// PRICING
|
||||
// ------------------------------------------------------------
|
||||
foreach ([
|
||||
'preis','preise','kosten','lizenz','lizenzmodell',
|
||||
'tarif','tarife','gebuehr','gebühr',
|
||||
'monatlich','jaehrlich','jährlich','abo','subscription'
|
||||
] as $word) {
|
||||
if (preg_match('/\b'.preg_quote($word,'/').'\b/u', $p)) {
|
||||
foreach ($this->config->getSalesSignals() as $word) {
|
||||
if (preg_match('/\b' . preg_quote($word, '/') . '\b/u', $p)) {
|
||||
$scores[self::PRICING] += 3;
|
||||
}
|
||||
}
|
||||
@@ -60,14 +53,7 @@ final class SalesIntentLite
|
||||
// ------------------------------------------------------------
|
||||
// COMPARISON
|
||||
// ------------------------------------------------------------
|
||||
foreach ([
|
||||
'/\bvergleich(en)?\b/u',
|
||||
'/\bvs\b/u',
|
||||
'/\bgegenueber\b/u',
|
||||
'/\balternative(n)?\b/u',
|
||||
'/\bunterschied(e)?\b/u',
|
||||
'/\bbesser\b/u'
|
||||
] as $pattern) {
|
||||
foreach ($this->config->getComparisonSignals() as $pattern) {
|
||||
if (preg_match($pattern, $p)) {
|
||||
$scores[self::COMPARISON] += 3;
|
||||
}
|
||||
@@ -76,12 +62,8 @@ final class SalesIntentLite
|
||||
// ------------------------------------------------------------
|
||||
// OBJECTION
|
||||
// ------------------------------------------------------------
|
||||
foreach ([
|
||||
'problem','risiko','nachteil','datenschutz',
|
||||
'dsgvo','sicherheit','compliance',
|
||||
'kritik','zweifel','unsicher'
|
||||
] as $word) {
|
||||
if (preg_match('/\b'.preg_quote($word,'/').'\b/u', $p)) {
|
||||
foreach ($this->config->getComparisonSignals() as $word) {
|
||||
if (preg_match('/\b' . preg_quote($word, '/') . '\b/u', $p)) {
|
||||
$scores[self::OBJECTION] += 3;
|
||||
}
|
||||
}
|
||||
@@ -89,15 +71,8 @@ final class SalesIntentLite
|
||||
// ------------------------------------------------------------
|
||||
// IMPLEMENTATION
|
||||
// ------------------------------------------------------------
|
||||
foreach ([
|
||||
'implementierung','implementieren',
|
||||
'integration','integrieren',
|
||||
'einführung','einfuehrung',
|
||||
'aufwand','setup','rollout',
|
||||
'migration','installation',
|
||||
'api','schnittstelle'
|
||||
] as $word) {
|
||||
if (preg_match('/\b'.preg_quote($word,'/').'\b/u', $p)) {
|
||||
foreach ($this->config->getImplementationSignals() as $word) {
|
||||
if (preg_match('/\b' . preg_quote($word, '/') . '\b/u', $p)) {
|
||||
$scores[self::IMPLEMENTATION] += 3;
|
||||
}
|
||||
}
|
||||
@@ -105,13 +80,8 @@ final class SalesIntentLite
|
||||
// ------------------------------------------------------------
|
||||
// ROI
|
||||
// ------------------------------------------------------------
|
||||
foreach ([
|
||||
'roi','rentabilitaet','rentabilität',
|
||||
'business case','einsparung',
|
||||
'kosten senken','umsatz steigern',
|
||||
'effizienz steigern'
|
||||
] as $word) {
|
||||
if (preg_match('/\b'.preg_quote($word,'/').'\b/u', $p)) {
|
||||
foreach ($this->config->getRoiSignals() as $word) {
|
||||
if (preg_match('/\b' . preg_quote($word, '/') . '\b/u', $p)) {
|
||||
$scores[self::ROI] += 3;
|
||||
}
|
||||
}
|
||||
@@ -123,31 +93,31 @@ final class SalesIntentLite
|
||||
arsort($scores);
|
||||
|
||||
$intents = array_keys($scores);
|
||||
$values = array_values($scores);
|
||||
$values = array_values($scores);
|
||||
|
||||
$topIntent = $intents[0] ?? self::DISCOVERY;
|
||||
$topScore = $values[0] ?? 0;
|
||||
$topScore = $values[0] ?? 0;
|
||||
$secondScore = $values[1] ?? 0;
|
||||
|
||||
// Kein relevanter Score → Discovery
|
||||
if ($topScore < self::MIN_SCORE_THRESHOLD) {
|
||||
if ($topScore < SalesIntentConfig::MIN_SCORE_THRESHOLD) {
|
||||
return [
|
||||
'intent' => self::DISCOVERY,
|
||||
'score' => 0,
|
||||
'score' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
// Keine klare Dominanz → Discovery
|
||||
if (($topScore - $secondScore) < self::DOMINANCE_DELTA) {
|
||||
if (($topScore - $secondScore) < SalesIntentConfig::DOMINANCE_DELTA) {
|
||||
return [
|
||||
'intent' => self::DISCOVERY,
|
||||
'score' => $topScore,
|
||||
'score' => $topScore,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'intent' => $topIntent,
|
||||
'score' => $topScore,
|
||||
'score' => $topScore,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user