From e9c464c057aa9965e52983e08ad258730ef9d043 Mon Sep 17 00:00:00 2001 From: team 1 Date: Wed, 6 May 2026 17:23:57 +0200 Subject: [PATCH] fix p55 --- ...CH_56_SINGLE_GENRE_CONFIG_WIRING_README.md | 62 +++++++++ config/services.yaml | 5 + src/Config/AgentRunnerConfig.php | 94 ++++++++++---- src/Config/CommerceIntentConfig.php | 33 +++-- src/Config/CommerceQueryParserConfig.php | 19 ++- src/Config/DomainVocabularyConfig.php | 93 ++++++++++++- src/Config/GenreConfig.php | 122 ++++++++++++++++++ src/Config/QueryEnricherConfig.php | 11 +- 8 files changed, 399 insertions(+), 40 deletions(-) create mode 100644 RETRIEX_PATCH_56_SINGLE_GENRE_CONFIG_WIRING_README.md diff --git a/RETRIEX_PATCH_56_SINGLE_GENRE_CONFIG_WIRING_README.md b/RETRIEX_PATCH_56_SINGLE_GENRE_CONFIG_WIRING_README.md new file mode 100644 index 0000000..b45584f --- /dev/null +++ b/RETRIEX_PATCH_56_SINGLE_GENRE_CONFIG_WIRING_README.md @@ -0,0 +1,62 @@ +# RetrieX Patch p56 - Single-Genre Config Wiring + +## Ziel + +Dieser Patch macht die in p55 eingefuehrte `config/retriex/genre.yaml` fuer eine erste, risikoarme Auswahl fachlicher Runtime-Parameter zur bevorzugten Quelle. + +Es bleibt strikt bei: eine Installation = ein Genre. Es gibt keine Multi-Genre-Umschaltung, keinen Tenant-Kontext und keinen Request-/Host-Resolver. + +## Was geaendert wurde + +- `GenreConfig` erhaelt typisierte Value-Getter fuer Listen, Maps, Strings, Booleans und Integer unter `configuration_values`. +- `DomainVocabularyConfig` liest bevorzugt passende Genre-Werte fuer zentrale Produktrollen, Shop-/Prompt-Views, Search-Repair-Views und relevante Vocabulary-Maps. +- `CommerceQueryParserConfig` liest bevorzugt Genre-Werte fuer `known_brands` und `search_token_canonical_map`. +- `QueryEnricherConfig` liest bevorzugt Genre-Werte fuer Query-Enrichment-Regeln. +- `CommerceIntentConfig` liest bevorzugt Genre-Werte fuer Commerce-Signale, Advisory-Patterns, Farben und Groessen. +- `AgentRunnerConfig` liest bevorzugt Genre-Werte fuer ausgewaehlte Shop-Runtime-Listen, Kontextanker, Direct-Answer-Texte und Laengen-Constraint-Patterns. +- `config/services.yaml` verdrahtet `GenreConfig` in diese Config-Fassaden. + +## Nicht geaendert + +- Keine neue Profil-/Layer-Architektur. +- Keine Multi-Domain-/Tenant-Loesung. +- Keine neuen fachlichen Listen. +- Keine Shopware-Kriterien-, Ranking-, Retrieval- oder LLM-Verhaltensaenderung. +- Legacy-YAML-Pfade bleiben als Fallback erhalten. + +## Erwartetes Verhalten + +Da p55 die aktuellen Werte bereits identisch gespiegelt hat, sollte sich das Laufzeitverhalten nicht aendern. Der Unterschied ist nur die bevorzugte Pflegequelle fuer die verdrahteten Parameter. + +## Geaenderte Dateien + +- `config/services.yaml` +- `src/Config/AgentRunnerConfig.php` +- `src/Config/CommerceIntentConfig.php` +- `src/Config/CommerceQueryParserConfig.php` +- `src/Config/DomainVocabularyConfig.php` +- `src/Config/GenreConfig.php` +- `src/Config/QueryEnricherConfig.php` + +## Lokale Checks + +```bash +php -l src/Config/AgentRunnerConfig.php +php -l src/Config/CommerceIntentConfig.php +php -l src/Config/CommerceQueryParserConfig.php +php -l src/Config/DomainVocabularyConfig.php +php -l src/Config/GenreConfig.php +php -l src/Config/QueryEnricherConfig.php +``` + +YAML-Parsing fuer alle Dateien unter `config/retriex/*.yaml`. + +Projektchecks nach dem Einspielen: + +```bash +bin/console cache:clear +bin/console mto:agent:config:validate +bin/console mto:agent:regression:test +bin/console mto:agent:config:audit-source --details +bin/console mto:agent:config:audit-patterns --details +``` diff --git a/config/services.yaml b/config/services.yaml index 0649fea..972f5f3 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -124,6 +124,7 @@ services: App\Config\DomainVocabularyConfig: arguments: $config: '%retriex.vocabulary.config%' + $genreConfig: '@App\Config\GenreConfig' App\Config\ContextServiceConfig: arguments: @@ -142,6 +143,7 @@ services: arguments: $config: '%retriex.agent.config%' $vocabulary: '@App\Config\DomainVocabularyConfig' + $genreConfig: '@App\Config\GenreConfig' App\Config\NdjsonHybridRetrieverConfig: arguments: @@ -159,6 +161,7 @@ services: App\Config\QueryEnricherConfig: arguments: $config: '%retriex.query_enrichment.config%' + $genreConfig: '@App\Config\GenreConfig' App\Config\GovernanceConfig: arguments: @@ -211,11 +214,13 @@ services: App\Config\CommerceIntentConfig: arguments: $config: '%retriex.intent.commerce.config%' + $genreConfig: '@App\Config\GenreConfig' App\Config\CommerceQueryParserConfig: arguments: $config: '%retriex.commerce_query.config%' $vocabulary: '@App\Config\DomainVocabularyConfig' + $genreConfig: '@App\Config\GenreConfig' App\Config\CommerceReferenceResolverConfig: arguments: diff --git a/src/Config/AgentRunnerConfig.php b/src/Config/AgentRunnerConfig.php index f262936..8d9d403 100644 --- a/src/Config/AgentRunnerConfig.php +++ b/src/Config/AgentRunnerConfig.php @@ -12,6 +12,7 @@ final class AgentRunnerConfig public function __construct( private readonly array $config = [], private readonly ?DomainVocabularyConfig $vocabulary = null, + private readonly ?GenreConfig $genreConfig = null, ) { } @@ -290,6 +291,27 @@ final class AgentRunnerConfig ); } + /** @return string[] */ + private function genreStringList(string $path): array + { + return $this->genreConfig?->getValueStringList($path) ?? []; + } + + private function genreString(string $path): string + { + return $this->genreConfig?->getValueString($path) ?? ''; + } + + private function genreBool(string $path): ?bool + { + return $this->genreConfig?->getValueBool($path); + } + + private function genreInt(string $path): ?int + { + return $this->genreConfig?->getValueInt($path); + } + private function getRequiredInt(string $key): int { $value = $this->requiredValue($key); @@ -749,37 +771,44 @@ final class AgentRunnerConfig public function isDirectShopResultAnswerEnabled(): bool { - return $this->getRequiredBool('shop_runtime.direct_answer.enabled'); + return $this->genreBool('shop_query_runtime.direct_answer.enabled') + ?? $this->getRequiredBool('shop_runtime.direct_answer.enabled'); } public function getDirectShopResultAnswerMaxResults(): int { - return $this->getRequiredInt('shop_runtime.direct_answer.max_results'); + return $this->genreInt('shop_query_runtime.direct_answer.max_results') + ?? $this->getRequiredInt('shop_runtime.direct_answer.max_results'); } public function getDirectShopResultAnswerIntro(): string { - return $this->getRequiredString('shop_runtime.direct_answer.intro'); + return $this->genreString('shop_query_runtime.direct_answer.intro') + ?: $this->getRequiredString('shop_runtime.direct_answer.intro'); } public function getDirectShopResultAnswerNoResultsMessage(): string { - return $this->getRequiredString('shop_runtime.direct_answer.no_results'); + return $this->genreString('shop_query_runtime.direct_answer.no_results') + ?: $this->getRequiredString('shop_runtime.direct_answer.no_results'); } public function getDirectShopResultAnswerSortedByLengthNote(): string { - return $this->getRequiredString('shop_runtime.direct_answer.sorted_by_length_note'); + return $this->genreString('shop_query_runtime.direct_answer.sorted_by_length_note') + ?: $this->getRequiredString('shop_runtime.direct_answer.sorted_by_length_note'); } public function getDirectShopResultAnswerMinLengthFilterNote(): string { - return $this->getRequiredString('shop_runtime.direct_answer.min_length_filter_note'); + return $this->genreString('shop_query_runtime.direct_answer.min_length_filter_note') + ?: $this->getRequiredString('shop_runtime.direct_answer.min_length_filter_note'); } public function getDirectShopResultAnswerMaxLengthFilterNote(): string { - return $this->getRequiredString('shop_runtime.direct_answer.max_length_filter_note'); + return $this->genreString('shop_query_runtime.direct_answer.max_length_filter_note') + ?: $this->getRequiredString('shop_runtime.direct_answer.max_length_filter_note'); } public function getNoLlmFallbackMaxShopResults(): int @@ -1073,7 +1102,8 @@ final class AgentRunnerConfig */ public function getShopQueryContextUsageReferentialTerms(): array { - return $this->getRequiredStringList('shop_runtime.context_resolution.context_usage.referential_terms'); + return $this->genreStringList('context_resolution.referential_terms.terms') + ?: $this->getRequiredStringList('shop_runtime.context_resolution.context_usage.referential_terms'); } public function isShopQueryCurrentInputPreservationEnabled(): bool @@ -1129,7 +1159,8 @@ final class AgentRunnerConfig */ public function getShopQueryProductAttributeCleanupComparativeConstraintPatterns(): array { - return $this->getRequiredStringList('shop_runtime.attribute_cleanup.comparative_constraint_patterns'); + return $this->genreStringList('product_attributes.direct_attribute_cleanup.comparative_constraint_patterns') + ?: $this->getRequiredStringList('shop_runtime.attribute_cleanup.comparative_constraint_patterns'); } public function isShopQueryStopwordCleanupEnabled(): bool @@ -1147,7 +1178,8 @@ final class AgentRunnerConfig */ public function getShopQueryStopwordCleanupTerms(): array { - return $this->getRequiredStringList('shop_runtime.query_cleanup.stopword_cleanup.terms'); + return $this->genreStringList('shop_query_runtime.stopword_cleanup.terms') + ?: $this->getRequiredStringList('shop_runtime.query_cleanup.stopword_cleanup.terms'); } public function isDirectShopResultGuardEnabled(): bool @@ -1170,7 +1202,8 @@ final class AgentRunnerConfig */ public function getDirectShopResultGuardCompoundPrefixTerms(): array { - return $this->getOptionalStringList('shop_runtime.result_identity.compound_prefix_match.terms'); + return $this->genreStringList('shop_query_runtime.compound_prefix_match.terms') + ?: $this->getOptionalStringList('shop_runtime.result_identity.compound_prefix_match.terms'); } public function isDirectShopResultGuardPrimaryIdentityRepairEnabled(): bool @@ -1188,7 +1221,8 @@ final class AgentRunnerConfig */ public function getDirectShopResultGuardPrimaryIdentityRepairStopTerms(): array { - return $this->getOptionalStringList('shop_runtime.result_identity.primary_identity_repair.stop_terms'); + return $this->genreStringList('shop_query_runtime.primary_identity_repair.stop_terms') + ?: $this->getOptionalStringList('shop_runtime.result_identity.primary_identity_repair.stop_terms'); } public function isShopResultLengthSortEnabled(): bool @@ -1201,7 +1235,8 @@ final class AgentRunnerConfig */ public function getShopResultLengthSortTriggerPatterns(): array { - return $this->getRequiredStringList('shop_runtime.answer_constraints.length_sort.trigger_patterns'); + return $this->genreStringList('product_attributes.numeric_length_constraints.length_sort.trigger_patterns') + ?: $this->getRequiredStringList('shop_runtime.answer_constraints.length_sort.trigger_patterns'); } /** @@ -1209,7 +1244,8 @@ final class AgentRunnerConfig */ public function getShopResultLengthSortValuePatterns(): array { - return $this->getRequiredStringList('shop_runtime.answer_constraints.length_sort.value_patterns'); + return $this->genreStringList('product_attributes.numeric_length_constraints.length_sort.value_patterns') + ?: $this->getRequiredStringList('shop_runtime.answer_constraints.length_sort.value_patterns'); } public function isShopResultLengthFilterEnabled(): bool @@ -1222,7 +1258,8 @@ final class AgentRunnerConfig */ public function getShopResultMinLengthFilterPatterns(): array { - return $this->getRequiredStringList('shop_runtime.answer_constraints.length_filter.min_patterns'); + return $this->genreStringList('product_attributes.numeric_length_constraints.length_filter.min_patterns') + ?: $this->getRequiredStringList('shop_runtime.answer_constraints.length_filter.min_patterns'); } /** @@ -1230,7 +1267,8 @@ final class AgentRunnerConfig */ public function getShopResultMaxLengthFilterPatterns(): array { - return $this->getRequiredStringList('shop_runtime.answer_constraints.length_filter.max_patterns'); + return $this->genreStringList('product_attributes.numeric_length_constraints.length_filter.max_patterns') + ?: $this->getRequiredStringList('shop_runtime.answer_constraints.length_filter.max_patterns'); } public function getShopPromptIntro(): string @@ -1323,7 +1361,8 @@ final class AgentRunnerConfig */ public function getShopQueryMetaOnlyTerms(): array { - return $this->getRequiredStringList('shop_runtime.context_resolution.meta_query_guard.meta_only_terms'); + return $this->genreStringList('context_resolution.meta_query_guard.meta_only_terms') + ?: $this->getRequiredStringList('shop_runtime.context_resolution.meta_query_guard.meta_only_terms'); } public function isShopQueryContextFallbackEnabled(): bool @@ -1356,7 +1395,8 @@ final class AgentRunnerConfig */ public function getShopQueryContextFallbackFilterTerms(): array { - return $this->getRequiredStringList('shop_runtime.context_resolution.meta_query_guard.context_fallback_filter_terms'); + return $this->genreStringList('context_resolution.meta_query_guard.context_fallback_filter_terms') + ?: $this->getRequiredStringList('shop_runtime.context_resolution.meta_query_guard.context_fallback_filter_terms'); } public function isShopQueryContextAnchorEnrichmentEnabled(): bool @@ -1385,12 +1425,14 @@ final class AgentRunnerConfig */ public function getShopQueryContextAnchorEnrichmentPatterns(): array { - return $this->getRequiredStringList('shop_runtime.context_resolution.history_anchor_enrichment.anchor_patterns'); + return $this->genreStringList('context_resolution.history_anchor_enrichment.anchor_patterns') + ?: $this->getRequiredStringList('shop_runtime.context_resolution.history_anchor_enrichment.anchor_patterns'); } public function getShopQueryContextAnchorEnrichmentTemplate(): string { - return $this->getRequiredString('shop_runtime.context_resolution.history_anchor_enrichment.template'); + return $this->genreString('context_resolution.history_anchor_enrichment.template') + ?: $this->getRequiredString('shop_runtime.context_resolution.history_anchor_enrichment.template'); } public function isShopQueryRagAnchorEnrichmentEnabled(): bool { @@ -1437,7 +1479,8 @@ final class AgentRunnerConfig */ public function getShopQueryRagAnchorEnrichmentNumericFocusPatterns(): array { - return $this->getRequiredStringList('shop_runtime.context_resolution.rag_anchor_enrichment.numeric_focus_patterns'); + return $this->genreStringList('context_resolution.rag_anchor_enrichment.numeric_focus_patterns') + ?: $this->getRequiredStringList('shop_runtime.context_resolution.rag_anchor_enrichment.numeric_focus_patterns'); } /** @@ -1445,7 +1488,8 @@ final class AgentRunnerConfig */ public function getShopQueryRagAnchorEnrichmentProductTitlePatterns(): array { - return $this->getRequiredStringList('shop_runtime.context_resolution.rag_anchor_enrichment.product_title_patterns'); + return $this->genreStringList('context_resolution.rag_anchor_enrichment.product_title_patterns') + ?: $this->getRequiredStringList('shop_runtime.context_resolution.rag_anchor_enrichment.product_title_patterns'); } /** @@ -1453,7 +1497,8 @@ final class AgentRunnerConfig */ public function getShopQueryRagAnchorEnrichmentAnchorBonusPatterns(): array { - return $this->getRequiredStringList('shop_runtime.context_resolution.rag_anchor_enrichment.anchor_bonus_patterns'); + return $this->genreStringList('context_resolution.rag_anchor_enrichment.anchor_bonus_patterns') + ?: $this->getRequiredStringList('shop_runtime.context_resolution.rag_anchor_enrichment.anchor_bonus_patterns'); } /** @@ -1461,7 +1506,8 @@ final class AgentRunnerConfig */ public function getShopQueryRagAnchorEnrichmentSubjectTerms(): array { - return $this->getRequiredStringList('shop_runtime.context_resolution.rag_anchor_enrichment.subject_terms'); + return $this->genreStringList('context_resolution.rag_anchor_enrichment.subject_terms') + ?: $this->getRequiredStringList('shop_runtime.context_resolution.rag_anchor_enrichment.subject_terms'); } public function getShopQueryTranslationReplacements(string $language): array diff --git a/src/Config/CommerceIntentConfig.php b/src/Config/CommerceIntentConfig.php index 54a7d23..554219f 100644 --- a/src/Config/CommerceIntentConfig.php +++ b/src/Config/CommerceIntentConfig.php @@ -9,26 +9,31 @@ final class CommerceIntentConfig /** * @param array $config */ - public function __construct(private readonly array $config = []) - { + public function __construct( + private readonly array $config = [], + private readonly ?GenreConfig $genreConfig = null, + ) { } /** @return string[] */ public function getStrongSignalsList(): array { - return $this->requiredStringList('strong_signals'); + return $this->genreStringList('intent_and_routing.commerce_intent.strong_signals') + ?: $this->requiredStringList('strong_signals'); } /** @return string[] */ public function getAdvisorySignals(): array { - return $this->requiredStringList('advisory_signals'); + return $this->genreStringList('intent_and_routing.commerce_intent.advisory_signals') + ?: $this->requiredStringList('advisory_signals'); } /** @return string[] */ public function getAdvisoryProductSelectionPatterns(): array { - return $this->requiredStringList('advisory_product_selection_patterns'); + return $this->genreStringList('intent_and_routing.commerce_intent.advisory_product_selection_patterns') + ?: $this->requiredStringList('advisory_product_selection_patterns'); } /** @return string[] */ @@ -54,6 +59,12 @@ final class CommerceIntentConfig return $this->requiredStringList('technical_factual_knowledge.fact_patterns'); } + /** @return string[] */ + private function genreStringList(string $path): array + { + return $this->genreConfig?->getValueStringList($path) ?? []; + } + /** @return string[] */ public function getPriceTerms(): array { @@ -68,7 +79,8 @@ final class CommerceIntentConfig /** @return string[] */ public function getColorTerms(): array { - return $this->requiredStringList('color_terms'); + return $this->genreStringList('product_attributes.size_and_color_terms.color_terms') + ?: $this->requiredStringList('color_terms'); } public function getColorPattern(): string @@ -79,7 +91,8 @@ final class CommerceIntentConfig /** @return string[] */ public function getSizeTokenTerms(): array { - return $this->requiredStringList('size_token_terms'); + return $this->genreStringList('product_attributes.size_and_color_terms.size_token_terms') + ?: $this->requiredStringList('size_token_terms'); } public function getSizeTokenPattern(): string @@ -90,7 +103,8 @@ final class CommerceIntentConfig /** @return string[] */ public function getSizeTerms(): array { - return $this->requiredStringList('size_terms'); + return $this->genreStringList('product_attributes.size_and_color_terms.size_terms') + ?: $this->requiredStringList('size_terms'); } public function getSizePattern(): string @@ -114,7 +128,8 @@ final class CommerceIntentConfig /** @return string[] */ public function getExplicitCommerceIntentPatterns(): array { - return $this->requiredStringList('explicit_commerce_intent_patterns'); + return $this->genreStringList('intent_and_routing.commerce_intent.explicit_commerce_intent_patterns') + ?: $this->requiredStringList('explicit_commerce_intent_patterns'); } public function getSkuLikePattern(): string diff --git a/src/Config/CommerceQueryParserConfig.php b/src/Config/CommerceQueryParserConfig.php index 07f501f..8e94111 100644 --- a/src/Config/CommerceQueryParserConfig.php +++ b/src/Config/CommerceQueryParserConfig.php @@ -14,6 +14,7 @@ final class CommerceQueryParserConfig public function __construct( private readonly array $config = [], private readonly ?DomainVocabularyConfig $vocabulary = null, + private readonly ?GenreConfig $genreConfig = null, ) { } @@ -26,7 +27,8 @@ final class CommerceQueryParserConfig /** @return string[] */ public function getKnownBrands(): array { - return $this->stringList('known_brands'); + return $this->genreStringList('brands_and_canonical_terms.known_brands.terms') + ?: $this->stringList('known_brands'); } /** @return string[] */ @@ -61,6 +63,18 @@ final class CommerceQueryParserConfig return $this->stringList('search_control_tokens'); } + /** @return string[] */ + private function genreStringList(string $path): array + { + return $this->genreConfig?->getValueStringList($path) ?? []; + } + + /** @return array */ + private function genreStringMap(string $path): array + { + return $this->genreConfig?->getValueStringMap($path) ?? []; + } + /** @return string[] */ private function whitespacePreservingStringList(string $path): array { @@ -99,7 +113,8 @@ final class CommerceQueryParserConfig /** @return array */ public function getSearchTokenCanonicalMap(): array { - return $this->stringMap('search_token_canonical_map'); + return $this->genreStringMap('brands_and_canonical_terms.canonical_terms.map') + ?: $this->stringMap('search_token_canonical_map'); } /** diff --git a/src/Config/DomainVocabularyConfig.php b/src/Config/DomainVocabularyConfig.php index 2f8e6cd..cbd1533 100644 --- a/src/Config/DomainVocabularyConfig.php +++ b/src/Config/DomainVocabularyConfig.php @@ -6,13 +6,57 @@ namespace App\Config; final class DomainVocabularyConfig { - public function __construct(private readonly array $config = []) - { + private const CLASS_GENRE_VALUE_PATHS = [ + 'device' => 'product_roles.primary_product_terms.terms', + 'accessory' => 'product_roles.accessory_product_terms.terms', + 'requested_accessory_code_terms' => 'product_roles.requested_accessory_code_terms.terms', + 'direct_product_attribute_stop_terms' => 'product_attributes.direct_attribute_cleanup.stop_terms', + 'input_normalization_fuzzy_routing_terms' => 'intent_and_routing.fuzzy_routing_terms.terms', + 'agent_no_llm_main_device_request_keywords' => 'product_roles.no_llm_fallback_terms.main_device_request_keywords', + 'agent_no_llm_accessory_product_keywords' => 'product_roles.no_llm_fallback_terms.accessory_product_keywords', + 'agent_shop_current_input_preservation_terms' => 'shop_query_runtime.current_input_preservation_terms.terms', + 'agent_shop_context_anchor_trigger_terms' => 'context_resolution.history_anchor_enrichment.trigger_terms', + ]; + + private const VIEW_GENRE_VALUE_PATHS = [ + 'shop.device_query' => 'product_roles.shop_views.device_query_terms', + 'shop.accessory_query' => 'product_roles.shop_views.accessory_query_terms', + 'shop.device_product' => 'product_roles.shop_views.device_product_terms', + 'shop.accessory_product' => 'product_roles.shop_views.accessory_product_terms', + 'shop.device_focus' => 'product_roles.shop_views.device_focus_terms', + 'shop.accessory_focus' => 'product_roles.shop_views.accessory_focus_terms', + 'prompt.main_device_request_keywords' => 'product_roles.prompt_views.main_device_request_keywords', + 'prompt.accessory_request_keywords' => 'product_roles.prompt_views.accessory_request_keywords', + 'prompt.main_device_product_keywords' => 'product_roles.prompt_views.main_device_product_keywords', + 'prompt.accessory_product_keywords' => 'product_roles.prompt_views.accessory_product_keywords', + 'search_repair.direct_product_type_terms' => 'product_attributes.direct_attribute_cleanup.product_type_terms', + 'search_repair.direct_product_attribute_stop_terms' => 'product_attributes.direct_attribute_cleanup.stop_terms', + 'search_repair.requested_accessory_code_terms' => 'product_roles.requested_accessory_code_terms.terms', + 'agent.rag_evidence_guard.accessory_lookup_guard_terms' => 'result_identity_and_answer_policy.measurement_evidence_guard_terms.accessory_lookup_guard_terms', + 'agent.rag_evidence_guard.accessory_lookup_passthrough_terms' => 'result_identity_and_answer_policy.measurement_evidence_guard_terms.accessory_lookup_passthrough_terms', + 'agent.rag_evidence_guard.generic_positive_context_terms' => 'result_identity_and_answer_policy.measurement_evidence_guard_terms.generic_positive_context_terms', + 'agent.rag_evidence_guard.generic_negative_context_terms' => 'result_identity_and_answer_policy.measurement_evidence_guard_terms.generic_negative_context_terms', + ]; + + private const MAP_GENRE_VALUE_PATHS = [ + 'shop.accessory_focus_variants' => 'brands_and_canonical_terms.accessory_focus_variants.map', + 'agent.rag_evidence_guard.synonyms' => 'brands_and_canonical_terms.rag_evidence_synonyms.map', + ]; + + public function __construct( + private readonly array $config = [], + private readonly ?GenreConfig $genreConfig = null, + ) { } /** @return string[] */ public function view(string $path, array $fallback = []): array { + $genreTerms = $this->genreStringListForView($path); + if ($genreTerms !== []) { + return $genreTerms; + } + $definition = $this->value('views.' . $path, null); if (!is_array($definition)) { return $this->uniqueStringList($fallback); @@ -35,12 +79,22 @@ final class DomainVocabularyConfig /** @return string[] */ public function domainClass(string $name): array { + $genreTerms = $this->genreStringListForClass($name); + if ($genreTerms !== []) { + return $genreTerms; + } + return $this->stringList('classes.' . $name, []); } /** @return array */ public function map(string $path, array $fallback = []): array { + $genreMap = $this->genreStringListMapForMap($path); + if ($genreMap !== []) { + return $genreMap; + } + $value = $this->value('maps.' . $path, null); if (!is_array($value)) { return $this->uniqueStringListMap($fallback); @@ -114,6 +168,41 @@ final class DomainVocabularyConfig return $this->config; } + /** @return string[] */ + private function genreStringListForClass(string $name): array + { + if ($this->genreConfig === null || !isset(self::CLASS_GENRE_VALUE_PATHS[$name])) { + return []; + } + + return $this->genreConfig->getValueStringList(self::CLASS_GENRE_VALUE_PATHS[$name]); + } + + /** @return string[] */ + private function genreStringListForView(string $path): array + { + if ($this->genreConfig === null || !isset(self::VIEW_GENRE_VALUE_PATHS[$path])) { + return []; + } + + return $this->genreConfig->getValueStringList(self::VIEW_GENRE_VALUE_PATHS[$path]); + } + + /** @return array */ + private function genreStringListMapForMap(string $path): array + { + if ($this->genreConfig === null || !isset(self::MAP_GENRE_VALUE_PATHS[$path])) { + return []; + } + + $value = $this->genreConfig->getValueArray(self::MAP_GENRE_VALUE_PATHS[$path]); + if ($value === []) { + return []; + } + + return $this->stringListMapWithAliases($value); + } + /** @return string[] */ private function stringList(string $path, array $fallback): array { diff --git a/src/Config/GenreConfig.php b/src/Config/GenreConfig.php index b090d69..d3ff573 100644 --- a/src/Config/GenreConfig.php +++ b/src/Config/GenreConfig.php @@ -64,6 +64,87 @@ final class GenreConfig return is_array($groupValues) ? $groupValues : []; } + /** + * Returns a non-empty string list from the central single-genre value surface. + * + * The path is relative to `configuration_values`, for example + * `product_roles.primary_product_terms.terms`. + * + * @return string[] + */ + public function getValueStringList(string $path): array + { + return $this->uniqueStringList($this->value('configuration_values.' . $path, [])); + } + + /** + * Returns a non-empty scalar string map from the central single-genre value surface. + * + * @return array + */ + public function getValueStringMap(string $path): array + { + $value = $this->value('configuration_values.' . $path, []); + + return is_array($value) ? $this->uniqueStringMap($value) : []; + } + + /** + * Returns a raw map/list value from the central single-genre value surface. + * + * @return array + */ + public function getValueArray(string $path): array + { + $value = $this->value('configuration_values.' . $path, []); + + return is_array($value) ? $value : []; + } + + public function getValueString(string $path): string + { + $value = $this->value('configuration_values.' . $path, null); + if (!is_scalar($value)) { + return ''; + } + + return trim((string) $value); + } + + public function getValueBool(string $path): ?bool + { + $value = $this->value('configuration_values.' . $path, null); + 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; + } + } + + return null; + } + + public function getValueInt(string $path): ?int + { + $value = $this->value('configuration_values.' . $path, null); + if (is_int($value)) { + return $value; + } + + if (is_string($value) && preg_match('/^-?\d+$/', trim($value)) === 1) { + return (int) trim($value); + } + + return null; + } + /** * @return array */ @@ -82,6 +163,47 @@ final class GenreConfig return trim((string) $value); } + /** @return string[] */ + private function uniqueStringList(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; + } + + /** @return array */ + private function uniqueStringMap(array $value): array + { + $out = []; + foreach ($value as $key => $mappedValue) { + if (!is_scalar($key) || !is_scalar($mappedValue)) { + continue; + } + + $cleanKey = trim((string) $key); + $cleanValue = trim((string) $mappedValue); + if ($cleanKey !== '' && $cleanValue !== '') { + $out[$cleanKey] = $cleanValue; + } + } + + return $out; + } + private function value(string $path, mixed $fallback): mixed { $current = $this->config; diff --git a/src/Config/QueryEnricherConfig.php b/src/Config/QueryEnricherConfig.php index c6a17f2..138a805 100644 --- a/src/Config/QueryEnricherConfig.php +++ b/src/Config/QueryEnricherConfig.php @@ -15,8 +15,10 @@ final readonly class QueryEnricherConfig /** * @param array $config */ - public function __construct(private array $config) - { + public function __construct( + private array $config, + private readonly ?GenreConfig $genreConfig = null, + ) { } /** @@ -39,7 +41,10 @@ final readonly class QueryEnricherConfig public function getEnrichQueryList(): array { $normalized = []; - $rules = $this->requiredArray('rules'); + $rules = $this->genreConfig?->getValueArray('brands_and_canonical_terms.query_enrichment_rules.rules') ?? []; + if ($rules === []) { + $rules = $this->requiredArray('rules'); + } foreach ($rules as $key => $value) { if (is_array($value)) {