$config */ public function __construct( private readonly array $config = [], ) { } public function getCommerceHistoryBudgetChars(): int { return $this->getInt('commerce_history_budget_chars', 1000); } public function getProductSearchKnowledgeChunkLimit(): int { return $this->getInt('product_search_knowledge_chunk_limit', 6); } public function getAdvisoryProductSearchKnowledgeChunkLimit(): int { return $this->getInt('advisory_product_search_knowledge_chunk_limit', 9); } public function getOptimizedShopQueryPrefixPattern(): string { return $this->getString('optimized_shop_query_prefix_pattern', '/^(?:keywords?|suchquery|search\\s*query|query)\\s*:\\s*/iu'); } public function getOptimizedShopQueryTrimCharacters(): string { return $this->getString('optimized_shop_query_trim_characters', " \t\n\r\0\x0B\"'`"); } private function getInt(string $key, int $default): int { $value = $this->value($key, $default); return is_numeric($value) ? (int) $value : $default; } private function getBool(string $key, bool $default): bool { $value = $this->value($key, $default); if (is_bool($value)) { return $value; } if (is_scalar($value)) { $normalized = strtolower(trim((string) $value)); if (in_array($normalized, ['1', 'true', 'yes', 'on'], true)) { return true; } if (in_array($normalized, ['0', 'false', 'no', 'off'], true)) { return false; } } return $default; } private function getString(string $key, string $default): string { $value = $this->value($key, $default); return is_string($value) && $value !== '' ? $value : $default; } /** * @param string[] $default * @return string[] */ private function getStringList(string $key, array $default): array { $value = $this->value($key, $default); if (!is_array($value)) { return $default; } $out = []; foreach ($value as $item) { if (!is_scalar($item)) { continue; } $item = trim((string) $item); if ($item !== '') { $out[] = $item; } } return $out !== [] ? $out : $default; } private function value(string $key, mixed $default): mixed { $current = $this->config; foreach (explode('.', $key) as $segment) { if (!is_array($current) || !array_key_exists($segment, $current)) { return $default; } $current = $current[$segment]; } return $current; } public function getEmptyPromptMessage(): string { return $this->getString('messages.empty_prompt', '❌ Empty prompt.'); } public function getAnalyzeRequestMessage(): string { return $this->getString('messages.analyze_request', 'Ich analysiere deine Anfrage...'); } public function getCheckInternetSourcesMessage(): string { return $this->getString('messages.check_internet_sources', 'Ich prüfe auf Internetquellen...'); } public function getRetrieveKnowledgeMessage(): string { return $this->getString('messages.retrieve_knowledge', 'Ich hole relevante Daten aus meinem RAG-Wissen...'); } public function getOptimizeSearchMessage(): string { return $this->getString('messages.optimize_search', 'Ich optimiere die Recherche...'); } public function getNoConcreteShopQueryMessage(): string { return $this->getString( 'messages.no_concrete_shop_query', 'Ich habe keine konkrete Shop-Suchanfrage erkannt. Bitte nenne das Produkt, Zubehör oder die Artikelnummer.' ); } public function getFetchSearchDataMessageTemplate(): string { return $this->getString('messages.fetch_search_data_template', 'Ich rufe Recherchedaten ab (type: %s)'); } public function getAnalyzeAllInformationMessage(): string { return $this->getString('messages.analyze_all_information', 'Ich analysiere alle Informationen...'); } public function getThinkingWhileStreamingMessage(): string { return $this->getString('messages.thinking_while_streaming', 'Denke nach...'); } public function getNoLlmDataReceivedMessage(): string { return $this->getString('messages.no_llm_data_received', '❌ Es wurden keine Daten vom LLM empfangen.'); } public function getGenericInternalErrorMessage(): string { return $this->getString('messages.generic_internal_error', '❌ Bei der Verarbeitung der Anfrage ist ein interner Fehler aufgetreten.'); } public function getDebugInternalErrorPrefix(): string { return $this->getString('messages.debug_internal_error_prefix', '❌ Interner Fehler: '); } public function getExternalUrlSourceLabel(): string { return $this->getString('source_labels.external_url', 'Externe URL'); } public function getRagKnowledgeSourceLabel(): string { return $this->getString('source_labels.rag_knowledge', 'RAG Wissen'); } public function getConversationHistorySourceLabel(): string { return $this->getString('source_labels.conversation_history', 'Chatverlauf'); } public function getShopSystemSourceLabel(): string { return $this->getString('source_labels.shop_system', 'Shopsystem'); } public function getExtendedShopSearchSourceLabel(): string { return $this->getString('source_labels.extended_shop_search', 'Erweiterte Shopsuche'); } public function getUsedSourcesPrefix(): string { return $this->getString('source_labels.used_sources_prefix', 'Genutzte Quellen: '); } public function getSourcesPrefix(): string { return $this->getString('source_labels.sources_prefix', 'Quellen: '); } public function getSourceBadgeHtmlTemplate(): string { return $this->getString('html.source_badge_template', '%s'); } public function getErrorHtmlTemplate(): string { return $this->getString('html.error_template', '
Hinweis
%s
' . "\n"); } public function getThinkHtmlTemplate(): string { return $this->getString('html.think_template', '%s' . "\n"); } public function getInfoHtmlTemplate(): string { return $this->getString('html.info_template', "\n\n" . '%s' . "\n"); } public function getDebugHtmlTemplate(): string { return $this->getString('html.debug_template', "\n\nDEBUG: %s\n"); } public function getShopPrompt(string $prompt, string $commerceHistoryContext = ''): string { $historyBlock = ''; if (trim($commerceHistoryContext) !== '') { $historyBlock = $this->buildHistoryBlock($commerceHistoryContext); } return $this->implodePromptBlocks([ $this->getShopPromptIntro(), $this->buildRulesBlock($this->getShopPromptRules()), $this->getShopPromptOutputFormatBlock(), $historyBlock, $this->getCurrentUserInputLabel() . ':', trim($prompt), ]); } private function buildHistoryBlock(string $commerceHistoryContext): string { return $this->implodePromptBlocks([ $this->getRecentConversationContextLabel() . ':', trim($commerceHistoryContext), $this->buildRulesBlock($this->getConversationContextRules(), 'Additional rules for conversation context:'), ]); } /** * @return string[] */ public function getShopPromptRules(): array { return $this->getStringList('shop_prompt.rules', [ '- Output only the final search query.', '- Always convert relevant search terms to their singular form.', '- No introduction, no explanation, no quotation marks.', '- Use only shop-relevant search terms from the user input for a shop search.', '- Maximum 6 search terms, preferably fewer.', '- Remove filler words, polite phrases, and irrelevant words.', '- Preserve product names, brands, model numbers, and compound terms exactly if they are relevant.', '- Preserve the language of the CURRENT USER INPUT for generic product/search terms; do not translate German search terms into English.', '- For German user input, output German shop terms, for example "freies Chlor Messung" instead of "free chlorine measurement".', '- Preserve domain terms from the current user input or resolved context in their original language.', '- Numbers that belong to a product name or model must be preserved (e.g. Indikator 300, Testomat 808, Testomat 2000).', '- Separate terms using spaces only.', '- If a relevant product name is present, it must be placed at the beginning of the final search query.', '- Try to always identify all products mentioned in the user input text, even in long prompts.', '- Look for terms such as Testomat, Horiba, Tritromat, or words like indicator/Indikator.', '- If the current user input is vague or referential, use the recent conversation context only as support.', '- Do not output words that only describe conversation flow, such as "same", "again", "also", or "like above".', ]); } /** * @return string[] */ public function getConversationContextRules(): array { return $this->getStringList('shop_prompt.conversation_context_rules', [ '- The current user input has highest priority.', '- Use the recent conversation context only to resolve omitted references.', '- Use it only for product carry-over, brand carry-over, model carry-over, or variant follow-ups.', '- Do not revive older products unless the current user input clearly refers to them.', '- If the current input starts a new topic, ignore older product context.', '- Prefer the most recent product reference over older ones.', ]); } public function getShopPromptIntro(): string { return $this->getString('shop_prompt.intro', 'Generate a short search query for Shopware 6 from the following user input text.'); } public function getShopPromptOutputFormatBlock(): string { return $this->getString('shop_prompt.output_format_block', "Output format:\nKeyword1 Keyword2 Keyword3"); } public function getRecentConversationContextLabel(): string { return $this->getString('shop_prompt.recent_conversation_context_label', 'RECENT CONVERSATION CONTEXT'); } public function getCurrentUserInputLabel(): string { return $this->getString('shop_prompt.current_user_input_label', 'CURRENT USER INPUT'); } public function isShopQueryLanguagePreservationEnabled(): bool { return $this->getBool('shop_prompt.language_preservation.enabled', true); } /** * @return array */ public function getShopQueryLanguageMarkers(): array { $default = [ 'de' => [ ' ä ', ' ö ', ' ü ', ' ß ', ' der ', ' die ', ' das ', ' ein ', ' eine ', ' einer ', ' einen ', ' welchem ', ' welchen ', ' welche ', ' welcher ', ' kann ', ' nutzen ', ' zur ', ' für ', ' fuer ', ' messung ', ' indikator ', ' reagenz ', ' chlor ', ], ]; $value = $this->value('shop_prompt.language_preservation.language_markers', $default); if (!is_array($value)) { return $default; } $out = []; foreach ($value as $language => $markers) { if (!is_string($language) || !is_array($markers)) { continue; } $cleanMarkers = []; foreach ($markers as $marker) { if (!is_scalar($marker)) { continue; } $marker = strtolower((string) $marker); if ($marker !== '') { $cleanMarkers[] = $marker; } } if ($cleanMarkers !== []) { $out[$language] = array_values(array_unique($cleanMarkers)); } } return $out !== [] ? $out : $default; } /** * @return array */ public function isShopQueryMetaGuardEnabled(): bool { return $this->getBool('shop_prompt.meta_query_guard.enabled', true); } /** * @return string[] */ public function getShopQueryMetaOnlyTerms(): array { return $this->getStringList('shop_prompt.meta_query_guard.meta_only_terms', [ 'shop', 'shopsuche', 'shop-suche', 'suche', 'suchen', 'such', 'finde', 'find', 'zeige', 'zeig', 'bitte', 'mal', 'im', 'in', 'nach', 'den', 'die', 'das', 'der', 'dem', ]); } public function isShopQueryContextFallbackEnabled(): bool { return $this->getBool('shop_prompt.meta_query_guard.context_fallback_enabled', true); } public function getShopQueryContextFallbackQuestionLimit(): int { return $this->getInt('shop_prompt.meta_query_guard.context_fallback_question_limit', 12); } public function getShopQueryContextFallbackHistoryBudgetChars(): int { return $this->getInt('shop_prompt.meta_query_guard.context_fallback_history_budget_chars', 20000); } public function shouldUseFullHistoryForShopQueryContextFallback(): bool { return $this->getBool('shop_prompt.meta_query_guard.context_fallback_use_full_history', true); } public function getShopQueryContextFallbackMaxTerms(): int { return $this->getInt('shop_prompt.meta_query_guard.context_fallback_max_terms', 6); } /** * @return string[] */ public function getShopQueryContextFallbackFilterTerms(): array { return $this->getStringList('shop_prompt.meta_query_guard.context_fallback_filter_terms', [ 'mit', 'welche', 'welcher', 'welches', 'welchem', 'welchen', 'was', 'wie', 'wo', 'kann', 'koennen', 'können', 'konnte', 'könnte', 'ich', 'wir', 'man', 'nutzen', 'benutzen', 'verwenden', 'verwende', 'nehmen', 'zur', 'zum', 'für', 'fuer', 'messen', 'gemessen', 'messung', ]); } public function isShopQueryContextAnchorEnrichmentEnabled(): bool { return $this->getBool('shop_prompt.context_anchor_enrichment.enabled', true); } public function getShopQueryContextAnchorEnrichmentMaxQueryTerms(): int { return $this->getInt('shop_prompt.context_anchor_enrichment.max_query_terms', 2); } /** * @return string[] */ public function getShopQueryContextAnchorEnrichmentTriggerTerms(): array { return $this->getStringList('shop_prompt.context_anchor_enrichment.trigger_terms', [ 'indikator', 'indikatortyp', 'indicator', 'reagenz', 'reagenzsatz', 'reagent', 'zubehör', 'zubehor', 'accessory', ]); } /** * @return string[] */ public function getShopQueryContextAnchorEnrichmentPatterns(): array { return $this->getStringList('shop_prompt.context_anchor_enrichment.anchor_patterns', [ '/\b(?:indikator(?:typ)?|indicator(?:\s+type)?|reagenz(?:satz|typ)?|reagent(?:\s+set|\s+type)?|typ|type)\s+[A-Za-zÄÖÜäöüß]{0,8}\s*\d{1,5}(?:\s*[A-ZÄÖÜ]{1,4})?(?:\s*%)?\b/iu', ]); } public function getShopQueryContextAnchorEnrichmentTemplate(): string { return $this->getString('shop_prompt.context_anchor_enrichment.template', '{anchor} {query}'); } public function getShopQueryTranslationReplacements(string $language): array { $default = [ 'de' => [ 'free chlorine' => 'freies chlor', 'free chlor' => 'freies chlor', 'total chlorine' => 'gesamtchlor', 'chlorine measurement' => 'chlor messung', 'water hardness' => 'wasserhärte', 'measurement' => 'messung', 'measuring' => 'messung', 'chlorine' => 'chlor', 'indicator' => 'indikator', 'indicators' => 'indikatoren', 'reagent' => 'reagenz', 'reagents' => 'reagenzien', 'accessory' => 'zubehör', 'accessories' => 'zubehör', ], ]; $value = $this->value( 'shop_prompt.language_preservation.translation_replacements.' . $language, $default[$language] ?? [] ); if (!is_array($value)) { return $default[$language] ?? []; } $out = []; foreach ($value as $source => $target) { if (!is_scalar($source) || !is_scalar($target)) { continue; } $source = strtolower(trim((string) $source)); $target = trim((string) $target); if ($source !== '' && $target !== '') { $out[$source] = $target; } } uksort($out, static fn(string $a, string $b): int => strlen($b) <=> strlen($a)); return $out !== [] ? $out : ($default[$language] ?? []); } private function buildRulesBlock(array $rules, string $headline = 'Rules:'): string { return $headline . "\n" . implode("\n", $rules); } /** * @param string[] $blocks */ private function implodePromptBlocks(array $blocks): string { $normalized = array_values(array_filter( array_map( static fn(string $block): string => trim($block), $blocks ), static fn(string $block): bool => $block !== '' )); return implode("\n\n", $normalized); } }