patch 20h

This commit is contained in:
team 1
2026-05-03 08:27:45 +02:00
parent dbfc079bde
commit e54d7ecbe7
2 changed files with 284 additions and 203 deletions

View File

@@ -58,15 +58,15 @@ final readonly class AgentRunner
$sources = [];
$optimizedShopQuery = '';
$shopSearchQuery = '';
$forcedShopSearchQuery = '';
$commerceHistoryContext = '';
$attemptedShopRepair = false;
$usedShopRepair = false;
$shopRepairQueries = [];
$shopSearchAttempted = false;
$explicitShopRoutingForced = false;
$primaryShopSearchHadSystemFailure = false;
$historyNotices = [];
$commerceIntent = CommerceIntentLite::NONE;
$shopSearchSkippedBecauseNoQuery = false;
$this->agentLogger->info('Agent run started', [
'userId' => $userId,
@@ -83,7 +83,12 @@ final readonly class AgentRunner
stageLabel: 'Antwort wird vorbereitet',
ragCount: null,
shopCount: null,
shopCountMode: 'not_requested',
shopCountMode: $this->resolveShopCountModeForMeta(
commerceIntent: $commerceIntent,
shopSearchAttempted: $shopSearchAttempted,
shopSearchHadSystemFailure: $primaryShopSearchHadSystemFailure,
shopSearchSkippedBecauseNoQuery: $shopSearchSkippedBecauseNoQuery
),
sourceLabels: $sources,
confidenceLabel: 'Beleglage wird geprüft'
),
@@ -109,54 +114,44 @@ final readonly class AgentRunner
$this->addSource($sources, $this->agentRunnerConfig->getExternalUrlSourceLabel());
}
$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(
$commerceIntent = $this->detectCommerceIntentForRouting(
$routingPrompt,
$userId,
$requestContextHint
);
$originalCommerceIntent = $this->detectCommerceIntentForRouting(
$originalPrompt,
$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', [
if (!$this->isCommerceIntent($commerceIntent) && $this->isCommerceIntent($originalCommerceIntent)) {
$this->agentLogger->info('Promoted normalized routing back to explicit original commerce intent', [
'userId' => $userId,
'prompt' => $prompt,
'originalPrompt' => $originalPrompt,
'routingPrompt' => $routingPrompt,
'forcedShopSearchQuery' => $forcedShopSearchQuery,
'hasRequestContextHint' => trim($requestContextHint) !== '',
'originalCommerceIntent' => $originalCommerceIntent,
]);
$commerceIntent = $originalCommerceIntent;
}
if ($explicitShopRoutingForced) {
$this->agentLogger->info('Forced explicit shop signal into commerce routing', [
'userId' => $userId,
'prompt' => $prompt,
'routingPrompt' => $routingPrompt,
'originalPromptHasExplicitShopSignal' => $originalPromptHasExplicitShopSignal,
'routingPromptHasExplicitShopSignal' => $routingPromptHasExplicitShopSignal,
]);
if ($this->isCommerceIntent($commerceIntent)) {
yield $this->systemMsg(
$this->buildProductionUiMetaMessage(
stageLabel: 'Shop-Routing erkannt',
ragCount: null,
shopCount: null,
shopCountMode: $this->resolveShopCountModeForMeta(
commerceIntent: $commerceIntent,
shopSearchAttempted: $shopSearchAttempted,
shopSearchHadSystemFailure: $primaryShopSearchHadSystemFailure,
shopSearchSkippedBecauseNoQuery: $shopSearchSkippedBecauseNoQuery
),
sourceLabels: $sources,
confidenceLabel: 'Shopdaten werden geprüft'
),
'meta'
);
}
yield $this->systemMsg($this->agentRunnerConfig->getRetrieveKnowledgeMessage(), 'think');
@@ -179,7 +174,12 @@ final readonly class AgentRunner
stageLabel: 'RAG-Wissen wurde durchsucht',
ragCount: count($knowledgeChunks),
shopCount: null,
shopCountMode: 'not_requested',
shopCountMode: $this->resolveShopCountModeForMeta(
commerceIntent: $commerceIntent,
shopSearchAttempted: $shopSearchAttempted,
shopSearchHadSystemFailure: $primaryShopSearchHadSystemFailure,
shopSearchSkippedBecauseNoQuery: $shopSearchSkippedBecauseNoQuery
),
sourceLabels: $sources,
confidenceLabel: $this->resolveRagEvidenceConfidenceLabel($knowledgeEvidenceState)
),
@@ -217,23 +217,18 @@ final readonly class AgentRunner
$this->addSource($sources, $this->agentRunnerConfig->getConversationHistorySourceLabel());
}
if ($forcedShopSearchQuery !== '') {
$optimizedShopQuery = '';
$shopSearchQuery = $forcedShopSearchQuery;
} else {
$optimizedShopQuery = yield from $this->buildOptimizedShopQuery(
$routingPrompt,
$userId,
$commerceHistoryContext
);
$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', [
@@ -247,6 +242,7 @@ final readonly class AgentRunner
'hasRequestContextHint' => trim($requestContextHint) !== '',
]);
$shopSearchSkippedBecauseNoQuery = true;
$noConcreteShopQueryMessage = $this->agentRunnerConfig->getNoConcreteShopQueryMessage();
yield $this->systemMsg(
@@ -254,7 +250,12 @@ final readonly class AgentRunner
stageLabel: 'Mehr Kontext nötig',
ragCount: count($knowledgeChunks),
shopCount: null,
shopCountMode: 'not_requested',
shopCountMode: $this->resolveShopCountModeForMeta(
commerceIntent: $commerceIntent,
shopSearchAttempted: $shopSearchAttempted,
shopSearchHadSystemFailure: $primaryShopSearchHadSystemFailure,
shopSearchSkippedBecauseNoQuery: $shopSearchSkippedBecauseNoQuery
),
sourceLabels: $sources,
confidenceLabel: 'mehr Kontext nötig',
completed: true
@@ -483,7 +484,12 @@ final readonly class AgentRunner
stageLabel: 'Antwort wird generiert',
ragCount: count($knowledgeChunks),
shopCount: $primaryShopSearchHadSystemFailure ? null : ($shopSearchAttempted ? count($shopResults) : null),
shopCountMode: $primaryShopSearchHadSystemFailure ? 'unavailable' : ($shopSearchAttempted ? 'count' : 'not_requested'),
shopCountMode: $this->resolveShopCountModeForMeta(
commerceIntent: $commerceIntent,
shopSearchAttempted: $shopSearchAttempted,
shopSearchHadSystemFailure: $primaryShopSearchHadSystemFailure,
shopSearchSkippedBecauseNoQuery: $shopSearchSkippedBecauseNoQuery
),
sourceLabels: $sources,
confidenceLabel: $this->resolveProductionUiConfidenceLabel(
hasKnowledge: $this->isDirectKnowledgeEvidence($knowledgeEvidenceState),
@@ -523,7 +529,12 @@ final readonly class AgentRunner
stageLabel: 'Abgeschlossen',
ragCount: count($knowledgeChunks),
shopCount: $primaryShopSearchHadSystemFailure ? null : ($shopSearchAttempted ? count($shopResults) : null),
shopCountMode: $primaryShopSearchHadSystemFailure ? 'unavailable' : ($shopSearchAttempted ? 'count' : 'not_requested'),
shopCountMode: $this->resolveShopCountModeForMeta(
commerceIntent: $commerceIntent,
shopSearchAttempted: $shopSearchAttempted,
shopSearchHadSystemFailure: $primaryShopSearchHadSystemFailure,
shopSearchSkippedBecauseNoQuery: $shopSearchSkippedBecauseNoQuery
),
sourceLabels: $sources,
confidenceLabel: $this->resolveProductionUiConfidenceLabel(
hasKnowledge: $this->isDirectKnowledgeEvidence($knowledgeEvidenceState),
@@ -598,7 +609,12 @@ final readonly class AgentRunner
stageLabel: 'Antwort wurde unterbrochen',
ragCount: count($knowledgeChunks),
shopCount: $primaryShopSearchHadSystemFailure ? null : ($shopSearchAttempted ? count($shopResults) : null),
shopCountMode: $primaryShopSearchHadSystemFailure ? 'unavailable' : ($shopSearchAttempted ? 'count' : 'not_requested'),
shopCountMode: $this->resolveShopCountModeForMeta(
commerceIntent: $commerceIntent,
shopSearchAttempted: $shopSearchAttempted,
shopSearchHadSystemFailure: $primaryShopSearchHadSystemFailure,
shopSearchSkippedBecauseNoQuery: $shopSearchSkippedBecauseNoQuery
),
sourceLabels: $sources,
confidenceLabel: 'nicht abgeschlossen',
completed: true
@@ -988,76 +1004,6 @@ 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,
@@ -1073,12 +1019,9 @@ final readonly class AgentRunner
return $commerceIntent;
}
$commerceHistoryContext = $this->buildCommerceHistoryContext($userId, $requestContextHint);
$this->agentLogger->info('Promoted commercial table follow-up to shop intent', [
'userId' => $userId,
'prompt' => $prompt,
'hasHistoryAnchor' => $this->commercialTableFollowUpHistoryHasAnchor($commerceHistoryContext),
'hasRequestContextHint' => trim($requestContextHint) !== '',
]);
@@ -1281,7 +1224,7 @@ final readonly class AgentRunner
$parts = preg_split($this->agentRunnerConfig->getFollowUpHistoryTurnSplitPattern(), $history);
if ($parts === false || $parts === []) {
return [$history];
return [];
}
$turns = array_values(array_filter(
@@ -1443,13 +1386,12 @@ final readonly class AgentRunner
string $userId
): string {
if ($this->isCommercialTableFollowUpPrompt($prompt)) {
$commercialTableContextQuery = $this->resolveCommercialTableFollowUpShopQuery(
$commerceHistoryContext,
$userId
);
foreach ($this->buildCommercialTableFollowUpContextCandidates($commerceHistoryContext, $userId) as $contextCandidate) {
$commercialTableContextQuery = $this->extractCommercialTableFollowUpShopQuery($contextCandidate);
if ($commercialTableContextQuery !== '' && !$this->isMetaOnlyShopQuery($commercialTableContextQuery)) {
return $commercialTableContextQuery;
if ($commercialTableContextQuery !== '' && !$this->isMetaOnlyShopQuery($commercialTableContextQuery)) {
return $commercialTableContextQuery;
}
}
}
@@ -1490,32 +1432,34 @@ final readonly class AgentRunner
return '';
}
private function resolveCommercialTableFollowUpShopQuery(string $commerceHistoryContext, string $userId): string
/**
* @return string[]
*/
private function buildCommercialTableFollowUpContextCandidates(string $commerceHistoryContext, string $userId): array
{
$query = $this->extractCommercialTableFollowUpShopQuery($commerceHistoryContext);
$candidates = [];
if ($query !== '') {
return $query;
$commerceHistoryContext = trim($commerceHistoryContext);
if ($commerceHistoryContext !== '') {
$candidates[] = $commerceHistoryContext;
}
$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;
$extendedHistory = trim($this->contextService->buildUserContextWithinBudget($userId, $extendedHistoryBudget));
if ($extendedHistory !== '') {
$candidates[] = $extendedHistory;
}
}
if ($this->agentRunnerConfig->shouldUseFullHistoryForShopQueryContextFallback()) {
return $this->extractCommercialTableFollowUpShopQuery(
$this->contextService->buildUserContext($userId, true)
);
$fullHistory = trim($this->contextService->buildUserContext($userId, true));
if ($fullHistory !== '') {
$candidates[] = $fullHistory;
}
}
return '';
return array_values(array_unique($candidates));
}
private function extractCommercialTableFollowUpShopQuery(string $commerceHistoryContext): string
@@ -1524,7 +1468,7 @@ final readonly class AgentRunner
return '';
}
$hasIndicatorContext = false;
$fallbackWithoutModel = '';
foreach ($this->extractHistoryTurnsNewestFirst($commerceHistoryContext) as $turn) {
if (!$this->matchesAnyConfiguredPattern(
@@ -1534,7 +1478,6 @@ final readonly class AgentRunner
continue;
}
$hasIndicatorContext = true;
$model = $this->extractFirstTestomatModelAnchor($turn);
if ($model !== '') {
@@ -1546,13 +1489,11 @@ final readonly class AgentRunner
return trim((string) preg_replace('/\s+/u', ' ', $query));
}
$fallbackWithoutModel = trim($this->agentRunnerConfig->getCommercialTableFollowUpQueryTemplateWithoutModel());
}
if ($hasIndicatorContext) {
return trim($this->agentRunnerConfig->getCommercialTableFollowUpQueryTemplateWithoutModel());
}
return '';
return $fallbackWithoutModel;
}
private function isCommercialTableFollowUpPrompt(string $prompt): bool
@@ -1561,47 +1502,9 @@ final readonly class AgentRunner
return false;
}
$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
{
if (trim($commerceHistoryContext) === '') {
return false;
}
return $this->matchesAnyConfiguredPattern(
$commerceHistoryContext,
$this->agentRunnerConfig->getCommercialTableFollowUpHistoryAnchorPatterns()
$this->normalizeFollowUpText($prompt),
$this->agentRunnerConfig->getCommercialTableFollowUpPromptPatterns()
);
}
@@ -2632,6 +2535,31 @@ final readonly class AgentRunner
return trim($value);
}
private function resolveShopCountModeForMeta(
string $commerceIntent,
bool $shopSearchAttempted,
bool $shopSearchHadSystemFailure,
bool $shopSearchSkippedBecauseNoQuery = false
): string {
if ($shopSearchHadSystemFailure) {
return 'unavailable';
}
if ($shopSearchAttempted) {
return 'count';
}
if ($shopSearchSkippedBecauseNoQuery) {
return 'not_resolved';
}
if ($this->isCommerceIntent($commerceIntent)) {
return 'loading';
}
return 'not_requested';
}
/**
* @param string[] $sourceLabels
*/
@@ -2652,6 +2580,7 @@ final readonly class AgentRunner
'count' => 'Shop-Treffer: ' . max(0, (int) $shopCount),
'loading' => 'Shop-Treffer: wird geladen',
'unavailable' => 'Shop-Treffer: nicht verfügbar',
'not_resolved' => 'Shop-Treffer: keine Suchquery',
default => 'Shop-Treffer: nicht angefragt',
};
$statusLabel = $completed ? 'Status: abgeschlossen' : 'Status: läuft';