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

@@ -1,55 +1,69 @@
imports:
- { resource: 'retriex/runtime.yaml' }
- { resource: 'retriex/index.yaml' }
- { resource: 'retriex/vector.yaml' }
- { resource: 'retriex/commerce.yaml' }
- { resource: 'retriex/model.yaml' }
- { resource: 'retriex/prompt.yaml' }
- { resource: 'retriex/agent.yaml' }
- { resource: 'retriex/retrieval.yaml' }
# ------------------------------------------------------------ # ------------------------------------------------------------
# Parameters # Parameters
# ------------------------------------------------------------ # ------------------------------------------------------------
parameters: parameters:
mto.root: '%kernel.project_dir%' mto.root: '%retriex.root%'
mto.kernel.dir: '%mto.root%' mto.kernel.dir: '%mto.root%'
mto.locks.dir: '%mto.knowledge.root%/locks' mto.locks.dir: '%retriex.locks.dir%'
mto.knowledge.root: '%mto.root%/var/knowledge' mto.knowledge.root: '%retriex.knowledge.root%'
mto.knowledge.ndjson: '%mto.knowledge.root%/index.ndjson' mto.knowledge.ndjson: '%retriex.knowledge.ndjson%'
mto.knowledge.index_meta: '%mto.knowledge.root%/index_meta.json' mto.knowledge.index_meta: '%retriex.knowledge.index_meta%'
mto.knowledge.vector_index: '%mto.knowledge.root%/vector.index' mto.knowledge.vector_index: '%retriex.knowledge.vector_index%'
mto.knowledge.vector_index_meta: '%mto.knowledge.root%/vector.index.meta.json' mto.knowledge.vector_index_meta: '%retriex.knowledge.vector_index_meta%'
mto.runtime.meta: '%mto.knowledge.root%/index_runtime.json' mto.runtime.meta: '%retriex.knowledge.runtime_meta%'
mto.knowledge.upload: '%mto.knowledge.root%/uploads' mto.knowledge.upload: '%retriex.knowledge.upload%'
mto.knowledge.tags_ndjson: '%mto.knowledge.root%/tags.ndjson' mto.knowledge.tags_ndjson: '%retriex.knowledge.tags_ndjson%'
mto.knowledge.vector_tags_index: '%mto.knowledge.root%/vector_tags.index' mto.knowledge.vector_tags_index: '%retriex.knowledge.vector_tags_index%'
mto.knowledge.vector_tags_index_meta: '%mto.knowledge.root%/vector_tags.index.meta.json' mto.knowledge.vector_tags_index_meta: '%retriex.knowledge.vector_tags_index_meta%'
mto.vector.script_dir: '%mto.root%/python/vector' mto.vector.script_dir: '%retriex.vector.script_dir%'
mto.vector.ingest_tags_script: '%mto.vector.script_dir%/vector_ingest_tags.py' mto.vector.ingest_tags_script: '%retriex.vector.ingest_tags_script%'
mto.vector.search_tags_script: '%mto.vector.script_dir%/vector_search_tags.py' mto.vector.search_tags_script: '%retriex.vector.search_tags_script%'
mto.tags.rebuild_lock: '%mto.locks.dir%/tag_rebuild.lock' mto.tags.rebuild_lock: '%retriex.tags.rebuild_lock%'
mto.vector.data.upload.path: '%mto.knowledge.upload%' mto.vector.data.upload.path: '%mto.knowledge.upload%'
mto.index.chunk_size: 250 mto.index.chunk_size: '%retriex.index.chunk_size%'
mto.index.chunk_overlap: 50 mto.index.chunk_overlap: '%retriex.index.chunk_overlap%'
mto.index.embedding_model: 'intfloat/multilingual-e5-base' mto.index.embedding_model: '%retriex.index.embedding_model%'
mto.index.embedding_dimension: 768 mto.index.embedding_dimension: '%retriex.index.embedding_dimension%'
mto.index.scoring_version: 1 mto.index.scoring_version: '%retriex.index.scoring_version%'
mto.vector.python_bin: '%kernel.project_dir%/.venv/bin/python3' mto.vector.python_bin: '%retriex.vector.python_bin%'
mto.vector.ingest_script: '%mto.vector.script_dir%/vector_ingest.py' mto.vector.control_script: '%retriex.vector.control_script%'
mto.vector.search_script: '%mto.vector.script_dir%/vector_search.py' mto.vector.ingest_script: '%retriex.vector.ingest_script%'
mto.vector.timeout: 600 mto.vector.search_script: '%retriex.vector.search_script%'
mto.vector.service_url: 'http://127.0.0.1:8090' mto.vector.timeout: '%retriex.vector.timeout%'
mto.vector.service_url: '%retriex.vector.service_url%'
mto.vector.host: '%retriex.vector.host%'
mto.vector.port: '%retriex.vector.port%'
mto.commerce.enabled: true mto.commerce.enabled: '%retriex.commerce.enabled%'
mto.commerce.max_shop_results: '%env(SHOPWARE_STORE_API_MAX_RESULT)%' mto.commerce.max_shop_results: '%retriex.commerce.max_shop_results%'
mto.commerce.shop_timeout: 5 mto.commerce.shop_timeout: '%retriex.commerce.shop_timeout%'
mto.commerce.store_api_base_url: '%env(SHOPWARE_STORE_API_BASE_URL)%' mto.commerce.store_api_base_url: '%retriex.commerce.store_api_base_url%'
mto.commerce.sales_channel_access_key: '%env(SHOPWARE_SALES_CHANNEL_ACCESS_KEY)%' mto.commerce.sales_channel_access_key: '%retriex.commerce.sales_channel_access_key%'
mto.commerce.search_repair.enabled: '%retriex.commerce.search_repair.enabled%'
mto.commerce.search_repair.max_queries: '%retriex.commerce.search_repair.max_queries%'
mto.commerce.search_repair.min_primary_results_without_repair: '%retriex.commerce.search_repair.min_primary_results_without_repair%'
mto.commerce.search_repair.enabled: true
mto.commerce.search_repair.max_queries: 3
mto.commerce.search_repair.min_primary_results_without_repair: 2
# ------------------------------------------------------------ # ------------------------------------------------------------
@@ -80,10 +94,35 @@ services:
# AI Agent Infrastructure # AI Agent Infrastructure
# ------------------------------------------------------------ # ------------------------------------------------------------
# ------------------------------------------------------------
# RetrieX Config Facades
# ------------------------------------------------------------
App\Config\ModelGenerationDefaultsConfig:
arguments:
$modelName: '%retriex.model.default_name%'
$stream: '%retriex.model.default_stream%'
$temperature: '%retriex.model.default_temperature%'
$topK: '%retriex.model.default_top_k%'
$topP: '%retriex.model.default_top_p%'
$repeatPenalty: '%retriex.model.default_repeat_penalty%'
$numCtx: '%retriex.model.default_num_ctx%'
$retrievalMaxChunks: '%retriex.model.default_retrieval_max_chunks%'
$retrievalVectorTopK: '%retriex.model.default_retrieval_vector_top_k%'
App\Config\PromptBuilderConfig:
arguments:
$config: '%retriex.prompt.config%'
App\Config\AgentRunnerConfig:
arguments:
$config: '%retriex.agent.config%'
App\Infrastructure\OllamaClient: App\Infrastructure\OllamaClient:
arguments: arguments:
$apiUrl: '%env(AI_LLM_API_URL)%' $apiUrl: '%env(AI_LLM_API_URL)%'
$timeoutSeconds: 600 $timeoutSeconds: '%retriex.llm.timeout_seconds%'
$configProvider: '@App\Service\ModelGenerationConfigProvider' $configProvider: '@App\Service\ModelGenerationConfigProvider'
# ------------------------------------------------------------ # ------------------------------------------------------------
@@ -120,6 +159,12 @@ services:
App\Commerce\CommerceQueryParser: ~ App\Commerce\CommerceQueryParser: ~
App\Config\SearchRepairConfig:
arguments:
$enabled: '%retriex.commerce.search_repair.enabled%'
$maxRepairQueries: '%retriex.commerce.search_repair.max_queries%'
$minPrimaryResultsWithoutRepair: '%retriex.commerce.search_repair.min_primary_results_without_repair%'
App\Commerce\SearchRepairService: ~ App\Commerce\SearchRepairService: ~
App\Shopware\ShopwareCriteriaBuilder: ~ App\Shopware\ShopwareCriteriaBuilder: ~
@@ -167,6 +212,9 @@ services:
arguments: arguments:
$serviceUrl: '%mto.vector.service_url%' $serviceUrl: '%mto.vector.service_url%'
$agentLogger: '@monolog.logger.agent' $agentLogger: '@monolog.logger.agent'
$minScore: '%retriex.vector.search.min_score%'
$maxLimit: '%retriex.vector.search.max_limit%'
$timeoutSeconds: '%retriex.vector.search.http_timeout%'
App\Vector\VectorIndexBuilder: App\Vector\VectorIndexBuilder:
arguments: arguments:
@@ -215,8 +263,20 @@ services:
arguments: arguments:
$serviceUrl: '%mto.vector.service_url%' $serviceUrl: '%mto.vector.service_url%'
$agentLogger: '@monolog.logger.agent' $agentLogger: '@monolog.logger.agent'
$minScore: '%retriex.vector.tags.min_score%'
$defaultLimit: '%retriex.vector.tags.default_limit%'
$maxLimit: '%retriex.vector.tags.max_limit%'
$timeoutSeconds: '%retriex.vector.tags.http_timeout%'
App\Tag\TagRoutingService: ~ App\Tag\TagRoutingService:
arguments:
$defaultTopK: '%retriex.vector.tag_routing.default_topk%'
$minBestScore: '%retriex.vector.tag_routing.min_best_score%'
$maxScoreDropFromBest: '%retriex.vector.tag_routing.max_score_drop_from_best%'
$maxRoutingTags: '%retriex.vector.tag_routing.max_routing_tags%'
$maxCandidateDocs: '%retriex.vector.tag_routing.max_candidate_docs%'
$multiTagBonusPerExtraTag: '%retriex.vector.tag_routing.multi_tag_bonus_per_extra_tag%'
$maxMultiTagBonus: '%retriex.vector.tag_routing.max_multi_tag_bonus%'
App\Tag\TagVectorIndexHealthService: App\Tag\TagVectorIndexHealthService:
arguments: arguments:
@@ -237,6 +297,23 @@ services:
arguments: arguments:
$lockFilePath: '%mto.tags.rebuild_lock%' $lockFilePath: '%mto.tags.rebuild_lock%'
App\Command\VectorControlCommand:
arguments:
$vectorPythonBin: '%mto.vector.python_bin%'
$vectorControlScript: '%mto.vector.control_script%'
$defaultHost: '%mto.vector.host%'
$defaultPort: '%mto.vector.port%'
$timeoutSeconds: '%mto.vector.timeout%'
App\Command\SystemRebuildCommand:
arguments:
$projectDir: '%mto.root%'
$vectorPythonBin: '%mto.vector.python_bin%'
$vectorControlScript: '%mto.vector.control_script%'
$vectorHost: '%mto.vector.host%'
$vectorPort: '%mto.vector.port%'
$vectorTimeoutSeconds: '%mto.vector.timeout%'
# ------------------------------------------------------------ # ------------------------------------------------------------
# Admin Utilities # Admin Utilities
# ------------------------------------------------------------ # ------------------------------------------------------------

