optimize retrieval

This commit is contained in:
team 1
2026-04-23 15:47:53 +02:00
parent 8a31e99669
commit 87417febf4
13 changed files with 2093 additions and 287 deletions

View File

@@ -4,6 +4,9 @@ declare(strict_types=1);
namespace App\Agent;
use App\Commerce\CommerceReferenceResolver;
use App\Commerce\CommerceReferenceStore;
use App\Commerce\Dto\CommerceReferenceContext;
use App\Commerce\SearchRepairService;
use App\Commerce\ShopSearchService;
use App\Config\AgentRunnerConfig;
@@ -30,6 +33,8 @@ final readonly class AgentRunner
private RetrieverInterface $retriever,
private ShopSearchService $shopSearchService,
private SearchRepairService $searchRepairService,
private CommerceReferenceStore $commerceReferenceStore,
private CommerceReferenceResolver $commerceReferenceResolver,
private CommerceIntentLite $commerceIntentLite,
private OllamaClient $ollamaClient,
private LoggerInterface $agentLogger,
@@ -52,7 +57,8 @@ final readonly class AgentRunner
$shopResults = [];
$primaryShopResults = [];
$sources = [];
$factSources = [];
$contextSignals = [];
$optimizedShopQuery = '';
$shopSearchQuery = '';
$commerceIntent = CommerceIntentLite::NONE;
@@ -60,6 +66,8 @@ final readonly class AgentRunner
$attemptedShopRepair = false;
$usedShopRepair = false;
$shopRepairQueries = [];
$activeCommerceReference = null;
$shopChecked = false;
$this->agentLogger->info('Agent run started', [
'userId' => $userId,
@@ -72,19 +80,18 @@ final readonly class AgentRunner
}
yield $this->systemMsg('Ich analysiere deine Anfrage...', 'think');
yield $this->systemMsg('Ich prüfe auf Internetquellen...', 'think');
$urlContent = $this->urlAnalyzer->extractContentFromPrompt($prompt);
if ($urlContent !== '') {
$this->addSource($sources, 'Externe URL');
$this->addBadge($factSources, 'Externe URL');
}
yield $this->systemMsg('Ich hole relevante Daten aus meinem RAG-Wissen...', 'think');
$knowledgeChunks = $this->retriever->retrieve($prompt);
if ($knowledgeChunks !== []) {
$this->addSource($sources, 'RAG Wissen');
$this->addBadge($factSources, 'RAG Wissen');
}
$commerceIntent = $this->detectCommerceIntent($prompt);
@@ -93,18 +100,53 @@ final readonly class AgentRunner
yield $this->systemMsg('Ich optimiere die Recherche...', 'think');
$commerceHistoryContext = $this->buildCommerceHistoryContext($userId);
$activeCommerceReference = $this->loadCommerceReference($userId);
if ($commerceHistoryContext !== '') {
$this->addSource($sources, 'Chatverlauf');
$this->addBadge($contextSignals, 'Gesprächskontext');
}
$optimizedShopQuery = $this->buildOptimizedShopQuery(
if ($activeCommerceReference !== null) {
$this->addBadge($contextSignals, 'Commerce-Referenz');
}
$isReferenceOnlyFollowUp = $this->isReferenceOnlyCommerceFollowUp(
$prompt,
$userId,
$commerceHistoryContext
$activeCommerceReference
);
$shopSearchQuery = $optimizedShopQuery !== '' ? $optimizedShopQuery : $prompt;
if ($isReferenceOnlyFollowUp) {
$shopSearchQuery = $this->buildDeterministicReferenceShopQuery($activeCommerceReference);
if ($shopSearchQuery !== '') {
$this->addBadge($contextSignals, 'Deterministische Referenzsuche');
}
$this->agentLogger->info('Using deterministic reference shop query', [
'userId' => $userId,
'commerceIntent' => $commerceIntent,
'prompt' => $prompt,
'shopSearchQuery' => $shopSearchQuery,
'referenceProductName' => $activeCommerceReference?->productName,
'referenceFocusTerms' => $activeCommerceReference?->focusTerms,
]);
} else {
$optimizedShopQuery = $this->buildOptimizedShopQuery(
$prompt,
$userId,
$commerceHistoryContext
);
if ($optimizedShopQuery !== '' && $optimizedShopQuery !== $prompt) {
$this->addBadge($contextSignals, 'Query-Optimierung');
}
$shopSearchQuery = $optimizedShopQuery !== '' ? $optimizedShopQuery : $prompt;
}
if ($shopSearchQuery === '') {
$shopSearchQuery = $prompt;
}
$this->agentLogger->info('Commerce search prepared', [
'userId' => $userId,
@@ -112,8 +154,11 @@ final readonly class AgentRunner
'usedOptimizedShopQuery' => $optimizedShopQuery !== '',
'optimizedShopQuery' => $optimizedShopQuery,
'shopSearchQuery' => $shopSearchQuery,
'usedDeterministicReferenceQuery' => $isReferenceOnlyFollowUp,
'hasCommerceHistoryContext' => $commerceHistoryContext !== '',
'commerceHistoryContextLength' => mb_strlen($commerceHistoryContext),
'hasActiveCommerceReference' => $activeCommerceReference !== null,
'activeCommerceReferenceProduct' => $activeCommerceReference?->productName,
]);
yield $this->systemMsg(
@@ -121,11 +166,14 @@ final readonly class AgentRunner
'think'
);
$shopChecked = true;
$primaryShopResults = $this->searchShop(
$shopSearchQuery,
$commerceIntent,
$userId,
$commerceHistoryContext
$commerceHistoryContext,
$activeCommerceReference
);
$repairPayload = $this->repairShopResults(
@@ -144,11 +192,13 @@ final readonly class AgentRunner
$shopRepairQueries = $repairPayload['repairQueries'];
if ($shopResults !== []) {
$this->addSource($sources, 'Shopsystem');
$this->addBadge($factSources, 'Shopsystem');
} elseif ($shopChecked) {
$this->addBadge($factSources, 'Shopsystem geprüft');
}
if ($attemptedShopRepair) {
$this->addSource($sources, 'Erweiterte Shopsuche');
$this->addBadge($contextSignals, 'Erweiterte Shopsuche');
}
}
@@ -176,6 +226,7 @@ final readonly class AgentRunner
'shopSearchQuery' => $shopSearchQuery,
'primaryShopResultsCount' => count($primaryShopResults),
'shopResultsCount' => count($shopResults),
'shopChecked' => $shopChecked,
'attemptedShopRepair' => $attemptedShopRepair,
'usedShopRepair' => $usedShopRepair,
'shopRepairQueries' => $shopRepairQueries,
@@ -192,14 +243,22 @@ final readonly class AgentRunner
]);
}
if ($sources !== []) {
yield $this->emitSources($sources, 'Genutzte Quellen: ');
if ($factSources !== [] || $contextSignals !== []) {
yield $this->emitSourceSummary(
$factSources,
$contextSignals,
'Genutzte Datenpfade'
);
}
$fullOutput = yield from $this->streamFinalAnswer($finalPrompt);
if ($sources !== []) {
yield $this->emitSources($sources, 'Quellen: ');
if ($factSources !== [] || $contextSignals !== []) {
yield $this->emitSourceSummary(
$factSources,
$contextSignals,
'Quellen und Signale'
);
}
if ($this->debug) {
@@ -207,10 +266,11 @@ final readonly class AgentRunner
}
if ($fullOutput !== '') {
$this->contextService->appendHistory(
$userId,
$prompt,
$fullOutput
$this->persistConversationState(
userId: $userId,
prompt: $prompt,
fullOutput: $fullOutput,
shopResults: $shopResults
);
}
@@ -221,6 +281,7 @@ final readonly class AgentRunner
'commerceIntent' => $commerceIntent,
'primaryShopResultsCount' => count($primaryShopResults),
'shopResultsCount' => count($shopResults),
'shopChecked' => $shopChecked,
'attemptedShopRepair' => $attemptedShopRepair,
'usedShopRepair' => $usedShopRepair,
'shopRepairQueries' => $shopRepairQueries,
@@ -231,6 +292,8 @@ final readonly class AgentRunner
'shopSearchQuery' => $shopSearchQuery,
'hasCommerceHistoryContext' => $commerceHistoryContext !== '',
'commerceHistoryContextLength' => mb_strlen($commerceHistoryContext),
'hasActiveCommerceReference' => $activeCommerceReference !== null,
'activeCommerceReferenceProduct' => $activeCommerceReference?->productName,
]);
} catch (Throwable $e) {
$this->agentLogger->error('Agent run failed', [
@@ -298,6 +361,42 @@ final readonly class AgentRunner
return $this->sanitizeOptimizedShopQuery($optimizedQuery);
}
private function isReferenceOnlyCommerceFollowUp(
string $prompt,
?CommerceReferenceContext $referenceContext
): bool {
if ($referenceContext === null) {
return false;
}
$normalizedPrompt = mb_strtolower(trim($prompt), 'UTF-8');
$normalizedPrompt = preg_replace('/[^\p{L}\p{N}\s]+/u', ' ', $normalizedPrompt) ?? $normalizedPrompt;
$normalizedPrompt = preg_replace('/\s+/u', ' ', $normalizedPrompt) ?? $normalizedPrompt;
$normalizedPrompt = trim($normalizedPrompt);
if ($normalizedPrompt === '') {
return false;
}
if (preg_match('/\b(testomat|lab|evo|eco|calc|thcl|808|2000)\b/u', $normalizedPrompt) === 1) {
return false;
}
return preg_match(
'/\b(preis|preise|kosten|kostet|dazu|dafuer|dafür|davon|was kostet das|verfuegbarkeit|verfügbarkeit|shop|link)\b/u',
$normalizedPrompt
) === 1;
}
private function buildDeterministicReferenceShopQuery(?CommerceReferenceContext $referenceContext): string
{
if ($referenceContext === null) {
return '';
}
return trim($referenceContext->buildReferenceSearchText());
}
/**
* @return array{
* results: array,
@@ -346,13 +445,15 @@ final readonly class AgentRunner
string $query,
string $commerceIntent,
string $userId,
string $commerceHistoryContext = ''
string $commerceHistoryContext = '',
?CommerceReferenceContext $referenceContext = null
): array {
try {
return $this->shopSearchService->search(
$query,
$commerceIntent,
$commerceHistoryContext
$commerceHistoryContext,
$referenceContext
);
} catch (Throwable $e) {
$this->agentLogger->warning('Shop search failed, continuing without shop results', [
@@ -361,6 +462,8 @@ final readonly class AgentRunner
'query' => $query,
'hasCommerceHistoryContext' => $commerceHistoryContext !== '',
'commerceHistoryContextLength' => mb_strlen($commerceHistoryContext),
'hasReferenceContext' => $referenceContext !== null,
'referenceProductName' => $referenceContext?->productName,
'exception' => $e,
]);
@@ -376,6 +479,64 @@ final readonly class AgentRunner
);
}
private function loadCommerceReference(string $userId): ?CommerceReferenceContext
{
try {
return $this->commerceReferenceStore->load($userId);
} catch (Throwable $e) {
$this->agentLogger->warning('Failed to load commerce reference context', [
'userId' => $userId,
'exception' => $e,
]);
return null;
}
}
/**
* @param array<int, mixed> $shopResults
*/
private function storeCommerceReference(string $userId, string $prompt, string $answer, array $shopResults): void
{
try {
$referenceContext = $this->commerceReferenceResolver->resolveFromCommerceTurn(
$prompt,
$answer,
$shopResults
);
if ($referenceContext === null) {
return;
}
$this->commerceReferenceStore->save($userId, $referenceContext);
} catch (Throwable $e) {
$this->agentLogger->warning('Failed to persist commerce reference context', [
'userId' => $userId,
'exception' => $e,
]);
}
}
/**
* @param array<int, mixed> $shopResults
*/
private function persistConversationState(
string $userId,
string $prompt,
string $fullOutput,
array $shopResults
): void {
$this->contextService->appendHistory($userId, $prompt, $fullOutput);
$this->storeCommerceReference(
userId: $userId,
prompt: $prompt,
answer: $fullOutput,
shopResults: $shopResults
);
}
private function limitKnowledgeChunks(array $knowledgeChunks, string $commerceIntent): array
{
return match ($commerceIntent) {
@@ -447,22 +608,36 @@ final readonly class AgentRunner
}
/**
* @param string[] $sources
* @param string[] $factSources
* @param string[] $contextSignals
*/
private function emitSources(array $sources, string $prefix): string
private function emitSourceSummary(array $factSources, array $contextSignals, string $label): string
{
return $this->systemMsg($prefix . implode(' ', $sources), 'info');
$parts = [];
if ($factSources !== []) {
$parts[] = 'Fakten: ' . implode(' ', $factSources);
}
if ($contextSignals !== []) {
$parts[] = 'Kontext: ' . implode(' ', $contextSignals);
}
return $this->systemMsg(
$label . ': ' . implode(' &nbsp;&nbsp; ', $parts),
'info'
);
}
/**
* @param string[] $sources
* @param string[] $target
*/
private function addSource(array &$sources, string $label): void
private function addBadge(array &$target, string $label): void
{
$badge = $this->badge($label);
if (!in_array($badge, $sources, true)) {
$sources[] = $badge;
if (!in_array($badge, $target, true)) {
$target[] = $badge;
}
}