patch 20f
This commit is contained in:
@@ -58,11 +58,13 @@ final readonly class AgentRunner
|
||||
$sources = [];
|
||||
$optimizedShopQuery = '';
|
||||
$shopSearchQuery = '';
|
||||
$forcedShopSearchQuery = '';
|
||||
$commerceHistoryContext = '';
|
||||
$attemptedShopRepair = false;
|
||||
$usedShopRepair = false;
|
||||
$shopRepairQueries = [];
|
||||
$shopSearchAttempted = false;
|
||||
$explicitShopRoutingForced = false;
|
||||
$primaryShopSearchHadSystemFailure = false;
|
||||
$historyNotices = [];
|
||||
|
||||
@@ -107,12 +109,56 @@ final readonly class AgentRunner
|
||||
$this->addSource($sources, $this->agentRunnerConfig->getExternalUrlSourceLabel());
|
||||
}
|
||||
|
||||
$commerceIntent = $this->detectCommerceIntentForRouting(
|
||||
$originalPromptHasExplicitShopSignal = $this->containsExplicitShopRoutingSignal($originalPrompt);
|
||||
$routingPromptHasExplicitShopSignal = $this->containsExplicitShopRoutingSignal($routingPrompt);
|
||||
|
||||
if (
|
||||
$originalPromptHasExplicitShopSignal
|
||||
&& !$this->isCommercialTableFollowUpPrompt($routingPrompt)
|
||||
) {
|
||||
// Explicit user routing terms such as "shop" must never be lost
|
||||
// through LLM normalization before the commerce gate is evaluated.
|
||||
$routingPrompt = $originalPrompt;
|
||||
$routingPromptHasExplicitShopSignal = true;
|
||||
}
|
||||
|
||||
$forcedShopSearchQuery = $this->resolveForcedCommercialFollowUpShopQuery(
|
||||
$routingPrompt,
|
||||
$userId,
|
||||
$requestContextHint
|
||||
);
|
||||
|
||||
$explicitShopRoutingForced = $forcedShopSearchQuery === ''
|
||||
&& ($originalPromptHasExplicitShopSignal || $routingPromptHasExplicitShopSignal);
|
||||
|
||||
$commerceIntent = ($forcedShopSearchQuery !== '' || $explicitShopRoutingForced)
|
||||
? CommerceIntentLite::PRODUCT_SEARCH
|
||||
: $this->detectCommerceIntentForRouting(
|
||||
$routingPrompt,
|
||||
$userId,
|
||||
$requestContextHint
|
||||
);
|
||||
|
||||
if ($forcedShopSearchQuery !== '') {
|
||||
$this->agentLogger->info('Forced commercial follow-up into shop routing', [
|
||||
'userId' => $userId,
|
||||
'prompt' => $prompt,
|
||||
'routingPrompt' => $routingPrompt,
|
||||
'forcedShopSearchQuery' => $forcedShopSearchQuery,
|
||||
'hasRequestContextHint' => trim($requestContextHint) !== '',
|
||||
]);
|
||||
}
|
||||
|
||||
if ($explicitShopRoutingForced) {
|
||||
$this->agentLogger->info('Forced explicit shop signal into commerce routing', [
|
||||
'userId' => $userId,
|
||||
'prompt' => $prompt,
|
||||
'routingPrompt' => $routingPrompt,
|
||||
'originalPromptHasExplicitShopSignal' => $originalPromptHasExplicitShopSignal,
|
||||
'routingPromptHasExplicitShopSignal' => $routingPromptHasExplicitShopSignal,
|
||||
]);
|
||||
}
|
||||
|
||||
yield $this->systemMsg($this->agentRunnerConfig->getRetrieveKnowledgeMessage(), 'think');
|
||||
|
||||
$knowledgeRetrievalPrompt = $this->buildKnowledgeRetrievalPrompt(
|
||||
@@ -171,18 +217,23 @@ final readonly class AgentRunner
|
||||
$this->addSource($sources, $this->agentRunnerConfig->getConversationHistorySourceLabel());
|
||||
}
|
||||
|
||||
$optimizedShopQuery = yield from $this->buildOptimizedShopQuery(
|
||||
$routingPrompt,
|
||||
$userId,
|
||||
$commerceHistoryContext
|
||||
);
|
||||
if ($forcedShopSearchQuery !== '') {
|
||||
$optimizedShopQuery = '';
|
||||
$shopSearchQuery = $forcedShopSearchQuery;
|
||||
} else {
|
||||
$optimizedShopQuery = yield from $this->buildOptimizedShopQuery(
|
||||
$routingPrompt,
|
||||
$userId,
|
||||
$commerceHistoryContext
|
||||
);
|
||||
|
||||
$shopSearchQuery = $this->resolveShopSearchQuery(
|
||||
prompt: $routingPrompt,
|
||||
optimizedShopQuery: $optimizedShopQuery,
|
||||
commerceHistoryContext: $commerceHistoryContext,
|
||||
userId: $userId
|
||||
);
|
||||
$shopSearchQuery = $this->resolveShopSearchQuery(
|
||||
prompt: $routingPrompt,
|
||||
optimizedShopQuery: $optimizedShopQuery,
|
||||
commerceHistoryContext: $commerceHistoryContext,
|
||||
userId: $userId
|
||||
);
|
||||
}
|
||||
|
||||
if ($shopSearchQuery === '') {
|
||||
$this->agentLogger->info('Commerce search skipped because no concrete shop query could be resolved', [
|
||||
@@ -937,6 +988,76 @@ final readonly class AgentRunner
|
||||
return (string) ($commerceMeta['intent'] ?? CommerceIntentLite::NONE);
|
||||
}
|
||||
|
||||
private function containsExplicitShopRoutingSignal(string $prompt): bool
|
||||
{
|
||||
$normalized = $this->normalizeFollowUpText($prompt);
|
||||
|
||||
if ($normalized === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($this->agentRunnerConfig->getFollowUpExplicitCommercialSignalTerms() as $signal) {
|
||||
$signal = $this->normalizeFollowUpText($signal);
|
||||
|
||||
if ($signal === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$pattern = '/(?<![\p{L}\p{N}])' . preg_quote($signal, '/') . '(?![\p{L}\p{N}])/u';
|
||||
|
||||
if (preg_match($pattern, $normalized) === 1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function resolveForcedCommercialFollowUpShopQuery(
|
||||
string $prompt,
|
||||
string $userId,
|
||||
string $requestContextHint
|
||||
): string {
|
||||
if (!$this->isCommercialTableFollowUpPrompt($prompt)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$commerceHistoryContext = $this->buildCommerceHistoryContext($userId, $requestContextHint);
|
||||
$query = $this->resolveCommercialTableFollowUpShopQuery($commerceHistoryContext, $userId);
|
||||
|
||||
if ($query !== '' && !$this->isMetaOnlyShopQuery($query)) {
|
||||
return $query;
|
||||
}
|
||||
|
||||
$contextQuery = $this->extractContextualShopSearchQuery($commerceHistoryContext);
|
||||
if ($contextQuery !== '' && !$this->isMetaOnlyShopQuery($contextQuery)) {
|
||||
return $contextQuery;
|
||||
}
|
||||
|
||||
$extendedHistoryBudget = $this->agentRunnerConfig->getShopQueryContextFallbackHistoryBudgetChars();
|
||||
if ($extendedHistoryBudget > mb_strlen($commerceHistoryContext, 'UTF-8')) {
|
||||
$extendedHistory = $this->contextService->buildUserContextWithinBudget($userId, $extendedHistoryBudget);
|
||||
$contextQuery = $this->extractContextualShopSearchQuery($extendedHistory);
|
||||
|
||||
if ($contextQuery !== '' && !$this->isMetaOnlyShopQuery($contextQuery)) {
|
||||
return $contextQuery;
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->agentRunnerConfig->shouldUseFullHistoryForShopQueryContextFallback()) {
|
||||
$fullHistory = $this->contextService->buildUserContext($userId, true);
|
||||
$contextQuery = $this->extractContextualShopSearchQuery($fullHistory);
|
||||
|
||||
if ($contextQuery !== '' && !$this->isMetaOnlyShopQuery($contextQuery)) {
|
||||
return $contextQuery;
|
||||
}
|
||||
}
|
||||
|
||||
// Last-resort fallback for explicit commercial table follow-ups.
|
||||
// This keeps the request in the shop path instead of falling back to RAG-only.
|
||||
return trim($this->agentRunnerConfig->getCommercialTableFollowUpQueryTemplateWithoutModel());
|
||||
}
|
||||
|
||||
private function detectCommerceIntentForRouting(
|
||||
string $prompt,
|
||||
string $userId,
|
||||
@@ -954,13 +1075,10 @@ final readonly class AgentRunner
|
||||
|
||||
$commerceHistoryContext = $this->buildCommerceHistoryContext($userId, $requestContextHint);
|
||||
|
||||
if (!$this->commercialTableFollowUpHistoryHasAnchor($commerceHistoryContext)) {
|
||||
return $commerceIntent;
|
||||
}
|
||||
|
||||
$this->agentLogger->info('Promoted commercial table follow-up to shop intent', [
|
||||
'userId' => $userId,
|
||||
'prompt' => $prompt,
|
||||
'hasHistoryAnchor' => $this->commercialTableFollowUpHistoryHasAnchor($commerceHistoryContext),
|
||||
'hasRequestContextHint' => trim($requestContextHint) !== '',
|
||||
]);
|
||||
|
||||
@@ -1149,6 +1267,31 @@ final readonly class AgentRunner
|
||||
return (string) end($turns);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private function extractHistoryTurnsNewestFirst(string $history): array
|
||||
{
|
||||
$history = trim($history);
|
||||
|
||||
if ($history === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
$parts = preg_split($this->agentRunnerConfig->getFollowUpHistoryTurnSplitPattern(), $history);
|
||||
|
||||
if ($parts === false || $parts === []) {
|
||||
return [$history];
|
||||
}
|
||||
|
||||
$turns = array_values(array_filter(
|
||||
array_map(static fn(string $part): string => trim($part), $parts),
|
||||
static fn(string $part): bool => $part !== ''
|
||||
));
|
||||
|
||||
return array_reverse($turns);
|
||||
}
|
||||
|
||||
private function extractFirstTestomatModelAnchor(string $text): string
|
||||
{
|
||||
if (preg_match($this->agentRunnerConfig->getFollowUpReferenceAnchorTestomatModelPattern(), $text, $matches) !== 1) {
|
||||
@@ -1300,7 +1443,10 @@ final readonly class AgentRunner
|
||||
string $userId
|
||||
): string {
|
||||
if ($this->isCommercialTableFollowUpPrompt($prompt)) {
|
||||
$commercialTableContextQuery = $this->extractCommercialTableFollowUpShopQuery($commerceHistoryContext);
|
||||
$commercialTableContextQuery = $this->resolveCommercialTableFollowUpShopQuery(
|
||||
$commerceHistoryContext,
|
||||
$userId
|
||||
);
|
||||
|
||||
if ($commercialTableContextQuery !== '' && !$this->isMetaOnlyShopQuery($commercialTableContextQuery)) {
|
||||
return $commercialTableContextQuery;
|
||||
@@ -1344,38 +1490,69 @@ final readonly class AgentRunner
|
||||
return '';
|
||||
}
|
||||
|
||||
private function resolveCommercialTableFollowUpShopQuery(string $commerceHistoryContext, string $userId): string
|
||||
{
|
||||
$query = $this->extractCommercialTableFollowUpShopQuery($commerceHistoryContext);
|
||||
|
||||
if ($query !== '') {
|
||||
return $query;
|
||||
}
|
||||
|
||||
$extendedHistoryBudget = $this->agentRunnerConfig->getShopQueryContextFallbackHistoryBudgetChars();
|
||||
|
||||
if ($extendedHistoryBudget > mb_strlen($commerceHistoryContext, 'UTF-8')) {
|
||||
$extendedHistory = $this->contextService->buildUserContextWithinBudget($userId, $extendedHistoryBudget);
|
||||
$query = $this->extractCommercialTableFollowUpShopQuery($extendedHistory);
|
||||
|
||||
if ($query !== '') {
|
||||
return $query;
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->agentRunnerConfig->shouldUseFullHistoryForShopQueryContextFallback()) {
|
||||
return $this->extractCommercialTableFollowUpShopQuery(
|
||||
$this->contextService->buildUserContext($userId, true)
|
||||
);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
private function extractCommercialTableFollowUpShopQuery(string $commerceHistoryContext): string
|
||||
{
|
||||
if (!$this->agentRunnerConfig->isCommercialTableFollowUpEnabled()) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$turn = $this->extractLatestHistoryTurn($commerceHistoryContext);
|
||||
$hasIndicatorContext = false;
|
||||
|
||||
if ($turn === '') {
|
||||
return '';
|
||||
foreach ($this->extractHistoryTurnsNewestFirst($commerceHistoryContext) as $turn) {
|
||||
if (!$this->matchesAnyConfiguredPattern(
|
||||
$turn,
|
||||
$this->agentRunnerConfig->getCommercialTableFollowUpIndicatorMarkerPatterns()
|
||||
)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$hasIndicatorContext = true;
|
||||
$model = $this->extractFirstTestomatModelAnchor($turn);
|
||||
|
||||
if ($model !== '') {
|
||||
$query = str_replace(
|
||||
'{model}',
|
||||
$model,
|
||||
$this->agentRunnerConfig->getCommercialTableFollowUpQueryTemplateWithModel()
|
||||
);
|
||||
|
||||
return trim((string) preg_replace('/\s+/u', ' ', $query));
|
||||
}
|
||||
}
|
||||
|
||||
if (!$this->matchesAnyConfiguredPattern(
|
||||
$turn,
|
||||
$this->agentRunnerConfig->getCommercialTableFollowUpIndicatorMarkerPatterns()
|
||||
)) {
|
||||
return '';
|
||||
if ($hasIndicatorContext) {
|
||||
return trim($this->agentRunnerConfig->getCommercialTableFollowUpQueryTemplateWithoutModel());
|
||||
}
|
||||
|
||||
$model = $this->extractFirstTestomatModelAnchor($turn);
|
||||
|
||||
if ($model !== '') {
|
||||
$query = str_replace(
|
||||
'{model}',
|
||||
$model,
|
||||
$this->agentRunnerConfig->getCommercialTableFollowUpQueryTemplateWithModel()
|
||||
);
|
||||
|
||||
return trim((string) preg_replace('/\s+/u', ' ', $query));
|
||||
}
|
||||
|
||||
return trim($this->agentRunnerConfig->getCommercialTableFollowUpQueryTemplateWithoutModel());
|
||||
return '';
|
||||
}
|
||||
|
||||
private function isCommercialTableFollowUpPrompt(string $prompt): bool
|
||||
@@ -1384,10 +1561,36 @@ final readonly class AgentRunner
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->matchesAnyConfiguredPattern(
|
||||
$this->normalizeFollowUpText($prompt),
|
||||
$normalized = $this->normalizeFollowUpText($prompt);
|
||||
|
||||
if ($this->matchesAnyConfiguredPattern(
|
||||
$normalized,
|
||||
$this->agentRunnerConfig->getCommercialTableFollowUpPromptPatterns()
|
||||
);
|
||||
)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$tokens = $this->tokenizeMetaGuardText($normalized);
|
||||
if ($tokens === []) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$hasTableReference = count(array_intersect(
|
||||
$tokens,
|
||||
$this->agentRunnerConfig->getCommercialTableFollowUpTableTerms()
|
||||
)) > 0;
|
||||
|
||||
if (!$hasTableReference) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($this->agentRunnerConfig->getCommercialTableFollowUpCommercialTerms() as $term) {
|
||||
if (in_array($this->normalizeFollowUpText($term), $tokens, true)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function commercialTableFollowUpHistoryHasAnchor(string $commerceHistoryContext): bool
|
||||
|
||||
Reference in New Issue
Block a user