This commit is contained in:
team 1
2026-05-09 20:18:14 +02:00
parent 943c213ac0
commit aae4935d69
6 changed files with 427 additions and 24 deletions

View File

@@ -4939,35 +4939,155 @@ final readonly class AgentRunner
/**
* @param ShopProductResult[] $shopResults
* @return array{shop_query:string, answer_has_price:bool, role_counts:array<string,int>}
* @return array{shop_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
{
$roleCounts = [
$plainAnswerText = $this->normalizeOneLine($this->plainTextFromHtml($answerText));
$roleCounts = $this->buildFollowUpActionRoleCounts($shopResults);
$displayedRoleCounts = $this->buildFollowUpActionDisplayedRoleCounts($shopResults, $plainAnswerText);
if (array_sum($displayedRoleCounts) > 0) {
$roleCounts = $displayedRoleCounts;
}
return [
'shop_query' => $this->normalizeOneLine($shopSearchQuery),
'answer_has_price' => $this->followUpActionAnswerAlreadyContainsPrice($plainAnswerText),
'answer_text' => $plainAnswerText,
'answer_anchor' => $this->buildFollowUpActionAnswerAnchor($plainAnswerText),
'answer_detail_score' => $this->calculateFollowUpActionAnswerDetailScore($plainAnswerText),
'role_counts' => $roleCounts,
];
}
/**
* @return array<string, int>
*/
private function emptyFollowUpActionRoleCounts(): array
{
return [
ProductRoleResolver::ROLE_MAIN_DEVICE => 0,
ProductRoleResolver::ROLE_ACCESSORY_OR_CONSUMABLE => 0,
ProductRoleResolver::ROLE_AMBIGUOUS_MIXED => 0,
ProductRoleResolver::ROLE_UNKNOWN => 0,
];
}
/**
* @param ShopProductResult[] $shopResults
* @return array<string, int>
*/
private function buildFollowUpActionRoleCounts(array $shopResults): array
{
$roleCounts = $this->emptyFollowUpActionRoleCounts();
foreach ($shopResults as $product) {
if (!$product instanceof ShopProductResult) {
continue;
}
$role = $this->resolveFollowUpActionShopProductRole($product);
if (!array_key_exists($role, $roleCounts)) {
$role = ProductRoleResolver::ROLE_UNKNOWN;
}
++$roleCounts[$role];
$this->countFollowUpActionProductRole($roleCounts, $product);
}
return [
'shop_query' => $this->normalizeOneLine($shopSearchQuery),
'answer_has_price' => $this->followUpActionAnswerAlreadyContainsPrice($answerText),
'role_counts' => $roleCounts,
];
return $roleCounts;
}
/**
* @param ShopProductResult[] $shopResults
* @return array<string, int>
*/
private function buildFollowUpActionDisplayedRoleCounts(array $shopResults, string $answerText): array
{
$roleCounts = $this->emptyFollowUpActionRoleCounts();
if ($answerText === '') {
return $roleCounts;
}
foreach ($shopResults as $product) {
if (!$product instanceof ShopProductResult) {
continue;
}
if (!$this->isFollowUpActionProductDisplayedInAnswer($product, $answerText)) {
continue;
}
$this->countFollowUpActionProductRole($roleCounts, $product);
}
return $roleCounts;
}
/**
* @param array<string, int> $roleCounts
*/
private function countFollowUpActionProductRole(array &$roleCounts, ShopProductResult $product): void
{
$role = $this->resolveFollowUpActionShopProductRole($product);
if (!array_key_exists($role, $roleCounts)) {
$role = ProductRoleResolver::ROLE_UNKNOWN;
}
++$roleCounts[$role];
}
private function isFollowUpActionProductDisplayedInAnswer(ShopProductResult $product, string $answerText): bool
{
$normalizedAnswer = mb_strtolower($this->normalizeOneLine($answerText), 'UTF-8');
if ($normalizedAnswer === '') {
return false;
}
$productNumber = $this->normalizeOneLine((string) $product->productNumber);
if ($productNumber !== '' && mb_strlen($productNumber, 'UTF-8') >= 3 && str_contains($normalizedAnswer, mb_strtolower($productNumber, 'UTF-8'))) {
return true;
}
$productName = mb_strtolower($this->normalizeOneLine($product->name), 'UTF-8');
if ($productName === '' || mb_strlen($productName, 'UTF-8') < 16) {
return false;
}
preg_match_all('/[\p{L}\p{N}]+/u', $productName, $matches);
$tokens = array_values(array_unique($matches[0] ?? []));
if (count($tokens) < 3) {
return false;
}
return str_contains($normalizedAnswer, $productName);
}
private function buildFollowUpActionAnswerAnchor(string $answerText): string
{
$anchors = [];
$modelAnchor = $this->referenceAnchorExtractor->extractFirstProductModelAnchor($answerText);
if ($modelAnchor !== '') {
$anchors[] = $modelAnchor;
}
$measurementAnchor = $this->referenceAnchorExtractor->extractFirstMeasurementValueAnchor($answerText);
if ($measurementAnchor !== '') {
$anchors[] = $measurementAnchor;
}
return $this->normalizeOneLine(implode(' ', array_values(array_unique($anchors))));
}
private function calculateFollowUpActionAnswerDetailScore(string $answerText): int
{
$score = 0;
if ($this->referenceAnchorExtractor->extractFirstProductModelAnchor($answerText) !== '') {
++$score;
}
if ($this->referenceAnchorExtractor->extractFirstMeasurementValueAnchor($answerText) !== '') {
++$score;
}
return $score;
}
private function resolveFollowUpActionShopProductRole(ShopProductResult $product): string
@@ -5026,7 +5146,7 @@ final readonly class AgentRunner
* @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, role_counts:array<string,int>} $context
* @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
*/
private function appendFollowUpActions(array &$actions, array &$seenActionKeys, array $items, array $context): void
{
@@ -5062,13 +5182,26 @@ final readonly class AgentRunner
/**
* @param array<string, mixed> $item
* @param array{shop_query:string, answer_has_price:bool, role_counts:array<string,int>} $context
* @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
*/
private function shouldShowFollowUpAction(array $item, array $context): bool
{
$actionType = isset($item['action_type']) && is_scalar($item['action_type']) ? trim((string) $item['action_type']) : '';
$targetRole = isset($item['target_role']) && is_scalar($item['target_role']) ? trim((string) $item['target_role']) : '';
if ($this->followUpActionAnswerMatchesAnyConfiguredPattern($context['answer_text'], $item['hide_when_answer_matches_any'] ?? [])) {
return false;
}
$hideAtDetailScore = $this->optionalFollowUpActionInt($item, 'hide_when_answer_detail_score_at_least');
if ($hideAtDetailScore !== null && $context['answer_detail_score'] >= $hideAtDetailScore) {
return false;
}
if ($this->optionalFollowUpActionBool($item, 'requires_answer_anchor') && $context['answer_anchor'] === '') {
return false;
}
if ($actionType === 'shop_search') {
return $context['shop_query'] !== '';
}
@@ -5081,9 +5214,79 @@ final readonly class AgentRunner
return $context['shop_query'] !== '' && $this->isFollowUpRoleFilterMeaningful($targetRole, $context['role_counts']);
}
if ($actionType === 'technical_details') {
return $context['answer_anchor'] !== '';
}
return true;
}
private function optionalFollowUpActionBool(array $item, string $key): bool
{
if (!array_key_exists($key, $item)) {
return false;
}
$value = $item[$key];
if (is_bool($value)) {
return $value;
}
if (is_scalar($value)) {
$normalized = mb_strtolower(trim((string) $value), 'UTF-8');
return in_array($normalized, ['1', 'true', 'yes', 'on'], true);
}
return false;
}
private function optionalFollowUpActionInt(array $item, string $key): ?int
{
if (!isset($item[$key]) || !is_scalar($item[$key])) {
return null;
}
$value = trim((string) $item[$key]);
if ($value === '' || !preg_match('/^-?\d+$/', $value)) {
return null;
}
return (int) $value;
}
/**
* @param mixed $patterns
*/
private function followUpActionAnswerMatchesAnyConfiguredPattern(string $answerText, mixed $patterns): bool
{
if (!is_array($patterns) || $answerText === '') {
return false;
}
foreach ($patterns as $pattern) {
if (!is_scalar($pattern)) {
continue;
}
$pattern = trim((string) $pattern);
if ($pattern === '') {
continue;
}
if (@preg_match($pattern, '') === false) {
continue;
}
if (@preg_match($pattern, $answerText) === 1) {
return true;
}
}
return false;
}
/**
* @param array<string, int> $roleCounts
*/
@@ -5093,21 +5296,25 @@ final readonly class AgentRunner
return false;
}
$totalProducts = array_sum($roleCounts);
if ($totalProducts <= 0) {
$knownRoleTotal = ($roleCounts[ProductRoleResolver::ROLE_MAIN_DEVICE] ?? 0)
+ ($roleCounts[ProductRoleResolver::ROLE_ACCESSORY_OR_CONSUMABLE] ?? 0);
if ($knownRoleTotal <= 0) {
return false;
}
return $roleCounts[$targetRole] < $totalProducts;
return $roleCounts[$targetRole] < $knownRoleTotal;
}
/**
* @param array{shop_query:string, answer_has_price:bool, role_counts:array<string,int>} $context
* @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
*/
private function renderFollowUpActionPrompt(string $prompt, array $context): string
{
$shopQuery = $context['shop_query'];
$rendered = str_replace('{shop_query}', $shopQuery, $prompt);
$rendered = strtr($prompt, [
'{shop_query}' => $context['shop_query'],
'{answer_anchor}' => $context['answer_anchor'],
]);
return $this->normalizeOneLine($rendered);
}