This commit is contained in:
team 1
2026-05-09 12:04:06 +02:00
parent 7f25335c44
commit dabbc33f07
5 changed files with 171 additions and 18 deletions

View File

@@ -234,15 +234,17 @@ parameters:
history_notice_with_detail: 'Systemhinweis: {title}. Ursache: {detail}'
history_response_system_notice: 'Systemhinweis: {message}'
follow_up_actions:
enabled: true
commerce:
- label: Im Shop suchen
prompt: Suche die aktuelle Produktauswahl im Shop.
prompt: Suche im Shop nach den aktuell genannten Produkten oder Messaufgaben.
shop_results:
- label: Preis anzeigen
prompt: Zeige mir die Preise der aktuell relevanten Produkte.
- 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.

View File

@@ -0,0 +1,52 @@
# RetrieX Patch p67 Contextual Follow-up Actions
## Ziel
`buildFollowUpActionsMessage()` war vorbereitet, aber nicht in den AgentRunner-Flow eingehängt. Dieser Patch aktiviert die Funktion kontrolliert und kontextsensitiv, damit die Production-UI nach einer fertigen Antwort sinnvolle nächste Schritte anbieten kann, ohne unsichere oder unpassende Aktionen zu zeigen.
## Änderung
- `AgentRunner` rendert nach der finalen Completed-Meta-Card optional eine Follow-up-Actions-Card.
- Die Card erscheint nur, wenn `agent.production_ui.follow_up_actions.enabled` aktiv ist und echte Kontextsignale vorhanden sind.
- Shop-Refinement-Actions werden nur bei vorhandenen Shop-Treffern angezeigt.
- Die generische Shop-Suchaktion wird nur bei Commerce-Kontext ohne Shop-Systemfehler angeboten.
- Die Knowledge-Action wird nur bei direkter/belastbarer RAG-Evidenz angezeigt.
- Action-Duplikate werden beim Rendern dedupliziert.
- Die bisherigen Action-Texte bleiben YAML-konfigurierbar und wurden in kontextbezogene Gruppen getrennt:
- `commerce`
- `shop_results`
- `knowledge`
## Bewusst nicht geändert
- Keine Änderung an Retrieval, Scoring, Ranking, Intent-Erkennung, Shop-Matching oder PromptBuilder.
- `buildShopProductCardsMessage()` bleibt weiterhin nicht eingehängt, weil Produktkarten zusätzlich zur Antwort deutlich mehr UI-Duplikation und Regressionsrisiko erzeugen würden.
- Keine neuen fachlichen Keywordlisten oder PHP-only Defaults.
## Geänderte Dateien
- `src/Agent/AgentRunner.php`
- `src/Config/AgentRunnerConfig.php`
- `src/Config/ChatMessagesConfig.php`
- `config/retriex/chat-messages.yaml`
## Lokale Checks
Ausgeführt im ZIP-Arbeitsstand ohne `vendor/`:
```bash
php -l src/Agent/AgentRunner.php
php -l src/Config/AgentRunnerConfig.php
php -l src/Config/ChatMessagesConfig.php
python3 -c 'import yaml; yaml.safe_load(open("config/retriex/chat-messages.yaml"))'
```
Zusätzlich wurde `ChatMessagesConfig::validate()` mit der geparsten `chat-messages.yaml` per Smoke-Test geprüft.
## Empfohlene Checks in der Zielumgebung
```bash
bin/console mto:agent:config:validate
bin/console mto:agent:regression:test
bin/console mto:agent:config:audit-source --details
```

View File

