first update to external config values

This commit is contained in:
team 1
2026-04-24 13:13:56 +02:00
parent 868f9a8857
commit 26ec0afc5c
11 changed files with 292 additions and 187 deletions

View File

@@ -35,6 +35,11 @@ final class SystemRebuildCommand extends Command
private readonly VectorIndexHealthService $health,
private readonly TagVectorIndexHealthService $tagHealth,
private readonly string $projectDir,
private readonly string $vectorPythonBin = '.venv/bin/python',
private readonly string $vectorControlScript = 'python/vector/vector_control.py',
private readonly string $vectorHost = '0.0.0.0',
private readonly int $vectorPort = 8090,
private readonly int $vectorTimeoutSeconds = 600,
) {
parent::__construct();
}
@@ -166,17 +171,17 @@ final class SystemRebuildCommand extends Command
}
$cmd = [
'.venv/bin/python',
'python/vector/vector_control.py',
$this->vectorPythonBin,
$this->vectorControlScript,
'--install',
'--start',
'--reload',
'--port', '8090',
'--host', '0.0.0.0',
'--port', (string) $this->vectorPort,
'--host', $this->vectorHost,
];
$process = new Process($cmd, $this->projectDir);
$process->setTimeout(600);
$process->setTimeout($this->vectorTimeoutSeconds);
$process->run();
$stdout = trim($process->getOutput());

View File

@@ -17,6 +17,16 @@ use Symfony\Component\Process\Process;
)]
final class VectorControlCommand extends Command
{
public function __construct(
private readonly string $vectorPythonBin = '.venv/bin/python',
private readonly string $vectorControlScript = 'python/vector/vector_control.py',
private readonly string $defaultHost = '0.0.0.0',
private readonly int $defaultPort = 8090,
private readonly int $timeoutSeconds = 300,
) {
parent::__construct();
}
protected function configure(): void
{
$this
@@ -27,13 +37,13 @@ final class VectorControlCommand extends Command
->addOption('reload', null, InputOption::VALUE_NONE, 'Trigger /reload')
->addOption('status', null, InputOption::VALUE_NONE, 'Print status')
->addOption('foreground', null, InputOption::VALUE_NONE, 'Start in foreground (rare)')
->addOption('port', null, InputOption::VALUE_OPTIONAL, 'Port (default 8090)', '8090')
->addOption('host', null, InputOption::VALUE_OPTIONAL, 'Host (default 0.0.0.0)', '0.0.0.0');
->addOption('port', null, InputOption::VALUE_OPTIONAL, 'Port', (string) $this->defaultPort)
->addOption('host', null, InputOption::VALUE_OPTIONAL, 'Host', $this->defaultHost);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$cmd = ['.venv/bin/python', 'python/vector/vector_control.py'];
$cmd = [$this->vectorPythonBin, $this->vectorControlScript];
if ($input->getOption('install')) {
$cmd[] = '--install';
@@ -58,17 +68,17 @@ final class VectorControlCommand extends Command
}
$cmd[] = '--port';
$cmd[] = (string)$input->getOption('port');
$cmd[] = (string) $input->getOption('port');
$cmd[] = '--host';
$cmd[] = (string)$input->getOption('host');
$cmd[] = (string) $input->getOption('host');
$process = new Process($cmd);
$process->setTimeout(300);
$process->setTimeout($this->timeoutSeconds);
$process->run();
$output->writeln($process->getOutput());
return $process->isSuccessful() ? Command::SUCCESS : Command::FAILURE;
}
}
}

View File

