first update to external config values
This commit is contained in:
@@ -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';
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user