fix p52
This commit is contained in:
@@ -513,6 +513,33 @@ parameters:
|
|||||||
terms:
|
terms:
|
||||||
- puffer
|
- puffer
|
||||||
- kalibrierpuffer
|
- kalibrierpuffer
|
||||||
|
primary_identity_repair:
|
||||||
|
enabled: true
|
||||||
|
min_query_tokens_after_cleanup: 2
|
||||||
|
# Only used for a retry query when the direct-result guard would
|
||||||
|
# otherwise suppress all shop results. Keep product words and context
|
||||||
|
# such as brand/pH/Redox, but remove target-device wording that can
|
||||||
|
# push Shopware ranking toward devices instead of the requested
|
||||||
|
# accessory/consumable.
|
||||||
|
stop_terms:
|
||||||
|
- messgerät
|
||||||
|
- messgeraet
|
||||||
|
- messgeräte
|
||||||
|
- messgeraete
|
||||||
|
- messgeräten
|
||||||
|
- messgeraeten
|
||||||
|
- gerät
|
||||||
|
- geraet
|
||||||
|
- geräte
|
||||||
|
- geraete
|
||||||
|
- geräten
|
||||||
|
- geraeten
|
||||||
|
- handmessgerät
|
||||||
|
- handmessgeraet
|
||||||
|
- handmessgeräte
|
||||||
|
- handmessgeraete
|
||||||
|
- messkoffer
|
||||||
|
- koffer
|
||||||
|
|
||||||
length_sort:
|
length_sort:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
# RetrieX Patch p52 - Direct Product Primary Identity Repair
|
||||||
|
|
||||||
|
## Ziel
|
||||||
|
|
||||||
|
Korrigiert die p51-Regression bei direkten Shop-Produktlisten, bei der echte Store-API-Treffer intern auf `Shop-Treffer: 0` fallen konnten.
|
||||||
|
|
||||||
|
Betroffener Fall:
|
||||||
|
|
||||||
|
```text
|
||||||
|
welche neomeris puffer gibt es für Kalibrierung von pH-Messgeräten
|
||||||
|
```
|
||||||
|
|
||||||
|
Die angezeigte Shopquery bleibt korrekt und soll nicht wieder mit Geräte-/Darstellungslogik vermischt werden:
|
||||||
|
|
||||||
|
```text
|
||||||
|
neomeris puffer kalibrierung ph messgeräten
|
||||||
|
```
|
||||||
|
|
||||||
|
Wenn die Store API mit dieser Suche Treffer liefert, darf RetrieX diese nicht intern durch den direkten Produkt-Guard verlieren.
|
||||||
|
|
||||||
|
## Ursache
|
||||||
|
|
||||||
|
p51 hat den Direct-Product-Guard bewusst verschärft: Bei direkten Produktlisten sollten nur noch Produkte ausgegeben werden, deren primäre Identität, also Produktname/URL, zur angefragten Produktart passt. Das verhindert Geräte/Koffer als Ersatztreffer für Verbrauchsmaterialien.
|
||||||
|
|
||||||
|
Der Guard war aber zu hart, wenn die vorliegende Shop-Trefferliste bereits durch Shopware-Ranking/Repair/Guardrails in Richtung Geräte verschoben war. Dann konnte der Guard alle Treffer entfernen und der Status fiel auf:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Shop-Treffer: 0
|
||||||
|
```
|
||||||
|
|
||||||
|
obwohl die Store API für die Suchquery echte Produktkandidaten liefert.
|
||||||
|
|
||||||
|
## Änderung
|
||||||
|
|
||||||
|
p52 ergänzt einen kleinen, defensiven Retry nur für diesen Fall:
|
||||||
|
|
||||||
|
1. Die normale Shopquery bleibt unverändert.
|
||||||
|
2. Der p51-Primary-Identity-Guard läuft weiterhin.
|
||||||
|
3. Nur wenn der Guard aus einer nicht-leeren Shopliste **alles** entfernt, wird eine bereinigte Repair-Query gebaut.
|
||||||
|
4. Diese Repair-Query entfernt nur konfigurierte Zielgeräte-Wörter wie `Messgerät`, `Gerät`, `Koffer` und behält Produktart/Brand/Kontext wie `neomeris`, `puffer`, `ph`.
|
||||||
|
5. Die Repair-Ergebnisse laufen erneut durch denselben Primary-Identity-Guard.
|
||||||
|
6. Wenn danach weiterhin nichts passt, bleibt die saubere Keine-Treffer-Antwort erhalten; es werden keine Geräte als Ersatz ausgegeben.
|
||||||
|
|
||||||
|
Beispiel Repair-Query:
|
||||||
|
|
||||||
|
```text
|
||||||
|
neomeris puffer kalibrierung ph
|
||||||
|
```
|
||||||
|
|
||||||
|
statt die Anfrage als Geräte-/Koffer-Liste zu behandeln.
|
||||||
|
|
||||||
|
## Wichtig
|
||||||
|
|
||||||
|
- Keine Änderung an der angezeigten primären Shopquery.
|
||||||
|
- Keine Änderung an Shopware-Kriterien.
|
||||||
|
- Kein Neomeris-/pH-/Redox-Sonderfall im PHP-Core.
|
||||||
|
- Die entfernten Zielgeräte-Wörter sind YAML-konfiguriert unter `shop_prompt.direct_result_guard.primary_identity_repair.stop_terms`.
|
||||||
|
- p51 bleibt erhalten: Geräte/Koffer werden nicht als Pufferliste ausgegeben.
|
||||||
|
- p48, p45/p46 und der Artikelnummer-Fix p47 bleiben unverändert.
|
||||||
|
|
||||||
|
## Geänderte Dateien
|
||||||
|
|
||||||
|
```text
|
||||||
|
config/retriex/agent.yaml
|
||||||
|
src/Agent/AgentRunner.php
|
||||||
|
src/Config/AgentRunnerConfig.php
|
||||||
|
patch_history/RETRIEX_PATCH_52_DIRECT_PRODUCT_PRIMARY_IDENTITY_REPAIR_README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Erwartetes Verhalten
|
||||||
|
|
||||||
|
```text
|
||||||
|
welche neomeris puffer gibt es für Kalibrierung von pH-Messgeräten
|
||||||
|
```
|
||||||
|
|
||||||
|
soll wieder echte Pufferprodukte ausgeben, sofern sie von der Store API geliefert werden, zum Beispiel:
|
||||||
|
|
||||||
|
```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
|
||||||
|
```
|
||||||
|
|
||||||
|
Es sollen nicht mehr nur Geräte/Koffer erscheinen, und es soll nicht fälschlich `Shop-Treffer: 0` entstehen, wenn die Store API passende Pufferprodukte liefert.
|
||||||
|
|
||||||
|
## 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
|
||||||
|
```
|
||||||
@@ -36,7 +36,7 @@ a {
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
li {
|
li {
|
||||||
margin-bottom: .5rem;
|
margin-bottom: .125rem;
|
||||||
}
|
}
|
||||||
a:hover {
|
a:hover {
|
||||||
color: #FFF;
|
color: #FFF;
|
||||||
|
|||||||
@@ -486,12 +486,29 @@ final readonly class AgentRunner
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$shopResults = $repairPayload['results'];
|
$unguardedShopResults = $repairPayload['results'];
|
||||||
$shopResults = $this->guardDirectProductShopResults($prompt, $shopSearchQuery, $shopResults);
|
$shopResults = $this->guardDirectProductShopResults($prompt, $shopSearchQuery, $unguardedShopResults);
|
||||||
|
|
||||||
|
$directIdentityRepairPayload = $this->repairEmptyDirectProductPrimaryIdentityResults(
|
||||||
|
prompt: $prompt,
|
||||||
|
userId: $userId,
|
||||||
|
commerceIntent: $commerceIntent,
|
||||||
|
shopSearchQuery: $shopSearchQuery,
|
||||||
|
unguardedShopResults: $unguardedShopResults,
|
||||||
|
guardedShopResults: $shopResults
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($directIdentityRepairPayload['results'] !== null) {
|
||||||
|
$shopResults = $directIdentityRepairPayload['results'];
|
||||||
|
}
|
||||||
|
|
||||||
$shopResults = $this->sortShopResultsForLengthRequest($prompt, $shopSearchQuery, $shopResults);
|
$shopResults = $this->sortShopResultsForLengthRequest($prompt, $shopSearchQuery, $shopResults);
|
||||||
$attemptedShopRepair = $repairPayload['attemptedRepair'];
|
$attemptedShopRepair = $repairPayload['attemptedRepair'] || $directIdentityRepairPayload['attemptedRepair'];
|
||||||
$usedShopRepair = $repairPayload['usedRepair'];
|
$usedShopRepair = $repairPayload['usedRepair'] || $directIdentityRepairPayload['usedRepair'];
|
||||||
$shopRepairQueries = $repairPayload['repairQueries'];
|
$shopRepairQueries = array_values(array_unique(array_merge(
|
||||||
|
$repairPayload['repairQueries'],
|
||||||
|
$directIdentityRepairPayload['repairQueries']
|
||||||
|
)));
|
||||||
|
|
||||||
if (!$primaryShopSearchHadSystemFailure) {
|
if (!$primaryShopSearchHadSystemFailure) {
|
||||||
yield $this->systemMsg(
|
yield $this->systemMsg(
|
||||||
@@ -3081,6 +3098,157 @@ final readonly class AgentRunner
|
|||||||
return array_values(array_merge($primaryMatches, $corpusMatches));
|
return array_values(array_merge($primaryMatches, $corpusMatches));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ShopProductResult[] $unguardedShopResults
|
||||||
|
* @param ShopProductResult[] $guardedShopResults
|
||||||
|
* @return array{results: array|null, attemptedRepair: bool, usedRepair: bool, repairQueries: string[]}
|
||||||
|
*/
|
||||||
|
private function repairEmptyDirectProductPrimaryIdentityResults(
|
||||||
|
string $prompt,
|
||||||
|
string $userId,
|
||||||
|
string $commerceIntent,
|
||||||
|
string $shopSearchQuery,
|
||||||
|
array $unguardedShopResults,
|
||||||
|
array $guardedShopResults
|
||||||
|
): array {
|
||||||
|
$emptyResult = [
|
||||||
|
'results' => null,
|
||||||
|
'attemptedRepair' => false,
|
||||||
|
'usedRepair' => false,
|
||||||
|
'repairQueries' => [],
|
||||||
|
];
|
||||||
|
|
||||||
|
if (
|
||||||
|
$guardedShopResults !== []
|
||||||
|
|| $unguardedShopResults === []
|
||||||
|
|| !$this->agentRunnerConfig->isDirectShopResultGuardPrimaryIdentityRepairEnabled()
|
||||||
|
) {
|
||||||
|
return $emptyResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
$requestedTerms = $this->extractRequestedDirectProductTerms($prompt, $shopSearchQuery);
|
||||||
|
if ($requestedTerms === []) {
|
||||||
|
return $emptyResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
$repairQuery = $this->buildDirectProductPrimaryIdentityRepairQuery(
|
||||||
|
shopSearchQuery: $shopSearchQuery,
|
||||||
|
requestedTerms: $requestedTerms
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($repairQuery === '' || $this->normalizeShopQueryForComparison($repairQuery) === $this->normalizeShopQueryForComparison($shopSearchQuery)) {
|
||||||
|
return $emptyResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->agentLogger->info('Direct product primary identity guard retrying with cleaned repair query', [
|
||||||
|
'userId' => $userId,
|
||||||
|
'commerceIntent' => $commerceIntent,
|
||||||
|
'prompt' => $prompt,
|
||||||
|
'shopSearchQuery' => $shopSearchQuery,
|
||||||
|
'repairQuery' => $repairQuery,
|
||||||
|
'unguardedShopResultsCount' => count($unguardedShopResults),
|
||||||
|
'requestedTerms' => $requestedTerms,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$repairResults = $this->searchShop(
|
||||||
|
$repairQuery,
|
||||||
|
$commerceIntent,
|
||||||
|
$userId,
|
||||||
|
''
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($this->shopSearchService->hadLastSearchSystemFailure()) {
|
||||||
|
return [
|
||||||
|
'results' => null,
|
||||||
|
'attemptedRepair' => true,
|
||||||
|
'usedRepair' => false,
|
||||||
|
'repairQueries' => [$repairQuery],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$guardedRepairResults = $this->guardDirectProductShopResults($prompt, $repairQuery, $repairResults);
|
||||||
|
|
||||||
|
if ($guardedRepairResults === []) {
|
||||||
|
$this->agentLogger->info('Direct product primary identity repair finished without matching products', [
|
||||||
|
'userId' => $userId,
|
||||||
|
'commerceIntent' => $commerceIntent,
|
||||||
|
'prompt' => $prompt,
|
||||||
|
'shopSearchQuery' => $shopSearchQuery,
|
||||||
|
'repairQuery' => $repairQuery,
|
||||||
|
'repairResultsCount' => count($repairResults),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'results' => null,
|
||||||
|
'attemptedRepair' => true,
|
||||||
|
'usedRepair' => false,
|
||||||
|
'repairQueries' => [$repairQuery],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'results' => $guardedRepairResults,
|
||||||
|
'attemptedRepair' => true,
|
||||||
|
'usedRepair' => true,
|
||||||
|
'repairQueries' => [$repairQuery],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string[] $requestedTerms
|
||||||
|
*/
|
||||||
|
private function buildDirectProductPrimaryIdentityRepairQuery(string $shopSearchQuery, array $requestedTerms): string
|
||||||
|
{
|
||||||
|
$tokens = $this->tokenizeShopQueryCandidate($shopSearchQuery);
|
||||||
|
if ($tokens === []) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$stopTokens = [];
|
||||||
|
foreach ($this->agentRunnerConfig->getDirectShopResultGuardPrimaryIdentityRepairStopTerms() as $term) {
|
||||||
|
foreach ($this->tokenizeShopQueryCandidate($term) as $token) {
|
||||||
|
$stopTokens[$token] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$requestedTokens = [];
|
||||||
|
foreach ($requestedTerms as $term) {
|
||||||
|
foreach ($this->tokenizeShopQueryCandidate($term) as $token) {
|
||||||
|
$requestedTokens[$token] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$kept = [];
|
||||||
|
foreach ($tokens as $token) {
|
||||||
|
if (isset($stopTokens[$token]) && !isset($requestedTokens[$token])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($kept[$token])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$kept[$token] = $token;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (array_keys($requestedTokens) as $requestedToken) {
|
||||||
|
if (!isset($kept[$requestedToken])) {
|
||||||
|
$kept[$requestedToken] = $requestedToken;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count($kept) < max(1, $this->agentRunnerConfig->getDirectShopResultGuardPrimaryIdentityRepairMinQueryTokens())) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return trim(implode(' ', array_values($kept)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeShopQueryForComparison(string $query): string
|
||||||
|
{
|
||||||
|
return trim(implode(' ', $this->tokenizeShopQueryCandidate($query)));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param ShopProductResult[] $shopResults
|
* @param ShopProductResult[] $shopResults
|
||||||
* @return ShopProductResult[]
|
* @return ShopProductResult[]
|
||||||
|
|||||||
@@ -301,6 +301,21 @@ final class AgentRunnerConfig
|
|||||||
throw new \InvalidArgumentException(sprintf('RetrieX agent config key "%s" must be numeric.', $key));
|
throw new \InvalidArgumentException(sprintf('RetrieX agent config key "%s" must be numeric.', $key));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function getOptionalInt(string $key, int $default): int
|
||||||
|
{
|
||||||
|
$value = $this->optionalValue($key);
|
||||||
|
|
||||||
|
if ($value === null) {
|
||||||
|
return $default;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_numeric($value)) {
|
||||||
|
return (int) $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new \InvalidArgumentException(sprintf('RetrieX agent config key "%s" must be numeric.', $key));
|
||||||
|
}
|
||||||
|
|
||||||
private function getRequiredBool(string $key): bool
|
private function getRequiredBool(string $key): bool
|
||||||
{
|
{
|
||||||
$value = $this->requiredValue($key);
|
$value = $this->requiredValue($key);
|
||||||
@@ -1158,6 +1173,24 @@ final class AgentRunnerConfig
|
|||||||
return $this->getOptionalStringList('shop_prompt.direct_result_guard.compound_prefix_match.terms');
|
return $this->getOptionalStringList('shop_prompt.direct_result_guard.compound_prefix_match.terms');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function isDirectShopResultGuardPrimaryIdentityRepairEnabled(): bool
|
||||||
|
{
|
||||||
|
return $this->getOptionalBool('shop_prompt.direct_result_guard.primary_identity_repair.enabled', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDirectShopResultGuardPrimaryIdentityRepairMinQueryTokens(): int
|
||||||
|
{
|
||||||
|
return $this->getOptionalInt('shop_prompt.direct_result_guard.primary_identity_repair.min_query_tokens_after_cleanup', 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string[]
|
||||||
|
*/
|
||||||
|
public function getDirectShopResultGuardPrimaryIdentityRepairStopTerms(): array
|
||||||
|
{
|
||||||
|
return $this->getOptionalStringList('shop_prompt.direct_result_guard.primary_identity_repair.stop_terms');
|
||||||
|
}
|
||||||
|
|
||||||
public function isShopResultLengthSortEnabled(): bool
|
public function isShopResultLengthSortEnabled(): bool
|
||||||
{
|
{
|
||||||
return $this->getRequiredBool('shop_prompt.length_sort.enabled');
|
return $this->getRequiredBool('shop_prompt.length_sort.enabled');
|
||||||
|
|||||||
Reference in New Issue
Block a user