diff --git a/patch_history/RETRIEX_PATCH_43B_PRODUCT_ROLE_RESOLVER_README.md b/patch_history/RETRIEX_PATCH_43B_PRODUCT_ROLE_RESOLVER_README.md new file mode 100644 index 0000000..a01baee --- /dev/null +++ b/patch_history/RETRIEX_PATCH_43B_PRODUCT_ROLE_RESOLVER_README.md @@ -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 +``` diff --git a/src/Agent/PromptBuilder.php b/src/Agent/PromptBuilder.php index 98606fa..a288c11 100644 --- a/src/Agent/PromptBuilder.php +++ b/src/Agent/PromptBuilder.php @@ -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); } /** diff --git a/src/Commerce/ShopSearchService.php b/src/Commerce/ShopSearchService.php index 43b1016..863907d 100644 --- a/src/Commerce/ShopSearchService.php +++ b/src/Commerce/ShopSearchService.php @@ -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 */