This commit is contained in:
team 1
2026-05-07 19:04:04 +02:00
parent 98577d4d25
commit 61f6841a5a
7 changed files with 419 additions and 214 deletions

View File

@@ -332,6 +332,21 @@ final readonly class AgentRunner
$optimizedShopQuery = '';
}
$positiveFilteredShopSearchQuery = $this->filterShopQueryToPositiveTokens($shopSearchQuery);
if ($positiveFilteredShopSearchQuery !== $shopSearchQuery) {
$this->agentLogger->info('Filtered final shop search query to positive product tokens', [
'userId' => $userId,
'prompt' => $prompt,
'routingPrompt' => $routingPrompt,
'optimizedShopQuery' => $optimizedShopQuery,
'shopSearchQuery' => $shopSearchQuery,
'positiveFilteredShopSearchQuery' => $positiveFilteredShopSearchQuery,
]);
$shopSearchQuery = $positiveFilteredShopSearchQuery;
$optimizedShopQuery = '';
}
if ($shopSearchQuery === '') {
$this->agentLogger->info('Commerce search skipped because no concrete shop query could be resolved', [
'userId' => $userId,
@@ -502,7 +517,6 @@ final readonly class AgentRunner
$shopResults = $directIdentityRepairPayload['results'];
}
$shopResults = $this->guardShopResultsByReferencedProductAnchor($shopSearchQuery, $shopResults);
$shopResults = $this->sortShopResultsForLengthRequest($prompt, $shopSearchQuery, $shopResults);
$attemptedShopRepair = $repairPayload['attemptedRepair'] || $directIdentityRepairPayload['attemptedRepair'];
$usedShopRepair = $repairPayload['usedRepair'] || $directIdentityRepairPayload['usedRepair'];
@@ -1664,6 +1678,118 @@ final readonly class AgentRunner
return $cleaned !== '' ? $cleaned : $shopSearchQuery;
}
private function filterShopQueryToPositiveTokens(string $shopSearchQuery): string
{
$shopSearchQuery = trim($shopSearchQuery);
if (
$shopSearchQuery === ''
|| !$this->agentRunnerConfig->isShopQueryPositiveTokenFilterEnabled()
) {
return $shopSearchQuery;
}
$tokens = $this->tokenizeShopQueryCandidate($shopSearchQuery);
if ($tokens === []) {
return $shopSearchQuery;
}
$allowedTokens = $this->buildPositiveShopQueryAllowedTokenSet();
$blockedTokens = $this->buildPositiveShopQueryBlockedTokenSet();
$codePatterns = $this->agentRunnerConfig->getShopQueryPositiveTokenFilterCodePatterns();
if ($allowedTokens === [] && $codePatterns === []) {
return $shopSearchQuery;
}
$kept = [];
foreach ($tokens as $token) {
if (isset($blockedTokens[$token]) || isset($kept[$token])) {
continue;
}
if (isset($allowedTokens[$token]) || $this->matchesAnyConfiguredShopQueryCodePattern($token, $codePatterns)) {
$kept[$token] = $token;
}
}
if (count($kept) < max(1, $this->agentRunnerConfig->getShopQueryPositiveTokenFilterMinTokens())) {
return $shopSearchQuery;
}
$filtered = implode(' ', array_values($kept));
return $filtered !== '' ? $filtered : $shopSearchQuery;
}
/**
* @return array<string, true>
*/
private function buildPositiveShopQueryAllowedTokenSet(): array
{
$terms = $this->agentRunnerConfig->getShopQueryPositiveTokenFilterAllowedTerms();
if ($this->agentRunnerConfig->shouldShopQueryPositiveTokenFilterIncludeCurrentInputPreservationTerms()) {
$terms = $this->mergeUniqueStrings(
$terms,
$this->agentRunnerConfig->getShopQueryCurrentInputPreservationTerms()
);
}
if ($this->agentRunnerConfig->shouldShopQueryPositiveTokenFilterIncludeSemanticShopSearchTokens()) {
$terms = $this->mergeUniqueStrings(
$terms,
$this->agentRunnerConfig->getShopQueryPositiveTokenFilterSemanticShopSearchTokens()
);
}
if ($this->agentRunnerConfig->shouldShopQueryPositiveTokenFilterIncludeProductRoleTerms()) {
$terms = $this->mergeUniqueStrings(
$terms,
$this->agentRunnerConfig->getShopQueryPositiveTokenFilterProductRoleTerms()
);
}
$tokens = [];
foreach ($terms as $term) {
foreach ($this->tokenizeShopQueryCandidate($term) as $token) {
$tokens[$token] = true;
}
}
return $tokens;
}
/**
* @return array<string, true>
*/
private function buildPositiveShopQueryBlockedTokenSet(): array
{
$tokens = [];
foreach ($this->agentRunnerConfig->getShopQueryPositiveTokenFilterBlockedTerms() as $term) {
foreach ($this->tokenizeShopQueryCandidate($term) as $token) {
$tokens[$token] = true;
}
}
return $tokens;
}
/**
* @param string[] $patterns
*/
private function matchesAnyConfiguredShopQueryCodePattern(string $token, array $patterns): bool
{
foreach ($patterns as $pattern) {
if (@preg_match($pattern, $token) === 1) {
return true;
}
}
return false;
}
private function cleanupDirectProductAttributeShopQuery(string $prompt, string $shopSearchQuery): string
{
$shopSearchQuery = trim($shopSearchQuery);
@@ -2673,40 +2799,20 @@ final readonly class AgentRunner
return '';
}
$triggerTokens = $this->buildShopQueryTokenSet(
$this->agentRunnerConfig->getShopQueryContextAnchorEnrichmentTriggerTerms()
);
$triggerTokens = [];
foreach ($this->agentRunnerConfig->getShopQueryContextAnchorEnrichmentTriggerTerms() as $term) {
foreach ($this->tokenizeShopQueryCandidate($term) as $termToken) {
$triggerTokens[$termToken] = true;
}
}
if ($triggerTokens === []) {
return '';
}
$hasTrigger = false;
foreach ($tokens as $token) {
if (isset($triggerTokens[$token])) {
$hasTrigger = true;
break;
}
}
if (!$hasTrigger) {
return '';
}
$queryTokens = $this->buildShopQueryTokenSet(
$this->agentRunnerConfig->getShopQueryContextAnchorEnrichmentQueryTerms()
);
if ($queryTokens === []) {
$queryTokens = $triggerTokens;
}
$noiseTokens = $this->buildShopQueryTokenSet(
$this->agentRunnerConfig->getShopQueryContextAnchorEnrichmentQueryNoiseTerms()
);
$out = [];
foreach ($tokens as $token) {
if (!isset($queryTokens[$token]) || isset($noiseTokens[$token]) || isset($out[$token])) {
if (!isset($triggerTokens[$token]) || isset($out[$token])) {
continue;
}
@@ -2716,23 +2822,6 @@ final readonly class AgentRunner
return implode(' ', array_values($out));
}
/**
* @param string[] $terms
* @return array<string, true>
*/
private function buildShopQueryTokenSet(array $terms): array
{
$tokens = [];
foreach ($terms as $term) {
foreach ($this->tokenizeShopQueryCandidate($term) as $termToken) {
$tokens[$termToken] = true;
}
}
return $tokens;
}
private function enrichReferentialShopQueryFromHistory(
string $query,
string $sourcePrompt,
@@ -2801,33 +2890,11 @@ final readonly class AgentRunner
}
private function extractLatestConfiguredShopQueryContextAnchor(string $commerceHistoryContext): string
{
foreach ($this->extractHistoryTurnsNewestFirst($commerceHistoryContext) as $turn) {
if (!$this->containsConfiguredShopQueryAnchorTrigger($turn)) {
continue;
}
$modelAnchor = $this->referenceAnchorExtractor->extractFirstProductModelAnchor($turn);
$turnAnchor = $this->extractLatestConfiguredShopQueryPatternAnchor($turn);
if ($modelAnchor !== '') {
return $this->buildModelQualifiedShopQueryAnchor($modelAnchor, $turnAnchor);
}
if ($turnAnchor !== '') {
return $turnAnchor;
}
}
return $this->extractLatestConfiguredShopQueryPatternAnchor($commerceHistoryContext);
}
private function extractLatestConfiguredShopQueryPatternAnchor(string $text): string
{
$latest = '';
foreach ($this->agentRunnerConfig->getShopQueryContextAnchorEnrichmentPatterns() as $pattern) {
if (@preg_match_all($pattern, $text, $matches, PREG_SET_ORDER) === false) {
if (@preg_match_all($pattern, $commerceHistoryContext, $matches, PREG_SET_ORDER) === false) {
continue;
}
@@ -2842,51 +2909,6 @@ final readonly class AgentRunner
return $latest;
}
private function buildModelQualifiedShopQueryAnchor(string $modelAnchor, string $detailAnchor): string
{
$modelAnchor = trim($modelAnchor);
if ($modelAnchor === '') {
return trim($detailAnchor);
}
$detailTokens = $this->extractShopQueryDetailAnchorTokens($detailAnchor, $modelAnchor);
if ($detailTokens === []) {
return $modelAnchor;
}
return trim($modelAnchor . ' ' . implode(' ', $detailTokens));
}
/**
* @return string[]
*/
private function extractShopQueryDetailAnchorTokens(string $detailAnchor, string $modelAnchor): array
{
$tokens = $this->tokenizeShopQueryCandidate($detailAnchor);
if ($tokens === []) {
return [];
}
$modelTokens = array_fill_keys($this->tokenizeShopQueryCandidate($modelAnchor), true);
$queryTokens = $this->buildShopQueryTokenSet(
$this->agentRunnerConfig->getShopQueryContextAnchorEnrichmentQueryTerms()
);
$noiseTokens = $this->buildShopQueryTokenSet(
$this->agentRunnerConfig->getShopQueryContextAnchorEnrichmentQueryNoiseTerms()
);
$out = [];
foreach ($tokens as $token) {
if (isset($modelTokens[$token]) || isset($queryTokens[$token]) || isset($noiseTokens[$token]) || isset($out[$token])) {
continue;
}
$out[$token] = $token;
}
return array_values($out);
}
private function normalizeShopQueryAnchor(string $anchor): string
{
$anchor = str_replace('®', '', $anchor);
@@ -3354,48 +3376,6 @@ final readonly class AgentRunner
return trim(implode(' ', $this->tokenizeShopQueryCandidate($query)));
}
/**
* @param ShopProductResult[] $shopResults
* @return ShopProductResult[]
*/
private function guardShopResultsByReferencedProductAnchor(string $shopSearchQuery, array $shopResults): array
{
if ($shopResults === []) {
return $shopResults;
}
$anchor = $this->referenceAnchorExtractor->extractFirstProductModelAnchor($shopSearchQuery);
if ($anchor === '') {
return $shopResults;
}
$filtered = [];
foreach ($shopResults as $product) {
if (!$product instanceof ShopProductResult) {
continue;
}
if ($this->shopProductMatchesReferencedProductAnchor($product, $anchor)) {
$filtered[] = $product;
}
}
return $filtered;
}
private function shopProductMatchesReferencedProductAnchor(ShopProductResult $product, string $anchor): bool
{
$productText = trim(implode(' ', array_filter([
$product->name,
$product->description,
implode(' ', $product->highlights),
$product->customFields,
$product->url,
])));
return $this->containsAllShopQueryTokens($productText, $anchor);
}
/**
* @param ShopProductResult[] $shopResults
* @return ShopProductResult[]

View File

@@ -962,11 +962,6 @@ final class AgentRunnerConfig
*/
public function getNoLlmMainDeviceRequestRoleKeywords(): array
{
$terms = $this->genreStringList('product_roles.no_llm_fallback_terms.main_device_request_keywords');
if ($terms !== []) {
return $terms;
}
return $this->getConfiguredStringListOrVocabularyView(
'no_llm_fallback.product_roles.main_device_request_keywords',
'no_llm_fallback.product_roles.vocabulary_views.main_device_request_keywords'
@@ -978,11 +973,6 @@ final class AgentRunnerConfig
*/
public function getNoLlmAccessoryProductRoleKeywords(): array
{
$terms = $this->genreStringList('product_roles.no_llm_fallback_terms.accessory_product_keywords');
if ($terms !== []) {
return $terms;
}
return $this->getConfiguredStringListOrVocabularyView(
'no_llm_fallback.product_roles.accessory_product_keywords',
'no_llm_fallback.product_roles.vocabulary_views.accessory_product_keywords'
@@ -1196,6 +1186,90 @@ final class AgentRunnerConfig
?: $this->getRequiredStringList('shop_runtime.query_cleanup.stopword_cleanup.terms');
}
public function isShopQueryPositiveTokenFilterEnabled(): bool
{
return $this->genreBool('shop_query_runtime.positive_token_filter.enabled')
?? $this->getOptionalBool('shop_runtime.query_cleanup.positive_token_filter.enabled', false);
}
public function getShopQueryPositiveTokenFilterMinTokens(): int
{
return $this->genreInt('shop_query_runtime.positive_token_filter.min_query_tokens_after_filter')
?? $this->getOptionalInt('shop_runtime.query_cleanup.positive_token_filter.min_query_tokens_after_filter', 2);
}
public function shouldShopQueryPositiveTokenFilterIncludeCurrentInputPreservationTerms(): bool
{
return $this->genreBool('shop_query_runtime.positive_token_filter.include_current_input_preservation_terms')
?? $this->getOptionalBool('shop_runtime.query_cleanup.positive_token_filter.include_current_input_preservation_terms', true);
}
public function shouldShopQueryPositiveTokenFilterIncludeSemanticShopSearchTokens(): bool
{
return $this->genreBool('shop_query_runtime.positive_token_filter.include_semantic_shop_search_tokens')
?? $this->getOptionalBool('shop_runtime.query_cleanup.positive_token_filter.include_semantic_shop_search_tokens', true);
}
public function shouldShopQueryPositiveTokenFilterIncludeProductRoleTerms(): bool
{
return $this->genreBool('shop_query_runtime.positive_token_filter.include_product_role_terms')
?? $this->getOptionalBool('shop_runtime.query_cleanup.positive_token_filter.include_product_role_terms', true);
}
/**
* @return string[]
*/
public function getShopQueryPositiveTokenFilterAllowedTerms(): array
{
return $this->genreStringList('shop_query_runtime.positive_token_filter.allowed_terms')
?: $this->getOptionalStringList('shop_runtime.query_cleanup.positive_token_filter.allowed_terms');
}
/**
* @return string[]
*/
public function getShopQueryPositiveTokenFilterBlockedTerms(): array
{
return $this->genreStringList('shop_query_runtime.positive_token_filter.blocked_terms')
?: $this->getOptionalStringList('shop_runtime.query_cleanup.positive_token_filter.blocked_terms');
}
/**
* @return string[]
*/
public function getShopQueryPositiveTokenFilterCodePatterns(): array
{
return $this->genreStringList('shop_query_runtime.positive_token_filter.code_patterns')
?: $this->getOptionalStringList('shop_runtime.query_cleanup.positive_token_filter.code_patterns');
}
/**
* @return string[]
*/
public function getShopQueryPositiveTokenFilterSemanticShopSearchTokens(): array
{
return $this->genreStringList('shop_query_runtime.semantic_shop_search_tokens.terms');
}
/**
* @return string[]
*/
public function getShopQueryPositiveTokenFilterProductRoleTerms(): array
{
return array_values(array_unique(array_merge(
$this->genreStringList('product_roles.primary_product_terms.terms'),
$this->genreStringList('product_roles.accessory_product_terms.terms'),
$this->genreStringList('product_roles.shop_views.device_query_terms'),
$this->genreStringList('product_roles.shop_views.accessory_query_terms'),
$this->genreStringList('product_roles.shop_views.device_product_terms'),
$this->genreStringList('product_roles.shop_views.accessory_product_terms'),
$this->genreStringList('product_roles.shop_views.device_focus_terms'),
$this->genreStringList('product_roles.shop_views.accessory_focus_terms'),
$this->genreStringList('product_roles.no_llm_fallback_terms.main_device_request_keywords'),
$this->genreStringList('product_roles.no_llm_fallback_terms.accessory_product_keywords')
)));
}
public function isDirectShopResultGuardEnabled(): bool
{
return $this->getRequiredBool('shop_runtime.result_identity.enabled');
@@ -1434,24 +1508,6 @@ final class AgentRunnerConfig
);
}
/**
* @return string[]
*/
public function getShopQueryContextAnchorEnrichmentQueryTerms(): array
{
return $this->genreStringList('context_resolution.history_anchor_enrichment.query_terms')
?: $this->getOptionalStringList('shop_runtime.context_resolution.history_anchor_enrichment.query_terms');
}
/**
* @return string[]
*/
public function getShopQueryContextAnchorEnrichmentQueryNoiseTerms(): array
{
return $this->genreStringList('context_resolution.history_anchor_enrichment.query_noise_terms')
?: $this->getOptionalStringList('shop_runtime.context_resolution.history_anchor_enrichment.query_noise_terms');
}
/**
* @return string[]
*/

View File

@@ -687,6 +687,16 @@ final readonly class RetriexEffectiveConfigProvider
'min_query_tokens_after_cleanup' => $this->agentRunnerConfig->getShopQueryStopwordCleanupMinTokens(),
'terms' => $this->agentRunnerConfig->getShopQueryStopwordCleanupTerms(),
],
'positive_token_filter' => [
'enabled' => $this->agentRunnerConfig->isShopQueryPositiveTokenFilterEnabled(),
'min_query_tokens_after_filter' => $this->agentRunnerConfig->getShopQueryPositiveTokenFilterMinTokens(),
'include_current_input_preservation_terms' => $this->agentRunnerConfig->shouldShopQueryPositiveTokenFilterIncludeCurrentInputPreservationTerms(),
'include_semantic_shop_search_tokens' => $this->agentRunnerConfig->shouldShopQueryPositiveTokenFilterIncludeSemanticShopSearchTokens(),
'include_product_role_terms' => $this->agentRunnerConfig->shouldShopQueryPositiveTokenFilterIncludeProductRoleTerms(),
'allowed_terms' => $this->agentRunnerConfig->getShopQueryPositiveTokenFilterAllowedTerms(),
'blocked_terms' => $this->agentRunnerConfig->getShopQueryPositiveTokenFilterBlockedTerms(),
'code_patterns' => $this->agentRunnerConfig->getShopQueryPositiveTokenFilterCodePatterns(),
],
],
'attribute_cleanup' => [
'enabled' => $this->agentRunnerConfig->isShopQueryProductAttributeCleanupEnabled(),
@@ -703,8 +713,6 @@ final readonly class RetriexEffectiveConfigProvider
'enabled' => $this->agentRunnerConfig->isShopQueryContextAnchorEnrichmentEnabled(),
'max_query_terms' => $this->agentRunnerConfig->getShopQueryContextAnchorEnrichmentMaxQueryTerms(),
'trigger_terms' => $this->agentRunnerConfig->getShopQueryContextAnchorEnrichmentTriggerTerms(),
'query_terms' => $this->agentRunnerConfig->getShopQueryContextAnchorEnrichmentQueryTerms(),
'query_noise_terms' => $this->agentRunnerConfig->getShopQueryContextAnchorEnrichmentQueryNoiseTerms(),
'anchor_patterns' => $this->agentRunnerConfig->getShopQueryContextAnchorEnrichmentPatterns(),
'template' => $this->agentRunnerConfig->getShopQueryContextAnchorEnrichmentTemplate(),
],
@@ -1314,6 +1322,33 @@ final readonly class RetriexEffectiveConfigProvider
}
}
$shopQueryRuntime = is_array($configurationValues['shop_query_runtime'] ?? null)
? $configurationValues['shop_query_runtime']
: [];
$positiveTokenFilter = is_array($shopQueryRuntime['positive_token_filter'] ?? null)
? $shopQueryRuntime['positive_token_filter']
: [];
if ($positiveTokenFilter !== []) {
foreach ([
'enabled',
'include_current_input_preservation_terms',
'include_semantic_shop_search_tokens',
'include_product_role_terms',
] as $boolKey) {
if (array_key_exists($boolKey, $positiveTokenFilter) && !is_bool($positiveTokenFilter[$boolKey])) {
$errors[] = sprintf('genre.configuration_values.shop_query_runtime.positive_token_filter.%s must be boolean.', $boolKey);
}
}
if (array_key_exists('min_query_tokens_after_filter', $positiveTokenFilter) && !is_numeric($positiveTokenFilter['min_query_tokens_after_filter'])) {
$errors[] = 'genre.configuration_values.shop_query_runtime.positive_token_filter.min_query_tokens_after_filter must be numeric.';
}
$this->validateStringList($this->toList($positiveTokenFilter['allowed_terms'] ?? []), 'genre.configuration_values.shop_query_runtime.positive_token_filter.allowed_terms', $errors, $warnings);
$this->validateStringList($this->toList($positiveTokenFilter['blocked_terms'] ?? []), 'genre.configuration_values.shop_query_runtime.positive_token_filter.blocked_terms', $errors, $warnings);
$this->validateRegexPatternList($positiveTokenFilter['code_patterns'] ?? [], 'genre.configuration_values.shop_query_runtime.positive_token_filter.code_patterns', $errors);
}
foreach ($this->collectGenreConfigurationValueSourcePaths($configurationValues) as $valuePath => $sourcePaths) {
foreach ($sourcePaths as $sourcePath) {
if (!isset($flattened[$sourcePath])) {
@@ -1836,8 +1871,6 @@ final readonly class RetriexEffectiveConfigProvider
$anchorEnrichment = $contextResolution['history_anchor_enrichment'] ?? [];
if (is_array($anchorEnrichment)) {
$this->validateStringList($this->toList($anchorEnrichment['trigger_terms'] ?? []), 'agent.shop_runtime.context_resolution.history_anchor_enrichment.trigger_terms', $errors, $warnings);
$this->validateStringList($this->toList($anchorEnrichment['query_terms'] ?? []), 'agent.shop_runtime.context_resolution.history_anchor_enrichment.query_terms', $errors, $warnings);
$this->validateStringList($this->toList($anchorEnrichment['query_noise_terms'] ?? []), 'agent.shop_runtime.context_resolution.history_anchor_enrichment.query_noise_terms', $errors, $warnings);
$this->validateRegexPatternList($anchorEnrichment['anchor_patterns'] ?? [], 'agent.shop_runtime.context_resolution.history_anchor_enrichment.anchor_patterns', $errors);
if (trim((string) ($anchorEnrichment['template'] ?? '')) === '') {
$errors[] = 'agent.shop_runtime.context_resolution.history_anchor_enrichment.template must not be empty.';