Protokoll #29334

ID29334
Zeitstempel2025-12-29 09:07:55.670995
Clientroot
IP145.224.72.140
Modellclaude-sonnet-4-20250514
Statuscompleted
Tokens2,671 (Input: 41, Output: 2,630)
Dauer117 ms
Request-Zeit2025-12-29 09:07:55.670995
Response-Zeit2025-12-29 09:07:55.788044

Request

{
    "event": "PreToolUse",
    "tool_name": "Read",
    "tool_input": {
        "file_path": "\/var\/www\/dev.campus.systemische-tools.de\/src\/UseCases\/Chat\/StreamingChatMessageUseCase.php"
    }
}

Response

{
    "tool_response": {
        "type": "text",
        "file": {
            "filePath": "\/var\/www\/dev.campus.systemische-tools.de\/src\/UseCases\/Chat\/StreamingChatMessageUseCase.php",
            "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace UseCases\\Chat;\n\n\/\/ @responsibility: Orchestriert Chat-Nachrichten mit SSE-Progress-Events\n\nuse Domain\\Constants;\nuse Domain\\Repository\\ChatMessageRepositoryInterface;\nuse Domain\\Repository\\ChatSessionRepositoryInterface;\nuse Infrastructure\\AI\\ClaudeService;\nuse Infrastructure\\AI\\ContentQualityValidator;\nuse Infrastructure\\AI\\OllamaService;\nuse Infrastructure\\AI\\QdrantService;\nuse Infrastructure\\AI\\ScoringService;\nuse Infrastructure\\Logging\\KiProtokollService;\n\nclass StreamingChatMessageUseCase\n{\n    \/** @var callable|null *\/\n    private $progressCallback;\n\n    private float $stepStart = 0.0;\n\n    public function __construct(\n        private OllamaService $ollama,\n        private QdrantService $qdrant,\n        private ClaudeService $claude,\n        ScoringService $scoring,\n        private ChatSessionRepositoryInterface $sessionRepo,\n        private ChatMessageRepositoryInterface $messageRepo,\n        private ChatPromptLoader $promptLoader,\n        private ContentQualityValidator $qualityValidator,\n        private RagContextBuilder $ragBuilder,\n        private KiProtokollService $protokollService\n    ) {\n        \/\/ $scoring kept for BC but no longer used - ContentSearchService handles reranking\n        unset($scoring);\n    }\n\n    \/**\n     * Set progress callback for SSE events\n     *\n     * @param callable $callback fn(string $step, string $message, ?int $durationMs): void\n     *\/\n    public function setProgressCallback(callable $callback): void\n    {\n        $this->progressCallback = $callback;\n    }\n\n    private function emit(string $step, string $message, ?int $durationMs = null): void\n    {\n        if ($this->progressCallback !== null) {\n            ($this->progressCallback)($step, $message, $durationMs);\n        }\n    }\n\n    private function startStep(): void\n    {\n        $this->stepStart = microtime(true);\n    }\n\n    private function endStep(string $step, string $message): void\n    {\n        $durationMs = (int) round((microtime(true) - $this->stepStart) * Constants::MS_PER_SECOND);\n        $this->emit($step, $message, $durationMs);\n    }\n\n    \/**\n     * Execute chat with streaming progress.\n     *\n     * @param array<string> $collections\n     *\/\n    public function execute(\n        string $sessionUuid, string $message, string $model, array $collections = ['documents'],\n        int $contextLimit = 5, int $authorProfileId = 0, int $systemPromptId = 1,\n        float $temperature = 0.7, int $maxTokens = 4096, int $structureId = 0, bool $qualityCheck = false,\n        string $requestIp = '127.0.0.1'\n    ): ChatResponse {\n        $totalStart = microtime(true);\n\n        \/\/ Log to protokoll (crash-safe, returns null on failure)\n        $protokollId = $this->protokollService->logRequest('web-chat', $message, $model, $requestIp);\n        \/\/ Step 1: Validate session\n        $this->emit('session', 'Session validieren...');\n        $this->startStep();\n        $session = $this->sessionRepo->findByUuid($sessionUuid);\n        if ($session === null) {\n            $this->emit('error', 'Session nicht gefunden');\n            if ($protokollId !== null) {\n                $this->protokollService->logFailure($protokollId, 'Session nicht gefunden');\n            }\n\n            return ChatResponse::error('Session nicht gefunden.');\n        }\n        $sessionId = $session->getId() ?? 0;\n        $this->endStep('session_done', 'Session validiert');\n        \/\/ Step 2: Validate message\n        $message = trim($message);\n        if ($message === '') {\n            $this->emit('error', 'Keine Nachricht');\n            if ($protokollId !== null) {\n                $this->protokollService->logFailure($protokollId, 'Leere Nachricht');\n            }\n\n            return ChatResponse::error('Bitte gib eine Frage ein.');\n        }\n        \/\/ Step 3: Save user message\n        $this->emit('save_user', 'User-Nachricht speichern...');\n        $this->startStep();\n        $this->messageRepo->save(sessionId: $sessionId, role: 'user', content: $message, model: $model);\n        $this->endStep('save_user_done', 'User-Nachricht gespeichert');\n        \/\/ Step 4: Auto-set title\n        $currentTitle = $session->getTitle();\n        if ($currentTitle === null || $currentTitle === 'Neuer Chat') {\n            $this->sessionRepo->updateTitle($sessionId, mb_substr($message, 0, 50) . (mb_strlen($message) > 50 ? '...' : ''));\n        }\n        \/\/ Step 5: Get prompts\n        $this->emit('prompts', 'Prompts laden...');\n        $this->startStep();\n        $stylePrompt = $this->promptLoader->getStylePrompt($authorProfileId);\n        $systemPrompt = $this->promptLoader->getSystemPrompt($systemPromptId);\n        $structurePrompt = $this->promptLoader->getStructurePrompt($structureId);\n        if ($structurePrompt !== null) {\n            $systemPrompt = ($systemPrompt ?? '') . \"\\n\\n\" . $structurePrompt;\n        }\n        $this->endStep('prompts_done', 'Prompts geladen');\n\n        \/\/ RAG Pipeline\n        $searchResults = [];\n        $context = '';\n        if ($collections !== []) {\n            \/\/ Step 6+7: Semantic search\n            $this->emit('search', 'Semantische Suche in ' . count($collections) . ' Collection(s)...');\n            $this->startStep();\n            $searchResults = $this->ragBuilder->search($message, $collections, $contextLimit);\n            \/\/ Add Qdrant fallback for non-documents collections\n            $searchResults = $this->addQdrantFallback($searchResults, $message, $collections, $contextLimit);\n            $semanticCount = count(array_filter($searchResults, static fn ($r) => isset($r['intent'])));\n            $this->endStep('search_done', count($searchResults) . ' Chunks gefunden (' . $semanticCount . ' mit Semantik)');\n\n            \/\/ Step 8: Build context\n            if ($searchResults !== []) {\n                $this->emit('context', 'Kontext aufbauen...');\n                $this->startStep();\n                $context = $this->ragBuilder->buildContext($searchResults);\n                $this->endStep('context_done', 'Kontext erstellt (' . strlen($context) . ' Zeichen)');\n            }\n        }\n\n        \/\/ Step 9: LLM Request\n        $isOllama = str_starts_with($model, 'ollama:');\n        $isClaude = str_starts_with($model, 'claude-');\n        $hasContext = $context !== '';\n        $this->emit('llm', 'Anfrage an ' . ($isOllama ? substr($model, 7) : $model) . '...');\n        $this->startStep();\n        $llmStart = microtime(true);\n        try {\n            if ($isClaude) {\n                $userPrompt = $hasContext ? $this->claude->buildRagPrompt($message, $context) : $message;\n                $effectiveSystemPrompt = $systemPrompt ?? ($hasContext ? $this->claude->getDefaultSystemPrompt() : 'Du bist ein hilfreicher Assistent. Antworte auf Deutsch, präzise und hilfreich.');\n                if ($stylePrompt !== null && $stylePrompt !== '') { $effectiveSystemPrompt .= \"\\n\\n\" . $stylePrompt; }\n                $llmResponse = $this->claude->ask($userPrompt, $effectiveSystemPrompt, $model, $maxTokens, $temperature);\n                $answer = $llmResponse['text'];\n                $usage = $llmResponse['usage'];\n            } elseif ($isOllama) {\n                $ollamaModel = substr($model, 7);\n                $instructions = array_filter([$systemPrompt, $stylePrompt]);\n                $instructionBlock = $instructions !== [] ? implode(\"\\n\\n\", $instructions) . \"\\n\\n\" : '';\n                $userPrompt = $hasContext ? sprintf(\"%sKontext:\\n\\n%s\\n\\n---\\n\\nFrage: %s\", $instructionBlock, $context, $message) : $instructionBlock . $message;\n                $answer = $this->ollama->generate($userPrompt, $ollamaModel, $temperature);\n                $usage = null;\n            } else {\n                $this->emit('error', \"Unbekanntes Modell: {$model}\");\n                if ($protokollId !== null) {\n                    $this->protokollService->logFailure($protokollId, \"Unbekanntes Modell: {$model}\");\n                }\n\n                return ChatResponse::error(\"Unknown model \\\"{$model}\\\".\");\n            }\n        } catch (\\RuntimeException $e) {\n            $this->emit('error', 'LLM-Fehler: ' . $e->getMessage());\n            if ($protokollId !== null) {\n                $this->protokollService->logFailure($protokollId, 'LLM-Fehler: ' . $e->getMessage());\n            }\n\n            return ChatResponse::error('LLM request failed: ' . $e->getMessage());\n        }\n        $llmDuration = (int) round((microtime(true) - $llmStart) * Constants::MS_PER_SECOND);\n        $tokenInfo = $usage !== null ? \" ({$usage['input_tokens']} in \/ {$usage['output_tokens']} out)\" : '';\n        $this->emit('llm_done', \"Antwort erhalten{$tokenInfo}\", $llmDuration);\n        \/\/ Step 10: Extract sources\n        $this->emit('sources', 'Quellen extrahieren...');\n        $this->startStep();\n        $sources = $this->ragBuilder->extractSources($searchResults);\n        $this->endStep('sources_done', count($sources) . ' Quellen extrahiert');\n        \/\/ Step 11: Save assistant message\n        $this->emit('save_assistant', 'Antwort speichern...');\n        $this->startStep();\n        $sourcesForStorage = array_map(static fn (array $s): string => json_encode($s, JSON_THROW_ON_ERROR), $sources);\n        $this->messageRepo->save(\n            sessionId: $sessionId, role: 'assistant', content: $answer, model: $model,\n            tokensInput: $usage['input_tokens'] ?? null, tokensOutput: $usage['output_tokens'] ?? null,\n            sources: $sourcesForStorage, startMicrotime: $llmStart, endMicrotime: microtime(true),\n            authorProfileId: $authorProfileId > 0 ? $authorProfileId : null,\n            systemPromptId: $systemPromptId > 0 ? $systemPromptId : null,\n            collectionsJson: json_encode($collections), contextLimit: $contextLimit,\n            llmRequestId: $protokollId\n        );\n        $this->endStep('save_assistant_done', 'Antwort gespeichert');\n\n        \/\/ Log success to protokoll\n        if ($protokollId !== null) {\n            $this->protokollService->lo... [TRUNCATED-dfa2eed913e0ece6]",
            "numLines": 301,
            "startLine": 1,
            "totalLines": 301
        }
    }
}
← Vorheriger Zur Liste Nächster →