systemMsgOn = true; } public function run(string $prompt, string $userId, bool $includeFullContext = false): Generator { $prompt = trim($prompt); if ($prompt === '') { yield $this->systemMsg('❌ Empty prompt.', 'err'); return; } $shopResults = []; $sources = []; $optimizedShopQuery = ''; $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. } 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); if ($urlContent !== '') { $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); if ($knowledgeChunks !== []) { $this->addSource($sources, 'RAG Wissen'); } // --------------------------------------------------------- // 4) Optional commerce/shop search // --------------------------------------------------------- $commerceIntent = $this->detectCommerceIntent($prompt); if ($this->isCommerceIntent($commerceIntent)) { yield $this->systemMsg('Ich optimiere die Recherche...', 'think'); $commerceHistoryContext = $this->buildCommerceHistoryContext($userId); if($commerceHistoryContext){ $this->addSource($sources, 'Chatverlauf'); } $optimizedShopQuery = $this->buildOptimizedShopQuery( $prompt, $userId, $commerceHistoryContext ); $shopSearchQuery = $optimizedShopQuery !== '' ? $optimizedShopQuery : $prompt; yield $this->systemMsg( 'Ich rufe Recherchedaten ab (type: ' . $commerceIntent . ')', 'think' ); $shopResults = $this->searchShop( $shopSearchQuery, $commerceIntent, $userId, $commerceHistoryContext ); if ($shopResults !== []) { $this->addSource($sources, 'Shopsystem'); } } $knowledgeChunks = $this->limitKnowledgeChunks($knowledgeChunks, $commerceIntent); yield $this->systemMsg('Ich analysiere alle Informationen...', 'think'); // --------------------------------------------------------- // 5) Build final prompt // --------------------------------------------------------- $finalPrompt = $this->promptBuilder->build( prompt: $prompt, userId: $userId, urlContent: $urlContent, knowledgeChunks: $knowledgeChunks, shopResults: $shopResults, fullContext: $includeFullContext, swagFullOutPut: $optimizedShopQuery ); if ($this->debug && $this->logPrompt) { $this->agentLogger->debug('Final prompt', [ 'userId' => $userId, 'finalPrompt' => $finalPrompt, 'optimizedShopQuery' => $optimizedShopQuery, ]); } if ($this->debug && $this->logContext) { $this->agentLogger->debug('Conversation context snapshot', [ 'userId' => $userId, 'context' => $this->contextService->buildUserContext( $userId, $includeFullContext ), ]); } if ($sources !== []) { yield $this->emitSources($sources, 'Genutzte Quellen: '); } // --------------------------------------------------------- // 6) Stream final LLM answer // --------------------------------------------------------- $fullOutput = yield from $this->streamFinalAnswer($finalPrompt); if ($sources !== []) { yield $this->emitSources($sources, 'Quellen: '); } if ($this->debug) { yield $this->systemMsg($finalPrompt, 'debug'); } // --------------------------------------------------------- // 7) Persist conversation history // --------------------------------------------------------- if ($fullOutput !== '') { $this->contextService->appendHistory( $userId, $prompt, $fullOutput ); } $this->agentLogger->info('Agent run finished', [ 'userId' => $userId, 'outputLength' => mb_strlen($fullOutput), 'contextMode' => $includeFullContext ? 'full' : 'recent', 'commerceIntent' => $commerceIntent, 'shopResultsCount' => count($shopResults), 'knowledgeChunkCount' => count($knowledgeChunks), 'hasUrlContent' => $urlContent !== '', 'usedOptimizedShopQuery' => $optimizedShopQuery !== '', ]); } catch (Throwable $e) { $this->agentLogger->error('Agent run failed', [ 'userId' => $userId, 'exception' => $e, ]); yield $this->systemMsg($this->buildUserErrorMessage($e), 'err'); } } private function detectCommerceIntent(string $prompt): string { $commerceMeta = $this->commerceIntentLite->detect($prompt); return (string) ($commerceMeta['intent'] ?? CommerceIntentLite::NONE); } private function isCommerceIntent(string $commerceIntent): bool { return $commerceIntent === CommerceIntentLite::PRODUCT_SEARCH || $commerceIntent === CommerceIntentLite::ADVISORY_PRODUCT_SEARCH; } private function buildOptimizedShopQuery( string $prompt, string $userId, string $commerceHistoryContext = '' ): string { $shopPrompt = trim($this->agentRunnerConfig->getShopPrompt( $prompt, $commerceHistoryContext )); if ($shopPrompt === '') { return ''; } $optimizedQuery = ''; $this->thinkSuppressor->reset(); try { foreach ($this->ollamaClient->stream($shopPrompt) as $token) { if (!is_string($token)) { continue; } $cleanToken = $this->thinkSuppressor->filter($token); if ($cleanToken === '') { continue; } $optimizedQuery .= $cleanToken; } } catch (Throwable $e) { $this->agentLogger->warning('Shop query optimization failed, falling back to original prompt', [ 'userId' => $userId, 'exception' => $e, ]); return ''; } return trim($optimizedQuery); } private function searchShop( string $query, string $commerceIntent, string $userId, string $commerceHistoryContext = '' ): array { try { return $this->shopSearchService->search( $query, $commerceIntent, $commerceHistoryContext ); } catch (Throwable $e) { $this->agentLogger->warning('Shop search failed, continuing without shop results', [ 'userId' => $userId, 'commerceIntent' => $commerceIntent, 'query' => $query, 'exception' => $e, ]); return []; } } private function buildCommerceHistoryContext(string $userId): string { return $this->contextService->buildUserContextWithinBudget( $userId, self::COMMERCE_HISTORY_BUDGET_CHARS ); } private function limitKnowledgeChunks(array $knowledgeChunks, string $commerceIntent): array { return match ($commerceIntent) { CommerceIntentLite::PRODUCT_SEARCH => array_slice($knowledgeChunks, 0, 2), CommerceIntentLite::ADVISORY_PRODUCT_SEARCH => array_slice($knowledgeChunks, 0, 3), default => $knowledgeChunks, }; } /** * @return Generator */ private function streamFinalAnswer(string $finalPrompt): Generator { $fullOutput = ''; $firstThinkLoop = true; $chunker = new StreamChunker(); $this->thinkSuppressor->reset(); foreach ($this->ollamaClient->stream($finalPrompt) as $token) { if (!is_string($token)) { continue; } $cleanToken = $this->thinkSuppressor->filter($token); if ($cleanToken === '') { if ($firstThinkLoop) { yield $this->systemMsg('Denke nach...', 'think'); $firstThinkLoop = false; } continue; } $fullOutput .= $cleanToken; $chunk = $chunker->push($cleanToken); if ($chunk !== null) { yield $this->systemMsg($chunk, 'answer'); } } $finalChunk = $chunker->flush(); if ($finalChunk !== null) { yield $this->systemMsg($finalChunk, 'answer'); } elseif ($fullOutput === '') { yield $this->systemMsg('❌ Es wurden keine Daten vom LLM empfangen.', 'err'); } return $fullOutput; } /** * @param string[] $sources */ private function emitSources(array $sources, string $prefix): string { return $this->systemMsg($prefix . implode(' ', $sources), 'info'); } /** * @param string[] $sources */ private function addSource(array &$sources, string $label): void { $badge = $this->badge($label); if (!in_array($badge, $sources, true)) { $sources[] = $badge; } } private function buildUserErrorMessage(Throwable $e): string { if (!$this->debug) { return '❌ Bei der Verarbeitung der Anfrage ist ein interner Fehler aufgetreten.'; } return '❌ Interner Fehler: ' . htmlspecialchars($e->getMessage(), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); } private function badge(string $label): string { return sprintf( '%s', htmlspecialchars($label, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ); } private function systemMsg(string $msg, string $type = ''): string { if (!$this->systemMsgOn) { return ''; } return match ($type) { 'answer' => $msg, 'err' => '' . $msg . "\n
\n", 'think' => '' . $msg . "\n", 'info' => "\n\n" . $msg . "\n", 'debug' => "\n\nDEBUG: " . htmlspecialchars($msg, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . "\n", default => $msg, }; } }