p77+78+79

This commit is contained in:
team 1
2026-05-10 09:09:51 +02:00
parent 96375668b2
commit 374c0c1f7f
8 changed files with 329 additions and 88 deletions

View File

@@ -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'],
]);