fix p45
This commit is contained in:
@@ -212,6 +212,13 @@ parameters:
|
||||
vocabulary_maps:
|
||||
synonyms: agent.rag_evidence_guard.synonyms
|
||||
|
||||
direct_shop_result_answer:
|
||||
enabled: true
|
||||
max_results: 10
|
||||
intro: 'Aus den Shopdaten ergeben sich folgende passende Treffer:'
|
||||
no_results: 'Ich finde in den Shopdaten keine passenden Treffer für die angefragte Produktsuche. Ich liste deshalb keine fachfremden Ersatzprodukte auf.'
|
||||
sorted_by_length_note: 'Sortierung: aufsteigend nach erkannter Kabellänge.'
|
||||
|
||||
no_llm_fallback:
|
||||
max_shop_results: 5
|
||||
messages:
|
||||
@@ -440,6 +447,66 @@ parameters:
|
||||
comparative_constraint_patterns:
|
||||
- '/\b(?:länger|laenger|kürzer|kuerzer|größer|groesser|kleiner|über|ueber|unter|mindestens|maximal|maximum|minimum|ab|bis|mehr\s+als|weniger\s+als)\s+(?P<value>\d+(?:[,.]\d+)?\s*[\p{L}µ°%]*)\b/iu'
|
||||
|
||||
query_stopword_cleanup:
|
||||
enabled: true
|
||||
min_query_tokens_after_cleanup: 2
|
||||
# Plain Shopware text search should contain product-relevant terms only.
|
||||
# These terms are UI, instruction, presentation or sorting words and are
|
||||
# removed after LLM query optimization. Keep this list simple and local.
|
||||
terms:
|
||||
- zeige
|
||||
- zeig
|
||||
- suche
|
||||
- such
|
||||
- finde
|
||||
- find
|
||||
- gib
|
||||
- gebe
|
||||
- nenne
|
||||
- mir
|
||||
- bitte
|
||||
- ich
|
||||
- wir
|
||||
- im
|
||||
- in
|
||||
- shop
|
||||
- für
|
||||
- fuer
|
||||
- nach
|
||||
- mit
|
||||
- ohne
|
||||
- von
|
||||
- zum
|
||||
- zur
|
||||
- der
|
||||
- die
|
||||
- das
|
||||
- ein
|
||||
- eine
|
||||
- einen
|
||||
- ordne
|
||||
- sortiere
|
||||
- sortiert
|
||||
- sortierung
|
||||
- liste
|
||||
- tabelle
|
||||
- übersicht
|
||||
- uebersicht
|
||||
- auflistung
|
||||
- meter
|
||||
- metern
|
||||
|
||||
direct_result_guard:
|
||||
enabled: true
|
||||
|
||||
length_sort:
|
||||
enabled: true
|
||||
trigger_patterns:
|
||||
- '/\b(?:ordne|sortiere|sortiert|sortierung)\b.{0,80}\b(?:meter|metern|m)\b/iu'
|
||||
- '/\bnach\s+(?:meter|metern|m)\b/iu'
|
||||
value_patterns:
|
||||
- '/(?P<value>\d+(?:[,.]\d+)?)\s*(?:m|meter|metern)\b/iu'
|
||||
|
||||
context_usage:
|
||||
referential_terms:
|
||||
- der
|
||||
|
||||
@@ -35,7 +35,9 @@ a {
|
||||
color: #7a9ed1;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: .5rem;
|
||||
}
|
||||
a:hover {
|
||||
color: #FFF;
|
||||
}
|
||||
|
||||
@@ -467,6 +467,8 @@ final readonly class AgentRunner
|
||||
}
|
||||
|
||||
$shopResults = $repairPayload['results'];
|
||||
$shopResults = $this->guardDirectProductShopResults($prompt, $shopSearchQuery, $shopResults);
|
||||
$shopResults = $this->sortShopResultsForLengthRequest($prompt, $shopSearchQuery, $shopResults);
|
||||
$attemptedShopRepair = $repairPayload['attemptedRepair'];
|
||||
$usedShopRepair = $repairPayload['usedRepair'];
|
||||
$shopRepairQueries = $repairPayload['repairQueries'];
|
||||
@@ -604,7 +606,21 @@ final readonly class AgentRunner
|
||||
knowledgeEvidenceState: $knowledgeEvidenceState
|
||||
);
|
||||
|
||||
$deterministicDirectShopAnswer = $this->buildDeterministicDirectShopResultAnswer(
|
||||
prompt: $prompt,
|
||||
shopResults: $shopResults,
|
||||
commerceIntent: $commerceIntent,
|
||||
shopSearchAttempted: $shopSearchAttempted,
|
||||
shopSearchHadSystemFailure: $primaryShopSearchHadSystemFailure,
|
||||
shopSearchQuery: $shopSearchQuery
|
||||
);
|
||||
|
||||
if ($deterministicDirectShopAnswer !== '') {
|
||||
$fullOutput = $deterministicDirectShopAnswer;
|
||||
yield $this->systemMsg($deterministicDirectShopAnswer, 'answer');
|
||||
} else {
|
||||
$fullOutput = yield from $this->streamFinalAnswer($finalPrompt, $noLlmFallbackAnswer);
|
||||
}
|
||||
|
||||
yield $this->systemMsg(
|
||||
$this->buildProductionUiMetaMessage(
|
||||
@@ -1565,7 +1581,49 @@ final readonly class AgentRunner
|
||||
? $this->preserveCurrentInputShopQueryTerms($prompt, $guardedQuery)
|
||||
: $this->preserveCurrentInputShopQueryTerms($prompt, $shopSearchQuery);
|
||||
|
||||
return $this->cleanupDirectProductAttributeShopQuery($prompt, $query);
|
||||
$query = $this->cleanupDirectProductAttributeShopQuery($prompt, $query);
|
||||
|
||||
return $this->cleanupShopQueryStopwords($query);
|
||||
}
|
||||
|
||||
private function cleanupShopQueryStopwords(string $shopSearchQuery): string
|
||||
{
|
||||
$shopSearchQuery = trim($shopSearchQuery);
|
||||
|
||||
if (
|
||||
$shopSearchQuery === ''
|
||||
|| !$this->agentRunnerConfig->isShopQueryStopwordCleanupEnabled()
|
||||
) {
|
||||
return $shopSearchQuery;
|
||||
}
|
||||
|
||||
$removeTokens = [];
|
||||
foreach ($this->agentRunnerConfig->getShopQueryStopwordCleanupTerms() as $term) {
|
||||
foreach ($this->tokenizeShopQueryCandidate($term) as $token) {
|
||||
$removeTokens[$token] = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ($removeTokens === []) {
|
||||
return $shopSearchQuery;
|
||||
}
|
||||
|
||||
$kept = [];
|
||||
foreach ($this->tokenizeShopQueryCandidate($shopSearchQuery) as $token) {
|
||||
if (isset($removeTokens[$token]) || isset($kept[$token])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$kept[$token] = $token;
|
||||
}
|
||||
|
||||
if (count($kept) < max(1, $this->agentRunnerConfig->getShopQueryStopwordCleanupMinTokens())) {
|
||||
return $shopSearchQuery;
|
||||
}
|
||||
|
||||
$cleaned = implode(' ', array_values($kept));
|
||||
|
||||
return $cleaned !== '' ? $cleaned : $shopSearchQuery;
|
||||
}
|
||||
|
||||
private function cleanupDirectProductAttributeShopQuery(string $prompt, string $shopSearchQuery): string
|
||||
@@ -2883,6 +2941,269 @@ final readonly class AgentRunner
|
||||
return mb_strtolower($line, 'UTF-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ShopProductResult[] $shopResults
|
||||
* @return ShopProductResult[]
|
||||
*/
|
||||
private function guardDirectProductShopResults(string $prompt, string $shopSearchQuery, array $shopResults): array
|
||||
{
|
||||
if (
|
||||
$shopResults === []
|
||||
|| !$this->agentRunnerConfig->isDirectShopResultGuardEnabled()
|
||||
) {
|
||||
return $shopResults;
|
||||
}
|
||||
|
||||
$requestedTerms = $this->extractRequestedDirectProductTerms($prompt, $shopSearchQuery);
|
||||
if ($requestedTerms === []) {
|
||||
return $shopResults;
|
||||
}
|
||||
|
||||
$filtered = [];
|
||||
foreach ($shopResults as $product) {
|
||||
if (!$product instanceof ShopProductResult) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($this->shopProductMatchesAnyDirectProductTerm($product, $requestedTerms)) {
|
||||
$filtered[] = $product;
|
||||
}
|
||||
}
|
||||
|
||||
return $filtered;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ShopProductResult[] $shopResults
|
||||
* @return ShopProductResult[]
|
||||
*/
|
||||
private function sortShopResultsForLengthRequest(string $prompt, string $shopSearchQuery, array $shopResults): array
|
||||
{
|
||||
if (
|
||||
count($shopResults) < 2
|
||||
|| !$this->agentRunnerConfig->isShopResultLengthSortEnabled()
|
||||
|| !$this->isShopResultLengthSortRequested($prompt . ' ' . $shopSearchQuery)
|
||||
) {
|
||||
return $shopResults;
|
||||
}
|
||||
|
||||
$hasLength = false;
|
||||
$decorated = [];
|
||||
|
||||
foreach (array_values($shopResults) as $index => $product) {
|
||||
$length = $product instanceof ShopProductResult
|
||||
? $this->extractShopProductLengthMeters($product)
|
||||
: null;
|
||||
$hasLength = $hasLength || $length !== null;
|
||||
$decorated[] = [
|
||||
'index' => $index,
|
||||
'length' => $length,
|
||||
'product' => $product,
|
||||
];
|
||||
}
|
||||
|
||||
if (!$hasLength) {
|
||||
return $shopResults;
|
||||
}
|
||||
|
||||
usort($decorated, static function (array $a, array $b): int {
|
||||
if ($a['length'] === null && $b['length'] === null) {
|
||||
return $a['index'] <=> $b['index'];
|
||||
}
|
||||
|
||||
if ($a['length'] === null) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if ($b['length'] === null) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
$lengthCompare = $a['length'] <=> $b['length'];
|
||||
|
||||
return $lengthCompare !== 0 ? $lengthCompare : ($a['index'] <=> $b['index']);
|
||||
});
|
||||
|
||||
return array_values(array_map(
|
||||
static fn(array $row): mixed => $row['product'],
|
||||
$decorated
|
||||
));
|
||||
}
|
||||
|
||||
private function isShopResultLengthSortRequested(string $text): bool
|
||||
{
|
||||
foreach ($this->agentRunnerConfig->getShopResultLengthSortTriggerPatterns() as $pattern) {
|
||||
if (@preg_match($pattern, $text) === 1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function extractShopProductLengthMeters(ShopProductResult $product): ?float
|
||||
{
|
||||
$text = trim(implode(' ', array_filter([
|
||||
$product->name,
|
||||
$product->description,
|
||||
implode(' ', $product->highlights),
|
||||
$product->customFields,
|
||||
])));
|
||||
|
||||
if ($text === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach ($this->agentRunnerConfig->getShopResultLengthSortValuePatterns() as $pattern) {
|
||||
if (@preg_match($pattern, $text, $matches) !== 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$value = $matches['value'] ?? ($matches[1] ?? null);
|
||||
if (!is_scalar($value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$normalized = str_replace(',', '.', (string) $value);
|
||||
if (is_numeric($normalized)) {
|
||||
return (float) $normalized;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private function extractRequestedDirectProductTerms(string $prompt, string $shopSearchQuery = ''): array
|
||||
{
|
||||
$combined = trim($prompt . ' ' . $shopSearchQuery);
|
||||
if ($combined === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
$terms = [];
|
||||
foreach ($this->agentRunnerConfig->getShopQueryProductAttributeCleanupProductTypeTerms() as $term) {
|
||||
if ($this->containsAllShopQueryTokens($combined, $term)) {
|
||||
$terms[] = $term;
|
||||
}
|
||||
}
|
||||
|
||||
return array_values(array_unique($terms));
|
||||
}
|
||||
|
||||
private function containsAllShopQueryTokens(string $text, string $term): bool
|
||||
{
|
||||
$tokens = array_fill_keys($this->tokenizeShopQueryCandidate($text), true);
|
||||
$termTokens = $this->tokenizeShopQueryCandidate($term);
|
||||
|
||||
if ($tokens === [] || $termTokens === []) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($termTokens as $termToken) {
|
||||
if (!isset($tokens[$termToken])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $requestedTerms
|
||||
*/
|
||||
private function shopProductMatchesAnyDirectProductTerm(ShopProductResult $product, array $requestedTerms): bool
|
||||
{
|
||||
$productText = trim(implode(' ', array_filter([
|
||||
$product->name,
|
||||
$product->description,
|
||||
implode(' ', $product->highlights),
|
||||
$product->customFields,
|
||||
])));
|
||||
|
||||
foreach ($requestedTerms as $term) {
|
||||
if ($this->containsAllShopQueryTokens($productText, $term)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ShopProductResult[] $shopResults
|
||||
*/
|
||||
private function buildDeterministicDirectShopResultAnswer(
|
||||
string $prompt,
|
||||
array $shopResults,
|
||||
string $commerceIntent,
|
||||
bool $shopSearchAttempted,
|
||||
bool $shopSearchHadSystemFailure,
|
||||
string $shopSearchQuery
|
||||
): string {
|
||||
if (
|
||||
!$this->agentRunnerConfig->isDirectShopResultAnswerEnabled()
|
||||
|| !$this->isCommerceIntent($commerceIntent)
|
||||
|| !$shopSearchAttempted
|
||||
|| $shopSearchHadSystemFailure
|
||||
|| $this->extractRequestedDirectProductTerms($prompt, $shopSearchQuery) === []
|
||||
) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if ($shopResults === []) {
|
||||
return $this->agentRunnerConfig->getDirectShopResultAnswerNoResultsMessage();
|
||||
}
|
||||
|
||||
$lines = [$this->agentRunnerConfig->getDirectShopResultAnswerIntro()];
|
||||
|
||||
if ($this->isShopResultLengthSortRequested($prompt . ' ' . $shopSearchQuery)) {
|
||||
$note = trim($this->agentRunnerConfig->getDirectShopResultAnswerSortedByLengthNote());
|
||||
if ($note !== '') {
|
||||
$lines[] = $note;
|
||||
}
|
||||
}
|
||||
|
||||
$lines[] = '';
|
||||
foreach ($this->buildDirectShopProductLines($shopResults, 'accessory_or_consumable') as $line) {
|
||||
$lines[] = $line;
|
||||
}
|
||||
|
||||
return trim(implode("\n", $lines));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ShopProductResult[] $shopResults
|
||||
* @return string[]
|
||||
*/
|
||||
private function buildDirectShopProductLines(array $shopResults, string $requestedProductRole): array
|
||||
{
|
||||
$maxResults = max(1, $this->agentRunnerConfig->getDirectShopResultAnswerMaxResults());
|
||||
$lines = [];
|
||||
$index = 1;
|
||||
|
||||
foreach ($shopResults as $product) {
|
||||
if (!$product instanceof ShopProductResult) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$lines[] = $this->formatNoLlmShopProductLine($product, $index, $requestedProductRole);
|
||||
$index++;
|
||||
|
||||
if (count($lines) >= $maxResults) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($lines === []) {
|
||||
return [$this->agentRunnerConfig->getNoLlmProductField('unreadable_results_message')];
|
||||
}
|
||||
|
||||
return $lines;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a deterministic safety answer for environments where the LLM returns no tokens.
|
||||
*
|
||||
|
||||
@@ -732,6 +732,31 @@ final class AgentRunnerConfig
|
||||
return $this->getRequiredStringList('final_answer_guard.repeated_line.ignore_patterns');
|
||||
}
|
||||
|
||||
public function isDirectShopResultAnswerEnabled(): bool
|
||||
{
|
||||
return $this->getRequiredBool('direct_shop_result_answer.enabled');
|
||||
}
|
||||
|
||||
public function getDirectShopResultAnswerMaxResults(): int
|
||||
{
|
||||
return $this->getRequiredInt('direct_shop_result_answer.max_results');
|
||||
}
|
||||
|
||||
public function getDirectShopResultAnswerIntro(): string
|
||||
{
|
||||
return $this->getRequiredString('direct_shop_result_answer.intro');
|
||||
}
|
||||
|
||||
public function getDirectShopResultAnswerNoResultsMessage(): string
|
||||
{
|
||||
return $this->getRequiredString('direct_shop_result_answer.no_results');
|
||||
}
|
||||
|
||||
public function getDirectShopResultAnswerSortedByLengthNote(): string
|
||||
{
|
||||
return $this->getRequiredString('direct_shop_result_answer.sorted_by_length_note');
|
||||
}
|
||||
|
||||
public function getNoLlmFallbackMaxShopResults(): int
|
||||
{
|
||||
return $this->getRequiredInt('no_llm_fallback.max_shop_results');
|
||||
@@ -1082,6 +1107,50 @@ final class AgentRunnerConfig
|
||||
return $this->getRequiredStringList('shop_prompt.product_attribute_query_cleanup.comparative_constraint_patterns');
|
||||
}
|
||||
|
||||
public function isShopQueryStopwordCleanupEnabled(): bool
|
||||
{
|
||||
return $this->getRequiredBool('shop_prompt.query_stopword_cleanup.enabled');
|
||||
}
|
||||
|
||||
public function getShopQueryStopwordCleanupMinTokens(): int
|
||||
{
|
||||
return $this->getRequiredInt('shop_prompt.query_stopword_cleanup.min_query_tokens_after_cleanup');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getShopQueryStopwordCleanupTerms(): array
|
||||
{
|
||||
return $this->getRequiredStringList('shop_prompt.query_stopword_cleanup.terms');
|
||||
}
|
||||
|
||||
public function isDirectShopResultGuardEnabled(): bool
|
||||
{
|
||||
return $this->getRequiredBool('shop_prompt.direct_result_guard.enabled');
|
||||
}
|
||||
|
||||
public function isShopResultLengthSortEnabled(): bool
|
||||
{
|
||||
return $this->getRequiredBool('shop_prompt.length_sort.enabled');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getShopResultLengthSortTriggerPatterns(): array
|
||||
{
|
||||
return $this->getRequiredStringList('shop_prompt.length_sort.trigger_patterns');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getShopResultLengthSortValuePatterns(): array
|
||||
{
|
||||
return $this->getRequiredStringList('shop_prompt.length_sort.value_patterns');
|
||||
}
|
||||
|
||||
public function getShopPromptIntro(): string
|
||||
{
|
||||
return $this->getRequiredString('shop_prompt.intro');
|
||||
|
||||
Reference in New Issue
Block a user