rm CachedRetriever.php

add second shopsearch
This commit is contained in:
team 1
2026-04-19 14:20:23 +02:00
parent a71426c300
commit 4d944a5113
9 changed files with 1075 additions and 129 deletions

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Agent;
use App\Commerce\SearchRepairService;
use App\Commerce\ShopSearchService;
use App\Config\AgentRunnerConfig;
use App\Context\ContextService;
@@ -28,6 +29,7 @@ final readonly class AgentRunner
private UrlAnalyzer $urlAnalyzer,
private RetrieverInterface $retriever,
private ShopSearchService $shopSearchService,
private SearchRepairService $searchRepairService,
private CommerceIntentLite $commerceIntentLite,
private OllamaClient $ollamaClient,
private LoggerInterface $agentLogger,
@@ -49,20 +51,21 @@ final readonly class AgentRunner
}
$shopResults = [];
$primaryShopResults = [];
$sources = [];
$optimizedShopQuery = '';
$shopSearchQuery = '';
$commerceIntent = CommerceIntentLite::NONE;
$commerceHistoryContext = '';
$attemptedShopRepair = false;
$usedShopRepair = false;
$shopRepairQueries = [];
$this->agentLogger->info('Agent run started', [
'userId' => $userId,
]);
try {
// ---------------------------------------------------------
// 1) Context strategy
// ---------------------------------------------------------
if ($includeFullContext) {
// Full context mode is already passed to PromptBuilder.
// Additional context strategies can be added here later.
@@ -70,9 +73,6 @@ final readonly class AgentRunner
yield $this->systemMsg('Ich analysiere deine Anfrage...', 'think');
// ---------------------------------------------------------
// 2) Extract URL content
// ---------------------------------------------------------
yield $this->systemMsg('Ich prüfe auf Internetquellen...', 'think');
$urlContent = $this->urlAnalyzer->extractContentFromPrompt($prompt);
@@ -80,9 +80,6 @@ final readonly class AgentRunner
$this->addSource($sources, 'Externe URL');
}
// ---------------------------------------------------------
// 3) Retrieve RAG knowledge
// ---------------------------------------------------------
yield $this->systemMsg('Ich hole relevante Daten aus meinem RAG-Wissen...', 'think');
$knowledgeChunks = $this->retriever->retrieve($prompt);
@@ -90,9 +87,6 @@ final readonly class AgentRunner
$this->addSource($sources, 'RAG Wissen');
}
// ---------------------------------------------------------
// 4) Optional commerce/shop search
// ---------------------------------------------------------
$commerceIntent = $this->detectCommerceIntent($prompt);
if ($this->isCommerceIntent($commerceIntent)) {
@@ -127,16 +121,35 @@ final readonly class AgentRunner
'think'
);
$shopResults = $this->searchShop(
$primaryShopResults = $this->searchShop(
$shopSearchQuery,
$commerceIntent,
$userId,
$commerceHistoryContext
);
$repairPayload = $this->repairShopResults(
prompt: $prompt,
userId: $userId,
commerceIntent: $commerceIntent,
commerceHistoryContext: $commerceHistoryContext,
primaryQuery: $shopSearchQuery,
primaryShopResults: $primaryShopResults,
knowledgeChunks: $knowledgeChunks
);
$shopResults = $repairPayload['results'];
$attemptedShopRepair = $repairPayload['attemptedRepair'];
$usedShopRepair = $repairPayload['usedRepair'];
$shopRepairQueries = $repairPayload['repairQueries'];
if ($shopResults !== []) {
$this->addSource($sources, 'Shopsystem');
}
if ($attemptedShopRepair) {
$this->addSource($sources, 'Erweiterte Shopsuche');
}
}
if ($shopResults !== []) {
@@ -145,9 +158,6 @@ final readonly class AgentRunner
yield $this->systemMsg('Ich analysiere alle Informationen...', 'think');
// ---------------------------------------------------------
// 5) Build final prompt
// ---------------------------------------------------------
$finalPrompt = $this->promptBuilder->build(
prompt: $prompt,
userId: $userId,
@@ -164,6 +174,11 @@ final readonly class AgentRunner
'finalPrompt' => $finalPrompt,
'optimizedShopQuery' => $optimizedShopQuery,
'shopSearchQuery' => $shopSearchQuery,
'primaryShopResultsCount' => count($primaryShopResults),
'shopResultsCount' => count($shopResults),
'attemptedShopRepair' => $attemptedShopRepair,
'usedShopRepair' => $usedShopRepair,
'shopRepairQueries' => $shopRepairQueries,
]);
}
@@ -181,9 +196,6 @@ final readonly class AgentRunner
yield $this->emitSources($sources, 'Genutzte Quellen: ');
}
// ---------------------------------------------------------
// 6) Stream final LLM answer
// ---------------------------------------------------------
$fullOutput = yield from $this->streamFinalAnswer($finalPrompt);
if ($sources !== []) {
@@ -194,9 +206,6 @@ final readonly class AgentRunner
yield $this->systemMsg($finalPrompt, 'debug');
}
// ---------------------------------------------------------
// 7) Persist conversation history
// ---------------------------------------------------------
if ($fullOutput !== '') {
$this->contextService->appendHistory(
$userId,
@@ -210,7 +219,11 @@ final readonly class AgentRunner
'outputLength' => mb_strlen($fullOutput),
'contextMode' => $includeFullContext ? 'full' : 'recent',
'commerceIntent' => $commerceIntent,
'primaryShopResultsCount' => count($primaryShopResults),
'shopResultsCount' => count($shopResults),
'attemptedShopRepair' => $attemptedShopRepair,
'usedShopRepair' => $usedShopRepair,
'shopRepairQueries' => $shopRepairQueries,
'knowledgeChunkCount' => count($knowledgeChunks),
'hasUrlContent' => $urlContent !== '',
'usedOptimizedShopQuery' => $optimizedShopQuery !== '',
@@ -282,7 +295,51 @@ final readonly class AgentRunner
return '';
}
return trim($optimizedQuery);
return $this->sanitizeOptimizedShopQuery($optimizedQuery);
}
/**
* @return array{
* results: array,
* attemptedRepair: bool,
* usedRepair: bool,
* repairQueries: string[]
* }
*/
private function repairShopResults(
string $prompt,
string $userId,
string $commerceIntent,
string $commerceHistoryContext,
string $primaryQuery,
array $primaryShopResults,
array $knowledgeChunks
): array {
try {
return $this->searchRepairService->repair(
prompt: $prompt,
commerceIntent: $commerceIntent,
commerceHistoryContext: $commerceHistoryContext,
primaryQuery: $primaryQuery,
primaryShopResults: $primaryShopResults,
knowledgeChunks: $knowledgeChunks
);
} catch (Throwable $e) {
$this->agentLogger->warning('Shop repair failed, continuing with primary shop results', [
'userId' => $userId,
'commerceIntent' => $commerceIntent,
'primaryQuery' => $primaryQuery,
'primaryShopResultsCount' => count($primaryShopResults),
'exception' => $e,
]);
return [
'results' => $primaryShopResults,
'attemptedRepair' => false,
'usedRepair' => false,
'repairQueries' => [],
];
}
}
private function searchShop(
@@ -328,6 +385,22 @@ final readonly class AgentRunner
};
}
private function sanitizeOptimizedShopQuery(string $query): string
{
$query = trim($query);
if ($query === '') {
return '';
}
$query = preg_split('/\R+/u', $query, 2)[0] ?? $query;
$query = preg_replace('/^(?:keywords?|suchquery|search\s*query|query)\s*:\s*/iu', '', $query) ?? $query;
$query = trim($query, " \t\n\r\0\x0B\"'`");
$query = preg_replace('/\s+/u', ' ', $query) ?? $query;
return trim($query);
}
/**
* @return Generator<int, string, mixed, string>
*/

