diff --git a/config/services.yaml b/config/services.yaml index 6703f52..963eee6 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -21,11 +21,12 @@ parameters: mto.knowledge.vector_index: '%mto.knowledge.root%/vector.index' mto.knowledge.vector_index_meta: '%mto.knowledge.root%/vector.index.meta.json' mto.knowledge.upload: '%mto.knowledge.root%/uploads' + # Backward compatibility alias mto.vector.data.upload.path: '%mto.knowledge.upload%' # ------------------------------------------------------------ - # Index Configuration (Guardrails) + # Index Configuration (Fallback Guardrails) # ------------------------------------------------------------ mto.index.chunk_size: 800 @@ -39,12 +40,11 @@ parameters: # ------------------------------------------------------------ mto.vector.python_bin: '/var/www/html/.venv/bin/python3' - mto.vector.ingest_script: '%mto.root%/src/Vector/vector_ingest.py' mto.vector.search_script: '%mto.root%/src/Vector/vector_search.py' - mto.vector.timeout: 600 + # ------------------------------------------------------------ # Services # ------------------------------------------------------------ @@ -117,12 +117,30 @@ services: alias: App\Knowledge\Retrieval\CachedRetriever # ------------------------------------------------------------ - # Vector Search (noch unverändert – Umbau kommt in Schritt 2) + # Index Configuration Provider (DB + Fallback) # ------------------------------------------------------------ + + App\Index\IndexConfigurationProvider: + arguments: + $repository: '@App\Repository\IngestProfileRepository' + $fallbackChunkSize: '%mto.index.chunk_size%' + $fallbackChunkOverlap: '%mto.index.chunk_overlap%' + $fallbackEmbeddingModel: '%mto.index.embedding_model%' + $fallbackEmbeddingDimension: '%mto.index.embedding_dimension%' + $fallbackScoringVersion: '%mto.index.scoring_version%' + + # ------------------------------------------------------------ + # Index Meta Manager (uses Provider) + # ------------------------------------------------------------ + App\Index\IndexMetaManager: arguments: $metaPath: '%mto.knowledge.index_meta%' - $config: '@App\Index\IndexConfiguration' + $provider: '@App\Index\IndexConfigurationProvider' + + # ------------------------------------------------------------ + # Vector Layer + # ------------------------------------------------------------ App\Vector\VectorSearchClient: arguments: @@ -141,22 +159,12 @@ services: $indexMetaPath: '%mto.knowledge.index_meta%' $vectorIndexPath: '%mto.knowledge.vector_index%' $timeoutSeconds: '%mto.vector.timeout%' - $indexConfiguration: '@App\Index\IndexConfiguration' + $configurationProvider: '@App\Index\IndexConfigurationProvider' # ------------------------------------------------------------ - # Index Configuration + # Admin Utilities # ------------------------------------------------------------ - App\Index\IndexConfiguration: - arguments: - $chunkSize: '%mto.index.chunk_size%' - $chunkOverlap: '%mto.index.chunk_overlap%' - $embeddingModel: '%mto.index.embedding_model%' - $embeddingDimension: '%mto.index.embedding_dimension%' - $scoringVersion: '%mto.index.scoring_version%' - $indexFormat: 'ndjson' - $vectorBackend: 'faiss' - App\Service\Admin\IndexNdjsonInspector: arguments: $ndJsonPath: '%mto.knowledge.ndjson%' diff --git a/migrations/Version20260216000100.php b/migrations/Version20260216000100.php new file mode 100644 index 0000000..5a76ffc --- /dev/null +++ b/migrations/Version20260216000100.php @@ -0,0 +1,38 @@ +createTable('ingest_profile'); + $table->addColumn('id', 'binary', ['length' => 16]); + $table->addColumn('version', 'integer'); + $table->addColumn('chunk_size', 'integer'); + $table->addColumn('chunk_overlap', 'integer'); + $table->addColumn('embedding_model', 'string', ['length' => 255]); + $table->addColumn('embedding_dimension', 'integer'); + $table->addColumn('scoring_version', 'integer'); + $table->addColumn('active', 'boolean'); + $table->addColumn('reindex_required', 'boolean'); + $table->addColumn('created_at', 'datetime_immutable'); + $table->setPrimaryKey(['id']); + } + + public function down(Schema $schema): void + { + $schema->dropTable('ingest_profile'); + } +} diff --git a/src/Controller/Admin/IngestProfileController.php b/src/Controller/Admin/IngestProfileController.php new file mode 100644 index 0000000..e2c4f78 --- /dev/null +++ b/src/Controller/Admin/IngestProfileController.php @@ -0,0 +1,102 @@ +findBy([], ['version' => 'DESC']); + $activeProfile = $repo->findActive(); + + $meta = $metaManager->readMeta(); + $currentStructure = $provider->getConfiguration()->toStructureArray(); + + $diff = $comparator->compare($meta, $currentStructure); + + $structureMismatch = false; + foreach ($diff as $row) { + if (!$row['equal']) { + $structureMismatch = true; + break; + } + } + + return $this->render('admin/ingest_profile/list.html.twig', [ + 'profiles' => $profiles, + 'activeProfile' => $activeProfile, + 'indexMeta' => $meta, + 'diff' => $diff, + 'structureMismatch' => $structureMismatch, + ]); + } + + #[Route('/create', name: 'admin_ingest_profile_create', methods: ['GET', 'POST'])] + public function create( + Request $request, + IngestProfileRepository $repo, + EntityManagerInterface $em + ): Response { + + if ($request->isMethod('POST')) { + + $latest = $repo->findLatestVersion(); + $nextVersion = $latest ? $latest->getVersion() + 1 : 1; + + $profile = new IngestProfile( + $nextVersion, + (int)$request->request->get('chunk_size'), + (int)$request->request->get('chunk_overlap'), + (string)$request->request->get('embedding_model'), + (int)$request->request->get('embedding_dimension'), + (int)$request->request->get('scoring_version') + ); + + $em->persist($profile); + $em->flush(); + + return $this->redirectToRoute('admin_ingest_profile_list'); + } + + return $this->render('admin/ingest_profile/create.html.twig'); + } + + #[Route('/activate/{id}', name: 'admin_ingest_profile_activate')] + public function activate( + IngestProfile $profile, + IngestProfileRepository $repo, + EntityManagerInterface $em + ): Response { + + $active = $repo->findActive(); + if ($active) { + $active->deactivate(); + } + + $profile->activate(); + + $em->flush(); + + return $this->redirectToRoute('admin_ingest_profile_list'); + } +} diff --git a/src/Entity/IngestProfile.php b/src/Entity/IngestProfile.php new file mode 100644 index 0000000..5393df1 --- /dev/null +++ b/src/Entity/IngestProfile.php @@ -0,0 +1,90 @@ +id = Uuid::v4(); + $this->version = $version; + $this->chunkSize = $chunkSize; + $this->chunkOverlap = $chunkOverlap; + $this->embeddingModel = $embeddingModel; + $this->embeddingDimension = $embeddingDimension; + $this->scoringVersion = $scoringVersion; + $this->createdAt = new \DateTimeImmutable(); + } + + public function getId(): Uuid { return $this->id; } + public function getVersion(): int { return $this->version; } + public function getChunkSize(): int { return $this->chunkSize; } + public function getChunkOverlap(): int { return $this->chunkOverlap; } + public function getEmbeddingModel(): string { return $this->embeddingModel; } + public function getEmbeddingDimension(): int { return $this->embeddingDimension; } + public function getScoringVersion(): int { return $this->scoringVersion; } + public function isActive(): bool { return $this->active; } + public function isReindexRequired(): bool { return $this->reindexRequired; } + public function getCreatedAt(): \DateTimeImmutable { return $this->createdAt; } + + public function activate(): void + { + $this->active = true; + $this->reindexRequired = true; + } + + public function deactivate(): void + { + $this->active = false; + } + + public function markReindexDone(): void + { + $this->reindexRequired = false; + } +} diff --git a/src/Index/IndexConfigurationProvider.php b/src/Index/IndexConfigurationProvider.php new file mode 100644 index 0000000..6202ea8 --- /dev/null +++ b/src/Index/IndexConfigurationProvider.php @@ -0,0 +1,63 @@ +repository = $repository; + + $this->fallbackChunkSize = $fallbackChunkSize; + $this->fallbackChunkOverlap = $fallbackChunkOverlap; + $this->fallbackEmbeddingModel = $fallbackEmbeddingModel; + $this->fallbackEmbeddingDimension = $fallbackEmbeddingDimension; + $this->fallbackScoringVersion = $fallbackScoringVersion; + } + + public function getConfiguration(): IndexConfiguration + { + $active = $this->repository->findActive(); + + if ($active === null) { + // Fallback auf YAML + return new IndexConfiguration( + $this->fallbackChunkSize, + $this->fallbackChunkOverlap, + $this->fallbackEmbeddingModel, + $this->fallbackEmbeddingDimension, + $this->fallbackScoringVersion, + 'ndjson', + 'faiss' + ); + } + + return new IndexConfiguration( + $active->getChunkSize(), + $active->getChunkOverlap(), + $active->getEmbeddingModel(), + $active->getEmbeddingDimension(), + $active->getScoringVersion(), + 'ndjson', + 'faiss' + ); + } +} diff --git a/src/Index/IndexMetaManager.php b/src/Index/IndexMetaManager.php index 3920bd8..c3a5b58 100644 --- a/src/Index/IndexMetaManager.php +++ b/src/Index/IndexMetaManager.php @@ -7,157 +7,90 @@ namespace App\Index; final class IndexMetaManager { private string $metaPath; - private IndexConfiguration $config; + private IndexConfigurationProvider $provider; public function __construct( string $metaPath, - IndexConfiguration $config + IndexConfigurationProvider $provider ) { $this->metaPath = $metaPath; - $this->config = $config; + $this->provider = $provider; } - public function getMetaPath(): string + // ----------------------------------------------------- + // Public API + // ----------------------------------------------------- + + public function ensureExists(): void { - return $this->metaPath; + if (!is_file($this->metaPath)) { + $this->writeMeta(1); + } } - /** - * @return array|null - */ public function readMeta(): ?array { if (!is_file($this->metaPath)) { return null; } - $raw = file_get_contents($this->metaPath); - if ($raw === false) { - throw new \RuntimeException('Unable to read index_meta.json'); - } - - $data = json_decode($raw, true); - if (!is_array($data)) { - throw new \RuntimeException('index_meta.json is invalid JSON'); - } - - return $data; + return json_decode( + (string) file_get_contents($this->metaPath), + true + ); } - /** - * Guardrail: - * - Wenn Meta fehlt → initialisieren - * - Wenn Struktur driftet → Exception - */ public function validateAgainstCurrent(): void { $meta = $this->readMeta(); + $current = $this->provider + ->getConfiguration() + ->toStructureArray(); if ($meta === null) { - $meta = $this->createInitialMeta(); + return; } - $expected = $this->config->toStructureArray(); - $diff = $this->diffStructure($meta, $expected); - - if ($diff !== []) { - throw new IndexStructureChangedException( - 'Index structure changed. Global Reindex required.', - $diff - ); - } - } - - /** - * Wird beim Global Reindex aufgerufen - */ - public function writeMetaForGlobalReindex(): array - { - $current = $this->readMeta(); - - $nextVersion = 1; - if (is_array($current) && isset($current['index_version']) && is_int($current['index_version'])) { - $nextVersion = $current['index_version'] + 1; - } - - $meta = $this->buildMetaPayload($nextVersion); - $this->atomicWriteJson($meta); - - return $meta; - } - - public function getConfig(): IndexConfiguration - { - return $this->config; - } - - // --------------------------------------------------------- - // Internals - // --------------------------------------------------------- - - private function createInitialMeta(): array - { - $meta = $this->buildMetaPayload(1); - $this->atomicWriteJson($meta); - return $meta; - } - - private function buildMetaPayload(int $indexVersion): array - { - $structure = $this->config->toStructureArray(); - - return [ - 'index_version' => $indexVersion, - 'created_at' => (new \DateTimeImmutable())->format(DATE_ATOM), - 'embedding_model' => $structure['embedding_model'], - 'embedding_dimension' => $structure['embedding_dimension'], - 'chunk_size' => $structure['chunk_size'], - 'chunk_overlap' => $structure['chunk_overlap'], - 'scoring_version' => $structure['scoring_version'], - 'index_format' => $structure['index_format'], - 'vector_backend' => $structure['vector_backend'], - ]; - } - - private function diffStructure(array $meta, array $expected): array - { - $diff = []; - - foreach ($expected as $key => $value) { - $actual = $meta[$key] ?? null; - if ($actual !== $value) { - $diff[$key] = [ - 'expected' => $value, - 'actual' => $actual, - ]; + foreach ($current as $key => $value) { + if (($meta[$key] ?? null) !== $value) { + throw new \RuntimeException( + 'Index structure changed. Global Reindex required.' + ); } } - - return $diff; } - private function atomicWriteJson(array $payload): void + public function writeMetaForGlobalReindex(): void { + $current = $this->readMeta(); + $nextVersion = ($current['index_version'] ?? 0) + 1; + + $this->writeMeta($nextVersion); + } + + public function writeMeta(int $indexVersion): void + { + $config = $this->provider->getConfiguration(); + + $payload = array_merge( + [ + 'index_version' => $indexVersion, + 'created_at' => (new \DateTimeImmutable())->format(DATE_ATOM), + ], + $config->toStructureArray() + ); + $dir = dirname($this->metaPath); - - if (!is_dir($dir) && !mkdir($dir, 0777, true) && !is_dir($dir)) { - throw new \RuntimeException('Unable to create directory: ' . $dir); + if (!is_dir($dir)) { + mkdir($dir, 0777, true); } - $tmp = $this->metaPath . '.tmp'; - - $json = json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); - if ($json === false) { - throw new \RuntimeException('Unable to encode index_meta.json'); - } - - if (file_put_contents($tmp, $json . PHP_EOL) === false) { - throw new \RuntimeException('Unable to write temp meta file'); - } - - if (!rename($tmp, $this->metaPath)) { - @unlink($tmp); - throw new \RuntimeException('Unable to switch meta file atomically'); - } + file_put_contents( + $this->metaPath, + json_encode( + $payload, + JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES + ) + ); } } diff --git a/src/Index/IndexStructureComparator.php b/src/Index/IndexStructureComparator.php new file mode 100644 index 0000000..effb232 --- /dev/null +++ b/src/Index/IndexStructureComparator.php @@ -0,0 +1,37 @@ + $metaValue, + 'profile' => $profileValue, + 'equal' => $metaValue === $profileValue, + ]; + } + + return $result; + } +} diff --git a/src/Knowledge/Ingest/SimpleChunker.php b/src/Knowledge/Ingest/SimpleChunker.php index 21944ca..3445537 100644 --- a/src/Knowledge/Ingest/SimpleChunker.php +++ b/src/Knowledge/Ingest/SimpleChunker.php @@ -5,22 +5,31 @@ declare(strict_types=1); namespace App\Knowledge\Ingest; +use App\Index\IndexConfigurationProvider; + final class SimpleChunker { + private IndexConfigurationProvider $configurationProvider; + public function __construct( - private int $maxWords = 180, - private int $overlapWords = 30 - ) {} + IndexConfigurationProvider $configurationProvider + ) { + $this->configurationProvider = $configurationProvider; + } /** @return string[] */ public function chunk(string $text): array { + $config = $this->configurationProvider->getConfiguration(); + + $maxWords = $config->getChunkSize(); + $overlapWords = $config->getChunkOverlap(); + $text = $this->normalize($text); if ($text === '') { return []; } - // Split into tokens: words + whitespace preserved $tokens = preg_split( '/(\s+)/u', $text, @@ -32,7 +41,6 @@ final class SimpleChunker return []; } - // Build word index → token index mapping $wordTokenIndexes = []; foreach ($tokens as $i => $token) { if (!preg_match('/^\s+$/u', $token)) { @@ -49,12 +57,11 @@ final class SimpleChunker $wordPos = 0; while ($wordPos < $totalWords) { - $wordEnd = min($wordPos + $this->maxWords, $totalWords); + $wordEnd = min($wordPos + $maxWords, $totalWords); $tokenStart = $wordTokenIndexes[$wordPos]; $tokenEnd = $wordTokenIndexes[$wordEnd - 1] + 1; - // Intelligent cut (sentence / paragraph aware) $tokenEnd = $this->adjustCutToBoundary($tokens, $tokenStart, $tokenEnd); $chunk = trim(implode('', array_slice( @@ -71,7 +78,7 @@ final class SimpleChunker break; } - $wordPos = max(0, $wordEnd - $this->overlapWords); + $wordPos = max(0, $wordEnd - $overlapWords); } return $this->dedupe($chunks); @@ -86,30 +93,19 @@ final class SimpleChunker return trim((string) $text); } - /** - * Move cut backwards to a natural boundary if possible. - * Rules: - * - Never cut inside markdown list items - * - Sentence end only if followed by a line break - * - Paragraph breaks always allowed - */ private function adjustCutToBoundary(array $tokens, int $start, int $end): int { - // Detect markdown list context (e.g. "- Foo: Bar") $startToken = $tokens[$start] ?? ''; if (preg_match('/^- /u', ltrim($startToken))) { - // Keep list blocks intact return $end; } for ($i = $end - 1; $i > $start; $i--) { - // Paragraph boundary if ($tokens[$i] === "\n\n") { return $i + 1; } - // Sentence boundary only if followed by newline if ( preg_match('/[.!?]\s*$/u', $tokens[$i]) && isset($tokens[$i + 1]) && diff --git a/src/Repository/IngestProfileRepository.php b/src/Repository/IngestProfileRepository.php new file mode 100644 index 0000000..9ba1f80 --- /dev/null +++ b/src/Repository/IngestProfileRepository.php @@ -0,0 +1,31 @@ +createQueryBuilder('p') + ->orderBy('p.version', 'DESC') + ->setMaxResults(1) + ->getQuery() + ->getOneOrNullResult(); + } + + public function findActive(): ?IngestProfile + { + return $this->findOneBy(['active' => true]); + } +} diff --git a/src/Vector/VectorIndexBuilder.php b/src/Vector/VectorIndexBuilder.php index fd21e47..b4598fb 100644 --- a/src/Vector/VectorIndexBuilder.php +++ b/src/Vector/VectorIndexBuilder.php @@ -4,7 +4,7 @@ declare(strict_types=1); namespace App\Vector; -use App\Index\IndexConfiguration; +use App\Index\IndexConfigurationProvider; use Symfony\Component\Process\Exception\ProcessFailedException; use Symfony\Component\Process\Process; @@ -18,7 +18,7 @@ final class VectorIndexBuilder private string $vectorMetaPath; private int $timeoutSeconds; - private IndexConfiguration $indexConfiguration; + private IndexConfigurationProvider $configurationProvider; public function __construct( string $pythonBin, @@ -27,7 +27,7 @@ final class VectorIndexBuilder string $indexMetaPath, string $vectorIndexPath, int $timeoutSeconds, - IndexConfiguration $indexConfiguration + IndexConfigurationProvider $configurationProvider ) { $this->pythonBin = $pythonBin; $this->scriptPath = $scriptPath; @@ -36,39 +36,29 @@ final class VectorIndexBuilder $this->vectorIndexPath = $vectorIndexPath; $this->vectorMetaPath = $vectorIndexPath . '.meta.json'; $this->timeoutSeconds = $timeoutSeconds; - $this->indexConfiguration = $indexConfiguration; + $this->configurationProvider = $configurationProvider; } + /** + * Rebuild FAISS Index deterministisch aus index.ndjson. + */ public function rebuildFromNdjson(?string $logPath = null): void { - if (!is_file($this->scriptPath)) { - throw new \RuntimeException('vector_ingest.py not found at: ' . $this->scriptPath); - } - - if (!is_file($this->indexNdjsonPath)) { - throw new \RuntimeException('index.ndjson not found at: ' . $this->indexNdjsonPath); - } + $this->assertPreconditions(); if (!is_file($this->indexMetaPath)) { $this->initializeIndexMeta(); } - $indexMeta = json_decode((string) file_get_contents($this->indexMetaPath), true); + $indexMeta = $this->readIndexMeta(); - if (!is_array($indexMeta) || empty($indexMeta['embedding_model'])) { - throw new \RuntimeException('Invalid index_meta.json'); - } - - $embeddingModel = (string) $indexMeta['embedding_model']; + $embeddingModel = $indexMeta['embedding_model']; $tmpVectorIndexPath = $this->vectorIndexPath . '.tmp'; - // Wichtig: Python erzeugt meta basierend auf endgültigem Namen - $finalMetaPath = $this->vectorMetaPath; - $tmpMetaPath = dirname($this->vectorIndexPath) . '/' . basename($this->vectorIndexPath, '.index') . '.index.meta.json'; - + // Clean leftovers @unlink($tmpVectorIndexPath); - @unlink($finalMetaPath); + @unlink($this->vectorMetaPath); $cmd = [ $this->pythonBin, @@ -80,21 +70,41 @@ final class VectorIndexBuilder $process = new Process($cmd); $process->setTimeout($this->timeoutSeconds); - $process->mustRun(); - if (!is_file($tmpVectorIndexPath) || filesize($tmpVectorIndexPath) === 0) { - throw new \RuntimeException('Vector index tmp missing or empty'); + $this->runProcess($process, $logPath); + + $this->validatePythonOutputs($tmpVectorIndexPath); + + $this->atomicSwitch($tmpVectorIndexPath); + } + + // ----------------------------------------------------- + // Internals + // ----------------------------------------------------- + + private function assertPreconditions(): void + { + if (!is_file($this->scriptPath)) { + throw new \RuntimeException('vector_ingest.py not found at: ' . $this->scriptPath); } - // Python erzeugt vector.index.meta.json (nicht tmp.meta!) - if (!is_file($this->vectorMetaPath) || filesize($this->vectorMetaPath) === 0) { - throw new \RuntimeException('Vector meta missing or empty'); + if (!is_file($this->indexNdjsonPath)) { + throw new \RuntimeException('index.ndjson not found at: ' . $this->indexNdjsonPath); + } + } + + private function readIndexMeta(): array + { + $meta = json_decode( + (string) file_get_contents($this->indexMetaPath), + true + ); + + if (!is_array($meta) || empty($meta['embedding_model'])) { + throw new \RuntimeException('Invalid index_meta.json'); } - // Atomarer Switch für Index - if (!rename($tmpVectorIndexPath, $this->vectorIndexPath)) { - throw new \RuntimeException('Atomic switch failed for vector index'); - } + return $meta; } private function initializeIndexMeta(): void @@ -105,14 +115,16 @@ final class VectorIndexBuilder throw new \RuntimeException('Cannot create knowledge directory'); } + $config = $this->configurationProvider->getConfiguration(); + $data = [ 'index_version' => 1, 'created_at' => (new \DateTimeImmutable())->format(DATE_ATOM), - 'embedding_model' => $this->indexConfiguration->getEmbeddingModel(), - 'embedding_dimension' => $this->indexConfiguration->getEmbeddingDimension(), - 'chunk_size' => $this->indexConfiguration->getChunkSize(), - 'chunk_overlap' => $this->indexConfiguration->getChunkOverlap(), - 'scoring_version' => $this->indexConfiguration->getScoringVersion(), + 'embedding_model' => $config->getEmbeddingModel(), + 'embedding_dimension' => $config->getEmbeddingDimension(), + 'chunk_size' => $config->getChunkSize(), + 'chunk_overlap' => $config->getChunkOverlap(), + 'scoring_version' => $config->getScoringVersion(), 'index_format' => 'ndjson', 'vector_backend' => 'faiss', ]; @@ -123,6 +135,24 @@ final class VectorIndexBuilder ); } + private function validatePythonOutputs(string $tmpVectorIndexPath): void + { + if (!is_file($tmpVectorIndexPath) || filesize($tmpVectorIndexPath) === 0) { + throw new \RuntimeException('Vector index tmp missing or empty'); + } + + if (!is_file($this->vectorMetaPath) || filesize($this->vectorMetaPath) === 0) { + throw new \RuntimeException('Vector meta missing or empty'); + } + } + + private function atomicSwitch(string $tmpVectorIndexPath): void + { + if (!rename($tmpVectorIndexPath, $this->vectorIndexPath)) { + throw new \RuntimeException('Atomic switch failed for vector index'); + } + } + private function runProcess(Process $process, ?string $logPath): void { if ($logPath !== null) { diff --git a/templates/admin/base.html.twig b/templates/admin/base.html.twig index 11c4313..9e26c17 100644 --- a/templates/admin/base.html.twig +++ b/templates/admin/base.html.twig @@ -28,22 +28,35 @@ + +
+

