From 446df191c0d1f0b4a79c55db5d06ee8e998e9649 Mon Sep 17 00:00:00 2001 From: team 1 Date: Sat, 2 May 2026 20:08:25 +0200 Subject: [PATCH] patch 20c --- ...0C_COMMERCIAL_TABLE_FOLLOWUP_FIX_README.md | 61 +++++++++ config/retriex/agent.yaml | 42 +++++++ config/retriex/intent.yaml | 6 +- src/Agent/AgentRunner.php | 116 +++++++++++++++++- src/Config/AgentRunnerConfig.php | 39 ++++++ src/Config/RetriexEffectiveConfigProvider.php | 22 ++++ 6 files changed, 284 insertions(+), 2 deletions(-) create mode 100644 RETRIEX_PATCH_20C_COMMERCIAL_TABLE_FOLLOWUP_FIX_README.md diff --git a/RETRIEX_PATCH_20C_COMMERCIAL_TABLE_FOLLOWUP_FIX_README.md b/RETRIEX_PATCH_20C_COMMERCIAL_TABLE_FOLLOWUP_FIX_README.md new file mode 100644 index 0000000..11d0322 --- /dev/null +++ b/RETRIEX_PATCH_20C_COMMERCIAL_TABLE_FOLLOWUP_FIX_README.md @@ -0,0 +1,61 @@ +# RetrieX Patch 20c – Commercial Table Follow-up Fix + +## Ziel + +Patch 20c korrigiert eine Regression aus dem p20/p20b-Normalisierungs- und Routing-Umfeld: kurze referenzielle Nachfragen wie `die tabelle mit preisen` müssen den letzten fachlichen Kontext übernehmen und eine Shop-Suche auslösen. + +## Reproduzierter Problemfall + +1. `welche grenzwerte kann der testomat 808 messen` +2. RetrieX antwortet korrekt mit Testomat 808 und einer Grenzwert-/Indikatortyp-Tabelle. +3. Folgefrage: `die tabelle mit preisen` + +Vor p20c blieb die Folgefrage RAG-only bzw. ohne Shop-Suche. Das ist fachlich zu schwach, weil `die tabelle` klar auf die vorherige Indikatortyp-Tabelle referenziert und `mit preisen` aktuelle Shopdaten verlangt. + +## Lösung + +- LLM-Input-Normalisierung aus p20/p20b bleibt erhalten. +- Es gibt eine zusätzliche, YAML-konfigurierbare Erkennung für kommerzielle Tabellen-Follow-ups. +- Wenn der normale Commerce-Intent ausfällt, kann ein kurzer Tabellen-/Preis-Follow-up anhand vorhandener History-Anker gezielt zu `product_search` hochgestuft werden. +- Für Preis-Tabellen-Follow-ups wird vor einer generischen optimierten Suchquery ein kontextueller Shop-Suchbegriff aus der letzten Antwort abgeleitet. +- Beim Testomat-808-/Indikatortyp-Fall ergibt der Fallback generisch `Testomat 808 indikator` statt nur `die tabelle mit preisen` oder `Testomat 808`. +- Es werden keine konkreten Tippfehlerlisten eingeführt. + +## Geänderte Dateien + +- `src/Agent/AgentRunner.php` +- `src/Config/AgentRunnerConfig.php` +- `src/Config/RetriexEffectiveConfigProvider.php` +- `config/retriex/agent.yaml` +- `config/retriex/intent.yaml` + +## Pflichtchecks + +```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 +``` + +## Manuelle Regressionen + +1. `was kpstet der indikator` + - Erwartung: LLM-/Fuzzy-Normalisierung bleibt wirksam. + - Shop-Suche wird ausgelöst. + +2. `ich suche eine preiswerte Lösung zur messung von pH & Chlor für mein schwimmbad` + - Erwartung: beratende Shop-/Produktsuche wird ausgelöst. + +3. `welche grenzwerte kann der testomat 808 messen` → `die tabelle mit preisen` + - Erwartung: Folgefrage wird als kommerzieller Tabellen-Follow-up erkannt. + - Shop-Suche wird ausgelöst. + - Gesendete Suchquery sollte sinngemäß `Testomat 808 indikator` sein. + - Antwort soll eine Preistabelle der passenden Indikatoren/Zubehörtreffer bilden, soweit Shopdaten vorhanden sind. + +## Nicht geändert + +- Kein Scoring-Umbau. +- Keine harte Tippfehlerliste. +- Keine Änderung an Retrieval-/Vectorlogik. +- Keine Änderung an Shop-Service-Suche selbst. diff --git a/config/retriex/agent.yaml b/config/retriex/agent.yaml index 57fd22e..447672b 100644 --- a/config/retriex/agent.yaml +++ b/config/retriex/agent.yaml @@ -59,6 +59,7 @@ parameters: - kosten - preis - preise + - preisen - preiswert - preiswerte - günstig @@ -138,6 +139,7 @@ parameters: - shop - preis - preise + - preisen - kostet - kosten - kaufen @@ -152,6 +154,20 @@ parameters: - artikelnummer - sku - produktnummer + + commercial_table_follow_up: + enabled: true + prompt_patterns: + - '/\b(?:tabelle|tabellarisch|übersicht|uebersicht|liste|auflistung)\b.{0,80}\b(?:preis|preise|preisen|kosten|kostet|shop)\b/u' + - '/\b(?:preis|preise|preisen|kosten|kostet|shop)\b.{0,80}\b(?:tabelle|tabellarisch|übersicht|uebersicht|liste|auflistung)\b/u' + - '/\b(?:mit|inkl|inklusive|plus)\s+(?:preis|preise|preisen|kosten|shopdaten)\b/u' + history_anchor_patterns: + - '/\bTestomat(?:®)?\s+\d{3,4}\b/iu' + - '/\b(?:Indikatortyp|Indikator|Indikatoren|Reagenz|Reagenzien|Zubehör|Zubehoer)\b/iu' + indicator_marker_patterns: + - '/\b(?:Indikatortyp|Indikator(?:en)?|indicator(?:\s+type)?|Reagenz(?:ien)?)\b/iu' + query_template_with_model: '{model} indikator' + query_template_without_model: 'indikator' history_question_pattern: '/^Question:\s*(.+)$/mi' history_turn_split_pattern: '/(?=^Question:\s)/m' history_question_strip_pattern: '/^Question:\s*.*(?:\R|$)/u' @@ -416,6 +432,20 @@ parameters: context_fallback_max_terms: 6 context_fallback_filter_terms: - mit + - tabelle + - tabellarisch + - übersicht + - uebersicht + - liste + - auflistung + - preis + - preise + - preisen + - kosten + - kostet + - grenzwert + - grenzwerte + - grenzwerten - welche - welcher - welches @@ -450,6 +480,18 @@ parameters: - gemessen meta_only_terms: - shop + - tabelle + - tabellarisch + - übersicht + - uebersicht + - liste + - auflistung + - preis + - preise + - preisen + - kosten + - kostet + - mit - shopsuche - shop-suche - suche diff --git a/config/retriex/intent.yaml b/config/retriex/intent.yaml index 52c7569..312071e 100644 --- a/config/retriex/intent.yaml +++ b/config/retriex/intent.yaml @@ -7,6 +7,8 @@ parameters: - shop - alle - preis + - preise + - preisen - kunde - online - produkt @@ -83,6 +85,8 @@ parameters: - eur - teuer - preis + - preise + - preisen - kosten - kostet - preiswert @@ -140,7 +144,7 @@ parameters: - '/\be\d{1,3}\b/u' explicit_commerce_intent_patterns: - '/\bshop\b/u' - - '/\bpreis\b/u' + - '/\bpreis(?:e|en)?\b/u' - '/\bkosten\b/u' - '/\bkostet\b/u' - '/\bkaufen\b/u' diff --git a/src/Agent/AgentRunner.php b/src/Agent/AgentRunner.php index 08aa5c9..0d9aa32 100644 --- a/src/Agent/AgentRunner.php +++ b/src/Agent/AgentRunner.php @@ -107,7 +107,11 @@ final readonly class AgentRunner $this->addSource($sources, $this->agentRunnerConfig->getExternalUrlSourceLabel()); } - $commerceIntent = $this->detectCommerceIntent($routingPrompt); + $commerceIntent = $this->detectCommerceIntentForRouting( + $routingPrompt, + $userId, + $requestContextHint + ); yield $this->systemMsg($this->agentRunnerConfig->getRetrieveKnowledgeMessage(), 'think'); @@ -933,6 +937,36 @@ final readonly class AgentRunner return (string) ($commerceMeta['intent'] ?? CommerceIntentLite::NONE); } + private function detectCommerceIntentForRouting( + string $prompt, + string $userId, + string $requestContextHint + ): string { + $commerceIntent = $this->detectCommerceIntent($prompt); + + if ($this->isCommerceIntent($commerceIntent)) { + return $commerceIntent; + } + + if (!$this->isCommercialTableFollowUpPrompt($prompt)) { + return $commerceIntent; + } + + $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, + 'hasRequestContextHint' => trim($requestContextHint) !== '', + ]); + + return CommerceIntentLite::PRODUCT_SEARCH; + } + private function isCommerceIntent(string $commerceIntent): bool { return $commerceIntent === CommerceIntentLite::PRODUCT_SEARCH @@ -1265,6 +1299,14 @@ final readonly class AgentRunner string $commerceHistoryContext, string $userId ): string { + if ($this->isCommercialTableFollowUpPrompt($prompt)) { + $commercialTableContextQuery = $this->extractCommercialTableFollowUpShopQuery($commerceHistoryContext); + + if ($commercialTableContextQuery !== '' && !$this->isMetaOnlyShopQuery($commercialTableContextQuery)) { + return $commercialTableContextQuery; + } + } + if ($optimizedShopQuery !== '' && !$this->isMetaOnlyShopQuery($optimizedShopQuery)) { return $optimizedShopQuery; } @@ -1302,6 +1344,78 @@ final readonly class AgentRunner return ''; } + private function extractCommercialTableFollowUpShopQuery(string $commerceHistoryContext): string + { + if (!$this->agentRunnerConfig->isCommercialTableFollowUpEnabled()) { + return ''; + } + + $turn = $this->extractLatestHistoryTurn($commerceHistoryContext); + + if ($turn === '') { + return ''; + } + + if (!$this->matchesAnyConfiguredPattern( + $turn, + $this->agentRunnerConfig->getCommercialTableFollowUpIndicatorMarkerPatterns() + )) { + return ''; + } + + $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()); + } + + private function isCommercialTableFollowUpPrompt(string $prompt): bool + { + if (!$this->agentRunnerConfig->isCommercialTableFollowUpEnabled()) { + return false; + } + + return $this->matchesAnyConfiguredPattern( + $this->normalizeFollowUpText($prompt), + $this->agentRunnerConfig->getCommercialTableFollowUpPromptPatterns() + ); + } + + private function commercialTableFollowUpHistoryHasAnchor(string $commerceHistoryContext): bool + { + if (trim($commerceHistoryContext) === '') { + return false; + } + + return $this->matchesAnyConfiguredPattern( + $commerceHistoryContext, + $this->agentRunnerConfig->getCommercialTableFollowUpHistoryAnchorPatterns() + ); + } + + /** + * @param string[] $patterns + */ + private function matchesAnyConfiguredPattern(string $text, array $patterns): bool + { + foreach ($patterns as $pattern) { + if (preg_match($pattern, $text) === 1) { + return true; + } + } + + return false; + } + private function extractContextualShopSearchQuery(string $commerceHistoryContext): string { if (!$this->agentRunnerConfig->isShopQueryContextFallbackEnabled()) { diff --git a/src/Config/AgentRunnerConfig.php b/src/Config/AgentRunnerConfig.php index a593eed..0f122da 100644 --- a/src/Config/AgentRunnerConfig.php +++ b/src/Config/AgentRunnerConfig.php @@ -55,6 +55,45 @@ final class AgentRunnerConfig return $this->getRequiredStringList('follow_up_context.explicit_commercial_signal_terms'); } + public function isCommercialTableFollowUpEnabled(): bool + { + return $this->getRequiredBool('follow_up_context.commercial_table_follow_up.enabled'); + } + + /** + * @return string[] + */ + public function getCommercialTableFollowUpPromptPatterns(): array + { + return $this->getRequiredStringList('follow_up_context.commercial_table_follow_up.prompt_patterns'); + } + + /** + * @return string[] + */ + public function getCommercialTableFollowUpHistoryAnchorPatterns(): array + { + return $this->getRequiredStringList('follow_up_context.commercial_table_follow_up.history_anchor_patterns'); + } + + /** + * @return string[] + */ + public function getCommercialTableFollowUpIndicatorMarkerPatterns(): array + { + return $this->getRequiredStringList('follow_up_context.commercial_table_follow_up.indicator_marker_patterns'); + } + + public function getCommercialTableFollowUpQueryTemplateWithModel(): string + { + return $this->getRequiredString('follow_up_context.commercial_table_follow_up.query_template_with_model'); + } + + public function getCommercialTableFollowUpQueryTemplateWithoutModel(): string + { + return $this->getRequiredString('follow_up_context.commercial_table_follow_up.query_template_without_model'); + } + public function getFollowUpHistoryQuestionPattern(): string { return $this->getRequiredString('follow_up_context.history_question_pattern'); diff --git a/src/Config/RetriexEffectiveConfigProvider.php b/src/Config/RetriexEffectiveConfigProvider.php index 8384380..5c5f684 100644 --- a/src/Config/RetriexEffectiveConfigProvider.php +++ b/src/Config/RetriexEffectiveConfigProvider.php @@ -441,6 +441,16 @@ final readonly class RetriexEffectiveConfigProvider 'product_search_knowledge_chunk_limit' => $this->agentRunnerConfig->getProductSearchKnowledgeChunkLimit(), 'advisory_product_search_knowledge_chunk_limit' => $this->agentRunnerConfig->getAdvisoryProductSearchKnowledgeChunkLimit(), 'optimized_shop_query_prefix_pattern' => $this->agentRunnerConfig->getOptimizedShopQueryPrefixPattern(), + 'follow_up_context' => [ + 'commercial_table_follow_up' => [ + 'enabled' => $this->agentRunnerConfig->isCommercialTableFollowUpEnabled(), + 'prompt_patterns' => $this->agentRunnerConfig->getCommercialTableFollowUpPromptPatterns(), + 'history_anchor_patterns' => $this->agentRunnerConfig->getCommercialTableFollowUpHistoryAnchorPatterns(), + 'indicator_marker_patterns' => $this->agentRunnerConfig->getCommercialTableFollowUpIndicatorMarkerPatterns(), + 'query_template_with_model' => $this->agentRunnerConfig->getCommercialTableFollowUpQueryTemplateWithModel(), + 'query_template_without_model' => $this->agentRunnerConfig->getCommercialTableFollowUpQueryTemplateWithoutModel(), + ], + ], 'input_normalization' => [ 'enabled' => $this->agentRunnerConfig->isInputNormalizationEnabled(), 'max_input_chars' => $this->agentRunnerConfig->getInputNormalizationMaxInputChars(), @@ -1044,6 +1054,18 @@ final readonly class RetriexEffectiveConfigProvider $this->validateStringListMap($agent['source_labels'] ?? [], 'agent.source_labels', $errors, $warnings); $this->validateStringListMap($agent['html_templates'] ?? [], 'agent.html_templates', $errors, $warnings); + $followUpContext = is_array($agent['follow_up_context'] ?? null) ? $agent['follow_up_context'] : []; + $commercialTableFollowUp = is_array($followUpContext['commercial_table_follow_up'] ?? null) ? $followUpContext['commercial_table_follow_up'] : []; + $this->validateRegexPatternList($commercialTableFollowUp['prompt_patterns'] ?? [], 'agent.follow_up_context.commercial_table_follow_up.prompt_patterns', $errors); + $this->validateRegexPatternList($commercialTableFollowUp['history_anchor_patterns'] ?? [], 'agent.follow_up_context.commercial_table_follow_up.history_anchor_patterns', $errors); + $this->validateRegexPatternList($commercialTableFollowUp['indicator_marker_patterns'] ?? [], 'agent.follow_up_context.commercial_table_follow_up.indicator_marker_patterns', $errors); + if (trim((string) ($commercialTableFollowUp['query_template_with_model'] ?? '')) === '') { + $errors[] = 'agent.follow_up_context.commercial_table_follow_up.query_template_with_model must not be empty.'; + } + if (trim((string) ($commercialTableFollowUp['query_template_without_model'] ?? '')) === '') { + $errors[] = 'agent.follow_up_context.commercial_table_follow_up.query_template_without_model must not be empty.'; + } + $ragEvidence = is_array($agent['rag_evidence_guard'] ?? null) ? $agent['rag_evidence_guard'] : []; $this->validateStringList($this->toList($ragEvidence['stop_terms'] ?? []), 'agent.rag_evidence_guard.stop_terms', $errors, $warnings); $this->validateStringListMap($ragEvidence['synonyms'] ?? [], 'agent.rag_evidence_guard.synonyms', $errors, $warnings);