diff --git a/config/retriex/chat-messages.yaml b/config/retriex/chat-messages.yaml index 5aa91b8..e9f2b21 100644 --- a/config/retriex/chat-messages.yaml +++ b/config/retriex/chat-messages.yaml @@ -234,15 +234,17 @@ parameters: history_notice_with_detail: 'Systemhinweis: {title}. Ursache: {detail}' history_response_system_notice: 'Systemhinweis: {message}' follow_up_actions: + enabled: true commerce: - label: Im Shop suchen - prompt: Suche die aktuelle Produktauswahl im Shop. + prompt: Suche im Shop nach den aktuell genannten Produkten oder Messaufgaben. + shop_results: + - label: Preis anzeigen + prompt: Zeige mir die Preise der aktuell relevanten Produkte. - label: Nur Zubehör anzeigen prompt: Zeige aus der aktuellen Produktauswahl nur Zubehör. - label: Nur Geräte anzeigen prompt: Zeige aus der aktuellen Produktauswahl nur Geräte. - - label: Preis anzeigen - prompt: Zeige mir die Preise der aktuell relevanten Produkte. knowledge: - label: Technische Details anzeigen prompt: Zeige technische Details zur aktuellen Antwort. diff --git a/patch_history/RETRIEX_PATCH_67_CONTEXTUAL_FOLLOWUP_ACTIONS_README.md b/patch_history/RETRIEX_PATCH_67_CONTEXTUAL_FOLLOWUP_ACTIONS_README.md new file mode 100644 index 0000000..22689fb --- /dev/null +++ b/patch_history/RETRIEX_PATCH_67_CONTEXTUAL_FOLLOWUP_ACTIONS_README.md @@ -0,0 +1,52 @@ +# RetrieX Patch p67 – Contextual Follow-up Actions + +## Ziel + +`buildFollowUpActionsMessage()` war vorbereitet, aber nicht in den AgentRunner-Flow eingehängt. Dieser Patch aktiviert die Funktion kontrolliert und kontextsensitiv, damit die Production-UI nach einer fertigen Antwort sinnvolle nächste Schritte anbieten kann, ohne unsichere oder unpassende Aktionen zu zeigen. + +## Änderung + +- `AgentRunner` rendert nach der finalen Completed-Meta-Card optional eine Follow-up-Actions-Card. +- Die Card erscheint nur, wenn `agent.production_ui.follow_up_actions.enabled` aktiv ist und echte Kontextsignale vorhanden sind. +- Shop-Refinement-Actions werden nur bei vorhandenen Shop-Treffern angezeigt. +- Die generische Shop-Suchaktion wird nur bei Commerce-Kontext ohne Shop-Systemfehler angeboten. +- Die Knowledge-Action wird nur bei direkter/belastbarer RAG-Evidenz angezeigt. +- Action-Duplikate werden beim Rendern dedupliziert. +- Die bisherigen Action-Texte bleiben YAML-konfigurierbar und wurden in kontextbezogene Gruppen getrennt: + - `commerce` + - `shop_results` + - `knowledge` + +## Bewusst nicht geändert + +- Keine Änderung an Retrieval, Scoring, Ranking, Intent-Erkennung, Shop-Matching oder PromptBuilder. +- `buildShopProductCardsMessage()` bleibt weiterhin nicht eingehängt, weil Produktkarten zusätzlich zur Antwort deutlich mehr UI-Duplikation und Regressionsrisiko erzeugen würden. +- Keine neuen fachlichen Keywordlisten oder PHP-only Defaults. + +## Geänderte Dateien + +- `src/Agent/AgentRunner.php` +- `src/Config/AgentRunnerConfig.php` +- `src/Config/ChatMessagesConfig.php` +- `config/retriex/chat-messages.yaml` + +## Lokale Checks + +Ausgeführt im ZIP-Arbeitsstand ohne `vendor/`: + +```bash +php -l src/Agent/AgentRunner.php +php -l src/Config/AgentRunnerConfig.php +php -l src/Config/ChatMessagesConfig.php +python3 -c 'import yaml; yaml.safe_load(open("config/retriex/chat-messages.yaml"))' +``` + +Zusätzlich wurde `ChatMessagesConfig::validate()` mit der geparsten `chat-messages.yaml` per Smoke-Test geprüft. + +## Empfohlene Checks in der Zielumgebung + +```bash +bin/console mto:agent:config:validate +bin/console mto:agent:regression:test +bin/console mto:agent:config:audit-source --details +``` diff --git a/src/Agent/AgentRunner.php b/src/Agent/AgentRunner.php index d605002..b961ff9 100644 --- a/src/Agent/AgentRunner.php +++ b/src/Agent/AgentRunner.php @@ -700,6 +700,17 @@ final readonly class AgentRunner 'meta' ); + $followUpActionsMessage = $this->buildFollowUpActionsMessage( + isCommerceIntent: $this->isCommerceIntent($commerceIntent), + hasShopResults: $shopResults !== [], + hasKnowledge: $this->isDirectKnowledgeEvidence($knowledgeEvidenceState), + shopSearchHadSystemFailure: $primaryShopSearchHadSystemFailure + ); + + if ($followUpActionsMessage !== '') { + yield $this->systemMsg($followUpActionsMessage, 'meta'); + } + /* if ($sources !== []) { yield $this->emitSources( $sources, @@ -4850,20 +4861,27 @@ final readonly class AgentRunner return $this->agentRunnerConfig->getProductionUiTemplate('relevance_default'); } - private function buildFollowUpActionsMessage(bool $isCommerceIntent, bool $hasShopResults, bool $hasKnowledge): string - { - if (!$isCommerceIntent && !$hasShopResults && !$hasKnowledge) { + private function buildFollowUpActionsMessage( + bool $isCommerceIntent, + bool $hasShopResults, + bool $hasKnowledge, + bool $shopSearchHadSystemFailure + ): string { + if (!$this->agentRunnerConfig->isProductionUiFollowUpActionsEnabled()) { return ''; } $actions = []; + $seenActionKeys = []; - if ($isCommerceIntent || $hasShopResults) { - $actions = array_merge($actions, $this->agentRunnerConfig->getProductionUiFollowUpActions('commerce')); + if ($hasShopResults) { + $this->appendFollowUpActions($actions, $seenActionKeys, $this->agentRunnerConfig->getProductionUiFollowUpActions('shop_results')); + } elseif ($isCommerceIntent && !$shopSearchHadSystemFailure) { + $this->appendFollowUpActions($actions, $seenActionKeys, $this->agentRunnerConfig->getProductionUiFollowUpActions('commerce')); } - if ($hasKnowledge || $hasShopResults) { - $actions = array_merge($actions, $this->agentRunnerConfig->getProductionUiFollowUpActions('knowledge')); + if ($hasKnowledge) { + $this->appendFollowUpActions($actions, $seenActionKeys, $this->agentRunnerConfig->getProductionUiFollowUpActions('knowledge')); } if ($actions === []) { @@ -4876,15 +4894,10 @@ final readonly class AgentRunner . '
'; foreach ($actions as $action) { - $label = (string) ($action['label'] ?? ''); - $actionPrompt = (string) ($action['prompt'] ?? ''); - if ($label === '' || $actionPrompt === '') { - continue; - } $html .= ''; } @@ -4893,6 +4906,34 @@ final readonly class AgentRunner return $html; } + /** + * @param array $actions + * @param array $seenActionKeys + * @param array $items + */ + private function appendFollowUpActions(array &$actions, array &$seenActionKeys, array $items): void + { + foreach ($items as $item) { + $label = trim((string) ($item['label'] ?? '')); + $actionPrompt = trim((string) ($item['prompt'] ?? '')); + + if ($label === '' || $actionPrompt === '') { + continue; + } + + $key = mb_strtolower($label . "\n" . $actionPrompt, 'UTF-8'); + if (isset($seenActionKeys[$key])) { + continue; + } + + $seenActionKeys[$key] = true; + $actions[] = [ + 'label' => $label, + 'prompt' => $actionPrompt, + ]; + } + } + private function buildShopSearchMetaMessage( string $query, string $commerceIntent, diff --git a/src/Config/AgentRunnerConfig.php b/src/Config/AgentRunnerConfig.php index f722cf7..196d692 100644 --- a/src/Config/AgentRunnerConfig.php +++ b/src/Config/AgentRunnerConfig.php @@ -979,12 +979,23 @@ final class AgentRunnerConfig return $this->getRequiredInt('production_ui.shop_results.max_cards'); } + public function isProductionUiFollowUpActionsEnabled(): bool + { + if ($this->chatMessages !== null) { + return $this->chatMessages->getBool('agent.production_ui.follow_up_actions.enabled'); + } + + return $this->getOptionalBool('production_ui.follow_up_actions.enabled', false); + } + /** * @return array */ public function getProductionUiFollowUpActions(string $group): array { - return $this->getChatActionList('agent.production_ui.follow_up_actions.' . $group, 'production_ui.follow_up_actions.' . $group); + $legacyGroup = $group === 'shop_results' ? 'commerce' : $group; + + return $this->getChatActionList('agent.production_ui.follow_up_actions.' . $group, 'production_ui.follow_up_actions.' . $legacyGroup); } public function getNoLlmProductField(string $key): string diff --git a/src/Config/ChatMessagesConfig.php b/src/Config/ChatMessagesConfig.php index 98543d6..f650c7d 100644 --- a/src/Config/ChatMessagesConfig.php +++ b/src/Config/ChatMessagesConfig.php @@ -172,6 +172,11 @@ final class ChatMessagesConfig throw new \InvalidArgumentException(sprintf('RetrieX chat messages config key "%s" must be a string.', $path)); } + public function getBool(string $path): bool + { + return $this->bool($path); + } + /** * @return array */ @@ -211,6 +216,14 @@ final class ChatMessagesConfig } } + foreach ($this->requiredBoolPaths() as $path) { + try { + $this->bool($path); + } catch (\InvalidArgumentException $e) { + $errors[] = $e->getMessage(); + } + } + foreach ($this->requiredActionListPaths() as $path) { try { $this->actionList($path); @@ -443,10 +456,21 @@ final class ChatMessagesConfig { return [ 'agent.production_ui.follow_up_actions.commerce', + 'agent.production_ui.follow_up_actions.shop_results', 'agent.production_ui.follow_up_actions.knowledge', ]; } + /** + * @return list + */ + private function requiredBoolPaths(): array + { + return [ + 'agent.production_ui.follow_up_actions.enabled', + ]; + } + /** * @return list */ @@ -566,6 +590,29 @@ final class ChatMessagesConfig throw new \InvalidArgumentException(sprintf('RetrieX chat messages config key "%s" must be a non-empty string.', $path)); } + private function bool(string $path): bool + { + $value = $this->value($path); + + if (is_bool($value)) { + return $value; + } + + if (is_scalar($value)) { + $normalized = strtolower(trim((string) $value)); + + if (in_array($normalized, ['1', 'true', 'yes', 'on'], true)) { + return true; + } + + if (in_array($normalized, ['0', 'false', 'no', 'off'], true)) { + return false; + } + } + + throw new \InvalidArgumentException(sprintf('RetrieX chat messages config key "%s" must be boolean.', $path)); + } + private function value(string $path): mixed { $current = $this->config;