Backup #282

ID282
Dateipfad/var/www/dev.campus.systemische-tools.de/src/Controller/ChatController.php
Version64
Typ modified
Größe23.3 KB
Hash6c206265dd9aa98601d17a82724619860f75cfab11b81ff34f6ded982a48aba9
Datum2025-12-22 02:19:25
Geändert vonclaude-code-hook
GrundClaude Code Pre-Hook Backup vor Edit-Operation
Datei existiert Ja

Dateiinhalt

<?php

namespace Controller;

use Framework\Controller;
use Infrastructure\AI\AIConfig;
use Infrastructure\AI\ChatService;
use Infrastructure\AI\ModelConfig;
use Infrastructure\Persistence\CollectionRepository;
use Infrastructure\Persistence\ContentConfigRepository;
use Infrastructure\Validation\CollectionValidator;
use UseCases\Chat\SendChatMessageUseCase;

class ChatController extends Controller
{
    private ChatService $chatService;
    private CollectionRepository $collectionRepository;
    private CollectionValidator $collectionValidator;
    private SendChatMessageUseCase $sendMessageUseCase;
    private \PDO $db;

    /** @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();
    }

    /**
     * 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();
        header('Location: /chat/' . $uuid);
        exit;
    }

    /**
     * GET /chat/{uuid}
     * Show specific chat session
     */
    public function show(string $uuid): void
    {
        $session = $this->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,
            '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,
            'currentUuid' => $_GET['current'] ?? null,
        ]);
    }

    /**
     * POST /chat/{uuid}/message (HTMX)
     * Process message and return HTML response
     */
    public function message(string $uuid): void
    {
        $session = $this->getSession($uuid);

        if ($session === null) {
            $this->view('chat.partials.error', ['error' => 'Session nicht gefunden.']);

            return;
        }

        $question = trim($_POST['message'] ?? '');
        $model = $this->validateModel($_POST['model'] ?? $session['model']);
        $sessionCollections = json_decode($session['collections'] ?? '["documents"]', true) ?: ['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));
        $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));

        // 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 ($question === '') {
            $this->view('chat.partials.error', ['error' => 'Bitte gib eine Frage ein.']);

            return;
        }

        // Validate collection compatibility (vector dimensions)
        if (!empty($collections)) {
            $compatibility = $this->validateCollectionCompatibility($collections);
            if (!$compatibility['valid']) {
                $this->view('chat.partials.error', [
                    'error' => 'Collection-Fehler: ' . $compatibility['error'],
                    'details' => 'Bitte wähle nur Collections mit gleichem Embedding-Modell.',
                ]);

                return;
            }
        }

        // Execute UseCase
        $response = $this->sendMessageUseCase->execute(
            sessionUuid: $uuid,
            message: $question,
            model: $model,
            collections: $collections,
            contextLimit: $contextLimit,
            authorProfileId: $authorProfileId,
            systemPromptId: $systemPromptId,
            temperature: $temperature,
            maxTokens: $maxTokens
        );

        if ($response->hasError()) {
            $this->view('chat.partials.error', ['error' => $response->getError()]);

            return;
        }

        $this->renderResponse($question, $response->toArray(), $model);
    }

    /**
     * POST /chat/{uuid}/title (HTMX)
     * Update session title
     */
    public function updateTitle(string $uuid): void
    {
        $session = $this->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
        echo htmlspecialchars($title);
    }

    /**
     * POST /chat/{uuid}/system-prompt (HTMX)
     * Update session system prompt
     */
    public function systemPrompt(string $uuid): void
    {
        $session = $this->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.']);
    }

    /**
     * GET /chat/{uuid}/system-prompt (HTMX)
     * Get system prompt form
     */
    public function getSystemPrompt(string $uuid): void
    {
        $session = $this->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,
        ]);
    }

    /**
     * DELETE /chat/{uuid}
     * Delete a session
     */
    public function delete(string $uuid): void
    {
        $session = $this->getSession($uuid);

        if ($session !== null) {
            $stmt = $this->db->prepare('DELETE FROM chat_sessions WHERE id = ?');
            $stmt->execute([$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 system prompt by ID from content_config
     */
    private function getSystemPromptById(int $promptId): ?array
    {
        $stmt = $this->db->prepare(
            "SELECT id, name, slug, content
             FROM content_config
             WHERE id = ? AND type = 'system_prompt' AND status = 'active'"
        );
        $stmt->execute([$promptId]);
        $result = $stmt->fetch(\PDO::FETCH_ASSOC);

        return $result !== false ? $result : null;
    }

    /**
     * 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];

... (229 weitere Zeilen)

Vollständig herunterladen

Aktionen

Herunterladen

Andere Versionen dieser Datei

ID Version Typ Größe Datum
2128 131 modified 17.2 KB 2025-12-29 15:04
2126 130 modified 15.7 KB 2025-12-29 15:00
2125 129 modified 15.6 KB 2025-12-29 15:00
2124 128 modified 15.6 KB 2025-12-29 15:00
2120 127 modified 15.5 KB 2025-12-29 09:18
2119 126 modified 15.3 KB 2025-12-29 09:17
2111 125 modified 15.3 KB 2025-12-29 08:49
2091 124 modified 13.5 KB 2025-12-29 00:15
2090 123 modified 14.3 KB 2025-12-29 00:14
2089 122 modified 16.9 KB 2025-12-29 00:14
2088 121 modified 17.8 KB 2025-12-29 00:13
2087 120 modified 14.2 KB 2025-12-29 00:13
1583 119 modified 14.1 KB 2025-12-26 21:07
1579 118 modified 13.9 KB 2025-12-26 20:56
1578 117 modified 13.8 KB 2025-12-26 20:55
1574 116 modified 13.6 KB 2025-12-26 20:40
1573 115 modified 13.5 KB 2025-12-26 20:39
1572 114 modified 13.5 KB 2025-12-26 20:39
1571 113 modified 13.2 KB 2025-12-26 20:39
1559 112 modified 8.6 KB 2025-12-26 20:28
1558 111 modified 8.4 KB 2025-12-26 20:28
1544 110 modified 8.3 KB 2025-12-26 20:05
1539 109 modified 9.6 KB 2025-12-26 20:04
1538 108 modified 10.1 KB 2025-12-26 20:04
1536 107 modified 8.8 KB 2025-12-26 19:45
1535 106 modified 8.3 KB 2025-12-26 19:45
1494 105 modified 8.3 KB 2025-12-25 17:31
1488 104 modified 8.3 KB 2025-12-25 17:04
1487 103 modified 8.2 KB 2025-12-25 17:04
1486 102 modified 8.2 KB 2025-12-25 17:04
1485 101 modified 8.2 KB 2025-12-25 17:04
1484 100 modified 8.2 KB 2025-12-25 17:04
1483 99 modified 8.2 KB 2025-12-25 17:04
1482 98 modified 8.2 KB 2025-12-25 17:03
1466 97 modified 8.2 KB 2025-12-25 17:02
1465 96 modified 8.2 KB 2025-12-25 17:01
1464 95 modified 8.1 KB 2025-12-25 17:01
1453 94 modified 8.1 KB 2025-12-25 17:00
1452 93 modified 8.1 KB 2025-12-25 17:00
1451 92 modified 8.1 KB 2025-12-25 17:00
1445 91 modified 7.7 KB 2025-12-25 17:00
1293 90 modified 7.6 KB 2025-12-25 13:27
1292 89 modified 7.6 KB 2025-12-25 13:27
1291 88 modified 7.9 KB 2025-12-25 13:27
1145 87 modified 7.9 KB 2025-12-25 09:45
1144 86 modified 7.8 KB 2025-12-25 09:45
1142 85 modified 7.7 KB 2025-12-25 09:45
1137 84 modified 7.7 KB 2025-12-25 09:42
1136 83 modified 7.7 KB 2025-12-25 09:42
1135 82 modified 7.6 KB 2025-12-25 09:42
685 81 modified 7.5 KB 2025-12-23 07:51
681 80 modified 7.5 KB 2025-12-23 07:44
632 79 modified 7.8 KB 2025-12-23 04:44
631 78 modified 7.8 KB 2025-12-23 04:44
630 77 modified 7.8 KB 2025-12-23 04:44
629 76 modified 7.6 KB 2025-12-23 04:44
619 75 modified 7.7 KB 2025-12-23 04:42
564 74 modified 7.6 KB 2025-12-23 03:35
551 73 modified 7.6 KB 2025-12-23 02:39
550 72 modified 7.5 KB 2025-12-23 02:39
546 71 modified 7.4 KB 2025-12-23 02:38
540 70 modified 6.4 KB 2025-12-22 22:30
539 69 modified 6.4 KB 2025-12-22 22:29
470 68 modified 22.7 KB 2025-12-22 10:36
321 67 modified 22.7 KB 2025-12-22 08:06
293 66 modified 22.7 KB 2025-12-22 08:03
283 65 modified 23.2 KB 2025-12-22 02:19
282 64 modified 23.3 KB 2025-12-22 02:19
281 63 modified 23.4 KB 2025-12-22 02:19
274 62 modified 24.1 KB 2025-12-22 02:14
273 61 modified 25.6 KB 2025-12-22 02:14
272 60 modified 27.0 KB 2025-12-22 02:14
271 59 modified 27.4 KB 2025-12-22 02:13
270 58 modified 29.0 KB 2025-12-22 02:13
269 57 modified 28.8 KB 2025-12-22 02:13
268 56 modified 28.7 KB 2025-12-22 02:12
257 55 modified 29.0 KB 2025-12-22 02:00
256 54 modified 32.1 KB 2025-12-22 01:59
255 53 modified 32.1 KB 2025-12-22 01:59
254 52 modified 32.1 KB 2025-12-22 01:59
253 51 modified 32.1 KB 2025-12-22 01:59
252 50 modified 32.2 KB 2025-12-22 01:59
251 49 modified 32.2 KB 2025-12-22 01:59
250 48 modified 32.2 KB 2025-12-22 01:59
217 47 modified 32.2 KB 2025-12-22 01:43
216 46 modified 32.3 KB 2025-12-22 01:43
215 45 modified 32.3 KB 2025-12-22 01:43
197 44 modified 32.4 KB 2025-12-21 14:39
196 43 modified 32.4 KB 2025-12-21 14:39
195 42 modified 32.5 KB 2025-12-21 14:39
183 41 modified 31.9 KB 2025-12-21 14:15
182 40 modified 31.4 KB 2025-12-21 14:15
181 39 modified 31.0 KB 2025-12-21 14:15
180 38 modified 30.6 KB 2025-12-21 14:15
169 37 modified 30.5 KB 2025-12-21 04:12
168 36 modified 30.6 KB 2025-12-21 04:12
167 35 modified 30.6 KB 2025-12-21 04:12
148 34 modified 30.6 KB 2025-12-21 02:30
147 33 modified 30.6 KB 2025-12-21 02:30
146 32 modified 29.4 KB 2025-12-21 02:30
145 31 modified 29.4 KB 2025-12-21 02:29
144 30 modified 29.4 KB 2025-12-21 02:29
143 29 modified 29.5 KB 2025-12-21 02:29
142 28 modified 29.3 KB 2025-12-21 02:16
132 27 modified 27.8 KB 2025-12-20 19:46
131 26 modified 27.7 KB 2025-12-20 19:46
130 25 modified 26.5 KB 2025-12-20 19:42
82 24 modified 26.4 KB 2025-12-20 19:13
81 23 modified 26.3 KB 2025-12-20 19:13
80 22 modified 26.3 KB 2025-12-20 19:13
79 21 modified 25.9 KB 2025-12-20 19:12
78 20 modified 25.9 KB 2025-12-20 19:12
77 19 modified 25.9 KB 2025-12-20 19:12
76 18 modified 25.9 KB 2025-12-20 19:10
75 17 modified 25.8 KB 2025-12-20 19:10
74 16 modified 25.7 KB 2025-12-20 19:10
68 15 modified 25.9 KB 2025-12-20 18:49
67 14 modified 25.4 KB 2025-12-20 18:49
66 13 modified 25.3 KB 2025-12-20 18:49
65 12 modified 25.0 KB 2025-12-20 18:49
60 11 modified 24.5 KB 2025-12-20 18:32
59 10 modified 24.4 KB 2025-12-20 18:31
58 9 modified 24.3 KB 2025-12-20 18:31
57 8 modified 24.3 KB 2025-12-20 18:31
56 7 modified 24.0 KB 2025-12-20 18:31
55 6 modified 23.8 KB 2025-12-20 18:31
40 5 modified 24.1 KB 2025-12-20 17:24
39 4 modified 24.9 KB 2025-12-20 17:24
9 3 modified 24.9 KB 2025-12-20 16:32
5 2 modified 24.8 KB 2025-12-20 16:08
4 1 modified 24.8 KB 2025-12-20 16:08

← Zurück zur Übersicht