p38
This commit is contained in:
@@ -0,0 +1,36 @@
|
|||||||
|
# RetrieX Patch p38b - Language Cleanup Required Term Hotfix
|
||||||
|
|
||||||
|
## Zweck
|
||||||
|
|
||||||
|
Kleiner Hotfix auf p38. Die Config-/Regression-Validierung erwartet den Pflichtbegriff `dieser` in den effektiven Stopword-Listen der Cleanup-Profile:
|
||||||
|
|
||||||
|
- `commerce_query`
|
||||||
|
- `rag_evidence`
|
||||||
|
- `shop_context_fallback`
|
||||||
|
|
||||||
|
p38 hatte die YAML-Externalisierung korrekt umgesetzt, aber diesen Guardrail-Begriff nicht in einer von allen drei Profilen genutzten Stopword-Gruppe bereitgestellt.
|
||||||
|
|
||||||
|
## Änderung
|
||||||
|
|
||||||
|
`config/retriex/language.yaml`
|
||||||
|
|
||||||
|
- `dieser` wurde in `stopword_groups.de_core` ergänzt.
|
||||||
|
- Dadurch ist der Begriff automatisch in allen drei betroffenen Profilen enthalten, weil sie `de_core` referenzieren.
|
||||||
|
|
||||||
|
## Nicht geändert
|
||||||
|
|
||||||
|
- Keine PHP-Runtime-Logik.
|
||||||
|
- Keine neuen PHP-Core-Listen.
|
||||||
|
- Keine Retrieval-/Ranking-/Prompt-Logik.
|
||||||
|
- Keine fachliche Sonderlogik.
|
||||||
|
|
||||||
|
## Prüfhinweis
|
||||||
|
|
||||||
|
Nach Einspielen erneut ausführen:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bin/console mto:agent:config:validate
|
||||||
|
bin/console mto:agent:regression:test
|
||||||
|
bin/console mto:agent:config:audit-source --details
|
||||||
|
bin/console mto:agent:config:audit-patterns --details
|
||||||
|
```
|
||||||
123
RETRIEX_PATCH_38_YAML_EXTERNALIZATION_HARDENING_README.md
Normal file
123
RETRIEX_PATCH_38_YAML_EXTERNALIZATION_HARDENING_README.md
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
# RetrieX Patch p38 - YAML Externalization Hardening
|
||||||
|
|
||||||
|
## Ziel
|
||||||
|
|
||||||
|
Dieser Patch schliesst die nach p37 verbliebenen sinnvollen YAML-Externalization-Luecken im PHP-Core. Fokus ist nicht technische Infrastruktur, sondern fachliche, sprachliche und UI-nahe Listen, Tokens, Vergleichswerte, Prompt-Regeln und Normalisierungsregeln.
|
||||||
|
|
||||||
|
Die Leitlinie bleibt:
|
||||||
|
|
||||||
|
- keine neuen Token-/Stringlisten im PHP-Core
|
||||||
|
- fachliche, sprachliche und UI-nahe Werte ueber YAML
|
||||||
|
- bestehende 1.5.3-Logik nicht fachlich veraendern
|
||||||
|
- keine neuen Spezialfaelle fuer konkrete Produkte
|
||||||
|
|
||||||
|
## Geaenderte Dateien
|
||||||
|
|
||||||
|
- `config/retriex/agent.yaml`
|
||||||
|
- `config/retriex/language.yaml`
|
||||||
|
- `config/retriex/prompt.yaml`
|
||||||
|
- `src/Config/AgentRunnerConfig.php`
|
||||||
|
- `src/Config/LanguageCleanupConfig.php`
|
||||||
|
- `src/Config/PromptBuilderConfig.php`
|
||||||
|
- `src/Agent/AgentRunner.php`
|
||||||
|
- `src/Agent/PromptBuilder.php`
|
||||||
|
- `src/Knowledge/Retrieval/QueryCleaner.php`
|
||||||
|
- `src/Knowledge/Retrieval/NdjsonLexicalIndexBuilder.php`
|
||||||
|
- `src/Knowledge/Retrieval/NdjsonKeywordRetriever.php`
|
||||||
|
- `src/Knowledge/Retrieval/NdjsonChunkLookup.php`
|
||||||
|
- `src/Knowledge/Retrieval/NdjsonHybridRetriever.php`
|
||||||
|
|
||||||
|
## Inhalt
|
||||||
|
|
||||||
|
### AgentRunner
|
||||||
|
|
||||||
|
Externalisiert wurden insbesondere:
|
||||||
|
|
||||||
|
- Placeholder-Ausgaben der Query-/Input-Normalisierung
|
||||||
|
- Shop-Repair-/Shop-Query-Heartbeat-Meldungen
|
||||||
|
- No-LLM-Produktfeldtexte und Produktzeilen-Templates
|
||||||
|
- Follow-up-Kontextlabels
|
||||||
|
- Production-UI-Stage-Labels
|
||||||
|
- Production-UI-Confidence-Labels
|
||||||
|
- Production-UI-Texte, Templates und Shop-Meta-Labels
|
||||||
|
- Follow-up-Action-Chips fuer Commerce und Knowledge
|
||||||
|
|
||||||
|
Zusaetzlich nutzt der Runner vorhandene Language-Cleanup-Konfiguration fuer ASCII-Transliteration, Separator-Normalisierung und Dash-Normalisierung.
|
||||||
|
|
||||||
|
### Language Cleanup
|
||||||
|
|
||||||
|
`language.yaml` enthaelt nun zusaetzlich zentrale Normalisierungsregeln fuer:
|
||||||
|
|
||||||
|
- Wortseparatoren (`-`, `/`, `_`)
|
||||||
|
- Unicode-Dash-Aequivalente
|
||||||
|
|
||||||
|
`LanguageCleanupConfig` stellt dafuer zentrale Methoden bereit:
|
||||||
|
|
||||||
|
- `replaceWordSeparatorsWithSpace()`
|
||||||
|
- `normalizeDashEquivalents()`
|
||||||
|
|
||||||
|
### PromptBuilder
|
||||||
|
|
||||||
|
Externalisiert wurden verbliebene Prompt-/Policy-Regeln rund um:
|
||||||
|
|
||||||
|
- Fallback-Escalation bei vorhandenen Shop-Ergebnissen
|
||||||
|
- Measurement-Evidence-Guard-Templates
|
||||||
|
- finale Measurement-Evidence-Regeln
|
||||||
|
- Parameter-Split-Pattern
|
||||||
|
- Parameter-Trim-Zeichen
|
||||||
|
|
||||||
|
Die Unicode-Dash-Normalisierung nutzt nun ebenfalls `LanguageCleanupConfig`.
|
||||||
|
|
||||||
|
### Retrieval-Normalisierung
|
||||||
|
|
||||||
|
Wiederholte Separatorlisten in Retrieval-Klassen wurden durch die zentrale Language-Cleanup-Methode ersetzt.
|
||||||
|
|
||||||
|
Betroffen:
|
||||||
|
|
||||||
|
- `QueryCleaner`
|
||||||
|
- `NdjsonLexicalIndexBuilder`
|
||||||
|
- `NdjsonKeywordRetriever`
|
||||||
|
- `NdjsonChunkLookup`
|
||||||
|
- `NdjsonHybridRetriever`
|
||||||
|
|
||||||
|
## Bewusst nicht externalisiert
|
||||||
|
|
||||||
|
Nicht Ziel dieses Patches waren rein technische Infrastrukturwerte, z. B.:
|
||||||
|
|
||||||
|
- Boolean-Parser-Werte wie `true`, `false`, `yes`, `no`, `on`, `off`
|
||||||
|
- technische Statuscodes und interne State-Keys
|
||||||
|
- DTO-/Array-Schluessel
|
||||||
|
- Log-/HTTP-/Encoding-Infrastruktur
|
||||||
|
- Zeilenumbruch-Normalisierung
|
||||||
|
|
||||||
|
Diese Werte sind keine fachlichen oder sprachlichen Matching-/Prompt-/UI-Listen.
|
||||||
|
|
||||||
|
## Lokale Validierung
|
||||||
|
|
||||||
|
Da im ZIP kein `vendor/` enthalten ist, konnten Symfony-Console-Checks nicht direkt ausgefuehrt werden.
|
||||||
|
|
||||||
|
Durchgefuehrt wurden:
|
||||||
|
|
||||||
|
- `php -l` fuer alle geaenderten PHP-Dateien: OK
|
||||||
|
- YAML-Parse fuer geaenderte YAML-Dateien: OK
|
||||||
|
- statische Greps auf die zuvor identifizierten Reststellen: OK
|
||||||
|
|
||||||
|
Gepruefte ehemalige Problemklassen:
|
||||||
|
|
||||||
|
- keine PHP-Funde mehr fuer `normalized user input` / `corrected user input`
|
||||||
|
- keine PHP-Funde mehr fuer die harten Measurement-Evidence-Regeltexte
|
||||||
|
- keine PHP-Funde mehr fuer die alten Separatorlisten `['-', '/', '_']`
|
||||||
|
- keine PHP-Funde mehr fuer die alten Unicode-Dash-Listen
|
||||||
|
- keine PHP-Funde mehr fuer die AgentRunner-UI-Labels wie `RetrieX-Status`, `Datenbasis`, `Folgeaktionen`, `Artikelnummer`, `Preis`, `Verfuegbarkeit`, `Relevanz`
|
||||||
|
|
||||||
|
## Empfohlene Checks nach Einspielen
|
||||||
|
|
||||||
|
Bitte im echten Projekt mit installiertem `vendor/` ausfuehren:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bin/console mto:agent:config:validate
|
||||||
|
bin/console mto:agent:regression:test
|
||||||
|
bin/console mto:agent:config:audit-source --details
|
||||||
|
bin/console mto:agent:config:audit-patterns --details
|
||||||
|
```
|
||||||
|
|
||||||
@@ -16,6 +16,12 @@ parameters:
|
|||||||
max_length_ratio_percent: 150
|
max_length_ratio_percent: 150
|
||||||
heartbeat_message: 'Ich optimiere die Anfrage…'
|
heartbeat_message: 'Ich optimiere die Anfrage…'
|
||||||
output_prefix_pattern: '/^(?:normalisiert|korrigiert|corrected|normalized)\s*:\s*/iu'
|
output_prefix_pattern: '/^(?:normalisiert|korrigiert|corrected|normalized)\s*:\s*/iu'
|
||||||
|
placeholder_outputs:
|
||||||
|
- 'normalized user input'
|
||||||
|
- 'corrected user input'
|
||||||
|
- 'user input'
|
||||||
|
- 'normalisierte nutzereingabe'
|
||||||
|
- 'korrigierte nutzereingabe'
|
||||||
skip_patterns:
|
skip_patterns:
|
||||||
- '/https?:\/\//iu'
|
- '/https?:\/\//iu'
|
||||||
- '/\bwww\./iu'
|
- '/\bwww\./iu'
|
||||||
@@ -188,6 +194,10 @@ parameters:
|
|||||||
history_question_pattern: '/^Question:\s*(.+)$/mi'
|
history_question_pattern: '/^Question:\s*(.+)$/mi'
|
||||||
history_turn_split_pattern: '/(?=^Question:\s)/m'
|
history_turn_split_pattern: '/(?=^Question:\s)/m'
|
||||||
history_question_strip_pattern: '/^Question:\s*.*(?:\R|$)/u'
|
history_question_strip_pattern: '/^Question:\s*.*(?:\R|$)/u'
|
||||||
|
context_labels:
|
||||||
|
previous_user_question_template: 'Vorherige Nutzerfrage: {question}'
|
||||||
|
previous_reference_anchors_template: 'Vorherige technische Referenzanker (nur zur Referenzauflösung, keine Faktenquelle): {anchors}'
|
||||||
|
current_follow_up_question_template: 'Aktuelle Folgefrage: {question}'
|
||||||
reference_anchor:
|
reference_anchor:
|
||||||
testomat_model_pattern: '/\bTestomat(?:®)?\s+(?:\d{3,4}(?:\s+[A-Z]{2,8})?|EVO(?:\s+[A-Z]{2,6})?|ECO(?:[-\s]?(?:PLUS|C))?|DUO(?:\s+\d{3,4})?|LAB(?:\s+[A-Z]{2,6})?)\b/iu'
|
testomat_model_pattern: '/\bTestomat(?:®)?\s+(?:\d{3,4}(?:\s+[A-Z]{2,8})?|EVO(?:\s+[A-Z]{2,6})?|ECO(?:[-\s]?(?:PLUS|C))?|DUO(?:\s+\d{3,4})?|LAB(?:\s+[A-Z]{2,6})?)\b/iu'
|
||||||
hardness_value_pattern: '/\b\d+(?:[,.]\d+)?\s*°\s*dH\b/iu'
|
hardness_value_pattern: '/\b\d+(?:[,.]\d+)?\s*°\s*dH\b/iu'
|
||||||
@@ -203,6 +213,8 @@ parameters:
|
|||||||
analyze_all_information: 'Ich analysiere alle Informationen...'
|
analyze_all_information: 'Ich analysiere alle Informationen...'
|
||||||
thinking_while_streaming: 'Denke nach...'
|
thinking_while_streaming: 'Denke nach...'
|
||||||
no_llm_data_received: '❌ Es wurden keine Daten vom LLM empfangen.'
|
no_llm_data_received: '❌ Es wurden keine Daten vom LLM empfangen.'
|
||||||
|
shop_repair_check: 'Erweiterte Shopsuche wird geprüft…'
|
||||||
|
shop_query_optimization_heartbeat: 'Shop-Suchanfrage wird optimiert…'
|
||||||
generic_internal_error: '❌ Bei der Verarbeitung der Anfrage ist ein interner Fehler aufgetreten.'
|
generic_internal_error: '❌ Bei der Verarbeitung der Anfrage ist ein interner Fehler aufgetreten.'
|
||||||
debug_internal_error_prefix: '❌ Interner Fehler: '
|
debug_internal_error_prefix: '❌ Interner Fehler: '
|
||||||
|
|
||||||
@@ -306,6 +318,21 @@ parameters:
|
|||||||
no_shop_results_no_knowledge: 'Ich finde weder belastbares RAG-Wissen noch passende Shop-Treffer zur aktuellen Suchanfrage. Das ist keine sichere Negativaussage. Bitte nenne Produkt, Messparameter oder Zubehör konkreter.'
|
no_shop_results_no_knowledge: 'Ich finde weder belastbares RAG-Wissen noch passende Shop-Treffer zur aktuellen Suchanfrage. Das ist keine sichere Negativaussage. Bitte nenne Produkt, Messparameter oder Zubehör konkreter.'
|
||||||
shop_unavailable_with_knowledge: 'Live-Shopdaten konnten nicht geladen werden. Ich kann keine Aussage zu aktueller Verfügbarkeit, Preis oder Shop-Portfolio treffen. Wenn das RAG-Wissen einen direkten Fachbeleg enthält, wird die fachliche Antwort davon getrennt betrachtet.'
|
shop_unavailable_with_knowledge: 'Live-Shopdaten konnten nicht geladen werden. Ich kann keine Aussage zu aktueller Verfügbarkeit, Preis oder Shop-Portfolio treffen. Wenn das RAG-Wissen einen direkten Fachbeleg enthält, wird die fachliche Antwort davon getrennt betrachtet.'
|
||||||
shop_unavailable_no_knowledge: 'Live-Shopdaten konnten nicht geladen werden und die RAG-Treffer enthalten keinen direkten Fachbeleg zur Anfrage. Ich kann daraus keine verlässliche Produkt-, Verfügbarkeits- oder Portfolioaussage ableiten.'
|
shop_unavailable_no_knowledge: 'Live-Shopdaten konnten nicht geladen werden und die RAG-Treffer enthalten keinen direkten Fachbeleg zur Anfrage. Ich kann daraus keine verlässliche Produkt-, Verfügbarkeits- oder Portfolioaussage ableiten.'
|
||||||
|
product_fields:
|
||||||
|
unreadable_results_message: '- Es wurden Shop-Treffer übergeben, aber keine lesbaren Produktdaten gefunden.'
|
||||||
|
unnamed_product: 'Unbenanntes Shop-Produkt'
|
||||||
|
product_number_template: 'Art.-Nr. {value}'
|
||||||
|
manufacturer_template: 'Hersteller: {value}'
|
||||||
|
price_template: 'Preis: {value}'
|
||||||
|
availability_template: 'Verfügbar: {value}'
|
||||||
|
availability_yes: 'ja'
|
||||||
|
availability_no: 'nein'
|
||||||
|
url_template: 'URL: {value}'
|
||||||
|
incompatible_role_note: 'Hinweis: Zubehör/Verbrauchsartikel; nicht als Messanlage/Gerät bestätigt'
|
||||||
|
line_template: '{index}. {parts}'
|
||||||
|
separator: ' | '
|
||||||
|
unavailable_reason_template: '{message} Ursache: {reason}'
|
||||||
|
|
||||||
product_roles:
|
product_roles:
|
||||||
main_device_request_keywords:
|
main_device_request_keywords:
|
||||||
- anlage
|
- anlage
|
||||||
@@ -354,6 +381,125 @@ parameters:
|
|||||||
- 100ml
|
- 100ml
|
||||||
- 500ml
|
- 500ml
|
||||||
|
|
||||||
|
|
||||||
|
production_ui:
|
||||||
|
stage_labels:
|
||||||
|
preparing_answer: 'Antwort wird vorbereitet'
|
||||||
|
shop_routing_detected: 'Shop-Routing erkannt'
|
||||||
|
rag_searched: 'RAG-Wissen wurde durchsucht'
|
||||||
|
shop_search_preparing: 'Shop-Suche wird vorbereitet'
|
||||||
|
more_context_needed: 'Mehr Kontext nötig'
|
||||||
|
shop_search_running: 'Shop wird durchsucht'
|
||||||
|
shop_unavailable: 'Shopdaten nicht verfügbar'
|
||||||
|
shop_completed: 'Shop-Suche abgeschlossen'
|
||||||
|
answer_generating: 'Antwort wird generiert'
|
||||||
|
completed: 'Abgeschlossen'
|
||||||
|
interrupted: 'Antwort wurde unterbrochen'
|
||||||
|
confidence_labels:
|
||||||
|
checking_evidence: 'Beleglage wird geprüft'
|
||||||
|
checking_shop_data: 'Shopdaten werden geprüft'
|
||||||
|
more_context_needed: 'mehr Kontext nötig'
|
||||||
|
interrupted: 'nicht abgeschlossen'
|
||||||
|
direct: 'fachlich belegt'
|
||||||
|
aggregate_missing: 'geprüfte Quellen, keine passende Zählinformation'
|
||||||
|
weak: 'RAG-Näherungstreffer, kein direkter Fachbeleg'
|
||||||
|
default: 'noch keine belastbaren Treffer'
|
||||||
|
direct_shop_check: 'fachlich belegt; Shopdaten werden geprüft'
|
||||||
|
aggregate_missing_shop_check: 'geprüfte Quellen ohne Zählinformation; Shopdaten werden geprüft'
|
||||||
|
weak_shop_check: 'RAG-Näherungstreffer; Shopdaten werden geprüft'
|
||||||
|
default_shop_check: 'Shopdaten werden geprüft'
|
||||||
|
aggregate_missing_shop_unavailable: 'geprüfte Quellen ohne Zählinformation; Shopdaten nicht verfügbar'
|
||||||
|
aggregate_missing_no_count: 'geprüfte Quellen, keine passende Zählinformation'
|
||||||
|
shop_unavailable_with_knowledge: 'fachlich belegt; Shopdaten nicht verfügbar'
|
||||||
|
shop_unavailable: 'Shopdaten nicht verfügbar'
|
||||||
|
rag_and_shop: 'RAG + Shopdaten'
|
||||||
|
shop_only: 'nur Shopdaten'
|
||||||
|
rag_no_shop_hits: 'RAG-Wissen, keine Shop-Treffer'
|
||||||
|
no_reliable_data: 'keine belastbaren Daten'
|
||||||
|
no_reliable_hits: 'noch keine belastbaren Treffer'
|
||||||
|
text:
|
||||||
|
live_shop_source_plain_label: 'Live-Shopdaten'
|
||||||
|
run_status_eyebrow: 'RetrieX-Status'
|
||||||
|
evidence_prefix: 'Beleglage: '
|
||||||
|
data_basis_label: 'Datenbasis'
|
||||||
|
data_basis_empty_completed: 'keine belastbare Datenbasis'
|
||||||
|
data_basis_empty_running: 'wird geprüft'
|
||||||
|
rag_hits_checking: 'RAG-Treffer: wird geprüft'
|
||||||
|
shop_hits_loading: 'Shop-Treffer: wird geladen'
|
||||||
|
shop_hits_unavailable: 'Shop-Treffer: nicht verfügbar'
|
||||||
|
shop_hits_no_query: 'Shop-Treffer: keine Suchquery'
|
||||||
|
shop_hits_not_requested: 'Shop-Treffer: nicht angefragt'
|
||||||
|
status_completed: 'Status: abgeschlossen'
|
||||||
|
status_running: 'Status: läuft'
|
||||||
|
shop_results_eyebrow: 'Shop-Ergebnisse'
|
||||||
|
shop_results_title: 'Shop-Ergebnisse'
|
||||||
|
evaluated_query_label: 'Ausgewertete Suchquery'
|
||||||
|
unnamed_product: 'Unbenanntes Produkt'
|
||||||
|
field_not_provided: 'nicht übermittelt'
|
||||||
|
product_number_label: 'Artikelnummer'
|
||||||
|
price_label: 'Preis'
|
||||||
|
availability_label: 'Verfügbarkeit'
|
||||||
|
manufacturer_label: 'Hersteller'
|
||||||
|
relevance_label: 'Relevanz'
|
||||||
|
availability_yes: 'verfügbar'
|
||||||
|
availability_no: 'nicht verfügbar'
|
||||||
|
availability_unknown: 'Shopstatus nicht übermittelt'
|
||||||
|
followup_eyebrow: 'Folgeaktionen'
|
||||||
|
followup_title: 'Was möchtest du als Nächstes tun?'
|
||||||
|
shop_meta_fallback_query: 'keine Suchquery ermittelt'
|
||||||
|
shop_meta_query_mode_optimized: 'optimiert'
|
||||||
|
shop_meta_query_mode_direct: 'direkt'
|
||||||
|
shop_meta_default_intent: 'commerce'
|
||||||
|
shop_meta_title_unavailable: 'Shopdaten nicht verfügbar'
|
||||||
|
shop_meta_title_completed: 'Shop-Suche abgeschlossen'
|
||||||
|
shop_meta_title_running: 'Shop-Suche wird ausgeführt'
|
||||||
|
shop_meta_status_completed: 'Status: abgeschlossen'
|
||||||
|
shop_meta_status_running: 'Status: läuft'
|
||||||
|
shop_meta_result_unavailable: 'Shoptreffer: nicht verfügbar'
|
||||||
|
shop_meta_result_loading: 'Shoptreffer: wird geladen'
|
||||||
|
shop_meta_repair_used: 'Erweiterte Suche: genutzt'
|
||||||
|
shop_meta_repair_checked: 'Erweiterte Suche: geprüft'
|
||||||
|
shop_meta_eyebrow: 'Shop-Suche'
|
||||||
|
shop_meta_query_label: 'Gesendete Suchquery'
|
||||||
|
shop_meta_query_prefix: 'Query: '
|
||||||
|
shop_meta_intent_prefix: 'Intent: '
|
||||||
|
shop_unavailable_default_reason: 'Keine Detailmeldung vom Shopware-Server.'
|
||||||
|
shop_unavailable_title: 'Shopdaten konnten nicht geladen werden'
|
||||||
|
shop_unavailable_text_prefix: 'RetrieX antwortet ohne Live-Shopdaten weiter. Ursache: '
|
||||||
|
no_llm_history_default: 'Es wurden keine Daten vom LLM empfangen.'
|
||||||
|
history_notice_default_title: 'Systemhinweis'
|
||||||
|
history_notice_shop_unavailable_title: 'Shopdaten konnten nicht geladen werden'
|
||||||
|
history_notice_answer_incomplete_title: 'Antwort konnte nicht abgeschlossen werden'
|
||||||
|
templates:
|
||||||
|
rag_hits_count: 'RAG-Treffer: {count}'
|
||||||
|
shop_hits_count: 'Shop-Treffer: {count}'
|
||||||
|
shop_results_summary: '{count} Shop-Treffer ausgewertet'
|
||||||
|
shop_results_top_displayed_suffix: ' · Top {max} angezeigt'
|
||||||
|
shop_results_repair_suffix: ' · erweiterte Shopsuche genutzt'
|
||||||
|
relevance_matched_queries: 'Gefunden über: {queries}'
|
||||||
|
relevance_highlight: 'Passender Shop-Hinweis: {highlight}'
|
||||||
|
relevance_match_source: 'Trefferquelle: {source}'
|
||||||
|
relevance_query: 'Passend zur Suchquery: {query}'
|
||||||
|
relevance_default: 'Aus den Live-Shopdaten übernommen'
|
||||||
|
shop_meta_result_count: 'Shoptreffer: {count}'
|
||||||
|
history_notice_without_detail: 'Systemhinweis: {title}.'
|
||||||
|
history_notice_with_detail: 'Systemhinweis: {title}. Ursache: {detail}'
|
||||||
|
shop_results:
|
||||||
|
max_cards: 5
|
||||||
|
follow_up_actions:
|
||||||
|
commerce:
|
||||||
|
- label: 'Im Shop suchen'
|
||||||
|
prompt: 'Suche die aktuelle Produktauswahl im Shop.'
|
||||||
|
- label: 'Nur Zubehör anzeigen'
|
||||||
|
prompt: 'Zeige aus der aktuellen Produktauswahl nur Zubehör.'
|
||||||
|
- label: 'Nur Geräte anzeigen'
|
||||||
|
prompt: 'Zeige aus der aktuellen Produktauswahl nur Geräte.'
|
||||||
|
- label: 'Preis anzeigen'
|
||||||
|
prompt: 'Zeige mir die Preise der aktuell relevanten Produkte.'
|
||||||
|
knowledge:
|
||||||
|
- label: 'Technische Details anzeigen'
|
||||||
|
prompt: 'Zeige technische Details zur aktuellen Antwort.'
|
||||||
|
|
||||||
source_labels:
|
source_labels:
|
||||||
external_url: 'Externe URL'
|
external_url: 'Externe URL'
|
||||||
rag_knowledge: 'RAG Wissen'
|
rag_knowledge: 'RAG Wissen'
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ parameters:
|
|||||||
words:
|
words:
|
||||||
- mit
|
- mit
|
||||||
- der
|
- der
|
||||||
- dieser
|
|
||||||
- die
|
- die
|
||||||
- das
|
- das
|
||||||
- ein
|
- ein
|
||||||
@@ -80,11 +79,20 @@ parameters:
|
|||||||
ö: oe
|
ö: oe
|
||||||
ü: ue
|
ü: ue
|
||||||
ß: ss
|
ß: ss
|
||||||
|
word_separator_chars:
|
||||||
|
- '-'
|
||||||
|
- '/'
|
||||||
|
- '_'
|
||||||
|
dash_equivalents:
|
||||||
|
- '‐'
|
||||||
|
- '‑'
|
||||||
|
- '‒'
|
||||||
|
- '–'
|
||||||
|
- '—'
|
||||||
|
|
||||||
stopword_groups:
|
stopword_groups:
|
||||||
de_core:
|
de_core:
|
||||||
- der
|
- der
|
||||||
- dieser
|
|
||||||
- die
|
- die
|
||||||
- das
|
- das
|
||||||
- den
|
- den
|
||||||
@@ -94,6 +102,7 @@ parameters:
|
|||||||
- eine
|
- eine
|
||||||
- einer
|
- einer
|
||||||
- eines
|
- eines
|
||||||
|
- dieser
|
||||||
- einen
|
- einen
|
||||||
- einem
|
- einem
|
||||||
- und
|
- und
|
||||||
|
|||||||
@@ -256,6 +256,29 @@ parameters:
|
|||||||
- stoerungsfrei
|
- stoerungsfrei
|
||||||
generic_safe_no_evidence_answer_template_de: Ich finde in den bereitgestellten Quellen keinen sicher belegten Treffer für die Messung von {label}.
|
generic_safe_no_evidence_answer_template_de: Ich finde in den bereitgestellten Quellen keinen sicher belegten Treffer für die Messung von {label}.
|
||||||
generic_safe_no_accessory_evidence_answer_template_de: Ich finde in den bereitgestellten Quellen keinen sicher belegten Indikator oder ein Reagenz für die Messung von {label}.
|
generic_safe_no_accessory_evidence_answer_template_de: Ich finde in den bereitgestellten Quellen keinen sicher belegten Indikator oder ein Reagenz für die Messung von {label}.
|
||||||
|
rule_templates:
|
||||||
|
shop_positive_evidence: '- Shop record {index} ({product}): explicit positive evidence for {label} is present in this same record.'
|
||||||
|
shop_no_evidence: '- No shop product record shown to the model contains explicit positive evidence for {label} in the same record.'
|
||||||
|
unnamed_product: 'unnamed product'
|
||||||
|
default_requested_parameter_label: 'requested measurement parameter'
|
||||||
|
shop_record_positive_evidence_line: 'Requested measurement evidence: explicit positive evidence for {label} is present in this same SHOP PRODUCT RECORD.'
|
||||||
|
shop_record_no_evidence_line: 'Requested measurement evidence: no explicit positive evidence for {label} is present in this SHOP PRODUCT RECORD. Do not present this record as technically suitable for that measurement parameter.'
|
||||||
|
requested_parameter: '- User requested measurement parameter: {label}.'
|
||||||
|
positive_terms: '- Positive parameter terms for this request: {terms}.'
|
||||||
|
positive_context_terms: '- These parameter terms count as suitability evidence only in a measurement-purpose context such as: {terms}.'
|
||||||
|
negative_context_terms: '- These contexts are not suitability evidence by themselves: {terms}.'
|
||||||
|
non_equivalent_terms: '- Terms that must NOT be treated as equivalent positive evidence: {terms}.'
|
||||||
|
rag_url_evidence_scan: '- RAG/URL evidence scan for this exact parameter: {state}.'
|
||||||
|
rag_url_evidence_found: 'explicit positive evidence found.'
|
||||||
|
rag_url_evidence_missing: 'no explicit positive evidence found.'
|
||||||
|
deterministic_scan_no_product_specific_evidence: '- The deterministic exact-term scan did not find product-specific evidence. The answer may still use a clearly equivalent named measurement parameter from the same source record, but must not infer suitability from generic categories, document titles, tags, search terms, neighbouring products, or broad umbrella-topic wording.'
|
||||||
|
mandatory_no_recommendation: '- Mandatory answer behavior: do not recommend a product as suitable for this measurement parameter.'
|
||||||
|
start_answer_meaning: '- Start the answer with this meaning in the user language: {answer}'
|
||||||
|
accessory_mismatch: '- 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.'
|
||||||
|
commercial_hits_only: '- You may list exact shop hits only as commercial/search hits under a heading such as "Shop-Treffer (technische Eignung nicht sicher belegt)".'
|
||||||
|
final_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.'
|
||||||
|
- '- The generated shop search query, search intent, ranking position, and user question are not factual evidence for product suitability.'
|
||||||
parameters:
|
parameters:
|
||||||
- id: ph
|
- id: ph
|
||||||
label: pH / pH-Wert
|
label: pH / pH-Wert
|
||||||
@@ -421,6 +444,7 @@ parameters:
|
|||||||
- '- 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 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.'
|
||||||
|
provided_shop_results_context_rule: '- Treat shop results as provided context only; do not imply that a live shop check was performed in this run.'
|
||||||
without_shop_check_rules:
|
without_shop_check_rules:
|
||||||
- '- If the question is product-related and no live shop check was performed in this run, do not make a portfolio-wide negative statement such as "there is no product".'
|
- '- If the question is product-related and no live shop check was performed in this run, do not make a portfolio-wide negative statement such as "there is no product".'
|
||||||
- '- Phrase missing evidence narrowly, for example: "Im RAG-Wissen finde ich dazu keine belastbare Information."'
|
- '- Phrase missing evidence narrowly, for example: "Im RAG-Wissen finde ich dazu keine belastbare Information."'
|
||||||
@@ -457,6 +481,10 @@ parameters:
|
|||||||
- '- State that live shop data could not be loaded. If retrieved knowledge or URL content contains a direct Fachbeleg, still answer the factual part from that source and clearly separate it from missing shop data.'
|
- '- State that live shop data could not be loaded. If retrieved knowledge or URL content contains a direct Fachbeleg, still answer the factual part from that source and clearly separate it from missing shop data.'
|
||||||
- '- Do not draw negative conclusions about current product availability, price, or shop portfolio while the shop is unavailable.'
|
- '- Do not draw negative conclusions about current product availability, price, or shop portfolio while the shop is unavailable.'
|
||||||
|
|
||||||
|
parameter_parsing:
|
||||||
|
split_pattern: '/\s*(?:,|;|\/|\boder\b|\bund\b|\bor\b|\band\b)\s*/iu'
|
||||||
|
trim_characters: " \t\n\r\0\x0B-–—:()[]{}\"'`“”„"
|
||||||
|
|
||||||
response_format:
|
response_format:
|
||||||
base_rules:
|
base_rules:
|
||||||
- '- Keep normal spacing between all words. Never fuse words together.'
|
- '- Keep normal spacing between all words. Never fuse words together.'
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ final readonly class AgentRunner
|
|||||||
|
|
||||||
yield $this->systemMsg(
|
yield $this->systemMsg(
|
||||||
$this->buildProductionUiMetaMessage(
|
$this->buildProductionUiMetaMessage(
|
||||||
stageLabel: 'Antwort wird vorbereitet',
|
stageLabel: $this->agentRunnerConfig->getProductionUiStageLabel('preparing_answer'),
|
||||||
ragCount: null,
|
ragCount: null,
|
||||||
shopCount: null,
|
shopCount: null,
|
||||||
shopCountMode: $this->resolveShopCountModeForMeta(
|
shopCountMode: $this->resolveShopCountModeForMeta(
|
||||||
@@ -93,7 +93,7 @@ final readonly class AgentRunner
|
|||||||
shopSearchSkippedBecauseNoQuery: $shopSearchSkippedBecauseNoQuery
|
shopSearchSkippedBecauseNoQuery: $shopSearchSkippedBecauseNoQuery
|
||||||
),
|
),
|
||||||
sourceLabels: $sources,
|
sourceLabels: $sources,
|
||||||
confidenceLabel: 'Beleglage wird geprüft'
|
confidenceLabel: $this->agentRunnerConfig->getProductionUiConfidenceLabel('checking_evidence')
|
||||||
),
|
),
|
||||||
'meta'
|
'meta'
|
||||||
);
|
);
|
||||||
@@ -141,7 +141,7 @@ final readonly class AgentRunner
|
|||||||
if ($this->isCommerceIntent($commerceIntent)) {
|
if ($this->isCommerceIntent($commerceIntent)) {
|
||||||
yield $this->systemMsg(
|
yield $this->systemMsg(
|
||||||
$this->buildProductionUiMetaMessage(
|
$this->buildProductionUiMetaMessage(
|
||||||
stageLabel: 'Shop-Routing erkannt',
|
stageLabel: $this->agentRunnerConfig->getProductionUiStageLabel('shop_routing_detected'),
|
||||||
ragCount: null,
|
ragCount: null,
|
||||||
shopCount: null,
|
shopCount: null,
|
||||||
shopCountMode: $this->resolveShopCountModeForMeta(
|
shopCountMode: $this->resolveShopCountModeForMeta(
|
||||||
@@ -151,7 +151,7 @@ final readonly class AgentRunner
|
|||||||
shopSearchSkippedBecauseNoQuery: $shopSearchSkippedBecauseNoQuery
|
shopSearchSkippedBecauseNoQuery: $shopSearchSkippedBecauseNoQuery
|
||||||
),
|
),
|
||||||
sourceLabels: $sources,
|
sourceLabels: $sources,
|
||||||
confidenceLabel: 'Shopdaten werden geprüft'
|
confidenceLabel: $this->agentRunnerConfig->getProductionUiConfidenceLabel('checking_shop_data')
|
||||||
),
|
),
|
||||||
'meta'
|
'meta'
|
||||||
);
|
);
|
||||||
@@ -174,7 +174,7 @@ final readonly class AgentRunner
|
|||||||
|
|
||||||
yield $this->systemMsg(
|
yield $this->systemMsg(
|
||||||
$this->buildProductionUiMetaMessage(
|
$this->buildProductionUiMetaMessage(
|
||||||
stageLabel: 'RAG-Wissen wurde durchsucht',
|
stageLabel: $this->agentRunnerConfig->getProductionUiStageLabel('rag_searched'),
|
||||||
ragCount: count($knowledgeChunks),
|
ragCount: count($knowledgeChunks),
|
||||||
shopCount: null,
|
shopCount: null,
|
||||||
shopCountMode: $this->resolveShopCountModeForMeta(
|
shopCountMode: $this->resolveShopCountModeForMeta(
|
||||||
@@ -202,7 +202,7 @@ final readonly class AgentRunner
|
|||||||
if ($this->isCommerceIntent($commerceIntent)) {
|
if ($this->isCommerceIntent($commerceIntent)) {
|
||||||
yield $this->systemMsg(
|
yield $this->systemMsg(
|
||||||
$this->buildProductionUiMetaMessage(
|
$this->buildProductionUiMetaMessage(
|
||||||
stageLabel: 'Shop-Suche wird vorbereitet',
|
stageLabel: $this->agentRunnerConfig->getProductionUiStageLabel('shop_search_preparing'),
|
||||||
ragCount: count($knowledgeChunks),
|
ragCount: count($knowledgeChunks),
|
||||||
shopCount: null,
|
shopCount: null,
|
||||||
shopCountMode: 'loading',
|
shopCountMode: 'loading',
|
||||||
@@ -308,7 +308,7 @@ final readonly class AgentRunner
|
|||||||
|
|
||||||
yield $this->systemMsg(
|
yield $this->systemMsg(
|
||||||
$this->buildProductionUiMetaMessage(
|
$this->buildProductionUiMetaMessage(
|
||||||
stageLabel: 'Mehr Kontext nötig',
|
stageLabel: $this->agentRunnerConfig->getProductionUiStageLabel('more_context_needed'),
|
||||||
ragCount: count($knowledgeChunks),
|
ragCount: count($knowledgeChunks),
|
||||||
shopCount: null,
|
shopCount: null,
|
||||||
shopCountMode: $this->resolveShopCountModeForMeta(
|
shopCountMode: $this->resolveShopCountModeForMeta(
|
||||||
@@ -318,7 +318,7 @@ final readonly class AgentRunner
|
|||||||
shopSearchSkippedBecauseNoQuery: $shopSearchSkippedBecauseNoQuery
|
shopSearchSkippedBecauseNoQuery: $shopSearchSkippedBecauseNoQuery
|
||||||
),
|
),
|
||||||
sourceLabels: $sources,
|
sourceLabels: $sources,
|
||||||
confidenceLabel: 'mehr Kontext nötig',
|
confidenceLabel: $this->agentRunnerConfig->getProductionUiConfidenceLabel('more_context_needed'),
|
||||||
completed: true
|
completed: true
|
||||||
),
|
),
|
||||||
'meta'
|
'meta'
|
||||||
@@ -370,7 +370,7 @@ final readonly class AgentRunner
|
|||||||
|
|
||||||
yield $this->systemMsg(
|
yield $this->systemMsg(
|
||||||
$this->buildProductionUiMetaMessage(
|
$this->buildProductionUiMetaMessage(
|
||||||
stageLabel: 'Shop wird durchsucht',
|
stageLabel: $this->agentRunnerConfig->getProductionUiStageLabel('shop_search_running'),
|
||||||
ragCount: count($knowledgeChunks),
|
ragCount: count($knowledgeChunks),
|
||||||
shopCount: null,
|
shopCount: null,
|
||||||
shopCountMode: 'loading',
|
shopCountMode: 'loading',
|
||||||
@@ -420,7 +420,7 @@ final readonly class AgentRunner
|
|||||||
'meta'
|
'meta'
|
||||||
);
|
);
|
||||||
$historyNotices[] = $this->buildHistoryNotice(
|
$historyNotices[] = $this->buildHistoryNotice(
|
||||||
'Shopdaten konnten nicht geladen werden',
|
$this->agentRunnerConfig->getProductionUiText('history_notice_shop_unavailable_title'),
|
||||||
$primaryShopSearchFailureReason
|
$primaryShopSearchFailureReason
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -431,7 +431,7 @@ final readonly class AgentRunner
|
|||||||
'repairQueries' => [],
|
'repairQueries' => [],
|
||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
yield $this->systemMsg('Erweiterte Shopsuche wird geprüft…', 'think');
|
yield $this->systemMsg($this->agentRunnerConfig->getShopRepairCheckMessage(), 'think');
|
||||||
|
|
||||||
$repairPayload = $this->repairShopResults(
|
$repairPayload = $this->repairShopResults(
|
||||||
prompt: $prompt,
|
prompt: $prompt,
|
||||||
@@ -476,7 +476,7 @@ final readonly class AgentRunner
|
|||||||
|
|
||||||
yield $this->systemMsg(
|
yield $this->systemMsg(
|
||||||
$this->buildProductionUiMetaMessage(
|
$this->buildProductionUiMetaMessage(
|
||||||
stageLabel: $primaryShopSearchHadSystemFailure ? 'Shopdaten nicht verfügbar' : 'Shop-Suche abgeschlossen',
|
stageLabel: $primaryShopSearchHadSystemFailure ? $this->agentRunnerConfig->getProductionUiStageLabel('shop_unavailable') : $this->agentRunnerConfig->getProductionUiStageLabel('shop_completed'),
|
||||||
ragCount: count($knowledgeChunks),
|
ragCount: count($knowledgeChunks),
|
||||||
shopCount: $primaryShopSearchHadSystemFailure ? null : count($shopResults),
|
shopCount: $primaryShopSearchHadSystemFailure ? null : count($shopResults),
|
||||||
shopCountMode: $primaryShopSearchHadSystemFailure ? 'unavailable' : 'count',
|
shopCountMode: $primaryShopSearchHadSystemFailure ? 'unavailable' : 'count',
|
||||||
@@ -542,7 +542,7 @@ final readonly class AgentRunner
|
|||||||
|
|
||||||
yield $this->systemMsg(
|
yield $this->systemMsg(
|
||||||
$this->buildProductionUiMetaMessage(
|
$this->buildProductionUiMetaMessage(
|
||||||
stageLabel: 'Antwort wird generiert',
|
stageLabel: $this->agentRunnerConfig->getProductionUiStageLabel('answer_generating'),
|
||||||
ragCount: count($knowledgeChunks),
|
ragCount: count($knowledgeChunks),
|
||||||
shopCount: $primaryShopSearchHadSystemFailure ? null : ($shopSearchAttempted ? count($shopResults) : null),
|
shopCount: $primaryShopSearchHadSystemFailure ? null : ($shopSearchAttempted ? count($shopResults) : null),
|
||||||
shopCountMode: $this->resolveShopCountModeForMeta(
|
shopCountMode: $this->resolveShopCountModeForMeta(
|
||||||
@@ -587,7 +587,7 @@ final readonly class AgentRunner
|
|||||||
|
|
||||||
yield $this->systemMsg(
|
yield $this->systemMsg(
|
||||||
$this->buildProductionUiMetaMessage(
|
$this->buildProductionUiMetaMessage(
|
||||||
stageLabel: 'Abgeschlossen',
|
stageLabel: $this->agentRunnerConfig->getProductionUiStageLabel('completed'),
|
||||||
ragCount: count($knowledgeChunks),
|
ragCount: count($knowledgeChunks),
|
||||||
shopCount: $primaryShopSearchHadSystemFailure ? null : ($shopSearchAttempted ? count($shopResults) : null),
|
shopCount: $primaryShopSearchHadSystemFailure ? null : ($shopSearchAttempted ? count($shopResults) : null),
|
||||||
shopCountMode: $this->resolveShopCountModeForMeta(
|
shopCountMode: $this->resolveShopCountModeForMeta(
|
||||||
@@ -667,7 +667,7 @@ final readonly class AgentRunner
|
|||||||
$userErrorMessage = $this->buildUserErrorMessage($e);
|
$userErrorMessage = $this->buildUserErrorMessage($e);
|
||||||
yield $this->systemMsg(
|
yield $this->systemMsg(
|
||||||
$this->buildProductionUiMetaMessage(
|
$this->buildProductionUiMetaMessage(
|
||||||
stageLabel: 'Antwort wurde unterbrochen',
|
stageLabel: $this->agentRunnerConfig->getProductionUiStageLabel('interrupted'),
|
||||||
ragCount: count($knowledgeChunks),
|
ragCount: count($knowledgeChunks),
|
||||||
shopCount: $primaryShopSearchHadSystemFailure ? null : ($shopSearchAttempted ? count($shopResults) : null),
|
shopCount: $primaryShopSearchHadSystemFailure ? null : ($shopSearchAttempted ? count($shopResults) : null),
|
||||||
shopCountMode: $this->resolveShopCountModeForMeta(
|
shopCountMode: $this->resolveShopCountModeForMeta(
|
||||||
@@ -677,7 +677,7 @@ final readonly class AgentRunner
|
|||||||
shopSearchSkippedBecauseNoQuery: $shopSearchSkippedBecauseNoQuery
|
shopSearchSkippedBecauseNoQuery: $shopSearchSkippedBecauseNoQuery
|
||||||
),
|
),
|
||||||
sourceLabels: $sources,
|
sourceLabels: $sources,
|
||||||
confidenceLabel: 'nicht abgeschlossen',
|
confidenceLabel: $this->agentRunnerConfig->getProductionUiConfidenceLabel('interrupted'),
|
||||||
completed: true
|
completed: true
|
||||||
),
|
),
|
||||||
'meta'
|
'meta'
|
||||||
@@ -686,7 +686,7 @@ final readonly class AgentRunner
|
|||||||
|
|
||||||
$historyResponse = $this->buildHistoryResponse('', array_merge(
|
$historyResponse = $this->buildHistoryResponse('', array_merge(
|
||||||
$historyNotices,
|
$historyNotices,
|
||||||
[$this->buildHistoryNotice('Antwort konnte nicht abgeschlossen werden', $e->getMessage())]
|
[$this->buildHistoryNotice($this->agentRunnerConfig->getProductionUiText('history_notice_answer_incomplete_title'), $e->getMessage())]
|
||||||
));
|
));
|
||||||
|
|
||||||
if ($historyResponse !== '') {
|
if ($historyResponse !== '') {
|
||||||
@@ -985,12 +985,7 @@ final readonly class AgentRunner
|
|||||||
private function normalizeFuzzyRoutingToken(string $token): string
|
private function normalizeFuzzyRoutingToken(string $token): string
|
||||||
{
|
{
|
||||||
$token = mb_strtolower(trim($token), 'UTF-8');
|
$token = mb_strtolower(trim($token), 'UTF-8');
|
||||||
$token = strtr($token, [
|
$token = $this->languageCleanupConfig->transliterateToAscii($token);
|
||||||
'ä' => 'ae',
|
|
||||||
'ö' => 'oe',
|
|
||||||
'ü' => 'ue',
|
|
||||||
'ß' => 'ss',
|
|
||||||
]);
|
|
||||||
$token = preg_replace('/[^a-z0-9]+/u', '', $token) ?? $token;
|
$token = preg_replace('/[^a-z0-9]+/u', '', $token) ?? $token;
|
||||||
|
|
||||||
return trim($token);
|
return trim($token);
|
||||||
@@ -1028,13 +1023,10 @@ final readonly class AgentRunner
|
|||||||
{
|
{
|
||||||
$normalized = $this->normalizeRoutingComparisonText($candidate);
|
$normalized = $this->normalizeRoutingComparisonText($candidate);
|
||||||
|
|
||||||
return in_array($normalized, [
|
return in_array($normalized, array_map(
|
||||||
'normalized user input',
|
fn (string $placeholder): string => $this->normalizeRoutingComparisonText($placeholder),
|
||||||
'corrected user input',
|
$this->agentRunnerConfig->getInputNormalizationPlaceholderOutputs()
|
||||||
'user input',
|
), true);
|
||||||
'normalisierte nutzereingabe',
|
|
||||||
'korrigierte nutzereingabe',
|
|
||||||
], true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function normalizeRoutingComparisonText(string $value): string
|
private function normalizeRoutingComparisonText(string $value): string
|
||||||
@@ -1132,7 +1124,7 @@ final readonly class AgentRunner
|
|||||||
$lines = [];
|
$lines = [];
|
||||||
|
|
||||||
foreach ($previousQuestions as $question) {
|
foreach ($previousQuestions as $question) {
|
||||||
$lines[] = 'Vorherige Nutzerfrage: ' . $question;
|
$lines[] = $this->renderAgentTemplate($this->agentRunnerConfig->getFollowUpContextPreviousUserQuestionTemplate(), ['question' => $question]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($referenceAnchors !== []) {
|
if ($referenceAnchors !== []) {
|
||||||
@@ -1140,7 +1132,7 @@ final readonly class AgentRunner
|
|||||||
. implode(' ', $referenceAnchors);
|
. implode(' ', $referenceAnchors);
|
||||||
}
|
}
|
||||||
|
|
||||||
$lines[] = 'Aktuelle Folgefrage: ' . $prompt;
|
$lines[] = $this->renderAgentTemplate($this->agentRunnerConfig->getFollowUpContextCurrentQuestionTemplate(), ['question' => $prompt]);
|
||||||
|
|
||||||
return implode("\n", $lines);
|
return implode("\n", $lines);
|
||||||
}
|
}
|
||||||
@@ -1354,7 +1346,7 @@ final readonly class AgentRunner
|
|||||||
private function normalizeFollowUpText(string $value): string
|
private function normalizeFollowUpText(string $value): string
|
||||||
{
|
{
|
||||||
$value = mb_strtolower(trim($value), 'UTF-8');
|
$value = mb_strtolower(trim($value), 'UTF-8');
|
||||||
$value = str_replace(['-', '/', '_'], ' ', $value);
|
$value = $this->languageCleanupConfig->replaceWordSeparatorsWithSpace($value);
|
||||||
$value = preg_replace('/[^\p{L}\p{N}\s]+/u', ' ', $value) ?? $value;
|
$value = preg_replace('/[^\p{L}\p{N}\s]+/u', ' ', $value) ?? $value;
|
||||||
$value = preg_replace('/\s+/u', ' ', $value) ?? $value;
|
$value = preg_replace('/\s+/u', ' ', $value) ?? $value;
|
||||||
|
|
||||||
@@ -1389,7 +1381,7 @@ final readonly class AgentRunner
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (time() - $lastHeartbeatAt >= 2) {
|
if (time() - $lastHeartbeatAt >= 2) {
|
||||||
yield $this->systemMsg('Shop-Suchanfrage wird optimiert…', 'think');
|
yield $this->systemMsg($this->agentRunnerConfig->getShopQueryOptimizationHeartbeatMessage(), 'think');
|
||||||
$lastHeartbeatAt = time();
|
$lastHeartbeatAt = time();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2027,7 +2019,7 @@ final readonly class AgentRunner
|
|||||||
private function tokenizeShopQueryCandidate(string $value): array
|
private function tokenizeShopQueryCandidate(string $value): array
|
||||||
{
|
{
|
||||||
$value = mb_strtolower(trim($value), 'UTF-8');
|
$value = mb_strtolower(trim($value), 'UTF-8');
|
||||||
$value = str_replace(['-', '/', '_'], ' ', $value);
|
$value = $this->languageCleanupConfig->replaceWordSeparatorsWithSpace($value);
|
||||||
|
|
||||||
if (preg_match_all('/\d+(?:[,.]\d+)?|[\p{L}\p{N}]+/u', $value, $matches) !== 1) {
|
if (preg_match_all('/\d+(?:[,.]\d+)?|[\p{L}\p{N}]+/u', $value, $matches) !== 1) {
|
||||||
return [];
|
return [];
|
||||||
@@ -2077,7 +2069,7 @@ final readonly class AgentRunner
|
|||||||
private function tokenizeMetaGuardText(string $value): array
|
private function tokenizeMetaGuardText(string $value): array
|
||||||
{
|
{
|
||||||
$value = mb_strtolower(trim($value), 'UTF-8');
|
$value = mb_strtolower(trim($value), 'UTF-8');
|
||||||
$value = str_replace(['-', '/', '_'], ' ', $value);
|
$value = $this->languageCleanupConfig->replaceWordSeparatorsWithSpace($value);
|
||||||
$value = preg_replace('/[^\p{L}\p{N}]+/u', ' ', $value) ?? $value;
|
$value = preg_replace('/[^\p{L}\p{N}]+/u', ' ', $value) ?? $value;
|
||||||
$value = preg_replace('/\s+/u', ' ', $value) ?? $value;
|
$value = preg_replace('/\s+/u', ' ', $value) ?? $value;
|
||||||
$value = trim($value);
|
$value = trim($value);
|
||||||
@@ -2232,7 +2224,10 @@ final readonly class AgentRunner
|
|||||||
}
|
}
|
||||||
|
|
||||||
$template = $this->agentRunnerConfig->getShopQueryContextAnchorEnrichmentTemplate();
|
$template = $this->agentRunnerConfig->getShopQueryContextAnchorEnrichmentTemplate();
|
||||||
$enriched = str_replace(['{anchor}', '{query}'], [$anchor, $query], $template);
|
$enriched = $this->renderAgentTemplate($template, [
|
||||||
|
'anchor' => $anchor,
|
||||||
|
'query' => $query,
|
||||||
|
]);
|
||||||
$enriched = preg_replace('/\s+/u', ' ', $enriched) ?? $enriched;
|
$enriched = preg_replace('/\s+/u', ' ', $enriched) ?? $enriched;
|
||||||
|
|
||||||
return trim($enriched) !== '' ? trim($enriched) : $query;
|
return trim($enriched) !== '' ? trim($enriched) : $query;
|
||||||
@@ -2505,7 +2500,10 @@ final readonly class AgentRunner
|
|||||||
: $this->agentRunnerConfig->getNoLlmFallbackShopUnavailableNoKnowledgeMessage();
|
: $this->agentRunnerConfig->getNoLlmFallbackShopUnavailableNoKnowledgeMessage();
|
||||||
|
|
||||||
if ($reason !== '') {
|
if ($reason !== '') {
|
||||||
$message .= ' Ursache: ' . $reason;
|
$message = $this->renderAgentTemplate($this->agentRunnerConfig->getNoLlmProductField('unavailable_reason_template'), [
|
||||||
|
'message' => $message,
|
||||||
|
'reason' => $reason,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return trim($message);
|
return trim($message);
|
||||||
@@ -2542,7 +2540,7 @@ final readonly class AgentRunner
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($lines === []) {
|
if ($lines === []) {
|
||||||
return ['- Es wurden Shop-Treffer übergeben, aber keine lesbaren Produktdaten gefunden.'];
|
return [$this->agentRunnerConfig->getNoLlmProductField('unreadable_results_message')];
|
||||||
}
|
}
|
||||||
|
|
||||||
return $lines;
|
return $lines;
|
||||||
@@ -2554,33 +2552,36 @@ final readonly class AgentRunner
|
|||||||
$productRole = $this->resolveNoLlmShopProductRole($product);
|
$productRole = $this->resolveNoLlmShopProductRole($product);
|
||||||
|
|
||||||
$name = $this->normalizeOneLine($product->name);
|
$name = $this->normalizeOneLine($product->name);
|
||||||
$parts[] = $name !== '' ? $name : 'Unbenanntes Shop-Produkt';
|
$parts[] = $name !== '' ? $name : $this->agentRunnerConfig->getNoLlmProductField('unnamed_product');
|
||||||
|
|
||||||
if ($product->productNumber !== null && trim($product->productNumber) !== '') {
|
if ($product->productNumber !== null && trim($product->productNumber) !== '') {
|
||||||
$parts[] = 'Art.-Nr. ' . $this->normalizeOneLine($product->productNumber);
|
$parts[] = $this->renderAgentTemplate($this->agentRunnerConfig->getNoLlmProductField('product_number_template'), ['value' => $this->normalizeOneLine($product->productNumber)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($product->manufacturer !== null && trim($product->manufacturer) !== '') {
|
if ($product->manufacturer !== null && trim($product->manufacturer) !== '') {
|
||||||
$parts[] = 'Hersteller: ' . $this->normalizeOneLine($product->manufacturer);
|
$parts[] = $this->renderAgentTemplate($this->agentRunnerConfig->getNoLlmProductField('manufacturer_template'), ['value' => $this->normalizeOneLine($product->manufacturer)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($product->price !== null && trim($product->price) !== '') {
|
if ($product->price !== null && trim($product->price) !== '') {
|
||||||
$parts[] = 'Preis: ' . $this->normalizeOneLine($product->price);
|
$parts[] = $this->renderAgentTemplate($this->agentRunnerConfig->getNoLlmProductField('price_template'), ['value' => $this->normalizeOneLine($product->price)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($product->available !== null) {
|
if ($product->available !== null) {
|
||||||
$parts[] = 'Verfügbar: ' . ($product->available ? 'ja' : 'nein');
|
$parts[] = $this->renderAgentTemplate($this->agentRunnerConfig->getNoLlmProductField('availability_template'), ['value' => $product->available ? $this->agentRunnerConfig->getNoLlmProductField('availability_yes') : $this->agentRunnerConfig->getNoLlmProductField('availability_no')]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($product->url !== null && trim($product->url) !== '') {
|
if ($product->url !== null && trim($product->url) !== '') {
|
||||||
$parts[] = 'URL: ' . $this->normalizeOneLine($product->url);
|
$parts[] = $this->renderAgentTemplate($this->agentRunnerConfig->getNoLlmProductField('url_template'), ['value' => $this->normalizeOneLine($product->url)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($requestedProductRole === 'main_device_or_system' && $productRole === 'accessory_or_consumable') {
|
if ($requestedProductRole === 'main_device_or_system' && $productRole === 'accessory_or_consumable') {
|
||||||
$parts[] = 'Hinweis: Zubehör/Verbrauchsartikel; nicht als Messanlage/Gerät bestätigt';
|
$parts[] = $this->agentRunnerConfig->getNoLlmProductField('incompatible_role_note');
|
||||||
}
|
}
|
||||||
|
|
||||||
return sprintf('%d. %s', $index, implode(' | ', $parts));
|
return $this->renderAgentTemplate($this->agentRunnerConfig->getNoLlmProductField('line_template'), [
|
||||||
|
'index' => (string) $index,
|
||||||
|
'parts' => implode($this->agentRunnerConfig->getNoLlmProductField('separator'), $parts),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -2722,20 +2723,20 @@ final readonly class AgentRunner
|
|||||||
private function resolveRagEvidenceConfidenceLabel(string $knowledgeEvidenceState): string
|
private function resolveRagEvidenceConfidenceLabel(string $knowledgeEvidenceState): string
|
||||||
{
|
{
|
||||||
return match ($knowledgeEvidenceState) {
|
return match ($knowledgeEvidenceState) {
|
||||||
'direct' => 'fachlich belegt',
|
'direct' => $this->agentRunnerConfig->getProductionUiConfidenceLabel('direct'),
|
||||||
'aggregate_missing' => 'geprüfte Quellen, keine passende Zählinformation',
|
'aggregate_missing' => $this->agentRunnerConfig->getProductionUiConfidenceLabel('aggregate_missing'),
|
||||||
'weak' => 'RAG-Näherungstreffer, kein direkter Fachbeleg',
|
'weak' => $this->agentRunnerConfig->getProductionUiConfidenceLabel('weak'),
|
||||||
default => 'noch keine belastbaren Treffer',
|
default => $this->agentRunnerConfig->getProductionUiConfidenceLabel('default'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private function resolveRagEvidenceShopCheckConfidenceLabel(string $knowledgeEvidenceState): string
|
private function resolveRagEvidenceShopCheckConfidenceLabel(string $knowledgeEvidenceState): string
|
||||||
{
|
{
|
||||||
return match ($knowledgeEvidenceState) {
|
return match ($knowledgeEvidenceState) {
|
||||||
'direct' => 'fachlich belegt; Shopdaten werden geprüft',
|
'direct' => $this->agentRunnerConfig->getProductionUiConfidenceLabel('direct_shop_check'),
|
||||||
'aggregate_missing' => 'geprüfte Quellen ohne Zählinformation; Shopdaten werden geprüft',
|
'aggregate_missing' => $this->agentRunnerConfig->getProductionUiConfidenceLabel('aggregate_missing_shop_check'),
|
||||||
'weak' => 'RAG-Näherungstreffer; Shopdaten werden geprüft',
|
'weak' => $this->agentRunnerConfig->getProductionUiConfidenceLabel('weak_shop_check'),
|
||||||
default => 'Shopdaten werden geprüft',
|
default => $this->agentRunnerConfig->getProductionUiConfidenceLabel('default_shop_check'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2902,13 +2903,8 @@ final readonly class AgentRunner
|
|||||||
{
|
{
|
||||||
$value = html_entity_decode(strip_tags($value), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
$value = html_entity_decode(strip_tags($value), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||||||
$value = mb_strtolower($value, 'UTF-8');
|
$value = mb_strtolower($value, 'UTF-8');
|
||||||
$value = str_replace(['‐', '‑', '‒', '–', '—'], '-', $value);
|
$value = $this->languageCleanupConfig->normalizeDashEquivalents($value);
|
||||||
$value = strtr($value, [
|
$value = $this->languageCleanupConfig->transliterateToAscii($value);
|
||||||
'ä' => 'ae',
|
|
||||||
'ö' => 'oe',
|
|
||||||
'ü' => 'ue',
|
|
||||||
'ß' => 'ss',
|
|
||||||
]);
|
|
||||||
$value = preg_replace('/\s+/u', ' ', $value) ?? $value;
|
$value = preg_replace('/\s+/u', ' ', $value) ?? $value;
|
||||||
|
|
||||||
return trim($value);
|
return trim($value);
|
||||||
@@ -2957,7 +2953,7 @@ final readonly class AgentRunner
|
|||||||
$noLlmMessage = $this->plainTextFromHtml($this->agentRunnerConfig->getNoLlmDataReceivedMessage());
|
$noLlmMessage = $this->plainTextFromHtml($this->agentRunnerConfig->getNoLlmDataReceivedMessage());
|
||||||
|
|
||||||
if ($noLlmMessage === '') {
|
if ($noLlmMessage === '') {
|
||||||
$noLlmMessage = 'Es wurden keine Daten vom LLM empfangen.';
|
$noLlmMessage = $this->agentRunnerConfig->getProductionUiText('no_llm_history_default');
|
||||||
}
|
}
|
||||||
|
|
||||||
$parts[] = 'Systemhinweis: ' . $noLlmMessage;
|
$parts[] = 'Systemhinweis: ' . $noLlmMessage;
|
||||||
@@ -2972,18 +2968,21 @@ final readonly class AgentRunner
|
|||||||
$detail = $this->normalizeOneLine($this->plainTextFromHtml((string) $detail));
|
$detail = $this->normalizeOneLine($this->plainTextFromHtml((string) $detail));
|
||||||
|
|
||||||
if ($title === '') {
|
if ($title === '') {
|
||||||
$title = 'Systemhinweis';
|
$title = $this->agentRunnerConfig->getProductionUiText('history_notice_default_title');
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($detail === '') {
|
if ($detail === '') {
|
||||||
return 'Systemhinweis: ' . $title . '.';
|
return $this->renderAgentTemplate($this->agentRunnerConfig->getProductionUiTemplate('history_notice_without_detail'), ['title' => $title]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mb_strlen($detail, 'UTF-8') > 500) {
|
if (mb_strlen($detail, 'UTF-8') > 500) {
|
||||||
$detail = rtrim(mb_substr($detail, 0, 497, 'UTF-8')) . '...';
|
$detail = rtrim(mb_substr($detail, 0, 497, 'UTF-8')) . '...';
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'Systemhinweis: ' . $title . '. Ursache: ' . $detail;
|
return $this->renderAgentTemplate($this->agentRunnerConfig->getProductionUiTemplate('history_notice_with_detail'), [
|
||||||
|
'title' => $title,
|
||||||
|
'detail' => $detail,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function plainTextFromHtml(string $value): string
|
private function plainTextFromHtml(string $value): string
|
||||||
@@ -3033,32 +3032,34 @@ final readonly class AgentRunner
|
|||||||
): string {
|
): string {
|
||||||
$state = $completed ? 'completed' : 'running';
|
$state = $completed ? 'completed' : 'running';
|
||||||
$ragLabel = $ragCount === null
|
$ragLabel = $ragCount === null
|
||||||
? 'RAG-Treffer: wird geprüft'
|
? $this->agentRunnerConfig->getProductionUiText('rag_hits_checking')
|
||||||
: 'RAG-Treffer: ' . max(0, $ragCount);
|
: $this->renderAgentTemplate($this->agentRunnerConfig->getProductionUiTemplate('rag_hits_count'), ['count' => (string) max(0, $ragCount)]);
|
||||||
$shopLabel = match ($shopCountMode) {
|
$shopLabel = match ($shopCountMode) {
|
||||||
'count' => 'Shop-Treffer: ' . max(0, (int) $shopCount),
|
'count' => $this->renderAgentTemplate($this->agentRunnerConfig->getProductionUiTemplate('shop_hits_count'), ['count' => (string) max(0, (int) $shopCount)]),
|
||||||
'loading' => 'Shop-Treffer: wird geladen',
|
'loading' => $this->agentRunnerConfig->getProductionUiText('shop_hits_loading'),
|
||||||
'unavailable' => 'Shop-Treffer: nicht verfügbar',
|
'unavailable' => $this->agentRunnerConfig->getProductionUiText('shop_hits_unavailable'),
|
||||||
'not_resolved' => 'Shop-Treffer: keine Suchquery',
|
'not_resolved' => $this->agentRunnerConfig->getProductionUiText('shop_hits_no_query'),
|
||||||
default => 'Shop-Treffer: nicht angefragt',
|
default => $this->agentRunnerConfig->getProductionUiText('shop_hits_not_requested'),
|
||||||
};
|
};
|
||||||
$statusLabel = $completed ? 'Status: abgeschlossen' : 'Status: läuft';
|
$statusLabel = $completed
|
||||||
|
? $this->agentRunnerConfig->getProductionUiText('status_completed')
|
||||||
|
: $this->agentRunnerConfig->getProductionUiText('status_running');
|
||||||
$sources = $this->formatProductionUiSourceLabels($sourceLabels);
|
$sources = $this->formatProductionUiSourceLabels($sourceLabels);
|
||||||
|
|
||||||
$html = '<div class="retriex-meta-card retriex-run-meta" data-retriex-meta-id="run-status" data-retriex-meta-state="'
|
$html = '<div class="retriex-meta-card retriex-run-meta" data-retriex-meta-id="run-status" data-retriex-meta-state="'
|
||||||
. htmlspecialchars($state, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')
|
. htmlspecialchars($state, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')
|
||||||
. '">'
|
. '">'
|
||||||
. '<div class="retriex-meta-card__eyebrow">RetrieX-Status</div>'
|
. '<div class="retriex-meta-card__eyebrow">' . htmlspecialchars($this->agentRunnerConfig->getProductionUiText('run_status_eyebrow'), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</div>'
|
||||||
. '<div class="retriex-meta-card__title">' . htmlspecialchars($stageLabel, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</div>'
|
. '<div class="retriex-meta-card__title">' . htmlspecialchars($stageLabel, 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--status">' . htmlspecialchars($statusLabel, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</span>'
|
. '<span class="retriex-meta-pill retriex-meta-pill--status">' . htmlspecialchars($statusLabel, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</span>'
|
||||||
. '<span class="retriex-meta-pill retriex-meta-pill--result">' . htmlspecialchars($ragLabel, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</span>'
|
. '<span class="retriex-meta-pill retriex-meta-pill--result">' . htmlspecialchars($ragLabel, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</span>'
|
||||||
. '<span class="retriex-meta-pill retriex-meta-pill--result">' . htmlspecialchars($shopLabel, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</span>'
|
. '<span class="retriex-meta-pill retriex-meta-pill--result">' . htmlspecialchars($shopLabel, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</span>'
|
||||||
. '<span class="retriex-meta-pill retriex-meta-pill--confidence">Beleglage: ' . htmlspecialchars($confidenceLabel, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</span>'
|
. '<span class="retriex-meta-pill retriex-meta-pill--confidence">' . htmlspecialchars($this->agentRunnerConfig->getProductionUiText('evidence_prefix'), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '' . htmlspecialchars($confidenceLabel, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</span>'
|
||||||
. '</div>';
|
. '</div>';
|
||||||
|
|
||||||
if ($sources !== []) {
|
if ($sources !== []) {
|
||||||
$html .= '<div class="retriex-source-overview"><span>Datenbasis</span><div class="retriex-source-overview__badges">';
|
$html .= '<div class="retriex-source-overview"><span>' . htmlspecialchars($this->agentRunnerConfig->getProductionUiText('data_basis_label'), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</span><div class="retriex-source-overview__badges">';
|
||||||
|
|
||||||
foreach ($sources as $source) {
|
foreach ($sources as $source) {
|
||||||
$html .= '<span class="retriex-source-chip">' . htmlspecialchars($source, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</span>';
|
$html .= '<span class="retriex-source-chip">' . htmlspecialchars($source, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</span>';
|
||||||
@@ -3066,8 +3067,10 @@ final readonly class AgentRunner
|
|||||||
|
|
||||||
$html .= '</div></div>';
|
$html .= '</div></div>';
|
||||||
} else {
|
} else {
|
||||||
$emptySourceLabel = $completed ? 'keine belastbare Datenbasis' : 'wird geprüft';
|
$emptySourceLabel = $completed
|
||||||
$html .= '<div class="retriex-source-overview"><span>Datenbasis</span><div class="retriex-source-overview__empty">'
|
? $this->agentRunnerConfig->getProductionUiText('data_basis_empty_completed')
|
||||||
|
: $this->agentRunnerConfig->getProductionUiText('data_basis_empty_running');
|
||||||
|
$html .= '<div class="retriex-source-overview"><span>' . htmlspecialchars($this->agentRunnerConfig->getProductionUiText('data_basis_label'), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</span><div class="retriex-source-overview__empty">'
|
||||||
. htmlspecialchars($emptySourceLabel, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')
|
. htmlspecialchars($emptySourceLabel, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')
|
||||||
. '</div></div>';
|
. '</div></div>';
|
||||||
}
|
}
|
||||||
@@ -3087,35 +3090,37 @@ final readonly class AgentRunner
|
|||||||
): string {
|
): string {
|
||||||
if ($knowledgeEvidenceState === 'aggregate_missing' && !$hasShopResults) {
|
if ($knowledgeEvidenceState === 'aggregate_missing' && !$hasShopResults) {
|
||||||
return $shopSearchHadSystemFailure
|
return $shopSearchHadSystemFailure
|
||||||
? 'geprüfte Quellen ohne Zählinformation; Shopdaten nicht verfügbar'
|
? $this->agentRunnerConfig->getProductionUiConfidenceLabel('aggregate_missing_shop_unavailable')
|
||||||
: 'geprüfte Quellen, keine passende Zählinformation';
|
: $this->agentRunnerConfig->getProductionUiConfidenceLabel('aggregate_missing_no_count');
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($shopSearchHadSystemFailure) {
|
if ($shopSearchHadSystemFailure) {
|
||||||
return $hasKnowledge ? 'fachlich belegt; Shopdaten nicht verfügbar' : 'Shopdaten nicht verfügbar';
|
return $hasKnowledge
|
||||||
|
? $this->agentRunnerConfig->getProductionUiConfidenceLabel('shop_unavailable_with_knowledge')
|
||||||
|
: $this->agentRunnerConfig->getProductionUiConfidenceLabel('shop_unavailable');
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($hasKnowledge && $hasShopResults) {
|
if ($hasKnowledge && $hasShopResults) {
|
||||||
return 'RAG + Shopdaten';
|
return $this->agentRunnerConfig->getProductionUiConfidenceLabel('rag_and_shop');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$hasKnowledge && $hasShopResults) {
|
if (!$hasKnowledge && $hasShopResults) {
|
||||||
return 'nur Shopdaten';
|
return $this->agentRunnerConfig->getProductionUiConfidenceLabel('shop_only');
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($hasKnowledge && $shopSearchAttempted) {
|
if ($hasKnowledge && $shopSearchAttempted) {
|
||||||
return 'RAG-Wissen, keine Shop-Treffer';
|
return $this->agentRunnerConfig->getProductionUiConfidenceLabel('rag_no_shop_hits');
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($hasKnowledge) {
|
if ($hasKnowledge) {
|
||||||
return 'fachlich belegt';
|
return $this->agentRunnerConfig->getProductionUiConfidenceLabel('direct');
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($isCommerceIntent || $shopSearchAttempted) {
|
if ($isCommerceIntent || $shopSearchAttempted) {
|
||||||
return 'keine belastbaren Daten';
|
return $this->agentRunnerConfig->getProductionUiConfidenceLabel('no_reliable_data');
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'noch keine belastbaren Treffer';
|
return $this->agentRunnerConfig->getProductionUiConfidenceLabel('no_reliable_hits');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -3137,7 +3142,7 @@ final readonly class AgentRunner
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($label === $this->plainTextFromHtml($this->agentRunnerConfig->getShopSystemSourceLabel())) {
|
if ($label === $this->plainTextFromHtml($this->agentRunnerConfig->getShopSystemSourceLabel())) {
|
||||||
$label = 'Live-Shopdaten';
|
$label = $this->agentRunnerConfig->getProductionUiText('live_shop_source_plain_label');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!in_array($label, $labels, true)) {
|
if (!in_array($label, $labels, true)) {
|
||||||
@@ -3153,27 +3158,27 @@ final readonly class AgentRunner
|
|||||||
*/
|
*/
|
||||||
private function buildShopProductCardsMessage(array $shopResults, string $query, bool $usedRepair): string
|
private function buildShopProductCardsMessage(array $shopResults, string $query, bool $usedRepair): string
|
||||||
{
|
{
|
||||||
$maxCards = 5;
|
$maxCards = max(1, $this->agentRunnerConfig->getProductionUiShopResultsMaxCards());
|
||||||
$visibleResults = array_slice($shopResults, 0, $maxCards);
|
$visibleResults = array_slice($shopResults, 0, $maxCards);
|
||||||
$totalCount = count($shopResults);
|
$totalCount = count($shopResults);
|
||||||
$query = $this->normalizeOneLine($query);
|
$query = $this->normalizeOneLine($query);
|
||||||
$summary = $totalCount . ' Shop-Treffer ausgewertet';
|
$summary = $this->renderAgentTemplate($this->agentRunnerConfig->getProductionUiTemplate('shop_results_summary'), ['count' => (string) $totalCount]);
|
||||||
|
|
||||||
if ($totalCount > $maxCards) {
|
if ($totalCount > $maxCards) {
|
||||||
$summary .= ' · Top ' . $maxCards . ' angezeigt';
|
$summary .= $this->renderAgentTemplate($this->agentRunnerConfig->getProductionUiTemplate('shop_results_top_displayed_suffix'), ['max' => (string) $maxCards]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($usedRepair) {
|
if ($usedRepair) {
|
||||||
$summary .= ' · erweiterte Shopsuche genutzt';
|
$summary .= $this->agentRunnerConfig->getProductionUiTemplate('shop_results_repair_suffix');
|
||||||
}
|
}
|
||||||
|
|
||||||
$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">Shop-Ergebnisse</div>'
|
. '<div class="retriex-meta-card__eyebrow">' . htmlspecialchars($this->agentRunnerConfig->getProductionUiText('shop_results_eyebrow'), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</div>'
|
||||||
. '<div class="retriex-meta-card__title">Shop-Ergebnisse</div>'
|
. '<div class="retriex-meta-card__title">' . htmlspecialchars($this->agentRunnerConfig->getProductionUiText('shop_results_title'), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</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>';
|
||||||
|
|
||||||
if ($query !== '') {
|
if ($query !== '') {
|
||||||
$html .= '<div class="retriex-meta-query retriex-meta-query--compact"><span>Ausgewertete Suchquery</span><code>'
|
$html .= '<div class="retriex-meta-query retriex-meta-query--compact"><span>' . htmlspecialchars($this->agentRunnerConfig->getProductionUiText('evaluated_query_label'), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</span><code>'
|
||||||
. htmlspecialchars($query, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')
|
. htmlspecialchars($query, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')
|
||||||
. '</code></div>';
|
. '</code></div>';
|
||||||
}
|
}
|
||||||
@@ -3195,7 +3200,7 @@ final readonly class AgentRunner
|
|||||||
|
|
||||||
private function buildShopProductCard(ShopProductResult $product, string $query): string
|
private function buildShopProductCard(ShopProductResult $product, string $query): string
|
||||||
{
|
{
|
||||||
$name = $this->normalizeOneLine($product->name) ?: 'Unbenanntes Produkt';
|
$name = $this->normalizeOneLine($product->name) ?: $this->agentRunnerConfig->getProductionUiText('unnamed_product');
|
||||||
$productNumber = $this->normalizeOneLine((string) $product->productNumber);
|
$productNumber = $this->normalizeOneLine((string) $product->productNumber);
|
||||||
$manufacturer = $this->normalizeOneLine((string) $product->manufacturer);
|
$manufacturer = $this->normalizeOneLine((string) $product->manufacturer);
|
||||||
$price = $this->normalizeOneLine((string) $product->price);
|
$price = $this->normalizeOneLine((string) $product->price);
|
||||||
@@ -3215,16 +3220,16 @@ final readonly class AgentRunner
|
|||||||
}
|
}
|
||||||
|
|
||||||
$html .= '</div><dl class="retriex-product-card__facts">';
|
$html .= '</div><dl class="retriex-product-card__facts">';
|
||||||
$html .= '<div><dt>Artikelnummer</dt><dd>' . htmlspecialchars($productNumber !== '' ? $productNumber : 'nicht übermittelt', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</dd></div>';
|
$html .= '<div><dt>' . htmlspecialchars($this->agentRunnerConfig->getProductionUiText('product_number_label'), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</dt><dd>' . htmlspecialchars($productNumber !== '' ? $productNumber : $this->agentRunnerConfig->getProductionUiText('field_not_provided'), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</dd></div>';
|
||||||
$html .= '<div><dt>Preis</dt><dd>' . htmlspecialchars($price !== '' ? $price : 'nicht übermittelt', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</dd></div>';
|
$html .= '<div><dt>' . htmlspecialchars($this->agentRunnerConfig->getProductionUiText('price_label'), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</dt><dd>' . htmlspecialchars($price !== '' ? $price : $this->agentRunnerConfig->getProductionUiText('field_not_provided'), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</dd></div>';
|
||||||
$html .= '<div><dt>Verfügbarkeit</dt><dd>' . htmlspecialchars($availability, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</dd></div>';
|
$html .= '<div><dt>' . htmlspecialchars($this->agentRunnerConfig->getProductionUiText('availability_label'), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</dt><dd>' . htmlspecialchars($availability, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</dd></div>';
|
||||||
|
|
||||||
if ($manufacturer !== '') {
|
if ($manufacturer !== '') {
|
||||||
$html .= '<div><dt>Hersteller</dt><dd>' . htmlspecialchars($manufacturer, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</dd></div>';
|
$html .= '<div><dt>' . htmlspecialchars($this->agentRunnerConfig->getProductionUiText('manufacturer_label'), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</dt><dd>' . htmlspecialchars($manufacturer, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</dd></div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
$html .= '</dl>'
|
$html .= '</dl>'
|
||||||
. '<div class="retriex-product-card__relevance"><span>Relevanz</span>'
|
. '<div class="retriex-product-card__relevance"><span>' . htmlspecialchars($this->agentRunnerConfig->getProductionUiText('relevance_label'), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</span>'
|
||||||
. htmlspecialchars($relevance, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')
|
. htmlspecialchars($relevance, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')
|
||||||
. '</div>'
|
. '</div>'
|
||||||
. '</article>';
|
. '</article>';
|
||||||
@@ -3235,9 +3240,9 @@ final readonly class AgentRunner
|
|||||||
private function formatProductAvailability(?bool $available): string
|
private function formatProductAvailability(?bool $available): string
|
||||||
{
|
{
|
||||||
return match ($available) {
|
return match ($available) {
|
||||||
true => 'verfügbar',
|
true => $this->agentRunnerConfig->getProductionUiText('availability_yes'),
|
||||||
false => 'nicht verfügbar',
|
false => $this->agentRunnerConfig->getProductionUiText('availability_no'),
|
||||||
default => 'Shopstatus nicht übermittelt',
|
default => $this->agentRunnerConfig->getProductionUiText('availability_unknown'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3254,28 +3259,32 @@ final readonly class AgentRunner
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($matchedQueries !== []) {
|
if ($matchedQueries !== []) {
|
||||||
return 'Gefunden über: ' . implode(', ', array_slice($matchedQueries, 0, 3));
|
return $this->renderAgentTemplate($this->agentRunnerConfig->getProductionUiTemplate('relevance_matched_queries'), [
|
||||||
|
'queries' => implode(', ', array_slice($matchedQueries, 0, 3)),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($product->highlights as $highlight) {
|
foreach ($product->highlights as $highlight) {
|
||||||
$highlight = $this->normalizeOneLine($this->plainTextFromHtml((string) $highlight));
|
$highlight = $this->normalizeOneLine($this->plainTextFromHtml((string) $highlight));
|
||||||
|
|
||||||
if ($highlight !== '') {
|
if ($highlight !== '') {
|
||||||
return 'Passender Shop-Hinweis: ' . mb_substr($highlight, 0, 140, 'UTF-8');
|
return $this->renderAgentTemplate($this->agentRunnerConfig->getProductionUiTemplate('relevance_highlight'), [
|
||||||
|
'highlight' => mb_substr($highlight, 0, 140, 'UTF-8'),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$matchSource = $this->normalizeOneLine((string) $product->matchSource);
|
$matchSource = $this->normalizeOneLine((string) $product->matchSource);
|
||||||
|
|
||||||
if ($matchSource !== '') {
|
if ($matchSource !== '') {
|
||||||
return 'Trefferquelle: ' . $matchSource;
|
return $this->renderAgentTemplate($this->agentRunnerConfig->getProductionUiTemplate('relevance_match_source'), ['source' => $matchSource]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($query !== '') {
|
if ($query !== '') {
|
||||||
return 'Passend zur Suchquery: ' . $query;
|
return $this->renderAgentTemplate($this->agentRunnerConfig->getProductionUiTemplate('relevance_query'), ['query' => $query]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'Aus den Live-Shopdaten übernommen';
|
return $this->agentRunnerConfig->getProductionUiTemplate('relevance_default');
|
||||||
}
|
}
|
||||||
|
|
||||||
private function buildFollowUpActionsMessage(bool $isCommerceIntent, bool $hasShopResults, bool $hasKnowledge): string
|
private function buildFollowUpActionsMessage(bool $isCommerceIntent, bool $hasShopResults, bool $hasKnowledge): string
|
||||||
@@ -3287,14 +3296,11 @@ final readonly class AgentRunner
|
|||||||
$actions = [];
|
$actions = [];
|
||||||
|
|
||||||
if ($isCommerceIntent || $hasShopResults) {
|
if ($isCommerceIntent || $hasShopResults) {
|
||||||
$actions[] = ['Im Shop suchen', 'Suche die aktuelle Produktauswahl im Shop.'];
|
$actions = array_merge($actions, $this->agentRunnerConfig->getProductionUiFollowUpActions('commerce'));
|
||||||
$actions[] = ['Nur Zubehör anzeigen', 'Zeige aus der aktuellen Produktauswahl nur Zubehör.'];
|
|
||||||
$actions[] = ['Nur Geräte anzeigen', 'Zeige aus der aktuellen Produktauswahl nur Geräte.'];
|
|
||||||
$actions[] = ['Preis anzeigen', 'Zeige mir die Preise der aktuell relevanten Produkte.'];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($hasKnowledge || $hasShopResults) {
|
if ($hasKnowledge || $hasShopResults) {
|
||||||
$actions[] = ['Technische Details anzeigen', 'Zeige technische Details zur aktuellen Antwort.'];
|
$actions = array_merge($actions, $this->agentRunnerConfig->getProductionUiFollowUpActions('knowledge'));
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($actions === []) {
|
if ($actions === []) {
|
||||||
@@ -3302,11 +3308,16 @@ final readonly class AgentRunner
|
|||||||
}
|
}
|
||||||
|
|
||||||
$html = '<div class="retriex-meta-card retriex-followup-actions" data-retriex-meta-id="followup-actions" data-retriex-meta-state="completed">'
|
$html = '<div class="retriex-meta-card retriex-followup-actions" data-retriex-meta-id="followup-actions" data-retriex-meta-state="completed">'
|
||||||
. '<div class="retriex-meta-card__eyebrow">Folgeaktionen</div>'
|
. '<div class="retriex-meta-card__eyebrow">' . htmlspecialchars($this->agentRunnerConfig->getProductionUiText('followup_eyebrow'), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</div>'
|
||||||
. '<div class="retriex-meta-card__title">Was möchtest du als Nächstes tun?</div>'
|
. '<div class="retriex-meta-card__title">' . htmlspecialchars($this->agentRunnerConfig->getProductionUiText('followup_title'), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</div>'
|
||||||
. '<div class="retriex-action-chip-row">';
|
. '<div class="retriex-action-chip-row">';
|
||||||
|
|
||||||
foreach ($actions as [$label, $actionPrompt]) {
|
foreach ($actions as $action) {
|
||||||
|
$label = (string) ($action['label'] ?? '');
|
||||||
|
$actionPrompt = (string) ($action['prompt'] ?? '');
|
||||||
|
if ($label === '' || $actionPrompt === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
$html .= '<button type="button" class="retriex-action-chip" data-retriex-action-prompt="'
|
$html .= '<button type="button" class="retriex-action-chip" data-retriex-action-prompt="'
|
||||||
. htmlspecialchars($actionPrompt, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')
|
. htmlspecialchars($actionPrompt, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')
|
||||||
. '">'
|
. '">'
|
||||||
@@ -3334,26 +3345,32 @@ final readonly class AgentRunner
|
|||||||
$originalQuery = $this->normalizeOneLine($originalQuery);
|
$originalQuery = $this->normalizeOneLine($originalQuery);
|
||||||
|
|
||||||
if ($query === '') {
|
if ($query === '') {
|
||||||
$query = $originalQuery !== '' ? $originalQuery : 'keine Suchquery ermittelt';
|
$query = $originalQuery !== '' ? $originalQuery : $this->agentRunnerConfig->getProductionUiText('shop_meta_fallback_query');
|
||||||
}
|
}
|
||||||
|
|
||||||
$queryModeLabel = $usedOptimizedQuery ? 'optimiert' : 'direkt';
|
$queryModeLabel = $usedOptimizedQuery ? $this->agentRunnerConfig->getProductionUiText('shop_meta_query_mode_optimized') : $this->agentRunnerConfig->getProductionUiText('shop_meta_query_mode_direct');
|
||||||
$intentLabel = $commerceIntent !== '' ? $commerceIntent : 'commerce';
|
$intentLabel = $commerceIntent !== '' ? $commerceIntent : $this->agentRunnerConfig->getProductionUiText('shop_meta_default_intent');
|
||||||
$title = $unavailable ? 'Shopdaten nicht verfügbar' : ($completed ? 'Shop-Suche abgeschlossen' : 'Shop-Suche wird ausgeführt');
|
$title = $unavailable
|
||||||
$statusLabel = $completed ? 'Status: abgeschlossen' : 'Status: läuft';
|
? $this->agentRunnerConfig->getProductionUiText('shop_meta_title_unavailable')
|
||||||
|
: ($completed
|
||||||
|
? $this->agentRunnerConfig->getProductionUiText('shop_meta_title_completed')
|
||||||
|
: $this->agentRunnerConfig->getProductionUiText('shop_meta_title_running'));
|
||||||
|
$statusLabel = $completed
|
||||||
|
? $this->agentRunnerConfig->getProductionUiText('shop_meta_status_completed')
|
||||||
|
: $this->agentRunnerConfig->getProductionUiText('shop_meta_status_running');
|
||||||
$resultLabel = $unavailable
|
$resultLabel = $unavailable
|
||||||
? 'Shoptreffer: nicht verfügbar'
|
? $this->agentRunnerConfig->getProductionUiText('shop_meta_result_unavailable')
|
||||||
: ($resultCount === null
|
: ($resultCount === null
|
||||||
? 'Shoptreffer: wird geladen'
|
? $this->agentRunnerConfig->getProductionUiText('shop_meta_result_loading')
|
||||||
: 'Shoptreffer: ' . max(0, $resultCount));
|
: $this->renderAgentTemplate($this->agentRunnerConfig->getProductionUiTemplate('shop_meta_result_count'), ['count' => (string) max(0, $resultCount)]));
|
||||||
$state = $completed ? 'completed' : 'running';
|
$state = $completed ? 'completed' : 'running';
|
||||||
$resultCountAttribute = $resultCount === null ? '' : (string) max(0, $resultCount);
|
$resultCountAttribute = $resultCount === null ? '' : (string) max(0, $resultCount);
|
||||||
$repairLabel = '';
|
$repairLabel = '';
|
||||||
|
|
||||||
if ($usedRepair) {
|
if ($usedRepair) {
|
||||||
$repairLabel = 'Erweiterte Suche: genutzt';
|
$repairLabel = $this->agentRunnerConfig->getProductionUiText('shop_meta_repair_used');
|
||||||
} elseif ($attemptedRepair) {
|
} elseif ($attemptedRepair) {
|
||||||
$repairLabel = 'Erweiterte Suche: geprüft';
|
$repairLabel = $this->agentRunnerConfig->getProductionUiText('shop_meta_repair_checked');
|
||||||
}
|
}
|
||||||
|
|
||||||
$html = '<div class="retriex-meta-card retriex-shop-meta" data-retriex-meta-id="shop-search" data-retriex-meta-state="'
|
$html = '<div class="retriex-meta-card retriex-shop-meta" data-retriex-meta-id="shop-search" data-retriex-meta-state="'
|
||||||
@@ -3361,20 +3378,20 @@ 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">Shop-Suche</div>'
|
. '<div class="retriex-meta-card__eyebrow">' . htmlspecialchars($this->agentRunnerConfig->getProductionUiText('shop_meta_eyebrow'), 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__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>'
|
||||||
. '<span class="retriex-meta-pill">' . htmlspecialchars($statusLabel, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</span>'
|
. '<span class="retriex-meta-pill">' . htmlspecialchars($statusLabel, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</span>'
|
||||||
. '<span class="retriex-meta-pill">Query: ' . htmlspecialchars($queryModeLabel, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</span>'
|
. '<span class="retriex-meta-pill">' . htmlspecialchars($this->agentRunnerConfig->getProductionUiText('shop_meta_query_prefix'), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . htmlspecialchars($queryModeLabel, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</span>'
|
||||||
. '<span class="retriex-meta-pill">Intent: ' . htmlspecialchars($intentLabel, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</span>';
|
. '<span class="retriex-meta-pill">' . htmlspecialchars($this->agentRunnerConfig->getProductionUiText('shop_meta_intent_prefix'), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . htmlspecialchars($intentLabel, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</span>';
|
||||||
|
|
||||||
if ($repairLabel !== '') {
|
if ($repairLabel !== '') {
|
||||||
$html .= '<span class="retriex-meta-pill">' . htmlspecialchars($repairLabel, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</span>';
|
$html .= '<span class="retriex-meta-pill">' . htmlspecialchars($repairLabel, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</span>';
|
||||||
}
|
}
|
||||||
|
|
||||||
$html .= '</div>'
|
$html .= '</div>'
|
||||||
. '<div class="retriex-meta-query"><span>Gesendete Suchquery</span><code>'
|
. '<div class="retriex-meta-query"><span>' . htmlspecialchars($this->agentRunnerConfig->getProductionUiText('shop_meta_query_label'), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</span><code>'
|
||||||
. htmlspecialchars($query, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')
|
. htmlspecialchars($query, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')
|
||||||
. '</code></div>'
|
. '</code></div>'
|
||||||
. '</div>';
|
. '</div>';
|
||||||
@@ -3387,7 +3404,7 @@ final readonly class AgentRunner
|
|||||||
$reason = $this->normalizeOneLine((string) $reason);
|
$reason = $this->normalizeOneLine((string) $reason);
|
||||||
|
|
||||||
if ($reason === '') {
|
if ($reason === '') {
|
||||||
$reason = 'Keine Detailmeldung vom Shopware-Server.';
|
$reason = $this->agentRunnerConfig->getProductionUiText('shop_unavailable_default_reason');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mb_strlen($reason, 'UTF-8') > 320) {
|
if (mb_strlen($reason, 'UTF-8') > 320) {
|
||||||
@@ -3397,14 +3414,27 @@ final readonly class AgentRunner
|
|||||||
return '<div class="retriex-alert retriex-alert--warning">'
|
return '<div class="retriex-alert retriex-alert--warning">'
|
||||||
. '<div class="retriex-alert__icon">⚠️</div>'
|
. '<div class="retriex-alert__icon">⚠️</div>'
|
||||||
. '<div class="retriex-alert__content">'
|
. '<div class="retriex-alert__content">'
|
||||||
. '<div class="retriex-alert__title">Shopdaten konnten nicht geladen werden</div>'
|
. '<div class="retriex-alert__title">' . htmlspecialchars($this->agentRunnerConfig->getProductionUiText('shop_unavailable_title'), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</div>'
|
||||||
. '<div class="retriex-alert__text">RetrieX antwortet ohne Live-Shopdaten weiter. Ursache: '
|
. '<div class="retriex-alert__text">' . htmlspecialchars($this->agentRunnerConfig->getProductionUiText('shop_unavailable_text_prefix'), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . ''
|
||||||
. htmlspecialchars($reason, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')
|
. htmlspecialchars($reason, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')
|
||||||
. '</div>'
|
. '</div>'
|
||||||
. '</div>'
|
. '</div>'
|
||||||
. '</div>';
|
. '</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, string> $values
|
||||||
|
*/
|
||||||
|
private function renderAgentTemplate(string $template, array $values): string
|
||||||
|
{
|
||||||
|
foreach ($values as $key => $value) {
|
||||||
|
$template = str_replace('{' . $key . '}', $value, $template);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $template;
|
||||||
|
}
|
||||||
|
|
||||||
private function normalizeOneLine(string $value): string
|
private function normalizeOneLine(string $value): string
|
||||||
{
|
{
|
||||||
$value = trim($value);
|
$value = trim($value);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace App\Agent;
|
namespace App\Agent;
|
||||||
|
|
||||||
use App\Commerce\Dto\ShopProductResult;
|
use App\Commerce\Dto\ShopProductResult;
|
||||||
|
use App\Config\LanguageCleanupConfig;
|
||||||
use App\Config\PromptBuilderConfig;
|
use App\Config\PromptBuilderConfig;
|
||||||
use App\Context\ContextService;
|
use App\Context\ContextService;
|
||||||
use App\Repository\SystemPromptRepository;
|
use App\Repository\SystemPromptRepository;
|
||||||
@@ -19,6 +20,7 @@ final readonly class PromptBuilder
|
|||||||
private SystemPromptRepository $systemPromptRepository,
|
private SystemPromptRepository $systemPromptRepository,
|
||||||
private ModelGenerationConfigProvider $modelGenerationConfigProvider,
|
private ModelGenerationConfigProvider $modelGenerationConfigProvider,
|
||||||
private PromptBuilderConfig $config,
|
private PromptBuilderConfig $config,
|
||||||
|
private LanguageCleanupConfig $languageCleanupConfig,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -335,7 +337,7 @@ final readonly class PromptBuilder
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($hasShopResults && !$commerceSearchAttempted) {
|
if ($hasShopResults && !$commerceSearchAttempted) {
|
||||||
$rules[] = '- Treat shop results as provided context only; do not imply that a live shop check was performed in this run.';
|
$rules[] = $this->config->getFallbackEscalationProvidedShopResultsContextRule();
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($rules === []) {
|
if ($rules === []) {
|
||||||
@@ -609,7 +611,7 @@ final readonly class PromptBuilder
|
|||||||
$positiveContextTerms = $this->extractMeasurementGuardStringList($guard, 'positive_context_terms');
|
$positiveContextTerms = $this->extractMeasurementGuardStringList($guard, 'positive_context_terms');
|
||||||
$negativeContextTerms = $this->extractMeasurementGuardStringList($guard, 'negative_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'] ?? $this->config->getMeasurementEvidenceRuleTemplate('default_requested_parameter_label')));
|
||||||
$strictNoEvidence = (bool) ($guard['strict_no_evidence'] ?? true);
|
$strictNoEvidence = (bool) ($guard['strict_no_evidence'] ?? true);
|
||||||
$resolvedRequestedRole = $requestedRole ?? $this->resolveRequestedProductRole($prompt);
|
$resolvedRequestedRole = $requestedRole ?? $this->resolveRequestedProductRole($prompt);
|
||||||
$safeNoEvidenceAnswer = $this->normalizeBlockText((string) (
|
$safeNoEvidenceAnswer = $this->normalizeBlockText((string) (
|
||||||
@@ -634,58 +636,59 @@ final readonly class PromptBuilder
|
|||||||
|
|
||||||
if ($hasEvidence) {
|
if ($hasEvidence) {
|
||||||
$shopHasEvidence = true;
|
$shopHasEvidence = true;
|
||||||
$shopEvidenceLines[] = sprintf(
|
$shopEvidenceLines[] = $this->renderPromptTemplate($this->config->getMeasurementEvidenceRuleTemplate('shop_positive_evidence'), [
|
||||||
'- Shop record %d (%s): explicit positive evidence for %s is present in this same record.',
|
'index' => (string) ($index + 1),
|
||||||
$index + 1,
|
'product' => $productName !== '' ? $productName : $this->config->getMeasurementEvidenceRuleTemplate('unnamed_product'),
|
||||||
$productName !== '' ? $productName : 'unnamed product',
|
'label' => $label,
|
||||||
$label
|
]);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($shopEvidenceLines === []) {
|
if ($shopEvidenceLines === []) {
|
||||||
$shopEvidenceLines[] = sprintf(
|
$shopEvidenceLines[] = $this->renderPromptTemplate($this->config->getMeasurementEvidenceRuleTemplate('shop_no_evidence'), [
|
||||||
'- No shop product record shown to the model contains explicit positive evidence for %s in the same record.',
|
'label' => $label,
|
||||||
$label
|
]);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$rules = $this->config->getMeasurementEvidenceIntroRules();
|
$rules = $this->config->getMeasurementEvidenceIntroRules();
|
||||||
$rules = array_merge($rules, $this->config->getMeasurementEvidenceProductSpecificRules());
|
$rules = array_merge($rules, $this->config->getMeasurementEvidenceProductSpecificRules());
|
||||||
$rules[] = '- User requested measurement parameter: ' . $label . '.';
|
$rules[] = $this->renderPromptTemplate($this->config->getMeasurementEvidenceRuleTemplate('requested_parameter'), ['label' => $label]);
|
||||||
$rules[] = '- Positive parameter terms for this request: ' . implode(', ', $positiveTerms) . '.';
|
$rules[] = $this->renderPromptTemplate($this->config->getMeasurementEvidenceRuleTemplate('positive_terms'), ['terms' => implode(', ', $positiveTerms)]);
|
||||||
if ($positiveContextTerms !== []) {
|
if ($positiveContextTerms !== []) {
|
||||||
$rules[] = '- These parameter terms count as suitability evidence only in a measurement-purpose context such as: ' . implode(', ', $positiveContextTerms) . '.';
|
$rules[] = $this->renderPromptTemplate($this->config->getMeasurementEvidenceRuleTemplate('positive_context_terms'), ['terms' => implode(', ', $positiveContextTerms)]);
|
||||||
}
|
}
|
||||||
if ($negativeContextTerms !== []) {
|
if ($negativeContextTerms !== []) {
|
||||||
$rules[] = '- These contexts are not suitability evidence by themselves: ' . implode(', ', $negativeContextTerms) . '.';
|
$rules[] = $this->renderPromptTemplate($this->config->getMeasurementEvidenceRuleTemplate('negative_context_terms'), ['terms' => implode(', ', $negativeContextTerms)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($nonEquivalentTerms !== []) {
|
if ($nonEquivalentTerms !== []) {
|
||||||
$rules[] = '- Terms that must NOT be treated as equivalent positive evidence: ' . implode(', ', $nonEquivalentTerms) . '.';
|
$rules[] = $this->renderPromptTemplate($this->config->getMeasurementEvidenceRuleTemplate('non_equivalent_terms'), ['terms' => implode(', ', $nonEquivalentTerms)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$rules[] = '- RAG/URL evidence scan for this exact parameter: ' . ($knowledgeHasEvidence ? 'explicit positive evidence found.' : 'no explicit positive evidence found.');
|
$rules[] = $this->renderPromptTemplate($this->config->getMeasurementEvidenceRuleTemplate('rag_url_evidence_scan'), [
|
||||||
|
'state' => $knowledgeHasEvidence
|
||||||
|
? $this->config->getMeasurementEvidenceRuleTemplate('rag_url_evidence_found')
|
||||||
|
: $this->config->getMeasurementEvidenceRuleTemplate('rag_url_evidence_missing'),
|
||||||
|
]);
|
||||||
$rules = array_merge($rules, $shopEvidenceLines);
|
$rules = array_merge($rules, $shopEvidenceLines);
|
||||||
|
|
||||||
if (!$strictNoEvidence && !$knowledgeHasEvidence && !$shopHasEvidence) {
|
if (!$strictNoEvidence && !$knowledgeHasEvidence && !$shopHasEvidence) {
|
||||||
$rules[] = '- The deterministic exact-term scan did not find product-specific evidence. The answer may still use a clearly equivalent named measurement parameter from the same source record, but must not infer suitability from generic categories, document titles, tags, search terms, neighbouring products, or broad umbrella-topic wording.';
|
$rules[] = $this->config->getMeasurementEvidenceRuleTemplate('deterministic_scan_no_product_specific_evidence');
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($strictNoEvidence && !$knowledgeHasEvidence && !$shopHasEvidence) {
|
if ($strictNoEvidence && !$knowledgeHasEvidence && !$shopHasEvidence) {
|
||||||
$rules[] = '- Mandatory answer behavior: do not recommend a product as suitable for this measurement parameter.';
|
$rules[] = $this->config->getMeasurementEvidenceRuleTemplate('mandatory_no_recommendation');
|
||||||
if ($safeNoEvidenceAnswer !== '') {
|
if ($safeNoEvidenceAnswer !== '') {
|
||||||
$rules[] = '- Start the answer with this meaning in the user language: ' . $safeNoEvidenceAnswer;
|
$rules[] = $this->renderPromptTemplate($this->config->getMeasurementEvidenceRuleTemplate('start_answer_meaning'), ['answer' => $safeNoEvidenceAnswer]);
|
||||||
}
|
}
|
||||||
if ($resolvedRequestedRole === 'accessory_or_consumable') {
|
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.';
|
$rules[] = $this->config->getMeasurementEvidenceRuleTemplate('accessory_mismatch');
|
||||||
} else {
|
} 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[] = $this->config->getMeasurementEvidenceRuleTemplate('commercial_hits_only');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$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 = array_merge($rules, $this->config->getMeasurementEvidenceFinalRules());
|
||||||
$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(
|
||||||
$this->config->getMeasurementEvidenceSectionLabel(),
|
$this->config->getMeasurementEvidenceSectionLabel(),
|
||||||
@@ -693,6 +696,19 @@ final readonly class PromptBuilder
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, string> $values
|
||||||
|
*/
|
||||||
|
private function renderPromptTemplate(string $template, array $values): string
|
||||||
|
{
|
||||||
|
foreach ($values as $key => $value) {
|
||||||
|
$template = str_replace('{' . $key . '}', $value, $template);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $template;
|
||||||
|
}
|
||||||
|
|
||||||
private function buildShopMeasurementEvidenceLine(ShopProductResult $product, ?array $guard): string
|
private function buildShopMeasurementEvidenceLine(ShopProductResult $product, ?array $guard): string
|
||||||
{
|
{
|
||||||
if ($guard === null) {
|
if ($guard === null) {
|
||||||
@@ -702,23 +718,21 @@ final readonly class PromptBuilder
|
|||||||
$positiveTerms = $this->extractMeasurementGuardStringList($guard, 'positive_terms');
|
$positiveTerms = $this->extractMeasurementGuardStringList($guard, 'positive_terms');
|
||||||
$positiveContextTerms = $this->extractMeasurementGuardStringList($guard, 'positive_context_terms');
|
$positiveContextTerms = $this->extractMeasurementGuardStringList($guard, 'positive_context_terms');
|
||||||
$negativeContextTerms = $this->extractMeasurementGuardStringList($guard, 'negative_context_terms');
|
$negativeContextTerms = $this->extractMeasurementGuardStringList($guard, 'negative_context_terms');
|
||||||
$label = $this->normalizeBlockText((string) ($guard['label'] ?? 'requested measurement parameter'));
|
$label = $this->normalizeBlockText((string) ($guard['label'] ?? $this->config->getMeasurementEvidenceRuleTemplate('default_requested_parameter_label')));
|
||||||
|
|
||||||
if ($positiveTerms === []) {
|
if ($positiveTerms === []) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->shopProductHasMeasurementEvidence($product, $positiveTerms, $positiveContextTerms, $negativeContextTerms)) {
|
if ($this->shopProductHasMeasurementEvidence($product, $positiveTerms, $positiveContextTerms, $negativeContextTerms)) {
|
||||||
return sprintf(
|
return $this->renderPromptTemplate($this->config->getMeasurementEvidenceRuleTemplate('shop_record_positive_evidence_line'), [
|
||||||
'Requested measurement evidence: explicit positive evidence for %s is present in this same SHOP PRODUCT RECORD.',
|
'label' => $label,
|
||||||
$label
|
]);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return sprintf(
|
return $this->renderPromptTemplate($this->config->getMeasurementEvidenceRuleTemplate('shop_record_no_evidence_line'), [
|
||||||
'Requested measurement evidence: no explicit positive evidence for %s is present in this SHOP PRODUCT RECORD. Do not present this record as technically suitable for that measurement parameter.',
|
'label' => $label,
|
||||||
$label
|
]);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function resolveRequestedMeasurementGuard(string $prompt): ?array
|
private function resolveRequestedMeasurementGuard(string $prompt): ?array
|
||||||
@@ -815,11 +829,11 @@ final readonly class PromptBuilder
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$parts = preg_split('/\s*(?:,|;|\/|\boder\b|\bund\b|\bor\b|\band\b)\s*/iu', $value) ?: [$value];
|
$parts = preg_split($this->config->getParameterParsingSplitPattern(), $value) ?: [$value];
|
||||||
|
|
||||||
foreach ($parts as $part) {
|
foreach ($parts as $part) {
|
||||||
$part = $this->normalizeBlockText((string) $part);
|
$part = $this->normalizeBlockText((string) $part);
|
||||||
$part = trim($part, " \t\n\r\0\x0B-–—:()[]{}\"'`“”„");
|
$part = trim($part, $this->config->getParameterParsingTrimCharacters());
|
||||||
|
|
||||||
if ($part === '' || preg_match('/[\p{L}\p{N}]/u', $part) !== 1) {
|
if ($part === '' || preg_match('/[\p{L}\p{N}]/u', $part) !== 1) {
|
||||||
continue;
|
continue;
|
||||||
@@ -835,7 +849,7 @@ final readonly class PromptBuilder
|
|||||||
|
|
||||||
private function renderMeasurementEvidenceTemplate(string $template, string $label): string
|
private function renderMeasurementEvidenceTemplate(string $template, string $label): string
|
||||||
{
|
{
|
||||||
return strtr($template, ['{label}' => $label]);
|
return $this->renderPromptTemplate($template, ['label' => $label]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -992,7 +1006,7 @@ final readonly class PromptBuilder
|
|||||||
private function normalizeForMeasurementMatching(string $value): string
|
private function normalizeForMeasurementMatching(string $value): string
|
||||||
{
|
{
|
||||||
$value = mb_strtolower($this->normalizeBlockText($value), 'UTF-8');
|
$value = mb_strtolower($this->normalizeBlockText($value), 'UTF-8');
|
||||||
$value = str_replace(['‐', '‑', '‒', '–', '—'], '-', $value);
|
$value = $this->languageCleanupConfig->normalizeDashEquivalents($value);
|
||||||
$value = preg_replace('/<[^>]+>/u', ' ', $value) ?? $value;
|
$value = preg_replace('/<[^>]+>/u', ' ', $value) ?? $value;
|
||||||
$value = preg_replace('/\s+/u', ' ', $value) ?? $value;
|
$value = preg_replace('/\s+/u', ' ', $value) ?? $value;
|
||||||
|
|
||||||
|
|||||||
@@ -135,6 +135,22 @@ final class AgentRunnerConfig
|
|||||||
return $this->getRequiredString('follow_up_context.reference_anchor.hardness_value_pattern');
|
return $this->getRequiredString('follow_up_context.reference_anchor.hardness_value_pattern');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function getFollowUpContextPreviousUserQuestionTemplate(): string
|
||||||
|
{
|
||||||
|
return $this->getRequiredString('follow_up_context.context_labels.previous_user_question_template');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFollowUpContextPreviousReferenceAnchorsTemplate(): string
|
||||||
|
{
|
||||||
|
return $this->getRequiredString('follow_up_context.context_labels.previous_reference_anchors_template');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFollowUpContextCurrentQuestionTemplate(): string
|
||||||
|
{
|
||||||
|
return $this->getRequiredString('follow_up_context.context_labels.current_follow_up_question_template');
|
||||||
|
}
|
||||||
|
|
||||||
public function isInputNormalizationEnabled(): bool
|
public function isInputNormalizationEnabled(): bool
|
||||||
{
|
{
|
||||||
return $this->getRequiredBool('input_normalization.enabled');
|
return $this->getRequiredBool('input_normalization.enabled');
|
||||||
@@ -170,6 +186,14 @@ final class AgentRunnerConfig
|
|||||||
return $this->getRequiredString('input_normalization.output_prefix_pattern');
|
return $this->getRequiredString('input_normalization.output_prefix_pattern');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string[]
|
||||||
|
*/
|
||||||
|
public function getInputNormalizationPlaceholderOutputs(): array
|
||||||
|
{
|
||||||
|
return $this->getRequiredStringList('input_normalization.placeholder_outputs');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return string[]
|
* @return string[]
|
||||||
*/
|
*/
|
||||||
@@ -396,6 +420,45 @@ final class AgentRunnerConfig
|
|||||||
return $out;
|
return $out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, array{label:string, prompt:string}>
|
||||||
|
*/
|
||||||
|
private function getRequiredActionList(string $key): array
|
||||||
|
{
|
||||||
|
$value = $this->requiredValue($key);
|
||||||
|
|
||||||
|
if (!is_array($value)) {
|
||||||
|
throw new \InvalidArgumentException(sprintf('RetrieX agent config key "%s" must be a list of action definitions.', $key));
|
||||||
|
}
|
||||||
|
|
||||||
|
$out = [];
|
||||||
|
|
||||||
|
foreach ($value as $item) {
|
||||||
|
if (!is_array($item)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$label = isset($item['label']) && is_scalar($item['label']) ? trim((string) $item['label']) : '';
|
||||||
|
$prompt = isset($item['prompt']) && is_scalar($item['prompt']) ? trim((string) $item['prompt']) : '';
|
||||||
|
|
||||||
|
if ($label === '' || $prompt === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$out[] = [
|
||||||
|
'label' => $label,
|
||||||
|
'prompt' => $prompt,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($out === []) {
|
||||||
|
throw new \InvalidArgumentException(sprintf('RetrieX agent config key "%s" must contain at least one valid action definition.', $key));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array<string, string[]>
|
* @return array<string, string[]>
|
||||||
*/
|
*/
|
||||||
@@ -637,6 +700,55 @@ final class AgentRunnerConfig
|
|||||||
return $this->getRequiredString('no_llm_fallback.messages.no_data');
|
return $this->getRequiredString('no_llm_fallback.messages.no_data');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function getShopRepairCheckMessage(): string
|
||||||
|
{
|
||||||
|
return $this->getRequiredString('messages.shop_repair_check');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getShopQueryOptimizationHeartbeatMessage(): string
|
||||||
|
{
|
||||||
|
return $this->getRequiredString('messages.shop_query_optimization_heartbeat');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getProductionUiStageLabel(string $key): string
|
||||||
|
{
|
||||||
|
return $this->getRequiredString('production_ui.stage_labels.' . $key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getProductionUiConfidenceLabel(string $key): string
|
||||||
|
{
|
||||||
|
return $this->getRequiredString('production_ui.confidence_labels.' . $key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getProductionUiText(string $key): string
|
||||||
|
{
|
||||||
|
return $this->getRequiredString('production_ui.text.' . $key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getProductionUiTemplate(string $key): string
|
||||||
|
{
|
||||||
|
return $this->getRequiredString('production_ui.templates.' . $key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getProductionUiShopResultsMaxCards(): int
|
||||||
|
{
|
||||||
|
return $this->getRequiredInt('production_ui.shop_results.max_cards');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, array{label:string, prompt:string}>
|
||||||
|
*/
|
||||||
|
public function getProductionUiFollowUpActions(string $group): array
|
||||||
|
{
|
||||||
|
return $this->getRequiredActionList('production_ui.follow_up_actions.' . $group);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getNoLlmProductField(string $key): string
|
||||||
|
{
|
||||||
|
return $this->getRequiredString('no_llm_fallback.product_fields.' . $key);
|
||||||
|
}
|
||||||
|
|
||||||
public function getNoLlmFallbackNoShopResultsWithKnowledgeMessage(): string
|
public function getNoLlmFallbackNoShopResultsWithKnowledgeMessage(): string
|
||||||
{
|
{
|
||||||
return $this->getRequiredString('no_llm_fallback.messages.no_shop_results_with_knowledge');
|
return $this->getRequiredString('no_llm_fallback.messages.no_shop_results_with_knowledge');
|
||||||
|
|||||||
@@ -65,6 +65,29 @@ final class LanguageCleanupConfig
|
|||||||
return strtr($value, $map);
|
return strtr($value, $map);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** @return string[] */
|
||||||
|
public function getWordSeparatorCharacters(): array
|
||||||
|
{
|
||||||
|
return $this->getNormalizationStringList('word_separator_chars');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return string[] */
|
||||||
|
public function getDashEquivalents(): array
|
||||||
|
{
|
||||||
|
return $this->getNormalizationStringList('dash_equivalents');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function replaceWordSeparatorsWithSpace(string $value): string
|
||||||
|
{
|
||||||
|
return str_replace($this->getWordSeparatorCharacters(), ' ', $value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function normalizeDashEquivalents(string $value): string
|
||||||
|
{
|
||||||
|
return str_replace($this->getDashEquivalents(), '-', $value);
|
||||||
|
}
|
||||||
|
|
||||||
/** @return string[] */
|
/** @return string[] */
|
||||||
public function getCleanupProfileNames(): array
|
public function getCleanupProfileNames(): array
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -306,6 +306,11 @@ final class PromptBuilderConfig
|
|||||||
return $this->getRequiredString('fallback_escalation.state_line_template');
|
return $this->getRequiredString('fallback_escalation.state_line_template');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getFallbackEscalationProvidedShopResultsContextRule(): string
|
||||||
|
{
|
||||||
|
return $this->getRequiredString('fallback_escalation.provided_shop_results_context_rule');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return string[]
|
* @return string[]
|
||||||
*/
|
*/
|
||||||
@@ -623,6 +628,29 @@ final class PromptBuilderConfig
|
|||||||
return $this->getRequiredString('measurement_evidence_guard.generic_safe_no_accessory_evidence_answer_template_de');
|
return $this->getRequiredString('measurement_evidence_guard.generic_safe_no_accessory_evidence_answer_template_de');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getMeasurementEvidenceRuleTemplate(string $key): string
|
||||||
|
{
|
||||||
|
return $this->getRequiredString('measurement_evidence_guard.rule_templates.' . $key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string[]
|
||||||
|
*/
|
||||||
|
public function getMeasurementEvidenceFinalRules(): array
|
||||||
|
{
|
||||||
|
return $this->getRequiredStringList('measurement_evidence_guard.final_rules');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getParameterParsingSplitPattern(): string
|
||||||
|
{
|
||||||
|
return $this->getRequiredString('parameter_parsing.split_pattern');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getParameterParsingTrimCharacters(): string
|
||||||
|
{
|
||||||
|
return $this->getRequiredString('parameter_parsing.trim_characters');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array<int, array<string, mixed>>
|
* @return array<int, array<string, mixed>>
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Knowledge\Retrieval;
|
namespace App\Knowledge\Retrieval;
|
||||||
|
|
||||||
|
use App\Config\LanguageCleanupConfig;
|
||||||
use App\Config\NdjsonHybridRetrieverConfig;
|
use App\Config\NdjsonHybridRetrieverConfig;
|
||||||
use App\Knowledge\ChunkManager;
|
use App\Knowledge\ChunkManager;
|
||||||
|
|
||||||
@@ -11,7 +12,8 @@ final readonly class NdjsonChunkLookup
|
|||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private ChunkManager $chunkManager,
|
private ChunkManager $chunkManager,
|
||||||
private NdjsonHybridRetrieverConfig $retrieverConfig
|
private NdjsonHybridRetrieverConfig $retrieverConfig,
|
||||||
|
private LanguageCleanupConfig $languageCleanupConfig
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -416,7 +418,7 @@ final readonly class NdjsonChunkLookup
|
|||||||
private function normalizeText(string $value): string
|
private function normalizeText(string $value): string
|
||||||
{
|
{
|
||||||
$value = mb_strtolower(trim($value), 'UTF-8');
|
$value = mb_strtolower(trim($value), 'UTF-8');
|
||||||
$value = str_replace(['-', '/', '_'], ' ', $value);
|
$value = $this->languageCleanupConfig->replaceWordSeparatorsWithSpace($value);
|
||||||
$value = preg_replace('/[^\p{L}\p{N}\s]+/u', ' ', $value) ?? $value;
|
$value = preg_replace('/[^\p{L}\p{N}\s]+/u', ' ', $value) ?? $value;
|
||||||
$value = preg_replace('/\s+/u', ' ', $value) ?? $value;
|
$value = preg_replace('/\s+/u', ' ', $value) ?? $value;
|
||||||
|
|
||||||
|
|||||||
@@ -1689,7 +1689,7 @@ final readonly class NdjsonHybridRetriever implements RetrieverInterface
|
|||||||
private function normalizeText(string $value): string
|
private function normalizeText(string $value): string
|
||||||
{
|
{
|
||||||
$value = mb_strtolower(trim($value), 'UTF-8');
|
$value = mb_strtolower(trim($value), 'UTF-8');
|
||||||
$value = str_replace(['-', '/', '_'], ' ', $value);
|
$value = $this->languageCleanupConfig->replaceWordSeparatorsWithSpace($value);
|
||||||
$value = preg_replace('/[^\p{L}\p{N}\s]+/u', ' ', $value) ?? $value;
|
$value = preg_replace('/[^\p{L}\p{N}\s]+/u', ' ', $value) ?? $value;
|
||||||
$value = preg_replace('/\s+/u', ' ', $value) ?? $value;
|
$value = preg_replace('/\s+/u', ' ', $value) ?? $value;
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Knowledge\Retrieval;
|
namespace App\Knowledge\Retrieval;
|
||||||
|
|
||||||
|
use App\Config\LanguageCleanupConfig;
|
||||||
use App\Knowledge\StopWords;
|
use App\Knowledge\StopWords;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
use SQLite3;
|
use SQLite3;
|
||||||
@@ -18,6 +19,7 @@ final readonly class NdjsonKeywordRetriever
|
|||||||
private string $projectDir,
|
private string $projectDir,
|
||||||
private LoggerInterface $agentLogger,
|
private LoggerInterface $agentLogger,
|
||||||
private StopWords $stopWords,
|
private StopWords $stopWords,
|
||||||
|
private LanguageCleanupConfig $languageCleanupConfig,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,7 +179,7 @@ final readonly class NdjsonKeywordRetriever
|
|||||||
private function normalizeText(string $value): string
|
private function normalizeText(string $value): string
|
||||||
{
|
{
|
||||||
$value = mb_strtolower(trim($value), 'UTF-8');
|
$value = mb_strtolower(trim($value), 'UTF-8');
|
||||||
$value = str_replace(['-', '/', '_'], ' ', $value);
|
$value = $this->languageCleanupConfig->replaceWordSeparatorsWithSpace($value);
|
||||||
$value = preg_replace('/[^\p{L}\p{N}\s]+/u', ' ', $value) ?? $value;
|
$value = preg_replace('/[^\p{L}\p{N}\s]+/u', ' ', $value) ?? $value;
|
||||||
$value = preg_replace('/\s+/u', ' ', $value) ?? $value;
|
$value = preg_replace('/\s+/u', ' ', $value) ?? $value;
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Knowledge\Retrieval;
|
namespace App\Knowledge\Retrieval;
|
||||||
|
|
||||||
|
use App\Config\LanguageCleanupConfig;
|
||||||
use App\Knowledge\StopWords;
|
use App\Knowledge\StopWords;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
use SQLite3;
|
use SQLite3;
|
||||||
@@ -23,6 +24,7 @@ final readonly class NdjsonLexicalIndexBuilder
|
|||||||
private string $projectDir,
|
private string $projectDir,
|
||||||
private LoggerInterface $agentLogger,
|
private LoggerInterface $agentLogger,
|
||||||
private StopWords $stopWords,
|
private StopWords $stopWords,
|
||||||
|
private LanguageCleanupConfig $languageCleanupConfig,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -350,7 +352,7 @@ final readonly class NdjsonLexicalIndexBuilder
|
|||||||
private function normalizeText(string $value): string
|
private function normalizeText(string $value): string
|
||||||
{
|
{
|
||||||
$value = mb_strtolower(trim($value), 'UTF-8');
|
$value = mb_strtolower(trim($value), 'UTF-8');
|
||||||
$value = str_replace(['-', '/', '_'], ' ', $value);
|
$value = $this->languageCleanupConfig->replaceWordSeparatorsWithSpace($value);
|
||||||
$value = preg_replace('/[^\p{L}\p{N}\s]+/u', ' ', $value) ?? $value;
|
$value = preg_replace('/[^\p{L}\p{N}\s]+/u', ' ', $value) ?? $value;
|
||||||
$value = preg_replace('/\s+/u', ' ', $value) ?? $value;
|
$value = preg_replace('/\s+/u', ' ', $value) ?? $value;
|
||||||
|
|
||||||
|
|||||||
@@ -4,12 +4,14 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Knowledge\Retrieval;
|
namespace App\Knowledge\Retrieval;
|
||||||
|
|
||||||
|
use App\Config\LanguageCleanupConfig;
|
||||||
use App\Knowledge\StopWords;
|
use App\Knowledge\StopWords;
|
||||||
|
|
||||||
final readonly class QueryCleaner
|
final readonly class QueryCleaner
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private StopWords $stopWords
|
private StopWords $stopWords,
|
||||||
|
private LanguageCleanupConfig $languageCleanupConfig
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,7 +35,7 @@ final readonly class QueryCleaner
|
|||||||
$query = mb_strtolower($query, 'UTF-8');
|
$query = mb_strtolower($query, 'UTF-8');
|
||||||
|
|
||||||
// 2. Treat hyphens and slashes as word separators
|
// 2. Treat hyphens and slashes as word separators
|
||||||
$query = str_replace(['-', '/'], ' ', $query);
|
$query = $this->languageCleanupConfig->replaceWordSeparatorsWithSpace($query);
|
||||||
|
|
||||||
// 3. Remove special characters, but keep:
|
// 3. Remove special characters, but keep:
|
||||||
// - letters
|
// - letters
|
||||||
|
|||||||
Reference in New Issue
Block a user