patch 7.1

This commit is contained in:
team 1
2026-04-30 15:39:24 +02:00
parent 652f54c674
commit 04fb092193
2 changed files with 162 additions and 214 deletions

View File

@@ -0,0 +1,44 @@
# RetrieX Patch 7.0b - AgentRunner messages, labels and HTML YAML-only
## Scope
This patch keeps the AgentRunnerConfig migration small and focused. It converts only these AgentRunnerConfig areas from YAML-with-PHP-fallback to YAML-only:
- runtime/status messages
- No-LLM fallback messages and product-role keyword lists
- RAG evidence guard stop terms and synonyms
- source labels
- HTML templates
It does not change Shop-Prompt, Meta-Guard, context-anchor enrichment, PromptBuilder, Retrieval, CommerceQueryParser, ShopService or SSE logic.
## Changed file
- `src/Config/AgentRunnerConfig.php`
## Expected audit effect
The following AgentRunner fallback accessors should disappear from `mto:agent:config:audit-source --details`:
- `messages.*`
- `no_llm_fallback.*`
- `rag_evidence_guard.*`
- `source_labels.*`
- `html.*`
The remaining AgentRunner fallbacks should mostly be the `shop_prompt.*` group, which is intentionally left for Patch 7.0c.
## Validation
Run after applying:
```bash
php bin/console cache:clear
php bin/console mto:agent:config:validate
php bin/console mto:agent:config:audit-source --details
php bin/console mto:agent:regression:test
```
## Notes
No YAML values were changed in this patch. It assumes the existing `config/retriex/agent.yaml` already contains the mapped values, which was confirmed by the previous audit state.

View File

