{
"tool_response": {
"type": "update",
"filePath": "\/var\/www\/dev.campus.systemische-tools.de\/src\/Controller\/ContentPipelineController.php",
"content": "<?php\n\nnamespace Controller;\n\nuse Application\\PipelineStepService;\nuse Framework\\Controller;\nuse Infrastructure\\AI\\ModelConfig;\nuse Infrastructure\\Config\\PipelineStepConfig;\nuse Infrastructure\\Persistence\\PipelineRepository;\n\nclass ContentPipelineController extends Controller\n{\n private PipelineRepository $repository;\n private PipelineStepService $stepService;\n\n public function __construct()\n {\n $this->repository = new PipelineRepository();\n $this->stepService = new PipelineStepService($this->repository);\n }\n\n \/**\n * GET \/content-pipeline\n *\/\n public function index(): void\n {\n $this->view('content-pipeline.index', [\n 'title' => 'Content Pipeline',\n 'pipelines' => $this->repository->findAll(),\n 'stats' => $this->repository->getStatistics(),\n ]);\n }\n\n \/**\n * GET \/content-pipeline\/import\n *\/\n public function import(): void\n {\n $pipeline = $this->repository->findDefault();\n\n if ($pipeline === null) {\n $pipelines = $this->repository->findAll(1);\n $pipeline = $pipelines[0] ?? null;\n }\n\n $this->view('content-pipeline.import', [\n 'title' => 'Import Pipeline',\n 'pipeline' => $pipeline,\n 'latestRun' => $pipeline !== null\n ? $this->repository->findLatestRun((int) $pipeline['id'])\n : null,\n ]);\n }\n\n \/**\n * GET \/content-pipeline\/new\n *\/\n public function pipelineNew(): void\n {\n $this->view('content-pipeline.form', [\n 'title' => 'Neue Pipeline',\n 'pipeline' => null,\n 'stepTypes' => PipelineStepConfig::getStepTypes(),\n ]);\n }\n\n \/**\n * GET \/content-pipeline\/{id}\n *\/\n public function show(string $id): void\n {\n $pipeline = $this->repository->findById((int) $id);\n\n if ($pipeline === null) {\n $this->notFound('Pipeline nicht gefunden');\n }\n\n $this->view('content-pipeline.show', [\n 'title' => 'Pipeline: ' . $pipeline['name'],\n 'pipeline' => $pipeline,\n 'runs' => $this->repository->findRuns((int) $id, 10),\n 'stepTypes' => PipelineStepConfig::getStepTypes(),\n 'models' => ModelConfig::getAll(),\n 'defaultModel' => ModelConfig::DEFAULT_MODEL,\n 'collections' => PipelineStepConfig::getCollections(),\n ]);\n }\n\n \/**\n * GET \/content-pipeline\/{id}\/edit\n *\/\n public function edit(string $id): void\n {\n $pipeline = $this->repository->findById((int) $id);\n\n if ($pipeline === null) {\n $this->notFound('Pipeline nicht gefunden');\n }\n\n $this->view('content-pipeline.form', [\n 'title' => 'Pipeline bearbeiten: ' . $pipeline['name'],\n 'pipeline' => $pipeline,\n 'stepTypes' => PipelineStepConfig::getStepTypes(),\n ]);\n }\n\n \/**\n * POST \/content-pipeline\n *\/\n public function store(): void\n {\n $this->requireCsrf();\n\n $name = trim($_POST['name'] ?? '');\n\n if ($name === '') {\n $_SESSION['error'] = 'Name ist erforderlich.';\n $this->redirect('\/content-pipeline\/new');\n }\n\n $pipelineId = $this->repository->create([\n 'name' => $name,\n 'description' => trim($_POST['description'] ?? ''),\n 'source_path' => trim($_POST['source_path'] ?? '\/var\/www\/nextcloud\/data\/root\/files\/Documents'),\n 'extensions' => PipelineStepConfig::parseExtensions($_POST['extensions'] ?? ''),\n 'is_default' => isset($_POST['is_default']) ? 1 : 0,\n ]);\n\n $this->stepService->createDefaultSteps($pipelineId);\n\n $_SESSION['success'] = 'Pipeline erfolgreich erstellt.';\n $this->redirect('\/content-pipeline\/' . $pipelineId);\n }\n\n \/**\n * POST \/content-pipeline\/{id}\n *\/\n public function update(string $id): void\n {\n $this->requireCsrf();\n\n $pipeline = $this->repository->findById((int) $id);\n\n if ($pipeline === null) {\n $this->notFound('Pipeline nicht gefunden');\n }\n\n $name = trim($_POST['name'] ?? '');\n\n if ($name === '') {\n $_SESSION['error'] = 'Name ist erforderlich.';\n $this->redirect('\/content-pipeline\/' . $id . '\/edit');\n }\n\n $this->repository->update((int) $id, [\n 'name' => $name,\n 'description' => trim($_POST['description'] ?? ''),\n 'source_path' => trim($_POST['source_path'] ?? ''),\n 'extensions' => PipelineStepConfig::parseExtensions($_POST['extensions'] ?? ''),\n 'is_default' => isset($_POST['is_default']) ? 1 : 0,\n ]);\n\n $_SESSION['success'] = 'Pipeline aktualisiert.';\n $this->redirect('\/content-pipeline\/' . $id);\n }\n\n \/**\n * POST \/content-pipeline\/{id}\/run\n *\/\n public function run(string $id): void\n {\n $this->requireCsrf();\n\n $pipeline = $this->repository->findById((int) $id);\n\n if ($pipeline === null) {\n $this->notFound('Pipeline nicht gefunden');\n }\n\n $runId = $this->repository->createRun((int) $id);\n\n \/\/ Pipeline im Hintergrund starten\n $cmd = sprintf(\n 'nohup %s %s all --pipeline-id=%d --run-id=%d > %s 2>&1 &',\n escapeshellarg('\/opt\/scripts\/pipeline\/venv\/bin\/python'),\n escapeshellarg('\/opt\/scripts\/pipeline\/pipeline.py'),\n (int) $id,\n $runId,\n escapeshellarg('\/tmp\/pipeline_run_' . $runId . '.log')\n );\n\n exec($cmd);\n\n $_SESSION['success'] = 'Pipeline gestartet (Run #' . $runId . ')';\n $this->redirect('\/content-pipeline\/' . $id);\n }\n\n \/**\n * GET \/content-pipeline\/{id}\/status (AJAX)\n *\/\n public function status(string $id): void\n {\n $pipeline = $this->repository->findById((int) $id);\n\n if ($pipeline === null) {\n $this->json(['error' => 'Pipeline nicht gefunden'], 404);\n\n return;\n }\n\n $this->json([\n 'pipeline_id' => (int) $id,\n 'run' => $this->repository->findLatestRun((int) $id),\n ]);\n }\n\n \/**\n * POST \/content-pipeline\/{id}\/steps\/{stepId}\/toggle\n *\/\n public function toggleStep(string $id, string $stepId): void\n {\n $this->requireCsrf();\n\n if (!$this->stepService->toggleStep((int) $id, (int) $stepId)) {\n $this->notFound('Pipeline oder Schritt nicht gefunden');\n }\n\n $this->redirect('\/content-pipeline\/' . $id);\n }\n\n \/**\n * POST \/content-pipeline\/{id}\/steps\/{stepId}\/model (AJAX)\n *\/\n public function updateStepModel(string $id, string $stepId): void\n {\n $result = $this->stepService->updateModel(\n (int) $id,\n (int) $stepId,\n $_POST['model'] ?? ''\n );\n\n if (!$result['success']) {\n $this->json(['error' => $result['error']], $result['error'] === 'Schritt nicht gefunden' ? 404 : 400);\n\n return;\n }\n\n $this->json($result);\n }\n\n \/**\n * POST \/content-pipeline\/{id}\/steps\/{stepId}\/collection (AJAX)\n *\/\n public function updateStepCollection(string $id, string $stepId): void\n {\n $result = $this->stepService->updateCollection(\n (int) $id,\n (int) $stepId,\n $_POST['collection'] ?? ''\n );\n\n if (!$result['success']) {\n $this->json(['error' => $result['error']], $result['error'] === 'Schritt nicht gefunden' ? 404 : 400);\n\n return;\n }\n\n $this->json($result);\n }\n\n \/**\n * POST \/content-pipeline\/{id}\/delete\n *\/\n public function delete(string $id): void\n {\n $this->requireCsrf();\n\n $pipeline = $this->repository->findById((int) $id);\n\n if ($pipeline === null) {\n $this->notFound('Pipeline nicht gefunden');\n }\n\n $this->repository->delete((int) $id);\n\n $_SESSION['success'] = 'Pipeline geloescht.';\n $this->redirect('\/content-pipeline');\n }\n}\n",
"structuredPatch": [
{
"oldStart": 2,
"oldLines": 17,
"newStart": 2,
"newLines": 21,
"lines": [
" ",
" namespace Controller;",
" ",
"+use Application\\PipelineStepService;",
" use Framework\\Controller;",
" use Infrastructure\\AI\\ModelConfig;",
"+use Infrastructure\\Config\\PipelineStepConfig;",
" use Infrastructure\\Persistence\\PipelineRepository;",
" ",
" class ContentPipelineController extends Controller",
" {",
" private PipelineRepository $repository;",
"+ private PipelineStepService $stepService;",
" ",
" public function __construct()",
" {",
" $this->repository = new PipelineRepository();",
"+ $this->stepService = new PipelineStepService($this->repository);",
" }",
" ",
" \/**"
]
},
{
"oldStart": 20,
"oldLines": 13,
"newStart": 24,
"newLines": 10,
"lines": [
" *\/",
" public function index(): void",
" {",
"- $pipelines = $this->repository->findAll();",
"- $stats = $this->repository->getStatistics();",
"-",
" $this->view('content-pipeline.index', [",
" 'title' => 'Content Pipeline',",
"- 'pipelines' => $pipelines,",
"- 'stats' => $stats,",
"+ 'pipelines' => $this->repository->findAll(),",
"+ 'stats' => $this->repository->getStatistics(),",
" ]);",
" }",
" "
]
},
{
"oldStart": 42,
"oldLines": 14,
"newStart": 43,
"newLines": 12,
"lines": [
" $pipeline = $pipelines[0] ?? null;",
" }",
" ",
"- $latestRun = $pipeline !== null",
"- ? $this->repository->findLatestRun((int) $pipeline['id'])",
"- : null;",
"-",
" $this->view('content-pipeline.import', [",
" 'title' => 'Import Pipeline',",
" 'pipeline' => $pipeline,",
"- 'latestRun' => $latestRun,",
"+ 'latestRun' => $pipeline !== null",
"+ ? $this->repository->findLatestRun((int) $pipeline['id'])",
"+ : null,",
" ]);",
" }",
" "
]
},
{
"oldStart": 61,
"oldLines": 7,
"newStart": 60,
"newLines": 7,
"lines": [
" $this->view('content-pipeline.form', [",
" 'title' => 'Neue Pipeline',",
" 'pipeline' => null,",
"- 'stepTypes' => $this->getStepTypes(),",
"+ 'stepTypes' => PipelineStepConfig::getStepTypes(),",
" ]);",
" }",
" "
]
},
{
"oldStart": 76,
"oldLines": 16,
"newStart": 75,
"newLines": 14,
"lines": [
" $this->notFound('Pipeline nicht gefunden');",
" }",
" ",
"- $runs = $this->repository->findRuns((int) $id, 10);",
"-",
" $this->view('content-pipeline.show', [",
" 'title' => 'Pipeline: ' . $pipeline['name'],",
" 'pipeline' => $pipeline,",
"- 'runs' => $runs,",
"- 'stepTypes' => $this->getStepTypes(),",
"+ 'runs' => $this->repository->findRuns((int) $id, 10),",
"+ 'stepTypes' => PipelineStepConfig::getStepTypes(),",
" 'models' => ModelConfig::getAll(),",
" 'defaultModel' => ModelConfig::DEFAULT_MODEL,",
"- 'collections' => $this->getAvailableCollections(),",
"+ 'collections' => PipelineStepConfig::getCollections(),",
" ]);",
" }",
" "
]
},
{
"oldStart": 103,
"oldLines": 7,
"newStart": 100,
"newLines": 7,
"lines": [
" $this->view('content-pipeline.form', [",
" 'title' => 'Pipeline bearbeiten: ' . $pipeline['name'],",
" 'pipeline' => $pipeline,",
"- 'stepTypes' => $this->getStepTypes(),",
"+ 'stepTypes' => PipelineStepConfig::getStepTypes(),",
" ]);",
" }",
" "
]
},
{
"oldStart": 115,
"oldLines": 31,
"newStart": 112,
"newLines": 24,
"lines": [
" $this->requireCsrf();",
" ",
" $name = trim($_POST['name'] ?? '');",
"- $description = trim($_POST['description'] ?? '');",
"- $sourcePath = trim($_POST['source_path'] ?? '\/var\/www\/nextcloud\/data\/root\/files\/Documents');",
"- $extensions = $this->parseExtensions($_POST['extensions'] ?? '');",
"- $isDefault = isset($_POST['is_default']) ? 1 : 0;",
" ",
" if ($name === '') {",
" $_SESSION['error'] = 'Name ist erforderlich.';",
"- header('Location: \/content-pipeline\/new');",
"- exit;",
"+ $this->redirect('\/content-pipeline\/new');",
" }",
" ",
" $pipelineId = $this->repository->create([",
" 'name' => $name,",
"- 'description' => $description,",
"- 'source_path' => $sourcePath,",
"- 'extensions' => $extensions,",
"- 'is_default' => $isDefault,",
"+ 'description' => trim($_POST['description'] ?? ''),",
"+ 'source_path' => trim($_POST['source_path'] ?? '\/var\/www\/nextcloud\/data\/root\/files\/Documents'),",
"+ 'extensions' => PipelineStepConfig::parseExtensions($_POST['extensions'] ?? ''),",
"+ 'is_default' => isset($_POST['is_default']) ? 1 : 0,",
" ]);",
" ",
"- \/\/ Standard-Steps hinzufuegen",
"- $this->createDefaultSteps($pipelineId);",
"+ $this->stepService->createDefaultSteps($pipelineId);",
" ",
" $_SESSION['success'] = 'Pipeline erfolgreich erstellt.';",
"- header('Location: \/content-pipeline\/' . $pipelineId);",
"- exit;",
"+ $this->redirect('\/content-pipeline\/' . $pipelineId);",
" }",
" ",
" \/**"
]
},
{
"oldStart": 156,
"oldLines": 28,
"newStart": 146,
"newLines": 22,
"lines": [
" }",
" ",
" $name = trim($_POST['name'] ?? '');",
"- $description = trim($_POST['description'] ?? '');",
"- $sourcePath = trim($_POST['source_path'] ?? '');",
"- $extensions = $this->parseExtensions($_POST['extensions'] ?? '');",
"- $isDefault = isset($_POST['is_default']) ? 1 : 0;",
" ",
" if ($name === '') {",
" $_SESSION['error'] = 'Name ist erforderlich.';",
"- header('Location: \/content-pipeline\/' . $id . '\/edit');",
"- exit;",
"+ $this->redirect('\/content-pipeline\/' . $id . '\/edit');",
" }",
" ",
" $this->repository->update((int) $id, [",
" 'name' => $name,",
"- 'description' => $description,",
"- 'source_path' => $sourcePath,",
"- 'extensions' => $extensions,",
"- 'is_default' => $isDefault,",
"+ 'description' => trim($_POST['description'] ?? ''),",
"+ 'source_path' => trim($_POST['source_path'] ?? ''),",
"+ 'extensions' => PipelineStepConfig::parseExtensions($_POST['extensions'] ?? ''),",
"+ 'is_default' => isset($_POST['is_default']) ? 1 : 0,",
" ]);",
" ",
" $_SESSION['success'] = 'Pipeline aktualisiert.';",
"- header('Location: \/content-pipeline\/' . $id);",
"- exit;",
"+ $this->redirect('\/content-pipeline\/' . $id);",
" }",
" ",
" \/**"
]
},
{
"oldStart": 193,
"oldLines": 33,
"newStart": 177,
"newLines": 26,
"lines": [
" $this->notFound('Pipeline nicht gefunden');",
" }",
" ",
"- \/\/ Neuen Run erstellen",
" $runId = $this->repository->createRun((int) $id);",
" ",
" \/\/ Pipeline im Hintergrund starten",
"- $pipelineScript = '\/opt\/scripts\/pipeline\/pipeline.py';",
"- $venvPython = '\/opt\/scripts\/pipeline\/venv\/bin\/python';",
"- $logFile = '\/tmp\/pipeline_run_' . $runId . '.log';",
"-",
" $cmd = sprintf(",
" 'nohup %s %s all --pipeline-id=%d --run-id=%d > %s 2>&1 &',",
"- escapeshellarg($venvPython),",
"- escapeshellarg($pipelineScript),",
"+ escapeshellarg('\/opt\/scripts\/pipeline\/venv\/bin\/python'),",
"+ escapeshellarg('\/opt\/scripts\/pipeline\/pipeline.py'),",
" (int) $id,",
" $runId,",
"- escapeshellarg($logFile)",
"+ escapeshellarg('\/tmp\/pipeline_run_' . $runId . '.log')",
" );",
" ",
" exec($cmd);",
" ",
" $_SESSION['success'] = 'Pipeline gestartet (Run #' . $runId . ')';",
"- header('Location: \/content-pipeline\/' . $id);",
"- exit;",
"+ $this->redirect('\/content-pipeline\/' . $id);",
" }",
" ",
" \/**",
"- * GET \/content-pipeline\/{id}\/status",
"- * AJAX endpoint for run status",
"+ * GET \/content-pipeline\/{id}\/status (AJAX)",
" *\/",
" public function status(string $id): void",
" {"
]
},
{
"oldStart": 231,
"oldLines": 11,
"newStart": 208,
"newLines": 9,
"lines": [
" return;",
" }",
" ",
"- $latestRun = $this->repository->findLatestRun((int) $id);",
"-",
" $this->json([",
" 'pipeline_id' => (int) $id,",
"- 'run' => $latestRun,",
"+ 'run' => $this->repository->findLatestRun((int) $id),",
" ]);",
" }",
" "
]
},
{
"oldStart": 246,
"oldLines": 82,
"newStart": 221,
"newLines": 51,
"lines": [
" {",
" $this->requireCsrf();",
" ",
"- $pipeline = $this->repository->findById((int) $id);",
"-",
"- if ($pipeline === null) {",
"- $this->notFound('Pipeline nicht gefunden');",
"+ if (!$this->stepService->toggleStep((int) $id, (int) $stepId)) {",
"+ $this->notFound('Pipeline oder Schritt nicht gefunden');",
" }",
" ",
"- \/\/ Find step and toggle",
"- foreach ($pipeline['steps'] as $step) {",
"- if ((int) $step['id'] === (int) $stepId) {",
"- $this->repository->updateStep((int) $stepId, [",
"- 'enabled' => $step['enabled'] ? 0 : 1,",
"- ]);",
"- break;",
"- }",
"- }",
"-",
"- header('Location: \/content-pipeline\/' . $id);",
"- exit;",
"+ $this->redirect('\/content-pipeline\/' . $id);",
" }",
" ",
" \/**",
" * POST \/content-pipeline\/{id}\/steps\/{stepId}\/model (AJAX)",
"- * Update step model configuration",
" *\/",
" public function updateStepModel(string $id, string $stepId): void",
" {",
"- $pipeline = $this->repository->findById((int) $id);",
"+ $result = $this->stepService->updateModel(",
"+ (int) $id,",
"+ (int) $stepId,",
"+ $_POST['model'] ?? ''",
"+ );",
" ",
"- if ($pipeline === null) {",
"- $this->json(['error' => 'Pipeline nicht gefunden'], 404);",
"+ if (!$result['success']) {",
"+ $this->json(['error' => $result['error']], $result['error'] === 'Schritt nicht gefunden' ? 404 : 400);",
" ",
" return;",
" }",
" ",
"- $model = trim($_POST['model'] ?? '');",
"+ $this->json($result);",
"+ }",
" ",
"- if ($model === '' || !ModelConfig::isValid($model)) {",
"- $this->json(['error' => 'Ungültiges Modell'], 400);",
"+ \/**",
"+ * POST \/content-pipeline\/{id}\/steps\/{stepId}\/collection (AJAX)",
"+ *\/",
"+ public function updateStepCollection(string $id, string $stepId): void",
"+ {",
"+ $result = $this->stepService->updateCollection(",
"+ (int) $id,",
"+ (int) $stepId,",
"+ $_POST['collection'] ?? ''",
"+ );",
" ",
"- return;",
"- }",
"+ if (!$result['success']) {",
"+ $this->json(['error' => $result['error']], $result['error'] === 'Schritt nicht gefunden' ? 404 : 400);",
" ",
"- \/\/ Find step",
"- $stepFound = false;",
"- foreach ($pipeline['steps'] as $step) {",
"- if ((int) $step['id'] === (int) $stepId) {",
"- $stepFound = true;",
"- $config = $step['config'] ?? [];",
"-",
"- \/\/ Determine provider from model",
"- $provider = ModelConfig::isLocal($model) ? 'ollama' : 'anthropic';",
"-",
"- \/\/ Update config with new model",
"- $config['model'] = ModelConfig::isLocal($model)",
"- ? substr($model, 7) \/\/ Remove 'ollama:' prefix",
"- : $model;",
"- $config['provider'] = $provider;",
"-",
"- $this->repository->updateStep((int) $stepId, [",
"- 'config' => $config,",
"- ]);",
"- break;",
"- }",
"- }",
"-",
"- if (!$stepFound) {",
"- $this->json(['error' => 'Schritt nicht gefunden'], 404);",
"-",
" return;",
" }",
" ",
"- $this->json([",
"- 'success' => true,",
"- 'model' => $model,",
"- 'label' => ModelConfig::getLabel($model),",
"- ]);",
"+ $this->json($result);",
" }",
" ",
" \/**"
]
},
{
"oldStart": 340,
"oldLines": 292,
"newStart": 284,
"newLines": 6,
"lines": [
" $this->repository->delete((int) $id);",
" ",
" $_SESSION['success'] = 'Pipeline geloescht.';",
"- header('Location: \/content-pipeline');",
"- exit;",
"+ $this->redirect('\/content-pipeline');",
" }",
"-",
"- \/**",
"- * @return array<string, array<string, mixed>>",
"- *\/",
"- private function getStepTypes(): array",
"- {",
"- return [",
"- \/\/ Phase 1: Vorverarbeitung",
"- 'detect' => [",
"- 'label' => 'Erkennung',",
"- 'description' => 'Dateien scannen und Format prüfen',",
"- 'phase' => 'Vorverarbeitung',",
"- 'storage' => null,",
"- ],",
"- 'validate' => [",
"- 'label' => 'Validierung',",
"- 'description' => 'Datei-Prüfung auf Lesbarkeit und Korruption',",
"- 'phase' => 'Vorverarbeitung',",
"- 'storage' => null,",
"- ],",
"- 'page_split' => [",
"- 'label' => 'Seitenzerlegung',",
"- 'description' => 'PDF in Einzelseiten zerlegen für Referenz und Vision-Analyse',",
"- 'phase' => 'Vorverarbeitung',",
"- 'storage' => 'ki_content.document_pages',",
"- ],",
"- 'vision_analyze' => [",
"- 'label' => 'Bildanalyse',",
"- 'description' => 'Seiten via Vision-Modell analysieren, Bilder und Grafiken erkennen',",
"- 'phase' => 'Vorverarbeitung',",
"- 'storage' => 'ki_content.document_pages (vision_analysis)',",
"- 'uses_vision' => true,",
"- ],",
"- 'extract' => [",
"- 'label' => 'Textextraktion',",
"- 'description' => 'Text extrahieren, OCR für Bilder mit Text',",
"- 'phase' => 'Vorverarbeitung',",
"- 'storage' => null,",
"- ],",
"- 'structure' => [",
"- 'label' => 'Strukturerkennung',",
"- 'description' => 'Überschriften, Listen und Hierarchie erkennen',",
"- 'phase' => 'Vorverarbeitung',",
"- 'storage' => 'ki_content.document_sections',",
"- ],",
"- 'segment' => [",
"- 'label' => 'Abschnitte',",
"- 'description' => 'Logische Dokumentgliederung nach Struktur',",
"- 'phase' => 'Vorverarbeitung',",
"- 'storage' => 'ki_content.document_sections',",
"- ],",
"- 'chunk' => [",
"- 'label' => 'Textbausteine',",
"- 'description' => 'Chunks erstellen (max 800 Token) mit Seitenreferenz',",
"- 'phase' => 'Vorverarbeitung',",
"- 'storage' => 'ki_content.chunks',",
"- ],",
"- \/\/ Phase 2: Speicherung & Vektorisierung",
"- 'metadata_store' => [",
"- 'label' => 'DB-Speicherung',",
"- 'description' => 'Dokument, Seiten und Chunks in MariaDB speichern',",
"- 'phase' => 'Speicherung',",
"- 'storage' => 'ki_content.documents, .document_pages, .chunks',",
"- ],",
"- 'embed' => [",
"- 'label' => 'Vektorisierung',",
"- 'description' => 'Embeddings erstellen für Vektor-Suche',",
"- 'phase' => 'Speicherung',",
"- 'storage' => 'Qdrant: {collection}',",
"- 'fixed_model' => 'mxbai-embed-large (1024-dim)',",
"- 'has_collection' => true,",
"- ],",
"- 'collection_setup' => [",
"- 'label' => 'Collection',",
"- 'description' => 'Qdrant-Collection einrichten falls nötig',",
"- 'phase' => 'Speicherung',",
"- 'storage' => 'Qdrant: {collection}',",
"- ],",
"- 'vector_store' => [",
"- 'label' => 'Vektorspeicherung',",
"- 'description' => 'Vektoren in Qdrant mit MariaDB-ID als Referenz',",
"- 'phase' => 'Speicherung',",
"- 'storage' => 'Qdrant: {collection}',",
"- ],",
"- 'index_optimize' => [",
"- 'label' => 'Index-Optimierung',",
"- 'description' => 'HNSW-Index für schnelle Suche optimieren',",
"- 'phase' => 'Speicherung',",
"- 'storage' => 'Qdrant: {collection}',",
"- ],",
"- \/\/ Phase 3: Wissensextraktion (3 Ebenen)",
"- 'knowledge_page' => [",
"- 'label' => 'Seiten-Wissen',",
"- 'description' => 'Pro Seite: Entitäten → Semantik → Ontologie → Taxonomie',",
"- 'phase' => 'Wissen',",
"- 'storage' => 'ki_content.page_knowledge, .entities, .entity_semantics',",
"- 'uses_llm' => true,",
"- ],",
"- 'knowledge_section' => [",
"- 'label' => 'Abschnitt-Wissen',",
"- 'description' => 'Pro Kapitel: Aggregierte Wissensrepräsentation',",
"- 'phase' => 'Wissen',",
"- 'storage' => 'ki_content.section_knowledge',",
"- 'uses_llm' => true,",
"- ],",
"- 'knowledge_document' => [",
"- 'label' => 'Dokument-Wissen',",
"- 'description' => 'Konsolidierte Gesamtsicht des Dokuments',",
"- 'phase' => 'Wissen',",
"- 'storage' => 'ki_content.document_knowledge',",
"- 'uses_llm' => true,",
"- ],",
"- 'knowledge_validate' => [",
"- 'label' => 'Wissens-Validierung',",
"- 'description' => 'Abgleich mit DB, Duplikate zusammenführen, neue validieren',",
"- 'phase' => 'Wissen',",
"- 'storage' => 'ki_content.entities (merged)',",
"- ],",
"- \/\/ Legacy Analyse-Schritte",
"- 'entity_extract' => [",
"- 'label' => 'Entitäten (Legacy)',",
"- 'description' => 'Personen, Organisationen, Konzepte, Methoden erkennen',",
"- 'phase' => 'Analyse',",
"- 'storage' => 'ki_content.chunk_entities',",
"- 'uses_llm' => true,",
"- ],",
"- 'relation_extract' => [",
"- 'label' => 'Beziehungen (Legacy)',",
"- 'description' => 'Relationen zwischen Entitäten extrahieren',",
"- 'phase' => 'Analyse',",
"- 'storage' => 'ki_content.entity_relations',",
"- 'uses_llm' => true,",
"- ],",
"- 'taxonomy_build' => [",
"- 'label' => 'Taxonomie (Legacy)',",
"- 'description' => 'Hierarchische Kategorisierung aufbauen',",
"- 'phase' => 'Analyse',",
"- 'storage' => 'ki_content.chunk_taxonomy, .taxonomy_terms',",
"- 'uses_llm' => true,",
"- ],",
"- 'semantic_analyze' => [",
"- 'label' => 'Semantik (Legacy)',",
"- 'description' => 'Bedeutungs-Analyse, Konzepte und Definitionen',",
"- 'phase' => 'Analyse',",
"- 'storage' => 'ki_content.chunk_semantics',",
"- 'uses_llm' => true,",
"- ],",
"- 'summarize' => [",
"- 'label' => 'Zusammenfassung',",
"- 'description' => 'Dokument- und Seiten-Zusammenfassungen erstellen',",
"- 'phase' => 'Analyse',",
"- 'storage' => 'ki_content.documents (summary), .document_pages',",
"- 'uses_llm' => true,",
"- ],",
"- 'question_generate' => [",
"- 'label' => 'Fragengenerierung',",
"- 'description' => 'Beispielfragen für RAG-Chat erstellen',",
"- 'phase' => 'Analyse',",
"- 'storage' => 'ki_content.generated_questions',",
"- 'uses_llm' => true,",
"- ],",
"- 'finalize' => [",
"- 'label' => 'Abschluss',",
"- 'description' => 'Status finalisieren und Job beenden',",
"- 'phase' => 'Analyse',",
"- 'storage' => 'ki_content.documents (status)',",
"- ],",
"- \/\/ Legacy",
"- 'analyze' => [",
"- 'label' => 'Analyse (Legacy)',",
"- 'description' => 'Kombinierte Analyse (veraltet)',",
"- 'phase' => 'Analyse',",
"- 'storage' => 'ki_content.chunk_entities, .chunk_semantics',",
"- 'uses_llm' => true,",
"- ],",
"- ];",
"- }",
"-",
"- \/**",
"- * @param string $input",
"- * @return array<string>",
"- *\/",
"- private function parseExtensions(string $input): array",
"- {",
"- $extensions = [];",
"- $parts = preg_split('\/[\\s,;]+\/', $input);",
"-",
"- if ($parts === false) {",
"- return ['.pdf', '.docx', '.pptx', '.md', '.txt'];",
"- }",
"-",
"- foreach ($parts as $ext) {",
"- $ext = trim($ext);",
"- if ($ext === '') {",
"- continue;",
"- }",
"- if ($ext[0] !== '.') {",
"- $ext = '.' . $ext;",
"- }",
"- $extensions[] = strtolower($ext);",
"- }",
"-",
"- return $extensions !== [] ? $extensions : ['.pdf', '.docx', '.pptx', '.md', '.txt'];",
"- }",
"-",
"- private function createDefaultSteps(int $pipelineId): void",
"- {",
"- $defaultSteps = [",
"- ['step_type' => 'detect', 'config' => ['hash_algorithm' => 'sha256'], 'sort_order' => 1, 'enabled' => 1],",
"- ['step_type' => 'extract', 'config' => ['ocr_enabled' => true, 'ocr_language' => 'deu'], 'sort_order' => 2, 'enabled' => 1],",
"- ['step_type' => 'chunk', 'config' => ['min_size' => 100, 'max_size' => 2000, 'overlap' => 0.1], 'sort_order' => 3, 'enabled' => 1],",
"- ['step_type' => 'embed', 'config' => ['model' => 'mxbai-embed-large', 'collection' => 'documents', 'dimensions' => 1024], 'sort_order' => 4, 'enabled' => 1],",
"- ['step_type' => 'analyze', 'config' => ['extract_entities' => true, 'extract_relations' => true, 'classify_taxonomy' => true], 'sort_order' => 5, 'enabled' => 0],",
"- ];",
"-",
"- foreach ($defaultSteps as $step) {",
"- $this->repository->addStep($pipelineId, $step);",
"- }",
"- }",
"-",
"- \/**",
"- * Get available Qdrant collections.",
"- *",
"- * @return array<string, string>",
"- *\/",
"- private function getAvailableCollections(): array",
"- {",
"- return [",
"- 'documents' => 'Documents (Schulungsunterlagen)',",
"- 'mail' => 'Mail (E-Mails)',",
"- 'entities' => 'Entities (Entitäten)',",
"- 'knowledge' => 'Knowledge (Wissensbasis)',",
"- ];",
"- }",
"-",
"- \/**",
"- * POST \/content-pipeline\/{id}\/steps\/{stepId}\/collection (AJAX)",
"- * Update step collection configuration",
"- *\/",
"- public function updateStepCollection(string $id, string $stepId): void",
"- {",
"- $pipeline = $this->repository->findById((int) $id);",
"-",
"- if ($pipeline === null) {",
"- $this->json(['error' => 'Pipeline nicht gefunden'], 404);",
"-",
"- return;",
"- }",
"-",
"- $collection = trim($_POST['collection'] ?? '');",
"- $validCollections = array_keys($this->getAvailableCollections());",
"-",
"- if ($collection === '' || !in_array($collection, $validCollections, true)) {",
"- $this->json(['error' => 'Ungültige Collection'], 400);",
"-",
"- return;",
"- }",
"-",
"- \/\/ Find step",
"- $stepFound = false;",
"- foreach ($pipeline['steps'] as $step) {",
"- if ((int) $step['id'] === (int) $stepId) {",
"- $stepFound = true;",
"- $config = $step['config'] ?? [];",
"- $config['collection'] = $collection;",
"-",
"- $this->repository->updateStep((int) $stepId, [",
"- 'config' => $config,",
"- ]);",
"- break;",
"- }",
"- }",
"-",
"- if (!$stepFound) {",
"- $this->json(['error' => 'Schritt nicht gefunden'], 404);",
"-",
"- return;",
"- }",
"-",
"- $this->json([",
"- 'success' => true,",
"- 'collection' => $collection,",
"- 'label' => $this->getAvailableCollections()[$collection] ?? $collection,",
"- ]);",
"- }",
" }"
]
}
],
"originalFile": "<?php\n\nnamespace Controller;\n\nuse Framework\\Controller;\nuse Infrastructure\\AI\\ModelConfig;\nuse Infrastructure\\Persistence\\PipelineRepository;\n\nclass ContentPipelineController extends Controller\n{\n private PipelineRepository $repository;\n\n public function __construct()\n {\n $this->repository = new PipelineRepository();\n }\n\n \/**\n * GET \/content-pipeline\n *\/\n public function index(): void\n {\n $pipelines = $this->repository->findAll();\n $stats = $this->repository->getStatistics();\n\n $this->view('content-pipeline.index', [\n 'title' => 'Content Pipeline',\n 'pipelines' => $pipelines,\n 'stats' => $stats,\n ]);\n }\n\n \/**\n * GET \/content-pipeline\/import\n *\/\n public function import(): void\n {\n $pipeline = $this->repository->findDefault();\n\n if ($pipeline === null) {\n $pipelines = $this->repository->findAll(1);\n $pipeline = $pipelines[0] ?? null;\n }\n\n $latestRun = $pipeline !== null\n ? $this->repository->findLatestRun((int) $pipeline['id'])\n : null;\n\n $this->view('content-pipeline.import', [\n 'title' => 'Import Pipeline',\n 'pipeline' => $pipeline,\n 'latestRun' => $latestRun,\n ]);\n }\n\n \/**\n * GET \/content-pipeline\/new\n *\/\n public function pipelineNew(): void\n {\n $this->view('content-pipeline.form', [\n 'title' => 'Neue Pipeline',\n 'pipeline' => null,\n 'stepTypes' => $this->getStepTypes(),\n ]);\n }\n\n \/**\n * GET \/content-pipeline\/{id}\n *\/\n public function show(string $id): void\n {\n $pipeline = $this->repository->findById((int) $id);\n\n if ($pipeline === null) {\n $this->notFound('Pipeline nicht gefunden');\n }\n\n $runs = $this->repository->findRuns((int) $id, 10);\n\n $this->view('content-pipeline.show', [\n 'title' => 'Pipeline: ' . $pipeline['name'],\n 'pipeline' => $pipeline,\n 'runs' => $runs,\n 'stepTypes' => $this->getStepTypes(),\n 'models' => ModelConfig::getAll(),\n 'defaultModel' => ModelConfig::DEFAULT_MODEL,\n 'collections' => $this->getAvailableCollections(),\n ]);\n }\n\n \/**\n * GET \/content-pipeline\/{id}\/edit\n *\/\n public function edit(string $id): void\n {\n $pipeline = $this->repository->findById((int) $id);\n\n if ($pipeline === null) {\n $this->notFound('Pipeline nicht gefunden');\n }\n\n $this->view('content-pipeline.form', [\n 'title' => 'Pipeline bearbeiten: ' . $pipeline['name'],\n 'pipeline' => $pipeline,\n 'stepTypes' => $this->getStepTypes(),\n ]);\n }\n\n \/**\n * POST \/content-pipeline\n *\/\n public function store(): void\n {\n $this->requireCsrf();\n\n $name = trim($_POST['name'] ?? '');\n $description = trim($_POST['description'] ?? '');\n $sourcePath = trim($_POST['source_path'] ?? '\/var\/www\/nextcloud\/data\/root\/files\/Documents');\n $extensions = $this->parseExtensions($_POST['extensions'] ?? '');\n $isDefault = isset($_POST['is_default']) ? 1 : 0;\n\n if ($name === '') {\n $_SESSION['error'] = 'Name ist erforderlich.';\n header('Location: \/content-pipeline\/new');\n exit;\n }\n\n $pipelineId = $this->repository->create([\n 'name' => $name,\n 'description' => $description,\n 'source_path' => $sourcePath,\n 'extensions' => $extensions,\n 'is_default' => $isDefault,\n ]);\n\n \/\/ Standard-Steps hinzufuegen\n $this->createDefaultSteps($pipelineId);\n\n $_SESSION['success'] = 'Pipeline erfolgreich erstellt.';\n header('Location: \/content-pipeline\/' . $pipelineId);\n exit;\n }\n\n \/**\n * POST \/content-pipeline\/{id}\n *\/\n public function update(string $id): void\n {\n $this->requireCsrf();\n\n $pipeline = $this->repository->findById((int) $id);\n\n if ($pipeline === null) {\n $this->notFound('Pipeline nicht gefunden');\n }\n\n $name = trim($_POST['name'] ?? '');\n $description = trim($_POST['description'] ?? '');\n $sourcePath = trim($_POST['source_path'] ?? '');\n $extensions = $this->parseExtensions($_POST['extensions'] ?? '');\n $isDefault = isset($_POST['is_default']) ? 1 : 0;\n\n if ($name === '') {\n $_SESSION['error'] = 'Name ist erforderlich.';\n header('Location: \/content-pipeline\/' . $id . '\/edit');\n exit;\n }\n\n $this->repository->update((int) $id, [\n 'name' => $name,\n 'description' => $description,\n 'source_path' => $sourcePath,\n 'extensions' => $extensions,\n 'is_default' => $isDefault,\n ]);\n\n $_SESSION['success'] = 'Pipeline aktualisiert.';\n header('Location: \/content-pipeline\/' . $id);\n exit;\n }\n\n \/**\n * POST \/content-pipeline\/{id}\/run\n *\/\n public function run(string $id): void\n {\n $this->requireCsrf();\n\n $pipeline = $this->repository->findById((int) $id);\n\n if ($pipeline === null) {\n $this->notFound('Pipeline nicht gefunden');\n }\n\n \/\/ Neuen Run erstellen\n $runId = $this->repository->createRun((int) $id);\n\n \/\/ Pipeline im Hintergrund starten\n $pipelineScript = '\/opt\/scripts\/pipeline\/pipeline.py';\n $venvPython = '\/opt\/scripts\/pipeline\/venv\/bin\/python';\n $logFile = '\/tmp\/pipeline_run_' . $runId . '.log';\n\n $cmd = sprintf(\n 'nohup %s %s all --pipeline-id=%d --run-id=%d > %s 2>&1 &',\n escapeshellarg($venvPython),\n escapeshellarg($pipelineScript),\n (int) $id,\n $runId,\n escapeshellarg($logFile)\n );\n\n exec($cmd);\n\n $_SESSION['success'] = 'Pipeline gestartet (Run #' . $runId . ')';\n header('Location: \/content-pipeline\/' . $id);\n exit;\n }\n\n \/**\n * GET \/content-pipeline\/{id}\/status\n * AJAX endpoint for run status\n *\/\n public function status(string $id): void\n {\n $pipeline = $this->repository->findById((int) $id);\n\n if ($pipeline === null) {\n $this->json(['error' => 'Pipeline nicht gefunden'], 404);\n\n return;\n }\n\n $latestRun = $this->repository->findLatestRun((int) $id);\n\n $this->json([\n 'pipeline_id' => (int) $id,\n 'run' => $latestRun,\n ]);\n }\n\n \/**\n * POST \/content-pipeline\/{id}\/steps\/{stepId}\/toggle\n *\/\n public function toggleStep(string $id, string $stepId): void\n {\n $this->requireCsrf();\n\n $pipeline = $this->repository->findById((int) $id);\n\n if ($pipeline === null) {\n $this->notFound('Pipeline nicht gefunden');\n }\n\n \/\/ Find step and toggle\n foreach ($pipeline['steps'] as $step) {\n if ((int) $step['id'] === (int) $stepId) {\n $this->repository->updateStep((int) $stepId, [\n 'enabled' => $step['enabled'] ? 0 : 1,\n ]);\n break;\n }\n }\n\n header('Location: \/content-pipeline\/' . $id);\n exit;\n }\n\n \/**\n * POST \/content-pipeline\/{id}\/steps\/{stepId}\/model (AJAX)\n * Update step model configuration\n *\/\n public function updateStepModel(string $id, string $stepId): void\n {\n $pipeline = $this->repository->findById((int) $id);\n\n if ($pipeline === null) {\n $this->json(['error' => 'Pipeline nicht gefunden'], 404);\n\n return;\n }\n\n $model = trim($_POST['model'] ?? '');\n\n if ($model === '' || !ModelConfig::isValid($model)) {\n $this->json(['error' => 'Ungültiges Modell'], 400);\n\n return;\n }\n\n \/\/ Find step\n $stepFound = false;\n foreach ($pipeline['steps'] as $step) {\n if ((int) $step['id'] === (int) $stepId) {\n $stepFound = true;\n $config = $step['config'] ?? [];\n\n \/\/ Determine provider from model\n $provider = ModelConfig::isLocal($model) ? 'ollama' : 'anthropic';\n\n \/\/ Update config with new model\n $config['model'] = ModelConfig::isLocal($model)\n ? substr($model, 7) \/\/ Remove 'ollama:' prefix\n : $model;\n $config['provider'] = $provider;\n\n $this->repository->updateStep((int) $stepId, [\n 'config' => $config,\n ]);\n break;\n }\n }\n\n if (!$stepFound) {\n $this->json(['error' => 'Schritt nicht gefunden'], 404);\n\n return;\n }\n\n $this->json([\n 'success' => true,\n 'model' => $model,\n 'label' => ModelConfig::getLabel($model),\n ]);\n }\n\n \/**\n * POST \/content-pipeline\/{id}\/delete\n *\/\n public function delete(string $id): void\n {\n $this->requireCsrf();\n\n $pipeline = $this->repository->findById((int) $id);\n\n if ($pipeline === null) {\n $this->notFound('Pipeline nicht gefunden');\n }\n\n $this->repository->delete((int) $id);\n\n $_SESSION['success'] = 'Pipeline geloescht.';\n header('Location: \/content-pipeline');\n exit;\n }\n\n \/**\n * @return array<string, array<string, mixed>>\n *\/\n private function getStepTypes(): array\n {\n return [\n \/\/ Phase 1: Vorverarbeitung\n 'detect' => [\n 'label' => 'Erkennung',\n 'description' => 'Dateien scannen und Format prüfen',\n 'phase' => 'Vorverarbeitung',\n 'storage' => null,\n ],\n 'validate' ... [TRUNCATED-0bcb1c3422ec6156]"
}
}