optimize technical truth
This commit is contained in:
13
RETRIEX_DEVICE_COMPONENT_SOURCE_UI_FIX_README.md
Normal file
13
RETRIEX_DEVICE_COMPONENT_SOURCE_UI_FIX_README.md
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# RetrieX Device Component and Source UI Fix
|
||||||
|
|
||||||
|
This patch refines product-role handling for generic measuring-device requests and removes the visible duplicate Live-Shopdaten label from shop meta cards.
|
||||||
|
|
||||||
|
Changes:
|
||||||
|
- Treat electrodes, probes, sensors, cables, adapters and similar components as accessories/components, not as complete measuring devices.
|
||||||
|
- Expand main-device recognition for testers, pocket testers, handheld/lab measuring devices, monitoring devices, transmitters and controllers.
|
||||||
|
- Suppress highlights for role-incompatible accessory/component records in main-device requests.
|
||||||
|
- Add prompt rules to keep complete devices separate from components/accessories.
|
||||||
|
- Rename shop meta card eyebrows from Live-Shopdaten to Shop-Suche / Shop-Ergebnisse so the data-basis chip is not visually duplicated.
|
||||||
|
|
||||||
|
Unchanged:
|
||||||
|
- Retrieval, scoring, Shopware search, SSE/jobs and LLM settings.
|
||||||
10
RETRIEX_GENERIC_PARAMETER_ROLE_GUARD_FIX_README.md
Normal file
10
RETRIEX_GENERIC_PARAMETER_ROLE_GUARD_FIX_README.md
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# RetrieX Generic Parameter Role Guard Fix
|
||||||
|
|
||||||
|
This patch keeps product-selection grounding generic while fixing parameter/accessory side effects.
|
||||||
|
|
||||||
|
Core idea:
|
||||||
|
- Role guard stays generic: main-device requests must not be answered with accessories, and explicit accessory requests must not be turned into main-device requests.
|
||||||
|
- Measurement evidence is context-sensitive: a parameter mention counts only when it is tied to measurement purpose such as measurement, measuring range, measurement parameter, determination, analysis, indicator/reagent for that parameter, sensor, or electrode.
|
||||||
|
- Operating conditions, sample/environment limits, reagent-solution pH values, and pH values at a temperature are not sufficient evidence by themselves.
|
||||||
|
|
||||||
|
This patch does not touch retrieval, scoring, SSE, job lifecycle, or LLM timeout logic.
|
||||||
14
RETRIEX_NEGATIVE_WORDING_SOURCE_CHIP_FIX_README.md
Normal file
14
RETRIEX_NEGATIVE_WORDING_SOURCE_CHIP_FIX_README.md
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# RetrieX Negative Wording and Source Chip Fix
|
||||||
|
|
||||||
|
This patch is a narrow finishing pass for answer wording and the production status UI.
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Adds stricter prompt/config rules for scoped negative wording.
|
||||||
|
- Discourages absolute statements such as "ausschließlich", "keines", "nicht geeignet" or "gibt es nicht" unless the sources explicitly support the full scope.
|
||||||
|
- Requires neutral wording for options outside the provided sources instead of external recommendations.
|
||||||
|
- Deduplicates production UI data-basis chips, especially repeated "Live-Shopdaten" labels.
|
||||||
|
|
||||||
|
## Unchanged
|
||||||
|
|
||||||
|
The patch does not change retrieval, scoring, shop querying, SSE/job handling, Ollama settings, or prompt/product role detection logic.
|
||||||
@@ -52,7 +52,6 @@ parameters:
|
|||||||
inferred_role_label: Inferred shop product role
|
inferred_role_label: Inferred shop product role
|
||||||
role_compatibility_label: Role compatibility with request
|
role_compatibility_label: Role compatibility with request
|
||||||
role_incompatible_commercial_suppression_note: 'Commercial fields suppressed: this shop record is not a matching main-device result for the requested product role.'
|
role_incompatible_commercial_suppression_note: 'Commercial fields suppressed: this shop record is not a matching main-device result for the requested product role.'
|
||||||
role_incompatible_record_note: 'Role guard: this is an accessory/consumable record for a main-device request. Do not use it as an answer heading, recommendation, or suitable main-device result.'
|
|
||||||
technical_product_keyword_match_threshold: 2
|
technical_product_keyword_match_threshold: 2
|
||||||
sections:
|
sections:
|
||||||
system_label: SYSTEM
|
system_label: SYSTEM
|
||||||
@@ -65,7 +64,6 @@ parameters:
|
|||||||
language_rules_label: LANGUAGE RULES
|
language_rules_label: LANGUAGE RULES
|
||||||
fact_grounding_rules_label: FACT GROUNDING RULES
|
fact_grounding_rules_label: FACT GROUNDING RULES
|
||||||
measurement_evidence_label: MEASUREMENT PARAMETER EVIDENCE CHECK
|
measurement_evidence_label: MEASUREMENT PARAMETER EVIDENCE CHECK
|
||||||
product_role_guard_label: PRODUCT ROLE GUARD
|
|
||||||
retrieved_knowledge_label: RETRIEVED KNOWLEDGE (primary for technical matching and factual explanation)
|
retrieved_knowledge_label: RETRIEVED KNOWLEDGE (primary for technical matching and factual explanation)
|
||||||
url_content_label: CONTENT FROM URL (authoritative if user-provided)
|
url_content_label: CONTENT FROM URL (authoritative if user-provided)
|
||||||
conversation_context:
|
conversation_context:
|
||||||
@@ -95,10 +93,14 @@ parameters:
|
|||||||
- monitor
|
- monitor
|
||||||
- controller
|
- controller
|
||||||
- testomat
|
- testomat
|
||||||
- testomaten
|
- tester
|
||||||
- testoamt
|
- pocket tester
|
||||||
- testomate
|
|
||||||
- pockettester
|
- pockettester
|
||||||
|
- handmessgerät
|
||||||
|
- handmessgeraet
|
||||||
|
- überwachungsgerät
|
||||||
|
- ueberwachungsgeraet
|
||||||
|
- testoamt
|
||||||
main_device_product_keywords:
|
main_device_product_keywords:
|
||||||
- messanlage
|
- messanlage
|
||||||
- messanlagen
|
- messanlagen
|
||||||
@@ -115,17 +117,28 @@ parameters:
|
|||||||
- system
|
- system
|
||||||
- monitor
|
- monitor
|
||||||
- controller
|
- controller
|
||||||
- testomat
|
- tester
|
||||||
- testomaten
|
- pocket tester
|
||||||
- pockettester
|
- 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
|
||||||
accessory_product_keywords:
|
accessory_product_keywords:
|
||||||
- indikator
|
- indikator
|
||||||
- indikatoren
|
- indikatoren
|
||||||
- ph-indikator
|
|
||||||
- ph indikator
|
|
||||||
- ph-indikatoren
|
|
||||||
- ph indikatoren
|
|
||||||
- indikatoren
|
|
||||||
- indicator
|
- indicator
|
||||||
- reagenz
|
- reagenz
|
||||||
- reagenzien
|
- reagenzien
|
||||||
@@ -151,34 +164,33 @@ parameters:
|
|||||||
- service set
|
- service set
|
||||||
- serviceset
|
- serviceset
|
||||||
- service-set
|
- service-set
|
||||||
- kalibrierlösung
|
- elektrode
|
||||||
- kalibrierloesung
|
- elektroden
|
||||||
- pufferlösung
|
- electrode
|
||||||
- pufferloesung
|
- electrodes
|
||||||
- reinigungslösung
|
- glasschaft-elektrode
|
||||||
- reinigungsloesung
|
- kunststoffschaft-elektrode
|
||||||
- flasche
|
- sensor
|
||||||
- bottle
|
- sensoren
|
||||||
- 100 ml
|
- sensors
|
||||||
- 500 ml
|
- sonde
|
||||||
- 100ml
|
- sonden
|
||||||
- 500ml
|
- probe
|
||||||
|
- probes
|
||||||
main_device_answer_rules:
|
- messsonde
|
||||||
- '- If the user asks for a Testomat, measuring device, analyzer, system, or main device, the answer must be anchored on a compatible main-device record or on a clear no-match statement.'
|
- elektrolyt
|
||||||
- '- Accessories, indicators, reagents, calibration solutions, spare parts, kits, sets, services, or consumables must not be used as the main answer heading for a main-device request.'
|
- kabel
|
||||||
- '- If only accessories or consumables are available in the shop results, start with a negative main-device finding and mention that only accessory/consumable hits were found.'
|
- adapter
|
||||||
- '- Do not provide price, availability, product number, URL, or a recommendation for role-incompatible accessory records unless the user explicitly asks for that accessory.'
|
- ph-indikator
|
||||||
- '- Do not propose alternative devices unless a provided source explicitly proves that exact device is suitable for the requested parameter.'
|
- ph indikator
|
||||||
no_main_device_match_template: 'No compatible main-device shop record is present for the requested main-device role. Accessory/consumable records must not be presented as the requested solution.'
|
- ph-indikatoren
|
||||||
|
- ph indikatoren
|
||||||
|
|
||||||
measurement_evidence_guard:
|
measurement_evidence_guard:
|
||||||
intro_rules:
|
intro_rules:
|
||||||
- '- This block is generated from the current user question and is stricter than broad product-selection wording.'
|
- '- 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.'
|
- '- For measurement-parameter questions, technical suitability requires explicit positive evidence for the requested parameter in the same source record.'
|
||||||
- '- Accessory/indicator requests are not main-device measurement-capability questions. If the user explicitly asks for indicators, reagents, accessories, or consumables, do not answer as if they asked for a measuring device that measures the parameter.'
|
|
||||||
- '- Similar water-treatment parameters, abbreviations, units, product families, search queries, or ranking positions are not enough.'
|
- '- Similar water-treatment parameters, abbreviations, units, product families, search queries, or ranking positions are not enough.'
|
||||||
- '- For pH requests, pH operating ranges, pH values of reagents/indicators, pH transfer/output fields, and general pH mentions are not evidence that a device measures pH.'
|
|
||||||
parameters:
|
parameters:
|
||||||
- id: ph
|
- id: ph
|
||||||
label: pH / pH-Wert
|
label: pH / pH-Wert
|
||||||
@@ -189,22 +201,46 @@ parameters:
|
|||||||
- ph-wert
|
- ph-wert
|
||||||
- ph wert
|
- ph wert
|
||||||
positive_terms:
|
positive_terms:
|
||||||
- pH-Messung
|
- pH
|
||||||
- pH Messung
|
- pH-Wert
|
||||||
- pH-Messgeraet
|
- ph-wert
|
||||||
- pH Messgeraet
|
- ph wert
|
||||||
- pH-Wert messen
|
positive_context_terms:
|
||||||
- pH Wert messen
|
- Messung
|
||||||
- pH-Werte messen
|
- messen
|
||||||
- pH Werte messen
|
- misst
|
||||||
- misst pH
|
- Messbereich
|
||||||
- misst den pH
|
- Messparameter
|
||||||
- misst pH-Wert
|
- Messgröße
|
||||||
- misst den pH-Wert
|
- Messgroesse
|
||||||
- Messparameter pH
|
- Bestimmung
|
||||||
- Messparameter pH-Wert
|
- bestimmen
|
||||||
- Messgroesse pH
|
- Analyse
|
||||||
- Messgroesse pH-Wert
|
- 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:
|
non_equivalent_terms:
|
||||||
- p-Wert
|
- p-Wert
|
||||||
- p Wert
|
- p Wert
|
||||||
@@ -213,15 +249,8 @@ parameters:
|
|||||||
- Alkalität
|
- Alkalität
|
||||||
- Säurekapazität
|
- Säurekapazität
|
||||||
- mmol/l
|
- mmol/l
|
||||||
- pH-Bereich
|
safe_no_evidence_answer_de: Ich finde in den bereitgestellten Quellen keinen sicher belegten Testomat für pH-Messung.
|
||||||
- Betriebsbereich
|
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.
|
||||||
- stoerungsfreier Betrieb
|
|
||||||
- pH-Wert bei
|
|
||||||
- Reagenz
|
|
||||||
- Indikator
|
|
||||||
- 4-20 mA Ausgang
|
|
||||||
- pH-Wertuebertragung
|
|
||||||
safe_no_evidence_answer_de: Ich finde in den bereitgestellten Quellen keinen sicher belegten Testomat, der pH als Messparameter misst.
|
|
||||||
- id: redox
|
- id: redox
|
||||||
label: Redox / ORP
|
label: Redox / ORP
|
||||||
request_terms:
|
request_terms:
|
||||||
@@ -234,8 +263,35 @@ parameters:
|
|||||||
- ORP
|
- ORP
|
||||||
- Oxidations-Reduktionspotential
|
- Oxidations-Reduktionspotential
|
||||||
- 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: []
|
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_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
|
- id: free_chlorine
|
||||||
label: freies Chlor
|
label: freies Chlor
|
||||||
request_terms:
|
request_terms:
|
||||||
@@ -248,11 +304,38 @@ parameters:
|
|||||||
- freiem Chlor
|
- freiem Chlor
|
||||||
- freien Chlor
|
- freien Chlor
|
||||||
- free chlorine
|
- 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:
|
non_equivalent_terms:
|
||||||
- Chlor gesamt
|
- Chlor gesamt
|
||||||
- Gesamtchlor
|
- Gesamtchlor
|
||||||
- total chlorine
|
- 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_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.
|
||||||
|
|
||||||
output_priority:
|
output_priority:
|
||||||
rules:
|
rules:
|
||||||
@@ -271,6 +354,7 @@ parameters:
|
|||||||
- '- Prefer transparent uncertainty over a confident but unsupported answer.'
|
- '- Prefer transparent uncertainty over a confident but unsupported answer.'
|
||||||
- '- Never present missing or weak evidence as proof that a product, value, accessory, or suitability does not exist.'
|
- '- Never present missing or weak evidence as proof that a product, value, accessory, or suitability does not exist.'
|
||||||
- '- A negative answer is allowed only when the provided sources explicitly support that negative finding for the asked scope.'
|
- '- A negative answer is allowed only when the provided sources explicitly support that negative finding for the asked scope.'
|
||||||
|
- '- If the sources merely do not prove suitability, answer as missing evidence instead of as a definitive exclusion. Avoid words such as "ausschließlich", "keines", or "nicht geeignet" unless directly grounded.'
|
||||||
- '- If several products, parameters, or accessories could match, ask one focused clarification question instead of guessing.'
|
- '- If several products, parameters, or accessories could match, ask one focused clarification question instead of guessing.'
|
||||||
- '- For risky or binding product selection, state that sales or support should verify the application before a final selection.'
|
- '- For risky or binding product selection, state that sales or support should verify the application before a final selection.'
|
||||||
without_shop_check_rules:
|
without_shop_check_rules:
|
||||||
@@ -308,6 +392,9 @@ parameters:
|
|||||||
- '- Do not generate external alternative lists, vendor suggestions, or purchase recommendations unless they are explicitly present in the provided sources.'
|
- '- Do not generate external alternative lists, vendor suggestions, or purchase recommendations unless they are explicitly present in the provided sources.'
|
||||||
- '- Do not combine technical identity from one source with commercial fields from a different product.'
|
- '- Do not combine technical identity from one source with commercial fields from a different product.'
|
||||||
- '- Product number, price, availability, and URL must belong to the same explicitly grounded product.'
|
- '- Product number, price, availability, and URL must belong to the same explicitly grounded product.'
|
||||||
|
- '- Avoid absolute negative wording such as "ausschließlich", "keines", "nicht geeignet", "gibt es nicht", or portfolio-wide negations unless the provided sources explicitly support that exact scope.'
|
||||||
|
- '- Prefer narrow evidence wording, for example "in den vorliegenden Quellen nicht sicher belegt", "in den aktuellen Treffern nicht belegt", or "die gezeigten Treffer belegen keine Eignung".'
|
||||||
|
- '- When mentioning options outside the provided sources, do not recommend specific external products, vendors, or purchases. Phrase only the required category neutrally and state that such evidence is not present in the provided sources.'
|
||||||
with_shop_rules:
|
with_shop_rules:
|
||||||
- '- If a product is identified, prefer this structure per product: product name, product number, price, availability, URL, then only the most relevant technical
|
- '- If a product is identified, prefer this structure per product: product name, product number, price, availability, URL, then only the most relevant technical
|
||||||
facts.'
|
facts.'
|
||||||
@@ -329,6 +416,7 @@ parameters:
|
|||||||
- '- For direct follow-up questions about an indicator, value, threshold, or device, answer the resolved mapping first before any table or explanation.'
|
- '- For direct follow-up questions about an indicator, value, threshold, or device, answer the resolved mapping first before any table or explanation.'
|
||||||
- '- If the sources only support a negative finding, output only that negative finding and do not add speculative alternatives.'
|
- '- If the sources only support a negative finding, output only that negative finding and do not add speculative alternatives.'
|
||||||
- '- For product-selection answers, keep the answer minimal: suitable product if explicitly supported, exact evidence, current shop fields if same product identity is clear. Do not add sections for Vorteile, Einsatzbereiche, Messprinzip, or Hinweise unless directly asked and explicitly sourced.'
|
- '- For product-selection answers, keep the answer minimal: suitable product if explicitly supported, exact evidence, current shop fields if same product identity is clear. Do not add sections for Vorteile, Einsatzbereiche, Messprinzip, or Hinweise unless directly asked and explicitly sourced.'
|
||||||
|
- '- If no suitable product is grounded, do not pivot to alternative product categories as a recommendation. You may only say neutrally what kind of explicitly designated product or accessory would be needed, and that it is not evidenced in the current sources.'
|
||||||
accessory_rules:
|
accessory_rules:
|
||||||
- '- If the user asks for a matching accessory, separate the answer into: main device and matching accessory.'
|
- '- If the user asks for a matching accessory, separate the answer into: main device and matching accessory.'
|
||||||
- '- The main device must come first. The accessory must not replace the main device.'
|
- '- The main device must come first. The accessory must not replace the main device.'
|
||||||
@@ -359,6 +447,8 @@ parameters:
|
|||||||
the same fact is present in the current retrieved sources.'
|
the same fact is present in the current retrieved sources.'
|
||||||
- '- Never mention external manufacturers, external brands, or external products unless they are explicitly present in the provided sources.'
|
- '- Never mention external manufacturers, external brands, or external products unless they are explicitly present in the provided sources.'
|
||||||
- '- If the sources do not identify a suitable product, do not invent one.'
|
- '- If the sources do not identify a suitable product, do not invent one.'
|
||||||
|
- '- Do not turn absence of evidence into a broad portfolio statement. Use scoped wording tied to the provided sources and current search results.'
|
||||||
|
- '- Strong negative terms such as "ausschließlich", "keines", "nicht geeignet", or "gibt es nicht" require explicit source support for the full stated scope.'
|
||||||
with_shop_rules:
|
with_shop_rules:
|
||||||
- '- Use shop data as highest priority for current commercial fields: price, availability, URL, current shop-visible naming, and explicitly shop-visible product suitability for product-selection questions.'
|
- '- Use shop data as highest priority for current commercial fields: price, availability, URL, current shop-visible naming, and explicitly shop-visible product suitability for product-selection questions.'
|
||||||
- '- Use retrieved knowledge as highest priority for technical matching, thresholds, measurement principles, and technical explanation when it contains a matching product or fact.'
|
- '- Use retrieved knowledge as highest priority for technical matching, thresholds, measurement principles, and technical explanation when it contains a matching product or fact.'
|
||||||
@@ -380,9 +470,10 @@ parameters:
|
|||||||
- '- Shop product names are authoritative for their own shop URL, product number, price, availability, image, description, and metadata.'
|
- '- Shop product names are authoritative for their own shop URL, product number, price, availability, image, description, and metadata.'
|
||||||
- '- Do not rewrite a shop record heading with a similar device name from retrieved knowledge. If identities differ or are uncertain, separate the RAG device from the shop hit.'
|
- '- Do not rewrite a shop record heading with a similar device name from retrieved knowledge. If identities differ or are uncertain, separate the RAG device from the shop hit.'
|
||||||
- '- If the user asks for a main device, measuring device, analyzer, system, or measuring installation, do not present an accessory, indicator, reagent, kit, set, consumable, or service item as the requested main solution.'
|
- '- If the user asks for a main device, measuring device, analyzer, system, or measuring installation, do not present an accessory, indicator, reagent, kit, set, consumable, or service item as the requested main solution.'
|
||||||
- '- If a main-device request has no compatible main-device shop record, start with a no-match statement for the requested main device, not with the first accessory hit.'
|
- '- If the user asks for a main device, measuring device, analyzer, system, or measuring installation, prioritize complete measuring devices/testers/analyzers. Do not mix electrodes, probes, sensors, cables, adapters, reagents, indicators, or spare parts into the main product list.'
|
||||||
- '- Role-incompatible accessory records may only be mentioned in a short separate section such as "Nur Zubehörtreffer gefunden" and without price, URL, availability, product number, or recommendation wording.'
|
- '- Components such as electrodes, probes, sensors, transmitters, controllers, or measuring converters may be mentioned only in a separate section such as "Komponenten/Zubehör (kein vollständiges Messgerät)" when this distinction is clear from the shop record.'
|
||||||
- '- Do not suggest alternative devices for the requested parameter unless the same provided source explicitly proves the exact alternative device is suitable.'
|
- '- If the user asks for an accessory, indicator, reagent, consumable, kit, or solution with a specific measurement parameter, do not replace the requested parameter with another parameter. A hardness indicator is not a valid answer to a pH-indicator request unless the same source explicitly states pH measurement, pH determination, pH measuring range, or an equivalent parameter-specific purpose.'
|
||||||
|
- '- Mentions of operating conditions, allowed sample pH, reagent-solution pH, storage pH, or pH values at a temperature are not measurement-parameter evidence by themselves.'
|
||||||
- '- If the only shop hit is role-incompatible with the requested product role, state that no matching main-device shop hit is available in the provided shop data; mention the incompatible hit only as a separate accessory/consumable hit if useful.'
|
- '- If the only shop hit is role-incompatible with the requested product role, state that no matching main-device shop hit is available in the provided shop data; mention the incompatible hit only as a separate accessory/consumable hit if useful.'
|
||||||
- '- If a SHOP PRODUCT RECORD says Commercial fields suppressed, do not output its price, availability, URL, product number, image, or metadata anywhere in the answer.'
|
- '- If a SHOP PRODUCT RECORD says Commercial fields suppressed, do not output its price, availability, URL, product number, image, or metadata anywhere in the answer.'
|
||||||
- '- Never write shop-hit lines such as price, availability, URL, product number, or Shop-Treffer below a RAG device unless the same exact SHOP PRODUCT RECORD names that device as the exact shop product.'
|
- '- Never write shop-hit lines such as price, availability, URL, product number, or Shop-Treffer below a RAG device unless the same exact SHOP PRODUCT RECORD names that device as the exact shop product.'
|
||||||
@@ -395,7 +486,6 @@ parameters:
|
|||||||
technical_rules:
|
technical_rules:
|
||||||
- '- For technical product questions, answer primarily with explicitly stated facts.'
|
- '- For technical product questions, answer primarily with explicitly stated facts.'
|
||||||
- '- For measurement-parameter questions, do not treat similar or neighboring abbreviations as equivalent. In particular, p-Wert is not pH-Wert unless the source explicitly says pH or pH-Wert.'
|
- '- For measurement-parameter questions, do not treat similar or neighboring abbreviations as equivalent. In particular, p-Wert is not pH-Wert unless the source explicitly says pH or pH-Wert.'
|
||||||
- '- For pH requests, do not present products as pH-capable when the source only states an allowable pH operating range, a pH value of a reagent/indicator solution, a 4-20 mA transfer/output reference, or a generic pH mention.'
|
|
||||||
- '- Do not invent or infer measurement principles, methods, calibration functions, benefits, advantages, application areas, or alternative products from product family names, search rank, or shop query wording.'
|
- '- Do not invent or infer measurement principles, methods, calibration functions, benefits, advantages, application areas, or alternative products from product family names, search rank, or shop query wording.'
|
||||||
- '- Behave like a technical documentation assistant, not like a sales advisor.'
|
- '- Behave like a technical documentation assistant, not like a sales advisor.'
|
||||||
- '- Keep interpretations minimal and do not generalize application areas beyond the provided sources.'
|
- '- Keep interpretations minimal and do not generalize application areas beyond the provided sources.'
|
||||||
|
|||||||
@@ -482,11 +482,49 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function canonicalRetriexSourceChipKey(value) {
|
||||||
|
return String(value || '')
|
||||||
|
.replace(/\u00a0/g, ' ')
|
||||||
|
.replace(/[‐‑‒–—]/g, '-')
|
||||||
|
.replace(/\s+/g, ' ')
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^\p{L}\p{N}]+/gu, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function deduplicateRetriexSourceChips(container) {
|
||||||
|
if (!container) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.querySelectorAll('.retriex-source-overview__badges').forEach((badgeGroup) => {
|
||||||
|
const seen = new Set();
|
||||||
|
|
||||||
|
badgeGroup.querySelectorAll('.retriex-source-chip').forEach((chip) => {
|
||||||
|
let key = canonicalRetriexSourceChipKey(chip.textContent);
|
||||||
|
|
||||||
|
if (key === 'shopsystem' || key === 'liveshopdaten') {
|
||||||
|
chip.textContent = 'Live-Shopdaten';
|
||||||
|
key = 'liveshopdaten';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seen.has(key)) {
|
||||||
|
chip.remove();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
seen.add(key);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function deduplicateRetriexMetaCards(container) {
|
function deduplicateRetriexMetaCards(container) {
|
||||||
if (!container) {
|
if (!container) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deduplicateRetriexSourceChips(container);
|
||||||
|
|
||||||
const cards = Array.from(container.querySelectorAll('.retriex-meta-card[data-retriex-meta-id]'));
|
const cards = Array.from(container.querySelectorAll('.retriex-meta-card[data-retriex-meta-id]'));
|
||||||
const cardsById = new Map();
|
const cardsById = new Map();
|
||||||
|
|
||||||
|
|||||||
@@ -53,14 +53,8 @@ final readonly class AgentRunner
|
|||||||
$primaryShopResults = [];
|
$primaryShopResults = [];
|
||||||
$knowledgeChunks = [];
|
$knowledgeChunks = [];
|
||||||
$sources = [];
|
$sources = [];
|
||||||
$urlContent = '';
|
|
||||||
$optimizedShopQuery = '';
|
$optimizedShopQuery = '';
|
||||||
$shopSearchQuery = '';
|
$shopSearchQuery = '';
|
||||||
$shopSearchDisplayQuery = '';
|
|
||||||
$shopSearchUsedOptimizedQuery = false;
|
|
||||||
$commerceIntent = CommerceIntentLite::NONE;
|
|
||||||
$knowledgeRetrievalPrompt = $prompt;
|
|
||||||
$usedFollowUpRetrievalContext = false;
|
|
||||||
$commerceHistoryContext = '';
|
$commerceHistoryContext = '';
|
||||||
$attemptedShopRepair = false;
|
$attemptedShopRepair = false;
|
||||||
$usedShopRepair = false;
|
$usedShopRepair = false;
|
||||||
@@ -311,7 +305,7 @@ final readonly class AgentRunner
|
|||||||
$usedShopRepair = $repairPayload['usedRepair'];
|
$usedShopRepair = $repairPayload['usedRepair'];
|
||||||
$shopRepairQueries = $repairPayload['repairQueries'];
|
$shopRepairQueries = $repairPayload['repairQueries'];
|
||||||
|
|
||||||
if ($shopSearchQuery !== '' && !$primaryShopSearchHadSystemFailure) {
|
if (!$primaryShopSearchHadSystemFailure) {
|
||||||
yield $this->systemMsg(
|
yield $this->systemMsg(
|
||||||
$this->buildShopSearchMetaMessage(
|
$this->buildShopSearchMetaMessage(
|
||||||
query: $shopSearchDisplayQuery !== '' ? $shopSearchDisplayQuery : $shopSearchQuery,
|
query: $shopSearchDisplayQuery !== '' ? $shopSearchDisplayQuery : $shopSearchQuery,
|
||||||
@@ -757,7 +751,7 @@ final readonly class AgentRunner
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
$value = $this->sanitizeHistoryQuestion((string) ($matches[0] ?? ''));
|
$value = $this->sanitizeHistoryQuestion(($matches[0] ?? ''));
|
||||||
$value = preg_replace('/\s+/u', ' ', $value) ?? $value;
|
$value = preg_replace('/\s+/u', ' ', $value) ?? $value;
|
||||||
|
|
||||||
return trim(str_replace('®', '', $value));
|
return trim(str_replace('®', '', $value));
|
||||||
@@ -769,7 +763,7 @@ final readonly class AgentRunner
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
$value = preg_replace('/\s+/u', ' ', (string) ($matches[0] ?? '')) ?? '';
|
$value = preg_replace('/\s+/u', ' ', ($matches[0] ?? '')) ?? '';
|
||||||
|
|
||||||
return trim($value);
|
return trim($value);
|
||||||
}
|
}
|
||||||
@@ -1820,6 +1814,10 @@ final readonly class AgentRunner
|
|||||||
private function formatProductionUiSourceLabels(array $sourceLabels): array
|
private function formatProductionUiSourceLabels(array $sourceLabels): array
|
||||||
{
|
{
|
||||||
$labels = [];
|
$labels = [];
|
||||||
|
$seen = [];
|
||||||
|
$shopSystemKey = $this->canonicalProductionUiSourceLabelKey(
|
||||||
|
$this->plainTextFromHtml($this->agentRunnerConfig->getShopSystemSourceLabel())
|
||||||
|
);
|
||||||
|
|
||||||
foreach ($sourceLabels as $label) {
|
foreach ($sourceLabels as $label) {
|
||||||
// Source labels are stored as badge HTML for the legacy "Genutzte Quellen" line.
|
// Source labels are stored as badge HTML for the legacy "Genutzte Quellen" line.
|
||||||
@@ -1831,18 +1829,35 @@ final readonly class AgentRunner
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($label === $this->plainTextFromHtml($this->agentRunnerConfig->getShopSystemSourceLabel())) {
|
$key = $this->canonicalProductionUiSourceLabelKey($label);
|
||||||
|
|
||||||
|
if ($key === $shopSystemKey || $key === 'liveshopdaten') {
|
||||||
$label = 'Live-Shopdaten';
|
$label = 'Live-Shopdaten';
|
||||||
|
$key = 'liveshopdaten';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!in_array($label, $labels, true)) {
|
if (isset($seen[$key])) {
|
||||||
$labels[] = $label;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$seen[$key] = true;
|
||||||
|
$labels[] = $label;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $labels;
|
return $labels;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function canonicalProductionUiSourceLabelKey(string $label): string
|
||||||
|
{
|
||||||
|
$label = html_entity_decode(strip_tags($label), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||||||
|
$label = str_replace(["\xc2\xa0", '‐', '‑', '‒', '–', '—'], [' ', '-', '-', '-', '-', '-'], $label);
|
||||||
|
$label = preg_replace('/\s+/u', ' ', $label) ?? $label;
|
||||||
|
$label = mb_strtolower(trim($label), 'UTF-8');
|
||||||
|
$label = preg_replace('/[^\p{L}\p{N}]+/u', '', $label) ?? $label;
|
||||||
|
|
||||||
|
return $label;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param ShopProductResult[] $shopResults
|
* @param ShopProductResult[] $shopResults
|
||||||
*/
|
*/
|
||||||
@@ -1863,7 +1878,7 @@ final readonly class AgentRunner
|
|||||||
}
|
}
|
||||||
|
|
||||||
$html = '<div class="retriex-meta-card retriex-product-results" data-retriex-meta-id="shop-results" data-retriex-meta-state="completed">'
|
$html = '<div class="retriex-meta-card retriex-product-results" data-retriex-meta-id="shop-results" data-retriex-meta-state="completed">'
|
||||||
. '<div class="retriex-meta-card__eyebrow">Live-Shopdaten</div>'
|
. '<div class="retriex-meta-card__eyebrow">Shop-Ergebnisse</div>'
|
||||||
. '<div class="retriex-meta-card__title">Shop-Ergebnisse</div>'
|
. '<div class="retriex-meta-card__title">Shop-Ergebnisse</div>'
|
||||||
. '<div class="retriex-product-results__summary">' . htmlspecialchars($summary, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</div>';
|
. '<div class="retriex-product-results__summary">' . htmlspecialchars($summary, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</div>';
|
||||||
|
|
||||||
@@ -2053,7 +2068,7 @@ final readonly class AgentRunner
|
|||||||
. '" data-retriex-shop-result-count="'
|
. '" data-retriex-shop-result-count="'
|
||||||
. htmlspecialchars($resultCountAttribute, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')
|
. htmlspecialchars($resultCountAttribute, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')
|
||||||
. '">'
|
. '">'
|
||||||
. '<div class="retriex-meta-card__eyebrow">Live-Shopdaten</div>'
|
. '<div class="retriex-meta-card__eyebrow">Shop-Suche</div>'
|
||||||
. '<div class="retriex-meta-card__title">' . htmlspecialchars($title, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</div>'
|
. '<div class="retriex-meta-card__title">' . htmlspecialchars($title, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</div>'
|
||||||
. '<div class="retriex-meta-card__body">'
|
. '<div class="retriex-meta-card__body">'
|
||||||
. '<span class="retriex-meta-pill retriex-meta-pill--result">' . htmlspecialchars($resultLabel, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</span>'
|
. '<span class="retriex-meta-pill retriex-meta-pill--result">' . htmlspecialchars($resultLabel, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</span>'
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ final readonly class PromptBuilder
|
|||||||
$hasKnowledge = $knowledgeChunks !== [] || $urlContent !== '';
|
$hasKnowledge = $knowledgeChunks !== [] || $urlContent !== '';
|
||||||
$isTechnicalProductQuestion = $this->isLikelyTechnicalProductQuestion($prompt);
|
$isTechnicalProductQuestion = $this->isLikelyTechnicalProductQuestion($prompt);
|
||||||
$asksForAccessoryOrBundle = $this->asksForAccessoryOrBundle($prompt);
|
$asksForAccessoryOrBundle = $this->asksForAccessoryOrBundle($prompt);
|
||||||
|
$requestedProductRole = $this->resolveRequestedProductRole($prompt);
|
||||||
$reliabilityState = $this->resolveReliabilityState(
|
$reliabilityState = $this->resolveReliabilityState(
|
||||||
hasKnowledge: $hasKnowledge,
|
hasKnowledge: $hasKnowledge,
|
||||||
hasShopResults: $hasShopResults,
|
hasShopResults: $hasShopResults,
|
||||||
@@ -60,13 +61,13 @@ final readonly class PromptBuilder
|
|||||||
);
|
);
|
||||||
|
|
||||||
$systemBlock = $this->buildSystemBlock();
|
$systemBlock = $this->buildSystemBlock();
|
||||||
$shopBlock = $this->buildShopBlock($prompt, $shopResults, $swagFullOutPut);
|
$shopBlock = $this->buildShopBlock($prompt, $shopResults, $swagFullOutPut, $requestedProductRole);
|
||||||
$productRoleGuardBlock = $this->buildProductRoleGuardBlock($prompt, $shopResults);
|
|
||||||
$measurementEvidenceBlock = $this->buildMeasurementEvidenceBlock(
|
$measurementEvidenceBlock = $this->buildMeasurementEvidenceBlock(
|
||||||
prompt: $prompt,
|
prompt: $prompt,
|
||||||
knowledgeChunks: $knowledgeChunks,
|
knowledgeChunks: $knowledgeChunks,
|
||||||
urlContent: $urlContent,
|
urlContent: $urlContent,
|
||||||
shopResults: $shopResults
|
shopResults: $shopResults,
|
||||||
|
requestedRole: $requestedProductRole
|
||||||
);
|
);
|
||||||
$outputPriorityBlock = $this->buildOutputPriorityBlock(
|
$outputPriorityBlock = $this->buildOutputPriorityBlock(
|
||||||
hasShopResults: $hasShopResults,
|
hasShopResults: $hasShopResults,
|
||||||
@@ -95,7 +96,6 @@ final readonly class PromptBuilder
|
|||||||
$fixedPrompt = $this->implodeBlocks([
|
$fixedPrompt = $this->implodeBlocks([
|
||||||
$systemBlock,
|
$systemBlock,
|
||||||
$shopBlock,
|
$shopBlock,
|
||||||
$productRoleGuardBlock,
|
|
||||||
$measurementEvidenceBlock,
|
$measurementEvidenceBlock,
|
||||||
$outputPriorityBlock,
|
$outputPriorityBlock,
|
||||||
$fallbackEscalationBlock,
|
$fallbackEscalationBlock,
|
||||||
@@ -113,7 +113,6 @@ final readonly class PromptBuilder
|
|||||||
return $this->implodeBlocks([
|
return $this->implodeBlocks([
|
||||||
$systemBlock,
|
$systemBlock,
|
||||||
$shopBlock,
|
$shopBlock,
|
||||||
$productRoleGuardBlock,
|
|
||||||
$measurementEvidenceBlock,
|
$measurementEvidenceBlock,
|
||||||
$outputPriorityBlock,
|
$outputPriorityBlock,
|
||||||
$fallbackEscalationBlock,
|
$fallbackEscalationBlock,
|
||||||
@@ -189,7 +188,7 @@ final readonly class PromptBuilder
|
|||||||
* Shop data is the most current source for commercial details.
|
* Shop data is the most current source for commercial details.
|
||||||
* It should not override technical matching logic.
|
* It should not override technical matching logic.
|
||||||
*/
|
*/
|
||||||
private function buildShopBlock(string $prompt, array $shopResults, ?string $swagFullOutPut): string
|
private function buildShopBlock(string $prompt, array $shopResults, ?string $swagFullOutPut, ?string $requestedProductRole = null): string
|
||||||
{
|
{
|
||||||
$parts = [];
|
$parts = [];
|
||||||
|
|
||||||
@@ -213,7 +212,7 @@ final readonly class PromptBuilder
|
|||||||
$totalCount = count($normalizedShopResults);
|
$totalCount = count($normalizedShopResults);
|
||||||
$limitedShopResults = array_slice($normalizedShopResults, 0, $this->config->getMaxShopResultsInPrompt());
|
$limitedShopResults = array_slice($normalizedShopResults, 0, $this->config->getMaxShopResultsInPrompt());
|
||||||
$isDetailed = count($limitedShopResults) <= $this->config->getDetailedShopResultsMaxCount();
|
$isDetailed = count($limitedShopResults) <= $this->config->getDetailedShopResultsMaxCount();
|
||||||
$requestedRole = $this->resolveRequestedProductRole($prompt);
|
$requestedRole = $requestedProductRole ?? $this->resolveRequestedProductRole($prompt);
|
||||||
$measurementGuard = $this->resolveRequestedMeasurementGuard($prompt);
|
$measurementGuard = $this->resolveRequestedMeasurementGuard($prompt);
|
||||||
$lines = [];
|
$lines = [];
|
||||||
|
|
||||||
@@ -507,7 +506,7 @@ final readonly class PromptBuilder
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$measurementEvidenceLine = $this->buildShopMeasurementEvidenceLine($product, $measurementGuard, $requestedRole);
|
$measurementEvidenceLine = $this->buildShopMeasurementEvidenceLine($product, $measurementGuard);
|
||||||
if ($measurementEvidenceLine !== '') {
|
if ($measurementEvidenceLine !== '') {
|
||||||
$entryParts[] = $measurementEvidenceLine;
|
$entryParts[] = $measurementEvidenceLine;
|
||||||
}
|
}
|
||||||
@@ -517,9 +516,6 @@ final readonly class PromptBuilder
|
|||||||
|
|
||||||
if ($suppressCommercialFields) {
|
if ($suppressCommercialFields) {
|
||||||
$entryParts[] = $this->config->getShopRoleIncompatibleCommercialSuppressionNote();
|
$entryParts[] = $this->config->getShopRoleIncompatibleCommercialSuppressionNote();
|
||||||
$entryParts[] = $this->config->getProductRoleGuardIncompatibleRecordNote();
|
|
||||||
|
|
||||||
return implode("\n", $entryParts);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$suppressCommercialFields && $product->productNumber) {
|
if (!$suppressCommercialFields && $product->productNumber) {
|
||||||
@@ -544,6 +540,7 @@ final readonly class PromptBuilder
|
|||||||
: $this->config->getShopAvailabilityNoLabel());
|
: $this->config->getShopAvailabilityNoLabel());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!$suppressCommercialFields) {
|
||||||
foreach ($product->highlights as $highlight) {
|
foreach ($product->highlights as $highlight) {
|
||||||
$highlight = $this->normalizeBlockText((string) $highlight);
|
$highlight = $this->normalizeBlockText((string) $highlight);
|
||||||
|
|
||||||
@@ -551,6 +548,7 @@ final readonly class PromptBuilder
|
|||||||
$entryParts[] = $this->config->getShopHighlightPrefix() . $highlight;
|
$entryParts[] = $this->config->getShopHighlightPrefix() . $highlight;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!$suppressCommercialFields && $product->url) {
|
if (!$suppressCommercialFields && $product->url) {
|
||||||
$entryParts[] = $this->config->getShopUrlLabel() . ': '
|
$entryParts[] = $this->config->getShopUrlLabel() . ': '
|
||||||
@@ -575,70 +573,6 @@ final readonly class PromptBuilder
|
|||||||
return implode("\n", $entryParts);
|
return implode("\n", $entryParts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param ShopProductResult[] $shopResults
|
|
||||||
*/
|
|
||||||
private function buildProductRoleGuardBlock(string $prompt, array $shopResults): string
|
|
||||||
{
|
|
||||||
$requestedRole = $this->resolveRequestedProductRole($prompt);
|
|
||||||
|
|
||||||
if ($requestedRole !== 'main_device') {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
$records = array_values(array_filter(
|
|
||||||
$shopResults,
|
|
||||||
static fn(mixed $product): bool => $product instanceof ShopProductResult
|
|
||||||
));
|
|
||||||
|
|
||||||
if ($records === []) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
$compatibleMainDeviceRecords = [];
|
|
||||||
$incompatibleAccessoryRecords = [];
|
|
||||||
$unknownRecords = [];
|
|
||||||
|
|
||||||
foreach ($records as $index => $product) {
|
|
||||||
$inferredRole = $this->resolveShopProductRole($product);
|
|
||||||
$roleCompatibility = $this->resolveShopRoleCompatibility($requestedRole, $inferredRole);
|
|
||||||
$name = $this->normalizeBlockText($product->name);
|
|
||||||
$label = sprintf('record %d%s', $index + 1, $name !== '' ? ' (' . $name . ')' : '');
|
|
||||||
|
|
||||||
if ($roleCompatibility === 'compatible' && $inferredRole === 'main_device') {
|
|
||||||
$compatibleMainDeviceRecords[] = $label;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($roleCompatibility === 'incompatible_accessory_for_main_device_request') {
|
|
||||||
$incompatibleAccessoryRecords[] = $label;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$unknownRecords[] = $label . ' role=' . $inferredRole . ' compatibility=' . $roleCompatibility;
|
|
||||||
}
|
|
||||||
|
|
||||||
$rules = $this->config->getProductRoleGuardMainDeviceRules();
|
|
||||||
$rules[] = '- Requested product role resolved from the user question: main_device.';
|
|
||||||
$rules[] = '- Compatible main-device shop records: ' . ($compatibleMainDeviceRecords !== [] ? implode('; ', $compatibleMainDeviceRecords) : 'none') . '.';
|
|
||||||
$rules[] = '- Role-incompatible accessory/consumable shop records: ' . ($incompatibleAccessoryRecords !== [] ? implode('; ', $incompatibleAccessoryRecords) : 'none') . '.';
|
|
||||||
|
|
||||||
if ($unknownRecords !== []) {
|
|
||||||
$rules[] = '- Unknown or ambiguous shop records must be kept separate and must not be upgraded into a main-device recommendation: ' . implode('; ', $unknownRecords) . '.';
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($compatibleMainDeviceRecords === [] && $incompatibleAccessoryRecords !== []) {
|
|
||||||
$rules[] = '- Mandatory answer behavior: ' . $this->config->getProductRoleGuardNoMainDeviceTemplate();
|
|
||||||
$rules[] = '- Start with the no-match finding for the requested main device. Do not start with an accessory product name.';
|
|
||||||
$rules[] = '- If mentioning incompatible shop hits, use a short separate section like "Nur Zubehörtreffer gefunden" and do not include price, availability, URL, product number, or recommendation wording for those records.';
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->buildRuleBlock(
|
|
||||||
$this->config->getProductRoleGuardSectionLabel(),
|
|
||||||
$rules
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string[] $knowledgeChunks
|
* @param string[] $knowledgeChunks
|
||||||
* @param ShopProductResult[] $shopResults
|
* @param ShopProductResult[] $shopResults
|
||||||
@@ -647,14 +581,9 @@ final readonly class PromptBuilder
|
|||||||
string $prompt,
|
string $prompt,
|
||||||
array $knowledgeChunks,
|
array $knowledgeChunks,
|
||||||
string $urlContent,
|
string $urlContent,
|
||||||
array $shopResults
|
array $shopResults,
|
||||||
|
?string $requestedRole = null
|
||||||
): string {
|
): string {
|
||||||
$requestedRole = $this->resolveRequestedProductRole($prompt);
|
|
||||||
|
|
||||||
if ($requestedRole === 'accessory_or_consumable') {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
$guard = $this->resolveRequestedMeasurementGuard($prompt);
|
$guard = $this->resolveRequestedMeasurementGuard($prompt);
|
||||||
|
|
||||||
if ($guard === null) {
|
if ($guard === null) {
|
||||||
@@ -662,14 +591,19 @@ final readonly class PromptBuilder
|
|||||||
}
|
}
|
||||||
|
|
||||||
$positiveTerms = $this->extractMeasurementGuardStringList($guard, 'positive_terms');
|
$positiveTerms = $this->extractMeasurementGuardStringList($guard, 'positive_terms');
|
||||||
|
$positiveContextTerms = $this->extractMeasurementGuardStringList($guard, 'positive_context_terms');
|
||||||
|
$negativeContextTerms = $this->extractMeasurementGuardStringList($guard, 'negative_context_terms');
|
||||||
$nonEquivalentTerms = $this->extractMeasurementGuardStringList($guard, 'non_equivalent_terms');
|
$nonEquivalentTerms = $this->extractMeasurementGuardStringList($guard, 'non_equivalent_terms');
|
||||||
$label = $this->normalizeBlockText((string) ($guard['label'] ?? 'requested measurement parameter'));
|
$label = $this->normalizeBlockText((string) ($guard['label'] ?? 'requested measurement parameter'));
|
||||||
$safeNoEvidenceAnswer = $this->normalizeBlockText((string) ($guard['safe_no_evidence_answer_de'] ?? ''));
|
$resolvedRequestedRole = $requestedRole ?? $this->resolveRequestedProductRole($prompt);
|
||||||
|
$safeNoEvidenceAnswer = $this->normalizeBlockText((string) (
|
||||||
|
$resolvedRequestedRole === 'accessory_or_consumable'
|
||||||
|
? ($guard['safe_no_accessory_evidence_answer_de'] ?? $guard['safe_no_evidence_answer_de'] ?? '')
|
||||||
|
: ($guard['safe_no_evidence_answer_de'] ?? '')
|
||||||
|
));
|
||||||
|
|
||||||
$knowledgeText = $this->normalizeBlockText(implode("\n\n", array_map('strval', $knowledgeChunks)) . "\n\n" . $urlContent);
|
$knowledgeText = $this->normalizeBlockText(implode("\n\n", array_map('strval', $knowledgeChunks)) . "\n\n" . $urlContent);
|
||||||
$knowledgeHasEvidence = $this->containsMeasurementPositiveEvidence($knowledgeText, $positiveTerms);
|
$knowledgeHasEvidence = $this->containsMeasurementPositiveEvidence($knowledgeText, $positiveTerms, $positiveContextTerms, $negativeContextTerms);
|
||||||
$knowledgeHasOnlyWeakMention = !$knowledgeHasEvidence
|
|
||||||
&& $this->containsAnyMeasurementWeakMention($knowledgeText, $guard);
|
|
||||||
|
|
||||||
$shopEvidenceLines = [];
|
$shopEvidenceLines = [];
|
||||||
$shopHasEvidence = false;
|
$shopHasEvidence = false;
|
||||||
@@ -679,7 +613,7 @@ final readonly class PromptBuilder
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$hasEvidence = $this->shopProductHasMeasurementEvidence($product, $positiveTerms);
|
$hasEvidence = $this->shopProductHasMeasurementEvidence($product, $positiveTerms, $positiveContextTerms, $negativeContextTerms);
|
||||||
$productName = $this->normalizeBlockText($product->name);
|
$productName = $this->normalizeBlockText($product->name);
|
||||||
|
|
||||||
if ($hasEvidence) {
|
if ($hasEvidence) {
|
||||||
@@ -702,16 +636,19 @@ final readonly class PromptBuilder
|
|||||||
|
|
||||||
$rules = $this->config->getMeasurementEvidenceIntroRules();
|
$rules = $this->config->getMeasurementEvidenceIntroRules();
|
||||||
$rules[] = '- User requested measurement parameter: ' . $label . '.';
|
$rules[] = '- User requested measurement parameter: ' . $label . '.';
|
||||||
$rules[] = '- Positive evidence terms that count for this request: ' . implode(', ', $positiveTerms) . '.';
|
$rules[] = '- Positive parameter terms for this request: ' . implode(', ', $positiveTerms) . '.';
|
||||||
|
if ($positiveContextTerms !== []) {
|
||||||
|
$rules[] = '- These parameter terms count as suitability evidence only in a measurement-purpose context such as: ' . implode(', ', $positiveContextTerms) . '.';
|
||||||
|
}
|
||||||
|
if ($negativeContextTerms !== []) {
|
||||||
|
$rules[] = '- These contexts are not suitability evidence by themselves: ' . implode(', ', $negativeContextTerms) . '.';
|
||||||
|
}
|
||||||
|
|
||||||
if ($nonEquivalentTerms !== []) {
|
if ($nonEquivalentTerms !== []) {
|
||||||
$rules[] = '- Terms that must NOT be treated as equivalent positive evidence: ' . implode(', ', $nonEquivalentTerms) . '.';
|
$rules[] = '- Terms that must NOT be treated as equivalent positive evidence: ' . implode(', ', $nonEquivalentTerms) . '.';
|
||||||
}
|
}
|
||||||
|
|
||||||
$rules[] = '- RAG/URL evidence scan for this exact measurement capability: ' . ($knowledgeHasEvidence ? 'explicit positive capability evidence found.' : 'no explicit positive capability evidence found.');
|
$rules[] = '- RAG/URL evidence scan for this exact parameter: ' . ($knowledgeHasEvidence ? 'explicit positive evidence found.' : 'no explicit positive evidence found.');
|
||||||
if ($knowledgeHasOnlyWeakMention) {
|
|
||||||
$rules[] = '- RAG/URL weak mention scan: the parameter is mentioned, but only as a weak/non-capability mention. Do not use this as suitability evidence.';
|
|
||||||
}
|
|
||||||
$rules = array_merge($rules, $shopEvidenceLines);
|
$rules = array_merge($rules, $shopEvidenceLines);
|
||||||
|
|
||||||
if (!$knowledgeHasEvidence && !$shopHasEvidence) {
|
if (!$knowledgeHasEvidence && !$shopHasEvidence) {
|
||||||
@@ -719,11 +656,14 @@ final readonly class PromptBuilder
|
|||||||
if ($safeNoEvidenceAnswer !== '') {
|
if ($safeNoEvidenceAnswer !== '') {
|
||||||
$rules[] = '- Start the answer with this meaning in the user language: ' . $safeNoEvidenceAnswer;
|
$rules[] = '- Start the answer with this meaning in the user language: ' . $safeNoEvidenceAnswer;
|
||||||
}
|
}
|
||||||
|
if ($resolvedRequestedRole === 'accessory_or_consumable') {
|
||||||
|
$rules[] = '- Do not recommend accessories for a different measurement parameter just because they are accessories. If only accessories for other parameters are present, say that only non-matching accessory hits were found.';
|
||||||
|
} else {
|
||||||
$rules[] = '- You may list exact shop hits only as commercial/search hits under a heading such as "Shop-Treffer (technische Eignung nicht sicher belegt)".';
|
$rules[] = '- You may list exact shop hits only as commercial/search hits under a heading such as "Shop-Treffer (technische Eignung nicht sicher belegt)".';
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$rules[] = '- Do not output measurement ranges, methods, application areas, advantages, or alternative suitable models unless the same source record contains explicit positive capability evidence for the requested measurement parameter.';
|
$rules[] = '- Do not output measurement ranges, methods, application areas, advantages, or alternative suitable models unless the same source record contains explicit positive evidence for the requested measurement parameter.';
|
||||||
$rules[] = '- Do not list products as relevant just because the requested parameter appears in an operating range, reagent/indicator property, output-transfer field, metadata, or generic mention.';
|
|
||||||
$rules[] = '- The generated shop search query, search intent, ranking position, and user question are not factual evidence for product suitability.';
|
$rules[] = '- The generated shop search query, search intent, ranking position, and user question are not factual evidence for product suitability.';
|
||||||
|
|
||||||
return $this->buildRuleBlock(
|
return $this->buildRuleBlock(
|
||||||
@@ -732,20 +672,22 @@ final readonly class PromptBuilder
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function buildShopMeasurementEvidenceLine(ShopProductResult $product, ?array $guard, string $requestedRole = 'unknown'): string
|
private function buildShopMeasurementEvidenceLine(ShopProductResult $product, ?array $guard): string
|
||||||
{
|
{
|
||||||
if ($guard === null || $requestedRole === 'accessory_or_consumable') {
|
if ($guard === null) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
$positiveTerms = $this->extractMeasurementGuardStringList($guard, 'positive_terms');
|
$positiveTerms = $this->extractMeasurementGuardStringList($guard, 'positive_terms');
|
||||||
|
$positiveContextTerms = $this->extractMeasurementGuardStringList($guard, 'positive_context_terms');
|
||||||
|
$negativeContextTerms = $this->extractMeasurementGuardStringList($guard, 'negative_context_terms');
|
||||||
$label = $this->normalizeBlockText((string) ($guard['label'] ?? 'requested measurement parameter'));
|
$label = $this->normalizeBlockText((string) ($guard['label'] ?? 'requested measurement parameter'));
|
||||||
|
|
||||||
if ($positiveTerms === []) {
|
if ($positiveTerms === []) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->shopProductHasMeasurementEvidence($product, $positiveTerms)) {
|
if ($this->shopProductHasMeasurementEvidence($product, $positiveTerms, $positiveContextTerms, $negativeContextTerms)) {
|
||||||
return sprintf(
|
return sprintf(
|
||||||
'Requested measurement evidence: explicit positive evidence for %s is present in this same SHOP PRODUCT RECORD.',
|
'Requested measurement evidence: explicit positive evidence for %s is present in this same SHOP PRODUCT RECORD.',
|
||||||
$label
|
$label
|
||||||
@@ -803,36 +745,86 @@ final readonly class PromptBuilder
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string[] $positiveTerms
|
* @param string[] $positiveTerms
|
||||||
|
* @param string[] $positiveContextTerms
|
||||||
|
* @param string[] $negativeContextTerms
|
||||||
*/
|
*/
|
||||||
private function shopProductHasMeasurementEvidence(ShopProductResult $product, array $positiveTerms): bool
|
private function shopProductHasMeasurementEvidence(
|
||||||
{
|
ShopProductResult $product,
|
||||||
return $this->containsMeasurementPositiveEvidence(
|
array $positiveTerms,
|
||||||
$this->buildShopProductEvidenceText($product),
|
array $positiveContextTerms,
|
||||||
$positiveTerms
|
array $negativeContextTerms
|
||||||
);
|
): bool {
|
||||||
|
foreach ($this->buildShopProductEvidenceFragments($product) as $fragment) {
|
||||||
|
if ($this->containsMeasurementPositiveEvidence($fragment, $positiveTerms, $positiveContextTerms, $negativeContextTerms)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function buildShopProductEvidenceText(ShopProductResult $product): string
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string[]
|
||||||
|
*/
|
||||||
|
private function buildShopProductEvidenceFragments(ShopProductResult $product): array
|
||||||
{
|
{
|
||||||
return $this->normalizeBlockText(implode(' ', array_filter([
|
$fragments = array_filter([
|
||||||
$product->name,
|
$product->name,
|
||||||
$product->productNumber,
|
|
||||||
$product->manufacturer,
|
$product->manufacturer,
|
||||||
|
$product->url,
|
||||||
implode(' ', array_map('strval', $product->highlights)),
|
implode(' ', array_map('strval', $product->highlights)),
|
||||||
$product->description,
|
$product->description,
|
||||||
$product->customFields,
|
$product->customFields,
|
||||||
$product->url,
|
], static fn($value): bool => is_scalar($value) && trim((string) $value) !== '');
|
||||||
], static fn($value): bool => is_scalar($value) && trim((string) $value) !== '')));
|
|
||||||
|
$out = [];
|
||||||
|
foreach ($fragments as $fragment) {
|
||||||
|
foreach ($this->splitMeasurementEvidenceFragments((string) $fragment) as $part) {
|
||||||
|
if ($part !== '') {
|
||||||
|
$out[] = $part;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $out;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string[] $positiveTerms
|
* @param string[] $positiveTerms
|
||||||
|
* @param string[] $positiveContextTerms
|
||||||
|
* @param string[] $negativeContextTerms
|
||||||
*/
|
*/
|
||||||
private function containsMeasurementPositiveEvidence(string $text, array $positiveTerms): bool
|
private function containsMeasurementPositiveEvidence(
|
||||||
{
|
string $text,
|
||||||
$normalizedText = $this->normalizeForMeasurementMatching($text);
|
array $positiveTerms,
|
||||||
|
array $positiveContextTerms,
|
||||||
|
array $negativeContextTerms
|
||||||
|
): bool {
|
||||||
|
foreach ($this->splitMeasurementEvidenceFragments($text) as $fragment) {
|
||||||
|
$normalizedFragment = $this->normalizeForMeasurementMatching($fragment);
|
||||||
|
|
||||||
foreach ($positiveTerms as $term) {
|
if ($normalizedFragment === '' || !$this->containsAnyMeasurementTerm($normalizedFragment, $positiveTerms)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($negativeContextTerms !== [] && $this->containsAnyMeasurementTerm($normalizedFragment, $negativeContextTerms)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($positiveContextTerms === [] || $this->containsAnyMeasurementTerm($normalizedFragment, $positiveContextTerms)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string[] $terms
|
||||||
|
*/
|
||||||
|
private function containsAnyMeasurementTerm(string $normalizedText, array $terms): bool
|
||||||
|
{
|
||||||
|
foreach ($terms as $term) {
|
||||||
if ($this->containsMeasurementTerm($normalizedText, $term)) {
|
if ($this->containsMeasurementTerm($normalizedText, $term)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -842,29 +834,21 @@ final readonly class PromptBuilder
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true when a measurement parameter is mentioned only as request wording or as a known non-equivalent/weak term.
|
* @return string[]
|
||||||
* This is used for prompt diagnostics and must not be treated as positive suitability evidence.
|
|
||||||
*/
|
*/
|
||||||
private function containsAnyMeasurementWeakMention(string $text, array $guard): bool
|
private function splitMeasurementEvidenceFragments(string $text): array
|
||||||
{
|
{
|
||||||
$weakTerms = array_merge(
|
$text = $this->normalizeBlockText($text);
|
||||||
$this->extractMeasurementGuardStringList($guard, 'request_terms'),
|
if ($text === '') {
|
||||||
$this->extractMeasurementGuardStringList($guard, 'non_equivalent_terms')
|
return [];
|
||||||
);
|
|
||||||
|
|
||||||
if ($weakTerms === []) {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$normalizedText = $this->normalizeForMeasurementMatching($text);
|
$parts = preg_split('/[\n.;|]+/u', $text) ?: [$text];
|
||||||
|
|
||||||
foreach ($weakTerms as $term) {
|
return array_values(array_filter(
|
||||||
if ($this->containsMeasurementTerm($normalizedText, $term)) {
|
array_map(fn(string $part): string => $this->normalizeBlockText($part), $parts),
|
||||||
return true;
|
static fn(string $part): bool => $part !== ''
|
||||||
}
|
));
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function containsMeasurementTerm(string $normalizedText, string $term): bool
|
private function containsMeasurementTerm(string $normalizedText, string $term): bool
|
||||||
@@ -945,21 +929,42 @@ final readonly class PromptBuilder
|
|||||||
|
|
||||||
private function resolveRequestedProductRole(string $prompt): string
|
private function resolveRequestedProductRole(string $prompt): string
|
||||||
{
|
{
|
||||||
$normalized = mb_strtolower($prompt, 'UTF-8');
|
$normalized = mb_strtolower($this->normalizeBlockText($prompt), 'UTF-8');
|
||||||
$asksForAccessory = $this->containsAnyPromptKeyword($normalized, $this->config->getAccessoryRequestKeywords())
|
$hasAccessoryIntent = $this->containsAnyPromptKeyword($normalized, $this->config->getAccessoryProductRoleKeywords());
|
||||||
|| $this->containsAnyPromptKeyword($normalized, $this->config->getAccessoryProductRoleKeywords());
|
$hasMainDeviceIntent = $this->containsAnyPromptKeyword($normalized, $this->config->getMainDeviceRequestRoleKeywords());
|
||||||
|
|
||||||
if ($asksForAccessory) {
|
if ($hasAccessoryIntent && !$this->hasDirectMainDeviceRequest($normalized)) {
|
||||||
return 'accessory_or_consumable';
|
return 'accessory_or_consumable';
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->containsAnyPromptKeyword($normalized, $this->config->getMainDeviceRequestRoleKeywords())) {
|
if ($hasMainDeviceIntent) {
|
||||||
return 'main_device';
|
return 'main_device';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($hasAccessoryIntent) {
|
||||||
|
return 'accessory_or_consumable';
|
||||||
|
}
|
||||||
|
|
||||||
return 'unknown';
|
return 'unknown';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function hasDirectMainDeviceRequest(string $normalizedPrompt): bool
|
||||||
|
{
|
||||||
|
$patterns = [
|
||||||
|
'/\b(welcher|welches|welche)\s+[^?.!,;]{0,40}(testomat|messgerät|messgeraet|analysegerät|analysegeraet|gerät|geraet|analysator)\b/u',
|
||||||
|
'/\b(suche|finde|empfiehl|empfehle)\s+[^?.!,;]{0,40}(testomat|messgerät|messgeraet|analysegerät|analysegeraet|gerät|geraet|analysator)\b/u',
|
||||||
|
'/\b(testomat|messgerät|messgeraet|analysegerät|analysegeraet|gerät|geraet|analysator)\s+[^?.!,;]{0,40}(messen|misst|überwachen|ueberwachen|kann|für|fuer)\b/u',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($patterns as $pattern) {
|
||||||
|
if (preg_match($pattern, $normalizedPrompt) === 1) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private function resolveShopProductRole(ShopProductResult $product): string
|
private function resolveShopProductRole(ShopProductResult $product): string
|
||||||
{
|
{
|
||||||
$primaryRole = $this->resolveShopPrimaryProductRole($product);
|
$primaryRole = $this->resolveShopPrimaryProductRole($product);
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ final class PromptBuilderConfig
|
|||||||
'messgeräte', 'messgeraete', 'analysegerät', 'analysegeraet', 'analysegeräte',
|
'messgeräte', 'messgeraete', 'analysegerät', 'analysegeraet', 'analysegeräte',
|
||||||
'analysegeraete', 'analysator', 'analysatoren', 'analyzer', 'gerät', 'geraet',
|
'analysegeraete', 'analysator', 'analysatoren', 'analyzer', 'gerät', 'geraet',
|
||||||
'geräte', 'geraete', 'system', 'systeme', 'monitor', 'monitore', 'controller',
|
'geräte', 'geraete', 'system', 'systeme', 'monitor', 'monitore', 'controller',
|
||||||
'testomat', 'testomaten', 'testoamt', 'testomate', 'pockettester',
|
'tester', 'pocket tester', 'pockettester', 'handmessgerät', 'handmessgeraet',
|
||||||
|
'überwachungsgerät', 'ueberwachungsgeraet', 'testomat', 'testoamt',
|
||||||
];
|
];
|
||||||
|
|
||||||
private const MAIN_DEVICE_PRODUCT_ROLE_KEYWORDS = [
|
private const MAIN_DEVICE_PRODUCT_ROLE_KEYWORDS = [
|
||||||
@@ -20,7 +21,11 @@ final class PromptBuilderConfig
|
|||||||
'analysatoren', 'analyzer', 'online-analysator', 'online analysator',
|
'analysatoren', 'analyzer', 'online-analysator', 'online analysator',
|
||||||
'online-analysegerät', 'online analysegeraet', 'gerät', 'geraet', 'geräte',
|
'online-analysegerät', 'online analysegeraet', 'gerät', 'geraet', 'geräte',
|
||||||
'geraete', 'system', 'systeme', 'monitor', 'monitore', 'controller',
|
'geraete', 'system', 'systeme', 'monitor', 'monitore', 'controller',
|
||||||
'testomat', 'testomaten', 'pockettester',
|
'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 = [
|
private const ACCESSORY_PRODUCT_ROLE_KEYWORDS = [
|
||||||
@@ -29,9 +34,10 @@ final class PromptBuilderConfig
|
|||||||
'verbrauchsmaterial', 'consumable', 'nachfüll', 'nachfuell', 'refill',
|
'verbrauchsmaterial', 'consumable', 'nachfüll', 'nachfuell', 'refill',
|
||||||
'lösung', 'loesung', 'solution', 'teststreifen', 'test strip', 'filter',
|
'lösung', 'loesung', 'solution', 'teststreifen', 'test strip', 'filter',
|
||||||
'pumpenkopf', 'motorblock', 'service set', 'serviceset', 'service-set',
|
'pumpenkopf', 'motorblock', 'service set', 'serviceset', 'service-set',
|
||||||
'kalibrierlösung', 'kalibrierloesung', 'pufferlösung', 'pufferloesung',
|
'elektrode', 'elektroden', 'electrode', 'electrodes', 'glasschaft-elektrode',
|
||||||
'reinigungslösung', 'reinigungsloesung', 'flasche', 'bottle', '100 ml', '500 ml',
|
'kunststoffschaft-elektrode', 'sensor', 'sensoren', 'sensors', 'sonde',
|
||||||
'100ml', '500ml',
|
'sonden', 'probe', 'probes', 'messsonde', 'elektrolyt', 'kabel', 'adapter',
|
||||||
|
'ph-indikator', 'ph indikator', 'ph-indikatoren', 'ph indikatoren',
|
||||||
];
|
];
|
||||||
|
|
||||||
private const TECHNICAL_PRODUCT_KEYWORDS = [
|
private const TECHNICAL_PRODUCT_KEYWORDS = [
|
||||||
@@ -87,58 +93,34 @@ final class PromptBuilderConfig
|
|||||||
'id' => 'ph',
|
'id' => 'ph',
|
||||||
'label' => 'pH / pH-Wert',
|
'label' => 'pH / pH-Wert',
|
||||||
'request_terms' => ['ph', 'pH', 'pH-Wert', 'ph-wert', 'ph wert'],
|
'request_terms' => ['ph', 'pH', 'pH-Wert', 'ph-wert', 'ph wert'],
|
||||||
'positive_terms' => [
|
'positive_terms' => ['pH', 'pH-Wert', 'ph-wert', 'ph wert'],
|
||||||
'pH-Messung',
|
'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'],
|
||||||
'pH Messung',
|
'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'],
|
||||||
'pH-Messgeraet',
|
'non_equivalent_terms' => ['p-Wert', 'p Wert', 'm-Wert', 'minus m-Wert', 'Alkalität', 'Säurekapazität', 'mmol/l'],
|
||||||
'pH Messgeraet',
|
'safe_no_evidence_answer_de' => 'Ich finde in den bereitgestellten Quellen keinen sicher belegten Testomat für pH-Messung.',
|
||||||
'pH-Wert messen',
|
'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.',
|
||||||
'pH Wert messen',
|
|
||||||
'pH-Werte messen',
|
|
||||||
'pH Werte messen',
|
|
||||||
'misst pH',
|
|
||||||
'misst den pH',
|
|
||||||
'misst pH-Wert',
|
|
||||||
'misst den pH-Wert',
|
|
||||||
'Messparameter pH',
|
|
||||||
'Messparameter pH-Wert',
|
|
||||||
'Messgroesse pH',
|
|
||||||
'Messgroesse pH-Wert',
|
|
||||||
],
|
|
||||||
'non_equivalent_terms' => [
|
|
||||||
'p-Wert',
|
|
||||||
'p Wert',
|
|
||||||
'm-Wert',
|
|
||||||
'minus m-Wert',
|
|
||||||
'Alkalität',
|
|
||||||
'Säurekapazität',
|
|
||||||
'mmol/l',
|
|
||||||
'pH-Bereich',
|
|
||||||
'Betriebsbereich',
|
|
||||||
'stoerungsfreier Betrieb',
|
|
||||||
'pH-Wert bei',
|
|
||||||
'Reagenz',
|
|
||||||
'Indikator',
|
|
||||||
'4-20 mA Ausgang',
|
|
||||||
'pH-Wertuebertragung',
|
|
||||||
],
|
|
||||||
'safe_no_evidence_answer_de' => 'Ich finde in den bereitgestellten Quellen keinen sicher belegten Testomat, der pH als Messparameter misst.',
|
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'id' => 'redox',
|
'id' => 'redox',
|
||||||
'label' => 'Redox / ORP',
|
'label' => 'Redox / ORP',
|
||||||
'request_terms' => ['redox', 'orp', 'oxidations-reduktionspotential', 'oxidations reduktionspotential'],
|
'request_terms' => ['redox', 'orp', 'oxidations-reduktionspotential', 'oxidations reduktionspotential'],
|
||||||
'positive_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' => [],
|
'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_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',
|
'id' => 'free_chlorine',
|
||||||
'label' => 'freies Chlor',
|
'label' => 'freies Chlor',
|
||||||
'request_terms' => ['freies chlor', 'freiem chlor', 'freien chlor', 'free chlorine'],
|
'request_terms' => ['freies chlor', 'freiem chlor', 'freien chlor', 'free chlorine'],
|
||||||
'positive_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'],
|
'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_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.',
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -150,6 +132,11 @@ final class PromptBuilderConfig
|
|||||||
'zubehor',
|
'zubehor',
|
||||||
'dazu',
|
'dazu',
|
||||||
'indikator',
|
'indikator',
|
||||||
|
'indikatoren',
|
||||||
|
'ph-indikator',
|
||||||
|
'ph indikator',
|
||||||
|
'ph-indikatoren',
|
||||||
|
'ph indikatoren',
|
||||||
'reagenz',
|
'reagenz',
|
||||||
'kit',
|
'kit',
|
||||||
'set',
|
'set',
|
||||||
@@ -440,6 +427,7 @@ final class PromptBuilderConfig
|
|||||||
'- Prefer transparent uncertainty over a confident but unsupported answer.',
|
'- Prefer transparent uncertainty over a confident but unsupported answer.',
|
||||||
'- Never present missing or weak evidence as proof that a product, value, accessory, or suitability does not exist.',
|
'- Never present missing or weak evidence as proof that a product, value, accessory, or suitability does not exist.',
|
||||||
'- A negative answer is allowed only when the provided sources explicitly support that negative finding for the asked scope.',
|
'- A negative answer is allowed only when the provided sources explicitly support that negative finding for the asked scope.',
|
||||||
|
'- If the sources merely do not prove suitability, answer as missing evidence instead of as a definitive exclusion. Avoid words such as "ausschließlich", "keines", or "nicht geeignet" unless directly grounded.',
|
||||||
'- If several products, parameters, or accessories could match, ask one focused clarification question instead of guessing.',
|
'- If several products, parameters, or accessories could match, ask one focused clarification question instead of guessing.',
|
||||||
'- For risky or binding product selection, state that sales or support should verify the application before a final selection.',
|
'- For risky or binding product selection, state that sales or support should verify the application before a final selection.',
|
||||||
]);
|
]);
|
||||||
@@ -485,6 +473,9 @@ final class PromptBuilderConfig
|
|||||||
'- Do not generate external alternative lists, vendor suggestions, or purchase recommendations unless they are explicitly present in the provided sources.',
|
'- Do not generate external alternative lists, vendor suggestions, or purchase recommendations unless they are explicitly present in the provided sources.',
|
||||||
'- Do not combine technical identity from one source with commercial fields from a different product.',
|
'- Do not combine technical identity from one source with commercial fields from a different product.',
|
||||||
'- Product number, price, availability, and URL must belong to the same explicitly grounded product.',
|
'- Product number, price, availability, and URL must belong to the same explicitly grounded product.',
|
||||||
|
'- Avoid absolute negative wording such as "ausschließlich", "keines", "nicht geeignet", "gibt es nicht", or portfolio-wide negations unless the provided sources explicitly support that exact scope.',
|
||||||
|
'- Prefer narrow evidence wording, for example "in den vorliegenden Quellen nicht sicher belegt", "in den aktuellen Treffern nicht belegt", or "die gezeigten Treffer belegen keine Eignung".',
|
||||||
|
'- When mentioning options outside the provided sources, do not recommend specific external products, vendors, or purchases. Phrase only the required category neutrally and state that such evidence is not present in the provided sources.',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -528,6 +519,7 @@ final class PromptBuilderConfig
|
|||||||
'- For direct follow-up questions about an indicator, value, threshold, or device, answer the resolved mapping first before any table or explanation.',
|
'- For direct follow-up questions about an indicator, value, threshold, or device, answer the resolved mapping first before any table or explanation.',
|
||||||
'- If the sources only support a negative finding, output only that negative finding and do not add speculative alternatives.',
|
'- If the sources only support a negative finding, output only that negative finding and do not add speculative alternatives.',
|
||||||
'- For product-selection answers, keep the answer minimal: suitable product if explicitly supported, exact evidence, current shop fields if same product identity is clear. Do not add sections for Vorteile, Einsatzbereiche, Messprinzip, or Hinweise unless directly asked and explicitly sourced.',
|
'- For product-selection answers, keep the answer minimal: suitable product if explicitly supported, exact evidence, current shop fields if same product identity is clear. Do not add sections for Vorteile, Einsatzbereiche, Messprinzip, or Hinweise unless directly asked and explicitly sourced.',
|
||||||
|
'- If no suitable product is grounded, do not pivot to alternative product categories as a recommendation. You may only say neutrally what kind of explicitly designated product or accessory would be needed, and that it is not evidenced in the current sources.',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -587,6 +579,8 @@ final class PromptBuilderConfig
|
|||||||
'- For follow-up questions, use the conversation only to resolve what the user refers to; do not copy technical facts from previous assistant answers unless the same fact is present in the current retrieved sources.',
|
'- For follow-up questions, use the conversation only to resolve what the user refers to; do not copy technical facts from previous assistant answers unless the same fact is present in the current retrieved sources.',
|
||||||
'- Never mention external manufacturers, external brands, or external products unless they are explicitly present in the provided sources.',
|
'- Never mention external manufacturers, external brands, or external products unless they are explicitly present in the provided sources.',
|
||||||
'- If the sources do not identify a suitable product, do not invent one.',
|
'- If the sources do not identify a suitable product, do not invent one.',
|
||||||
|
'- Do not turn absence of evidence into a broad portfolio statement. Use scoped wording tied to the provided sources and current search results.',
|
||||||
|
'- Strong negative terms such as "ausschließlich", "keines", "nicht geeignet", or "gibt es nicht" require explicit source support for the full stated scope.',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -602,7 +596,6 @@ final class PromptBuilderConfig
|
|||||||
'- For product-selection questions, a shop result proves technical suitability only when the same SHOP PRODUCT RECORD explicitly states the requested measurement parameter, application, or compatibility. Search ranking, generated query terms, generic category matches, and similar wording are not proof.',
|
'- For product-selection questions, a shop result proves technical suitability only when the same SHOP PRODUCT RECORD explicitly states the requested measurement parameter, application, or compatibility. Search ranking, generated query terms, generic category matches, and similar wording are not proof.',
|
||||||
'- If the requested parameter appears only in the generated shop query, metadata, unrelated highlights, or another product record, treat suitability as unverified and say that the shop hit requires technical verification.',
|
'- If the requested parameter appears only in the generated shop query, metadata, unrelated highlights, or another product record, treat suitability as unverified and say that the shop hit requires technical verification.',
|
||||||
'- Do not convert p-Wert, m-Wert, minus m-Wert, alkalinity, acid capacity, or other water-treatment parameters into pH or pH-Wert unless the same source explicitly says pH or pH-Wert.',
|
'- Do not convert p-Wert, m-Wert, minus m-Wert, alkalinity, acid capacity, or other water-treatment parameters into pH or pH-Wert unless the same source explicitly says pH or pH-Wert.',
|
||||||
'- For pH requests, do not treat pH operating ranges, reagent/indicator pH values, output-transfer references, or generic pH mentions as proof that a device measures pH.',
|
|
||||||
'- When shop results are present and relevant, include current price and the actual URL if available.',
|
'- When shop results are present and relevant, include current price and the actual URL if available.',
|
||||||
'- If the shop data does not provide a positive price for a result, do not output any price for that result.',
|
'- If the shop data does not provide a positive price for a result, do not output any price for that result.',
|
||||||
'- Do not let accessories, bundles, or service items override a technically better product match unless the user explicitly asks for them.',
|
'- Do not let accessories, bundles, or service items override a technically better product match unless the user explicitly asks for them.',
|
||||||
@@ -638,7 +631,6 @@ final class PromptBuilderConfig
|
|||||||
return $this->getStringList('fact_grounding.technical_rules', [
|
return $this->getStringList('fact_grounding.technical_rules', [
|
||||||
'- For technical product questions, answer primarily with explicitly stated facts.',
|
'- For technical product questions, answer primarily with explicitly stated facts.',
|
||||||
'- For measurement-parameter questions, do not treat similar or neighboring abbreviations as equivalent. In particular, p-Wert is not pH-Wert unless the source explicitly says pH or pH-Wert.',
|
'- For measurement-parameter questions, do not treat similar or neighboring abbreviations as equivalent. In particular, p-Wert is not pH-Wert unless the source explicitly says pH or pH-Wert.',
|
||||||
'- For pH requests, do not present products as pH-capable when the source only states an allowable pH operating range, a pH value of a reagent/indicator solution, a 4-20 mA transfer/output reference, or a generic pH mention.',
|
|
||||||
'- Do not invent or infer measurement principles, methods, calibration functions, benefits, advantages, application areas, or alternative products from product family names, search rank, or shop query wording.',
|
'- Do not invent or infer measurement principles, methods, calibration functions, benefits, advantages, application areas, or alternative products from product family names, search rank, or shop query wording.',
|
||||||
'- Behave like a technical documentation assistant, not like a sales advisor.',
|
'- Behave like a technical documentation assistant, not like a sales advisor.',
|
||||||
'- Keep interpretations minimal and do not generalize application areas beyond the provided sources.',
|
'- Keep interpretations minimal and do not generalize application areas beyond the provided sources.',
|
||||||
@@ -767,41 +759,6 @@ final class PromptBuilderConfig
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getProductRoleGuardSectionLabel(): string
|
|
||||||
{
|
|
||||||
return $this->getString('sections.product_role_guard_label', 'PRODUCT ROLE GUARD');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return string[]
|
|
||||||
*/
|
|
||||||
public function getProductRoleGuardMainDeviceRules(): array
|
|
||||||
{
|
|
||||||
return $this->getStringList('role_guard.main_device_answer_rules', [
|
|
||||||
'- If the user asks for a Testomat, measuring device, analyzer, system, or main device, the answer must be anchored on a compatible main-device record or on a clear no-match statement.',
|
|
||||||
'- Accessories, indicators, reagents, calibration solutions, spare parts, kits, sets, services, or consumables must not be used as the main answer heading for a main-device request.',
|
|
||||||
'- If only accessories or consumables are available in the shop results, start with a negative main-device finding and mention that only accessory/consumable hits were found.',
|
|
||||||
'- Do not provide price, availability, product number, URL, or a recommendation for role-incompatible accessory records unless the user explicitly asks for that accessory.',
|
|
||||||
'- Do not propose alternative devices unless a provided source explicitly proves that exact device is suitable for the requested parameter.',
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getProductRoleGuardNoMainDeviceTemplate(): string
|
|
||||||
{
|
|
||||||
return $this->getString(
|
|
||||||
'role_guard.no_main_device_match_template',
|
|
||||||
'No compatible main-device shop record is present for the requested main-device role. Accessory/consumable records must not be presented as the requested solution.'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getProductRoleGuardIncompatibleRecordNote(): string
|
|
||||||
{
|
|
||||||
return $this->getString(
|
|
||||||
'shop_results.fields.role_incompatible_record_note',
|
|
||||||
'Role guard: this is an accessory/consumable record for a main-device request. Do not use it as an answer heading, recommendation, or suitable main-device result.'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return string[]
|
* @return string[]
|
||||||
*/
|
*/
|
||||||
@@ -862,7 +819,6 @@ final class PromptBuilderConfig
|
|||||||
'- This block is generated from the current user question and is stricter than broad product-selection wording.',
|
'- 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.',
|
'- 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.',
|
'- Similar water-treatment parameters, abbreviations, units, product families, search queries, or ranking positions are not enough.',
|
||||||
'- For pH requests, pH operating ranges, pH values of reagents/indicators, pH transfer/output fields, and general pH mentions are not evidence that a device measures pH.',
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -896,10 +852,15 @@ final class PromptBuilderConfig
|
|||||||
'label' => $label,
|
'label' => $label,
|
||||||
'request_terms' => $this->normalizeMixedStringList($item['request_terms'] ?? []),
|
'request_terms' => $this->normalizeMixedStringList($item['request_terms'] ?? []),
|
||||||
'positive_terms' => $this->normalizeMixedStringList($item['positive_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'] ?? []),
|
'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'])
|
'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'])
|
? 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'])
|
||||||
|
: '',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ use Symfony\Component\HttpFoundation\Request;
|
|||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||||
use Symfony\Component\Routing\Annotation\Route;
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
final readonly class AskSseController
|
final readonly class AskSseController
|
||||||
{
|
{
|
||||||
@@ -70,7 +71,7 @@ final readonly class AskSseController
|
|||||||
'createdAt' => $now,
|
'createdAt' => $now,
|
||||||
'updatedAt' => $now,
|
'updatedAt' => $now,
|
||||||
]);
|
]);
|
||||||
} catch (\Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
return new JsonResponse(
|
return new JsonResponse(
|
||||||
['error' => 'Stream job could not be created: ' . $this->formatThrowableForClient($e)],
|
['error' => 'Stream job could not be created: ' . $this->formatThrowableForClient($e)],
|
||||||
Response::HTTP_INTERNAL_SERVER_ERROR
|
Response::HTTP_INTERNAL_SERVER_ERROR
|
||||||
@@ -86,7 +87,7 @@ final readonly class AskSseController
|
|||||||
return $response;
|
return $response;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Route('/ask-jobs/{jobId}', name: 'ask_job_status', methods: ['GET'], requirements: ['jobId' => '[a-f0-9]{48}'])]
|
#[Route('/ask-jobs/{jobId}', name: 'ask_job_status', requirements: ['jobId' => '[a-f0-9]{48}'], methods: ['GET'])]
|
||||||
public function jobStatus(string $jobId): JsonResponse
|
public function jobStatus(string $jobId): JsonResponse
|
||||||
{
|
{
|
||||||
$job = $this->readJob($jobId);
|
$job = $this->readJob($jobId);
|
||||||
@@ -233,7 +234,7 @@ final readonly class AskSseController
|
|||||||
$eventId = $this->appendJobOutput($jobId, $chunk);
|
$eventId = $this->appendJobOutput($jobId, $chunk);
|
||||||
$this->sendData($chunk, $eventId);
|
$this->sendData($chunk, $eventId);
|
||||||
}
|
}
|
||||||
} catch (\Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
$message = 'Stream abgebrochen: ' . $this->formatThrowableForClient($e);
|
$message = 'Stream abgebrochen: ' . $this->formatThrowableForClient($e);
|
||||||
$this->markJobStatus($jobId, self::JOB_STATUS_FAILED, $message);
|
$this->markJobStatus($jobId, self::JOB_STATUS_FAILED, $message);
|
||||||
$this->sendEvent(
|
$this->sendEvent(
|
||||||
@@ -267,7 +268,7 @@ final readonly class AskSseController
|
|||||||
$prompt,
|
$prompt,
|
||||||
'Systemhinweis: Antwort konnte nicht abgeschlossen werden. Ursache: ' . $message
|
'Systemhinweis: Antwort konnte nicht abgeschlossen werden. Ursache: ' . $message
|
||||||
);
|
);
|
||||||
} catch (\Throwable) {
|
} catch (Throwable) {
|
||||||
// History persistence must never break the SSE error response.
|
// History persistence must never break the SSE error response.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -318,7 +319,7 @@ final readonly class AskSseController
|
|||||||
return trim($contextHint);
|
return trim($contextHint);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function formatThrowableForClient(\Throwable $e): string
|
private function formatThrowableForClient(Throwable $e): string
|
||||||
{
|
{
|
||||||
$message = trim($e->getMessage());
|
$message = trim($e->getMessage());
|
||||||
|
|
||||||
@@ -412,6 +413,7 @@ final readonly class AskSseController
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<string, mixed> $payload
|
* @param array<string, mixed> $payload
|
||||||
|
* @throws \JsonException
|
||||||
*/
|
*/
|
||||||
private function writeJob(string $jobId, array $payload): void
|
private function writeJob(string $jobId, array $payload): void
|
||||||
{
|
{
|
||||||
@@ -518,7 +520,7 @@ final readonly class AskSseController
|
|||||||
'result' => ['ok' => true],
|
'result' => ['ok' => true],
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
} catch (\Throwable) {
|
} catch (Throwable) {
|
||||||
// Job status persistence must never break the already-running stream.
|
// Job status persistence must never break the already-running stream.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -581,9 +583,8 @@ final readonly class AskSseController
|
|||||||
flock($handle, LOCK_UN);
|
flock($handle, LOCK_UN);
|
||||||
|
|
||||||
return $result;
|
return $result;
|
||||||
} catch (\Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
@flock($handle, LOCK_UN);
|
@flock($handle, LOCK_UN);
|
||||||
throw $e;
|
|
||||||
} finally {
|
} finally {
|
||||||
if (is_resource($handle)) {
|
if (is_resource($handle)) {
|
||||||
@fclose($handle);
|
@fclose($handle);
|
||||||
@@ -785,7 +786,7 @@ final readonly class AskSseController
|
|||||||
if (file_put_contents($this->jobOutputPath($jobId), $line, FILE_APPEND | LOCK_EX) === false) {
|
if (file_put_contents($this->jobOutputPath($jobId), $line, FILE_APPEND | LOCK_EX) === false) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
} catch (\Throwable) {
|
} catch (Throwable) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ final class ShopwareCriteriaBuilder
|
|||||||
query: $query,
|
query: $query,
|
||||||
limit: $limit,
|
limit: $limit,
|
||||||
grouping: $grouping,
|
grouping: $grouping,
|
||||||
includeRichTextFields: false,
|
includeRichTextFields: true,
|
||||||
includeMediaFields: false,
|
includeMediaFields: false,
|
||||||
includeSeoUrls: true,
|
includeSeoUrls: true,
|
||||||
includeManufacturer: true
|
includeManufacturer: true
|
||||||
|
|||||||
Reference in New Issue
Block a user