diff --git a/RETRIEX_PATCH_7_0B_AGENT_MESSAGES_LABELS_HTML_YAML_ONLY_README.md b/RETRIEX_PATCH_7_0B_AGENT_MESSAGES_LABELS_HTML_YAML_ONLY_README.md new file mode 100644 index 0000000..fd634f8 --- /dev/null +++ b/RETRIEX_PATCH_7_0B_AGENT_MESSAGES_LABELS_HTML_YAML_ONLY_README.md @@ -0,0 +1,44 @@ +# RetrieX Patch 7.0b - AgentRunner messages, labels and HTML YAML-only + +## Scope + +This patch keeps the AgentRunnerConfig migration small and focused. It converts only these AgentRunnerConfig areas from YAML-with-PHP-fallback to YAML-only: + +- runtime/status messages +- No-LLM fallback messages and product-role keyword lists +- RAG evidence guard stop terms and synonyms +- source labels +- HTML templates + +It does not change Shop-Prompt, Meta-Guard, context-anchor enrichment, PromptBuilder, Retrieval, CommerceQueryParser, ShopService or SSE logic. + +## Changed file + +- `src/Config/AgentRunnerConfig.php` + +## Expected audit effect + +The following AgentRunner fallback accessors should disappear from `mto:agent:config:audit-source --details`: + +- `messages.*` +- `no_llm_fallback.*` +- `rag_evidence_guard.*` +- `source_labels.*` +- `html.*` + +The remaining AgentRunner fallbacks should mostly be the `shop_prompt.*` group, which is intentionally left for Patch 7.0c. + +## Validation + +Run after applying: + +```bash +php bin/console cache:clear +php bin/console mto:agent:config:validate +php bin/console mto:agent:config:audit-source --details +php bin/console mto:agent:regression:test +``` + +## Notes + +No YAML values were changed in this patch. It assumes the existing `config/retriex/agent.yaml` already contains the mapped values, which was confirmed by the previous audit state. diff --git a/src/Config/AgentRunnerConfig.php b/src/Config/AgentRunnerConfig.php index 384c200..879e3c2 100644 --- a/src/Config/AgentRunnerConfig.php +++ b/src/Config/AgentRunnerConfig.php @@ -6,106 +6,6 @@ namespace App\Config; final class AgentRunnerConfig { - private const NO_LLM_MAIN_DEVICE_REQUEST_ROLE_KEYWORDS = [ - 'anlage', - 'messanlage', - 'gerät', - 'geraet', - 'messgerät', - 'messgeraet', - 'analysegerät', - 'analysegeraet', - 'analysator', - 'analyzer', - 'system', - 'testomat', - 'testomaten', - 'testoamt', - 'testomate', - 'pockettester', - ]; - - private const RAG_EVIDENCE_STOP_TERMS = [ - 'suche', - 'suchen', - 'finde', - 'finden', - 'zeige', - 'einen', - 'eine', - 'einem', - 'einer', - 'der', - 'die', - 'das', - 'den', - 'dem', - 'des', - 'für', - 'fuer', - 'mit', - 'ohne', - 'und', - 'oder', - 'kann', - 'können', - 'koennen', - 'messen', - 'messung', - 'tester', - 'testgerät', - 'testgeraet', - 'gerät', - 'geraet', - 'messgerät', - 'messgeraet', - 'produkt', - 'produkte', - 'artikel', - 'shop', - ]; - - private const RAG_EVIDENCE_SYNONYMS = [ - 'salinität' => ['salinität', 'salinitaet', 'salinity', 'salzgehalt', 'tds', 'leitfähigkeit', 'leitfaehigkeit'], - 'salinitaet' => ['salinität', 'salinitaet', 'salinity', 'salzgehalt', 'tds', 'leitfähigkeit', 'leitfaehigkeit'], - 'salinity' => ['salinität', 'salinitaet', 'salinity', 'salzgehalt', 'tds', 'leitfähigkeit', 'leitfaehigkeit'], - 'redox' => ['redox', 'orp', 'oxidations-reduktionspotential', 'oxidations reduktionspotential'], - 'orp' => ['redox', 'orp', 'oxidations-reduktionspotential', 'oxidations reduktionspotential'], - 'ph' => ['ph', 'ph-wert', 'ph wert'], - 'chlor' => ['chlor', 'freies chlor', 'gesamtchlor', 'chlorine'], - ]; - - private const NO_LLM_ACCESSORY_PRODUCT_ROLE_KEYWORDS = [ - 'indikator', - 'indicator', - 'indikatortyp', - 'reagenz', - 'reagent', - 'reagenzsatz', - 'kalibrierlösung', - 'kalibrierloesung', - 'pufferlösung', - 'pufferloesung', - 'reinigungslösung', - 'reinigungsloesung', - 'kalibrier', - 'puffer', - 'zubehör', - 'zubehor', - 'accessory', - 'ersatzteil', - 'verbrauch', - 'consumable', - 'kit', - 'set', - 'flasche', - 'bottle', - '100 ml', - '500 ml', - '100ml', - '500ml', - ]; - /** * @param array $config */ @@ -198,6 +98,85 @@ final class AgentRunnerConfig throw new \InvalidArgumentException(sprintf('RetrieX agent config key "%s" must be a non-empty string.', $key)); } + /** + * @return string[] + */ + private function getRequiredStringList(string $key): array + { + $value = $this->requiredValue($key); + + if (!is_array($value)) { + throw new \InvalidArgumentException(sprintf('RetrieX agent config key "%s" must be a list.', $key)); + } + + $out = []; + + foreach ($value as $item) { + if (!is_scalar($item)) { + continue; + } + + $item = trim((string) $item); + + if ($item !== '') { + $out[] = $item; + } + } + + if ($out === []) { + throw new \InvalidArgumentException(sprintf('RetrieX agent config key "%s" must contain at least one non-empty value.', $key)); + } + + return $out; + } + + /** + * @return array + */ + private function getRequiredStringListMap(string $key): array + { + $value = $this->requiredValue($key); + + if (!is_array($value)) { + throw new \InvalidArgumentException(sprintf('RetrieX agent config key "%s" must be a map of string lists.', $key)); + } + + $out = []; + + foreach ($value as $mapKey => $items) { + if (!is_scalar($mapKey) || !is_array($items)) { + continue; + } + + $mapKey = trim((string) $mapKey); + if ($mapKey === '') { + continue; + } + + $terms = []; + foreach ($items as $item) { + if (!is_scalar($item)) { + continue; + } + + $item = trim((string) $item); + if ($item !== '' && !in_array($item, $terms, true)) { + $terms[] = $item; + } + } + + if ($terms !== []) { + $out[$mapKey] = $terms; + } + } + + if ($out === []) { + throw new \InvalidArgumentException(sprintf('RetrieX agent config key "%s" must contain at least one valid entry.', $key)); + } + + return $out; + } + /** * @param string[] $default * @return string[] @@ -259,60 +238,57 @@ final class AgentRunnerConfig public function getEmptyPromptMessage(): string { - return $this->getString('messages.empty_prompt', '❌ Empty prompt.'); + return $this->getRequiredString('messages.empty_prompt'); } public function getAnalyzeRequestMessage(): string { - return $this->getString('messages.analyze_request', 'Ich analysiere deine Anfrage...'); + return $this->getRequiredString('messages.analyze_request'); } public function getCheckInternetSourcesMessage(): string { - return $this->getString('messages.check_internet_sources', 'Ich prüfe auf Internetquellen...'); + return $this->getRequiredString('messages.check_internet_sources'); } public function getRetrieveKnowledgeMessage(): string { - return $this->getString('messages.retrieve_knowledge', 'Ich hole relevante Daten aus meinem RAG-Wissen...'); + return $this->getRequiredString('messages.retrieve_knowledge'); } public function getOptimizeSearchMessage(): string { - return $this->getString('messages.optimize_search', 'Ich optimiere die Recherche...'); + return $this->getRequiredString('messages.optimize_search'); } public function getNoConcreteShopQueryMessage(): string { - return $this->getString( - 'messages.no_concrete_shop_query', - 'Ich kann die Shop-Suche noch nicht belastbar auflösen. Bitte nenne das Produkt, den Messparameter oder das Zubehör etwas konkreter.' - ); + return $this->getRequiredString('messages.no_concrete_shop_query'); } public function getFetchSearchDataMessageTemplate(): string { - return $this->getString('messages.fetch_search_data_template', 'Ich rufe Recherchedaten ab (type: %s)'); + return $this->getRequiredString('messages.fetch_search_data_template'); } public function getAnalyzeAllInformationMessage(): string { - return $this->getString('messages.analyze_all_information', 'Ich analysiere alle Informationen...'); + return $this->getRequiredString('messages.analyze_all_information'); } public function getThinkingWhileStreamingMessage(): string { - return $this->getString('messages.thinking_while_streaming', 'Denke nach...'); + return $this->getRequiredString('messages.thinking_while_streaming'); } public function getNoLlmDataReceivedMessage(): string { - return $this->getString('messages.no_llm_data_received', '❌ Es wurden keine Daten vom LLM empfangen.'); + return $this->getRequiredString('messages.no_llm_data_received'); } public function getNoLlmFallbackMaxShopResults(): int { - return $this->getInt('no_llm_fallback.max_shop_results', 5); + return $this->getRequiredInt('no_llm_fallback.max_shop_results'); } /** @@ -320,7 +296,7 @@ final class AgentRunnerConfig */ public function getRagEvidenceStopTerms(): array { - return $this->getStringList('rag_evidence_guard.stop_terms', self::RAG_EVIDENCE_STOP_TERMS); + return $this->getRequiredStringList('rag_evidence_guard.stop_terms'); } /** @@ -328,106 +304,47 @@ final class AgentRunnerConfig */ public function getRagEvidenceSynonyms(): array { - $value = $this->value('rag_evidence_guard.synonyms', self::RAG_EVIDENCE_SYNONYMS); - - if (!is_array($value)) { - return self::RAG_EVIDENCE_SYNONYMS; - } - - $out = []; - - foreach ($value as $key => $items) { - if (!is_scalar($key) || !is_array($items)) { - continue; - } - - $key = trim((string) $key); - if ($key === '') { - continue; - } - - $terms = []; - foreach ($items as $item) { - if (!is_scalar($item)) { - continue; - } - - $item = trim((string) $item); - if ($item !== '' && !in_array($item, $terms, true)) { - $terms[] = $item; - } - } - - if ($terms !== []) { - $out[$key] = $terms; - } - } - - return $out !== [] ? $out : self::RAG_EVIDENCE_SYNONYMS; + return $this->getRequiredStringListMap('rag_evidence_guard.synonyms'); } public function getNoLlmFallbackShopOnlyMessage(): string { - return $this->getString( - 'no_llm_fallback.messages.shop_only', - 'Ich finde dazu im RAG-Wissen keine belastbare Fachinformation. Aus den Shopdaten ergeben sich folgende Treffer; technische Eignung bitte prüfen:' - ); + return $this->getRequiredString('no_llm_fallback.messages.shop_only'); } public function getNoLlmFallbackShopWithKnowledgeMessage(): string { - return $this->getString( - 'no_llm_fallback.messages.shop_with_knowledge', - 'Es liegen RAG-/Kontexttreffer und Shopdaten vor. Ohne LLM leite ich daraus keine technische Eignung ab. Die Shopdaten zeigen folgende Treffer; technische Eignung bitte prüfen:' - ); + return $this->getRequiredString('no_llm_fallback.messages.shop_with_knowledge'); } public function getNoLlmFallbackAccessoryOnlyForMainDeviceMessage(): string { - return $this->getString( - 'no_llm_fallback.messages.accessory_only_for_main_device', - 'Die Shop-Treffer wirken wie Zubehör/Verbrauchsmaterial und nicht wie eine angefragte Messanlage oder ein Hauptgerät. Ich werte sie deshalb nicht als passende Hauptlösung.' - ); + return $this->getRequiredString('no_llm_fallback.messages.accessory_only_for_main_device'); } public function getNoLlmFallbackEscalationMessage(): string { - return $this->getString( - 'no_llm_fallback.messages.escalation', - 'Für eine verbindliche Produktauswahl sollte der konkrete Anwendungsfall durch Vertrieb oder Support geprüft werden.' - ); + return $this->getRequiredString('no_llm_fallback.messages.escalation'); } public function getNoLlmFallbackKnowledgeOnlyMessage(): string { - return $this->getString( - 'no_llm_fallback.messages.knowledge_only', - 'Ich habe Treffer im RAG-Wissen gefunden, aber ohne LLM kann ich daraus keine belastbare fachliche Antwort synthetisieren. Ich gebe deshalb keine sichere Produktaussage aus. Bitte aktiviere das LLM oder konkretisiere die Frage für eine gezielte Prüfung.' - ); + return $this->getRequiredString('no_llm_fallback.messages.knowledge_only'); } public function getNoLlmFallbackNoDataMessage(): string { - return $this->getString( - 'no_llm_fallback.messages.no_data', - 'Ich finde dazu keine belastbaren Daten in den vorliegenden Quellen. Bitte nenne Produkt, Messparameter, Zubehör oder Anwendungsfall genauer.' - ); + return $this->getRequiredString('no_llm_fallback.messages.no_data'); } public function getNoLlmFallbackNoShopResultsWithKnowledgeMessage(): string { - return $this->getString( - 'no_llm_fallback.messages.no_shop_results_with_knowledge', - 'Ich finde RAG-/Kontexttreffer, aber keine passenden Shop-Treffer zur aktuellen Suchanfrage. Das ist keine Aussage, dass es das Produkt nicht gibt. Ohne LLM gebe ich keine technische Negativaussage aus; bitte prüfe den Suchbegriff oder den Anwendungsfall gezielter.' - ); + return $this->getRequiredString('no_llm_fallback.messages.no_shop_results_with_knowledge'); } public function getNoLlmFallbackNoShopResultsNoKnowledgeMessage(): string { - return $this->getString( - 'no_llm_fallback.messages.no_shop_results_no_knowledge', - 'Ich finde weder belastbares RAG-Wissen noch passende Shop-Treffer zur aktuellen Suchanfrage. Das ist keine sichere Negativaussage. Bitte nenne Produkt, Messparameter oder Zubehör konkreter.' - ); + return $this->getRequiredString('no_llm_fallback.messages.no_shop_results_no_knowledge'); } /** @@ -435,10 +352,7 @@ final class AgentRunnerConfig */ public function getNoLlmMainDeviceRequestRoleKeywords(): array { - return $this->getStringList( - 'no_llm_fallback.product_roles.main_device_request_keywords', - self::NO_LLM_MAIN_DEVICE_REQUEST_ROLE_KEYWORDS - ); + return $this->getRequiredStringList('no_llm_fallback.product_roles.main_device_request_keywords'); } /** @@ -446,98 +360,88 @@ final class AgentRunnerConfig */ public function getNoLlmAccessoryProductRoleKeywords(): array { - return $this->getStringList( - 'no_llm_fallback.product_roles.accessory_product_keywords', - self::NO_LLM_ACCESSORY_PRODUCT_ROLE_KEYWORDS - ); + return $this->getRequiredStringList('no_llm_fallback.product_roles.accessory_product_keywords'); } public function getNoLlmFallbackShopUnavailableWithKnowledgeMessage(): string { - return $this->getString( - 'no_llm_fallback.messages.shop_unavailable_with_knowledge', - 'Live-Shopdaten konnten nicht geladen werden. Ohne Shop-Check treffe ich keine Aussage zu aktueller Verfügbarkeit, Preis oder Shop-Portfolio. Vorhandenes RAG-Wissen darf nur als fachlicher Kontext verstanden werden.' - ); + return $this->getRequiredString('no_llm_fallback.messages.shop_unavailable_with_knowledge'); } public function getNoLlmFallbackShopUnavailableNoKnowledgeMessage(): string { - return $this->getString( - 'no_llm_fallback.messages.shop_unavailable_no_knowledge', - 'Live-Shopdaten konnten nicht geladen werden und ich finde kein belastbares RAG-Wissen. Ich kann daraus keine verlässliche Produkt- oder Verfügbarkeitsaussage ableiten.' - ); + return $this->getRequiredString('no_llm_fallback.messages.shop_unavailable_no_knowledge'); } public function getGenericInternalErrorMessage(): string { - return $this->getString('messages.generic_internal_error', '❌ Bei der Verarbeitung der Anfrage ist ein interner Fehler aufgetreten.'); + return $this->getRequiredString('messages.generic_internal_error'); } public function getDebugInternalErrorPrefix(): string { - return $this->getString('messages.debug_internal_error_prefix', '❌ Interner Fehler: '); + return $this->getRequiredString('messages.debug_internal_error_prefix'); } public function getExternalUrlSourceLabel(): string { - return $this->getString('source_labels.external_url', 'Externe URL'); + return $this->getRequiredString('source_labels.external_url'); } public function getRagKnowledgeSourceLabel(): string { - return $this->getString('source_labels.rag_knowledge', 'RAG Wissen'); + return $this->getRequiredString('source_labels.rag_knowledge'); } public function getConversationHistorySourceLabel(): string { - return $this->getString('source_labels.conversation_history', 'Chatverlauf'); + return $this->getRequiredString('source_labels.conversation_history'); } public function getShopSystemSourceLabel(): string { - return $this->getString('source_labels.shop_system', 'Shopsystem'); + return $this->getRequiredString('source_labels.shop_system'); } public function getExtendedShopSearchSourceLabel(): string { - return $this->getString('source_labels.extended_shop_search', 'Erweiterte Shopsuche'); + return $this->getRequiredString('source_labels.extended_shop_search'); } public function getUsedSourcesPrefix(): string { - return $this->getString('source_labels.used_sources_prefix', 'Genutzte Quellen: '); + return $this->getRequiredString('source_labels.used_sources_prefix'); } public function getSourcesPrefix(): string { - return $this->getString('source_labels.sources_prefix', 'Quellen: '); + return $this->getRequiredString('source_labels.sources_prefix'); } public function getSourceBadgeHtmlTemplate(): string { - return $this->getString('html.source_badge_template', '%s'); + return $this->getRequiredString('html.source_badge_template'); } public function getErrorHtmlTemplate(): string { - return $this->getString('html.error_template', '
Hinweis
%s
' . "\n"); + return $this->getRequiredString('html.error_template'); } public function getThinkHtmlTemplate(): string { - return $this->getString('html.think_template', '%s' . "\n"); + return $this->getRequiredString('html.think_template'); } public function getInfoHtmlTemplate(): string { - return $this->getString('html.info_template', "\n\n" . '%s' . "\n"); + return $this->getRequiredString('html.info_template'); } public function getDebugHtmlTemplate(): string { - return $this->getString('html.debug_template', "\n\nDEBUG: %s\n"); + return $this->getRequiredString('html.debug_template'); } - public function getShopPrompt(string $prompt, string $commerceHistoryContext = ''): string { $historyBlock = '';