p75+76
This commit is contained in:
@@ -245,18 +245,30 @@ parameters:
|
|||||||
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'
|
- '/\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
|
- 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:
|
hide_when_answer_matches_any:
|
||||||
- '/\bkeine?\s+(?:passende[nrs]?\s+)?(?:produktbezeichnung|shop-?treffer|treffer|produkte?)\b/iu'
|
- '/\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:
|
hide_when_answer_matches_any:
|
||||||
- '/\bkeine?\s+(?:passende[nrs]?\s+)?(?:produktbezeichnung|shop-?treffer|treffer|produkte?)\b/iu'
|
- '/\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}.
|
||||||
@@ -264,7 +276,8 @@ 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)?|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:
|
source_labels:
|
||||||
external_url: Externe URL
|
external_url: Externe URL
|
||||||
rag_knowledge: RAG Wissen
|
rag_knowledge: RAG Wissen
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ parameters:
|
|||||||
retriex.commerce.sales_channel_access_key: '%env(SHOPWARE_SALES_CHANNEL_ACCESS_KEY)%'
|
retriex.commerce.sales_channel_access_key: '%env(SHOPWARE_SALES_CHANNEL_ACCESS_KEY)%'
|
||||||
|
|
||||||
retriex.commerce.search_repair.enabled: true
|
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
|
retriex.commerce.search_repair.min_primary_results_without_repair: 2
|
||||||
|
|
||||||
# Commerce query parser configuration.
|
# 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 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.'
|
- '- 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.'
|
- '- 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:
|
prompt_keyword_views:
|
||||||
origin: genre_native
|
origin: genre_native
|
||||||
technical_product_keywords:
|
technical_product_keywords:
|
||||||
@@ -1593,11 +1595,11 @@ parameters:
|
|||||||
specific_model_candidate_patterns:
|
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
|
- /\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:
|
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
|
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
|
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
|
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
|
specificity_boost_template: /\b(?:{terms})\b/iu
|
||||||
contains_digit: /\d/u
|
contains_digit: /\d/u
|
||||||
whitespace_collapse: /\s+/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;
|
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 (isset($allowedTokens[$token]) || $this->matchesAnyConfiguredShopQueryCodePattern($token, $codePatterns)) {
|
if (
|
||||||
|
isset($allowedTokens[$token])
|
||||||
|
|| isset($modelVariantSuffixTokens[$token])
|
||||||
|
|| $this->matchesAnyConfiguredShopQueryCodePattern($token, $codePatterns)
|
||||||
|
) {
|
||||||
$kept[$token] = $token;
|
$kept[$token] = $token;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1807,6 +1813,53 @@ 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);
|
||||||
@@ -3321,10 +3374,6 @@ final readonly class AgentRunner
|
|||||||
return $shopResults;
|
return $shopResults;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->isMixedDeviceAndAccessoryProductRequest($prompt, $shopSearchQuery)) {
|
|
||||||
return $shopResults;
|
|
||||||
}
|
|
||||||
|
|
||||||
$primaryMatches = [];
|
$primaryMatches = [];
|
||||||
$corpusMatches = [];
|
$corpusMatches = [];
|
||||||
|
|
||||||
@@ -3383,10 +3432,6 @@ final readonly class AgentRunner
|
|||||||
return $emptyResult;
|
return $emptyResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->isMixedDeviceAndAccessoryProductRequest($prompt, $shopSearchQuery)) {
|
|
||||||
return $emptyResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
$repairQuery = $this->buildDirectProductPrimaryIdentityRepairQuery(
|
$repairQuery = $this->buildDirectProductPrimaryIdentityRepairQuery(
|
||||||
shopSearchQuery: $shopSearchQuery,
|
shopSearchQuery: $shopSearchQuery,
|
||||||
requestedTerms: $requestedTerms
|
requestedTerms: $requestedTerms
|
||||||
@@ -4070,7 +4115,7 @@ final readonly class AgentRunner
|
|||||||
}
|
}
|
||||||
|
|
||||||
$terms = [];
|
$terms = [];
|
||||||
foreach ($this->agentRunnerConfig->getDirectShopResultProductIdentityTerms() as $term) {
|
foreach ($this->agentRunnerConfig->getShopQueryProductAttributeCleanupProductTypeTerms() as $term) {
|
||||||
if ($this->containsAllShopQueryTokens($combined, $term)) {
|
if ($this->containsAllShopQueryTokens($combined, $term)) {
|
||||||
$terms[] = $term;
|
$terms[] = $term;
|
||||||
}
|
}
|
||||||
@@ -4079,17 +4124,6 @@ final readonly class AgentRunner
|
|||||||
return array_values(array_unique($terms));
|
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
|
private function containsAllShopQueryTokens(string $text, string $term): bool
|
||||||
{
|
{
|
||||||
$tokens = array_fill_keys($this->tokenizeShopQueryCandidate($text), true);
|
$tokens = array_fill_keys($this->tokenizeShopQueryCandidate($text), true);
|
||||||
@@ -4235,7 +4269,6 @@ final readonly class AgentRunner
|
|||||||
|| !$shopSearchAttempted
|
|| !$shopSearchAttempted
|
||||||
|| $shopSearchHadSystemFailure
|
|| $shopSearchHadSystemFailure
|
||||||
|| $this->extractRequestedDirectProductTerms($prompt, $shopSearchQuery) === []
|
|| $this->extractRequestedDirectProductTerms($prompt, $shopSearchQuery) === []
|
||||||
|| $this->isMixedDeviceAndAccessoryProductRequest($prompt, $shopSearchQuery)
|
|
||||||
) {
|
) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
@@ -5457,7 +5490,7 @@ final readonly class AgentRunner
|
|||||||
|
|
||||||
private function followUpActionAnswerAlreadyContainsPrice(string $answerText): bool
|
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;
|
$keep[$previousIndex] = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
$nextIndex = $index + 1;
|
for ($nextIndex = $index + 1; isset($tokens[$nextIndex]); $nextIndex++) {
|
||||||
if (isset($tokens[$nextIndex]) && $this->isModelSuffixToken($tokens[$nextIndex])) {
|
if (!$this->isModelSuffixToken($tokens[$nextIndex])) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
$keep[$nextIndex] = true;
|
$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 ?? '';
|
$topPrimaryName = $primaryShopResults[0]->name ?? '';
|
||||||
$topPrimaryProductNumber = $primaryShopResults[0]->productNumber ?? null;
|
$topPrimaryProductNumber = $primaryShopResults[0]->productNumber ?? null;
|
||||||
$topPrimaryPhrase = trim($topPrimaryName . ' ' . ($topPrimaryProductNumber ?? ''));
|
$topPrimaryPhrase = trim($topPrimaryName . ' ' . ($topPrimaryProductNumber ?? ''));
|
||||||
@@ -339,6 +354,121 @@ final readonly class SearchRepairService
|
|||||||
return $query !== '' ? [$query] : [];
|
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 */
|
/** @param string[] $terms */
|
||||||
private function buildTokenSet(array $terms): array
|
private function buildTokenSet(array $terms): array
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user