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 @@
- Ingest Jobs
+ Indexierung Jobs (Ingest)
+
+
+
+
+ Wissen (Chunk-Index)
+
+
+
+
+ System Prompt Settings
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 %}
+
+ | {{ key }} |
+
+ {% if value is iterable %}
+
+{{ value|json_encode(constant('JSON_PRETTY_PRINT')) }}
+
+ {% else %}
+ {{ value }}
+ {% endif %}
+ |
+
+ {% endfor %}
+
+
+ {% endif %}
+
+
+
#}
+
+ {# ============================= #}
+ {# NDJSON Section #}
+ {# ============================= #}
+
+ {% set currentPage = ndjson.page|default(1) %}
+ {% set currentLimit = ndjson.limit|default(50) %}
+
+
+
+
+
+
NdJson Index Übersicht Chunks (index.ndjson)
+
+
+
+
+ {% 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 }}
+
+
+
+
+
+
+ | chunk_id |
+ document_id |
+ text (gekürzt) |
+
+
+
+ {% for item in ndjson.items|default([]) %}
+
+ | {{ 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')) }}
+
+
+ |
+
+ {% else %}
+
+ |
+ Keine Einträge gefunden.
+ |
+
+ {% endfor %}
+
+
+
+
+
+
+
+{% 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
+
+
+
+
+ | Version |
+ Aktiv |
+ Kommentar |
+ Erstellt |
+ Aktionen |
+
+
+
+ {% for p in all %}
+
+ | {{ p.version }} |
+
+ {% if p.active %}
+ ACTIVE
+ {% endif %}
+ |
+ {{ p.comment ?? '-' }} |
+ {{ p.createdAt|date('d.m.Y H:i:s') }} |
+
+
+ {% if not p.active %}
+
+
+
+ {% else %}
+ -
+ {% endif %}
+
+ |
+
+ {% endfor %}
+
+
+
+
+
+
+{% endblock %}