p77+78+79
This commit is contained in:
@@ -175,6 +175,8 @@ parameters:
|
|||||||
allowed_terms: []
|
allowed_terms: []
|
||||||
blocked_terms: []
|
blocked_terms: []
|
||||||
code_patterns: []
|
code_patterns: []
|
||||||
|
adjacent_variant_patterns: []
|
||||||
|
adjacent_variant_terms: []
|
||||||
|
|
||||||
|
|
||||||
attribute_cleanup:
|
attribute_cleanup:
|
||||||
|
|||||||
@@ -241,34 +241,20 @@ parameters:
|
|||||||
action_type: shop_search
|
action_type: shop_search
|
||||||
shop_results:
|
shop_results:
|
||||||
- label: Preis anzeigen
|
- label: Preis anzeigen
|
||||||
prompt: Zeige mir die Preise zu {shop_query}.
|
prompt: Zeige mir die Preise zu {shop_price_query}.
|
||||||
action_type: price_details
|
action_type: price_details
|
||||||
hide_when_answer_matches_any:
|
hide_when_answer_matches_any:
|
||||||
- '/\bkeine?\s+(?:passende[nrs]?\s+)?(?:produktbezeichnung|shop-?treffer|treffer|produkte?)\b/iu'
|
- '/\b(?:preis(?:angabe|information|e)?|preise?)\b.{0,100}\b(?:nicht|kein(?:e|en)?|ohne)\b.{0,100}\b(?:angegeben|vorhanden|enthalten|ausgewiesen|gefunden|verfügbar|verfuegbar)\b/iu'
|
||||||
- '/\bkeine?\b.{0,80}\b(?:explizit|direkt|passende[nrs]?|exakte[nrs]?)\b.{0,80}\b(?:produktbezeichnung|produktreihe|produktnummer|produkte?|shop-?treffer|treffer)\b/iu'
|
- '/\b(?:nicht|kein(?:e|en)?|ohne)\b.{0,100}\b(?:preis(?:angabe|information|e)?|preise?)\b/iu'
|
||||||
- '/\b(?:technische\s+eignung\s+nicht\s+sicher\s+belegt|ohne\s+technische[nr]?\s+eignung(?:snachweis)?|keine?\b.{0,80}\btechnische[nr]?\s+eignung)\b/iu'
|
- '/\bpreis(?:angabe|information|e)?\b.{0,100}\bauf anfrage\b/iu'
|
||||||
- '/\b(?:preis|preise|kosten)\b.{0,140}\b(?:nicht\s+(?:explizit\s+)?(?:angegeben|enthalten|verfuegbar|verfügbar|bekannt|ausgewiesen)|nur\s+auf\s+anfrage|auf\s+anfrage|keine?\s+preis(?:angabe|information|e)?)\b/iu'
|
|
||||||
- '/\b(?:exakte[nrs]?|konkrete[nrs]?|aktuelle[nrs]?)\s+preis\b.{0,140}\b(?:nicht|muesste|müsste|anfrage|abrufen|angefragt)\b/iu'
|
|
||||||
- label: Nur Zubehör anzeigen
|
- label: Nur Zubehör anzeigen
|
||||||
prompt: Suche im Shop nach {shop_query} und zeige daraus nur Zubehör.
|
prompt: Suche im Shop nach {shop_query} und zeige daraus nur Zubehör.
|
||||||
action_type: role_filter
|
action_type: role_filter
|
||||||
target_role: accessory_or_consumable
|
target_role: accessory_or_consumable
|
||||||
hide_when_answer_matches_any:
|
|
||||||
- '/\bkeine?\s+(?:passende[nrs]?\s+)?(?:produktbezeichnung|shop-?treffer|treffer|produkte?)\b/iu'
|
|
||||||
- '/\bkeine?\b.{0,80}\b(?:explizit|direkt|passende[nrs]?|exakte[nrs]?)\b.{0,80}\b(?:produktbezeichnung|produktreihe|produktnummer|produkte?|shop-?treffer|treffer)\b/iu'
|
|
||||||
- '/\b(?:technische\s+eignung\s+nicht\s+sicher\s+belegt|ohne\s+technische[nr]?\s+eignung(?:snachweis)?|keine?\b.{0,80}\btechnische[nr]?\s+eignung)\b/iu'
|
|
||||||
- '/\b(?:preis|preise|kosten)\b.{0,140}\b(?:nicht\s+(?:explizit\s+)?(?:angegeben|enthalten|verfuegbar|verfügbar|bekannt|ausgewiesen)|nur\s+auf\s+anfrage|auf\s+anfrage|keine?\s+preis(?:angabe|information|e)?)\b/iu'
|
|
||||||
- '/\b(?:einzige[nrs]?\s+(?:direkte[nrs]?\s+)?(?:lösung|loesung|messgerät|messgeraet|option)|nur\s+ein\s+(?:direkte[nrs]?\s+)?(?:messgerät|messgeraet|produkt))\b/iu'
|
|
||||||
- label: Nur Geräte anzeigen
|
- label: Nur Geräte anzeigen
|
||||||
prompt: Suche im Shop nach {shop_query} und zeige daraus nur Geräte.
|
prompt: Suche im Shop nach {shop_query} und zeige daraus nur Geräte.
|
||||||
action_type: role_filter
|
action_type: role_filter
|
||||||
target_role: main_device
|
target_role: main_device
|
||||||
hide_when_answer_matches_any:
|
|
||||||
- '/\bkeine?\s+(?:passende[nrs]?\s+)?(?:produktbezeichnung|shop-?treffer|treffer|produkte?)\b/iu'
|
|
||||||
- '/\bkeine?\b.{0,80}\b(?:explizit|direkt|passende[nrs]?|exakte[nrs]?)\b.{0,80}\b(?:produktbezeichnung|produktreihe|produktnummer|produkte?|shop-?treffer|treffer)\b/iu'
|
|
||||||
- '/\b(?:technische\s+eignung\s+nicht\s+sicher\s+belegt|ohne\s+technische[nr]?\s+eignung(?:snachweis)?|keine?\b.{0,80}\btechnische[nr]?\s+eignung)\b/iu'
|
|
||||||
- '/\b(?:preis|preise|kosten)\b.{0,140}\b(?:nicht\s+(?:explizit\s+)?(?:angegeben|enthalten|verfuegbar|verfügbar|bekannt|ausgewiesen)|nur\s+auf\s+anfrage|auf\s+anfrage|keine?\s+preis(?:angabe|information|e)?)\b/iu'
|
|
||||||
- '/\b(?:einzige[nrs]?\s+(?:direkte[nrs]?\s+)?(?:lösung|loesung|messgerät|messgeraet|option)|nur\s+ein\s+(?:direkte[nrs]?\s+)?(?:messgerät|messgeraet|produkt))\b/iu'
|
|
||||||
knowledge:
|
knowledge:
|
||||||
- label: Technische Details anzeigen
|
- label: Technische Details anzeigen
|
||||||
prompt: Zeige nur zusätzliche technische Details zu {answer_anchor}.
|
prompt: Zeige nur zusätzliche technische Details zu {answer_anchor}.
|
||||||
@@ -276,8 +262,7 @@ parameters:
|
|||||||
requires_answer_anchor: true
|
requires_answer_anchor: true
|
||||||
hide_when_answer_detail_score_at_least: 2
|
hide_when_answer_detail_score_at_least: 2
|
||||||
hide_when_answer_matches_any:
|
hide_when_answer_matches_any:
|
||||||
- '/\b(?:Grenzwert(?:e)?|Messbereich(?:e)?|Messparameter|Indikator(?:typ)?|Einsatzgebiet(?:e)?|Technische Einordnung|Technische Eignung|Produktnummer|Verfügbarkeit)\b/iu'
|
- '/\b(?:Grenzwert(?:e)?|Messbereich(?:e)?|Messparameter|Indikator(?:typ)?|Einsatzgebiet(?:e)?|Technische Einordnung|Technische Eignung|Produkt-?Nummer|Verfügbar|Verfuegbar|Preisinformation|Preisangabe)\b/iu'
|
||||||
- '/\b(?:preis|preise|kosten)\b.{0,140}\b(?:nicht\s+(?:explizit\s+)?(?:angegeben|enthalten|verfuegbar|verfügbar|bekannt|ausgewiesen)|nur\s+auf\s+anfrage|auf\s+anfrage|keine?\s+preis(?:angabe|information|e)?)\b/iu'
|
|
||||||
source_labels:
|
source_labels:
|
||||||
external_url: Externe URL
|
external_url: Externe URL
|
||||||
rag_knowledge: RAG Wissen
|
rag_knowledge: RAG Wissen
|
||||||
|
|||||||
@@ -1267,6 +1267,18 @@ parameters:
|
|||||||
- '/^\d+(?:[,.]\d+)?(?:m|mm|cm|ml|l)$/iu'
|
- '/^\d+(?:[,.]\d+)?(?:m|mm|cm|ml|l)$/iu'
|
||||||
- '/^[a-z]{1,4}\d{1,5}[a-z0-9-]*$/iu'
|
- '/^[a-z]{1,4}\d{1,5}[a-z0-9-]*$/iu'
|
||||||
- '/^\d{1,5}[a-z0-9-]*$/iu'
|
- '/^\d{1,5}[a-z0-9-]*$/iu'
|
||||||
|
# Pure alpha model suffixes are only preserved when they appear next
|
||||||
|
# to numeric/model context in the same query, e.g. family number suffix.
|
||||||
|
# The explicit list is a domain-maintained safety anchor for known model
|
||||||
|
# suffixes; the pattern keeps the behavior extensible for new variants.
|
||||||
|
adjacent_variant_terms:
|
||||||
|
- thcl
|
||||||
|
- clf
|
||||||
|
- clt
|
||||||
|
- cl
|
||||||
|
- cal
|
||||||
|
adjacent_variant_patterns:
|
||||||
|
- '/^[a-z]{2,8}\d{0,4}$/iu'
|
||||||
compound_prefix_match:
|
compound_prefix_match:
|
||||||
origin: genre_native
|
origin: genre_native
|
||||||
terms:
|
terms:
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# RETRIEX PATCH 78 - Follow-up Price Target Anchor
|
||||||
|
|
||||||
|
## Ziel
|
||||||
|
|
||||||
|
`Preis anzeigen` darf bei einer bereits sichtbaren konkreten Produktempfehlung nicht wieder auf die ursprüngliche breite Shopquery zurückfallen.
|
||||||
|
|
||||||
|
Beispiel:
|
||||||
|
|
||||||
|
- Antwort empfiehlt sichtbar `Testomat 2000 THCL®` mit Produktnummer `100276`.
|
||||||
|
- Die Follow-up-Action darf nicht `Zeige mir die Preise zu gesamtchlor messgerät.` senden.
|
||||||
|
- Sie sendet nun fokussiert auf das sichtbare Produkt, z. B. `Zeige mir die Preise zu Testomat 2000 THCL® 100276.`
|
||||||
|
|
||||||
|
## Hintergrund
|
||||||
|
|
||||||
|
Die Action-Prompts nutzten bisher nur `{shop_query}`. Bei beratenden Fragen ist diese Query oft bewusst breit (`gesamtchlor messgerät`). Nach einer konkreten Antwort führt das bei Preis-Follow-ups zu generischen Shop-/RAG-Antworten und kann die zuvor korrekte Zielvariante wieder verlieren.
|
||||||
|
|
||||||
|
## Umsetzung
|
||||||
|
|
||||||
|
- `buildFollowUpActionContext()` erzeugt zusätzlich `shop_price_query`.
|
||||||
|
- `shop_price_query` wird bevorzugt aus sichtbaren Shop-Produkten in der Antwort gebildet.
|
||||||
|
- Bei genau einem sichtbaren Hauptgerät wird Produktname + Produktnummer verwendet.
|
||||||
|
- Bei genau einem sichtbaren Produkt allgemein wird ebenfalls Produktname + Produktnummer verwendet.
|
||||||
|
- Fallback bleibt der vorhandene Antwortanker bzw. die ursprüngliche Shopquery.
|
||||||
|
- `chat-messages.yaml` nutzt für die Preis-Action `{shop_price_query}` statt `{shop_query}`.
|
||||||
|
|
||||||
|
## Nicht geändert
|
||||||
|
|
||||||
|
- Kein Retrieval-/Ranking-Eingriff.
|
||||||
|
- Keine Shop-Matching-Änderung.
|
||||||
|
- Keine Testomat-/THCL-/Gesamtchlor-Sonderlogik im PHP-Core.
|
||||||
|
- Rollenfilter-Actions bleiben unverändert und profitieren nur indirekt von fokussierteren Preis-Follow-ups.
|
||||||
|
|
||||||
|
## Lokale Checks
|
||||||
|
|
||||||
|
- `php -l src/Agent/AgentRunner.php`
|
||||||
|
- YAML parse von `config/retriex/chat-messages.yaml`
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# RetrieX Patch p79 - Model Variant Positive Filter Guard
|
||||||
|
|
||||||
|
## Ziel
|
||||||
|
|
||||||
|
Der positive Shopquery-Filter durfte numerische Modell-/Artikelcodes behalten, hat aber reine Modellvariantensuffixe wie `THCL` aus fokussierten Folgefragen entfernt. Dadurch wurde aus einem konkreten Preis-Follow-up wie `Testomat 2000 THCL 100276` die zu breite Shopquery `testomat 2000 100276`.
|
||||||
|
|
||||||
|
p79 erhält solche Variantentokens generisch, wenn sie in unmittelbarem numerischem Modellkontext stehen.
|
||||||
|
|
||||||
|
## Änderungen
|
||||||
|
|
||||||
|
- `AgentRunner::filterShopQueryToPositiveTokens()` bewahrt jetzt konfigurierte benachbarte Variantentokens, wenn sie:
|
||||||
|
- entweder in der YAML-gepflegten Liste `adjacent_variant_terms` stehen oder gegen `adjacent_variant_patterns` matchen,
|
||||||
|
- direkt neben einem numerischen Modell-/Artikelkontext stehen,
|
||||||
|
- und in der Umgebung bereits mindestens zwei positive Kontexttokens erhalten bleiben.
|
||||||
|
- Neue YAML-Konfiguration:
|
||||||
|
- `shop_query_runtime.positive_token_filter.adjacent_variant_terms`
|
||||||
|
- `shop_query_runtime.positive_token_filter.adjacent_variant_patterns`
|
||||||
|
- Legacy-Fallbacks unter `agent.shop_runtime.query_cleanup.positive_token_filter.*`
|
||||||
|
- Die Water-Analysis-Genre-Konfiguration ergänzt bekannte Modell-/Variantensuffixe wie `thcl`, `clf`, `clt`, `cl`, `cal` als zentrale Liste. Die generische Pattern-Erkennung bleibt zusätzlich aktiv für neue Varianten.
|
||||||
|
- `Preis anzeigen` wird über YAML ausgeblendet, wenn die Antwort bereits eine fehlende/nicht verfügbare Preisinformation formuliert.
|
||||||
|
- `Technische Details anzeigen` wird über YAML defensiver ausgeblendet, wenn die Antwort bereits Produktnummer, Verfügbarkeit, Messparameter oder technische Eignung enthält.
|
||||||
|
|
||||||
|
## Nicht geändert
|
||||||
|
|
||||||
|
- Kein Eingriff in Retrieval, Ranking, Shop-Matching oder PromptBuilder.
|
||||||
|
- Keine Testomat-/THCL-/Gesamtchlor-Sonderlogik im PHP-Core.
|
||||||
|
- Keine neue harte Variantentokenliste im PHP-Core; bekannte Suffixe liegen in YAML.
|
||||||
|
|
||||||
|
## Beispiel
|
||||||
|
|
||||||
|
Vorher:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Zeige mir die Preise zu Testomat 2000 THCL® 100276.
|
||||||
|
=> testomat 2000 100276
|
||||||
|
```
|
||||||
|
|
||||||
|
Nachher:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Zeige mir die Preise zu Testomat 2000 THCL® 100276.
|
||||||
|
=> testomat 2000 thcl 100276
|
||||||
|
```
|
||||||
|
|
||||||
|
## Lokale Checks
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php -l src/Agent/AgentRunner.php
|
||||||
|
php -l src/Config/AgentRunnerConfig.php
|
||||||
|
php -l src/Config/ChatMessagesConfig.php
|
||||||
|
python3 YAML parse OK
|
||||||
|
PCRE patterns OK
|
||||||
|
variant preservation smoke OK
|
||||||
|
adjacent variant term list smoke OK
|
||||||
|
```
|
||||||
@@ -1714,24 +1714,32 @@ final readonly class AgentRunner
|
|||||||
$allowedTokens = $this->buildPositiveShopQueryAllowedTokenSet();
|
$allowedTokens = $this->buildPositiveShopQueryAllowedTokenSet();
|
||||||
$blockedTokens = $this->buildPositiveShopQueryBlockedTokenSet();
|
$blockedTokens = $this->buildPositiveShopQueryBlockedTokenSet();
|
||||||
$codePatterns = $this->agentRunnerConfig->getShopQueryPositiveTokenFilterCodePatterns();
|
$codePatterns = $this->agentRunnerConfig->getShopQueryPositiveTokenFilterCodePatterns();
|
||||||
|
$adjacentVariantPatterns = $this->agentRunnerConfig->getShopQueryPositiveTokenFilterAdjacentVariantPatterns();
|
||||||
|
$adjacentVariantTokens = $this->buildShopQueryTokenSet(
|
||||||
|
$this->agentRunnerConfig->getShopQueryPositiveTokenFilterAdjacentVariantTerms()
|
||||||
|
);
|
||||||
|
|
||||||
if ($allowedTokens === [] && $codePatterns === []) {
|
if ($allowedTokens === [] && $codePatterns === [] && $adjacentVariantPatterns === [] && $adjacentVariantTokens === []) {
|
||||||
return $shopSearchQuery;
|
return $shopSearchQuery;
|
||||||
}
|
}
|
||||||
|
|
||||||
$modelVariantSuffixTokens = $this->extractPositiveShopQueryModelVariantSuffixTokens($tokens, $blockedTokens, $codePatterns);
|
|
||||||
|
|
||||||
$kept = [];
|
$kept = [];
|
||||||
foreach ($tokens as $token) {
|
foreach ($tokens as $token) {
|
||||||
if (isset($blockedTokens[$token]) || isset($kept[$token])) {
|
if (isset($blockedTokens[$token]) || isset($kept[$token])) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (isset($allowedTokens[$token]) || $this->matchesAnyConfiguredShopQueryCodePattern($token, $codePatterns)) {
|
||||||
isset($allowedTokens[$token])
|
$kept[$token] = $token;
|
||||||
|| isset($modelVariantSuffixTokens[$token])
|
}
|
||||||
|| $this->matchesAnyConfiguredShopQueryCodePattern($token, $codePatterns)
|
}
|
||||||
) {
|
|
||||||
|
foreach ($tokens as $index => $token) {
|
||||||
|
if (isset($blockedTokens[$token]) || isset($kept[$token])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->shouldKeepAdjacentVariantShopQueryToken($token, $index, $tokens, $kept, $adjacentVariantPatterns, $adjacentVariantTokens)) {
|
||||||
$kept[$token] = $token;
|
$kept[$token] = $token;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1740,11 +1748,62 @@ final readonly class AgentRunner
|
|||||||
return $shopSearchQuery;
|
return $shopSearchQuery;
|
||||||
}
|
}
|
||||||
|
|
||||||
$filtered = implode(' ', array_values($kept));
|
$filteredTokens = [];
|
||||||
|
foreach ($tokens as $token) {
|
||||||
|
if (isset($kept[$token])) {
|
||||||
|
$filteredTokens[] = $token;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$filtered = implode(' ', $filteredTokens);
|
||||||
|
|
||||||
return $filtered !== '' ? $filtered : $shopSearchQuery;
|
return $filtered !== '' ? $filtered : $shopSearchQuery;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string[] $tokens
|
||||||
|
* @param array<string, string> $kept
|
||||||
|
* @param string[] $variantPatterns
|
||||||
|
* @param array<string, true> $variantTokens
|
||||||
|
*/
|
||||||
|
private function shouldKeepAdjacentVariantShopQueryToken(
|
||||||
|
string $token,
|
||||||
|
int $index,
|
||||||
|
array $tokens,
|
||||||
|
array $kept,
|
||||||
|
array $variantPatterns,
|
||||||
|
array $variantTokens
|
||||||
|
): bool {
|
||||||
|
if (!isset($variantTokens[$token]) && !$this->matchesAnyConfiguredShopQueryCodePattern($token, $variantPatterns)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$hasAdjacentNumericContext = false;
|
||||||
|
$nearbyKeptContextCount = 0;
|
||||||
|
|
||||||
|
for ($offset = -2; $offset <= 2; ++$offset) {
|
||||||
|
if ($offset === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$nearbyIndex = $index + $offset;
|
||||||
|
if (!isset($tokens[$nearbyIndex])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$nearbyToken = $tokens[$nearbyIndex];
|
||||||
|
if (isset($kept[$nearbyToken])) {
|
||||||
|
++$nearbyKeptContextCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (abs($offset) === 1 && preg_match('/\d/u', $nearbyToken) === 1) {
|
||||||
|
$hasAdjacentNumericContext = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $hasAdjacentNumericContext && $nearbyKeptContextCount >= 2;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array<string, true>
|
* @return array<string, true>
|
||||||
*/
|
*/
|
||||||
@@ -1813,53 +1872,6 @@ final readonly class AgentRunner
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Preserve model variant suffixes that are attached to an already retained
|
|
||||||
* model number in the same query, for example family-number-code product
|
|
||||||
* names. This prevents the positive-token filter from degrading a specific
|
|
||||||
* model variant to its generic base model.
|
|
||||||
*
|
|
||||||
* @param string[] $tokens
|
|
||||||
* @param array<string, true> $blockedTokens
|
|
||||||
* @param string[] $codePatterns
|
|
||||||
* @return array<string, true>
|
|
||||||
*/
|
|
||||||
private function extractPositiveShopQueryModelVariantSuffixTokens(
|
|
||||||
array $tokens,
|
|
||||||
array $blockedTokens,
|
|
||||||
array $codePatterns
|
|
||||||
): array {
|
|
||||||
$suffixTokens = [];
|
|
||||||
$count = count($tokens);
|
|
||||||
|
|
||||||
for ($index = 0; $index < $count; $index++) {
|
|
||||||
$token = $tokens[$index] ?? '';
|
|
||||||
if (!$this->matchesAnyConfiguredShopQueryCodePattern($token, $codePatterns)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
for ($suffixIndex = $index + 1; $suffixIndex < $count; $suffixIndex++) {
|
|
||||||
$suffix = $tokens[$suffixIndex] ?? '';
|
|
||||||
|
|
||||||
if (isset($blockedTokens[$suffix]) || !$this->isPositiveShopQueryModelVariantSuffixToken($suffix)) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
$suffixTokens[$suffix] = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $suffixTokens;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function isPositiveShopQueryModelVariantSuffixToken(string $token): bool
|
|
||||||
{
|
|
||||||
$token = trim($token);
|
|
||||||
|
|
||||||
return $token !== ''
|
|
||||||
&& preg_match('/^[\p{L}]{2,8}\d{0,3}$/u', $token) === 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function cleanupDirectProductAttributeShopQuery(string $prompt, string $shopSearchQuery): string
|
private function cleanupDirectProductAttributeShopQuery(string $prompt, string $shopSearchQuery): string
|
||||||
{
|
{
|
||||||
$shopSearchQuery = trim($shopSearchQuery);
|
$shopSearchQuery = trim($shopSearchQuery);
|
||||||
@@ -5290,20 +5302,24 @@ final readonly class AgentRunner
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param ShopProductResult[] $shopResults
|
* @param ShopProductResult[] $shopResults
|
||||||
* @return array{shop_query:string, answer_has_price:bool, answer_text:string, answer_anchor:string, answer_detail_score:int, role_counts:array<string,int>}
|
* @return array{shop_query:string, shop_price_query:string, answer_has_price:bool, answer_text:string, answer_anchor:string, answer_detail_score:int, role_counts:array<string,int>}
|
||||||
*/
|
*/
|
||||||
private function buildFollowUpActionContext(array $shopResults, string $shopSearchQuery, string $answerText): array
|
private function buildFollowUpActionContext(array $shopResults, string $shopSearchQuery, string $answerText): array
|
||||||
{
|
{
|
||||||
$plainAnswerText = $this->normalizeOneLine($this->plainTextFromHtml($answerText));
|
$plainAnswerText = $this->normalizeOneLine($this->plainTextFromHtml($answerText));
|
||||||
$roleCounts = $this->buildFollowUpActionRoleCounts($shopResults);
|
$roleCounts = $this->buildFollowUpActionRoleCounts($shopResults);
|
||||||
$displayedRoleCounts = $this->buildFollowUpActionDisplayedRoleCounts($shopResults, $plainAnswerText);
|
$displayedProducts = $this->buildFollowUpActionDisplayedProducts($shopResults, $plainAnswerText);
|
||||||
|
$displayedRoleCounts = $this->buildFollowUpActionProductRoleCounts($displayedProducts);
|
||||||
|
|
||||||
if (array_sum($displayedRoleCounts) > 0) {
|
if (array_sum($displayedRoleCounts) > 0) {
|
||||||
$roleCounts = $displayedRoleCounts;
|
$roleCounts = $displayedRoleCounts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$normalizedShopQuery = $this->normalizeOneLine($shopSearchQuery);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'shop_query' => $this->normalizeOneLine($shopSearchQuery),
|
'shop_query' => $normalizedShopQuery,
|
||||||
|
'shop_price_query' => $this->buildFollowUpActionPriceQuery($displayedProducts, $plainAnswerText, $normalizedShopQuery),
|
||||||
'answer_has_price' => $this->followUpActionAnswerAlreadyContainsPrice($plainAnswerText),
|
'answer_has_price' => $this->followUpActionAnswerAlreadyContainsPrice($plainAnswerText),
|
||||||
'answer_text' => $plainAnswerText,
|
'answer_text' => $plainAnswerText,
|
||||||
'answer_anchor' => $this->buildFollowUpActionAnswerAnchor($plainAnswerText),
|
'answer_anchor' => $this->buildFollowUpActionAnswerAnchor($plainAnswerText),
|
||||||
@@ -5346,15 +5362,17 @@ final readonly class AgentRunner
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param ShopProductResult[] $shopResults
|
* @param ShopProductResult[] $shopResults
|
||||||
* @return array<string, int>
|
* @return ShopProductResult[]
|
||||||
*/
|
*/
|
||||||
private function buildFollowUpActionDisplayedRoleCounts(array $shopResults, string $answerText): array
|
private function buildFollowUpActionDisplayedProducts(array $shopResults, string $answerText): array
|
||||||
{
|
{
|
||||||
$roleCounts = $this->emptyFollowUpActionRoleCounts();
|
|
||||||
if ($answerText === '') {
|
if ($answerText === '') {
|
||||||
return $roleCounts;
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$products = [];
|
||||||
|
$seen = [];
|
||||||
|
|
||||||
foreach ($shopResults as $product) {
|
foreach ($shopResults as $product) {
|
||||||
if (!$product instanceof ShopProductResult) {
|
if (!$product instanceof ShopProductResult) {
|
||||||
continue;
|
continue;
|
||||||
@@ -5364,6 +5382,31 @@ final readonly class AgentRunner
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$key = mb_strtolower($this->normalizeOneLine(($product->productNumber ?? '') . ' ' . $product->name), 'UTF-8');
|
||||||
|
if ($key === '' || isset($seen[$key])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$seen[$key] = true;
|
||||||
|
$products[] = $product;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $products;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ShopProductResult[] $products
|
||||||
|
* @return array<string, int>
|
||||||
|
*/
|
||||||
|
private function buildFollowUpActionProductRoleCounts(array $products): array
|
||||||
|
{
|
||||||
|
$roleCounts = $this->emptyFollowUpActionRoleCounts();
|
||||||
|
|
||||||
|
foreach ($products as $product) {
|
||||||
|
if (!$product instanceof ShopProductResult) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
$this->countFollowUpActionProductRole($roleCounts, $product);
|
$this->countFollowUpActionProductRole($roleCounts, $product);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5409,6 +5452,93 @@ final readonly class AgentRunner
|
|||||||
return str_contains($normalizedAnswer, $productName);
|
return str_contains($normalizedAnswer, $productName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ShopProductResult[] $displayedProducts
|
||||||
|
*/
|
||||||
|
private function buildFollowUpActionPriceQuery(array $displayedProducts, string $answerText, string $fallbackShopQuery): string
|
||||||
|
{
|
||||||
|
$numberedProducts = $this->filterFollowUpActionDisplayedProductsWithNumber($displayedProducts, $answerText);
|
||||||
|
if (count($numberedProducts) === 1) {
|
||||||
|
return $this->buildFollowUpActionProductQuery($numberedProducts[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$focusedProducts = $this->filterFollowUpActionPrimaryDisplayedProducts($displayedProducts);
|
||||||
|
if (count($focusedProducts) === 1) {
|
||||||
|
return $this->buildFollowUpActionProductQuery($focusedProducts[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count($displayedProducts) === 1) {
|
||||||
|
return $this->buildFollowUpActionProductQuery($displayedProducts[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$answerAnchor = $this->buildFollowUpActionAnswerAnchor($answerText);
|
||||||
|
if ($answerAnchor !== '') {
|
||||||
|
return $answerAnchor;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $fallbackShopQuery;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ShopProductResult[] $products
|
||||||
|
* @return ShopProductResult[]
|
||||||
|
*/
|
||||||
|
private function filterFollowUpActionDisplayedProductsWithNumber(array $products, string $answerText): array
|
||||||
|
{
|
||||||
|
$numberedProducts = [];
|
||||||
|
$normalizedAnswer = mb_strtolower($this->normalizeOneLine($answerText), 'UTF-8');
|
||||||
|
|
||||||
|
foreach ($products as $product) {
|
||||||
|
if (!$product instanceof ShopProductResult) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$productNumber = $this->normalizeOneLine((string) $product->productNumber);
|
||||||
|
if ($productNumber === '' || mb_strlen($productNumber, 'UTF-8') < 3) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_contains($normalizedAnswer, mb_strtolower($productNumber, 'UTF-8'))) {
|
||||||
|
$numberedProducts[] = $product;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $numberedProducts;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ShopProductResult[] $products
|
||||||
|
* @return ShopProductResult[]
|
||||||
|
*/
|
||||||
|
private function filterFollowUpActionPrimaryDisplayedProducts(array $products): array
|
||||||
|
{
|
||||||
|
$mainDevices = [];
|
||||||
|
|
||||||
|
foreach ($products as $product) {
|
||||||
|
if (!$product instanceof ShopProductResult) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->resolveFollowUpActionShopProductRole($product) === ProductRoleResolver::ROLE_MAIN_DEVICE) {
|
||||||
|
$mainDevices[] = $product;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $mainDevices;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildFollowUpActionProductQuery(ShopProductResult $product): string
|
||||||
|
{
|
||||||
|
$parts = [$product->name];
|
||||||
|
|
||||||
|
$productNumber = $this->normalizeOneLine((string) $product->productNumber);
|
||||||
|
if ($productNumber !== '') {
|
||||||
|
$parts[] = $productNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->normalizeOneLine(implode(' ', array_filter($parts, static fn(string $part): bool => trim($part) !== '')));
|
||||||
|
}
|
||||||
|
|
||||||
private function buildFollowUpActionAnswerAnchor(string $answerText): string
|
private function buildFollowUpActionAnswerAnchor(string $answerText): string
|
||||||
{
|
{
|
||||||
$anchors = [];
|
$anchors = [];
|
||||||
@@ -5490,14 +5620,14 @@ final readonly class AgentRunner
|
|||||||
|
|
||||||
private function followUpActionAnswerAlreadyContainsPrice(string $answerText): bool
|
private function followUpActionAnswerAlreadyContainsPrice(string $answerText): bool
|
||||||
{
|
{
|
||||||
return preg_match('/(?:\bpreise?\b.{0,80}\d+[,.]\d{2}\s*(?:€|eur\b)|\d+[,.]\d{2}\s*(?:€|eur\b)|(?:€|eur\b)\s*\d+[,.]\d{2})/iu', $answerText) === 1;
|
return preg_match('/(?:\bpreis\b.{0,24}\d+[,.]\d{2}|\d+[,.]\d{2}\s*(?:€|eur)\b|(?:€|eur)\s*\d+[,.]\d{2})/iu', $answerText) === 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<int, array<string, mixed>> $actions
|
* @param array<int, array<string, mixed>> $actions
|
||||||
* @param array<string, bool> $seenActionKeys
|
* @param array<string, bool> $seenActionKeys
|
||||||
* @param array<int, array<string, mixed>> $items
|
* @param array<int, array<string, mixed>> $items
|
||||||
* @param array{shop_query:string, answer_has_price:bool, answer_text:string, answer_anchor:string, answer_detail_score:int, role_counts:array<string,int>} $context
|
* @param array{shop_query:string, shop_price_query:string, answer_has_price:bool, answer_text:string, answer_anchor:string, answer_detail_score:int, role_counts:array<string,int>} $context
|
||||||
*/
|
*/
|
||||||
private function appendFollowUpActions(array &$actions, array &$seenActionKeys, array $items, array $context): void
|
private function appendFollowUpActions(array &$actions, array &$seenActionKeys, array $items, array $context): void
|
||||||
{
|
{
|
||||||
@@ -5533,7 +5663,7 @@ final readonly class AgentRunner
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<string, mixed> $item
|
* @param array<string, mixed> $item
|
||||||
* @param array{shop_query:string, answer_has_price:bool, answer_text:string, answer_anchor:string, answer_detail_score:int, role_counts:array<string,int>} $context
|
* @param array{shop_query:string, shop_price_query:string, answer_has_price:bool, answer_text:string, answer_anchor:string, answer_detail_score:int, role_counts:array<string,int>} $context
|
||||||
*/
|
*/
|
||||||
private function shouldShowFollowUpAction(array $item, array $context): bool
|
private function shouldShowFollowUpAction(array $item, array $context): bool
|
||||||
{
|
{
|
||||||
@@ -5558,7 +5688,7 @@ final readonly class AgentRunner
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($actionType === 'price_details') {
|
if ($actionType === 'price_details') {
|
||||||
return $context['shop_query'] !== '' && !$context['answer_has_price'];
|
return $context['shop_price_query'] !== '' && !$context['answer_has_price'];
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($actionType === 'role_filter') {
|
if ($actionType === 'role_filter') {
|
||||||
@@ -5658,12 +5788,13 @@ final readonly class AgentRunner
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array{shop_query:string, answer_has_price:bool, answer_text:string, answer_anchor:string, answer_detail_score:int, role_counts:array<string,int>} $context
|
* @param array{shop_query:string, shop_price_query:string, answer_has_price:bool, answer_text:string, answer_anchor:string, answer_detail_score:int, role_counts:array<string,int>} $context
|
||||||
*/
|
*/
|
||||||
private function renderFollowUpActionPrompt(string $prompt, array $context): string
|
private function renderFollowUpActionPrompt(string $prompt, array $context): string
|
||||||
{
|
{
|
||||||
$rendered = strtr($prompt, [
|
$rendered = strtr($prompt, [
|
||||||
'{shop_query}' => $context['shop_query'],
|
'{shop_query}' => $context['shop_query'],
|
||||||
|
'{shop_price_query}' => $context['shop_price_query'],
|
||||||
'{answer_anchor}' => $context['answer_anchor'],
|
'{answer_anchor}' => $context['answer_anchor'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -1364,6 +1364,24 @@ final class AgentRunnerConfig
|
|||||||
?: $this->getOptionalStringList('shop_runtime.query_cleanup.positive_token_filter.code_patterns');
|
?: $this->getOptionalStringList('shop_runtime.query_cleanup.positive_token_filter.code_patterns');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string[]
|
||||||
|
*/
|
||||||
|
public function getShopQueryPositiveTokenFilterAdjacentVariantPatterns(): array
|
||||||
|
{
|
||||||
|
return $this->genreStringList('shop_query_runtime.positive_token_filter.adjacent_variant_patterns')
|
||||||
|
?: $this->getOptionalStringList('shop_runtime.query_cleanup.positive_token_filter.adjacent_variant_patterns');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string[]
|
||||||
|
*/
|
||||||
|
public function getShopQueryPositiveTokenFilterAdjacentVariantTerms(): array
|
||||||
|
{
|
||||||
|
return $this->genreStringList('shop_query_runtime.positive_token_filter.adjacent_variant_terms')
|
||||||
|
?: $this->getOptionalStringList('shop_runtime.query_cleanup.positive_token_filter.adjacent_variant_terms');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return string[]
|
* @return string[]
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1366,7 +1366,9 @@ final readonly class RetriexEffectiveConfigProvider
|
|||||||
|
|
||||||
$this->validateStringList($this->toList($positiveTokenFilter['allowed_terms'] ?? []), 'genre.configuration_values.shop_query_runtime.positive_token_filter.allowed_terms', $errors, $warnings);
|
$this->validateStringList($this->toList($positiveTokenFilter['allowed_terms'] ?? []), 'genre.configuration_values.shop_query_runtime.positive_token_filter.allowed_terms', $errors, $warnings);
|
||||||
$this->validateStringList($this->toList($positiveTokenFilter['blocked_terms'] ?? []), 'genre.configuration_values.shop_query_runtime.positive_token_filter.blocked_terms', $errors, $warnings);
|
$this->validateStringList($this->toList($positiveTokenFilter['blocked_terms'] ?? []), 'genre.configuration_values.shop_query_runtime.positive_token_filter.blocked_terms', $errors, $warnings);
|
||||||
|
$this->validateStringList($this->toList($positiveTokenFilter['adjacent_variant_terms'] ?? []), 'genre.configuration_values.shop_query_runtime.positive_token_filter.adjacent_variant_terms', $errors, $warnings);
|
||||||
$this->validateRegexPatternList($positiveTokenFilter['code_patterns'] ?? [], 'genre.configuration_values.shop_query_runtime.positive_token_filter.code_patterns', $errors);
|
$this->validateRegexPatternList($positiveTokenFilter['code_patterns'] ?? [], 'genre.configuration_values.shop_query_runtime.positive_token_filter.code_patterns', $errors);
|
||||||
|
$this->validateRegexPatternList($positiveTokenFilter['adjacent_variant_patterns'] ?? [], 'genre.configuration_values.shop_query_runtime.positive_token_filter.adjacent_variant_patterns', $errors);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($this->collectGenreConfigurationValueSourcePaths($configurationValues) as $valuePath => $sourcePaths) {
|
foreach ($this->collectGenreConfigurationValueSourcePaths($configurationValues) as $valuePath => $sourcePaths) {
|
||||||
|
|||||||
Reference in New Issue
Block a user