harden history find tokens and shops earch
This commit is contained in:
@@ -46,6 +46,29 @@ final class AgentRunnerConfig
|
||||
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);
|
||||
@@ -122,6 +145,14 @@ final class AgentRunnerConfig
|
||||
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)');
|
||||
@@ -252,11 +283,14 @@ final class AgentRunnerConfig
|
||||
'- 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.',
|
||||
'- 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".',
|
||||
]);
|
||||
@@ -297,6 +331,253 @@ final class AgentRunnerConfig
|
||||
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<string, string[]>
|
||||
*/
|
||||
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<string, string>
|
||||
*/
|
||||
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);
|
||||
@@ -317,4 +598,4 @@ final class AgentRunnerConfig
|
||||
|
||||
return implode("\n\n", $normalized);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,6 +192,47 @@ final readonly class RetriexEffectiveConfigProvider
|
||||
$errors[] = 'Shop query optimizer prompt no longer contains the original query.';
|
||||
}
|
||||
|
||||
$metaOnlyTerms = $this->agentRunnerConfig->getShopQueryMetaOnlyTerms();
|
||||
foreach (['shop', 'suche'] as $term) {
|
||||
$key = 'shop_query_meta_guard_term_' . $term;
|
||||
$checks[$key] = in_array($term, $metaOnlyTerms, true);
|
||||
if (!$checks[$key]) {
|
||||
$errors[] = 'Missing shop query meta guard term: ' . $term;
|
||||
}
|
||||
}
|
||||
$checks['shop_query_context_fallback_enabled'] = $this->agentRunnerConfig->isShopQueryContextFallbackEnabled();
|
||||
if (!$checks['shop_query_context_fallback_enabled']) {
|
||||
$errors[] = 'Shop query context fallback is disabled.';
|
||||
}
|
||||
|
||||
$contextFallbackFilterTerms = $this->agentRunnerConfig->getShopQueryContextFallbackFilterTerms();
|
||||
foreach (['welchem', 'kann', 'messen'] as $term) {
|
||||
$key = 'shop_query_context_fallback_filter_' . $term;
|
||||
$checks[$key] = in_array($term, $contextFallbackFilterTerms, true);
|
||||
if (!$checks[$key]) {
|
||||
$errors[] = 'Missing shop query context fallback filter term: ' . $term;
|
||||
}
|
||||
}
|
||||
$checks['shop_query_context_fallback_history_budget_positive'] = $this->agentRunnerConfig->getShopQueryContextFallbackHistoryBudgetChars() > 0;
|
||||
if (!$checks['shop_query_context_fallback_history_budget_positive']) {
|
||||
$errors[] = 'Shop query context fallback history budget must be greater than zero.';
|
||||
}
|
||||
|
||||
$checks['shop_query_context_fallback_full_history_enabled'] = $this->agentRunnerConfig->shouldUseFullHistoryForShopQueryContextFallback();
|
||||
if (!$checks['shop_query_context_fallback_full_history_enabled']) {
|
||||
$errors[] = 'Shop query context fallback full-history fallback is disabled.';
|
||||
}
|
||||
|
||||
$checks['shop_query_context_fallback_question_limit_minimum'] = $this->agentRunnerConfig->getShopQueryContextFallbackQuestionLimit() >= 6;
|
||||
if (!$checks['shop_query_context_fallback_question_limit_minimum']) {
|
||||
$errors[] = 'Shop query context fallback question limit is too low for repeated meta follow-ups.';
|
||||
}
|
||||
|
||||
$checks['shop_query_context_fallback_max_terms_positive'] = $this->agentRunnerConfig->getShopQueryContextFallbackMaxTerms() > 0;
|
||||
if (!$checks['shop_query_context_fallback_max_terms_positive']) {
|
||||
$errors[] = 'Shop query context fallback max terms must be greater than zero.';
|
||||
}
|
||||
|
||||
$status = $errors === [] ? 'OK' : 'ERROR';
|
||||
|
||||
return [
|
||||
@@ -362,6 +403,7 @@ final readonly class RetriexEffectiveConfigProvider
|
||||
'check_internet_sources' => $this->agentRunnerConfig->getCheckInternetSourcesMessage(),
|
||||
'retrieve_knowledge' => $this->agentRunnerConfig->getRetrieveKnowledgeMessage(),
|
||||
'optimize_search' => $this->agentRunnerConfig->getOptimizeSearchMessage(),
|
||||
'no_concrete_shop_query' => $this->agentRunnerConfig->getNoConcreteShopQueryMessage(),
|
||||
'fetch_search_data_template' => $this->agentRunnerConfig->getFetchSearchDataMessageTemplate(),
|
||||
'analyze_all_information' => $this->agentRunnerConfig->getAnalyzeAllInformationMessage(),
|
||||
'thinking_while_streaming' => $this->agentRunnerConfig->getThinkingWhileStreamingMessage(),
|
||||
@@ -392,6 +434,28 @@ final readonly class RetriexEffectiveConfigProvider
|
||||
'output_format_block' => $this->agentRunnerConfig->getShopPromptOutputFormatBlock(),
|
||||
'recent_conversation_context_label' => $this->agentRunnerConfig->getRecentConversationContextLabel(),
|
||||
'current_user_input_label' => $this->agentRunnerConfig->getCurrentUserInputLabel(),
|
||||
'language_preservation' => [
|
||||
'enabled' => $this->agentRunnerConfig->isShopQueryLanguagePreservationEnabled(),
|
||||
'language_markers' => $this->agentRunnerConfig->getShopQueryLanguageMarkers(),
|
||||
'translation_replacements_de' => $this->agentRunnerConfig->getShopQueryTranslationReplacements('de'),
|
||||
],
|
||||
'context_anchor_enrichment' => [
|
||||
'enabled' => $this->agentRunnerConfig->isShopQueryContextAnchorEnrichmentEnabled(),
|
||||
'max_query_terms' => $this->agentRunnerConfig->getShopQueryContextAnchorEnrichmentMaxQueryTerms(),
|
||||
'trigger_terms' => $this->agentRunnerConfig->getShopQueryContextAnchorEnrichmentTriggerTerms(),
|
||||
'anchor_patterns' => $this->agentRunnerConfig->getShopQueryContextAnchorEnrichmentPatterns(),
|
||||
'template' => $this->agentRunnerConfig->getShopQueryContextAnchorEnrichmentTemplate(),
|
||||
],
|
||||
'meta_query_guard' => [
|
||||
'enabled' => $this->agentRunnerConfig->isShopQueryMetaGuardEnabled(),
|
||||
'context_fallback_use_full_history' => $this->agentRunnerConfig->shouldUseFullHistoryForShopQueryContextFallback(),
|
||||
'meta_only_terms' => $this->agentRunnerConfig->getShopQueryMetaOnlyTerms(),
|
||||
'context_fallback_enabled' => $this->agentRunnerConfig->isShopQueryContextFallbackEnabled(),
|
||||
'context_fallback_question_limit' => $this->agentRunnerConfig->getShopQueryContextFallbackQuestionLimit(),
|
||||
'context_fallback_history_budget_chars' => $this->agentRunnerConfig->getShopQueryContextFallbackHistoryBudgetChars(),
|
||||
'context_fallback_max_terms' => $this->agentRunnerConfig->getShopQueryContextFallbackMaxTerms(),
|
||||
'context_fallback_filter_terms' => $this->agentRunnerConfig->getShopQueryContextFallbackFilterTerms(),
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
@@ -795,6 +859,15 @@ final readonly class RetriexEffectiveConfigProvider
|
||||
$this->validateStringListMap($agent['html_templates'] ?? [], 'agent.html_templates', $errors, $warnings);
|
||||
$this->validateStringListMap($agent['shop_query_optimizer'] ?? [], 'agent.shop_query_optimizer', $errors, $warnings);
|
||||
$this->validateRegexPattern($agent['optimized_shop_query_prefix_pattern'] ?? null, 'agent.optimized_shop_query_prefix_pattern', $errors);
|
||||
|
||||
$anchorEnrichment = $agent['shop_query_optimizer']['context_anchor_enrichment'] ?? [];
|
||||
if (is_array($anchorEnrichment)) {
|
||||
$this->validateStringList($this->toList($anchorEnrichment['trigger_terms'] ?? []), 'agent.shop_query_optimizer.context_anchor_enrichment.trigger_terms', $errors, $warnings);
|
||||
$this->validateRegexPatternList($anchorEnrichment['anchor_patterns'] ?? [], 'agent.shop_query_optimizer.context_anchor_enrichment.anchor_patterns', $errors);
|
||||
if (trim((string) ($anchorEnrichment['template'] ?? '')) === '') {
|
||||
$errors[] = 'agent.shop_query_optimizer.context_anchor_enrichment.template must not be empty.';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user