p86a-e
This commit is contained in:
@@ -105,6 +105,7 @@ parameters:
|
||||
- configuration_values.context_resolution.commercial_table_follow_up
|
||||
- configuration_values.context_resolution.referential_terms
|
||||
- configuration_values.context_resolution.history_anchor_enrichment
|
||||
- configuration_values.context_resolution.product_list_followup
|
||||
- configuration_values.context_resolution.meta_query_guard
|
||||
- configuration_values.context_resolution.rag_anchor_enrichment
|
||||
review_path_groups:
|
||||
@@ -1106,6 +1107,92 @@ parameters:
|
||||
- /\b(?:indikator(?:typ)?|indicator(?:\s+type)?|reagenz(?:satz|typ)?|reagent(?:\s+set|\s+type)?|typ|type)\s+[A-Za-zÄÖÜäöüß]{0,8}\s*\d{1,5}(?:\s*[A-ZÄÖÜ]{1,4})?(?:\s*%)?\b/iu
|
||||
template: '{anchor} {query}'
|
||||
max_query_terms: 2
|
||||
product_list_followup:
|
||||
origin: genre_native
|
||||
enabled: true
|
||||
# Handles referential follow-ups such as "links/preise zu den
|
||||
# produkten aus dem shop". It only rewrites weak meta queries and
|
||||
# uses product/model anchors from the latest assistant answer.
|
||||
weak_query_max_terms: 4
|
||||
weak_query_max_residual_terms: 0
|
||||
max_anchors: 4
|
||||
template: '{anchors}'
|
||||
product_terms:
|
||||
- produkt
|
||||
- produkte
|
||||
- produkten
|
||||
- produkteintrag
|
||||
- produkteinträge
|
||||
- produkteintraege
|
||||
- artikel
|
||||
- gerät
|
||||
- geraet
|
||||
- geräte
|
||||
- geraete
|
||||
- modell
|
||||
- modelle
|
||||
shop_terms:
|
||||
- shop
|
||||
- shopdaten
|
||||
- shop-daten
|
||||
- link
|
||||
- links
|
||||
- url
|
||||
- urls
|
||||
- produktlink
|
||||
- produktlinks
|
||||
- preis
|
||||
- preise
|
||||
- kosten
|
||||
- kostet
|
||||
noise_terms:
|
||||
- gebe
|
||||
- gib
|
||||
- zeige
|
||||
- zeig
|
||||
- nenne
|
||||
- mir
|
||||
- bitte
|
||||
- link
|
||||
- links
|
||||
- url
|
||||
- urls
|
||||
- produktlink
|
||||
- produktlinks
|
||||
- shop
|
||||
- shopdaten
|
||||
- shop-daten
|
||||
- preis
|
||||
- preise
|
||||
- kosten
|
||||
- kostet
|
||||
- produkt
|
||||
- produkte
|
||||
- produkten
|
||||
- artikel
|
||||
- gerät
|
||||
- geraet
|
||||
- geräte
|
||||
- geraete
|
||||
- modell
|
||||
- modelle
|
||||
- zu
|
||||
- zum
|
||||
- zur
|
||||
- aus
|
||||
- den
|
||||
- der
|
||||
- die
|
||||
- das
|
||||
- dem
|
||||
- diese
|
||||
- diesen
|
||||
- dieser
|
||||
- dazu
|
||||
- davon
|
||||
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'
|
||||
meta_query_guard:
|
||||
origin: genre_native
|
||||
meta_only_terms:
|
||||
@@ -1336,6 +1423,8 @@ parameters:
|
||||
- evo
|
||||
- eco
|
||||
- plus
|
||||
- self
|
||||
- clean
|
||||
- c
|
||||
- duo
|
||||
adjacent_variant_patterns:
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
# RetrieX Patch p86b - Referential Product Links RAG Fallback
|
||||
|
||||
## Ziel
|
||||
|
||||
Referenzielle Shop-Follow-ups wie:
|
||||
|
||||
```text
|
||||
gebe mir links zu den produkten aus dem shop
|
||||
```
|
||||
|
||||
duerfen nicht als schwache Meta-Query wie `links zu aus` an Shopware gehen, wenn im vorherigen Kontext konkrete Produkte/Modelle genannt wurden.
|
||||
|
||||
## Problem in p86
|
||||
|
||||
p86 ersetzte schwache Produktlisten-Follow-up-Queries nur, wenn Produktanker im Commerce-History-Kontext gefunden wurden. In manchen Runs war dieser Kontext fuer diesen Follow-up-Pfad leer, gekuerzt oder nicht ausreichend nutzbar, obwohl die aktuelle RAG-Retrieval-Stufe passende Produktkontexte geladen hatte. Dadurch blieb die Query unveraendert bei `links zu aus`.
|
||||
|
||||
## Aenderung
|
||||
|
||||
`guardReferentialProductListShopQueryWithHistoryAnchors()` erhaelt nun zusaetzlich die aktuellen RAG-`knowledgeChunks`.
|
||||
|
||||
Reihenfolge:
|
||||
|
||||
1. Produkt-/Modellanker aus dem aktuellen Commerce-History-Kontext extrahieren.
|
||||
2. Falls dort keine Anker gefunden werden: Produkt-/Modellanker aus den aktuellen RAG-Knowledge-Chunks extrahieren.
|
||||
3. Nur bei referenziellen Produktlisten-Shop-Follow-ups und nur bei schwachen/noisy Shopqueries ersetzen.
|
||||
4. Produktlisten-Anker-Patterns werden auf einzelne Zeilen begrenzt, damit `Testomat 808` nicht versehentlich mit dem naechsten Satz/Wort zusammengezogen wird.
|
||||
|
||||
Damit bleibt der Fix generisch:
|
||||
|
||||
- keine Sonderlogik fuer medizinische Geraete
|
||||
- keine festen Produktnamen im PHP-Core
|
||||
- keine neue Ranking-/Retrieval-/Shop-Matching-Logik
|
||||
- keine automatische Shop-Ausloesung durch `geraet`
|
||||
|
||||
## Erwartetes Verhalten
|
||||
|
||||
```text
|
||||
geraet zur messung Prozesswasser in medizinischen Geraeten
|
||||
-> RAG-Antwort nennt z. B. Testomat 2000 self clean, Testomat 2000 CAL, Testomat 808
|
||||
|
||||
gebe mir links zu den produkten aus dem shop
|
||||
-> Shopquery wird aus Produktankern gebildet, nicht aus `links zu aus`
|
||||
```
|
||||
|
||||
## Geaenderte Dateien
|
||||
|
||||
- `src/Agent/AgentRunner.php`
|
||||
- `config/retriex/genre.yaml`
|
||||
|
||||
## Lokale Checks
|
||||
|
||||
- `php -l src/Agent/AgentRunner.php`
|
||||
- `php -l src/Config/AgentRunnerConfig.php`
|
||||
- `php -l src/Config/RetriexEffectiveConfigProvider.php`
|
||||
- YAML parse
|
||||
- p86b referential product-link fallback smoke
|
||||
|
||||
Symfony-Console-Checks muessen in der Zielumgebung mit vorhandenem `vendor/` ausgefuehrt werden.
|
||||
@@ -0,0 +1,65 @@
|
||||
# RETRIEX Patch 86C - Referential Product Links Empty-History Fallback
|
||||
|
||||
## Ziel
|
||||
|
||||
Behebt den Fall, dass referenzielle Shop-Follow-ups wie
|
||||
|
||||
```text
|
||||
gebe mir links zu den produkten aus dem shop
|
||||
```
|
||||
|
||||
weiterhin als schwache Meta-Query wie `links zu aus` an Shopware gesendet werden, wenn der vorherige fachliche Antwortkontext nicht im Commerce-History-Kontext vorhanden ist.
|
||||
|
||||
## Ursache
|
||||
|
||||
p86/p86b enthielten bereits die generische Produktlisten-Follow-up-Logik inklusive RAG-Fallback. Die Guard-Methode wurde jedoch zu früh verlassen, sobald der Commerce-History-Kontext leer war:
|
||||
|
||||
```php
|
||||
trim($commerceHistoryContext) === ''
|
||||
```
|
||||
|
||||
Damit konnte der RAG-Fallback nicht greifen, obwohl aktuelle Knowledge-Chunks passende Produkt-/Modellanker enthielten.
|
||||
|
||||
## Änderung
|
||||
|
||||
- `src/Agent/AgentRunner.php`
|
||||
- Entfernt den frühen Return bei leerem Commerce-History-Kontext in `guardReferentialProductListShopQueryWithHistoryAnchors()`.
|
||||
- Nutzt History-Anker nur, wenn History vorhanden ist.
|
||||
- Fällt sonst auf Produktanker aus aktuellen Knowledge-Chunks zurück.
|
||||
- Log-Meldung präzisiert: History **oder RAG** Product Anchors.
|
||||
|
||||
## Erwartetes Verhalten
|
||||
|
||||
Vorher:
|
||||
|
||||
```text
|
||||
Follow-up: gebe mir links zu den produkten aus dem shop
|
||||
Gesendete Suchquery: links zu aus
|
||||
```
|
||||
|
||||
Nachher, wenn die vorherige/fachliche RAG-Antwort Produktanker wie Testomat-Modelle enthält:
|
||||
|
||||
```text
|
||||
Follow-up: gebe mir links zu den produkten aus dem shop
|
||||
Gesendete Suchquery: testomat 2000 self clean testomat 2000 cal testomat 808
|
||||
```
|
||||
|
||||
Die Logik bleibt generisch:
|
||||
|
||||
- keine Sonderlogik für medizinische Geräte
|
||||
- keine feste Produktliste im Core
|
||||
- kein pauschales `gerät => testomat`
|
||||
- nur bei bereits aktivem Shop-/Commerce-Follow-up
|
||||
- nur bei schwacher/noisy Meta-Query
|
||||
|
||||
## Lokale Checks
|
||||
|
||||
```text
|
||||
php -l src/Agent/AgentRunner.php
|
||||
php -l src/Config/AgentRunnerConfig.php
|
||||
php -l src/Config/RetriexEffectiveConfigProvider.php
|
||||
YAML parse OK
|
||||
p86c smoke OK
|
||||
```
|
||||
|
||||
Symfony-Console-Checks müssen in einer Umgebung mit installiertem `vendor/` ausgeführt werden.
|
||||
@@ -0,0 +1,70 @@
|
||||
# RETRIEX Patch 86D - Referential Product Links Deep Context Anchors
|
||||
|
||||
## Ziel
|
||||
|
||||
Behebt den weiterhin fehlerhaften Flow:
|
||||
|
||||
```text
|
||||
gerät zur messung Prozesswasser in medizinischen Geräten
|
||||
→ gebe mir links zu den produkten aus dem shop
|
||||
```
|
||||
|
||||
Die Shopquery durfte nicht als schwache Meta-/Noise-Query an Shopware gehen:
|
||||
|
||||
```text
|
||||
links zu aus
|
||||
```
|
||||
|
||||
sondern muss bei einem referenziellen Produktlisten-Follow-up die zuvor genannten Produkt-/Modellanker aus dem Verlauf verwenden.
|
||||
|
||||
## Tiefenanalyse
|
||||
|
||||
p86/p86b/p86c lagen am richtigen Themenbereich, aber der Hebel war noch nicht robust genug:
|
||||
|
||||
1. Die finale Shopquery entsteht in diesem Flow über den Standalone-/Fallback-Pfad und wird durch Stopword-/Positive-Filterung zu `links zu aus` reduziert.
|
||||
2. Der Produktlisten-Guard sitzt zwar vor der Shop-Suche, war aber zu abhängig vom direkt übergebenen `commerceHistoryContext`.
|
||||
3. In der aktuellen Basis war außerdem noch der frühe Return bei leerem `commerceHistoryContext` vorhanden, wodurch der RAG-/History-Fallback nicht zuverlässig erreicht wurde.
|
||||
4. Selbst bei History-Treffern konnten Produktanker wie `Testomat 2000 CAL-Version bietet` zu breit extrahiert werden, weil die vorhandenen Patterns mit `iu` arbeiten und dadurch Großbuchstabenklassen case-insensitiv wirken.
|
||||
|
||||
## Änderung
|
||||
|
||||
- `src/Agent/AgentRunner.php`
|
||||
- Der Produktlisten-Follow-up-Guard erhält jetzt zusätzlich die `userId`.
|
||||
- Der Guard verlässt die Methode nicht mehr nur wegen leerem `commerceHistoryContext`.
|
||||
- Er durchsucht mehrere Kontextkandidaten:
|
||||
- direkt übergebenen Commerce-History-Kontext
|
||||
- erweiterten Verlauf innerhalb des bestehenden Context-Fallback-Budgets
|
||||
- Full-History-Kontext, wenn in der bestehenden Config erlaubt
|
||||
- danach weiterhin aktuelle Knowledge-Chunks als Fallback
|
||||
- Produktlisten-Anker werden kanonisiert:
|
||||
- `Testomat 2000 self clean` bleibt erhalten.
|
||||
- `Testomat 2000 CAL-Version bietet` wird zu `testomat 2000 cal` gekürzt.
|
||||
- `Testomat 808-Version` wird zu `testomat 808` gekürzt.
|
||||
- Die Kanonisierung nutzt die bereits YAML-gepflegte `adjacent_variant_terms`-Liste aus `genre.yaml`; keine neue harte Produktliste im PHP-Core.
|
||||
|
||||
## Erwartetes Verhalten
|
||||
|
||||
```text
|
||||
Follow-up: gebe mir links zu den produkten aus dem shop
|
||||
Vorher: links zu aus
|
||||
Nachher: testomat 2000 self clean testomat 2000 cal testomat 808
|
||||
```
|
||||
|
||||
Die Logik bleibt generisch:
|
||||
|
||||
- keine Sonderlogik für medizinische Geräte
|
||||
- keine feste Produktliste im Core
|
||||
- kein pauschales `gerät => testomat`
|
||||
- nur bei bereits aktivem Shop-/Commerce-Follow-up
|
||||
- nur bei schwacher/noisy Meta-Query
|
||||
- Produktanker kommen aus Verlauf/RAG und werden gegen vorhandene YAML-Variantentokens normalisiert
|
||||
|
||||
## Lokale Checks
|
||||
|
||||
```text
|
||||
php -l src/Agent/AgentRunner.php
|
||||
YAML parse OK
|
||||
p86d anchor canonicalization smoke OK
|
||||
```
|
||||
|
||||
Symfony-Console-Checks müssen in einer Umgebung mit installiertem `vendor/` ausgeführt werden.
|
||||
@@ -0,0 +1,66 @@
|
||||
# RetrieX Patch p86e - Referential Product Links Early Context Resolution
|
||||
|
||||
## Problem
|
||||
|
||||
Referential shop follow-ups such as:
|
||||
|
||||
```text
|
||||
gebe mir links zu den produkten aus dem shop
|
||||
```
|
||||
|
||||
could still be converted into a weak/noisy Shopware query such as:
|
||||
|
||||
```text
|
||||
links zu aus
|
||||
```
|
||||
|
||||
In the affected flow, the previous RAG answer listed concrete products such as `Testomat® 2000 self clean`, `Testomat® 2000 CAL` and `Testomat® 808`, but the shop-query resolver treated the current follow-up as a standalone/current-input query before the product-list history anchor guard could reliably replace it.
|
||||
|
||||
## Root cause
|
||||
|
||||
The prompt is referential and commerce-related, but it is not covered by the older `meta-only` fallback path. Therefore `resolveShopSearchQuery()` could return the current cleaned prompt before consulting product anchors from recent history or the frontend context hint.
|
||||
|
||||
The later p86 guard existed, but this flow showed that relying only on a late correction after standalone cleanup was not robust enough.
|
||||
|
||||
## Change
|
||||
|
||||
p86e moves product-list follow-up resolution into the early shop-query resolution path:
|
||||
|
||||
- product-list follow-up prompts now explicitly require commerce history/context usage;
|
||||
- they are no longer isolated as standalone shop queries;
|
||||
- deterministic standalone query generation is disabled for this prompt class;
|
||||
- before returning optimized/current prompt fallback queries, `resolveShopSearchQuery()` tries to replace weak product-list meta queries with product/model anchors from history/context/RAG fallback;
|
||||
- product-list meta prompts whose tokens are all configured noise terms are treated as weak even when the raw prompt is longer than the normal weak-query term limit.
|
||||
|
||||
Expected behavior:
|
||||
|
||||
```text
|
||||
gerät zur messung Prozesswasser in medizinischen Geräten
|
||||
-> RAG answer lists products
|
||||
|
||||
gebe mir links zu den produkten aus dem shop
|
||||
-> testomat 2000 self clean testomat 2000 cal testomat 808
|
||||
```
|
||||
|
||||
## Guardrails
|
||||
|
||||
- No hardcoded medical-device rule.
|
||||
- No fixed product list in PHP.
|
||||
- No `gerät => testomat` shortcut.
|
||||
- Active only for referential product-list shop follow-ups.
|
||||
- Still requires weak/noisy shop query detection before replacing the query.
|
||||
- No changes to retrieval, ranking, scoring or Shopware matching.
|
||||
|
||||
## Local checks
|
||||
|
||||
```text
|
||||
php -l src/Agent/AgentRunner.php
|
||||
```
|
||||
|
||||
Additional reflection smoke test verified that the product-list guard and the early resolver return:
|
||||
|
||||
```text
|
||||
testomat 2000 self clean testomat 2000 cal testomat 808
|
||||
```
|
||||
|
||||
for the failing product-link follow-up when the previous turn contains the listed Testomat products.
|
||||
@@ -0,0 +1,70 @@
|
||||
# RetrieX Patch p86 - Referential Product List Shop Anchors
|
||||
|
||||
## Ziel
|
||||
|
||||
Referenzielle Shop-Follow-ups wie:
|
||||
|
||||
```text
|
||||
gebe mir links zu den produkten aus dem shop
|
||||
```
|
||||
|
||||
durften nicht mehr zu schwachen Meta-/Noise-Queries wie `links zu aus` werden, wenn die vorherige Antwort bereits konkrete Produkte oder Gerätemodelle genannt hat.
|
||||
|
||||
## Problem
|
||||
|
||||
Der bestehende History-/Shopquery-Repair war auf Zubehör-/Indikator-Referenzen und einzelne Hauptgeräte-Follow-ups fokussiert. Eine generische Nachfrage nach Links/Preisen zu "den Produkten" wurde zwar als Shop-Intent geroutet, verlor aber die zuvor genannten Produktanker und sendete eine nutzlose Query wie `links zu aus` an die Shopsuche.
|
||||
|
||||
## Lösung
|
||||
|
||||
p86 ergänzt einen generischen Product-List-Follow-up-Guard:
|
||||
|
||||
- erkennt referenzielle Produktlisten-Follow-ups über YAML-gepflegte `product_terms` + `shop_terms`
|
||||
- greift nur bei schwachen Shopqueries, deren Tokens vollständig aus YAML-gepflegten `noise_terms` bestehen
|
||||
- extrahiert Produkt-/Modellanker aus der neuesten Assistant-Antwort über YAML-gepflegte `anchor_patterns`
|
||||
- ersetzt die schwache Query durch die extrahierten Produktanker
|
||||
- bleibt auf Shopquery-Reparatur beschränkt und erzeugt keinen neuen Shop-Intent
|
||||
|
||||
Beispiel:
|
||||
|
||||
```text
|
||||
Vorherige Antwort nennt:
|
||||
- Testomat 2000 self clean
|
||||
- Testomat 2000 CAL
|
||||
- Testomat 808
|
||||
|
||||
Follow-up:
|
||||
gebe mir links zu den produkten aus dem shop
|
||||
|
||||
Vor p86:
|
||||
links zu aus
|
||||
|
||||
Nach p86:
|
||||
testomat 2000 self clean testomat 2000 cal testomat 808
|
||||
```
|
||||
|
||||
## Guardrails
|
||||
|
||||
- Keine harte Branchenlogik fuer "medizinische Geräte".
|
||||
- Keine Änderung an Retrieval, Ranking, Scoring oder Shop-Matching.
|
||||
- Kein pauschales `produkt => testomat` oder `gerät => testomat`.
|
||||
- Der Guard greift nur, wenn bereits Commerce-/Shop-Intent aktiv ist.
|
||||
- Bereits konkrete Shopqueries mit Produkt-/Modellanker werden nicht überschrieben.
|
||||
|
||||
## Dateien
|
||||
|
||||
- `config/retriex/genre.yaml`
|
||||
- `src/Agent/AgentRunner.php`
|
||||
- `src/Config/AgentRunnerConfig.php`
|
||||
- `src/Config/RetriexEffectiveConfigProvider.php`
|
||||
|
||||
## Lokale Checks
|
||||
|
||||
```text
|
||||
php -l src/Agent/AgentRunner.php
|
||||
php -l src/Config/AgentRunnerConfig.php
|
||||
php -l src/Config/RetriexEffectiveConfigProvider.php
|
||||
YAML parse OK
|
||||
p86 smoke OK: referential product-list prompt + weak query + product-anchor extraction
|
||||
```
|
||||
|
||||
Die Symfony-Console-Checks muessen in der Zielumgebung mit installiertem `vendor/` ausgefuehrt werden.
|
||||
@@ -272,7 +272,8 @@ final readonly class AgentRunner
|
||||
optimizedShopQuery: $optimizedShopQuery,
|
||||
commerceHistoryContext: $shopQueryHistoryContext,
|
||||
userId: $userId,
|
||||
currentPromptFallback: $routingPrompt
|
||||
currentPromptFallback: $routingPrompt,
|
||||
knowledgeChunks: $knowledgeChunks
|
||||
);
|
||||
}
|
||||
|
||||
@@ -335,6 +336,28 @@ final readonly class AgentRunner
|
||||
$optimizedShopQuery = '';
|
||||
}
|
||||
|
||||
$productListAnchoredShopSearchQuery = $this->guardReferentialProductListShopQueryWithHistoryAnchors(
|
||||
prompt: $originalPrompt,
|
||||
shopSearchQuery: $shopSearchQuery,
|
||||
commerceHistoryContext: $commerceHistoryContext,
|
||||
userId: $userId,
|
||||
knowledgeChunks: $knowledgeChunks
|
||||
);
|
||||
|
||||
if ($productListAnchoredShopSearchQuery !== $shopSearchQuery) {
|
||||
$this->agentLogger->info('Enriched referential product-list shop query with history or RAG product anchors', [
|
||||
'userId' => $userId,
|
||||
'prompt' => $prompt,
|
||||
'routingPrompt' => $routingPrompt,
|
||||
'optimizedShopQuery' => $optimizedShopQuery,
|
||||
'shopSearchQuery' => $shopSearchQuery,
|
||||
'productListAnchoredShopSearchQuery' => $productListAnchoredShopSearchQuery,
|
||||
]);
|
||||
|
||||
$shopSearchQuery = $productListAnchoredShopSearchQuery;
|
||||
$optimizedShopQuery = '';
|
||||
}
|
||||
|
||||
$ragAnchoredShopSearchQuery = $this->enrichShopSearchQueryWithRagAnchor(
|
||||
prompt: $originalPrompt,
|
||||
shopSearchQuery: $shopSearchQuery,
|
||||
@@ -1516,6 +1539,26 @@ final readonly class AgentRunner
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private function buildProductListFollowUpWeakQueryCandidates(
|
||||
string $prompt,
|
||||
string $optimizedShopQuery,
|
||||
string $currentPromptFallback
|
||||
): array {
|
||||
$candidates = [];
|
||||
|
||||
foreach ([$optimizedShopQuery, $currentPromptFallback, $prompt] as $candidate) {
|
||||
$candidate = trim($candidate);
|
||||
if ($candidate !== '' && !in_array($candidate, $candidates, true)) {
|
||||
$candidates[] = $candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return $candidates;
|
||||
}
|
||||
|
||||
private function shouldUseCommerceHistoryForShopQuery(string $prompt): bool
|
||||
{
|
||||
$prompt = trim($prompt);
|
||||
@@ -1528,6 +1571,10 @@ final readonly class AgentRunner
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($this->isReferentialProductListShopFollowUpPrompt($prompt)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($this->isMetaOnlyShopQuery($prompt)) {
|
||||
return true;
|
||||
}
|
||||
@@ -1579,7 +1626,11 @@ final readonly class AgentRunner
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->isCommercialTableFollowUpPrompt($prompt) || $this->isMetaOnlyShopQuery($prompt)) {
|
||||
if (
|
||||
$this->isCommercialTableFollowUpPrompt($prompt)
|
||||
|| $this->isReferentialProductListShopFollowUpPrompt($prompt)
|
||||
|| $this->isMetaOnlyShopQuery($prompt)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1632,6 +1683,10 @@ final readonly class AgentRunner
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->isReferentialProductListShopFollowUpPrompt($prompt)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->isMetaOnlyShopQuery($prompt)) {
|
||||
return false;
|
||||
}
|
||||
@@ -2547,12 +2602,16 @@ final readonly class AgentRunner
|
||||
return $overlap === 0 && !$this->isMetaOnlyShopQuery($prompt);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $knowledgeChunks
|
||||
*/
|
||||
private function resolveShopSearchQuery(
|
||||
string $prompt,
|
||||
string $optimizedShopQuery,
|
||||
string $commerceHistoryContext,
|
||||
string $userId,
|
||||
string $currentPromptFallback = ''
|
||||
string $currentPromptFallback = '',
|
||||
array $knowledgeChunks = []
|
||||
): string {
|
||||
if ($this->isCommercialTableFollowUpPrompt($prompt)) {
|
||||
foreach ($this->buildCommercialTableFollowUpContextCandidates($commerceHistoryContext, $userId) as $contextCandidate) {
|
||||
@@ -2564,11 +2623,25 @@ final readonly class AgentRunner
|
||||
}
|
||||
}
|
||||
|
||||
$currentPromptFallback = trim($currentPromptFallback);
|
||||
foreach ($this->buildProductListFollowUpWeakQueryCandidates($prompt, $optimizedShopQuery, $currentPromptFallback) as $productListFallbackQuery) {
|
||||
$productListContextQuery = $this->guardReferentialProductListShopQueryWithHistoryAnchors(
|
||||
prompt: $prompt,
|
||||
shopSearchQuery: $productListFallbackQuery,
|
||||
commerceHistoryContext: $commerceHistoryContext,
|
||||
userId: $userId,
|
||||
knowledgeChunks: $knowledgeChunks
|
||||
);
|
||||
|
||||
if ($productListContextQuery !== $productListFallbackQuery) {
|
||||
return $productListContextQuery;
|
||||
}
|
||||
}
|
||||
|
||||
if ($optimizedShopQuery !== '' && !$this->isMetaOnlyShopQuery($optimizedShopQuery)) {
|
||||
return $this->guardStandaloneOptimizedShopQuery($prompt, $optimizedShopQuery);
|
||||
}
|
||||
|
||||
$currentPromptFallback = trim($currentPromptFallback);
|
||||
if ($currentPromptFallback !== '' && !$this->isMetaOnlyShopQuery($currentPromptFallback)) {
|
||||
return $currentPromptFallback;
|
||||
}
|
||||
@@ -3062,6 +3135,261 @@ final readonly class AgentRunner
|
||||
return $enriched !== '' ? $enriched : $shopSearchQuery;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $knowledgeChunks
|
||||
*/
|
||||
private function guardReferentialProductListShopQueryWithHistoryAnchors(
|
||||
string $prompt,
|
||||
string $shopSearchQuery,
|
||||
string $commerceHistoryContext,
|
||||
string $userId,
|
||||
array $knowledgeChunks = []
|
||||
): string {
|
||||
$shopSearchQuery = trim($shopSearchQuery);
|
||||
|
||||
if (
|
||||
$shopSearchQuery === ''
|
||||
|| !$this->agentRunnerConfig->isShopQueryProductListFollowUpEnabled()
|
||||
|| !$this->isReferentialProductListShopFollowUpPrompt($prompt)
|
||||
|| !$this->isWeakProductListFollowUpShopQuery($shopSearchQuery)
|
||||
) {
|
||||
return $shopSearchQuery;
|
||||
}
|
||||
|
||||
$anchors = [];
|
||||
foreach ($this->buildProductListFollowUpAnchorContextCandidates($commerceHistoryContext, $userId) as $contextCandidate) {
|
||||
$anchors = $this->extractLatestHistoryProductListAnchors($contextCandidate);
|
||||
if ($anchors !== []) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($anchors === []) {
|
||||
$anchors = $this->extractProductListAnchorsFromKnowledgeChunks($knowledgeChunks);
|
||||
}
|
||||
|
||||
if ($anchors === []) {
|
||||
return $shopSearchQuery;
|
||||
}
|
||||
|
||||
$template = $this->agentRunnerConfig->getShopQueryProductListFollowUpTemplate();
|
||||
$rendered = $this->renderAgentTemplate($template, [
|
||||
'anchors' => implode(' ', $anchors),
|
||||
'query' => $shopSearchQuery,
|
||||
]);
|
||||
$rendered = preg_replace('/\s+/u', ' ', $rendered) ?? $rendered;
|
||||
$rendered = trim($rendered);
|
||||
|
||||
return $rendered !== '' ? $rendered : $shopSearchQuery;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private function buildProductListFollowUpAnchorContextCandidates(string $commerceHistoryContext, string $userId): array
|
||||
{
|
||||
$candidates = [];
|
||||
|
||||
$commerceHistoryContext = trim($commerceHistoryContext);
|
||||
if ($commerceHistoryContext !== '') {
|
||||
$candidates[] = $commerceHistoryContext;
|
||||
}
|
||||
|
||||
$extendedHistoryBudget = $this->agentRunnerConfig->getShopQueryContextFallbackHistoryBudgetChars();
|
||||
if ($extendedHistoryBudget > mb_strlen($commerceHistoryContext, 'UTF-8')) {
|
||||
$extendedHistory = trim($this->contextService->buildUserContextWithinBudget($userId, $extendedHistoryBudget));
|
||||
if ($extendedHistory !== '') {
|
||||
$candidates[] = $extendedHistory;
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->agentRunnerConfig->shouldUseFullHistoryForShopQueryContextFallback()) {
|
||||
$fullHistory = trim($this->contextService->buildUserContext($userId, true));
|
||||
if ($fullHistory !== '') {
|
||||
$candidates[] = $fullHistory;
|
||||
}
|
||||
}
|
||||
|
||||
return array_values(array_unique($candidates));
|
||||
}
|
||||
|
||||
private function isReferentialProductListShopFollowUpPrompt(string $prompt): bool
|
||||
{
|
||||
$tokens = $this->tokenizeShopQueryCandidate($prompt);
|
||||
if ($tokens === []) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$tokenSet = array_fill_keys($tokens, true);
|
||||
$productTokens = $this->buildShopQueryTokenSet(
|
||||
$this->agentRunnerConfig->getShopQueryProductListFollowUpProductTerms()
|
||||
);
|
||||
$shopTokens = $this->buildShopQueryTokenSet(
|
||||
$this->agentRunnerConfig->getShopQueryProductListFollowUpShopTerms()
|
||||
);
|
||||
|
||||
return $this->tokenSetIntersects($tokenSet, $productTokens)
|
||||
&& $this->tokenSetIntersects($tokenSet, $shopTokens);
|
||||
}
|
||||
|
||||
private function isWeakProductListFollowUpShopQuery(string $shopSearchQuery): bool
|
||||
{
|
||||
if ($this->referenceAnchorExtractor->extractFirstProductModelAnchor($shopSearchQuery) !== '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$tokens = $this->tokenizeShopQueryCandidate($shopSearchQuery);
|
||||
if ($tokens === []) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$noiseTokens = $this->buildShopQueryTokenSet(
|
||||
$this->agentRunnerConfig->getShopQueryProductListFollowUpNoiseTerms()
|
||||
);
|
||||
|
||||
if ($noiseTokens === []) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$residualTokens = [];
|
||||
foreach ($tokens as $token) {
|
||||
if (isset($noiseTokens[$token])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$residualTokens[$token] = $token;
|
||||
}
|
||||
|
||||
if (count($residualTokens) === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (count($tokens) > max(1, $this->agentRunnerConfig->getShopQueryProductListFollowUpWeakQueryMaxTerms())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return count($residualTokens) <= max(0, $this->agentRunnerConfig->getShopQueryProductListFollowUpWeakQueryMaxResidualTerms());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private function extractLatestHistoryProductListAnchors(string $commerceHistoryContext): array
|
||||
{
|
||||
$maxAnchors = max(1, $this->agentRunnerConfig->getShopQueryProductListFollowUpMaxAnchors());
|
||||
|
||||
foreach ($this->extractHistoryTurnsNewestFirst($commerceHistoryContext) as $turn) {
|
||||
$answer = preg_replace($this->agentRunnerConfig->getFollowUpHistoryQuestionStripPattern(), '', $turn, 1) ?? $turn;
|
||||
$anchors = $this->extractProductListAnchorsFromText($answer, $maxAnchors);
|
||||
|
||||
if ($anchors !== []) {
|
||||
return $anchors;
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $knowledgeChunks
|
||||
* @return string[]
|
||||
*/
|
||||
private function extractProductListAnchorsFromKnowledgeChunks(array $knowledgeChunks): array
|
||||
{
|
||||
if ($knowledgeChunks === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$maxAnchors = max(1, $this->agentRunnerConfig->getShopQueryProductListFollowUpMaxAnchors());
|
||||
$text = trim(implode("\n\n", array_map('strval', $knowledgeChunks)));
|
||||
|
||||
if ($text === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $this->extractProductListAnchorsFromText($text, $maxAnchors);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private function extractProductListAnchorsFromText(string $text, int $maxAnchors): array
|
||||
{
|
||||
$anchors = [];
|
||||
$seen = [];
|
||||
|
||||
foreach ($this->agentRunnerConfig->getShopQueryProductListFollowUpAnchorPatterns() as $pattern) {
|
||||
if (@preg_match_all($pattern, $text, $matches, PREG_SET_ORDER) === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($matches as $match) {
|
||||
$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 = $this->normalizeShopQueryAnchor($candidate);
|
||||
$candidate = $this->canonicalizeProductListAnchor($candidate);
|
||||
if ($candidate === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$key = implode(' ', $this->tokenizeShopQueryCandidate($candidate));
|
||||
if ($key === '' || isset($seen[$key])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$seen[$key] = true;
|
||||
$anchors[] = $candidate;
|
||||
|
||||
if (count($anchors) >= $maxAnchors) {
|
||||
return $anchors;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $anchors;
|
||||
}
|
||||
|
||||
private function canonicalizeProductListAnchor(string $anchor): string
|
||||
{
|
||||
$tokens = $this->tokenizeShopQueryCandidate($anchor);
|
||||
if ($tokens === []) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (($tokens[0] ?? '') !== 'testomat') {
|
||||
return trim((string) preg_replace('/\s+/u', ' ', $anchor));
|
||||
}
|
||||
|
||||
if (!isset($tokens[1])) {
|
||||
return 'testomat';
|
||||
}
|
||||
|
||||
$canonical = ['testomat', $tokens[1]];
|
||||
$variantTerms = $this->buildShopQueryTokenSet(
|
||||
$this->agentRunnerConfig->getShopQueryPositiveTokenFilterAdjacentVariantTerms()
|
||||
);
|
||||
|
||||
for ($i = 2, $count = count($tokens); $i < $count; $i++) {
|
||||
$token = $tokens[$i];
|
||||
|
||||
if (isset($variantTerms[$token]) || preg_match('/\d/u', $token) === 1) {
|
||||
$canonical[] = $token;
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return trim(implode(' ', $canonical));
|
||||
}
|
||||
|
||||
private function guardMainDeviceReferentialShopQueryWithHistoryModelAnchor(
|
||||
string $prompt,
|
||||
string $shopSearchQuery,
|
||||
|
||||
@@ -1805,6 +1805,64 @@ final class AgentRunnerConfig
|
||||
return $this->genreString('context_resolution.history_anchor_enrichment.template')
|
||||
?: $this->getRequiredString('shop_runtime.context_resolution.history_anchor_enrichment.template');
|
||||
}
|
||||
|
||||
|
||||
public function isShopQueryProductListFollowUpEnabled(): bool
|
||||
{
|
||||
return $this->genreBool('context_resolution.product_list_followup.enabled') ?? false;
|
||||
}
|
||||
|
||||
public function getShopQueryProductListFollowUpWeakQueryMaxTerms(): int
|
||||
{
|
||||
return $this->genreInt('context_resolution.product_list_followup.weak_query_max_terms') ?? 4;
|
||||
}
|
||||
|
||||
public function getShopQueryProductListFollowUpWeakQueryMaxResidualTerms(): int
|
||||
{
|
||||
return $this->genreInt('context_resolution.product_list_followup.weak_query_max_residual_terms') ?? 0;
|
||||
}
|
||||
|
||||
public function getShopQueryProductListFollowUpMaxAnchors(): int
|
||||
{
|
||||
return $this->genreInt('context_resolution.product_list_followup.max_anchors') ?? 4;
|
||||
}
|
||||
|
||||
public function getShopQueryProductListFollowUpTemplate(): string
|
||||
{
|
||||
return $this->genreString('context_resolution.product_list_followup.template') ?: '{anchors}';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getShopQueryProductListFollowUpProductTerms(): array
|
||||
{
|
||||
return $this->genreStringList('context_resolution.product_list_followup.product_terms');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getShopQueryProductListFollowUpShopTerms(): array
|
||||
{
|
||||
return $this->genreStringList('context_resolution.product_list_followup.shop_terms');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getShopQueryProductListFollowUpNoiseTerms(): array
|
||||
{
|
||||
return $this->genreStringList('context_resolution.product_list_followup.noise_terms');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getShopQueryProductListFollowUpAnchorPatterns(): array
|
||||
{
|
||||
return $this->genreStringList('context_resolution.product_list_followup.anchor_patterns');
|
||||
}
|
||||
public function isShopQueryRagAnchorEnrichmentEnabled(): bool
|
||||
{
|
||||
return $this->getRequiredBool('shop_runtime.context_resolution.rag_anchor_enrichment.enabled');
|
||||
|
||||
@@ -1342,6 +1342,37 @@ final readonly class RetriexEffectiveConfigProvider
|
||||
}
|
||||
}
|
||||
|
||||
$contextResolution = is_array($configurationValues['context_resolution'] ?? null)
|
||||
? $configurationValues['context_resolution']
|
||||
: [];
|
||||
$productListFollowUp = is_array($contextResolution['product_list_followup'] ?? null)
|
||||
? $contextResolution['product_list_followup']
|
||||
: [];
|
||||
if ($productListFollowUp !== []) {
|
||||
if (array_key_exists('enabled', $productListFollowUp) && !is_bool($productListFollowUp['enabled'])) {
|
||||
$errors[] = 'genre.configuration_values.context_resolution.product_list_followup.enabled must be boolean.';
|
||||
}
|
||||
|
||||
foreach ([
|
||||
'weak_query_max_terms',
|
||||
'weak_query_max_residual_terms',
|
||||
'max_anchors',
|
||||
] as $intKey) {
|
||||
if (array_key_exists($intKey, $productListFollowUp) && (($this->asInt($productListFollowUp[$intKey]) ?? -1) < 0)) {
|
||||
$errors[] = sprintf('genre.configuration_values.context_resolution.product_list_followup.%s must be numeric and non-negative.', $intKey);
|
||||
}
|
||||
}
|
||||
|
||||
if (array_key_exists('template', $productListFollowUp) && (!is_string($productListFollowUp['template']) || trim($productListFollowUp['template']) === '')) {
|
||||
$errors[] = 'genre.configuration_values.context_resolution.product_list_followup.template must be a non-empty string.';
|
||||
}
|
||||
|
||||
$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->validateRegexPatternList($productListFollowUp['anchor_patterns'] ?? [], 'genre.configuration_values.context_resolution.product_list_followup.anchor_patterns', $errors);
|
||||
}
|
||||
|
||||
$shopQueryRuntime = is_array($configurationValues['shop_query_runtime'] ?? null)
|
||||
? $configurationValues['shop_query_runtime']
|
||||
: [];
|
||||
|
||||
Reference in New Issue
Block a user