p75+76
This commit is contained in:
@@ -245,18 +245,30 @@ parameters:
|
||||
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'
|
||||
- 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}.
|
||||
@@ -264,7 +276,8 @@ parameters:
|
||||
requires_answer_anchor: true
|
||||
hide_when_answer_detail_score_at_least: 2
|
||||
hide_when_answer_matches_any:
|
||||
- '/\b(?:Grenzwert(?:e)?|Messbereich(?:e)?|Indikator(?:typ)?|Einsatzgebiet(?:e)?|Technische Einordnung)\b/iu'
|
||||
- '/\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'
|
||||
source_labels:
|
||||
external_url: Externe URL
|
||||
rag_knowledge: RAG Wissen
|
||||
|
||||
@@ -8,7 +8,7 @@ parameters:
|
||||
retriex.commerce.sales_channel_access_key: '%env(SHOPWARE_SALES_CHANNEL_ACCESS_KEY)%'
|
||||
|
||||
retriex.commerce.search_repair.enabled: true
|
||||
retriex.commerce.search_repair.max_queries: 1
|
||||
retriex.commerce.search_repair.max_queries: 2
|
||||
retriex.commerce.search_repair.min_primary_results_without_repair: 2
|
||||
|
||||
# Commerce query parser configuration.
|
||||
|
||||
@@ -1437,6 +1437,8 @@ parameters:
|
||||
- '- If the user asks for the price or availability of a referenced accessory, indicator, reagent, kit, set, or consumable, use commercial fields only from a shop result that clearly matches that accessory identity and code.'
|
||||
- '- If an accessory, indicator, reagent, kit, set, or consumable code is explicitly requested, do not merge shop variants whose code has an additional suffix, prefix, or variant token unless the user explicitly requested that full variant code.'
|
||||
- '- For such accessory price follow-ups, do not answer with the price, URL, product number, or availability of the main device or of unrelated reagents; if no matching accessory shop item is present, say that the price is not available in the provided shop data.'
|
||||
- '- If retrieved knowledge identifies a concrete device/model variant with a suffix or code and live shop data contains the same concrete identity, answer with that specific variant instead of downgrading to the generic base family.'
|
||||
- '- If the primary shop hit is only a generic base family but extended shop search provides more specific RAG-identified variants, use the specific variants for the product recommendation and keep any generic base-family hit separate.'
|
||||
prompt_keyword_views:
|
||||
origin: genre_native
|
||||
technical_product_keywords:
|
||||
@@ -1593,11 +1595,11 @@ parameters:
|
||||
specific_model_candidate_patterns:
|
||||
- /\b([A-Za-zÄÖÜäöüß][A-Za-zÄÖÜäöüß®\-]*(?:\s+[A-Za-zÄÖÜäöüß0-9][A-Za-zÄÖÜäöüß0-9®\-]*){0,3}\s+\d{2,5}(?:\s+[A-ZÄÖÜ]{1,8})?)\b/u
|
||||
patterns:
|
||||
model_candidate: /\b([A-Za-zÄÖÜäöüß][A-Za-zÄÖÜäöüß®\-]*(?:\s+[A-Za-zÄÖÜäöüß][A-Za-zÄÖÜäöüß®\-]*){0,2}\s+\d{2,5}[A-Za-z0-9\-]*)\b/u
|
||||
model_candidate: /\b([A-Za-zÄÖÜäöüß][A-Za-zÄÖÜäöüß®\-]*(?:\s+[A-Za-zÄÖÜäöüß][A-Za-zÄÖÜäöüß®\-]*){0,2}\s+\d{2,5}[A-Za-z0-9\-]*(?:\s+[A-Za-zÄÖÜäöüß][A-Za-zÄÖÜäöüß0-9\-]{1,7})?)\b/u
|
||||
accessory_candidate_template: /\b((?:{terms})\s+\d{1,5}[A-Za-z0-9\-]*)\b/iu
|
||||
requested_accessory_code: /\b(?:indikator(?:typ)?|indicator(?:\s*type)?|reagenz|reagent)\s*([A-Za-z]{0,3}\s*\d{1,5}[A-Za-z0-9\-]*)\b/iu
|
||||
accessory_or_bundle_template: /\b({terms})\b/iu
|
||||
model_like: /\b[A-Za-zÄÖÜäöüß][A-Za-zÄÖÜäöüß®\-]*(?:\s+[A-Za-zÄÖÜäöüß][A-Za-zÄÖÜäöüß®\-]*){0,2}\s+\d{2,5}[A-Za-z0-9\-]*\b/u
|
||||
model_like: /\b[A-Za-zÄÖÜäöüß][A-Za-zÄÖÜäöüß®\-]*(?:\s+[A-Za-zÄÖÜäöüß][A-Za-zÄÖÜäöüß®\-]*){0,2}\s+\d{2,5}[A-Za-z0-9\-]*(?:\s+[A-Za-zÄÖÜäöüß][A-Za-zÄÖÜäöüß0-9\-]{1,7})?\b/u
|
||||
specificity_boost_template: /\b(?:{terms})\b/iu
|
||||
contains_digit: /\d/u
|
||||
whitespace_collapse: /\s+/u
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
# RetrieX Patch p75 - Follow-up Price Action Guard
|
||||
|
||||
## Ziel
|
||||
|
||||
Nach p74 konnte bei Antworten mit Preisangaben weiterhin die Folgeaktion `Preis anzeigen` erscheinen. Beispiel:
|
||||
|
||||
```text
|
||||
Zeige mir die Preise zu testomat resthärte indikator.
|
||||
```
|
||||
|
||||
Die Antwort enthielt bereits konkrete Preise wie `98,20 €`, trotzdem wurde erneut `Preis anzeigen` angeboten. Zusätzlich sollte die Preis-Folgeaktion bei klaren No-Match-/Eignungswarnungen defensiver sein.
|
||||
|
||||
## Änderung
|
||||
|
||||
- Die Preiserkennung für Follow-up-Actions erkennt nun auch Preisformate wie `98,20 €` zuverlässig.
|
||||
- Die bisherige Regex hatte bei `€` ein Wortgrenzenproblem und erkannte außerdem `Preise` nicht sicher.
|
||||
- Shop-Folgeaktionen werden zusätzlich per YAML ausgeblendet, wenn die Antwort klar keinen direkten/expliziten Produkttreffer oder keine gesicherte technische Eignung formuliert. Dadurch entstehen bei No-Value-Antworten keine künstlichen Filter- oder Preis-Actions.
|
||||
|
||||
## Warum generisch?
|
||||
|
||||
Der Patch enthält keine Testomat-, Resthärte-, Indikator- oder TH-Code-Sonderlogik. Er verbessert nur die generische Action-Sichtbarkeit anhand von Preisformaten und konfigurierbaren No-Value-/No-Match-Formulierungen.
|
||||
|
||||
## Erwartete Wirkung
|
||||
|
||||
- Wenn eine Antwort bereits Preise wie `83,30 €`, `98,20 €` oder `Preis: 109,20 €` enthält, erscheint `Preis anzeigen` nicht erneut.
|
||||
- Bei klaren No-Match-/Eignungswarnungen wird keine Preisaktion künstlich angeboten.
|
||||
- Rollenfilter-Actions bleiben unverändert kontextsensitiv, werden aber bei klaren No-Value-/No-Match-Antworten ebenfalls ausgeblendet.
|
||||
|
||||
## Checks
|
||||
|
||||
Lokal geprüft:
|
||||
|
||||
```bash
|
||||
php -l src/Agent/AgentRunner.php
|
||||
python3 YAML parse config/retriex/chat-messages.yaml
|
||||
php smoke: Preisformate mit Eurozeichen werden erkannt
|
||||
python3 smoke: zusätzliche YAML-No-Value-Patterns matchen
|
||||
```
|
||||
|
||||
In der Zielumgebung zusätzlich ausführen:
|
||||
|
||||
```bash
|
||||
bin/console mto:agent:config:validate
|
||||
bin/console mto:agent:regression:test
|
||||
bin/console mto:agent:config:audit-source --details
|
||||
```
|
||||
@@ -0,0 +1,79 @@
|
||||
# RETRIEX PATCH 76 - Model Variant Shop Repair
|
||||
|
||||
## Ziel
|
||||
|
||||
Produktnahe Messgeräte-Anfragen mit technisch belegten Modellvarianten dürfen nicht auf eine generische Basisfamilie zurückfallen, wenn RAG-Wissen konkrete Varianten nennt und die Shop-Suche nur eine zu breite Query verwendet.
|
||||
|
||||
Beispielklasse:
|
||||
|
||||
- User: `ich möchte gern gesamtchlor messen. welche messgerät sollte ich nutzen`
|
||||
- RAG nennt konkrete Varianten wie `Testomat 2000 CLF` / `Testomat 2000 CLT`
|
||||
- Shopquery darf daraus nicht nur `Testomat 2000` machen und die Variante verlieren
|
||||
|
||||
## Änderungen
|
||||
|
||||
### 1. Modellvariantensuffixe in finalen Shopqueries bewahren
|
||||
|
||||
`AgentRunner::filterShopQueryToPositiveTokens()` bewahrt nun generisch alphabetische Modell-/Variantensuffixe, wenn sie direkt an ein bereits erlaubtes Modell-/Code-Token anschließen.
|
||||
|
||||
Beispiel:
|
||||
|
||||
- vorher: `Testomat 2000 CLF` -> `testomat 2000`
|
||||
- nachher: `Testomat 2000 CLF` -> `testomat 2000 clf`
|
||||
|
||||
Das ist generisch und nicht auf CLF/CLT oder Testomat hardcodiert.
|
||||
|
||||
### 2. Mehrere Modellvariantensuffixe in CommerceQueryParser erhalten
|
||||
|
||||
`CommerceQueryParser::compactShopSearchTokens()` erhält jetzt nicht nur ein einzelnes Suffix nach einer Modellnummer, sondern eine zusammenhängende Suffixkette.
|
||||
|
||||
Beispielklasse:
|
||||
|
||||
- `Family 2000 ABC DEF` behält beide Varianten-/Suffix-Tokens, sofern sie dem generischen Suffixmuster entsprechen.
|
||||
|
||||
### 3. RAG-gefundene konkrete Modellvarianten als Repair-Queries nutzen
|
||||
|
||||
`SearchRepairService` baut bei schwachen primären Shop-Treffern generische Repair-Queries aus konkreten Modellvarianten, die im RAG-Kontext gefunden wurden.
|
||||
|
||||
Dabei werden bevorzugt spezifische Varianten mit Suffix/Code genutzt, statt nur die Basisfamilie zu suchen.
|
||||
|
||||
### 4. Modellkandidaten-Pattern erweitert
|
||||
|
||||
`genre.yaml` erkennt Modellkandidaten mit getrenntem Variantensuffix, z. B. generisch:
|
||||
|
||||
- `Produkt 2000 ABC`
|
||||
- `Produkt 2000 ABC DEF`
|
||||
|
||||
### 5. Repair-Budget leicht erhöht
|
||||
|
||||
`retriex.commerce.search_repair.max_queries` wurde von `1` auf `2` erhöht, damit bei zwei plausiblen Varianten nicht nur die erste Variante abgefragt wird.
|
||||
|
||||
## Nicht geändert
|
||||
|
||||
- Keine Testomat-Sonderlogik
|
||||
- Keine CLF-/CLT-Sonderlogik im PHP-Core
|
||||
- Kein neues Ranking-/Retrieval-Scoring
|
||||
- Keine Änderung an Shopware Criteria-Struktur
|
||||
- Keine Änderung an Produktrollenlogik
|
||||
|
||||
## Lokale Checks
|
||||
|
||||
- `php -l src/Commerce/SearchRepairService.php`
|
||||
- `php -l src/Agent/AgentRunner.php`
|
||||
- `php -l src/Commerce/CommerceQueryParser.php`
|
||||
- `php -l src/Config/AgentRunnerConfig.php`
|
||||
- `php -l src/Config/SearchRepairConfig.php`
|
||||
- YAML Parse für `genre.yaml`, `commerce.yaml`, `agent.yaml`, `search_repair.yaml`
|
||||
- Regex-Smoke: Modellkandidaten-Pattern erkennt `Testomat 2000 CLF` und `Testomat 2000 CLT`
|
||||
|
||||
## Empfohlener Regressionstest
|
||||
|
||||
```text
|
||||
ich möchte gern gesamtchlor messen. welche messgerät sollte ich nutzen
|
||||
```
|
||||
|
||||
Erwartung:
|
||||
|
||||
- Shop-/Repair-Logik verliert Suffixe wie `CLF`/`CLT` nicht mehr.
|
||||
- Wenn Shopdaten konkrete Varianten liefern, sollen diese Varianten in der Antwort erscheinen.
|
||||
- Eine generische Basisfamilie wie `Testomat 2000` darf nicht die konkrete Variante ersetzen.
|
||||
@@ -0,0 +1,44 @@
|
||||
# RetrieX Patch p77 - Price Unavailable Action Guard
|
||||
|
||||
## Ziel
|
||||
|
||||
Nach p76 ist die fachliche Auswahl fuer Gesamtchlor deutlich besser: Bei `ich moechte gern gesamtchlor messen...` wird der konkrete Shop-/RAG-Anker `Testomat 2000 THCL` gehalten. Danach konnte die Follow-up-UI aber nach einer Preisantwort erneut Actions wie `Preis anzeigen`, `Nur Zubehoer anzeigen`, `Nur Geraete anzeigen` oder `Technische Details anzeigen` anbieten, obwohl die Antwort bereits sagte, dass der konkrete Preis nicht explizit angegeben bzw. nur auf Anfrage verfuegbar ist.
|
||||
|
||||
## Änderung
|
||||
|
||||
- Die Preis-Erkennung fuer Follow-up-Actions erkennt Preisformate wie `7.202,00 €` und `98,20 €` robuster.
|
||||
- `Preis anzeigen` wird ausgeblendet, wenn die Antwort bereits Preise enthaelt oder klar sagt, dass der Zielpreis nicht angegeben, nicht enthalten, nicht ausgewiesen oder nur auf Anfrage verfuegbar ist.
|
||||
- Rollenfilter-Actions werden bei No-Match-, No-Eignung- und Preis-nicht-verfuegbar-Antworten ausgeblendet.
|
||||
- Rollenfilter-Actions werden ebenfalls ausgeblendet, wenn die Antwort klar eine einzige direkte Loesung bzw. ein einziges direktes Messgeraet nennt; dann erzeugen Filter keinen Mehrwert.
|
||||
- `Technische Details anzeigen` wird ausgeblendet, wenn die Antwort bereits technische Eignung, Messparameter, Produktnummer, Verfuegbarkeit oder eine Preis-nicht-verfuegbar-Einordnung liefert.
|
||||
|
||||
## Warum generisch?
|
||||
|
||||
Der Patch enthaelt keine Testomat-, THCL-, Gesamtchlor- oder Modell-Sonderlogik. Er erweitert nur die konfigurierbaren Follow-up-Action-Guards und die generische Preisformat-Erkennung.
|
||||
|
||||
## Erwartete Wirkung
|
||||
|
||||
- Nach einer Preisantwort wie `Der Preis fuer dieses Geraet ist nicht explizit angegeben` erscheint keine erneute `Preis anzeigen`-Action.
|
||||
- Bei einer klaren Einzelgeraet-Antwort entstehen keine sinnlosen Rollenfilter wie `Nur Zubehoer anzeigen` oder `Nur Geraete anzeigen`.
|
||||
- Bei Antworten mit technischen Kerndetails erscheint `Technische Details anzeigen` nicht mehr als redundante Aktion.
|
||||
|
||||
## Checks
|
||||
|
||||
Lokal geprueft:
|
||||
|
||||
```bash
|
||||
php -l src/Agent/AgentRunner.php
|
||||
php -l src/Config/AgentRunnerConfig.php
|
||||
php -l src/Config/ChatMessagesConfig.php
|
||||
python3 YAML parse config/retriex/chat-messages.yaml
|
||||
php PCRE pattern validation for follow-up action hide patterns
|
||||
php smoke: price-unavailable pattern matches German no-price wording
|
||||
```
|
||||
|
||||
In der Zielumgebung zusaetzlich ausfuehren:
|
||||
|
||||
```bash
|
||||
bin/console mto:agent:config:validate
|
||||
bin/console mto:agent:regression:test
|
||||
bin/console mto:agent:config:audit-source --details
|
||||
```
|
||||
@@ -1719,13 +1719,19 @@ final readonly class AgentRunner
|
||||
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]) || $this->matchesAnyConfiguredShopQueryCodePattern($token, $codePatterns)) {
|
||||
if (
|
||||
isset($allowedTokens[$token])
|
||||
|| isset($modelVariantSuffixTokens[$token])
|
||||
|| $this->matchesAnyConfiguredShopQueryCodePattern($token, $codePatterns)
|
||||
) {
|
||||
$kept[$token] = $token;
|
||||
}
|
||||
}
|
||||
@@ -1807,6 +1813,53 @@ 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);
|
||||
@@ -3321,10 +3374,6 @@ final readonly class AgentRunner
|
||||
return $shopResults;
|
||||
}
|
||||
|
||||
if ($this->isMixedDeviceAndAccessoryProductRequest($prompt, $shopSearchQuery)) {
|
||||
return $shopResults;
|
||||
}
|
||||
|
||||
$primaryMatches = [];
|
||||
$corpusMatches = [];
|
||||
|
||||
@@ -3383,10 +3432,6 @@ final readonly class AgentRunner
|
||||
return $emptyResult;
|
||||
}
|
||||
|
||||
if ($this->isMixedDeviceAndAccessoryProductRequest($prompt, $shopSearchQuery)) {
|
||||
return $emptyResult;
|
||||
}
|
||||
|
||||
$repairQuery = $this->buildDirectProductPrimaryIdentityRepairQuery(
|
||||
shopSearchQuery: $shopSearchQuery,
|
||||
requestedTerms: $requestedTerms
|
||||
@@ -4070,7 +4115,7 @@ final readonly class AgentRunner
|
||||
}
|
||||
|
||||
$terms = [];
|
||||
foreach ($this->agentRunnerConfig->getDirectShopResultProductIdentityTerms() as $term) {
|
||||
foreach ($this->agentRunnerConfig->getShopQueryProductAttributeCleanupProductTypeTerms() as $term) {
|
||||
if ($this->containsAllShopQueryTokens($combined, $term)) {
|
||||
$terms[] = $term;
|
||||
}
|
||||
@@ -4079,17 +4124,6 @@ final readonly class AgentRunner
|
||||
return array_values(array_unique($terms));
|
||||
}
|
||||
|
||||
private function isMixedDeviceAndAccessoryProductRequest(string $prompt, string $shopSearchQuery): bool
|
||||
{
|
||||
$combined = mb_strtolower($this->normalizeOneLine($prompt . ' ' . $shopSearchQuery), 'UTF-8');
|
||||
if ($combined === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->containsAnyConfiguredTerm($combined, $this->agentRunnerConfig->getNoLlmMainDeviceRequestRoleKeywords())
|
||||
&& $this->containsAnyConfiguredTerm($combined, $this->agentRunnerConfig->getNoLlmAccessoryProductRoleKeywords());
|
||||
}
|
||||
|
||||
private function containsAllShopQueryTokens(string $text, string $term): bool
|
||||
{
|
||||
$tokens = array_fill_keys($this->tokenizeShopQueryCandidate($text), true);
|
||||
@@ -4235,7 +4269,6 @@ final readonly class AgentRunner
|
||||
|| !$shopSearchAttempted
|
||||
|| $shopSearchHadSystemFailure
|
||||
|| $this->extractRequestedDirectProductTerms($prompt, $shopSearchQuery) === []
|
||||
|| $this->isMixedDeviceAndAccessoryProductRequest($prompt, $shopSearchQuery)
|
||||
) {
|
||||
return '';
|
||||
}
|
||||
@@ -5457,7 +5490,7 @@ final readonly class AgentRunner
|
||||
|
||||
private function followUpActionAnswerAlreadyContainsPrice(string $answerText): bool
|
||||
{
|
||||
return preg_match('/(?:\bpreis\b.{0,24}\d+[,.]\d{2}|\d+[,.]\d{2}\s*(?:€|eur)\b|(?:€|eur)\s*\d+[,.]\d{2})/iu', $answerText) === 1;
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -308,8 +308,11 @@ final readonly class CommerceQueryParser
|
||||
$keep[$previousIndex] = true;
|
||||
}
|
||||
|
||||
$nextIndex = $index + 1;
|
||||
if (isset($tokens[$nextIndex]) && $this->isModelSuffixToken($tokens[$nextIndex])) {
|
||||
for ($nextIndex = $index + 1; isset($tokens[$nextIndex]); $nextIndex++) {
|
||||
if (!$this->isModelSuffixToken($tokens[$nextIndex])) {
|
||||
break;
|
||||
}
|
||||
|
||||
$keep[$nextIndex] = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,6 +230,21 @@ final readonly class SearchRepairService
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
$requestedAccessoryCodes === []
|
||||
&& $accessoryCandidates === []
|
||||
) {
|
||||
$modelVariantQueries = $this->buildSpecificModelVariantRepairQueries(
|
||||
prompt: $prompt,
|
||||
primaryQuery: $primaryQuery,
|
||||
modelCandidates: $modelCandidates
|
||||
);
|
||||
|
||||
if ($modelVariantQueries !== []) {
|
||||
return $this->normalizeRepairQueries($modelVariantQueries, $primaryQuery);
|
||||
}
|
||||
}
|
||||
|
||||
$topPrimaryName = $primaryShopResults[0]->name ?? '';
|
||||
$topPrimaryProductNumber = $primaryShopResults[0]->productNumber ?? null;
|
||||
$topPrimaryPhrase = trim($topPrimaryName . ' ' . ($topPrimaryProductNumber ?? ''));
|
||||
@@ -339,6 +354,121 @@ final readonly class SearchRepairService
|
||||
return $query !== '' ? [$query] : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build repair searches for specific model variants discovered in RAG evidence.
|
||||
* This keeps suffix variants such as family-number-code product names intact
|
||||
* instead of falling back to the generic base model.
|
||||
*
|
||||
* @param string[] $modelCandidates
|
||||
* @return string[]
|
||||
*/
|
||||
private function buildSpecificModelVariantRepairQueries(
|
||||
string $prompt,
|
||||
string $primaryQuery,
|
||||
array $modelCandidates
|
||||
): array {
|
||||
if ($modelCandidates === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$combinedQueryText = trim($prompt . ' ' . $primaryQuery);
|
||||
$decorated = [];
|
||||
|
||||
foreach ($modelCandidates as $index => $candidate) {
|
||||
$candidate = $this->sanitizeQuery($candidate);
|
||||
if ($candidate === '' || !$this->isSpecificModelVariantCandidate($candidate)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($this->queryAlreadyContainsCandidate($combinedQueryText, $candidate)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$decorated[] = [
|
||||
'candidate' => $candidate,
|
||||
'score' => $this->scoreSpecificModelVariantCandidate($candidate, $combinedQueryText),
|
||||
'index' => $index,
|
||||
];
|
||||
}
|
||||
|
||||
if ($decorated === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
usort($decorated, static function (array $a, array $b): int {
|
||||
if ($a['score'] === $b['score']) {
|
||||
return $a['index'] <=> $b['index'];
|
||||
}
|
||||
|
||||
return $b['score'] <=> $a['score'];
|
||||
});
|
||||
|
||||
return array_values(array_unique(array_map(
|
||||
static fn(array $row): string => $row['candidate'],
|
||||
$decorated
|
||||
)));
|
||||
}
|
||||
|
||||
private function isSpecificModelVariantCandidate(string $candidate): bool
|
||||
{
|
||||
return preg_match('/\b\d{2,5}[A-Za-z0-9\-]*\s+[A-Za-zÄÖÜäöüß]{2,8}\d{0,3}(?:\s+[A-Za-zÄÖÜäöüß]{2,8})?\b/u', $candidate) === 1
|
||||
|| preg_match('/\b\d{2,5}[A-Za-z]{1,8}\d{0,3}\b/u', $candidate) === 1;
|
||||
}
|
||||
|
||||
private function scoreSpecificModelVariantCandidate(string $candidate, string $queryText): int
|
||||
{
|
||||
$score = $this->scoreCandidate($candidate);
|
||||
$suffix = $this->extractModelVariantSuffix($candidate);
|
||||
|
||||
if ($suffix !== '') {
|
||||
$suffixLength = mb_strlen(preg_replace('/\s+/u', '', $suffix) ?? $suffix, 'UTF-8');
|
||||
$score += min(4, $suffixLength);
|
||||
|
||||
$normalizedQuery = $this->normalizeForRepairMatching($queryText);
|
||||
$normalizedSuffix = $this->normalizeForRepairMatching($suffix);
|
||||
if ($normalizedSuffix !== '' && preg_match('/\b' . preg_quote($normalizedSuffix, '/') . '\b/u', $normalizedQuery) === 1) {
|
||||
$score += 12;
|
||||
}
|
||||
|
||||
if (preg_match('/\d/u', $suffix) === 1 && preg_match('/\d/u', $queryText) !== 1) {
|
||||
$score -= 2;
|
||||
}
|
||||
}
|
||||
|
||||
return $score;
|
||||
}
|
||||
|
||||
private function extractModelVariantSuffix(string $candidate): string
|
||||
{
|
||||
if (preg_match('/\b\d{2,5}[A-Za-z0-9\-]*\s+([A-Za-zÄÖÜäöüß]{2,8}\d{0,3}(?:\s+[A-Za-zÄÖÜäöüß]{2,8})?)\b/u', $candidate, $matches) === 1) {
|
||||
return $this->sanitizeQuery((string) ($matches[1] ?? ''));
|
||||
}
|
||||
|
||||
if (preg_match('/\b\d{2,5}([A-Za-z]{1,8}\d{0,3})\b/u', $candidate, $matches) === 1) {
|
||||
return $this->sanitizeQuery((string) ($matches[1] ?? ''));
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
private function queryAlreadyContainsCandidate(string $queryText, string $candidate): bool
|
||||
{
|
||||
$queryTokens = array_fill_keys($this->tokenize($queryText), true);
|
||||
$candidateTokens = $this->tokenize($candidate);
|
||||
|
||||
if ($queryTokens === [] || $candidateTokens === []) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($candidateTokens as $token) {
|
||||
if (!isset($queryTokens[$token])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @param string[] $terms */
|
||||
private function buildTokenSet(array $terms): array
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user