This commit is contained in:
team 1
2026-05-06 13:51:22 +02:00
parent 1b261c26d7
commit ced1431a35
5 changed files with 281 additions and 11 deletions

View File

@@ -500,6 +500,11 @@ parameters:
direct_result_guard: direct_result_guard:
enabled: true 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: compound_prefix_match:
enabled: true enabled: true
# Some Shopware product names combine the requested product type with # Some Shopware product names combine the requested product type with

View File

@@ -329,12 +329,6 @@ parameters:
- lösung - lösung
- loesung - loesung
- solution - solution
- puffer
- pufferlösung
- pufferloesung
- kalibrierpuffer
- kalibrierlösung
- kalibrierloesung
- teststreifen - teststreifen
- test strip - test strip
- filter - filter
@@ -343,6 +337,12 @@ parameters:
- service set - service set
- serviceset - serviceset
- service-set - service-set
- puffer
- pufferlösung
- pufferloesung
- kalibrierpuffer
- kalibrierlösung
- kalibrierloesung
device_product: device_product:
add: add:
- analysegerät - analysegerät
@@ -629,6 +629,10 @@ parameters:
- sensor - sensor
- puffer - puffer
- kalibrierpuffer - kalibrierpuffer
- pufferlösung
- pufferloesung
- kalibrierlösung
- kalibrierloesung
direct_product_attribute_stop_terms: direct_product_attribute_stop_terms:
include: include:
- direct_product_attribute_stop_terms - direct_product_attribute_stop_terms

View File

@@ -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
```

View File

@@ -292,6 +292,26 @@ final readonly class AgentRunner
$optimizedShopQuery = ''; $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( $ragAnchoredShopSearchQuery = $this->enrichShopSearchQueryWithRagAnchor(
prompt: $originalPrompt, prompt: $originalPrompt,
shopSearchQuery: $shopSearchQuery, shopSearchQuery: $shopSearchQuery,
@@ -2581,6 +2601,83 @@ final readonly class AgentRunner
return trim($query); 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( private function enrichReferentialShopQueryFromHistory(
string $query, string $query,
string $sourcePrompt, string $sourcePrompt,
@@ -2959,18 +3056,29 @@ final readonly class AgentRunner
return $shopResults; return $shopResults;
} }
$filtered = []; $primaryMatches = [];
$corpusMatches = [];
foreach ($shopResults as $product) { foreach ($shopResults as $product) {
if (!$product instanceof ShopProductResult) { if (!$product instanceof ShopProductResult) {
continue; continue;
} }
if ($this->shopProductPrimaryIdentityMatchesAnyDirectProductTerm($product, $requestedTerms)) {
$primaryMatches[] = $product;
continue;
}
if ($this->shopProductMatchesAnyDirectProductTerm($product, $requestedTerms)) { 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; 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 * @param string[] $requestedTerms
*/ */
@@ -3236,14 +3357,27 @@ final readonly class AgentRunner
$product->description, $product->description,
implode(' ', $product->highlights), implode(' ', $product->highlights),
$product->customFields, $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) { foreach ($requestedTerms as $term) {
if ($this->containsAllShopQueryTokens($productText, $term)) { if ($this->containsAllShopQueryTokens($text, $term)) {
return true; return true;
} }
if ($this->containsAllShopQueryTokensWithCompoundPrefixes($productText, $term)) { if ($this->containsAllShopQueryTokensWithCompoundPrefixes($text, $term)) {
return true; return true;
} }
} }

View File

@@ -1140,6 +1140,11 @@ final class AgentRunnerConfig
return $this->getRequiredBool('shop_prompt.direct_result_guard.enabled'); 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 public function isDirectShopResultGuardCompoundPrefixMatchEnabled(): bool
{ {
return $this->getOptionalBool('shop_prompt.direct_result_guard.compound_prefix_match.enabled', false); return $this->getOptionalBool('shop_prompt.direct_result_guard.compound_prefix_match.enabled', false);