From 3416678cf4696bf08a27241961747f9cf6dc8b5c Mon Sep 17 00:00:00 2001 From: team2 Date: Sun, 15 Feb 2026 21:33:31 +0100 Subject: [PATCH] add system prompt and chunks index views and edit --- config/services.yaml | 5 + migrations/Version20260215193707.php | 31 ++++ migrations/Version20260215201902.php | 31 ++++ src/Agent/PromptBuilder.php | 22 ++- .../Admin/SystemAgentController.php | 35 +++++ .../Admin/SystemPromptController.php | 94 ++++++++++++ src/Entity/SystemPrompt.php | 111 ++++++++++++++ src/Repository/SystemPromptRepository.php | 38 +++++ src/Service/Admin/IndexNdjsonInspector.php | 132 +++++++++++++++++ templates/admin/base.html.twig | 12 +- .../admin/system/agent_overview.html.twig | 135 ++++++++++++++++++ templates/admin/system/prompt.html.twig | 104 ++++++++++++++ 12 files changed, 743 insertions(+), 7 deletions(-) create mode 100644 migrations/Version20260215193707.php create mode 100644 migrations/Version20260215201902.php create mode 100644 src/Controller/Admin/SystemAgentController.php create mode 100644 src/Controller/Admin/SystemPromptController.php create mode 100644 src/Entity/SystemPrompt.php create mode 100644 src/Repository/SystemPromptRepository.php create mode 100644 src/Service/Admin/IndexNdjsonInspector.php create mode 100644 templates/admin/system/agent_overview.html.twig create mode 100644 templates/admin/system/prompt.html.twig diff --git a/config/services.yaml b/config/services.yaml index 4a3b2ea..b444826 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -125,3 +125,8 @@ services: $scoringVersion: '%mto.index.scoring_version%' $indexFormat: 'ndjson' $vectorBackend: 'faiss' + + App\Service\Admin\IndexNdjsonInspector: + arguments: + $ndJsonPath: '%mto.vector.data.ndjson.path%' + $indexMetaPath: '%mto.vector.data.vector_index_meta_json.path%' diff --git a/migrations/Version20260215193707.php b/migrations/Version20260215193707.php new file mode 100644 index 0000000..b5f85fb --- /dev/null +++ b/migrations/Version20260215193707.php @@ -0,0 +1,31 @@ +addSql('CREATE TABLE system_prompt (id BINARY(16) NOT NULL, version INT NOT NULL, content LONGTEXT NOT NULL, active TINYINT NOT NULL, created_at DATETIME NOT NULL, PRIMARY KEY (id)) DEFAULT CHARACTER SET utf8mb4'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('DROP TABLE system_prompt'); + } +} diff --git a/migrations/Version20260215201902.php b/migrations/Version20260215201902.php new file mode 100644 index 0000000..1eea8e6 --- /dev/null +++ b/migrations/Version20260215201902.php @@ -0,0 +1,31 @@ +addSql('ALTER TABLE system_prompt ADD comment VARCHAR(255) DEFAULT NULL, ADD updated_at DATETIME DEFAULT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE system_prompt DROP comment, DROP updated_at'); + } +} diff --git a/src/Agent/PromptBuilder.php b/src/Agent/PromptBuilder.php index b2f95f4..3704a34 100644 --- a/src/Agent/PromptBuilder.php +++ b/src/Agent/PromptBuilder.php @@ -5,14 +5,14 @@ declare(strict_types=1); namespace App\Agent; use App\Context\ContextService; -use App\Context\UrlAnalyzer; +use App\Repository\SystemPromptRepository; use DateTimeImmutable; -final class PromptBuilder +final readonly class PromptBuilder { public function __construct( - private readonly ContextService $contextService, - private readonly UrlAnalyzer $urlAnalyzer, + private ContextService $contextService, + private SystemPromptRepository $systemPromptRepository, ) { } @@ -25,6 +25,7 @@ final class PromptBuilder * @param string $urlContent * @param string[] $knowledgeChunks * @param bool $fullContext + * @return string */ public function build( string $prompt, @@ -39,7 +40,8 @@ final class PromptBuilder // ------------------------------------------------------------ // 1) SYSTEM INSTRUCTIONS // ------------------------------------------------------------ - $systemLines = [ + //ToDO: remove old systemLines + /*$systemLines = [ 'You are a conversational AI assistant.', 'Respond clearly, precisely, and in context of the ongoing conversation.', 'The conversation context is authoritative and must be respected.', @@ -66,7 +68,15 @@ final class PromptBuilder '- Answer directly and confidently using always correct canonical terminology.' ]; - $systemBlock = "SYSTEM:\n" . implode("\n", $systemLines); + $systemBlock = "SYSTEM:\n" . implode("\n", $systemLines);*/ + + $activePrompt = $this->systemPromptRepository->findActive(); + + if (!$activePrompt) { + throw new \RuntimeException('No active system prompt configured.'); + } + + $systemBlock = "SYSTEM:\n" . $activePrompt->getContent(); // ------------------------------------------------------------ // 2) CONVERSATION CONTEXT (AUTHORITATIVE) diff --git a/src/Controller/Admin/SystemAgentController.php b/src/Controller/Admin/SystemAgentController.php new file mode 100644 index 0000000..b72b5ba --- /dev/null +++ b/src/Controller/Admin/SystemAgentController.php @@ -0,0 +1,35 @@ +query->get('page', 1)); + $limit = max(1, min(200, (int) $request->query->get('limit', 50))); + + $paths = $inspector->getPaths(); + $meta = $inspector->readMeta(); + $ndjsonPage = $inspector->readNdjsonPage($page, $limit); + + return $this->render('admin/system/agent_overview.html.twig', [ + 'paths' => $paths, + 'meta' => $meta, + 'ndjson' => $ndjsonPage, + 'debugCount'=>count($ndjsonPage['items']) + ]); + } +} diff --git a/src/Controller/Admin/SystemPromptController.php b/src/Controller/Admin/SystemPromptController.php new file mode 100644 index 0000000..019ca4b --- /dev/null +++ b/src/Controller/Admin/SystemPromptController.php @@ -0,0 +1,94 @@ +isMethod('POST')) { + $content = trim($request->request->get('content', '')); + $comment = trim($request->request->get('comment', '')); + + if ($content !== '') { + + $active = $repo->findActive(); + if ($active) { + $active->deactivate(); + } + + $new = new SystemPrompt( + version: $repo->getNextVersion(), + content: $content, + comment: $comment ?: null, + active: true + ); + + $em->persist($new); + $em->flush(); + + $this->addFlash('success', 'Neue Version gespeichert.'); + return $this->redirectToRoute('admin_system_prompt'); + } + } + + return $this->render('admin/system/prompt.html.twig', [ + 'active' => $repo->findActive(), + 'all' => $repo->findBy([], ['version' => 'DESC']), + ]); + } + + #[Route('/admin/system/prompt/{id}/activate', name: 'admin_system_prompt_activate', methods: ['POST'])] + public function activate( + SystemPrompt $prompt, + SystemPromptRepository $repo, + EntityManagerInterface $em + ): Response { + + foreach ($repo->findBy(['active' => true]) as $p) { + $p->deactivate(); + } + + $prompt->activate(); + + $em->flush(); + + $this->addFlash('success', 'Version aktiviert.'); + return $this->redirectToRoute('admin_system_prompt'); + } + + #[Route('/admin/system/prompt/{id}/delete', name: 'admin_system_prompt_delete', methods: ['POST'])] + public function delete( + SystemPrompt $prompt, + EntityManagerInterface $em + ): Response { + + if ($prompt->isActive()) { + $this->addFlash('danger', 'Aktive Version kann nicht gelöscht werden.'); + return $this->redirectToRoute('admin_system_prompt'); + } + + $em->remove($prompt); + $em->flush(); + + $this->addFlash('success', 'Version gelöscht.'); + return $this->redirectToRoute('admin_system_prompt'); + } +} diff --git a/src/Entity/SystemPrompt.php b/src/Entity/SystemPrompt.php new file mode 100644 index 0000000..d441c45 --- /dev/null +++ b/src/Entity/SystemPrompt.php @@ -0,0 +1,111 @@ +id = Uuid::v4(); + $this->version = $version; + $this->content = $content; + $this->comment = $comment; + $this->active = $active; + $this->createdAt = new \DateTimeImmutable(); + } + + public function getId(): Uuid + { + return $this->id; + } + + public function getVersion(): int + { + return $this->version; + } + + public function getContent(): string + { + return $this->content; + } + + public function getComment(): ?string + { + return $this->comment; + } + + public function isActive(): bool + { + return $this->active; + } + + public function getCreatedAt(): \DateTimeImmutable + { + return $this->createdAt; + } + + public function getUpdatedAt(): ?\DateTimeImmutable + { + return $this->updatedAt; + } + + public function activate(): void + { + $this->active = true; + $this->touch(); + } + + public function deactivate(): void + { + $this->active = false; + $this->touch(); + } + + public function updateContent(string $content, ?string $comment = null): void + { + $this->content = $content; + $this->comment = $comment; + $this->touch(); + } + + private function touch(): void + { + $this->updatedAt = new \DateTimeImmutable(); + } +} diff --git a/src/Repository/SystemPromptRepository.php b/src/Repository/SystemPromptRepository.php new file mode 100644 index 0000000..a6f4993 --- /dev/null +++ b/src/Repository/SystemPromptRepository.php @@ -0,0 +1,38 @@ +createQueryBuilder('p') + ->andWhere('p.active = true') + ->orderBy('p.version', 'DESC') + ->setMaxResults(1) + ->getQuery() + ->getOneOrNullResult(); + } + + public function getNextVersion(): int + { + $max = $this->createQueryBuilder('p') + ->select('MAX(p.version)') + ->getQuery() + ->getSingleScalarResult(); + + return ((int)$max) + 1; + } +} diff --git a/src/Service/Admin/IndexNdjsonInspector.php b/src/Service/Admin/IndexNdjsonInspector.php new file mode 100644 index 0000000..d134783 --- /dev/null +++ b/src/Service/Admin/IndexNdjsonInspector.php @@ -0,0 +1,132 @@ +ndjsonPath = $ndJsonPath; + $this->metaPath = $indexMetaPath; + } + + public function getPaths(): array + { + return [ + 'ndjson' => $this->ndjsonPath, + 'meta' => $this->metaPath, + ]; + } + + public function readMeta(): array + { + if (!is_file($this->metaPath)) { + return [ + 'error' => 'index_meta.json nicht gefunden', + 'path' => $this->metaPath, + ]; + } + + $raw = @file_get_contents($this->metaPath); + if ($raw === false) { + return [ + 'error' => 'index_meta.json konnte nicht gelesen werden', + 'path' => $this->metaPath, + ]; + } + + $data = json_decode($raw, true); + if (!is_array($data)) { + return [ + 'error' => 'index_meta.json ist kein valides JSON-Objekt', + 'path' => $this->metaPath, + ]; + } + + return $data; + } + + /** + * Liest NDJSON "streaming" und gibt nur einen Ausschnitt zurück. + * - Keine Voll-Loads + * - Maximal limit Zeilen + * - Offset = (page-1)*limit + */ + public function readNdjsonPage(int $page, int $limit): array + { + $page = max(1, $page); + $limit = max(1, min(200, $limit)); // hard cap für Admin-UI + + if (!is_file($this->ndjsonPath)) { + return [ + 'error' => 'index.ndjson nicht gefunden', + 'path' => $this->ndjsonPath, + 'items' => [], + ]; + } + + $handle = @fopen($this->ndjsonPath, 'rb'); + if ($handle === false) { + return [ + 'error' => 'index.ndjson konnte nicht geöffnet werden', + 'path' => $this->ndjsonPath, + 'items' => [], + ]; + } + + $offsetLines = ($page - 1) * $limit; + $items = []; + $lineNo = 0; + + // Sicherheitslimit: wir lesen max. X Bytes pro Line (um Monster-Zeilen abzufangen) + $maxLineBytes = 1024 * 1024; // 1MB pro Zeile + + while (!feof($handle)) { + $line = fgets($handle, $maxLineBytes); + if ($line === false) { + break; + } + + $lineNo++; + + if ($lineNo <= $offsetLines) { + continue; + } + + $line = trim($line); + if ($line === '') { + continue; + } + + $decoded = json_decode($line, true); + if (!is_array($decoded)) { + // Ungültige Zeile ignorieren, aber sichtbar machen wäre auch ok. + continue; + } + + $items[] = $decoded; + + if (count($items) >= $limit) { + break; + } + } + + fclose($handle); + + return [ + 'error' => null, + 'path' => $this->ndjsonPath, + 'items' => $items, + 'page' => $page, + 'limit' => $limit, + ]; + } +} diff --git a/templates/admin/base.html.twig b/templates/admin/base.html.twig index 481b107..11c4313 100644 --- a/templates/admin/base.html.twig +++ b/templates/admin/base.html.twig @@ -33,7 +33,17 @@ + + diff --git a/templates/admin/system/agent_overview.html.twig b/templates/admin/system/agent_overview.html.twig new file mode 100644 index 0000000..083b1dc --- /dev/null +++ b/templates/admin/system/agent_overview.html.twig @@ -0,0 +1,135 @@ +{% extends 'admin/base.html.twig' %} + +{% block title %}Agent System Overview{% endblock %} + +{% block body %} + + + ← Zurück + + +

Agent System Overview

+ + {# ============================= #} + {# Index Meta Section #} + {# ============================= #} + +
+
+ +
Index Meta (index_meta.json)
+ + {% if meta.error is defined %} +
+ Fehler:
+ {{ meta.error }}
+ {{ meta.path }} +
+ {% else %} + + + {% for key, value in meta %} + + + + + {% endfor %} + +
{{ key }} + {% if value is iterable %} +
+{{ value|json_encode(constant('JSON_PRETTY_PRINT')) }}
+                                    
+ {% else %} + {{ value }} + {% endif %} +
+ {% endif %} + +
+
#} + + {# ============================= #} + {# NDJSON Section #} + {# ============================= #} + + {% set currentPage = ndjson.page|default(1) %} + {% set currentLimit = ndjson.limit|default(50) %} + +
+
+ +
+
NdJson Index Übersicht Chunks (index.ndjson)
+ +
+ + ← Zurück + + + + Weiter → + +
+
+ + {% if ndjson.error %} +
+ Fehler:
+ {{ ndjson.error }}
+ {{ ndjson.path|default('-') }} +
+ {% endif %} + +
+ Datei vorhanden: {{ ndjson.path ? 'JA' : 'NEIN' }} | + Geladene Einträge: {{ debugCount|default(0) }} | + Seite {{ currentPage }} • Limit {{ currentLimit }} +
+ +
+ + + + + + + + + + {% for item in ndjson.items|default([]) %} + + + + + + {% else %} + + + + {% endfor %} + +
chunk_iddocument_idtext (gekürzt)
{{ item.chunk_id ?? '-' }}{{ item.document_id ?? '-' }} + {% set text = item.text ?? '' %} + {{ text|slice(0, 240) }}{% if text|length > 240 %}…{% endif %} + +
+ + JSON anzeigen + +
+{{ item|json_encode(constant('JSON_PRETTY_PRINT')) }}
+                                    
+
+
+ Keine Einträge gefunden. +
+
+ +
+
+ +{% endblock %} diff --git a/templates/admin/system/prompt.html.twig b/templates/admin/system/prompt.html.twig new file mode 100644 index 0000000..c256056 --- /dev/null +++ b/templates/admin/system/prompt.html.twig @@ -0,0 +1,104 @@ +{% extends 'admin/base.html.twig' %} + +{% block title %}System Prompt{% endblock %} + +{% block body %} + +

System Prompt

+ + {% for message in app.flashes('success') %} +
{{ message }}
+ {% endfor %} + {% for message in app.flashes('danger') %} +
{{ message }}
+ {% endfor %} + +
+
+ +
+ +
+ + +
+ +
+ + +
+ + + +
+ +
+
+ +
+
+ +
Versionen
+ + + + + + + + + + + + + {% for p in all %} + + + + + + + + {% endfor %} + +
VersionAktivKommentarErstelltAktionen
{{ p.version }} + {% if p.active %} + ACTIVE + {% endif %} + {{ p.comment ?? '-' }}{{ p.createdAt|date('d.m.Y H:i:s') }} + + {% if not p.active %} +
+ +
+ +
+ +
+ {% else %} + - + {% endif %} + +
+ +
+
+ +{% endblock %}