This commit is contained in:
team 1
2026-05-05 14:15:49 +02:00
parent d3fab6e84a
commit 913616a3df
3 changed files with 75 additions and 160 deletions

View File

@@ -0,0 +1,40 @@
# RetrieX Patch 43B - Product Role Resolver Consolidation
## Ziel
Dieser Patch reduziert doppelte PHP-Rollenlogik fuer Produktrollen, ohne fachliche YAML-Werte, Ranking-Gewichte oder Prompt-Regeln zu aendern.
## Inhalt
Neu:
- `src/Commerce/ProductRoleResolver.php`
Geaendert:
- `src/Commerce/ShopSearchService.php`
- `src/Agent/PromptBuilder.php`
Die bisher lokal duplizierte Logik fuer angefragte Produktrolle, Produktrolle und Rollenkompatibilitaet wird ueber den zentralen Resolver gefuehrt.
## Bewusst nicht geaendert
- Keine YAML-Werte geaendert
- Keine neuen Token-/Stringlisten im PHP-Core
- Keine Scoring-Gewichte geaendert
- Keine Search-Repair-Logik geaendert
- Kein Adminbereich
- Keine fachliche Runtime-Strategie geaendert
## Erwartete Wirkung
Der Effekt soll gleich bleiben. Der Patch bereitet die weitere Reduktion und generischere Rollenbehandlung vor, indem die mehrfach vorhandene Rollenlogik zentralisiert wird.
## Empfohlene Checks
```bash
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

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace App\Agent; namespace App\Agent;
use App\Commerce\Dto\ShopProductResult; use App\Commerce\Dto\ShopProductResult;
use App\Commerce\ProductRoleResolver;
use App\Config\LanguageCleanupConfig; use App\Config\LanguageCleanupConfig;
use App\Config\PromptBuilderConfig; use App\Config\PromptBuilderConfig;
use App\Context\ContextService; use App\Context\ContextService;
@@ -20,6 +21,7 @@ final readonly class PromptBuilder
private SystemPromptRepository $systemPromptRepository, private SystemPromptRepository $systemPromptRepository,
private ModelGenerationConfigProvider $modelGenerationConfigProvider, private ModelGenerationConfigProvider $modelGenerationConfigProvider,
private PromptBuilderConfig $config, private PromptBuilderConfig $config,
private ProductRoleResolver $productRoleResolver,
private LanguageCleanupConfig $languageCleanupConfig, private LanguageCleanupConfig $languageCleanupConfig,
) { ) {
} }
@@ -1258,112 +1260,40 @@ final readonly class PromptBuilder
private function resolveRequestedProductRole(string $prompt): string private function resolveRequestedProductRole(string $prompt): string
{ {
$normalized = mb_strtolower($this->normalizeBlockText($prompt), 'UTF-8'); return $this->productRoleResolver->resolveRequestedRole(
$hasAccessoryIntent = $this->containsAnyPromptKeyword($normalized, $this->config->getAccessoryProductRoleKeywords()); prompt: $prompt,
$hasMainDeviceIntent = $this->containsAnyPromptKeyword($normalized, $this->config->getMainDeviceRequestRoleKeywords()); accessoryIntentKeywords: $this->config->getAccessoryProductRoleKeywords(),
mainDeviceIntentKeywords: $this->config->getMainDeviceRequestRoleKeywords(),
if ($hasAccessoryIntent && !$this->hasDirectMainDeviceRequest($normalized)) { directMainDeviceRequestPatterns: $this->config->getDirectMainDeviceRequestPatterns(),
return 'accessory_or_consumable'; normalize: fn(string $value): string => $this->normalizeBlockText($value)
} );
if ($hasMainDeviceIntent) {
return 'main_device';
}
if ($hasAccessoryIntent) {
return 'accessory_or_consumable';
}
return 'unknown';
}
private function hasDirectMainDeviceRequest(string $normalizedPrompt): bool
{
foreach ($this->config->getDirectMainDeviceRequestPatterns() as $pattern) {
if (preg_match($pattern, $normalizedPrompt) === 1) {
return true;
}
}
return false;
} }
private function resolveShopProductRole(ShopProductResult $product): string private function resolveShopProductRole(ShopProductResult $product): string
{ {
$primaryRole = $this->resolveShopPrimaryProductRole($product); return $this->productRoleResolver->resolveProductRole(
product: $product,
if ($primaryRole !== 'unknown') { accessoryKeywords: $this->config->getAccessoryProductRoleKeywords(),
return $primaryRole; deviceKeywords: $this->config->getMainDeviceProductRoleKeywords(),
} normalize: fn(string $value): string => $this->normalizeBlockText($value),
detectAmbiguousPrimaryRole: false
$corpus = mb_strtolower(implode(' ', array_filter([ );
$product->name,
$product->productNumber,
$product->manufacturer,
implode(' ', $product->highlights),
$product->description,
$product->customFields,
$product->url,
])), 'UTF-8');
$isAccessory = $this->containsAnyPromptKeyword($corpus, $this->config->getAccessoryProductRoleKeywords());
$isMainDevice = $this->containsAnyPromptKeyword($corpus, $this->config->getMainDeviceProductRoleKeywords());
if ($isAccessory) {
return 'accessory_or_consumable';
}
if ($isMainDevice) {
return 'main_device';
}
return 'unknown';
} }
private function resolveShopPrimaryProductRole(ShopProductResult $product): string private function resolveShopPrimaryProductRole(ShopProductResult $product): string
{ {
$primaryText = mb_strtolower(implode(' ', array_filter([ return $this->productRoleResolver->resolvePrimaryProductRole(
$product->name, product: $product,
$product->url, accessoryKeywords: $this->config->getAccessoryProductRoleKeywords(),
])), 'UTF-8'); deviceKeywords: $this->config->getMainDeviceProductRoleKeywords(),
normalize: fn(string $value): string => $this->normalizeBlockText($value),
if ($this->normalizeBlockText($primaryText) === '') { detectAmbiguousPrimaryRole: false
return 'unknown'; );
}
$isAccessory = $this->containsAnyPromptKeyword($primaryText, $this->config->getAccessoryProductRoleKeywords());
$isMainDevice = $this->containsAnyPromptKeyword($primaryText, $this->config->getMainDeviceProductRoleKeywords());
if ($isAccessory) {
return 'accessory_or_consumable';
}
if ($isMainDevice) {
return 'main_device';
}
return 'unknown';
} }
private function resolveShopRoleCompatibility(string $requestedRole, string $inferredRole): string private function resolveShopRoleCompatibility(string $requestedRole, string $inferredRole): string
{ {
if ($requestedRole === 'unknown' || $inferredRole === 'unknown') { return $this->productRoleResolver->resolveCompatibility($requestedRole, $inferredRole);
return 'unknown';
}
if ($requestedRole === 'main_device' && $inferredRole === 'accessory_or_consumable') {
return 'incompatible_accessory_for_main_device_request';
}
if ($requestedRole === 'accessory_or_consumable' && $inferredRole === 'main_device') {
return 'incompatible_main_device_for_accessory_request';
}
if ($inferredRole === 'ambiguous_mixed_role') {
return 'ambiguous_keep_separate';
}
return 'compatible';
} }
/** /**

View File

@@ -31,6 +31,7 @@ final class ShopSearchService
private readonly ShopwareCriteriaBuilder $criteriaBuilder, private readonly ShopwareCriteriaBuilder $criteriaBuilder,
private readonly StoreApiClient $storeApiClient, private readonly StoreApiClient $storeApiClient,
private readonly ShopServiceConfig $shopConfig, private readonly ShopServiceConfig $shopConfig,
private readonly ProductRoleResolver $productRoleResolver,
private readonly LoggerInterface $logger, private readonly LoggerInterface $logger,
private readonly bool $enabled = true, private readonly bool $enabled = true,
private readonly int $maxResults = 25, private readonly int $maxResults = 25,
@@ -1330,80 +1331,24 @@ final class ShopSearchService
private function isAccessoryLikeProduct(ShopProductResult $product): bool private function isAccessoryLikeProduct(ShopProductResult $product): bool
{ {
$primaryRole = $this->resolvePrimaryShopProductRole($product); return $this->productRoleResolver->isAccessoryLikeProduct(
product: $product,
if ($primaryRole === 'accessory_or_consumable') { accessoryKeywords: $this->shopConfig->getAccessoryProductKeywords(),
return true; deviceKeywords: $this->shopConfig->getDeviceProductKeywords(),
} normalize: fn(string $value): string => $this->normalizeForMatching($value)
if ($primaryRole === 'main_device') {
return false;
}
return $this->containsAnyShopKeyword(
$this->buildNormalizedProductCorpus($product),
$this->shopConfig->getAccessoryProductKeywords()
); );
} }
private function isDeviceLikeProduct(ShopProductResult $product): bool private function isDeviceLikeProduct(ShopProductResult $product): bool
{ {
$primaryRole = $this->resolvePrimaryShopProductRole($product); return $this->productRoleResolver->isDeviceLikeProduct(
product: $product,
if ($primaryRole === 'main_device') { accessoryKeywords: $this->shopConfig->getAccessoryProductKeywords(),
return true; deviceKeywords: $this->shopConfig->getDeviceProductKeywords(),
} normalize: fn(string $value): string => $this->normalizeForMatching($value)
if ($primaryRole === 'accessory_or_consumable') {
return false;
}
return $this->containsAnyShopKeyword(
$this->buildNormalizedProductCorpus($product),
$this->shopConfig->getDeviceProductKeywords()
); );
} }
private function resolvePrimaryShopProductRole(ShopProductResult $product): string
{
$primaryText = $this->buildNormalizedPrimaryProductIdentity($product);
if ($primaryText === '') {
return 'unknown';
}
$isAccessoryLike = $this->containsAnyShopKeyword(
$primaryText,
$this->shopConfig->getAccessoryProductKeywords()
);
$isDeviceLike = $this->containsAnyShopKeyword(
$primaryText,
$this->shopConfig->getDeviceProductKeywords()
);
if ($isAccessoryLike && !$isDeviceLike) {
return 'accessory_or_consumable';
}
if ($isDeviceLike && !$isAccessoryLike) {
return 'main_device';
}
if ($isAccessoryLike && $isDeviceLike) {
return 'ambiguous_mixed_role';
}
return 'unknown';
}
private function buildNormalizedPrimaryProductIdentity(ShopProductResult $product): string
{
return $this->normalizeForMatching(implode(' ', array_filter([
$product->name,
$product->url,
])));
}
/** /**
* @param string[] $keywords * @param string[] $keywords
*/ */