diff --git a/src/Config/AgentRunnerConfig.php b/src/Config/AgentRunnerConfig.php index 879e3c2..86c0a56 100644 --- a/src/Config/AgentRunnerConfig.php +++ b/src/Config/AgentRunnerConfig.php @@ -87,6 +87,29 @@ final class AgentRunnerConfig 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); @@ -177,6 +200,39 @@ final class AgentRunnerConfig 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; + } + /** * @param string[] $default * @return string[] @@ -236,6 +292,21 @@ final class AgentRunnerConfig 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'); @@ -474,25 +545,7 @@ final class AgentRunnerConfig */ public function getShopPromptRules(): array { - return $this->getStringList('shop_prompt.rules', [ - '- Output only the final search query.', - '- Always convert relevant search terms to their singular form.', - '- No introduction, no explanation, no quotation marks.', - '- Use only shop-relevant search terms from the user input for a shop search.', - '- Maximum 6 search terms, preferably fewer.', - '- Remove filler words, polite phrases, and irrelevant words.', - '- Preserve product names, brands, model numbers, and compound terms exactly if they are relevant.', - '- Preserve the language of the CURRENT USER INPUT for generic product/search terms; do not translate German search terms into English.', - '- For German user input, output German shop terms, for example "freies Chlor Messung" instead of "free chlorine measurement".', - '- Preserve domain terms from the current user input or resolved context in their original language.', - '- Numbers that belong to a product name or model must be preserved (e.g. Indikator 300, Testomat 808, Testomat 2000).', - '- Separate terms using spaces only.', - '- If a relevant product name is present, it must be placed at the beginning of the final search query.', - '- Try to always identify all products mentioned in the user input text, even in long prompts.', - '- Look for terms such as Testomat, Horiba, Tritromat, or words like indicator/Indikator.', - '- If the current user input is vague or referential, use the recent conversation context only as support.', - '- Do not output words that only describe conversation flow, such as "same", "again", "also", or "like above".', - ]); + return $this->getRequiredStringList('shop_prompt.rules'); } /** @@ -500,39 +553,32 @@ final class AgentRunnerConfig */ public function getConversationContextRules(): array { - return $this->getStringList('shop_prompt.conversation_context_rules', [ - '- The current user input has highest priority.', - '- Use the recent conversation context only to resolve omitted references.', - '- Use it only for product carry-over, brand carry-over, model carry-over, or variant follow-ups.', - '- Do not revive older products unless the current user input clearly refers to them.', - '- If the current input starts a new topic, ignore older product context.', - '- Prefer the most recent product reference over older ones.', - ]); + return $this->getRequiredStringList('shop_prompt.conversation_context_rules'); } public function getShopPromptIntro(): string { - return $this->getString('shop_prompt.intro', 'Generate a short search query for Shopware 6 from the following user input text.'); + return $this->getRequiredString('shop_prompt.intro'); } public function getShopPromptOutputFormatBlock(): string { - return $this->getString('shop_prompt.output_format_block', "Output format:\nKeyword1 Keyword2 Keyword3"); + return $this->getRequiredString('shop_prompt.output_format_block'); } public function getRecentConversationContextLabel(): string { - return $this->getString('shop_prompt.recent_conversation_context_label', 'RECENT CONVERSATION CONTEXT'); + return $this->getRequiredString('shop_prompt.recent_conversation_context_label'); } public function getCurrentUserInputLabel(): string { - return $this->getString('shop_prompt.current_user_input_label', 'CURRENT USER INPUT'); + return $this->getRequiredString('shop_prompt.current_user_input_label'); } public function isShopQueryLanguagePreservationEnabled(): bool { - return $this->getBool('shop_prompt.language_preservation.enabled', true); + return $this->getRequiredBool('shop_prompt.language_preservation.enabled'); } /** @@ -540,20 +586,10 @@ final class AgentRunnerConfig */ public function getShopQueryLanguageMarkers(): array { - $default = [ - 'de' => [ - ' ä ', ' ö ', ' ü ', ' ß ', - ' der ', ' die ', ' das ', ' ein ', ' eine ', ' einer ', ' einen ', - ' welchem ', ' welchen ', ' welche ', ' welcher ', - ' kann ', ' nutzen ', ' zur ', ' für ', ' fuer ', - ' messung ', ' indikator ', ' reagenz ', ' chlor ', - ], - ]; - - $value = $this->value('shop_prompt.language_preservation.language_markers', $default); + $value = $this->requiredValue('shop_prompt.language_preservation.language_markers'); if (!is_array($value)) { - return $default; + throw new \InvalidArgumentException('RetrieX agent config key "shop_prompt.language_preservation.language_markers" must be a map of marker lists.'); } $out = []; @@ -582,15 +618,16 @@ final class AgentRunnerConfig } } - return $out !== [] ? $out : $default; + 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; } - /** - * @return array - */ public function isShopQueryMetaGuardEnabled(): bool { - return $this->getBool('shop_prompt.meta_query_guard.enabled', true); + return $this->getRequiredBool('shop_prompt.meta_query_guard.enabled'); } /** @@ -598,59 +635,32 @@ final class AgentRunnerConfig */ public function getShopQueryMetaOnlyTerms(): array { - return $this->getStringList('shop_prompt.meta_query_guard.meta_only_terms', [ - 'shop', - 'shopsuche', - 'shop-suche', - 'suche', - 'suchen', - 'such', - 'finde', - 'find', - 'zeige', - 'zeig', - 'bitte', - 'mal', - 'im', - 'in', - 'nach', - 'danach', - 'dazu', - 'damit', - 'dafür', - 'dafuer', - 'hierzu', - 'den', - 'die', - 'das', - 'der', - 'dem', - ]); + return $this->getRequiredStringList('shop_prompt.meta_query_guard.meta_only_terms'); } public function isShopQueryContextFallbackEnabled(): bool { - return $this->getBool('shop_prompt.meta_query_guard.context_fallback_enabled', true); + return $this->getRequiredBool('shop_prompt.meta_query_guard.context_fallback_enabled'); } public function getShopQueryContextFallbackQuestionLimit(): int { - return $this->getInt('shop_prompt.meta_query_guard.context_fallback_question_limit', 12); + return $this->getRequiredInt('shop_prompt.meta_query_guard.context_fallback_question_limit'); } public function getShopQueryContextFallbackHistoryBudgetChars(): int { - return $this->getInt('shop_prompt.meta_query_guard.context_fallback_history_budget_chars', 20000); + return $this->getRequiredInt('shop_prompt.meta_query_guard.context_fallback_history_budget_chars'); } public function shouldUseFullHistoryForShopQueryContextFallback(): bool { - return $this->getBool('shop_prompt.meta_query_guard.context_fallback_use_full_history', true); + return $this->getRequiredBool('shop_prompt.meta_query_guard.context_fallback_use_full_history'); } public function getShopQueryContextFallbackMaxTerms(): int { - return $this->getInt('shop_prompt.meta_query_guard.context_fallback_max_terms', 6); + return $this->getRequiredInt('shop_prompt.meta_query_guard.context_fallback_max_terms'); } /** @@ -658,50 +668,17 @@ final class AgentRunnerConfig */ public function getShopQueryContextFallbackFilterTerms(): array { - return $this->getStringList('shop_prompt.meta_query_guard.context_fallback_filter_terms', [ - 'mit', - 'welche', - 'welcher', - 'welches', - 'welchem', - 'welchen', - 'ist', - 'sind', - 'gut', - 'geeignet', - 'was', - 'wie', - 'wo', - 'kann', - 'koennen', - 'können', - 'konnte', - 'könnte', - 'ich', - 'wir', - 'man', - 'nutzen', - 'benutzen', - 'verwenden', - 'verwende', - 'nehmen', - 'zur', - 'zum', - 'für', - 'fuer', - 'messen', - 'gemessen', - ]); + return $this->getRequiredStringList('shop_prompt.meta_query_guard.context_fallback_filter_terms'); } public function isShopQueryContextAnchorEnrichmentEnabled(): bool { - return $this->getBool('shop_prompt.context_anchor_enrichment.enabled', true); + return $this->getRequiredBool('shop_prompt.context_anchor_enrichment.enabled'); } public function getShopQueryContextAnchorEnrichmentMaxQueryTerms(): int { - return $this->getInt('shop_prompt.context_anchor_enrichment.max_query_terms', 2); + return $this->getRequiredInt('shop_prompt.context_anchor_enrichment.max_query_terms'); } /** @@ -709,17 +686,7 @@ final class AgentRunnerConfig */ public function getShopQueryContextAnchorEnrichmentTriggerTerms(): array { - return $this->getStringList('shop_prompt.context_anchor_enrichment.trigger_terms', [ - 'indikator', - 'indikatortyp', - 'indicator', - 'reagenz', - 'reagenzsatz', - 'reagent', - 'zubehör', - 'zubehor', - 'accessory', - ]); + return $this->getRequiredStringList('shop_prompt.context_anchor_enrichment.trigger_terms'); } /** @@ -727,54 +694,21 @@ final class AgentRunnerConfig */ public function getShopQueryContextAnchorEnrichmentPatterns(): array { - return $this->getStringList('shop_prompt.context_anchor_enrichment.anchor_patterns', [ - '/\b(?:indikator(?:typ)?|indicator(?:\s+type)?|reagenz(?:satz|typ)?|reagent(?:\s+set|\s+type)?|typ|type)\s+[A-Za-zÄÖÜäöüß]{0,8}\s*\d{1,5}(?:\s*[A-ZÄÖÜ]{1,4})?(?:\s*%)?\b/iu', - ]); + return $this->getRequiredStringList('shop_prompt.context_anchor_enrichment.anchor_patterns'); } public function getShopQueryContextAnchorEnrichmentTemplate(): string { - return $this->getString('shop_prompt.context_anchor_enrichment.template', '{anchor} {query}'); + return $this->getRequiredString('shop_prompt.context_anchor_enrichment.template'); } public function getShopQueryTranslationReplacements(string $language): array { - $default = [ - 'de' => [ - 'free chlorine' => 'freies chlor', - 'free chlor' => 'freies chlor', - 'total chlorine' => 'gesamtchlor', - 'chlorine measurement' => 'chlor messung', - 'water hardness' => 'wasserhärte', - 'measurement' => 'messung', - 'measuring' => 'messung', - 'chlorine' => 'chlor', - 'indicator' => 'indikator', - 'indicators' => 'indikatoren', - 'reagent' => 'reagenz', - 'reagents' => 'reagenzien', - 'accessory' => 'zubehör', - 'accessories' => 'zubehör', - ], - ]; - - $value = $this->value( - 'shop_prompt.language_preservation.translation_replacements.' . $language, - $default[$language] ?? [] - ); - - if (!is_array($value)) { - return $default[$language] ?? []; - } - + $value = $this->getOptionalStringMap('shop_prompt.language_preservation.translation_replacements.' . $language); $out = []; foreach ($value as $source => $target) { - if (!is_scalar($source) || !is_scalar($target)) { - continue; - } - - $source = strtolower(trim((string) $source)); - $target = trim((string) $target); + $source = strtolower(trim($source)); + $target = trim($target); if ($source !== '' && $target !== '') { $out[$source] = $target; @@ -783,7 +717,7 @@ final class AgentRunnerConfig uksort($out, static fn(string $a, string $b): int => strlen($b) <=> strlen($a)); - return $out !== [] ? $out : ($default[$language] ?? []); + return $out; } private function buildRulesBlock(array $rules, string $headline = 'Rules:'): string