@@ -6,24 +6,32 @@ namespace App\Config;
final class AgentRunnerConfig
{
/**
* @param array<string, mixed> $config
*/
public function __construct(
private readonly array $config = [],
) {
}
public function getCommerceHistoryBudgetChars(): int
{
return 1000;
return $this->getInt('commerce_history_budget_chars', 1000);
}
public function getProductSearchKnowledgeChunkLimit(): int
{
return 6;
return $this->getInt('product_search_knowledge_chunk_limit', 6);
}
public function getAdvisoryProductSearchKnowledgeChunkLimit(): int
{
return 9;
return $this->getInt('advisory_product_search_knowledge_chunk_limit', 9);
}
public function getOptimizedShopQueryPrefixPattern(): string
{
return '/^(?:keywords?|suchquery|search\s*query|query)\s*:\s*/iu';
return $this->getString('optimized_shop_query_prefix_pattern', '/^(?:keywords?|suchquery|search\\s*query|query)\\s*:\\s*/iu');
}
public function getOptimizedShopQueryTrimCharacters(): string
@@ -31,6 +39,20 @@ final class AgentRunnerConfig
return " \t\n\r\0\x0B\"'`";
}
private function getInt(string $key, int $default): int
{
$value = $this->config[$key] ?? $default;
return is_numeric($value) ? (int) $value : $default;
}
private function getString(string $key, string $default): string
{
$value = $this->config[$key] ?? $default;
return is_string($value) && $value !== '' ? $value : $default;
}
public function getEmptyPromptMessage(): string
{
return '❌ Empty prompt.';

View File

@@ -6,64 +6,101 @@ namespace App\Config;
final class PromptBuilderConfig
{
/**
* @param array<string, mixed> $config
*/
public function __construct(
private readonly array $config = [],
) {
}
public function getCharsPerToken(): int
{
return 4;
return $this->getInt('budget.chars_per_token', 4);
}
public function getHistoryPaddingChars(): int
{
return 400;
return $this->getInt('budget.history_padding_chars', 400);
}
public function getOutputReserveRatio(): float
{
return 0.25;
return $this->getFloat('budget.output_reserve_ratio', 0.25);
}
public function getOutputReserveMinTokens(): int
{
return 768;
return $this->getInt('budget.output_reserve_min_tokens', 768);
}
public function getOutputReserveMaxTokens(): int
{
return 6000;
return $this->getInt('budget.output_reserve_max_tokens', 6000);
}
public function getSafetyReserveRatio(): float
{
return 0.05;
return $this->getFloat('budget.safety_reserve_ratio', 0.05);
}
public function getSafetyReserveMinTokens(): int
{
return 256;
return $this->getInt('budget.safety_reserve_min_tokens', 256);
}
public function getSafetyReserveMaxTokens(): int
{
return 1024;
return $this->getInt('budget.safety_reserve_max_tokens', 1024);
}
public function getMinPromptBudgetTokens(): int
{
return 1024;
return $this->getInt('budget.min_prompt_budget_tokens', 1024);
}
public function getMaxShopResultsInPrompt(): int
{
return 24;
return $this->getInt('shop_results.max_results_in_prompt', 24);
}
public function getDetailedShopResultsMaxCount(): int
{
return 5;
return $this->getInt('shop_results.detailed_max_count', 5);
}
public function getTechnicalProductKeywordMatchThreshold(): int
{
return 2;
return $this->getInt('technical_product_keyword_match_threshold', 2);
}
private function getInt(string $path, int $default): int
{
$value = $this->getValue($path, $default);
return is_numeric($value) ? (int) $value : $default;
}
private function getFloat(string $path, float $default): float
{
$value = $this->getValue($path, $default);
return is_numeric($value) ? (float) $value : $default;
}
private function getValue(string $path, mixed $default): mixed
{
$current = $this->config;
foreach (explode('.', $path) as $segment) {
if (!is_array($current) || !array_key_exists($segment, $current)) {
return $default;
}
$current = $current[$segment];
}
return $current;
}
public function getSystemSectionLabel(): string

View File

@@ -6,19 +6,26 @@ namespace App\Config;
final class SearchRepairConfig
{
public function __construct(
private readonly bool $enabled = true,
private readonly int $maxRepairQueries = 3,
private readonly int $minPrimaryResultsWithoutRepair = 2,
) {
}
public function isEnabled(): bool
{
return true;
return $this->enabled;
}
public function getMaxRepairQueries(): int
{
return 3;
return $this->maxRepairQueries;
}
public function getMinPrimaryResultsWithoutRepair(): int
{
return 2;
return $this->minPrimaryResultsWithoutRepair;
}
public function getTopProductLogLimit(): int

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Service\Admin;
use App\Config\ModelGenerationDefaultsConfig;
use App\Entity\ModelGenerationConfig;
use App\Knowledge\Retrieval\NdjsonHybridRetriever;
use App\Repository\ModelGenerationConfigRepository;
@@ -17,6 +18,7 @@ final class ModelGenerationConfigAdminService
private readonly EntityManagerInterface $em,
private readonly ModelGenerationConfigManager $manager,
private readonly NdjsonHybridRetriever $retriever,
private readonly ModelGenerationDefaultsConfig $defaults,
) {}
public function list(): array
@@ -36,15 +38,15 @@ final class ModelGenerationConfigAdminService
$config = new ModelGenerationConfig(
modelName: $modelName,
version: $version,
stream: (bool)($data['stream'] ?? false),
temperature: (float)($data['temperature'] ?? 0.7),
topK: (int)($data['top_k'] ?? 40),
topP: (float)($data['top_p'] ?? 0.9),
repeatPenalty: (float)($data['repeat_penalty'] ?? 1.1),
numCtx: (int)($data['num_ctx'] ?? 4096),
stream: (bool)($data['stream'] ?? $this->defaults->isStream()),
temperature: (float)($data['temperature'] ?? $this->defaults->getTemperature()),
topK: (int)($data['top_k'] ?? $this->defaults->getTopK()),
topP: (float)($data['top_p'] ?? $this->defaults->getTopP()),
repeatPenalty: (float)($data['repeat_penalty'] ?? $this->defaults->getRepeatPenalty()),
numCtx: (int)($data['num_ctx'] ?? $this->defaults->getNumCtx()),
active: false,
retrievalMaxChunks: (int)($data['retrieval_max_chunks'] ?? 25),
retrievalVectorTopK: (int)($data['retrieval_vector_top_k'] ?? 25),
retrievalMaxChunks: (int)($data['retrieval_max_chunks'] ?? $this->defaults->getRetrievalMaxChunks()),
retrievalVectorTopK: (int)($data['retrieval_vector_top_k'] ?? $this->defaults->getRetrievalVectorTopK()),
);
$this->em->persist($config);
@@ -61,7 +63,7 @@ final class ModelGenerationConfigAdminService
public function delete(ModelGenerationConfig $config): void
{
if ($config->isActive()) {
throw new \RuntimeException('Aktive Konfiguration kann nicht gelöscht werden.');
throw new \RuntimeException('Aktive Konfiguration kann nicht geloescht werden.');
}
$this->em->remove($config);
@@ -74,7 +76,7 @@ final class ModelGenerationConfigAdminService
return [];
}
return $this->retriever->retrieveDebug($prompt,$config);
return $this->retriever->retrieveDebug($prompt, $config);
}
private function requireString(mixed $value, string $field): string
@@ -84,4 +86,4 @@ final class ModelGenerationConfigAdminService
}
return trim($value);
}
}
}

