Protokoll #2137

ID2137
Zeitstempel2025-12-21 10:03:22.610579
Clientroot
IP145.224.96.190
Modellclaude-sonnet-4-20250514
Statuscompleted
Tokens9,991 (Input: 2,510, Output: 7,481)
Dauer90 ms
Request-Zeit2025-12-21 10:03:22.610579
Response-Zeit2025-12-21 10:03:22.700814

Request

{
    "event": "PreToolUse",
    "tool_name": "Edit",
    "tool_input": {
        "file_path": "\/var\/www\/dev.campus.systemische-tools.de\/docs\/plan-form-components-refactoring.md",
        "old_string": "### 0.7 Migrations-Schritte (LLM-Verwaltung)\n\n1. [ ] Tabelle `llm_models` in `ki_dev` erstellen\n2. [ ] `LlmModelRepository` erstellen\n3. [ ] `LlmSyncService` erstellen (Anthropic + Ollama)\n4. [ ] Route `\/ressourcen\/llm` anlegen\n5. [ ] `RessourcenController::llmIndex()` implementieren\n6. [ ] View `\/ressourcen\/llm\/index.php` erstellen\n7. [ ] Sync-Buttons implementieren\n8. [ ] Bearbeiten-Funktionalität\n9. [ ] Initial-Sync durchführen (bestehende Modelle importieren)\n10. [ ] `ModelConfig.php` durch `LlmModelRepository` ersetzen\n11. [ ] Partial `model-select.php` erstellen\n12. [ ] Chat und Content auf Partial umstellen\n\n---\n\n## 1. IST-Analyse",
        "new_string": "### 0.7 Migrations-Schritte (LLM-Verwaltung)\n\n1. [ ] Tabelle `llm_models` in `ki_dev` erstellen\n2. [ ] `LlmModelRepository` erstellen\n3. [ ] `LlmSyncService` erstellen (Anthropic + Ollama)\n4. [ ] Route `\/ressourcen\/llm` anlegen\n5. [ ] `RessourcenController::llmIndex()` implementieren\n6. [ ] View `\/ressourcen\/llm\/index.php` erstellen\n7. [ ] Sync-Buttons implementieren\n8. [ ] Bearbeiten-Funktionalität\n9. [ ] Initial-Sync durchführen (bestehende Modelle importieren)\n10. [ ] `ModelConfig.php` durch `LlmModelRepository` ersetzen\n11. [ ] Partial `model-select.php` erstellen\n12. [ ] Chat und Content auf Partial umstellen\n\n---\n\n## 0b. Collection-Verwaltung (Grundlage für Collection-Dropdown)\n\n### 0b.1 Architektur-Entscheidung\n\n**Gewählt: DB-Tabelle analog zu LLMs**\n\n- Zentrale Admin-Seite `\/ressourcen\/collections`\n- Datenbank-Tabelle als Single Source of Truth\n- Sync mit Qdrant (Metadaten abrufen)\n- Sprechende Namen und Beschreibungen\n- **Multi-Select** als einheitliche Darstellung\n\n### 0b.2 Datenbank-Tabelle `rag_collections`\n\n```sql\nCREATE TABLE rag_collections (\n    id INT AUTO_INCREMENT PRIMARY KEY,\n    collection_id VARCHAR(100) NOT NULL UNIQUE,  -- Qdrant-Name: \"documents\"\n    display_name VARCHAR(100) NOT NULL,          -- Anzeige: \"Dokumente\"\n    description TEXT,                            -- \"PDF-Dokumente aus Nextcloud\"\n\n    -- Qdrant-Metadaten (via Sync)\n    vector_size INT,                             -- 1024\n    distance_metric VARCHAR(20),                 -- \"Cosine\"\n    points_count INT DEFAULT 0,                  -- Anzahl Vektoren\n\n    -- Konfiguration\n    embedding_model VARCHAR(100),                -- \"mxbai-embed-large\"\n    chunk_size INT,                              -- 2000\n    chunk_overlap INT,                           -- 200\n\n    -- Verwaltung\n    source_type ENUM('nextcloud', 'mail', 'manual', 'system') DEFAULT 'manual',\n    source_path VARCHAR(500),                    -- \"\/var\/www\/nextcloud\/data\/...\"\n    is_active BOOLEAN DEFAULT TRUE,              -- In Dropdowns anzeigen?\n    is_searchable BOOLEAN DEFAULT TRUE,          -- Für RAG verfügbar?\n    sort_order INT DEFAULT 0,\n\n    -- Timestamps\n    last_synced_at DATETIME,\n    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;\n```\n\n### 0b.3 Impact-Analyse: Wer nutzt Collections?\n\n| System\/Seite | Aktueller Zugriff | Neuer Zugriff | Änderung |\n|--------------|-------------------|---------------|----------|\n| **Chat** | `$qdrantService->listCollections()` | `CollectionRepository::getActive()` | Query aus DB |\n| **Content Studio** | `$qdrantService->listCollections()` | `CollectionRepository::getActive()` | Query aus DB |\n| **Content Show** | `$qdrantService->listCollections()` | `CollectionRepository::getActive()` | Query aus DB |\n| **Pipeline (Python)** | `config.py QDRANT_COLLECTIONS` | DB-Query oder Config-Sync | Python liest DB |\n| **Semantic Explorer** | Direkt Qdrant | `CollectionRepository` + Qdrant | Metadaten aus DB |\n| **System Explorer** | - | Statistiken aus DB | Neu |\n| **API \/api\/v1\/search** | Qdrant direkt | Validierung gegen DB | Nur aktive Collections |\n| **Embedding-Service** | `config.py` | DB oder Sync | Konsistenz |\n\n### 0b.4 Admin-Seite `\/ressourcen\/collections`\n\n**URL:** `\/ressourcen\/collections`\n**Controller:** `RessourcenController::collectionsIndex()`\n\n**Funktionen:**\n\n| Button | Aktion |\n|--------|--------|\n| \"Qdrant synchronisieren\" | Collections + Metadaten von Qdrant abrufen |\n| \"Neue Collection\" | Manuell Collection registrieren |\n\n**Tabellen-Ansicht:**\n\n| Collection-ID | Display-Name | Vektoren | Größe | Quelle | Aktiv | Suchbar | Aktionen |\n|---------------|--------------|----------|-------|--------|-------|---------|----------|\n| documents | Dokumente | 1.247 | 1024d | Nextcloud | ✓ | ✓ | Bearbeiten |\n| dokumentation_chunks | Doku-Chunks | 892 | 1024d | System | ✓ | ✓ | Bearbeiten |\n| mail | E-Mails | 3.421 | 1024d | Mail | ✓ | ✓ | Bearbeiten |\n| entities | Entitäten | 156 | 1024d | System | ✓ | - | Bearbeiten |\n\n**Bearbeiten-Dialog:**\n- Display-Name ändern\n- Beschreibung hinzufügen\n- Quelle\/Pfad dokumentieren\n- Aktivieren\/Deaktivieren (Dropdown)\n- Suchbar ja\/nein (RAG-Verfügbarkeit)\n- Sortierung ändern\n\n### 0b.5 Sync-Logik\n\n**Qdrant Sync:**\n```php\n\/\/ QdrantService::getCollectionsInfo()\npublic function syncCollections(): array\n{\n    $qdrantCollections = $this->client->getCollections();\n\n    foreach ($qdrantCollections as $col) {\n        $info = $this->client->getCollectionInfo($col['name']);\n\n        \/\/ INSERT ... ON DUPLICATE KEY UPDATE\n        $this->repository->upsert([\n            'collection_id' => $col['name'],\n            'display_name' => $col['name'],  \/\/ Initial = ID\n            'vector_size' => $info['config']['params']['size'],\n            'distance_metric' => $info['config']['params']['distance'],\n            'points_count' => $info['points_count'],\n            'last_synced_at' => now(),\n        ]);\n    }\n}\n```\n\n### 0b.6 Repository-Interface\n\n```php\n\/\/ CollectionRepository.php\ninterface CollectionRepositoryInterface\n{\n    \/\/ Für Dropdowns\n    public function getActive(): array;\n    public function getSearchable(): array;\n\n    \/\/ Für Admin\n    public function findAll(): array;\n    public function findById(int $id): ?array;\n    public function findByCollectionId(string $collectionId): ?array;\n\n    \/\/ Für Sync\n    public function upsert(array $data): void;\n    public function updateMetadata(string $collectionId, array $metadata): void;\n\n    \/\/ Validierung\n    public function isActive(string $collectionId): bool;\n    public function isSearchable(string $collectionId): bool;\n}\n```\n\n### 0b.7 Dropdown-Query\n\n```php\n\/\/ CollectionRepository::getSearchable()\npublic function getSearchable(): array\n{\n    return $this->db->query(\"\n        SELECT collection_id, display_name, points_count\n        FROM rag_collections\n        WHERE is_active = 1 AND is_searchable = 1\n        ORDER BY sort_order ASC, display_name ASC\n    \")->fetchAll();\n}\n```\n\n### 0b.8 Partial: collections-select.php\n\n```php\n\/\/ \/src\/View\/partials\/form\/collections-select.php\n<?php\n$collections = $collections ?? [];\n$selected = $selected ?? [];\nif (is_string($selected)) {\n    $selected = json_decode($selected, true) ?: [$selected];\n}\n$name = $name ?? 'collections[]';\n$variant = $variant ?? 'default';\n$class = $variant === 'inline' ? 'form-select--inline' : 'form-select';\n?>\n<select name=\"<?= $name ?>\" multiple class=\"<?= $class ?> collections-select\"\n        title=\"Collections für RAG auswählen\">\n    <?php foreach ($collections as $col): ?>\n    <option value=\"<?= htmlspecialchars($col['collection_id']) ?>\"\n            <?= in_array($col['collection_id'], $selected, true) ? 'selected' : '' ?>\n            title=\"<?= number_format($col['points_count']) ?> Dokumente\">\n        <?= htmlspecialchars($col['display_name']) ?>\n    <\/option>\n    <?php endforeach; ?>\n<\/select>\n```\n\n### 0b.9 Python-Pipeline Integration\n\n**Option A: Python liest direkt aus DB**\n```python\n# db.py\ndef get_active_collections():\n    cursor = db.execute(\"\"\"\n        SELECT collection_id, vector_size, embedding_model\n        FROM rag_collections\n        WHERE is_active = 1\n    \"\"\")\n    return cursor.fetchall()\n```\n\n**Option B: Config-Sync via Cron**\n```bash\n# \/opt\/scripts\/sync-collections-config.py\n# Liest aus DB, schreibt config.py\n```\n\n**Empfehlung:** Option A - direkte DB-Abfrage für Konsistenz.\n\n### 0b.10 Migrations-Schritte (Collection-Verwaltung)\n\n1. [ ] Tabelle `rag_collections` in `ki_dev` erstellen\n2. [ ] `CollectionRepository` erstellen\n3. [ ] `QdrantService::syncCollections()` implementieren\n4. [ ] Route `\/ressourcen\/collections` anlegen\n5. [ ] `RessourcenController::collectionsIndex()` implementieren\n6. [ ] View `\/ressourcen\/collections\/index.php` erstellen\n7. [ ] Sync-Button implementieren\n8. [ ] Bearbeiten-Funktionalität\n9. [ ] Initial-Sync durchführen\n10. [ ] `ChatController` auf `CollectionRepository` umstellen\n11. [ ] `ContentController` auf `CollectionRepository` umstellen\n12. [ ] Partial `collections-select.php` erstellen\n13. [ ] Chat und Content auf Partial umstellen\n14. [ ] Python-Pipeline auf DB-Query umstellen\n15. [ ] Semantic Explorer auf Repository umstellen\n\n---\n\n## 0c. Ressourcen-Übersicht\n\n### Navigationsstruktur\n\n```\n\/ressourcen\n├── \/llm           → LLM-Modelle verwalten\n├── \/collections   → RAG-Collections verwalten\n└── (später weitere: \/embeddings, \/prompts, ...)\n```\n\n### Gemeinsame Patterns\n\n| Aspekt | LLMs | Collections |\n|--------|------|-------------|\n| Tabelle | `llm_models` | `rag_collections` |\n| Repository | `LlmModelRepository` | `CollectionRepository` |\n| Sync-Service | `LlmSyncService` | `QdrantService` |\n| Admin-URL | `\/ressourcen\/llm` | `\/ressourcen\/collections` |\n| Partial | `model-select.php` | `collections-select.php` |\n\n---\n\n## 1. IST-Analyse"
    }
}

