fix p51
This commit is contained in:
@@ -500,6 +500,11 @@ parameters:
|
||||
|
||||
direct_result_guard:
|
||||
enabled: true
|
||||
# Direct product-list answers should only list products whose primary
|
||||
# identity (name/URL) matches the requested product type. This prevents
|
||||
# devices from being listed as a requested consumable merely because the
|
||||
# description mentions such consumables as accessories.
|
||||
prefer_primary_identity_matches: true
|
||||
compound_prefix_match:
|
||||
enabled: true
|
||||
# Some Shopware product names combine the requested product type with
|
||||
|
||||
@@ -329,12 +329,6 @@ parameters:
|
||||
- lösung
|
||||
- loesung
|
||||
- solution
|
||||
- puffer
|
||||
- pufferlösung
|
||||
- pufferloesung
|
||||
- kalibrierpuffer
|
||||
- kalibrierlösung
|
||||
- kalibrierloesung
|
||||
- teststreifen
|
||||
- test strip
|
||||
- filter
|
||||
@@ -343,6 +337,12 @@ parameters:
|
||||
- service set
|
||||
- serviceset
|
||||
- service-set
|
||||
- puffer
|
||||
- pufferlösung
|
||||
- pufferloesung
|
||||
- kalibrierpuffer
|
||||
- kalibrierlösung
|
||||
- kalibrierloesung
|
||||
device_product:
|
||||
add:
|
||||
- analysegerät
|
||||
@@ -629,6 +629,10 @@ parameters:
|
||||
- sensor
|
||||
- puffer
|
||||
- kalibrierpuffer
|
||||
- pufferlösung
|
||||
- pufferloesung
|
||||
- kalibrierlösung
|
||||
- kalibrierloesung
|
||||
direct_product_attribute_stop_terms:
|
||||
include:
|
||||
- direct_product_attribute_stop_terms
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
# RetrieX Patch p51 - Direct Product Primary Identity Guard
|
||||
|
||||
## Ziel
|
||||
|
||||
Behebt die verbleibende Regression bei direkten Shop-Produktlisten, bei der eine Anfrage nach Pufferlösungen Geräte/Koffer ausgeben konnte.
|
||||
|
||||
Beispiel:
|
||||
|
||||
```text
|
||||
welche neomeris puffer gibt es für Kalibrierung von pH-Messgeräten
|
||||
```
|
||||
|
||||
Die Shopquery ist korrekt und soll unverändert bleiben:
|
||||
|
||||
```text
|
||||
neomeris puffer kalibrierung ph messgeräten
|
||||
```
|
||||
|
||||
Trotzdem wurden zuletzt nur pH-Messgeräte/Messkoffer ausgegeben, obwohl der Shop Pufferlösungen liefert.
|
||||
|
||||
## Ursache
|
||||
|
||||
Der direkte Produkt-Guard aus p45/p49/p50 hat Produktmatches über den gesamten Produkt-Corpus zugelassen:
|
||||
|
||||
- Produktname
|
||||
- Beschreibung
|
||||
- Highlights
|
||||
- Custom Fields
|
||||
- URL
|
||||
|
||||
Dadurch konnten Hauptgeräte oder Koffer als Treffer für `puffer` gelten, wenn ihre Beschreibung oder ihr Zubehörtext Pufferlösungen erwähnt. Für eine direkte Produktlistenfrage nach einer Produktart ist das zu breit: Ein Gerät, das Pufferlösungen im Zubehörtext erwähnt, ist selbst keine Pufferlösung.
|
||||
|
||||
## Änderung
|
||||
|
||||
Für direkte Produktlisten mit erkanntem Produkttyp gilt nun:
|
||||
|
||||
1. Es werden zuerst nur Produkte behalten, deren primäre Identität den angefragten Produkttyp matcht.
|
||||
- primäre Identität = Produktname + Produkt-URL
|
||||
2. Corpus-Matches über Beschreibung/Custom Fields werden nicht mehr als Ersatzliste ausgegeben, wenn der direkte Guard aktiv ist.
|
||||
3. Compound-Matches bleiben erhalten:
|
||||
- `puffer` matcht `pufferlösung`
|
||||
- `kalibrierpuffer` matcht zusammengesetzte Kalibrierpuffer-Begriffe
|
||||
4. Puffer-/Kalibrierbegriffe sind weiterhin YAML-konfigurierbare Zubehör-/Verbrauchsmaterialbegriffe.
|
||||
|
||||
Damit werden Produkte wie diese akzeptiert:
|
||||
|
||||
```text
|
||||
Neomeris pH-Pufferlösung pH 7.00
|
||||
Neomeris Redox-Pufferlösung 475 mV
|
||||
```
|
||||
|
||||
und Produkte wie diese nicht mehr als Pufferliste ausgegeben:
|
||||
|
||||
```text
|
||||
Professional pH/mV/Redox/Temperatur Handmessgerät ...
|
||||
Wassermeister Messkoffer
|
||||
```
|
||||
|
||||
## Wichtig
|
||||
|
||||
- Keine Änderung an der Shopquery.
|
||||
- Keine Änderung an Shopware-Kriterien.
|
||||
- Keine Neomeris-/pH-/Redox-Sonderlogik im PHP-Core.
|
||||
- Keine neuen fachlichen Tokenlisten im Core; die Begriffe bleiben in YAML.
|
||||
- Der Patch enthält die p50-Compound-/Puffer-Konfiguration vollständig, damit er auf dem aktuellen Arbeitsstand sicher anwendbar ist.
|
||||
- Der p48-Referential-Anchor-Fallback-Guard bleibt enthalten, damit referenzielle Shop-Preisfragen nicht regressieren.
|
||||
|
||||
## Geänderte Dateien
|
||||
|
||||
```text
|
||||
config/retriex/agent.yaml
|
||||
config/retriex/vocabulary.yaml
|
||||
src/Agent/AgentRunner.php
|
||||
src/Config/AgentRunnerConfig.php
|
||||
patch_history/RETRIEX_PATCH_51_DIRECT_PRODUCT_PRIMARY_IDENTITY_GUARD_README.md
|
||||
```
|
||||
|
||||
## Erwartetes Verhalten
|
||||
|
||||
```text
|
||||
welche neomeris puffer gibt es für Kalibrierung von pH-Messgeräten
|
||||
```
|
||||
|
||||
soll Pufferlösungen ausgeben, sofern sie in den Shopdaten enthalten sind, z. B.:
|
||||
|
||||
```text
|
||||
Neomeris pH-Pufferlösung pH 4.01
|
||||
Neomeris pH-Pufferlösung pH 7.00
|
||||
Neomeris pH-Pufferlösung pH 9.21
|
||||
Neomeris pH-Pufferlösung pH 10.01
|
||||
Neomeris Redox-Pufferlösung 200 mV / 475 mV / 650 mV
|
||||
```
|
||||
|
||||
Falls die Shopdaten tatsächlich keine primären Pufferprodukte enthalten, soll RetrieX keine Geräte/Koffer als Ersatzprodukte listen, sondern sauber "keine passenden Shopdaten" ausgeben.
|
||||
|
||||
## Lokale Checks
|
||||
|
||||
Ausgeführt:
|
||||
|
||||
```bash
|
||||
php -l src/Agent/AgentRunner.php
|
||||
php -l src/Config/AgentRunnerConfig.php
|
||||
python3 - <<'PY'
|
||||
import yaml
|
||||
from pathlib import Path
|
||||
for f in ['config/retriex/agent.yaml', 'config/retriex/vocabulary.yaml']:
|
||||
yaml.safe_load(Path(f).read_text())
|
||||
print('YAML ok')
|
||||
PY
|
||||
```
|
||||
|
||||
`bin/console` wurde lokal nicht ausgeführt, weil `vendor/` im ZIP nicht enthalten ist.
|
||||
|
||||
## Nach dem Einspielen prüfen
|
||||
|
||||
```bash
|
||||
bin/console cache:clear
|
||||
bin/console mto:agent:config:validate
|
||||
bin/console mto:agent:regression:test
|
||||
bin/console mto:agent:config:audit-source --details
|
||||
bin/console mto:agent:config:audit-patterns --details
|
||||
```
|
||||
@@ -292,6 +292,26 @@ final readonly class AgentRunner
|
||||
$optimizedShopQuery = '';
|
||||
}
|
||||
|
||||
$referentialAnchoredShopSearchQuery = $this->guardReferentialShopQueryFallbackWithHistoryAnchor(
|
||||
prompt: $originalPrompt,
|
||||
shopSearchQuery: $shopSearchQuery,
|
||||
commerceHistoryContext: $shopQueryHistoryContext
|
||||
);
|
||||
|
||||
if ($referentialAnchoredShopSearchQuery !== $shopSearchQuery) {
|
||||
$this->agentLogger->info('Enriched referential shop fallback query with history anchor', [
|
||||
'userId' => $userId,
|
||||
'prompt' => $prompt,
|
||||
'routingPrompt' => $routingPrompt,
|
||||
'optimizedShopQuery' => $optimizedShopQuery,
|
||||
'shopSearchQuery' => $shopSearchQuery,
|
||||
'referentialAnchoredShopSearchQuery' => $referentialAnchoredShopSearchQuery,
|
||||
]);
|
||||
|
||||
$shopSearchQuery = $referentialAnchoredShopSearchQuery;
|
||||
$optimizedShopQuery = '';
|
||||
}
|
||||
|
||||
$ragAnchoredShopSearchQuery = $this->enrichShopSearchQueryWithRagAnchor(
|
||||
prompt: $originalPrompt,
|
||||
shopSearchQuery: $shopSearchQuery,
|
||||
@@ -2581,6 +2601,83 @@ final readonly class AgentRunner
|
||||
return trim($query);
|
||||
}
|
||||
|
||||
private function guardReferentialShopQueryFallbackWithHistoryAnchor(
|
||||
string $prompt,
|
||||
string $shopSearchQuery,
|
||||
string $commerceHistoryContext
|
||||
): string {
|
||||
if (!$this->agentRunnerConfig->isShopQueryContextAnchorEnrichmentEnabled()) {
|
||||
return $shopSearchQuery;
|
||||
}
|
||||
|
||||
if (trim($commerceHistoryContext) === '') {
|
||||
return $shopSearchQuery;
|
||||
}
|
||||
|
||||
if (!$this->shouldUseCommerceHistoryForShopQuery($prompt)) {
|
||||
return $shopSearchQuery;
|
||||
}
|
||||
|
||||
$combined = trim($shopSearchQuery . ' ' . $prompt);
|
||||
if (!$this->containsConfiguredShopQueryAnchorTrigger($combined)) {
|
||||
return $shopSearchQuery;
|
||||
}
|
||||
|
||||
$anchor = $this->normalizeShopQueryAnchor(
|
||||
$this->extractLatestConfiguredShopQueryContextAnchor($commerceHistoryContext)
|
||||
);
|
||||
|
||||
if ($anchor === '' || $this->queryAlreadyContainsAllAnchorTokens($shopSearchQuery, $anchor)) {
|
||||
return $shopSearchQuery;
|
||||
}
|
||||
|
||||
$referentialQuery = $this->extractReferentialShopQueryTriggerTerms($combined);
|
||||
if ($referentialQuery === '') {
|
||||
return $shopSearchQuery;
|
||||
}
|
||||
|
||||
$template = $this->agentRunnerConfig->getShopQueryContextAnchorEnrichmentTemplate();
|
||||
$enriched = $this->renderAgentTemplate($template, [
|
||||
'anchor' => $anchor,
|
||||
'query' => $referentialQuery,
|
||||
]);
|
||||
$enriched = preg_replace('/\s+/u', ' ', $enriched) ?? $enriched;
|
||||
$enriched = trim($enriched);
|
||||
|
||||
return $enriched !== '' ? $enriched : $shopSearchQuery;
|
||||
}
|
||||
|
||||
private function extractReferentialShopQueryTriggerTerms(string $text): string
|
||||
{
|
||||
$tokens = $this->tokenizeShopQueryCandidate($text);
|
||||
|
||||
if ($tokens === []) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$triggerTokens = [];
|
||||
foreach ($this->agentRunnerConfig->getShopQueryContextAnchorEnrichmentTriggerTerms() as $term) {
|
||||
foreach ($this->tokenizeShopQueryCandidate($term) as $termToken) {
|
||||
$triggerTokens[$termToken] = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ($triggerTokens === []) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$out = [];
|
||||
foreach ($tokens as $token) {
|
||||
if (!isset($triggerTokens[$token]) || isset($out[$token])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$out[$token] = $token;
|
||||
}
|
||||
|
||||
return implode(' ', array_values($out));
|
||||
}
|
||||
|
||||
private function enrichReferentialShopQueryFromHistory(
|
||||
string $query,
|
||||
string $sourcePrompt,
|
||||
@@ -2959,18 +3056,29 @@ final readonly class AgentRunner
|
||||
return $shopResults;
|
||||
}
|
||||
|
||||
$filtered = [];
|
||||
$primaryMatches = [];
|
||||
$corpusMatches = [];
|
||||
|
||||
foreach ($shopResults as $product) {
|
||||
if (!$product instanceof ShopProductResult) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($this->shopProductPrimaryIdentityMatchesAnyDirectProductTerm($product, $requestedTerms)) {
|
||||
$primaryMatches[] = $product;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($this->shopProductMatchesAnyDirectProductTerm($product, $requestedTerms)) {
|
||||
$filtered[] = $product;
|
||||
$corpusMatches[] = $product;
|
||||
}
|
||||
}
|
||||
|
||||
return $filtered;
|
||||
if ($this->agentRunnerConfig->shouldPreferDirectShopResultGuardPrimaryIdentityMatches()) {
|
||||
return $primaryMatches;
|
||||
}
|
||||
|
||||
return array_values(array_merge($primaryMatches, $corpusMatches));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -3226,6 +3334,19 @@ final readonly class AgentRunner
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $requestedTerms
|
||||
*/
|
||||
private function shopProductPrimaryIdentityMatchesAnyDirectProductTerm(ShopProductResult $product, array $requestedTerms): bool
|
||||
{
|
||||
$primaryText = trim(implode(' ', array_filter([
|
||||
$product->name,
|
||||
$product->url,
|
||||
])));
|
||||
|
||||
return $this->textMatchesAnyDirectProductTerm($primaryText, $requestedTerms);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $requestedTerms
|
||||
*/
|
||||
@@ -3236,14 +3357,27 @@ final readonly class AgentRunner
|
||||
$product->description,
|
||||
implode(' ', $product->highlights),
|
||||
$product->customFields,
|
||||
$product->url,
|
||||
])));
|
||||
|
||||
return $this->textMatchesAnyDirectProductTerm($productText, $requestedTerms);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $requestedTerms
|
||||
*/
|
||||
private function textMatchesAnyDirectProductTerm(string $text, array $requestedTerms): bool
|
||||
{
|
||||
if (trim($text) === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($requestedTerms as $term) {
|
||||
if ($this->containsAllShopQueryTokens($productText, $term)) {
|
||||
if ($this->containsAllShopQueryTokens($text, $term)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($this->containsAllShopQueryTokensWithCompoundPrefixes($productText, $term)) {
|
||||
if ($this->containsAllShopQueryTokensWithCompoundPrefixes($text, $term)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1140,6 +1140,11 @@ final class AgentRunnerConfig
|
||||
return $this->getRequiredBool('shop_prompt.direct_result_guard.enabled');
|
||||
}
|
||||
|
||||
public function shouldPreferDirectShopResultGuardPrimaryIdentityMatches(): bool
|
||||
{
|
||||
return $this->getOptionalBool('shop_prompt.direct_result_guard.prefer_primary_identity_matches', true);
|
||||
}
|
||||
|
||||
public function isDirectShopResultGuardCompoundPrefixMatchEnabled(): bool
|
||||
{
|
||||
return $this->getOptionalBool('shop_prompt.direct_result_guard.compound_prefix_match.enabled', false);
|
||||
|
||||
Reference in New Issue
Block a user