View File

@@ -4,15 +4,16 @@ declare(strict_types=1);
namespace App\Service;
use App\Config\ModelGenerationDefaultsConfig;
use App\Entity\ModelGenerationConfig;
use App\Repository\ModelGenerationConfigRepository;
final readonly class ModelGenerationConfigProvider
{
public function __construct(
private ModelGenerationConfigRepository $repository
)
{
private ModelGenerationConfigRepository $repository,
private ModelGenerationDefaultsConfig $defaults,
) {
}
public function getActiveForModel(): ModelGenerationConfig
@@ -23,19 +24,18 @@ final readonly class ModelGenerationConfigProvider
return $config;
}
// ------------------------------
// Safe Enterprise Default Fallback
// ------------------------------
return new ModelGenerationConfig(
modelName: 'mto-model',
modelName: $this->defaults->getModelName(),
version: 0,
stream: false,
temperature: 0.1,
topK: 20,
topP: 0.8,
repeatPenalty: 1.05,
numCtx: 4096,
active: false
stream: $this->defaults->isStream(),
temperature: $this->defaults->getTemperature(),
topK: $this->defaults->getTopK(),
topP: $this->defaults->getTopP(),
repeatPenalty: $this->defaults->getRepeatPenalty(),
numCtx: $this->defaults->getNumCtx(),
active: false,
retrievalMaxChunks: $this->defaults->getRetrievalMaxChunks(),
retrievalVectorTopK: $this->defaults->getRetrievalVectorTopK(),
);
}
@@ -45,4 +45,4 @@ final readonly class ModelGenerationConfigProvider
return max(512, $numCtx);
}
}
}

