Backup #74

ID74
Dateipfad/var/www/dev.campus.systemische-tools.de/src/Controller/ChatController.php
Version16
Typ modified
Größe25.7 KB
Hash1b91cde54f330411264bc4b19f8fe3fad7f5f1d3f69da1284f567309a137f358
Datum2025-12-20 19:10:15
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\QdrantService;
use Infrastructure\Persistence\SystemPromptRepository;

class ChatController extends Controller
{
    private ChatService $chatService;
    private QdrantService $qdrantService;
    private \PDO $db;
    private SystemPromptRepository $systemPromptRepo;

    /** @var array<string>|null Cached collections list */
    private ?array $collectionsCache = null;

    public function __construct()
    {
        $config = AIConfig::fromCredentialsFile();
        $this->chatService = $config->createChatService();
        $this->qdrantService = $config->createQdrantService();
        $this->db = $this->initializeDatabase();
        $this->systemPromptRepo = new SystemPromptRepository();
    }

    /**
     * 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->systemPromptRepo->findAllActive();
        $collections = $this->getAvailableCollections();

        $this->view('chat.index', [
            'title' => $session['title'] ?? 'KI-Chat',
            'session' => $session,
            'messages' => $messages,
            'sessions' => $sessions,
            'authorProfiles' => $authorProfiles,
            'systemPrompts' => $systemPrompts,
            'collections' => $collections,
        ]);
    }

    /**
     * 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) {
            echo '<div class="chat-error">Session nicht gefunden.</div>';

            return;
        }

        $question = trim($_POST['message'] ?? '');
        $model = $this->validateModel($_POST['model'] ?? $session['model']);
        $collection = $this->validateCollection($_POST['collection'] ?? $session['collection']);
        $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);
        if ($model !== $session['model'] || $collection !== $session['collection'] || $contextLimit !== $currentLimit || $authorProfileId !== $currentProfileId || $temperature !== $currentTemperature || $maxTokens !== $currentMaxTokens) {
            $this->updateSessionSettings($session['id'], $model, $collection, $contextLimit, $authorProfileId, $temperature, $maxTokens);
        }

        if ($question === '') {
            echo '<div class="chat-error">Bitte gib eine Frage ein.</div>';

            return;
        }

        try {
            // Save user message
            $this->saveMessage($session['id'], 'user', $question, $model);

            // Auto-set title from first message
            if ($session['title'] === null) {
                $title = mb_substr($question, 0, 50) . (mb_strlen($question) > 50 ? '...' : '');
                $this->updateSessionTitle($session['id'], $title);
            }

            // Get style prompt from author profile
            $stylePrompt = $this->getStylePromptFromProfile($authorProfileId);

            // Get system prompt from database
            $systemPromptData = $this->systemPromptRepo->findById($systemPromptId);
            $customSystemPrompt = $systemPromptData['content'] ?? null;

            // Track timing
            $startMicrotime = microtime(true);

            // Get response from AI
            $result = $this->askChat(
                $question,
                $model,
                $collection,
                $contextLimit,
                $stylePrompt,
                $customSystemPrompt,
                $temperature,
                $maxTokens
            );

            $endMicrotime = microtime(true);

            if (isset($result['error'])) {
                echo '<div class="chat-error">' . htmlspecialchars($result['error']) . '</div>';

                return;
            }

            // Save assistant message with full tracking
            $tokensInput = $result['usage']['input_tokens'] ?? null;
            $tokensOutput = $result['usage']['output_tokens'] ?? null;
            $sources = $result['sources'] ?? [];

            $this->saveMessage(
                $session['id'],
                'assistant',
                $result['answer'],
                $model,
                $tokensInput,
                $tokensOutput,
                $sources,
                $startMicrotime,
                $endMicrotime,
                $authorProfileId > 0 ? $authorProfileId : null,
                $systemPromptId > 0 ? $systemPromptId : null,
                $collection,
                $contextLimit
            );

            $this->renderResponse($question, $result, $model);
        } catch (\Exception $e) {
            echo '<div class="chat-error">Fehler: ' . htmlspecialchars($e->getMessage()) . '</div>';
        }
    }

    /**
     * POST /chat/{uuid}/title (HTMX)
     * Update session title
     */
    public function updateTitle(string $uuid): void
    {
        $session = $this->getSession($uuid);

        if ($session === null) {
            http_response_code(404);
            echo '';

            return;
        }

        $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) {
            http_response_code(404);
            echo '<div class="chat-error">Session nicht gefunden.</div>';

            return;
        }