Response

{
    "tool_response": {
        "filePath": "\/var\/www\/dev.campus.systemische-tools.de\/docs\/plan-form-components-refactoring.md",
        "oldString": "### 0.7 Migrations-Schritte (LLM-Verwaltung)\n\n1. [ ] Tabelle `llm_models` in `ki_dev` erstellen\n2. [ ] `LlmModelRepository` erstellen\n3. [ ] `LlmSyncService` erstellen (Anthropic + Ollama)\n4. [ ] Route `\/ressourcen\/llm` anlegen\n5. [ ] `RessourcenController::llmIndex()` implementieren\n6. [ ] View `\/ressourcen\/llm\/index.php` erstellen\n7. [ ] Sync-Buttons implementieren\n8. [ ] Bearbeiten-Funktionalität\n9. [ ] Initial-Sync durchführen (bestehende Modelle importieren)\n10. [ ] `ModelConfig.php` durch `LlmModelRepository` ersetzen\n11. [ ] Partial `model-select.php` erstellen\n12. [ ] Chat und Content auf Partial umstellen\n\n---\n\n## 1. IST-Analyse",
        "newString": "### 0.7 Migrations-Schritte (LLM-Verwaltung)\n\n1. [ ] Tabelle `llm_models` in `ki_dev` erstellen\n2. [ ] `LlmModelRepository` erstellen\n3. [ ] `LlmSyncService` erstellen (Anthropic + Ollama)\n4. [ ] Route `\/ressourcen\/llm` anlegen\n5. [ ] `RessourcenController::llmIndex()` implementieren\n6. [ ] View `\/ressourcen\/llm\/index.php` erstellen\n7. [ ] Sync-Buttons implementieren\n8. [ ] Bearbeiten-Funktionalität\n9. [ ] Initial-Sync durchführen (bestehende Modelle importieren)\n10. [ ] `ModelConfig.php` durch `LlmModelRepository` ersetzen\n11. [ ] Partial `model-select.php` erstellen\n12. [ ] Chat und Content auf Partial umstellen\n\n---\n\n## 0b. Collection-Verwaltung (Grundlage für Collection-Dropdown)\n\n### 0b.1 Architektur-Entscheidung\n\n**Gewählt: DB-Tabelle analog zu LLMs**\n\n- Zentrale Admin-Seite `\/ressourcen\/collections`\n- Datenbank-Tabelle als Single Source of Truth\n- Sync mit Qdrant (Metadaten abrufen)\n- Sprechende Namen und Beschreibungen\n- **Multi-Select** als einheitliche Darstellung\n\n### 0b.2 Datenbank-Tabelle `rag_collections`\n\n```sql\nCREATE TABLE rag_collections (\n    id INT AUTO_INCREMENT PRIMARY KEY,\n    collection_id VARCHAR(100) NOT NULL UNIQUE,  -- Qdrant-Name: \"documents\"\n    display_name VARCHAR(100) NOT NULL,          -- Anzeige: \"Dokumente\"\n    description TEXT,                            -- \"PDF-Dokumente aus Nextcloud\"\n\n    -- Qdrant-Metadaten (via Sync)\n    vector_size INT,                             -- 1024\n    distance_metric VARCHAR(20),                 -- \"Cosine\"\n    points_count INT DEFAULT 0,                  -- Anzahl Vektoren\n\n    -- Konfiguration\n    embedding_model VARCHAR(100),                -- \"mxbai-embed-large\"\n    chunk_size INT,                              -- 2000\n    chunk_overlap INT,                           -- 200\n\n    -- Verwaltung\n    source_type ENUM('nextcloud', 'mail', 'manual', 'system') DEFAULT 'manual',\n    source_path VARCHAR(500),                    -- \"\/var\/www\/nextcloud\/data\/...\"\n    is_active BOOLEAN DEFAULT TRUE,              -- In Dropdowns anzeigen?\n    is_searchable BOOLEAN DEFAULT TRUE,          -- Für RAG verfügbar?\n    sort_order INT DEFAULT 0,\n\n    -- Timestamps\n    last_synced_at DATETIME,\n    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;\n```\n\n### 0b.3 Impact-Analyse: Wer nutzt Collections?\n\n| System\/Seite | Aktueller Zugriff | Neuer Zugriff | Änderung |\n|--------------|-------------------|---------------|----------|\n| **Chat** | `$qdrantService->listCollections()` | `CollectionRepository::getActive()` | Query aus DB |\n| **Content Studio** | `$qdrantService->listCollections()` | `CollectionRepository::getActive()` | Query aus DB |\n| **Content Show** | `$qdrantService->listCollections()` | `CollectionRepository::getActive()` | Query aus DB |\n| **Pipeline (Python)** | `config.py QDRANT_COLLECTIONS` | DB-Query oder Config-Sync | Python liest DB |\n| **Semantic Explorer** | Direkt Qdrant | `CollectionRepository` + Qdrant | Metadaten aus DB |\n| **System Explorer** | - | Statistiken aus DB | Neu |\n| **API \/api\/v1\/search** | Qdrant direkt | Validierung gegen DB | Nur aktive Collections |\n| **Embedding-Service** | `config.py` | DB oder Sync | Konsistenz |\n\n### 0b.4 Admin-Seite `\/ressourcen\/collections`\n\n**URL:** `\/ressourcen\/collections`\n**Controller:** `RessourcenController::collectionsIndex()`\n\n**Funktionen:**\n\n| Button | Aktion |\n|--------|--------|\n| \"Qdrant synchronisieren\" | Collections + Metadaten von Qdrant abrufen |\n| \"Neue Collection\" | Manuell Collection registrieren |\n\n**Tabellen-Ansicht:**\n\n| Collection-ID | Display-Name | Vektoren | Größe | Quelle | Aktiv | Suchbar | Aktionen |\n|---------------|--------------|----------|-------|--------|-------|---------|----------|\n| documents | Dokumente | 1.247 | 1024d | Nextcloud | ✓ | ✓ | Bearbeiten |\n| dokumentation_chunks | Doku-Chunks | 892 | 1024d | System | ✓ | ✓ | Bearbeiten |\n| mail | E-Mails | 3.421 | 1024d | Mail | ✓ | ✓ | Bearbeiten |\n| entities | Entitäten | 156 | 1024d | System | ✓ | - | Bearbeiten |\n\n**Bearbeiten-Dialog:**\n- Display-Name ändern\n- Beschreibung hinzufügen\n- Quelle\/Pfad dokumentieren\n- Aktivieren\/Deaktivieren (Dropdown)\n- Suchbar ja\/nein (RAG-Verfügbarkeit)\n- Sortierung ändern\n\n### 0b.5 Sync-Logik\n\n**Qdrant Sync:**\n```php\n\/\/ QdrantService::getCollectionsInfo()\npublic function syncCollections(): array\n{\n    $qdrantCollections = $this->client->getCollections();\n\n    foreach ($qdrantCollections as $col) {\n        $info = $this->client->getCollectionInfo($col['name']);\n\n        \/\/ INSERT ... ON DUPLICATE KEY UPDATE\n        $this->repository->upsert([\n            'collection_id' => $col['name'],\n            'display_name' => $col['name'],  \/\/ Initial = ID\n            'vector_size' => $info['config']['params']['size'],\n            'distance_metric' => $info['config']['params']['distance'],\n            'points_count' => $info['points_count'],\n            'last_synced_at' => now(),\n        ]);\n    }\n}\n```\n\n### 0b.6 Repository-Interface\n\n```php\n\/\/ CollectionRepository.php\ninterface CollectionRepositoryInterface\n{\n    \/\/ Für Dropdowns\n    public function getActive(): array;\n    public function getSearchable(): array;\n\n    \/\/ Für Admin\n    public function findAll(): array;\n    public function findById(int $id): ?array;\n    public function findByCollectionId(string $collectionId): ?array;\n\n    \/\/ Für Sync\n    public function upsert(array $data): void;\n    public function updateMetadata(string $collectionId, array $metadata): void;\n\n    \/\/ Validierung\n    public function isActive(string $collectionId): bool;\n    public function isSearchable(string $collectionId): bool;\n}\n```\n\n### 0b.7 Dropdown-Query\n\n```php\n\/\/ CollectionRepository::getSearchable()\npublic function getSearchable(): array\n{\n    return $this->db->query(\"\n        SELECT collection_id, display_name, points_count\n        FROM rag_collections\n        WHERE is_active = 1 AND is_searchable = 1\n        ORDER BY sort_order ASC, display_name ASC\n    \")->fetchAll();\n}\n```\n\n### 0b.8 Partial: collections-select.php\n\n```php\n\/\/ \/src\/View\/partials\/form\/collections-select.php\n<?php\n$collections = $collections ?? [];\n$selected = $selected ?? [];\nif (is_string($selected)) {\n    $selected = json_decode($selected, true) ?: [$selected];\n}\n$name = $name ?? 'collections[]';\n$variant = $variant ?? 'default';\n$class = $variant === 'inline' ? 'form-select--inline' : 'form-select';\n?>\n<select name=\"<?= $name ?>\" multiple class=\"<?= $class ?> collections-select\"\n        title=\"Collections für RAG auswählen\">\n    <?php foreach ($collections as $col): ?>\n    <option value=\"<?= htmlspecialchars($col['collection_id']) ?>\"\n            <?= in_array($col['collection_id'], $selected, true) ? 'selected' : '' ?>\n            title=\"<?= number_format($col['points_count']) ?> Dokumente\">\n        <?= htmlspecialchars($col['display_name']) ?>\n    <\/option>\n    <?php endforeach; ?>\n<\/select>\n```\n\n### 0b.9 Python-Pipeline Integration\n\n**Option A: Python liest direkt aus DB**\n```python\n# db.py\ndef get_active_collections():\n    cursor = db.execute(\"\"\"\n        SELECT collection_id, vector_size, embedding_model\n        FROM rag_collections\n        WHERE is_active = 1\n    \"\"\")\n    return cursor.fetchall()\n```\n\n**Option B: Config-Sync via Cron**\n```bash\n# \/opt\/scripts\/sync-collections-config.py\n# Liest aus DB, schreibt config.py\n```\n\n**Empfehlung:** Option A - direkte DB-Abfrage für Konsistenz.\n\n### 0b.10 Migrations-Schritte (Collection-Verwaltung)\n\n1. [ ] Tabelle `rag_collections` in `ki_dev` erstellen\n2. [ ] `CollectionRepository` erstellen\n3. [ ] `QdrantService::syncCollections()` implementieren\n4. [ ] Route `\/ressourcen\/collections` anlegen\n5. [ ] `RessourcenController::collectionsIndex()` implementieren\n6. [ ] View `\/ressourcen\/collections\/index.php` erstellen\n7. [ ] Sync-Button implementieren\n8. [ ] Bearbeiten-Funktionalität\n9. [ ] Initial-Sync durchführen\n10. [ ] `ChatController` auf `CollectionRepository` umstellen\n11. [ ] `ContentController` auf `CollectionRepository` umstellen\n12. [ ] Partial `collections-select.php` erstellen\n13. [ ] Chat und Content auf Partial umstellen\n14. [ ] Python-Pipeline auf DB-Query umstellen\n15. [ ] Semantic Explorer auf Repository umstellen\n\n---\n\n## 0c. Ressourcen-Übersicht\n\n### Navigationsstruktur\n\n```\n\/ressourcen\n├── \/llm           → LLM-Modelle verwalten\n├── \/collections   → RAG-Collections verwalten\n└── (später weitere: \/embeddings, \/prompts, ...)\n```\n\n### Gemeinsame Patterns\n\n| Aspekt | LLMs | Collections |\n|--------|------|-------------|\n| Tabelle | `llm_models` | `rag_collections` |\n| Repository | `LlmModelRepository` | `CollectionRepository` |\n| Sync-Service | `LlmSyncService` | `QdrantService` |\n| Admin-URL | `\/ressourcen\/llm` | `\/ressourcen\/collections` |\n| Partial | `model-select.php` | `collections-select.php` |\n\n---\n\n## 1. IST-Analyse",
        "originalFile": "# Plan: Einheitliche Form-Komponenten (Chat & Content Studio)\n\n## Status: PLANUNG\n**Erstellt:** 2025-12-21\n**Ziel:** DRY, KISS, SRP - Shared Partials für alle wiederverwendbaren Form-Elemente\n\n---\n\n## 0. LLM-Verwaltung (Grundlage für Modell-Dropdown)\n\n### 0.1 Architektur-Entscheidung\n\n**Gewählt: Variante B - Dedizierte LLM-Verwaltung**\n\nStatt dynamischer API-Abfrage bei jedem Seitenaufruf:\n- Zentrale Admin-Seite `\/ressourcen\/llm`\n- Datenbank-Tabelle als Single Source of Truth\n- Sync-Buttons für Provider (Anthropic, Ollama, weitere)\n- Sprechende Namen und Zusatzmetadaten\n\n### 0.2 Datenbank-Tabelle `llm_models`\n\n```sql\nCREATE TABLE llm_models (\n    id INT AUTO_INCREMENT PRIMARY KEY,\n    provider ENUM('anthropic', 'ollama', 'openai', 'google', 'mistral', 'custom') NOT NULL,\n    model_id VARCHAR(100) NOT NULL,        -- API-ID: \"claude-opus-4-5-20251101\"\n    display_name VARCHAR(100) NOT NULL,    -- Anzeige: \"Claude Opus 4.5\"\n    description TEXT,                      -- Kurzbeschreibung\n    context_window INT,                    -- Max. Tokens Input: 200000\n    max_output_tokens INT,                 -- Max. Tokens Output: 8192\n    input_price_per_mtok DECIMAL(10,4),    -- Preis Input $\/MTok\n    output_price_per_mtok DECIMAL(10,4),   -- Preis Output $\/MTok\n    capabilities JSON,                     -- {\"vision\": true, \"function_calling\": true}\n    is_local BOOLEAN DEFAULT FALSE,        -- Lokal (Ollama) vs. Cloud\n    is_active BOOLEAN DEFAULT TRUE,        -- In Dropdowns anzeigen?\n    sort_order INT DEFAULT 0,              -- Reihenfolge in Dropdowns\n    last_synced_at DATETIME,               -- Letzte Synchronisation\n    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,\n    UNIQUE KEY unique_provider_model (provider, model_id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;\n```\n\n### 0.3 Admin-Seite `\/ressourcen\/llm`\n\n**URL:** `\/ressourcen\/llm`\n**Controller:** `RessourcenController::llmIndex()`\n\n**Funktionen:**\n\n| Button | Aktion |\n|--------|--------|\n| \"Anthropic synchronisieren\" | `GET api.anthropic.com\/v1\/models` → DB aktualisieren |\n| \"Ollama synchronisieren\" | `ollama list` → DB aktualisieren |\n| \"Neuer Provider\" | Manuell weiteren Provider hinzufügen |\n\n**Tabellen-Ansicht:**\n\n| Provider | Model-ID | Display-Name | Context | Preis In\/Out | Lokal | Aktiv | Aktionen |\n|----------|----------|--------------|---------|--------------|-------|-------|----------|\n| Anthropic | claude-opus-4-5-20251101 | Claude Opus 4.5 | 200k | $15\/$75 | - | ✓ | Bearbeiten |\n| Ollama | mistral:latest | Mistral 7B | 32k | - | ✓ | ✓ | Bearbeiten |\n\n**Bearbeiten-Dialog:**\n- Display-Name ändern\n- Beschreibung hinzufügen\n- Context-Window \/ Max-Output korrigieren\n- Preise eintragen (für Kostenberechnung)\n- Aktivieren\/Deaktivieren\n- Sortierung ändern\n\n### 0.4 Sync-Logik\n\n**Anthropic Sync:**\n```php\n\/\/ GET https:\/\/api.anthropic.com\/v1\/models\n\/\/ Response: {\"data\": [{\"id\": \"claude-opus-4-5-20251101\", ...}]}\n\nforeach ($apiModels as $model) {\n    \/\/ INSERT ... ON DUPLICATE KEY UPDATE\n    \/\/ Neue Modelle: is_active = true, display_name = model_id (initial)\n    \/\/ Existierende: last_synced_at aktualisieren\n    \/\/ Fehlende: NICHT löschen, nur last_synced_at bleibt alt\n}\n```\n\n**Ollama Sync:**\n```php\n\/\/ $ ollama list\n\/\/ NAME              ID           SIZE    MODIFIED\n\/\/ mistral:latest    abc123...    4.1 GB  2 days ago\n\n$output = shell_exec('ollama list');\n\/\/ Parsen und in DB einfügen\n```\n\n### 0.5 Dropdown-Query\n\n```php\n\/\/ ModelService::getActiveModels()\npublic function getActiveModels(): array\n{\n    return $this->db->query(\"\n        SELECT model_id, display_name, provider, is_local, context_window\n        FROM llm_models\n        WHERE is_active = 1\n        ORDER BY is_local ASC, sort_order ASC, display_name ASC\n    \")->fetchAll();\n}\n```\n\n### 0.6 Partial nutzt DB-Daten\n\n```php\n\/\/ \/src\/View\/partials\/form\/model-select.php\n<?php\n$models = $models ?? [];\n$selected = $selected ?? '';\n$variant = $variant ?? 'default';\n$class = $variant === 'inline' ? 'form-select--inline' : 'form-select';\n?>\n<select name=\"model\" id=\"model\" class=\"<?= $class ?>\">\n    <optgroup label=\"Cloud\">\n        <?php foreach ($models as $m): ?>\n        <?php if (!$m['is_local']): ?>\n        <option value=\"<?= $m['model_id'] ?>\" <?= $selected === $m['model_id'] ? 'selected' : '' ?>>\n            <?= htmlspecialchars($m['display_name']) ?>\n        <\/option>\n        <?php endif; ?>\n        <?php endforeach; ?>\n    <\/optgroup>\n    <optgroup label=\"Lokal\">\n        <?php foreach ($models as $m): ?>\n        <?php if ($m['is_local']): ?>\n        <option value=\"<?= $m['model_id'] ?>\" <?= $selected === $m['model_id'] ? 'selected' : '' ?>>\n            <?= htmlspecialchars($m['display_name']) ?>\n        <\/option>\n        <?php endif; ?>\n        <?php endforeach; ?>\n    <\/optgroup>\n<\/select>\n```\n\n### 0.7 Migrations-Schritte (LLM-Verwaltung)\n\n1. [ ] Tabelle `llm_models` in `ki_dev` erstellen\n2. [ ] `LlmModelRepository` erstellen\n3. [ ] `LlmSyncService` erstellen (Anthropic + Ollama)\n4. [ ] Route `\/ressourcen\/llm` anlegen\n5. [ ] `RessourcenController::llmIndex()` implementieren\n6. [ ] View `\/ressourcen\/llm\/index.php` erstellen\n7. [ ] Sync-Buttons implementieren\n8. [ ] Bearbeiten-Funktionalität\n9. [ ] Initial-Sync durchführen (bestehende Modelle importieren)\n10. [ ] `ModelConfig.php` durch `LlmModelRepository` ersetzen\n11. [ ] Partial `model-select.php` erstellen\n12. [ ] Chat und Content auf Partial umstellen\n\n---\n\n## 1. IST-Analyse\n\n### 1.1 Betroffene Views\n\n| View | Pfad | Verwendung |\n|------|------|------------|\n| Chat | `\/src\/View\/chat\/index.php` | Session-basierter Chat mit Einstellungen |\n| Content New | `\/src\/View\/content\/new.php` | Neuer Content-Auftrag erstellen |\n| Content Show | `\/src\/View\/content\/show.php` | Content-Details mit Generierungs-Optionen |\n\n### 1.2 Gemeinsame Elemente (aktuell inkonsistent)\n\n| Element | Chat | Content New | Content Show |\n|---------|------|-------------|--------------|\n| Modell-Dropdown | ✓ (optgroups) | ✓ (flach) | ✓ (inline) |\n| Collections | ✓ (multi-select) | ✓ (checkboxes) | ✓ (single-select) |\n| Context Limit | ✓ | ✓ | ✓ |\n| Author Profile | ✓ | ✓ | - |\n| System Prompt | ✓ | - | - |\n| Temperature | ✓ | - | - |\n| Max Tokens | ✓ | - | - |\n| Contract | - | ✓ | - |\n| Structure | - | ✓ | - |\n\n### 1.3 Detaillierte Inkonsistenzen\n\n#### Modell-Dropdown\n```\nChat:\n- <select name=\"model\"> (keine Klasse)\n- <optgroup label=\"Anthropic\"> \/ <optgroup label=\"Ollama (lokal)\">\n- Default aus $defaultModel\n\nContent New:\n- <select id=\"model\" name=\"model\" class=\"form-select\">\n- Flache Liste ohne Gruppierung\n- Default hardcoded: 'claude-sonnet-4-20250514'\n\nContent Show:\n- <select name=\"model\" class=\"form-select--inline\">\n- Flache Liste\n- Default aus $orderModel\n```\n\n#### Collections\n```\nChat:\n- <select name=\"collections[]\" multiple class=\"collections-select\">\n- Label-Mapping via match(): 'documents' → 'Dokumente'\n\nContent New:\n- <input type=\"checkbox\" name=\"collections[]\">\n- Kein Label-Mapping, zeigt rohe Namen\n\nContent Show:\n- <select name=\"collection\" class=\"form-select--inline\"> (SINGLE, nicht multi!)\n- Kein Label-Mapping\n```\n\n#### Context Limit\n```\nChat:\n- <select name=\"context_limit\"> (keine Klasse, kein ID)\n\nContent New:\n- <select id=\"context_limit\" name=\"context_limit\" class=\"form-select\">\n- Mit Label \"Kontext-Quellen\"\n\nContent Show:\n- <select name=\"context_limit\" class=\"form-select--inline\">\n```\n\n#### Author Profile\n```\nChat:\n- Variable: $authorProfiles\n- Default: value=\"0\", Text: \"Kein Profil\"\n\nContent New:\n- Variable: $profiles\n- Default: value=\"\", Text: \"-- Kein Profil --\"\n```\n\n---\n\n## 2. SOLL-Architektur\n\n### 2.1 Verzeichnisstruktur\n\n```\n\/src\/View\/\n├── partials\/\n│   └── form\/\n│       ├── model-select.php      # Modell-Dropdown (mit Optgroups)\n│       ├── collections.php       # Collections (einheitlich)\n│       ├── context-limit.php     # Context Limit\n│       ├── author-profile.php    # Autorenprofil\n│       ├── system-prompt.php     # System Prompt\n│       ├── temperature.php       # Temperature Slider\n│       ├── max-tokens.php        # Max Tokens\n│       ├── contract.php          # Contract\n│       └── structure.php         # Struktur\n├── chat\/\n│   └── index.php                 # Nutzt Partials\n└── content\/\n    ├── new.php                   # Nutzt Partials\n    └── show.php                  # Nutzt Partials\n```\n\n### 2.2 Partial-Interface (Konvention)\n\nJedes Partial akzeptiert ein `$options`-Array:\n\n```php\n<?php\n\/**\n * @param array $options [\n *     'models'    => array,      \/\/ ModelConfig::getAll()\n *     'selected'  => string,     \/\/ Aktuell gewählter Wert\n *     'name'      => string,     \/\/ Form-Name (default: 'model')\n *     'id'        => string,     \/\/ HTML-ID (optional)\n *     'class'     => string,     \/\/ CSS-Klasse (default: 'form-select')\n *     'variant'   => string,     \/\/ 'default' | 'inline' | 'compact'\n *     'label'     => string,     \/\/ Label-Text (optional)\n *     'required'  => bool,       \/\/ Required-Attribut\n * ]\n *\/\n```\n\n### 2.3 Einheitliche Variablennamen\n\n| Element | Variable (Controller) | Variable (View) |\n|---------|----------------------|-----------------|\n| Modelle | `ModelConfig::getAll()` | `$models` |\n| Default-Modell | `ModelConfig::DEFAULT_MODEL` | `$defaultModel` |\n| Collections | `$qdrantService->listCollections()` | `$collections` |\n| Author Profiles | `findByType('author_profile')` | `$authorProfiles` |\n| System Prompts | `findByType('system_prompt')` | `$systemPrompts` |\n| Contracts | `findByType('contract')` | `$contracts` |\n| Structures | `findByType('structure')` | `$structures` |\n\n### 2.4 Collection Label-Mapping (zentral)\n\n```php\n\/\/ \/src\/Infrastructure\/AI\/CollectionConfig.php\nfinal class CollectionConfig\n{\n    public const LABELS = [\n        'documents' => 'Dokumente',\n        'dokumentation_chunks' => 'Doku-Chunks',\n        'mail' => 'E-Mails',\n        'entities' => 'Entitäten',\n    ];\n\n    public static function getLabel(string $collection): string\n... [TRUNCATED-f4a569d0f7d092b0]",
        "structuredPatch": [
            {
                "oldStart": 159,
                "oldLines": 6,
                "newStart": 159,
                "newLines": 249,
                "lines": [
                    " ",
                    " ---",
                    " ",
                    "+## 0b. Collection-Verwaltung (Grundlage für Collection-Dropdown)",
                    "+",
                    "+### 0b.1 Architektur-Entscheidung",
                    "+",
                    "+**Gewählt: DB-Tabelle analog zu LLMs**",
                    "+",
                    "+- Zentrale Admin-Seite `\/ressourcen\/collections`",
                    "+- Datenbank-Tabelle als Single Source of Truth",
                    "+- Sync mit Qdrant (Metadaten abrufen)",
                    "+- Sprechende Namen und Beschreibungen",
                    "+- **Multi-Select** als einheitliche Darstellung",
                    "+",
                    "+### 0b.2 Datenbank-Tabelle `rag_collections`",
                    "+",
                    "+```sql",
                    "+CREATE TABLE rag_collections (",
                    "+    id INT AUTO_INCREMENT PRIMARY KEY,",
                    "+    collection_id VARCHAR(100) NOT NULL UNIQUE,  -- Qdrant-Name: \"documents\"",
                    "+    display_name VARCHAR(100) NOT NULL,          -- Anzeige: \"Dokumente\"",
                    "+    description TEXT,                            -- \"PDF-Dokumente aus Nextcloud\"",
                    "+",
                    "+    -- Qdrant-Metadaten (via Sync)",
                    "+    vector_size INT,                             -- 1024",
                    "+    distance_metric VARCHAR(20),                 -- \"Cosine\"",
                    "+    points_count INT DEFAULT 0,                  -- Anzahl Vektoren",
                    "+",
                    "+    -- Konfiguration",
                    "+    embedding_model VARCHAR(100),                -- \"mxbai-embed-large\"",
                    "+    chunk_size INT,                              -- 2000",
                    "+    chunk_overlap INT,                           -- 200",
                    "+",
                    "+    -- Verwaltung",
                    "+    source_type ENUM('nextcloud', 'mail', 'manual', 'system') DEFAULT 'manual',",
                    "+    source_path VARCHAR(500),                    -- \"\/var\/www\/nextcloud\/data\/...\"",
                    "+    is_active BOOLEAN DEFAULT TRUE,              -- In Dropdowns anzeigen?",
                    "+    is_searchable BOOLEAN DEFAULT TRUE,          -- Für RAG verfügbar?",
                    "+    sort_order INT DEFAULT 0,",
                    "+",
                    "+    -- Timestamps",
                    "+    last_synced_at DATETIME,",
                    "+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,",
                    "+    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP",
                    "+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;",
                    "+```",
                    "+",
                    "+### 0b.3 Impact-Analyse: Wer nutzt Collections?",
                    "+",
                    "+| System\/Seite | Aktueller Zugriff | Neuer Zugriff | Änderung |",
                    "+|--------------|-------------------|---------------|----------|",
                    "+| **Chat** | `$qdrantService->listCollections()` | `CollectionRepository::getActive()` | Query aus DB |",
                    "+| **Content Studio** | `$qdrantService->listCollections()` | `CollectionRepository::getActive()` | Query aus DB |",
                    "+| **Content Show** | `$qdrantService->listCollections()` | `CollectionRepository::getActive()` | Query aus DB |",
                    "+| **Pipeline (Python)** | `config.py QDRANT_COLLECTIONS` | DB-Query oder Config-Sync | Python liest DB |",
                    "+| **Semantic Explorer** | Direkt Qdrant | `CollectionRepository` + Qdrant | Metadaten aus DB |",
                    "+| **System Explorer** | - | Statistiken aus DB | Neu |",
                    "+| **API \/api\/v1\/search** | Qdrant direkt | Validierung gegen DB | Nur aktive Collections |",
                    "+| **Embedding-Service** | `config.py` | DB oder Sync | Konsistenz |",
                    "+",
                    "+### 0b.4 Admin-Seite `\/ressourcen\/collections`",
                    "+",
                    "+**URL:** `\/ressourcen\/collections`",
                    "+**Controller:** `RessourcenController::collectionsIndex()`",
                    "+",
                    "+**Funktionen:**",
                    "+",
                    "+| Button | Aktion |",
                    "+|--------|--------|",
                    "+| \"Qdrant synchronisieren\" | Collections + Metadaten von Qdrant abrufen |",
                    "+| \"Neue Collection\" | Manuell Collection registrieren |",
                    "+",
                    "+**Tabellen-Ansicht:**",
                    "+",
                    "+| Collection-ID | Display-Name | Vektoren | Größe | Quelle | Aktiv | Suchbar | Aktionen |",
                    "+|---------------|--------------|----------|-------|--------|-------|---------|----------|",
                    "+| documents | Dokumente | 1.247 | 1024d | Nextcloud | ✓ | ✓ | Bearbeiten |",
                    "+| dokumentation_chunks | Doku-Chunks | 892 | 1024d | System | ✓ | ✓ | Bearbeiten |",
                    "+| mail | E-Mails | 3.421 | 1024d | Mail | ✓ | ✓ | Bearbeiten |",
                    "+| entities | Entitäten | 156 | 1024d | System | ✓ | - | Bearbeiten |",
                    "+",
                    "+**Bearbeiten-Dialog:**",
                    "+- Display-Name ändern",
                    "+- Beschreibung hinzufügen",
                    "+- Quelle\/Pfad dokumentieren",
                    "+- Aktivieren\/Deaktivieren (Dropdown)",
                    "+- Suchbar ja\/nein (RAG-Verfügbarkeit)",
                    "+- Sortierung ändern",
                    "+",
                    "+### 0b.5 Sync-Logik",
                    "+",
                    "+**Qdrant Sync:**",
                    "+```php",
                    "+\/\/ QdrantService::getCollectionsInfo()",
                    "+public function syncCollections(): array",
                    "+{",
                    "+    $qdrantCollections = $this->client->getCollections();",
                    "+",
                    "+    foreach ($qdrantCollections as $col) {",
                    "+        $info = $this->client->getCollectionInfo($col['name']);",
                    "+",
                    "+        \/\/ INSERT ... ON DUPLICATE KEY UPDATE",
                    "+        $this->repository->upsert([",
                    "+            'collection_id' => $col['name'],",
                    "+            'display_name' => $col['name'],  \/\/ Initial = ID",
                    "+            'vector_size' => $info['config']['params']['size'],",
                    "+            'distance_metric' => $info['config']['params']['distance'],",
                    "+            'points_count' => $info['points_count'],",
                    "+            'last_synced_at' => now(),",
                    "+        ]);",
                    "+    }",
                    "+}",
                    "+```",
                    "+",
                    "+### 0b.6 Repository-Interface",
                    "+",
                    "+```php",
                    "+\/\/ CollectionRepository.php",
                    "+interface CollectionRepositoryInterface",
                    "+{",
                    "+    \/\/ Für Dropdowns",
                    "+    public function getActive(): array;",
                    "+    public function getSearchable(): array;",
                    "+",
                    "+    \/\/ Für Admin",
                    "+    public function findAll(): array;",
                    "+    public function findById(int $id): ?array;",
                    "+    public function findByCollectionId(string $collectionId): ?array;",
                    "+",
                    "+    \/\/ Für Sync",
                    "+    public function upsert(array $data): void;",
                    "+    public function updateMetadata(string $collectionId, array $metadata): void;",
                    "+",
                    "+    \/\/ Validierung",
                    "+    public function isActive(string $collectionId): bool;",
                    "+    public function isSearchable(string $collectionId): bool;",
                    "+}",
                    "+```",
                    "+",
                    "+### 0b.7 Dropdown-Query",
                    "+",
                    "+```php",
                    "+\/\/ CollectionRepository::getSearchable()",
                    "+public function getSearchable(): array",
                    "+{",
                    "+    return $this->db->query(\"",
                    "+        SELECT collection_id, display_name, points_count",
                    "+        FROM rag_collections",
                    "+        WHERE is_active = 1 AND is_searchable = 1",
                    "+        ORDER BY sort_order ASC, display_name ASC",
                    "+    \")->fetchAll();",
                    "+}",
                    "+```",
                    "+",
                    "+### 0b.8 Partial: collections-select.php",
                    "+",
                    "+```php",
                    "+\/\/ \/src\/View\/partials\/form\/collections-select.php",
                    "+<?php",
                    "+$collections = $collections ?? [];",
                    "+$selected = $selected ?? [];",
                    "+if (is_string($selected)) {",
                    "+    $selected = json_decode($selected, true) ?: [$selected];",
                    "+}",
                    "+$name = $name ?? 'collections[]';",
                    "+$variant = $variant ?? 'default';",
                    "+$class = $variant === 'inline' ? 'form-select--inline' : 'form-select';",
                    "+?>",
                    "+<select name=\"<?= $name ?>\" multiple class=\"<?= $class ?> collections-select\"",
                    "+        title=\"Collections für RAG auswählen\">",
                    "+    <?php foreach ($collections as $col): ?>",
                    "+    <option value=\"<?= htmlspecialchars($col['collection_id']) ?>\"",
                    "+            <?= in_array($col['collection_id'], $selected, true) ? 'selected' : '' ?>",
                    "+            title=\"<?= number_format($col['points_count']) ?> Dokumente\">",
                    "+        <?= htmlspecialchars($col['display_name']) ?>",
                    "+    <\/option>",
                    "+    <?php endforeach; ?>",
                    "+<\/select>",
                    "+```",
                    "+",
                    "+### 0b.9 Python-Pipeline Integration",
                    "+",
                    "+**Option A: Python liest direkt aus DB**",
                    "+```python",
                    "+# db.py",
                    "+def get_active_collections():",
                    "+    cursor = db.execute(\"\"\"",
                    "+        SELECT collection_id, vector_size, embedding_model",
                    "+        FROM rag_collections",
                    "+        WHERE is_active = 1",
                    "+    \"\"\")",
                    "+    return cursor.fetchall()",
                    "+```",
                    "+",
                    "+**Option B: Config-Sync via Cron**",
                    "+```bash",
                    "+# \/opt\/scripts\/sync-collections-config.py",
                    "+# Liest aus DB, schreibt config.py",
                    "+```",
                    "+",
                    "+**Empfehlung:** Option A - direkte DB-Abfrage für Konsistenz.",
                    "+",
                    "+### 0b.10 Migrations-Schritte (Collection-Verwaltung)",
                    "+",
                    "+1. [ ] Tabelle `rag_collections` in `ki_dev` erstellen",
                    "+2. [ ] `CollectionRepository` erstellen",
                    "+3. [ ] `QdrantService::syncCollections()` implementieren",
                    "+4. [ ] Route `\/ressourcen\/collections` anlegen",
                    "+5. [ ] `RessourcenController::collectionsIndex()` implementieren",
                    "+6. [ ] View `\/ressourcen\/collections\/index.php` erstellen",
                    "+7. [ ] Sync-Button implementieren",
                    "+8. [ ] Bearbeiten-Funktionalität",
                    "+9. [ ] Initial-Sync durchführen",
                    "+10. [ ] `ChatController` auf `CollectionRepository` umstellen",
                    "+11. [ ] `ContentController` auf `CollectionRepository` umstellen",
                    "+12. [ ] Partial `collections-select.php` erstellen",
                    "+13. [ ] Chat und Content auf Partial umstellen",
                    "+14. [ ] Python-Pipeline auf DB-Query umstellen",
                    "+15. [ ] Semantic Explorer auf Repository umstellen",
                    "+",
                    "+---",
                    "+",
                    "+## 0c. Ressourcen-Übersicht",
                    "+",
                    "+### Navigationsstruktur",
                    "+",
                    "+```",
                    "+\/ressourcen",
                    "+├── \/llm           → LLM-Modelle verwalten",
                    "+├── \/collections   → RAG-Collections verwalten",
                    "+└── (später weitere: \/embeddings, \/prompts, ...)",
                    "+```",
                    "+",
                    "+### Gemeinsame Patterns",
                    "+",
                    "+| Aspekt | LLMs | Collections |",
                    "+|--------|------|-------------|",
                    "+| Tabelle | `llm_models` | `rag_collections` |",
                    "+| Repository | `LlmModelRepository` | `CollectionRepository` |",
                    "+| Sync-Service | `LlmSyncService` | `QdrantService` |",
                    "+| Admin-URL | `\/ressourcen\/llm` | `\/ressourcen\/collections` |",
                    "+| Partial | `model-select.php` | `collections-select.php` |",
                    "+",
                    "+---",
                    "+",
                    " ## 1. IST-Analyse",
                    " ",
                    " ### 1.1 Betroffene Views"
                ]
            }
        ],
        "userModified": false,
        "replaceAll": false
    }
}
← Vorheriger Zur Liste Nächster →