p77+78+79
This commit is contained in:
@@ -175,6 +175,8 @@ parameters:
|
||||
allowed_terms: []
|
||||
blocked_terms: []
|
||||
code_patterns: []
|
||||
adjacent_variant_patterns: []
|
||||
adjacent_variant_terms: []
|
||||
|
||||
|
||||
attribute_cleanup:
|
||||
|
||||
@@ -241,34 +241,20 @@ parameters:
|
||||
action_type: shop_search
|
||||
shop_results:
|
||||
- label: Preis anzeigen
|
||||
prompt: Zeige mir die Preise zu {shop_query}.
|
||||
prompt: Zeige mir die Preise zu {shop_price_query}.
|
||||
action_type: price_details
|
||||
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(?:exakte[nrs]?|konkrete[nrs]?|aktuelle[nrs]?)\s+preis\b.{0,140}\b(?:nicht|muesste|müsste|anfrage|abrufen|angefragt)\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'
|
||||
- '/\b(?:nicht|kein(?:e|en)?|ohne)\b.{0,100}\b(?:preis(?:angabe|information|e)?|preise?)\b/iu'
|
||||
- '/\bpreis(?:angabe|information|e)?\b.{0,100}\bauf anfrage\b/iu'
|
||||
- label: Nur Zubehör anzeigen
|
||||
prompt: Suche im Shop nach {shop_query} und zeige daraus nur Zubehör.
|
||||
action_type: role_filter
|
||||
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
|
||||
prompt: Suche im Shop nach {shop_query} und zeige daraus nur Geräte.
|
||||
action_type: role_filter
|
||||
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:
|
||||
- label: Technische Details anzeigen
|
||||
prompt: Zeige nur zusätzliche technische Details zu {answer_anchor}.
|
||||
@@ -276,8 +262,7 @@ parameters:
|
||||
requires_answer_anchor: true
|
||||
hide_when_answer_detail_score_at_least: 2
|
||||
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(?: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(?:Grenzwert(?:e)?|Messbereich(?:e)?|Messparameter|Indikator(?:typ)?|Einsatzgebiet(?:e)?|Technische Einordnung|Technische Eignung|Produkt-?Nummer|Verfügbar|Verfuegbar|Preisinformation|Preisangabe)\b/iu'
|
||||
source_labels:
|
||||
external_url: Externe URL
|
||||
rag_knowledge: RAG Wissen
|
||||
|
||||
@@ -1267,6 +1267,18 @@ parameters:
|
||||
- '/^\d+(?:[,.]\d+)?(?:m|mm|cm|ml|l)$/iu'
|
||||
- '/^[a-z]{1,4}\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:
|
||||
origin: genre_native
|
||||
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();
|
||||
$blockedTokens = $this->buildPositiveShopQueryBlockedTokenSet();
|
||||
$codePatterns = $this->agentRunnerConfig->getShopQueryPositiveTokenFilterCodePatterns();
|
||||
$adjacentVariantPatterns = $this->agentRunnerConfig->getShopQueryPositiveTokenFilterAdjacentVariantPatterns();
|
||||
$adjacentVariantTokens = $this->buildShopQueryTokenSet(
|
||||
$this->agentRunnerConfig->getShopQueryPositiveTokenFilterAdjacentVariantTerms()
|
||||
);
|
||||
|
||||
if ($allowedTokens === [] && $codePatterns === []) {
|
||||
if ($allowedTokens === [] && $codePatterns === [] && $adjacentVariantPatterns === [] && $adjacentVariantTokens === []) {
|
||||
return $shopSearchQuery;
|
||||
}
|
||||
|
||||
$modelVariantSuffixTokens = $this->extractPositiveShopQueryModelVariantSuffixTokens($tokens, $blockedTokens, $codePatterns);
|
||||
|
||||
$kept = [];
|
||||
foreach ($tokens as $token) {
|
||||
if (isset($blockedTokens[$token]) || isset($kept[$token])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
isset($allowedTokens[$token])
|
||||
|| isset($modelVariantSuffixTokens[$token])
|
||||
|| $this->matchesAnyConfiguredShopQueryCodePattern($token, $codePatterns)
|
||||
) {
|
||||
if (isset($allowedTokens[$token]) || $this->matchesAnyConfiguredShopQueryCodePattern($token, $codePatterns)) {
|
||||
$kept[$token] = $token;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1740,11 +1748,62 @@ final readonly class AgentRunner
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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>
|
||||
*/
|
||||
@@ -1813,53 +1872,6 @@ final readonly class AgentRunner
|
||||
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
|
||||
{
|
||||
$shopSearchQuery = trim($shopSearchQuery);
|
||||
@@ -5290,20 +5302,24 @@ final readonly class AgentRunner
|
||||
|
||||
/**
|
||||
* @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
|
||||
{
|
||||
$plainAnswerText = $this->normalizeOneLine($this->plainTextFromHtml($answerText));
|
||||
$roleCounts = $this->buildFollowUpActionRoleCounts($shopResults);
|
||||
$displayedRoleCounts = $this->buildFollowUpActionDisplayedRoleCounts($shopResults, $plainAnswerText);
|
||||
$displayedProducts = $this->buildFollowUpActionDisplayedProducts($shopResults, $plainAnswerText);
|
||||
$displayedRoleCounts = $this->buildFollowUpActionProductRoleCounts($displayedProducts);
|
||||
|
||||
if (array_sum($displayedRoleCounts) > 0) {
|
||||
$roleCounts = $displayedRoleCounts;
|
||||
}
|
||||
|
||||
$normalizedShopQuery = $this->normalizeOneLine($shopSearchQuery);
|
||||
|
||||
return [
|
||||
'shop_query' => $this->normalizeOneLine($shopSearchQuery),
|
||||
'shop_query' => $normalizedShopQuery,
|
||||
'shop_price_query' => $this->buildFollowUpActionPriceQuery($displayedProducts, $plainAnswerText, $normalizedShopQuery),
|
||||
'answer_has_price' => $this->followUpActionAnswerAlreadyContainsPrice($plainAnswerText),
|
||||
'answer_text' => $plainAnswerText,
|
||||
'answer_anchor' => $this->buildFollowUpActionAnswerAnchor($plainAnswerText),
|
||||
@@ -5346,15 +5362,17 @@ final readonly class AgentRunner
|
||||
|
||||
/**
|
||||
* @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 === '') {
|
||||
return $roleCounts;
|
||||
return [];
|
||||
}
|
||||
|
||||
$products = [];
|
||||
$seen = [];
|
||||
|
||||
foreach ($shopResults as $product) {
|
||||
if (!$product instanceof ShopProductResult) {
|
||||
continue;
|
||||
@@ -5364,6 +5382,31 @@ final readonly class AgentRunner
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -5409,6 +5452,93 @@ final readonly class AgentRunner
|
||||
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
|
||||
{
|
||||
$anchors = [];
|
||||
@@ -5490,14 +5620,14 @@ final readonly class AgentRunner
|
||||
|
||||
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<string, bool> $seenActionKeys
|
||||
* @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
|
||||
{
|
||||
@@ -5533,7 +5663,7 @@ final readonly class AgentRunner
|
||||
|
||||
/**
|
||||
* @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
|
||||
{
|
||||
@@ -5558,7 +5688,7 @@ final readonly class AgentRunner
|
||||
}
|
||||
|
||||
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') {
|
||||
@@ -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
|
||||
{
|
||||
$rendered = strtr($prompt, [
|
||||
'{shop_query}' => $context['shop_query'],
|
||||
'{shop_price_query}' => $context['shop_price_query'],
|
||||
'{answer_anchor}' => $context['answer_anchor'],
|
||||
]);
|
||||
|
||||
|
||||
@@ -1364,6 +1364,24 @@ final class AgentRunnerConfig
|
||||
?: $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[]
|
||||
*/
|
||||
|
||||
@@ -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['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['adjacent_variant_patterns'] ?? [], 'genre.configuration_values.shop_query_runtime.positive_token_filter.adjacent_variant_patterns', $errors);
|
||||
}
|
||||
|
||||
foreach ($this->collectGenreConfigurationValueSourcePaths($configurationValues) as $valuePath => $sourcePaths) {
|
||||
|
||||
Reference in New Issue
Block a user