$config */ public function __construct( private readonly array $config = [], ) { } public function getCommerceHistoryBudgetChars(): int { return $this->getRequiredInt('commerce_history_budget_chars'); } public function getProductSearchKnowledgeChunkLimit(): int { return $this->getRequiredInt('product_search_knowledge_chunk_limit'); } public function getAdvisoryProductSearchKnowledgeChunkLimit(): int { return $this->getRequiredInt('advisory_product_search_knowledge_chunk_limit'); } public function getOptimizedShopQueryPrefixPattern(): string { return $this->getRequiredString('optimized_shop_query_prefix_pattern'); } public function getOptimizedShopQueryTrimCharacters(): string { return $this->getRequiredString('optimized_shop_query_trim_characters'); } /** * @return string[] */ public function getFollowUpStrongReferencePatterns(): array { return $this->getRequiredStringList('follow_up_context.strong_reference_patterns'); } /** * @return string[] */ public function getFollowUpExplicitCommercialSignalTerms(): array { 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'); } public function getFollowUpHistoryTurnSplitPattern(): string { return $this->getRequiredString('follow_up_context.history_turn_split_pattern'); } public function getFollowUpHistoryQuestionStripPattern(): string { return $this->getRequiredString('follow_up_context.history_question_strip_pattern'); } public function getFollowUpReferenceAnchorTestomatModelPattern(): string { return $this->getRequiredString('follow_up_context.reference_anchor.testomat_model_pattern'); } public function getFollowUpReferenceAnchorHardnessValuePattern(): string { return $this->getRequiredString('follow_up_context.reference_anchor.hardness_value_pattern'); } public function isInputNormalizationEnabled(): bool { return $this->getRequiredBool('input_normalization.enabled'); } public function getInputNormalizationMaxInputChars(): int { return $this->getRequiredInt('input_normalization.max_input_chars'); } public function getInputNormalizationMaxOutputChars(): int { return $this->getRequiredInt('input_normalization.max_output_chars'); } public function getInputNormalizationMaxAddedTokens(): int { return $this->getRequiredInt('input_normalization.max_added_tokens'); } public function getInputNormalizationMaxLengthRatioPercent(): int { return $this->getRequiredInt('input_normalization.max_length_ratio_percent'); } public function getInputNormalizationHeartbeatMessage(): string { return $this->getRequiredString('input_normalization.heartbeat_message'); } public function getInputNormalizationOutputPrefixPattern(): string { return $this->getRequiredString('input_normalization.output_prefix_pattern'); } /** * @return string[] */ public function getInputNormalizationSkipPatterns(): array { return $this->getRequiredStringList('input_normalization.skip_patterns'); } public function getInputNormalizationPrompt(string $prompt): string { return $this->implodePromptBlocks([ $this->getInputNormalizationIntro(), $this->buildRulesBlock($this->getInputNormalizationRules()), $this->getInputNormalizationOutputFormatBlock(), $this->getInputNormalizationCurrentUserInputLabel() . ':', trim($prompt), ]); } /** * @return string[] */ public function getInputNormalizationRules(): array { return $this->getRequiredStringList('input_normalization.prompt.rules'); } public function getInputNormalizationIntro(): string { return $this->getRequiredString('input_normalization.prompt.intro'); } public function getInputNormalizationOutputFormatBlock(): string { return $this->getRequiredString('input_normalization.prompt.output_format_block'); } public function getInputNormalizationCurrentUserInputLabel(): string { return $this->getRequiredString('input_normalization.prompt.current_user_input_label'); } public function isInputNormalizationFuzzyRoutingEnabled(): bool { return $this->getRequiredBool('input_normalization.fuzzy_routing.enabled'); } public function getInputNormalizationFuzzyRoutingMinTokenLength(): int { return $this->getRequiredInt('input_normalization.fuzzy_routing.min_token_length'); } public function getInputNormalizationFuzzyRoutingMediumTokenLength(): int { return $this->getRequiredInt('input_normalization.fuzzy_routing.medium_token_length'); } public function getInputNormalizationFuzzyRoutingLongTokenLength(): int { return $this->getRequiredInt('input_normalization.fuzzy_routing.long_token_length'); } public function getInputNormalizationFuzzyRoutingMaxDistanceShort(): int { return $this->getRequiredInt('input_normalization.fuzzy_routing.max_distance_short'); } public function getInputNormalizationFuzzyRoutingMaxDistanceMedium(): int { return $this->getRequiredInt('input_normalization.fuzzy_routing.max_distance_medium'); } public function getInputNormalizationFuzzyRoutingMaxDistanceLong(): int { return $this->getRequiredInt('input_normalization.fuzzy_routing.max_distance_long'); } public function getInputNormalizationFuzzyRoutingMinSimilarityPercent(): int { return $this->getRequiredInt('input_normalization.fuzzy_routing.min_similarity_percent'); } /** * @return string[] */ public function getInputNormalizationFuzzyRoutingTerms(): array { return $this->getRequiredStringList('input_normalization.fuzzy_routing.terms'); } private function getRequiredInt(string $key): int { $value = $this->requiredValue($key); if (is_numeric($value)) { return (int) $value; } throw new \InvalidArgumentException(sprintf('RetrieX agent config key "%s" must be numeric.', $key)); } private function getRequiredBool(string $key): bool { $value = $this->requiredValue($key); 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 agent config key "%s" must be boolean.', $key)); } private function getRequiredString(string $key): string { $value = $this->requiredValue($key); if (is_string($value) && $value !== '') { return $value; } 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; } /** * @return array */ private function getOptionalStringMap(string $key): array { $value = $this->optionalValue($key); if ($value === null) { return []; } if (!is_array($value)) { throw new \InvalidArgumentException(sprintf('RetrieX agent config key "%s" must be a string map.', $key)); } $out = []; foreach ($value as $mapKey => $mapValue) { if (!is_scalar($mapKey) || !is_scalar($mapValue)) { continue; } $mapKey = trim((string) $mapKey); $mapValue = trim((string) $mapValue); if ($mapKey !== '' && $mapValue !== '') { $out[$mapKey] = $mapValue; } } return $out; } private function requiredValue(string $key): mixed { $current = $this->config; foreach (explode('.', $key) as $segment) { if (!is_array($current) || !array_key_exists($segment, $current)) { throw new \InvalidArgumentException(sprintf('RetrieX agent config key "%s" is required.', $key)); } $current = $current[$segment]; } return $current; } private function optionalValue(string $key): mixed { $current = $this->config; foreach (explode('.', $key) as $segment) { if (!is_array($current) || !array_key_exists($segment, $current)) { return null; } $current = $current[$segment]; } return $current; } public function getEmptyPromptMessage(): string { return $this->getRequiredString('messages.empty_prompt'); } public function getAnalyzeRequestMessage(): string { return $this->getRequiredString('messages.analyze_request'); } public function getCheckInternetSourcesMessage(): string { return $this->getRequiredString('messages.check_internet_sources'); } public function getRetrieveKnowledgeMessage(): string { return $this->getRequiredString('messages.retrieve_knowledge'); } public function getOptimizeSearchMessage(): string { return $this->getRequiredString('messages.optimize_search'); } public function getNoConcreteShopQueryMessage(): string { return $this->getRequiredString('messages.no_concrete_shop_query'); } public function getFetchSearchDataMessageTemplate(): string { return $this->getRequiredString('messages.fetch_search_data_template'); } public function getAnalyzeAllInformationMessage(): string { return $this->getRequiredString('messages.analyze_all_information'); } public function getThinkingWhileStreamingMessage(): string { return $this->getRequiredString('messages.thinking_while_streaming'); } public function getNoLlmDataReceivedMessage(): string { return $this->getRequiredString('messages.no_llm_data_received'); } public function getNoLlmFallbackMaxShopResults(): int { return $this->getRequiredInt('no_llm_fallback.max_shop_results'); } /** * @return string[] */ public function getRagEvidenceStopTerms(): array { return $this->getRequiredStringList('rag_evidence_guard.stop_terms'); } /** * @return array */ public function getRagEvidenceSynonyms(): array { return $this->getRequiredStringListMap('rag_evidence_guard.synonyms'); } /** * @return string[] */ public function getRagEvidenceAggregateQueryPatterns(): array { return $this->getRequiredStringList('rag_evidence_guard.aggregate_query_patterns'); } /** * @return string[] */ public function getRagEvidenceAggregateEvidenceTerms(): array { return $this->getRequiredStringList('rag_evidence_guard.aggregate_evidence_terms'); } /** * @return string[] */ public function getRagEvidenceAggregateAnswerEvidencePatterns(): array { return $this->getRequiredStringList('rag_evidence_guard.aggregate_answer_evidence_patterns'); } public function getNoLlmFallbackShopOnlyMessage(): string { return $this->getRequiredString('no_llm_fallback.messages.shop_only'); } public function getNoLlmFallbackShopWithKnowledgeMessage(): string { return $this->getRequiredString('no_llm_fallback.messages.shop_with_knowledge'); } public function getNoLlmFallbackAccessoryOnlyForMainDeviceMessage(): string { return $this->getRequiredString('no_llm_fallback.messages.accessory_only_for_main_device'); } public function getNoLlmFallbackEscalationMessage(): string { return $this->getRequiredString('no_llm_fallback.messages.escalation'); } public function getNoLlmFallbackKnowledgeOnlyMessage(): string { return $this->getRequiredString('no_llm_fallback.messages.knowledge_only'); } public function getNoLlmFallbackNoDataMessage(): string { return $this->getRequiredString('no_llm_fallback.messages.no_data'); } public function getNoLlmFallbackNoShopResultsWithKnowledgeMessage(): string { return $this->getRequiredString('no_llm_fallback.messages.no_shop_results_with_knowledge'); } public function getNoLlmFallbackNoShopResultsNoKnowledgeMessage(): string { return $this->getRequiredString('no_llm_fallback.messages.no_shop_results_no_knowledge'); } /** * @return string[] */ public function getNoLlmMainDeviceRequestRoleKeywords(): array { return $this->getRequiredStringList('no_llm_fallback.product_roles.main_device_request_keywords'); } /** * @return string[] */ public function getNoLlmAccessoryProductRoleKeywords(): array { return $this->getRequiredStringList('no_llm_fallback.product_roles.accessory_product_keywords'); } public function getNoLlmFallbackShopUnavailableWithKnowledgeMessage(): string { return $this->getRequiredString('no_llm_fallback.messages.shop_unavailable_with_knowledge'); } public function getNoLlmFallbackShopUnavailableNoKnowledgeMessage(): string { return $this->getRequiredString('no_llm_fallback.messages.shop_unavailable_no_knowledge'); } public function getGenericInternalErrorMessage(): string { return $this->getRequiredString('messages.generic_internal_error'); } public function getDebugInternalErrorPrefix(): string { return $this->getRequiredString('messages.debug_internal_error_prefix'); } public function getExternalUrlSourceLabel(): string { return $this->getRequiredString('source_labels.external_url'); } public function getRagKnowledgeSourceLabel(): string { return $this->getRequiredString('source_labels.rag_knowledge'); } public function getConversationHistorySourceLabel(): string { return $this->getRequiredString('source_labels.conversation_history'); } public function getShopSystemSourceLabel(): string { return $this->getRequiredString('source_labels.shop_system'); } public function getExtendedShopSearchSourceLabel(): string { return $this->getRequiredString('source_labels.extended_shop_search'); } public function getUsedSourcesPrefix(): string { return $this->getRequiredString('source_labels.used_sources_prefix'); } public function getSourcesPrefix(): string { return $this->getRequiredString('source_labels.sources_prefix'); } public function getSourceBadgeHtmlTemplate(): string { return $this->getRequiredString('html.source_badge_template'); } public function getErrorHtmlTemplate(): string { return $this->getRequiredString('html.error_template'); } public function getThinkHtmlTemplate(): string { return $this->getRequiredString('html.think_template'); } public function getInfoHtmlTemplate(): string { return $this->getRequiredString('html.info_template'); } public function getDebugHtmlTemplate(): string { return $this->getRequiredString('html.debug_template'); } public function getShopPrompt(string $prompt, string $commerceHistoryContext = ''): string { $historyBlock = ''; if (trim($commerceHistoryContext) !== '') { $historyBlock = $this->buildHistoryBlock($commerceHistoryContext); } return $this->implodePromptBlocks([ $this->getShopPromptIntro(), $this->buildRulesBlock($this->getShopPromptRules()), $this->getShopPromptOutputFormatBlock(), $historyBlock, $this->getCurrentUserInputLabel() . ':', trim($prompt), ]); } private function buildHistoryBlock(string $commerceHistoryContext): string { return $this->implodePromptBlocks([ $this->getRecentConversationContextLabel() . ':', trim($commerceHistoryContext), $this->buildRulesBlock($this->getConversationContextRules(), 'Additional rules for conversation context:'), ]); } /** * @return string[] */ public function getShopPromptRules(): array { return $this->getRequiredStringList('shop_prompt.rules'); } /** * @return string[] */ public function getConversationContextRules(): array { return $this->getRequiredStringList('shop_prompt.conversation_context_rules'); } public function getShopPromptIntro(): string { return $this->getRequiredString('shop_prompt.intro'); } public function getShopPromptOutputFormatBlock(): string { return $this->getRequiredString('shop_prompt.output_format_block'); } public function getRecentConversationContextLabel(): string { return $this->getRequiredString('shop_prompt.recent_conversation_context_label'); } public function getCurrentUserInputLabel(): string { return $this->getRequiredString('shop_prompt.current_user_input_label'); } public function isShopQueryLanguagePreservationEnabled(): bool { return $this->getRequiredBool('shop_prompt.language_preservation.enabled'); } /** * @return array */ public function getShopQueryLanguageMarkers(): array { $value = $this->requiredValue('shop_prompt.language_preservation.language_markers'); if (!is_array($value)) { throw new \InvalidArgumentException('RetrieX agent config key "shop_prompt.language_preservation.language_markers" must be a map of marker lists.'); } $out = []; foreach ($value as $language => $markers) { if (!is_string($language) || !is_array($markers)) { continue; } $cleanMarkers = []; foreach ($markers as $marker) { if (!is_scalar($marker)) { continue; } $marker = strtolower((string) $marker); if ($marker !== '') { $cleanMarkers[] = $marker; } } if ($cleanMarkers !== []) { $out[$language] = array_values(array_unique($cleanMarkers)); } } if ($out === []) { throw new \InvalidArgumentException('RetrieX agent config key "shop_prompt.language_preservation.language_markers" must contain at least one valid marker list.'); } return $out; } public function isShopQueryMetaGuardEnabled(): bool { return $this->getRequiredBool('shop_prompt.meta_query_guard.enabled'); } /** * @return string[] */ public function getShopQueryMetaOnlyTerms(): array { return $this->getRequiredStringList('shop_prompt.meta_query_guard.meta_only_terms'); } public function isShopQueryContextFallbackEnabled(): bool { return $this->getRequiredBool('shop_prompt.meta_query_guard.context_fallback_enabled'); } public function getShopQueryContextFallbackQuestionLimit(): int { return $this->getRequiredInt('shop_prompt.meta_query_guard.context_fallback_question_limit'); } public function getShopQueryContextFallbackHistoryBudgetChars(): int { return $this->getRequiredInt('shop_prompt.meta_query_guard.context_fallback_history_budget_chars'); } public function shouldUseFullHistoryForShopQueryContextFallback(): bool { return $this->getRequiredBool('shop_prompt.meta_query_guard.context_fallback_use_full_history'); } public function getShopQueryContextFallbackMaxTerms(): int { return $this->getRequiredInt('shop_prompt.meta_query_guard.context_fallback_max_terms'); } /** * @return string[] */ public function getShopQueryContextFallbackFilterTerms(): array { return $this->getRequiredStringList('shop_prompt.meta_query_guard.context_fallback_filter_terms'); } public function isShopQueryContextAnchorEnrichmentEnabled(): bool { return $this->getRequiredBool('shop_prompt.context_anchor_enrichment.enabled'); } public function getShopQueryContextAnchorEnrichmentMaxQueryTerms(): int { return $this->getRequiredInt('shop_prompt.context_anchor_enrichment.max_query_terms'); } /** * @return string[] */ public function getShopQueryContextAnchorEnrichmentTriggerTerms(): array { return $this->getRequiredStringList('shop_prompt.context_anchor_enrichment.trigger_terms'); } /** * @return string[] */ public function getShopQueryContextAnchorEnrichmentPatterns(): array { return $this->getRequiredStringList('shop_prompt.context_anchor_enrichment.anchor_patterns'); } public function getShopQueryContextAnchorEnrichmentTemplate(): string { return $this->getRequiredString('shop_prompt.context_anchor_enrichment.template'); } public function getShopQueryTranslationReplacements(string $language): array { $value = $this->getOptionalStringMap('shop_prompt.language_preservation.translation_replacements.' . $language); $out = []; foreach ($value as $source => $target) { $source = strtolower(trim($source)); $target = trim($target); if ($source !== '' && $target !== '') { $out[$source] = $target; } } uksort($out, static fn(string $a, string $b): int => strlen($b) <=> strlen($a)); return $out; } private function buildRulesBlock(array $rules, string $headline = 'Rules:'): string { return $headline . "\n" . implode("\n", $rules); } /** * @param string[] $blocks */ private function implodePromptBlocks(array $blocks): string { $normalized = array_values(array_filter( array_map( static fn(string $block): string => trim($block), $blocks ), static fn(string $block): bool => $block !== '' )); return implode("\n\n", $normalized); } }