$config */ public function __construct( private readonly array $config = [], private readonly ?DomainVocabularyConfig $vocabulary = null, ) { } public function getCharsPerToken(): int { return $this->getRequiredInt('budget.chars_per_token'); } public function getHistoryPaddingChars(): int { return $this->getRequiredInt('budget.history_padding_chars'); } public function getOutputReserveRatio(): float { return $this->getRequiredFloat('budget.output_reserve_ratio'); } public function getOutputReserveMinTokens(): int { return $this->getRequiredInt('budget.output_reserve_min_tokens'); } public function getOutputReserveMaxTokens(): int { return $this->getRequiredInt('budget.output_reserve_max_tokens'); } public function getSafetyReserveRatio(): float { return $this->getRequiredFloat('budget.safety_reserve_ratio'); } public function getSafetyReserveMinTokens(): int { return $this->getRequiredInt('budget.safety_reserve_min_tokens'); } public function getSafetyReserveMaxTokens(): int { return $this->getRequiredInt('budget.safety_reserve_max_tokens'); } public function getMinPromptBudgetTokens(): int { return $this->getRequiredInt('budget.min_prompt_budget_tokens'); } public function getMaxShopResultsInPrompt(): int { return $this->getRequiredInt('shop_results.max_results_in_prompt'); } public function getDetailedShopResultsMaxCount(): int { return $this->getRequiredInt('shop_results.detailed_max_count'); } public function getTechnicalProductKeywordMatchThreshold(): int { return $this->getRequiredInt('technical_product_keyword_match_threshold'); } private function getRequiredInt(string $path): int { $value = $this->getRequiredValue($path); if (!is_numeric($value)) { throw new \InvalidArgumentException(sprintf('RetrieX prompt config value "%s" must be numeric.', $path)); } return (int) $value; } private function getRequiredFloat(string $path): float { $value = $this->getRequiredValue($path); if (!is_numeric($value)) { throw new \InvalidArgumentException(sprintf('RetrieX prompt config value "%s" must be numeric.', $path)); } return (float) $value; } private function getRequiredBool(string $path): bool { $value = $this->getRequiredValue($path); if (!is_bool($value)) { throw new \InvalidArgumentException(sprintf('RetrieX prompt config value "%s" must be boolean.', $path)); } return $value; } private function getRequiredString(string $path): string { $value = $this->getRequiredValue($path); if (!is_scalar($value)) { throw new \InvalidArgumentException(sprintf('RetrieX prompt config value "%s" must be a scalar string.', $path)); } $value = (string) $value; if (trim($value) === '') { throw new \InvalidArgumentException(sprintf('RetrieX prompt config value "%s" must not be empty.', $path)); } return $value; } /** * @return string[] */ private function getRequiredStringList(string $path): array { $value = $this->getRequiredValue($path); if (!is_array($value)) { throw new \InvalidArgumentException(sprintf('RetrieX prompt config value "%s" must be a string list.', $path)); } $out = []; foreach ($value as $item) { if (!is_scalar($item)) { continue; } $item = trim((string) $item); if ($item === '' || in_array($item, $out, true)) { continue; } $out[] = $item; } if ($out === []) { throw new \InvalidArgumentException(sprintf('RetrieX prompt config value "%s" must contain at least one string.', $path)); } return $out; } /** * @return string[] */ private function getConfiguredStringListOrVocabularyView(string $configPath, string $viewPathConfigPath): array { if ($this->hasPath($configPath)) { return $this->getRequiredStringList($configPath); } if ($this->vocabulary === null) { throw new \InvalidArgumentException(sprintf( 'RetrieX prompt config path "%s" is missing and no vocabulary resolver is available.', $configPath )); } $viewPath = $this->getRequiredString($viewPathConfigPath); $terms = $this->vocabulary->view($viewPath, []); if ($terms === []) { throw new \InvalidArgumentException(sprintf( 'RetrieX prompt vocabulary view "%s" resolved to an empty list.', $viewPath )); } return $terms; } /** * @return array */ private function getVocabularyStringListMap(string $mapPathConfigPath): array { if (!$this->hasPath($mapPathConfigPath)) { return []; } if ($this->vocabulary === null) { throw new \InvalidArgumentException(sprintf( 'RetrieX prompt vocabulary map config path "%s" is set but no vocabulary resolver is available.', $mapPathConfigPath )); } $mapPath = $this->getRequiredString($mapPathConfigPath); $map = $this->vocabulary->map($mapPath, []); if ($map === []) { throw new \InvalidArgumentException(sprintf( 'RetrieX prompt vocabulary map "%s" resolved to an empty map.', $mapPath )); } return $map; } /** * @param array $item * @param array $vocabularyMap * @return string[] */ private function getParameterStringList(array $item, string $id, string $localKey, array $vocabularyMap): array { if (array_key_exists($localKey, $item)) { return $this->normalizeMixedStringList($item[$localKey]); } return $vocabularyMap[$id] ?? []; } /** * @return string[] */ private function getOptionalStringList(string $path): array { $value = $this->getOptionalValue($path); if ($value === null) { return []; } if (!is_array($value)) { throw new \InvalidArgumentException(sprintf('RetrieX prompt config value "%s" must be a string list.', $path)); } $out = []; foreach ($value as $item) { if (!is_scalar($item)) { continue; } $item = trim((string) $item); if ($item === '' || in_array($item, $out, true)) { continue; } $out[] = $item; } return $out; } private function hasPath(string $path): bool { $current = $this->config; foreach (explode('.', $path) as $segment) { if (!is_array($current) || !array_key_exists($segment, $current)) { return false; } $current = $current[$segment]; } return true; } private function getOptionalValue(string $path): mixed { $current = $this->config; foreach (explode('.', $path) as $segment) { if (!is_array($current) || !array_key_exists($segment, $current)) { return null; } $current = $current[$segment]; } return $current; } private function getRequiredValue(string $path): mixed { $current = $this->config; foreach (explode('.', $path) as $segment) { if (!is_array($current) || !array_key_exists($segment, $current)) { throw new \InvalidArgumentException(sprintf('Missing required RetrieX prompt config path "%s".', $path)); } $current = $current[$segment]; } return $current; } public function getSystemSectionLabel(): string { return $this->getRequiredString('sections.system_label'); } public function getUserQuestionSectionLabel(): string { return $this->getRequiredString('sections.user_question_label'); } public function getConversationContextSectionLabel(): string { return $this->getRequiredString('sections.conversation_context_label'); } /** * @return string[] */ public function getConversationContextIntroLines(): array { return $this->getRequiredStringList('conversation_context.intro_lines'); } public function getShopSearchQuerySectionLabel(): string { return $this->getRequiredString('sections.shop_search_query_label'); } public function getShopSearchQuerySourceLine(): string { return $this->getRequiredString('shop_search.source_line'); } /** * @return string[] */ public function getLiveShopResultsHeaderLines(): array { return $this->getRequiredStringList('shop_results.header_lines'); } public function getLiveShopResultsOverflowNoticeTemplate(): string { return $this->getRequiredString('shop_results.overflow_notice_template'); } public function getShopRecordHeaderTemplate(): string { return $this->getRequiredString('shop_results.record_header_template'); } public function getShopExactProductNameLabel(): string { return $this->getRequiredString('shop_results.exact_product_name_label'); } /** * @return string[] */ public function getShopAtomicRecordNoteLines(): array { return $this->getRequiredStringList('shop_results.atomic_record_note_lines'); } public function getOutputPrioritySectionLabel(): string { return $this->getRequiredString('sections.output_priority_label'); } /** * @return string[] */ public function getOutputPriorityRules(): array { return $this->getRequiredStringList('output_priority.rules'); } /** * @return string[] */ public function getOutputPriorityTechnicalRules(): array { return $this->getRequiredStringList('output_priority.technical_rules'); } public function getFallbackEscalationSectionLabel(): string { return $this->getRequiredString('sections.fallback_escalation_label'); } public function getFallbackEscalationStateLineTemplate(): string { return $this->getRequiredString('fallback_escalation.state_line_template'); } public function getFallbackEscalationProvidedShopResultsContextRule(): string { return $this->getRequiredString('fallback_escalation.provided_shop_results_context_rule'); } /** * @return string[] */ public function getFallbackEscalationBaseRules(): array { return $this->getRequiredStringList('fallback_escalation.base_rules'); } /** * @return string[] */ public function getFallbackEscalationStateRules(string $state): array { return $this->getOptionalStringList('fallback_escalation.states.' . $state); } /** * @return string[] */ public function getFallbackEscalationWithoutShopCheckRules(): array { return $this->getRequiredStringList('fallback_escalation.without_shop_check_rules'); } public function getResponseFormatSectionLabel(): string { return $this->getRequiredString('sections.response_format_label'); } /** * @return string[] */ public function getResponseFormatBaseRules(): array { return $this->getRequiredStringList('response_format.base_rules'); } /** * @return string[] */ public function getResponseFormatWithShopRules(): array { return $this->getRequiredStringList('response_format.with_shop_rules'); } /** * @return string[] */ public function getResponseFormatWithoutShopRules(): array { return $this->getRequiredStringList('response_format.without_shop_rules'); } /** * @return string[] */ public function getResponseFormatTechnicalRules(): array { return $this->getRequiredStringList('response_format.technical_rules'); } /** * @return string[] */ public function getResponseFormatAccessoryRules(): array { return $this->getRequiredStringList('response_format.accessory_rules'); } public function getLanguageRulesSectionLabel(): string { return $this->getRequiredString('sections.language_rules_label'); } /** * @return string[] */ public function getLanguageRules(): array { return $this->getRequiredStringList('language.rules'); } public function getFactGroundingRulesSectionLabel(): string { return $this->getRequiredString('sections.fact_grounding_rules_label'); } /** * @return string[] */ public function getFactGroundingBaseRules(): array { return $this->getRequiredStringList('fact_grounding.base_rules'); } /** * @return string[] */ public function getFactGroundingWithShopRules(): array { return $this->getRequiredStringList('fact_grounding.with_shop_rules'); } /** * @return string[] */ public function getFactGroundingWithoutShopRules(): array { return $this->getRequiredStringList('fact_grounding.without_shop_rules'); } /** * @return string[] */ public function getFactGroundingTechnicalRules(): array { return $this->getRequiredStringList('fact_grounding.technical_rules'); } public function getRetrievedKnowledgeSectionLabel(): string { return $this->getRequiredString('sections.retrieved_knowledge_label'); } public function getRetrievedKnowledgeSourceLine(): string { return $this->getRequiredString('retrieved_knowledge.source_line'); } public function getUrlContentSectionLabel(): string { return $this->getRequiredString('sections.url_content_label'); } public function getUrlContentSourceLine(): string { return $this->getRequiredString('url_content.source_line'); } public function getShopProductNumberLabel(): string { return $this->getRequiredString('shop_results.fields.product_number_label'); } public function getShopManufacturerLabel(): string { return $this->getRequiredString('shop_results.fields.manufacturer_label'); } public function getShopPriceLabel(): string { return $this->getRequiredString('shop_results.fields.price_label'); } public function getShopAvailabilityLabel(): string { return $this->getRequiredString('shop_results.fields.availability_label'); } public function getShopAvailabilityYesLabel(): string { return $this->getRequiredString('shop_results.fields.availability_yes_label'); } public function getShopAvailabilityNoLabel(): string { return $this->getRequiredString('shop_results.fields.availability_no_label'); } public function getShopHighlightPrefix(): string { return $this->getRequiredString('shop_results.fields.highlight_prefix'); } public function getShopUrlLabel(): string { return $this->getRequiredString('shop_results.fields.url_label'); } public function getShopProductImageLabel(): string { return $this->getRequiredString('shop_results.fields.product_image_label'); } public function getShopDescriptionLabel(): string { return $this->getRequiredString('shop_results.fields.description_label'); } public function getShopMetaInformationLabel(): string { return $this->getRequiredString('shop_results.fields.meta_information_label'); } public function getShopRequestedRoleLabel(): string { return $this->getRequiredString('shop_results.fields.requested_role_label'); } public function getShopInferredRoleLabel(): string { return $this->getRequiredString('shop_results.fields.inferred_role_label'); } public function getShopRoleCompatibilityLabel(): string { return $this->getRequiredString('shop_results.fields.role_compatibility_label'); } public function getShopRoleIncompatibleCommercialSuppressionNote(): string { return $this->getRequiredString('shop_results.fields.role_incompatible_commercial_suppression_note'); } /** * @return string[] */ public function getMainDeviceRequestRoleKeywords(): array { return $this->getConfiguredStringListOrVocabularyView( 'role_guard.main_device_request_keywords', 'vocabulary_views.main_device_request_keywords' ); } /** * @return string[] */ public function getMainDeviceProductRoleKeywords(): array { return $this->getConfiguredStringListOrVocabularyView( 'role_guard.main_device_product_keywords', 'vocabulary_views.main_device_product_keywords' ); } /** * @return string[] */ public function getAccessoryProductRoleKeywords(): array { return $this->getConfiguredStringListOrVocabularyView( 'role_guard.accessory_product_keywords', 'vocabulary_views.accessory_product_keywords' ); } /** * @return string[] */ public function getDirectMainDeviceRequestPatterns(): array { return $this->getRequiredStringList('role_guard.direct_main_device_request_patterns'); } /** * @return string[] */ public function getTechnicalProductKeywords(): array { return $this->getConfiguredStringListOrVocabularyView( 'technical_product_keywords', 'vocabulary_views.technical_product_keywords' ); } /** * @return string[] */ public function getAccessoryRequestKeywords(): array { return $this->getConfiguredStringListOrVocabularyView( 'accessory_request_keywords', 'vocabulary_views.accessory_request_keywords' ); } public function getMeasurementEvidenceSectionLabel(): string { return $this->getRequiredString('sections.measurement_evidence_label'); } public function isNumericValueFocusEnabled(): bool { return $this->getRequiredBool('numeric_value_focus.enabled'); } public function getNumericValueFocusSectionLabel(): string { return $this->getRequiredString('sections.numeric_value_focus_label'); } public function getNumericValueFocusMaxValues(): int { return $this->getRequiredInt('numeric_value_focus.max_values'); } /** * @return string[] */ public function getNumericValueFocusPatterns(): array { return $this->getRequiredStringList('numeric_value_focus.value_patterns'); } /** * @return string[] */ public function getNumericValueFocusRules(): array { return $this->getRequiredStringList('numeric_value_focus.rules'); } /** * @return string[] */ public function getMeasurementEvidenceIntroRules(): array { return $this->getRequiredStringList('measurement_evidence_guard.intro_rules'); } /** * @return string[] */ public function getMeasurementEvidenceProductSpecificRules(): array { return $this->getRequiredStringList('measurement_evidence_guard.product_specific_rules'); } /** * @return string[] */ public function getMeasurementEvidenceGenericRequestPatterns(): array { return $this->getRequiredStringList('measurement_evidence_guard.generic_request_patterns'); } /** * @return string[] */ public function getMeasurementEvidenceGenericPositiveContextTerms(): array { return $this->getConfiguredStringListOrVocabularyView( 'measurement_evidence_guard.generic_positive_context_terms', 'measurement_evidence_guard.vocabulary_views.generic_positive_context_terms' ); } /** * @return string[] */ public function getMeasurementEvidenceGenericNegativeContextTerms(): array { return $this->getConfiguredStringListOrVocabularyView( 'measurement_evidence_guard.generic_negative_context_terms', 'measurement_evidence_guard.vocabulary_views.generic_negative_context_terms' ); } public function getMeasurementEvidenceGenericSafeNoEvidenceAnswerTemplate(): string { return $this->getRequiredString('measurement_evidence_guard.generic_safe_no_evidence_answer_template_de'); } public function getMeasurementEvidenceGenericSafeNoAccessoryEvidenceAnswerTemplate(): string { return $this->getRequiredString('measurement_evidence_guard.generic_safe_no_accessory_evidence_answer_template_de'); } /** * @return string[] */ public function getMeasurementEvidenceAccessoryLookupGuardTerms(): array { return $this->getConfiguredStringListOrVocabularyView( 'measurement_evidence_guard.accessory_lookup_guard_terms', 'measurement_evidence_guard.vocabulary_views.accessory_lookup_guard_terms' ); } /** * @return string[] */ public function getMeasurementEvidenceAccessoryLookupPassthroughTerms(): array { return $this->getConfiguredStringListOrVocabularyView( 'measurement_evidence_guard.accessory_lookup_passthrough_terms', 'measurement_evidence_guard.vocabulary_views.accessory_lookup_passthrough_terms' ); } public function getMeasurementEvidenceRuleTemplate(string $key): string { return $this->getRequiredString('measurement_evidence_guard.rule_templates.' . $key); } /** * @return string[] */ public function getMeasurementEvidenceFinalRules(): array { return $this->getRequiredStringList('measurement_evidence_guard.final_rules'); } public function getParameterParsingSplitPattern(): string { return $this->getRequiredString('parameter_parsing.split_pattern'); } public function getParameterParsingTrimCharacters(): string { return $this->getRequiredString('parameter_parsing.trim_characters'); } /** * @return array> */ public function getMeasurementEvidenceParameters(): array { $value = $this->getRequiredValue('measurement_evidence_guard.parameters'); if (!is_array($value)) { throw new \InvalidArgumentException('RetrieX prompt config value "measurement_evidence_guard.parameters" must be a list of parameter definitions.'); } $out = []; $genericPositiveContextTerms = $this->getMeasurementEvidenceGenericPositiveContextTerms(); $genericNegativeContextTerms = $this->getMeasurementEvidenceGenericNegativeContextTerms(); $requestTermsByParameter = $this->getVocabularyStringListMap('measurement_evidence_guard.vocabulary_maps.request_terms'); $positiveTermsByParameter = $this->getVocabularyStringListMap('measurement_evidence_guard.vocabulary_maps.positive_terms'); $nonEquivalentTermsByParameter = $this->getVocabularyStringListMap('measurement_evidence_guard.vocabulary_maps.non_equivalent_terms'); foreach ($value as $item) { if (!is_array($item)) { continue; } $id = isset($item['id']) && is_scalar($item['id']) ? trim((string) $item['id']) : ''; $label = isset($item['label']) && is_scalar($item['label']) ? trim((string) $item['label']) : ''; if ($id === '' || $label === '') { continue; } $out[] = [ 'id' => $id, 'label' => $label, 'request_terms' => $this->getParameterStringList($item, $id, 'request_terms', $requestTermsByParameter), 'positive_terms' => $this->getParameterStringList($item, $id, 'positive_terms', $positiveTermsByParameter), 'positive_context_terms' => array_key_exists('positive_context_terms', $item) ? $this->normalizeMixedStringList($item['positive_context_terms']) : $genericPositiveContextTerms, 'negative_context_terms' => array_key_exists('negative_context_terms', $item) ? $this->normalizeMixedStringList($item['negative_context_terms']) : $genericNegativeContextTerms, 'non_equivalent_terms' => $this->getParameterStringList($item, $id, 'non_equivalent_terms', $nonEquivalentTermsByParameter), 'safe_no_evidence_answer_de' => isset($item['safe_no_evidence_answer_de']) && is_scalar($item['safe_no_evidence_answer_de']) ? trim((string) $item['safe_no_evidence_answer_de']) : '', 'safe_no_accessory_evidence_answer_de' => isset($item['safe_no_accessory_evidence_answer_de']) && is_scalar($item['safe_no_accessory_evidence_answer_de']) ? trim((string) $item['safe_no_accessory_evidence_answer_de']) : '', ]; } if ($out === []) { throw new \InvalidArgumentException('RetrieX prompt config value "measurement_evidence_guard.parameters" must contain at least one valid parameter definition.'); } return $out; } /** * @return string[] */ private function normalizeMixedStringList(mixed $value): array { if (!is_array($value)) { return []; } $out = []; foreach ($value as $item) { if (!is_scalar($item)) { continue; } $item = trim((string) $item); if ($item !== '' && !in_array($item, $out, true)) { $out[] = $item; } } return $out; } public function getTechnicalProductModelPattern(): string { return $this->getRequiredString('technical_product_model_pattern'); } }