$collections Qdrant collections to search * @param int $contextLimit Number of context chunks * @param int $authorProfileId Author profile ID (0 = none) * @param int $systemPromptId System prompt config ID * @param float $temperature AI temperature setting * @param int $maxTokens Max tokens for response * @param int $structureId Output structure ID (0 = frei) * @param bool $qualityCheck Run quality validation on response * @return ChatResponse */ public function execute( string $sessionUuid, string $message, string $model, array $collections = ['documents'], int $contextLimit = 5, int $authorProfileId = 0, int $systemPromptId = 1, float $temperature = 0.7, int $maxTokens = 4096, int $structureId = 0, bool $qualityCheck = false ): ChatResponse { // 1. Validate session $session = $this->sessionRepo->findByUuid($sessionUuid); if ($session === null) { return ChatResponse::error('Session nicht gefunden.'); } // 2. Validate message $message = trim($message); if ($message === '') { return ChatResponse::error('Bitte gib eine Frage ein.'); } // 3. Save user message $this->messageRepo->save( sessionId: $session['id'], role: 'user', content: $message, model: $model ); // 4. Auto-set title from first message if ($session['title'] === null) { $title = mb_substr($message, 0, 50) . (mb_strlen($message) > 50 ? '...' : ''); $this->sessionRepo->updateTitle($session['id'], $title); } // 5. Get style prompt from author profile $stylePrompt = $this->getStylePromptFromProfile($authorProfileId); // 6. Get system prompt $systemPrompt = $this->getSystemPromptById($systemPromptId); // 6b. Add structure formatting if selected $structurePrompt = $this->getStructurePrompt($structureId); if ($structurePrompt !== null) { $systemPrompt = ($systemPrompt ?? '') . "\n\n" . $structurePrompt; } // 7. Track timing and get AI response $startTime = microtime(true); try { $result = $this->chatService->chat( question: $message, model: $model, collections: $collections, limit: $contextLimit, stylePrompt: $stylePrompt, customSystemPrompt: $systemPrompt, temperature: $temperature, maxTokens: $maxTokens ); } catch (\Exception $e) { return ChatResponse::error('Chat-Service Fehler: ' . $e->getMessage()); } $endTime = microtime(true); // 8. Prepare sources as JSON strings for storage /** @var array $sourcesForStorage */ $sourcesForStorage = array_map( static fn (array $source): string => json_encode($source, JSON_THROW_ON_ERROR), $result['sources'] ); // 9. Save assistant message with tracking $collectionsJson = json_encode($collections); $this->messageRepo->save( sessionId: $session['id'], role: 'assistant', content: $result['answer'], model: $model, tokensInput: $result['usage']['input_tokens'] ?? null, tokensOutput: $result['usage']['output_tokens'] ?? null, sources: $sourcesForStorage, startMicrotime: $startTime, endMicrotime: $endTime, authorProfileId: $authorProfileId > 0 ? $authorProfileId : null, systemPromptId: $systemPromptId > 0 ? $systemPromptId : null, collectionsJson: $collectionsJson, contextLimit: $contextLimit ); // 10. Create response $response = ChatResponse::fromServiceResponse($result, $endTime - $startTime); // 11. Run quality validation if enabled if ($qualityCheck) { $structureName = $structureId > 0 ? $this->getStructureName($structureId) : null; $validation = $this->qualityValidator->validate( question: $message, answer: $result['answer'], sources: $result['sources'], structureName: $structureName ); $response = $response->withQualityValidation($validation); } // 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; } $parts = ["Formatiere deine Antwort als: {$name}"]; // Add structure-specific instructions 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) . '.'; } }