alpha new hybridretriver line
This commit is contained in:
32
migrations/Version20260225000100.php
Normal file
32
migrations/Version20260225000100.php
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20260225000100 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Adds retrieval_max_chunks and retrieval_vector_top_k to model_generation_config (without DB defaults)';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE model_generation_config
|
||||||
|
ADD retrieval_max_chunks INT NOT NULL,
|
||||||
|
ADD retrieval_vector_top_k INT NOT NULL
|
||||||
|
');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE model_generation_config
|
||||||
|
DROP retrieval_max_chunks,
|
||||||
|
DROP retrieval_vector_top_k
|
||||||
|
');
|
||||||
|
}
|
||||||
|
}
|
||||||
35
migrations/Version20260225000200.php
Normal file
35
migrations/Version20260225000200.php
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20260225000200 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Fix schema drift (indexes, FK, longtext nullability)';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE knowledge_tag CHANGE description description LONGTEXT DEFAULT NULL');
|
||||||
|
$this->addSql('ALTER TABLE knowledge_tag RENAME INDEX uniq_knowledge_tag_slug TO UNIQ_86B46255989D9B62');
|
||||||
|
|
||||||
|
$this->addSql('ALTER TABLE document DROP FOREIGN KEY FK_D8698A769407EE77');
|
||||||
|
$this->addSql('ALTER TABLE document ADD CONSTRAINT FK_D8698A769407EE77 FOREIGN KEY (current_version_id) REFERENCES document_version (id)');
|
||||||
|
|
||||||
|
$this->addSql('ALTER TABLE tag_rebuild_job CHANGE error_message error_message LONGTEXT DEFAULT NULL');
|
||||||
|
|
||||||
|
$this->addSql('ALTER TABLE document_tag RENAME INDEX idx_document_tag_document TO IDX_D0234567C33F7837');
|
||||||
|
$this->addSql('ALTER TABLE document_tag RENAME INDEX idx_document_tag_tag TO IDX_D0234567BAD26311');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// Optional – in Drift-Fix-Migration meist nicht notwendig
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace App\Controller\Admin;
|
namespace App\Controller\Admin;
|
||||||
|
|
||||||
use App\Entity\ModelGenerationConfig;
|
use App\Entity\ModelGenerationConfig;
|
||||||
|
use App\Knowledge\Retrieval\NdjsonHybridRetriever;
|
||||||
use App\Repository\ModelGenerationConfigRepository;
|
use App\Repository\ModelGenerationConfigRepository;
|
||||||
use App\Service\ModelGenerationConfigManager;
|
use App\Service\ModelGenerationConfigManager;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
@@ -21,7 +22,10 @@ class ModelGenerationConfigController extends AbstractController
|
|||||||
{
|
{
|
||||||
$this->denyAccessUnlessGranted('ROLE_KNOWLEDGE_ADMIN');
|
$this->denyAccessUnlessGranted('ROLE_KNOWLEDGE_ADMIN');
|
||||||
|
|
||||||
$configs = $repository->findBy([], ['active' => 'DESC', 'modelName' => 'ASC', 'version' => 'DESC']);
|
$configs = $repository->findBy(
|
||||||
|
[],
|
||||||
|
['active' => 'DESC', 'modelName' => 'ASC', 'version' => 'DESC']
|
||||||
|
);
|
||||||
|
|
||||||
return $this->render('admin/model_config/list.html.twig', [
|
return $this->render('admin/model_config/list.html.twig', [
|
||||||
'configs' => $configs,
|
'configs' => $configs,
|
||||||
@@ -38,20 +42,24 @@ class ModelGenerationConfigController extends AbstractController
|
|||||||
|
|
||||||
if ($request->isMethod('POST')) {
|
if ($request->isMethod('POST')) {
|
||||||
|
|
||||||
$modelName = $request->request->get('model_name');
|
$modelName = (string) $request->request->get('model_name');
|
||||||
|
|
||||||
$version = $repository->findNextVersion($modelName);
|
$version = $repository->findNextVersion($modelName);
|
||||||
|
|
||||||
|
$retrievalMaxChunks = (int) $request->request->get('retrieval_max_chunks', 25);
|
||||||
|
$retrievalVectorTopK = (int) $request->request->get('retrieval_vector_top_k', 25);
|
||||||
|
|
||||||
$config = new ModelGenerationConfig(
|
$config = new ModelGenerationConfig(
|
||||||
modelName: $modelName,
|
modelName: $modelName,
|
||||||
version: $version,
|
version: $version,
|
||||||
stream: (bool)$request->request->get('stream'),
|
stream: (bool) $request->request->get('stream'),
|
||||||
temperature: (float)$request->request->get('temperature'),
|
temperature: (float) $request->request->get('temperature'),
|
||||||
topK: (int)$request->request->get('top_k'),
|
topK: (int) $request->request->get('top_k'),
|
||||||
topP: (float)$request->request->get('top_p'),
|
topP: (float) $request->request->get('top_p'),
|
||||||
repeatPenalty: (float)$request->request->get('repeat_penalty'),
|
repeatPenalty: (float) $request->request->get('repeat_penalty'),
|
||||||
numCtx: (int)$request->request->get('num_ctx'),
|
numCtx: (int) $request->request->get('num_ctx'),
|
||||||
active: false
|
active: false,
|
||||||
|
retrievalMaxChunks: $retrievalMaxChunks,
|
||||||
|
retrievalVectorTopK: $retrievalVectorTopK
|
||||||
);
|
);
|
||||||
|
|
||||||
$em->persist($config);
|
$em->persist($config);
|
||||||
@@ -75,6 +83,32 @@ class ModelGenerationConfigController extends AbstractController
|
|||||||
return $this->redirectToRoute('admin_model_config_list');
|
return $this->redirectToRoute('admin_model_config_list');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Route('/{id}/test-retrieval', name: 'admin_model_config_test_retrieval')]
|
||||||
|
public function testRetrieval(
|
||||||
|
ModelGenerationConfig $config,
|
||||||
|
Request $request,
|
||||||
|
NdjsonHybridRetriever $retriever
|
||||||
|
): Response {
|
||||||
|
$this->denyAccessUnlessGranted('ROLE_KNOWLEDGE_ADMIN');
|
||||||
|
|
||||||
|
$results = [];
|
||||||
|
$prompt = '';
|
||||||
|
|
||||||
|
if ($request->isMethod('POST')) {
|
||||||
|
$prompt = trim((string) $request->request->get('prompt'));
|
||||||
|
|
||||||
|
if ($prompt !== '') {
|
||||||
|
$results = $retriever->retrieveForConfig($prompt, $config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('admin/model_config/test_retrieval.html.twig', [
|
||||||
|
'config' => $config,
|
||||||
|
'results' => $results,
|
||||||
|
'prompt' => $prompt,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
#[Route('/{id}/delete', name: 'admin_model_config_delete', methods: ['POST'])]
|
#[Route('/{id}/delete', name: 'admin_model_config_delete', methods: ['POST'])]
|
||||||
public function delete(
|
public function delete(
|
||||||
ModelGenerationConfig $config,
|
ModelGenerationConfig $config,
|
||||||
@@ -99,5 +133,4 @@ class ModelGenerationConfigController extends AbstractController
|
|||||||
|
|
||||||
return $this->redirectToRoute('admin_model_config_list');
|
return $this->redirectToRoute('admin_model_config_list');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
|
||||||
@@ -9,9 +9,15 @@ use Symfony\Component\Uid\Uuid;
|
|||||||
|
|
||||||
#[ORM\Entity(repositoryClass: \App\Repository\ModelGenerationConfigRepository::class)]
|
#[ORM\Entity(repositoryClass: \App\Repository\ModelGenerationConfigRepository::class)]
|
||||||
#[ORM\Table(name: 'model_generation_config')]
|
#[ORM\Table(name: 'model_generation_config')]
|
||||||
#[ORM\Index(columns: ['model_name', 'active'], name: 'idx_model_active')]
|
#[ORM\Index(name: 'idx_model_active', columns: ['model_name', 'active'])]
|
||||||
class ModelGenerationConfig
|
class ModelGenerationConfig
|
||||||
{
|
{
|
||||||
|
// -----------------------------
|
||||||
|
// Hard Guardrails
|
||||||
|
// -----------------------------
|
||||||
|
private const MAX_RETRIEVAL_CHUNKS = 200;
|
||||||
|
private const MAX_VECTOR_TOPK = 200;
|
||||||
|
|
||||||
#[ORM\Id]
|
#[ORM\Id]
|
||||||
#[ORM\Column(type: 'uuid', unique: true)]
|
#[ORM\Column(type: 'uuid', unique: true)]
|
||||||
private Uuid $id;
|
private Uuid $id;
|
||||||
@@ -37,6 +43,16 @@ class ModelGenerationConfig
|
|||||||
#[ORM\Column(name: 'num_ctx', type: 'integer')]
|
#[ORM\Column(name: 'num_ctx', type: 'integer')]
|
||||||
private int $numCtx;
|
private int $numCtx;
|
||||||
|
|
||||||
|
// -------------------------------------
|
||||||
|
// Retrieval-Parameter (NEU)
|
||||||
|
// -------------------------------------
|
||||||
|
|
||||||
|
#[ORM\Column(name: 'retrieval_max_chunks', type: 'integer')]
|
||||||
|
private int $retrievalMaxChunks;
|
||||||
|
|
||||||
|
#[ORM\Column(name: 'retrieval_vector_top_k', type: 'integer')]
|
||||||
|
private int $retrievalVectorTopK;
|
||||||
|
|
||||||
#[ORM\Column(type: 'boolean')]
|
#[ORM\Column(type: 'boolean')]
|
||||||
private bool $active;
|
private bool $active;
|
||||||
|
|
||||||
@@ -55,7 +71,9 @@ class ModelGenerationConfig
|
|||||||
float $topP = 0.8,
|
float $topP = 0.8,
|
||||||
float $repeatPenalty = 1.05,
|
float $repeatPenalty = 1.05,
|
||||||
int $numCtx = 4096,
|
int $numCtx = 4096,
|
||||||
bool $active = false
|
bool $active = false,
|
||||||
|
int $retrievalMaxChunks = 25,
|
||||||
|
int $retrievalVectorTopK = 25,
|
||||||
) {
|
) {
|
||||||
$this->id = Uuid::v4();
|
$this->id = Uuid::v4();
|
||||||
$this->modelName = $modelName;
|
$this->modelName = $modelName;
|
||||||
@@ -68,8 +86,15 @@ class ModelGenerationConfig
|
|||||||
$this->numCtx = $numCtx;
|
$this->numCtx = $numCtx;
|
||||||
$this->active = $active;
|
$this->active = $active;
|
||||||
$this->createdAt = new \DateTimeImmutable();
|
$this->createdAt = new \DateTimeImmutable();
|
||||||
|
|
||||||
|
$this->setRetrievalMaxChunks($retrievalMaxChunks);
|
||||||
|
$this->setRetrievalVectorTopK($retrievalVectorTopK);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -----------------------------
|
||||||
|
// Getter
|
||||||
|
// -----------------------------
|
||||||
|
|
||||||
public function getId(): Uuid { return $this->id; }
|
public function getId(): Uuid { return $this->id; }
|
||||||
public function getModelName(): string { return $this->modelName; }
|
public function getModelName(): string { return $this->modelName; }
|
||||||
public function isStream(): bool { return $this->stream; }
|
public function isStream(): bool { return $this->stream; }
|
||||||
@@ -82,9 +107,35 @@ class ModelGenerationConfig
|
|||||||
public function getVersion(): int { return $this->version; }
|
public function getVersion(): int { return $this->version; }
|
||||||
public function getCreatedAt(): \DateTimeImmutable { return $this->createdAt; }
|
public function getCreatedAt(): \DateTimeImmutable { return $this->createdAt; }
|
||||||
|
|
||||||
|
public function getRetrievalMaxChunks(): int
|
||||||
|
{
|
||||||
|
return $this->retrievalMaxChunks;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRetrievalVectorTopK(): int
|
||||||
|
{
|
||||||
|
return $this->retrievalVectorTopK;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------
|
||||||
|
// Controlled Mutators
|
||||||
|
// -----------------------------
|
||||||
|
|
||||||
// Nur vom Manager nutzen
|
// Nur vom Manager nutzen
|
||||||
public function setActive(bool $active): void
|
public function setActive(bool $active): void
|
||||||
{
|
{
|
||||||
$this->active = $active;
|
$this->active = $active;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
public function setRetrievalMaxChunks(int $value): void
|
||||||
|
{
|
||||||
|
$value = max(1, min($value, self::MAX_RETRIEVAL_CHUNKS));
|
||||||
|
$this->retrievalMaxChunks = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setRetrievalVectorTopK(int $value): void
|
||||||
|
{
|
||||||
|
$value = max(1, min($value, self::MAX_VECTOR_TOPK));
|
||||||
|
$this->retrievalVectorTopK = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,43 +4,65 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Knowledge\Retrieval;
|
namespace App\Knowledge\Retrieval;
|
||||||
|
|
||||||
|
use App\Entity\ModelGenerationConfig;
|
||||||
use App\Knowledge\ChunkManager;
|
use App\Knowledge\ChunkManager;
|
||||||
|
use App\Repository\ModelGenerationConfigRepository;
|
||||||
use App\Tag\TagRoutingService;
|
use App\Tag\TagRoutingService;
|
||||||
use App\Vector\VectorSearchClient;
|
use App\Vector\VectorSearchClient;
|
||||||
|
|
||||||
final class NdjsonHybridRetriever implements RetrieverInterface
|
final class NdjsonHybridRetriever implements RetrieverInterface
|
||||||
{
|
{
|
||||||
private const VECTOR_SCORE_THRESHOLD = 0.25;
|
private const VECTOR_SCORE_THRESHOLD = 0.25;
|
||||||
|
|
||||||
/**
|
|
||||||
* Wenn Tag-Routing aktiv ist, erhöhen wir TopK,
|
|
||||||
* weil wir danach per document_id filtern.
|
|
||||||
*/
|
|
||||||
private const VECTOR_TOPK_MULTIPLIER_WHEN_ROUTED = 10;
|
private const VECTOR_TOPK_MULTIPLIER_WHEN_ROUTED = 10;
|
||||||
|
|
||||||
/**
|
|
||||||
* Keyword-Scan: Mindest-Trefferanzahl an Terms, damit ein Chunk als Kandidat gilt.
|
|
||||||
*/
|
|
||||||
private const KEYWORD_MIN_HITS = 1;
|
private const KEYWORD_MIN_HITS = 1;
|
||||||
|
|
||||||
|
private const HARD_MAX_CHUNKS = 200;
|
||||||
|
private const HARD_MAX_VECTORK = 200;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly ChunkManager $chunkManager,
|
private readonly ChunkManager $chunkManager,
|
||||||
private readonly NdjsonChunkLookup $lookup,
|
private readonly NdjsonChunkLookup $lookup,
|
||||||
private readonly VectorSearchClient $vectorClient,
|
private readonly VectorSearchClient $vectorClient,
|
||||||
private readonly TagRoutingService $tagRouting,
|
private readonly TagRoutingService $tagRouting,
|
||||||
private readonly int $maxChunks = 100,
|
private readonly ModelGenerationConfigRepository $configRepository,
|
||||||
private readonly int $vectorTopK = 100,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function retrieve(string $prompt, int $limit = null): array
|
/**
|
||||||
|
* Normalbetrieb – ausschließlich aktive Config.
|
||||||
|
*/
|
||||||
|
public function retrieve(string $prompt): array
|
||||||
{
|
{
|
||||||
$limit = $this->maxChunks;
|
$config = $this->configRepository->findActiveForModel();
|
||||||
|
|
||||||
|
if ($config === null) {
|
||||||
|
throw new \RuntimeException('No active ModelGenerationConfig found.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->retrieveInternal($prompt, $config);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Admin-Testbetrieb – explizite Config.
|
||||||
|
* Verändert KEINEN globalen Zustand.
|
||||||
|
*/
|
||||||
|
public function retrieveForConfig(string $prompt, ModelGenerationConfig $config): array
|
||||||
|
{
|
||||||
|
return $this->retrieveInternal($prompt, $config);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zentrale Retrieval-Logik (keine Duplikation).
|
||||||
|
*/
|
||||||
|
private function retrieveInternal(string $prompt, ModelGenerationConfig $config): array
|
||||||
|
{
|
||||||
|
$limit = max(1, min($config->getRetrievalMaxChunks(), self::HARD_MAX_CHUNKS));
|
||||||
|
|
||||||
|
$vectorTopKBase = max(1, min($config->getRetrievalVectorTopK(), self::HARD_MAX_VECTORK));
|
||||||
|
|
||||||
// ---------------------------------------------------------
|
// ---------------------------------------------------------
|
||||||
// 0) Tag-Routing FIRST (soft gate)
|
// 1) Tag-Vector FIRST -> candidateSet (DocIDs)
|
||||||
// ---------------------------------------------------------
|
// ---------------------------------------------------------
|
||||||
$candidateDocIds = $this->tagRouting->route($prompt);
|
$candidateDocIds = $this->tagRouting->route($prompt); // <= DAS muss intern auf Tag-Vector gehen
|
||||||
|
|
||||||
$candidateSet = null;
|
$candidateSet = null;
|
||||||
|
|
||||||
if (is_array($candidateDocIds) && $candidateDocIds !== []) {
|
if (is_array($candidateDocIds) && $candidateDocIds !== []) {
|
||||||
@@ -48,31 +70,22 @@ final class NdjsonHybridRetriever implements RetrieverInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------
|
// ---------------------------------------------------------
|
||||||
// 1) Keyword first (simple streaming scan)
|
// 2) Vector chunks (primary)
|
||||||
// ---------------------------------------------------------
|
// ---------------------------------------------------------
|
||||||
$terms = $this->extractTerms($prompt);
|
$topK = $vectorTopKBase;
|
||||||
|
|
||||||
$keywordChunks = $this->keywordSearchStreaming($terms, $limit, $candidateSet);
|
|
||||||
|
|
||||||
if (\count($keywordChunks) >= $limit) {
|
|
||||||
return array_slice($keywordChunks, 0, $limit);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------
|
|
||||||
// 2) Vector fallback / enrichment
|
|
||||||
// - If routed: increase TopK, then filter by document_id
|
|
||||||
// - Soft fallback: if filtering yields nothing -> global vector once
|
|
||||||
// ---------------------------------------------------------
|
|
||||||
$topK = $this->vectorTopK;
|
|
||||||
|
|
||||||
if ($candidateSet !== null) {
|
if ($candidateSet !== null) {
|
||||||
$topK = max($this->vectorTopK * self::VECTOR_TOPK_MULTIPLIER_WHEN_ROUTED, $this->vectorTopK);
|
$topK = min(
|
||||||
$topK = min($topK, 200); // guardrail
|
max($vectorTopKBase * self::VECTOR_TOPK_MULTIPLIER_WHEN_ROUTED, $vectorTopKBase),
|
||||||
|
self::HARD_MAX_VECTORK
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$hits = $this->vectorClient->search($prompt, $topK);
|
$hits = $this->vectorClient->search($prompt, $topK);
|
||||||
|
|
||||||
if ($hits === []) {
|
if ($hits === []) {
|
||||||
return $keywordChunks;
|
// Tags-only System: kein Vector-Hit -> keine Chunks
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
$chunkIds = [];
|
$chunkIds = [];
|
||||||
@@ -87,14 +100,15 @@ final class NdjsonHybridRetriever implements RetrieverInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($chunkIds === []) {
|
if ($chunkIds === []) {
|
||||||
return $keywordChunks;
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
$rows = $this->lookup->findByChunkIds($chunkIds);
|
$rows = $this->lookup->findByChunkIds($chunkIds);
|
||||||
|
|
||||||
// routed filtering by document_id
|
|
||||||
$finalChunkIds = $chunkIds;
|
$finalChunkIds = $chunkIds;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------
|
||||||
|
// 3) Routed filtering (wenn candidateSet vorhanden)
|
||||||
|
// ---------------------------------------------------------
|
||||||
if ($candidateSet !== null) {
|
if ($candidateSet !== null) {
|
||||||
$filtered = [];
|
$filtered = [];
|
||||||
|
|
||||||
@@ -103,18 +117,20 @@ final class NdjsonHybridRetriever implements RetrieverInterface
|
|||||||
if (!is_array($row)) {
|
if (!is_array($row)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$docId = $row['document_id'] ?? null;
|
$docId = $row['document_id'] ?? null;
|
||||||
if (!is_string($docId) || !isset($candidateSet[$docId])) {
|
if (!is_string($docId) || !isset($candidateSet[$docId])) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$filtered[] = $id;
|
$filtered[] = $id;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Soft fallback: if routing filtered everything away, retry global vector once
|
// Wenn Routing ALLES wegfiltert -> einmal global retry
|
||||||
if ($filtered === []) {
|
if ($filtered === []) {
|
||||||
$hits2 = $this->vectorClient->search($prompt, $this->vectorTopK);
|
$hits2 = $this->vectorClient->search($prompt, $vectorTopKBase);
|
||||||
if ($hits2 === []) {
|
if ($hits2 === []) {
|
||||||
return $keywordChunks;
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
$chunkIds2 = [];
|
$chunkIds2 = [];
|
||||||
@@ -129,7 +145,7 @@ final class NdjsonHybridRetriever implements RetrieverInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($chunkIds2 === []) {
|
if ($chunkIds2 === []) {
|
||||||
return $keywordChunks;
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
$rows = $this->lookup->findByChunkIds($chunkIds2);
|
$rows = $this->lookup->findByChunkIds($chunkIds2);
|
||||||
@@ -139,24 +155,25 @@ final class NdjsonHybridRetriever implements RetrieverInterface
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($finalChunkIds as $id) {
|
|
||||||
if (!isset($rows[$id]['text']) || !is_string($rows[$id]['text'])) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$keywordChunks[] = trim($rows[$id]['text']);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------
|
// ---------------------------------------------------------
|
||||||
// 3) dedupe + limit
|
// 4) Collect texts + Dedupe + Limit
|
||||||
// ---------------------------------------------------------
|
// ---------------------------------------------------------
|
||||||
$seen = [];
|
$seen = [];
|
||||||
$out = [];
|
$out = [];
|
||||||
|
|
||||||
foreach ($keywordChunks as $chunk) {
|
foreach ($finalChunkIds as $id) {
|
||||||
|
$text = $rows[$id]['text'] ?? null;
|
||||||
|
if (!is_string($text) || $text === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$chunk = trim($text);
|
||||||
$key = mb_strtolower((string)preg_replace('/\s+/u', ' ', $chunk));
|
$key = mb_strtolower((string)preg_replace('/\s+/u', ' ', $chunk));
|
||||||
|
|
||||||
if (isset($seen[$key])) {
|
if (isset($seen[$key])) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$seen[$key] = true;
|
$seen[$key] = true;
|
||||||
$out[] = $chunk;
|
$out[] = $chunk;
|
||||||
|
|
||||||
@@ -168,16 +185,6 @@ final class NdjsonHybridRetriever implements RetrieverInterface
|
|||||||
return $out;
|
return $out;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Streaming Keyword Search über index.ndjson.
|
|
||||||
* Minimal, aber nützlich:
|
|
||||||
* - Score = Anzahl gefundener Terms
|
|
||||||
* - CandidateDocs (Tag-Routing) reduziert Scan massiv
|
|
||||||
*
|
|
||||||
* @param string[] $terms
|
|
||||||
* @param array<string,true>|null $candidateSet
|
|
||||||
* @return string[]
|
|
||||||
*/
|
|
||||||
private function keywordSearchStreaming(array $terms, int $limit, ?array $candidateSet): array
|
private function keywordSearchStreaming(array $terms, int $limit, ?array $candidateSet): array
|
||||||
{
|
{
|
||||||
if ($terms === []) {
|
if ($terms === []) {
|
||||||
@@ -185,31 +192,28 @@ final class NdjsonHybridRetriever implements RetrieverInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
$maxScore = \count($terms);
|
$maxScore = \count($terms);
|
||||||
|
|
||||||
// top list: each item = ['score' => int, 'text' => string]
|
|
||||||
$top = [];
|
$top = [];
|
||||||
|
|
||||||
foreach ($this->chunkManager->streamAll() as $row) {
|
foreach ($this->chunkManager->streamAll() as $row) {
|
||||||
$text = $row['text'] ?? null;
|
$text = $row['text'] ?? null;
|
||||||
|
|
||||||
if (!is_string($text) || $text === '') {
|
if (!is_string($text) || $text === '') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($candidateSet !== null) {
|
if ($candidateSet !== null) {
|
||||||
$docId = $row['document_id'] ?? null;
|
$docId = $row['document_id'] ?? null;
|
||||||
|
|
||||||
if (!is_string($docId) || !isset($candidateSet[$docId])) {
|
if (!is_string($docId) || !isset($candidateSet[$docId])) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$haystack = mb_strtolower($text);
|
$haystack = mb_strtolower($text);
|
||||||
|
|
||||||
$score = 0;
|
$score = 0;
|
||||||
|
|
||||||
foreach ($terms as $t) {
|
foreach ($terms as $t) {
|
||||||
if ($t === '') {
|
if ($t !== '' && mb_stripos($haystack, $t) !== false) {
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (mb_stripos($haystack, $t) !== false) {
|
|
||||||
$score++;
|
$score++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -223,14 +227,11 @@ final class NdjsonHybridRetriever implements RetrieverInterface
|
|||||||
'text' => trim($text),
|
'text' => trim($text),
|
||||||
];
|
];
|
||||||
|
|
||||||
// keep only best N (simple sort, N is tiny)
|
|
||||||
usort($top, static function (array $a, array $b): int {
|
usort($top, static function (array $a, array $b): int {
|
||||||
// higher score first
|
|
||||||
$cmp = ($b['score'] <=> $a['score']);
|
$cmp = ($b['score'] <=> $a['score']);
|
||||||
if ($cmp !== 0) {
|
if ($cmp !== 0) {
|
||||||
return $cmp;
|
return $cmp;
|
||||||
}
|
}
|
||||||
// shorter chunk first (often more precise)
|
|
||||||
return (mb_strlen($a['text']) <=> mb_strlen($b['text']));
|
return (mb_strlen($a['text']) <=> mb_strlen($b['text']));
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -238,25 +239,14 @@ final class NdjsonHybridRetriever implements RetrieverInterface
|
|||||||
$top = array_slice($top, 0, $limit);
|
$top = array_slice($top, 0, $limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
// early exit: perfect matches filled
|
|
||||||
if (\count($top) === $limit && ($top[0]['score'] ?? 0) >= $maxScore) {
|
if (\count($top) === $limit && ($top[0]['score'] ?? 0) >= $maxScore) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$out = [];
|
return array_map(static fn($item) => (string)$item['text'], $top);
|
||||||
foreach ($top as $item) {
|
|
||||||
$out[] = (string)$item['text'];
|
|
||||||
}
|
|
||||||
|
|
||||||
return $out;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Minimal term extraction (stabiles Verhalten, wenig Magie)
|
|
||||||
*
|
|
||||||
* @return string[]
|
|
||||||
*/
|
|
||||||
private function extractTerms(string $text): array
|
private function extractTerms(string $text): array
|
||||||
{
|
{
|
||||||
$text = mb_strtolower((string)preg_replace('/[^\p{L}\p{N}\s]/u', '', $text));
|
$text = mb_strtolower((string)preg_replace('/[^\p{L}\p{N}\s]/u', '', $text));
|
||||||
@@ -266,15 +256,14 @@ final class NdjsonHybridRetriever implements RetrieverInterface
|
|||||||
static fn(string $w) => mb_strlen($w) > 2
|
static fn(string $w) => mb_strlen($w) > 2
|
||||||
));
|
));
|
||||||
|
|
||||||
// unique, order preserved
|
|
||||||
$seen = [];
|
$seen = [];
|
||||||
$out = [];
|
$out = [];
|
||||||
|
|
||||||
foreach ($parts as $w) {
|
foreach ($parts as $w) {
|
||||||
if (isset($seen[$w])) {
|
if (!isset($seen[$w])) {
|
||||||
continue;
|
$seen[$w] = true;
|
||||||
|
$out[] = $w;
|
||||||
}
|
}
|
||||||
$seen[$w] = true;
|
|
||||||
$out[] = $w;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $out;
|
return $out;
|
||||||
|
|||||||
@@ -1,11 +1,28 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Knowledge\Retrieval;
|
namespace App\Knowledge\Retrieval;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieval ist vollständig konfigurationsgetrieben.
|
||||||
|
*
|
||||||
|
* - retrievalMaxChunks stammt ausschließlich aus der aktiven ModelGenerationConfig.
|
||||||
|
* - retrievalVectorTopK stammt ausschließlich aus der aktiven ModelGenerationConfig.
|
||||||
|
* - Es existiert kein Runtime-Override.
|
||||||
|
*
|
||||||
|
* Ziel:
|
||||||
|
* Deterministisches, auditierbares Retrieval-Verhalten.
|
||||||
|
*/
|
||||||
interface RetrieverInterface
|
interface RetrieverInterface
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @return string[] Plain text knowledge chunks
|
* Retrieves relevant knowledge chunks for a given prompt.
|
||||||
|
*
|
||||||
|
* The number of returned chunks is strictly defined by
|
||||||
|
* the active ModelGenerationConfig (retrievalMaxChunks).
|
||||||
|
*
|
||||||
|
* @return string[] Plain text knowledge chunks
|
||||||
*/
|
*/
|
||||||
public function retrieve(string $prompt, int $limit = 10): array;
|
public function retrieve(string $prompt): array;
|
||||||
}
|
}
|
||||||
@@ -22,7 +22,10 @@
|
|||||||
|
|
||||||
<div class="row g-4">
|
<div class="row g-4">
|
||||||
|
|
||||||
<!-- Modell -->
|
<!-- ============================== -->
|
||||||
|
<!-- Modell-Basis -->
|
||||||
|
<!-- ============================== -->
|
||||||
|
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label">Modellname</label>
|
<label class="form-label">Modellname</label>
|
||||||
<input type="text"
|
<input type="text"
|
||||||
@@ -31,11 +34,10 @@
|
|||||||
placeholder="z. B. qwen3:latest"
|
placeholder="z. B. qwen3:latest"
|
||||||
required>
|
required>
|
||||||
<div class="form-text text-secondary">
|
<div class="form-text text-secondary">
|
||||||
Exakter Modellname wie im KI-Endpunkt konfiguriert (z. B. Ollama oder API).
|
Exakter Modellname wie im KI-Endpunkt konfiguriert.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Stream -->
|
|
||||||
<div class="col-md-6 d-flex align-items-center">
|
<div class="col-md-6 d-flex align-items-center">
|
||||||
<div class="form-check form-switch mt-4">
|
<div class="form-check form-switch mt-4">
|
||||||
<input class="form-check-input"
|
<input class="form-check-input"
|
||||||
@@ -48,11 +50,22 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-text text-secondary ms-3">
|
<div class="form-text text-secondary ms-3">
|
||||||
Aktiviert Token-Streaming im Chat (empfohlen für bessere UX).
|
Token-Streaming im Chat (empfohlen).
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Temperature -->
|
</div>
|
||||||
|
|
||||||
|
<hr class="border-secondary my-4">
|
||||||
|
|
||||||
|
<!-- ============================== -->
|
||||||
|
<!-- GENERATION -->
|
||||||
|
<!-- ============================== -->
|
||||||
|
|
||||||
|
<h5 class="text-info mb-3">Generation (LLM Sampling)</h5>
|
||||||
|
|
||||||
|
<div class="row g-4">
|
||||||
|
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<label class="form-label">Temperature</label>
|
<label class="form-label">Temperature</label>
|
||||||
<input type="number"
|
<input type="number"
|
||||||
@@ -64,29 +77,24 @@
|
|||||||
class="form-control bg-dark text-light border-secondary"
|
class="form-control bg-dark text-light border-secondary"
|
||||||
required>
|
required>
|
||||||
<div class="form-text text-secondary">
|
<div class="form-text text-secondary">
|
||||||
Steuert die Kreativität der Antworten.
|
Kreativität des Modells. Für RAG 0.2–0.4 empfohlen.
|
||||||
Niedrige Werte (0.2–0.4) erzeugen stabile, sachliche Ergebnisse – empfohlen für RAG-Systeme.
|
|
||||||
Höhere Werte führen zu freieren, weniger deterministischen Antworten.
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Top K -->
|
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<label class="form-label">Top K</label>
|
<label class="form-label">Top K</label>
|
||||||
<input type="number"
|
<input type="number"
|
||||||
min="1"
|
min="1"
|
||||||
|
max="200"
|
||||||
name="top_k"
|
name="top_k"
|
||||||
value="40"
|
value="40"
|
||||||
class="form-control bg-dark text-light border-secondary"
|
class="form-control bg-dark text-light border-secondary"
|
||||||
required>
|
required>
|
||||||
<div class="form-text text-secondary">
|
<div class="form-text text-secondary">
|
||||||
Begrenzt die Anzahl der wahrscheinlichsten Token, aus denen das Modell auswählt.
|
Begrenzt die Auswahl wahrscheinlicher Token.
|
||||||
Niedrigere Werte = konservativer, höhere Werte = flexibler.
|
|
||||||
20–50 ist für Wissenssysteme üblich.
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Top P -->
|
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<label class="form-label">Top P</label>
|
<label class="form-label">Top P</label>
|
||||||
<input type="number"
|
<input type="number"
|
||||||
@@ -98,13 +106,10 @@
|
|||||||
class="form-control bg-dark text-light border-secondary"
|
class="form-control bg-dark text-light border-secondary"
|
||||||
required>
|
required>
|
||||||
<div class="form-text text-secondary">
|
<div class="form-text text-secondary">
|
||||||
Nucleus Sampling: Das Modell berücksichtigt nur Token,
|
Nucleus Sampling – Balance zwischen Stabilität und Natürlichkeit.
|
||||||
deren kumulative Wahrscheinlichkeit innerhalb dieses Werts liegt.
|
|
||||||
0.8–0.95 bietet eine gute Balance zwischen Stabilität und Natürlichkeit.
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Repeat Penalty -->
|
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label">Repeat Penalty</label>
|
<label class="form-label">Repeat Penalty</label>
|
||||||
<input type="number"
|
<input type="number"
|
||||||
@@ -116,12 +121,10 @@
|
|||||||
class="form-control bg-dark text-light border-secondary"
|
class="form-control bg-dark text-light border-secondary"
|
||||||
required>
|
required>
|
||||||
<div class="form-text text-secondary">
|
<div class="form-text text-secondary">
|
||||||
Bestraft Wortwiederholungen. Werte leicht über 1.0 (z. B. 1.1–1.15)
|
Verhindert Wiederholungen (1.1–1.15 empfohlen).
|
||||||
verhindern Schleifen und redundante Antworten.
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Num Ctx -->
|
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label">Context Window (num_ctx)</label>
|
<label class="form-label">Context Window (num_ctx)</label>
|
||||||
<input type="number"
|
<input type="number"
|
||||||
@@ -132,9 +135,50 @@
|
|||||||
class="form-control bg-dark text-light border-secondary"
|
class="form-control bg-dark text-light border-secondary"
|
||||||
required>
|
required>
|
||||||
<div class="form-text text-secondary">
|
<div class="form-text text-secondary">
|
||||||
Maximale Kontextlänge in Tokens (Systemprompt + Benutzerfrage + Retrieval-Chunks).
|
Maximale Kontextlänge (System + Frage + Retrieval).
|
||||||
Muss vom Modell unterstützt werden.
|
</div>
|
||||||
Höhere Werte ermöglichen größere Wissenskontexte, erhöhen jedoch Speicher- und Rechenbedarf.
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="border-secondary my-4">
|
||||||
|
|
||||||
|
<!-- ============================== -->
|
||||||
|
<!-- RETRIEVAL -->
|
||||||
|
<!-- ============================== -->
|
||||||
|
|
||||||
|
<h5 class="text-warning mb-3">Retrieval (Wissensabruf)</h5>
|
||||||
|
|
||||||
|
<div class="row g-4">
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Max Chunks</label>
|
||||||
|
<input type="number"
|
||||||
|
min="1"
|
||||||
|
max="200"
|
||||||
|
name="retrieval_max_chunks"
|
||||||
|
value="25"
|
||||||
|
class="form-control bg-dark text-light border-secondary"
|
||||||
|
required>
|
||||||
|
<div class="form-text text-secondary">
|
||||||
|
Maximale Anzahl an Wissens-Chunks,
|
||||||
|
die dem Modell übergeben werden.
|
||||||
|
20–40 ist für die meisten Systeme optimal.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Vector Top K</label>
|
||||||
|
<input type="number"
|
||||||
|
min="1"
|
||||||
|
max="200"
|
||||||
|
name="retrieval_vector_top_k"
|
||||||
|
value="25"
|
||||||
|
class="form-control bg-dark text-light border-secondary"
|
||||||
|
required>
|
||||||
|
<div class="form-text text-secondary">
|
||||||
|
Anzahl der Vektor-Treffer vor Filterung.
|
||||||
|
Höhere Werte erhöhen Recall, können aber Rauschen verstärken.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -157,4 +201,4 @@
|
|||||||
Hinweis: Neue Konfigurationen werden zunächst inaktiv gespeichert und
|
Hinweis: Neue Konfigurationen werden zunächst inaktiv gespeichert und
|
||||||
müssen separat aktiviert werden. Pro Modell kann nur eine Version aktiv sein.
|
müssen separat aktiviert werden. Pro Modell kann nur eine Version aktiv sein.
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -24,11 +24,8 @@
|
|||||||
<th>Modell</th>
|
<th>Modell</th>
|
||||||
<th>Version</th>
|
<th>Version</th>
|
||||||
<th>Stream</th>
|
<th>Stream</th>
|
||||||
<th>Temp</th>
|
<th>Sampling</th>
|
||||||
<th>Top K</th>
|
<th class="text-warning">Retrieval</th>
|
||||||
<th>Top P</th>
|
|
||||||
<th>Repeat</th>
|
|
||||||
<th>Ctx</th>
|
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
<th class="text-end">Aktionen</th>
|
<th class="text-end">Aktionen</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -52,27 +49,47 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td>{{ config.temperature }}</td>
|
{# ========================= #}
|
||||||
<td>{{ config.topK }}</td>
|
{# Sampling Group #}
|
||||||
<td>{{ config.topP }}</td>
|
{# ========================= #}
|
||||||
<td>{{ config.repeatPenalty }}</td>
|
<td>
|
||||||
<td>{{ config.numCtx }}</td>
|
<div class="small">
|
||||||
|
<div><strong>T:</strong> {{ config.temperature }}</div>
|
||||||
|
<div><strong>K:</strong> {{ config.topK }}</div>
|
||||||
|
<div><strong>P:</strong> {{ config.topP }}</div>
|
||||||
|
<div><strong>R:</strong> {{ config.repeatPenalty }}</div>
|
||||||
|
<div><strong>Ctx:</strong> {{ config.numCtx }}</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{# ========================= #}
|
||||||
|
{# Retrieval Group #}
|
||||||
|
{# ========================= #}
|
||||||
|
<td class="text-warning">
|
||||||
|
<div class="small">
|
||||||
|
<div><strong>Chunks:</strong> {{ config.retrievalMaxChunks }}</div>
|
||||||
|
<div><strong>VectorK:</strong> {{ config.retrievalVectorTopK }}</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
{% if config.active %}
|
{% if config.active %}
|
||||||
<span class="badge bg-success">Aktiv</span>
|
<span class="badge bg-success">Aktiv</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="badge bg-dark border border-secondary">
|
<span class="badge bg-dark border border-secondary">
|
||||||
Inaktiv
|
Inaktiv
|
||||||
</span>
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td class="text-end">
|
<td class="text-end">
|
||||||
|
<a href="{{ path('admin_model_config_test_retrieval', {id: config.id}) }}"
|
||||||
|
class="btn btn-sm btn-outline-warning me-1">
|
||||||
|
Test Retrieval
|
||||||
|
</a>
|
||||||
|
|
||||||
{% if not config.active and is_granted('ROLE_SUPER_ADMIN') %}
|
{% if not config.active and is_granted('ROLE_SUPER_ADMIN') %}
|
||||||
|
|
||||||
{# Aktivieren via POST + CSRF #}
|
|
||||||
<form method="post"
|
<form method="post"
|
||||||
action="{{ path('admin_model_config_activate', {id: config.id}) }}"
|
action="{{ path('admin_model_config_activate', {id: config.id}) }}"
|
||||||
class="d-inline"
|
class="d-inline"
|
||||||
@@ -82,12 +99,11 @@
|
|||||||
name="_token"
|
name="_token"
|
||||||
value="{{ csrf_token('activate_model_config_' ~ config.id) }}">
|
value="{{ csrf_token('activate_model_config_' ~ config.id) }}">
|
||||||
|
|
||||||
<button class="btn btn-sm btn-outline-success me-2">
|
<button class="btn btn-sm btn-outline-success me-1">
|
||||||
Aktivieren
|
Aktivieren
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{# Löschen via POST + CSRF #}
|
|
||||||
<form method="post"
|
<form method="post"
|
||||||
action="{{ path('admin_model_config_delete', {id: config.id}) }}"
|
action="{{ path('admin_model_config_delete', {id: config.id}) }}"
|
||||||
class="d-inline"
|
class="d-inline"
|
||||||
@@ -105,13 +121,12 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
<span class="text-secondary small">—</span>
|
<span class="text-secondary small">—</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="10" class="text-center text-secondary py-4">
|
<td colspan="7" class="text-center text-secondary py-4">
|
||||||
Keine Konfiguration vorhanden.
|
Keine Konfiguration vorhanden.
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -132,13 +147,11 @@
|
|||||||
|
|
||||||
<div class="card bg-black border-info">
|
<div class="card bg-black border-info">
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
|
|
||||||
<iframe
|
<iframe
|
||||||
src="/index.html?admin_test=1"
|
src="/index.html?admin_test=1"
|
||||||
class="w-100 border-0"
|
class="w-100 border-0"
|
||||||
style="height:75vh;"
|
style="height:75vh;"
|
||||||
></iframe>
|
></iframe>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -146,4 +159,5 @@
|
|||||||
Der Agent läuft im isolierten Admin-Test-Modus.
|
Der Agent läuft im isolierten Admin-Test-Modus.
|
||||||
Keine Persistenz. Keine produktive Session.
|
Keine Persistenz. Keine produktive Session.
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
|
||||||
|
{% endblock %}
|
||||||
93
templates/admin/model_config/test_retrieval.html.twig
Normal file
93
templates/admin/model_config/test_retrieval.html.twig
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
{% extends 'admin/base.html.twig' %}
|
||||||
|
|
||||||
|
{% block title %}Retrieval-Test{% endblock %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h1 class="h3">
|
||||||
|
Retrieval-Test – {{ config.modelName }} (v{{ config.version }})
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<a href="{{ path('admin_model_config_list') }}"
|
||||||
|
class="btn btn-sm btn-outline-secondary">
|
||||||
|
Zurück
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# ====================================================== #}
|
||||||
|
{# Konfigurationsübersicht #}
|
||||||
|
{# ====================================================== #}
|
||||||
|
|
||||||
|
<div class="card bg-black border-secondary text-light mb-4">
|
||||||
|
<div class="card-body small">
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<strong>Max Chunks:</strong>
|
||||||
|
{{ config.retrievalMaxChunks }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<strong>Vector Top K:</strong>
|
||||||
|
{{ config.retrievalVectorTopK }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# ====================================================== #}
|
||||||
|
{# Testformular #}
|
||||||
|
{# ====================================================== #}
|
||||||
|
|
||||||
|
<div class="card bg-black border-secondary text-light mb-4">
|
||||||
|
<div class="card-body">
|
||||||
|
|
||||||
|
<form method="post">
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Test-Prompt</label>
|
||||||
|
<textarea name="prompt"
|
||||||
|
rows="3"
|
||||||
|
class="form-control bg-dark text-light border-secondary"
|
||||||
|
required>{{ prompt }}</textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit"
|
||||||
|
class="btn btn-outline-warning">
|
||||||
|
Retrieval testen
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# ====================================================== #}
|
||||||
|
{# Ergebnisse #}
|
||||||
|
{# ====================================================== #}
|
||||||
|
|
||||||
|
{% if results is not empty %}
|
||||||
|
<div class="card bg-black border-secondary text-light">
|
||||||
|
<div class="card-body">
|
||||||
|
|
||||||
|
<h5 class="text-warning mb-3">
|
||||||
|
Gefundene Chunks ({{ results|length }})
|
||||||
|
</h5>
|
||||||
|
|
||||||
|
<div style="max-height: 500px; overflow-y: auto;">
|
||||||
|
|
||||||
|
{% for chunk in results %}
|
||||||
|
<div class="border border-secondary p-3 mb-3 small">
|
||||||
|
{{ chunk }}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user