Protokoll #27160

ID27160
Zeitstempel2025-12-28 02:34:24.781003
Clientroot
IP145.224.72.140
Modellclaude-sonnet-4-20250514
Statuscompleted
Tokens7,841 (Input: 1,827, Output: 6,014)
Dauer93 ms
Request-Zeit2025-12-28 02:34:24.781003
Response-Zeit2025-12-28 02:34:24.873516

Request

{
    "event": "PreToolUse",
    "tool_name": "Edit",
    "tool_input": {
        "file_path": "\/var\/www\/dev.campus.systemische-tools.de\/src\/UseCases\/Chat\/StreamingChatMessageUseCase.php",
        "old_string": "    private function getStylePromptFromProfile(int $profileId): ?string\n    {\n        if ($profileId === 0) {\n            return null;\n        }\n\n        $profile = $this->configRepo->findByIdAndType($profileId, 'author_profile');\n        if ($profile === null) {\n            return null;\n        }\n\n        $config = json_decode($profile['content'] ?? '{}', true);\n        if ($config === null) {\n            return null;\n        }\n\n        $parts = [];\n        if (isset($config['stimme']['ton'])) {\n            $parts[] = 'Ton: ' . $config['stimme']['ton'];\n        }\n        if (isset($config['stimme']['perspektive'])) {\n            $parts[] = 'Perspektive: ' . $config['stimme']['perspektive'];\n        }\n        if (isset($config['stil']['fachsprache']) && $config['stil']['fachsprache']) {\n            $parts[] = 'Verwende Fachsprache';\n        }\n        if (isset($config['stil']['beispiele']) && $config['stil']['beispiele'] === 'häufig') {\n            $parts[] = 'Nutze häufig Beispiele';\n        }\n        if (isset($config['stil']['listen']) && $config['stil']['listen'] === 'bevorzugt') {\n            $parts[] = 'Bevorzuge Listen und Bullet-Points';\n        }\n        if (isset($config['tabus']) && is_array($config['tabus'])) {\n            $parts[] = 'Vermeide: ' . implode(', ', $config['tabus']);\n        }\n\n        if ($parts === []) {\n            return null;\n        }\n\n        return 'Schreibstil (' . ($profile['name'] ?? 'Profil') . '): ' . implode('. ', $parts) . '.';\n    }\n\n    private function getSystemPromptById(int $promptId): ?string\n    {\n        if ($promptId === 0) {\n            return null;\n        }\n\n        $prompt = $this->configRepo->findByIdAndType($promptId, 'system_prompt');\n        if ($prompt === null) {\n            return null;\n        }\n\n        $content = json_decode($prompt['content'] ?? '{}', true);\n\n        return $content['prompt'] ?? null;\n    }\n\n    private function getStructurePrompt(int $structureId): ?string\n    {\n        if ($structureId === 0) {\n            return null;\n        }\n\n        $structure = $this->configRepo->findByIdAndType($structureId, 'structure');\n        if ($structure === null) {\n            return null;\n        }\n\n        $name = $structure['name'] ?? 'Struktur';\n        $config = json_decode($structure['content'] ?? '{}', true);\n\n        if ($config === null) {\n            return null;\n        }\n\n        $parts = [\"Formatiere deine Antwort als: {$name}\"];\n\n        if (isset($config['sections']) && is_array($config['sections'])) {\n            $parts[] = 'Struktur: ' . implode(' → ', $config['sections']);\n        }\n        if (isset($config['max_chars'])) {\n            $parts[] = 'Maximale Länge: ' . $config['max_chars'] . ' Zeichen';\n        }\n        if (isset($config['min_words'])) {\n            $parts[] = 'Mindestens ' . $config['min_words'] . ' Wörter';\n        }\n        if (isset($config['max_words'])) {\n            $parts[] = 'Maximal ' . $config['max_words'] . ' Wörter';\n        }\n        if (isset($config['format']) && $config['format'] === 'qa') {\n            $min = $config['min_questions'] ?? 3;\n            $parts[] = \"Formatiere als FAQ mit mindestens {$min} Frage-Antwort-Paaren\";\n        }\n        if (isset($config['hashtags']) && $config['hashtags']) {\n            $parts[] = 'Füge passende Hashtags hinzu';\n        }\n        if (isset($config['cta']) && $config['cta']) {\n            $parts[] = 'Schließe mit einem Call-to-Action ab';\n        }\n\n        return implode('. ', $parts) . '.';\n    }\n\n    private function getStructureName(int $structureId): ?string\n    {\n        $structure = $this->configRepo->findByIdAndType($structureId, 'structure');\n\n        return $structure['name'] ?? null;\n    }\n}",
        "new_string": "    private function getStylePromptFromProfile(int $profileId): ?string\n    {\n        if ($profileId === 0) { return null; }\n        $profile = $this->configRepo->findByIdAndType($profileId, 'author_profile');\n        if ($profile === null) { return null; }\n        $config = json_decode($profile['content'] ?? '{}', true);\n        if ($config === null) { return null; }\n        $parts = [];\n        if (isset($config['stimme']['ton'])) { $parts[] = 'Ton: ' . $config['stimme']['ton']; }\n        if (isset($config['stimme']['perspektive'])) { $parts[] = 'Perspektive: ' . $config['stimme']['perspektive']; }\n        if (isset($config['stil']['fachsprache']) && $config['stil']['fachsprache']) { $parts[] = 'Verwende Fachsprache'; }\n        if (isset($config['stil']['beispiele']) && $config['stil']['beispiele'] === 'häufig') { $parts[] = 'Nutze häufig Beispiele'; }\n        if (isset($config['stil']['listen']) && $config['stil']['listen'] === 'bevorzugt') { $parts[] = 'Bevorzuge Listen und Bullet-Points'; }\n        if (isset($config['tabus']) && is_array($config['tabus'])) { $parts[] = 'Vermeide: ' . implode(', ', $config['tabus']); }\n        if ($parts === []) { return null; }\n        return 'Schreibstil (' . ($profile['name'] ?? 'Profil') . '): ' . implode('. ', $parts) . '.';\n    }\n\n    private function getSystemPromptById(int $promptId): ?string\n    {\n        if ($promptId === 0) { return null; }\n        $prompt = $this->configRepo->findByIdAndType($promptId, 'system_prompt');\n        if ($prompt === null) { return null; }\n        $content = json_decode($prompt['content'] ?? '{}', true);\n        return $content['prompt'] ?? null;\n    }\n\n    private function getStructurePrompt(int $structureId): ?string\n    {\n        if ($structureId === 0) { return null; }\n        $structure = $this->configRepo->findByIdAndType($structureId, 'structure');\n        if ($structure === null) { return null; }\n        $name = $structure['name'] ?? 'Struktur';\n        $config = json_decode($structure['content'] ?? '{}', true);\n        if ($config === null) { return null; }\n        $parts = [\"Formatiere deine Antwort als: {$name}\"];\n        if (isset($config['sections']) && is_array($config['sections'])) { $parts[] = 'Struktur: ' . implode(' → ', $config['sections']); }\n        if (isset($config['max_chars'])) { $parts[] = 'Maximale Länge: ' . $config['max_chars'] . ' Zeichen'; }\n        if (isset($config['min_words'])) { $parts[] = 'Mindestens ' . $config['min_words'] . ' Wörter'; }\n        if (isset($config['max_words'])) { $parts[] = 'Maximal ' . $config['max_words'] . ' Wörter'; }\n        if (isset($config['format']) && $config['format'] === 'qa') { $parts[] = 'FAQ mit ' . ($config['min_questions'] ?? 3) . ' Paaren'; }\n        if (isset($config['hashtags']) && $config['hashtags']) { $parts[] = 'Füge passende Hashtags hinzu'; }\n        if (isset($config['cta']) && $config['cta']) { $parts[] = 'Schließe mit einem Call-to-Action ab'; }\n        return implode('. ', $parts) . '.';\n    }\n\n    private function getStructureName(int $structureId): ?string\n    {\n        $structure = $this->configRepo->findByIdAndType($structureId, 'structure');\n        return $structure['name'] ?? null;\n    }\n}"
    }
}