Dokumente und Wissen

+ +
+

System-Profile

+ diff --git a/templates/admin/ingest_profile/create.html.twig b/templates/admin/ingest_profile/create.html.twig new file mode 100644 index 0000000..5802047 --- /dev/null +++ b/templates/admin/ingest_profile/create.html.twig @@ -0,0 +1,26 @@ +{% extends 'admin/base.html.twig' %} + +{% block title %}System Prompt{% endblock %} + +{% block body %} +

Create Ingest Profile

+ +
+ +
+ + +
+ + +
+ + +
+ + +
+ + +
+{% endblock %} diff --git a/templates/admin/ingest_profile/list.html.twig b/templates/admin/ingest_profile/list.html.twig new file mode 100644 index 0000000..10c6c1a --- /dev/null +++ b/templates/admin/ingest_profile/list.html.twig @@ -0,0 +1,89 @@ +{% extends 'admin/base.html.twig' %} + +{% block title %}Ingest Profiles{% endblock %} + +{% block body %} +

Ingest Profiles

+ + {% if structureMismatch %} +
+ ⚠ Strukturabweichung festgestellt – Globale Neuindizierung erforderlich | Global Reindex aufrufen +
+ {% else %} +
+ ✅ Die Indexstruktur entspricht dem aktiven Profil +
+ {% endif %} + +

