p77+78+79
This commit is contained in:
@@ -1714,24 +1714,32 @@ final readonly class AgentRunner
|
||||
$allowedTokens = $this->buildPositiveShopQueryAllowedTokenSet();
|
||||
$blockedTokens = $this->buildPositiveShopQueryBlockedTokenSet();
|
||||
$codePatterns = $this->agentRunnerConfig->getShopQueryPositiveTokenFilterCodePatterns();
|
||||
$adjacentVariantPatterns = $this->agentRunnerConfig->getShopQueryPositiveTokenFilterAdjacentVariantPatterns();
|
||||
$adjacentVariantTokens = $this->buildShopQueryTokenSet(
|
||||
$this->agentRunnerConfig->getShopQueryPositiveTokenFilterAdjacentVariantTerms()
|
||||
);
|
||||
|
||||
if ($allowedTokens === [] && $codePatterns === []) {
|
||||
if ($allowedTokens === [] && $codePatterns === [] && $adjacentVariantPatterns === [] && $adjacentVariantTokens === []) {
|
||||
return $shopSearchQuery;
|
||||
}
|
||||
|
||||
$modelVariantSuffixTokens = $this->extractPositiveShopQueryModelVariantSuffixTokens($tokens, $blockedTokens, $codePatterns);
|
||||
|
||||
$kept = [];
|
||||
foreach ($tokens as $token) {
|
||||
if (isset($blockedTokens[$token]) || isset($kept[$token])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
isset($allowedTokens[$token])
|
||||
|| isset($modelVariantSuffixTokens[$token])
|
||||
|| $this->matchesAnyConfiguredShopQueryCodePattern($token, $codePatterns)
|
||||
) {
|
||||
if (isset($allowedTokens[$token]) || $this->matchesAnyConfiguredShopQueryCodePattern($token, $codePatterns)) {
|
||||
$kept[$token] = $token;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($tokens as $index => $token) {
|
||||
if (isset($blockedTokens[$token]) || isset($kept[$token])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($this->shouldKeepAdjacentVariantShopQueryToken($token, $index, $tokens, $kept, $adjacentVariantPatterns, $adjacentVariantTokens)) {
|
||||
$kept[$token] = $token;
|
||||
}
|
||||
}
|
||||
@@ -1740,11 +1748,62 @@ final readonly class AgentRunner
|
||||
return $shopSearchQuery;
|
||||
}
|
||||
|
||||
$filtered = implode(' ', array_values($kept));
|
||||
$filteredTokens = [];
|
||||
foreach ($tokens as $token) {
|
||||
if (isset($kept[$token])) {
|
||||
$filteredTokens[] = $token;
|
||||
}
|
||||
}
|
||||
|
||||
$filtered = implode(' ', $filteredTokens);
|
||||
|
||||
return $filtered !== '' ? $filtered : $shopSearchQuery;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $tokens
|
||||
* @param array<string, string> $kept
|
||||
* @param string[] $variantPatterns
|
||||
* @param array<string, true> $variantTokens
|
||||
*/
|
||||
private function shouldKeepAdjacentVariantShopQueryToken(
|
||||
string $token,
|
||||
int $index,
|
||||
array $tokens,
|
||||
array $kept,
|
||||
array $variantPatterns,
|
||||
array $variantTokens
|
||||
): bool {
|
||||
if (!isset($variantTokens[$token]) && !$this->matchesAnyConfiguredShopQueryCodePattern($token, $variantPatterns)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$hasAdjacentNumericContext = false;
|
||||
$nearbyKeptContextCount = 0;
|
||||
|
||||
for ($offset = -2; $offset <= 2; ++$offset) {
|
||||
if ($offset === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$nearbyIndex = $index + $offset;
|
||||
if (!isset($tokens[$nearbyIndex])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$nearbyToken = $tokens[$nearbyIndex];
|
||||
if (isset($kept[$nearbyToken])) {
|
||||
++$nearbyKeptContextCount;
|
||||
}
|
||||
|
||||
if (abs($offset) === 1 && preg_match('/\d/u', $nearbyToken) === 1) {
|
||||
$hasAdjacentNumericContext = true;
|
||||
}
|
||||
}
|
||||
|
||||
return $hasAdjacentNumericContext && $nearbyKeptContextCount >= 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, true>
|
||||
*/
|
||||
@@ -1813,53 +1872,6 @@ final readonly class AgentRunner
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Preserve model variant suffixes that are attached to an already retained
|
||||
* model number in the same query, for example family-number-code product
|
||||
* names. This prevents the positive-token filter from degrading a specific
|
||||
* model variant to its generic base model.
|
||||
*
|
||||
* @param string[] $tokens
|
||||
* @param array<string, true> $blockedTokens
|
||||
* @param string[] $codePatterns
|
||||
* @return array<string, true>
|
||||
*/
|
||||
private function extractPositiveShopQueryModelVariantSuffixTokens(
|
||||
array $tokens,
|
||||
array $blockedTokens,
|
||||
array $codePatterns
|
||||
): array {
|
||||
$suffixTokens = [];
|
||||
$count = count($tokens);
|
||||
|
||||
for ($index = 0; $index < $count; $index++) {
|
||||
$token = $tokens[$index] ?? '';
|
||||
if (!$this->matchesAnyConfiguredShopQueryCodePattern($token, $codePatterns)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for ($suffixIndex = $index + 1; $suffixIndex < $count; $suffixIndex++) {
|
||||
$suffix = $tokens[$suffixIndex] ?? '';
|
||||
|
||||
if (isset($blockedTokens[$suffix]) || !$this->isPositiveShopQueryModelVariantSuffixToken($suffix)) {
|
||||
break;
|
||||
}
|
||||
|
||||
$suffixTokens[$suffix] = true;
|
||||
}
|
||||
}
|
||||
|
||||
return $suffixTokens;
|
||||
}
|
||||
|
||||
private function isPositiveShopQueryModelVariantSuffixToken(string $token): bool
|
||||
{
|
||||
$token = trim($token);
|
||||
|
||||
return $token !== ''
|
||||
&& preg_match('/^[\p{L}]{2,8}\d{0,3}$/u', $token) === 1;
|
||||
}
|
||||
|
||||
private function cleanupDirectProductAttributeShopQuery(string $prompt, string $shopSearchQuery): string
|
||||
{
|
||||
$shopSearchQuery = trim($shopSearchQuery);
|
||||
@@ -5290,20 +5302,24 @@ final readonly class AgentRunner
|
||||
|
||||
/**
|
||||
* @param ShopProductResult[] $shopResults
|
||||
* @return array{shop_query:string, answer_has_price:bool, answer_text:string, answer_anchor:string, answer_detail_score:int, role_counts:array<string,int>}
|
||||
* @return array{shop_query:string, shop_price_query:string, answer_has_price:bool, answer_text:string, answer_anchor:string, answer_detail_score:int, role_counts:array<string,int>}
|
||||
*/
|
||||
private function buildFollowUpActionContext(array $shopResults, string $shopSearchQuery, string $answerText): array
|
||||
{
|
||||
$plainAnswerText = $this->normalizeOneLine($this->plainTextFromHtml($answerText));
|
||||
$roleCounts = $this->buildFollowUpActionRoleCounts($shopResults);
|
||||
$displayedRoleCounts = $this->buildFollowUpActionDisplayedRoleCounts($shopResults, $plainAnswerText);
|
||||
$displayedProducts = $this->buildFollowUpActionDisplayedProducts($shopResults, $plainAnswerText);
|
||||
$displayedRoleCounts = $this->buildFollowUpActionProductRoleCounts($displayedProducts);
|
||||
|
||||
if (array_sum($displayedRoleCounts) > 0) {
|
||||
$roleCounts = $displayedRoleCounts;
|
||||
}
|
||||
|
||||
$normalizedShopQuery = $this->normalizeOneLine($shopSearchQuery);
|
||||
|
||||
return [
|
||||
'shop_query' => $this->normalizeOneLine($shopSearchQuery),
|
||||
'shop_query' => $normalizedShopQuery,
|
||||
'shop_price_query' => $this->buildFollowUpActionPriceQuery($displayedProducts, $plainAnswerText, $normalizedShopQuery),
|
||||
'answer_has_price' => $this->followUpActionAnswerAlreadyContainsPrice($plainAnswerText),
|
||||
'answer_text' => $plainAnswerText,
|
||||
'answer_anchor' => $this->buildFollowUpActionAnswerAnchor($plainAnswerText),
|
||||
@@ -5346,15 +5362,17 @@ final readonly class AgentRunner
|
||||
|
||||
/**
|
||||
* @param ShopProductResult[] $shopResults
|
||||
* @return array<string, int>
|
||||
* @return ShopProductResult[]
|
||||
*/
|
||||
private function buildFollowUpActionDisplayedRoleCounts(array $shopResults, string $answerText): array
|
||||
private function buildFollowUpActionDisplayedProducts(array $shopResults, string $answerText): array
|
||||
{
|
||||
$roleCounts = $this->emptyFollowUpActionRoleCounts();
|
||||
if ($answerText === '') {
|
||||
return $roleCounts;
|
||||
return [];
|
||||
}
|
||||
|
||||
$products = [];
|
||||
$seen = [];
|
||||
|
||||
foreach ($shopResults as $product) {
|
||||
if (!$product instanceof ShopProductResult) {
|
||||
continue;
|
||||
@@ -5364,6 +5382,31 @@ final readonly class AgentRunner
|
||||
continue;
|
||||
}
|
||||
|
||||
$key = mb_strtolower($this->normalizeOneLine(($product->productNumber ?? '') . ' ' . $product->name), 'UTF-8');
|
||||
if ($key === '' || isset($seen[$key])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$seen[$key] = true;
|
||||
$products[] = $product;
|
||||
}
|
||||
|
||||
return $products;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ShopProductResult[] $products
|
||||
* @return array<string, int>
|
||||
*/
|
||||
private function buildFollowUpActionProductRoleCounts(array $products): array
|
||||
{
|
||||
$roleCounts = $this->emptyFollowUpActionRoleCounts();
|
||||
|
||||
foreach ($products as $product) {
|
||||
if (!$product instanceof ShopProductResult) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->countFollowUpActionProductRole($roleCounts, $product);
|
||||
}
|
||||
|
||||
@@ -5409,6 +5452,93 @@ final readonly class AgentRunner
|
||||
return str_contains($normalizedAnswer, $productName);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ShopProductResult[] $displayedProducts
|
||||
*/
|
||||
private function buildFollowUpActionPriceQuery(array $displayedProducts, string $answerText, string $fallbackShopQuery): string
|
||||
{
|
||||
$numberedProducts = $this->filterFollowUpActionDisplayedProductsWithNumber($displayedProducts, $answerText);
|
||||
if (count($numberedProducts) === 1) {
|
||||
return $this->buildFollowUpActionProductQuery($numberedProducts[0]);
|
||||
}
|
||||
|
||||
$focusedProducts = $this->filterFollowUpActionPrimaryDisplayedProducts($displayedProducts);
|
||||
if (count($focusedProducts) === 1) {
|
||||
return $this->buildFollowUpActionProductQuery($focusedProducts[0]);
|
||||
}
|
||||
|
||||
if (count($displayedProducts) === 1) {
|
||||
return $this->buildFollowUpActionProductQuery($displayedProducts[0]);
|
||||
}
|
||||
|
||||
$answerAnchor = $this->buildFollowUpActionAnswerAnchor($answerText);
|
||||
if ($answerAnchor !== '') {
|
||||
return $answerAnchor;
|
||||
}
|
||||
|
||||
return $fallbackShopQuery;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ShopProductResult[] $products
|
||||
* @return ShopProductResult[]
|
||||
*/
|
||||
private function filterFollowUpActionDisplayedProductsWithNumber(array $products, string $answerText): array
|
||||
{
|
||||
$numberedProducts = [];
|
||||
$normalizedAnswer = mb_strtolower($this->normalizeOneLine($answerText), 'UTF-8');
|
||||
|
||||
foreach ($products as $product) {
|
||||
if (!$product instanceof ShopProductResult) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$productNumber = $this->normalizeOneLine((string) $product->productNumber);
|
||||
if ($productNumber === '' || mb_strlen($productNumber, 'UTF-8') < 3) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (str_contains($normalizedAnswer, mb_strtolower($productNumber, 'UTF-8'))) {
|
||||
$numberedProducts[] = $product;
|
||||
}
|
||||
}
|
||||
|
||||
return $numberedProducts;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ShopProductResult[] $products
|
||||
* @return ShopProductResult[]
|
||||
*/
|
||||
private function filterFollowUpActionPrimaryDisplayedProducts(array $products): array
|
||||
{
|
||||
$mainDevices = [];
|
||||
|
||||
foreach ($products as $product) {
|
||||
if (!$product instanceof ShopProductResult) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($this->resolveFollowUpActionShopProductRole($product) === ProductRoleResolver::ROLE_MAIN_DEVICE) {
|
||||
$mainDevices[] = $product;
|
||||
}
|
||||
}
|
||||
|
||||
return $mainDevices;
|
||||
}
|
||||
|
||||
private function buildFollowUpActionProductQuery(ShopProductResult $product): string
|
||||
{
|
||||
$parts = [$product->name];
|
||||
|
||||
$productNumber = $this->normalizeOneLine((string) $product->productNumber);
|
||||
if ($productNumber !== '') {
|
||||
$parts[] = $productNumber;
|
||||
}
|
||||
|
||||
return $this->normalizeOneLine(implode(' ', array_filter($parts, static fn(string $part): bool => trim($part) !== '')));
|
||||
}
|
||||
|
||||
private function buildFollowUpActionAnswerAnchor(string $answerText): string
|
||||
{
|
||||
$anchors = [];
|
||||
@@ -5490,14 +5620,14 @@ final readonly class AgentRunner
|
||||
|
||||
private function followUpActionAnswerAlreadyContainsPrice(string $answerText): bool
|
||||
{
|
||||
return preg_match('/(?:\bpreise?\b.{0,80}\d+[,.]\d{2}\s*(?:€|eur\b)|\d+[,.]\d{2}\s*(?:€|eur\b)|(?:€|eur\b)\s*\d+[,.]\d{2})/iu', $answerText) === 1;
|
||||
return preg_match('/(?:\bpreis\b.{0,24}\d+[,.]\d{2}|\d+[,.]\d{2}\s*(?:€|eur)\b|(?:€|eur)\s*\d+[,.]\d{2})/iu', $answerText) === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array<string, mixed>> $actions
|
||||
* @param array<string, bool> $seenActionKeys
|
||||
* @param array<int, array<string, mixed>> $items
|
||||
* @param array{shop_query:string, answer_has_price:bool, answer_text:string, answer_anchor:string, answer_detail_score:int, role_counts:array<string,int>} $context
|
||||
* @param array{shop_query:string, shop_price_query:string, answer_has_price:bool, answer_text:string, answer_anchor:string, answer_detail_score:int, role_counts:array<string,int>} $context
|
||||
*/
|
||||
private function appendFollowUpActions(array &$actions, array &$seenActionKeys, array $items, array $context): void
|
||||
{
|
||||
@@ -5533,7 +5663,7 @@ final readonly class AgentRunner
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $item
|
||||
* @param array{shop_query:string, answer_has_price:bool, answer_text:string, answer_anchor:string, answer_detail_score:int, role_counts:array<string,int>} $context
|
||||
* @param array{shop_query:string, shop_price_query:string, answer_has_price:bool, answer_text:string, answer_anchor:string, answer_detail_score:int, role_counts:array<string,int>} $context
|
||||
*/
|
||||
private function shouldShowFollowUpAction(array $item, array $context): bool
|
||||
{
|
||||
@@ -5558,7 +5688,7 @@ final readonly class AgentRunner
|
||||
}
|
||||
|
||||
if ($actionType === 'price_details') {
|
||||
return $context['shop_query'] !== '' && !$context['answer_has_price'];
|
||||
return $context['shop_price_query'] !== '' && !$context['answer_has_price'];
|
||||
}
|
||||
|
||||
if ($actionType === 'role_filter') {
|
||||
@@ -5658,12 +5788,13 @@ final readonly class AgentRunner
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{shop_query:string, answer_has_price:bool, answer_text:string, answer_anchor:string, answer_detail_score:int, role_counts:array<string,int>} $context
|
||||
* @param array{shop_query:string, shop_price_query:string, answer_has_price:bool, answer_text:string, answer_anchor:string, answer_detail_score:int, role_counts:array<string,int>} $context
|
||||
*/
|
||||
private function renderFollowUpActionPrompt(string $prompt, array $context): string
|
||||
{
|
||||
$rendered = strtr($prompt, [
|
||||
'{shop_query}' => $context['shop_query'],
|
||||
'{shop_price_query}' => $context['shop_price_query'],
|
||||
'{answer_anchor}' => $context['answer_anchor'],
|
||||
]);
|
||||
|
||||
|
||||
@@ -1364,6 +1364,24 @@ final class AgentRunnerConfig
|
||||
?: $this->getOptionalStringList('shop_runtime.query_cleanup.positive_token_filter.code_patterns');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getShopQueryPositiveTokenFilterAdjacentVariantPatterns(): array
|
||||
{
|
||||
return $this->genreStringList('shop_query_runtime.positive_token_filter.adjacent_variant_patterns')
|
||||
?: $this->getOptionalStringList('shop_runtime.query_cleanup.positive_token_filter.adjacent_variant_patterns');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getShopQueryPositiveTokenFilterAdjacentVariantTerms(): array
|
||||
{
|
||||
return $this->genreStringList('shop_query_runtime.positive_token_filter.adjacent_variant_terms')
|
||||
?: $this->getOptionalStringList('shop_runtime.query_cleanup.positive_token_filter.adjacent_variant_terms');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
|
||||
@@ -1366,7 +1366,9 @@ final readonly class RetriexEffectiveConfigProvider
|
||||
|
||||
$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->validateStringList($this->toList($positiveTokenFilter['adjacent_variant_terms'] ?? []), 'genre.configuration_values.shop_query_runtime.positive_token_filter.adjacent_variant_terms', $errors, $warnings);
|
||||
$this->validateRegexPatternList($positiveTokenFilter['code_patterns'] ?? [], 'genre.configuration_values.shop_query_runtime.positive_token_filter.code_patterns', $errors);
|
||||
$this->validateRegexPatternList($positiveTokenFilter['adjacent_variant_patterns'] ?? [], 'genre.configuration_values.shop_query_runtime.positive_token_filter.adjacent_variant_patterns', $errors);
|
||||
}
|
||||
|
||||
foreach ($this->collectGenreConfigurationValueSourcePaths($configurationValues) as $valuePath => $sourcePaths) {
|
||||
|
||||
Reference in New Issue
Block a user