optimize ui
add new ki endpoint params
This commit is contained in:
@@ -76,9 +76,10 @@ services:
|
|||||||
|
|
||||||
App\Infrastructure\OllamaClient:
|
App\Infrastructure\OllamaClient:
|
||||||
arguments:
|
arguments:
|
||||||
$apiUrl: '%env(AI_LLM_API_URL)%'
|
$apiUrl: '%env(OLLAMA_API_URL)%'
|
||||||
$model: '%env(AI_LLM_MODEL)%'
|
$model: '%env(OLLAMA_MODEL)%'
|
||||||
$timeoutSeconds: '%env(int:AI_LLM_TIMEOUT)%'
|
$timeoutSeconds: 600
|
||||||
|
$configProvider: '@App\Service\ModelGenerationConfigProvider'
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
# AI Agent – Context & Runner
|
# AI Agent – Context & Runner
|
||||||
|
|||||||
103
src/Controller/Admin/ModelGenerationConfigController.php
Normal file
103
src/Controller/Admin/ModelGenerationConfigController.php
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Controller\Admin;
|
||||||
|
|
||||||
|
use App\Entity\ModelGenerationConfig;
|
||||||
|
use App\Repository\ModelGenerationConfigRepository;
|
||||||
|
use App\Service\ModelGenerationConfigManager;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
|
||||||
|
#[Route('/admin/model-config')]
|
||||||
|
class ModelGenerationConfigController extends AbstractController
|
||||||
|
{
|
||||||
|
#[Route('/', name: 'admin_model_config_list')]
|
||||||
|
public function list(ModelGenerationConfigRepository $repository): Response
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_KNOWLEDGE_ADMIN');
|
||||||
|
|
||||||
|
$configs = $repository->findBy([], ['modelName' => 'ASC', 'version' => 'DESC']);
|
||||||
|
|
||||||
|
return $this->render('admin/model_config/list.html.twig', [
|
||||||
|
'configs' => $configs,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/create', name: 'admin_model_config_create')]
|
||||||
|
public function create(
|
||||||
|
Request $request,
|
||||||
|
EntityManagerInterface $em,
|
||||||
|
ModelGenerationConfigRepository $repository
|
||||||
|
): Response {
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
|
||||||
|
|
||||||
|
if ($request->isMethod('POST')) {
|
||||||
|
|
||||||
|
$modelName = $request->request->get('model_name');
|
||||||
|
|
||||||
|
$version = $repository->findNextVersion($modelName);
|
||||||
|
|
||||||
|
$config = new ModelGenerationConfig(
|
||||||
|
modelName: $modelName,
|
||||||
|
version: $version,
|
||||||
|
stream: (bool)$request->request->get('stream'),
|
||||||
|
temperature: (float)$request->request->get('temperature'),
|
||||||
|
topK: (int)$request->request->get('top_k'),
|
||||||
|
topP: (float)$request->request->get('top_p'),
|
||||||
|
repeatPenalty: (float)$request->request->get('repeat_penalty'),
|
||||||
|
numCtx: (int)$request->request->get('num_ctx'),
|
||||||
|
active: false
|
||||||
|
);
|
||||||
|
|
||||||
|
$em->persist($config);
|
||||||
|
$em->flush();
|
||||||
|
|
||||||
|
return $this->redirectToRoute('admin_model_config_list');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('admin/model_config/create.html.twig');
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/{id}/activate', name: 'admin_model_config_activate')]
|
||||||
|
public function activate(
|
||||||
|
ModelGenerationConfig $config,
|
||||||
|
ModelGenerationConfigManager $manager
|
||||||
|
): Response {
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
|
||||||
|
|
||||||
|
$manager->activate($config);
|
||||||
|
|
||||||
|
return $this->redirectToRoute('admin_model_config_list');
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/{id}/delete', name: 'admin_model_config_delete', methods: ['POST'])]
|
||||||
|
public function delete(
|
||||||
|
ModelGenerationConfig $config,
|
||||||
|
Request $request,
|
||||||
|
EntityManagerInterface $em
|
||||||
|
): Response {
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
|
||||||
|
|
||||||
|
if ($config->isActive()) {
|
||||||
|
$this->addFlash('danger', 'Aktive Konfiguration kann nicht gelöscht werden.');
|
||||||
|
return $this->redirectToRoute('admin_model_config_list');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->isCsrfTokenValid('delete_model_config_'.$config->getId(), $request->request->get('_token'))) {
|
||||||
|
throw $this->createAccessDeniedException('Invalid CSRF token.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$em->remove($config);
|
||||||
|
$em->flush();
|
||||||
|
|
||||||
|
$this->addFlash('success', 'Konfiguration gelöscht.');
|
||||||
|
|
||||||
|
return $this->redirectToRoute('admin_model_config_list');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -32,4 +32,5 @@ final class SystemAgentController extends AbstractController
|
|||||||
'debugCount'=>count($ndjsonPage['items'])
|
'debugCount'=>count($ndjsonPage['items'])
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
90
src/Entity/ModelGenerationConfig.php
Normal file
90
src/Entity/ModelGenerationConfig.php
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Entity;
|
||||||
|
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
use Symfony\Component\Uid\Uuid;
|
||||||
|
|
||||||
|
#[ORM\Entity(repositoryClass: \App\Repository\ModelGenerationConfigRepository::class)]
|
||||||
|
#[ORM\Table(name: 'model_generation_config')]
|
||||||
|
#[ORM\Index(columns: ['model_name', 'active'], name: 'idx_model_active')]
|
||||||
|
class ModelGenerationConfig
|
||||||
|
{
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\Column(type: 'uuid', unique: true)]
|
||||||
|
private Uuid $id;
|
||||||
|
|
||||||
|
#[ORM\Column(name: 'model_name', type: 'string', length: 120)]
|
||||||
|
private string $modelName;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'boolean')]
|
||||||
|
private bool $stream;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'float')]
|
||||||
|
private float $temperature;
|
||||||
|
|
||||||
|
#[ORM\Column(name: 'top_k', type: 'integer')]
|
||||||
|
private int $topK;
|
||||||
|
|
||||||
|
#[ORM\Column(name: 'top_p', type: 'float')]
|
||||||
|
private float $topP;
|
||||||
|
|
||||||
|
#[ORM\Column(name: 'repeat_penalty', type: 'float')]
|
||||||
|
private float $repeatPenalty;
|
||||||
|
|
||||||
|
#[ORM\Column(name: 'num_ctx', type: 'integer')]
|
||||||
|
private int $numCtx;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'boolean')]
|
||||||
|
private bool $active;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'integer')]
|
||||||
|
private int $version;
|
||||||
|
|
||||||
|
#[ORM\Column(name: 'created_at', type: 'datetime_immutable')]
|
||||||
|
private \DateTimeImmutable $createdAt;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
string $modelName,
|
||||||
|
int $version,
|
||||||
|
bool $stream = false,
|
||||||
|
float $temperature = 0.1,
|
||||||
|
int $topK = 20,
|
||||||
|
float $topP = 0.8,
|
||||||
|
float $repeatPenalty = 1.05,
|
||||||
|
int $numCtx = 4096,
|
||||||
|
bool $active = false
|
||||||
|
) {
|
||||||
|
$this->id = Uuid::v4();
|
||||||
|
$this->modelName = $modelName;
|
||||||
|
$this->version = $version;
|
||||||
|
$this->stream = $stream;
|
||||||
|
$this->temperature = $temperature;
|
||||||
|
$this->topK = $topK;
|
||||||
|
$this->topP = $topP;
|
||||||
|
$this->repeatPenalty = $repeatPenalty;
|
||||||
|
$this->numCtx = $numCtx;
|
||||||
|
$this->active = $active;
|
||||||
|
$this->createdAt = new \DateTimeImmutable();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getId(): Uuid { return $this->id; }
|
||||||
|
public function getModelName(): string { return $this->modelName; }
|
||||||
|
public function isStream(): bool { return $this->stream; }
|
||||||
|
public function getTemperature(): float { return $this->temperature; }
|
||||||
|
public function getTopK(): int { return $this->topK; }
|
||||||
|
public function getTopP(): float { return $this->topP; }
|
||||||
|
public function getRepeatPenalty(): float { return $this->repeatPenalty; }
|
||||||
|
public function getNumCtx(): int { return $this->numCtx; }
|
||||||
|
public function isActive(): bool { return $this->active; }
|
||||||
|
public function getVersion(): int { return $this->version; }
|
||||||
|
public function getCreatedAt(): \DateTimeImmutable { return $this->createdAt; }
|
||||||
|
|
||||||
|
// Nur vom Manager nutzen
|
||||||
|
public function setActive(bool $active): void
|
||||||
|
{
|
||||||
|
$this->active = $active;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,69 +4,54 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Infrastructure;
|
namespace App\Infrastructure;
|
||||||
|
|
||||||
|
use App\Entity\ModelGenerationConfig;
|
||||||
|
use App\Service\ModelGenerationConfigProvider;
|
||||||
use Generator;
|
use Generator;
|
||||||
use JsonException;
|
use JsonException;
|
||||||
use RuntimeException;
|
use RuntimeException;
|
||||||
use Throwable;
|
use Throwable;
|
||||||
|
|
||||||
/**
|
|
||||||
* OllamaClient
|
|
||||||
*
|
|
||||||
* Production-ready streaming client for Ollama-compatible LLM backends.
|
|
||||||
*
|
|
||||||
* Key properties:
|
|
||||||
* - True live streaming (tokens are yielded while the request is running)
|
|
||||||
* - PHP-safe (no yield inside cURL callbacks)
|
|
||||||
* - Works for both HTTP streaming and CLI usage
|
|
||||||
* - Deterministic and resource-safe
|
|
||||||
*
|
|
||||||
* Implementation strategy:
|
|
||||||
* - Use curl_multi_* to keep control of the execution loop
|
|
||||||
* - Accumulate partial chunks into a rolling buffer
|
|
||||||
* - Extract JSON lines incrementally
|
|
||||||
* - Yield tokens immediately when they arrive
|
|
||||||
*/
|
|
||||||
final class OllamaClient
|
final class OllamaClient
|
||||||
{
|
{
|
||||||
private string $apiUrl;
|
private ?ModelGenerationConfig $cachedConfig = null;
|
||||||
private string $model;
|
|
||||||
private int $timeoutSeconds;
|
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
string $apiUrl,
|
private string $apiUrl,
|
||||||
string $model,
|
private string $model,
|
||||||
int $timeoutSeconds,
|
private int $timeoutSeconds,
|
||||||
)
|
private ModelGenerationConfigProvider $configProvider
|
||||||
{
|
) {}
|
||||||
$this->apiUrl = $apiUrl;
|
|
||||||
$this->model = $model;
|
|
||||||
$this->timeoutSeconds = $timeoutSeconds;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Streams tokens from the LLM backend in real time.
|
* Public Streaming API
|
||||||
*
|
|
||||||
* @param string $prompt Fully constructed prompt
|
|
||||||
*
|
|
||||||
* @return Generator<string>
|
|
||||||
* @throws JsonException
|
|
||||||
*/
|
*/
|
||||||
public function stream(string $prompt): Generator
|
public function stream(string $prompt): Generator
|
||||||
{
|
{
|
||||||
$json = [];
|
$config = $this->getConfig();
|
||||||
|
|
||||||
$payload = json_encode([
|
if ($config->isStream()) {
|
||||||
'model' => $this->model,
|
yield from $this->streamInternal($prompt);
|
||||||
'prompt' => $prompt,
|
return;
|
||||||
'stream' => true,
|
}
|
||||||
'options' => [
|
|
||||||
"temperature" => 0.9,
|
// Fallback: Blocking generate → Generator-kompatibel ausgeben
|
||||||
"top_k" => 35,
|
yield $this->generateInternal($prompt);
|
||||||
"top_p" => 0.9,
|
}
|
||||||
"repeat_penalty" => 1.1,
|
|
||||||
"num_ctx" => 8192
|
/**
|
||||||
]
|
* Public Blocking API
|
||||||
], JSON_THROW_ON_ERROR);
|
*/
|
||||||
|
public function generate(string $prompt): string
|
||||||
|
{
|
||||||
|
return $this->generateInternal($prompt);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal streaming transport
|
||||||
|
*/
|
||||||
|
private function streamInternal(string $prompt): Generator
|
||||||
|
{
|
||||||
|
$payload = $this->buildPayload($prompt, true);
|
||||||
|
|
||||||
$buffer = '';
|
$buffer = '';
|
||||||
$done = false;
|
$done = false;
|
||||||
@@ -82,7 +67,7 @@ final class OllamaClient
|
|||||||
CURLOPT_POSTFIELDS => $payload,
|
CURLOPT_POSTFIELDS => $payload,
|
||||||
CURLOPT_RETURNTRANSFER => false,
|
CURLOPT_RETURNTRANSFER => false,
|
||||||
CURLOPT_TIMEOUT => $this->timeoutSeconds,
|
CURLOPT_TIMEOUT => $this->timeoutSeconds,
|
||||||
CURLOPT_WRITEFUNCTION => function ($curl, string $data) use (&$buffer, &$done): int {
|
CURLOPT_WRITEFUNCTION => function ($curl, string $data) use (&$buffer): int {
|
||||||
$buffer .= $data;
|
$buffer .= $data;
|
||||||
return strlen($data);
|
return strlen($data);
|
||||||
},
|
},
|
||||||
@@ -98,12 +83,10 @@ final class OllamaClient
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
do {
|
do {
|
||||||
// Execute the multi handle
|
|
||||||
do {
|
do {
|
||||||
$status = curl_multi_exec($mh, $running);
|
$status = curl_multi_exec($mh, $running);
|
||||||
} while ($status === CURLM_CALL_MULTI_PERFORM);
|
} while ($status === CURLM_CALL_MULTI_PERFORM);
|
||||||
|
|
||||||
// Read incoming data from the buffer
|
|
||||||
while (($pos = strpos($buffer, "\n")) !== false) {
|
while (($pos = strpos($buffer, "\n")) !== false) {
|
||||||
$line = trim(substr($buffer, 0, $pos));
|
$line = trim(substr($buffer, 0, $pos));
|
||||||
$buffer = substr($buffer, $pos + 1);
|
$buffer = substr($buffer, $pos + 1);
|
||||||
@@ -127,37 +110,94 @@ final class OllamaClient
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for network activity
|
|
||||||
if ($running) {
|
if ($running) {
|
||||||
curl_multi_select($mh, 0.2);
|
curl_multi_select($mh, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
} while ($running && !$done);
|
} while ($running && !$done);
|
||||||
|
|
||||||
// Flush remaining buffer (edge case)
|
|
||||||
if (!$done && trim($buffer) !== '') {
|
|
||||||
try {
|
|
||||||
$json = json_decode(trim($buffer), true, flags: JSON_THROW_ON_ERROR);
|
|
||||||
if (isset($json['response'])) {
|
|
||||||
yield $json['response'];
|
|
||||||
}
|
|
||||||
} catch (Throwable) {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isset($json['response'])) {
|
|
||||||
yield $json;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (curl_errno($ch)) {
|
if (curl_errno($ch)) {
|
||||||
$error = curl_error($ch);
|
throw new RuntimeException('LLM connection error: ' . curl_error($ch));
|
||||||
throw new RuntimeException('LLM connection error: ' . $error);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} finally {
|
} finally {
|
||||||
curl_multi_remove_handle($mh, $ch);
|
curl_multi_remove_handle($mh, $ch);
|
||||||
curl_multi_close($mh);
|
curl_multi_close($mh);
|
||||||
curl_close($ch);
|
curl_close($ch);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal blocking transport
|
||||||
|
*/
|
||||||
|
private function generateInternal(string $prompt): string
|
||||||
|
{
|
||||||
|
$payload = $this->buildPayload($prompt, false);
|
||||||
|
|
||||||
|
$ch = curl_init($this->apiUrl);
|
||||||
|
if ($ch === false) {
|
||||||
|
throw new RuntimeException('Failed to initialize cURL');
|
||||||
|
}
|
||||||
|
|
||||||
|
curl_setopt_array($ch, [
|
||||||
|
CURLOPT_POST => true,
|
||||||
|
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
|
||||||
|
CURLOPT_POSTFIELDS => $payload,
|
||||||
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
|
CURLOPT_TIMEOUT => $this->timeoutSeconds,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
|
||||||
|
if ($response === false) {
|
||||||
|
throw new RuntimeException('LLM error: ' . curl_error($ch));
|
||||||
|
}
|
||||||
|
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
$json = json_decode($response, true, flags: JSON_THROW_ON_ERROR);
|
||||||
|
|
||||||
|
return $json['response'] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Central Payload Builder (DRY)
|
||||||
|
*/
|
||||||
|
private function buildPayload(string $prompt, bool $stream): string
|
||||||
|
{
|
||||||
|
return json_encode([
|
||||||
|
'model' => $this->model,
|
||||||
|
'prompt' => $prompt,
|
||||||
|
'stream' => $stream,
|
||||||
|
'options' => $this->buildOptions()
|
||||||
|
], JSON_THROW_ON_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Central Options Builder (DRY)
|
||||||
|
*/
|
||||||
|
private function buildOptions(): array
|
||||||
|
{
|
||||||
|
$config = $this->getConfig();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'temperature' => $config->getTemperature(),
|
||||||
|
'top_k' => $config->getTopK(),
|
||||||
|
'top_p' => $config->getTopP(),
|
||||||
|
'repeat_penalty' => $config->getRepeatPenalty(),
|
||||||
|
'num_ctx' => $config->getNumCtx(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Config caching per request
|
||||||
|
*/
|
||||||
|
private function getConfig(): ModelGenerationConfig
|
||||||
|
{
|
||||||
|
if ($this->cachedConfig === null) {
|
||||||
|
$this->cachedConfig = $this->configProvider->getActiveForModel($this->model);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->cachedConfig;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
41
src/Repository/ModelGenerationConfigRepository.php
Normal file
41
src/Repository/ModelGenerationConfigRepository.php
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Repository;
|
||||||
|
|
||||||
|
use App\Entity\ModelGenerationConfig;
|
||||||
|
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||||
|
use Doctrine\Persistence\ManagerRegistry;
|
||||||
|
|
||||||
|
final class ModelGenerationConfigRepository extends ServiceEntityRepository
|
||||||
|
{
|
||||||
|
public function __construct(ManagerRegistry $registry)
|
||||||
|
{
|
||||||
|
parent::__construct($registry, ModelGenerationConfig::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findActiveForModel(string $modelName): ?ModelGenerationConfig
|
||||||
|
{
|
||||||
|
return $this->createQueryBuilder('c')
|
||||||
|
->andWhere('c.modelName = :model')
|
||||||
|
->andWhere('c.active = true')
|
||||||
|
->setParameter('model', $modelName)
|
||||||
|
->orderBy('c.version', 'DESC')
|
||||||
|
->setMaxResults(1)
|
||||||
|
->getQuery()
|
||||||
|
->getOneOrNullResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findNextVersion(string $modelName): int
|
||||||
|
{
|
||||||
|
$qb = $this->createQueryBuilder('c')
|
||||||
|
->select('MAX(c.version)')
|
||||||
|
->andWhere('c.modelName = :model')
|
||||||
|
->setParameter('model', $modelName);
|
||||||
|
|
||||||
|
$max = $qb->getQuery()->getSingleScalarResult();
|
||||||
|
|
||||||
|
return $max ? ((int)$max + 1) : 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
72
src/Service/ModelGenerationConfigManager.php
Normal file
72
src/Service/ModelGenerationConfigManager.php
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Service;
|
||||||
|
|
||||||
|
use App\Entity\ModelGenerationConfig;
|
||||||
|
use App\Repository\ModelGenerationConfigRepository;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
|
||||||
|
final class ModelGenerationConfigManager
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private EntityManagerInterface $em,
|
||||||
|
private ModelGenerationConfigRepository $repository
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function activate(ModelGenerationConfig $config): void
|
||||||
|
{
|
||||||
|
$this->validate($config);
|
||||||
|
|
||||||
|
$this->em->wrapInTransaction(function () use ($config) {
|
||||||
|
|
||||||
|
// Alle aktiven für dieses Modell deaktivieren
|
||||||
|
$activeConfigs = $this->repository->findBy([
|
||||||
|
'modelName' => $config->getModelName(),
|
||||||
|
'active' => true
|
||||||
|
]);
|
||||||
|
|
||||||
|
foreach ($activeConfigs as $active) {
|
||||||
|
$active->setActive(false);
|
||||||
|
$this->em->persist($active);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Diese aktivieren
|
||||||
|
$config->setActive(true);
|
||||||
|
$this->em->persist($config);
|
||||||
|
|
||||||
|
$this->em->flush();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private function validate(ModelGenerationConfig $config): void
|
||||||
|
{
|
||||||
|
// Guardrails
|
||||||
|
|
||||||
|
if ($config->getTemperature() < 0.0 || $config->getTemperature() > 2.0) {
|
||||||
|
throw new \InvalidArgumentException('Temperature must be between 0.0 and 2.0');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($config->getTopP() <= 0.0 || $config->getTopP() > 1.0) {
|
||||||
|
throw new \InvalidArgumentException('Top P must be between 0.0 and 1.0');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($config->getTopK() <= 0) {
|
||||||
|
throw new \InvalidArgumentException('Top K must be > 0');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($config->getRepeatPenalty() < 0.0 || $config->getRepeatPenalty() > 5.0) {
|
||||||
|
throw new \InvalidArgumentException('Repeat Penalty out of allowed range');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($config->getNumCtx() < 512 || $config->getNumCtx() > 32768) {
|
||||||
|
throw new \InvalidArgumentException('Num Ctx outside safe range');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enterprise RAG Warnbereich (optional, nur Logging)
|
||||||
|
if ($config->getTemperature() > 0.7) {
|
||||||
|
// hier könntest du optional Logging einbauen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
42
src/Service/ModelGenerationConfigProvider.php
Normal file
42
src/Service/ModelGenerationConfigProvider.php
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Service;
|
||||||
|
|
||||||
|
use App\Entity\ModelGenerationConfig;
|
||||||
|
use App\Repository\ModelGenerationConfigRepository;
|
||||||
|
|
||||||
|
final class ModelGenerationConfigProvider
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private ModelGenerationConfigRepository $repository
|
||||||
|
)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getActiveForModel(string $modelName): ModelGenerationConfig
|
||||||
|
{
|
||||||
|
$config = $this->repository->findActiveForModel($modelName);
|
||||||
|
|
||||||
|
if ($config !== null) {
|
||||||
|
return $config;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------
|
||||||
|
// Safe Enterprise Default Fallback
|
||||||
|
// ------------------------------
|
||||||
|
return new ModelGenerationConfig(
|
||||||
|
modelName: $modelName,
|
||||||
|
version: 0,
|
||||||
|
stream: false,
|
||||||
|
temperature: 0.1,
|
||||||
|
topK: 20,
|
||||||
|
topP: 0.8,
|
||||||
|
repeatPenalty: 1.05,
|
||||||
|
numCtx: 4096,
|
||||||
|
active: false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,63 +8,113 @@
|
|||||||
<link href="/assets/styles/bootstrap.min.css" rel="stylesheet"/>
|
<link href="/assets/styles/bootstrap.min.css" rel="stylesheet"/>
|
||||||
<link rel="stylesheet" href="/assets/styles/base.css">
|
<link rel="stylesheet" href="/assets/styles/base.css">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="bg-dark text-light">
|
<body class="bg-dark text-light">
|
||||||
|
|
||||||
<nav class="navbar navbar-dark bg-black text-info px-3">
|
{# ============================= #}
|
||||||
<span class="navbar-brand">mitho Admin</span>
|
{# Top Navigation #}
|
||||||
|
{# ============================= #}
|
||||||
|
|
||||||
|
<nav class="navbar navbar-dark bg-black border-bottom border-secondary px-3">
|
||||||
|
<span class="navbar-brand fw-semibold text-info">
|
||||||
|
mitho Admin
|
||||||
|
</span>
|
||||||
|
|
||||||
<div class="ms-auto d-flex align-items-center gap-3">
|
<div class="ms-auto d-flex align-items-center gap-3">
|
||||||
{% if app.user %}
|
{% if app.user %}
|
||||||
<span class="small text-secondary">{{ app.user.userIdentifier }}</span>
|
<span class="small text-light">
|
||||||
<a class="btn btn-sm btn-outline-light" href="{{ path('admin_logout') }}">Logout</a>
|
{{ app.user.userIdentifier }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<a class="btn btn-sm btn-outline-light"
|
||||||
|
href="{{ path('admin_logout') }}">
|
||||||
|
Logout
|
||||||
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
{# ============================= #}
|
||||||
|
{# Layout Container #}
|
||||||
|
{# ============================= #}
|
||||||
|
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<div class="row">
|
<div class="row flex-nowrap">
|
||||||
<div class="col-2 bg-black text-info border-end border-secondary min-vh-100 pt-3">
|
|
||||||
<ul class="nav flex-column">
|
{# ============================= #}
|
||||||
<li class="nav-item">
|
{# Sidebar #}
|
||||||
<a class="nav-link text-light" href="{{ path('admin_dashboard') }}">Dashboard</a>
|
{# ============================= #}
|
||||||
</li>
|
{% if app.user %}
|
||||||
</ul>
|
<aside class="col-auto col-md-3 col-lg-2 bg-black border-end border-secondary min-vh-100 pt-3">
|
||||||
<hr>
|
|
||||||
<h3>Dokumente und Wissen</h3>
|
{% set route = app.request.attributes.get('_route') %}
|
||||||
<ul class="nav flex-column">
|
|
||||||
<li class="nav-item">
|
<nav class="nav flex-column small">
|
||||||
<a class="nav-link text-light" href="{{ path('admin_documents') }}">Dokumente</a>
|
|
||||||
</li>
|
<a class="nav-link text-light {% if route starts with 'admin_dashboard' %}active fw-bold{% endif %}"
|
||||||
<li class="nav-item">
|
href="{{ path('admin_dashboard') }}">
|
||||||
<a class="nav-link text-light" href="{{ path('admin_jobs') }}">
|
Dashboard
|
||||||
Indexierung (Ingest Jobs)
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
<hr class="border-secondary">
|
||||||
<a class="nav-link text-light" href="{{ path('admin_system_agent') }}">
|
|
||||||
Wissensdaten (Chunk-Index)
|
<div class="text-info text-uppercase small mb-2">
|
||||||
</a>
|
Dokumente & Wissen
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<hr>
|
|
||||||
<h3>System-Profile</h3>
|
|
||||||
<ul class="nav flex-column">
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link text-light" href="{{ path('admin_system_prompt') }}">
|
|
||||||
System-Prompt-Profil
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link text-light" href="{{ path('admin_ingest_profile_list') }}">
|
|
||||||
Indexierungs-Profil (Ingest Profiles)
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-10 pt-3">
|
<a class="nav-link text-light {% if route starts with 'admin_document' %}active fw-bold{% endif %}"
|
||||||
{% block body %}{% endblock %}
|
href="{{ path('admin_documents') }}">
|
||||||
|
Dokumente
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a class="nav-link text-light {% if route starts with 'admin_job' %}active fw-bold{% endif %}"
|
||||||
|
href="{{ path('admin_jobs') }}">
|
||||||
|
Ingest Jobs
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a class="nav-link text-light {% if route starts with 'admin_system_agent' %}active fw-bold{% endif %}"
|
||||||
|
href="{{ path('admin_system_agent') }}">
|
||||||
|
Chunk-Index
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<hr class="border-secondary">
|
||||||
|
|
||||||
|
<div class="text-info text-uppercase small mb-2">
|
||||||
|
System-Profile
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<a class="nav-link text-light {% if route starts with 'admin_system_prompt' %}active fw-bold{% endif %}"
|
||||||
|
href="{{ path('admin_system_prompt') }}">
|
||||||
|
System Prompt
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a class="nav-link text-light {% if route starts with 'admin_ingest_profile' %}active fw-bold{% endif %}"
|
||||||
|
href="{{ path('admin_ingest_profile_list') }}">
|
||||||
|
Ingest Profiles
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<hr class="border-secondary">
|
||||||
|
|
||||||
|
<div class="text-info text-uppercase small mb-2">
|
||||||
|
KI-Endpunkte
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a class="nav-link text-light {% if route starts with 'admin_model_config' %}active fw-bold{% endif %}"
|
||||||
|
href="{{ path('admin_model_config_list') }}">
|
||||||
|
Modell-Generierung
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
{% endif %}
|
||||||
|
{# ============================= #}
|
||||||
|
{# Main Content #}
|
||||||
|
{# ============================= #}
|
||||||
|
|
||||||
|
<main class="col py-4 px-4">
|
||||||
|
{% block body %}{% endblock %}
|
||||||
|
</main>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -3,52 +3,92 @@
|
|||||||
{% block title %}Admin Dashboard{% endblock %}
|
{% block title %}Admin Dashboard{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<h1 class="h4 mb-3">Dashboard</h1>
|
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h1 class="h3 mb-0">Dashboard</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
{# ============================= #}
|
{# ============================= #}
|
||||||
{# USER + RESET CARD #}
|
{# USER INFO CARD #}
|
||||||
{# ============================= #}
|
{# ============================= #}
|
||||||
<div class="card bg-black text-info border-secondary mb-4">
|
|
||||||
|
<div class="card bg-black border-secondary mb-4 text-light">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
|
||||||
|
<h5 class="text-info mb-3">System Benutzer</h5>
|
||||||
|
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<strong>User:</strong> {{ app.user.userIdentifier }}
|
<strong>User:</strong>
|
||||||
</div>
|
{{ app.user.userIdentifier }}
|
||||||
<div class="mb-2">
|
|
||||||
<strong>Rollen:</strong> {{ app.user.roles|join(', ') }}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class="border-secondary">
|
<div class="mb-2">
|
||||||
|
<strong>Rollen:</strong>
|
||||||
|
{{ app.user.roles|join(', ') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="text-light">
|
</div>
|
||||||
<p class="fw-bold">Reset des Systems</p>
|
</div>
|
||||||
<p>Unwiderruflicher Reset des gesamten Systems</p>
|
|
||||||
|
{# ============================= #}
|
||||||
|
{# SYSTEM RESET CARD #}
|
||||||
|
{# ============================= #}
|
||||||
|
|
||||||
|
{% if is_granted('ROLE_SUPER_ADMIN') %}
|
||||||
|
|
||||||
|
<div class="card bg-black border-danger mb-4 text-light">
|
||||||
|
<div class="card-body">
|
||||||
|
|
||||||
|
<h5 class="text-danger mb-3">System Reset</h5>
|
||||||
|
|
||||||
|
<div class="small text-light mb-3">
|
||||||
|
Der Reset entfernt:
|
||||||
|
<ul class="mb-2">
|
||||||
|
<li>Alle Dokumente und Versionen</li>
|
||||||
|
<li>Den gesamten NDJSON-Index</li>
|
||||||
|
<li>Den FAISS-Vektorindex</li>
|
||||||
|
<li>Alle Ingest-Jobs</li>
|
||||||
|
</ul>
|
||||||
|
Diese Aktion ist <strong>irreversibel</strong>.
|
||||||
|
</div>
|
||||||
|
|
||||||
{% for label, messages in app.flashes %}
|
{% for label, messages in app.flashes %}
|
||||||
{% for message in messages %}
|
{% for message in messages %}
|
||||||
<div class="alert alert-{{ label }} fade show" role="alert">
|
<div class="alert alert-{{ label }}">
|
||||||
{{ message }}
|
{{ message }}
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
<form method="post" action="/admin/documents/reset" onsubmit="return resetSystem()">
|
<form method="post"
|
||||||
<button type="submit" class="btn btn-outline-danger">
|
action="{{ path('admin_document_reset') }}"
|
||||||
Reset System
|
onsubmit="return confirm('Wirklich das gesamte System zurücksetzen? Diese Aktion ist endgültig.');">
|
||||||
|
|
||||||
|
<input type="hidden"
|
||||||
|
name="_token"
|
||||||
|
value="{{ csrf_token('system_reset') }}">
|
||||||
|
|
||||||
|
<button type="submit"
|
||||||
|
class="btn btn-outline-danger">
|
||||||
|
System vollständig zurücksetzen
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{# ============================= #}
|
{# ============================= #}
|
||||||
{# KNOWLEDGE INDEX STATUS CARD #}
|
{# KNOWLEDGE INDEX STATUS #}
|
||||||
{# ============================= #}
|
{# ============================= #}
|
||||||
|
|
||||||
{% set percent = chunkLimit > 0 ? (chunkCount / chunkLimit * 100)|round(1) : 0 %}
|
{% set percent = chunkLimit > 0 ? (chunkCount / chunkLimit * 100)|round(1) : 0 %}
|
||||||
|
|
||||||
<div class="card bg-black text-light border-secondary">
|
<div class="card bg-black border-secondary text-light">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
|
||||||
<h5 class="text-info mb-3">Knowledge Index</h5>
|
<h5 class="text-info mb-3">Knowledge Index Status</h5>
|
||||||
|
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<strong>Chunks:</strong>
|
<strong>Chunks:</strong>
|
||||||
@@ -60,31 +100,31 @@
|
|||||||
<div class="progress bg-dark" style="height: 18px;">
|
<div class="progress bg-dark" style="height: 18px;">
|
||||||
<div
|
<div
|
||||||
class="progress-bar
|
class="progress-bar
|
||||||
{% if chunkCount > 115000 %}
|
{% if percent >= 95 %}
|
||||||
bg-danger
|
bg-danger
|
||||||
{% elseif chunkCount > 100000 %}
|
{% elseif percent >= 85 %}
|
||||||
bg-warning text-dark
|
bg-warning text-dark
|
||||||
{% else %}
|
{% else %}
|
||||||
bg-success
|
bg-success
|
||||||
{% endif %}
|
{% endif %}"
|
||||||
"
|
|
||||||
role="progressbar"
|
role="progressbar"
|
||||||
|
aria-valuenow="{{ percent }}"
|
||||||
|
aria-valuemin="0"
|
||||||
|
aria-valuemax="100"
|
||||||
style="width: {{ percent }}%;"
|
style="width: {{ percent }}%;"
|
||||||
>
|
>
|
||||||
{{ percent }}%
|
{{ percent }}%
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-2 small text-light">
|
<div class="mt-3 small text-secondary">
|
||||||
System ist für maximal 120.000 Chunks optimiert.
|
System ist für maximal {{ chunkLimit|number_format(0, ',', '.') }} Chunks optimiert.
|
||||||
|
{% if percent >= 95 %}
|
||||||
|
<br><strong class="text-danger">Kapazitätsgrenze nahezu erreicht.</strong>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
|
||||||
function resetSystem() {
|
|
||||||
return confirm('Sind Sie sicher, dass Sie das gesamte System zurücksetzen möchten?');
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -3,53 +3,73 @@
|
|||||||
{% block title %}Dokumente{% endblock %}
|
{% block title %}Dokumente{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
||||||
<h1 class="h4 mb-0">Dokumente</h1>
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
<a href="{{ path('admin_document_new') }}" class="btn btn-sm btn-light">
|
<h1 class="h3 mb-0">Dokumente</h1>
|
||||||
+ Neues Dokument
|
|
||||||
|
<a href="{{ path('admin_document_new') }}"
|
||||||
|
class="btn btn-sm btn-outline-info">
|
||||||
|
Neues Dokument
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if documents is empty %}
|
{% if documents is empty %}
|
||||||
|
|
||||||
<div class="alert alert-secondary">
|
<div class="alert alert-secondary">
|
||||||
Keine Dokumente vorhanden.
|
Keine Dokumente vorhanden.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="card bg-black text-info border-secondary">
|
|
||||||
|
<div class="card bg-black border-secondary">
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
<table class="table table-dark table-hover mb-0 align-middle">
|
|
||||||
<thead>
|
<table class="table table-dark table-striped table-hover align-middle mb-0">
|
||||||
|
<thead class="table-secondary text-dark">
|
||||||
<tr>
|
<tr>
|
||||||
<th>Titel</th>
|
<th>Titel</th>
|
||||||
<th>ID</th>
|
<th>ID</th>
|
||||||
<th>Typ</th>
|
<th>Typ</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
<th>Indexiert</th>
|
<th>Indexierung</th>
|
||||||
<th>Versionen</th>
|
<th>Versionen</th>
|
||||||
<th>Aktive Version</th>
|
<th>Aktive Version</th>
|
||||||
<th>Erstellt am</th>
|
<th>Erstellt</th>
|
||||||
<th>Aktionen</th>
|
<th class="text-end">Aktionen</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|
||||||
{% for document in documents %}
|
{% for document in documents %}
|
||||||
<tr>
|
<tr>
|
||||||
|
|
||||||
|
{# Titel #}
|
||||||
<td>
|
<td>
|
||||||
<a href="{{ path('admin_document_show', {id: document.id}) }}"
|
<a href="{{ path('admin_document_show', {id: document.id}) }}"
|
||||||
class="text-decoration-none text-light">
|
class="text-light text-decoration-none">
|
||||||
{{ document.title }}
|
{{ document.title }}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ document.id }}</td>
|
|
||||||
|
{# ID #}
|
||||||
|
<td class="small text-secondary">
|
||||||
|
{{ document.id }}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{# Typ #}
|
||||||
<td>
|
<td>
|
||||||
{% if document.currentVersion %}
|
{% if document.currentVersion %}
|
||||||
<span class="badge bg-secondary">
|
<span class="badge bg-secondary">
|
||||||
{{ document.currentVersion.fileTypeLabel }}
|
{{ document.currentVersion.fileTypeLabel }}
|
||||||
</span>
|
</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="badge bg-dark">-</span>
|
<span class="badge bg-dark border border-secondary">
|
||||||
|
-
|
||||||
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
{# Dokument Status #}
|
||||||
<td>
|
<td>
|
||||||
{% if document.status == 'ACTIVE' %}
|
{% if document.status == 'ACTIVE' %}
|
||||||
<span class="badge bg-success">Aktiv</span>
|
<span class="badge bg-success">Aktiv</span>
|
||||||
@@ -57,18 +77,32 @@
|
|||||||
<span class="badge bg-secondary">Archiviert</span>
|
<span class="badge bg-secondary">Archiviert</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
{# Ingest Status #}
|
||||||
<td>
|
<td>
|
||||||
|
{% if document.currentVersion %}
|
||||||
{% if document.currentVersion.ingestStatus == 'INDEXED' %}
|
{% if document.currentVersion.ingestStatus == 'INDEXED' %}
|
||||||
<span class="badge bg-success">
|
<span class="badge bg-success">INDEXED</span>
|
||||||
{{ document.currentVersion.ingestStatus }}
|
{% elseif document.currentVersion.ingestStatus == 'PENDING' %}
|
||||||
</span>
|
<span class="badge bg-warning text-dark">PENDING</span>
|
||||||
|
{% elseif document.currentVersion.ingestStatus == 'FAILED' %}
|
||||||
|
<span class="badge bg-danger">FAILED</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="badge bg-danger">
|
<span class="badge bg-dark border border-secondary">
|
||||||
{{ document.currentVersion.ingestStatus }}
|
{{ document.currentVersion.ingestStatus }}
|
||||||
</span>
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-dark border border-secondary">-</span>
|
||||||
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ document.versions|length }}</td>
|
|
||||||
|
{# Version Count #}
|
||||||
|
<td>
|
||||||
|
{{ document.versions|length }}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{# Aktive Version #}
|
||||||
<td>
|
<td>
|
||||||
{% if document.currentVersion %}
|
{% if document.currentVersion %}
|
||||||
v{{ document.currentVersion.versionNumber }}
|
v{{ document.currentVersion.versionNumber }}
|
||||||
@@ -76,33 +110,52 @@
|
|||||||
-
|
-
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ document.createdAt|date('d.m.Y H:i') }}</td>
|
|
||||||
<td class="d-flex gap-2">
|
|
||||||
|
|
||||||
<a class="btn btn-sm btn-outline-light"
|
{# Created At #}
|
||||||
|
<td class="small">
|
||||||
|
{{ document.createdAt|date('d.m.Y H:i') }}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{# Aktionen #}
|
||||||
|
<td class="text-end">
|
||||||
|
|
||||||
|
<a class="btn btn-sm btn-outline-light me-2"
|
||||||
href="{{ path('admin_document_show', {id: document.id}) }}">
|
href="{{ path('admin_document_show', {id: document.id}) }}">
|
||||||
Details
|
Details
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
{% if is_granted('ROLE_SUPER_ADMIN') %}
|
||||||
<form method="post"
|
<form method="post"
|
||||||
action="{{ path('admin_document_delete', {id: document.id}) }}"
|
action="{{ path('admin_document_delete', {id: document.id}) }}"
|
||||||
onsubmit="return confirm('Dokument wirklich endgültig löschen? Diese Aktion entfernt das Dokument aus Datenbank und Index.');">
|
class="d-inline"
|
||||||
|
onsubmit="return confirm('Dokument wirklich endgültig löschen? Diese Aktion entfernt Dokument, Versionen und Index-Daten.');">
|
||||||
|
|
||||||
<input type="hidden"
|
<input type="hidden"
|
||||||
name="_token"
|
name="_token"
|
||||||
value="{{ csrf_token('delete_document') }}">
|
value="{{ csrf_token('delete_document_' ~ document.id) }}">
|
||||||
|
|
||||||
<button class="btn btn-sm btn-outline-danger">
|
<button class="btn btn-sm btn-outline-danger">
|
||||||
Löschen
|
Löschen
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="mt-4 small text-secondary">
|
||||||
|
Hinweis: Das Löschen eines Dokuments entfernt alle Versionen und
|
||||||
|
erfordert eine Aktualisierung des NDJSON-Indexes.
|
||||||
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -3,24 +3,84 @@
|
|||||||
{% block title %}Neues Dokument{% endblock %}
|
{% block title %}Neues Dokument{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<h1 class="h4 mb-4">Neues Dokument</h1>
|
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h1 class="h3">Neues Dokument</h1>
|
||||||
|
|
||||||
|
<a href="{{ path('admin_documents') }}"
|
||||||
|
class="btn btn-sm btn-outline-secondary">
|
||||||
|
Zurück zur Übersicht
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card bg-black border-secondary text-light">
|
||||||
|
<div class="card-body">
|
||||||
|
|
||||||
<form method="post" enctype="multipart/form-data">
|
<form method="post" enctype="multipart/form-data">
|
||||||
|
|
||||||
<div class="mb-3">
|
<input type="hidden"
|
||||||
<label class="form-label">Titel:</label>
|
name="_token"
|
||||||
<div class="mb-2"><b>Bitte geben Sie einen aussagekräftigen Titel ein.</b><br>
|
value="{{ csrf_token('create_document') }}">
|
||||||
Der Titel ist entscheidend, damit in jedem Chunk ein sinnvoller thematischer Bezug hergestellt und eine saubere semantische Zuordnung ermöglicht werden kann.<br>
|
|
||||||
Wenn kein Titel angegeben wird, wird automatisch der Dateiname als Titel verwendet (nicht empfohlen).</div>
|
{# ============================= #}
|
||||||
<input class="form-control" name="title">
|
{# Titel #}
|
||||||
|
{# ============================= #}
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="form-label">Titel</label>
|
||||||
|
|
||||||
|
<div class="alert alert-secondary small">
|
||||||
|
<strong>Hinweis zur Qualität:</strong><br>
|
||||||
|
Der Titel ist entscheidend für die semantische Einordnung
|
||||||
|
der erzeugten Chunks. Jeder Chunk erhält den Titel als Kontext,
|
||||||
|
wodurch Retrieval und Antwortqualität signifikant verbessert werden.<br><br>
|
||||||
|
|
||||||
|
Wird kein Titel angegeben, wird automatisch der Dateiname
|
||||||
|
verwendet (nicht empfohlen).
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<input class="form-control bg-dark text-light border-secondary"
|
||||||
<label class="form-label">Datei:</label>
|
name="title"
|
||||||
<input type="file" class="form-control" name="file" required>
|
placeholder="z. B. Sicherheitsdatenblatt – Produkt XY">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="btn btn-light">Speichern</button>
|
{# ============================= #}
|
||||||
|
{# Datei Upload #}
|
||||||
|
{# ============================= #}
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="form-label">Datei</label>
|
||||||
|
|
||||||
|
<input type="file"
|
||||||
|
class="form-control bg-dark text-light border-secondary"
|
||||||
|
name="file"
|
||||||
|
required>
|
||||||
|
|
||||||
|
<div class="form-text text-secondary">
|
||||||
|
Unterstützte Formate: PDF, DOCX, TXT, MD.
|
||||||
|
Das Dokument wird versioniert gespeichert und anschließend
|
||||||
|
indexiert.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# ============================= #}
|
||||||
|
{# Submit #}
|
||||||
|
{# ============================= #}
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-end">
|
||||||
|
<button class="btn btn-outline-info">
|
||||||
|
Dokument speichern
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 small text-secondary">
|
||||||
|
Hinweis: Nach dem Upload wird automatisch eine neue Dokumentversion erstellt.
|
||||||
|
Die Indexierung erfolgt asynchron über einen Ingest-Job.
|
||||||
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,29 +1,86 @@
|
|||||||
{% extends 'admin/base.html.twig' %}
|
{% extends 'admin/base.html.twig' %}
|
||||||
|
|
||||||
{% block title %}Neue Version{% endblock %}
|
{% block title %}Neue Dokumentversion{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
|
|
||||||
<a href="{{ path('admin_document_show', {id: document.id}) }}"
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
class="btn btn-sm btn-outline-light mb-3">
|
<h1 class="h3 mb-0">
|
||||||
← Zurück
|
Neue Version
|
||||||
</a>
|
|
||||||
|
|
||||||
<h1 class="h4 mb-4">
|
|
||||||
Neue Version für: {{ document.title }}
|
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
|
<a href="{{ path('admin_document_show', {id: document.id}) }}"
|
||||||
|
class="btn btn-sm btn-outline-secondary">
|
||||||
|
Zurück zum Dokument
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card bg-black border-secondary mb-4 text-light">
|
||||||
|
<div class="card-body">
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<strong>Dokument:</strong>
|
||||||
|
<span class="text-light">{{ document.title }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="small text-secondary">
|
||||||
|
Das Hochladen einer neuen Version erzeugt eine zusätzliche
|
||||||
|
unveränderliche Dokumentversion. Die Aktivierung erfolgt separat
|
||||||
|
und löst einen deterministischen Re-Ingest aus.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card bg-black border-secondary text-light">
|
||||||
|
<div class="card-body">
|
||||||
|
|
||||||
<form method="post" enctype="multipart/form-data">
|
<form method="post" enctype="multipart/form-data">
|
||||||
|
|
||||||
<div class="mb-3">
|
<input type="hidden"
|
||||||
|
name="_token"
|
||||||
|
value="{{ csrf_token('create_document_version_' ~ document.id) }}">
|
||||||
|
|
||||||
|
{# ============================= #}
|
||||||
|
{# Datei Upload #}
|
||||||
|
{# ============================= #}
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
<label class="form-label">Datei auswählen</label>
|
<label class="form-label">Datei auswählen</label>
|
||||||
<input type="file" class="form-control" name="file" required>
|
|
||||||
|
<input type="file"
|
||||||
|
class="form-control bg-dark text-light border-secondary"
|
||||||
|
name="file"
|
||||||
|
required>
|
||||||
|
|
||||||
|
<div class="form-text text-secondary">
|
||||||
|
Unterstützte Formate: PDF, DOCX, TXT, MD.<br>
|
||||||
|
Die Datei wird versioniert gespeichert und mit einer
|
||||||
|
eindeutigen Checksum versehen.
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="btn btn-light">
|
{# ============================= #}
|
||||||
|
{# Submit #}
|
||||||
|
{# ============================= #}
|
||||||
|
|
||||||
|
{% if is_granted('ROLE_SUPER_ADMIN') %}
|
||||||
|
<div class="d-flex justify-content-end">
|
||||||
|
<button class="btn btn-outline-info">
|
||||||
Version hochladen
|
Version hochladen
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 small text-secondary">
|
||||||
|
Hinweis: Eine neue Version ersetzt nicht automatisch die aktive Version.
|
||||||
|
Erst nach Aktivierung wird ein Re-Ingest durchgeführt und der Index
|
||||||
|
neu aufgebaut.
|
||||||
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -4,14 +4,22 @@
|
|||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
|
|
||||||
<a href="{{ path('admin_documents') }}" class="btn btn-sm btn-outline-light mb-3">
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
← Zurück
|
<h1 class="h3 mb-0">{{ document.title }}</h1>
|
||||||
|
|
||||||
|
<a href="{{ path('admin_documents') }}"
|
||||||
|
class="btn btn-sm btn-outline-secondary">
|
||||||
|
Zurück zur Übersicht
|
||||||
</a>
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% if document %}
|
{% if document %}
|
||||||
<h1 class="h4 mb-3">{{ document.title }}</h1>
|
|
||||||
|
|
||||||
<div class="card bg-black text-info border-secondary mb-4">
|
{# ============================= #}
|
||||||
|
{# Dokument-Meta #}
|
||||||
|
{# ============================= #}
|
||||||
|
|
||||||
|
<div class="card bg-black border-secondary mb-5 text-light">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
@@ -25,7 +33,7 @@
|
|||||||
|
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<strong>Erstellt von:</strong>
|
<strong>Erstellt von:</strong>
|
||||||
{{ document.createdBy.email }}
|
{{ document.createdBy ? document.createdBy.email : '-' }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
@@ -36,7 +44,9 @@
|
|||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<strong>Aktive Version:</strong>
|
<strong>Aktive Version:</strong>
|
||||||
{% if document.currentVersion %}
|
{% if document.currentVersion %}
|
||||||
|
<span class="badge bg-info text-dark">
|
||||||
v{{ document.currentVersion.versionNumber }}
|
v{{ document.currentVersion.versionNumber }}
|
||||||
|
</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
-
|
-
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -45,43 +55,65 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 class="h5 mb-3">Versionen</h2>
|
{# ============================= #}
|
||||||
|
{# Versionen #}
|
||||||
|
{# ============================= #}
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<h2 class="h5 mb-0">Versionen</h2>
|
||||||
|
|
||||||
|
{% if is_granted('ROLE_SUPER_ADMIN') %}
|
||||||
<a href="{{ path('admin_document_version_new', {id: document.id}) }}"
|
<a href="{{ path('admin_document_version_new', {id: document.id}) }}"
|
||||||
class="btn btn-sm btn-light mb-3">
|
class="btn btn-sm btn-outline-info">
|
||||||
+ Neue Version
|
Neue Version
|
||||||
</a>
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
{% if document.versions is empty %}
|
{% if document.versions is empty %}
|
||||||
|
|
||||||
<div class="alert alert-secondary">
|
<div class="alert alert-secondary">
|
||||||
Keine Versionen vorhanden.
|
Keine Versionen vorhanden.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="card bg-black text-info border-secondary">
|
|
||||||
|
<div class="card bg-black border-secondary">
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
<table class="table table-dark table-hover mb-0">
|
|
||||||
<thead>
|
<table class="table table-dark table-striped table-hover align-middle mb-0">
|
||||||
|
<thead class="table-secondary text-dark">
|
||||||
<tr>
|
<tr>
|
||||||
<th>Version</th>
|
<th>Version</th>
|
||||||
<th>Aktiv</th>
|
<th>Status</th>
|
||||||
<th>Ingest</th>
|
<th>Ingest</th>
|
||||||
<th>Checksum</th>
|
<th>Checksum</th>
|
||||||
<th>Erstellt von</th>
|
<th>Erstellt von</th>
|
||||||
<th>Datum</th>
|
<th>Datum</th>
|
||||||
<th>Aktion</th>
|
<th class="text-end">Aktionen</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for version in document.versions %}
|
|
||||||
<tr>
|
|
||||||
<td>v{{ version.versionNumber }}</td>
|
|
||||||
|
|
||||||
|
{% for version in document.versions %}
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<strong>v{{ version.versionNumber }}</strong>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{# Aktivstatus #}
|
||||||
<td>
|
<td>
|
||||||
{% if version.isActive %}
|
{% if version.isActive %}
|
||||||
<span class="badge bg-success">Ja</span>
|
<span class="badge bg-success">Aktiv</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="badge bg-secondary">Nein</span>
|
<span class="badge bg-dark border border-secondary">
|
||||||
|
Inaktiv
|
||||||
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
{# Ingest Status #}
|
||||||
<td>
|
<td>
|
||||||
{% if version.ingestStatus == 'INDEXED' %}
|
{% if version.ingestStatus == 'INDEXED' %}
|
||||||
<span class="badge bg-success">INDEXED</span>
|
<span class="badge bg-success">INDEXED</span>
|
||||||
@@ -89,69 +121,103 @@
|
|||||||
<span class="badge bg-warning text-dark">RUNNING</span>
|
<span class="badge bg-warning text-dark">RUNNING</span>
|
||||||
{% elseif version.ingestStatus == 'FAILED' %}
|
{% elseif version.ingestStatus == 'FAILED' %}
|
||||||
<span class="badge bg-danger">FAILED</span>
|
<span class="badge bg-danger">FAILED</span>
|
||||||
{% else %}
|
{% elseif version.ingestStatus == 'PENDING' %}
|
||||||
<span class="badge bg-secondary">PENDING</span>
|
<span class="badge bg-secondary">PENDING</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-dark border border-secondary">
|
||||||
|
{{ version.ingestStatus }}
|
||||||
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td>
|
{# Checksum #}
|
||||||
{{ version.checksum[:10] }}...
|
<td class="small text-secondary">
|
||||||
|
{{ version.checksum ? version.checksum[:10] ~ '…' : '-' }}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
{# Created by #}
|
||||||
<td>
|
<td>
|
||||||
{{ version.createdBy.email }}
|
{{ version.createdBy ? version.createdBy.email : '-' }}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td>
|
{# Date #}
|
||||||
|
<td class="small">
|
||||||
{{ version.createdAt|date('d.m.Y H:i') }}
|
{{ version.createdAt|date('d.m.Y H:i') }}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
|
||||||
|
{# Aktionen #}
|
||||||
|
<td class="text-end">
|
||||||
|
|
||||||
{% if version.isActive %}
|
{% if version.isActive %}
|
||||||
|
|
||||||
{# Optional: manuelles Re-Ingest nur bei PENDING/FAILED #}
|
{% if version.ingestStatus in ['PENDING', 'FAILED'] and is_granted('ROLE_SUPER_ADMIN') %}
|
||||||
{% if version.ingestStatus in ['PENDING', 'FAILED'] %}
|
|
||||||
|
|
||||||
<form method="post"
|
<form method="post"
|
||||||
action="{{ path('admin_document_version_ingest', {versionId: version.id}) }}"
|
action="{{ path('admin_document_version_ingest', {versionId: version.id}) }}"
|
||||||
style="display:inline;">
|
class="d-inline"
|
||||||
<input type="hidden" name="_token"
|
onsubmit="return confirm('Ingest erneut starten?');">
|
||||||
value="{{ csrf_token('ingest_version') }}">
|
|
||||||
|
<input type="hidden"
|
||||||
|
name="_token"
|
||||||
|
value="{{ csrf_token('ingest_version_' ~ version.id) }}">
|
||||||
|
|
||||||
<button class="btn btn-sm btn-outline-info">
|
<button class="btn btn-sm btn-outline-info">
|
||||||
Ingest starten
|
Ingest starten
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="text-success">Ingested</span>
|
<span class="text-success small">
|
||||||
|
Bereits indexiert
|
||||||
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
||||||
|
{% if is_granted('ROLE_SUPER_ADMIN') %}
|
||||||
<form method="post"
|
<form method="post"
|
||||||
action="{{ path('admin_document_version_activate', {versionId: version.id}) }}"
|
action="{{ path('admin_document_version_activate', {versionId: version.id}) }}"
|
||||||
style="display:inline;">
|
class="d-inline"
|
||||||
<input type="hidden" name="_token"
|
onsubmit="return confirm('Diese Version aktivieren? Es wird ein Re-Ingest ausgelöst.');">
|
||||||
value="{{ csrf_token('activate_version') }}">
|
|
||||||
|
<input type="hidden"
|
||||||
|
name="_token"
|
||||||
|
value="{{ csrf_token('activate_version_' ~ version.id) }}">
|
||||||
|
|
||||||
<button class="btn btn-sm btn-outline-light">
|
<button class="btn btn-sm btn-outline-light">
|
||||||
Aktivieren
|
Aktivieren
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="mt-4 small text-secondary">
|
||||||
|
Hinweis: Beim Aktivieren einer Version wird automatisch ein Re-Ingest
|
||||||
|
durchgeführt. Der NDJSON-Index und der FAISS-Index werden deterministisch
|
||||||
|
neu aufgebaut.
|
||||||
|
</div>
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<h1 class="h4 mb-3">Ein Fehler trat auf</h1>
|
|
||||||
<h2 class="h5 mb-3">Fehler:</h2>
|
|
||||||
{% for message in app.flashes('danger') %}
|
|
||||||
<div class="alert alert-danger">
|
<div class="alert alert-danger">
|
||||||
{{ message }}
|
Dokument nicht gefunden.
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -1,76 +1,145 @@
|
|||||||
{% extends 'admin/base.html.twig' %}
|
{% extends 'admin/base.html.twig' %}
|
||||||
|
|
||||||
{% block title %}System Prompt{% endblock %}
|
{% block title %}Neues Indexierungsprofil{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<h1>Create Ingest Profile</h1>
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h1 class="h3">Neues Indexierungsprofil</h1>
|
||||||
|
|
||||||
|
<a href="{{ path('admin_ingest_profile_list') }}"
|
||||||
|
class="btn btn-sm btn-outline-secondary">
|
||||||
|
Zurück
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card bg-black border-secondary text-light">
|
||||||
|
<div class="card-body">
|
||||||
|
|
||||||
<form method="post">
|
<form method="post">
|
||||||
<table class="table table-sm table-dark align-middle">
|
<input type="hidden"
|
||||||
<tbody>
|
name="_token"
|
||||||
<tr>
|
value="{{ csrf_token('create_ingest_profile') }}">
|
||||||
<th scope="row" class="w-25">Chunk Size (500-2500)</th>
|
|
||||||
<td>
|
|
||||||
<label>
|
|
||||||
<select name="chunk_size" class="form-select">
|
|
||||||
{% for i in range(250, 2500, 50) %}
|
|
||||||
<option value="{{ i }}" {{ selectedValue is defined and selectedValue == i ? 'selected' : '' }}>
|
|
||||||
{{ i }}
|
|
||||||
</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">Chunk Overlap (50-150)</th>
|
|
||||||
<td>
|
|
||||||
<label>
|
|
||||||
<select name="chunk_overlap" class="form-select">
|
|
||||||
{% for i in range(50, 150, 25) %}
|
|
||||||
<option value="{{ i }}" {{ selectedValue is defined and selectedValue == i ? 'selected' : '' }}>
|
|
||||||
{{ i }}
|
|
||||||
</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">Embedding Model (default)</th>
|
|
||||||
<td>
|
|
||||||
<label>
|
|
||||||
<select name="embedding_model" class="form-control" required>
|
|
||||||
<option value="all-MiniLM-L6-v2">all-MiniLM-L6-v2</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">Embedding Dimension (default)</th>
|
|
||||||
<td>
|
|
||||||
<label>
|
|
||||||
<select name="embedding_dimension" class="form-control" required>
|
|
||||||
<option value="768">768</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">Scoring Version (default)</th>
|
|
||||||
<td>
|
|
||||||
<label>
|
|
||||||
<input type="number" name="scoring_version" class="form-control" value="1" placeholder="1" readonly required>
|
|
||||||
</label>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td colspan="2" class="text-start">
|
|
||||||
<button type="submit" class="btn btn-primary">Create</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
|
<div class="row g-4">
|
||||||
|
|
||||||
|
<!-- ===================== -->
|
||||||
|
<!-- Chunking Section -->
|
||||||
|
<!-- ===================== -->
|
||||||
|
<div class="col-12">
|
||||||
|
<h5 class="text-info">Chunking</h5>
|
||||||
|
<hr class="border-secondary">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">
|
||||||
|
Chunk Size
|
||||||
|
</label>
|
||||||
|
<select name="chunk_size"
|
||||||
|
class="form-select bg-dark text-light border-secondary"
|
||||||
|
required>
|
||||||
|
{% for i in range(250, 2500, 50) %}
|
||||||
|
<option value="{{ i }}">
|
||||||
|
{{ i }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
<div class="form-text text-secondary">
|
||||||
|
Größere Werte = weniger Chunks, mehr Kontext pro Chunk.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">
|
||||||
|
Chunk Overlap
|
||||||
|
</label>
|
||||||
|
<select name="chunk_overlap"
|
||||||
|
class="form-select bg-dark text-light border-secondary"
|
||||||
|
required>
|
||||||
|
{% for i in range(50, 200, 25) %}
|
||||||
|
<option value="{{ i }}">
|
||||||
|
{{ i }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
<div class="form-text text-secondary">
|
||||||
|
Überlappung zwischen Chunks zur Kontextstabilisierung.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ===================== -->
|
||||||
|
<!-- Embedding Section -->
|
||||||
|
<!-- ===================== -->
|
||||||
|
<div class="col-12 mt-4">
|
||||||
|
<h5 class="text-info">Embedding</h5>
|
||||||
|
<hr class="border-secondary">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">
|
||||||
|
Embedding Model
|
||||||
|
</label>
|
||||||
|
<select name="embedding_model"
|
||||||
|
class="form-select bg-dark text-light border-secondary"
|
||||||
|
required>
|
||||||
|
<option value="all-MiniLM-L6-v2">
|
||||||
|
all-MiniLM-L6-v2 (768d)
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">
|
||||||
|
Embedding Dimension
|
||||||
|
</label>
|
||||||
|
<input type="number"
|
||||||
|
name="embedding_dimension"
|
||||||
|
value="768"
|
||||||
|
class="form-control bg-dark text-light border-secondary"
|
||||||
|
readonly>
|
||||||
|
<div class="form-text text-secondary">
|
||||||
|
Muss mit dem Embedding-Modell übereinstimmen.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ===================== -->
|
||||||
|
<!-- Scoring Section -->
|
||||||
|
<!-- ===================== -->
|
||||||
|
<div class="col-12 mt-4">
|
||||||
|
<h5 class="text-info">Scoring</h5>
|
||||||
|
<hr class="border-secondary">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">
|
||||||
|
Scoring Version
|
||||||
|
</label>
|
||||||
|
<input type="number"
|
||||||
|
name="scoring_version"
|
||||||
|
value="1"
|
||||||
|
class="form-control bg-dark text-light border-secondary"
|
||||||
|
readonly>
|
||||||
|
<div class="form-text text-secondary">
|
||||||
|
Erhöhung erzwingt Global Reindex.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="border-secondary my-4">
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-end">
|
||||||
|
<button type="submit"
|
||||||
|
class="btn btn-outline-info">
|
||||||
|
Profil erstellen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 small text-secondary">
|
||||||
|
Hinweis: Änderungen am Indexierungsprofil wirken sich auf die Struktur des
|
||||||
|
Vektor-Indexes aus. Nach Aktivierung ist ein vollständiger Reindex erforderlich.
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,80 +1,173 @@
|
|||||||
{% extends 'admin/base.html.twig' %}
|
{% extends 'admin/base.html.twig' %}
|
||||||
|
|
||||||
{% block title %}Ingest Profiles{% endblock %}
|
{% block title %}Indexierungsprofile{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<h1>Ingest Profiles</h1>
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h1 class="h3">Indexierungsprofile</h1>
|
||||||
|
|
||||||
|
<a class="btn btn-sm btn-outline-info"
|
||||||
|
href="{{ path('admin_ingest_profile_create') }}">
|
||||||
|
Neues Profil anlegen
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# ============================= #}
|
||||||
|
{# Strukturstatus Alert #}
|
||||||
|
{# ============================= #}
|
||||||
|
|
||||||
{% if structureMismatch %}
|
{% if structureMismatch %}
|
||||||
<div class="alert alert-danger">
|
<div class="alert alert-danger d-flex justify-content-between align-items-center">
|
||||||
⚠ Strukturabweichung festgestellt – Globale Neuindizierung erforderlich | <a href="{{ path('admin_jobs') }}">Global Reindex aufrufen</a>
|
<div>
|
||||||
|
<strong>Strukturabweichung erkannt.</strong>
|
||||||
|
Die aktuelle Indexstruktur entspricht nicht dem aktiven Profil.
|
||||||
|
Eine globale Neuindizierung ist erforderlich.
|
||||||
|
</div>
|
||||||
|
<a href="{{ path('admin_jobs') }}"
|
||||||
|
class="btn btn-sm btn-outline-light">
|
||||||
|
Global Reindex starten
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="alert alert-success">
|
<div class="alert alert-success">
|
||||||
✅ Die Indexstruktur entspricht dem aktiven Profil
|
Die Indexstruktur entspricht dem aktiven Profil.
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<p><a class="btn btn-outline-light" href="{{ path('admin_ingest_profile_create') }}">+ Neues Profil anlegen</a></p>
|
{# ============================= #}
|
||||||
|
{# Profile Tabelle #}
|
||||||
|
{# ============================= #}
|
||||||
|
|
||||||
<h2>Profiles</h2>
|
<div class="card bg-black border-secondary mb-5">
|
||||||
|
<div class="card-body p-0">
|
||||||
|
|
||||||
<table border="1" cellpadding="6" class="table table-sm table-dark align-middle">
|
<table class="table table-dark table-striped table-hover align-middle mb-0">
|
||||||
|
<thead class="table-secondary text-dark">
|
||||||
<tr>
|
<tr>
|
||||||
<th>Version</th>
|
<th>Version</th>
|
||||||
<th>Chunk Size</th>
|
<th>Chunk Size</th>
|
||||||
<th>Overlap</th>
|
<th>Overlap</th>
|
||||||
<th>Model</th>
|
<th>Embedding</th>
|
||||||
<th>Dimension</th>
|
<th>Dim</th>
|
||||||
<th>Scoring</th>
|
<th>Scoring</th>
|
||||||
<th>Active</th>
|
<th>Status</th>
|
||||||
<th>Reindex Required</th>
|
<th>Reindex</th>
|
||||||
<th>Actions</th>
|
<th class="text-end">Aktionen</th>
|
||||||
</tr>
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
{% for p in profiles %}
|
{% for p in profiles %}
|
||||||
<tr>
|
<tr>
|
||||||
<td {% if p.active %}class="text-success"{% endif %}>{{ p.version }}</td>
|
<td>v{{ p.version }}</td>
|
||||||
<td>{{ p.chunkSize }}</td>
|
<td>{{ p.chunkSize }}</td>
|
||||||
<td>{{ p.chunkOverlap }}</td>
|
<td>{{ p.chunkOverlap }}</td>
|
||||||
<td>{{ p.embeddingModel }}</td>
|
<td>{{ p.embeddingModel }}</td>
|
||||||
<td>{{ p.embeddingDimension }}</td>
|
<td>{{ p.embeddingDimension }}</td>
|
||||||
<td>{{ p.scoringVersion }}</td>
|
<td>{{ p.scoringVersion }}</td>
|
||||||
<td {% if p.active %}class="text-success"{% endif %}>{{ p.active ? 'Yes' : 'No' }}</td>
|
|
||||||
<td>{{ p.reindexRequired ? 'Yes' : 'No' }}</td>
|
|
||||||
<td>
|
<td>
|
||||||
{% if not p.active %}
|
{% if p.active %}
|
||||||
<a class="btn btn-outline-success btn-sm" href="{{ path('admin_ingest_profile_activate', {id: p.id}) }}">
|
<span class="badge bg-success">Aktiv</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-dark border border-secondary">
|
||||||
|
Inaktiv
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
{% if p.reindexRequired %}
|
||||||
|
<span class="badge bg-warning text-dark">
|
||||||
|
Erforderlich
|
||||||
|
</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-secondary">
|
||||||
|
Nein
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td class="text-end">
|
||||||
|
|
||||||
|
{% if not p.active and is_granted('ROLE_SUPER_ADMIN') %}
|
||||||
|
|
||||||
|
{# Aktivieren via POST #}
|
||||||
|
<form method="post"
|
||||||
|
action="{{ path('admin_ingest_profile_activate', {id: p.id}) }}"
|
||||||
|
class="d-inline"
|
||||||
|
onsubmit="return confirm('Profil aktivieren? Global Reindex kann erforderlich sein.');">
|
||||||
|
|
||||||
|
<input type="hidden"
|
||||||
|
name="_token"
|
||||||
|
value="{{ csrf_token('activate_ingest_profile_' ~ p.id) }}">
|
||||||
|
|
||||||
|
<button class="btn btn-sm btn-outline-success me-2">
|
||||||
Aktivieren
|
Aktivieren
|
||||||
</a>
|
</button>
|
||||||
{% endif %}
|
</form>
|
||||||
{% if not p.active %}
|
|
||||||
<a class="btn btn-outline-danger btn-sm" href="{{ path('admin_ingest_profile_remove', {id: p.id}) }}">
|
{# Löschen via POST #}
|
||||||
|
<form method="post"
|
||||||
|
action="{{ path('admin_ingest_profile_remove', {id: p.id}) }}"
|
||||||
|
class="d-inline"
|
||||||
|
onsubmit="return confirm('Profil wirklich löschen?');">
|
||||||
|
|
||||||
|
<input type="hidden"
|
||||||
|
name="_token"
|
||||||
|
value="{{ csrf_token('delete_ingest_profile_' ~ p.id) }}">
|
||||||
|
|
||||||
|
<button class="btn btn-sm btn-outline-danger">
|
||||||
Löschen
|
Löschen
|
||||||
</a>
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% else %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="9" class="text-center text-secondary py-4">
|
||||||
|
Keine Profile vorhanden.
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<hr>
|
</div>
|
||||||
<h2>Index-Struktur-Profil Diff</h2>
|
</div>
|
||||||
|
|
||||||
|
{# ============================= #}
|
||||||
|
{# Struktur-Diff #}
|
||||||
|
{# ============================= #}
|
||||||
|
|
||||||
|
<div class="card bg-black border-secondary">
|
||||||
|
<div class="card-body">
|
||||||
|
|
||||||
|
<h5 class="text-info mb-3">Index-Struktur Vergleich</h5>
|
||||||
|
|
||||||
{% if indexMeta %}
|
{% if indexMeta %}
|
||||||
<p><strong>Index Version:</strong> {{ indexMeta.index_version }}</p>
|
<div class="mb-3 small text-secondary">
|
||||||
|
Aktuelle Index-Version:
|
||||||
|
<strong>{{ indexMeta.index_version }}</strong>
|
||||||
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p>No index_meta.json found.</p>
|
<div class="alert alert-warning">
|
||||||
|
index_meta.json nicht gefunden.
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<table border="1" cellpadding="6" class="table table-sm table-dark align-middle">
|
<table class="table table-dark table-striped table-hover align-middle">
|
||||||
|
<thead class="table-secondary text-dark">
|
||||||
<tr>
|
<tr>
|
||||||
<th>Parameter</th>
|
<th>Parameter</th>
|
||||||
<th>Index Meta</th>
|
<th>Index Meta</th>
|
||||||
<th>Active Profile</th>
|
<th>Aktives Profil</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
</tr>
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
{% for key, row in diff %}
|
{% for key, row in diff %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ key }}</td>
|
<td>{{ key }}</td>
|
||||||
@@ -82,13 +175,28 @@
|
|||||||
<td>{{ row.profile }}</td>
|
<td>{{ row.profile }}</td>
|
||||||
<td>
|
<td>
|
||||||
{% if row.equal %}
|
{% if row.equal %}
|
||||||
<span style="color:green;">✓</span>
|
<span class="badge bg-success">Identisch</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span style="color:red;">✗</span>
|
<span class="badge bg-danger">Abweichung</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
{% else %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="4" class="text-center text-secondary py-4">
|
||||||
|
Keine Vergleichsdaten verfügbar.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 small text-secondary">
|
||||||
|
Hinweis: Strukturänderungen (Chunking, Embedding, Scoring) führen zu
|
||||||
|
inkonsistentem Retrieval, bis eine vollständige Neuindizierung durchgeführt wird.
|
||||||
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,32 +1,43 @@
|
|||||||
{% extends 'admin/base.html.twig' %}
|
{% extends 'admin/base.html.twig' %}
|
||||||
|
|
||||||
{% block title %}Ingest Jobs{% endblock %}
|
{% block title %}Indexierung (Ingest Jobs){% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
|
|
||||||
<h1 class="h4 mb-4">Indexierung (Ingest Jobs-Liste)</h1>
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h1 class="h3">Indexierung (Ingest Jobs)</h1>
|
||||||
|
|
||||||
|
{% if is_granted('ROLE_SUPER_ADMIN') %}
|
||||||
<form method="post"
|
<form method="post"
|
||||||
action="{{ path('admin_global_reindex') }}"
|
action="{{ path('admin_global_reindex') }}"
|
||||||
onsubmit="return confirm('Global Reindex starten? Dies kann einige Zeit dauern.');">
|
onsubmit="return confirm('Global Reindex starten? Dies kann je nach Datenmenge mehrere Minuten dauern.');"
|
||||||
|
class="mb-0">
|
||||||
|
|
||||||
|
<input type="hidden"
|
||||||
|
name="_token"
|
||||||
|
value="{{ csrf_token('global_reindex') }}">
|
||||||
|
|
||||||
<button type="submit"
|
<button type="submit"
|
||||||
class="btn btn-danger mb-3">
|
class="btn btn-sm btn-outline-danger">
|
||||||
Global Reindex starten
|
Global Reindex starten
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
{% if jobs is empty %}
|
{% if jobs is empty %}
|
||||||
|
|
||||||
<div class="alert alert-secondary">
|
<div class="alert alert-secondary">
|
||||||
Keine Jobs vorhanden.
|
Keine Ingest Jobs vorhanden.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
||||||
<div class="card bg-black text-info border-secondary">
|
<div class="card bg-black border-secondary">
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
|
|
||||||
<table class="table table-dark table-hover mb-0">
|
<table class="table table-dark table-striped table-hover align-middle mb-0">
|
||||||
<thead>
|
<thead class="table-secondary text-dark">
|
||||||
<tr>
|
<tr>
|
||||||
<th>Job-ID</th>
|
<th>Job-ID</th>
|
||||||
<th>Typ</th>
|
<th>Typ</th>
|
||||||
@@ -35,20 +46,26 @@
|
|||||||
<th>Version</th>
|
<th>Version</th>
|
||||||
<th>Gestartet</th>
|
<th>Gestartet</th>
|
||||||
<th>Beendet</th>
|
<th>Beendet</th>
|
||||||
<th>User</th>
|
<th>Benutzer</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|
||||||
{% for job in jobs %}
|
{% for job in jobs %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
|
||||||
|
<td class="small">
|
||||||
<a href="{{ path('admin_job_show', {id: job.id}) }}"
|
<a href="{{ path('admin_job_show', {id: job.id}) }}"
|
||||||
class="text-light">
|
class="text-light text-decoration-none">
|
||||||
{{ job.id }}
|
{{ job.id }}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ job.type }}</td>
|
|
||||||
|
<td>
|
||||||
|
<span class="badge bg-info text-dark">
|
||||||
|
{{ job.type }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
{% if job.status == 'COMPLETED' %}
|
{% if job.status == 'COMPLETED' %}
|
||||||
@@ -60,52 +77,59 @@
|
|||||||
{% elseif job.status == 'FAILED' %}
|
{% elseif job.status == 'FAILED' %}
|
||||||
<span class="badge bg-danger">FAILED</span>
|
<span class="badge bg-danger">FAILED</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="badge bg-secondary">{{ job.status }}</span>
|
<span class="badge bg-dark border border-secondary">
|
||||||
|
{{ job.status }}
|
||||||
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
{% if job.documentId %}
|
{% if job.documentId %}
|
||||||
<a href="/admin/documents/{{ job.documentId }}" class="text-light">{{ job.documentId }}</a>
|
<a href="{{ path('admin_document_show', {id: job.documentId}) }}"
|
||||||
|
class="text-light text-decoration-none">
|
||||||
|
{{ job.documentId }}
|
||||||
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
-
|
-
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
{% if job.documentVersionId %}
|
{{ job.documentVersionId ?? '-' }}
|
||||||
{{ job.documentVersionId }}
|
|
||||||
{% else %}
|
|
||||||
-
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td>{{ job.startedAt|date('d.m.Y H:i:s') }}</td>
|
<td class="small">
|
||||||
|
{{ job.startedAt ? job.startedAt|date('d.m.Y H:i:s') : '-' }}
|
||||||
<td>
|
|
||||||
{% if job.finishedAt %}
|
|
||||||
{{ job.finishedAt|date('d.m.Y H:i:s') }}
|
|
||||||
{% else %}
|
|
||||||
-
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td>
|
<td class="small">
|
||||||
{% if job.startedBy %}
|
{{ job.finishedAt ? job.finishedAt|date('d.m.Y H:i:s') : '-' }}
|
||||||
{{ job.startedBy.email }}
|
|
||||||
{% else %}
|
|
||||||
-
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
<td class="small">
|
||||||
|
{{ job.startedBy ? job.startedBy.email : '-' }}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
</tr>
|
||||||
|
{% else %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="8" class="text-center text-secondary py-4">
|
||||||
|
Keine Jobs gefunden.
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="mt-4 small text-secondary">
|
||||||
|
Hinweis: Während laufender Jobs (Status RUNNING) sollten keine
|
||||||
|
parallelen Reindex-Prozesse gestartet werden.
|
||||||
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -4,48 +4,38 @@
|
|||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h1 class="h3 mb-0">Ingest Job</h1>
|
||||||
|
|
||||||
<a href="{{ path('admin_jobs') }}"
|
<a href="{{ path('admin_jobs') }}"
|
||||||
class="btn btn-sm btn-outline-light mb-3">
|
class="btn btn-sm btn-outline-secondary">
|
||||||
← Zurück
|
Zurück zur Übersicht
|
||||||
</a>
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h1 class="h4 mb-4">Ingest Job</h1>
|
<div class="card bg-black border-secondary text-light">
|
||||||
|
|
||||||
<div class="card bg-black text-info border-secondary">
|
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<strong>ID:</strong> {{ job.id }}
|
<strong>ID:</strong>
|
||||||
|
<span class="small text-light">{{ job.id }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<strong>Typ:</strong> {{ job.type }}
|
<strong>Typ:</strong>
|
||||||
|
<span class="badge bg-info text-dark">{{ job.type }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<strong>Status:</strong>
|
<strong>Status:</strong>
|
||||||
<span id="job-status-badge">
|
<span id="job-status-badge"></span>
|
||||||
{% if job.status == 'COMPLETED' %}
|
|
||||||
<span class="badge bg-success">COMPLETED</span>
|
|
||||||
{% elseif job.status == 'QUEUED' %}
|
|
||||||
<span class="badge bg-secondary">QUEUED</span>
|
|
||||||
{% elseif job.status == 'RUNNING' %}
|
|
||||||
<span class="badge bg-warning text-dark">RUNNING</span>
|
|
||||||
{% elseif job.status == 'FAILED' %}
|
|
||||||
<span class="badge bg-danger">FAILED</span>
|
|
||||||
{% elseif job.status == 'ABORTED' %}
|
|
||||||
<span class="badge bg-dark">ABORTED</span>
|
|
||||||
{% else %}
|
|
||||||
<span class="badge bg-secondary">{{ job.status }}</span>
|
|
||||||
{% endif %}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<strong>Dokument:</strong>
|
<strong>Dokument:</strong>
|
||||||
{% if job.documentId %}
|
{% if job.documentId %}
|
||||||
<a href="{{ path('admin_document_show', {id: job.documentId}) }}"
|
<a href="{{ path('admin_document_show', {id: job.documentId}) }}"
|
||||||
class="text-light">
|
class="text-light text-decoration-none">
|
||||||
{{ job.documentId }}
|
{{ job.documentId }}
|
||||||
</a>
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
@@ -75,32 +65,33 @@
|
|||||||
{{ job.startedBy ? job.startedBy.email : '-' }}
|
{{ job.startedBy ? job.startedBy.email : '-' }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{# Loader #}
|
||||||
<div id="job-loader"
|
<div id="job-loader"
|
||||||
class="mt-3"
|
class="mt-3 d-none">
|
||||||
style="{% if job.status in ['QUEUED','RUNNING'] %}{% else %}display:none;{% endif %}">
|
|
||||||
<div class="d-flex align-items-center gap-2">
|
<div class="d-flex align-items-center gap-2">
|
||||||
<div class="spinner-border spinner-border-sm text-info" role="status"></div>
|
<div class="spinner-border spinner-border-sm text-info" role="status"></div>
|
||||||
<div>
|
<div>
|
||||||
<strong>Prozess läuft…</strong><br>
|
<strong>Prozess läuft…</strong><br>
|
||||||
<small class="text-secondary">
|
<small class="text-secondary">
|
||||||
Diese Seite aktualisiert den Status automatisch.
|
Der Status wird automatisch aktualisiert.
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{# Fehlerbereich #}
|
||||||
<div id="job-error"
|
<div id="job-error"
|
||||||
class="alert alert-danger mt-3"
|
class="alert alert-danger mt-3 d-none">
|
||||||
style="{% if job.status == 'FAILED' %}{% else %}display:none;{% endif %}">
|
|
||||||
{% if job.errorMessage %}
|
|
||||||
<strong>Fehler:</strong><br>
|
|
||||||
{{ job.errorMessage }}
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 small text-secondary">
|
||||||
|
Hinweis: Bei DOCUMENT_VERSION_ACTIVATE-Jobs wird ein vollständiger
|
||||||
|
NDJSON-Rebuild und FAISS-Reindex durchgeführt.
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
|
|
||||||
@@ -112,31 +103,27 @@
|
|||||||
|
|
||||||
let timer = null;
|
let timer = null;
|
||||||
|
|
||||||
|
function renderBadge(status) {
|
||||||
|
const map = {
|
||||||
|
COMPLETED: 'bg-success',
|
||||||
|
QUEUED: 'bg-secondary',
|
||||||
|
RUNNING: 'bg-warning text-dark',
|
||||||
|
FAILED: 'bg-danger',
|
||||||
|
ABORTED: 'bg-dark'
|
||||||
|
};
|
||||||
|
|
||||||
|
const css = map[status] || 'bg-secondary';
|
||||||
|
badgeWrap.innerHTML =
|
||||||
|
`<span class="badge ${css}">${status}</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
function stopPolling() {
|
function stopPolling() {
|
||||||
if (timer !== null) {
|
if (timer) {
|
||||||
clearInterval(timer);
|
clearInterval(timer);
|
||||||
timer = null;
|
timer = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setBadge(status) {
|
|
||||||
let html = '';
|
|
||||||
if (status === 'COMPLETED')
|
|
||||||
html = '<span class="badge bg-success">COMPLETED</span>';
|
|
||||||
else if (status === 'QUEUED')
|
|
||||||
html = '<span class="badge bg-secondary">QUEUED</span>';
|
|
||||||
else if (status === 'RUNNING')
|
|
||||||
html = '<span class="badge bg-warning text-dark">RUNNING</span>';
|
|
||||||
else if (status === 'FAILED')
|
|
||||||
html = '<span class="badge bg-danger">FAILED</span>';
|
|
||||||
else if (status === 'ABORTED')
|
|
||||||
html = '<span class="badge bg-dark">ABORTED</span>';
|
|
||||||
else
|
|
||||||
html = '<span class="badge bg-secondary">' + status + '</span>';
|
|
||||||
|
|
||||||
badgeWrap.innerHTML = html;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function poll() {
|
async function poll() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(statusUrl);
|
const res = await fetch(statusUrl);
|
||||||
@@ -145,23 +132,24 @@
|
|||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
const status = (data.status || '').toUpperCase();
|
const status = (data.status || '').toUpperCase();
|
||||||
|
|
||||||
setBadge(status);
|
renderBadge(status);
|
||||||
|
|
||||||
finishedAtEl.textContent = data.finishedAt
|
finishedAtEl.textContent =
|
||||||
|
data.finishedAt
|
||||||
? new Date(data.finishedAt).toLocaleString('de-DE')
|
? new Date(data.finishedAt).toLocaleString('de-DE')
|
||||||
: '-';
|
: '-';
|
||||||
|
|
||||||
if (status === 'QUEUED' || status === 'RUNNING') {
|
if (status === 'QUEUED' || status === 'RUNNING') {
|
||||||
loaderEl.style.display = '';
|
loaderEl.classList.remove('d-none');
|
||||||
} else {
|
} else {
|
||||||
loaderEl.style.display = 'none';
|
loaderEl.classList.add('d-none');
|
||||||
stopPolling();
|
stopPolling();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === 'FAILED' && data.errorMessage) {
|
if (status === 'FAILED' && data.errorMessage) {
|
||||||
errorEl.style.display = '';
|
errorEl.classList.remove('d-none');
|
||||||
errorEl.innerHTML =
|
errorEl.innerHTML =
|
||||||
'<strong>Fehler:</strong><br>' + data.errorMessage;
|
`<strong>Fehler:</strong><br>${data.errorMessage}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -169,8 +157,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
timer = setInterval(poll, 1000);
|
// Initial render from server state
|
||||||
poll();
|
renderBadge("{{ job.status|upper }}");
|
||||||
|
|
||||||
|
if (["QUEUED","RUNNING"].includes("{{ job.status|upper }}")) {
|
||||||
|
loaderEl.classList.remove('d-none');
|
||||||
|
timer = setInterval(poll, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
139
templates/admin/model_config/create.html.twig
Normal file
139
templates/admin/model_config/create.html.twig
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
{% extends 'admin/base.html.twig' %}
|
||||||
|
|
||||||
|
{% block title %}Neue Modell-Generierungskonfiguration{% endblock %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h1 class="h3">Neue Modell-Generierungskonfiguration</h1>
|
||||||
|
|
||||||
|
<a href="{{ path('admin_model_config_list') }}"
|
||||||
|
class="btn btn-sm btn-outline-secondary">
|
||||||
|
Zurück
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card bg-black border-secondary text-light">
|
||||||
|
<div class="card-body">
|
||||||
|
|
||||||
|
<form method="post">
|
||||||
|
|
||||||
|
<input type="hidden" name="_token"
|
||||||
|
value="{{ csrf_token('create_model_config') }}">
|
||||||
|
|
||||||
|
<div class="row g-4">
|
||||||
|
|
||||||
|
<!-- Modell -->
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Modellname</label>
|
||||||
|
<input type="text"
|
||||||
|
name="model_name"
|
||||||
|
class="form-control bg-dark text-light border-secondary"
|
||||||
|
placeholder="z. B. qwen3:latest"
|
||||||
|
required>
|
||||||
|
<div class="form-text text-secondary">
|
||||||
|
Exakter Modellname wie im Endpunkt konfiguriert.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Stream -->
|
||||||
|
<div class="col-md-6 d-flex align-items-center">
|
||||||
|
<div class="form-check form-switch mt-4">
|
||||||
|
<input class="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
name="stream"
|
||||||
|
value="1"
|
||||||
|
id="streamSwitch">
|
||||||
|
<label class="form-check-label" for="streamSwitch">
|
||||||
|
Streaming aktivieren
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Temperature -->
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Temperature</label>
|
||||||
|
<input type="number"
|
||||||
|
step="0.1"
|
||||||
|
min="0"
|
||||||
|
max="2"
|
||||||
|
name="temperature"
|
||||||
|
value="0.1"
|
||||||
|
class="form-control bg-dark text-light border-secondary"
|
||||||
|
required>
|
||||||
|
<div class="form-text text-secondary">
|
||||||
|
Niedrige Werte = deterministisch (empfohlen für RAG).
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Top K -->
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Top K</label>
|
||||||
|
<input type="number"
|
||||||
|
min="1"
|
||||||
|
name="top_k"
|
||||||
|
value="20"
|
||||||
|
class="form-control bg-dark text-light border-secondary"
|
||||||
|
required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Top P -->
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Top P</label>
|
||||||
|
<input type="number"
|
||||||
|
step="0.05"
|
||||||
|
min="0"
|
||||||
|
max="1"
|
||||||
|
name="top_p"
|
||||||
|
value="0.8"
|
||||||
|
class="form-control bg-dark text-light border-secondary"
|
||||||
|
required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Repeat Penalty -->
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Repeat Penalty</label>
|
||||||
|
<input type="number"
|
||||||
|
step="0.05"
|
||||||
|
min="0"
|
||||||
|
max="5"
|
||||||
|
name="repeat_penalty"
|
||||||
|
value="1.05"
|
||||||
|
class="form-control bg-dark text-light border-secondary"
|
||||||
|
required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Num Ctx -->
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Context Window (num_ctx)</label>
|
||||||
|
<input type="number"
|
||||||
|
min="512"
|
||||||
|
max="32768"
|
||||||
|
name="num_ctx"
|
||||||
|
value="4096"
|
||||||
|
class="form-control bg-dark text-light border-secondary"
|
||||||
|
required>
|
||||||
|
<div class="form-text text-secondary">
|
||||||
|
Muss zum Modell passen. Zu hohe Werte können Performance beeinflussen.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="border-secondary my-4">
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-end">
|
||||||
|
<button type="submit"
|
||||||
|
class="btn btn-outline-info">
|
||||||
|
Konfiguration speichern
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 small text-secondary">
|
||||||
|
Hinweis: Neue Konfigurationen werden zunächst inaktiv gespeichert und
|
||||||
|
müssen separat aktiviert werden. Pro Modell kann nur eine Version aktiv sein.
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
102
templates/admin/model_config/list.html.twig
Normal file
102
templates/admin/model_config/list.html.twig
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
{% extends 'admin/base.html.twig' %}
|
||||||
|
|
||||||
|
{% block title %}KI Modell-Generierung Config{% endblock %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h1 class="h3">KI Modell-Generierung Config</h1>
|
||||||
|
|
||||||
|
{% if is_granted('ROLE_SUPER_ADMIN') %}
|
||||||
|
<a href="{{ path('admin_model_config_create') }}"
|
||||||
|
class="btn btn-sm btn-outline-info">
|
||||||
|
Neue Konfiguration
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card bg-black border-secondary">
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<table class="table table-dark table-striped table-hover mb-0 align-middle">
|
||||||
|
<thead class="table-secondary text-dark">
|
||||||
|
<tr>
|
||||||
|
<th>Modell</th>
|
||||||
|
<th>Version</th>
|
||||||
|
<th>Stream</th>
|
||||||
|
<th>Temp</th>
|
||||||
|
<th>Top K</th>
|
||||||
|
<th>Top P</th>
|
||||||
|
<th>Repeat</th>
|
||||||
|
<th>Ctx</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th class="text-end">Aktion</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for config in configs %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ config.modelName }}</td>
|
||||||
|
<td>v{{ config.version }}</td>
|
||||||
|
<td>
|
||||||
|
{% if config.stream %}
|
||||||
|
<span class="badge bg-info text-dark">Streaming</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-secondary">Blocking</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>{{ config.temperature }}</td>
|
||||||
|
<td>{{ config.topK }}</td>
|
||||||
|
<td>{{ config.topP }}</td>
|
||||||
|
<td>{{ config.repeatPenalty }}</td>
|
||||||
|
<td>{{ config.numCtx }}</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
{% if config.active %}
|
||||||
|
<span class="badge bg-success">Aktiv</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-dark border border-secondary">
|
||||||
|
Inaktiv
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td class="text-end">
|
||||||
|
{% if not config.active and is_granted('ROLE_SUPER_ADMIN') %}
|
||||||
|
|
||||||
|
<a href="{{ path('admin_model_config_activate', {id: config.id}) }}"
|
||||||
|
class="btn btn-sm btn-outline-success me-2">
|
||||||
|
Aktivieren
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<form method="post"
|
||||||
|
action="{{ path('admin_model_config_delete', {id: config.id}) }}"
|
||||||
|
style="display:inline-block"
|
||||||
|
onsubmit="return confirm('Wirklich löschen?');">
|
||||||
|
|
||||||
|
<input type="hidden" name="_token"
|
||||||
|
value="{{ csrf_token('delete_model_config_' ~ config.id) }}">
|
||||||
|
|
||||||
|
<button class="btn btn-sm btn-outline-danger">
|
||||||
|
Löschen
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% else %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="10" class="text-center text-secondary py-4">
|
||||||
|
Keine Konfiguration vorhanden.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 small text-secondary">
|
||||||
|
Hinweis: Änderungen wirken sich unmittelbar auf die Generierungsparameter
|
||||||
|
des aktiven Modells aus. Nur eine Konfiguration pro Modell kann aktiv sein.
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -3,10 +3,16 @@
|
|||||||
{% block title %}Admin Login{% endblock %}
|
{% block title %}Admin Login{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div class="row justify-content-center">
|
|
||||||
|
<div class="row justify-content-center align-items-center" style="min-height:70vh;">
|
||||||
<div class="col-12 col-md-5 col-lg-4">
|
<div class="col-12 col-md-5 col-lg-4">
|
||||||
|
|
||||||
<h1 class="h4 mb-3">Admin Login</h1>
|
<div class="card bg-black border-secondary text-info">
|
||||||
|
<div class="card-body">
|
||||||
|
|
||||||
|
<h1 class="h4 mb-4 text-center text-info">
|
||||||
|
mitho® KI RAG Login
|
||||||
|
</h1>
|
||||||
|
|
||||||
{% if error %}
|
{% if error %}
|
||||||
<div class="alert alert-danger">
|
<div class="alert alert-danger">
|
||||||
@@ -15,22 +21,53 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<form method="post" action="{{ path('admin_login') }}">
|
<form method="post" action="{{ path('admin_login') }}">
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">E-Mail</label>
|
{# ============================= #}
|
||||||
<input class="form-control" name="_username" value="{{ last_username }}" autocomplete="email" required>
|
{# Email #}
|
||||||
</div>
|
{# ============================= #}
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
|
<label class="form-label">E-Mail</label>
|
||||||
|
<input class="form-control bg-dark text-light border-secondary"
|
||||||
|
name="_username"
|
||||||
|
value="{{ last_username }}"
|
||||||
|
autocomplete="email"
|
||||||
|
required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# ============================= #}
|
||||||
|
{# Passwort #}
|
||||||
|
{# ============================= #}
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
<label class="form-label">Passwort</label>
|
<label class="form-label">Passwort</label>
|
||||||
<input class="form-control" type="password" name="_password" autocomplete="current-password" required>
|
<input class="form-control bg-dark text-light border-secondary"
|
||||||
|
type="password"
|
||||||
|
name="_password"
|
||||||
|
autocomplete="current-password"
|
||||||
|
required>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# CSRF #}
|
{# CSRF #}
|
||||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}">
|
<input type="hidden"
|
||||||
|
name="_csrf_token"
|
||||||
|
value="{{ csrf_token('authenticate') }}">
|
||||||
|
|
||||||
|
<button class="btn btn-outline-info w-100"
|
||||||
|
type="submit">
|
||||||
|
Einloggen
|
||||||
|
</button>
|
||||||
|
|
||||||
<button class="btn btn-light w-100" type="submit">Einloggen</button>
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center mt-3 small text-secondary">
|
||||||
|
Zugriff nur für autorisierte Administratoren.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -4,37 +4,43 @@
|
|||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
|
|
||||||
<a href="{{ path('admin_dashboard') }}"
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
class="btn btn-sm btn-outline-light mb-3">
|
<h1 class="h3">Wissensdaten (Chunk-Index)</h1>
|
||||||
← Zurück
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<h1 class="h4 mb-4">Wissensdaten (Chunk-Index)</h1>
|
<a href="{{ path('admin_dashboard') }}"
|
||||||
|
class="btn btn-sm btn-outline-secondary">
|
||||||
|
Zurück zum Dashboard
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
{# ============================= #}
|
{# ============================= #}
|
||||||
{# Index Meta Section #}
|
{# Index Meta Section #}
|
||||||
{# ============================= #}
|
{# ============================= #}
|
||||||
|
|
||||||
<div class="card bg-black text-info border-secondary mb-4"{#>
|
<div class="card bg-black border-secondary mb-5">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
|
||||||
<h5 class="mb-3">Index Meta (index_meta.json)</h5>
|
<h5 class="text-info mb-3">Index Meta (index_meta.json)</h5>
|
||||||
|
|
||||||
{% if meta.error is defined %}
|
{% if meta.error is defined %}
|
||||||
<div class="alert alert-danger">
|
<div class="alert alert-danger">
|
||||||
<strong>Fehler:</strong><br>
|
<strong>Fehler:</strong><br>
|
||||||
{{ meta.error }}<br>
|
{{ meta.error }}<br>
|
||||||
<small>{{ meta.path }}</small>
|
<small class="text-muted">{{ meta.path }}</small>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<table class="table table-dark table-sm table-bordered align-middle mb-0">
|
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-dark table-striped align-middle mb-0">
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for key, value in meta %}
|
{% for key, value in meta %}
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width:280px;">{{ key }}</th>
|
<th style="width:260px;" class="text-secondary">
|
||||||
|
{{ key }}
|
||||||
|
</th>
|
||||||
<td>
|
<td>
|
||||||
{% if value is iterable %}
|
{% if value is iterable %}
|
||||||
<pre class="mb-0 text-info">
|
<pre class="mb-0 small text-info">
|
||||||
{{ value|json_encode(constant('JSON_PRETTY_PRINT')) }}
|
{{ value|json_encode(constant('JSON_PRETTY_PRINT')) }}
|
||||||
</pre>
|
</pre>
|
||||||
{% else %}
|
{% else %}
|
||||||
@@ -42,13 +48,20 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
{% else %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="2" class="text-secondary">
|
||||||
|
Keine Meta-Daten vorhanden.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>#}
|
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{# ============================= #}
|
{# ============================= #}
|
||||||
{# NDJSON Section #}
|
{# NDJSON Section #}
|
||||||
@@ -57,19 +70,27 @@
|
|||||||
{% set currentPage = ndjson.page|default(1) %}
|
{% set currentPage = ndjson.page|default(1) %}
|
||||||
{% set currentLimit = ndjson.limit|default(50) %}
|
{% set currentLimit = ndjson.limit|default(50) %}
|
||||||
|
|
||||||
<div class="card bg-black text-info border-secondary mb-4">
|
<div class="card bg-black border-secondary">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
|
||||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
<h5 class="mb-0">NdJson Index Übersicht Chunks (index.ndjson)</h5>
|
<h5 class="text-info mb-0">
|
||||||
|
NDJSON-Index Übersicht (index.ndjson)
|
||||||
|
</h5>
|
||||||
|
|
||||||
<div>
|
<div class="btn-group">
|
||||||
<a href="{{ path('admin_system_agent', {page: (currentPage - 1 < 1 ? 1 : currentPage - 1), limit: currentLimit}) }}"
|
<a href="{{ path('admin_system_agent', {
|
||||||
|
page: currentPage > 1 ? currentPage - 1 : 1,
|
||||||
|
limit: currentLimit
|
||||||
|
}) }}"
|
||||||
class="btn btn-sm btn-outline-light">
|
class="btn btn-sm btn-outline-light">
|
||||||
← Zurück
|
← Zurück
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a href="{{ path('admin_system_agent', {page: currentPage + 1, limit: currentLimit}) }}"
|
<a href="{{ path('admin_system_agent', {
|
||||||
|
page: currentPage + 1,
|
||||||
|
limit: currentLimit
|
||||||
|
}) }}"
|
||||||
class="btn btn-sm btn-outline-light">
|
class="btn btn-sm btn-outline-light">
|
||||||
Weiter →
|
Weiter →
|
||||||
</a>
|
</a>
|
||||||
@@ -84,50 +105,74 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="mb-2 text-secondary">
|
<div class="mb-3 small text-secondary">
|
||||||
Datei vorhanden: {{ ndjson.path ? 'JA' : 'NEIN' }} |
|
Datei:
|
||||||
Geladene Einträge: {{ debugCount|default(0) }} |
|
{% if ndjson.path %}
|
||||||
Seite {{ currentPage }} • Limit {{ currentLimit }}
|
<span class="badge bg-success">Vorhanden</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-danger">Nicht gefunden</span>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
• Geladene Einträge: {{ debugCount|default(0) }}
|
||||||
|
• Seite {{ currentPage }}
|
||||||
|
• Limit {{ currentLimit }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-dark table-sm table-bordered align-middle">
|
<table class="table table-dark table-striped table-hover align-middle">
|
||||||
<thead>
|
<thead class="table-secondary text-dark">
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width:220px;">chunk_id</th>
|
<th style="width:200px;">chunk_id</th>
|
||||||
<th style="width:180px;">document_id</th>
|
<th style="width:180px;">document_id</th>
|
||||||
<th>Text (gekürzt)</th>
|
<th>Text (gekürzt)</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|
||||||
{% for item in ndjson.items|default([]) %}
|
{% for item in ndjson.items|default([]) %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ item.chunk_id ?? '-' }}</td>
|
<td class="small">{{ item.chunk_id ?? '-' }}</td>
|
||||||
<td> <a href="{{ path('admin_document_show', {id: item.document_id}) }}"
|
|
||||||
class="text-decoration-none text-light">
|
|
||||||
{{ item.document_id ?? '-' }}
|
|
||||||
</a></td>
|
|
||||||
<td>
|
<td>
|
||||||
|
{% if item.document_id %}
|
||||||
|
<a href="{{ path('admin_document_show', {id: item.document_id}) }}"
|
||||||
|
class="text-decoration-none text-light">
|
||||||
|
{{ item.document_id }}
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
-
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
|
||||||
{% set text = item.text ?? '' %}
|
{% set text = item.text ?? '' %}
|
||||||
{{ text|slice(0, 240) }}{% if text|length > 240 %}…{% endif %}
|
<div class="small">
|
||||||
|
{{ text|slice(0, 240) }}
|
||||||
|
{% if text|length > 240 %}…{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
<details class="mt-2">
|
<details class="mt-2">
|
||||||
<summary class="text-secondary" style="cursor:pointer;">
|
<summary class="text-secondary small">
|
||||||
JSON anzeigen
|
JSON anzeigen
|
||||||
</summary>
|
</summary>
|
||||||
<pre class="bg-dark text-info p-2 border border-secondary rounded mt-2">
|
|
||||||
|
<pre class="bg-dark text-info p-2 border border-secondary rounded mt-2 small">
|
||||||
{{ item|json_encode(constant('JSON_PRETTY_PRINT')) }}
|
{{ item|json_encode(constant('JSON_PRETTY_PRINT')) }}
|
||||||
</pre>
|
</pre>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="3" class="text-secondary">
|
<td colspan="3" class="text-center text-secondary py-4">
|
||||||
Keine Einträge gefunden.
|
Keine Einträge gefunden.
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@@ -135,4 +180,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 small text-secondary">
|
||||||
|
Hinweis: Änderungen am NDJSON-Index oder an der Indexstruktur können
|
||||||
|
inkonsistente Retrieval-Ergebnisse verursachen, bis ein vollständiger
|
||||||
|
Reindex durchgeführt wurde.
|
||||||
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -4,7 +4,13 @@
|
|||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
|
|
||||||
<h1 class="h4 mb-4">System Prompt</h1>
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h1 class="h3">System Prompt</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# ============================= #}
|
||||||
|
{# Flash Messages #}
|
||||||
|
{# ============================= #}
|
||||||
|
|
||||||
{% for message in app.flashes('success') %}
|
{% for message in app.flashes('success') %}
|
||||||
<div class="alert alert-success">{{ message }}</div>
|
<div class="alert alert-success">{{ message }}</div>
|
||||||
@@ -13,92 +19,154 @@
|
|||||||
<div class="alert alert-danger">{{ message }}</div>
|
<div class="alert alert-danger">{{ message }}</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
<div class="card bg-black text-info border-secondary mb-4">
|
{# ============================= #}
|
||||||
|
{# Neue Version erstellen #}
|
||||||
|
{# ============================= #}
|
||||||
|
|
||||||
|
<div class="card bg-black border-secondary mb-5 text-light">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
|
||||||
|
<h5 class="text-info mb-3">Neue Version erstellen</h5>
|
||||||
|
|
||||||
<form method="post">
|
<form method="post">
|
||||||
|
<input type="hidden"
|
||||||
|
name="_token"
|
||||||
|
value="{{ csrf_token('create_system_prompt') }}">
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Kommentar (optional)</label>
|
<label class="form-label">Kommentar (optional)</label>
|
||||||
<input type="text"
|
<input type="text"
|
||||||
name="comment"
|
name="comment"
|
||||||
class="form-control bg-dark text-info border-secondary"
|
class="form-control bg-dark text-light border-secondary"
|
||||||
placeholder="Warum wurde der Prompt geändert?">
|
placeholder="Warum wurde der Prompt geändert?">
|
||||||
|
<div class="form-text text-secondary">
|
||||||
|
Dokumentation der Änderung für spätere Nachvollziehbarkeit.
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Prompt Inhalt | Variablen: {% verbatim %}{% now %}{% endverbatim %} = Datum/Zeit</label>
|
<label class="form-label">
|
||||||
|
Prompt-Inhalt
|
||||||
|
</label>
|
||||||
|
<div class="form-text text-secondary mb-2">
|
||||||
|
Verfügbare Variable:
|
||||||
|
<code>{% verbatim %}{% now %}{% endverbatim %}</code>
|
||||||
|
(aktuelles Datum/Zeit)
|
||||||
|
</div>
|
||||||
<textarea name="content"
|
<textarea name="content"
|
||||||
rows="16"
|
rows="16"
|
||||||
class="form-control bg-dark text-info border-secondary"
|
class="form-control bg-dark text-light border-secondary"
|
||||||
>{{ active ? active.content : '' }}</textarea>
|
required>{{ active ? active.content : '' }}</textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="btn btn-outline-light">
|
{% if is_granted('ROLE_SUPER_ADMIN') %}
|
||||||
|
<div class="d-flex justify-content-end">
|
||||||
|
<button class="btn btn-outline-info">
|
||||||
Neue Version speichern
|
Neue Version speichern
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card bg-black text-info border-secondary">
|
{# ============================= #}
|
||||||
|
{# Versionen Übersicht #}
|
||||||
|
{# ============================= #}
|
||||||
|
|
||||||
|
<div class="card bg-black border-secondary">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
|
||||||
<h5>Versionen</h5>
|
<h5 class="text-info mb-3">Versionen</h5>
|
||||||
|
|
||||||
<table class="table table-dark table-sm table-bordered align-middle">
|
<table class="table table-dark table-striped table-hover align-middle">
|
||||||
<thead>
|
<thead class="table-secondary text-dark">
|
||||||
<tr>
|
<tr>
|
||||||
<th>Version</th>
|
<th>Version</th>
|
||||||
<th>Aktiv</th>
|
<th>Status</th>
|
||||||
<th>Kommentar</th>
|
<th>Kommentar</th>
|
||||||
<th>Erstellt</th>
|
<th>Erstellt</th>
|
||||||
<th>Aktionen</th>
|
<th class="text-end">Aktionen</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|
||||||
{% for p in all %}
|
{% for p in all %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ p.version }}</td>
|
<td>v{{ p.version }}</td>
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
{% if p.active %}
|
{% if p.active %}
|
||||||
<span class="badge bg-success">ACTIVE</span>
|
<span class="badge bg-success">Aktiv</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-dark border border-secondary">
|
||||||
|
Inaktiv
|
||||||
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ p.comment ?? '-' }}</td>
|
|
||||||
<td>{{ p.createdAt|date('d.m.Y H:i:s') }}</td>
|
|
||||||
<td>
|
|
||||||
|
|
||||||
{% if not p.active %}
|
<td>{{ p.comment ?? '-' }}</td>
|
||||||
|
|
||||||
|
<td>{{ p.createdAt|date('d.m.Y H:i:s') }}</td>
|
||||||
|
|
||||||
|
<td class="text-end">
|
||||||
|
|
||||||
|
{% if not p.active and is_granted('ROLE_SUPER_ADMIN') %}
|
||||||
|
|
||||||
|
{# Aktivieren #}
|
||||||
<form method="post"
|
<form method="post"
|
||||||
action="{{ path('admin_system_prompt_activate', {id: p.id}) }}"
|
action="{{ path('admin_system_prompt_activate', {id: p.id}) }}"
|
||||||
style="display:inline-block;">
|
class="d-inline"
|
||||||
<button class="btn btn-sm btn-outline-light">
|
onsubmit="return confirm('Diese Version aktivieren?');">
|
||||||
|
|
||||||
|
<input type="hidden"
|
||||||
|
name="_token"
|
||||||
|
value="{{ csrf_token('activate_system_prompt_' ~ p.id) }}">
|
||||||
|
|
||||||
|
<button class="btn btn-sm btn-outline-success me-2">
|
||||||
Aktivieren
|
Aktivieren
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
{# Löschen #}
|
||||||
<form method="post"
|
<form method="post"
|
||||||
action="{{ path('admin_system_prompt_delete', {id: p.id}) }}"
|
action="{{ path('admin_system_prompt_delete', {id: p.id}) }}"
|
||||||
style="display:inline-block;"
|
class="d-inline"
|
||||||
onsubmit="return confirm('Version wirklich löschen?');">
|
onsubmit="return confirm('Version wirklich löschen?');">
|
||||||
|
|
||||||
|
<input type="hidden"
|
||||||
|
name="_token"
|
||||||
|
value="{{ csrf_token('delete_system_prompt_' ~ p.id) }}">
|
||||||
|
|
||||||
<button class="btn btn-sm btn-outline-danger">
|
<button class="btn btn-sm btn-outline-danger">
|
||||||
Löschen
|
Löschen
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
-
|
<span class="text-secondary">—</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
{% else %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="5" class="text-center text-secondary py-4">
|
||||||
|
Keine Versionen vorhanden.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 small text-secondary">
|
||||||
|
Hinweis: Der aktive System Prompt beeinflusst das Antwortverhalten
|
||||||
|
des LLM unmittelbar. Änderungen sollten dokumentiert und versioniert erfolgen.
|
||||||
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
Reference in New Issue
Block a user