mirror of
https://github.com/Kaelio/ktx.git
synced 2026-06-19 08:28:06 +02:00
* feat(cli): add tryUseManagedLocalEmbeddingsDaemon for read-only callers * feat(cli): add resolveProjectEmbeddingProvider helper * fix(cli): wire sl search through resolveProjectEmbeddingProvider so semantic lane works * fix(cli): wire wiki/knowledge search through resolveProjectEmbeddingProvider * feat(cli): surface embeddings-unavailable status when sl search returns empty * refactor(cli): route admin reindex through resolveProjectEmbeddingProvider * refactor: pass embeddingProvider into ingest/scan instead of resolving inside @ktx/context * refactor(mcp): resolve embedding provider in CLI factory, pass into context ports * refactor(context): delete MANAGED_SENTENCE_TRANSFORMERS_BASE_URL sentinel * refactor(cli): delete sentinel-based managed-embeddings indirection * chore: scrub stale managed-embeddings sentinel references from tests and smoke script * chore: unexport unused EmbeddingResolutionMode alias * fix(cli): force pathPrefix="" when targeting the managed embeddings daemon The managed daemon serves /embeddings/compute directly. The default pathPrefix in @ktx/llm is /api, so omitting sentenceTransformers from ktx.yaml produced /api/embeddings/compute -> 404. The resolver now sets pathPrefix='' explicitly when wiring the managed daemon URL, matching what the daemon actually exposes.
159 lines
6.6 KiB
TypeScript
159 lines
6.6 KiB
TypeScript
import { type Command, InvalidArgumentError, Option } from '@commander-js/extra-typings';
|
|
import {
|
|
collectOption,
|
|
type KtxCliCommandContext,
|
|
parsePositiveIntegerOption,
|
|
resolveCommandProjectDir,
|
|
} from '../cli-program.js';
|
|
import { slQueryCommandSchema } from '../command-schemas.js';
|
|
import { runtimeInstallPolicyFromFlags } from '../managed-python-command.js';
|
|
import type { KtxSlArgs } from '../sl.js';
|
|
import { profileMark } from '../startup-profile.js';
|
|
|
|
profileMark('module:commands/sl-commands');
|
|
|
|
function parseOrderBy(value: string): string | { field: string; direction?: string } {
|
|
const [field, direction] = value.split(':');
|
|
if (!field) {
|
|
throw new InvalidArgumentError('requires a field');
|
|
}
|
|
if (!direction) {
|
|
return field;
|
|
}
|
|
if (direction !== 'asc' && direction !== 'desc') {
|
|
throw new InvalidArgumentError('direction must be asc or desc');
|
|
}
|
|
return { field, direction };
|
|
}
|
|
|
|
function collectOrderBy(
|
|
value: string,
|
|
previous: Array<string | { field: string; direction?: string }> = [],
|
|
): Array<string | { field: string; direction?: string }> {
|
|
return [...previous, parseOrderBy(value)];
|
|
}
|
|
|
|
async function runSlArgs(context: KtxCliCommandContext, args: KtxSlArgs): Promise<void> {
|
|
const runner = context.deps.sl ?? (await import('../sl.js')).runKtxSl;
|
|
context.setExitCode(await runner(args, context.io));
|
|
}
|
|
|
|
export function registerSlCommands(program: Command, context: KtxCliCommandContext, commandName = 'sl'): void {
|
|
const sl = program
|
|
.command(commandName)
|
|
.description('List, search, validate, or query local semantic-layer sources')
|
|
.usage('[options] [query...]')
|
|
.argument('[query...]', 'Search query; omit to list all sources')
|
|
.option('--connection-id <id>', 'KTX connection id')
|
|
.option('--limit <number>', 'Maximum search results (search mode only)', parsePositiveIntegerOption)
|
|
.addOption(
|
|
new Option('--output <mode>', 'Output mode: pretty (default in TTY), plain (TSV), or json').choices([
|
|
'pretty',
|
|
'plain',
|
|
'json',
|
|
]),
|
|
)
|
|
.option('--json', 'Shortcut for --output=json (overrides --output)', false)
|
|
.showHelpAfterError()
|
|
.addHelpText(
|
|
'after',
|
|
'\nProject directory defaults to KTX_PROJECT_DIR when set, otherwise the current working directory.\n',
|
|
)
|
|
.action(
|
|
async (
|
|
query: string[],
|
|
options: {
|
|
connectionId?: string;
|
|
limit?: number;
|
|
output?: 'pretty' | 'plain' | 'json';
|
|
json?: boolean;
|
|
},
|
|
command,
|
|
) => {
|
|
if (query.length === 0) {
|
|
await runSlArgs(context, {
|
|
command: 'list',
|
|
projectDir: resolveCommandProjectDir(command),
|
|
connectionId: options.connectionId,
|
|
output: options.output,
|
|
json: options.json,
|
|
cliVersion: context.packageInfo.version,
|
|
});
|
|
return;
|
|
}
|
|
await runSlArgs(context, {
|
|
command: 'search',
|
|
projectDir: resolveCommandProjectDir(command),
|
|
connectionId: options.connectionId,
|
|
query: query.join(' '),
|
|
...(options.limit !== undefined ? { limit: options.limit } : {}),
|
|
output: options.output,
|
|
json: options.json,
|
|
cliVersion: context.packageInfo.version,
|
|
});
|
|
},
|
|
);
|
|
|
|
sl.command('validate')
|
|
.description('Validate a semantic-layer source (set --connection-id on `ktx sl`)')
|
|
.argument('<sourceName>', 'Semantic-layer source name')
|
|
.action(async (sourceName: string, _options, command) => {
|
|
const parentOpts = command.parent?.opts() as { connectionId?: string } | undefined;
|
|
const connectionId = parentOpts?.connectionId;
|
|
if (connectionId === undefined) {
|
|
command.error("error: required option '--connection-id <id>' not specified");
|
|
}
|
|
await runSlArgs(context, {
|
|
command: 'validate',
|
|
projectDir: resolveCommandProjectDir(command),
|
|
connectionId: connectionId as string,
|
|
sourceName,
|
|
});
|
|
});
|
|
|
|
sl.command('query')
|
|
.description('Compile or execute a semantic-layer query (set --connection-id on `ktx sl`)')
|
|
.option('--query-file <path>', 'JSON semantic-layer query file')
|
|
.option('--measure <measure>', 'Measure to query; repeatable', collectOption, [])
|
|
.option('--dimension <dimension>', 'Dimension to include; repeatable', collectOption, [])
|
|
.option('--filter <filter>', 'Filter expression; repeatable', collectOption, [])
|
|
.option('--segment <segment>', 'Segment to include; repeatable', collectOption, [])
|
|
.option('--order-by <field[:direction]>', 'Order field, optionally suffixed with :asc or :desc', collectOrderBy, [])
|
|
.option('--limit <n>', 'Query limit', parsePositiveIntegerOption)
|
|
.option('--include-empty', 'Include empty rows', false)
|
|
.addOption(new Option('--format <format>', 'json or sql').choices(['json', 'sql']).default('json'))
|
|
.option('--execute', 'Execute the compiled query', false)
|
|
.option('--yes', 'Install the managed Python runtime without prompting when required', false)
|
|
.option('--no-input', 'Disable interactive managed runtime installation')
|
|
.option('--max-rows <n>', 'Maximum rows to return when executing', parsePositiveIntegerOption)
|
|
.action(async (options, command) => {
|
|
if (options.measure.length === 0 && !options.queryFile) {
|
|
throw new Error('sl query requires at least one --measure');
|
|
}
|
|
const parentOpts = command.parent?.opts() as { connectionId?: string } | undefined;
|
|
const args = slQueryCommandSchema.parse({
|
|
command: 'query',
|
|
projectDir: resolveCommandProjectDir(command),
|
|
connectionId: parentOpts?.connectionId,
|
|
...(options.queryFile
|
|
? { queryFile: options.queryFile }
|
|
: {
|
|
query: {
|
|
measures: options.measure,
|
|
dimensions: options.dimension,
|
|
...(options.filter.length > 0 ? { filters: options.filter } : {}),
|
|
...(options.segment.length > 0 ? { segments: options.segment } : {}),
|
|
...(options.orderBy.length > 0 ? { order_by: options.orderBy } : {}),
|
|
...(options.limit !== undefined ? { limit: options.limit } : {}),
|
|
...(options.includeEmpty === true ? { include_empty: true } : {}),
|
|
},
|
|
}),
|
|
format: options.format,
|
|
execute: options.execute === true,
|
|
cliVersion: context.packageInfo.version,
|
|
runtimeInstallPolicy: runtimeInstallPolicyFromFlags(options),
|
|
...(options.maxRows !== undefined ? { maxRows: options.maxRows } : {}),
|
|
});
|
|
await runSlArgs(context, args);
|
|
});
|
|
}
|