p86f-g
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
# RetrieX Patch p86f - Multi Product Link Lookup
|
||||
|
||||
## Goal
|
||||
|
||||
Fix the remaining weakness after p86e for referential product-list follow-ups such as:
|
||||
|
||||
```text
|
||||
gebe mir links zu den produkten aus dem shop
|
||||
```
|
||||
|
||||
p86e correctly resolves product anchors from the previous answer, but several anchors can still be collapsed into one strict Shopware query. If that combined query returns no hits, p86f retries the already-resolved product anchors separately and merges the results.
|
||||
|
||||
## Scope
|
||||
|
||||
Changed file:
|
||||
|
||||
- `src/Agent/AgentRunner.php`
|
||||
|
||||
No YAML/config changes are required.
|
||||
|
||||
## Behavior
|
||||
|
||||
Before p86f, a follow-up could resolve to one combined query such as:
|
||||
|
||||
```text
|
||||
<product anchor A> <product anchor B> <product anchor C>
|
||||
```
|
||||
|
||||
This may be too strict for Shopware because it looks like one single product identity. p86f keeps the p86e context resolution, but when that search plus normal repair still yields no products, it performs separate safe lookups for the extracted product anchors.
|
||||
|
||||
## Guardrails
|
||||
|
||||
The new repair step is intentionally narrow:
|
||||
|
||||
- only runs for configured referential product-list follow-ups
|
||||
- only runs when the current primary/repair result set is empty
|
||||
- only uses product anchors extracted by the existing product-list-follow-up logic
|
||||
- reuses the existing referenced-product-anchor result guard for every separate lookup
|
||||
- deduplicates merged products by product number, id, URL or name
|
||||
- does not change intent detection
|
||||
- does not change Retrieval, Scoring, Ranking or Shop-Matching
|
||||
- does not add hard-coded product names or domain-specific product lists
|
||||
|
||||
## Regression notes
|
||||
|
||||
The following flows should remain unaffected because the new method is gated behind `isReferentialProductListShopFollowUpPrompt()` and empty shop results:
|
||||
|
||||
- direct technical/RAG questions
|
||||
- direct product searches
|
||||
- p84 model acronym preservation, e.g. `Testomat LAB CL`
|
||||
- p85b generic device exact-parameter anchor, e.g. device plus a configured measurement parameter
|
||||
- accessory/indicator price follow-ups
|
||||
- `Nur Zubehör anzeigen` / `Nur Geräte anzeigen` action flows unless they are explicitly product-list + shop-link/price follow-ups and have empty results
|
||||
|
||||
## Local checks
|
||||
|
||||
Executed locally in the ZIP workspace:
|
||||
|
||||
```text
|
||||
php -l src/Agent/AgentRunner.php
|
||||
find src -name '*.php' -print0 | xargs -0 -n1 php -l
|
||||
YAML files readable smoke
|
||||
p86f smoke OK
|
||||
```
|
||||
|
||||
Symfony console checks were not executable in the ZIP workspace because `vendor/` is not included. Please run in the target environment:
|
||||
|
||||
```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,106 @@
|
||||
# RetrieX Patch p86g - Product List Follow-up Identity Link Guard
|
||||
|
||||
## Ziel
|
||||
|
||||
Referenzielle Shop-Follow-ups wie:
|
||||
|
||||
```text
|
||||
gebe mir links zu den produkten aus dem shop
|
||||
```
|
||||
|
||||
duerfen nach p86e/p86f zwar Produktanker aus dem Verlauf nutzen, sollen aber nicht auf Zubehoer-/Indikator-Treffer abdriften, nur weil diese Treffer in Beschreibung, Highlights oder Custom Fields kompatible Geraetenamen erwaehnen.
|
||||
|
||||
Beispielproblem:
|
||||
|
||||
```text
|
||||
Vorherige Antwort nennt:
|
||||
Testomat 2000 CAL, Testomat EVO TH, Testomat 808
|
||||
|
||||
Follow-up:
|
||||
gebe mir links zu den produkten aus dem shop
|
||||
|
||||
Query:
|
||||
testomat 2000 cal evo th 808
|
||||
|
||||
Problem:
|
||||
Shop liefert Indikatoren/Zubehoer, weil diese in Metadaten zu Testomat passen.
|
||||
```
|
||||
|
||||
## Ansatz
|
||||
|
||||
p86g erweitert den p86f-Repair von "nur bei leerer Trefferliste" auf "Trefferliste ist nicht als Produktidentitaet zum Verlaufanker passend".
|
||||
|
||||
Der Fix bleibt generisch:
|
||||
|
||||
- keine Sonderlogik fuer medizinische Geraete
|
||||
- keine feste Produktliste im PHP-Core
|
||||
- kein pauschales `geraet => testomat`
|
||||
- kein Eingriff in Intent, Retrieval, Scoring, Ranking oder Shop-Matching
|
||||
- greift nur bei referenziellen Produktlisten-/Shoplink-Follow-ups
|
||||
|
||||
## Technische Umsetzung
|
||||
|
||||
Geaendert:
|
||||
|
||||
- `src/Agent/AgentRunner.php`
|
||||
|
||||
Neue/angepasste Logik:
|
||||
|
||||
1. `repairProductListFollowUpShopResultsWithAnchorLookups()` ersetzt die bisherige reine Empty-Repair-Variante.
|
||||
2. Bei referenziellen Produktlisten-Follow-ups werden vorhandene Shop-Treffer zuerst gegen die erkannten Produktanker gefiltert.
|
||||
3. Ein Treffer gilt nur dann als Identitaetsmatch, wenn Produktname oder URL-Slug mit dem Produktanker beginnt.
|
||||
4. Treffer, deren Name/Slug zusaetzliche Zubehoer-Rollenbegriffe enthalten, werden fuer reine Produktidentitaets-Link-Follow-ups nicht als Hauptprodukt akzeptiert.
|
||||
5. Falls die vorhandenen Treffer keine Identitaetsmatches enthalten, werden die Produktanker einzeln gesucht.
|
||||
6. Auch diese Einzel-Lookups behalten nur Identitaetsmatches, nicht Treffer, die den Geraetenamen nur in Beschreibung/Kompatibilitaet nennen.
|
||||
|
||||
## Erwartetes Verhalten
|
||||
|
||||
```text
|
||||
gebe mir links zu den produkten aus dem shop
|
||||
```
|
||||
|
||||
nach einer Antwort mit:
|
||||
|
||||
```text
|
||||
Testomat 2000 CAL
|
||||
Testomat EVO TH
|
||||
Testomat 808
|
||||
```
|
||||
|
||||
soll nicht mehr Indikator-/Zubehoerlisten auf Basis einer kombinierten Query ausgeben, sondern nur Produktidentitaeten behalten oder getrennte Anker-Lookups versuchen:
|
||||
|
||||
```text
|
||||
testomat 2000 cal
|
||||
testomat evo th
|
||||
testomat 808
|
||||
```
|
||||
|
||||
## Regression Guardrails
|
||||
|
||||
Nicht betroffen sein sollen:
|
||||
|
||||
- direkte Shop-Suchen
|
||||
- reine RAG-Fragen
|
||||
- p84 LAB-CL-Kuerzel-Erhalt
|
||||
- p85b Silikat/SIO2-Geräteanker
|
||||
- Indikator-/Zubehoer-Follow-ups, wenn der Nutzer explizit Zubehoer/Indikator/Reagenz anfragt
|
||||
- bestehende Shop-Matching-/Ranking-Logik
|
||||
|
||||
## Lokale Checks
|
||||
|
||||
Ausgefuehrt im Patch-Arbeitsverzeichnis:
|
||||
|
||||
```text
|
||||
find src -name '*.php' -print0 | xargs -0 -n1 php -l
|
||||
YAML parse OK
|
||||
p86g identity smoke OK
|
||||
```
|
||||
|
||||
Nicht lokal ausgefuehrt, weil `vendor/` im ZIP nicht enthalten ist:
|
||||
|
||||
```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
|
||||
```
|
||||
@@ -559,6 +559,16 @@ final readonly class AgentRunner
|
||||
primaryShopResults: $primaryShopResults,
|
||||
knowledgeChunks: $knowledgeChunks
|
||||
);
|
||||
|
||||
$repairPayload = $this->repairProductListFollowUpShopResultsWithAnchorLookups(
|
||||
prompt: $prompt,
|
||||
userId: $userId,
|
||||
commerceIntent: $commerceIntent,
|
||||
commerceHistoryContext: $shopQueryHistoryContext,
|
||||
shopSearchQuery: $shopSearchQuery,
|
||||
repairPayload: $repairPayload,
|
||||
knowledgeChunks: $knowledgeChunks
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1524,6 +1534,403 @@ final readonly class AgentRunner
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep referential product-list follow-ups aligned with the concrete product
|
||||
* identities mentioned in the previous context. A combined query containing
|
||||
* several product anchors can be too strict for Shopware or can return
|
||||
* accessories whose descriptions merely mention a requested device. In that
|
||||
* case retry each resolved product anchor separately and keep only identity
|
||||
* matches (product name / URL), not accessory compatibility matches.
|
||||
*
|
||||
* @param array{results: ?array, attemptedRepair: bool, usedRepair: bool, repairQueries: string[]} $repairPayload
|
||||
* @param string[] $knowledgeChunks
|
||||
* @return array{results: ?array, attemptedRepair: bool, usedRepair: bool, repairQueries: string[]}
|
||||
*/
|
||||
private function repairProductListFollowUpShopResultsWithAnchorLookups(
|
||||
string $prompt,
|
||||
string $userId,
|
||||
string $commerceIntent,
|
||||
string $commerceHistoryContext,
|
||||
string $shopSearchQuery,
|
||||
array $repairPayload,
|
||||
array $knowledgeChunks
|
||||
): array {
|
||||
$currentResults = $repairPayload['results'] ?? [];
|
||||
if (!is_array($currentResults)) {
|
||||
$currentResults = [];
|
||||
}
|
||||
|
||||
if (!$this->isReferentialProductListShopFollowUpPrompt($prompt)) {
|
||||
return $repairPayload;
|
||||
}
|
||||
|
||||
$anchors = $this->extractProductListFollowUpAnchorsForLookup(
|
||||
commerceHistoryContext: $commerceHistoryContext,
|
||||
userId: $userId,
|
||||
knowledgeChunks: $knowledgeChunks
|
||||
);
|
||||
if ($anchors === []) {
|
||||
return $repairPayload;
|
||||
}
|
||||
|
||||
if ($currentResults !== []) {
|
||||
$identityResults = $this->filterProductListFollowUpResultsByAnchorIdentity($currentResults, $anchors);
|
||||
if ($identityResults !== []) {
|
||||
if (count($identityResults) !== count($currentResults)) {
|
||||
$this->agentLogger->info('Filtered product-list follow-up shop results to referenced product identities', [
|
||||
'userId' => $userId,
|
||||
'commerceIntent' => $commerceIntent,
|
||||
'prompt' => $prompt,
|
||||
'shopSearchQuery' => $shopSearchQuery,
|
||||
'anchors' => $anchors,
|
||||
'originalResultCount' => count($currentResults),
|
||||
'filteredResultCount' => count($identityResults),
|
||||
]);
|
||||
|
||||
return [
|
||||
'results' => $identityResults,
|
||||
'attemptedRepair' => true,
|
||||
'usedRepair' => true,
|
||||
'repairQueries' => $repairPayload['repairQueries'] ?? [],
|
||||
];
|
||||
}
|
||||
|
||||
return $repairPayload;
|
||||
}
|
||||
}
|
||||
|
||||
$queries = $this->buildProductListFollowUpAnchorLookupQueries($anchors, $shopSearchQuery);
|
||||
if ($queries === []) {
|
||||
return $currentResults === [] ? $repairPayload : [
|
||||
'results' => [],
|
||||
'attemptedRepair' => true,
|
||||
'usedRepair' => false,
|
||||
'repairQueries' => $repairPayload['repairQueries'] ?? [],
|
||||
];
|
||||
}
|
||||
|
||||
$mergedResults = [];
|
||||
$seenProducts = [];
|
||||
$usedQueries = [];
|
||||
|
||||
foreach ($queries as $query) {
|
||||
$queryResults = $this->searchShop(
|
||||
$query,
|
||||
$commerceIntent,
|
||||
$userId,
|
||||
$commerceHistoryContext
|
||||
);
|
||||
|
||||
if ($this->shopSearchService->hadLastSearchSystemFailure()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$queryResults = $this->filterProductListFollowUpResultsByAnchorIdentity($queryResults, [$query]);
|
||||
if ($queryResults === []) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$usedQueries[] = $query;
|
||||
|
||||
foreach ($queryResults as $product) {
|
||||
if (!$product instanceof ShopProductResult) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$key = $this->buildShopProductDedupeKey($product);
|
||||
if (isset($seenProducts[$key])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$seenProducts[$key] = true;
|
||||
$mergedResults[] = $product;
|
||||
}
|
||||
}
|
||||
|
||||
if ($mergedResults === []) {
|
||||
return [
|
||||
'results' => [],
|
||||
'attemptedRepair' => true,
|
||||
'usedRepair' => false,
|
||||
'repairQueries' => array_values(array_unique(array_merge(
|
||||
$repairPayload['repairQueries'] ?? [],
|
||||
$queries
|
||||
))),
|
||||
];
|
||||
}
|
||||
|
||||
$this->agentLogger->info('Repaired product-list follow-up shop search with separate product identity anchor lookups', [
|
||||
'userId' => $userId,
|
||||
'commerceIntent' => $commerceIntent,
|
||||
'prompt' => $prompt,
|
||||
'shopSearchQuery' => $shopSearchQuery,
|
||||
'anchorLookupQueries' => $usedQueries,
|
||||
'resultCount' => count($mergedResults),
|
||||
]);
|
||||
|
||||
return [
|
||||
'results' => $mergedResults,
|
||||
'attemptedRepair' => true,
|
||||
'usedRepair' => true,
|
||||
'repairQueries' => array_values(array_unique(array_merge(
|
||||
$repairPayload['repairQueries'] ?? [],
|
||||
$usedQueries
|
||||
))),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $knowledgeChunks
|
||||
* @return string[]
|
||||
*/
|
||||
private function extractProductListFollowUpAnchorsForLookup(
|
||||
string $commerceHistoryContext,
|
||||
string $userId,
|
||||
array $knowledgeChunks
|
||||
): array {
|
||||
$anchors = [];
|
||||
foreach ($this->buildProductListFollowUpAnchorContextCandidates($commerceHistoryContext, $userId) as $contextCandidate) {
|
||||
$anchors = $this->extractLatestHistoryProductListAnchors($contextCandidate);
|
||||
if ($anchors !== []) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($anchors === []) {
|
||||
$anchors = $this->extractProductListAnchorsFromKnowledgeChunks($knowledgeChunks);
|
||||
}
|
||||
|
||||
return $this->normalizeProductListFollowUpAnchors($anchors);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $anchors
|
||||
* @return string[]
|
||||
*/
|
||||
private function buildProductListFollowUpAnchorLookupQueries(array $anchors, string $shopSearchQuery): array
|
||||
{
|
||||
$queries = [];
|
||||
$combinedTokens = array_fill_keys($this->tokenizeShopQueryCandidate($shopSearchQuery), true);
|
||||
|
||||
foreach ($this->normalizeProductListFollowUpAnchors($anchors) as $anchor) {
|
||||
$tokens = $this->tokenizeShopQueryCandidate($anchor);
|
||||
if ($tokens === []) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
$queries[] = $anchor;
|
||||
}
|
||||
|
||||
return array_values(array_unique($queries));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $anchors
|
||||
* @return string[]
|
||||
*/
|
||||
private function normalizeProductListFollowUpAnchors(array $anchors): array
|
||||
{
|
||||
$maxAnchors = max(1, $this->agentRunnerConfig->getShopQueryProductListFollowUpMaxAnchors());
|
||||
$normalized = [];
|
||||
$seen = [];
|
||||
|
||||
foreach ($anchors as $anchor) {
|
||||
$anchor = $this->canonicalizeProductListAnchor($this->normalizeShopQueryAnchor((string) $anchor));
|
||||
if ($anchor === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$key = implode(' ', $this->tokenizeShopQueryCandidate($anchor));
|
||||
if ($key === '' || isset($seen[$key])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$seen[$key] = true;
|
||||
$normalized[] = $anchor;
|
||||
|
||||
if (count($normalized) >= $maxAnchors) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ShopProductResult[] $shopResults
|
||||
* @param string[] $anchors
|
||||
* @return ShopProductResult[]
|
||||
*/
|
||||
private function filterProductListFollowUpResultsByAnchorIdentity(array $shopResults, array $anchors): array
|
||||
{
|
||||
$anchors = $this->normalizeProductListFollowUpAnchors($anchors);
|
||||
if ($shopResults === [] || $anchors === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$filtered = [];
|
||||
$seenProducts = [];
|
||||
|
||||
foreach ($shopResults as $product) {
|
||||
if (!$product instanceof ShopProductResult) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($anchors as $anchor) {
|
||||
if (!$this->shopProductIdentityMatchesProductListAnchor($product, $anchor)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$key = $this->buildShopProductDedupeKey($product);
|
||||
if (!isset($seenProducts[$key])) {
|
||||
$seenProducts[$key] = true;
|
||||
$filtered[] = $product;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $filtered;
|
||||
}
|
||||
|
||||
private function shopProductIdentityMatchesProductListAnchor(ShopProductResult $product, string $anchor): bool
|
||||
{
|
||||
$anchorTokens = $this->tokenizeShopQueryCandidate($anchor);
|
||||
if ($anchorTokens === []) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$nameTokens = $this->tokenizeShopQueryCandidate($product->name);
|
||||
if (
|
||||
($this->tokenSequenceStartsWith($nameTokens, $anchorTokens)
|
||||
|| $this->tokenSequenceStartsWithAfterQuantityPrefix($nameTokens, $anchorTokens))
|
||||
&& !$this->productListIdentityContainsAccessoryRoleOutsideAnchor($product->name, $anchor)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$urlPath = is_string($product->url) ? (string) (parse_url($product->url, PHP_URL_PATH) ?? '') : '';
|
||||
$urlSegments = array_values(array_filter(
|
||||
explode('/', trim($urlPath, '/')),
|
||||
static fn(string $segment): bool => $segment !== ''
|
||||
));
|
||||
$slug = $urlSegments !== [] ? (string) $urlSegments[0] : '';
|
||||
$slugTokens = $slug !== '' ? $this->tokenizeShopQueryCandidate($slug) : [];
|
||||
|
||||
if (
|
||||
($this->tokenSequenceStartsWith($slugTokens, $anchorTokens)
|
||||
|| $this->tokenSequenceStartsWithAfterQuantityPrefix($slugTokens, $anchorTokens))
|
||||
&& !$this->productListIdentityContainsAccessoryRoleOutsideAnchor($slug, $anchor)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function productListIdentityContainsAccessoryRoleOutsideAnchor(string $identityText, string $anchor): bool
|
||||
{
|
||||
$identityTokens = array_fill_keys($this->tokenizeShopQueryCandidate($identityText), true);
|
||||
if ($identityTokens === []) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$anchorTokens = array_fill_keys($this->tokenizeShopQueryCandidate($anchor), true);
|
||||
$accessoryTokens = $this->buildShopQueryTokenSet(
|
||||
$this->agentRunnerConfig->getNoLlmAccessoryProductRoleKeywords()
|
||||
);
|
||||
|
||||
foreach (array_keys($accessoryTokens) as $accessoryToken) {
|
||||
if (isset($anchorTokens[$accessoryToken])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isset($identityTokens[$accessoryToken])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $tokens
|
||||
* @param string[] $prefix
|
||||
*/
|
||||
private function tokenSequenceStartsWith(array $tokens, array $prefix): bool
|
||||
{
|
||||
if ($tokens === [] || $prefix === [] || count($tokens) < count($prefix)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($prefix as $index => $token) {
|
||||
if (($tokens[$index] ?? null) !== $token) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $tokens
|
||||
* @param string[] $prefix
|
||||
*/
|
||||
private function tokenSequenceStartsWithAfterQuantityPrefix(array $tokens, array $prefix): bool
|
||||
{
|
||||
if (count($tokens) < count($prefix) + 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$firstToken = (string) ($tokens[0] ?? '');
|
||||
if (preg_match('/^\d+x$/iu', $firstToken) === 1) {
|
||||
return $this->tokenSequenceStartsWith(array_slice($tokens, 1), $prefix);
|
||||
}
|
||||
|
||||
if (preg_match('/^\d+$/u', $firstToken) === 1 && (($tokens[1] ?? null) === 'x')) {
|
||||
return $this->tokenSequenceStartsWith(array_slice($tokens, 2), $prefix);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function buildShopProductDedupeKey(ShopProductResult $product): string
|
||||
{
|
||||
$productNumber = trim((string) $product->productNumber);
|
||||
if ($productNumber !== '') {
|
||||
return 'number:' . mb_strtolower($productNumber, 'UTF-8');
|
||||
}
|
||||
|
||||
$id = trim($product->id);
|
||||
if ($id !== '') {
|
||||
return 'id:' . mb_strtolower($id, 'UTF-8');
|
||||
}
|
||||
|
||||
$url = trim((string) $product->url);
|
||||
if ($url !== '') {
|
||||
return 'url:' . mb_strtolower($url, 'UTF-8');
|
||||
}
|
||||
|
||||
return 'name:' . mb_strtolower(trim($product->name), 'UTF-8');
|
||||
}
|
||||
|
||||
private function resolveShopQueryHistoryContext(string $prompt, string $commerceHistoryContext): string
|
||||
{
|
||||
$commerceHistoryContext = trim($commerceHistoryContext);
|
||||
|
||||
Reference in New Issue
Block a user