p67
This commit is contained in:
@@ -234,15 +234,17 @@ parameters:
|
|||||||
history_notice_with_detail: 'Systemhinweis: {title}. Ursache: {detail}'
|
history_notice_with_detail: 'Systemhinweis: {title}. Ursache: {detail}'
|
||||||
history_response_system_notice: 'Systemhinweis: {message}'
|
history_response_system_notice: 'Systemhinweis: {message}'
|
||||||
follow_up_actions:
|
follow_up_actions:
|
||||||
|
enabled: true
|
||||||
commerce:
|
commerce:
|
||||||
- label: Im Shop suchen
|
- 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
|
- label: Nur Zubehör anzeigen
|
||||||
prompt: Zeige aus der aktuellen Produktauswahl nur Zubehör.
|
prompt: Zeige aus der aktuellen Produktauswahl nur Zubehör.
|
||||||
- label: Nur Geräte anzeigen
|
- label: Nur Geräte anzeigen
|
||||||
prompt: Zeige aus der aktuellen Produktauswahl nur Geräte.
|
prompt: Zeige aus der aktuellen Produktauswahl nur Geräte.
|
||||||
- label: Preis anzeigen
|
|
||||||
prompt: Zeige mir die Preise der aktuell relevanten Produkte.
|
|
||||||
knowledge:
|
knowledge:
|
||||||
- label: Technische Details anzeigen
|
- label: Technische Details anzeigen
|
||||||
prompt: Zeige technische Details zur aktuellen Antwort.
|
prompt: Zeige technische Details zur aktuellen Antwort.
|
||||||
|
|||||||
@@ -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
|
||||||
|
```
|
||||||
@@ -700,6 +700,17 @@ final readonly class AgentRunner
|
|||||||
'meta'
|
'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 !== []) {
|
/* if ($sources !== []) {
|
||||||
yield $this->emitSources(
|
yield $this->emitSources(
|
||||||
$sources,
|
$sources,
|
||||||
@@ -4850,20 +4861,27 @@ final readonly class AgentRunner
|
|||||||
return $this->agentRunnerConfig->getProductionUiTemplate('relevance_default');
|
return $this->agentRunnerConfig->getProductionUiTemplate('relevance_default');
|
||||||
}
|
}
|
||||||
|
|
||||||
private function buildFollowUpActionsMessage(bool $isCommerceIntent, bool $hasShopResults, bool $hasKnowledge): string
|
private function buildFollowUpActionsMessage(
|
||||||
{
|
bool $isCommerceIntent,
|
||||||
if (!$isCommerceIntent && !$hasShopResults && !$hasKnowledge) {
|
bool $hasShopResults,
|
||||||
|
bool $hasKnowledge,
|
||||||
|
bool $shopSearchHadSystemFailure
|
||||||
|
): string {
|
||||||
|
if (!$this->agentRunnerConfig->isProductionUiFollowUpActionsEnabled()) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
$actions = [];
|
$actions = [];
|
||||||
|
$seenActionKeys = [];
|
||||||
|
|
||||||
if ($isCommerceIntent || $hasShopResults) {
|
if ($hasShopResults) {
|
||||||
$actions = array_merge($actions, $this->agentRunnerConfig->getProductionUiFollowUpActions('commerce'));
|
$this->appendFollowUpActions($actions, $seenActionKeys, $this->agentRunnerConfig->getProductionUiFollowUpActions('shop_results'));
|
||||||
|
} elseif ($isCommerceIntent && !$shopSearchHadSystemFailure) {
|
||||||
|
$this->appendFollowUpActions($actions, $seenActionKeys, $this->agentRunnerConfig->getProductionUiFollowUpActions('commerce'));
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($hasKnowledge || $hasShopResults) {
|
if ($hasKnowledge) {
|
||||||
$actions = array_merge($actions, $this->agentRunnerConfig->getProductionUiFollowUpActions('knowledge'));
|
$this->appendFollowUpActions($actions, $seenActionKeys, $this->agentRunnerConfig->getProductionUiFollowUpActions('knowledge'));
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($actions === []) {
|
if ($actions === []) {
|
||||||
@@ -4876,15 +4894,10 @@ final readonly class AgentRunner
|
|||||||
. '<div class="retriex-action-chip-row">';
|
. '<div class="retriex-action-chip-row">';
|
||||||
|
|
||||||
foreach ($actions as $action) {
|
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="'
|
$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>';
|
. '</button>';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4893,6 +4906,34 @@ final readonly class AgentRunner
|
|||||||
return $html;
|
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(
|
private function buildShopSearchMetaMessage(
|
||||||
string $query,
|
string $query,
|
||||||
string $commerceIntent,
|
string $commerceIntent,
|
||||||
|
|||||||
@@ -979,12 +979,23 @@ final class AgentRunnerConfig
|
|||||||
return $this->getRequiredInt('production_ui.shop_results.max_cards');
|
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}>
|
* @return array<int, array{label:string, prompt:string}>
|
||||||
*/
|
*/
|
||||||
public function getProductionUiFollowUpActions(string $group): array
|
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
|
public function getNoLlmProductField(string $key): string
|
||||||
|
|||||||
@@ -172,6 +172,11 @@ final class ChatMessagesConfig
|
|||||||
throw new \InvalidArgumentException(sprintf('RetrieX chat messages config key "%s" must be a string.', $path));
|
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}>
|
* @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) {
|
foreach ($this->requiredActionListPaths() as $path) {
|
||||||
try {
|
try {
|
||||||
$this->actionList($path);
|
$this->actionList($path);
|
||||||
@@ -443,10 +456,21 @@ final class ChatMessagesConfig
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'agent.production_ui.follow_up_actions.commerce',
|
'agent.production_ui.follow_up_actions.commerce',
|
||||||
|
'agent.production_ui.follow_up_actions.shop_results',
|
||||||
'agent.production_ui.follow_up_actions.knowledge',
|
'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>
|
* @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));
|
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
|
private function value(string $path): mixed
|
||||||
{
|
{
|
||||||
$current = $this->config;
|
$current = $this->config;
|
||||||
|
|||||||
Reference in New Issue
Block a user