'ph', 'label' => 'pH / pH-Wert', 'request_terms' => ['ph', 'pH', 'pH-Wert', 'ph-wert', 'ph wert'], 'positive_terms' => ['pH', 'pH-Wert', 'ph-wert', 'ph wert'], 'positive_context_terms' => ['Messung', 'messen', 'misst', 'Messbereich', 'Messparameter', 'Messgröße', 'Messgroesse', 'Bestimmung', 'bestimmen', 'Analyse', 'analysiert', 'überwachen', 'ueberwachen', 'Indikator für', 'Indikator fuer', 'Reagenz für', 'Reagenz fuer', 'Sensor', 'Elektrode'], 'negative_context_terms' => ['Betriebsbereich', 'Betriebsumgebung', 'Einsatzbedingungen', 'störungsfrei', 'stoerungsfrei', 'pH-Wert bei', 'ph wert bei', 'ph-wert bei', 'bei 20 °C', 'bei 20 °c', 'bei 20°C', 'bei 20°c', 'Reagenzlösung hat', 'Loesung hat', 'Lösung hat'], 'non_equivalent_terms' => ['p-Wert', 'p Wert', 'm-Wert', 'minus m-Wert', 'Alkalität', 'Säurekapazität', 'mmol/l'], 'safe_no_evidence_answer_de' => 'Ich finde in den bereitgestellten Quellen keinen sicher belegten Testomat für pH-Messung.', 'safe_no_accessory_evidence_answer_de' => 'Ich finde in den bereitgestellten Quellen keinen sicher belegten pH-Indikator oder ein pH-Reagenz für Messgeräte.', ], [ 'id' => 'redox', 'label' => 'Redox / ORP', 'request_terms' => ['redox', 'orp', 'oxidations-reduktionspotential', 'oxidations reduktionspotential'], 'positive_terms' => ['Redox', 'ORP', 'Oxidations-Reduktionspotential', 'Oxidations Reduktionspotential'], 'positive_context_terms' => ['Messung', 'messen', 'misst', 'Messbereich', 'Messparameter', 'Messgröße', 'Messgroesse', 'Bestimmung', 'bestimmen', 'Analyse', 'analysiert', 'überwachen', 'ueberwachen', 'Indikator für', 'Indikator fuer', 'Reagenz für', 'Reagenz fuer', 'Sensor', 'Elektrode'], 'negative_context_terms' => ['Betriebsbereich', 'Betriebsumgebung', 'Einsatzbedingungen', 'störungsfrei', 'stoerungsfrei'], 'non_equivalent_terms' => [], 'safe_no_evidence_answer_de' => 'Ich finde in den bereitgestellten Quellen keinen sicher belegten Treffer für Redox-/ORP-Messung.', 'safe_no_accessory_evidence_answer_de' => 'Ich finde in den bereitgestellten Quellen keinen sicher belegten Redox-/ORP-Indikator oder ein Redox-/ORP-Reagenz für Messgeräte.', ], [ 'id' => 'free_chlorine', 'label' => 'freies Chlor', 'request_terms' => ['freies chlor', 'freiem chlor', 'freien chlor', 'free chlorine'], 'positive_terms' => ['freies Chlor', 'freiem Chlor', 'freien Chlor', 'free chlorine'], 'positive_context_terms' => ['Messung', 'messen', 'misst', 'Messbereich', 'Messparameter', 'Messgröße', 'Messgroesse', 'Bestimmung', 'bestimmen', 'Analyse', 'analysiert', 'überwachen', 'ueberwachen', 'Indikator für', 'Indikator fuer', 'Reagenz für', 'Reagenz fuer', 'Sensor', 'Elektrode'], 'negative_context_terms' => ['Betriebsbereich', 'Betriebsumgebung', 'Einsatzbedingungen', 'störungsfrei', 'stoerungsfrei'], 'non_equivalent_terms' => ['Chlor gesamt', 'Gesamtchlor', 'total chlorine'], 'safe_no_evidence_answer_de' => 'Ich finde in den bereitgestellten Quellen keinen sicher belegten Treffer für die Messung von freiem Chlor.', 'safe_no_accessory_evidence_answer_de' => 'Ich finde in den bereitgestellten Quellen keinen sicher belegten Indikator oder ein Reagenz für die Messung von freiem Chlor.', ], ]; private const ACCESSORY_REQUEST_KEYWORDS = [ 'passend', 'passende', 'passendes', 'zubehör', 'zubehor', 'dazu', 'indikator', 'indikatoren', 'ph-indikator', 'ph indikator', 'ph-indikatoren', 'ph indikatoren', 'reagenz', 'kit', 'set', 'zusatz', 'ergänzung', 'ergaenzung', ]; /** * @param array $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 getInt(string $path, int $default): int { $value = $this->getValue($path, $default); return is_numeric($value) ? (int) $value : $default; } private function getFloat(string $path, float $default): float { $value = $this->getValue($path, $default); return is_numeric($value) ? (float) $value : $default; } 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 getString(string $path, string $default): string { $value = $this->getValue($path, $default); if (!is_scalar($value)) { return $default; } $value = (string) $value; return $value !== '' ? $value : $default; } 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 getStringList(string $path, array $default): array { $value = $this->getValue($path, $default); if (!is_array($value)) { return $default; } $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 !== [] ? $out : $default; } /** * @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 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; } /** * @return string[] */ private function vocabularyView(string $path, array $fallback): array { return $this->vocabulary?->view($path, $fallback) ?? $fallback; } private function getValue(string $path, mixed $default): mixed { $current = $this->config; foreach (explode('.', $path) as $segment) { if (!is_array($current) || !array_key_exists($segment, $current)) { return $default; } $current = $current[$segment]; } return $current; } 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'); } /** * @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->getStringList('role_guard.main_device_request_keywords', self::MAIN_DEVICE_REQUEST_ROLE_KEYWORDS); } /** * @return string[] */ public function getMainDeviceProductRoleKeywords(): array { return $this->getStringList('role_guard.main_device_product_keywords', self::MAIN_DEVICE_PRODUCT_ROLE_KEYWORDS); } /** * @return string[] */ public function getAccessoryProductRoleKeywords(): array { return $this->getStringList('role_guard.accessory_product_keywords', self::ACCESSORY_PRODUCT_ROLE_KEYWORDS); } /** * @return string[] */ public function getTechnicalProductKeywords(): array { return $this->getStringList( 'technical_product_keywords', $this->vocabularyView('prompt.technical_product_keywords', self::TECHNICAL_PRODUCT_KEYWORDS) ); } /** * @return string[] */ public function getAccessoryRequestKeywords(): array { return $this->getStringList( 'accessory_request_keywords', $this->vocabularyView('prompt.accessory_request_keywords', self::ACCESSORY_REQUEST_KEYWORDS) ); } public function getMeasurementEvidenceSectionLabel(): string { return $this->getString('sections.measurement_evidence_label', 'MEASUREMENT PARAMETER EVIDENCE CHECK'); } /** * @return string[] */ public function getMeasurementEvidenceIntroRules(): array { return $this->getStringList('measurement_evidence_guard.intro_rules', [ '- This block is generated from the current user question and is stricter than broad product-selection wording.', '- For measurement-parameter questions, technical suitability requires explicit positive evidence for the requested parameter in the same source record.', '- Similar water-treatment parameters, abbreviations, units, product families, search queries, or ranking positions are not enough.', ]); } /** * @return array> */ public function getMeasurementEvidenceParameters(): array { $value = $this->getValue('measurement_evidence_guard.parameters', self::MEASUREMENT_EVIDENCE_PARAMETERS); if (!is_array($value)) { return self::MEASUREMENT_EVIDENCE_PARAMETERS; } $out = []; 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->normalizeMixedStringList($item['request_terms'] ?? []), 'positive_terms' => $this->normalizeMixedStringList($item['positive_terms'] ?? []), 'positive_context_terms' => $this->normalizeMixedStringList($item['positive_context_terms'] ?? []), 'negative_context_terms' => $this->normalizeMixedStringList($item['negative_context_terms'] ?? []), 'non_equivalent_terms' => $this->normalizeMixedStringList($item['non_equivalent_terms'] ?? []), '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']) : '', ]; } return $out !== [] ? $out : self::MEASUREMENT_EVIDENCE_PARAMETERS; } /** * @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->getString('technical_product_model_pattern', '/\b[\p{L}]{2,}\s?\d{2,5}\b/u'); } }