p70+71
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -601,7 +601,7 @@ final class AgentRunnerConfig
|
||||
'prompt' => $prompt,
|
||||
];
|
||||
|
||||
foreach (['action_type', 'target_role'] as $optionalKey) {
|
||||
foreach (['action_type', 'target_role', 'hide_when_answer_detail_score_at_least'] as $optionalKey) {
|
||||
if (isset($item[$optionalKey]) && is_scalar($item[$optionalKey])) {
|
||||
$optionalValue = trim((string) $item[$optionalKey]);
|
||||
if ($optionalValue !== '') {
|
||||
@@ -610,6 +610,28 @@ final class AgentRunnerConfig
|
||||
}
|
||||
}
|
||||
|
||||
if (array_key_exists('requires_answer_anchor', $item) && (is_bool($item['requires_answer_anchor']) || is_scalar($item['requires_answer_anchor']))) {
|
||||
$action['requires_answer_anchor'] = $item['requires_answer_anchor'];
|
||||
}
|
||||
|
||||
if (isset($item['hide_when_answer_matches_any']) && is_array($item['hide_when_answer_matches_any'])) {
|
||||
$patterns = [];
|
||||
foreach ($item['hide_when_answer_matches_any'] as $pattern) {
|
||||
if (!is_scalar($pattern)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$pattern = trim((string) $pattern);
|
||||
if ($pattern !== '') {
|
||||
$patterns[] = $pattern;
|
||||
}
|
||||
}
|
||||
|
||||
if ($patterns !== []) {
|
||||
$action['hide_when_answer_matches_any'] = array_values(array_unique($patterns));
|
||||
}
|
||||
}
|
||||
|
||||
$out[] = $action;
|
||||
}
|
||||
|
||||
|
||||
@@ -543,7 +543,7 @@ final class ChatMessagesConfig
|
||||
'prompt' => $prompt,
|
||||
];
|
||||
|
||||
foreach (['action_type', 'target_role'] as $optionalKey) {
|
||||
foreach (['action_type', 'target_role', 'hide_when_answer_detail_score_at_least'] as $optionalKey) {
|
||||
if (isset($item[$optionalKey]) && is_scalar($item[$optionalKey])) {
|
||||
$optionalValue = trim((string) $item[$optionalKey]);
|
||||
if ($optionalValue !== '') {
|
||||
@@ -552,6 +552,28 @@ final class ChatMessagesConfig
|
||||
}
|
||||
}
|
||||
|
||||
if (array_key_exists('requires_answer_anchor', $item) && (is_bool($item['requires_answer_anchor']) || is_scalar($item['requires_answer_anchor']))) {
|
||||
$action['requires_answer_anchor'] = $item['requires_answer_anchor'];
|
||||
}
|
||||
|
||||
if (isset($item['hide_when_answer_matches_any']) && is_array($item['hide_when_answer_matches_any'])) {
|
||||
$patterns = [];
|
||||
foreach ($item['hide_when_answer_matches_any'] as $pattern) {
|
||||
if (!is_scalar($pattern)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$pattern = trim((string) $pattern);
|
||||
if ($pattern !== '') {
|
||||
$patterns[] = $pattern;
|
||||
}
|
||||
}
|
||||
|
||||
if ($patterns !== []) {
|
||||
$action['hide_when_answer_matches_any'] = array_values(array_unique($patterns));
|
||||
}
|
||||
}
|
||||
|
||||
$out[] = $action;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user