p86h-k
This commit is contained in:
@@ -49,7 +49,7 @@ parameters:
|
||||
assistant:
|
||||
loader: 'Antwort wird vorbereitet…'
|
||||
aborted: '[aborted]'
|
||||
history_cleared: 'History cleared.'
|
||||
history_cleared: 'Chat-History wurde gelöscht.'
|
||||
source_chips:
|
||||
live_shop_data: 'Live-Shopdaten'
|
||||
run_meta:
|
||||
@@ -209,6 +209,7 @@ parameters:
|
||||
shop_meta_repair_checked: 'Erweiterte Suche: geprüft'
|
||||
shop_meta_eyebrow: Shop-Suche
|
||||
shop_meta_query_label: Gesendete Suchquery
|
||||
shop_meta_queries_label: Gesendete Einzelqueries
|
||||
shop_meta_query_prefix: 'Query: '
|
||||
shop_meta_intent_prefix: 'Intent: '
|
||||
shop_unavailable_default_reason: Keine Detailmeldung vom Shopware-Server.
|
||||
|
||||
@@ -1193,6 +1193,10 @@ parameters:
|
||||
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'
|
||||
- '/\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:
|
||||
origin: genre_native
|
||||
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;
|
||||
}
|
||||
|
||||
.retriex-meta-query__list {
|
||||
display: grid;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.retriex-meta-query--multi code {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.retriex-alert {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
|
||||
@@ -30,7 +30,9 @@
|
||||
<img src="/assets/img/logo.svg" style="max-height: 20px;">
|
||||
<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 id="ai-cloud" class="ai-cloud d-none"></div>
|
||||
<div id="chat" class="chat"></div>
|
||||
|
||||
@@ -464,13 +464,34 @@ final readonly class AgentRunner
|
||||
? $shopQueryPreview->searchText
|
||||
: $shopSearchQuery;
|
||||
$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(
|
||||
$this->buildShopSearchMetaMessage(
|
||||
query: $shopSearchDisplayQuery,
|
||||
commerceIntent: $commerceIntent,
|
||||
usedOptimizedQuery: $shopSearchUsedOptimizedQuery,
|
||||
originalQuery: $shopSearchQuery
|
||||
originalQuery: $shopSearchQuery,
|
||||
individualQueries: $shopSearchDisplayIndividualQueries
|
||||
),
|
||||
'meta'
|
||||
);
|
||||
@@ -481,6 +502,7 @@ final readonly class AgentRunner
|
||||
'usedOptimizedShopQuery' => $optimizedShopQuery !== '',
|
||||
'optimizedShopQuery' => $optimizedShopQuery,
|
||||
'shopSearchQuery' => $shopSearchQuery,
|
||||
'individualShopQueries' => $shopSearchDisplayIndividualQueries,
|
||||
'hasCommerceHistoryContext' => $commerceHistoryContext !== '',
|
||||
'commerceHistoryContextLength' => mb_strlen($commerceHistoryContext),
|
||||
]);
|
||||
@@ -503,14 +525,28 @@ final readonly class AgentRunner
|
||||
);
|
||||
|
||||
$shopSearchAttempted = true;
|
||||
$primaryShopResults = $this->searchShop(
|
||||
$shopSearchQuery,
|
||||
$commerceIntent,
|
||||
$userId,
|
||||
$shopQueryHistoryContext
|
||||
);
|
||||
$primaryShopSearchHadSystemFailure = $this->shopSearchService->hadLastSearchSystemFailure();
|
||||
$primaryShopSearchFailureReason = $this->shopSearchService->getLastSearchFailureReason();
|
||||
$productListSplitLookupPayload = null;
|
||||
if ($productListSplitLookupQueries !== []) {
|
||||
$productListSplitLookupPayload = $this->searchProductListFollowUpSplitLookupQueries(
|
||||
prompt: $originalPrompt,
|
||||
userId: $userId,
|
||||
commerceIntent: $commerceIntent,
|
||||
commerceHistoryContext: $shopQueryHistoryContext,
|
||||
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) {
|
||||
$this->agentLogger->warning('Shop repair skipped after Store API system failure', [
|
||||
@@ -532,6 +568,7 @@ final readonly class AgentRunner
|
||||
usedOptimizedQuery: $shopSearchUsedOptimizedQuery,
|
||||
originalQuery: $shopSearchQuery,
|
||||
completed: true,
|
||||
individualQueries: $shopSearchDisplayIndividualQueries,
|
||||
unavailable: true
|
||||
),
|
||||
'meta'
|
||||
@@ -548,27 +585,36 @@ final readonly class AgentRunner
|
||||
'repairQueries' => [],
|
||||
];
|
||||
} 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(
|
||||
prompt: $prompt,
|
||||
userId: $userId,
|
||||
commerceIntent: $commerceIntent,
|
||||
commerceHistoryContext: $shopQueryHistoryContext,
|
||||
primaryQuery: $shopSearchQuery,
|
||||
primaryShopResults: $primaryShopResults,
|
||||
knowledgeChunks: $knowledgeChunks
|
||||
);
|
||||
$repairPayload = $this->repairShopResults(
|
||||
prompt: $prompt,
|
||||
userId: $userId,
|
||||
commerceIntent: $commerceIntent,
|
||||
commerceHistoryContext: $shopQueryHistoryContext,
|
||||
primaryQuery: $shopSearchQuery,
|
||||
primaryShopResults: $primaryShopResults,
|
||||
knowledgeChunks: $knowledgeChunks
|
||||
);
|
||||
|
||||
$repairPayload = $this->repairProductListFollowUpShopResultsWithAnchorLookups(
|
||||
prompt: $prompt,
|
||||
userId: $userId,
|
||||
commerceIntent: $commerceIntent,
|
||||
commerceHistoryContext: $shopQueryHistoryContext,
|
||||
shopSearchQuery: $shopSearchQuery,
|
||||
repairPayload: $repairPayload,
|
||||
knowledgeChunks: $knowledgeChunks
|
||||
);
|
||||
$repairPayload = $this->repairProductListFollowUpShopResultsWithAnchorLookups(
|
||||
prompt: $prompt,
|
||||
userId: $userId,
|
||||
commerceIntent: $commerceIntent,
|
||||
commerceHistoryContext: $shopQueryHistoryContext,
|
||||
shopSearchQuery: $shopSearchQuery,
|
||||
repairPayload: $repairPayload,
|
||||
knowledgeChunks: $knowledgeChunks
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -588,7 +634,9 @@ final readonly class AgentRunner
|
||||
$shopResults = $directIdentityRepairPayload['results'];
|
||||
}
|
||||
|
||||
$shopResults = $this->guardShopResultsByReferencedProductAnchor($shopSearchQuery, $shopResults);
|
||||
if ($shopSearchDisplayIndividualQueries === []) {
|
||||
$shopResults = $this->guardShopResultsByReferencedProductAnchor($shopSearchQuery, $shopResults);
|
||||
}
|
||||
$shopResults = $this->guardShopResultsByExactRequestedAccessoryCode($prompt, $shopSearchQuery, $shopResults);
|
||||
$shopResults = $this->sortShopResultsForLengthRequest($prompt, $shopSearchQuery, $shopResults);
|
||||
$attemptedShopRepair = $repairPayload['attemptedRepair'] || $directIdentityRepairPayload['attemptedRepair'];
|
||||
@@ -598,14 +646,25 @@ final readonly class AgentRunner
|
||||
$directIdentityRepairPayload['repairQueries']
|
||||
)));
|
||||
|
||||
$completedShopSearchDisplayQuery = $shopSearchDisplayQuery !== '' ? $shopSearchDisplayQuery : $shopSearchQuery;
|
||||
$completedShopSearchIndividualQueries = $shopSearchDisplayIndividualQueries;
|
||||
if (
|
||||
$usedShopRepair
|
||||
&& $shopRepairQueries !== []
|
||||
&& $this->isReferentialProductListShopFollowUpPrompt($prompt)
|
||||
) {
|
||||
$completedShopSearchIndividualQueries = $shopRepairQueries;
|
||||
}
|
||||
|
||||
if (!$primaryShopSearchHadSystemFailure) {
|
||||
yield $this->systemMsg(
|
||||
$this->buildShopSearchMetaMessage(
|
||||
query: $shopSearchDisplayQuery !== '' ? $shopSearchDisplayQuery : $shopSearchQuery,
|
||||
query: $completedShopSearchDisplayQuery,
|
||||
commerceIntent: $commerceIntent,
|
||||
usedOptimizedQuery: $shopSearchUsedOptimizedQuery,
|
||||
originalQuery: $shopSearchQuery,
|
||||
resultCount: count($shopResults),
|
||||
individualQueries: $completedShopSearchIndividualQueries,
|
||||
completed: true,
|
||||
attemptedRepair: $attemptedShopRepair,
|
||||
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
|
||||
* identities mentioned in the previous context. A combined query containing
|
||||
@@ -1573,9 +1745,14 @@ final readonly class AgentRunner
|
||||
return $repairPayload;
|
||||
}
|
||||
|
||||
$identityResults = [];
|
||||
$coveredAnchorKeys = [];
|
||||
|
||||
if ($currentResults !== []) {
|
||||
$identityResults = $this->filterProductListFollowUpResultsByAnchorIdentity($currentResults, $anchors);
|
||||
if ($identityResults !== []) {
|
||||
$coveredAnchorKeys = $this->resolveProductListFollowUpCoveredAnchorKeys($identityResults, $anchors);
|
||||
|
||||
if ($identityResults !== [] && count($coveredAnchorKeys) >= count($anchors)) {
|
||||
if (count($identityResults) !== count($currentResults)) {
|
||||
$this->agentLogger->info('Filtered product-list follow-up shop results to referenced product identities', [
|
||||
'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 ($identityResults !== []) {
|
||||
return [
|
||||
'results' => $identityResults,
|
||||
'attemptedRepair' => true,
|
||||
'usedRepair' => true,
|
||||
'repairQueries' => $repairPayload['repairQueries'] ?? [],
|
||||
];
|
||||
}
|
||||
|
||||
return $currentResults === [] ? $repairPayload : [
|
||||
'results' => [],
|
||||
'attemptedRepair' => true,
|
||||
@@ -1613,6 +1804,20 @@ final readonly class AgentRunner
|
||||
$seenProducts = [];
|
||||
$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) {
|
||||
$queryResults = $this->searchShop(
|
||||
$query,
|
||||
@@ -1647,15 +1852,17 @@ final readonly class AgentRunner
|
||||
}
|
||||
}
|
||||
|
||||
$repairQueries = array_values(array_unique(array_merge(
|
||||
$repairPayload['repairQueries'] ?? [],
|
||||
$queries
|
||||
)));
|
||||
|
||||
if ($mergedResults === []) {
|
||||
return [
|
||||
'results' => [],
|
||||
'attemptedRepair' => true,
|
||||
'usedRepair' => false,
|
||||
'repairQueries' => array_values(array_unique(array_merge(
|
||||
$repairPayload['repairQueries'] ?? [],
|
||||
$queries
|
||||
))),
|
||||
'repairQueries' => $repairQueries,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1664,6 +1871,8 @@ final readonly class AgentRunner
|
||||
'commerceIntent' => $commerceIntent,
|
||||
'prompt' => $prompt,
|
||||
'shopSearchQuery' => $shopSearchQuery,
|
||||
'anchors' => $anchors,
|
||||
'missingAnchors' => $missingAnchors,
|
||||
'anchorLookupQueries' => $usedQueries,
|
||||
'resultCount' => count($mergedResults),
|
||||
]);
|
||||
@@ -1672,10 +1881,7 @@ final readonly class AgentRunner
|
||||
'results' => $mergedResults,
|
||||
'attemptedRepair' => true,
|
||||
'usedRepair' => true,
|
||||
'repairQueries' => array_values(array_unique(array_merge(
|
||||
$repairPayload['repairQueries'] ?? [],
|
||||
$usedQueries
|
||||
))),
|
||||
'repairQueries' => $repairQueries,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1718,21 +1924,15 @@ final readonly class AgentRunner
|
||||
continue;
|
||||
}
|
||||
|
||||
// Avoid converting a single already-focused product query into a
|
||||
// redundant retry. The multi-product case remains eligible because
|
||||
// not all combined-query tokens belong to each individual anchor.
|
||||
if (count($anchors) === 1) {
|
||||
$missing = false;
|
||||
foreach ($tokens as $token) {
|
||||
if (!isset($combinedTokens[$token])) {
|
||||
$missing = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$missing) {
|
||||
continue;
|
||||
}
|
||||
// Avoid a redundant retry only when the current query already is
|
||||
// exactly the same focused product query. A combined multi-product
|
||||
// query may contain all tokens of one anchor, but that still needs
|
||||
// an individual lookup for the missing product identity.
|
||||
if (
|
||||
count($anchors) === 1
|
||||
&& $this->buildProductListFollowUpAnchorKey($anchor) === implode(' ', array_keys($combinedTokens))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$queries[] = $anchor;
|
||||
@@ -1741,6 +1941,66 @@ final readonly class AgentRunner
|
||||
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
|
||||
* @return string[]
|
||||
@@ -2998,7 +3258,7 @@ final readonly class AgentRunner
|
||||
// A standalone query optimizer may remove words, but it must not add
|
||||
// model numbers or article-like numbers that are absent from the
|
||||
// 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) {
|
||||
return true;
|
||||
}
|
||||
@@ -3765,20 +4025,26 @@ final readonly class AgentRunner
|
||||
|
||||
private function canonicalizeProductListAnchor(string $anchor): string
|
||||
{
|
||||
$anchor = $this->trimProductListAnchorToConfiguredStart($anchor);
|
||||
|
||||
$tokens = $this->tokenizeShopQueryCandidate($anchor);
|
||||
if ($tokens === []) {
|
||||
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));
|
||||
}
|
||||
|
||||
if (!isset($tokens[1])) {
|
||||
return 'testomat';
|
||||
return $familyToken;
|
||||
}
|
||||
|
||||
$canonical = ['testomat', $tokens[1]];
|
||||
$canonical = [$familyToken, $tokens[1]];
|
||||
$variantTerms = $this->buildShopQueryTokenSet(
|
||||
$this->agentRunnerConfig->getShopQueryPositiveTokenFilterAdjacentVariantTerms()
|
||||
);
|
||||
@@ -3797,6 +4063,36 @@ final readonly class AgentRunner
|
||||
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(
|
||||
string $prompt,
|
||||
string $shopSearchQuery,
|
||||
@@ -6903,6 +7199,9 @@ final readonly class AgentRunner
|
||||
return $this->normalizeOneLine($rendered);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $individualQueries Actual individual Shopware queries that were prepared/executed for split product-list follow-ups.
|
||||
*/
|
||||
private function buildShopSearchMetaMessage(
|
||||
string $query,
|
||||
string $commerceIntent,
|
||||
@@ -6912,10 +7211,12 @@ final readonly class AgentRunner
|
||||
bool $completed = false,
|
||||
bool $attemptedRepair = false,
|
||||
bool $usedRepair = false,
|
||||
bool $unavailable = false
|
||||
bool $unavailable = false,
|
||||
array $individualQueries = []
|
||||
): string {
|
||||
$query = $this->normalizeOneLine($query);
|
||||
$originalQuery = $this->normalizeOneLine($originalQuery);
|
||||
$individualQueries = $this->normalizeProductListFollowUpIndividualQueriesForDisplay($individualQueries);
|
||||
|
||||
if ($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 .= '</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')
|
||||
. '</code></div>'
|
||||
. '</div>';
|
||||
$html .= '</div>';
|
||||
|
||||
if ($individualQueries !== []) {
|
||||
$html .= '<div class="retriex-meta-query retriex-meta-query--multi"><span>'
|
||||
. 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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
|
||||
{
|
||||
$reason = $this->normalizeOneLine((string) $reason);
|
||||
|
||||
@@ -1863,6 +1863,22 @@ final class AgentRunnerConfig
|
||||
{
|
||||
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
|
||||
{
|
||||
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_eyebrow',
|
||||
'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_intent_prefix',
|
||||
'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['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['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['canonical_start_patterns'] ?? [], 'genre.configuration_values.context_resolution.product_list_followup.canonical_start_patterns', $errors);
|
||||
}
|
||||
|
||||
$shopQueryRuntime = is_array($configurationValues['shop_query_runtime'] ?? null)
|
||||
|
||||
Reference in New Issue
Block a user