+ Neues Profil anlegen

+ +

Profiles

+ + + + + + + + + + + + + + + {% for p in profiles %} + + + + + + + + + + + + {% endfor %} +
VersionChunk SizeOverlapModelDimensionScoringActiveReindex RequiredActions
{{ p.version }}{{ p.chunkSize }}{{ p.chunkOverlap }}{{ p.embeddingModel }}{{ p.embeddingDimension }}{{ p.scoringVersion }}{{ p.active ? 'Yes' : 'No' }}{{ p.reindexRequired ? 'Yes' : 'No' }} + {% if not p.active %} + + Aktivieren + + {% endif %} +
+ +
+

Index-Struktur-Profil Diff

+ + {% if indexMeta %} +

Index Version: {{ indexMeta.index_version }}

+ {% else %} +

No index_meta.json found.

+ {% endif %} + + + + + + + + + + {% for key, row in diff %} + + + + + + + {% endfor %} +
ParameterIndex MetaActive ProfileStatus
{{ key }}{{ row.meta }}{{ row.profile }} + {% if row.equal %} + + {% else %} + + {% endif %} +
+ +{% endblock %} diff --git a/templates/admin/job/index.html.twig b/templates/admin/job/index.html.twig index 083cbf4..eb86322 100644 --- a/templates/admin/job/index.html.twig +++ b/templates/admin/job/index.html.twig @@ -4,7 +4,7 @@ {% block body %} -

Ingest Jobs

+

Indexierung (Ingest Jobs-Liste)

-

Agent System Overview

+

Wissensdaten (Chunk-Index)

{# ============================= #} {# Index Meta Section #}