View File

@@ -87,6 +87,22 @@ final readonly class PromptBuilder
'testomat',
];
private const ACCESSORY_REQUEST_KEYWORDS = [
'passend',
'passende',
'passendes',
'zubehör',
'zubehor',
'dazu',
'indikator',
'reagenz',
'kit',
'set',
'zusatz',
'ergänzung',
'ergaenzung',
];
public function __construct(
private ContextService $contextService,
private SystemPromptRepository $systemPromptRepository,
@@ -119,18 +135,20 @@ final readonly class PromptBuilder
$swagFullOutPut = $this->normalizeNullableBlockText($swagFullOutPut);
$hasShopResults = $shopResults !== [];
$isTechnicalProductQuestion = $this->isLikelyTechnicalProductQuestion($prompt);
$systemBlock = $this->buildSystemBlock();
$shopBlock = $this->buildShopBlock($shopResults, $swagFullOutPut);
$outputPriorityBlock = $this->buildOutputPriorityBlock($hasShopResults);
$responseFormatBlock = $this->buildResponseFormatBlock($prompt, $hasShopResults, $isTechnicalProductQuestion);
$knowledgeBlock = $this->buildKnowledgeBlock($knowledgeChunks, $urlContent, $prompt, $hasShopResults);
$userBlock = $this->buildUserBlock($prompt);
// Build fixed blocks first so history only receives the remaining budget.
$fixedPrompt = $this->implodeBlocks([
$systemBlock,
$shopBlock,
$outputPriorityBlock,
$responseFormatBlock,
$knowledgeBlock,
$userBlock,
]);
@@ -145,6 +163,7 @@ final readonly class PromptBuilder
$systemBlock,
$shopBlock,
$outputPriorityBlock,
$responseFormatBlock,
$knowledgeBlock,
$contextBlock,
$userBlock,
@@ -326,6 +345,39 @@ final readonly class PromptBuilder
"Do not let bundles, accessories, or service items override a better technical match unless the user explicitly asks for them.\n";
}
private function buildResponseFormatBlock(
string $prompt,
bool $hasShopResults,
bool $isTechnicalProductQuestion
): string {
$rules = [
"RESPONSE FORMAT RULES:",
"- Keep normal spacing between all words. Never fuse words together.",
"- Use short, clean paragraphs or short labeled sections.",
"- Do not use persuasive or promotional wording.",
"- Do not repeat the same fact in slightly different wording.",
];
if ($hasShopResults) {
$rules[] = "- If a product is identified, prefer this structure per product: product name, product number, price, availability, URL, then only the most relevant technical facts.";
$rules[] = "- Keep price, availability, and URL on separate lines when they are present.";
}
if ($isTechnicalProductQuestion) {
$rules[] = "- Write like technical documentation: precise, neutral, and source-close.";
$rules[] = "- Prefer exact values, ranges, thresholds, compatibility notes, and application areas over general explanation.";
}
if ($this->asksForAccessoryOrBundle($prompt)) {
$rules[] = "- If the user asks for a matching accessory, separate the answer into: main device and matching accessory.";
$rules[] = "- The main device must come first. The accessory must not replace the main device.";
$rules[] = "- Only name an accessory as matching if compatibility is explicitly grounded in the provided sources.";
$rules[] = "- Do not call accessories, indicators, reagents, kits, sets, or consumables a device, measuring device, or main product unless the source explicitly says so.";
}
return implode("\n", $rules);
}
/**
* Build the knowledge block.
*
@@ -451,6 +503,8 @@ final readonly class PromptBuilder
"- Use retrieved knowledge as highest priority for technical matching, thresholds, measurement principles, and technical explanation.",
"- When shop results are present and relevant, include current price and the actual URL if available.",
"- Do not let accessories, bundles, or service items override a technically better product match unless the user explicitly asks for them.",
"- Do not call accessories, indicators, reagents, kits, sets, or consumables a device, measuring device, or main product unless the source explicitly says so.",
"- Do not claim that an accessory is required, necessary, used for calibration, or sets the measurement range unless this is explicitly stated in the provided sources.",
]);
} else {
$rules[] = "- Use retrieved knowledge as authoritative for factual answers.";
@@ -484,10 +538,10 @@ final readonly class PromptBuilder
{
$filtered = array_values(array_filter(
array_map(
fn ($block): string => is_string($block) ? $this->normalizeBlockText($block) : '',
fn($block): string => is_string($block) ? $this->normalizeBlockText($block) : '',
$blocks
),
static fn (string $block): bool => $block !== ''
static fn(string $block): bool => $block !== ''
));
return implode("\n\n", $filtered);
@@ -536,6 +590,19 @@ final readonly class PromptBuilder
return preg_match('/\b[\p{L}]{2,}\s?\d{2,5}\b/u', $prompt) === 1;
}
private function asksForAccessoryOrBundle(string $prompt): bool
{
$normalized = mb_strtolower($prompt, 'UTF-8');
foreach (self::ACCESSORY_REQUEST_KEYWORDS as $keyword) {
if (str_contains($normalized, $keyword)) {
return true;
}
}
return false;
}
private function clamp(int $value, int $min, int $max): int
{
return max($min, min($max, $value));