diff --git a/RETRIEX_PATCH_9_0F_PROMPT_ROLE_MEASUREMENT_YAML_ONLY_README.md b/RETRIEX_PATCH_9_0F_PROMPT_ROLE_MEASUREMENT_YAML_ONLY_README.md new file mode 100644 index 0000000..d649aee --- /dev/null +++ b/RETRIEX_PATCH_9_0F_PROMPT_ROLE_MEASUREMENT_YAML_ONLY_README.md @@ -0,0 +1,45 @@ +# RetrieX Patch 9.0f - PromptBuilder Role Guard & Measurement Evidence YAML-only + +## Scope + +This patch continues the PromptBuilder YAML-only migration after Patch 9.0e. + +It converts these remaining PromptBuilder truth/product-role related areas to required YAML configuration: + +- `role_guard.main_device_request_keywords` +- `role_guard.main_device_product_keywords` +- `role_guard.accessory_product_keywords` +- `technical_product_keywords` +- `accessory_request_keywords` +- `sections.measurement_evidence_label` +- `measurement_evidence_guard.intro_rules` +- `measurement_evidence_guard.parameters` +- `technical_product_model_pattern` + +## Intent + +PHP no longer provides fallback truth for these PromptBuilder areas. If a required YAML value is missing or invalid, `PromptBuilderConfig` now throws a clear `InvalidArgumentException`. + +## Not changed + +This patch does not change: + +- Retrieval logic +- Commerce logic +- Shop matching +- AgentRunner +- SSE / frontend +- Prompt wording content in YAML + +## Notes + +`measurement_evidence_guard.parameters` is now required to contain at least one valid parameter definition. Individual list fields inside a parameter are still normalized as before to preserve behavior. + +## Recommended checks + +```bash +php bin/console cache:clear +php bin/console mto:agent:config:validate +php bin/console mto:agent:config:audit-source --details +php bin/console mto:agent:regression:test +``` diff --git a/src/Config/PromptBuilderConfig.php b/src/Config/PromptBuilderConfig.php index cedb831..5cc7da2 100644 --- a/src/Config/PromptBuilderConfig.php +++ b/src/Config/PromptBuilderConfig.php @@ -6,151 +6,11 @@ namespace App\Config; final class PromptBuilderConfig { - private const MAIN_DEVICE_REQUEST_ROLE_KEYWORDS = [ - 'messanlage', 'messanlagen', 'anlage', 'anlagen', 'messgerät', 'messgeraet', - 'messgeräte', 'messgeraete', 'analysegerät', 'analysegeraet', 'analysegeräte', - 'analysegeraete', 'analysator', 'analysatoren', 'analyzer', 'gerät', 'geraet', - 'geräte', 'geraete', 'system', 'systeme', 'monitor', 'monitore', 'controller', - 'tester', 'pocket tester', 'pockettester', 'handmessgerät', 'handmessgeraet', - 'überwachungsgerät', 'ueberwachungsgeraet', 'testomat', 'testoamt', - ]; - - private const MAIN_DEVICE_PRODUCT_ROLE_KEYWORDS = [ - 'messanlage', 'messanlagen', 'messgerät', 'messgeraet', 'messgeräte', 'messgeraete', - 'analysegerät', 'analysegeraet', 'analysegeräte', 'analysegeraete', 'analysator', - 'analysatoren', 'analyzer', 'online-analysator', 'online analysator', - 'online-analysegerät', 'online analysegeraet', 'gerät', 'geraet', 'geräte', - 'geraete', 'system', 'systeme', 'monitor', 'monitore', 'controller', - 'tester', 'pocket tester', 'pockettester', 'handmessgerät', 'handmessgeraet', - 'labor messgerät', 'labor-messgerät', 'labor messgeraet', 'labor-messgeraet', - 'kombimessgerät', 'kombi-messgerät', 'kombimessgeraet', 'kombi-messgeraet', - 'überwachungsgerät', 'ueberwachungsgeraet', 'messumformer', 'transmitter', - 'regler', 'testomat', - ]; - - private const ACCESSORY_PRODUCT_ROLE_KEYWORDS = [ - 'indikator', 'indikatoren', 'indicator', 'reagenz', 'reagenzien', 'reagent', - 'zubehör', 'zubehor', 'ersatzteil', 'ersatzteile', 'kit', 'set', - 'verbrauchsmaterial', 'consumable', 'nachfüll', 'nachfuell', 'refill', - 'lösung', 'loesung', 'solution', 'teststreifen', 'test strip', 'filter', - 'pumpenkopf', 'motorblock', 'service set', 'serviceset', 'service-set', - 'elektrode', 'elektroden', 'electrode', 'electrodes', 'glasschaft-elektrode', - 'kunststoffschaft-elektrode', 'sensor', 'sensoren', 'sensors', 'sonde', - 'sonden', 'probe', 'probes', 'messsonde', 'elektrolyt', 'kabel', 'adapter', - 'ph-indikator', 'ph indikator', 'ph-indikatoren', 'ph indikatoren', - ]; - - private const TECHNICAL_PRODUCT_KEYWORDS = [ - 'technisch', - 'technical', - 'produkt', - 'product', - 'gerät', - 'device', - 'modell', - 'model', - 'messprinzip', - 'measurement principle', - 'schnittstelle', - 'interface', - 'relais', - 'relay', - 'indikator', - 'indicator', - 'grenzwert', - 'threshold', - 'messbereich', - 'measurement range', - 'gemessen', - 'measured', - 'minimaler', - 'minimum', - 'resthärte', - 'resthaerte', - '°dh', - 'dh', - 'spannung', - 'voltage', - 'strom', - 'current', - 'druck', - 'pressure', - 'temperatur', - 'temperature', - 'schutzart', - 'ip', - 'fehlercode', - 'error code', - 'wasserhärte', - 'hardness', - 'testomat', - 'chlor', - 'chlormessung', - ]; - - private const MEASUREMENT_EVIDENCE_PARAMETERS = [ - [ - 'id' => '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, ) { } @@ -214,19 +74,7 @@ final class PromptBuilderConfig 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 { @@ -250,18 +98,6 @@ final class PromptBuilderConfig 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 { @@ -279,33 +115,6 @@ final class PromptBuilderConfig 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[] @@ -371,28 +180,7 @@ final class PromptBuilderConfig 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 { @@ -737,7 +525,7 @@ final class PromptBuilderConfig */ public function getMainDeviceRequestRoleKeywords(): array { - return $this->getStringList('role_guard.main_device_request_keywords', self::MAIN_DEVICE_REQUEST_ROLE_KEYWORDS); + return $this->getRequiredStringList('role_guard.main_device_request_keywords'); } /** @@ -745,7 +533,7 @@ final class PromptBuilderConfig */ public function getMainDeviceProductRoleKeywords(): array { - return $this->getStringList('role_guard.main_device_product_keywords', self::MAIN_DEVICE_PRODUCT_ROLE_KEYWORDS); + return $this->getRequiredStringList('role_guard.main_device_product_keywords'); } /** @@ -753,7 +541,7 @@ final class PromptBuilderConfig */ public function getAccessoryProductRoleKeywords(): array { - return $this->getStringList('role_guard.accessory_product_keywords', self::ACCESSORY_PRODUCT_ROLE_KEYWORDS); + return $this->getRequiredStringList('role_guard.accessory_product_keywords'); } /** @@ -761,10 +549,7 @@ final class PromptBuilderConfig */ public function getTechnicalProductKeywords(): array { - return $this->getStringList( - 'technical_product_keywords', - $this->vocabularyView('prompt.technical_product_keywords', self::TECHNICAL_PRODUCT_KEYWORDS) - ); + return $this->getRequiredStringList('technical_product_keywords'); } /** @@ -772,15 +557,12 @@ final class PromptBuilderConfig */ public function getAccessoryRequestKeywords(): array { - return $this->getStringList( - 'accessory_request_keywords', - $this->vocabularyView('prompt.accessory_request_keywords', self::ACCESSORY_REQUEST_KEYWORDS) - ); + return $this->getRequiredStringList('accessory_request_keywords'); } public function getMeasurementEvidenceSectionLabel(): string { - return $this->getString('sections.measurement_evidence_label', 'MEASUREMENT PARAMETER EVIDENCE CHECK'); + return $this->getRequiredString('sections.measurement_evidence_label'); } /** @@ -788,11 +570,7 @@ final class PromptBuilderConfig */ 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 $this->getRequiredStringList('measurement_evidence_guard.intro_rules'); } /** @@ -800,10 +578,10 @@ final class PromptBuilderConfig */ public function getMeasurementEvidenceParameters(): array { - $value = $this->getValue('measurement_evidence_guard.parameters', self::MEASUREMENT_EVIDENCE_PARAMETERS); + $value = $this->getRequiredValue('measurement_evidence_guard.parameters'); if (!is_array($value)) { - return self::MEASUREMENT_EVIDENCE_PARAMETERS; + throw new \InvalidArgumentException('RetrieX prompt config value "measurement_evidence_guard.parameters" must be a list of parameter definitions.'); } $out = []; @@ -837,7 +615,11 @@ final class PromptBuilderConfig ]; } - return $out !== [] ? $out : self::MEASUREMENT_EVIDENCE_PARAMETERS; + if ($out === []) { + throw new \InvalidArgumentException('RetrieX prompt config value "measurement_evidence_guard.parameters" must contain at least one valid parameter definition.'); + } + + return $out; } /** @@ -866,6 +648,6 @@ final class PromptBuilderConfig public function getTechnicalProductModelPattern(): string { - return $this->getString('technical_product_model_pattern', '/\b[\p{L}]{2,}\s?\d{2,5}\b/u'); + return $this->getRequiredString('technical_product_model_pattern'); } } \ No newline at end of file