View File

@@ -15,41 +15,16 @@ final class TagRoutingService
/**
* Number of raw tag hits requested from the vector service.
*/
private const DEFAULT_TOPK = 8;
/**
* Hard minimum confidence required to activate tag-based document routing.
*
* This intentionally aligns with the tag vector client gate to avoid
* misleading secondary thresholds in this class.
*/
private const MIN_BEST_SCORE = 0.72;
/**
* Only keep tag hits that stay reasonably close to the best hit.
* This reduces semantic spillover into weakly related document spaces.
*/
private const MAX_SCORE_DROP_FROM_BEST = 0.08;
/**
* Maximum number of tag hits that may influence routing.
*/
private const MAX_ROUTING_TAGS = 5;
/**
* Maximum number of candidate documents passed into scoped chunk search.
*/
private const MAX_CANDIDATE_DOCS = 80;
/**
* Small bonus for documents matched by multiple routed tags.
*/
private const MULTI_TAG_BONUS_PER_EXTRA_TAG = 0.05;
private const MAX_MULTI_TAG_BONUS = 0.15;
public function __construct(
private readonly TagVectorSearchClient $tagSearch,
private readonly EntityManagerInterface $em,
private readonly int $defaultTopK = 8,
private readonly float $minBestScore = 0.72,
private readonly float $maxScoreDropFromBest = 0.08,
private readonly int $maxRoutingTags = 5,
private readonly int $maxCandidateDocs = 80,
private readonly float $multiTagBonusPerExtraTag = 0.05,
private readonly float $maxMultiTagBonus = 0.15,
) {
}
@@ -71,7 +46,7 @@ final class TagRoutingService
}
$hits = $this->filterRoutingHits(
$this->tagSearch->search($query, self::DEFAULT_TOPK)
$this->tagSearch->search($query, $this->defaultTopK)
);
if ($hits === []) {
@@ -159,8 +134,8 @@ final class TagRoutingService
if ($matchedTagCount > 1) {
$documentScores[$documentId] += min(
self::MAX_MULTI_TAG_BONUS,
($matchedTagCount - 1) * self::MULTI_TAG_BONUS_PER_EXTRA_TAG
$this->maxMultiTagBonus,
($matchedTagCount - 1) * $this->multiTagBonusPerExtraTag
);
}
}
@@ -170,7 +145,7 @@ final class TagRoutingService
return array_slice(
array_keys($documentScores),
0,
self::MAX_CANDIDATE_DOCS
$this->maxCandidateDocs
);
}
@@ -196,13 +171,13 @@ final class TagRoutingService
$bestScore = (float) ($hits[0]['score'] ?? 0.0);
if ($bestScore < self::MIN_BEST_SCORE) {
if ($bestScore < $this->minBestScore) {
return [];
}
$minimumAcceptedScore = max(
self::MIN_BEST_SCORE,
$bestScore - self::MAX_SCORE_DROP_FROM_BEST
$this->minBestScore,
$bestScore - $this->maxScoreDropFromBest
);
$filtered = [];
@@ -230,7 +205,7 @@ final class TagRoutingService
'tag_type' => $tagType,
];
if (count($filtered) >= self::MAX_ROUTING_TAGS) {
if (count($filtered) >= $this->maxRoutingTags) {
break;
}
}

View File

