$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', '
%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