Response

{
    "tool_response": {
        "filePath": "\/var\/www\/dev.campus.systemische-tools.de\/src\/UseCases\/Chat\/StreamingChatMessageUseCase.php",
        "oldString": "    private function getStylePromptFromProfile(int $profileId): ?string\n    {\n        if ($profileId === 0) {\n            return null;\n        }\n\n        $profile = $this->configRepo->findByIdAndType($profileId, 'author_profile');\n        if ($profile === null) {\n            return null;\n        }\n\n        $config = json_decode($profile['content'] ?? '{}', true);\n        if ($config === null) {\n            return null;\n        }\n\n        $parts = [];\n        if (isset($config['stimme']['ton'])) {\n            $parts[] = 'Ton: ' . $config['stimme']['ton'];\n        }\n        if (isset($config['stimme']['perspektive'])) {\n            $parts[] = 'Perspektive: ' . $config['stimme']['perspektive'];\n        }\n        if (isset($config['stil']['fachsprache']) && $config['stil']['fachsprache']) {\n            $parts[] = 'Verwende Fachsprache';\n        }\n        if (isset($config['stil']['beispiele']) && $config['stil']['beispiele'] === 'häufig') {\n            $parts[] = 'Nutze häufig Beispiele';\n        }\n        if (isset($config['stil']['listen']) && $config['stil']['listen'] === 'bevorzugt') {\n            $parts[] = 'Bevorzuge Listen und Bullet-Points';\n        }\n        if (isset($config['tabus']) && is_array($config['tabus'])) {\n            $parts[] = 'Vermeide: ' . implode(', ', $config['tabus']);\n        }\n\n        if ($parts === []) {\n            return null;\n        }\n\n        return 'Schreibstil (' . ($profile['name'] ?? 'Profil') . '): ' . implode('. ', $parts) . '.';\n    }\n\n    private function getSystemPromptById(int $promptId): ?string\n    {\n        if ($promptId === 0) {\n            return null;\n        }\n\n        $prompt = $this->configRepo->findByIdAndType($promptId, 'system_prompt');\n        if ($prompt === null) {\n            return null;\n        }\n\n        $content = json_decode($prompt['content'] ?? '{}', true);\n\n        return $content['prompt'] ?? null;\n    }\n\n    private function getStructurePrompt(int $structureId): ?string\n    {\n        if ($structureId === 0) {\n            return null;\n        }\n\n        $structure = $this->configRepo->findByIdAndType($structureId, 'structure');\n        if ($structure === null) {\n            return null;\n        }\n\n        $name = $structure['name'] ?? 'Struktur';\n        $config = json_decode($structure['content'] ?? '{}', true);\n\n        if ($config === null) {\n            return null;\n        }\n\n        $parts = [\"Formatiere deine Antwort als: {$name}\"];\n\n        if (isset($config['sections']) && is_array($config['sections'])) {\n            $parts[] = 'Struktur: ' . implode(' → ', $config['sections']);\n        }\n        if (isset($config['max_chars'])) {\n            $parts[] = 'Maximale Länge: ' . $config['max_chars'] . ' Zeichen';\n        }\n        if (isset($config['min_words'])) {\n            $parts[] = 'Mindestens ' . $config['min_words'] . ' Wörter';\n        }\n        if (isset($config['max_words'])) {\n            $parts[] = 'Maximal ' . $config['max_words'] . ' Wörter';\n        }\n        if (isset($config['format']) && $config['format'] === 'qa') {\n            $min = $config['min_questions'] ?? 3;\n            $parts[] = \"Formatiere als FAQ mit mindestens {$min} Frage-Antwort-Paaren\";\n        }\n        if (isset($config['hashtags']) && $config['hashtags']) {\n            $parts[] = 'Füge passende Hashtags hinzu';\n        }\n        if (isset($config['cta']) && $config['cta']) {\n            $parts[] = 'Schließe mit einem Call-to-Action ab';\n        }\n\n        return implode('. ', $parts) . '.';\n    }\n\n    private function getStructureName(int $structureId): ?string\n    {\n        $structure = $this->configRepo->findByIdAndType($structureId, 'structure');\n\n        return $structure['name'] ?? null;\n    }\n}",
        "newString": "    private function getStylePromptFromProfile(int $profileId): ?string\n    {\n        if ($profileId === 0) { return null; }\n        $profile = $this->configRepo->findByIdAndType($profileId, 'author_profile');\n        if ($profile === null) { return null; }\n        $config = json_decode($profile['content'] ?? '{}', true);\n        if ($config === null) { return null; }\n        $parts = [];\n        if (isset($config['stimme']['ton'])) { $parts[] = 'Ton: ' . $config['stimme']['ton']; }\n        if (isset($config['stimme']['perspektive'])) { $parts[] = 'Perspektive: ' . $config['stimme']['perspektive']; }\n        if (isset($config['stil']['fachsprache']) && $config['stil']['fachsprache']) { $parts[] = 'Verwende Fachsprache'; }\n        if (isset($config['stil']['beispiele']) && $config['stil']['beispiele'] === 'häufig') { $parts[] = 'Nutze häufig Beispiele'; }\n        if (isset($config['stil']['listen']) && $config['stil']['listen'] === 'bevorzugt') { $parts[] = 'Bevorzuge Listen und Bullet-Points'; }\n        if (isset($config['tabus']) && is_array($config['tabus'])) { $parts[] = 'Vermeide: ' . implode(', ', $config['tabus']); }\n        if ($parts === []) { return null; }\n        return 'Schreibstil (' . ($profile['name'] ?? 'Profil') . '): ' . implode('. ', $parts) . '.';\n    }\n\n    private function getSystemPromptById(int $promptId): ?string\n    {\n        if ($promptId === 0) { return null; }\n        $prompt = $this->configRepo->findByIdAndType($promptId, 'system_prompt');\n        if ($prompt === null) { return null; }\n        $content = json_decode($prompt['content'] ?? '{}', true);\n        return $content['prompt'] ?? null;\n    }\n\n    private function getStructurePrompt(int $structureId): ?string\n    {\n        if ($structureId === 0) { return null; }\n        $structure = $this->configRepo->findByIdAndType($structureId, 'structure');\n        if ($structure === null) { return null; }\n        $name = $structure['name'] ?? 'Struktur';\n        $config = json_decode($structure['content'] ?? '{}', true);\n        if ($config === null) { return null; }\n        $parts = [\"Formatiere deine Antwort als: {$name}\"];\n        if (isset($config['sections']) && is_array($config['sections'])) { $parts[] = 'Struktur: ' . implode(' → ', $config['sections']); }\n        if (isset($config['max_chars'])) { $parts[] = 'Maximale Länge: ' . $config['max_chars'] . ' Zeichen'; }\n        if (isset($config['min_words'])) { $parts[] = 'Mindestens ' . $config['min_words'] . ' Wörter'; }\n        if (isset($config['max_words'])) { $parts[] = 'Maximal ' . $config['max_words'] . ' Wörter'; }\n        if (isset($config['format']) && $config['format'] === 'qa') { $parts[] = 'FAQ mit ' . ($config['min_questions'] ?? 3) . ' Paaren'; }\n        if (isset($config['hashtags']) && $config['hashtags']) { $parts[] = 'Füge passende Hashtags hinzu'; }\n        if (isset($config['cta']) && $config['cta']) { $parts[] = 'Schließe mit einem Call-to-Action ab'; }\n        return implode('. ', $parts) . '.';\n    }\n\n    private function getStructureName(int $structureId): ?string\n    {\n        $structure = $this->configRepo->findByIdAndType($structureId, 'structure');\n        return $structure['name'] ?? null;\n    }\n}",
        "originalFile": "<?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\\Persistence\\ContentConfigRepository;\n\nclass StreamingChatMessageUseCase\n{\n    \/** @var callable|null *\/\n    private $progressCallback;\n\n    private float $stepStart;\n\n    public function __construct(\n        private OllamaService $ollama,\n        private QdrantService $qdrant,\n        private ClaudeService $claude,\n        private ScoringService $scoring,\n        private ChatSessionRepositoryInterface $sessionRepo,\n        private ChatMessageRepositoryInterface $messageRepo,\n        private ContentConfigRepository $configRepo,\n        private ContentQualityValidator $qualityValidator\n    ) {\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,\n        string $message,\n        string $model,\n        array $collections = ['documents'],\n        int $contextLimit = 5,\n        int $authorProfileId = 0,\n        int $systemPromptId = 1,\n        float $temperature = 0.7,\n        int $maxTokens = 4096,\n        int $structureId = 0,\n        bool $qualityCheck = false\n    ): ChatResponse {\n        $totalStart = microtime(true);\n\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\n            return ChatResponse::error('Session nicht gefunden.');\n        }\n        $sessionId = $session->getId() ?? 0;\n        $this->endStep('session_done', 'Session validiert');\n\n        \/\/ Step 2: Validate message\n        $message = trim($message);\n        if ($message === '') {\n            $this->emit('error', 'Keine Nachricht');\n\n            return ChatResponse::error('Bitte gib eine Frage ein.');\n        }\n\n        \/\/ Step 3: Save user message\n        $this->emit('save_user', 'User-Nachricht speichern...');\n        $this->startStep();\n        $this->messageRepo->save(\n            sessionId: $sessionId,\n            role: 'user',\n            content: $message,\n            model: $model\n        );\n        $this->endStep('save_user_done', 'User-Nachricht gespeichert');\n\n        \/\/ Step 4: Auto-set title from first message\n        $currentTitle = $session->getTitle();\n        if ($currentTitle === null || $currentTitle === 'Neuer Chat') {\n            $title = mb_substr($message, 0, 50) . (mb_strlen($message) > 50 ? '...' : '');\n            $this->sessionRepo->updateTitle($sessionId, $title);\n        }\n\n        \/\/ Step 5: Get prompts\n        $this->emit('prompts', 'Prompts laden...');\n        $this->startStep();\n        $stylePrompt = $this->getStylePromptFromProfile($authorProfileId);\n        $systemPrompt = $this->getSystemPromptById($systemPromptId);\n        $structurePrompt = $this->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\n        if ($collections !== []) {\n            \/\/ Step 6: Generate embedding\n            $this->emit('embedding', 'Embedding generieren...');\n            $this->startStep();\n\n            try {\n                $queryEmbedding = $this->ollama->getEmbedding($message);\n            } catch (\\RuntimeException $e) {\n                $this->emit('error', 'Embedding fehlgeschlagen: ' . $e->getMessage());\n\n                return ChatResponse::error('Embedding generation failed: ' . $e->getMessage());\n            }\n            $this->endStep('embedding_done', 'Embedding generiert (' . count($queryEmbedding) . ' Dimensionen)');\n\n            \/\/ Step 7: Search collections\n            $collectionCount = count($collections);\n            $this->emit('search', \"Suche in {$collectionCount} Collection(s)...\");\n            $this->startStep();\n            $searchResults = $this->searchMultipleCollections($queryEmbedding, $collections, $contextLimit);\n            $resultCount = count($searchResults);\n            $this->endStep('search_done', \"{$resultCount} relevante Chunks gefunden\");\n\n            \/\/ Step 8: Build context\n            if ($searchResults !== []) {\n                $this->emit('context', 'Kontext aufbauen...');\n                $this->startStep();\n                $context = $this->buildContext($searchResults);\n                $contextLen = strlen($context);\n                $this->endStep('context_done', \"Kontext erstellt ({$contextLen} 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        $modelLabel = $isOllama ? substr($model, 7) : $model;\n\n        $this->emit('llm', \"Anfrage an {$modelLabel}...\");\n        $this->startStep();\n        $llmStart = microtime(true);\n\n        try {\n            if ($isClaude) {\n                $userPrompt = $hasContext\n                    ? $this->claude->buildRagPrompt($message, $context)\n                    : $message;\n\n                $effectiveSystemPrompt = $systemPrompt ?? ($hasContext\n                    ? $this->claude->getDefaultSystemPrompt()\n                    : 'Du bist ein hilfreicher Assistent. Antworte auf Deutsch, präzise und hilfreich.');\n\n                if ($stylePrompt !== null && $stylePrompt !== '') {\n                    $effectiveSystemPrompt .= \"\\n\\n\" . $stylePrompt;\n                }\n\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 = [];\n                if ($systemPrompt !== null && $systemPrompt !== '') {\n                    $instructions[] = $systemPrompt;\n                }\n                if ($stylePrompt !== null && $stylePrompt !== '') {\n                    $instructions[] = $stylePrompt;\n                }\n                $instructionBlock = $instructions !== [] ? implode(\"\\n\\n\", $instructions) . \"\\n\\n\" : '';\n\n                $userPrompt = $hasContext\n                    ? sprintf(\"%sKontext aus den Dokumenten:\\n\\n%s\\n\\n---\\n\\nFrage: %s\", $instructionBlock, $context, $message)\n                    : $instructionBlock . $message;\n\n                $answer = $this->ollama->generate($userPrompt, $ollamaModel, $temperature);\n                $usage = null;\n            } else {\n                $this->emit('error', \"Unbekanntes Modell: {$model}\");\n\n                return ChatResponse::error(\"Unknown model \\\"{$model}\\\".\");\n            }\n        } catch (\\RuntimeException $e) {\n            $this->emit('error', 'LLM-Fehler: ' . $e->getMessage());\n\n            return ChatResponse::error('LLM request failed: ' . $e->getMessage());\n        }\n\n        $llmDuration = (int) round((microtime(true) - $llmStart) * Constants::MS_PER_SECOND);\n        $tokenInfo = '';\n        if ($usage !== null) {\n            $tokenInfo = \" ({$usage['input_tokens']} in \/ {$usage['output_tokens']} out)\";\n        }\n        $this->emit('llm_done', \"Antwort erhalten{$tokenInfo}\", $llmDuration);\n\n        \/\/ Step 10: Extract sources\n        $this->emit('sources', 'Quellen extrahieren...');\n        $this->startStep();\n        $sources = $this->extractSources($searchResults);\n        $this->endStep('sources_done', count($sources) . ' Quellen extrahiert');\n\n        \/\/ Step 11: Save assistant message\n        $this->emit('save_assistant', 'Antwort speichern...');\n        $this->startStep();\n\n        \/** @var array<int, non-empty-string> $sourcesForStorage *\/\n        $sourcesForStorage = array_map(\n            static fn (array $source): string => json_encode($source, JSON_THROW_ON_ERROR),\n            $sources\n        );\n\n        $collectionsJson = json_encode($collections);\n        $this->messageRepo->save(\n            sessionId: $sessionId,\n            role: 'assistant',\n            content: $answer,\n            model: $model,\n            tokensInput: $usage['input_tokens'] ?? null,\n            tokensOutput: $usage['output_tokens'] ?? null,\n            sources: $sourcesForStorage,\n            startMicrotime: $llmStart,\n            endMicrotime: microtime(true),\n            authorProfileId: $authorProfileId > 0 ? $authorProfileId : null,\n            systemPromptId: $systemPromptId > 0 ? $systemPromptId : null,\n            collectionsJson: $collectionsJson,\n            contextLimit: $con... [TRUNCATED-728054d10ab80fa9]",
        "structuredPatch": [
            {
                "oldStart": 403,
                "oldLines": 113,
                "newStart": 403,
                "newLines": 53,
                "lines": [
                    " ",
                    "     private function getStylePromptFromProfile(int $profileId): ?string",
                    "     {",
                    "-        if ($profileId === 0) {",
                    "-            return null;",
                    "-        }",
                    "-",
                    "+        if ($profileId === 0) { return null; }",
                    "         $profile = $this->configRepo->findByIdAndType($profileId, 'author_profile');",
                    "-        if ($profile === null) {",
                    "-            return null;",
                    "-        }",
                    "-",
                    "+        if ($profile === null) { return null; }",
                    "         $config = json_decode($profile['content'] ?? '{}', true);",
                    "-        if ($config === null) {",
                    "-            return null;",
                    "-        }",
                    "-",
                    "+        if ($config === null) { return null; }",
                    "         $parts = [];",
                    "-        if (isset($config['stimme']['ton'])) {",
                    "-            $parts[] = 'Ton: ' . $config['stimme']['ton'];",
                    "-        }",
                    "-        if (isset($config['stimme']['perspektive'])) {",
                    "-            $parts[] = 'Perspektive: ' . $config['stimme']['perspektive'];",
                    "-        }",
                    "-        if (isset($config['stil']['fachsprache']) && $config['stil']['fachsprache']) {",
                    "-            $parts[] = 'Verwende Fachsprache';",
                    "-        }",
                    "-        if (isset($config['stil']['beispiele']) && $config['stil']['beispiele'] === 'häufig') {",
                    "-            $parts[] = 'Nutze häufig Beispiele';",
                    "-        }",
                    "-        if (isset($config['stil']['listen']) && $config['stil']['listen'] === 'bevorzugt') {",
                    "-            $parts[] = 'Bevorzuge Listen und Bullet-Points';",
                    "-        }",
                    "-        if (isset($config['tabus']) && is_array($config['tabus'])) {",
                    "-            $parts[] = 'Vermeide: ' . implode(', ', $config['tabus']);",
                    "-        }",
                    "-",
                    "-        if ($parts === []) {",
                    "-            return null;",
                    "-        }",
                    "-",
                    "+        if (isset($config['stimme']['ton'])) { $parts[] = 'Ton: ' . $config['stimme']['ton']; }",
                    "+        if (isset($config['stimme']['perspektive'])) { $parts[] = 'Perspektive: ' . $config['stimme']['perspektive']; }",
                    "+        if (isset($config['stil']['fachsprache']) && $config['stil']['fachsprache']) { $parts[] = 'Verwende Fachsprache'; }",
                    "+        if (isset($config['stil']['beispiele']) && $config['stil']['beispiele'] === 'häufig') { $parts[] = 'Nutze häufig Beispiele'; }",
                    "+        if (isset($config['stil']['listen']) && $config['stil']['listen'] === 'bevorzugt') { $parts[] = 'Bevorzuge Listen und Bullet-Points'; }",
                    "+        if (isset($config['tabus']) && is_array($config['tabus'])) { $parts[] = 'Vermeide: ' . implode(', ', $config['tabus']); }",
                    "+        if ($parts === []) { return null; }",
                    "         return 'Schreibstil (' . ($profile['name'] ?? 'Profil') . '): ' . implode('. ', $parts) . '.';",
                    "     }",
                    " ",
                    "     private function getSystemPromptById(int $promptId): ?string",
                    "     {",
                    "-        if ($promptId === 0) {",
                    "-            return null;",
                    "-        }",
                    "-",
                    "+        if ($promptId === 0) { return null; }",
                    "         $prompt = $this->configRepo->findByIdAndType($promptId, 'system_prompt');",
                    "-        if ($prompt === null) {",
                    "-            return null;",
                    "-        }",
                    "-",
                    "+        if ($prompt === null) { return null; }",
                    "         $content = json_decode($prompt['content'] ?? '{}', true);",
                    "-",
                    "         return $content['prompt'] ?? null;",
                    "     }",
                    " ",
                    "     private function getStructurePrompt(int $structureId): ?string",
                    "     {",
                    "-        if ($structureId === 0) {",
                    "-            return null;",
                    "-        }",
                    "-",
                    "+        if ($structureId === 0) { return null; }",
                    "         $structure = $this->configRepo->findByIdAndType($structureId, 'structure');",
                    "-        if ($structure === null) {",
                    "-            return null;",
                    "-        }",
                    "-",
                    "+        if ($structure === null) { return null; }",
                    "         $name = $structure['name'] ?? 'Struktur';",
                    "         $config = json_decode($structure['content'] ?? '{}', true);",
                    "-",
                    "-        if ($config === null) {",
                    "-            return null;",
                    "-        }",
                    "-",
                    "+        if ($config === null) { return null; }",
                    "         $parts = [\"Formatiere deine Antwort als: {$name}\"];",
                    "-",
                    "-        if (isset($config['sections']) && is_array($config['sections'])) {",
                    "-            $parts[] = 'Struktur: ' . implode(' → ', $config['sections']);",
                    "-        }",
                    "-        if (isset($config['max_chars'])) {",
                    "-            $parts[] = 'Maximale Länge: ' . $config['max_chars'] . ' Zeichen';",
                    "-        }",
                    "-        if (isset($config['min_words'])) {",
                    "-            $parts[] = 'Mindestens ' . $config['min_words'] . ' Wörter';",
                    "-        }",
                    "-        if (isset($config['max_words'])) {",
                    "-            $parts[] = 'Maximal ' . $config['max_words'] . ' Wörter';",
                    "-        }",
                    "-        if (isset($config['format']) && $config['format'] === 'qa') {",
                    "-            $min = $config['min_questions'] ?? 3;",
                    "-            $parts[] = \"Formatiere als FAQ mit mindestens {$min} Frage-Antwort-Paaren\";",
                    "-        }",
                    "-        if (isset($config['hashtags']) && $config['hashtags']) {",
                    "-            $parts[] = 'Füge passende Hashtags hinzu';",
                    "-        }",
                    "-        if (isset($config['cta']) && $config['cta']) {",
                    "-            $parts[] = 'Schließe mit einem Call-to-Action ab';",
                    "-        }",
                    "-",
                    "+        if (isset($config['sections']) && is_array($config['sections'])) { $parts[] = 'Struktur: ' . implode(' → ', $config['sections']); }",
                    "+        if (isset($config['max_chars'])) { $parts[] = 'Maximale Länge: ' . $config['max_chars'] . ' Zeichen'; }",
                    "+        if (isset($config['min_words'])) { $parts[] = 'Mindestens ' . $config['min_words'] . ' Wörter'; }",
                    "+        if (isset($config['max_words'])) { $parts[] = 'Maximal ' . $config['max_words'] . ' Wörter'; }",
                    "+        if (isset($config['format']) && $config['format'] === 'qa') { $parts[] = 'FAQ mit ' . ($config['min_questions'] ?? 3) . ' Paaren'; }",
                    "+        if (isset($config['hashtags']) && $config['hashtags']) { $parts[] = 'Füge passende Hashtags hinzu'; }",
                    "+        if (isset($config['cta']) && $config['cta']) { $parts[] = 'Schließe mit einem Call-to-Action ab'; }",
                    "         return implode('. ', $parts) . '.';",
                    "     }",
                    " ",
                    "     private function getStructureName(int $structureId): ?string",
                    "     {",
                    "         $structure = $this->configRepo->findByIdAndType($structureId, 'structure');",
                    "-",
                    "         return $structure['name'] ?? null;",
                    "     }",
                    " }"
                ]
            }
        ],
        "userModified": false,
        "replaceAll": false
    }
}
← Vorheriger Zur Liste Nächster →