From 6822c8f3f88d4840a8491978d0d0c3648b2e1f08 Mon Sep 17 00:00:00 2001 From: team2 Date: Tue, 17 Feb 2026 20:36:47 +0100 Subject: [PATCH] optimize ui add new ki endpoint params --- config/services.yaml | 7 +- .../Admin/ModelGenerationConfigController.php | 103 +++++++ .../Admin/SystemAgentController.php | 1 + src/Entity/ModelGenerationConfig.php | 90 +++++++ src/Infrastructure/OllamaClient.php | 182 ++++++++----- .../ModelGenerationConfigRepository.php | 41 +++ src/Service/ModelGenerationConfigManager.php | 72 +++++ src/Service/ModelGenerationConfigProvider.php | 42 +++ templates/admin/base.html.twig | 142 ++++++---- templates/admin/dashboard/index.html.twig | 108 +++++--- templates/admin/document/index.html.twig | 125 ++++++--- templates/admin/document/new.html.twig | 90 +++++-- .../admin/document/new_version.html.twig | 89 ++++-- templates/admin/document/show.html.twig | 168 ++++++++---- .../admin/ingest_profile/create.html.twig | 197 +++++++++----- templates/admin/ingest_profile/list.html.twig | 254 +++++++++++++----- templates/admin/job/index.html.twig | 102 ++++--- templates/admin/job/show.html.twig | 121 ++++----- templates/admin/model_config/create.html.twig | 139 ++++++++++ templates/admin/model_config/list.html.twig | 102 +++++++ templates/admin/security/login.html.twig | 81 ++++-- .../admin/system/agent_overview.html.twig | 147 ++++++---- templates/admin/system/prompt.html.twig | 120 +++++++-- 23 files changed, 1915 insertions(+), 608 deletions(-) create mode 100644 src/Controller/Admin/ModelGenerationConfigController.php create mode 100644 src/Entity/ModelGenerationConfig.php create mode 100644 src/Repository/ModelGenerationConfigRepository.php create mode 100644 src/Service/ModelGenerationConfigManager.php create mode 100644 src/Service/ModelGenerationConfigProvider.php create mode 100644 templates/admin/model_config/create.html.twig create mode 100644 templates/admin/model_config/list.html.twig diff --git a/config/services.yaml b/config/services.yaml index 94a8c3a..54ed98f 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -76,9 +76,10 @@ services: App\Infrastructure\OllamaClient: arguments: - $apiUrl: '%env(AI_LLM_API_URL)%' - $model: '%env(AI_LLM_MODEL)%' - $timeoutSeconds: '%env(int:AI_LLM_TIMEOUT)%' + $apiUrl: '%env(OLLAMA_API_URL)%' + $model: '%env(OLLAMA_MODEL)%' + $timeoutSeconds: 600 + $configProvider: '@App\Service\ModelGenerationConfigProvider' # ------------------------------------------------------------ # AI Agent – Context & Runner diff --git a/src/Controller/Admin/ModelGenerationConfigController.php b/src/Controller/Admin/ModelGenerationConfigController.php new file mode 100644 index 0000000..e17c5ff --- /dev/null +++ b/src/Controller/Admin/ModelGenerationConfigController.php @@ -0,0 +1,103 @@ +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'); + } + +} diff --git a/src/Controller/Admin/SystemAgentController.php b/src/Controller/Admin/SystemAgentController.php index b72b5ba..5c12001 100644 --- a/src/Controller/Admin/SystemAgentController.php +++ b/src/Controller/Admin/SystemAgentController.php @@ -32,4 +32,5 @@ final class SystemAgentController extends AbstractController 'debugCount'=>count($ndjsonPage['items']) ]); } + } diff --git a/src/Entity/ModelGenerationConfig.php b/src/Entity/ModelGenerationConfig.php new file mode 100644 index 0000000..bd9b156 --- /dev/null +++ b/src/Entity/ModelGenerationConfig.php @@ -0,0 +1,90 @@ +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; + } +} diff --git a/src/Infrastructure/OllamaClient.php b/src/Infrastructure/OllamaClient.php index 4a407a4..bac6718 100644 --- a/src/Infrastructure/OllamaClient.php +++ b/src/Infrastructure/OllamaClient.php @@ -4,69 +4,54 @@ declare(strict_types=1); namespace App\Infrastructure; +use App\Entity\ModelGenerationConfig; +use App\Service\ModelGenerationConfigProvider; use Generator; use JsonException; use RuntimeException; 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 { - private string $apiUrl; - private string $model; - private int $timeoutSeconds; + private ?ModelGenerationConfig $cachedConfig = null; public function __construct( - string $apiUrl, - string $model, - int $timeoutSeconds, - ) - { - $this->apiUrl = $apiUrl; - $this->model = $model; - $this->timeoutSeconds = $timeoutSeconds; - } + private string $apiUrl, + private string $model, + private int $timeoutSeconds, + private ModelGenerationConfigProvider $configProvider + ) {} /** - * Streams tokens from the LLM backend in real time. - * - * @param string $prompt Fully constructed prompt - * - * @return Generator - * @throws JsonException + * Public Streaming API */ public function stream(string $prompt): Generator { - $json = []; + $config = $this->getConfig(); - $payload = json_encode([ - 'model' => $this->model, - 'prompt' => $prompt, - 'stream' => true, - 'options' => [ - "temperature" => 0.9, - "top_k" => 35, - "top_p" => 0.9, - "repeat_penalty" => 1.1, - "num_ctx" => 8192 - ] - ], JSON_THROW_ON_ERROR); + if ($config->isStream()) { + yield from $this->streamInternal($prompt); + return; + } + + // Fallback: Blocking generate → Generator-kompatibel ausgeben + yield $this->generateInternal($prompt); + } + + /** + * Public Blocking API + */ + 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 = ''; $done = false; @@ -82,7 +67,7 @@ final class OllamaClient CURLOPT_POSTFIELDS => $payload, CURLOPT_RETURNTRANSFER => false, 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; return strlen($data); }, @@ -98,12 +83,10 @@ final class OllamaClient try { do { - // Execute the multi handle do { $status = curl_multi_exec($mh, $running); } while ($status === CURLM_CALL_MULTI_PERFORM); - // Read incoming data from the buffer while (($pos = strpos($buffer, "\n")) !== false) { $line = trim(substr($buffer, 0, $pos)); $buffer = substr($buffer, $pos + 1); @@ -127,37 +110,94 @@ final class OllamaClient } } - // Wait for network activity if ($running) { curl_multi_select($mh, 0.2); } + } 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)) { - $error = curl_error($ch); - throw new RuntimeException('LLM connection error: ' . $error); + throw new RuntimeException('LLM connection error: ' . curl_error($ch)); } + } finally { curl_multi_remove_handle($mh, $ch); curl_multi_close($mh); 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; + } } diff --git a/src/Repository/ModelGenerationConfigRepository.php b/src/Repository/ModelGenerationConfigRepository.php new file mode 100644 index 0000000..91298cf --- /dev/null +++ b/src/Repository/ModelGenerationConfigRepository.php @@ -0,0 +1,41 @@ +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; + } +} diff --git a/src/Service/ModelGenerationConfigManager.php b/src/Service/ModelGenerationConfigManager.php new file mode 100644 index 0000000..c40b079 --- /dev/null +++ b/src/Service/ModelGenerationConfigManager.php @@ -0,0 +1,72 @@ +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 + } + } +} diff --git a/src/Service/ModelGenerationConfigProvider.php b/src/Service/ModelGenerationConfigProvider.php new file mode 100644 index 0000000..85c29fd --- /dev/null +++ b/src/Service/ModelGenerationConfigProvider.php @@ -0,0 +1,42 @@ +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 + ); + } +} diff --git a/templates/admin/base.html.twig b/templates/admin/base.html.twig index 9e26c17..f32b07a 100644 --- a/templates/admin/base.html.twig +++ b/templates/admin/base.html.twig @@ -8,63 +8,113 @@ + -