first update to external config values
This commit is contained in:
@@ -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:
|
||||
mto.root: '%kernel.project_dir%'
|
||||
mto.root: '%retriex.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.index_meta: '%mto.knowledge.root%/index_meta.json'
|
||||
mto.knowledge.vector_index: '%mto.knowledge.root%/vector.index'
|
||||
mto.knowledge.vector_index_meta: '%mto.knowledge.root%/vector.index.meta.json'
|
||||
mto.runtime.meta: '%mto.knowledge.root%/index_runtime.json'
|
||||
mto.knowledge.upload: '%mto.knowledge.root%/uploads'
|
||||
mto.knowledge.ndjson: '%retriex.knowledge.ndjson%'
|
||||
mto.knowledge.index_meta: '%retriex.knowledge.index_meta%'
|
||||
mto.knowledge.vector_index: '%retriex.knowledge.vector_index%'
|
||||
mto.knowledge.vector_index_meta: '%retriex.knowledge.vector_index_meta%'
|
||||
mto.runtime.meta: '%retriex.knowledge.runtime_meta%'
|
||||
mto.knowledge.upload: '%retriex.knowledge.upload%'
|
||||
|
||||
mto.knowledge.tags_ndjson: '%mto.knowledge.root%/tags.ndjson'
|
||||
mto.knowledge.vector_tags_index: '%mto.knowledge.root%/vector_tags.index'
|
||||
mto.knowledge.vector_tags_index_meta: '%mto.knowledge.root%/vector_tags.index.meta.json'
|
||||
mto.knowledge.tags_ndjson: '%retriex.knowledge.tags_ndjson%'
|
||||
mto.knowledge.vector_tags_index: '%retriex.knowledge.vector_tags_index%'
|
||||
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.search_tags_script: '%mto.vector.script_dir%/vector_search_tags.py'
|
||||
mto.vector.ingest_tags_script: '%retriex.vector.ingest_tags_script%'
|
||||
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.index.chunk_size: 250
|
||||
mto.index.chunk_overlap: 50
|
||||
mto.index.embedding_model: 'intfloat/multilingual-e5-base'
|
||||
mto.index.embedding_dimension: 768
|
||||
mto.index.scoring_version: 1
|
||||
mto.index.chunk_size: '%retriex.index.chunk_size%'
|
||||
mto.index.chunk_overlap: '%retriex.index.chunk_overlap%'
|
||||
mto.index.embedding_model: '%retriex.index.embedding_model%'
|
||||
mto.index.embedding_dimension: '%retriex.index.embedding_dimension%'
|
||||
mto.index.scoring_version: '%retriex.index.scoring_version%'
|
||||
|
||||
mto.vector.python_bin: '%kernel.project_dir%/.venv/bin/python3'
|
||||
mto.vector.ingest_script: '%mto.vector.script_dir%/vector_ingest.py'
|
||||
mto.vector.search_script: '%mto.vector.script_dir%/vector_search.py'
|
||||
mto.vector.timeout: 600
|
||||
mto.vector.service_url: 'http://127.0.0.1:8090'
|
||||
mto.vector.python_bin: '%retriex.vector.python_bin%'
|
||||
mto.vector.control_script: '%retriex.vector.control_script%'
|
||||
mto.vector.ingest_script: '%retriex.vector.ingest_script%'
|
||||
mto.vector.search_script: '%retriex.vector.search_script%'
|
||||
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.max_shop_results: '%env(SHOPWARE_STORE_API_MAX_RESULT)%'
|
||||
mto.commerce.shop_timeout: 5
|
||||
mto.commerce.store_api_base_url: '%env(SHOPWARE_STORE_API_BASE_URL)%'
|
||||
mto.commerce.sales_channel_access_key: '%env(SHOPWARE_SALES_CHANNEL_ACCESS_KEY)%'
|
||||
mto.commerce.enabled: '%retriex.commerce.enabled%'
|
||||
mto.commerce.max_shop_results: '%retriex.commerce.max_shop_results%'
|
||||
mto.commerce.shop_timeout: '%retriex.commerce.shop_timeout%'
|
||||
mto.commerce.store_api_base_url: '%retriex.commerce.store_api_base_url%'
|
||||
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
|
||||
# ------------------------------------------------------------
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# 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:
|
||||
arguments:
|
||||
$apiUrl: '%env(AI_LLM_API_URL)%'
|
||||
$timeoutSeconds: 600
|
||||
$timeoutSeconds: '%retriex.llm.timeout_seconds%'
|
||||
|
||||
$configProvider: '@App\Service\ModelGenerationConfigProvider'
|
||||
|
||||
# ------------------------------------------------------------
|
||||
@@ -120,6 +159,12 @@ services:
|
||||
|
||||
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\Shopware\ShopwareCriteriaBuilder: ~
|
||||
@@ -167,6 +212,9 @@ services:
|
||||
arguments:
|
||||
$serviceUrl: '%mto.vector.service_url%'
|
||||
$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:
|
||||
arguments:
|
||||
@@ -215,8 +263,20 @@ services:
|
||||
arguments:
|
||||
$serviceUrl: '%mto.vector.service_url%'
|
||||
$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:
|
||||
arguments:
|
||||
@@ -237,6 +297,23 @@ services:
|
||||
arguments:
|
||||
$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
|
||||
# ------------------------------------------------------------
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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';
|
||||
@@ -64,7 +74,7 @@ final class VectorControlCommand extends Command
|
||||
$cmd[] = (string) $input->getOption('host');
|
||||
|
||||
$process = new Process($cmd);
|
||||
$process->setTimeout(300);
|
||||
$process->setTimeout($this->timeoutSeconds);
|
||||
$process->run();
|
||||
|
||||
$output->writeln($process->getOutput());
|
||||
|
||||
@@ -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.';
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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,8 +170,8 @@ final class VectorSearchClient
|
||||
return 1;
|
||||
}
|
||||
|
||||
if ($limit > self::MAX_LIMIT) {
|
||||
return self::MAX_LIMIT;
|
||||
if ($limit > $this->maxLimit) {
|
||||
return $this->maxLimit;
|
||||
}
|
||||
|
||||
return $limit;
|
||||
|
||||
Reference in New Issue
Block a user