{
"tool_response": {
"filePath": "\/var\/www\/dev.campus.systemische-tools.de\/src\/UseCases\/Chat\/SendChatMessageUseCase.php",
"oldString": " \/\/ 12. Return response\n return $response;\n }\n\n \/**\n * Get structure name by ID\n *\/\n private function getStructureName(int $structureId): ?string\n {\n $structure = $this->configRepo->findByIdAndType($structureId, 'structure');\n return $structure['name'] ?? null;\n }\n\n \/**\n * Create default ChatService from credentials\n *\/\n private function createDefaultChatService(): ChatService\n {\n $config = \\Infrastructure\\AI\\AIConfig::fromCredentialsFile();\n return $config->createChatService();\n }\n\n \/**\n * Get style prompt from author profile\n *\/\n private function getStylePromptFromProfile(int $profileId): ?string\n {\n if ($profileId === 0) {\n return null;\n }\n $profile = $this->configRepo->findByIdAndType($profileId, 'author_profile');\n if ($profile === null) {\n return null;\n }\n $config = json_decode($profile['content'] ?? '{}', true);\n if ($config === null) {\n return null;\n }\n $parts = [];\n\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 \/**\n * Get system prompt by ID\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 \/**\n * Get structure formatting prompt\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 \/\/ Complex structure (Blog-Beitrag, LinkedIn-Beitrag etc.)\n if (isset($config['ausgabe']) || isset($config['aufbau']) || isset($config['gesamtaufbau'])) {\n return $this->buildComplexStructurePrompt($name, $config);\n }\n\n \/\/ Simple structure (legacy format)\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\n if (isset($config['max_chars'])) {\n $parts[] = 'Maximale Länge: ' . $config['max_chars'] . ' Zeichen';\n }\n\n if (isset($config['min_words'])) {\n $parts[] = 'Mindestens ' . $config['min_words'] . ' Wörter';\n }\n\n if (isset($config['max_words'])) {\n $parts[] = 'Maximal ' . $config['max_words'] . ' Wörter';\n }\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\n if (isset($config['hashtags']) && $config['hashtags']) {\n $parts[] = 'Füge passende Hashtags hinzu';\n }\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 \/**\n * Build prompt from complex structure config\n *\/\n private function buildComplexStructurePrompt(string $name, array $config): string\n {\n $prompt = \"## Ausgabeformat: {$name}\\n\\n\";\n $prompt .= \"Befolge diese Struktur-Vorgaben exakt:\\n\\n\";\n $prompt .= $this->configToYaml($config, 0);\n\n return $prompt;\n }\n\n \/**\n * Convert config array to readable YAML-like format\n *\/\n private function configToYaml(array $config, int $indent): string\n {\n $result = '';\n $prefix = str_repeat(' ', $indent);\n\n foreach ($config as $key => $value) {\n if (is_array($value)) {\n if ($this->isIndexedArray($value)) {\n $result .= \"{$prefix}{$key}:\\n\";\n foreach ($value as $item) {\n if (is_array($item)) {\n $result .= \"{$prefix}- \" . trim($this->configToYaml($item, 0)) . \"\\n\";\n } else {\n $result .= \"{$prefix}- {$item}\\n\";\n }\n }\n } else {\n $result .= \"{$prefix}{$key}:\\n\";\n $result .= $this->configToYaml($value, $indent + 1);\n }\n } else {\n $result .= \"{$prefix}{$key}: {$value}\\n\";\n }\n }\n\n return $result;\n }\n\n \/**\n * Check if array is indexed (list) vs associative\n *\/\n private function isIndexedArray(array $arr): bool\n {\n if ($arr === []) {\n return true;\n }\n\n return array_keys($arr) === range(0, count($arr) - 1);\n }\n}",
"newString": " \/\/ 12. Return response\n return $response;\n }\n}",
"originalFile": "<?php\n\ndeclare(strict_types=1);\n\nnamespace UseCases\\Chat;\n\n\/\/ @responsibility: Orchestriert Chat-Nachrichten mit AI-Service\n\nuse Domain\\Repository\\ChatMessageRepositoryInterface;\nuse Domain\\Repository\\ChatSessionRepositoryInterface;\nuse Infrastructure\\AI\\ChatService;\nuse Infrastructure\\AI\\ContentQualityValidator;\n\nclass SendChatMessageUseCase\n{\n public function __construct(\n private ChatService $chatService,\n private ChatSessionRepositoryInterface $sessionRepo,\n private ChatMessageRepositoryInterface $messageRepo,\n private ChatPromptLoader $promptLoader,\n private ContentQualityValidator $qualityValidator\n ) {\n }\n\n \/**\n * Execute the use case\n *\n * @param string $sessionUuid Session UUID\n * @param string $message User message text\n * @param string $model AI model identifier\n * @param array<string> $collections Qdrant collections to search\n * @param int $contextLimit Number of context chunks\n * @param int $authorProfileId Author profile ID (0 = none)\n * @param int $systemPromptId System prompt config ID\n * @param float $temperature AI temperature setting\n * @param int $maxTokens Max tokens for response\n * @param int $structureId Output structure ID (0 = frei)\n * @param bool $qualityCheck Run quality validation on response\n * @return ChatResponse\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 \/\/ 1. Validate session\n $session = $this->sessionRepo->findByUuid($sessionUuid);\n if ($session === null) {\n return ChatResponse::error('Session nicht gefunden.');\n }\n $sessionId = $session->getId() ?? 0;\n\n \/\/ 2. Validate message\n $message = trim($message);\n if ($message === '') {\n return ChatResponse::error('Bitte gib eine Frage ein.');\n }\n\n \/\/ 3. Save user message\n $this->messageRepo->save(\n sessionId: $sessionId,\n role: 'user',\n content: $message,\n model: $model\n );\n\n \/\/ 4. Auto-set title from first message\n if ($session->getTitle() === null) {\n $title = mb_substr($message, 0, 50) . (mb_strlen($message) > 50 ? '...' : '');\n $this->sessionRepo->updateTitle($sessionId, $title);\n }\n\n \/\/ 5. Get prompts via ChatPromptLoader\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\n \/\/ 7. Track timing and get AI response\n $startTime = microtime(true);\n\n try {\n $result = $this->chatService->chat(\n question: $message,\n model: $model,\n collections: $collections,\n limit: $contextLimit,\n stylePrompt: $stylePrompt,\n customSystemPrompt: $systemPrompt,\n temperature: $temperature,\n maxTokens: $maxTokens\n );\n } catch (\\Exception $e) {\n return ChatResponse::error('Chat-Service Fehler: ' . $e->getMessage());\n }\n\n $endTime = microtime(true);\n\n \/\/ 8. Prepare sources as JSON strings for storage\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 $result['sources']\n );\n\n \/\/ 9. Save assistant message with tracking\n $collectionsJson = json_encode($collections);\n $this->messageRepo->save(\n sessionId: $sessionId,\n role: 'assistant',\n content: $result['answer'],\n model: $model,\n tokensInput: $result['usage']['input_tokens'] ?? null,\n tokensOutput: $result['usage']['output_tokens'] ?? null,\n sources: $sourcesForStorage,\n startMicrotime: $startTime,\n endMicrotime: $endTime,\n authorProfileId: $authorProfileId > 0 ? $authorProfileId : null,\n systemPromptId: $systemPromptId > 0 ? $systemPromptId : null,\n collectionsJson: $collectionsJson,\n contextLimit: $contextLimit\n );\n\n \/\/ 10. Create response\n $response = ChatResponse::fromServiceResponse($result, $endTime - $startTime);\n \/\/ 11. Run quality validation if enabled\n if ($qualityCheck) {\n $structureName = $this->promptLoader->getStructureName($structureId);\n $validation = $this->qualityValidator->validate(\n question: $message,\n answer: $result['answer'],\n sources: $result['sources'],\n structureName: $structureName\n );\n $response = $response->withQualityValidation($validation);\n }\n \/\/ 12. Return response\n return $response;\n }\n\n \/**\n * Get structure name by ID\n *\/\n private function getStructureName(int $structureId): ?string\n {\n $structure = $this->configRepo->findByIdAndType($structureId, 'structure');\n return $structure['name'] ?? null;\n }\n\n \/**\n * Create default ChatService from credentials\n *\/\n private function createDefaultChatService(): ChatService\n {\n $config = \\Infrastructure\\AI\\AIConfig::fromCredentialsFile();\n return $config->createChatService();\n }\n\n \/**\n * Get style prompt from author profile\n *\/\n private function getStylePromptFromProfile(int $profileId): ?string\n {\n if ($profileId === 0) {\n return null;\n }\n $profile = $this->configRepo->findByIdAndType($profileId, 'author_profile');\n if ($profile === null) {\n return null;\n }\n $config = json_decode($profile['content'] ?? '{}', true);\n if ($config === null) {\n return null;\n }\n $parts = [];\n\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 \/**\n * Get system prompt by ID\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 \/**\n * Get structure formatting prompt\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 \/\/ Complex structure (Blog-Beitrag, LinkedIn-Beitrag etc.)\n if (isset($config['ausgabe']) || isset($config['aufbau']) || isset($config['gesamtaufbau'])) {\n return $this->buildComplexStructurePrompt($name, $config);\n }\n\n \/\/ Simple structure (legacy format)\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\n if (isset($config['max_chars'])) {\n $parts[] = 'Maximale Länge: ' . $config['max_chars'] . ' Zeichen';\n }\n\n if (isset($config['min_words'])) {\n $parts[] = 'Mindestens ' . $config['min_words'] . ' Wörter';\n }\n\n if (isset($config['max_words'])) {\n $parts[] = 'Maximal ' . $config['max_words'] . ' Wörter';\n }\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\n if (isset($config['hashtags']) && $config['hashtags']) {\n $parts[] = 'Füge passende Hashtags hinzu';\n }\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 \/**\n * Build prompt from co... [TRUNCATED-5bec052a1e1c4075]",
"structuredPatch": [
{
"oldStart": 147,
"oldLines": 201,
"newStart": 147,
"newLines": 4,
"lines": [
" \/\/ 12. Return response",
" return $response;",
" }",
"-",
"- \/**",
"- * Get structure name by ID",
"- *\/",
"- private function getStructureName(int $structureId): ?string",
"- {",
"- $structure = $this->configRepo->findByIdAndType($structureId, 'structure');",
"- return $structure['name'] ?? null;",
"- }",
"-",
"- \/**",
"- * Create default ChatService from credentials",
"- *\/",
"- private function createDefaultChatService(): ChatService",
"- {",
"- $config = \\Infrastructure\\AI\\AIConfig::fromCredentialsFile();",
"- return $config->createChatService();",
"- }",
"-",
"- \/**",
"- * Get style prompt from author profile",
"- *\/",
"- private function getStylePromptFromProfile(int $profileId): ?string",
"- {",
"- if ($profileId === 0) {",
"- return null;",
"- }",
"- $profile = $this->configRepo->findByIdAndType($profileId, 'author_profile');",
"- if ($profile === null) {",
"- return null;",
"- }",
"- $config = json_decode($profile['content'] ?? '{}', true);",
"- 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;",
"- }",
"-",
"- return 'Schreibstil (' . ($profile['name'] ?? 'Profil') . '): ' . implode('. ', $parts) . '.';",
"- }",
"-",
"- \/**",
"- * Get system prompt by ID",
"- *\/",
"- private function getSystemPromptById(int $promptId): ?string",
"- {",
"- if ($promptId === 0) {",
"- return null;",
"- }",
"-",
"- $prompt = $this->configRepo->findByIdAndType($promptId, 'system_prompt');",
"- if ($prompt === null) {",
"- return null;",
"- }",
"-",
"- $content = json_decode($prompt['content'] ?? '{}', true);",
"-",
"- return $content['prompt'] ?? null;",
"- }",
"-",
"- \/**",
"- * Get structure formatting prompt",
"- *\/",
"- private function getStructurePrompt(int $structureId): ?string",
"- {",
"- if ($structureId === 0) {",
"- return null;",
"- }",
"-",
"- $structure = $this->configRepo->findByIdAndType($structureId, 'structure');",
"- if ($structure === null) {",
"- return null;",
"- }",
"-",
"- $name = $structure['name'] ?? 'Struktur';",
"- $config = json_decode($structure['content'] ?? '{}', true);",
"-",
"- if ($config === null) {",
"- return null;",
"- }",
"-",
"- \/\/ Complex structure (Blog-Beitrag, LinkedIn-Beitrag etc.)",
"- if (isset($config['ausgabe']) || isset($config['aufbau']) || isset($config['gesamtaufbau'])) {",
"- return $this->buildComplexStructurePrompt($name, $config);",
"- }",
"-",
"- \/\/ Simple structure (legacy format)",
"- $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';",
"- }",
"-",
"- return implode('. ', $parts) . '.';",
"- }",
"-",
"- \/**",
"- * Build prompt from complex structure config",
"- *\/",
"- private function buildComplexStructurePrompt(string $name, array $config): string",
"- {",
"- $prompt = \"## Ausgabeformat: {$name}\\n\\n\";",
"- $prompt .= \"Befolge diese Struktur-Vorgaben exakt:\\n\\n\";",
"- $prompt .= $this->configToYaml($config, 0);",
"-",
"- return $prompt;",
"- }",
"-",
"- \/**",
"- * Convert config array to readable YAML-like format",
"- *\/",
"- private function configToYaml(array $config, int $indent): string",
"- {",
"- $result = '';",
"- $prefix = str_repeat(' ', $indent);",
"-",
"- foreach ($config as $key => $value) {",
"- if (is_array($value)) {",
"- if ($this->isIndexedArray($value)) {",
"- $result .= \"{$prefix}{$key}:\\n\";",
"- foreach ($value as $item) {",
"- if (is_array($item)) {",
"- $result .= \"{$prefix}- \" . trim($this->configToYaml($item, 0)) . \"\\n\";",
"- } else {",
"- $result .= \"{$prefix}- {$item}\\n\";",
"- }",
"- }",
"- } else {",
"- $result .= \"{$prefix}{$key}:\\n\";",
"- $result .= $this->configToYaml($value, $indent + 1);",
"- }",
"- } else {",
"- $result .= \"{$prefix}{$key}: {$value}\\n\";",
"- }",
"- }",
"-",
"- return $result;",
"- }",
"-",
"- \/**",
"- * Check if array is indexed (list) vs associative",
"- *\/",
"- private function isIndexedArray(array $arr): bool",
"- {",
"- if ($arr === []) {",
"- return true;",
"- }",
"-",
"- return array_keys($arr) === range(0, count($arr) - 1);",
"- }",
" }"
]
}
],
"userModified": false,
"replaceAll": false
}
}