patch 20h
This commit is contained in:
152
RETRIEX_PATCH_20H_TOTAL_COMMERCE_ROUTING_STATUS_FIX_README.md
Normal file
152
RETRIEX_PATCH_20H_TOTAL_COMMERCE_ROUTING_STATUS_FIX_README.md
Normal 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`.
|
||||||
@@ -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(
|
||||||
$explicitShopRoutingForced = $forcedShopSearchQuery === ''
|
$originalPrompt,
|
||||||
&& ($originalPromptHasExplicitShopSignal || $routingPromptHasExplicitShopSignal);
|
|
||||||
|
|
||||||
$commerceIntent = ($forcedShopSearchQuery !== '' || $explicitShopRoutingForced)
|
|
||||||
? CommerceIntentLite::PRODUCT_SEARCH
|
|
||||||
: $this->detectCommerceIntentForRouting(
|
|
||||||
$routingPrompt,
|
|
||||||
$userId,
|
$userId,
|
||||||
$requestContextHint
|
$requestContextHint
|
||||||
);
|
);
|
||||||
|
|
||||||
if ($forcedShopSearchQuery !== '') {
|
if (!$this->isCommerceIntent($commerceIntent) && $this->isCommerceIntent($originalCommerceIntent)) {
|
||||||
$this->agentLogger->info('Forced commercial follow-up into shop routing', [
|
$this->agentLogger->info('Promoted normalized routing back to explicit original commerce intent', [
|
||||||
'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,10 +217,6 @@ final readonly class AgentRunner
|
|||||||
$this->addSource($sources, $this->agentRunnerConfig->getConversationHistorySourceLabel());
|
$this->addSource($sources, $this->agentRunnerConfig->getConversationHistorySourceLabel());
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($forcedShopSearchQuery !== '') {
|
|
||||||
$optimizedShopQuery = '';
|
|
||||||
$shopSearchQuery = $forcedShopSearchQuery;
|
|
||||||
} else {
|
|
||||||
$optimizedShopQuery = yield from $this->buildOptimizedShopQuery(
|
$optimizedShopQuery = yield from $this->buildOptimizedShopQuery(
|
||||||
$routingPrompt,
|
$routingPrompt,
|
||||||
$userId,
|
$userId,
|
||||||
@@ -233,7 +229,6 @@ final readonly class AgentRunner
|
|||||||
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,15 +1386,14 @@ 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ($optimizedShopQuery !== '' && !$this->isMetaOnlyShopQuery($optimizedShopQuery)) {
|
if ($optimizedShopQuery !== '' && !$this->isMetaOnlyShopQuery($optimizedShopQuery)) {
|
||||||
return $optimizedShopQuery;
|
return $optimizedShopQuery;
|
||||||
@@ -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';
|
||||||
|
|||||||
Reference in New Issue
Block a user