{
"tool_response": {
"type": "update",
"filePath": "\/var\/www\/dev.campus.systemische-tools.de\/src\/Controller\/ChatController.php",
"content": "<?php\n\nnamespace Controller;\n\nuse Framework\\Controller;\nuse Infrastructure\\AI\\ModelConfig;\nuse Infrastructure\\Formatting\\ChatMessageFormatter;\nuse UseCases\\Chat\\ManageChatSessionsUseCase;\nuse UseCases\\Chat\\SendChatMessageUseCase;\n\nclass ChatController extends Controller\n{\n private ManageChatSessionsUseCase $sessionsUseCase;\n private SendChatMessageUseCase $messageUseCase;\n private ChatMessageFormatter $formatter;\n\n public function __construct(\n ?ManageChatSessionsUseCase $sessionsUseCase = null,\n ?SendChatMessageUseCase $messageUseCase = null,\n ?ChatMessageFormatter $formatter = null\n ) {\n $this->sessionsUseCase = $sessionsUseCase ?? new ManageChatSessionsUseCase();\n $this->messageUseCase = $messageUseCase ?? new SendChatMessageUseCase();\n $this->formatter = $formatter ?? new ChatMessageFormatter();\n }\n\n public function index(): void\n {\n $uuid = $this->sessionsUseCase->createSession();\n header('Location: \/chat\/' . $uuid);\n exit;\n }\n\n public function show(string $uuid): void\n {\n $session = $this->sessionsUseCase->getSession($uuid);\n\n if ($session === null) {\n header('Location: \/chat');\n exit;\n }\n\n $this->view('chat.index', [\n 'title' => $session['title'] ?? 'KI-Chat',\n 'session' => $session,\n 'messages' => $this->sessionsUseCase->getMessages($session['id']),\n 'sessions' => $this->sessionsUseCase->getAllSessions(),\n 'authorProfiles' => $this->sessionsUseCase->getAuthorProfiles(),\n 'systemPrompts' => $this->sessionsUseCase->getSystemPrompts(),\n 'collections' => $this->sessionsUseCase->getAvailableCollections(),\n 'models' => ModelConfig::getAll(),\n 'defaultModel' => ModelConfig::DEFAULT_MODEL,\n ]);\n }\n\n public function sessionList(): void\n {\n $this->view('chat.partials.session-list', [\n 'sessions' => $this->sessionsUseCase->getAllSessions(),\n 'currentUuid' => $this->getString('current') ?: null,\n ]);\n }\n\n public function message(string $uuid): void\n {\n $session = $this->sessionsUseCase->getSession($uuid);\n\n if ($session === null) {\n $this->view('chat.partials.error', ['error' => 'Session nicht gefunden.']);\n\n return;\n }\n\n $question = trim($_POST['message'] ?? '');\n $model = ModelConfig::validate($_POST['model'] ?? $session['model']);\n $sessionCollections = $this->decodeJson($session['collections'] ?? null) ?: ['documents'];\n $collections = $_POST['collections'] ?? $sessionCollections;\n $contextLimit = (int) ($_POST['context_limit'] ?? $session['context_limit'] ?? 5);\n $authorProfileId = (int) ($_POST['author_profile_id'] ?? $session['author_profile_id'] ?? 0);\n $systemPromptId = (int) ($_POST['system_prompt_id'] ?? $session['system_prompt_id'] ?? 1);\n $temperature = (float) ($_POST['temperature'] ?? $session['temperature'] ?? 0.7);\n $maxTokens = (int) ($_POST['max_tokens'] ?? $session['max_tokens'] ?? 4096);\n\n if ($this->sessionsUseCase->settingsHaveChanged($session, $model, $collections, $contextLimit, $authorProfileId, $temperature, $maxTokens)) {\n $this->sessionsUseCase->updateSettings($session['id'], $model, $collections, $contextLimit, $authorProfileId, $temperature, $maxTokens);\n }\n\n if ($question === '') {\n $this->view('chat.partials.error', ['error' => 'Bitte gib eine Frage ein.']);\n\n return;\n }\n\n if (!empty($collections)) {\n $compatibility = $this->sessionsUseCase->validateCollectionCompatibility($collections);\n if (!$compatibility['valid']) {\n $this->view('chat.partials.error', [\n 'error' => 'Collection-Fehler: ' . $compatibility['error'],\n 'details' => 'Bitte wähle nur Collections mit gleichem Embedding-Modell.',\n ]);\n\n return;\n }\n }\n\n $response = $this->messageUseCase->execute(\n sessionUuid: $uuid,\n message: $question,\n model: $model,\n collections: $collections,\n contextLimit: $contextLimit,\n authorProfileId: $authorProfileId,\n systemPromptId: $systemPromptId,\n temperature: $temperature,\n maxTokens: $maxTokens\n );\n\n if ($response->hasError()) {\n $this->view('chat.partials.error', ['error' => $response->getError()]);\n\n return;\n }\n\n $result = $response->toArray();\n $this->view('chat.partials.response', [\n 'question' => $question,\n 'result' => $result,\n 'model' => $model,\n 'formattedAnswer' => $this->formatter->formatAnswer($result['answer'] ?? ''),\n ]);\n }\n\n public function updateTitle(string $uuid): void\n {\n $session = $this->sessionsUseCase->getSession($uuid);\n\n if ($session === null) {\n $this->notFound('Session nicht gefunden');\n }\n\n $title = $this->sessionsUseCase->updateTitle($session['id'], $_POST['title'] ?? '');\n echo htmlspecialchars($title);\n }\n\n public function systemPrompt(string $uuid): void\n {\n $session = $this->sessionsUseCase->getSession($uuid);\n\n if ($session === null) {\n $this->notFound('Session nicht gefunden');\n }\n\n $result = $this->sessionsUseCase->updateSystemPrompt($session['id'], $_POST['system_prompt'] ?? '');\n $this->view('chat.partials.success', ['message' => $result->message]);\n }\n\n public function getSystemPrompt(string $uuid): void\n {\n $session = $this->sessionsUseCase->getSession($uuid);\n\n if ($session === null) {\n $this->notFound('Session nicht gefunden');\n }\n\n $this->view('chat.partials.system-prompt-modal', [\n 'session' => $session,\n 'currentPrompt' => $session['system_prompt'] ?? '',\n 'defaultPrompt' => $this->sessionsUseCase->getDefaultSystemPrompt(),\n ]);\n }\n\n public function delete(string $uuid): void\n {\n $session = $this->sessionsUseCase->getSession($uuid);\n\n if ($session !== null) {\n $this->sessionsUseCase->deleteSession($session['id']);\n }\n\n header('HX-Redirect: \/chat');\n echo 'OK';\n }\n}\n",
"structuredPatch": [
{
"oldStart": 3,
"oldLines": 100,
"newStart": 3,
"newLines": 67,
"lines": [
" namespace Controller;",
" ",
" use Framework\\Controller;",
"-use Infrastructure\\AI\\AIConfig;",
"-use Infrastructure\\AI\\ChatService;",
" use Infrastructure\\AI\\ModelConfig;",
"-use Infrastructure\\Persistence\\CollectionRepository;",
"-use Infrastructure\\Validation\\CollectionValidator;",
"+use Infrastructure\\Formatting\\ChatMessageFormatter;",
"+use UseCases\\Chat\\ManageChatSessionsUseCase;",
" use UseCases\\Chat\\SendChatMessageUseCase;",
" ",
" class ChatController extends Controller",
" {",
"- private ChatService $chatService;",
"- private CollectionRepository $collectionRepository;",
"- private CollectionValidator $collectionValidator;",
"- private SendChatMessageUseCase $sendMessageUseCase;",
"- private \\PDO $db;",
"+ private ManageChatSessionsUseCase $sessionsUseCase;",
"+ private SendChatMessageUseCase $messageUseCase;",
"+ private ChatMessageFormatter $formatter;",
" ",
"- \/** @var array<array<string,mixed>>|null Cached collections list *\/",
"- private ?array $collectionsCache = null;",
"-",
"- public function __construct()",
"- {",
"- $config = AIConfig::fromCredentialsFile();",
"- $this->chatService = $config->createChatService();",
"- $this->collectionRepository = new CollectionRepository();",
"- $this->collectionValidator = new CollectionValidator($this->collectionRepository);",
"- $this->sendMessageUseCase = new SendChatMessageUseCase($this->chatService);",
"- $this->db = $this->initializeDatabase();",
"+ public function __construct(",
"+ ?ManageChatSessionsUseCase $sessionsUseCase = null,",
"+ ?SendChatMessageUseCase $messageUseCase = null,",
"+ ?ChatMessageFormatter $formatter = null",
"+ ) {",
"+ $this->sessionsUseCase = $sessionsUseCase ?? new ManageChatSessionsUseCase();",
"+ $this->messageUseCase = $messageUseCase ?? new SendChatMessageUseCase();",
"+ $this->formatter = $formatter ?? new ChatMessageFormatter();",
" }",
" ",
"- \/**",
"- * GET \/chat",
"- * Show chat interface with session list, create new session if none",
"- *\/",
" public function index(): void",
" {",
"- $sessions = $this->getSessions();",
"-",
"- \/\/ Create new session and redirect",
"- $uuid = $this->createSession();",
"+ $uuid = $this->sessionsUseCase->createSession();",
" header('Location: \/chat\/' . $uuid);",
" exit;",
" }",
" ",
"- \/**",
"- * GET \/chat\/{uuid}",
"- * Show specific chat session",
"- *\/",
" public function show(string $uuid): void",
" {",
"- $session = $this->getSession($uuid);",
"+ $session = $this->sessionsUseCase->getSession($uuid);",
" ",
" if ($session === null) {",
" header('Location: \/chat');",
" exit;",
" }",
" ",
"- $messages = $this->getMessages($session['id']);",
"- $sessions = $this->getSessions();",
"- $authorProfiles = $this->getAuthorProfiles();",
"- $systemPrompts = $this->getSystemPrompts();",
"- $collections = $this->getAvailableCollections();",
"-",
" $this->view('chat.index', [",
" 'title' => $session['title'] ?? 'KI-Chat',",
" 'session' => $session,",
"- 'messages' => $messages,",
"- 'sessions' => $sessions,",
"- 'authorProfiles' => $authorProfiles,",
"- 'systemPrompts' => $systemPrompts,",
"- 'collections' => $collections,",
"+ 'messages' => $this->sessionsUseCase->getMessages($session['id']),",
"+ 'sessions' => $this->sessionsUseCase->getAllSessions(),",
"+ 'authorProfiles' => $this->sessionsUseCase->getAuthorProfiles(),",
"+ 'systemPrompts' => $this->sessionsUseCase->getSystemPrompts(),",
"+ 'collections' => $this->sessionsUseCase->getAvailableCollections(),",
" 'models' => ModelConfig::getAll(),",
" 'defaultModel' => ModelConfig::DEFAULT_MODEL,",
" ]);",
" }",
" ",
"- \/**",
"- * GET \/chat\/sessions (HTMX partial)",
"- * Return session list HTML",
"- *\/",
" public function sessionList(): void",
" {",
"- $sessions = $this->getSessions();",
" $this->view('chat.partials.session-list', [",
"- 'sessions' => $sessions,",
"+ 'sessions' => $this->sessionsUseCase->getAllSessions(),",
" 'currentUuid' => $this->getString('current') ?: null,",
" ]);",
" }",
" ",
"- \/**",
"- * POST \/chat\/{uuid}\/message (HTMX)",
"- * Process message and return HTML response",
"- *\/",
" public function message(string $uuid): void",
" {",
"- $session = $this->getSession($uuid);",
"+ $session = $this->sessionsUseCase->getSession($uuid);",
" ",
" if ($session === null) {",
" $this->view('chat.partials.error', ['error' => 'Session nicht gefunden.']);"
]
},
{
"oldStart": 105,
"oldLines": 23,
"newStart": 72,
"newLines": 17,
"lines": [
" }",
" ",
" $question = trim($_POST['message'] ?? '');",
"- $model = $this->validateModel($_POST['model'] ?? $session['model']);",
"+ $model = ModelConfig::validate($_POST['model'] ?? $session['model']);",
" $sessionCollections = $this->decodeJson($session['collections'] ?? null) ?: ['documents'];",
"- $collections = $this->validateCollections($_POST['collections'] ?? $sessionCollections);",
"- $contextLimit = $this->validateContextLimit((int) ($_POST['context_limit'] ?? $session['context_limit'] ?? 5));",
"- $authorProfileId = $this->validateAuthorProfileId((int) ($_POST['author_profile_id'] ?? $session['author_profile_id'] ?? 0));",
"+ $collections = $_POST['collections'] ?? $sessionCollections;",
"+ $contextLimit = (int) ($_POST['context_limit'] ?? $session['context_limit'] ?? 5);",
"+ $authorProfileId = (int) ($_POST['author_profile_id'] ?? $session['author_profile_id'] ?? 0);",
" $systemPromptId = (int) ($_POST['system_prompt_id'] ?? $session['system_prompt_id'] ?? 1);",
"- $temperature = $this->validateTemperature((float) ($_POST['temperature'] ?? $session['temperature'] ?? 0.7));",
"- $maxTokens = $this->validateMaxTokens((int) ($_POST['max_tokens'] ?? $session['max_tokens'] ?? 4096));",
"+ $temperature = (float) ($_POST['temperature'] ?? $session['temperature'] ?? 0.7);",
"+ $maxTokens = (int) ($_POST['max_tokens'] ?? $session['max_tokens'] ?? 4096);",
" ",
"- \/\/ Update session if settings changed",
"- $currentLimit = (int) ($session['context_limit'] ?? 5);",
"- $currentProfileId = (int) ($session['author_profile_id'] ?? 0);",
"- $currentTemperature = (float) ($session['temperature'] ?? 0.7);",
"- $currentMaxTokens = (int) ($session['max_tokens'] ?? 4096);",
"- $collectionsJson = json_encode($collections);",
"- if ($model !== $session['model'] || $collectionsJson !== ($session['collections'] ?? '[\"documents\"]') || $contextLimit !== $currentLimit || $authorProfileId !== $currentProfileId || $temperature !== $currentTemperature || $maxTokens !== $currentMaxTokens) {",
"- $this->updateSessionSettings($session['id'], $model, $collections, $contextLimit, $authorProfileId, $temperature, $maxTokens);",
"+ if ($this->sessionsUseCase->settingsHaveChanged($session, $model, $collections, $contextLimit, $authorProfileId, $temperature, $maxTokens)) {",
"+ $this->sessionsUseCase->updateSettings($session['id'], $model, $collections, $contextLimit, $authorProfileId, $temperature, $maxTokens);",
" }",
" ",
" if ($question === '') {"
]
},
{
"oldStart": 130,
"oldLines": 9,
"newStart": 91,
"newLines": 8,
"lines": [
" return;",
" }",
" ",
"- \/\/ Validate collection compatibility (vector dimensions)",
" if (!empty($collections)) {",
"- $compatibility = $this->validateCollectionCompatibility($collections);",
"+ $compatibility = $this->sessionsUseCase->validateCollectionCompatibility($collections);",
" if (!$compatibility['valid']) {",
" $this->view('chat.partials.error', [",
" 'error' => 'Collection-Fehler: ' . $compatibility['error'],"
]
},
{
"oldStart": 143,
"oldLines": 8,
"newStart": 103,
"newLines": 7,
"lines": [
" }",
" }",
" ",
"- \/\/ Execute UseCase",
"- $response = $this->sendMessageUseCase->execute(",
"+ $response = $this->messageUseCase->execute(",
" sessionUuid: $uuid,",
" message: $question,",
" model: $model,"
]
},
{
"oldStart": 162,
"oldLines": 550,
"newStart": 121,
"newLines": 63,
"lines": [
" return;",
" }",
" ",
"- $this->renderResponse($question, $response->toArray(), $model);",
"+ $result = $response->toArray();",
"+ $this->view('chat.partials.response', [",
"+ 'question' => $question,",
"+ 'result' => $result,",
"+ 'model' => $model,",
"+ 'formattedAnswer' => $this->formatter->formatAnswer($result['answer'] ?? ''),",
"+ ]);",
" }",
" ",
"- \/**",
"- * POST \/chat\/{uuid}\/title (HTMX)",
"- * Update session title",
"- *\/",
" public function updateTitle(string $uuid): void",
" {",
"- $session = $this->getSession($uuid);",
"+ $session = $this->sessionsUseCase->getSession($uuid);",
" ",
" if ($session === null) {",
" $this->notFound('Session nicht gefunden');",
" }",
" ",
"- $title = trim($_POST['title'] ?? '');",
"-",
"- \/\/ Validate title",
"- if ($title === '') {",
"- $title = 'Neuer Chat';",
"- }",
"-",
"- \/\/ Max 100 characters",
"- $title = mb_substr($title, 0, 100);",
"-",
"- \/\/ Save to database",
"- $this->updateSessionTitle($session['id'], $title);",
"-",
"- \/\/ Return updated title HTML",
"+ $title = $this->sessionsUseCase->updateTitle($session['id'], $_POST['title'] ?? '');",
" echo htmlspecialchars($title);",
" }",
" ",
"- \/**",
"- * POST \/chat\/{uuid}\/system-prompt (HTMX)",
"- * Update session system prompt",
"- *\/",
" public function systemPrompt(string $uuid): void",
" {",
"- $session = $this->getSession($uuid);",
"+ $session = $this->sessionsUseCase->getSession($uuid);",
" ",
" if ($session === null) {",
" $this->notFound('Session nicht gefunden');",
" }",
" ",
"- $systemPrompt = trim($_POST['system_prompt'] ?? '');",
"-",
"- \/\/ Validate and sanitize system prompt",
"- $systemPrompt = $this->validateSystemPrompt($systemPrompt);",
"-",
"- \/\/ Save to database",
"- $this->updateSystemPrompt($session['id'], $systemPrompt);",
"-",
"- \/\/ Return success message",
"- $this->view('chat.partials.success', ['message' => 'System-Prompt gespeichert.']);",
"+ $result = $this->sessionsUseCase->updateSystemPrompt($session['id'], $_POST['system_prompt'] ?? '');",
"+ $this->view('chat.partials.success', ['message' => $result->message]);",
" }",
" ",
"- \/**",
"- * GET \/chat\/{uuid}\/system-prompt (HTMX)",
"- * Get system prompt form",
"- *\/",
" public function getSystemPrompt(string $uuid): void",
" {",
"- $session = $this->getSession($uuid);",
"+ $session = $this->sessionsUseCase->getSession($uuid);",
" ",
" if ($session === null) {",
" $this->notFound('Session nicht gefunden');",
" }",
" ",
"- $defaultPrompt = $this->getDefaultSystemPrompt();",
"- $currentPrompt = $session['system_prompt'] ?? '';",
"-",
" $this->view('chat.partials.system-prompt-modal', [",
" 'session' => $session,",
"- 'currentPrompt' => $currentPrompt,",
"- 'defaultPrompt' => $defaultPrompt,",
"+ 'currentPrompt' => $session['system_prompt'] ?? '',",
"+ 'defaultPrompt' => $this->sessionsUseCase->getDefaultSystemPrompt(),",
" ]);",
" }",
" ",
"- \/**",
"- * DELETE \/chat\/{uuid}",
"- * Delete a session",
"- *\/",
" public function delete(string $uuid): void",
" {",
"- $session = $this->getSession($uuid);",
"+ $session = $this->sessionsUseCase->getSession($uuid);",
" ",
" if ($session !== null) {",
"- $stmt = $this->db->prepare('DELETE FROM chat_sessions WHERE id = ?');",
"- $stmt->execute([$session['id']]);",
"+ $this->sessionsUseCase->deleteSession($session['id']);",
" }",
" ",
"- \/\/ Return success for HTMX",
" header('HX-Redirect: \/chat');",
" echo 'OK';",
" }",
"-",
"- \/\/ ========== Private Helper Methods ==========",
"-",
"- \/**",
"- * Initialize database connection",
"- *\/",
"- private function initializeDatabase(): \\PDO",
"- {",
"- return \\Infrastructure\\Config\\DatabaseFactory::content();",
"- }",
"-",
"- \/**",
"- * Create a new chat session",
"- *\/",
"- private function createSession(): string",
"- {",
"- $uuid = $this->generateUuid();",
"-",
"- $stmt = $this->db->prepare(",
"- 'INSERT INTO chat_sessions (uuid, model, collections, context_limit) VALUES (?, ?, ?, ?)'",
"- );",
"- $stmt->execute([$uuid, 'claude-opus-4-5-20251101', '[\"documents\"]', 5]);",
"-",
"- return $uuid;",
"- }",
"-",
"- \/**",
"- * Get session by UUID",
"- *\/",
"- private function getSession(string $uuid): ?array",
"- {",
"- $stmt = $this->db->prepare('SELECT * FROM chat_sessions WHERE uuid = ?');",
"- $stmt->execute([$uuid]);",
"- $result = $stmt->fetch(\\PDO::FETCH_ASSOC);",
"-",
"- return $result !== false ? $result : null;",
"- }",
"-",
"- \/**",
"- * Get all sessions ordered by most recent",
"- *\/",
"- private function getSessions(): array",
"- {",
"- $stmt = $this->db->query(",
"- 'SELECT s.*,",
"- (SELECT COUNT(*) FROM chat_messages WHERE session_id = s.id) as message_count,",
"- (SELECT COALESCE(SUM(tokens_input), 0) FROM chat_messages WHERE session_id = s.id) as total_input_tokens,",
"- (SELECT COALESCE(SUM(tokens_output), 0) FROM chat_messages WHERE session_id = s.id) as total_output_tokens,",
"- (SELECT COALESCE(SUM(end_microtime - start_microtime), 0) FROM chat_messages WHERE session_id = s.id AND start_microtime IS NOT NULL) as total_duration,",
"- (SELECT model FROM chat_messages WHERE session_id = s.id AND role = \"assistant\" ORDER BY id DESC LIMIT 1) as last_model",
"- FROM chat_sessions s",
"- ORDER BY s.last_activity DESC",
"- LIMIT 50'",
"- );",
"-",
"- return $stmt->fetchAll(\\PDO::FETCH_ASSOC);",
"- }",
"-",
"- \/**",
"- * Get author profiles from content_config table",
"- *\/",
"- private function getAuthorProfiles(): array",
"- {",
"- $stmt = $this->db->query(",
"- \"SELECT id, name, slug, content as config",
"- FROM content_config",
"- WHERE type = 'author_profile' AND status = 'active'",
"- ORDER BY name\"",
"- );",
"-",
"- return $stmt->fetchAll(\\PDO::FETCH_ASSOC);",
"- }",
"-",
"- \/**",
"- * Get author profile by ID",
"- *\/",
"- private function getAuthorProfile(int $profileId): ?array",
"- {",
"- $stmt = $this->db->prepare(",
"- \"SELECT id, name, slug, content as config",
"- FROM content_config",
"- WHERE id = ? AND type = 'author_profile' AND status = 'active'\"",
"- );",
"- $stmt->execute([$profileId]);",
"- $result = $stmt->fetch(\\PDO::FETCH_ASSOC);",
"-",
"- return $result !== false ? $result : null;",
"- }",
"-",
"- \/**",
"- * Get system prompts from content_config table",
"- *\/",
"- private function getSystemPrompts(): array",
"- {",
"- $stmt = $this->db->query(",
"- \"SELECT id, name, slug, content",
"- FROM content_config",
"- WHERE type = 'system_prompt' AND status = 'active'",
"- ORDER BY name\"",
"- );",
"-",
"- return $stmt->fetchAll(\\PDO::FETCH_ASSOC);",
"- }",
"-",
"- \/**",
"- * Get messages for a session",
"- *\/",
"- private function getMessages(int $sessionId): array",
"- {",
"- $stmt = $this->db->prepare(",
"- 'SELECT * FROM chat_messages WHERE session_id = ? ORDER BY created_at ASC'",
"- );",
"- $stmt->execute([$sessionId]);",
"-",
"- return $stmt->fetchAll(\\PDO::FETCH_ASSOC);",
"- }",
"-",
"- \/**",
"- * Update session title",
"- *\/",
"- private function updateSessionTitle(int $sessionId, string $title): void",
"- {",
"- $stmt = $this->db->prepare('UPDATE chat_sessions SET title = ? WHERE id = ?');",
"- $stmt->execute([$title, $sessionId]);",
"- }",
"-",
"- \/**",
"- * Generate UUID v4",
"- *\/",
"- private function generateUuid(): string",
"- {",
"- $data = random_bytes(16);",
"- $data[6] = chr(ord($data[6]) & 0x0f | 0x40);",
"- $data[8] = chr(ord($data[8]) & 0x3f | 0x80);",
"-",
"- return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));",
"- }",
"-",
"- \/**",
"- * Validate model parameter",
"- *\/",
"- private function validateModel(string $model): string",
"- {",
"- return ModelConfig::validate($model);",
"- }",
"-",
"- \/**",
"- * Get available collections from database (cached per request)",
"- *",
"- * @return array<int, array<string,mixed>> List of collection data",
"- *\/",
"- private function getAvailableCollections(): array",
"- {",
"- if ($this->collectionsCache === null) {",
"- $this->collectionsCache = $this->collectionRepository->getSearchable();",
"-",
"- \/\/ Fallback if database is empty",
"- if ($this->collectionsCache === []) {",
"- $this->collectionsCache = [",
"- ['collection_id' => 'documents', 'display_name' => 'Dokumente', 'points_count' => 0, 'vector_size' => 1024],",
"- ];",
"- }",
"- }",
"-",
"- return $this->collectionsCache;",
"- }",
"-",
"- \/**",
"- * Get collection IDs only (for backwards compatibility)",
"- *",
"- * @return array<string>",
"- *\/",
"- private function getAvailableCollectionIds(): array",
"- {",
"- return array_column($this->getAvailableCollections(), 'collection_id');",
"- }",
"-",
"- \/**",
"- * Validate collections array against available collections",
"- *",
"- * @param array<string>|string $collections Collections to validate (array or single string)",
"- * @return array<string> Validated collections array (may be empty for no RAG)",
"- *\/",
"- private function validateCollections(array|string $collections): array",
"- {",
"- $availableIds = $this->getAvailableCollectionIds();",
"-",
"- \/\/ Handle string input (backwards compatibility)",
"- if (is_string($collections)) {",
"- $collections = [$collections];",
"- }",
"-",
"- \/\/ Filter to only valid collections",
"- $valid = array_filter($collections, fn ($c) => in_array($c, $availableIds, true));",
"-",
"- return array_values($valid);",
"- }",
"-",
"- \/**",
"- * Validate collection compatibility (vector dimensions)",
"- *",
"- * @param array<string> $collectionIds",
"- * @return array{valid: bool, error: string|null}",
"- *\/",
"- private function validateCollectionCompatibility(array $collectionIds): array",
"- {",
"- if (empty($collectionIds)) {",
"- return ['valid' => true, 'error' => null];",
"- }",
"-",
"- $result = $this->collectionValidator->validateSelection($collectionIds);",
"-",
"- return [",
"- 'valid' => $result->isValid(),",
"- 'error' => $result->getError(),",
"- ];",
"- }",
"-",
"- \/**",
"- * Validate context limit parameter against allowed values",
"- *\/",
"- private function validateContextLimit(int $limit): int",
"- {",
"- $allowedLimits = [3, 5, 10, 15];",
"-",
"- return in_array($limit, $allowedLimits, true) ? $limit : 5;",
"- }",
"-",
"- \/**",
"- * Validate author profile ID (0 = no profile)",
"- *\/",
"- private function validateAuthorProfileId(int $profileId): int",
"- {",
"- if ($profileId === 0) {",
"- return 0;",
"- }",
"- $profile = $this->getAuthorProfile($profileId);",
"-",
"- return $profile !== null ? $profileId : 0;",
"- }",
"-",
"- \/**",
"- * Validate temperature parameter (0.0 - 1.0)",
"- *\/",
"- private function validateTemperature(float $temperature): float",
"- {",
"- return max(0.0, min(1.0, $temperature));",
"- }",
"-",
"- \/**",
"- * Validate max_tokens parameter against allowed values",
"- *\/",
"- private function validateMaxTokens(int $maxTokens): int",
"- {",
"- $allowedValues = [1024, 2048, 4096, 8192];",
"-",
"- return in_array($maxTokens, $allowedValues, true) ? $maxTokens : 4096;",
"- }",
"-",
"- \/**",
"- * Update session model, collections, context limit, author profile, temperature and max_tokens settings",
"- *",
"- * @param array<string> $collections Array of collection names",
"- *\/",
"- private function updateSessionSettings(int $sessionId, string $model, array $collections, int $contextLimit = 5, int $authorProfileId = 0, float $temperature = 0.7, int $maxTokens = 4096): void",
"- {",
"- $collectionsJson = json_encode($collections);",
"- $stmt = $this->db->prepare('UPDATE chat_sessions SET model = ?, collections = ?, context_limit = ?, author_profile_id = ?, temperature = ?, max_tokens = ? WHERE id = ?');",
"- $stmt->execute([$model, $collectionsJson, $contextLimit, $authorProfileId > 0 ? $authorProfileId : null, $temperature, $maxTokens, $sessionId]);",
"- }",
"-",
"- \/**",
"- * Validate system prompt input.",
"- *",
"- * NOTE: Pattern-based prompt injection filtering was intentionally removed.",
"- * Such filtering provides false security since attackers can easily rephrase",
"- * malicious instructions. LLM prompt injection cannot be reliably prevented",
"- * through input sanitization alone.",
"- *",
"- * Instead, we apply:",
"- * - Length limits (to prevent resource abuse)",
"- * - Logging (for monitoring and incident response)",
"- *",
"- * True prompt injection mitigation requires architectural measures like",
"- * sandboxing LLM outputs and limiting LLM capabilities.",
"- *\/",
"- private function validateSystemPrompt(string $prompt): string",
"- {",
"- \/\/ Length limit: prevent resource abuse",
"- $maxLength = 2000;",
"- $wasTruncated = mb_strlen($prompt) > $maxLength;",
"- $prompt = mb_substr($prompt, 0, $maxLength);",
"-",
"- \/\/ Log custom system prompts for monitoring",
"- $this->logSystemPromptChange($prompt, $wasTruncated);",
"-",
"- return trim($prompt);",
"- }",
"-",
"- \/**",
"- * Log system prompt changes for security monitoring.",
"- *\/",
"- private function logSystemPromptChange(string $prompt, bool $wasTruncated): void",
"- {",
"- $logData = [",
"- 'prompt_length' => mb_strlen($prompt),",
"- 'truncated' => $wasTruncated,",
"- 'ip' => $_SERVER['REMOTE_ADDR'] ?? 'unknown',",
"- 'timestamp' => date('c'),",
"- ];",
"-",
"- \/\/ Log to database via ki_dev.mcp_log",
"- try {",
"- $devDb = \\Infrastructure\\Config\\DatabaseFactory::dev();",
"- $stmt = $devDb->prepare(",
"- \"INSERT INTO mcp_log (tool, operation, parameters, result, logged_at)",
"- VALUES ('chat', 'system_prompt_change', :params, 'logged', NOW())\"",
"- );",
"- $stmt->execute(['params' => json_encode($logData)]);",
"- } catch (\\Exception $e) {",
"- \/\/ Silently fail - logging should not break functionality",
"- }",
"- }",
"-",
"- \/**",
"- * Update session system prompt",
"- *\/",
"- private function updateSystemPrompt(int $sessionId, string $systemPrompt): void",
"- {",
"- $stmt = $this->db->prepare('UPDATE chat_sessions SET system_prompt = ? WHERE id = ?');",
"- $stmt->execute([$systemPrompt !== '' ? $systemPrompt : null, $sessionId]);",
"- }",
"-",
"- \/**",
"- * Get default system prompt from ClaudeService",
"- *\/",
"- private function getDefaultSystemPrompt(): string",
"- {",
"- return <<<'PROMPT'",
"- Du bist ein hilfreicher Assistent für Fragen zu systemischem Teamcoaching und Teamentwicklung.",
"-",
"- Beantworte die Frage des Nutzers basierend auf dem bereitgestellten Kontext.",
"- - Antworte auf Deutsch",
"- - Sei präzise und hilfreich",
"- - Wenn der Kontext die Frage nicht beantwortet, sage das ehrlich",
"- - Verweise auf die Quellen wenn passend",
"- PROMPT;",
"- }",
"-",
"- \/**",
"- * Render chat response HTML using view partial",
"- *\/",
"- private function renderResponse(string $question, array $result, string $model): void",
"- {",
"- \/\/ Convert markdown-style formatting to HTML",
"- $formattedAnswer = $this->formatAnswer($result['answer'] ?? '');",
"-",
"- $this->view('chat.partials.response', [",
"- 'question' => $question,",
"- 'result' => $result,",
"- 'model' => $model,",
"- 'formattedAnswer' => $formattedAnswer,",
"- ]);",
"- }",
"-",
"- \/**",
"- * Format markdown-like answer to HTML.",
"- *",
"- * Converts basic markdown syntax to HTML. Uses line-based processing",
"- * to correctly handle list blocks mixed with regular text.",
"- *\/",
"- private function formatAnswer(string $text): string",
"- {",
"- \/\/ Escape HTML first",
"- $text = htmlspecialchars($text);",
"-",
"- \/\/ Headers",
"- $text = preg_replace('\/^### (.+)$\/m', '<h4>$1<\/h4>', $text);",
"- $text = preg_replace('\/^## (.+)$\/m', '<h3>$1<\/h3>', $text);",
"- $text = preg_replace('\/^# (.+)$\/m', '<h3>$1<\/h3>', $text);",
"-",
"- \/\/ Bold",
"- $text = preg_replace('\/\\*\\*(.+?)\\*\\*\/', '<strong>$1<\/strong>', $text);",
"-",
"- \/\/ Italic",
"- $text = preg_replace('\/\\*(.+?)\\*\/', '<em>$1<\/em>', $text);",
"-",
"- \/\/ Code",
"- $text = preg_replace('\/`(.+?)`\/', '<code>$1<\/code>', $text);",
"-",
"- \/\/ Blockquotes",
"- $text = preg_replace('\/^> (.+)$\/m', '<blockquote>$1<\/blockquote>', $text);",
"-",
"- \/\/ Lists: Process line by line to correctly wrap consecutive list items",
"- $text = $this->formatLists($text);",
"-",
"- \/\/ Line breaks",
"- $text = nl2br($text);",
"-",
"- \/\/ Clean up multiple <br> tags",
"- $text = preg_replace('\/(<br\\s*\\\/?>\\s*){3,}\/', '<br><br>', $text);",
"-",
"- return $text;",
"- }",
"-",
"- \/**",
"- * Format list items and wrap consecutive items in <ul> tags.",
"- *",
"- * Processes text line by line to correctly handle:",
"- * - Multiple separate lists with text between them",
"- * - Mixed content (text + list + text + list)",
"- * - Numbered lists (converted to unordered for simplicity)",
"- *\/",
"- private function formatLists(string $text): string",
"- {",
"- $lines = explode(\"\\n\", $text);",
"- $result = [];",
"- $inList = false;",
"-",
"- foreach ($lines as $line) {",
"- \/\/ Check if line is a list item (- or * or numbered)",
"- if (preg_match('\/^[-*] (.+)$\/', $line, $matches)) {",
"- if (!$inList) {",
"- $result[] = '<ul>';",
"- $inList = true;",
"- }",
"- $result[] = '<li>' . $matches[1] . '<\/li>';",
"- } elseif (preg_match('\/^\\d+\\. (.+)$\/', $line, $matches)) {",
"- \/\/ Numbered list item",
"- if (!$inList) {",
"- $result[] = '<ul>';",
"- $inList = true;",
"- }",
"- $result[] = '<li>' . $matches[1] . '<\/li>';",
"- } else {",
"- \/\/ Not a list item",
"- if ($inList) {",
"- $result[] = '<\/ul>';",
"- $inList = false;",
"- }",
"- $result[] = $line;",
"- }",
"- }",
"-",
"- \/\/ Close any open list",
"- if ($inList) {",
"- $result[] = '<\/ul>';",
"- }",
"-",
"- return implode(\"\\n\", $result);",
"- }",
" }"
]
}
],
"originalFile": "<?php\n\nnamespace Controller;\n\nuse Framework\\Controller;\nuse Infrastructure\\AI\\AIConfig;\nuse Infrastructure\\AI\\ChatService;\nuse Infrastructure\\AI\\ModelConfig;\nuse Infrastructure\\Persistence\\CollectionRepository;\nuse Infrastructure\\Validation\\CollectionValidator;\nuse UseCases\\Chat\\SendChatMessageUseCase;\n\nclass ChatController extends Controller\n{\n private ChatService $chatService;\n private CollectionRepository $collectionRepository;\n private CollectionValidator $collectionValidator;\n private SendChatMessageUseCase $sendMessageUseCase;\n private \\PDO $db;\n\n \/** @var array<array<string,mixed>>|null Cached collections list *\/\n private ?array $collectionsCache = null;\n\n public function __construct()\n {\n $config = AIConfig::fromCredentialsFile();\n $this->chatService = $config->createChatService();\n $this->collectionRepository = new CollectionRepository();\n $this->collectionValidator = new CollectionValidator($this->collectionRepository);\n $this->sendMessageUseCase = new SendChatMessageUseCase($this->chatService);\n $this->db = $this->initializeDatabase();\n }\n\n \/**\n * GET \/chat\n * Show chat interface with session list, create new session if none\n *\/\n public function index(): void\n {\n $sessions = $this->getSessions();\n\n \/\/ Create new session and redirect\n $uuid = $this->createSession();\n header('Location: \/chat\/' . $uuid);\n exit;\n }\n\n \/**\n * GET \/chat\/{uuid}\n * Show specific chat session\n *\/\n public function show(string $uuid): void\n {\n $session = $this->getSession($uuid);\n\n if ($session === null) {\n header('Location: \/chat');\n exit;\n }\n\n $messages = $this->getMessages($session['id']);\n $sessions = $this->getSessions();\n $authorProfiles = $this->getAuthorProfiles();\n $systemPrompts = $this->getSystemPrompts();\n $collections = $this->getAvailableCollections();\n\n $this->view('chat.index', [\n 'title' => $session['title'] ?? 'KI-Chat',\n 'session' => $session,\n 'messages' => $messages,\n 'sessions' => $sessions,\n 'authorProfiles' => $authorProfiles,\n 'systemPrompts' => $systemPrompts,\n 'collections' => $collections,\n 'models' => ModelConfig::getAll(),\n 'defaultModel' => ModelConfig::DEFAULT_MODEL,\n ]);\n }\n\n \/**\n * GET \/chat\/sessions (HTMX partial)\n * Return session list HTML\n *\/\n public function sessionList(): void\n {\n $sessions = $this->getSessions();\n $this->view('chat.partials.session-list', [\n 'sessions' => $sessions,\n 'currentUuid' => $this->getString('current') ?: null,\n ]);\n }\n\n \/**\n * POST \/chat\/{uuid}\/message (HTMX)\n * Process message and return HTML response\n *\/\n public function message(string $uuid): void\n {\n $session = $this->getSession($uuid);\n\n if ($session === null) {\n $this->view('chat.partials.error', ['error' => 'Session nicht gefunden.']);\n\n return;\n }\n\n $question = trim($_POST['message'] ?? '');\n $model = $this->validateModel($_POST['model'] ?? $session['model']);\n $sessionCollections = $this->decodeJson($session['collections'] ?? null) ?: ['documents'];\n $collections = $this->validateCollections($_POST['collections'] ?? $sessionCollections);\n $contextLimit = $this->validateContextLimit((int) ($_POST['context_limit'] ?? $session['context_limit'] ?? 5));\n $authorProfileId = $this->validateAuthorProfileId((int) ($_POST['author_profile_id'] ?? $session['author_profile_id'] ?? 0));\n $systemPromptId = (int) ($_POST['system_prompt_id'] ?? $session['system_prompt_id'] ?? 1);\n $temperature = $this->validateTemperature((float) ($_POST['temperature'] ?? $session['temperature'] ?? 0.7));\n $maxTokens = $this->validateMaxTokens((int) ($_POST['max_tokens'] ?? $session['max_tokens'] ?? 4096));\n\n \/\/ Update session if settings changed\n $currentLimit = (int) ($session['context_limit'] ?? 5);\n $currentProfileId = (int) ($session['author_profile_id'] ?? 0);\n $currentTemperature = (float) ($session['temperature'] ?? 0.7);\n $currentMaxTokens = (int) ($session['max_tokens'] ?? 4096);\n $collectionsJson = json_encode($collections);\n if ($model !== $session['model'] || $collectionsJson !== ($session['collections'] ?? '[\"documents\"]') || $contextLimit !== $currentLimit || $authorProfileId !== $currentProfileId || $temperature !== $currentTemperature || $maxTokens !== $currentMaxTokens) {\n $this->updateSessionSettings($session['id'], $model, $collections, $contextLimit, $authorProfileId, $temperature, $maxTokens);\n }\n\n if ($question === '') {\n $this->view('chat.partials.error', ['error' => 'Bitte gib eine Frage ein.']);\n\n return;\n }\n\n \/\/ Validate collection compatibility (vector dimensions)\n if (!empty($collections)) {\n $compatibility = $this->validateCollectionCompatibility($collections);\n if (!$compatibility['valid']) {\n $this->view('chat.partials.error', [\n 'error' => 'Collection-Fehler: ' . $compatibility['error'],\n 'details' => 'Bitte wähle nur Collections mit gleichem Embedding-Modell.',\n ]);\n\n return;\n }\n }\n\n \/\/ Execute UseCase\n $response = $this->sendMessageUseCase->execute(\n sessionUuid: $uuid,\n message: $question,\n model: $model,\n collections: $collections,\n contextLimit: $contextLimit,\n authorProfileId: $authorProfileId,\n systemPromptId: $systemPromptId,\n temperature: $temperature,\n maxTokens: $maxTokens\n );\n\n if ($response->hasError()) {\n $this->view('chat.partials.error', ['error' => $response->getError()]);\n\n return;\n }\n\n $this->renderResponse($question, $response->toArray(), $model);\n }\n\n \/**\n * POST \/chat\/{uuid}\/title (HTMX)\n * Update session title\n *\/\n public function updateTitle(string $uuid): void\n {\n $session = $this->getSession($uuid);\n\n if ($session === null) {\n $this->notFound('Session nicht gefunden');\n }\n\n $title = trim($_POST['title'] ?? '');\n\n \/\/ Validate title\n if ($title === '') {\n $title = 'Neuer Chat';\n }\n\n \/\/ Max 100 characters\n $title = mb_substr($title, 0, 100);\n\n \/\/ Save to database\n $this->updateSessionTitle($session['id'], $title);\n\n \/\/ Return updated title HTML\n echo htmlspecialchars($title);\n }\n\n \/**\n * POST \/chat\/{uuid}\/system-prompt (HTMX)\n * Update session system prompt\n *\/\n public function systemPrompt(string $uuid): void\n {\n $session = $this->getSession($uuid);\n\n if ($session === null) {\n $this->notFound('Session nicht gefunden');\n }\n\n $systemPrompt = trim($_POST['system_prompt'] ?? '');\n\n \/\/ Validate and sanitize system prompt\n $systemPrompt = $this->validateSystemPrompt($systemPrompt);\n\n \/\/ Save to database\n $this->updateSystemPrompt($session['id'], $systemPrompt);\n\n \/\/ Return success message\n $this->view('chat.partials.success', ['message' => 'System-Prompt gespeichert.']);\n }\n\n \/**\n * GET \/chat\/{uuid}\/system-prompt (HTMX)\n * Get system prompt form\n *\/\n public function getSystemPrompt(string $uuid): void\n {\n $session = $this->getSession($uuid);\n\n if ($session === null) {\n $this->notFound('Session nicht gefunden');\n }\n\n $defaultPrompt = $this->getDefaultSystemPrompt();\n $currentPrompt = $session['system_prompt'] ?? '';\n\n $this->view('chat.partials.system-prompt-modal', [\n 'session' => $session,\n 'currentPrompt' => $currentPrompt,\n 'defaultPrompt' => $defaultPrompt,\n ]);\n }\n\n \/**\n * DELETE \/chat\/{uuid}\n * Delete a session\n *\/\n public function delete(string $uuid): void\n {\n $session = $this->getSession($uuid);\n\n if ($session !== null) {\n $stmt = $this->db->prepare('DELETE FROM chat_sessions WHERE id = ?');\n $stmt->execute([$session['id']]);\n }\n\n \/\/ Return success for HTMX\n header('HX-Redirect: \/chat');\n echo 'OK';\n }\n\n \/\/ ========== Private Helper Methods ==========\n\n \/**\n * Initialize database connection\n *\/\n private function initializeDatabase(): \\PDO\n {\n return \\Infrastructure\\Config\\DatabaseFactory::content();\n }\n\n \/**\n * Create a new chat session\n *\/\n private function createSession(): string\n {\n $uuid = $this->generateUuid();\n\n $stmt = $this->db->prepare(\n 'INSERT INTO chat_sessions (uuid, model, collections, context_limit) VALUES (?, ?, ?, ?)'\n );\n $stmt->execute([$uuid, 'claude-opus-4-5-20251101', '[\"documents\"]', 5]);\n\n return $uuid;\n }\n\n \/**\n * Get session by UUID\n *\/\n private function getSession(string $uuid): ?array\n {\n $stmt = $this->db->prepare('SELECT * FROM chat_sessions WHERE uuid = ?');\n $stmt->execute([$uuid]);\n $result = $stmt->fetch(\\PDO::FETCH_ASSOC);\n\n return $result !== false ? $result : null;\n }\n\n \/**\n * Get all sessions ordered by most recent\n *\/\n private function getSessions(): array\n {\n $stmt = $this->db->query(\n 'SELECT s.*,\n (SELECT COUNT(*) FROM chat_messages WHERE session_id = s.id) as message_count,\n (SELECT COALESCE(SUM(tokens_input), 0) FROM chat_messages WHERE session_id = s.id... [TRUNCATED-e64f2bf35a1c435f]"
}
}