View File

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

View File

@@ -17,6 +17,16 @@ use Symfony\Component\Process\Process;
)] )]
final class VectorControlCommand extends Command 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 protected function configure(): void
{ {
$this $this
@@ -27,13 +37,13 @@ final class VectorControlCommand extends Command
->addOption('reload', null, InputOption::VALUE_NONE, 'Trigger /reload') ->addOption('reload', null, InputOption::VALUE_NONE, 'Trigger /reload')
->addOption('status', null, InputOption::VALUE_NONE, 'Print status') ->addOption('status', null, InputOption::VALUE_NONE, 'Print status')
->addOption('foreground', null, InputOption::VALUE_NONE, 'Start in foreground (rare)') ->addOption('foreground', null, InputOption::VALUE_NONE, 'Start in foreground (rare)')
->addOption('port', null, InputOption::VALUE_OPTIONAL, 'Port (default 8090)', '8090') ->addOption('port', null, InputOption::VALUE_OPTIONAL, 'Port', (string) $this->defaultPort)
->addOption('host', null, InputOption::VALUE_OPTIONAL, 'Host (default 0.0.0.0)', '0.0.0.0'); ->addOption('host', null, InputOption::VALUE_OPTIONAL, 'Host', $this->defaultHost);
} }
protected function execute(InputInterface $input, OutputInterface $output): int 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')) { if ($input->getOption('install')) {
$cmd[] = '--install'; $cmd[] = '--install';
@@ -64,7 +74,7 @@ final class VectorControlCommand extends Command
$cmd[] = (string) $input->getOption('host'); $cmd[] = (string) $input->getOption('host');
$process = new Process($cmd); $process = new Process($cmd);
$process->setTimeout(300); $process->setTimeout($this->timeoutSeconds);
$process->run(); $process->run();
$output->writeln($process->getOutput()); $output->writeln($process->getOutput());

