p86h-k
This commit is contained in:
@@ -49,7 +49,7 @@ parameters:
|
|||||||
assistant:
|
assistant:
|
||||||
loader: 'Antwort wird vorbereitet…'
|
loader: 'Antwort wird vorbereitet…'
|
||||||
aborted: '[aborted]'
|
aborted: '[aborted]'
|
||||||
history_cleared: 'History cleared.'
|
history_cleared: 'Chat-History wurde gelöscht.'
|
||||||
source_chips:
|
source_chips:
|
||||||
live_shop_data: 'Live-Shopdaten'
|
live_shop_data: 'Live-Shopdaten'
|
||||||
run_meta:
|
run_meta:
|
||||||
@@ -209,6 +209,7 @@ parameters:
|
|||||||
shop_meta_repair_checked: 'Erweiterte Suche: geprüft'
|
shop_meta_repair_checked: 'Erweiterte Suche: geprüft'
|
||||||
shop_meta_eyebrow: Shop-Suche
|
shop_meta_eyebrow: Shop-Suche
|
||||||
shop_meta_query_label: Gesendete Suchquery
|
shop_meta_query_label: Gesendete Suchquery
|
||||||
|
shop_meta_queries_label: Gesendete Einzelqueries
|
||||||
shop_meta_query_prefix: 'Query: '
|
shop_meta_query_prefix: 'Query: '
|
||||||
shop_meta_intent_prefix: 'Intent: '
|
shop_meta_intent_prefix: 'Intent: '
|
||||||
shop_unavailable_default_reason: Keine Detailmeldung vom Shopware-Server.
|
shop_unavailable_default_reason: Keine Detailmeldung vom Shopware-Server.
|
||||||
|
|||||||
@@ -1193,6 +1193,10 @@ parameters:
|
|||||||
anchor_patterns:
|
anchor_patterns:
|
||||||
- '/(?:^|\R)[^\S\r\n]*(?:\d+[.)][^\S\r\n]*)?(?P<anchor>Testomat(?:®)?[^\S\r\n]+\d{3,4}(?:[^\S\r\n]+[A-Za-z0-9-]{1,12}){0,2})\b/iu'
|
- '/(?:^|\R)[^\S\r\n]*(?:\d+[.)][^\S\r\n]*)?(?P<anchor>Testomat(?:®)?[^\S\r\n]+\d{3,4}(?:[^\S\r\n]+[A-Za-z0-9-]{1,12}){0,2})\b/iu'
|
||||||
- '/\b(?P<anchor>Testomat(?:®)?[^\S\r\n]+(?:\d{3,4}(?:[^\S\r\n]+(?=[A-Z0-9-]*[A-Z])[A-Z0-9-]{2,12}){0,2}|EVO(?:[^\S\r\n]+[A-Z]{2,8})?|ECO(?:[- ]?(?:PLUS|C))?|DUO(?:[^\S\r\n]+\d{3,4})?|LAB(?:[^\S\r\n]+[A-Z-]{1,8}){1,2}))\b/iu'
|
- '/\b(?P<anchor>Testomat(?:®)?[^\S\r\n]+(?:\d{3,4}(?:[^\S\r\n]+(?=[A-Z0-9-]*[A-Z])[A-Z0-9-]{2,12}){0,2}|EVO(?:[^\S\r\n]+[A-Z]{2,8})?|ECO(?:[- ]?(?:PLUS|C))?|DUO(?:[^\S\r\n]+\d{3,4})?|LAB(?:[^\S\r\n]+[A-Z-]{1,8}){1,2}))\b/iu'
|
||||||
|
canonical_start_patterns:
|
||||||
|
- '/\b(?P<anchor>Testomat(?:®)?\b.*)$/iu'
|
||||||
|
canonical_family_terms:
|
||||||
|
- testomat
|
||||||
meta_query_guard:
|
meta_query_guard:
|
||||||
origin: genre_native
|
origin: genre_native
|
||||||
meta_only_terms:
|
meta_only_terms:
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
# RetrieX Patch p86h - Multi Product Follow-up Split Lookup Completion
|
||||||
|
|
||||||
|
## Ziel
|
||||||
|
|
||||||
|
p86e/p86g konnten referenzielle Produktlisten-Follow-ups bereits weg von schwachen Meta-Queries wie `links zu aus` auf Produktanker aus dem Verlauf aufloesen. In einem Multi-Produkt-Fall blieb aber eine kombinierte Shopquery sichtbar und teilweise wirksam, z. B.:
|
||||||
|
|
||||||
|
```text
|
||||||
|
testomat 2000 cal 808
|
||||||
|
```
|
||||||
|
|
||||||
|
Diese kombinierte Query kann nur einen Teil der gemeinten Produkte treffen oder ueber Beschreibungen/Zubehoer unpassende Treffer liefern.
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
p86g pruefte zwar die Produktidentitaet, stoppte aber sobald mindestens ein passender Treffer vorhanden war. Dadurch wurden fehlende Verlaufanker nicht separat nachgesucht.
|
||||||
|
|
||||||
|
Beispiel:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Vorherige Antwort nennt:
|
||||||
|
- Testomat 2000 CAL
|
||||||
|
- Testomat 808
|
||||||
|
|
||||||
|
Follow-up:
|
||||||
|
gebe mir links zu den produkten aus dem shop
|
||||||
|
|
||||||
|
Primaere Query:
|
||||||
|
testomat 2000 cal 808
|
||||||
|
```
|
||||||
|
|
||||||
|
Wenn die primaere Query nur `Testomat 2000 CAL` fand, wurde nicht mehr separat nach `testomat 808` gesucht.
|
||||||
|
|
||||||
|
## Loesung
|
||||||
|
|
||||||
|
p86h erweitert die p86g-Logik:
|
||||||
|
|
||||||
|
1. Bestehende Shop-Treffer werden weiterhin gegen die Produktidentitaeten der Verlaufanker gefiltert.
|
||||||
|
2. Danach wird ermittelt, welche Verlaufanker bereits abgedeckt sind.
|
||||||
|
3. Wenn bei mehreren Ankern mindestens einer fehlt, werden die Produktanker separat nachgesucht.
|
||||||
|
4. Bereits gefundene Identitaetstreffer und separate Anchor-Lookup-Treffer werden dedupliziert gemerged.
|
||||||
|
5. Die abgeschlossene Shop-Meta-Anzeige zeigt bei referenziellen Produktlisten-Follow-ups die verwendeten Anchor-Queries, z. B.:
|
||||||
|
|
||||||
|
```text
|
||||||
|
testomat 2000 cal | testomat 808
|
||||||
|
```
|
||||||
|
|
||||||
|
## Guardrails
|
||||||
|
|
||||||
|
Der Patch ist bewusst eng begrenzt:
|
||||||
|
|
||||||
|
- nur referenzielle Produktlisten-/Shoplink-Follow-ups
|
||||||
|
- keine neue Intent-Erkennung
|
||||||
|
- keine Aenderung an Retrieval, Ranking, Scoring oder Shop-Matching
|
||||||
|
- keine harte Produktliste im Core
|
||||||
|
- keine Sonderlogik fuer medizinische Geraete
|
||||||
|
- p84 LAB-CL-Kuerzel, p85b SIO2-Anker und Accessory-/Indicator-Flows bleiben unberuehrt
|
||||||
|
|
||||||
|
## Geaenderte Dateien
|
||||||
|
|
||||||
|
- `src/Agent/AgentRunner.php`
|
||||||
|
- `patch_history/RETRIEX_PATCH_86H_MULTI_PRODUCT_FOLLOWUP_SPLIT_LOOKUP_README.md`
|
||||||
|
|
||||||
|
## Lokale Checks
|
||||||
|
|
||||||
|
```text
|
||||||
|
php -l src/Agent/AgentRunner.php
|
||||||
|
find src -name '*.php' -print0 | xargs -0 -n1 php -l
|
||||||
|
YAML parse OK
|
||||||
|
vendor missing - console checks skipped
|
||||||
|
```
|
||||||
|
|
||||||
|
Console-Checks muessen in einer Umgebung mit `vendor/` ausgefuehrt werden:
|
||||||
|
|
||||||
|
```text
|
||||||
|
php bin/console mto:agent:config:validate
|
||||||
|
php bin/console mto:agent:regression:test
|
||||||
|
php bin/console mto:agent:config:audit-source --details
|
||||||
|
php bin/console mto:agent:config:audit-patterns --details
|
||||||
|
```
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
# RetrieX Patch p86j - Multi Product Split Query Meta Clarity
|
||||||
|
|
||||||
|
## Ziel
|
||||||
|
|
||||||
|
Referenzielle Produktlisten-Follow-ups wie:
|
||||||
|
|
||||||
|
```text
|
||||||
|
gebe mir links zu den produkten aus dem shop
|
||||||
|
```
|
||||||
|
|
||||||
|
koennen seit p86i mehrere Produktanker als getrennte Shopware-Suchen ausfuehren. Die UI zeigte diese Einzelqueries jedoch weiterhin als zusammengefuehrte Semikolon-Zeile unter `Gesendete Suchquery`, z. B.:
|
||||||
|
|
||||||
|
```text
|
||||||
|
testomat 2000 self clean; testomat 2000 cal; testomat 808
|
||||||
|
```
|
||||||
|
|
||||||
|
Das war missverstaendlich, weil die Shopware-API keine Pipe-/Semikolon-Syntax als einzelne Query versteht.
|
||||||
|
|
||||||
|
## Aenderung
|
||||||
|
|
||||||
|
- Split-Produktlisten-Follow-ups werden in der Shop-Meta-Card nun als `Gesendete Einzelqueries` dargestellt.
|
||||||
|
- Jede Einzelquery wird separat gerendert, statt zu einer Semikolon- oder Pipe-Query zusammengefuehrt zu werden.
|
||||||
|
- Die technische Ausfuehrung aus p86i bleibt erhalten: `searchProductListFollowUpSplitLookupQueries()` fuehrt weiterhin einzelne, begrenzte und sequenzielle `searchShop()`-Aufrufe pro Produktanker aus.
|
||||||
|
- Die bestehende semikolonbasierte Display-Formatierung wurde entfernt, damit sie nicht versehentlich wieder als Shopware-Query interpretiert wird.
|
||||||
|
- Die Commerce-Search-Logs enthalten nun zusaetzlich `individualShopQueries`, wenn Split-Lookups vorbereitet wurden.
|
||||||
|
- Der einzelne Referenzanker-Guard wird bei bereits identitaetsgefilterten Split-Lookup-Ergebnissen uebersprungen, damit ein Treffer zu `Testomat 2000 self clean` nicht spaeter `Testomat 2000 CAL` oder `Testomat 808` aus der zusammengefuehrten Anzeige-Query herausfiltert.
|
||||||
|
|
||||||
|
## Bewusst nicht geaendert
|
||||||
|
|
||||||
|
- Keine Async-/Parallelisierung. Der Store-API-Pfad ist synchron; Async waere ein separater, groesserer Service-Umbau.
|
||||||
|
- Keine Aenderung an Intent-Erkennung, Retrieval, Scoring, Ranking oder Shop-Matching.
|
||||||
|
- Keine neue Produkt-/Marken-Sonderlogik.
|
||||||
|
- Keine Veraenderung normaler Einzel-Shopqueries.
|
||||||
|
|
||||||
|
## Erwartung
|
||||||
|
|
||||||
|
Vorher:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Gesendete Suchquery
|
||||||
|
testomat 2000 self clean; testomat 2000 cal; testomat 808
|
||||||
|
```
|
||||||
|
|
||||||
|
Nachher:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Gesendete Einzelqueries
|
||||||
|
testomat 2000 self clean
|
||||||
|
testomat 2000 cal
|
||||||
|
testomat 808
|
||||||
|
```
|
||||||
|
|
||||||
|
## Geaenderte Dateien
|
||||||
|
|
||||||
|
- `src/Agent/AgentRunner.php`
|
||||||
|
- `config/retriex/chat-messages.yaml`
|
||||||
|
- `src/Config/ChatMessagesConfig.php`
|
||||||
|
- `public/assets/styles/base.css`
|
||||||
|
|
||||||
|
## Lokale Checks
|
||||||
|
|
||||||
|
```text
|
||||||
|
find src -name '*.php' -print0 | xargs -0 -n1 php -l
|
||||||
|
python yaml parse for config/retriex/*.yaml
|
||||||
|
static check: no semicolon display formatter remains
|
||||||
|
```
|
||||||
|
|
||||||
|
Alle lokalen Checks waren gruen. Symfony-Console-Checks muessen in der Zielumgebung mit `vendor/` ausgefuehrt werden.
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
# RetrieX Patch p86k - YAML-backed Product Anchor Canonicalization
|
||||||
|
|
||||||
|
## Zweck
|
||||||
|
|
||||||
|
p86j hat die Multi-Product-Link-Follow-up-Logik verbessert, aber der Core-Pattern-Audit meldete danach einen Warnfund in `src/Agent/AgentRunner.php`:
|
||||||
|
|
||||||
|
```text
|
||||||
|
preg_match('/\btestomat(?:®)?\b.*$/iu', $anchor, $match)
|
||||||
|
```
|
||||||
|
|
||||||
|
p86k entfernt dieses domain-sensitive Pattern aus dem PHP-Core und verlagert die fachliche Start-/Familien-Erkennung in `config/retriex/genre.yaml`.
|
||||||
|
|
||||||
|
## Änderung
|
||||||
|
|
||||||
|
- `AgentRunner::canonicalizeProductListAnchor()` nutzt keine hart codierte Produktfamilie mehr.
|
||||||
|
- Neuer YAML-backed Trim-Schritt `trimProductListAnchorToConfiguredStart()`:
|
||||||
|
- liest `context_resolution.product_list_followup.canonical_start_patterns`
|
||||||
|
- akzeptiert Named Group `anchor`, Fallback auf Gruppe 1 oder Full Match
|
||||||
|
- Produktfamilien-Erkennung liest nun aus:
|
||||||
|
- `context_resolution.product_list_followup.canonical_family_terms`
|
||||||
|
- `AgentRunnerConfig` stellt Accessor-Methoden bereit.
|
||||||
|
- `RetriexEffectiveConfigProvider` validiert die neuen YAML-Werte.
|
||||||
|
|
||||||
|
## Keine fachliche Änderung
|
||||||
|
|
||||||
|
Der Patch ändert nicht:
|
||||||
|
|
||||||
|
- Intent-Erkennung
|
||||||
|
- Retrieval
|
||||||
|
- Scoring
|
||||||
|
- Ranking
|
||||||
|
- Shop-Matching
|
||||||
|
- Split-Request-Logik aus p86i/p86j
|
||||||
|
- LAB-CL-Kürzelschutz aus p84
|
||||||
|
- SiO2-Geräteanker aus p85b
|
||||||
|
|
||||||
|
## Erwartung
|
||||||
|
|
||||||
|
Der bisherige Effekt bleibt erhalten:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Systembeschreibung Der Testomat® 2000 CAL
|
||||||
|
=> testomat 2000 cal
|
||||||
|
```
|
||||||
|
|
||||||
|
Die dafür nötige Produktfamilienlogik liegt nun in YAML statt im PHP-Core.
|
||||||
|
|
||||||
|
## Lokale Checks
|
||||||
|
|
||||||
|
Ausgeführt im Patch-Arbeitsverzeichnis:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
find src -name '*.php' -print0 | xargs -0 -n1 php -l
|
||||||
|
python3 - <<'PY'
|
||||||
|
from pathlib import Path
|
||||||
|
import yaml
|
||||||
|
for path in Path('config/retriex').glob('*.yaml'):
|
||||||
|
yaml.safe_load(path.read_text(encoding='utf-8'))
|
||||||
|
print('YAML parse OK')
|
||||||
|
PY
|
||||||
|
grep -RIn "preg_match.*testomat\|testomat.*preg_match" src/Agent/AgentRunner.php
|
||||||
|
```
|
||||||
|
|
||||||
|
Ergebnis:
|
||||||
|
|
||||||
|
- PHP lint OK
|
||||||
|
- YAML parse OK
|
||||||
|
- kein `preg_match` mit `testomat` in `AgentRunner.php`
|
||||||
|
|
||||||
|
## Noch in Zielumgebung ausführen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php bin/console mto:agent:config:validate
|
||||||
|
php bin/console mto:agent:regression:test
|
||||||
|
php bin/console mto:agent:config:audit-source --details
|
||||||
|
php bin/console mto:agent:config:audit-patterns --details
|
||||||
|
```
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
# RetrieX Patch p86i - Multi Product Split Shopware Requests
|
||||||
|
|
||||||
|
## Ziel
|
||||||
|
|
||||||
|
Referenzielle Produktlisten-Follow-ups wie
|
||||||
|
|
||||||
|
```text
|
||||||
|
gebe mir links zu den produkten aus dem shop
|
||||||
|
```
|
||||||
|
|
||||||
|
sollen bei mehreren Produktankern keine kombinierte Pseudo-Query mehr an Shopware senden.
|
||||||
|
|
||||||
|
Vorher konnte aus dem Verlauf eine zusammengeführte Query entstehen, z. B.:
|
||||||
|
|
||||||
|
```text
|
||||||
|
testomat 2000 cal 808
|
||||||
|
```
|
||||||
|
|
||||||
|
oder als Anzeige/Repair-Query eine Pipe-Liste:
|
||||||
|
|
||||||
|
```text
|
||||||
|
testomat 2000 self clean | testomat 2000 cal | testomat 808
|
||||||
|
```
|
||||||
|
|
||||||
|
Diese Pipe-/Kombi-Query ist für die Shopware-API keine gültige Einzelproduktsuche.
|
||||||
|
|
||||||
|
## Änderung
|
||||||
|
|
||||||
|
p86i führt einen engen Split-Lookup-Pfad nur für referenzielle Produktlisten-Follow-ups ein:
|
||||||
|
|
||||||
|
1. Produktanker werden aus Verlauf/RAG wie bisher generisch extrahiert.
|
||||||
|
2. Wenn mindestens zwei Produktanker vorhanden sind, wird keine kombinierte Produktlisten-Query an Shopware gesendet.
|
||||||
|
3. Stattdessen werden echte separate Shopware-Suchen ausgeführt, z. B.:
|
||||||
|
|
||||||
|
```text
|
||||||
|
testomat 2000 self clean
|
||||||
|
testomat 2000 cal
|
||||||
|
testomat 808
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Ergebnisse werden per Produktidentität gefiltert und dedupliziert zusammengeführt.
|
||||||
|
5. Die Query-Anzeige nutzt Semikolon-Trennung als reine Anzeige der Einzelabfragen, keine Pipe-Syntax.
|
||||||
|
|
||||||
|
## Scope / Guardrails
|
||||||
|
|
||||||
|
Der Patch ist bewusst eng begrenzt:
|
||||||
|
|
||||||
|
- nur für `isReferentialProductListShopFollowUpPrompt()`
|
||||||
|
- nur wenn mindestens zwei Produktanker gefunden wurden
|
||||||
|
- keine Änderung für normale Einzel-Shopqueries
|
||||||
|
- keine Änderung an Intent-Erkennung, RAG-Retrieval, Ranking, Scoring oder Shop-Matching
|
||||||
|
- keine feste Produktliste im Core
|
||||||
|
- keine Sonderlogik für medizinische Geräte
|
||||||
|
- kein pauschales `gerät => testomat`
|
||||||
|
- bestehende Identity-Filterung aus p86g/p86h bleibt aktiv
|
||||||
|
|
||||||
|
## Async-Bewertung
|
||||||
|
|
||||||
|
Die getrennten Lookups werden bewusst sequenziell und durch `max_anchors` begrenzt ausgeführt.
|
||||||
|
Der vorhandene `ShopSearchService::search()` / `StoreApiClient`-Pfad ist synchron aufgebaut. Eine echte parallele Store-API-Ausführung würde einen breiteren Service-/HTTP-Client-Umbau erfordern und wurde bewusst nicht in diesen kleinen Hotfix aufgenommen, um keine unrelated Shop-Flows zu riskieren.
|
||||||
|
|
||||||
|
## Lokale Checks
|
||||||
|
|
||||||
|
```text
|
||||||
|
php -l src/Agent/AgentRunner.php
|
||||||
|
find src -name '*.php' -print0 | xargs -0 -n1 php -l
|
||||||
|
YAML parse OK
|
||||||
|
```
|
||||||
|
|
||||||
|
Symfony-Console-Checks müssen in der Zielumgebung mit `vendor/` ausgeführt werden:
|
||||||
|
|
||||||
|
```text
|
||||||
|
php bin/console mto:agent:config:validate
|
||||||
|
php bin/console mto:agent:regression:test
|
||||||
|
php bin/console mto:agent:config:audit-source --details
|
||||||
|
php bin/console mto:agent:config:audit-patterns --details
|
||||||
|
```
|
||||||
@@ -620,6 +620,15 @@ body:not(.retriex-show-detail-cards) #chat .retriex-alert {
|
|||||||
color: #f8f9fa;
|
color: #f8f9fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.retriex-meta-query__list {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.retriex-meta-query--multi code {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.retriex-alert {
|
.retriex-alert {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
|
|||||||
@@ -30,7 +30,9 @@
|
|||||||
<img src="/assets/img/logo.svg" style="max-height: 20px;">
|
<img src="/assets/img/logo.svg" style="max-height: 20px;">
|
||||||
<div class="spacer"></div>
|
<div class="spacer"></div>
|
||||||
|
|
||||||
<button id="clear" class="btn btn-trans" data-chat-message-text="ui.buttons.clear"></button>
|
<button id="clear" class="btn btn-trans" data-chat-message-text="ui.buttons.clear"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash-fill" viewBox="0 0 16 16">
|
||||||
|
<path d="M2.5 1a1 1 0 0 0-1 1v1a1 1 0 0 0 1 1H3v9a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V4h.5a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1H10a1 1 0 0 0-1-1H7a1 1 0 0 0-1 1zm3 4a.5.5 0 0 1 .5.5v7a.5.5 0 0 1-1 0v-7a.5.5 0 0 1 .5-.5M8 5a.5.5 0 0 1 .5.5v7a.5.5 0 0 1-1 0v-7A.5.5 0 0 1 8 5m3 .5v7a.5.5 0 0 1-1 0v-7a.5.5 0 0 1 1 0"/>
|
||||||
|
</svg></button>
|
||||||
</div>
|
</div>
|
||||||
<div id="ai-cloud" class="ai-cloud d-none"></div>
|
<div id="ai-cloud" class="ai-cloud d-none"></div>
|
||||||
<div id="chat" class="chat"></div>
|
<div id="chat" class="chat"></div>
|
||||||
|
|||||||
@@ -464,13 +464,34 @@ final readonly class AgentRunner
|
|||||||
? $shopQueryPreview->searchText
|
? $shopQueryPreview->searchText
|
||||||
: $shopSearchQuery;
|
: $shopSearchQuery;
|
||||||
$shopSearchUsedOptimizedQuery = $optimizedShopQuery !== '';
|
$shopSearchUsedOptimizedQuery = $optimizedShopQuery !== '';
|
||||||
|
$shopSearchDisplayIndividualQueries = [];
|
||||||
|
$productListSplitLookupQueries = $this->resolveProductListFollowUpSplitLookupQueries(
|
||||||
|
prompt: $originalPrompt,
|
||||||
|
userId: $userId,
|
||||||
|
commerceHistoryContext: $shopQueryHistoryContext,
|
||||||
|
knowledgeChunks: $knowledgeChunks
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($productListSplitLookupQueries !== []) {
|
||||||
|
$shopSearchDisplayIndividualQueries = $productListSplitLookupQueries;
|
||||||
|
$shopSearchUsedOptimizedQuery = false;
|
||||||
|
|
||||||
|
$this->agentLogger->info('Prepared product-list follow-up shop search as separate product anchor lookups', [
|
||||||
|
'userId' => $userId,
|
||||||
|
'commerceIntent' => $commerceIntent,
|
||||||
|
'prompt' => $prompt,
|
||||||
|
'shopSearchQuery' => $shopSearchQuery,
|
||||||
|
'splitLookupQueries' => $productListSplitLookupQueries,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
yield $this->systemMsg(
|
yield $this->systemMsg(
|
||||||
$this->buildShopSearchMetaMessage(
|
$this->buildShopSearchMetaMessage(
|
||||||
query: $shopSearchDisplayQuery,
|
query: $shopSearchDisplayQuery,
|
||||||
commerceIntent: $commerceIntent,
|
commerceIntent: $commerceIntent,
|
||||||
usedOptimizedQuery: $shopSearchUsedOptimizedQuery,
|
usedOptimizedQuery: $shopSearchUsedOptimizedQuery,
|
||||||
originalQuery: $shopSearchQuery
|
originalQuery: $shopSearchQuery,
|
||||||
|
individualQueries: $shopSearchDisplayIndividualQueries
|
||||||
),
|
),
|
||||||
'meta'
|
'meta'
|
||||||
);
|
);
|
||||||
@@ -481,6 +502,7 @@ final readonly class AgentRunner
|
|||||||
'usedOptimizedShopQuery' => $optimizedShopQuery !== '',
|
'usedOptimizedShopQuery' => $optimizedShopQuery !== '',
|
||||||
'optimizedShopQuery' => $optimizedShopQuery,
|
'optimizedShopQuery' => $optimizedShopQuery,
|
||||||
'shopSearchQuery' => $shopSearchQuery,
|
'shopSearchQuery' => $shopSearchQuery,
|
||||||
|
'individualShopQueries' => $shopSearchDisplayIndividualQueries,
|
||||||
'hasCommerceHistoryContext' => $commerceHistoryContext !== '',
|
'hasCommerceHistoryContext' => $commerceHistoryContext !== '',
|
||||||
'commerceHistoryContextLength' => mb_strlen($commerceHistoryContext),
|
'commerceHistoryContextLength' => mb_strlen($commerceHistoryContext),
|
||||||
]);
|
]);
|
||||||
@@ -503,14 +525,28 @@ final readonly class AgentRunner
|
|||||||
);
|
);
|
||||||
|
|
||||||
$shopSearchAttempted = true;
|
$shopSearchAttempted = true;
|
||||||
$primaryShopResults = $this->searchShop(
|
$productListSplitLookupPayload = null;
|
||||||
$shopSearchQuery,
|
if ($productListSplitLookupQueries !== []) {
|
||||||
$commerceIntent,
|
$productListSplitLookupPayload = $this->searchProductListFollowUpSplitLookupQueries(
|
||||||
$userId,
|
prompt: $originalPrompt,
|
||||||
$shopQueryHistoryContext
|
userId: $userId,
|
||||||
);
|
commerceIntent: $commerceIntent,
|
||||||
$primaryShopSearchHadSystemFailure = $this->shopSearchService->hadLastSearchSystemFailure();
|
commerceHistoryContext: $shopQueryHistoryContext,
|
||||||
$primaryShopSearchFailureReason = $this->shopSearchService->getLastSearchFailureReason();
|
queries: $productListSplitLookupQueries
|
||||||
|
);
|
||||||
|
$primaryShopResults = $productListSplitLookupPayload['results'];
|
||||||
|
$primaryShopSearchHadSystemFailure = $productListSplitLookupPayload['hadSystemFailure'];
|
||||||
|
$primaryShopSearchFailureReason = $productListSplitLookupPayload['failureReason'];
|
||||||
|
} else {
|
||||||
|
$primaryShopResults = $this->searchShop(
|
||||||
|
$shopSearchQuery,
|
||||||
|
$commerceIntent,
|
||||||
|
$userId,
|
||||||
|
$shopQueryHistoryContext
|
||||||
|
);
|
||||||
|
$primaryShopSearchHadSystemFailure = $this->shopSearchService->hadLastSearchSystemFailure();
|
||||||
|
$primaryShopSearchFailureReason = $this->shopSearchService->getLastSearchFailureReason();
|
||||||
|
}
|
||||||
|
|
||||||
if ($primaryShopSearchHadSystemFailure) {
|
if ($primaryShopSearchHadSystemFailure) {
|
||||||
$this->agentLogger->warning('Shop repair skipped after Store API system failure', [
|
$this->agentLogger->warning('Shop repair skipped after Store API system failure', [
|
||||||
@@ -532,6 +568,7 @@ final readonly class AgentRunner
|
|||||||
usedOptimizedQuery: $shopSearchUsedOptimizedQuery,
|
usedOptimizedQuery: $shopSearchUsedOptimizedQuery,
|
||||||
originalQuery: $shopSearchQuery,
|
originalQuery: $shopSearchQuery,
|
||||||
completed: true,
|
completed: true,
|
||||||
|
individualQueries: $shopSearchDisplayIndividualQueries,
|
||||||
unavailable: true
|
unavailable: true
|
||||||
),
|
),
|
||||||
'meta'
|
'meta'
|
||||||
@@ -548,27 +585,36 @@ final readonly class AgentRunner
|
|||||||
'repairQueries' => [],
|
'repairQueries' => [],
|
||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
yield $this->systemMsg($this->agentRunnerConfig->getShopRepairCheckMessage(), 'think');
|
if ($productListSplitLookupPayload !== null) {
|
||||||
|
$repairPayload = [
|
||||||
|
'results' => $primaryShopResults,
|
||||||
|
'attemptedRepair' => true,
|
||||||
|
'usedRepair' => $primaryShopResults !== [],
|
||||||
|
'repairQueries' => $productListSplitLookupPayload['queries'],
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
yield $this->systemMsg($this->agentRunnerConfig->getShopRepairCheckMessage(), 'think');
|
||||||
|
|
||||||
$repairPayload = $this->repairShopResults(
|
$repairPayload = $this->repairShopResults(
|
||||||
prompt: $prompt,
|
prompt: $prompt,
|
||||||
userId: $userId,
|
userId: $userId,
|
||||||
commerceIntent: $commerceIntent,
|
commerceIntent: $commerceIntent,
|
||||||
commerceHistoryContext: $shopQueryHistoryContext,
|
commerceHistoryContext: $shopQueryHistoryContext,
|
||||||
primaryQuery: $shopSearchQuery,
|
primaryQuery: $shopSearchQuery,
|
||||||
primaryShopResults: $primaryShopResults,
|
primaryShopResults: $primaryShopResults,
|
||||||
knowledgeChunks: $knowledgeChunks
|
knowledgeChunks: $knowledgeChunks
|
||||||
);
|
);
|
||||||
|
|
||||||
$repairPayload = $this->repairProductListFollowUpShopResultsWithAnchorLookups(
|
$repairPayload = $this->repairProductListFollowUpShopResultsWithAnchorLookups(
|
||||||
prompt: $prompt,
|
prompt: $prompt,
|
||||||
userId: $userId,
|
userId: $userId,
|
||||||
commerceIntent: $commerceIntent,
|
commerceIntent: $commerceIntent,
|
||||||
commerceHistoryContext: $shopQueryHistoryContext,
|
commerceHistoryContext: $shopQueryHistoryContext,
|
||||||
shopSearchQuery: $shopSearchQuery,
|
shopSearchQuery: $shopSearchQuery,
|
||||||
repairPayload: $repairPayload,
|
repairPayload: $repairPayload,
|
||||||
knowledgeChunks: $knowledgeChunks
|
knowledgeChunks: $knowledgeChunks
|
||||||
);
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -588,7 +634,9 @@ final readonly class AgentRunner
|
|||||||
$shopResults = $directIdentityRepairPayload['results'];
|
$shopResults = $directIdentityRepairPayload['results'];
|
||||||
}
|
}
|
||||||
|
|
||||||
$shopResults = $this->guardShopResultsByReferencedProductAnchor($shopSearchQuery, $shopResults);
|
if ($shopSearchDisplayIndividualQueries === []) {
|
||||||
|
$shopResults = $this->guardShopResultsByReferencedProductAnchor($shopSearchQuery, $shopResults);
|
||||||
|
}
|
||||||
$shopResults = $this->guardShopResultsByExactRequestedAccessoryCode($prompt, $shopSearchQuery, $shopResults);
|
$shopResults = $this->guardShopResultsByExactRequestedAccessoryCode($prompt, $shopSearchQuery, $shopResults);
|
||||||
$shopResults = $this->sortShopResultsForLengthRequest($prompt, $shopSearchQuery, $shopResults);
|
$shopResults = $this->sortShopResultsForLengthRequest($prompt, $shopSearchQuery, $shopResults);
|
||||||
$attemptedShopRepair = $repairPayload['attemptedRepair'] || $directIdentityRepairPayload['attemptedRepair'];
|
$attemptedShopRepair = $repairPayload['attemptedRepair'] || $directIdentityRepairPayload['attemptedRepair'];
|
||||||
@@ -598,14 +646,25 @@ final readonly class AgentRunner
|
|||||||
$directIdentityRepairPayload['repairQueries']
|
$directIdentityRepairPayload['repairQueries']
|
||||||
)));
|
)));
|
||||||
|
|
||||||
|
$completedShopSearchDisplayQuery = $shopSearchDisplayQuery !== '' ? $shopSearchDisplayQuery : $shopSearchQuery;
|
||||||
|
$completedShopSearchIndividualQueries = $shopSearchDisplayIndividualQueries;
|
||||||
|
if (
|
||||||
|
$usedShopRepair
|
||||||
|
&& $shopRepairQueries !== []
|
||||||
|
&& $this->isReferentialProductListShopFollowUpPrompt($prompt)
|
||||||
|
) {
|
||||||
|
$completedShopSearchIndividualQueries = $shopRepairQueries;
|
||||||
|
}
|
||||||
|
|
||||||
if (!$primaryShopSearchHadSystemFailure) {
|
if (!$primaryShopSearchHadSystemFailure) {
|
||||||
yield $this->systemMsg(
|
yield $this->systemMsg(
|
||||||
$this->buildShopSearchMetaMessage(
|
$this->buildShopSearchMetaMessage(
|
||||||
query: $shopSearchDisplayQuery !== '' ? $shopSearchDisplayQuery : $shopSearchQuery,
|
query: $completedShopSearchDisplayQuery,
|
||||||
commerceIntent: $commerceIntent,
|
commerceIntent: $commerceIntent,
|
||||||
usedOptimizedQuery: $shopSearchUsedOptimizedQuery,
|
usedOptimizedQuery: $shopSearchUsedOptimizedQuery,
|
||||||
originalQuery: $shopSearchQuery,
|
originalQuery: $shopSearchQuery,
|
||||||
resultCount: count($shopResults),
|
resultCount: count($shopResults),
|
||||||
|
individualQueries: $completedShopSearchIndividualQueries,
|
||||||
completed: true,
|
completed: true,
|
||||||
attemptedRepair: $attemptedShopRepair,
|
attemptedRepair: $attemptedShopRepair,
|
||||||
usedRepair: $usedShopRepair
|
usedRepair: $usedShopRepair
|
||||||
@@ -1534,6 +1593,119 @@ final readonly class AgentRunner
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve product-list follow-ups into individual product identity lookups
|
||||||
|
* before calling Shopware. This keeps referential prompts such as "links to
|
||||||
|
* the products" from sending a combined pseudo-query that Shopware cannot
|
||||||
|
* interpret as separate products.
|
||||||
|
*
|
||||||
|
* @param string[] $knowledgeChunks
|
||||||
|
* @return string[]
|
||||||
|
*/
|
||||||
|
private function resolveProductListFollowUpSplitLookupQueries(
|
||||||
|
string $prompt,
|
||||||
|
string $userId,
|
||||||
|
string $commerceHistoryContext,
|
||||||
|
array $knowledgeChunks
|
||||||
|
): array {
|
||||||
|
if (!$this->isReferentialProductListShopFollowUpPrompt($prompt)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$anchors = $this->extractProductListFollowUpAnchorsForLookup(
|
||||||
|
commerceHistoryContext: $commerceHistoryContext,
|
||||||
|
userId: $userId,
|
||||||
|
knowledgeChunks: $knowledgeChunks
|
||||||
|
);
|
||||||
|
|
||||||
|
if (count($anchors) < 2) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->buildProductListFollowUpAnchorLookupQueries($anchors, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute product-list follow-up lookups as actual separate Shopware
|
||||||
|
* searches. The execution is intentionally sequential and bounded by the
|
||||||
|
* configured max anchor count. The Store API client currently exposes a
|
||||||
|
* synchronous search contract; introducing concurrent transport here would
|
||||||
|
* be a wider service-level change and risk unrelated shop flows.
|
||||||
|
*
|
||||||
|
* @param string[] $queries
|
||||||
|
* @return array{results: array<int, ShopProductResult>, hadSystemFailure: bool, failureReason: ?string, queries: string[]}
|
||||||
|
*/
|
||||||
|
private function searchProductListFollowUpSplitLookupQueries(
|
||||||
|
string $prompt,
|
||||||
|
string $userId,
|
||||||
|
string $commerceIntent,
|
||||||
|
string $commerceHistoryContext,
|
||||||
|
array $queries
|
||||||
|
): array {
|
||||||
|
$queries = array_values(array_unique(array_filter(array_map(
|
||||||
|
static fn(string $query): string => trim($query),
|
||||||
|
$queries
|
||||||
|
), static fn(string $query): bool => $query !== '')));
|
||||||
|
|
||||||
|
$mergedResults = [];
|
||||||
|
$seenProducts = [];
|
||||||
|
$usedQueries = [];
|
||||||
|
$hadAnySystemFailure = false;
|
||||||
|
$hadAnySuccessfulSearch = false;
|
||||||
|
$failureReason = null;
|
||||||
|
|
||||||
|
foreach ($queries as $query) {
|
||||||
|
$queryResults = $this->searchShop(
|
||||||
|
$query,
|
||||||
|
$commerceIntent,
|
||||||
|
$userId,
|
||||||
|
$commerceHistoryContext
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($this->shopSearchService->hadLastSearchSystemFailure()) {
|
||||||
|
$hadAnySystemFailure = true;
|
||||||
|
$failureReason ??= $this->shopSearchService->getLastSearchFailureReason();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$hadAnySuccessfulSearch = true;
|
||||||
|
$usedQueries[] = $query;
|
||||||
|
|
||||||
|
$identityResults = $this->filterProductListFollowUpResultsByAnchorIdentity($queryResults, [$query]);
|
||||||
|
foreach ($identityResults as $product) {
|
||||||
|
if (!$product instanceof ShopProductResult) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$key = $this->buildShopProductDedupeKey($product);
|
||||||
|
if (isset($seenProducts[$key])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$seenProducts[$key] = true;
|
||||||
|
$mergedResults[] = $product;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->agentLogger->info('Executed product-list follow-up as separate product anchor shop searches', [
|
||||||
|
'userId' => $userId,
|
||||||
|
'commerceIntent' => $commerceIntent,
|
||||||
|
'prompt' => $prompt,
|
||||||
|
'queries' => $queries,
|
||||||
|
'usedQueries' => $usedQueries,
|
||||||
|
'resultCount' => count($mergedResults),
|
||||||
|
'hadAnySystemFailure' => $hadAnySystemFailure,
|
||||||
|
'hadAnySuccessfulSearch' => $hadAnySuccessfulSearch,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'results' => $mergedResults,
|
||||||
|
'hadSystemFailure' => $hadAnySystemFailure && !$hadAnySuccessfulSearch,
|
||||||
|
'failureReason' => $hadAnySystemFailure && !$hadAnySuccessfulSearch ? $failureReason : null,
|
||||||
|
'queries' => $usedQueries !== [] ? $usedQueries : $queries,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Keep referential product-list follow-ups aligned with the concrete product
|
* Keep referential product-list follow-ups aligned with the concrete product
|
||||||
* identities mentioned in the previous context. A combined query containing
|
* identities mentioned in the previous context. A combined query containing
|
||||||
@@ -1573,9 +1745,14 @@ final readonly class AgentRunner
|
|||||||
return $repairPayload;
|
return $repairPayload;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$identityResults = [];
|
||||||
|
$coveredAnchorKeys = [];
|
||||||
|
|
||||||
if ($currentResults !== []) {
|
if ($currentResults !== []) {
|
||||||
$identityResults = $this->filterProductListFollowUpResultsByAnchorIdentity($currentResults, $anchors);
|
$identityResults = $this->filterProductListFollowUpResultsByAnchorIdentity($currentResults, $anchors);
|
||||||
if ($identityResults !== []) {
|
$coveredAnchorKeys = $this->resolveProductListFollowUpCoveredAnchorKeys($identityResults, $anchors);
|
||||||
|
|
||||||
|
if ($identityResults !== [] && count($coveredAnchorKeys) >= count($anchors)) {
|
||||||
if (count($identityResults) !== count($currentResults)) {
|
if (count($identityResults) !== count($currentResults)) {
|
||||||
$this->agentLogger->info('Filtered product-list follow-up shop results to referenced product identities', [
|
$this->agentLogger->info('Filtered product-list follow-up shop results to referenced product identities', [
|
||||||
'userId' => $userId,
|
'userId' => $userId,
|
||||||
@@ -1599,8 +1776,22 @@ final readonly class AgentRunner
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$queries = $this->buildProductListFollowUpAnchorLookupQueries($anchors, $shopSearchQuery);
|
$missingAnchors = $this->filterProductListFollowUpMissingAnchors($anchors, $coveredAnchorKeys);
|
||||||
|
$lookupAnchors = count($anchors) > 1 && count($coveredAnchorKeys) < count($anchors)
|
||||||
|
? $anchors
|
||||||
|
: ($missingAnchors !== [] ? $missingAnchors : $anchors);
|
||||||
|
$queries = $this->buildProductListFollowUpAnchorLookupQueries($lookupAnchors, $shopSearchQuery);
|
||||||
|
|
||||||
if ($queries === []) {
|
if ($queries === []) {
|
||||||
|
if ($identityResults !== []) {
|
||||||
|
return [
|
||||||
|
'results' => $identityResults,
|
||||||
|
'attemptedRepair' => true,
|
||||||
|
'usedRepair' => true,
|
||||||
|
'repairQueries' => $repairPayload['repairQueries'] ?? [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
return $currentResults === [] ? $repairPayload : [
|
return $currentResults === [] ? $repairPayload : [
|
||||||
'results' => [],
|
'results' => [],
|
||||||
'attemptedRepair' => true,
|
'attemptedRepair' => true,
|
||||||
@@ -1613,6 +1804,20 @@ final readonly class AgentRunner
|
|||||||
$seenProducts = [];
|
$seenProducts = [];
|
||||||
$usedQueries = [];
|
$usedQueries = [];
|
||||||
|
|
||||||
|
foreach ($identityResults as $product) {
|
||||||
|
if (!$product instanceof ShopProductResult) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$key = $this->buildShopProductDedupeKey($product);
|
||||||
|
if (isset($seenProducts[$key])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$seenProducts[$key] = true;
|
||||||
|
$mergedResults[] = $product;
|
||||||
|
}
|
||||||
|
|
||||||
foreach ($queries as $query) {
|
foreach ($queries as $query) {
|
||||||
$queryResults = $this->searchShop(
|
$queryResults = $this->searchShop(
|
||||||
$query,
|
$query,
|
||||||
@@ -1647,15 +1852,17 @@ final readonly class AgentRunner
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$repairQueries = array_values(array_unique(array_merge(
|
||||||
|
$repairPayload['repairQueries'] ?? [],
|
||||||
|
$queries
|
||||||
|
)));
|
||||||
|
|
||||||
if ($mergedResults === []) {
|
if ($mergedResults === []) {
|
||||||
return [
|
return [
|
||||||
'results' => [],
|
'results' => [],
|
||||||
'attemptedRepair' => true,
|
'attemptedRepair' => true,
|
||||||
'usedRepair' => false,
|
'usedRepair' => false,
|
||||||
'repairQueries' => array_values(array_unique(array_merge(
|
'repairQueries' => $repairQueries,
|
||||||
$repairPayload['repairQueries'] ?? [],
|
|
||||||
$queries
|
|
||||||
))),
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1664,6 +1871,8 @@ final readonly class AgentRunner
|
|||||||
'commerceIntent' => $commerceIntent,
|
'commerceIntent' => $commerceIntent,
|
||||||
'prompt' => $prompt,
|
'prompt' => $prompt,
|
||||||
'shopSearchQuery' => $shopSearchQuery,
|
'shopSearchQuery' => $shopSearchQuery,
|
||||||
|
'anchors' => $anchors,
|
||||||
|
'missingAnchors' => $missingAnchors,
|
||||||
'anchorLookupQueries' => $usedQueries,
|
'anchorLookupQueries' => $usedQueries,
|
||||||
'resultCount' => count($mergedResults),
|
'resultCount' => count($mergedResults),
|
||||||
]);
|
]);
|
||||||
@@ -1672,10 +1881,7 @@ final readonly class AgentRunner
|
|||||||
'results' => $mergedResults,
|
'results' => $mergedResults,
|
||||||
'attemptedRepair' => true,
|
'attemptedRepair' => true,
|
||||||
'usedRepair' => true,
|
'usedRepair' => true,
|
||||||
'repairQueries' => array_values(array_unique(array_merge(
|
'repairQueries' => $repairQueries,
|
||||||
$repairPayload['repairQueries'] ?? [],
|
|
||||||
$usedQueries
|
|
||||||
))),
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1718,21 +1924,15 @@ final readonly class AgentRunner
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Avoid converting a single already-focused product query into a
|
// Avoid a redundant retry only when the current query already is
|
||||||
// redundant retry. The multi-product case remains eligible because
|
// exactly the same focused product query. A combined multi-product
|
||||||
// not all combined-query tokens belong to each individual anchor.
|
// query may contain all tokens of one anchor, but that still needs
|
||||||
if (count($anchors) === 1) {
|
// an individual lookup for the missing product identity.
|
||||||
$missing = false;
|
if (
|
||||||
foreach ($tokens as $token) {
|
count($anchors) === 1
|
||||||
if (!isset($combinedTokens[$token])) {
|
&& $this->buildProductListFollowUpAnchorKey($anchor) === implode(' ', array_keys($combinedTokens))
|
||||||
$missing = true;
|
) {
|
||||||
break;
|
continue;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$missing) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$queries[] = $anchor;
|
$queries[] = $anchor;
|
||||||
@@ -1741,6 +1941,66 @@ final readonly class AgentRunner
|
|||||||
return array_values(array_unique($queries));
|
return array_values(array_unique($queries));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ShopProductResult[] $shopResults
|
||||||
|
* @param string[] $anchors
|
||||||
|
* @return array<string, true>
|
||||||
|
*/
|
||||||
|
private function resolveProductListFollowUpCoveredAnchorKeys(array $shopResults, array $anchors): array
|
||||||
|
{
|
||||||
|
$covered = [];
|
||||||
|
$anchors = $this->normalizeProductListFollowUpAnchors($anchors);
|
||||||
|
|
||||||
|
if ($shopResults === [] || $anchors === []) {
|
||||||
|
return $covered;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($shopResults as $product) {
|
||||||
|
if (!$product instanceof ShopProductResult) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($anchors as $anchor) {
|
||||||
|
if (!$this->shopProductIdentityMatchesProductListAnchor($product, $anchor)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$key = $this->buildProductListFollowUpAnchorKey($anchor);
|
||||||
|
if ($key !== '') {
|
||||||
|
$covered[$key] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $covered;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string[] $anchors
|
||||||
|
* @param array<string, true> $coveredAnchorKeys
|
||||||
|
* @return string[]
|
||||||
|
*/
|
||||||
|
private function filterProductListFollowUpMissingAnchors(array $anchors, array $coveredAnchorKeys): array
|
||||||
|
{
|
||||||
|
$missing = [];
|
||||||
|
|
||||||
|
foreach ($this->normalizeProductListFollowUpAnchors($anchors) as $anchor) {
|
||||||
|
$key = $this->buildProductListFollowUpAnchorKey($anchor);
|
||||||
|
if ($key === '' || isset($coveredAnchorKeys[$key])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$missing[] = $anchor;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $missing;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildProductListFollowUpAnchorKey(string $anchor): string
|
||||||
|
{
|
||||||
|
return implode(' ', $this->tokenizeShopQueryCandidate($anchor));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string[] $anchors
|
* @param string[] $anchors
|
||||||
* @return string[]
|
* @return string[]
|
||||||
@@ -2998,7 +3258,7 @@ final readonly class AgentRunner
|
|||||||
// A standalone query optimizer may remove words, but it must not add
|
// A standalone query optimizer may remove words, but it must not add
|
||||||
// model numbers or article-like numbers that are absent from the
|
// model numbers or article-like numbers that are absent from the
|
||||||
// current user input. Otherwise old context can leak into new shop
|
// current user input. Otherwise old context can leak into new shop
|
||||||
// searches, for example "Anschlusskabel pH/Redox" -> "testomat 808".
|
// searches.
|
||||||
if (preg_match('/\d/u', $token) === 1) {
|
if (preg_match('/\d/u', $token) === 1) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -3765,20 +4025,26 @@ final readonly class AgentRunner
|
|||||||
|
|
||||||
private function canonicalizeProductListAnchor(string $anchor): string
|
private function canonicalizeProductListAnchor(string $anchor): string
|
||||||
{
|
{
|
||||||
|
$anchor = $this->trimProductListAnchorToConfiguredStart($anchor);
|
||||||
|
|
||||||
$tokens = $this->tokenizeShopQueryCandidate($anchor);
|
$tokens = $this->tokenizeShopQueryCandidate($anchor);
|
||||||
if ($tokens === []) {
|
if ($tokens === []) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (($tokens[0] ?? '') !== 'testomat') {
|
$familyTerms = $this->buildShopQueryTokenSet(
|
||||||
|
$this->agentRunnerConfig->getShopQueryProductListFollowUpCanonicalFamilyTerms()
|
||||||
|
);
|
||||||
|
$familyToken = (string) ($tokens[0] ?? '');
|
||||||
|
if ($familyTerms === [] || !isset($familyTerms[$familyToken])) {
|
||||||
return trim((string) preg_replace('/\s+/u', ' ', $anchor));
|
return trim((string) preg_replace('/\s+/u', ' ', $anchor));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isset($tokens[1])) {
|
if (!isset($tokens[1])) {
|
||||||
return 'testomat';
|
return $familyToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
$canonical = ['testomat', $tokens[1]];
|
$canonical = [$familyToken, $tokens[1]];
|
||||||
$variantTerms = $this->buildShopQueryTokenSet(
|
$variantTerms = $this->buildShopQueryTokenSet(
|
||||||
$this->agentRunnerConfig->getShopQueryPositiveTokenFilterAdjacentVariantTerms()
|
$this->agentRunnerConfig->getShopQueryPositiveTokenFilterAdjacentVariantTerms()
|
||||||
);
|
);
|
||||||
@@ -3797,6 +4063,36 @@ final readonly class AgentRunner
|
|||||||
return trim(implode(' ', $canonical));
|
return trim(implode(' ', $canonical));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function trimProductListAnchorToConfiguredStart(string $anchor): string
|
||||||
|
{
|
||||||
|
$anchor = trim($anchor);
|
||||||
|
if ($anchor === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($this->agentRunnerConfig->getShopQueryProductListFollowUpCanonicalStartPatterns() as $pattern) {
|
||||||
|
if (@preg_match($pattern, $anchor, $match) !== 1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$candidate = '';
|
||||||
|
if (isset($match['anchor']) && is_string($match['anchor'])) {
|
||||||
|
$candidate = $match['anchor'];
|
||||||
|
} elseif (isset($match[1]) && is_string($match[1])) {
|
||||||
|
$candidate = $match[1];
|
||||||
|
} elseif (isset($match[0]) && is_string($match[0])) {
|
||||||
|
$candidate = $match[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
$candidate = trim((string) preg_replace('/\s+/u', ' ', $candidate));
|
||||||
|
if ($candidate !== '') {
|
||||||
|
return $candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return trim((string) preg_replace('/\s+/u', ' ', $anchor));
|
||||||
|
}
|
||||||
|
|
||||||
private function guardMainDeviceReferentialShopQueryWithHistoryModelAnchor(
|
private function guardMainDeviceReferentialShopQueryWithHistoryModelAnchor(
|
||||||
string $prompt,
|
string $prompt,
|
||||||
string $shopSearchQuery,
|
string $shopSearchQuery,
|
||||||
@@ -6903,6 +7199,9 @@ final readonly class AgentRunner
|
|||||||
return $this->normalizeOneLine($rendered);
|
return $this->normalizeOneLine($rendered);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string[] $individualQueries Actual individual Shopware queries that were prepared/executed for split product-list follow-ups.
|
||||||
|
*/
|
||||||
private function buildShopSearchMetaMessage(
|
private function buildShopSearchMetaMessage(
|
||||||
string $query,
|
string $query,
|
||||||
string $commerceIntent,
|
string $commerceIntent,
|
||||||
@@ -6912,10 +7211,12 @@ final readonly class AgentRunner
|
|||||||
bool $completed = false,
|
bool $completed = false,
|
||||||
bool $attemptedRepair = false,
|
bool $attemptedRepair = false,
|
||||||
bool $usedRepair = false,
|
bool $usedRepair = false,
|
||||||
bool $unavailable = false
|
bool $unavailable = false,
|
||||||
|
array $individualQueries = []
|
||||||
): string {
|
): string {
|
||||||
$query = $this->normalizeOneLine($query);
|
$query = $this->normalizeOneLine($query);
|
||||||
$originalQuery = $this->normalizeOneLine($originalQuery);
|
$originalQuery = $this->normalizeOneLine($originalQuery);
|
||||||
|
$individualQueries = $this->normalizeProductListFollowUpIndividualQueriesForDisplay($individualQueries);
|
||||||
|
|
||||||
if ($query === '') {
|
if ($query === '') {
|
||||||
$query = $originalQuery !== '' ? $originalQuery : $this->agentRunnerConfig->getProductionUiText('shop_meta_fallback_query');
|
$query = $originalQuery !== '' ? $originalQuery : $this->agentRunnerConfig->getProductionUiText('shop_meta_fallback_query');
|
||||||
@@ -6963,15 +7264,56 @@ final readonly class AgentRunner
|
|||||||
$html .= '<span class="retriex-meta-pill">' . htmlspecialchars($repairLabel, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</span>';
|
$html .= '<span class="retriex-meta-pill">' . htmlspecialchars($repairLabel, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</span>';
|
||||||
}
|
}
|
||||||
|
|
||||||
$html .= '</div>'
|
$html .= '</div>';
|
||||||
. '<div class="retriex-meta-query"><span>' . htmlspecialchars($this->agentRunnerConfig->getProductionUiText('shop_meta_query_label'), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</span><code>'
|
|
||||||
. htmlspecialchars($query, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')
|
if ($individualQueries !== []) {
|
||||||
. '</code></div>'
|
$html .= '<div class="retriex-meta-query retriex-meta-query--multi"><span>'
|
||||||
. '</div>';
|
. htmlspecialchars($this->agentRunnerConfig->getProductionUiText('shop_meta_queries_label'), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')
|
||||||
|
. '</span><span class="retriex-meta-query__list">';
|
||||||
|
|
||||||
|
foreach ($individualQueries as $individualQuery) {
|
||||||
|
$html .= '<code>' . htmlspecialchars($individualQuery, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</code>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$html .= '</span></div>';
|
||||||
|
} else {
|
||||||
|
$html .= '<div class="retriex-meta-query"><span>' . htmlspecialchars($this->agentRunnerConfig->getProductionUiText('shop_meta_query_label'), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</span><code>'
|
||||||
|
. htmlspecialchars($query, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')
|
||||||
|
. '</code></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$html .= '</div>';
|
||||||
|
|
||||||
return $html;
|
return $html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string[] $queries
|
||||||
|
* @return string[]
|
||||||
|
*/
|
||||||
|
private function normalizeProductListFollowUpIndividualQueriesForDisplay(array $queries): array
|
||||||
|
{
|
||||||
|
$normalized = [];
|
||||||
|
$seen = [];
|
||||||
|
|
||||||
|
foreach ($queries as $query) {
|
||||||
|
$query = $this->normalizeOneLine((string) $query);
|
||||||
|
if ($query === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$key = mb_strtolower($query, 'UTF-8');
|
||||||
|
if (isset($seen[$key])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$seen[$key] = true;
|
||||||
|
$normalized[] = $query;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $normalized;
|
||||||
|
}
|
||||||
|
|
||||||
private function buildShopUnavailableMessage(?string $reason): string
|
private function buildShopUnavailableMessage(?string $reason): string
|
||||||
{
|
{
|
||||||
$reason = $this->normalizeOneLine((string) $reason);
|
$reason = $this->normalizeOneLine((string) $reason);
|
||||||
|
|||||||
@@ -1863,6 +1863,22 @@ final class AgentRunnerConfig
|
|||||||
{
|
{
|
||||||
return $this->genreStringList('context_resolution.product_list_followup.anchor_patterns');
|
return $this->genreStringList('context_resolution.product_list_followup.anchor_patterns');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string[]
|
||||||
|
*/
|
||||||
|
public function getShopQueryProductListFollowUpCanonicalStartPatterns(): array
|
||||||
|
{
|
||||||
|
return $this->genreStringList('context_resolution.product_list_followup.canonical_start_patterns');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string[]
|
||||||
|
*/
|
||||||
|
public function getShopQueryProductListFollowUpCanonicalFamilyTerms(): array
|
||||||
|
{
|
||||||
|
return $this->genreStringList('context_resolution.product_list_followup.canonical_family_terms');
|
||||||
|
}
|
||||||
public function isShopQueryRagAnchorEnrichmentEnabled(): bool
|
public function isShopQueryRagAnchorEnrichmentEnabled(): bool
|
||||||
{
|
{
|
||||||
return $this->getRequiredBool('shop_runtime.context_resolution.rag_anchor_enrichment.enabled');
|
return $this->getRequiredBool('shop_runtime.context_resolution.rag_anchor_enrichment.enabled');
|
||||||
|
|||||||
@@ -411,6 +411,7 @@ final class ChatMessagesConfig
|
|||||||
'agent.production_ui.text.shop_meta_repair_checked',
|
'agent.production_ui.text.shop_meta_repair_checked',
|
||||||
'agent.production_ui.text.shop_meta_eyebrow',
|
'agent.production_ui.text.shop_meta_eyebrow',
|
||||||
'agent.production_ui.text.shop_meta_query_label',
|
'agent.production_ui.text.shop_meta_query_label',
|
||||||
|
'agent.production_ui.text.shop_meta_queries_label',
|
||||||
'agent.production_ui.text.shop_meta_query_prefix',
|
'agent.production_ui.text.shop_meta_query_prefix',
|
||||||
'agent.production_ui.text.shop_meta_intent_prefix',
|
'agent.production_ui.text.shop_meta_intent_prefix',
|
||||||
'agent.production_ui.text.shop_unavailable_default_reason',
|
'agent.production_ui.text.shop_unavailable_default_reason',
|
||||||
|
|||||||
@@ -1370,7 +1370,9 @@ final readonly class RetriexEffectiveConfigProvider
|
|||||||
$this->validateStringList($this->toList($productListFollowUp['product_terms'] ?? []), 'genre.configuration_values.context_resolution.product_list_followup.product_terms', $errors, $warnings);
|
$this->validateStringList($this->toList($productListFollowUp['product_terms'] ?? []), 'genre.configuration_values.context_resolution.product_list_followup.product_terms', $errors, $warnings);
|
||||||
$this->validateStringList($this->toList($productListFollowUp['shop_terms'] ?? []), 'genre.configuration_values.context_resolution.product_list_followup.shop_terms', $errors, $warnings);
|
$this->validateStringList($this->toList($productListFollowUp['shop_terms'] ?? []), 'genre.configuration_values.context_resolution.product_list_followup.shop_terms', $errors, $warnings);
|
||||||
$this->validateStringList($this->toList($productListFollowUp['noise_terms'] ?? []), 'genre.configuration_values.context_resolution.product_list_followup.noise_terms', $errors, $warnings);
|
$this->validateStringList($this->toList($productListFollowUp['noise_terms'] ?? []), 'genre.configuration_values.context_resolution.product_list_followup.noise_terms', $errors, $warnings);
|
||||||
|
$this->validateStringList($this->toList($productListFollowUp['canonical_family_terms'] ?? []), 'genre.configuration_values.context_resolution.product_list_followup.canonical_family_terms', $errors, $warnings);
|
||||||
$this->validateRegexPatternList($productListFollowUp['anchor_patterns'] ?? [], 'genre.configuration_values.context_resolution.product_list_followup.anchor_patterns', $errors);
|
$this->validateRegexPatternList($productListFollowUp['anchor_patterns'] ?? [], 'genre.configuration_values.context_resolution.product_list_followup.anchor_patterns', $errors);
|
||||||
|
$this->validateRegexPatternList($productListFollowUp['canonical_start_patterns'] ?? [], 'genre.configuration_values.context_resolution.product_list_followup.canonical_start_patterns', $errors);
|
||||||
}
|
}
|
||||||
|
|
||||||
$shopQueryRuntime = is_array($configurationValues['shop_query_runtime'] ?? null)
|
$shopQueryRuntime = is_array($configurationValues['shop_query_runtime'] ?? null)
|
||||||
|
|||||||
Reference in New Issue
Block a user