@@ -700,6 +700,17 @@ final readonly class AgentRunner
'meta'
);
$followUpActionsMessage = $this->buildFollowUpActionsMessage(
isCommerceIntent: $this->isCommerceIntent($commerceIntent),
hasShopResults: $shopResults !== [],
hasKnowledge: $this->isDirectKnowledgeEvidence($knowledgeEvidenceState),
shopSearchHadSystemFailure: $primaryShopSearchHadSystemFailure
);
if ($followUpActionsMessage !== '') {
yield $this->systemMsg($followUpActionsMessage, 'meta');
}
/* if ($sources !== []) {
yield $this->emitSources(
$sources,
@@ -4850,20 +4861,27 @@ final readonly class AgentRunner
return $this->agentRunnerConfig->getProductionUiTemplate('relevance_default');
}
private function buildFollowUpActionsMessage(bool $isCommerceIntent, bool $hasShopResults, bool $hasKnowledge): string
{
if (!$isCommerceIntent && !$hasShopResults && !$hasKnowledge) {
private function buildFollowUpActionsMessage(
bool $isCommerceIntent,
bool $hasShopResults,
bool $hasKnowledge,
bool $shopSearchHadSystemFailure
): string {
if (!$this->agentRunnerConfig->isProductionUiFollowUpActionsEnabled()) {
return '';
}
$actions = [];
$seenActionKeys = [];
if ($isCommerceIntent || $hasShopResults) {
$actions = array_merge($actions, $this->agentRunnerConfig->getProductionUiFollowUpActions('commerce'));
if ($hasShopResults) {
$this->appendFollowUpActions($actions, $seenActionKeys, $this->agentRunnerConfig->getProductionUiFollowUpActions('shop_results'));
} elseif ($isCommerceIntent && !$shopSearchHadSystemFailure) {
$this->appendFollowUpActions($actions, $seenActionKeys, $this->agentRunnerConfig->getProductionUiFollowUpActions('commerce'));
}
if ($hasKnowledge || $hasShopResults) {
$actions = array_merge($actions, $this->agentRunnerConfig->getProductionUiFollowUpActions('knowledge'));
if ($hasKnowledge) {
$this->appendFollowUpActions($actions, $seenActionKeys, $this->agentRunnerConfig->getProductionUiFollowUpActions('knowledge'));
}
if ($actions === []) {
@@ -4876,15 +4894,10 @@ final readonly class AgentRunner
. '<div class="retriex-action-chip-row">';
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="'
. htmlspecialchars($actionPrompt, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')
. htmlspecialchars($action['prompt'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')
. '">'
. htmlspecialchars($label, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')
. htmlspecialchars($action['label'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')
. '</button>';
}
@@ -4893,6 +4906,34 @@ final readonly class AgentRunner
return $html;
}
/**
* @param array<int, array{label:string, prompt:string}> $actions
* @param array<string, bool> $seenActionKeys
* @param array<int, array{label:string, prompt:string}> $items
*/
private function appendFollowUpActions(array &$actions, array &$seenActionKeys, array $items): void
{
foreach ($items as $item) {
$label = trim((string) ($item['label'] ?? ''));
$actionPrompt = trim((string) ($item['prompt'] ?? ''));
if ($label === '' || $actionPrompt === '') {
continue;
}
$key = mb_strtolower($label . "\n" . $actionPrompt, 'UTF-8');
if (isset($seenActionKeys[$key])) {
continue;
}
$seenActionKeys[$key] = true;
$actions[] = [
'label' => $label,
'prompt' => $actionPrompt,
];
}
}
private function buildShopSearchMetaMessage(
string $query,
string $commerceIntent,

View File

@@ -979,12 +979,23 @@ final class AgentRunnerConfig
return $this->getRequiredInt('production_ui.shop_results.max_cards');
}
public function isProductionUiFollowUpActionsEnabled(): bool
{
if ($this->chatMessages !== null) {
return $this->chatMessages->getBool('agent.production_ui.follow_up_actions.enabled');
}
return $this->getOptionalBool('production_ui.follow_up_actions.enabled', false);
}
/**
* @return array<int, array{label:string, prompt:string}>
*/
public function getProductionUiFollowUpActions(string $group): array
{
return $this->getChatActionList('agent.production_ui.follow_up_actions.' . $group, 'production_ui.follow_up_actions.' . $group);
$legacyGroup = $group === 'shop_results' ? 'commerce' : $group;
return $this->getChatActionList('agent.production_ui.follow_up_actions.' . $group, 'production_ui.follow_up_actions.' . $legacyGroup);
}
public function getNoLlmProductField(string $key): string

View File

@@ -172,6 +172,11 @@ final class ChatMessagesConfig
throw new \InvalidArgumentException(sprintf('RetrieX chat messages config key "%s" must be a string.', $path));
}
public function getBool(string $path): bool
{
return $this->bool($path);
}
/**
* @return array<int, array{label:string, prompt:string}>
*/
@@ -211,6 +216,14 @@ final class ChatMessagesConfig
}
}
foreach ($this->requiredBoolPaths() as $path) {
try {
$this->bool($path);
} catch (\InvalidArgumentException $e) {
$errors[] = $e->getMessage();
}
}
foreach ($this->requiredActionListPaths() as $path) {
try {
$this->actionList($path);
@@ -443,10 +456,21 @@ final class ChatMessagesConfig
{
return [
'agent.production_ui.follow_up_actions.commerce',
'agent.production_ui.follow_up_actions.shop_results',
'agent.production_ui.follow_up_actions.knowledge',
];
}
/**
* @return list<string>
*/
private function requiredBoolPaths(): array
{
return [
'agent.production_ui.follow_up_actions.enabled',
];
}
/**
* @return list<string>
*/
@@ -566,6 +590,29 @@ final class ChatMessagesConfig
throw new \InvalidArgumentException(sprintf('RetrieX chat messages config key "%s" must be a non-empty string.', $path));
}
private function bool(string $path): bool
{
$value = $this->value($path);
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;
}
}
throw new \InvalidArgumentException(sprintf('RetrieX chat messages config key "%s" must be boolean.', $path));
}
private function value(string $path): mixed
{
$current = $this->config;