        $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
        echo '<div class="chat-success">System-Prompt gespeichert.</div>';
    }

    /**
     * GET /chat/{uuid}/system-prompt (HTMX)
     * Get system prompt form
     */
    public function getSystemPrompt(string $uuid): void
    {
        $session = $this->getSession($uuid);

        if ($session === null) {
            http_response_code(404);
            echo '<div class="chat-error">Session nicht gefunden.</div>';

            return;
        }

        $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, collection, 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 ki_content database
     */
    private function getAuthorProfiles(): array
    {
        $stmt = $this->db->query('SELECT id, name, slug, config FROM author_profiles WHERE is_active = 1 ORDER BY id');

        return $stmt->fetchAll(\PDO::FETCH_ASSOC);
    }

    /**
     * Get author profile by ID
     */
    private function getAuthorProfile(int $profileId): ?array
    {
        $profiles = $this->getAuthorProfiles();
        foreach ($profiles as $profile) {
            if ((int) $profile['id'] === $profileId) {
                return $profile;
            }
        }

        return 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);
    }

    /**
     * Save a message to the database
     */
    private function saveMessage(
        int $sessionId,
        string $role,
        string $content,
        string $model,
        ?int $tokensInput = null,
        ?int $tokensOutput = null,
        array $sources = [],
        ?float $startMicrotime = null,
        ?float $endMicrotime = null,
        ?int $authorProfileId = null,
        ?int $systemPromptId = null,
        ?string $collection = null,
        ?int $contextLimit = null
    ): void {
        $stmt = $this->db->prepare(
            'INSERT INTO chat_messages
             (session_id, role, content, model, tokens_input, tokens_output, sources,
              start_microtime, end_microtime, author_profile_id, system_prompt_id, collection, context_limit)
             VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
        );
        $stmt->execute([
            $sessionId,
            $role,
            $content,
            $model,
            $tokensInput,
            $tokensOutput,
            $sources !== [] ? json_encode($sources) : null,
            $startMicrotime,
            $endMicrotime,
            $authorProfileId,
            $systemPromptId,
            $collection,
            $contextLimit,
        ]);

        // Update session timestamp
        $this->db->prepare('UPDATE chat_sessions SET updated_at = NOW() WHERE id = ?')
            ->execute([$sessionId]);
    }

    /**
     * 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
     * Format: claude-* or ollama:*
     */
    private function validateModel(string $model): string
    {
        if (str_starts_with($model, 'claude-') || str_starts_with($model, 'ollama:')) {
            return $model;
        }

        return 'claude-opus-4-5-20251101';
    }

    /**
     * Get available Qdrant collections (cached per request)
     *
     * @return array<string> List of collection names
     */
    private function getAvailableCollections(): array
    {
        if ($this->collectionsCache === null) {
            $this->collectionsCache = $this->qdrantService->listCollections();

            // Fallback if Qdrant is unavailable
            if ($this->collectionsCache === []) {
                $this->collectionsCache = ['documents'];
            }
        }

        return $this->collectionsCache;
    }

    /**
     * Validate collection parameter against available collections
     */
    private function validateCollection(string $collection): string
    {
        $availableCollections = $this->getAvailableCollections();

        return in_array($collection, $availableCollections, true)
            ? $collection
            : ($availableCollections[0] ?? 'documents');
    }

    /**
     * Validate context limit parameter against allowed values

... (287 weitere Zeilen)

Vollständig herunterladen

Aktionen

Herunterladen

Andere Versionen dieser Datei

ID Version Typ Größe Datum
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