p69
This commit is contained in:
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace App\Agent;
|
||||
|
||||
use App\Commerce\Dto\ShopProductResult;
|
||||
use App\Commerce\ProductRoleResolver;
|
||||
use App\Commerce\SearchRepairService;
|
||||
use App\Commerce\ShopSearchService;
|
||||
use App\Config\AgentRunnerConfig;
|
||||
@@ -704,7 +705,10 @@ final readonly class AgentRunner
|
||||
isCommerceIntent: $this->isCommerceIntent($commerceIntent),
|
||||
hasShopResults: $shopResults !== [],
|
||||
hasKnowledge: $this->isDirectKnowledgeEvidence($knowledgeEvidenceState),
|
||||
shopSearchHadSystemFailure: $primaryShopSearchHadSystemFailure
|
||||
shopSearchHadSystemFailure: $primaryShopSearchHadSystemFailure,
|
||||
shopResults: $shopResults,
|
||||
shopSearchQuery: $shopSearchQuery,
|
||||
answerText: $fullOutput
|
||||
);
|
||||
|
||||
if ($followUpActionsMessage !== '') {
|
||||
@@ -4861,27 +4865,54 @@ final readonly class AgentRunner
|
||||
return $this->agentRunnerConfig->getProductionUiTemplate('relevance_default');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ShopProductResult[] $shopResults
|
||||
*/
|
||||
private function buildFollowUpActionsMessage(
|
||||
bool $isCommerceIntent,
|
||||
bool $hasShopResults,
|
||||
bool $hasKnowledge,
|
||||
bool $shopSearchHadSystemFailure
|
||||
bool $shopSearchHadSystemFailure,
|
||||
array $shopResults,
|
||||
string $shopSearchQuery,
|
||||
string $answerText
|
||||
): string {
|
||||
if (!$this->agentRunnerConfig->isProductionUiFollowUpActionsEnabled()) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$context = $this->buildFollowUpActionContext(
|
||||
shopResults: $shopResults,
|
||||
shopSearchQuery: $shopSearchQuery,
|
||||
answerText: $answerText
|
||||
);
|
||||
|
||||
$actions = [];
|
||||
$seenActionKeys = [];
|
||||
|
||||
if ($hasShopResults) {
|
||||
$this->appendFollowUpActions($actions, $seenActionKeys, $this->agentRunnerConfig->getProductionUiFollowUpActions('shop_results'));
|
||||
} elseif ($isCommerceIntent && !$shopSearchHadSystemFailure) {
|
||||
$this->appendFollowUpActions($actions, $seenActionKeys, $this->agentRunnerConfig->getProductionUiFollowUpActions('commerce'));
|
||||
$this->appendFollowUpActions(
|
||||
actions: $actions,
|
||||
seenActionKeys: $seenActionKeys,
|
||||
items: $this->agentRunnerConfig->getProductionUiFollowUpActions('shop_results'),
|
||||
context: $context
|
||||
);
|
||||
} elseif ($isCommerceIntent && !$shopSearchHadSystemFailure && $context['shop_query'] !== '') {
|
||||
$this->appendFollowUpActions(
|
||||
actions: $actions,
|
||||
seenActionKeys: $seenActionKeys,
|
||||
items: $this->agentRunnerConfig->getProductionUiFollowUpActions('commerce'),
|
||||
context: $context
|
||||
);
|
||||
}
|
||||
|
||||
if ($hasKnowledge) {
|
||||
$this->appendFollowUpActions($actions, $seenActionKeys, $this->agentRunnerConfig->getProductionUiFollowUpActions('knowledge'));
|
||||
$this->appendFollowUpActions(
|
||||
actions: $actions,
|
||||
seenActionKeys: $seenActionKeys,
|
||||
items: $this->agentRunnerConfig->getProductionUiFollowUpActions('knowledge'),
|
||||
context: $context
|
||||
);
|
||||
}
|
||||
|
||||
if ($actions === []) {
|
||||
@@ -4907,11 +4938,97 @@ final readonly class AgentRunner
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array{label:string, prompt:string}> $actions
|
||||
* @param array<string, bool> $seenActionKeys
|
||||
* @param array<int, array{label:string, prompt:string}> $items
|
||||
* @param ShopProductResult[] $shopResults
|
||||
* @return array{shop_query:string, answer_has_price:bool, role_counts:array<string,int>}
|
||||
*/
|
||||
private function appendFollowUpActions(array &$actions, array &$seenActionKeys, array $items): void
|
||||
private function buildFollowUpActionContext(array $shopResults, string $shopSearchQuery, string $answerText): array
|
||||
{
|
||||
$roleCounts = [
|
||||
ProductRoleResolver::ROLE_MAIN_DEVICE => 0,
|
||||
ProductRoleResolver::ROLE_ACCESSORY_OR_CONSUMABLE => 0,
|
||||
ProductRoleResolver::ROLE_AMBIGUOUS_MIXED => 0,
|
||||
ProductRoleResolver::ROLE_UNKNOWN => 0,
|
||||
];
|
||||
|
||||
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];
|
||||
}
|
||||
|
||||
return [
|
||||
'shop_query' => $this->normalizeOneLine($shopSearchQuery),
|
||||
'answer_has_price' => $this->followUpActionAnswerAlreadyContainsPrice($answerText),
|
||||
'role_counts' => $roleCounts,
|
||||
];
|
||||
}
|
||||
|
||||
private function resolveFollowUpActionShopProductRole(ShopProductResult $product): string
|
||||
{
|
||||
$primaryText = mb_strtolower($this->normalizeOneLine(implode(' ', [
|
||||
$product->name,
|
||||
(string) $product->productNumber,
|
||||
])), 'UTF-8');
|
||||
|
||||
$hasPrimaryAccessory = $this->containsAnyConfiguredTerm($primaryText, $this->agentRunnerConfig->getNoLlmAccessoryProductRoleKeywords());
|
||||
$hasPrimaryDevice = $this->containsAnyConfiguredTerm($primaryText, $this->agentRunnerConfig->getNoLlmMainDeviceRequestRoleKeywords());
|
||||
|
||||
if ($hasPrimaryAccessory && !$hasPrimaryDevice) {
|
||||
return ProductRoleResolver::ROLE_ACCESSORY_OR_CONSUMABLE;
|
||||
}
|
||||
|
||||
if ($hasPrimaryDevice && !$hasPrimaryAccessory) {
|
||||
return ProductRoleResolver::ROLE_MAIN_DEVICE;
|
||||
}
|
||||
|
||||
if ($hasPrimaryAccessory && $hasPrimaryDevice) {
|
||||
return ProductRoleResolver::ROLE_ACCESSORY_OR_CONSUMABLE;
|
||||
}
|
||||
|
||||
$corpus = mb_strtolower($this->normalizeOneLine(implode(' ', [
|
||||
$product->name,
|
||||
(string) $product->description,
|
||||
(string) $product->customFields,
|
||||
implode(' ', $product->highlights),
|
||||
])), 'UTF-8');
|
||||
|
||||
$hasAccessory = $this->containsAnyConfiguredTerm($corpus, $this->agentRunnerConfig->getNoLlmAccessoryProductRoleKeywords());
|
||||
$hasDevice = $this->containsAnyConfiguredTerm($corpus, $this->agentRunnerConfig->getNoLlmMainDeviceRequestRoleKeywords());
|
||||
|
||||
if ($hasAccessory && !$hasDevice) {
|
||||
return ProductRoleResolver::ROLE_ACCESSORY_OR_CONSUMABLE;
|
||||
}
|
||||
|
||||
if ($hasDevice && !$hasAccessory) {
|
||||
return ProductRoleResolver::ROLE_MAIN_DEVICE;
|
||||
}
|
||||
|
||||
if ($hasAccessory && $hasDevice) {
|
||||
return ProductRoleResolver::ROLE_AMBIGUOUS_MIXED;
|
||||
}
|
||||
|
||||
return ProductRoleResolver::ROLE_UNKNOWN;
|
||||
}
|
||||
|
||||
private function followUpActionAnswerAlreadyContainsPrice(string $answerText): bool
|
||||
{
|
||||
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, role_counts:array<string,int>} $context
|
||||
*/
|
||||
private function appendFollowUpActions(array &$actions, array &$seenActionKeys, array $items, array $context): void
|
||||
{
|
||||
foreach ($items as $item) {
|
||||
$label = trim((string) ($item['label'] ?? ''));
|
||||
@@ -4921,6 +5038,15 @@ final readonly class AgentRunner
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$this->shouldShowFollowUpAction($item, $context)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$actionPrompt = $this->renderFollowUpActionPrompt($actionPrompt, $context);
|
||||
if ($actionPrompt === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$key = mb_strtolower($label . "\n" . $actionPrompt, 'UTF-8');
|
||||
if (isset($seenActionKeys[$key])) {
|
||||
continue;
|
||||
@@ -4934,6 +5060,58 @@ final readonly class AgentRunner
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $item
|
||||
* @param array{shop_query:string, answer_has_price:bool, 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 ($actionType === 'shop_search') {
|
||||
return $context['shop_query'] !== '';
|
||||
}
|
||||
|
||||
if ($actionType === 'price_details') {
|
||||
return $context['shop_query'] !== '' && !$context['answer_has_price'];
|
||||
}
|
||||
|
||||
if ($actionType === 'role_filter') {
|
||||
return $context['shop_query'] !== '' && $this->isFollowUpRoleFilterMeaningful($targetRole, $context['role_counts']);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, int> $roleCounts
|
||||
*/
|
||||
private function isFollowUpRoleFilterMeaningful(string $targetRole, array $roleCounts): bool
|
||||
{
|
||||
if (!isset($roleCounts[$targetRole]) || $roleCounts[$targetRole] <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$totalProducts = array_sum($roleCounts);
|
||||
if ($totalProducts <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $roleCounts[$targetRole] < $totalProducts;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{shop_query:string, answer_has_price:bool, 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);
|
||||
|
||||
return $this->normalizeOneLine($rendered);
|
||||
}
|
||||
|
||||
private function buildShopSearchMetaMessage(
|
||||
string $query,
|
||||
string $commerceIntent,
|
||||
|
||||
Reference in New Issue
Block a user