@@ -6,106 +6,6 @@ namespace App\Config;
final class AgentRunnerConfig final class AgentRunnerConfig
{ {
private const NO_LLM_MAIN_DEVICE_REQUEST_ROLE_KEYWORDS = [
'anlage',
'messanlage',
'gerät',
'geraet',
'messgerät',
'messgeraet',
'analysegerät',
'analysegeraet',
'analysator',
'analyzer',
'system',
'testomat',
'testomaten',
'testoamt',
'testomate',
'pockettester',
];
private const RAG_EVIDENCE_STOP_TERMS = [
'suche',
'suchen',
'finde',
'finden',
'zeige',
'einen',
'eine',
'einem',
'einer',
'der',
'die',
'das',
'den',
'dem',
'des',
'für',
'fuer',
'mit',
'ohne',
'und',
'oder',
'kann',
'können',
'koennen',
'messen',
'messung',
'tester',
'testgerät',
'testgeraet',
'gerät',
'geraet',
'messgerät',
'messgeraet',
'produkt',
'produkte',
'artikel',
'shop',
];
private const RAG_EVIDENCE_SYNONYMS = [
'salinität' => ['salinität', 'salinitaet', 'salinity', 'salzgehalt', 'tds', 'leitfähigkeit', 'leitfaehigkeit'],
'salinitaet' => ['salinität', 'salinitaet', 'salinity', 'salzgehalt', 'tds', 'leitfähigkeit', 'leitfaehigkeit'],
'salinity' => ['salinität', 'salinitaet', 'salinity', 'salzgehalt', 'tds', 'leitfähigkeit', 'leitfaehigkeit'],
'redox' => ['redox', 'orp', 'oxidations-reduktionspotential', 'oxidations reduktionspotential'],
'orp' => ['redox', 'orp', 'oxidations-reduktionspotential', 'oxidations reduktionspotential'],
'ph' => ['ph', 'ph-wert', 'ph wert'],
'chlor' => ['chlor', 'freies chlor', 'gesamtchlor', 'chlorine'],
];
private const NO_LLM_ACCESSORY_PRODUCT_ROLE_KEYWORDS = [
'indikator',
'indicator',
'indikatortyp',
'reagenz',
'reagent',
'reagenzsatz',
'kalibrierlösung',
'kalibrierloesung',
'pufferlösung',
'pufferloesung',
'reinigungslösung',
'reinigungsloesung',
'kalibrier',
'puffer',
'zubehör',
'zubehor',
'accessory',
'ersatzteil',
'verbrauch',
'consumable',
'kit',
'set',
'flasche',
'bottle',
'100 ml',
'500 ml',
'100ml',
'500ml',
];
/** /**
* @param array<string, mixed> $config * @param array<string, mixed> $config
*/ */
@@ -198,6 +98,85 @@ final class AgentRunnerConfig
throw new \InvalidArgumentException(sprintf('RetrieX agent config key "%s" must be a non-empty string.', $key)); throw new \InvalidArgumentException(sprintf('RetrieX agent config key "%s" must be a non-empty string.', $key));
} }
/**
* @return string[]
*/
private function getRequiredStringList(string $key): array
{
$value = $this->requiredValue($key);
if (!is_array($value)) {
throw new \InvalidArgumentException(sprintf('RetrieX agent config key "%s" must be a list.', $key));
}
$out = [];
foreach ($value as $item) {
if (!is_scalar($item)) {
continue;
}
$item = trim((string) $item);
if ($item !== '') {
$out[] = $item;
}
}
if ($out === []) {
throw new \InvalidArgumentException(sprintf('RetrieX agent config key "%s" must contain at least one non-empty value.', $key));
}
return $out;
}
/**
* @return array<string, string[]>
*/
private function getRequiredStringListMap(string $key): array
{
$value = $this->requiredValue($key);
if (!is_array($value)) {
throw new \InvalidArgumentException(sprintf('RetrieX agent config key "%s" must be a map of string lists.', $key));
}
$out = [];
foreach ($value as $mapKey => $items) {
if (!is_scalar($mapKey) || !is_array($items)) {
continue;
}
$mapKey = trim((string) $mapKey);
if ($mapKey === '') {
continue;
}
$terms = [];
foreach ($items as $item) {
if (!is_scalar($item)) {
continue;
}
$item = trim((string) $item);
if ($item !== '' && !in_array($item, $terms, true)) {
$terms[] = $item;
}
}
if ($terms !== []) {
$out[$mapKey] = $terms;
}
}
if ($out === []) {
throw new \InvalidArgumentException(sprintf('RetrieX agent config key "%s" must contain at least one valid entry.', $key));
}
return $out;
}
/** /**
* @param string[] $default * @param string[] $default
* @return string[] * @return string[]
@@ -259,60 +238,57 @@ final class AgentRunnerConfig
public function getEmptyPromptMessage(): string public function getEmptyPromptMessage(): string
{ {
return $this->getString('messages.empty_prompt', '❌ Empty prompt.'); return $this->getRequiredString('messages.empty_prompt');
} }
public function getAnalyzeRequestMessage(): string public function getAnalyzeRequestMessage(): string
{ {
return $this->getString('messages.analyze_request', 'Ich analysiere deine Anfrage...'); return $this->getRequiredString('messages.analyze_request');
} }
public function getCheckInternetSourcesMessage(): string public function getCheckInternetSourcesMessage(): string
{ {
return $this->getString('messages.check_internet_sources', 'Ich prüfe auf Internetquellen...'); return $this->getRequiredString('messages.check_internet_sources');
} }
public function getRetrieveKnowledgeMessage(): string public function getRetrieveKnowledgeMessage(): string
{ {
return $this->getString('messages.retrieve_knowledge', 'Ich hole relevante Daten aus meinem RAG-Wissen...'); return $this->getRequiredString('messages.retrieve_knowledge');
} }
public function getOptimizeSearchMessage(): string public function getOptimizeSearchMessage(): string
{ {
return $this->getString('messages.optimize_search', 'Ich optimiere die Recherche...'); return $this->getRequiredString('messages.optimize_search');
} }
public function getNoConcreteShopQueryMessage(): string public function getNoConcreteShopQueryMessage(): string
{ {
return $this->getString( return $this->getRequiredString('messages.no_concrete_shop_query');
'messages.no_concrete_shop_query',
'Ich kann die Shop-Suche noch nicht belastbar auflösen. Bitte nenne das Produkt, den Messparameter oder das Zubehör etwas konkreter.'
);
} }
public function getFetchSearchDataMessageTemplate(): string public function getFetchSearchDataMessageTemplate(): string
{ {
return $this->getString('messages.fetch_search_data_template', 'Ich rufe Recherchedaten ab (type: %s)'); return $this->getRequiredString('messages.fetch_search_data_template');
} }
public function getAnalyzeAllInformationMessage(): string public function getAnalyzeAllInformationMessage(): string
{ {
return $this->getString('messages.analyze_all_information', 'Ich analysiere alle Informationen...'); return $this->getRequiredString('messages.analyze_all_information');
} }
public function getThinkingWhileStreamingMessage(): string public function getThinkingWhileStreamingMessage(): string
{ {
return $this->getString('messages.thinking_while_streaming', 'Denke nach...'); return $this->getRequiredString('messages.thinking_while_streaming');
} }
public function getNoLlmDataReceivedMessage(): string public function getNoLlmDataReceivedMessage(): string
{ {
return $this->getString('messages.no_llm_data_received', '❌ Es wurden keine Daten vom LLM empfangen.'); return $this->getRequiredString('messages.no_llm_data_received');
} }
public function getNoLlmFallbackMaxShopResults(): int public function getNoLlmFallbackMaxShopResults(): int
{ {
return $this->getInt('no_llm_fallback.max_shop_results', 5); return $this->getRequiredInt('no_llm_fallback.max_shop_results');
} }
/** /**
@@ -320,7 +296,7 @@ final class AgentRunnerConfig
*/ */
public function getRagEvidenceStopTerms(): array public function getRagEvidenceStopTerms(): array
{ {
return $this->getStringList('rag_evidence_guard.stop_terms', self::RAG_EVIDENCE_STOP_TERMS); return $this->getRequiredStringList('rag_evidence_guard.stop_terms');
} }
/** /**
@@ -328,106 +304,47 @@ final class AgentRunnerConfig
*/ */
public function getRagEvidenceSynonyms(): array public function getRagEvidenceSynonyms(): array
{ {
$value = $this->value('rag_evidence_guard.synonyms', self::RAG_EVIDENCE_SYNONYMS); return $this->getRequiredStringListMap('rag_evidence_guard.synonyms');
if (!is_array($value)) {
return self::RAG_EVIDENCE_SYNONYMS;
}
$out = [];
foreach ($value as $key => $items) {
if (!is_scalar($key) || !is_array($items)) {
continue;
}
$key = trim((string) $key);
if ($key === '') {
continue;
}
$terms = [];
foreach ($items as $item) {
if (!is_scalar($item)) {
continue;
}
$item = trim((string) $item);
if ($item !== '' && !in_array($item, $terms, true)) {
$terms[] = $item;
}
}
if ($terms !== []) {
$out[$key] = $terms;
}
}
return $out !== [] ? $out : self::RAG_EVIDENCE_SYNONYMS;
} }
public function getNoLlmFallbackShopOnlyMessage(): string public function getNoLlmFallbackShopOnlyMessage(): string
{ {
return $this->getString( return $this->getRequiredString('no_llm_fallback.messages.shop_only');
'no_llm_fallback.messages.shop_only',
'Ich finde dazu im RAG-Wissen keine belastbare Fachinformation. Aus den Shopdaten ergeben sich folgende Treffer; technische Eignung bitte prüfen:'
);
} }
public function getNoLlmFallbackShopWithKnowledgeMessage(): string public function getNoLlmFallbackShopWithKnowledgeMessage(): string
{ {
return $this->getString( return $this->getRequiredString('no_llm_fallback.messages.shop_with_knowledge');
'no_llm_fallback.messages.shop_with_knowledge',
'Es liegen RAG-/Kontexttreffer und Shopdaten vor. Ohne LLM leite ich daraus keine technische Eignung ab. Die Shopdaten zeigen folgende Treffer; technische Eignung bitte prüfen:'
);
} }
public function getNoLlmFallbackAccessoryOnlyForMainDeviceMessage(): string public function getNoLlmFallbackAccessoryOnlyForMainDeviceMessage(): string
{ {
return $this->getString( return $this->getRequiredString('no_llm_fallback.messages.accessory_only_for_main_device');
'no_llm_fallback.messages.accessory_only_for_main_device',
'Die Shop-Treffer wirken wie Zubehör/Verbrauchsmaterial und nicht wie eine angefragte Messanlage oder ein Hauptgerät. Ich werte sie deshalb nicht als passende Hauptlösung.'
);
} }
public function getNoLlmFallbackEscalationMessage(): string public function getNoLlmFallbackEscalationMessage(): string
{ {
return $this->getString( return $this->getRequiredString('no_llm_fallback.messages.escalation');
'no_llm_fallback.messages.escalation',
'Für eine verbindliche Produktauswahl sollte der konkrete Anwendungsfall durch Vertrieb oder Support geprüft werden.'
);
} }
public function getNoLlmFallbackKnowledgeOnlyMessage(): string public function getNoLlmFallbackKnowledgeOnlyMessage(): string
{ {
return $this->getString( return $this->getRequiredString('no_llm_fallback.messages.knowledge_only');
'no_llm_fallback.messages.knowledge_only',
'Ich habe Treffer im RAG-Wissen gefunden, aber ohne LLM kann ich daraus keine belastbare fachliche Antwort synthetisieren. Ich gebe deshalb keine sichere Produktaussage aus. Bitte aktiviere das LLM oder konkretisiere die Frage für eine gezielte Prüfung.'
);
} }
public function getNoLlmFallbackNoDataMessage(): string public function getNoLlmFallbackNoDataMessage(): string
{ {
return $this->getString( return $this->getRequiredString('no_llm_fallback.messages.no_data');
'no_llm_fallback.messages.no_data',
'Ich finde dazu keine belastbaren Daten in den vorliegenden Quellen. Bitte nenne Produkt, Messparameter, Zubehör oder Anwendungsfall genauer.'
);
} }
public function getNoLlmFallbackNoShopResultsWithKnowledgeMessage(): string public function getNoLlmFallbackNoShopResultsWithKnowledgeMessage(): string
{ {
return $this->getString( return $this->getRequiredString('no_llm_fallback.messages.no_shop_results_with_knowledge');
'no_llm_fallback.messages.no_shop_results_with_knowledge',
'Ich finde RAG-/Kontexttreffer, aber keine passenden Shop-Treffer zur aktuellen Suchanfrage. Das ist keine Aussage, dass es das Produkt nicht gibt. Ohne LLM gebe ich keine technische Negativaussage aus; bitte prüfe den Suchbegriff oder den Anwendungsfall gezielter.'
);
} }
public function getNoLlmFallbackNoShopResultsNoKnowledgeMessage(): string public function getNoLlmFallbackNoShopResultsNoKnowledgeMessage(): string
{ {
return $this->getString( return $this->getRequiredString('no_llm_fallback.messages.no_shop_results_no_knowledge');
'no_llm_fallback.messages.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.'
);
} }
/** /**
@@ -435,10 +352,7 @@ final class AgentRunnerConfig
*/ */
public function getNoLlmMainDeviceRequestRoleKeywords(): array public function getNoLlmMainDeviceRequestRoleKeywords(): array
{ {
return $this->getStringList( return $this->getRequiredStringList('no_llm_fallback.product_roles.main_device_request_keywords');
'no_llm_fallback.product_roles.main_device_request_keywords',
self::NO_LLM_MAIN_DEVICE_REQUEST_ROLE_KEYWORDS
);
} }
/** /**
@@ -446,98 +360,88 @@ final class AgentRunnerConfig
*/ */
public function getNoLlmAccessoryProductRoleKeywords(): array public function getNoLlmAccessoryProductRoleKeywords(): array
{ {
return $this->getStringList( return $this->getRequiredStringList('no_llm_fallback.product_roles.accessory_product_keywords');
'no_llm_fallback.product_roles.accessory_product_keywords',
self::NO_LLM_ACCESSORY_PRODUCT_ROLE_KEYWORDS
);
} }
public function getNoLlmFallbackShopUnavailableWithKnowledgeMessage(): string public function getNoLlmFallbackShopUnavailableWithKnowledgeMessage(): string
{ {
return $this->getString( return $this->getRequiredString('no_llm_fallback.messages.shop_unavailable_with_knowledge');
'no_llm_fallback.messages.shop_unavailable_with_knowledge',
'Live-Shopdaten konnten nicht geladen werden. Ohne Shop-Check treffe ich keine Aussage zu aktueller Verfügbarkeit, Preis oder Shop-Portfolio. Vorhandenes RAG-Wissen darf nur als fachlicher Kontext verstanden werden.'
);
} }
public function getNoLlmFallbackShopUnavailableNoKnowledgeMessage(): string public function getNoLlmFallbackShopUnavailableNoKnowledgeMessage(): string
{ {
return $this->getString( return $this->getRequiredString('no_llm_fallback.messages.shop_unavailable_no_knowledge');
'no_llm_fallback.messages.shop_unavailable_no_knowledge',
'Live-Shopdaten konnten nicht geladen werden und ich finde kein belastbares RAG-Wissen. Ich kann daraus keine verlässliche Produkt- oder Verfügbarkeitsaussage ableiten.'
);
} }
public function getGenericInternalErrorMessage(): string public function getGenericInternalErrorMessage(): string
{ {
return $this->getString('messages.generic_internal_error', '❌ Bei der Verarbeitung der Anfrage ist ein interner Fehler aufgetreten.'); return $this->getRequiredString('messages.generic_internal_error');
} }
public function getDebugInternalErrorPrefix(): string public function getDebugInternalErrorPrefix(): string
{ {
return $this->getString('messages.debug_internal_error_prefix', '❌ Interner Fehler: '); return $this->getRequiredString('messages.debug_internal_error_prefix');
} }
public function getExternalUrlSourceLabel(): string public function getExternalUrlSourceLabel(): string
{ {
return $this->getString('source_labels.external_url', 'Externe URL'); return $this->getRequiredString('source_labels.external_url');
} }
public function getRagKnowledgeSourceLabel(): string public function getRagKnowledgeSourceLabel(): string
{ {
return $this->getString('source_labels.rag_knowledge', 'RAG Wissen'); return $this->getRequiredString('source_labels.rag_knowledge');
} }
public function getConversationHistorySourceLabel(): string public function getConversationHistorySourceLabel(): string
{ {
return $this->getString('source_labels.conversation_history', 'Chatverlauf'); return $this->getRequiredString('source_labels.conversation_history');
} }
public function getShopSystemSourceLabel(): string public function getShopSystemSourceLabel(): string
{ {
return $this->getString('source_labels.shop_system', 'Shopsystem'); return $this->getRequiredString('source_labels.shop_system');
} }
public function getExtendedShopSearchSourceLabel(): string public function getExtendedShopSearchSourceLabel(): string
{ {
return $this->getString('source_labels.extended_shop_search', 'Erweiterte Shopsuche'); return $this->getRequiredString('source_labels.extended_shop_search');
} }
public function getUsedSourcesPrefix(): string public function getUsedSourcesPrefix(): string
{ {
return $this->getString('source_labels.used_sources_prefix', 'Genutzte Quellen: '); return $this->getRequiredString('source_labels.used_sources_prefix');
} }
public function getSourcesPrefix(): string public function getSourcesPrefix(): string
{ {
return $this->getString('source_labels.sources_prefix', 'Quellen: '); return $this->getRequiredString('source_labels.sources_prefix');
} }
public function getSourceBadgeHtmlTemplate(): string public function getSourceBadgeHtmlTemplate(): string
{ {
return $this->getString('html.source_badge_template', '<span class="badge bg-info text-black">%s</span>'); return $this->getRequiredString('html.source_badge_template');
} }
public function getErrorHtmlTemplate(): string public function getErrorHtmlTemplate(): string
{ {
return $this->getString('html.error_template', '<div class="retriex-alert retriex-alert--error"><div class="retriex-alert__icon">❌</div><div class="retriex-alert__content"><div class="retriex-alert__title">Hinweis</div><div class="retriex-alert__text">%s</div></div></div>' . "\n"); return $this->getRequiredString('html.error_template');
} }
public function getThinkHtmlTemplate(): string public function getThinkHtmlTemplate(): string
{ {
return $this->getString('html.think_template', '<span class="text-info think">%s</span>' . "\n"); return $this->getRequiredString('html.think_template');
} }
public function getInfoHtmlTemplate(): string public function getInfoHtmlTemplate(): string
{ {
return $this->getString('html.info_template', "\n\n" . '<span class="text-info fw-bolder">%s</span>' . "\n"); return $this->getRequiredString('html.info_template');
} }
public function getDebugHtmlTemplate(): string public function getDebugHtmlTemplate(): string
{ {
return $this->getString('html.debug_template', "\n\nDEBUG: <code>%s</code>\n"); return $this->getRequiredString('html.debug_template');
} }
public function getShopPrompt(string $prompt, string $commerceHistoryContext = ''): string public function getShopPrompt(string $prompt, string $commerceHistoryContext = ''): string
{ {
$historyBlock = ''; $historyBlock = '';