p77+78+79

This commit is contained in:
team 1
2026-05-10 09:09:51 +02:00
parent 96375668b2
commit 374c0c1f7f
8 changed files with 329 additions and 88 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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'],
]); ]);

View File

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

View File

@@ -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) {