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;
use App\Commerce\Dto\ShopProductResult;
use App\Commerce\ProductRoleResolver;
use App\Config\LanguageCleanupConfig;
use App\Config\PromptBuilderConfig;
use App\Context\ContextService;
@@ -20,6 +21,7 @@ final readonly class PromptBuilder
private SystemPromptRepository $systemPromptRepository,
private ModelGenerationConfigProvider $modelGenerationConfigProvider,
private PromptBuilderConfig $config,
private ProductRoleResolver $productRoleResolver,
private LanguageCleanupConfig $languageCleanupConfig,
) {
}
@@ -1258,112 +1260,40 @@ final readonly class PromptBuilder
private function resolveRequestedProductRole(string $prompt): string
{
$normalized = mb_strtolower($this->normalizeBlockText($prompt), 'UTF-8');
$hasAccessoryIntent = $this->containsAnyPromptKeyword($normalized, $this->config->getAccessoryProductRoleKeywords());
$hasMainDeviceIntent = $this->containsAnyPromptKeyword($normalized, $this->config->getMainDeviceRequestRoleKeywords());
if ($hasAccessoryIntent && !$this->hasDirectMainDeviceRequest($normalized)) {
return 'accessory_or_consumable';
}
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;
return $this->productRoleResolver->resolveRequestedRole(
prompt: $prompt,
accessoryIntentKeywords: $this->config->getAccessoryProductRoleKeywords(),
mainDeviceIntentKeywords: $this->config->getMainDeviceRequestRoleKeywords(),
directMainDeviceRequestPatterns: $this->config->getDirectMainDeviceRequestPatterns(),
normalize: fn(string $value): string => $this->normalizeBlockText($value)
);
}
private function resolveShopProductRole(ShopProductResult $product): string
{
$primaryRole = $this->resolveShopPrimaryProductRole($product);
if ($primaryRole !== 'unknown') {
return $primaryRole;
}
$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';
return $this->productRoleResolver->resolveProductRole(
product: $product,
accessoryKeywords: $this->config->getAccessoryProductRoleKeywords(),
deviceKeywords: $this->config->getMainDeviceProductRoleKeywords(),
normalize: fn(string $value): string => $this->normalizeBlockText($value),
detectAmbiguousPrimaryRole: false
);
}
private function resolveShopPrimaryProductRole(ShopProductResult $product): string
{
$primaryText = mb_strtolower(implode(' ', array_filter([
$product->name,
$product->url,
])), 'UTF-8');
if ($this->normalizeBlockText($primaryText) === '') {
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';
return $this->productRoleResolver->resolvePrimaryProductRole(
product: $product,
accessoryKeywords: $this->config->getAccessoryProductRoleKeywords(),
deviceKeywords: $this->config->getMainDeviceProductRoleKeywords(),
normalize: fn(string $value): string => $this->normalizeBlockText($value),
detectAmbiguousPrimaryRole: false
);
}
private function resolveShopRoleCompatibility(string $requestedRole, string $inferredRole): string
{
if ($requestedRole === 'unknown' || $inferredRole === 'unknown') {
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';
return $this->productRoleResolver->resolveCompatibility($requestedRole, $inferredRole);
}
/**

View File

@@ -31,6 +31,7 @@ final class ShopSearchService
private readonly ShopwareCriteriaBuilder $criteriaBuilder,
private readonly StoreApiClient $storeApiClient,
private readonly ShopServiceConfig $shopConfig,
private readonly ProductRoleResolver $productRoleResolver,
private readonly LoggerInterface $logger,
private readonly bool $enabled = true,
private readonly int $maxResults = 25,
@@ -1330,80 +1331,24 @@ final class ShopSearchService
private function isAccessoryLikeProduct(ShopProductResult $product): bool
{
$primaryRole = $this->resolvePrimaryShopProductRole($product);
if ($primaryRole === 'accessory_or_consumable') {
return true;
}
if ($primaryRole === 'main_device') {
return false;
}
return $this->containsAnyShopKeyword(
$this->buildNormalizedProductCorpus($product),
$this->shopConfig->getAccessoryProductKeywords()
return $this->productRoleResolver->isAccessoryLikeProduct(
product: $product,
accessoryKeywords: $this->shopConfig->getAccessoryProductKeywords(),
deviceKeywords: $this->shopConfig->getDeviceProductKeywords(),
normalize: fn(string $value): string => $this->normalizeForMatching($value)
);
}
private function isDeviceLikeProduct(ShopProductResult $product): bool
{
$primaryRole = $this->resolvePrimaryShopProductRole($product);
if ($primaryRole === 'main_device') {
return true;
}
if ($primaryRole === 'accessory_or_consumable') {
return false;
}
return $this->containsAnyShopKeyword(
$this->buildNormalizedProductCorpus($product),
$this->shopConfig->getDeviceProductKeywords()
return $this->productRoleResolver->isDeviceLikeProduct(
product: $product,
accessoryKeywords: $this->shopConfig->getAccessoryProductKeywords(),
deviceKeywords: $this->shopConfig->getDeviceProductKeywords(),
normalize: fn(string $value): string => $this->normalizeForMatching($value)
);
}
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
*/