@@ -9,46 +9,20 @@ use Symfony\Contracts\HttpClient\HttpClientInterface;
final readonly class TagVectorSearchClient
{
/**
* Minimum similarity score required for a tag to be considered.
*/
public const MIN_SCORE = 0.72;
/**
* Default result size when callers do not specify a limit.
*/
private const DEFAULT_LIMIT = 8;
/**
* Hard limit to prevent excessive requests.
*/
private const MAX_LIMIT = 50;
/**
* HTTP timeout for the Python vector service.
*/
private const TIMEOUT_SECONDS = 10;
public function __construct(
private HttpClientInterface $http,
private string $serviceUrl,
private LoggerInterface $agentLogger,
private float $minScore = 0.72,
private int $defaultLimit = 8,
private int $maxLimit = 50,
private int $timeoutSeconds = 10,
) {
}
/**
* Executes a vector search against the Python tag index.
*
* Expected response rows:
* [
* {
* "tag_id": "...",
* "score": 0.73,
* "label": "Geräte",
* "tag_type": "catalog_entity"
* }
* ]
*
* @return list<array{
* tag_id:string,
* score:float,
@@ -56,7 +30,7 @@ final readonly class TagVectorSearchClient
* tag_type:string
* }>
*/
public function search(string $query, int $limit = self::DEFAULT_LIMIT): array
public function search(string $query, ?int $limit = null): array
{
$query = trim($query);
@@ -64,7 +38,7 @@ final readonly class TagVectorSearchClient
return [];
}
$limit = max(1, min($limit, self::MAX_LIMIT));
$limit = $this->clampLimit($limit ?? $this->defaultLimit);
$serviceUrl = rtrim(trim($this->serviceUrl), '/');
if ($serviceUrl === '') {
@@ -82,7 +56,7 @@ final readonly class TagVectorSearchClient
'query' => $query,
'limit' => $limit,
],
'timeout' => self::TIMEOUT_SECONDS,
'timeout' => $this->timeoutSeconds,
]
);
@@ -141,7 +115,7 @@ final readonly class TagVectorSearchClient
$score = (float) $score;
if ($score < self::MIN_SCORE) {
if ($score < $this->minScore) {
continue;
}
@@ -186,4 +160,9 @@ final readonly class TagVectorSearchClient
return array_slice($hits, 0, $limit);
}
}
private function clampLimit(int $limit): int
{
return max(1, min($limit, $this->maxLimit));
}
}

View File

@@ -9,17 +9,6 @@ use Symfony\Contracts\HttpClient\HttpClientInterface;
final class VectorSearchClient
{
/**
* Soft minimum similarity threshold.
* Lower than tag gate to allow broader recall.
*/
private const MIN_SCORE = 0.30;
/**
* Hard limit clamp to avoid abusive queries.
*/
private const MAX_LIMIT = 200;
private HttpClientInterface $http;
private string $serviceUrl;
private LoggerInterface $agentLogger;
@@ -27,7 +16,10 @@ final class VectorSearchClient
public function __construct(
HttpClientInterface $http,
string $serviceUrl,
LoggerInterface $agentLogger
LoggerInterface $agentLogger,
private readonly float $minScore = 0.30,
private readonly int $maxLimit = 200,
private readonly int $timeoutSeconds = 10,
) {
$this->http = $http;
$this->serviceUrl = rtrim($serviceUrl, '/');
@@ -100,7 +92,7 @@ final class VectorSearchClient
$this->serviceUrl . '/search-chunks',
[
'json' => $payload,
'timeout' => 10,
'timeout' => $this->timeoutSeconds,
]
);
@@ -143,8 +135,7 @@ final class VectorSearchClient
$score = (float)$score;
// 🔥 Soft Confidence Gate
if ($score < self::MIN_SCORE) {
if ($score < $this->minScore) {
continue;
}
@@ -179,10 +170,10 @@ final class VectorSearchClient
return 1;
}
if ($limit > self::MAX_LIMIT) {
return self::MAX_LIMIT;
if ($limit > $this->maxLimit) {
return $this->maxLimit;
}
return $limit;
}
}
}