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

@@ -0,0 +1,152 @@
# RetrieX Patch 20h Total Commerce Routing & Shop Status Fix
## Ziel
Dieser Patch ersetzt die fragmentierten p20d/p20e/p20f/p20g-Nachbesserungen durch einen kumulativen Fix gegen den aktuellen `rag-inprogress.zip`-Stand.
Er adressiert zwei zusammenhängende Fehlerklassen:
1. Explizite oder referenzielle Commerce-/Shop-Anfragen dürfen nicht im RAG-only-Pfad landen.
2. Sobald ein Shop-/Commerce-Routing erkannt ist, darf die Statuskarte während des Ablaufs nicht weiter `Shop-Treffer: nicht angefragt` anzeigen.
## Behobene Fälle
### Expliziter Shop-Intent
Beispiel:
```text
shop testomat 808
```
Erwartung:
- Commerce-Intent wird abgesichert.
- Shop-Pfad wird betreten.
- Status springt nach der Intent-Erkennung auf `Shop-Treffer: wird geladen`.
### Tippfehler-Normalisierung bleibt erhalten
Beispiel:
```text
was kpstet der indikator
```
Erwartung:
- LLM-/Fuzzy-Normalisierung bleibt erhalten.
- Die Anfrage kann intern als Preis-/Shop-Follow-up geroutet werden.
- Es wird keine konkrete Tippfehlerliste eingeführt.
### Referenzieller Tabellen-/Preis-Follow-up
Beispiel:
```text
welche grenzwerte kann der testomat 808 messen
die tabelle mit preisen
```
Erwartung:
- `die tabelle mit preisen` wird als kommerzieller Tabellen-Follow-up in den Shop-Pfad gehoben.
- Die Shop-Query wird aus dem Verlauf robuster abgeleitet, z. B. `Testomat 808 indikator`.
- Die History wird newest-first durchsucht und nicht nur der letzte Turn betrachtet.
### Status-/Meta-Konsistenz
Sobald Commerce-Intent erkannt ist, wird `shopCountMode` zentral berechnet:
- vor Shop-Suche: `loading``Shop-Treffer: wird geladen`
- nach Shop-Suche: `count` → echte Trefferzahl
- Shop-Systemfehler: `unavailable``Shop-Treffer: nicht verfügbar`
- Commerce erkannt, aber keine konkrete Suchquery lösbar: `not_resolved``Shop-Treffer: keine Suchquery`
- nur Nicht-Commerce bleibt `not_requested`
Damit gibt es im Shop-/Commerce-Ablauf keine verstreuten harten `not_requested`-Zwischenstatus mehr.
## Geänderte Datei
- `src/Agent/AgentRunner.php`
## Technische Änderungen
- Initialisierung von `$commerceIntent` und `$shopSearchSkippedBecauseNoQuery` vor dem Try-Block.
- Absicherung gegen Normalisierungsverluste: falls der normalisierte Routing-Prompt keinen Commerce-Intent ergibt, der Originalprompt aber schon, wird der Original-Commerce-Intent übernommen.
- Frühes Meta-Event `Shop-Routing erkannt` bei Commerce-Intent.
- RAG-Zwischenmeta nutzt bei Commerce-Intent `loading` statt `not_requested`.
- `not_requested` wird nicht mehr an mehreren Stellen hart gesetzt, sondern über `resolveShopCountModeForMeta()` zentral berechnet.
- Referenzielle Tabellen-Follow-ups werden ohne vorherige harte History-Anchor-Bedingung in den Shop-Pfad gehoben.
- Tabellen-/Preis-Follow-up-Query sucht History-Kontext robuster über Commerce-Kontext, erweiterten Verlauf und optional Full-History.
- History-Turns werden newest-first geprüft.
- Wenn ein Indikator-/Tabellenkontext ohne Modell gefunden wird, bleibt ein Fallback erhalten; ein später gefundener Modellanker hat Vorrang.
## Lokale Prüfungen
Durchgeführt in der Container-Umgebung:
```bash
php -l src/Agent/AgentRunner.php
```
Ergebnis: OK.
Zusätzliche statische Checks:
- kein hartes `shopCountMode: 'not_requested'` mehr im AgentRunner
- `not_requested` nur noch zentral in `resolveShopCountModeForMeta()`
- `loading`, `not_resolved`, `unavailable` und `count` werden zentral unterschieden
- keine konkreten Tippfehlerlisten wie `kpstet` / `ksotet`
Nicht vollständig ausführbar in dieser Umgebung:
```bash
bin/console mto:agent:config:validate
bin/console mto:agent:regression:test
bin/console mto:agent:config:audit-source --details
bin/console mto:agent:config:audit-patterns --details
```
Grund: Die ZIP enthält keine installierten Composer-/Runtime-Dependencies; die lokale Umgebung hat außerdem nicht alle benötigten PHP-Extensions.
## Pflichtprüfung nach Einspielen
```bash
bin/console cache:clear
bin/console mto:agent:config:validate
bin/console mto:agent:regression:test
bin/console mto:agent:config:audit-source --details
bin/console mto:agent:config:audit-patterns --details
```
Falls OPcache/PHP-FPM aktiv ist, danach PHP-FPM bzw. Container neu laden.
## Manuelle Regressionstests
```text
shop testomat 808
```
Erwartung: Shop wird angefragt; nach Shop-Routing nicht mehr `Shop-Treffer: nicht angefragt`.
```text
was kpstet der indikator
```
Erwartung: Shop-/Preis-Follow-up greift weiterhin über Normalisierung/Fuzzy-Routing.
```text
welche grenzwerte kann der testomat 808 messen
die tabelle mit preisen
```
Erwartung: Shop-Suche wird ausgelöst; Query sinngemäß `Testomat 808 indikator`.
```text
die tabelle mit shop preisen
```
Erwartung: Shop-Suche wird ausgelöst, sofern Verlaufskontext vorhanden ist; sonst `Shop-Treffer: keine Suchquery` statt irreführend `nicht angefragt`.

View File

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