rm CachedRetriever.php
add second shopsearch
This commit is contained in:
@@ -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>
|
||||
*/
|
||||
|
||||
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user