From aae4935d69b65dbb6292dae05aab421226c2d185 Mon Sep 17 00:00:00 2001 From: team 1 Date: Sat, 9 May 2026 20:18:14 +0200 Subject: [PATCH] p70+71 --- config/retriex/chat-messages.yaml | 7 +- ...H_70_FOLLOWUP_ACTION_VALUE_GUARD_README.md | 77 ++++++ ...ACTION_DISPLAYED_SELECTION_GUARD_README.md | 70 +++++ src/Agent/AgentRunner.php | 249 ++++++++++++++++-- src/Config/AgentRunnerConfig.php | 24 +- src/Config/ChatMessagesConfig.php | 24 +- 6 files changed, 427 insertions(+), 24 deletions(-) create mode 100644 patch_history/RETRIEX_PATCH_70_FOLLOWUP_ACTION_VALUE_GUARD_README.md create mode 100644 patch_history/RETRIEX_PATCH_71_FOLLOWUP_ACTION_DISPLAYED_SELECTION_GUARD_README.md diff --git a/config/retriex/chat-messages.yaml b/config/retriex/chat-messages.yaml index ee7b580..972258d 100644 --- a/config/retriex/chat-messages.yaml +++ b/config/retriex/chat-messages.yaml @@ -253,7 +253,12 @@ parameters: target_role: main_device knowledge: - label: Technische Details anzeigen - prompt: Zeige technische Details zur aktuellen Antwort. + prompt: Zeige nur zusätzliche technische Details zu {answer_anchor}. + action_type: technical_details + requires_answer_anchor: true + hide_when_answer_detail_score_at_least: 2 + hide_when_answer_matches_any: + - '/\b(?:Grenzwert(?:e)?|Messbereich(?:e)?|Indikator(?:typ)?|Einsatzgebiet(?:e)?|Technische Einordnung)\b/iu' source_labels: external_url: Externe URL rag_knowledge: RAG Wissen diff --git a/patch_history/RETRIEX_PATCH_70_FOLLOWUP_ACTION_VALUE_GUARD_README.md b/patch_history/RETRIEX_PATCH_70_FOLLOWUP_ACTION_VALUE_GUARD_README.md new file mode 100644 index 0000000..6260b38 --- /dev/null +++ b/patch_history/RETRIEX_PATCH_70_FOLLOWUP_ACTION_VALUE_GUARD_README.md @@ -0,0 +1,77 @@ +# RetrieX Patch p70 - Follow-up Action Value Guard + +## Ziel + +Dieser Patch härtet die Folgeaktionen aus p67-p69 so, dass nur noch wirklich hilfreiche Actions angezeigt werden. Eine Action soll nicht erscheinen, wenn die aktuelle Antwort die angeforderte Information bereits geliefert hat oder wenn der Klick ohne stabilen Kontextanker zu unsauberen Folgeabfragen führen könnte. + +## Problem + +Im Flow `Was ist der niedrigste Grenzwert fuer die Wasserhaerte, welcher mit einem Testomaten ueberwacht werden kann?` konnte nach einer bereits technischen Antwort weiterhin `Technische Details anzeigen` erscheinen. Beim Klick war der Prompt zu generisch (`zur aktuellen Antwort`) und konnte dadurch fremde technische Kontexte wie `TH2005` anziehen, obwohl der relevante Kontext `Testomat 808 / 0,02 °dH / Indikatortyp 300` ist. + +Außerdem sollten Rollenfilter wie `Nur Zubehoer anzeigen` nicht angezeigt werden, wenn die erkannte aktuelle Auswahl bereits nur aus Zubehoer besteht. Unbekannte Rollen duerfen eine eigentlich redundante Filteraktion nicht kuenstlich sinnvoll erscheinen lassen. + +## Loesung + +- `buildFollowUpActionContext()` normalisiert die finale Antwort und berechnet zusaetzliche Action-Kontextsignale: + - `answer_text` + - `answer_anchor` aus bestehenden Referenzanker-Patterns (Produktmodell + Messwert) + - `answer_detail_score` aus Produktmodell-/Messwertankern +- Knowledge-Action `Technische Details anzeigen` ist jetzt als `action_type: technical_details` konfiguriert. +- Die technische Detail-Action wird unterdrueckt, wenn: + - kein stabiler Antwortanker vorhanden ist, + - die Antwort bereits genug technische Detailanker enthaelt, + - oder konfigurierte Antwortmuster wie Grenzwert, Messbereich, Indikatortyp, Einsatzgebiet oder technische Einordnung bereits vorkommen. +- Falls die technische Detail-Action doch sinnvoll ist, wird der Klick-Prompt mit `{answer_anchor}` verankert statt generisch auf `aktuelle Antwort` zu zeigen. +- Rollenfilter werden nur noch anhand bekannter Hauptgeraet-/Zubehoer-Rollen bewertet. `unknown` oder `ambiguous` zaehlt nicht mehr als Grund, einen redundanten Rollenfilter anzuzeigen. +- Action-Definitionen koennen nun YAML-seitig folgende Guard-Metadaten tragen: + - `requires_answer_anchor` + - `hide_when_answer_detail_score_at_least` + - `hide_when_answer_matches_any` + +## Bewusst nicht geaendert + +- Keine Aenderung an Retrieval, Scoring, Ranking, Intent, Shop-Matching oder PromptBuilder. +- Keine neuen PHP-Core-Keywordlisten fuer Testomat, Indikator oder TH2005. +- Die neuen Guard-Begriffe liegen YAML-konfigurierbar in `chat-messages.yaml`. +- `buildShopProductCardsMessage()` bleibt weiterhin nicht eingehängt. + +## Geaenderte Dateien + +- `src/Agent/AgentRunner.php` +- `src/Config/AgentRunnerConfig.php` +- `src/Config/ChatMessagesConfig.php` +- `config/retriex/chat-messages.yaml` + +## Lokale Checks + +Ausgefuehrt 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 per Smoke-Test geprueft, dass `ChatMessagesConfig::getActionList()` die neuen Action-Metadaten erhaelt. + +## Empfohlene Regressionstests + +1. `Was ist der niedrigste Grenzwert fuer die Wasserhaerte, welcher mit einem Testomaten ueberwacht werden kann?` + - Erwartung: Antwort bleibt fachlich bei `0,02 °dH / Testomat 808`. + - Erwartung: keine redundante Action `Technische Details anzeigen`, wenn die Antwort den technischen Kern bereits enthaelt. + +2. Folgefrage: `mit welchem indikator wird der wert gemessen` + - Erwartung: Antwort bleibt bei `Indikatortyp 300`. + - Erwartung: keine technische Detail-Action, wenn Indikatortyp/Grenzwert bereits beantwortet wurde. + +3. `testomat 808 indikatoren` + - Erwartung: keine `Nur Geraete anzeigen`-Action bei reiner Indikator-/Zubehoerliste. + - Erwartung: keine `Nur Zubehoer anzeigen`-Action, wenn die erkannte Auswahl bereits nur Zubehoer ist. + - Erwartung: keine Action-Karte, wenn keine sinnvolle Action uebrig bleibt. + +4. Gemischte Shop-Liste mit erkannten Hauptgeraeten und erkanntem Zubehoer + - Erwartung: Rollenfilter erscheinen nur, wenn sie die aktuelle bekannte Rollenmenge wirklich verengen. + +5. Smalltalk / No-Evidence + - Erwartung: keine Follow-up-Actions. diff --git a/patch_history/RETRIEX_PATCH_71_FOLLOWUP_ACTION_DISPLAYED_SELECTION_GUARD_README.md b/patch_history/RETRIEX_PATCH_71_FOLLOWUP_ACTION_DISPLAYED_SELECTION_GUARD_README.md new file mode 100644 index 0000000..09483d6 --- /dev/null +++ b/patch_history/RETRIEX_PATCH_71_FOLLOWUP_ACTION_DISPLAYED_SELECTION_GUARD_README.md @@ -0,0 +1,70 @@ +# RetrieX Patch p71 - Follow-up Action Displayed Selection Guard + +## Ziel + +Dieser Patch haertet die Folgeaktionen aus p67-p70 weiter. Rollenfilter-Actions duerfen nicht mehr aus der kompletten Shop-Rohmenge abgeleitet werden, wenn die finale Antwort nur eine engere Produktauswahl zeigt. + +## Problem + +Im Flow: + +1. `Was ist der niedrigste Grenzwert fuer die Wasserhaerte, welcher mit einem Testomaten ueberwacht werden kann?` +2. `mit welchem indikator wird der wert gemessen` +3. `was kostet der indikator` + +lieferte die Antwort korrekt den `Testomat® 808 Indikator 300 2 x 100 ml` mit Preis. Trotzdem erschienen danach `Nur Zubehoer anzeigen` und `Nur Geraete anzeigen`, weil die Follow-up-Actions noch die gesamte Shop-Rohmenge auswerteten. Diese Rohmenge konnte weitere Treffer enthalten, z. B. das zugehoerige Geraet `Testomat® 808`. Dadurch wirkte `Nur Geraete anzeigen` scheinbar sinnvoll, obwohl die sichtbare Antwort bereits ein einzelner Zubehoer-/Indikator-Treffer war. + +Beim Klick entstand eine Folgefrage wie `Suche im Shop nach 300 indikator und zeige daraus nur Geraete.`, wodurch der Kontext in eine fragwuerdige Geraete-Schleife kippen konnte. + +## Loesung + +- `buildFollowUpActionContext()` berechnet weiterhin Rollen aus den Shopdaten. +- Wenn die finale Antwort konkrete Shop-Produkte sichtbar enthaelt, werden fuer Rollenfilter aber bevorzugt nur diese angezeigten Produkte bewertet. +- Angezeigte Produkte werden robust erkannt ueber: + - Produktnummern in der Antwort, + - oder hinreichend spezifische Produktnamen in der Antwort. +- Sehr kurze/generische Produktnamen wie `Testomat® 808` werden nicht per bloßer Namens-Substring-Erkennung als sichtbar gezaehlt, damit sie nicht durch laengere Indikatornamen falsch mitgezogen werden. +- Wenn die sichtbare Auswahl nur aus Zubehoer besteht, erscheinen weder `Nur Zubehoer anzeigen` noch `Nur Geraete anzeigen`. +- Wenn die sichtbare Auswahl nur aus Geraeten besteht, erscheinen ebenfalls keine redundanten Rollenfilter. +- Nur echte gemischte sichtbare Auswahlen koennen Rollenfilter-Actions erzeugen. + +## Bewusst nicht geaendert + +- Keine Aenderung an Retrieval, Scoring, Ranking, Intent, Shop-Matching oder PromptBuilder. +- Keine neuen fachlichen Sonderregeln fuer Testomat 808, Indikatortyp 300 oder TH2005. +- Keine Aenderung an YAML-Action-Labels oder Frontend-Rendering. +- `buildShopProductCardsMessage()` bleibt weiterhin nicht eingehängt. + +## Geaenderte Dateien + +- `src/Agent/AgentRunner.php` +- `patch_history/RETRIEX_PATCH_71_FOLLOWUP_ACTION_DISPLAYED_SELECTION_GUARD_README.md` + +## Lokale Checks + +Ausgefuehrt 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"))' +``` + +## Empfohlene Regressionstests + +1. Flow `0,02 °dH -> Indikatortyp 300 -> was kostet der indikator` + - Erwartung: Antwort bleibt bei `Testomat® 808 Indikator 300 2 x 100 ml` / `140001` / `83,30 €`. + - Erwartung: keine Rollenfilter-Actions, weil die sichtbare Auswahl bereits ein einzelner Zubehoer-/Indikator-Treffer ist. + +2. Klick auf eine alte/noch vorhandene `Nur Geraete anzeigen`-Action aus dem Verlauf + - Erwartung: Nach der Antwort mit dem einzelnen Geraet entstehen keine neuen redundanten Rollenfilter-Actions. + +3. `testomat 808 indikatoren` + - Erwartung: Wenn die sichtbare Antwort nur Indikatoren/Zubehoer zeigt, keine `Nur Geraete anzeigen`-Action und keine redundante `Nur Zubehoer anzeigen`-Action. + +4. Gemischte sichtbare Shop-Auswahl mit Hauptgeraeten und Zubehoer + - Erwartung: Rollenfilter duerfen erscheinen, weil sie die sichtbare Auswahl wirklich verengen. + +5. Smalltalk / No-Evidence + - Erwartung: keine Follow-up-Actions. diff --git a/src/Agent/AgentRunner.php b/src/Agent/AgentRunner.php index b47c119..b1858a2 100644 --- a/src/Agent/AgentRunner.php +++ b/src/Agent/AgentRunner.php @@ -4939,35 +4939,155 @@ final readonly class AgentRunner /** * @param ShopProductResult[] $shopResults - * @return array{shop_query:string, answer_has_price:bool, role_counts:array} + * @return array{shop_query:string, answer_has_price:bool, answer_text:string, answer_anchor:string, answer_detail_score:int, role_counts:array} */ private function buildFollowUpActionContext(array $shopResults, string $shopSearchQuery, string $answerText): array { - $roleCounts = [ + $plainAnswerText = $this->normalizeOneLine($this->plainTextFromHtml($answerText)); + $roleCounts = $this->buildFollowUpActionRoleCounts($shopResults); + $displayedRoleCounts = $this->buildFollowUpActionDisplayedRoleCounts($shopResults, $plainAnswerText); + + if (array_sum($displayedRoleCounts) > 0) { + $roleCounts = $displayedRoleCounts; + } + + return [ + 'shop_query' => $this->normalizeOneLine($shopSearchQuery), + 'answer_has_price' => $this->followUpActionAnswerAlreadyContainsPrice($plainAnswerText), + 'answer_text' => $plainAnswerText, + 'answer_anchor' => $this->buildFollowUpActionAnswerAnchor($plainAnswerText), + 'answer_detail_score' => $this->calculateFollowUpActionAnswerDetailScore($plainAnswerText), + 'role_counts' => $roleCounts, + ]; + } + + /** + * @return array + */ + private function emptyFollowUpActionRoleCounts(): array + { + return [ ProductRoleResolver::ROLE_MAIN_DEVICE => 0, ProductRoleResolver::ROLE_ACCESSORY_OR_CONSUMABLE => 0, ProductRoleResolver::ROLE_AMBIGUOUS_MIXED => 0, ProductRoleResolver::ROLE_UNKNOWN => 0, ]; + } + + /** + * @param ShopProductResult[] $shopResults + * @return array + */ + private function buildFollowUpActionRoleCounts(array $shopResults): array + { + $roleCounts = $this->emptyFollowUpActionRoleCounts(); foreach ($shopResults as $product) { if (!$product instanceof ShopProductResult) { continue; } - $role = $this->resolveFollowUpActionShopProductRole($product); - if (!array_key_exists($role, $roleCounts)) { - $role = ProductRoleResolver::ROLE_UNKNOWN; - } - - ++$roleCounts[$role]; + $this->countFollowUpActionProductRole($roleCounts, $product); } - return [ - 'shop_query' => $this->normalizeOneLine($shopSearchQuery), - 'answer_has_price' => $this->followUpActionAnswerAlreadyContainsPrice($answerText), - 'role_counts' => $roleCounts, - ]; + return $roleCounts; + } + + /** + * @param ShopProductResult[] $shopResults + * @return array + */ + private function buildFollowUpActionDisplayedRoleCounts(array $shopResults, string $answerText): array + { + $roleCounts = $this->emptyFollowUpActionRoleCounts(); + if ($answerText === '') { + return $roleCounts; + } + + foreach ($shopResults as $product) { + if (!$product instanceof ShopProductResult) { + continue; + } + + if (!$this->isFollowUpActionProductDisplayedInAnswer($product, $answerText)) { + continue; + } + + $this->countFollowUpActionProductRole($roleCounts, $product); + } + + return $roleCounts; + } + + /** + * @param array $roleCounts + */ + private function countFollowUpActionProductRole(array &$roleCounts, ShopProductResult $product): void + { + $role = $this->resolveFollowUpActionShopProductRole($product); + if (!array_key_exists($role, $roleCounts)) { + $role = ProductRoleResolver::ROLE_UNKNOWN; + } + + ++$roleCounts[$role]; + } + + private function isFollowUpActionProductDisplayedInAnswer(ShopProductResult $product, string $answerText): bool + { + $normalizedAnswer = mb_strtolower($this->normalizeOneLine($answerText), 'UTF-8'); + if ($normalizedAnswer === '') { + return false; + } + + $productNumber = $this->normalizeOneLine((string) $product->productNumber); + if ($productNumber !== '' && mb_strlen($productNumber, 'UTF-8') >= 3 && str_contains($normalizedAnswer, mb_strtolower($productNumber, 'UTF-8'))) { + return true; + } + + $productName = mb_strtolower($this->normalizeOneLine($product->name), 'UTF-8'); + if ($productName === '' || mb_strlen($productName, 'UTF-8') < 16) { + return false; + } + + preg_match_all('/[\p{L}\p{N}]+/u', $productName, $matches); + $tokens = array_values(array_unique($matches[0] ?? [])); + if (count($tokens) < 3) { + return false; + } + + return str_contains($normalizedAnswer, $productName); + } + + private function buildFollowUpActionAnswerAnchor(string $answerText): string + { + $anchors = []; + + $modelAnchor = $this->referenceAnchorExtractor->extractFirstProductModelAnchor($answerText); + if ($modelAnchor !== '') { + $anchors[] = $modelAnchor; + } + + $measurementAnchor = $this->referenceAnchorExtractor->extractFirstMeasurementValueAnchor($answerText); + if ($measurementAnchor !== '') { + $anchors[] = $measurementAnchor; + } + + return $this->normalizeOneLine(implode(' ', array_values(array_unique($anchors)))); + } + + private function calculateFollowUpActionAnswerDetailScore(string $answerText): int + { + $score = 0; + + if ($this->referenceAnchorExtractor->extractFirstProductModelAnchor($answerText) !== '') { + ++$score; + } + + if ($this->referenceAnchorExtractor->extractFirstMeasurementValueAnchor($answerText) !== '') { + ++$score; + } + + return $score; } private function resolveFollowUpActionShopProductRole(ShopProductResult $product): string @@ -5026,7 +5146,7 @@ final readonly class AgentRunner * @param array> $actions * @param array $seenActionKeys * @param array> $items - * @param array{shop_query:string, answer_has_price:bool, role_counts:array} $context + * @param array{shop_query:string, answer_has_price:bool, answer_text:string, answer_anchor:string, answer_detail_score:int, role_counts:array} $context */ private function appendFollowUpActions(array &$actions, array &$seenActionKeys, array $items, array $context): void { @@ -5062,13 +5182,26 @@ final readonly class AgentRunner /** * @param array $item - * @param array{shop_query:string, answer_has_price:bool, role_counts:array} $context + * @param array{shop_query:string, answer_has_price:bool, answer_text:string, answer_anchor:string, answer_detail_score:int, role_counts:array} $context */ private function shouldShowFollowUpAction(array $item, array $context): bool { $actionType = isset($item['action_type']) && is_scalar($item['action_type']) ? trim((string) $item['action_type']) : ''; $targetRole = isset($item['target_role']) && is_scalar($item['target_role']) ? trim((string) $item['target_role']) : ''; + if ($this->followUpActionAnswerMatchesAnyConfiguredPattern($context['answer_text'], $item['hide_when_answer_matches_any'] ?? [])) { + return false; + } + + $hideAtDetailScore = $this->optionalFollowUpActionInt($item, 'hide_when_answer_detail_score_at_least'); + if ($hideAtDetailScore !== null && $context['answer_detail_score'] >= $hideAtDetailScore) { + return false; + } + + if ($this->optionalFollowUpActionBool($item, 'requires_answer_anchor') && $context['answer_anchor'] === '') { + return false; + } + if ($actionType === 'shop_search') { return $context['shop_query'] !== ''; } @@ -5081,9 +5214,79 @@ final readonly class AgentRunner return $context['shop_query'] !== '' && $this->isFollowUpRoleFilterMeaningful($targetRole, $context['role_counts']); } + if ($actionType === 'technical_details') { + return $context['answer_anchor'] !== ''; + } + return true; } + private function optionalFollowUpActionBool(array $item, string $key): bool + { + if (!array_key_exists($key, $item)) { + return false; + } + + $value = $item[$key]; + + if (is_bool($value)) { + return $value; + } + + if (is_scalar($value)) { + $normalized = mb_strtolower(trim((string) $value), 'UTF-8'); + + return in_array($normalized, ['1', 'true', 'yes', 'on'], true); + } + + return false; + } + + private function optionalFollowUpActionInt(array $item, string $key): ?int + { + if (!isset($item[$key]) || !is_scalar($item[$key])) { + return null; + } + + $value = trim((string) $item[$key]); + if ($value === '' || !preg_match('/^-?\d+$/', $value)) { + return null; + } + + return (int) $value; + } + + /** + * @param mixed $patterns + */ + private function followUpActionAnswerMatchesAnyConfiguredPattern(string $answerText, mixed $patterns): bool + { + if (!is_array($patterns) || $answerText === '') { + return false; + } + + foreach ($patterns as $pattern) { + if (!is_scalar($pattern)) { + continue; + } + + $pattern = trim((string) $pattern); + if ($pattern === '') { + continue; + } + + if (@preg_match($pattern, '') === false) { + continue; + } + + if (@preg_match($pattern, $answerText) === 1) { + return true; + } + } + + return false; + } + /** * @param array $roleCounts */ @@ -5093,21 +5296,25 @@ final readonly class AgentRunner return false; } - $totalProducts = array_sum($roleCounts); - if ($totalProducts <= 0) { + $knownRoleTotal = ($roleCounts[ProductRoleResolver::ROLE_MAIN_DEVICE] ?? 0) + + ($roleCounts[ProductRoleResolver::ROLE_ACCESSORY_OR_CONSUMABLE] ?? 0); + + if ($knownRoleTotal <= 0) { return false; } - return $roleCounts[$targetRole] < $totalProducts; + return $roleCounts[$targetRole] < $knownRoleTotal; } /** - * @param array{shop_query:string, answer_has_price:bool, role_counts:array} $context + * @param array{shop_query:string, answer_has_price:bool, answer_text:string, answer_anchor:string, answer_detail_score:int, role_counts:array} $context */ private function renderFollowUpActionPrompt(string $prompt, array $context): string { - $shopQuery = $context['shop_query']; - $rendered = str_replace('{shop_query}', $shopQuery, $prompt); + $rendered = strtr($prompt, [ + '{shop_query}' => $context['shop_query'], + '{answer_anchor}' => $context['answer_anchor'], + ]); return $this->normalizeOneLine($rendered); } diff --git a/src/Config/AgentRunnerConfig.php b/src/Config/AgentRunnerConfig.php index 564c770..49ff925 100644 --- a/src/Config/AgentRunnerConfig.php +++ b/src/Config/AgentRunnerConfig.php @@ -601,7 +601,7 @@ final class AgentRunnerConfig 'prompt' => $prompt, ]; - foreach (['action_type', 'target_role'] as $optionalKey) { + foreach (['action_type', 'target_role', 'hide_when_answer_detail_score_at_least'] as $optionalKey) { if (isset($item[$optionalKey]) && is_scalar($item[$optionalKey])) { $optionalValue = trim((string) $item[$optionalKey]); if ($optionalValue !== '') { @@ -610,6 +610,28 @@ final class AgentRunnerConfig } } + if (array_key_exists('requires_answer_anchor', $item) && (is_bool($item['requires_answer_anchor']) || is_scalar($item['requires_answer_anchor']))) { + $action['requires_answer_anchor'] = $item['requires_answer_anchor']; + } + + if (isset($item['hide_when_answer_matches_any']) && is_array($item['hide_when_answer_matches_any'])) { + $patterns = []; + foreach ($item['hide_when_answer_matches_any'] as $pattern) { + if (!is_scalar($pattern)) { + continue; + } + + $pattern = trim((string) $pattern); + if ($pattern !== '') { + $patterns[] = $pattern; + } + } + + if ($patterns !== []) { + $action['hide_when_answer_matches_any'] = array_values(array_unique($patterns)); + } + } + $out[] = $action; } diff --git a/src/Config/ChatMessagesConfig.php b/src/Config/ChatMessagesConfig.php index 6159bc5..df240b1 100644 --- a/src/Config/ChatMessagesConfig.php +++ b/src/Config/ChatMessagesConfig.php @@ -543,7 +543,7 @@ final class ChatMessagesConfig 'prompt' => $prompt, ]; - foreach (['action_type', 'target_role'] as $optionalKey) { + foreach (['action_type', 'target_role', 'hide_when_answer_detail_score_at_least'] as $optionalKey) { if (isset($item[$optionalKey]) && is_scalar($item[$optionalKey])) { $optionalValue = trim((string) $item[$optionalKey]); if ($optionalValue !== '') { @@ -552,6 +552,28 @@ final class ChatMessagesConfig } } + if (array_key_exists('requires_answer_anchor', $item) && (is_bool($item['requires_answer_anchor']) || is_scalar($item['requires_answer_anchor']))) { + $action['requires_answer_anchor'] = $item['requires_answer_anchor']; + } + + if (isset($item['hide_when_answer_matches_any']) && is_array($item['hide_when_answer_matches_any'])) { + $patterns = []; + foreach ($item['hide_when_answer_matches_any'] as $pattern) { + if (!is_scalar($pattern)) { + continue; + } + + $pattern = trim((string) $pattern); + if ($pattern !== '') { + $patterns[] = $pattern; + } + } + + if ($patterns !== []) { + $action['hide_when_answer_matches_any'] = array_values(array_unique($patterns)); + } + } + $out[] = $action; }