View File

@@ -6,24 +6,32 @@ namespace App\Config;
final class AgentRunnerConfig final class AgentRunnerConfig
{ {
/**
* @param array<string, mixed> $config
*/
public function __construct(
private readonly array $config = [],
) {
}
public function getCommerceHistoryBudgetChars(): int public function getCommerceHistoryBudgetChars(): int
{ {
return 1000; return $this->getInt('commerce_history_budget_chars', 1000);
} }
public function getProductSearchKnowledgeChunkLimit(): int public function getProductSearchKnowledgeChunkLimit(): int
{ {
return 6; return $this->getInt('product_search_knowledge_chunk_limit', 6);
} }
public function getAdvisoryProductSearchKnowledgeChunkLimit(): int public function getAdvisoryProductSearchKnowledgeChunkLimit(): int
{ {
return 9; return $this->getInt('advisory_product_search_knowledge_chunk_limit', 9);
} }
public function getOptimizedShopQueryPrefixPattern(): string 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 public function getOptimizedShopQueryTrimCharacters(): string
@@ -31,6 +39,20 @@ final class AgentRunnerConfig
return " \t\n\r\0\x0B\"'`"; 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 public function getEmptyPromptMessage(): string
{ {
return '❌ Empty prompt.'; return '❌ Empty prompt.';

View File

@@ -6,64 +6,101 @@ namespace App\Config;
final class PromptBuilderConfig final class PromptBuilderConfig
{ {
/**
* @param array<string, mixed> $config
*/
public function __construct(
private readonly array $config = [],
) {
}
public function getCharsPerToken(): int public function getCharsPerToken(): int
{ {
return 4; return $this->getInt('budget.chars_per_token', 4);
} }
public function getHistoryPaddingChars(): int public function getHistoryPaddingChars(): int
{ {
return 400; return $this->getInt('budget.history_padding_chars', 400);
} }
public function getOutputReserveRatio(): float public function getOutputReserveRatio(): float
{ {
return 0.25; return $this->getFloat('budget.output_reserve_ratio', 0.25);
} }
public function getOutputReserveMinTokens(): int public function getOutputReserveMinTokens(): int
{ {
return 768; return $this->getInt('budget.output_reserve_min_tokens', 768);
} }
public function getOutputReserveMaxTokens(): int public function getOutputReserveMaxTokens(): int
{ {
return 6000; return $this->getInt('budget.output_reserve_max_tokens', 6000);
} }
public function getSafetyReserveRatio(): float public function getSafetyReserveRatio(): float
{ {
return 0.05; return $this->getFloat('budget.safety_reserve_ratio', 0.05);
} }
public function getSafetyReserveMinTokens(): int public function getSafetyReserveMinTokens(): int
{ {
return 256; return $this->getInt('budget.safety_reserve_min_tokens', 256);
} }
public function getSafetyReserveMaxTokens(): int public function getSafetyReserveMaxTokens(): int
{ {
return 1024; return $this->getInt('budget.safety_reserve_max_tokens', 1024);
} }
public function getMinPromptBudgetTokens(): int public function getMinPromptBudgetTokens(): int
{ {
return 1024; return $this->getInt('budget.min_prompt_budget_tokens', 1024);
} }
public function getMaxShopResultsInPrompt(): int public function getMaxShopResultsInPrompt(): int
{ {
return 24; return $this->getInt('shop_results.max_results_in_prompt', 24);
} }
public function getDetailedShopResultsMaxCount(): int public function getDetailedShopResultsMaxCount(): int
{ {
return 5; return $this->getInt('shop_results.detailed_max_count', 5);
} }
public function getTechnicalProductKeywordMatchThreshold(): int 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 public function getSystemSectionLabel(): string

View File

@@ -6,19 +6,26 @@ namespace App\Config;
final class SearchRepairConfig 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 public function isEnabled(): bool
{ {
return true; return $this->enabled;
} }
public function getMaxRepairQueries(): int public function getMaxRepairQueries(): int
{ {
return 3; return $this->maxRepairQueries;
} }
public function getMinPrimaryResultsWithoutRepair(): int public function getMinPrimaryResultsWithoutRepair(): int
{ {
return 2; return $this->minPrimaryResultsWithoutRepair;
} }
public function getTopProductLogLimit(): int public function getTopProductLogLimit(): int

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Service\Admin; namespace App\Service\Admin;
use App\Config\ModelGenerationDefaultsConfig;
use App\Entity\ModelGenerationConfig; use App\Entity\ModelGenerationConfig;
use App\Knowledge\Retrieval\NdjsonHybridRetriever; use App\Knowledge\Retrieval\NdjsonHybridRetriever;
use App\Repository\ModelGenerationConfigRepository; use App\Repository\ModelGenerationConfigRepository;
@@ -17,6 +18,7 @@ final class ModelGenerationConfigAdminService
private readonly EntityManagerInterface $em, private readonly EntityManagerInterface $em,
private readonly ModelGenerationConfigManager $manager, private readonly ModelGenerationConfigManager $manager,
private readonly NdjsonHybridRetriever $retriever, private readonly NdjsonHybridRetriever $retriever,
private readonly ModelGenerationDefaultsConfig $defaults,
) {} ) {}
public function list(): array public function list(): array
@@ -36,15 +38,15 @@ final class ModelGenerationConfigAdminService
$config = new ModelGenerationConfig( $config = new ModelGenerationConfig(
modelName: $modelName, modelName: $modelName,
version: $version, version: $version,
stream: (bool)($data['stream'] ?? false), stream: (bool)($data['stream'] ?? $this->defaults->isStream()),
temperature: (float)($data['temperature'] ?? 0.7), temperature: (float)($data['temperature'] ?? $this->defaults->getTemperature()),
topK: (int)($data['top_k'] ?? 40), topK: (int)($data['top_k'] ?? $this->defaults->getTopK()),
topP: (float)($data['top_p'] ?? 0.9), topP: (float)($data['top_p'] ?? $this->defaults->getTopP()),
repeatPenalty: (float)($data['repeat_penalty'] ?? 1.1), repeatPenalty: (float)($data['repeat_penalty'] ?? $this->defaults->getRepeatPenalty()),
numCtx: (int)($data['num_ctx'] ?? 4096), numCtx: (int)($data['num_ctx'] ?? $this->defaults->getNumCtx()),
active: false, active: false,
retrievalMaxChunks: (int)($data['retrieval_max_chunks'] ?? 25), retrievalMaxChunks: (int)($data['retrieval_max_chunks'] ?? $this->defaults->getRetrievalMaxChunks()),
retrievalVectorTopK: (int)($data['retrieval_vector_top_k'] ?? 25), retrievalVectorTopK: (int)($data['retrieval_vector_top_k'] ?? $this->defaults->getRetrievalVectorTopK()),
); );
$this->em->persist($config); $this->em->persist($config);
@@ -61,7 +63,7 @@ final class ModelGenerationConfigAdminService
public function delete(ModelGenerationConfig $config): void public function delete(ModelGenerationConfig $config): void
{ {
if ($config->isActive()) { 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); $this->em->remove($config);

View File

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

View File

@@ -15,41 +15,16 @@ final class TagRoutingService
/** /**
* Number of raw tag hits requested from the vector service. * 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( public function __construct(
private readonly TagVectorSearchClient $tagSearch, private readonly TagVectorSearchClient $tagSearch,
private readonly EntityManagerInterface $em, 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( $hits = $this->filterRoutingHits(
$this->tagSearch->search($query, self::DEFAULT_TOPK) $this->tagSearch->search($query, $this->defaultTopK)
); );
if ($hits === []) { if ($hits === []) {
@@ -159,8 +134,8 @@ final class TagRoutingService
if ($matchedTagCount > 1) { if ($matchedTagCount > 1) {
$documentScores[$documentId] += min( $documentScores[$documentId] += min(
self::MAX_MULTI_TAG_BONUS, $this->maxMultiTagBonus,
($matchedTagCount - 1) * self::MULTI_TAG_BONUS_PER_EXTRA_TAG ($matchedTagCount - 1) * $this->multiTagBonusPerExtraTag
); );
} }
} }
@@ -170,7 +145,7 @@ final class TagRoutingService
return array_slice( return array_slice(
array_keys($documentScores), array_keys($documentScores),
0, 0,
self::MAX_CANDIDATE_DOCS $this->maxCandidateDocs
); );
} }
@@ -196,13 +171,13 @@ final class TagRoutingService
$bestScore = (float) ($hits[0]['score'] ?? 0.0); $bestScore = (float) ($hits[0]['score'] ?? 0.0);
if ($bestScore < self::MIN_BEST_SCORE) { if ($bestScore < $this->minBestScore) {
return []; return [];
} }
$minimumAcceptedScore = max( $minimumAcceptedScore = max(
self::MIN_BEST_SCORE, $this->minBestScore,
$bestScore - self::MAX_SCORE_DROP_FROM_BEST $bestScore - $this->maxScoreDropFromBest
); );
$filtered = []; $filtered = [];
@@ -230,7 +205,7 @@ final class TagRoutingService
'tag_type' => $tagType, 'tag_type' => $tagType,
]; ];
if (count($filtered) >= self::MAX_ROUTING_TAGS) { if (count($filtered) >= $this->maxRoutingTags) {
break; break;
} }
} }

View File

@@ -9,46 +9,20 @@ use Symfony\Contracts\HttpClient\HttpClientInterface;
final readonly class TagVectorSearchClient 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( public function __construct(
private HttpClientInterface $http, private HttpClientInterface $http,
private string $serviceUrl, private string $serviceUrl,
private LoggerInterface $agentLogger, 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. * 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{ * @return list<array{
* tag_id:string, * tag_id:string,
* score:float, * score:float,
@@ -56,7 +30,7 @@ final readonly class TagVectorSearchClient
* tag_type:string * 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); $query = trim($query);
@@ -64,7 +38,7 @@ final readonly class TagVectorSearchClient
return []; return [];
} }
$limit = max(1, min($limit, self::MAX_LIMIT)); $limit = $this->clampLimit($limit ?? $this->defaultLimit);
$serviceUrl = rtrim(trim($this->serviceUrl), '/'); $serviceUrl = rtrim(trim($this->serviceUrl), '/');
if ($serviceUrl === '') { if ($serviceUrl === '') {
@@ -82,7 +56,7 @@ final readonly class TagVectorSearchClient
'query' => $query, 'query' => $query,
'limit' => $limit, 'limit' => $limit,
], ],
'timeout' => self::TIMEOUT_SECONDS, 'timeout' => $this->timeoutSeconds,
] ]
); );
@@ -141,7 +115,7 @@ final readonly class TagVectorSearchClient
$score = (float) $score; $score = (float) $score;
if ($score < self::MIN_SCORE) { if ($score < $this->minScore) {
continue; continue;
} }
@@ -186,4 +160,9 @@ final readonly class TagVectorSearchClient
return array_slice($hits, 0, $limit); 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 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 HttpClientInterface $http;
private string $serviceUrl; private string $serviceUrl;
private LoggerInterface $agentLogger; private LoggerInterface $agentLogger;
@@ -27,7 +16,10 @@ final class VectorSearchClient
public function __construct( public function __construct(
HttpClientInterface $http, HttpClientInterface $http,
string $serviceUrl, 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->http = $http;
$this->serviceUrl = rtrim($serviceUrl, '/'); $this->serviceUrl = rtrim($serviceUrl, '/');
@@ -100,7 +92,7 @@ final class VectorSearchClient
$this->serviceUrl . '/search-chunks', $this->serviceUrl . '/search-chunks',
[ [
'json' => $payload, 'json' => $payload,
'timeout' => 10, 'timeout' => $this->timeoutSeconds,
] ]
); );
@@ -143,8 +135,7 @@ final class VectorSearchClient
$score = (float)$score; $score = (float)$score;
// 🔥 Soft Confidence Gate if ($score < $this->minScore) {
if ($score < self::MIN_SCORE) {
continue; continue;
} }
@@ -179,8 +170,8 @@ final class VectorSearchClient
return 1; return 1;
} }
if ($limit > self::MAX_LIMIT) { if ($limit > $this->maxLimit) {
return self::MAX_LIMIT; return $this->maxLimit;
} }
return $limit; return $limit;