mirror of
https://github.com/Kaelio/ktx.git
synced 2026-06-07 07:55:13 +02:00
fix(cli): resolve embedding provider explicitly and surface lane status in sl search (#192)
* 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.
This commit is contained in:
parent
56a967278a
commit
9d92c79988
36 changed files with 750 additions and 442 deletions
|
|
@ -1,15 +1,11 @@
|
||||||
import {
|
import { KtxIngestEmbeddingPortAdapter, type KtxEmbeddingPort } from '@ktx/context';
|
||||||
createLocalKtxEmbeddingProviderFromConfig,
|
|
||||||
KtxIngestEmbeddingPortAdapter,
|
|
||||||
type KtxEmbeddingPort,
|
|
||||||
} from '@ktx/context';
|
|
||||||
import { reindexLocalIndexes, type ReindexScopeResult, type ReindexSummary } from '@ktx/context/index-sync';
|
import { reindexLocalIndexes, type ReindexScopeResult, type ReindexSummary } from '@ktx/context/index-sync';
|
||||||
import { type KtxLocalProject } from '@ktx/context/project';
|
import { loadKtxProject } from '@ktx/context/project';
|
||||||
import { Option, type Command } from '@commander-js/extra-typings';
|
import { Option, type Command } from '@commander-js/extra-typings';
|
||||||
import { cancel, intro, log, note, outro } from '@clack/prompts';
|
import { cancel, intro, log, note, outro } from '@clack/prompts';
|
||||||
import type { KtxCliCommandContext } from './cli-program.js';
|
import type { KtxCliCommandContext } from './cli-program.js';
|
||||||
import { loadKtxCliProject } from './cli-project.js';
|
|
||||||
import type { KtxCliIo } from './cli-runtime.js';
|
import type { KtxCliIo } from './cli-runtime.js';
|
||||||
|
import { resolveProjectEmbeddingProvider } from './embedding-resolution.js';
|
||||||
import { resolveOutputMode } from './io/mode.js';
|
import { resolveOutputMode } from './io/mode.js';
|
||||||
import { green, red, SYMBOLS } from './io/symbols.js';
|
import { green, red, SYMBOLS } from './io/symbols.js';
|
||||||
|
|
||||||
|
|
@ -48,15 +44,6 @@ export function registerAdminReindexCommand(admin: Command, context: KtxCliComma
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveReindexEmbeddingService(project: KtxLocalProject): KtxEmbeddingPort | null {
|
|
||||||
const config = project.config.ingest.embeddings;
|
|
||||||
if (config.backend === 'none') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const provider = createLocalKtxEmbeddingProviderFromConfig(config);
|
|
||||||
return provider ? new KtxIngestEmbeddingPortAdapter(provider) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function scopeKey(scope: ReindexScopeResult): string {
|
function scopeKey(scope: ReindexScopeResult): string {
|
||||||
if (scope.kind === 'wiki') {
|
if (scope.kind === 'wiki') {
|
||||||
return scope.scope === 'user' ? `wiki/user/${scope.scopeId ?? 'local'}` : 'wiki/global';
|
return scope.scope === 'user' ? `wiki/user/${scope.scopeId ?? 'local'}` : 'wiki/global';
|
||||||
|
|
@ -166,13 +153,16 @@ function renderReindexPretty(summary: ReindexSummary, io: KtxCliIo): void {
|
||||||
|
|
||||||
async function runKtxAdminReindex(args: KtxAdminReindexArgs, io: KtxCliIo = process): Promise<number> {
|
async function runKtxAdminReindex(args: KtxAdminReindexArgs, io: KtxCliIo = process): Promise<number> {
|
||||||
try {
|
try {
|
||||||
const project = await loadKtxCliProject({
|
const project = await loadKtxProject({ projectDir: args.projectDir });
|
||||||
projectDir: args.projectDir,
|
const resolution = await resolveProjectEmbeddingProvider(project, {
|
||||||
|
mode: 'use-if-running',
|
||||||
cliVersion: args.cliVersion,
|
cliVersion: args.cliVersion,
|
||||||
installPolicy: 'never',
|
|
||||||
io,
|
io,
|
||||||
});
|
});
|
||||||
const embeddingService = resolveReindexEmbeddingService(project);
|
const embeddingService: KtxEmbeddingPort | null =
|
||||||
|
resolution.kind === 'configured' || resolution.kind === 'managed-running' || resolution.kind === 'managed-started'
|
||||||
|
? new KtxIngestEmbeddingPortAdapter(resolution.provider)
|
||||||
|
: null;
|
||||||
const summary = await reindexLocalIndexes(project, { force: args.force, embeddingService });
|
const summary = await reindexLocalIndexes(project, { force: args.force, embeddingService });
|
||||||
const mode = resolveOutputMode({ explicit: args.output, json: args.json, io });
|
const mode = resolveOutputMode({ explicit: args.output, json: args.json, io });
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,6 @@
|
||||||
import { describe, expect, it, vi } from 'vitest';
|
import { describe, expect, it, vi } from 'vitest';
|
||||||
import { MANAGED_SENTENCE_TRANSFORMERS_BASE_URL } from '@ktx/context';
|
|
||||||
import { buildDefaultKtxProjectConfig, type KtxLocalProject, type KtxProjectConfig } from '@ktx/context/project';
|
import { buildDefaultKtxProjectConfig, type KtxLocalProject, type KtxProjectConfig } from '@ktx/context/project';
|
||||||
import {
|
import { loadKtxCliProject } from './cli-project.js';
|
||||||
loadKtxCliProject,
|
|
||||||
projectNeedsManagedLocalEmbeddings,
|
|
||||||
substituteManagedLocalEmbeddingsUrl,
|
|
||||||
} from './cli-project.js';
|
|
||||||
import type { ManagedLocalEmbeddingsDaemon } from './managed-local-embeddings.js';
|
|
||||||
|
|
||||||
const RESOLVED_BASE_URL = 'http://127.0.0.1:51234';
|
|
||||||
|
|
||||||
function makeIo() {
|
|
||||||
let stderr = '';
|
|
||||||
return {
|
|
||||||
io: {
|
|
||||||
stdout: { write: (_chunk: string) => {} },
|
|
||||||
stderr: {
|
|
||||||
write: (chunk: string) => {
|
|
||||||
stderr += chunk;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
stderr: () => stderr,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function projectWithConfig(config: KtxProjectConfig): KtxLocalProject {
|
function projectWithConfig(config: KtxProjectConfig): KtxLocalProject {
|
||||||
return {
|
return {
|
||||||
|
|
@ -36,147 +13,14 @@ function projectWithConfig(config: KtxProjectConfig): KtxLocalProject {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function withManagedIngestEmbedding(config: KtxProjectConfig): KtxProjectConfig {
|
|
||||||
return {
|
|
||||||
...config,
|
|
||||||
ingest: {
|
|
||||||
...config.ingest,
|
|
||||||
embeddings: {
|
|
||||||
backend: 'sentence-transformers',
|
|
||||||
model: 'all-MiniLM-L6-v2',
|
|
||||||
dimensions: 384,
|
|
||||||
sentenceTransformers: { base_url: MANAGED_SENTENCE_TRANSFORMERS_BASE_URL, pathPrefix: '' },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function withManagedScanEnrichmentEmbedding(config: KtxProjectConfig): KtxProjectConfig {
|
|
||||||
return {
|
|
||||||
...config,
|
|
||||||
scan: {
|
|
||||||
...config.scan,
|
|
||||||
enrichment: {
|
|
||||||
...config.scan.enrichment,
|
|
||||||
embeddings: {
|
|
||||||
backend: 'sentence-transformers',
|
|
||||||
model: 'all-MiniLM-L6-v2',
|
|
||||||
dimensions: 384,
|
|
||||||
sentenceTransformers: { base_url: MANAGED_SENTENCE_TRANSFORMERS_BASE_URL, pathPrefix: '' },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const fakeDaemon: ManagedLocalEmbeddingsDaemon = {
|
|
||||||
baseUrl: RESOLVED_BASE_URL,
|
|
||||||
stdoutLog: '/work/proj/.ktx/runtime/daemon.stdout.log',
|
|
||||||
stderrLog: '/work/proj/.ktx/runtime/daemon.stderr.log',
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('projectNeedsManagedLocalEmbeddings', () => {
|
|
||||||
it('returns false when neither ingest nor scan embeddings reference the managed sentinel', () => {
|
|
||||||
expect(projectNeedsManagedLocalEmbeddings(buildDefaultKtxProjectConfig())).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns true when ingest.embeddings uses the managed sentinel', () => {
|
|
||||||
expect(projectNeedsManagedLocalEmbeddings(withManagedIngestEmbedding(buildDefaultKtxProjectConfig()))).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns true when scan.enrichment.embeddings uses the managed sentinel', () => {
|
|
||||||
expect(
|
|
||||||
projectNeedsManagedLocalEmbeddings(withManagedScanEnrichmentEmbedding(buildDefaultKtxProjectConfig())),
|
|
||||||
).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('substituteManagedLocalEmbeddingsUrl', () => {
|
|
||||||
it('rewrites the managed sentinel in both ingest.embeddings and scan.enrichment.embeddings', () => {
|
|
||||||
const config = withManagedScanEnrichmentEmbedding(withManagedIngestEmbedding(buildDefaultKtxProjectConfig()));
|
|
||||||
const resolved = substituteManagedLocalEmbeddingsUrl(config, RESOLVED_BASE_URL);
|
|
||||||
expect(resolved.ingest.embeddings.sentenceTransformers?.base_url).toBe(RESOLVED_BASE_URL);
|
|
||||||
expect(resolved.scan.enrichment.embeddings?.sentenceTransformers?.base_url).toBe(RESOLVED_BASE_URL);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns the input unchanged when no sentinel is present', () => {
|
|
||||||
const config = buildDefaultKtxProjectConfig();
|
|
||||||
const resolved = substituteManagedLocalEmbeddingsUrl(config, RESOLVED_BASE_URL);
|
|
||||||
expect(resolved.ingest.embeddings).toEqual(config.ingest.embeddings);
|
|
||||||
expect(resolved.scan.enrichment.embeddings).toEqual(config.scan.enrichment.embeddings);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not touch non-sentinel sentence-transformers URLs', () => {
|
|
||||||
const config: KtxProjectConfig = {
|
|
||||||
...buildDefaultKtxProjectConfig(),
|
|
||||||
ingest: {
|
|
||||||
...buildDefaultKtxProjectConfig().ingest,
|
|
||||||
embeddings: {
|
|
||||||
backend: 'sentence-transformers',
|
|
||||||
model: 'all-MiniLM-L6-v2',
|
|
||||||
dimensions: 384,
|
|
||||||
sentenceTransformers: { base_url: 'http://localhost:9999', pathPrefix: '' },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const resolved = substituteManagedLocalEmbeddingsUrl(config, RESOLVED_BASE_URL);
|
|
||||||
expect(resolved.ingest.embeddings.sentenceTransformers?.base_url).toBe('http://localhost:9999');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('loadKtxCliProject', () => {
|
describe('loadKtxCliProject', () => {
|
||||||
it('returns the project unchanged and does not start the daemon when no sentinel is present', async () => {
|
it('delegates to loadKtxProject and returns the project unchanged', async () => {
|
||||||
const io = makeIo();
|
|
||||||
const project = projectWithConfig(buildDefaultKtxProjectConfig());
|
const project = projectWithConfig(buildDefaultKtxProjectConfig());
|
||||||
const loadProject = vi.fn(async () => project);
|
const loadProject = vi.fn(async () => project);
|
||||||
const ensureLocalEmbeddings = vi.fn(async () => fakeDaemon);
|
|
||||||
|
|
||||||
const result = await loadKtxCliProject(
|
const result = await loadKtxCliProject({ projectDir: '/work/proj' }, { loadProject });
|
||||||
{ projectDir: '/work/proj', cliVersion: '0.2.0', installPolicy: 'never', io: io.io },
|
|
||||||
{ loadProject, ensureLocalEmbeddings },
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result).toBe(project);
|
expect(result).toBe(project);
|
||||||
expect(ensureLocalEmbeddings).not.toHaveBeenCalled();
|
expect(loadProject).toHaveBeenCalledWith({ projectDir: '/work/proj' });
|
||||||
});
|
|
||||||
|
|
||||||
it('starts the daemon and substitutes the resolved URL when ingest.embeddings uses the sentinel', async () => {
|
|
||||||
const io = makeIo();
|
|
||||||
const project = projectWithConfig(withManagedIngestEmbedding(buildDefaultKtxProjectConfig()));
|
|
||||||
const loadProject = vi.fn(async () => project);
|
|
||||||
const ensureLocalEmbeddings = vi.fn(async () => fakeDaemon);
|
|
||||||
|
|
||||||
const result = await loadKtxCliProject(
|
|
||||||
{ projectDir: '/work/proj', cliVersion: '0.2.0', installPolicy: 'never', io: io.io },
|
|
||||||
{ loadProject, ensureLocalEmbeddings },
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(ensureLocalEmbeddings).toHaveBeenCalledWith({
|
|
||||||
cliVersion: '0.2.0',
|
|
||||||
projectDir: '/work/proj',
|
|
||||||
installPolicy: 'never',
|
|
||||||
io: io.io,
|
|
||||||
});
|
|
||||||
expect(result.config.ingest.embeddings.sentenceTransformers?.base_url).toBe(RESOLVED_BASE_URL);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not mutate process.env', async () => {
|
|
||||||
const io = makeIo();
|
|
||||||
const before = process.env.KTX_MANAGED_SENTENCE_TRANSFORMERS_BASE_URL;
|
|
||||||
delete process.env.KTX_MANAGED_SENTENCE_TRANSFORMERS_BASE_URL;
|
|
||||||
try {
|
|
||||||
const project = projectWithConfig(withManagedIngestEmbedding(buildDefaultKtxProjectConfig()));
|
|
||||||
await loadKtxCliProject(
|
|
||||||
{ projectDir: '/work/proj', cliVersion: '0.2.0', installPolicy: 'never', io: io.io },
|
|
||||||
{ loadProject: vi.fn(async () => project), ensureLocalEmbeddings: vi.fn(async () => fakeDaemon) },
|
|
||||||
);
|
|
||||||
expect(process.env.KTX_MANAGED_SENTENCE_TRANSFORMERS_BASE_URL).toBeUndefined();
|
|
||||||
} finally {
|
|
||||||
if (before === undefined) {
|
|
||||||
delete process.env.KTX_MANAGED_SENTENCE_TRANSFORMERS_BASE_URL;
|
|
||||||
} else {
|
|
||||||
process.env.KTX_MANAGED_SENTENCE_TRANSFORMERS_BASE_URL = before;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,91 +1,20 @@
|
||||||
import { MANAGED_SENTENCE_TRANSFORMERS_BASE_URL } from '@ktx/context';
|
|
||||||
import { loadKtxProject, type KtxLocalProject } from '@ktx/context/project';
|
import { loadKtxProject, type KtxLocalProject } from '@ktx/context/project';
|
||||||
import type { KtxProjectConfig, KtxProjectEmbeddingConfig } from '@ktx/context/project';
|
|
||||||
import type { KtxCliIo } from './cli-runtime.js';
|
|
||||||
import {
|
|
||||||
ensureManagedLocalEmbeddingsDaemon,
|
|
||||||
type ManagedLocalEmbeddingsDaemon,
|
|
||||||
} from './managed-local-embeddings.js';
|
|
||||||
import type { KtxManagedPythonInstallPolicy } from './managed-python-command.js';
|
|
||||||
|
|
||||||
export interface LoadKtxCliProjectOptions {
|
export interface LoadKtxCliProjectOptions {
|
||||||
projectDir: string;
|
projectDir: string;
|
||||||
cliVersion: string;
|
|
||||||
installPolicy: KtxManagedPythonInstallPolicy;
|
|
||||||
io: KtxCliIo;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LoadKtxCliProjectDeps {
|
export interface LoadKtxCliProjectDeps {
|
||||||
loadProject?: typeof loadKtxProject;
|
loadProject?: typeof loadKtxProject;
|
||||||
ensureLocalEmbeddings?: (
|
|
||||||
options: Parameters<typeof ensureManagedLocalEmbeddingsDaemon>[0],
|
|
||||||
) => Promise<ManagedLocalEmbeddingsDaemon>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thin wrapper around `loadKtxProject`. Kept as a single entrypoint so the CLI can grow shared
|
||||||
|
* pre-load behavior later (telemetry, project lock, etc.). Today it does no extra work.
|
||||||
|
*/
|
||||||
export async function loadKtxCliProject(
|
export async function loadKtxCliProject(
|
||||||
options: LoadKtxCliProjectOptions,
|
options: LoadKtxCliProjectOptions,
|
||||||
deps: LoadKtxCliProjectDeps = {},
|
deps: LoadKtxCliProjectDeps = {},
|
||||||
): Promise<KtxLocalProject> {
|
): Promise<KtxLocalProject> {
|
||||||
const loadProject = deps.loadProject ?? loadKtxProject;
|
return (deps.loadProject ?? loadKtxProject)({ projectDir: options.projectDir });
|
||||||
const ensureLocalEmbeddings = deps.ensureLocalEmbeddings ?? ensureManagedLocalEmbeddingsDaemon;
|
|
||||||
|
|
||||||
const project = await loadProject({ projectDir: options.projectDir });
|
|
||||||
if (!projectNeedsManagedLocalEmbeddings(project.config)) {
|
|
||||||
return project;
|
|
||||||
}
|
|
||||||
|
|
||||||
const daemon = await ensureLocalEmbeddings({
|
|
||||||
cliVersion: options.cliVersion,
|
|
||||||
projectDir: options.projectDir,
|
|
||||||
installPolicy: options.installPolicy,
|
|
||||||
io: options.io,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
...project,
|
|
||||||
config: substituteManagedLocalEmbeddingsUrl(project.config, daemon.baseUrl),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function projectNeedsManagedLocalEmbeddings(config: KtxProjectConfig): boolean {
|
|
||||||
return (
|
|
||||||
embeddingUsesManagedSentinel(config.ingest.embeddings) ||
|
|
||||||
embeddingUsesManagedSentinel(config.scan.enrichment.embeddings)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function substituteManagedLocalEmbeddingsUrl(
|
|
||||||
config: KtxProjectConfig,
|
|
||||||
baseUrl: string,
|
|
||||||
): KtxProjectConfig {
|
|
||||||
const ingestEmbeddings = rewriteManagedEmbeddingConfig(config.ingest.embeddings, baseUrl);
|
|
||||||
const scanEnrichmentEmbeddings = rewriteManagedEmbeddingConfig(config.scan.enrichment.embeddings, baseUrl);
|
|
||||||
return {
|
|
||||||
...config,
|
|
||||||
ingest: { ...config.ingest, embeddings: ingestEmbeddings },
|
|
||||||
scan: {
|
|
||||||
...config.scan,
|
|
||||||
enrichment: { ...config.scan.enrichment, embeddings: scanEnrichmentEmbeddings },
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function embeddingUsesManagedSentinel(embedding: KtxProjectEmbeddingConfig | undefined): boolean {
|
|
||||||
return embedding?.sentenceTransformers?.base_url === MANAGED_SENTENCE_TRANSFORMERS_BASE_URL;
|
|
||||||
}
|
|
||||||
|
|
||||||
function rewriteManagedEmbeddingConfig<T extends KtxProjectEmbeddingConfig | undefined>(
|
|
||||||
embedding: T,
|
|
||||||
baseUrl: string,
|
|
||||||
): T {
|
|
||||||
if (!embedding || !embeddingUsesManagedSentinel(embedding)) {
|
|
||||||
return embedding;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
...embedding,
|
|
||||||
sentenceTransformers: {
|
|
||||||
...embedding.sentenceTransformers,
|
|
||||||
base_url: baseUrl,
|
|
||||||
},
|
|
||||||
} as T;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,7 @@ export function registerWikiCommands(program: Command, context: KtxCliCommandCon
|
||||||
userId: options.userId,
|
userId: options.userId,
|
||||||
output: options.output,
|
output: options.output,
|
||||||
json: options.json,
|
json: options.json,
|
||||||
|
cliVersion: context.packageInfo.version,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -71,6 +72,7 @@ export function registerWikiCommands(program: Command, context: KtxCliCommandCon
|
||||||
json: options.json,
|
json: options.json,
|
||||||
...(isDebugEnabled(command) ? { debug: true } : {}),
|
...(isDebugEnabled(command) ? { debug: true } : {}),
|
||||||
...(options.limit !== undefined ? { limit: options.limit } : {}),
|
...(options.limit !== undefined ? { limit: options.limit } : {}),
|
||||||
|
cliVersion: context.packageInfo.version,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,7 @@ export function registerSlCommands(program: Command, context: KtxCliCommandConte
|
||||||
connectionId: options.connectionId,
|
connectionId: options.connectionId,
|
||||||
output: options.output,
|
output: options.output,
|
||||||
json: options.json,
|
json: options.json,
|
||||||
|
cliVersion: context.packageInfo.version,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -88,6 +89,7 @@ export function registerSlCommands(program: Command, context: KtxCliCommandConte
|
||||||
...(options.limit !== undefined ? { limit: options.limit } : {}),
|
...(options.limit !== undefined ? { limit: options.limit } : {}),
|
||||||
output: options.output,
|
output: options.output,
|
||||||
json: options.json,
|
json: options.json,
|
||||||
|
cliVersion: context.packageInfo.version,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
||||||
145
packages/cli/src/embedding-resolution.test.ts
Normal file
145
packages/cli/src/embedding-resolution.test.ts
Normal file
|
|
@ -0,0 +1,145 @@
|
||||||
|
import { describe, expect, it, vi } from 'vitest';
|
||||||
|
import { buildDefaultKtxProjectConfig, type KtxLocalProject, type KtxProjectConfig } from '@ktx/context/project';
|
||||||
|
import { resolveProjectEmbeddingProvider } from './embedding-resolution.js';
|
||||||
|
import type { ManagedLocalEmbeddingsDaemon } from './managed-local-embeddings.js';
|
||||||
|
|
||||||
|
function projectWithConfig(config: KtxProjectConfig): KtxLocalProject {
|
||||||
|
return {
|
||||||
|
projectDir: '/work/proj',
|
||||||
|
configPath: '/work/proj/ktx.yaml',
|
||||||
|
config,
|
||||||
|
coreConfig: {} as KtxLocalProject['coreConfig'],
|
||||||
|
git: {} as KtxLocalProject['git'],
|
||||||
|
fileStore: {} as KtxLocalProject['fileStore'],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function withManagedEmbedding(config: KtxProjectConfig, base_url?: string): KtxProjectConfig {
|
||||||
|
return {
|
||||||
|
...config,
|
||||||
|
ingest: {
|
||||||
|
...config.ingest,
|
||||||
|
embeddings: {
|
||||||
|
backend: 'sentence-transformers',
|
||||||
|
model: 'all-MiniLM-L6-v2',
|
||||||
|
dimensions: 384,
|
||||||
|
...(base_url === undefined
|
||||||
|
? {}
|
||||||
|
: { sentenceTransformers: { base_url, pathPrefix: '' } }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const noopIo = {
|
||||||
|
stdout: { write: (_chunk: string) => {} },
|
||||||
|
stderr: { write: (_chunk: string) => {} },
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const fakeDaemon: ManagedLocalEmbeddingsDaemon = {
|
||||||
|
baseUrl: 'http://127.0.0.1:51234',
|
||||||
|
stdoutLog: '/tmp/o',
|
||||||
|
stderrLog: '/tmp/e',
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('resolveProjectEmbeddingProvider', () => {
|
||||||
|
it('returns disabled when backend is none', async () => {
|
||||||
|
const project = projectWithConfig(buildDefaultKtxProjectConfig());
|
||||||
|
const result = await resolveProjectEmbeddingProvider(project, {
|
||||||
|
mode: 'use-if-running',
|
||||||
|
cliVersion: '0.5.0',
|
||||||
|
io: noopIo,
|
||||||
|
});
|
||||||
|
expect(result.kind).toBe('disabled');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns a configured provider when base_url is explicit', async () => {
|
||||||
|
const project = projectWithConfig(withManagedEmbedding(buildDefaultKtxProjectConfig(), 'http://my-st:8080'));
|
||||||
|
const createKtxEmbeddingProvider = vi.fn(() => ({ id: 'fake' }) as never);
|
||||||
|
const result = await resolveProjectEmbeddingProvider(project, {
|
||||||
|
mode: 'use-if-running',
|
||||||
|
cliVersion: '0.5.0',
|
||||||
|
io: noopIo,
|
||||||
|
createKtxEmbeddingProvider,
|
||||||
|
});
|
||||||
|
expect(result.kind).toBe('configured');
|
||||||
|
expect(createKtxEmbeddingProvider).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('connects to the running managed daemon when base_url is omitted', async () => {
|
||||||
|
const project = projectWithConfig(withManagedEmbedding(buildDefaultKtxProjectConfig(), undefined));
|
||||||
|
const tryUseManaged = vi.fn(async () => fakeDaemon);
|
||||||
|
const createKtxEmbeddingProvider = vi.fn(() => ({ id: 'fake' }) as never);
|
||||||
|
const ensureManaged = vi.fn(async () => fakeDaemon);
|
||||||
|
const result = await resolveProjectEmbeddingProvider(project, {
|
||||||
|
mode: 'use-if-running',
|
||||||
|
cliVersion: '0.5.0',
|
||||||
|
io: noopIo,
|
||||||
|
createKtxEmbeddingProvider,
|
||||||
|
tryUseManagedDaemon: tryUseManaged,
|
||||||
|
ensureManagedDaemon: ensureManaged,
|
||||||
|
});
|
||||||
|
expect(result.kind).toBe('managed-running');
|
||||||
|
expect(tryUseManaged).toHaveBeenCalledOnce();
|
||||||
|
expect(ensureManaged).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('passes pathPrefix="" to the embedding provider when targeting the managed daemon', async () => {
|
||||||
|
const project = projectWithConfig(withManagedEmbedding(buildDefaultKtxProjectConfig(), undefined));
|
||||||
|
const tryUseManaged = vi.fn(async () => fakeDaemon);
|
||||||
|
const createKtxEmbeddingProvider = vi.fn(() => ({ id: 'fake' }) as never);
|
||||||
|
await resolveProjectEmbeddingProvider(project, {
|
||||||
|
mode: 'use-if-running',
|
||||||
|
cliVersion: '0.5.0',
|
||||||
|
io: noopIo,
|
||||||
|
createKtxEmbeddingProvider,
|
||||||
|
tryUseManagedDaemon: tryUseManaged,
|
||||||
|
});
|
||||||
|
expect(createKtxEmbeddingProvider).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
sentenceTransformers: expect.objectContaining({
|
||||||
|
baseURL: fakeDaemon.baseUrl,
|
||||||
|
pathPrefix: '',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns managed-unavailable when no daemon is running and mode is use-if-running', async () => {
|
||||||
|
const project = projectWithConfig(withManagedEmbedding(buildDefaultKtxProjectConfig(), ''));
|
||||||
|
const tryUseManaged = vi.fn(async () => null);
|
||||||
|
const ensureManaged = vi.fn(async () => fakeDaemon);
|
||||||
|
const result = await resolveProjectEmbeddingProvider(project, {
|
||||||
|
mode: 'use-if-running',
|
||||||
|
cliVersion: '0.5.0',
|
||||||
|
io: noopIo,
|
||||||
|
tryUseManagedDaemon: tryUseManaged,
|
||||||
|
ensureManagedDaemon: ensureManaged,
|
||||||
|
});
|
||||||
|
expect(result.kind).toBe('managed-unavailable');
|
||||||
|
expect(ensureManaged).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('starts the managed daemon when mode is ensure', async () => {
|
||||||
|
const project = projectWithConfig(withManagedEmbedding(buildDefaultKtxProjectConfig(), undefined));
|
||||||
|
const tryUseManaged = vi.fn(async () => null);
|
||||||
|
const ensureManaged = vi.fn(async () => fakeDaemon);
|
||||||
|
const createKtxEmbeddingProvider = vi.fn(() => ({ id: 'fake' }) as never);
|
||||||
|
const result = await resolveProjectEmbeddingProvider(project, {
|
||||||
|
mode: 'ensure',
|
||||||
|
installPolicy: 'auto',
|
||||||
|
cliVersion: '0.5.0',
|
||||||
|
io: noopIo,
|
||||||
|
createKtxEmbeddingProvider,
|
||||||
|
tryUseManagedDaemon: tryUseManaged,
|
||||||
|
ensureManagedDaemon: ensureManaged,
|
||||||
|
});
|
||||||
|
expect(result.kind).toBe('managed-started');
|
||||||
|
expect(ensureManaged).toHaveBeenCalledWith({
|
||||||
|
cliVersion: '0.5.0',
|
||||||
|
projectDir: '/work/proj',
|
||||||
|
installPolicy: 'auto',
|
||||||
|
io: noopIo,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
107
packages/cli/src/embedding-resolution.ts
Normal file
107
packages/cli/src/embedding-resolution.ts
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
import {
|
||||||
|
type KtxEmbeddingProvider,
|
||||||
|
createKtxEmbeddingProvider as defaultCreateKtxEmbeddingProvider,
|
||||||
|
} from '@ktx/llm';
|
||||||
|
import type { KtxLocalProject, KtxProjectEmbeddingConfig } from '@ktx/context/project';
|
||||||
|
import { resolveLocalKtxEmbeddingConfig } from '@ktx/context';
|
||||||
|
import type { KtxCliIo } from './cli-runtime.js';
|
||||||
|
import {
|
||||||
|
ensureManagedLocalEmbeddingsDaemon as defaultEnsureManagedDaemon,
|
||||||
|
tryUseManagedLocalEmbeddingsDaemon as defaultTryUseManagedDaemon,
|
||||||
|
} from './managed-local-embeddings.js';
|
||||||
|
import type { KtxManagedPythonInstallPolicy } from './managed-python-command.js';
|
||||||
|
|
||||||
|
type EmbeddingResolutionMode = 'ensure' | 'use-if-running';
|
||||||
|
|
||||||
|
export type EmbeddingProviderResolution =
|
||||||
|
| { kind: 'disabled' }
|
||||||
|
| { kind: 'configured'; provider: KtxEmbeddingProvider; baseUrl: string }
|
||||||
|
| { kind: 'managed-running'; provider: KtxEmbeddingProvider; baseUrl: string }
|
||||||
|
| { kind: 'managed-started'; provider: KtxEmbeddingProvider; baseUrl: string }
|
||||||
|
| { kind: 'managed-unavailable'; reason: string };
|
||||||
|
|
||||||
|
export interface ResolveProjectEmbeddingProviderOptions {
|
||||||
|
mode: EmbeddingResolutionMode;
|
||||||
|
cliVersion: string;
|
||||||
|
io: KtxCliIo;
|
||||||
|
/** Required when mode === 'ensure'. */
|
||||||
|
installPolicy?: KtxManagedPythonInstallPolicy;
|
||||||
|
tryUseManagedDaemon?: typeof defaultTryUseManagedDaemon;
|
||||||
|
ensureManagedDaemon?: typeof defaultEnsureManagedDaemon;
|
||||||
|
createKtxEmbeddingProvider?: typeof defaultCreateKtxEmbeddingProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
function usesManagedDaemon(embeddings: KtxProjectEmbeddingConfig): boolean {
|
||||||
|
if (embeddings.backend !== 'sentence-transformers') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const baseUrl = embeddings.sentenceTransformers?.base_url;
|
||||||
|
return baseUrl === undefined || baseUrl === '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function resolveProjectEmbeddingProvider(
|
||||||
|
project: KtxLocalProject,
|
||||||
|
options: ResolveProjectEmbeddingProviderOptions,
|
||||||
|
): Promise<EmbeddingProviderResolution> {
|
||||||
|
const embeddings = project.config.ingest.embeddings;
|
||||||
|
if (embeddings.backend === 'none') {
|
||||||
|
return { kind: 'disabled' };
|
||||||
|
}
|
||||||
|
const createProvider = options.createKtxEmbeddingProvider ?? defaultCreateKtxEmbeddingProvider;
|
||||||
|
|
||||||
|
if (!usesManagedDaemon(embeddings)) {
|
||||||
|
const resolved = resolveLocalKtxEmbeddingConfig(embeddings, process.env);
|
||||||
|
if (!resolved) {
|
||||||
|
return { kind: 'managed-unavailable', reason: 'embedding config missing required fields' };
|
||||||
|
}
|
||||||
|
const provider = createProvider(resolved);
|
||||||
|
const baseUrl = embeddings.sentenceTransformers?.base_url ?? '';
|
||||||
|
return { kind: 'configured', provider, baseUrl };
|
||||||
|
}
|
||||||
|
|
||||||
|
const tryUse = options.tryUseManagedDaemon ?? defaultTryUseManagedDaemon;
|
||||||
|
const running = await tryUse({ cliVersion: options.cliVersion, projectDir: project.projectDir });
|
||||||
|
|
||||||
|
if (running) {
|
||||||
|
const provider = buildManagedProvider(embeddings, running.baseUrl, createProvider);
|
||||||
|
return provider
|
||||||
|
? { kind: 'managed-running', provider, baseUrl: running.baseUrl }
|
||||||
|
: { kind: 'managed-unavailable', reason: 'failed to build embedding provider from running daemon' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.mode === 'use-if-running') {
|
||||||
|
return { kind: 'managed-unavailable', reason: 'managed embeddings daemon is not running' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const ensure = options.ensureManagedDaemon ?? defaultEnsureManagedDaemon;
|
||||||
|
if (!options.installPolicy) {
|
||||||
|
throw new Error("installPolicy is required when mode === 'ensure'");
|
||||||
|
}
|
||||||
|
const daemon = await ensure({
|
||||||
|
cliVersion: options.cliVersion,
|
||||||
|
projectDir: project.projectDir,
|
||||||
|
installPolicy: options.installPolicy,
|
||||||
|
io: options.io,
|
||||||
|
});
|
||||||
|
const provider = buildManagedProvider(embeddings, daemon.baseUrl, createProvider);
|
||||||
|
return provider
|
||||||
|
? { kind: 'managed-started', provider, baseUrl: daemon.baseUrl }
|
||||||
|
: { kind: 'managed-unavailable', reason: 'failed to build embedding provider after starting daemon' };
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildManagedProvider(
|
||||||
|
embeddings: KtxProjectEmbeddingConfig,
|
||||||
|
baseUrl: string,
|
||||||
|
createProvider: typeof defaultCreateKtxEmbeddingProvider,
|
||||||
|
): KtxEmbeddingProvider | null {
|
||||||
|
const merged: KtxProjectEmbeddingConfig = {
|
||||||
|
...embeddings,
|
||||||
|
sentenceTransformers: {
|
||||||
|
...embeddings.sentenceTransformers,
|
||||||
|
base_url: baseUrl,
|
||||||
|
pathPrefix: '',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const resolved = resolveLocalKtxEmbeddingConfig(merged, process.env);
|
||||||
|
return resolved ? createProvider(resolved) : null;
|
||||||
|
}
|
||||||
|
|
@ -152,12 +152,12 @@ describe('runKtxCli', () => {
|
||||||
await expect(runKtxCli(['--project-dir', tempDir, 'wiki', '--json'], listIo.io, { knowledge }))
|
await expect(runKtxCli(['--project-dir', tempDir, 'wiki', '--json'], listIo.io, { knowledge }))
|
||||||
.resolves.toBe(0);
|
.resolves.toBe(0);
|
||||||
expect(knowledge).toHaveBeenCalledWith(
|
expect(knowledge).toHaveBeenCalledWith(
|
||||||
{
|
expect.objectContaining({
|
||||||
command: 'list',
|
command: 'list',
|
||||||
projectDir: tempDir,
|
projectDir: tempDir,
|
||||||
userId: 'local',
|
userId: 'local',
|
||||||
json: true,
|
json: true,
|
||||||
},
|
}),
|
||||||
listIo.io,
|
listIo.io,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -166,14 +166,14 @@ describe('runKtxCli', () => {
|
||||||
runKtxCli(['--project-dir', tempDir, 'wiki', 'revenue', '--limit', '5'], searchIo.io, { knowledge }),
|
runKtxCli(['--project-dir', tempDir, 'wiki', 'revenue', '--limit', '5'], searchIo.io, { knowledge }),
|
||||||
).resolves.toBe(0);
|
).resolves.toBe(0);
|
||||||
expect(knowledge).toHaveBeenLastCalledWith(
|
expect(knowledge).toHaveBeenLastCalledWith(
|
||||||
{
|
expect.objectContaining({
|
||||||
command: 'search',
|
command: 'search',
|
||||||
projectDir: tempDir,
|
projectDir: tempDir,
|
||||||
query: 'revenue',
|
query: 'revenue',
|
||||||
userId: 'local',
|
userId: 'local',
|
||||||
json: false,
|
json: false,
|
||||||
limit: 5,
|
limit: 5,
|
||||||
},
|
}),
|
||||||
searchIo.io,
|
searchIo.io,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -182,14 +182,14 @@ describe('runKtxCli', () => {
|
||||||
runKtxCli(['--project-dir', tempDir, '--debug', 'wiki', 'revenue'], debugSearchIo.io, { knowledge }),
|
runKtxCli(['--project-dir', tempDir, '--debug', 'wiki', 'revenue'], debugSearchIo.io, { knowledge }),
|
||||||
).resolves.toBe(0);
|
).resolves.toBe(0);
|
||||||
expect(knowledge).toHaveBeenLastCalledWith(
|
expect(knowledge).toHaveBeenLastCalledWith(
|
||||||
{
|
expect.objectContaining({
|
||||||
command: 'search',
|
command: 'search',
|
||||||
projectDir: tempDir,
|
projectDir: tempDir,
|
||||||
query: 'revenue',
|
query: 'revenue',
|
||||||
userId: 'local',
|
userId: 'local',
|
||||||
json: false,
|
json: false,
|
||||||
debug: true,
|
debug: true,
|
||||||
},
|
}),
|
||||||
debugSearchIo.io,
|
debugSearchIo.io,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -198,13 +198,13 @@ describe('runKtxCli', () => {
|
||||||
runKtxCli(['--project-dir', tempDir, 'wiki', 'revenue', 'policy'], multiWordIo.io, { knowledge }),
|
runKtxCli(['--project-dir', tempDir, 'wiki', 'revenue', 'policy'], multiWordIo.io, { knowledge }),
|
||||||
).resolves.toBe(0);
|
).resolves.toBe(0);
|
||||||
expect(knowledge).toHaveBeenLastCalledWith(
|
expect(knowledge).toHaveBeenLastCalledWith(
|
||||||
{
|
expect.objectContaining({
|
||||||
command: 'search',
|
command: 'search',
|
||||||
projectDir: tempDir,
|
projectDir: tempDir,
|
||||||
query: 'revenue policy',
|
query: 'revenue policy',
|
||||||
userId: 'local',
|
userId: 'local',
|
||||||
json: false,
|
json: false,
|
||||||
},
|
}),
|
||||||
multiWordIo.io,
|
multiWordIo.io,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
@ -248,7 +248,7 @@ describe('runKtxCli', () => {
|
||||||
),
|
),
|
||||||
).resolves.toBe(0);
|
).resolves.toBe(0);
|
||||||
expect(sl).toHaveBeenCalledWith(
|
expect(sl).toHaveBeenCalledWith(
|
||||||
{
|
expect.objectContaining({
|
||||||
command: 'search',
|
command: 'search',
|
||||||
projectDir: tempDir,
|
projectDir: tempDir,
|
||||||
connectionId: 'warehouse',
|
connectionId: 'warehouse',
|
||||||
|
|
@ -256,7 +256,7 @@ describe('runKtxCli', () => {
|
||||||
limit: 5,
|
limit: 5,
|
||||||
json: true,
|
json: true,
|
||||||
output: undefined,
|
output: undefined,
|
||||||
},
|
}),
|
||||||
searchIo.io,
|
searchIo.io,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -265,13 +265,13 @@ describe('runKtxCli', () => {
|
||||||
runKtxCli(['--project-dir', tempDir, 'sl', '--connection-id', 'warehouse', '--json'], bareIo.io, { sl }),
|
runKtxCli(['--project-dir', tempDir, 'sl', '--connection-id', 'warehouse', '--json'], bareIo.io, { sl }),
|
||||||
).resolves.toBe(0);
|
).resolves.toBe(0);
|
||||||
expect(sl).toHaveBeenLastCalledWith(
|
expect(sl).toHaveBeenLastCalledWith(
|
||||||
{
|
expect.objectContaining({
|
||||||
command: 'list',
|
command: 'list',
|
||||||
projectDir: tempDir,
|
projectDir: tempDir,
|
||||||
connectionId: 'warehouse',
|
connectionId: 'warehouse',
|
||||||
json: true,
|
json: true,
|
||||||
output: undefined,
|
output: undefined,
|
||||||
},
|
}),
|
||||||
bareIo.io,
|
bareIo.io,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,6 @@ export type {
|
||||||
export {
|
export {
|
||||||
ensureManagedLocalEmbeddingsDaemon,
|
ensureManagedLocalEmbeddingsDaemon,
|
||||||
managedLocalEmbeddingHealthConfig,
|
managedLocalEmbeddingHealthConfig,
|
||||||
managedLocalEmbeddingProjectConfig,
|
|
||||||
type ManagedLocalEmbeddingsDaemon,
|
type ManagedLocalEmbeddingsDaemon,
|
||||||
type ManagedLocalEmbeddingsOptions,
|
type ManagedLocalEmbeddingsOptions,
|
||||||
} from './managed-local-embeddings.js';
|
} from './managed-local-embeddings.js';
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,8 @@ import {
|
||||||
sanitizeMemoryFlowError,
|
sanitizeMemoryFlowError,
|
||||||
} from '@ktx/context/ingest';
|
} from '@ktx/context/ingest';
|
||||||
import type { KtxSqlQueryExecutorPort } from '@ktx/context/connections';
|
import type { KtxSqlQueryExecutorPort } from '@ktx/context/connections';
|
||||||
import { type KtxLocalProject } from '@ktx/context/project';
|
import { loadKtxProject, type KtxLocalProject } from '@ktx/context/project';
|
||||||
import { loadKtxCliProject } from './cli-project.js';
|
import { resolveProjectEmbeddingProvider } from './embedding-resolution.js';
|
||||||
import { createKtxCliIngestQueryExecutor } from './ingest-query-executor.js';
|
import { createKtxCliIngestQueryExecutor } from './ingest-query-executor.js';
|
||||||
import { readIngestReportSnapshotFile } from './ingest-report-file.js';
|
import { readIngestReportSnapshotFile } from './ingest-report-file.js';
|
||||||
import { createCliOperationalLogger } from './io/logger.js';
|
import { createCliOperationalLogger } from './io/logger.js';
|
||||||
|
|
@ -682,16 +682,17 @@ export async function runKtxIngest(
|
||||||
deps: KtxIngestDeps = {},
|
deps: KtxIngestDeps = {},
|
||||||
): Promise<number> {
|
): Promise<number> {
|
||||||
try {
|
try {
|
||||||
const cliVersion = args.command === 'run' ? args.cliVersion : undefined;
|
const project = await loadKtxProject({ projectDir: args.projectDir });
|
||||||
const runtimeInstallPolicy = args.command === 'run' ? args.runtimeInstallPolicy : undefined;
|
|
||||||
const project = await loadKtxCliProject({
|
|
||||||
projectDir: args.projectDir,
|
|
||||||
cliVersion: cliVersion ?? '0.0.0-private',
|
|
||||||
installPolicy: runtimeInstallPolicy ?? 'never',
|
|
||||||
io,
|
|
||||||
});
|
|
||||||
const env = deps.env ?? process.env;
|
const env = deps.env ?? process.env;
|
||||||
if (args.command === 'run') {
|
if (args.command === 'run') {
|
||||||
|
const resolution = await resolveProjectEmbeddingProvider(project, {
|
||||||
|
mode: 'ensure',
|
||||||
|
installPolicy: args.runtimeInstallPolicy ?? 'never',
|
||||||
|
cliVersion: args.cliVersion ?? '0.0.0-private',
|
||||||
|
io,
|
||||||
|
});
|
||||||
|
const embeddingProvider =
|
||||||
|
resolution.kind === 'disabled' || resolution.kind === 'managed-unavailable' ? null : resolution.provider;
|
||||||
const ingestProject =
|
const ingestProject =
|
||||||
args.allowImplicitAdapter && !project.config.ingest.adapters.includes(args.adapter)
|
args.allowImplicitAdapter && !project.config.ingest.adapters.includes(args.adapter)
|
||||||
? {
|
? {
|
||||||
|
|
@ -771,6 +772,7 @@ export async function runKtxIngest(
|
||||||
queryExecutor,
|
queryExecutor,
|
||||||
trigger: 'manual_resync',
|
trigger: 'manual_resync',
|
||||||
jobIdFactory: deps.jobIdFactory,
|
jobIdFactory: deps.jobIdFactory,
|
||||||
|
embeddingProvider,
|
||||||
...(memoryFlow ? { memoryFlow } : {}),
|
...(memoryFlow ? { memoryFlow } : {}),
|
||||||
...(progress ? { progress } : {}),
|
...(progress ? { progress } : {}),
|
||||||
});
|
});
|
||||||
|
|
@ -843,6 +845,7 @@ export async function runKtxIngest(
|
||||||
...localIngestOptions,
|
...localIngestOptions,
|
||||||
queryExecutor,
|
queryExecutor,
|
||||||
pullConfigOptions: adapterOptions,
|
pullConfigOptions: adapterOptions,
|
||||||
|
embeddingProvider,
|
||||||
...(args.debugLlmRequestFile ? { llmDebugRequestFile: args.debugLlmRequestFile } : {}),
|
...(args.debugLlmRequestFile ? { llmDebugRequestFile: args.debugLlmRequestFile } : {}),
|
||||||
...(memoryFlow ? { memoryFlow } : {}),
|
...(memoryFlow ? { memoryFlow } : {}),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { stripVTControlCharacters } from 'node:util';
|
||||||
import { initKtxProject, loadKtxProject } from '@ktx/context/project';
|
import { initKtxProject, loadKtxProject } from '@ktx/context/project';
|
||||||
import type { KtxEmbeddingPort } from '@ktx/context';
|
import type { KtxEmbeddingPort } from '@ktx/context';
|
||||||
import { writeLocalKnowledgePage } from '@ktx/context/wiki';
|
import { writeLocalKnowledgePage } from '@ktx/context/wiki';
|
||||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
import { runKtxKnowledge } from './knowledge.js';
|
import { runKtxKnowledge } from './knowledge.js';
|
||||||
|
|
||||||
function makeIo() {
|
function makeIo() {
|
||||||
|
|
@ -81,12 +81,17 @@ describe('runKtxKnowledge', () => {
|
||||||
await seedWikiPage(projectDir);
|
await seedWikiPage(projectDir);
|
||||||
|
|
||||||
const listIo = makeIo();
|
const listIo = makeIo();
|
||||||
await expect(runKtxKnowledge({ command: 'list', projectDir, userId: 'local' }, listIo.io)).resolves.toBe(0);
|
await expect(
|
||||||
|
runKtxKnowledge({ command: 'list', projectDir, userId: 'local', cliVersion: '0.0.0-test' }, listIo.io),
|
||||||
|
).resolves.toBe(0);
|
||||||
expect(listIo.stdout()).toContain('GLOBAL\tmetrics-revenue\tRevenue');
|
expect(listIo.stdout()).toContain('GLOBAL\tmetrics-revenue\tRevenue');
|
||||||
|
|
||||||
const searchIo = makeIo();
|
const searchIo = makeIo();
|
||||||
await expect(
|
await expect(
|
||||||
runKtxKnowledge({ command: 'search', projectDir, query: 'paid order', userId: 'local' }, searchIo.io),
|
runKtxKnowledge(
|
||||||
|
{ command: 'search', projectDir, query: 'paid order', userId: 'local', cliVersion: '0.0.0-test' },
|
||||||
|
searchIo.io,
|
||||||
|
),
|
||||||
).resolves.toBe(0);
|
).resolves.toBe(0);
|
||||||
expect(searchIo.stdout()).toContain('metrics-revenue');
|
expect(searchIo.stdout()).toContain('metrics-revenue');
|
||||||
});
|
});
|
||||||
|
|
@ -99,7 +104,14 @@ describe('runKtxKnowledge', () => {
|
||||||
const searchIo = makeIo();
|
const searchIo = makeIo();
|
||||||
await expect(
|
await expect(
|
||||||
runKtxKnowledge(
|
runKtxKnowledge(
|
||||||
{ command: 'search', projectDir, query: 'paid order', userId: 'local', output: 'pretty' },
|
{
|
||||||
|
command: 'search',
|
||||||
|
projectDir,
|
||||||
|
query: 'paid order',
|
||||||
|
userId: 'local',
|
||||||
|
output: 'pretty',
|
||||||
|
cliVersion: '0.0.0-test',
|
||||||
|
},
|
||||||
searchIo.io,
|
searchIo.io,
|
||||||
),
|
),
|
||||||
).resolves.toBe(0);
|
).resolves.toBe(0);
|
||||||
|
|
@ -115,9 +127,12 @@ describe('runKtxKnowledge', () => {
|
||||||
await seedWikiPage(projectDir);
|
await seedWikiPage(projectDir);
|
||||||
|
|
||||||
const listIo = makeIo();
|
const listIo = makeIo();
|
||||||
await expect(runKtxKnowledge({ command: 'list', projectDir, userId: 'local', json: true }, listIo.io)).resolves.toBe(
|
await expect(
|
||||||
0,
|
runKtxKnowledge(
|
||||||
);
|
{ command: 'list', projectDir, userId: 'local', json: true, cliVersion: '0.0.0-test' },
|
||||||
|
listIo.io,
|
||||||
|
),
|
||||||
|
).resolves.toBe(0);
|
||||||
expect(JSON.parse(listIo.stdout())).toMatchObject({
|
expect(JSON.parse(listIo.stdout())).toMatchObject({
|
||||||
kind: 'list',
|
kind: 'list',
|
||||||
data: { items: [expect.objectContaining({ key: 'metrics-revenue', summary: 'Revenue' })] },
|
data: { items: [expect.objectContaining({ key: 'metrics-revenue', summary: 'Revenue' })] },
|
||||||
|
|
@ -127,7 +142,15 @@ describe('runKtxKnowledge', () => {
|
||||||
const searchIo = makeIo();
|
const searchIo = makeIo();
|
||||||
await expect(
|
await expect(
|
||||||
runKtxKnowledge(
|
runKtxKnowledge(
|
||||||
{ command: 'search', projectDir, query: 'paid order', userId: 'local', json: true, limit: 5 },
|
{
|
||||||
|
command: 'search',
|
||||||
|
projectDir,
|
||||||
|
query: 'paid order',
|
||||||
|
userId: 'local',
|
||||||
|
json: true,
|
||||||
|
limit: 5,
|
||||||
|
cliVersion: '0.0.0-test',
|
||||||
|
},
|
||||||
searchIo.io,
|
searchIo.io,
|
||||||
),
|
),
|
||||||
).resolves.toBe(0);
|
).resolves.toBe(0);
|
||||||
|
|
@ -144,7 +167,10 @@ describe('runKtxKnowledge', () => {
|
||||||
|
|
||||||
const searchIo = makeIo();
|
const searchIo = makeIo();
|
||||||
await expect(
|
await expect(
|
||||||
runKtxKnowledge({ command: 'search', projectDir, query: 'revenue', userId: 'local' }, searchIo.io),
|
runKtxKnowledge(
|
||||||
|
{ command: 'search', projectDir, query: 'revenue', userId: 'local', cliVersion: '0.0.0-test' },
|
||||||
|
searchIo.io,
|
||||||
|
),
|
||||||
).resolves.toBe(0);
|
).resolves.toBe(0);
|
||||||
|
|
||||||
expect(searchIo.stdout()).toBe('');
|
expect(searchIo.stdout()).toBe('');
|
||||||
|
|
@ -166,7 +192,7 @@ describe('runKtxKnowledge', () => {
|
||||||
const searchIo = makeIo();
|
const searchIo = makeIo();
|
||||||
await expect(
|
await expect(
|
||||||
runKtxKnowledge(
|
runKtxKnowledge(
|
||||||
{ command: 'search', projectDir, query: 'revenue', userId: 'local' },
|
{ command: 'search', projectDir, query: 'revenue', userId: 'local', cliVersion: '0.0.0-test' },
|
||||||
searchIo.io,
|
searchIo.io,
|
||||||
{ embeddingService: new FakeEmbeddingPort() },
|
{ embeddingService: new FakeEmbeddingPort() },
|
||||||
),
|
),
|
||||||
|
|
@ -176,6 +202,37 @@ describe('runKtxKnowledge', () => {
|
||||||
expect(searchIo.stderr()).toBe('');
|
expect(searchIo.stderr()).toBe('');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('routes wiki search through resolveEmbeddingProvider when no embeddingService is injected', async () => {
|
||||||
|
const projectDir = join(tempDir, 'resolver-project');
|
||||||
|
await initKtxProject({ projectDir });
|
||||||
|
const search = vi.fn(async () => []);
|
||||||
|
const searchIo = makeIo();
|
||||||
|
await expect(
|
||||||
|
runKtxKnowledge(
|
||||||
|
{
|
||||||
|
command: 'search',
|
||||||
|
projectDir,
|
||||||
|
query: 'income',
|
||||||
|
userId: 'local',
|
||||||
|
cliVersion: '0.5.0',
|
||||||
|
},
|
||||||
|
searchIo.io,
|
||||||
|
{
|
||||||
|
resolveEmbeddingProvider: async () => ({
|
||||||
|
kind: 'managed-running',
|
||||||
|
provider: { id: 'fake' } as never,
|
||||||
|
baseUrl: 'http://127.0.0.1:51234',
|
||||||
|
}),
|
||||||
|
searchLocalKnowledgePages: search,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
).resolves.toBe(0);
|
||||||
|
expect(search).toHaveBeenCalledWith(
|
||||||
|
expect.anything(),
|
||||||
|
expect.objectContaining({ embeddingService: expect.any(Object) }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('writes wiki search lane diagnostics to stderr when debug is enabled', async () => {
|
it('writes wiki search lane diagnostics to stderr when debug is enabled', async () => {
|
||||||
const projectDir = join(tempDir, 'debug-project');
|
const projectDir = join(tempDir, 'debug-project');
|
||||||
await initKtxProject({ projectDir });
|
await initKtxProject({ projectDir });
|
||||||
|
|
@ -184,7 +241,15 @@ describe('runKtxKnowledge', () => {
|
||||||
const searchIo = makeIo();
|
const searchIo = makeIo();
|
||||||
await expect(
|
await expect(
|
||||||
runKtxKnowledge(
|
runKtxKnowledge(
|
||||||
{ command: 'search', projectDir, query: 'paid order', userId: 'local', json: true, debug: true },
|
{
|
||||||
|
command: 'search',
|
||||||
|
projectDir,
|
||||||
|
query: 'paid order',
|
||||||
|
userId: 'local',
|
||||||
|
json: true,
|
||||||
|
debug: true,
|
||||||
|
cliVersion: '0.0.0-test',
|
||||||
|
},
|
||||||
searchIo.io,
|
searchIo.io,
|
||||||
{ embeddingService: new FakeEmbeddingPort() },
|
{ embeddingService: new FakeEmbeddingPort() },
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,20 @@
|
||||||
import {
|
import { KtxIngestEmbeddingPortAdapter, type KtxEmbeddingPort } from '@ktx/context';
|
||||||
createLocalKtxEmbeddingProviderFromConfig,
|
|
||||||
KtxIngestEmbeddingPortAdapter,
|
|
||||||
type KtxEmbeddingPort,
|
|
||||||
} from '@ktx/context';
|
|
||||||
import { loadKtxProject } from '@ktx/context/project';
|
import { loadKtxProject } from '@ktx/context/project';
|
||||||
import {
|
import {
|
||||||
type LocalKnowledgeSearchResult,
|
type LocalKnowledgeSearchResult,
|
||||||
type LocalKnowledgeSummary,
|
type LocalKnowledgeSummary,
|
||||||
listLocalKnowledgePages,
|
listLocalKnowledgePages,
|
||||||
searchLocalKnowledgePages,
|
searchLocalKnowledgePages as defaultSearchLocalKnowledgePages,
|
||||||
} from '@ktx/context/wiki';
|
} from '@ktx/context/wiki';
|
||||||
|
import {
|
||||||
|
resolveProjectEmbeddingProvider,
|
||||||
|
type EmbeddingProviderResolution,
|
||||||
|
} from './embedding-resolution.js';
|
||||||
import { resolveOutputMode } from './io/mode.js';
|
import { resolveOutputMode } from './io/mode.js';
|
||||||
import { createRankBadgeFormatter, printList, type PrintListColumn } from './io/print-list.js';
|
import { createRankBadgeFormatter, printList, type PrintListColumn } from './io/print-list.js';
|
||||||
|
|
||||||
export type KtxKnowledgeArgs =
|
export type KtxKnowledgeArgs =
|
||||||
| { command: 'list'; projectDir: string; userId: string; output?: string; json?: boolean }
|
| { command: 'list'; projectDir: string; userId: string; output?: string; json?: boolean; cliVersion: string }
|
||||||
| {
|
| {
|
||||||
command: 'search';
|
command: 'search';
|
||||||
projectDir: string;
|
projectDir: string;
|
||||||
|
|
@ -24,6 +24,7 @@ export type KtxKnowledgeArgs =
|
||||||
json?: boolean;
|
json?: boolean;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
debug?: boolean;
|
debug?: boolean;
|
||||||
|
cliVersion: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type KtxKnowledgeIo = import('./cli-runtime.js').KtxCliIo;
|
type KtxKnowledgeIo = import('./cli-runtime.js').KtxCliIo;
|
||||||
|
|
@ -54,20 +55,36 @@ function wikiSearchColumns(
|
||||||
|
|
||||||
interface KtxKnowledgeDeps {
|
interface KtxKnowledgeDeps {
|
||||||
embeddingService?: KtxEmbeddingPort | null;
|
embeddingService?: KtxEmbeddingPort | null;
|
||||||
createEmbeddingProvider?: typeof createLocalKtxEmbeddingProviderFromConfig;
|
resolveEmbeddingProvider?: typeof resolveProjectEmbeddingProvider;
|
||||||
|
searchLocalKnowledgePages?: typeof defaultSearchLocalKnowledgePages;
|
||||||
}
|
}
|
||||||
|
|
||||||
function wikiSearchEmbeddingService(
|
function resolutionToEmbeddingPort(resolution: EmbeddingProviderResolution): KtxEmbeddingPort | null {
|
||||||
|
if (
|
||||||
|
resolution.kind === 'configured' ||
|
||||||
|
resolution.kind === 'managed-running' ||
|
||||||
|
resolution.kind === 'managed-started'
|
||||||
|
) {
|
||||||
|
return new KtxIngestEmbeddingPortAdapter(resolution.provider);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function wikiSearchEmbeddingService(
|
||||||
project: Awaited<ReturnType<typeof loadKtxProject>>,
|
project: Awaited<ReturnType<typeof loadKtxProject>>,
|
||||||
deps: KtxKnowledgeDeps,
|
deps: KtxKnowledgeDeps,
|
||||||
): KtxEmbeddingPort | null {
|
args: { cliVersion: string },
|
||||||
|
io: KtxKnowledgeIo,
|
||||||
|
): Promise<KtxEmbeddingPort | null> {
|
||||||
if ('embeddingService' in deps) {
|
if ('embeddingService' in deps) {
|
||||||
return deps.embeddingService ?? null;
|
return deps.embeddingService ?? null;
|
||||||
}
|
}
|
||||||
const provider = (deps.createEmbeddingProvider ?? createLocalKtxEmbeddingProviderFromConfig)(
|
const resolution = await (deps.resolveEmbeddingProvider ?? resolveProjectEmbeddingProvider)(project, {
|
||||||
project.config.ingest.embeddings,
|
mode: 'use-if-running',
|
||||||
);
|
cliVersion: args.cliVersion,
|
||||||
return provider ? new KtxIngestEmbeddingPortAdapter(provider) : null;
|
io,
|
||||||
|
});
|
||||||
|
return resolutionToEmbeddingPort(resolution);
|
||||||
}
|
}
|
||||||
|
|
||||||
function writeWikiSearchDebug(
|
function writeWikiSearchDebug(
|
||||||
|
|
@ -114,8 +131,9 @@ export async function runKtxKnowledge(
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
if (args.command === 'search') {
|
if (args.command === 'search') {
|
||||||
const embeddingService = wikiSearchEmbeddingService(project, deps);
|
const embeddingService = await wikiSearchEmbeddingService(project, deps, { cliVersion: args.cliVersion }, io);
|
||||||
const results = await searchLocalKnowledgePages(project, {
|
const search = deps.searchLocalKnowledgePages ?? defaultSearchLocalKnowledgePages;
|
||||||
|
const results = await search(project, {
|
||||||
query: args.query,
|
query: args.query,
|
||||||
userId: args.userId,
|
userId: args.userId,
|
||||||
embeddingService,
|
embeddingService,
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
import { describe, expect, it, vi } from 'vitest';
|
import { describe, expect, it, vi } from 'vitest';
|
||||||
import { MANAGED_SENTENCE_TRANSFORMERS_BASE_URL } from '@ktx/context';
|
|
||||||
import {
|
import {
|
||||||
ensureManagedLocalEmbeddingsDaemon,
|
ensureManagedLocalEmbeddingsDaemon,
|
||||||
managedLocalEmbeddingHealthConfig,
|
managedLocalEmbeddingHealthConfig,
|
||||||
managedLocalEmbeddingProjectConfig,
|
tryUseManagedLocalEmbeddingsDaemon,
|
||||||
} from './managed-local-embeddings.js';
|
} from './managed-local-embeddings.js';
|
||||||
import type { ManagedPythonCommandRuntime } from './managed-python-command.js';
|
import type { ManagedPythonCommandRuntime } from './managed-python-command.js';
|
||||||
import type { ManagedPythonDaemonStartResult } from './managed-python-daemon.js';
|
import type { ManagedPythonDaemonStartResult } from './managed-python-daemon.js';
|
||||||
|
import type { ManagedPythonDaemonLayout } from './managed-python-runtime.js';
|
||||||
|
|
||||||
function makeIo() {
|
function makeIo() {
|
||||||
let stdout = '';
|
let stdout = '';
|
||||||
|
|
@ -94,25 +94,6 @@ function daemonResult(status: 'started' | 'reused' = 'reused'): ManagedPythonDae
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('managedLocalEmbeddingProjectConfig', () => {
|
|
||||||
it('uses a stable managed runtime marker instead of a random daemon port', () => {
|
|
||||||
expect(
|
|
||||||
managedLocalEmbeddingProjectConfig({
|
|
||||||
model: 'all-MiniLM-L6-v2',
|
|
||||||
dimensions: 384,
|
|
||||||
}),
|
|
||||||
).toEqual({
|
|
||||||
backend: 'sentence-transformers',
|
|
||||||
model: 'all-MiniLM-L6-v2',
|
|
||||||
dimensions: 384,
|
|
||||||
sentenceTransformers: {
|
|
||||||
base_url: MANAGED_SENTENCE_TRANSFORMERS_BASE_URL,
|
|
||||||
pathPrefix: '',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('managedLocalEmbeddingHealthConfig', () => {
|
describe('managedLocalEmbeddingHealthConfig', () => {
|
||||||
it('uses the active KTX daemon URL for the immediate health check', () => {
|
it('uses the active KTX daemon URL for the immediate health check', () => {
|
||||||
expect(
|
expect(
|
||||||
|
|
@ -181,3 +162,93 @@ describe('ensureManagedLocalEmbeddingsDaemon', () => {
|
||||||
expect(io.stderr()).toContain('Using KTX daemon: http://127.0.0.1:61234');
|
expect(io.stderr()).toContain('Using KTX daemon: http://127.0.0.1:61234');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('tryUseManagedLocalEmbeddingsDaemon', () => {
|
||||||
|
it('returns the daemon when one is running and healthy', async () => {
|
||||||
|
const readStatus = vi.fn(async () => ({
|
||||||
|
kind: 'running' as const,
|
||||||
|
detail: 'ok',
|
||||||
|
layout: {} as ManagedPythonDaemonLayout,
|
||||||
|
state: {
|
||||||
|
schemaVersion: 1 as const,
|
||||||
|
pid: 123,
|
||||||
|
host: '127.0.0.1' as const,
|
||||||
|
port: 4321,
|
||||||
|
version: '0.5.0',
|
||||||
|
features: ['local-embeddings' as const],
|
||||||
|
startedAt: '2026-05-21T00:00:00Z',
|
||||||
|
stdoutLog: '/tmp/stdout.log',
|
||||||
|
stderrLog: '/tmp/stderr.log',
|
||||||
|
},
|
||||||
|
baseUrl: 'http://127.0.0.1:4321',
|
||||||
|
}));
|
||||||
|
const result = await tryUseManagedLocalEmbeddingsDaemon({
|
||||||
|
cliVersion: '0.5.0',
|
||||||
|
projectDir: '/work/proj',
|
||||||
|
readStatus,
|
||||||
|
});
|
||||||
|
expect(result).toEqual({
|
||||||
|
baseUrl: 'http://127.0.0.1:4321',
|
||||||
|
stdoutLog: '/tmp/stdout.log',
|
||||||
|
stderrLog: '/tmp/stderr.log',
|
||||||
|
});
|
||||||
|
expect(readStatus).toHaveBeenCalledWith({
|
||||||
|
cliVersion: '0.5.0',
|
||||||
|
projectDir: '/work/proj',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null when no daemon state exists', async () => {
|
||||||
|
const readStatus = vi.fn(async () => ({
|
||||||
|
kind: 'stopped' as const,
|
||||||
|
detail: 'no state',
|
||||||
|
layout: {} as ManagedPythonDaemonLayout,
|
||||||
|
}));
|
||||||
|
const result = await tryUseManagedLocalEmbeddingsDaemon({
|
||||||
|
cliVersion: '0.5.0',
|
||||||
|
projectDir: '/work/proj',
|
||||||
|
readStatus,
|
||||||
|
});
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null when daemon is stale', async () => {
|
||||||
|
const readStatus = vi.fn(async () => ({
|
||||||
|
kind: 'stale' as const,
|
||||||
|
detail: 'process gone',
|
||||||
|
layout: {} as ManagedPythonDaemonLayout,
|
||||||
|
}));
|
||||||
|
const result = await tryUseManagedLocalEmbeddingsDaemon({
|
||||||
|
cliVersion: '0.5.0',
|
||||||
|
projectDir: '/work/proj',
|
||||||
|
readStatus,
|
||||||
|
});
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects daemons that do not advertise local-embeddings', async () => {
|
||||||
|
const readStatus = vi.fn(async () => ({
|
||||||
|
kind: 'running' as const,
|
||||||
|
detail: 'ok',
|
||||||
|
layout: {} as ManagedPythonDaemonLayout,
|
||||||
|
state: {
|
||||||
|
schemaVersion: 1 as const,
|
||||||
|
pid: 123,
|
||||||
|
host: '127.0.0.1' as const,
|
||||||
|
port: 4321,
|
||||||
|
version: '0.5.0',
|
||||||
|
features: ['core' as const],
|
||||||
|
startedAt: '2026-05-21T00:00:00Z',
|
||||||
|
stdoutLog: '/tmp/stdout.log',
|
||||||
|
stderrLog: '/tmp/stderr.log',
|
||||||
|
},
|
||||||
|
baseUrl: 'http://127.0.0.1:4321',
|
||||||
|
}));
|
||||||
|
const result = await tryUseManagedLocalEmbeddingsDaemon({
|
||||||
|
cliVersion: '0.5.0',
|
||||||
|
projectDir: '/work/proj',
|
||||||
|
readStatus,
|
||||||
|
});
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
import { MANAGED_SENTENCE_TRANSFORMERS_BASE_URL } from '@ktx/context';
|
|
||||||
import type { KtxProjectEmbeddingConfig } from '@ktx/context/project';
|
|
||||||
import type { KtxEmbeddingConfig } from '@ktx/llm';
|
import type { KtxEmbeddingConfig } from '@ktx/llm';
|
||||||
import type { KtxCliIo } from './cli-runtime.js';
|
import type { KtxCliIo } from './cli-runtime.js';
|
||||||
import {
|
import {
|
||||||
|
|
@ -7,7 +5,12 @@ import {
|
||||||
type KtxManagedPythonInstallPolicy,
|
type KtxManagedPythonInstallPolicy,
|
||||||
type ManagedPythonCommandRuntime,
|
type ManagedPythonCommandRuntime,
|
||||||
} from './managed-python-command.js';
|
} from './managed-python-command.js';
|
||||||
import { startManagedPythonDaemon, type ManagedPythonDaemonStartResult } from './managed-python-daemon.js';
|
import {
|
||||||
|
readManagedPythonDaemonStatus,
|
||||||
|
startManagedPythonDaemon,
|
||||||
|
type ManagedPythonDaemonStartResult,
|
||||||
|
type ManagedPythonDaemonStatus,
|
||||||
|
} from './managed-python-daemon.js';
|
||||||
|
|
||||||
export interface ManagedLocalEmbeddingsDaemon {
|
export interface ManagedLocalEmbeddingsDaemon {
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
|
|
@ -34,21 +37,6 @@ export interface ManagedLocalEmbeddingsOptions {
|
||||||
}) => Promise<ManagedPythonDaemonStartResult>;
|
}) => Promise<ManagedPythonDaemonStartResult>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function managedLocalEmbeddingProjectConfig(input: {
|
|
||||||
model: string;
|
|
||||||
dimensions: number;
|
|
||||||
}): KtxProjectEmbeddingConfig {
|
|
||||||
return {
|
|
||||||
backend: 'sentence-transformers',
|
|
||||||
model: input.model,
|
|
||||||
dimensions: input.dimensions,
|
|
||||||
sentenceTransformers: {
|
|
||||||
base_url: MANAGED_SENTENCE_TRANSFORMERS_BASE_URL,
|
|
||||||
pathPrefix: '',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function managedLocalEmbeddingHealthConfig(input: {
|
export function managedLocalEmbeddingHealthConfig(input: {
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
model: string;
|
model: string;
|
||||||
|
|
@ -93,3 +81,30 @@ export async function ensureManagedLocalEmbeddingsDaemon(
|
||||||
stderrLog: daemon.state.stderrLog,
|
stderrLog: daemon.state.stderrLog,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TryUseManagedLocalEmbeddingsOptions {
|
||||||
|
cliVersion: string;
|
||||||
|
projectDir: string;
|
||||||
|
readStatus?: typeof readManagedPythonDaemonStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function tryUseManagedLocalEmbeddingsDaemon(
|
||||||
|
options: TryUseManagedLocalEmbeddingsOptions,
|
||||||
|
): Promise<ManagedLocalEmbeddingsDaemon | null> {
|
||||||
|
const readStatus = options.readStatus ?? readManagedPythonDaemonStatus;
|
||||||
|
const status: ManagedPythonDaemonStatus = await readStatus({
|
||||||
|
cliVersion: options.cliVersion,
|
||||||
|
projectDir: options.projectDir,
|
||||||
|
});
|
||||||
|
if (status.kind !== 'running') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!status.state.features.includes('local-embeddings')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
baseUrl: status.baseUrl,
|
||||||
|
stdoutLog: status.state.stdoutLog,
|
||||||
|
stderrLog: status.state.stderrLog,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
|
import { KtxIngestEmbeddingPortAdapter } from '@ktx/context';
|
||||||
import { createDefaultKtxMcpServer, createLocalProjectMcpContextPorts } from '@ktx/context/mcp';
|
import { createDefaultKtxMcpServer, createLocalProjectMcpContextPorts } from '@ktx/context/mcp';
|
||||||
import { createLocalProjectMemoryIngest } from '@ktx/context/memory';
|
import { createLocalProjectMemoryIngest } from '@ktx/context/memory';
|
||||||
import type { KtxLocalProject } from '@ktx/context/project';
|
import type { KtxLocalProject } from '@ktx/context/project';
|
||||||
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||||
import type { KtxCliIo } from './cli-runtime.js';
|
import type { KtxCliIo } from './cli-runtime.js';
|
||||||
|
import { resolveProjectEmbeddingProvider } from './embedding-resolution.js';
|
||||||
import { createKtxCliIngestQueryExecutor } from './ingest-query-executor.js';
|
import { createKtxCliIngestQueryExecutor } from './ingest-query-executor.js';
|
||||||
import { createKtxCliScanConnector } from './local-scan-connectors.js';
|
import { createKtxCliScanConnector } from './local-scan-connectors.js';
|
||||||
import { createManagedPythonSemanticLayerComputePort } from './managed-python-command.js';
|
import { createManagedPythonSemanticLayerComputePort } from './managed-python-command.js';
|
||||||
|
|
@ -34,10 +36,20 @@ export async function createKtxMcpServerFactory(input: {
|
||||||
installPolicy: 'auto',
|
installPolicy: 'auto',
|
||||||
io,
|
io,
|
||||||
});
|
});
|
||||||
|
const resolution = await resolveProjectEmbeddingProvider(input.project, {
|
||||||
|
mode: 'use-if-running',
|
||||||
|
cliVersion: input.cliVersion,
|
||||||
|
io,
|
||||||
|
});
|
||||||
|
const embeddingService =
|
||||||
|
resolution.kind === 'configured' || resolution.kind === 'managed-running' || resolution.kind === 'managed-started'
|
||||||
|
? new KtxIngestEmbeddingPortAdapter(resolution.provider)
|
||||||
|
: null;
|
||||||
const contextTools = createLocalProjectMcpContextPorts(input.project, {
|
const contextTools = createLocalProjectMcpContextPorts(input.project, {
|
||||||
semanticLayerCompute,
|
semanticLayerCompute,
|
||||||
queryExecutor,
|
queryExecutor,
|
||||||
sqlAnalysis,
|
sqlAnalysis,
|
||||||
|
embeddingService,
|
||||||
localScan: {
|
localScan: {
|
||||||
createConnector: async (connectionId) => createKtxCliScanConnector(input.project, connectionId),
|
createConnector: async (connectionId) => createKtxCliScanConnector(input.project, connectionId),
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { type KtxLocalProject, type KtxProjectConnectionConfig } from '@ktx/context/project';
|
import { loadKtxProject, type KtxLocalProject, type KtxProjectConnectionConfig } from '@ktx/context/project';
|
||||||
import { loadKtxCliProject } from './cli-project.js';
|
|
||||||
import type { KtxProgressPort } from '@ktx/context/scan';
|
import type { KtxProgressPort } from '@ktx/context/scan';
|
||||||
import type { KtxCliIo } from './index.js';
|
import type { KtxCliIo } from './index.js';
|
||||||
import type { KtxIngestArgs, KtxIngestDeps, KtxIngestProgressUpdate } from './ingest.js';
|
import type { KtxIngestArgs, KtxIngestDeps, KtxIngestProgressUpdate } from './ingest.js';
|
||||||
|
|
@ -869,14 +868,7 @@ export async function runKtxPublicIngest(
|
||||||
deps: KtxPublicIngestDeps = {},
|
deps: KtxPublicIngestDeps = {},
|
||||||
): Promise<number> {
|
): Promise<number> {
|
||||||
const loadProject =
|
const loadProject =
|
||||||
deps.loadProject ??
|
deps.loadProject ?? ((options: { projectDir: string }) => loadKtxProject({ projectDir: options.projectDir }));
|
||||||
((options: { projectDir: string }) =>
|
|
||||||
loadKtxCliProject({
|
|
||||||
projectDir: options.projectDir,
|
|
||||||
cliVersion: args.cliVersion ?? '0.0.0-private',
|
|
||||||
installPolicy: args.runtimeInstallPolicy ?? 'never',
|
|
||||||
io,
|
|
||||||
}));
|
|
||||||
const project = await loadProject({ projectDir: args.projectDir });
|
const project = await loadProject({ projectDir: args.projectDir });
|
||||||
if (shouldUseForegroundContextBuildView(args, io)) {
|
if (shouldUseForegroundContextBuildView(args, io)) {
|
||||||
const plan = buildPublicIngestPlan(project, args);
|
const plan = buildPublicIngestPlan(project, args);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import { MANAGED_SENTENCE_TRANSFORMERS_BASE_URL } from '@ktx/context';
|
|
||||||
import { buildDefaultKtxProjectConfig, type KtxProjectConfig } from '@ktx/context/project';
|
import { buildDefaultKtxProjectConfig, type KtxProjectConfig } from '@ktx/context/project';
|
||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
import {
|
import {
|
||||||
|
|
@ -51,7 +50,7 @@ describe('runtime requirement detection', () => {
|
||||||
model: 'all-MiniLM-L6-v2',
|
model: 'all-MiniLM-L6-v2',
|
||||||
dimensions: 384,
|
dimensions: 384,
|
||||||
sentenceTransformers: {
|
sentenceTransformers: {
|
||||||
base_url: MANAGED_SENTENCE_TRANSFORMERS_BASE_URL,
|
base_url: '',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import { MANAGED_SENTENCE_TRANSFORMERS_BASE_URL } from '@ktx/context';
|
|
||||||
import type {
|
import type {
|
||||||
KtxProjectConfig,
|
KtxProjectConfig,
|
||||||
KtxProjectConnectionConfig,
|
KtxProjectConnectionConfig,
|
||||||
|
|
@ -63,7 +62,7 @@ function requiresManagedLocalEmbeddings(embeddings: KtxProjectEmbeddingConfig):
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const baseUrl = embeddings.sentenceTransformers?.base_url;
|
const baseUrl = embeddings.sentenceTransformers?.base_url;
|
||||||
return baseUrl === undefined || baseUrl === '' || baseUrl === MANAGED_SENTENCE_TRANSFORMERS_BASE_URL;
|
return baseUrl === undefined || baseUrl === '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function uniqueRequirements(requirements: KtxRuntimeRequirement[]): KtxRuntimeRequirements {
|
function uniqueRequirements(requirements: KtxRuntimeRequirement[]): KtxRuntimeRequirements {
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,8 @@ import {
|
||||||
type KtxScanWarning,
|
type KtxScanWarning,
|
||||||
runLocalScan,
|
runLocalScan,
|
||||||
} from '@ktx/context/scan';
|
} from '@ktx/context/scan';
|
||||||
import { loadKtxCliProject } from './cli-project.js';
|
import { loadKtxProject } from '@ktx/context/project';
|
||||||
|
import { resolveProjectEmbeddingProvider } from './embedding-resolution.js';
|
||||||
import type { KtxCliIo } from './index.js';
|
import type { KtxCliIo } from './index.js';
|
||||||
import { createKtxCliLocalIngestAdapters } from './local-adapters.js';
|
import { createKtxCliLocalIngestAdapters } from './local-adapters.js';
|
||||||
import { createKtxCliScanConnector } from './local-scan-connectors.js';
|
import { createKtxCliScanConnector } from './local-scan-connectors.js';
|
||||||
|
|
@ -313,12 +314,15 @@ export function createCliScanProgress(
|
||||||
|
|
||||||
export async function runKtxScan(args: KtxScanArgs, io: KtxCliIo = process, deps: KtxScanDeps = {}): Promise<number> {
|
export async function runKtxScan(args: KtxScanArgs, io: KtxCliIo = process, deps: KtxScanDeps = {}): Promise<number> {
|
||||||
try {
|
try {
|
||||||
const project = await loadKtxCliProject({
|
const project = await loadKtxProject({ projectDir: args.projectDir });
|
||||||
projectDir: args.projectDir,
|
const resolution = await resolveProjectEmbeddingProvider(project, {
|
||||||
cliVersion: args.cliVersion ?? '0.0.0-private',
|
mode: 'ensure',
|
||||||
installPolicy: args.runtimeInstallPolicy ?? 'never',
|
installPolicy: args.runtimeInstallPolicy ?? 'never',
|
||||||
|
cliVersion: args.cliVersion ?? '0.0.0-private',
|
||||||
io,
|
io,
|
||||||
});
|
});
|
||||||
|
const embeddingProvider =
|
||||||
|
resolution.kind === 'disabled' || resolution.kind === 'managed-unavailable' ? null : resolution.provider;
|
||||||
const managedDaemon = managedDaemonOptionsForScanRun(args, deps.runtimeIo ?? io);
|
const managedDaemon = managedDaemonOptionsForScanRun(args, deps.runtimeIo ?? io);
|
||||||
const connector =
|
const connector =
|
||||||
args.mode !== 'structural' || args.detectRelationships
|
args.mode !== 'structural' || args.detectRelationships
|
||||||
|
|
@ -336,6 +340,7 @@ export async function runKtxScan(args: KtxScanArgs, io: KtxCliIo = process, deps
|
||||||
trigger: 'cli',
|
trigger: 'cli',
|
||||||
databaseIntrospectionUrl: args.databaseIntrospectionUrl,
|
databaseIntrospectionUrl: args.databaseIntrospectionUrl,
|
||||||
connector,
|
connector,
|
||||||
|
embeddingProvider,
|
||||||
adapters: (deps.createLocalIngestAdapters ?? createKtxCliLocalIngestAdapters)(project, {
|
adapters: (deps.createLocalIngestAdapters ?? createKtxCliLocalIngestAdapters)(project, {
|
||||||
...(args.databaseIntrospectionUrl ? { databaseIntrospectionUrl: args.databaseIntrospectionUrl } : {}),
|
...(args.databaseIntrospectionUrl ? { databaseIntrospectionUrl: args.databaseIntrospectionUrl } : {}),
|
||||||
...(managedDaemon ? { managedDaemon } : {}),
|
...(managedDaemon ? { managedDaemon } : {}),
|
||||||
|
|
|
||||||
|
|
@ -54,9 +54,6 @@ function managedDaemon(
|
||||||
baseUrl,
|
baseUrl,
|
||||||
stdoutLog: logs.stdoutLog ?? '/tmp/ktx-daemon.stdout.log',
|
stdoutLog: logs.stdoutLog ?? '/tmp/ktx-daemon.stdout.log',
|
||||||
stderrLog: logs.stderrLog ?? '/tmp/ktx-daemon.stderr.log',
|
stderrLog: logs.stderrLog ?? '/tmp/ktx-daemon.stderr.log',
|
||||||
env: {
|
|
||||||
KTX_MANAGED_SENTENCE_TRANSFORMERS_BASE_URL: baseUrl,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -176,8 +173,8 @@ describe('setup embeddings step', () => {
|
||||||
backend: 'sentence-transformers',
|
backend: 'sentence-transformers',
|
||||||
model: 'all-MiniLM-L6-v2',
|
model: 'all-MiniLM-L6-v2',
|
||||||
dimensions: 384,
|
dimensions: 384,
|
||||||
sentenceTransformers: { base_url: 'managed:local-embeddings', pathPrefix: '' },
|
|
||||||
});
|
});
|
||||||
|
expect(config.ingest.embeddings.sentenceTransformers).toBeUndefined();
|
||||||
expect(config.scan.enrichment.embeddings).toMatchObject(config.ingest.embeddings);
|
expect(config.scan.enrichment.embeddings).toMatchObject(config.ingest.embeddings);
|
||||||
expect(await readFile(join(tempDir, 'ktx.yaml'), 'utf-8')).not.toContain('completed_steps:');
|
expect(await readFile(join(tempDir, 'ktx.yaml'), 'utf-8')).not.toContain('completed_steps:');
|
||||||
expect((await readKtxSetupState(tempDir)).completed_steps).toContain('embeddings');
|
expect((await readKtxSetupState(tempDir)).completed_steps).toContain('embeddings');
|
||||||
|
|
@ -275,8 +272,8 @@ describe('setup embeddings step', () => {
|
||||||
backend: 'sentence-transformers',
|
backend: 'sentence-transformers',
|
||||||
model: 'all-MiniLM-L6-v2',
|
model: 'all-MiniLM-L6-v2',
|
||||||
dimensions: 384,
|
dimensions: 384,
|
||||||
sentenceTransformers: { base_url: 'managed:local-embeddings', pathPrefix: '' },
|
|
||||||
});
|
});
|
||||||
|
expect(config.ingest.embeddings.sentenceTransformers).toBeUndefined();
|
||||||
expect(config.scan.enrichment.embeddings).toMatchObject(config.ingest.embeddings);
|
expect(config.scan.enrichment.embeddings).toMatchObject(config.ingest.embeddings);
|
||||||
expect(await readFile(join(tempDir, 'ktx.yaml'), 'utf-8')).not.toContain('completed_steps:');
|
expect(await readFile(join(tempDir, 'ktx.yaml'), 'utf-8')).not.toContain('completed_steps:');
|
||||||
expect((await readKtxSetupState(tempDir)).completed_steps).toContain('embeddings');
|
expect((await readKtxSetupState(tempDir)).completed_steps).toContain('embeddings');
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@ import { createStaticCliSpinner, type KtxCliSpinner } from './clack.js';
|
||||||
import {
|
import {
|
||||||
ensureManagedLocalEmbeddingsDaemon,
|
ensureManagedLocalEmbeddingsDaemon,
|
||||||
managedLocalEmbeddingHealthConfig,
|
managedLocalEmbeddingHealthConfig,
|
||||||
managedLocalEmbeddingProjectConfig,
|
|
||||||
type ManagedLocalEmbeddingsDaemon,
|
type ManagedLocalEmbeddingsDaemon,
|
||||||
} from './managed-local-embeddings.js';
|
} from './managed-local-embeddings.js';
|
||||||
import type { KtxManagedPythonInstallPolicy } from './managed-python-command.js';
|
import type { KtxManagedPythonInstallPolicy } from './managed-python-command.js';
|
||||||
|
|
@ -455,7 +454,11 @@ export async function runKtxSetupEmbeddingsStep(
|
||||||
await persistEmbeddingConfig(
|
await persistEmbeddingConfig(
|
||||||
args.projectDir,
|
args.projectDir,
|
||||||
selectedBackend === LOCAL_EMBEDDING_BACKEND
|
selectedBackend === LOCAL_EMBEDDING_BACKEND
|
||||||
? managedLocalEmbeddingProjectConfig({ model, dimensions })
|
? {
|
||||||
|
backend: 'sentence-transformers' as const,
|
||||||
|
model,
|
||||||
|
dimensions,
|
||||||
|
}
|
||||||
: buildProjectEmbeddingConfig({
|
: buildProjectEmbeddingConfig({
|
||||||
backend: selectedBackend,
|
backend: selectedBackend,
|
||||||
model,
|
model,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import { mkdtemp, rm } from 'node:fs/promises';
|
import { mkdtemp, rm } from 'node:fs/promises';
|
||||||
import { tmpdir } from 'node:os';
|
import { tmpdir } from 'node:os';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import { MANAGED_SENTENCE_TRANSFORMERS_BASE_URL } from '@ktx/context';
|
|
||||||
import { buildDefaultKtxProjectConfig, readKtxSetupState, type KtxProjectConfig } from '@ktx/context/project';
|
import { buildDefaultKtxProjectConfig, readKtxSetupState, type KtxProjectConfig } from '@ktx/context/project';
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
import { runKtxSetupRuntimeStep } from './setup-runtime.js';
|
import { runKtxSetupRuntimeStep } from './setup-runtime.js';
|
||||||
|
|
@ -103,7 +102,6 @@ describe('runKtxSetupRuntimeStep', () => {
|
||||||
baseUrl: 'http://127.0.0.1:61234',
|
baseUrl: 'http://127.0.0.1:61234',
|
||||||
stdoutLog: join(tempDir, '.ktx', 'runtime', 'daemon.stdout.log'),
|
stdoutLog: join(tempDir, '.ktx', 'runtime', 'daemon.stdout.log'),
|
||||||
stderrLog: join(tempDir, '.ktx', 'runtime', 'daemon.stderr.log'),
|
stderrLog: join(tempDir, '.ktx', 'runtime', 'daemon.stderr.log'),
|
||||||
env: { KTX_MANAGED_SENTENCE_TRANSFORMERS_BASE_URL: 'http://127.0.0.1:61234' },
|
|
||||||
}));
|
}));
|
||||||
const config: KtxProjectConfig = {
|
const config: KtxProjectConfig = {
|
||||||
...buildDefaultKtxProjectConfig(),
|
...buildDefaultKtxProjectConfig(),
|
||||||
|
|
@ -113,7 +111,7 @@ describe('runKtxSetupRuntimeStep', () => {
|
||||||
backend: 'sentence-transformers',
|
backend: 'sentence-transformers',
|
||||||
model: 'all-MiniLM-L6-v2',
|
model: 'all-MiniLM-L6-v2',
|
||||||
dimensions: 384,
|
dimensions: 384,
|
||||||
sentenceTransformers: { base_url: MANAGED_SENTENCE_TRANSFORMERS_BASE_URL },
|
sentenceTransformers: { base_url: '' },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -77,12 +77,24 @@ describe('runKtxSl', () => {
|
||||||
expect(validateIo.stdout()).toContain('Valid semantic-layer source: warehouse/orders');
|
expect(validateIo.stdout()).toContain('Valid semantic-layer source: warehouse/orders');
|
||||||
|
|
||||||
const listIo = makeIo();
|
const listIo = makeIo();
|
||||||
await expect(runKtxSl({ command: 'list', projectDir, connectionId: 'warehouse' }, listIo.io)).resolves.toBe(0);
|
await expect(
|
||||||
|
runKtxSl({ command: 'list', projectDir, connectionId: 'warehouse', cliVersion: '0.0.0-test' }, listIo.io),
|
||||||
|
).resolves.toBe(0);
|
||||||
expect(listIo.stdout()).toContain('warehouse\torders\tcolumns=1\tmeasures=0\tjoins=0');
|
expect(listIo.stdout()).toContain('warehouse\torders\tcolumns=1\tmeasures=0\tjoins=0');
|
||||||
|
|
||||||
const searchIo = makeIo();
|
const searchIo = makeIo();
|
||||||
await expect(
|
await expect(
|
||||||
runKtxSl({ command: 'search', projectDir, connectionId: 'warehouse', query: 'order', json: true }, searchIo.io),
|
runKtxSl(
|
||||||
|
{
|
||||||
|
command: 'search',
|
||||||
|
projectDir,
|
||||||
|
connectionId: 'warehouse',
|
||||||
|
query: 'order',
|
||||||
|
json: true,
|
||||||
|
cliVersion: '0.0.0-test',
|
||||||
|
},
|
||||||
|
searchIo.io,
|
||||||
|
),
|
||||||
).resolves.toBe(0);
|
).resolves.toBe(0);
|
||||||
expect(JSON.parse(searchIo.stdout())).toMatchObject({
|
expect(JSON.parse(searchIo.stdout())).toMatchObject({
|
||||||
kind: 'list',
|
kind: 'list',
|
||||||
|
|
@ -106,7 +118,14 @@ describe('runKtxSl', () => {
|
||||||
const searchIo = makeIo();
|
const searchIo = makeIo();
|
||||||
await expect(
|
await expect(
|
||||||
runKtxSl(
|
runKtxSl(
|
||||||
{ command: 'search', projectDir, connectionId: 'warehouse', query: 'order', output: 'pretty' },
|
{
|
||||||
|
command: 'search',
|
||||||
|
projectDir,
|
||||||
|
connectionId: 'warehouse',
|
||||||
|
query: 'order',
|
||||||
|
output: 'pretty',
|
||||||
|
cliVersion: '0.0.0-test',
|
||||||
|
},
|
||||||
searchIo.io,
|
searchIo.io,
|
||||||
),
|
),
|
||||||
).resolves.toBe(0);
|
).resolves.toBe(0);
|
||||||
|
|
@ -136,7 +155,14 @@ describe('runKtxSl', () => {
|
||||||
const listIo = makeIo();
|
const listIo = makeIo();
|
||||||
await expect(
|
await expect(
|
||||||
runKtxSl(
|
runKtxSl(
|
||||||
{ command: 'search', projectDir, connectionId: 'warehouse', query: 'paid', json: true },
|
{
|
||||||
|
command: 'search',
|
||||||
|
projectDir,
|
||||||
|
connectionId: 'warehouse',
|
||||||
|
query: 'paid',
|
||||||
|
json: true,
|
||||||
|
cliVersion: '0.0.0-test',
|
||||||
|
},
|
||||||
listIo.io,
|
listIo.io,
|
||||||
),
|
),
|
||||||
).resolves.toBe(0);
|
).resolves.toBe(0);
|
||||||
|
|
@ -575,7 +601,7 @@ joins: []
|
||||||
|
|
||||||
const listIo = makeIo();
|
const listIo = makeIo();
|
||||||
const code = await runKtxSl(
|
const code = await runKtxSl(
|
||||||
{ command: 'list', projectDir, connectionId: 'warehouse', output: 'json' },
|
{ command: 'list', projectDir, connectionId: 'warehouse', output: 'json', cliVersion: '0.0.0-test' },
|
||||||
listIo.io,
|
listIo.io,
|
||||||
);
|
);
|
||||||
expect(code).toBe(0);
|
expect(code).toBe(0);
|
||||||
|
|
@ -601,13 +627,80 @@ joins: []
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('search prints embeddings status when results are empty', async () => {
|
||||||
|
const stderr: string[] = [];
|
||||||
|
const io = {
|
||||||
|
stdout: { write: (_chunk: string) => {} },
|
||||||
|
stderr: {
|
||||||
|
write: (chunk: string) => {
|
||||||
|
stderr.push(chunk);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const projectDir = join(tempDir, 'empty-status');
|
||||||
|
const project = await initKtxProject({ projectDir });
|
||||||
|
await expect(
|
||||||
|
runKtxSl(
|
||||||
|
{
|
||||||
|
command: 'search',
|
||||||
|
projectDir: project.projectDir,
|
||||||
|
query: 'nope',
|
||||||
|
cliVersion: '0.5.0',
|
||||||
|
},
|
||||||
|
io,
|
||||||
|
{
|
||||||
|
loadProject: async () => project,
|
||||||
|
resolveEmbeddingProvider: async () => ({
|
||||||
|
kind: 'managed-unavailable',
|
||||||
|
reason: 'managed embeddings daemon is not running',
|
||||||
|
}),
|
||||||
|
searchLocalSlSources: async () => [],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
).resolves.toBe(0);
|
||||||
|
expect(stderr.join('')).toMatch(/embeddings: unavailable/);
|
||||||
|
expect(stderr.join('')).toMatch(/managed embeddings daemon is not running/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('passes a managed-daemon-backed embedding service into the search', async () => {
|
||||||
|
const projectDir = join(tempDir, 'resolver-project');
|
||||||
|
const project = await initKtxProject({ projectDir });
|
||||||
|
const search = vi.fn(async () => []);
|
||||||
|
const searchIo = makeIo();
|
||||||
|
await expect(
|
||||||
|
runKtxSl(
|
||||||
|
{
|
||||||
|
command: 'search',
|
||||||
|
projectDir: project.projectDir,
|
||||||
|
query: 'income',
|
||||||
|
cliVersion: '0.5.0',
|
||||||
|
json: true,
|
||||||
|
},
|
||||||
|
searchIo.io,
|
||||||
|
{
|
||||||
|
loadProject: async () => project,
|
||||||
|
resolveEmbeddingProvider: async () => ({
|
||||||
|
kind: 'managed-running',
|
||||||
|
provider: { id: 'fake' } as never,
|
||||||
|
baseUrl: 'http://127.0.0.1:51234',
|
||||||
|
}),
|
||||||
|
searchLocalSlSources: search,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
).resolves.toBe(0);
|
||||||
|
expect(search).toHaveBeenCalledWith(
|
||||||
|
project,
|
||||||
|
expect.objectContaining({ embeddingService: expect.any(Object) }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('emits sl list with grouping and Clack-style framing when output=pretty', async () => {
|
it('emits sl list with grouping and Clack-style framing when output=pretty', async () => {
|
||||||
const projectDir = join(tempDir, 'project');
|
const projectDir = join(tempDir, 'project');
|
||||||
await seedSlSource({ projectDir });
|
await seedSlSource({ projectDir });
|
||||||
|
|
||||||
const listIo = makeIo();
|
const listIo = makeIo();
|
||||||
const code = await runKtxSl(
|
const code = await runKtxSl(
|
||||||
{ command: 'list', projectDir, connectionId: 'warehouse', output: 'pretty' },
|
{ command: 'list', projectDir, connectionId: 'warehouse', output: 'pretty', cliVersion: '0.0.0-test' },
|
||||||
listIo.io,
|
listIo.io,
|
||||||
);
|
);
|
||||||
expect(code).toBe(0);
|
expect(code).toBe(0);
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,22 @@
|
||||||
import { readFile } from 'node:fs/promises';
|
import { readFile } from 'node:fs/promises';
|
||||||
import { createDefaultLocalQueryExecutor, type KtxSqlQueryExecutorPort } from '@ktx/context/connections';
|
import { createDefaultLocalQueryExecutor, type KtxSqlQueryExecutorPort } from '@ktx/context/connections';
|
||||||
import {
|
import { KtxIngestEmbeddingPortAdapter, type KtxEmbeddingPort } from '@ktx/context';
|
||||||
createLocalKtxEmbeddingProviderFromConfig,
|
|
||||||
KtxIngestEmbeddingPortAdapter,
|
|
||||||
type KtxEmbeddingPort,
|
|
||||||
} from '@ktx/context';
|
|
||||||
import type { KtxSemanticLayerComputePort } from '@ktx/context/daemon';
|
import type { KtxSemanticLayerComputePort } from '@ktx/context/daemon';
|
||||||
import { loadKtxProject, type KtxLocalProject } from '@ktx/context/project';
|
import { loadKtxProject, type KtxLocalProject } from '@ktx/context/project';
|
||||||
import {
|
import {
|
||||||
compileLocalSlQuery,
|
compileLocalSlQuery,
|
||||||
listLocalSlSources,
|
listLocalSlSources,
|
||||||
readLocalSlSource,
|
readLocalSlSource,
|
||||||
searchLocalSlSources,
|
searchLocalSlSources as defaultSearchLocalSlSources,
|
||||||
validateLocalSlSource,
|
validateLocalSlSource,
|
||||||
type LocalSlSourceSearchResult,
|
type LocalSlSourceSearchResult,
|
||||||
type LocalSlSourceSummary,
|
type LocalSlSourceSummary,
|
||||||
type SemanticLayerQueryInput,
|
type SemanticLayerQueryInput,
|
||||||
} from '@ktx/context/sl';
|
} from '@ktx/context/sl';
|
||||||
|
import {
|
||||||
|
resolveProjectEmbeddingProvider,
|
||||||
|
type EmbeddingProviderResolution,
|
||||||
|
} from './embedding-resolution.js';
|
||||||
import type { PrintListColumn } from './io/print-list.js';
|
import type { PrintListColumn } from './io/print-list.js';
|
||||||
import {
|
import {
|
||||||
createManagedPythonSemanticLayerComputePort,
|
createManagedPythonSemanticLayerComputePort,
|
||||||
|
|
@ -29,7 +29,14 @@ profileMark('module:sl');
|
||||||
type SlQueryFormat = 'json' | 'sql';
|
type SlQueryFormat = 'json' | 'sql';
|
||||||
|
|
||||||
export type KtxSlArgs =
|
export type KtxSlArgs =
|
||||||
| { command: 'list'; projectDir: string; connectionId?: string; output?: string; json?: boolean }
|
| {
|
||||||
|
command: 'list';
|
||||||
|
projectDir: string;
|
||||||
|
connectionId?: string;
|
||||||
|
output?: string;
|
||||||
|
json?: boolean;
|
||||||
|
cliVersion: string;
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
command: 'search';
|
command: 'search';
|
||||||
projectDir: string;
|
projectDir: string;
|
||||||
|
|
@ -38,6 +45,7 @@ export type KtxSlArgs =
|
||||||
limit?: number;
|
limit?: number;
|
||||||
output?: string;
|
output?: string;
|
||||||
json?: boolean;
|
json?: boolean;
|
||||||
|
cliVersion: string;
|
||||||
}
|
}
|
||||||
| { command: 'validate'; projectDir: string; connectionId: string; sourceName: string }
|
| { command: 'validate'; projectDir: string; connectionId: string; sourceName: string }
|
||||||
| {
|
| {
|
||||||
|
|
@ -60,8 +68,8 @@ interface KtxSlIo {
|
||||||
|
|
||||||
interface KtxSlDeps {
|
interface KtxSlDeps {
|
||||||
loadProject?: typeof loadKtxProject;
|
loadProject?: typeof loadKtxProject;
|
||||||
embeddingService?: KtxEmbeddingPort | null;
|
resolveEmbeddingProvider?: typeof resolveProjectEmbeddingProvider;
|
||||||
createEmbeddingProvider?: typeof createLocalKtxEmbeddingProviderFromConfig;
|
searchLocalSlSources?: typeof defaultSearchLocalSlSources;
|
||||||
createSemanticLayerCompute?: () => KtxSemanticLayerComputePort;
|
createSemanticLayerCompute?: () => KtxSemanticLayerComputePort;
|
||||||
createManagedSemanticLayerCompute?: (options: {
|
createManagedSemanticLayerCompute?: (options: {
|
||||||
cliVersion: string;
|
cliVersion: string;
|
||||||
|
|
@ -71,14 +79,15 @@ interface KtxSlDeps {
|
||||||
createQueryExecutor?: () => KtxSqlQueryExecutorPort;
|
createQueryExecutor?: () => KtxSqlQueryExecutorPort;
|
||||||
}
|
}
|
||||||
|
|
||||||
function slSearchEmbeddingService(project: KtxLocalProject, deps: KtxSlDeps): KtxEmbeddingPort | null {
|
function resolutionToEmbeddingPort(resolution: EmbeddingProviderResolution): KtxEmbeddingPort | null {
|
||||||
if ('embeddingService' in deps) {
|
if (
|
||||||
return deps.embeddingService ?? null;
|
resolution.kind === 'configured' ||
|
||||||
|
resolution.kind === 'managed-running' ||
|
||||||
|
resolution.kind === 'managed-started'
|
||||||
|
) {
|
||||||
|
return new KtxIngestEmbeddingPortAdapter(resolution.provider);
|
||||||
}
|
}
|
||||||
const provider = (deps.createEmbeddingProvider ?? createLocalKtxEmbeddingProviderFromConfig)(
|
return null;
|
||||||
project.config.ingest.embeddings,
|
|
||||||
);
|
|
||||||
return provider ? new KtxIngestEmbeddingPortAdapter(provider) : null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function printSlSources(input: {
|
async function printSlSources(input: {
|
||||||
|
|
@ -188,12 +197,24 @@ export async function runKtxSl(args: KtxSlArgs, io: KtxSlIo = process, deps: Ktx
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
if (args.command === 'search') {
|
if (args.command === 'search') {
|
||||||
const sources = await searchLocalSlSources(project, {
|
const resolver = deps.resolveEmbeddingProvider ?? resolveProjectEmbeddingProvider;
|
||||||
|
const resolution = await resolver(project, {
|
||||||
|
mode: 'use-if-running',
|
||||||
|
cliVersion: args.cliVersion,
|
||||||
|
io,
|
||||||
|
});
|
||||||
|
const embeddingService = resolutionToEmbeddingPort(resolution);
|
||||||
|
const search = deps.searchLocalSlSources ?? defaultSearchLocalSlSources;
|
||||||
|
const sources = await search(project, {
|
||||||
connectionId: args.connectionId,
|
connectionId: args.connectionId,
|
||||||
query: args.query,
|
query: args.query,
|
||||||
embeddingService: slSearchEmbeddingService(project, deps),
|
embeddingService,
|
||||||
limit: args.limit,
|
limit: args.limit,
|
||||||
});
|
});
|
||||||
|
if (sources.length === 0 && resolution.kind === 'managed-unavailable' && !args.json) {
|
||||||
|
const { SYMBOLS } = await import('./io/symbols.js');
|
||||||
|
io.stderr.write(`embeddings: unavailable ${SYMBOLS.emDash} ${resolution.reason}\n`);
|
||||||
|
}
|
||||||
await printSlSources({
|
await printSlSources({
|
||||||
rows: sources,
|
rows: sources,
|
||||||
emptyMessage: `No semantic-layer sources matched "${args.query}" in ${project.projectDir}`,
|
emptyMessage: `No semantic-layer sources matched "${args.query}" in ${project.projectDir}`,
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ import { noopLogger, SessionWorktreeService } from '../core/index.js';
|
||||||
import type { KtxSemanticLayerComputePort } from '../daemon/index.js';
|
import type { KtxSemanticLayerComputePort } from '../daemon/index.js';
|
||||||
import {
|
import {
|
||||||
createRuntimeToolDescriptorFromAiTool,
|
createRuntimeToolDescriptorFromAiTool,
|
||||||
createLocalKtxEmbeddingProviderFromConfig,
|
|
||||||
createLocalKtxLlmRuntimeFromConfig,
|
createLocalKtxLlmRuntimeFromConfig,
|
||||||
KtxIngestEmbeddingPortAdapter,
|
KtxIngestEmbeddingPortAdapter,
|
||||||
RuntimeAgentRunner,
|
RuntimeAgentRunner,
|
||||||
|
|
@ -16,6 +15,7 @@ import {
|
||||||
type KtxLlmRuntimePort,
|
type KtxLlmRuntimePort,
|
||||||
type KtxRuntimeToolSet,
|
type KtxRuntimeToolSet,
|
||||||
} from '../llm/index.js';
|
} from '../llm/index.js';
|
||||||
|
import type { KtxEmbeddingProvider } from '@ktx/llm';
|
||||||
import type { KtxLocalProject } from '../project/index.js';
|
import type { KtxLocalProject } from '../project/index.js';
|
||||||
import { ktxLocalStateDbPath } from '../project/index.js';
|
import { ktxLocalStateDbPath } from '../project/index.js';
|
||||||
import { PromptService } from '../prompts/index.js';
|
import { PromptService } from '../prompts/index.js';
|
||||||
|
|
@ -114,6 +114,7 @@ export interface CreateLocalBundleIngestRuntimeOptions {
|
||||||
queryExecutor?: KtxSqlQueryExecutorPort;
|
queryExecutor?: KtxSqlQueryExecutorPort;
|
||||||
jobIdFactory?: () => string;
|
jobIdFactory?: () => string;
|
||||||
logger?: KtxLogger;
|
logger?: KtxLogger;
|
||||||
|
embeddingProvider?: KtxEmbeddingProvider | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LocalBundleIngestRuntime {
|
export interface LocalBundleIngestRuntime {
|
||||||
|
|
@ -669,7 +670,7 @@ export function createLocalBundleIngestRuntime(
|
||||||
mkdirSync(join(options.project.projectDir, '.ktx/cache/local-ingest'), { recursive: true });
|
mkdirSync(join(options.project.projectDir, '.ktx/cache/local-ingest'), { recursive: true });
|
||||||
const store = new SqliteBundleIngestStore({ dbPath });
|
const store = new SqliteBundleIngestStore({ dbPath });
|
||||||
const contextStore = new SqliteContextEvidenceStore({ dbPath });
|
const contextStore = new SqliteContextEvidenceStore({ dbPath });
|
||||||
const embeddingProvider = createLocalKtxEmbeddingProviderFromConfig(options.project.config.ingest.embeddings);
|
const embeddingProvider = options.embeddingProvider ?? null;
|
||||||
const embedding = embeddingProvider ? new KtxIngestEmbeddingPortAdapter(embeddingProvider) : new NoopEmbeddingPort();
|
const embedding = embeddingProvider ? new KtxIngestEmbeddingPortAdapter(embeddingProvider) : new NoopEmbeddingPort();
|
||||||
const connections = new LocalConnectionCatalog(options.project, options.queryExecutor);
|
const connections = new LocalConnectionCatalog(options.project, options.queryExecutor);
|
||||||
const rootFileStore = options.project.fileStore;
|
const rootFileStore = options.project.fileStore;
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ export interface RunLocalIngestOptions {
|
||||||
semanticLayerCompute?: KtxSemanticLayerComputePort;
|
semanticLayerCompute?: KtxSemanticLayerComputePort;
|
||||||
queryExecutor?: KtxSqlQueryExecutorPort;
|
queryExecutor?: KtxSqlQueryExecutorPort;
|
||||||
logger?: KtxLogger;
|
logger?: KtxLogger;
|
||||||
|
embeddingProvider?: import('@ktx/llm').KtxEmbeddingProvider | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LocalIngestMcpOptions
|
export interface LocalIngestMcpOptions
|
||||||
|
|
@ -172,6 +173,7 @@ async function runScheduledPullJob(options: {
|
||||||
semanticLayerCompute?: KtxSemanticLayerComputePort;
|
semanticLayerCompute?: KtxSemanticLayerComputePort;
|
||||||
queryExecutor?: KtxSqlQueryExecutorPort;
|
queryExecutor?: KtxSqlQueryExecutorPort;
|
||||||
logger?: KtxLogger;
|
logger?: KtxLogger;
|
||||||
|
embeddingProvider?: import('@ktx/llm').KtxEmbeddingProvider | null;
|
||||||
}): Promise<LocalIngestResult> {
|
}): Promise<LocalIngestResult> {
|
||||||
const runtime = createLocalBundleIngestRuntime(options);
|
const runtime = createLocalBundleIngestRuntime(options);
|
||||||
const jobId = options.jobId ?? runtime.nextJobId();
|
const jobId = options.jobId ?? runtime.nextJobId();
|
||||||
|
|
@ -225,6 +227,7 @@ export async function runLocalIngest(options: RunLocalIngestOptions): Promise<Lo
|
||||||
semanticLayerCompute: options.semanticLayerCompute,
|
semanticLayerCompute: options.semanticLayerCompute,
|
||||||
queryExecutor: options.queryExecutor,
|
queryExecutor: options.queryExecutor,
|
||||||
logger: options.logger,
|
logger: options.logger,
|
||||||
|
embeddingProvider: options.embeddingProvider,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -403,6 +406,7 @@ export async function runLocalMetabaseIngest(
|
||||||
semanticLayerCompute: options.semanticLayerCompute,
|
semanticLayerCompute: options.semanticLayerCompute,
|
||||||
queryExecutor: options.queryExecutor,
|
queryExecutor: options.queryExecutor,
|
||||||
logger: options.logger,
|
logger: options.logger,
|
||||||
|
embeddingProvider: options.embeddingProvider,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
child = await recordLocalMetabaseChildFailure({
|
child = await recordLocalMetabaseChildFailure({
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,6 @@ export {
|
||||||
summarizeKtxLlmDebugRequest,
|
summarizeKtxLlmDebugRequest,
|
||||||
} from './debug-request-recorder.js';
|
} from './debug-request-recorder.js';
|
||||||
export {
|
export {
|
||||||
MANAGED_SENTENCE_TRANSFORMERS_BASE_URL,
|
|
||||||
createLocalKtxEmbeddingProviderFromConfig,
|
createLocalKtxEmbeddingProviderFromConfig,
|
||||||
createLocalKtxLlmProviderFromConfig,
|
createLocalKtxLlmProviderFromConfig,
|
||||||
createLocalKtxLlmRuntimeFromConfig,
|
createLocalKtxLlmRuntimeFromConfig,
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import {
|
||||||
type KtxProjectLlmConfig,
|
type KtxProjectLlmConfig,
|
||||||
} from '../project/config.js';
|
} from '../project/config.js';
|
||||||
import {
|
import {
|
||||||
MANAGED_SENTENCE_TRANSFORMERS_BASE_URL,
|
|
||||||
createLocalKtxEmbeddingProviderFromConfig,
|
createLocalKtxEmbeddingProviderFromConfig,
|
||||||
createLocalKtxLlmProviderFromConfig,
|
createLocalKtxLlmProviderFromConfig,
|
||||||
resolveLocalKtxEmbeddingConfig,
|
resolveLocalKtxEmbeddingConfig,
|
||||||
|
|
@ -151,13 +150,13 @@ describe('local KTX embedding config', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns null when sentence-transformers base_url is still the unresolved managed sentinel', () => {
|
it('returns null when sentence-transformers has no base_url (managed daemon delegation)', () => {
|
||||||
const config: KtxProjectEmbeddingConfig = {
|
const config: KtxProjectEmbeddingConfig = {
|
||||||
backend: 'sentence-transformers',
|
backend: 'sentence-transformers',
|
||||||
model: 'all-MiniLM-L6-v2',
|
model: 'all-MiniLM-L6-v2',
|
||||||
dimensions: 384,
|
dimensions: 384,
|
||||||
sentenceTransformers: {
|
sentenceTransformers: {
|
||||||
base_url: MANAGED_SENTENCE_TRANSFORMERS_BASE_URL,
|
base_url: '',
|
||||||
pathPrefix: '',
|
pathPrefix: '',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,6 @@ interface LocalConfigDeps {
|
||||||
createAiSdkRuntime?: (deps: { llmProvider: KtxLlmProvider }) => KtxLlmRuntimePort;
|
createAiSdkRuntime?: (deps: { llmProvider: KtxLlmProvider }) => KtxLlmRuntimePort;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MANAGED_SENTENCE_TRANSFORMERS_BASE_URL = 'managed:local-embeddings';
|
|
||||||
|
|
||||||
function resolveOptional(value: string | undefined, env: NodeJS.ProcessEnv): string | undefined {
|
function resolveOptional(value: string | undefined, env: NodeJS.ProcessEnv): string | undefined {
|
||||||
return resolveKtxConfigReference(value, env) || undefined;
|
return resolveKtxConfigReference(value, env) || undefined;
|
||||||
}
|
}
|
||||||
|
|
@ -149,7 +147,7 @@ export function resolveLocalKtxEmbeddingConfig(
|
||||||
}
|
}
|
||||||
if (config.backend === 'sentence-transformers') {
|
if (config.backend === 'sentence-transformers') {
|
||||||
const baseURL = config.sentenceTransformers?.base_url;
|
const baseURL = config.sentenceTransformers?.base_url;
|
||||||
if (!baseURL || baseURL === MANAGED_SENTENCE_TRANSFORMERS_BASE_URL) {
|
if (!baseURL) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -174,7 +174,7 @@ describe('createLocalProjectMcpContextPorts', () => {
|
||||||
driver: 'postgres',
|
driver: 'postgres',
|
||||||
url: 'env:DATABASE_URL',
|
url: 'env:DATABASE_URL',
|
||||||
};
|
};
|
||||||
const ports = createLocalProjectMcpContextPorts(project);
|
const ports = createLocalProjectMcpContextPorts(project, { embeddingService: null });
|
||||||
|
|
||||||
expect(Object.keys(ports).sort()).toEqual([
|
expect(Object.keys(ports).sort()).toEqual([
|
||||||
'connections',
|
'connections',
|
||||||
|
|
@ -216,6 +216,7 @@ describe('createLocalProjectMcpContextPorts', () => {
|
||||||
localScan: {
|
localScan: {
|
||||||
createConnector,
|
createConnector,
|
||||||
},
|
},
|
||||||
|
embeddingService: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(Object.keys(ports).sort()).toContain('sqlExecution');
|
expect(Object.keys(ports).sort()).toContain('sqlExecution');
|
||||||
|
|
@ -269,6 +270,7 @@ describe('createLocalProjectMcpContextPorts', () => {
|
||||||
localScan: {
|
localScan: {
|
||||||
createConnector,
|
createConnector,
|
||||||
},
|
},
|
||||||
|
embeddingService: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await ports.sqlExecution?.execute(
|
const result = await ports.sqlExecution?.execute(
|
||||||
|
|
@ -313,6 +315,7 @@ describe('createLocalProjectMcpContextPorts', () => {
|
||||||
localScan: {
|
localScan: {
|
||||||
createConnector: vi.fn(async () => connector),
|
createConnector: vi.fn(async () => connector),
|
||||||
},
|
},
|
||||||
|
embeddingService: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
|
|
@ -332,7 +335,7 @@ describe('createLocalProjectMcpContextPorts', () => {
|
||||||
url: 'env:DATABASE_URL',
|
url: 'env:DATABASE_URL',
|
||||||
};
|
};
|
||||||
await seedScanReport(project.projectDir);
|
await seedScanReport(project.projectDir);
|
||||||
const ports = createLocalProjectMcpContextPorts(project);
|
const ports = createLocalProjectMcpContextPorts(project, { embeddingService: null });
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
ports.entityDetails?.read({
|
ports.entityDetails?.read({
|
||||||
|
|
@ -358,7 +361,7 @@ describe('createLocalProjectMcpContextPorts', () => {
|
||||||
driver: 'postgres',
|
driver: 'postgres',
|
||||||
url: 'env:DATABASE_URL',
|
url: 'env:DATABASE_URL',
|
||||||
};
|
};
|
||||||
const ports = createLocalProjectMcpContextPorts(project);
|
const ports = createLocalProjectMcpContextPorts(project, { embeddingService: null });
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
ports.entityDetails?.read({
|
ports.entityDetails?.read({
|
||||||
|
|
@ -411,7 +414,7 @@ describe('createLocalProjectMcpContextPorts', () => {
|
||||||
'Seed dictionary profile',
|
'Seed dictionary profile',
|
||||||
);
|
);
|
||||||
|
|
||||||
const ports = createLocalProjectMcpContextPorts(project);
|
const ports = createLocalProjectMcpContextPorts(project, { embeddingService: null });
|
||||||
|
|
||||||
await expect(ports.dictionarySearch?.search({ values: ['paid'] })).resolves.toMatchObject({
|
await expect(ports.dictionarySearch?.search({ values: ['paid'] })).resolves.toMatchObject({
|
||||||
searched: [{ connectionId: 'warehouse', status: 'ready' }],
|
searched: [{ connectionId: 'warehouse', status: 'ready' }],
|
||||||
|
|
@ -432,7 +435,7 @@ describe('createLocalProjectMcpContextPorts', () => {
|
||||||
url: 'env:DATABASE_URL',
|
url: 'env:DATABASE_URL',
|
||||||
};
|
};
|
||||||
|
|
||||||
const ports = createLocalProjectMcpContextPorts(project);
|
const ports = createLocalProjectMcpContextPorts(project, { embeddingService: null });
|
||||||
|
|
||||||
await expect(ports.dictionarySearch?.search({ values: ['paid'] })).resolves.toEqual({
|
await expect(ports.dictionarySearch?.search({ values: ['paid'] })).resolves.toEqual({
|
||||||
searched: [
|
searched: [
|
||||||
|
|
@ -601,7 +604,7 @@ describe('createLocalProjectMcpContextPorts', () => {
|
||||||
'seed scan report',
|
'seed scan report',
|
||||||
);
|
);
|
||||||
|
|
||||||
const ports = createLocalProjectMcpContextPorts(project);
|
const ports = createLocalProjectMcpContextPorts(project, { embeddingService: null });
|
||||||
const results = await ports.discover?.search({ query: 'paid orders', connectionId: 'warehouse', limit: 10 });
|
const results = await ports.discover?.search({ query: 'paid orders', connectionId: 'warehouse', limit: 10 });
|
||||||
|
|
||||||
expect(results).toEqual(
|
expect(results).toEqual(
|
||||||
|
|
@ -635,7 +638,7 @@ describe('createLocalProjectMcpContextPorts', () => {
|
||||||
'ktx@example.com',
|
'ktx@example.com',
|
||||||
'Seed wiki',
|
'Seed wiki',
|
||||||
);
|
);
|
||||||
const ports = createLocalProjectMcpContextPorts(project);
|
const ports = createLocalProjectMcpContextPorts(project, { embeddingService: null });
|
||||||
|
|
||||||
await expect(ports.knowledge?.read({ userId: 'local-user', key: 'revenue' })).resolves.toMatchObject({
|
await expect(ports.knowledge?.read({ userId: 'local-user', key: 'revenue' })).resolves.toMatchObject({
|
||||||
key: 'revenue',
|
key: 'revenue',
|
||||||
|
|
@ -680,7 +683,7 @@ describe('createLocalProjectMcpContextPorts', () => {
|
||||||
'',
|
'',
|
||||||
].join('\n'),
|
].join('\n'),
|
||||||
});
|
});
|
||||||
const ports = createLocalProjectMcpContextPorts(project);
|
const ports = createLocalProjectMcpContextPorts(project, { embeddingService: null });
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
ports.semanticLayer?.readSource({ connectionId: 'warehouse', sourceName: 'orders' }),
|
ports.semanticLayer?.readSource({ connectionId: 'warehouse', sourceName: 'orders' }),
|
||||||
|
|
@ -692,7 +695,7 @@ describe('createLocalProjectMcpContextPorts', () => {
|
||||||
|
|
||||||
it('rejects path traversal keys before touching the project directory', async () => {
|
it('rejects path traversal keys before touching the project directory', async () => {
|
||||||
const project = await initKtxProject({ projectDir: tempDir });
|
const project = await initKtxProject({ projectDir: tempDir });
|
||||||
const ports = createLocalProjectMcpContextPorts(project);
|
const ports = createLocalProjectMcpContextPorts(project, { embeddingService: null });
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
ports.knowledge?.read({
|
ports.knowledge?.read({
|
||||||
|
|
@ -746,7 +749,7 @@ describe('createLocalProjectMcpContextPorts', () => {
|
||||||
})),
|
})),
|
||||||
generateSources: vi.fn(),
|
generateSources: vi.fn(),
|
||||||
};
|
};
|
||||||
const ports = createLocalProjectMcpContextPorts(project, { semanticLayerCompute });
|
const ports = createLocalProjectMcpContextPorts(project, { semanticLayerCompute, embeddingService: null });
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
ports.semanticLayer?.query({
|
ports.semanticLayer?.query({
|
||||||
|
|
@ -817,6 +820,7 @@ describe('createLocalProjectMcpContextPorts', () => {
|
||||||
const ports = createLocalProjectMcpContextPorts(project, {
|
const ports = createLocalProjectMcpContextPorts(project, {
|
||||||
semanticLayerCompute: compute,
|
semanticLayerCompute: compute,
|
||||||
queryExecutor,
|
queryExecutor,
|
||||||
|
embeddingService: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await ports.semanticLayer?.query({
|
const result = await ports.semanticLayer?.query({
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import { type KtxSqlQueryExecutorPort, localConnectionInfoFromConfig } from '../connections/index.js';
|
import { type KtxSqlQueryExecutorPort, localConnectionInfoFromConfig } from '../connections/index.js';
|
||||||
import type { KtxEmbeddingPort } from '../core/index.js';
|
import type { KtxEmbeddingPort } from '../core/index.js';
|
||||||
import type { KtxSemanticLayerComputePort } from '../daemon/index.js';
|
import type { KtxSemanticLayerComputePort } from '../daemon/index.js';
|
||||||
import { createLocalKtxEmbeddingProviderFromConfig, KtxIngestEmbeddingPortAdapter } from '../llm/index.js';
|
|
||||||
import type { KtxLocalProject } from '../project/index.js';
|
import type { KtxLocalProject } from '../project/index.js';
|
||||||
import { createKtxEntityDetailsService, type KtxScanConnector, type LocalScanMcpOptions } from '../scan/index.js';
|
import { createKtxEntityDetailsService, type KtxScanConnector, type LocalScanMcpOptions } from '../scan/index.js';
|
||||||
import { createKtxDiscoverDataService } from '../search/index.js';
|
import { createKtxDiscoverDataService } from '../search/index.js';
|
||||||
|
|
@ -15,7 +14,7 @@ interface CreateLocalProjectMcpContextPortsOptions {
|
||||||
queryExecutor?: KtxSqlQueryExecutorPort;
|
queryExecutor?: KtxSqlQueryExecutorPort;
|
||||||
sqlAnalysis?: SqlAnalysisPort;
|
sqlAnalysis?: SqlAnalysisPort;
|
||||||
localScan?: LocalScanMcpOptions;
|
localScan?: LocalScanMcpOptions;
|
||||||
embeddingService?: KtxEmbeddingPort | null;
|
embeddingService: KtxEmbeddingPort | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function dialectForDriver(driver: string | undefined): string {
|
function dialectForDriver(driver: string | undefined): string {
|
||||||
|
|
@ -133,12 +132,9 @@ async function executeValidatedReadOnlySql(
|
||||||
|
|
||||||
export function createLocalProjectMcpContextPorts(
|
export function createLocalProjectMcpContextPorts(
|
||||||
project: KtxLocalProject,
|
project: KtxLocalProject,
|
||||||
options: CreateLocalProjectMcpContextPortsOptions = {},
|
options: CreateLocalProjectMcpContextPortsOptions,
|
||||||
): KtxMcpContextPorts {
|
): KtxMcpContextPorts {
|
||||||
const configuredEmbeddingProvider = createLocalKtxEmbeddingProviderFromConfig(project.config.ingest.embeddings);
|
const embeddingService = options.embeddingService;
|
||||||
const embeddingService =
|
|
||||||
options.embeddingService ??
|
|
||||||
(configuredEmbeddingProvider ? new KtxIngestEmbeddingPortAdapter(configuredEmbeddingProvider) : null);
|
|
||||||
const ports: KtxMcpContextPorts = {
|
const ports: KtxMcpContextPorts = {
|
||||||
connections: {
|
connections: {
|
||||||
async list() {
|
async list() {
|
||||||
|
|
|
||||||
|
|
@ -143,7 +143,6 @@ describe('@ktx/context package exports', () => {
|
||||||
expect(root.assertSearchBackendConformanceCase).toBeTypeOf('function');
|
expect(root.assertSearchBackendConformanceCase).toBeTypeOf('function');
|
||||||
expect(root.assertSearchBackendCapabilities).toBeTypeOf('function');
|
expect(root.assertSearchBackendCapabilities).toBeTypeOf('function');
|
||||||
expect(root.createLocalKtxEmbeddingProviderFromConfig).toBeTypeOf('function');
|
expect(root.createLocalKtxEmbeddingProviderFromConfig).toBeTypeOf('function');
|
||||||
expect(root.MANAGED_SENTENCE_TRANSFORMERS_BASE_URL).toBe('managed:local-embeddings');
|
|
||||||
expect(agent).toBeDefined();
|
expect(agent).toBeDefined();
|
||||||
expect(agent.AgentRunnerService).toBeTypeOf('function');
|
expect(agent.AgentRunnerService).toBeTypeOf('function');
|
||||||
expect(root.AgentRunnerService).toBeTypeOf('function');
|
expect(root.AgentRunnerService).toBeTypeOf('function');
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ const vertexProviderSchema = z
|
||||||
|
|
||||||
const sentenceTransformersSchema = z
|
const sentenceTransformersSchema = z
|
||||||
.strictObject({
|
.strictObject({
|
||||||
base_url: z.string().default('').describe('Base URL of the sentence-transformers HTTP server. Empty string uses the managed local runtime.'),
|
base_url: z.string().default('').describe('Base URL of the sentence-transformers HTTP server. Leave empty (or omit) to use the project-managed local daemon.'),
|
||||||
pathPrefix: z.string().optional().describe('Optional URL path prefix prepended to embedding requests.'),
|
pathPrefix: z.string().optional().describe('Optional URL path prefix prepended to embedding requests.'),
|
||||||
})
|
})
|
||||||
.describe('Sentence-transformers embedding server configuration.');
|
.describe('Sentence-transformers embedding server configuration.');
|
||||||
|
|
|
||||||
|
|
@ -813,16 +813,16 @@ describe('local scan enrichment', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('resolves gateway LLM providers and OpenAI embeddings from local scan config', () => {
|
it('resolves gateway LLM providers and passes injected embedding provider through to scan enrichment', () => {
|
||||||
const createKtxLlmProvider = vi.fn(() => ({
|
const createKtxLlmProvider = vi.fn(() => ({
|
||||||
getModel: vi.fn().mockReturnValue({ modelId: 'provider/language-model', provider: 'gateway' }),
|
getModel: vi.fn().mockReturnValue({ modelId: 'provider/language-model', provider: 'gateway' }),
|
||||||
}));
|
}));
|
||||||
const createKtxEmbeddingProvider = vi.fn(() => ({
|
const embeddingProvider = {
|
||||||
dimensions: 1536,
|
dimensions: 1536,
|
||||||
maxBatchSize: 8,
|
maxBatchSize: 8,
|
||||||
embed: vi.fn(),
|
embed: vi.fn(),
|
||||||
[['embed', 'Many'].join('')]: vi.fn(),
|
[['embed', 'Many'].join('')]: vi.fn(),
|
||||||
}));
|
};
|
||||||
|
|
||||||
const providers = createLocalScanEnrichmentProvidersFromConfig(
|
const providers = createLocalScanEnrichmentProvidersFromConfig(
|
||||||
{
|
{
|
||||||
|
|
@ -844,8 +844,8 @@ describe('local scan enrichment', () => {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
createKtxLlmProvider: createKtxLlmProvider as any,
|
createKtxLlmProvider: createKtxLlmProvider as any,
|
||||||
createKtxEmbeddingProvider: createKtxEmbeddingProvider as any,
|
|
||||||
env: { OPENAI_API_KEY: 'openai-key' }, // pragma: allowlist secret
|
env: { OPENAI_API_KEY: 'openai-key' }, // pragma: allowlist secret
|
||||||
|
embeddingProvider: embeddingProvider as any,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -854,8 +854,5 @@ describe('local scan enrichment', () => {
|
||||||
expect(createKtxLlmProvider).toHaveBeenCalledWith(
|
expect(createKtxLlmProvider).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({ backend: 'gateway', modelSlots: { default: 'provider/language-model' } }),
|
expect.objectContaining({ backend: 'gateway', modelSlots: { default: 'provider/language-model' } }),
|
||||||
);
|
);
|
||||||
expect(createKtxEmbeddingProvider).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({ backend: 'openai', model: 'provider/embedding-model' }),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import type { createKtxEmbeddingProvider, createKtxLlmProvider } from '@ktx/llm';
|
import type { createKtxEmbeddingProvider, createKtxLlmProvider, KtxEmbeddingProvider } from '@ktx/llm';
|
||||||
import {
|
import {
|
||||||
createDefaultLocalIngestAdapters,
|
createDefaultLocalIngestAdapters,
|
||||||
getLocalStageOnlyIngestStatus,
|
getLocalStageOnlyIngestStatus,
|
||||||
|
|
@ -6,11 +6,7 @@ import {
|
||||||
runLocalStageOnlyIngest,
|
runLocalStageOnlyIngest,
|
||||||
type SourceAdapter,
|
type SourceAdapter,
|
||||||
} from '../ingest/index.js';
|
} from '../ingest/index.js';
|
||||||
import {
|
import { createLocalKtxLlmRuntimeFromConfig, KtxScanEmbeddingPortAdapter } from '../llm/index.js';
|
||||||
createLocalKtxEmbeddingProviderFromConfig,
|
|
||||||
createLocalKtxLlmRuntimeFromConfig,
|
|
||||||
KtxScanEmbeddingPortAdapter,
|
|
||||||
} from '../llm/index.js';
|
|
||||||
import type { KtxProjectLlmConfig, KtxScanEnrichmentConfig, KtxScanRelationshipConfig } from '../project/config.js';
|
import type { KtxProjectLlmConfig, KtxScanEnrichmentConfig, KtxScanRelationshipConfig } from '../project/config.js';
|
||||||
import type { KtxLocalProject } from '../project/index.js';
|
import type { KtxLocalProject } from '../project/index.js';
|
||||||
import { ktxLocalStateDbPath } from '../project/local-state-db.js';
|
import { ktxLocalStateDbPath } from '../project/local-state-db.js';
|
||||||
|
|
@ -55,6 +51,7 @@ export interface RunLocalScanOptions {
|
||||||
enrichmentProviders?: KtxLocalScanEnrichmentProviders | null;
|
enrichmentProviders?: KtxLocalScanEnrichmentProviders | null;
|
||||||
enrichmentStateStore?: SqliteLocalScanEnrichmentStateStore | null;
|
enrichmentStateStore?: SqliteLocalScanEnrichmentStateStore | null;
|
||||||
progress?: KtxProgressPort;
|
progress?: KtxProgressPort;
|
||||||
|
embeddingProvider?: KtxEmbeddingProvider | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LocalScanRunResult {
|
export interface LocalScanRunResult {
|
||||||
|
|
@ -152,6 +149,7 @@ interface LocalScanEnrichmentProviderDeps {
|
||||||
createKtxEmbeddingProvider?: typeof createKtxEmbeddingProvider;
|
createKtxEmbeddingProvider?: typeof createKtxEmbeddingProvider;
|
||||||
env?: NodeJS.ProcessEnv;
|
env?: NodeJS.ProcessEnv;
|
||||||
projectDir?: string;
|
projectDir?: string;
|
||||||
|
embeddingProvider?: KtxEmbeddingProvider | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createLocalScanEnrichmentProvidersFromConfig(
|
export function createLocalScanEnrichmentProvidersFromConfig(
|
||||||
|
|
@ -171,7 +169,7 @@ export function createLocalScanEnrichmentProvidersFromConfig(
|
||||||
...deps,
|
...deps,
|
||||||
projectDir: deps.projectDir,
|
projectDir: deps.projectDir,
|
||||||
});
|
});
|
||||||
const embeddingProvider = createLocalKtxEmbeddingProviderFromConfig(config.embeddings, deps);
|
const embeddingProvider = deps.embeddingProvider ?? null;
|
||||||
if (!llmRuntime || !embeddingProvider) {
|
if (!llmRuntime || !embeddingProvider) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -371,6 +369,7 @@ export async function runLocalScan(options: RunLocalScanOptions): Promise<LocalS
|
||||||
? options.enrichmentProviders
|
? options.enrichmentProviders
|
||||||
: createLocalScanEnrichmentProvidersFromConfig(options.project.config.scan.enrichment, options.project.config.llm, {
|
: createLocalScanEnrichmentProvidersFromConfig(options.project.config.scan.enrichment, options.project.config.llm, {
|
||||||
projectDir: options.project.projectDir,
|
projectDir: options.project.projectDir,
|
||||||
|
embeddingProvider: options.embeddingProvider ?? null,
|
||||||
})
|
})
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -357,10 +357,13 @@ export async function runLocalEmbeddingsRuntimeSmoke(options = {}) {
|
||||||
requireOutput(commands[5].label, setup, /Embeddings ready: yes \(all-MiniLM-L6-v2\)/);
|
requireOutput(commands[5].label, setup, /Embeddings ready: yes \(all-MiniLM-L6-v2\)/);
|
||||||
|
|
||||||
const config = await readFile(join(projectDir, 'ktx.yaml'), 'utf8');
|
const config = await readFile(join(projectDir, 'ktx.yaml'), 'utf8');
|
||||||
if (!config.includes('base_url: managed:local-embeddings')) {
|
if (!/backend:\s*sentence-transformers/.test(config)) {
|
||||||
throw new Error(`ktx.yaml did not contain managed local embeddings marker:\n${config}`);
|
throw new Error(`ktx.yaml did not declare sentence-transformers embedding backend:\n${config}`);
|
||||||
}
|
}
|
||||||
process.stdout.write('KTX setup persisted managed local embeddings marker\n');
|
if (/base_url:/.test(config)) {
|
||||||
|
throw new Error(`ktx.yaml should omit base_url for managed local embeddings:\n${config}`);
|
||||||
|
}
|
||||||
|
process.stdout.write('KTX setup persisted managed local embeddings (no base_url)\n');
|
||||||
|
|
||||||
const stop = await run(commands[6].command, commands[6].args, {
|
const stop = await run(commands[6].command, commands[6].args, {
|
||||||
cwd: installDir,
|
cwd: installDir,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue