mirror of
https://github.com/Kaelio/ktx.git
synced 2026-07-07 11:02:11 +02:00
feat: add claude-code llm backend with runtime port (#115)
* docs: revise claude-code ingest backend spec * docs: keep claude-code spec focused on ingest * docs: expand claude-code spec to full llm parity * Refine claude-code backend spec after adversarial review iteration 1 * Refine claude-code backend spec after adversarial review iteration 2 * Refine claude-code backend spec after adversarial review iteration 3 * feat: recognize claude-code llm backend * feat: add ktx llm runtime port * feat: add claude-code llm runtime * feat: route non-agent llm calls through runtime * feat: run ingest agents through llm runtime * feat: support claude-code setup and status * test: verify claude-code backend runtime * docs: add claude-code backend v1 runtime plan * fix: close claude-code runtime isolation checks * fix: warn on claude-code prompt caching during setup * chore: verify claude-code v1 closure * docs: add claude-code backend v1 isolation closure plan * fix: update claude-code ingest setup guidance * docs: add claude-code backend v1 ingest guidance closure plan * docs: align claude-code isolation spec with sdk metadata * test: cover claude-code host discovery metadata * fix: tolerate claude-code host discovery metadata * docs: clarify claude-code host discovery metadata * docs: add claude-code auth-probe isolation fix plan * chore: prepare kaelio ktx rc1 release * chore: add semantic release workflow * fix: unblock ci checks * chore(release): 0.1.0-rc.1 * feat: add Claude Code model selection to setup * fix: keep git maintenance attached in local repos
This commit is contained in:
parent
e6d578c03f
commit
b565e44a22
109 changed files with 10218 additions and 1093 deletions
|
|
@ -31,46 +31,32 @@ function createCache(initial: Record<string, string> = {}): KtxDescriptionCacheP
|
|||
function createLlmProvider(text = 'generated description') {
|
||||
vi.mocked(generateText).mockResolvedValue({ text } as never);
|
||||
return {
|
||||
getModel: vi.fn().mockReturnValue({ modelId: 'claude-sonnet-4-6', provider: 'anthropic' }),
|
||||
getModelByName: vi.fn(),
|
||||
cacheMarker: vi.fn(),
|
||||
repairToolCallHandler: vi.fn(),
|
||||
thinkingProviderOptions: vi.fn(),
|
||||
telemetryConfig: vi.fn(),
|
||||
promptCachingConfig: vi.fn(() => ({
|
||||
enabled: false,
|
||||
systemTtl: '1h',
|
||||
toolsTtl: '1h',
|
||||
historyTtl: '5m',
|
||||
cacheSystem: true,
|
||||
cacheTools: true,
|
||||
cacheHistory: true,
|
||||
vertexFallbackTo5m: false,
|
||||
})),
|
||||
activeBackend: vi.fn(() => 'anthropic'),
|
||||
generateText: vi.fn(async (input) => {
|
||||
const result = await generateText({
|
||||
system: input.system ? { role: 'system', content: input.system } : undefined,
|
||||
messages: [{ role: 'user', content: input.prompt }],
|
||||
temperature: input.temperature,
|
||||
} as never);
|
||||
return result.text;
|
||||
}),
|
||||
generateObject: vi.fn(),
|
||||
runAgentLoop: vi.fn(),
|
||||
} as any;
|
||||
}
|
||||
|
||||
function createFailingLlmProvider(message = 'timeout exceeded when trying to connect') {
|
||||
vi.mocked(generateText).mockRejectedValue(new Error(message) as never);
|
||||
return {
|
||||
getModel: vi.fn().mockReturnValue({ modelId: 'claude-sonnet-4-6', provider: 'anthropic' }),
|
||||
getModelByName: vi.fn(),
|
||||
cacheMarker: vi.fn(),
|
||||
repairToolCallHandler: vi.fn(),
|
||||
thinkingProviderOptions: vi.fn(),
|
||||
telemetryConfig: vi.fn(),
|
||||
promptCachingConfig: vi.fn(() => ({
|
||||
enabled: false,
|
||||
systemTtl: '1h',
|
||||
toolsTtl: '1h',
|
||||
historyTtl: '5m',
|
||||
cacheSystem: true,
|
||||
cacheTools: true,
|
||||
cacheHistory: true,
|
||||
vertexFallbackTo5m: false,
|
||||
})),
|
||||
activeBackend: vi.fn(() => 'anthropic'),
|
||||
generateText: vi.fn(async (input) => {
|
||||
const result = await generateText({
|
||||
system: input.system ? { role: 'system', content: input.system } : undefined,
|
||||
messages: [{ role: 'user', content: input.prompt }],
|
||||
temperature: input.temperature,
|
||||
} as never);
|
||||
return result.text;
|
||||
}),
|
||||
generateObject: vi.fn(),
|
||||
runAgentLoop: vi.fn(),
|
||||
} as any;
|
||||
}
|
||||
|
||||
|
|
@ -158,10 +144,10 @@ describe('KTX description prompt builders', () => {
|
|||
describe('KtxDescriptionGenerator', () => {
|
||||
it('generates column descriptions with pre-fetched values, cache hits, and word-limit metadata', async () => {
|
||||
const cache = createCache({ 'warehouse.public.orders.cached_status': 'Cached status description' });
|
||||
const llmProvider = createLlmProvider('Payment state');
|
||||
const llmRuntime = createLlmProvider('Payment state');
|
||||
const connector = createConnector();
|
||||
const generator = new KtxDescriptionGenerator({
|
||||
llmProvider,
|
||||
llmRuntime,
|
||||
cache,
|
||||
settings: {
|
||||
columnMaxWords: 12,
|
||||
|
|
@ -222,7 +208,7 @@ describe('KtxDescriptionGenerator', () => {
|
|||
it('samples through the connector when column values are not pre-fetched', async () => {
|
||||
const connector = createConnector();
|
||||
const generator = new KtxDescriptionGenerator({
|
||||
llmProvider: createLlmProvider('Current order state'),
|
||||
llmRuntime: createLlmProvider('Current order state'),
|
||||
settings: {
|
||||
columnMaxWords: 12,
|
||||
tableMaxWords: 18,
|
||||
|
|
@ -271,7 +257,7 @@ describe('KtxDescriptionGenerator', () => {
|
|||
})),
|
||||
};
|
||||
const generator = new KtxDescriptionGenerator({
|
||||
llmProvider: createLlmProvider('Generated through sampler'),
|
||||
llmRuntime: createLlmProvider('Generated through sampler'),
|
||||
settings: {
|
||||
columnMaxWords: 12,
|
||||
tableMaxWords: 18,
|
||||
|
|
@ -310,7 +296,7 @@ describe('KtxDescriptionGenerator', () => {
|
|||
const cache = createCache();
|
||||
const connector = createConnector();
|
||||
const generator = new KtxDescriptionGenerator({
|
||||
llmProvider: createFailingLlmProvider(),
|
||||
llmRuntime: createFailingLlmProvider(),
|
||||
cache,
|
||||
settings: {
|
||||
columnMaxWords: 12,
|
||||
|
|
@ -355,7 +341,7 @@ describe('KtxDescriptionGenerator', () => {
|
|||
const cache = createCache();
|
||||
const connector = createConnector();
|
||||
const generator = new KtxDescriptionGenerator({
|
||||
llmProvider: createLlmProvider('Commerce orders'),
|
||||
llmRuntime: createLlmProvider('Commerce orders'),
|
||||
cache,
|
||||
settings: {
|
||||
columnMaxWords: 12,
|
||||
|
|
@ -424,7 +410,7 @@ describe('KtxDescriptionGenerator resilience', () => {
|
|||
const logger = createLogger();
|
||||
const warnings: Array<{ code: string; table?: string }> = [];
|
||||
const generator = new KtxDescriptionGenerator({
|
||||
llmProvider: createLlmProvider('Commerce orders'),
|
||||
llmRuntime: createLlmProvider('Commerce orders'),
|
||||
logger,
|
||||
onWarning: (warning) => warnings.push({ code: warning.code, ...(warning.table ? { table: warning.table } : {}) }),
|
||||
settings: { columnMaxWords: 12, tableMaxWords: 18, dataSourceMaxWords: 24, concurrencyLimit: 2 },
|
||||
|
|
@ -455,7 +441,7 @@ describe('KtxDescriptionGenerator resilience', () => {
|
|||
const logger = createLogger();
|
||||
const warnings: Array<{ code: string; table?: string; metadata?: Record<string, unknown> }> = [];
|
||||
const generator = new KtxDescriptionGenerator({
|
||||
llmProvider: createLlmProvider('Customer reference data'),
|
||||
llmRuntime: createLlmProvider('Customer reference data'),
|
||||
logger,
|
||||
onWarning: (warning) =>
|
||||
warnings.push({
|
||||
|
|
@ -503,7 +489,7 @@ describe('KtxDescriptionGenerator resilience', () => {
|
|||
};
|
||||
const warnings: string[] = [];
|
||||
const generator = new KtxDescriptionGenerator({
|
||||
llmProvider: createFailingLlmProvider(),
|
||||
llmRuntime: createFailingLlmProvider(),
|
||||
onWarning: (warning) => warnings.push(warning.code),
|
||||
settings: { columnMaxWords: 12, tableMaxWords: 18, dataSourceMaxWords: 24 },
|
||||
});
|
||||
|
|
@ -528,7 +514,7 @@ describe('KtxDescriptionGenerator resilience', () => {
|
|||
};
|
||||
const warnings: string[] = [];
|
||||
const generator = new KtxDescriptionGenerator({
|
||||
llmProvider: createLlmProvider('Orders mart'),
|
||||
llmRuntime: createLlmProvider('Orders mart'),
|
||||
onWarning: (warning) => warnings.push(warning.code),
|
||||
settings: { columnMaxWords: 12, tableMaxWords: 18, dataSourceMaxWords: 24 },
|
||||
});
|
||||
|
|
@ -562,7 +548,7 @@ describe('KtxDescriptionGenerator resilience', () => {
|
|||
};
|
||||
const warnings: string[] = [];
|
||||
const generator = new KtxDescriptionGenerator({
|
||||
llmProvider: createLlmProvider('should not be called'),
|
||||
llmRuntime: createLlmProvider('should not be called'),
|
||||
onWarning: (warning) => warnings.push(warning.code),
|
||||
settings: { columnMaxWords: 12, tableMaxWords: 18, dataSourceMaxWords: 24 },
|
||||
});
|
||||
|
|
@ -588,7 +574,7 @@ describe('KtxDescriptionGenerator resilience', () => {
|
|||
};
|
||||
const logger = createLogger();
|
||||
const generator = new KtxDescriptionGenerator({
|
||||
llmProvider: createLlmProvider('Payment lifecycle state'),
|
||||
llmRuntime: createLlmProvider('Payment lifecycle state'),
|
||||
logger,
|
||||
settings: { columnMaxWords: 12, tableMaxWords: 18, dataSourceMaxWords: 24 },
|
||||
});
|
||||
|
|
@ -625,7 +611,7 @@ describe('KtxDescriptionGenerator resilience', () => {
|
|||
sampleColumn,
|
||||
};
|
||||
const generator = new KtxDescriptionGenerator({
|
||||
llmProvider: createLlmProvider('Customer reference identifier'),
|
||||
llmRuntime: createLlmProvider('Customer reference identifier'),
|
||||
settings: { columnMaxWords: 12, tableMaxWords: 18, dataSourceMaxWords: 24 },
|
||||
});
|
||||
|
||||
|
|
@ -657,7 +643,7 @@ describe('KtxDescriptionGenerator resilience', () => {
|
|||
};
|
||||
vi.mocked(generateText).mockClear();
|
||||
const generator = new KtxDescriptionGenerator({
|
||||
llmProvider: createLlmProvider('should not be called'),
|
||||
llmRuntime: createLlmProvider('should not be called'),
|
||||
settings: { columnMaxWords: 12, tableMaxWords: 18, dataSourceMaxWords: 24 },
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import type { KtxLlmProvider } from '@ktx/llm';
|
||||
import { generateKtxText } from '../llm/index.js';
|
||||
import type { KtxLlmRuntimePort } from '../llm/index.js';
|
||||
import type {
|
||||
KtxColumnSampleInput,
|
||||
KtxColumnSampleResult,
|
||||
|
|
@ -120,7 +119,7 @@ export interface KtxGenerateDataSourceDescriptionInput {
|
|||
}
|
||||
|
||||
export interface KtxDescriptionGeneratorOptions {
|
||||
llmProvider: KtxLlmProvider;
|
||||
llmRuntime: KtxLlmRuntimePort;
|
||||
cache?: KtxDescriptionCachePort;
|
||||
logger?: KtxScanLoggerPort;
|
||||
onWarning?: (warning: KtxScanWarning) => void;
|
||||
|
|
@ -400,14 +399,14 @@ Data source type: ${input.dataSourceType}`;
|
|||
}
|
||||
|
||||
export class KtxDescriptionGenerator {
|
||||
private readonly llmProvider: KtxLlmProvider;
|
||||
private readonly llmRuntime: KtxLlmRuntimePort;
|
||||
private readonly cache?: KtxDescriptionCachePort;
|
||||
private readonly logger?: KtxScanLoggerPort;
|
||||
private readonly onWarning?: (warning: KtxScanWarning) => void;
|
||||
private readonly settings: ResolvedKtxDescriptionGenerationSettings;
|
||||
|
||||
constructor(options: KtxDescriptionGeneratorOptions) {
|
||||
this.llmProvider = options.llmProvider;
|
||||
this.llmRuntime = options.llmRuntime;
|
||||
this.cache = options.cache;
|
||||
this.logger = options.logger;
|
||||
this.onWarning = options.onWarning;
|
||||
|
|
@ -779,8 +778,7 @@ export class KtxDescriptionGenerator {
|
|||
|
||||
private async generateAiDescription(prompt: KtxDescriptionPrompt, _operationName: string): Promise<string | null> {
|
||||
try {
|
||||
const text = await generateKtxText({
|
||||
llmProvider: this.llmProvider,
|
||||
const text = await this.llmRuntime.generateText({
|
||||
role: 'candidateExtraction',
|
||||
system: prompt.system,
|
||||
prompt: prompt.user,
|
||||
|
|
|
|||
|
|
@ -264,7 +264,6 @@ export type {
|
|||
} from './relationship-graph-resolver.js';
|
||||
export { resolveKtxRelationshipGraph } from './relationship-graph-resolver.js';
|
||||
export type {
|
||||
KtxRelationshipLlmProposalGenerateText,
|
||||
KtxRelationshipLlmProposalResult,
|
||||
KtxRelationshipLlmProposalSettings,
|
||||
ProposeKtxRelationshipCandidatesWithLlmInput,
|
||||
|
|
|
|||
|
|
@ -356,7 +356,7 @@ describe('local scan enrichment', () => {
|
|||
|
||||
it('honors scan relationship config when LLM proposals are disabled', async () => {
|
||||
const providers = createDeterministicLocalScanEnrichmentProviders({ embeddingDimensions: 3 });
|
||||
const getModel = vi.fn(() => ({ modelId: 'provider/language-model', provider: 'gateway' }));
|
||||
const generateObject = vi.fn();
|
||||
const result = await runLocalScanEnrichment({
|
||||
connectionId: 'warehouse',
|
||||
mode: 'relationships',
|
||||
|
|
@ -365,9 +365,9 @@ describe('local scan enrichment', () => {
|
|||
context: { runId: 'scan-run-llm-disabled' },
|
||||
providers: {
|
||||
...providers,
|
||||
llm: {
|
||||
...providers.llm,
|
||||
getModel: getModel as never,
|
||||
llmRuntime: {
|
||||
...providers.llmRuntime,
|
||||
generateObject: generateObject as never,
|
||||
},
|
||||
},
|
||||
relationshipSettings: {
|
||||
|
|
@ -378,7 +378,7 @@ describe('local scan enrichment', () => {
|
|||
});
|
||||
|
||||
expect(result.summary.llmRelationshipValidation).toBe('skipped');
|
||||
expect(getModel).not.toHaveBeenCalledWith('candidateExtraction');
|
||||
expect(generateObject).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('skips relationship detection when scan relationships are disabled', async () => {
|
||||
|
|
@ -628,7 +628,7 @@ describe('local scan enrichment', () => {
|
|||
connector: scanConnector,
|
||||
context: { runId: 'scan-run-batched-embeddings' },
|
||||
providers: {
|
||||
llm: deterministicProviders.llm,
|
||||
llmRuntime: deterministicProviders.llmRuntime,
|
||||
embedding: {
|
||||
dimensions: 3,
|
||||
maxBatchSize: 2,
|
||||
|
|
@ -658,7 +658,7 @@ describe('local scan enrichment', () => {
|
|||
providerIdentity: { provider: 'deterministic', embeddingDimensions: 6 },
|
||||
});
|
||||
|
||||
const getModel = vi.spyOn(providers.llm, 'getModel');
|
||||
const generateText = vi.spyOn(providers.llmRuntime, 'generateText');
|
||||
const embedBatch = vi.spyOn(providers.embedding, 'embedBatch');
|
||||
const second = await runLocalScanEnrichment({
|
||||
connectionId: 'warehouse',
|
||||
|
|
@ -676,7 +676,7 @@ describe('local scan enrichment', () => {
|
|||
expect(first.state.resumedStages).toEqual([]);
|
||||
expect(second.state.resumedStages).toEqual(['descriptions', 'embeddings', 'relationships']);
|
||||
expect(second.state.completedStages).toEqual(['descriptions', 'embeddings', 'relationships']);
|
||||
expect(getModel).not.toHaveBeenCalled();
|
||||
expect(generateText).not.toHaveBeenCalled();
|
||||
expect(embedBatch).not.toHaveBeenCalled();
|
||||
expect(second.descriptionUpdates).toEqual(first.descriptionUpdates);
|
||||
expect(second.embeddingUpdates).toEqual(first.embeddingUpdates);
|
||||
|
|
@ -711,7 +711,7 @@ describe('local scan enrichment', () => {
|
|||
tables: [{ ...firstTable, name: 'customers' }],
|
||||
})),
|
||||
};
|
||||
const getModel = vi.spyOn(providers.llm, 'getModel');
|
||||
const generateText = vi.spyOn(providers.llmRuntime, 'generateText');
|
||||
|
||||
const result = await runLocalScanEnrichment({
|
||||
connectionId: 'warehouse',
|
||||
|
|
@ -727,7 +727,7 @@ describe('local scan enrichment', () => {
|
|||
|
||||
expect(result.state.resumedStages).toEqual([]);
|
||||
expect(result.state.completedStages).toEqual(['descriptions', 'embeddings', 'relationships']);
|
||||
expect(getModel).toHaveBeenCalled();
|
||||
expect(generateText).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('runs providerless enriched scans as relationship-only discovery enrichment', async () => {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { KtxLlmProvider } from '@ktx/llm';
|
||||
import pLimit from 'p-limit';
|
||||
import type { KtxLlmRuntimePort } from '../llm/index.js';
|
||||
import { buildDefaultKtxProjectConfig, type KtxScanRelationshipConfig } from '../project/config.js';
|
||||
import { type KtxDescriptionColumnTable, KtxDescriptionGenerator } from './description-generation.js';
|
||||
import { buildKtxColumnEmbeddingText } from './embedding-text.js';
|
||||
|
|
@ -49,7 +49,7 @@ export interface DeterministicLocalScanEnrichmentProviderOptions {
|
|||
}
|
||||
|
||||
export interface KtxLocalScanEnrichmentProviders {
|
||||
llm: KtxLlmProvider;
|
||||
llmRuntime: KtxLlmRuntimePort;
|
||||
embedding: KtxEmbeddingPort;
|
||||
}
|
||||
|
||||
|
|
@ -190,7 +190,7 @@ export function createDeterministicLocalScanEnrichmentProviders(
|
|||
const dimensions = options.embeddingDimensions ?? 8;
|
||||
const maxBatchSize = options.maxBatchSize ?? 64;
|
||||
return {
|
||||
llm: deterministicLlmProvider(),
|
||||
llmRuntime: deterministicLlmRuntime(),
|
||||
embedding: {
|
||||
dimensions,
|
||||
maxBatchSize,
|
||||
|
|
@ -201,41 +201,16 @@ export function createDeterministicLocalScanEnrichmentProviders(
|
|||
};
|
||||
}
|
||||
|
||||
function deterministicLlmProvider(): KtxLlmProvider {
|
||||
const model = { modelId: 'deterministic-scan', provider: 'deterministic' };
|
||||
function deterministicLlmRuntime(): KtxLlmRuntimePort {
|
||||
return {
|
||||
getModel() {
|
||||
return model as ReturnType<KtxLlmProvider['getModel']>;
|
||||
async generateText(input) {
|
||||
return `Deterministic description for ${input.prompt.slice(0, 64).trim() || 'data source'}`;
|
||||
},
|
||||
getModelByName() {
|
||||
return model as ReturnType<KtxLlmProvider['getModelByName']>;
|
||||
async generateObject() {
|
||||
return { pkCandidates: [], fkCandidates: [] } as never;
|
||||
},
|
||||
cacheMarker() {
|
||||
return undefined;
|
||||
},
|
||||
repairToolCallHandler() {
|
||||
throw new Error('deterministic scan provider does not support tool-call repair');
|
||||
},
|
||||
thinkingProviderOptions() {
|
||||
return {};
|
||||
},
|
||||
telemetryConfig() {
|
||||
return undefined;
|
||||
},
|
||||
promptCachingConfig() {
|
||||
return {
|
||||
enabled: false,
|
||||
systemTtl: '1h',
|
||||
toolsTtl: '1h',
|
||||
historyTtl: '5m',
|
||||
cacheSystem: true,
|
||||
cacheTools: true,
|
||||
cacheHistory: true,
|
||||
vertexFallbackTo5m: false,
|
||||
};
|
||||
},
|
||||
activeBackend() {
|
||||
return 'gateway';
|
||||
async runAgentLoop() {
|
||||
return { stopReason: 'natural' };
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -324,7 +299,7 @@ async function generateDescriptions(input: {
|
|||
}): Promise<KtxLocalScanEnrichmentResult['descriptionUpdates']> {
|
||||
const warningSink = input.warnings;
|
||||
const generator = new KtxDescriptionGenerator({
|
||||
llmProvider: input.providers.llm,
|
||||
llmRuntime: input.providers.llmRuntime,
|
||||
...(input.context.logger ? { logger: input.context.logger } : {}),
|
||||
...(warningSink
|
||||
? {
|
||||
|
|
@ -643,7 +618,7 @@ export async function runLocalScanEnrichment(
|
|||
schema,
|
||||
context: input.context,
|
||||
settings: relationshipSettings,
|
||||
llmProvider: input.providers?.llm ?? null,
|
||||
llmRuntime: input.providers?.llmRuntime ?? null,
|
||||
});
|
||||
|
||||
await relationshipProgress?.update(
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import type { KtxLlmProvider } from '@ktx/llm';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import YAML from 'yaml';
|
||||
import type { SourceAdapter } from '../ingest/index.js';
|
||||
import type { KtxLlmRuntimePort } from '../llm/index.js';
|
||||
import { initKtxProject, type KtxLocalProject, loadKtxProject } from '../project/index.js';
|
||||
import { filterSnapshotTables, getLocalScanReport, getLocalScanStatus, resolveEnabledTables, runLocalScan } from './local-scan.js';
|
||||
import type { KtxQueryResult, KtxReadOnlyQueryInput, KtxSchemaSnapshot, KtxSchemaTable } from './types.js';
|
||||
|
|
@ -79,25 +79,11 @@ function relationshipSqlResult(
|
|||
throw new Error(`Unexpected relationship SQL: ${input.sql}`);
|
||||
}
|
||||
|
||||
function deterministicLlmProvider(): KtxLlmProvider {
|
||||
function deterministicLlmRuntime(): KtxLlmRuntimePort {
|
||||
return {
|
||||
getModel: () => ({ provider: 'deterministic', modelId: 'deterministic' }) as never,
|
||||
getModelByName: () => ({ provider: 'deterministic', modelId: 'deterministic' }) as never,
|
||||
cacheMarker: () => undefined,
|
||||
repairToolCallHandler: (() => undefined) as never,
|
||||
thinkingProviderOptions: () => ({}),
|
||||
telemetryConfig: () => undefined,
|
||||
promptCachingConfig: () => ({
|
||||
enabled: false,
|
||||
systemTtl: '1h',
|
||||
toolsTtl: '1h',
|
||||
historyTtl: '5m',
|
||||
cacheSystem: true,
|
||||
cacheTools: true,
|
||||
cacheHistory: true,
|
||||
vertexFallbackTo5m: false,
|
||||
}),
|
||||
activeBackend: () => 'gateway',
|
||||
generateText: vi.fn(async (input) => `Deterministic description for ${input.prompt.slice(0, 64).trim() || 'data source'}`),
|
||||
generateObject: vi.fn(async () => ({ pkCandidates: [], fkCandidates: [] }) as never),
|
||||
runAgentLoop: vi.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -571,7 +557,7 @@ describe('local scan', () => {
|
|||
llmProposals: false,
|
||||
maxLlmTablesPerBatch: 7,
|
||||
};
|
||||
const getModel = vi.fn(() => ({ modelId: 'provider/language-model', provider: 'gateway' }));
|
||||
const generateObject = vi.fn(async () => ({ pkCandidates: [], fkCandidates: [] }));
|
||||
const connector = {
|
||||
id: 'test:warehouse',
|
||||
driver: 'postgres' as const,
|
||||
|
|
@ -650,9 +636,9 @@ describe('local scan', () => {
|
|||
detectRelationships: true,
|
||||
connector,
|
||||
enrichmentProviders: {
|
||||
llm: {
|
||||
...deterministicLlmProvider(),
|
||||
getModel: getModel as never,
|
||||
llmRuntime: {
|
||||
...deterministicLlmRuntime(),
|
||||
generateObject: generateObject as never,
|
||||
},
|
||||
embedding: {
|
||||
dimensions: 8,
|
||||
|
|
@ -668,7 +654,7 @@ describe('local scan', () => {
|
|||
|
||||
expect(result.report.relationships.accepted).toBe(1);
|
||||
expect(result.report.enrichment.llmRelationshipValidation).toBe('skipped');
|
||||
expect(getModel).not.toHaveBeenCalledWith('candidateExtraction');
|
||||
expect(generateObject).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('accepts no-declared-constraint relationships and writes relationship artifacts', async () => {
|
||||
|
|
@ -1206,7 +1192,7 @@ describe('local scan', () => {
|
|||
mode: 'enriched',
|
||||
connector,
|
||||
enrichmentProviders: {
|
||||
llm: deterministicLlmProvider(),
|
||||
llmRuntime: deterministicLlmRuntime(),
|
||||
embedding: {
|
||||
dimensions: 8,
|
||||
maxBatchSize: 64,
|
||||
|
|
@ -1314,7 +1300,7 @@ describe('local scan', () => {
|
|||
return { values: ['1'], nullCount: 0, distinctCount: 1 };
|
||||
},
|
||||
};
|
||||
const llm = deterministicLlmProvider();
|
||||
const llmRuntime = deterministicLlmRuntime();
|
||||
|
||||
const first = await runLocalScan({
|
||||
project,
|
||||
|
|
@ -1323,7 +1309,7 @@ describe('local scan', () => {
|
|||
mode: 'enriched',
|
||||
connector,
|
||||
enrichmentProviders: {
|
||||
llm,
|
||||
llmRuntime,
|
||||
embedding: {
|
||||
dimensions: 8,
|
||||
maxBatchSize: 64,
|
||||
|
|
@ -1344,7 +1330,7 @@ describe('local scan', () => {
|
|||
});
|
||||
expect(first.report.enrichment.embeddings).toBe('failed');
|
||||
|
||||
const getModel = vi.spyOn(llm, 'getModel');
|
||||
const generateObject = vi.spyOn(llmRuntime, 'generateObject');
|
||||
const retry = await runLocalScan({
|
||||
project,
|
||||
adapters: [fetchOnlyAdapter()],
|
||||
|
|
@ -1352,7 +1338,7 @@ describe('local scan', () => {
|
|||
mode: 'enriched',
|
||||
connector,
|
||||
enrichmentProviders: {
|
||||
llm,
|
||||
llmRuntime,
|
||||
embedding: {
|
||||
dimensions: 8,
|
||||
maxBatchSize: 64,
|
||||
|
|
@ -1373,8 +1359,8 @@ describe('local scan', () => {
|
|||
failedStages: [],
|
||||
});
|
||||
expect(retry.report.enrichment.embeddings).toBe('completed');
|
||||
expect(getModel).toHaveBeenCalledTimes(1);
|
||||
expect(getModel).toHaveBeenCalledWith('candidateExtraction');
|
||||
expect(generateObject).toHaveBeenCalledTimes(1);
|
||||
expect(generateObject).toHaveBeenCalledWith(expect.objectContaining({ role: 'candidateExtraction' }));
|
||||
expect(embeddingAttempts).toBe(2);
|
||||
|
||||
const reportPath = retry.report.artifactPaths.reportPath;
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import {
|
|||
} from '../ingest/index.js';
|
||||
import {
|
||||
createLocalKtxEmbeddingProviderFromConfig,
|
||||
createLocalKtxLlmProviderFromConfig,
|
||||
createLocalKtxLlmRuntimeFromConfig,
|
||||
KtxScanEmbeddingPortAdapter,
|
||||
} from '../llm/index.js';
|
||||
import type { KtxProjectLlmConfig, KtxScanEnrichmentConfig, KtxScanRelationshipConfig } from '../project/config.js';
|
||||
|
|
@ -150,6 +150,7 @@ interface LocalScanEnrichmentProviderDeps {
|
|||
createKtxLlmProvider?: typeof createKtxLlmProvider;
|
||||
createKtxEmbeddingProvider?: typeof createKtxEmbeddingProvider;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
projectDir?: string;
|
||||
}
|
||||
|
||||
export function createLocalScanEnrichmentProvidersFromConfig(
|
||||
|
|
@ -165,14 +166,17 @@ export function createLocalScanEnrichmentProvidersFromConfig(
|
|||
return null;
|
||||
}
|
||||
|
||||
const llm = createLocalKtxLlmProviderFromConfig(llmConfig, deps);
|
||||
const llmRuntime = createLocalKtxLlmRuntimeFromConfig(llmConfig, {
|
||||
...deps,
|
||||
projectDir: deps.projectDir,
|
||||
});
|
||||
const embeddingProvider = createLocalKtxEmbeddingProviderFromConfig(config.embeddings, deps);
|
||||
if (!llm || !embeddingProvider) {
|
||||
if (!llmRuntime || !embeddingProvider) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
llm,
|
||||
llmRuntime,
|
||||
embedding: new KtxScanEmbeddingPortAdapter(embeddingProvider),
|
||||
};
|
||||
}
|
||||
|
|
@ -378,7 +382,9 @@ export async function runLocalScan(options: RunLocalScanOptions): Promise<LocalS
|
|||
connector && (mode !== 'structural' || options.detectRelationships)
|
||||
? options.enrichmentProviders !== undefined
|
||||
? 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,
|
||||
})
|
||||
: null;
|
||||
|
||||
await options.progress?.update(0.15, 'Inspecting database schema');
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { gunzipSync } from 'node:zlib';
|
|||
import Database from 'better-sqlite3';
|
||||
import YAML from 'yaml';
|
||||
import { z } from 'zod';
|
||||
import type { KtxLlmRuntimePort } from '../llm/index.js';
|
||||
import type { KtxEnrichedRelationship, KtxEnrichedSchema, KtxRelationshipType } from './enrichment-types.js';
|
||||
import { snapshotToKtxEnrichedSchema } from './local-enrichment.js';
|
||||
import type { KtxRelationshipDiscoveryCandidate } from './relationship-candidates.js';
|
||||
|
|
@ -13,7 +14,6 @@ import {
|
|||
generateKtxRelationshipDiscoveryCandidates,
|
||||
mergeKtxRelationshipDiscoveryCandidates,
|
||||
} from './relationship-candidates.js';
|
||||
import type { KtxLlmProvider } from '@ktx/llm';
|
||||
import { proposeKtxRelationshipCandidatesWithLlm } from './relationship-llm-proposal.js';
|
||||
import {
|
||||
discoverKtxCompositeRelationships,
|
||||
|
|
@ -527,7 +527,7 @@ export function isKtxRelationshipBenchmarkTuningEligible(input: {
|
|||
}
|
||||
|
||||
export function ktxRelationshipBenchmarkDetectorWithLlm(
|
||||
llmProvider: KtxLlmProvider,
|
||||
llmRuntime: KtxLlmRuntimePort,
|
||||
): KtxRelationshipBenchmarkDetector {
|
||||
return {
|
||||
async detect(input) {
|
||||
|
|
@ -566,7 +566,7 @@ export function ktxRelationshipBenchmarkDetectorWithLlm(
|
|||
connectionId: input.snapshot.connectionId,
|
||||
schema: input.schema,
|
||||
profile: profiles,
|
||||
llmProvider,
|
||||
llmRuntime,
|
||||
});
|
||||
const candidates = mergeKtxRelationshipDiscoveryCandidates([
|
||||
...broadRelationshipCandidates,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { KtxLlmProvider } from '@ktx/llm';
|
||||
import Database from 'better-sqlite3';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import type { KtxLlmRuntimePort } from '../llm/index.js';
|
||||
import { buildDefaultKtxProjectConfig } from '../project/config.js';
|
||||
import { snapshotToKtxEnrichedSchema } from './local-enrichment.js';
|
||||
import {
|
||||
|
|
@ -216,29 +216,11 @@ function connector(executor: InMemorySqliteExecutor | null): KtxScanConnector {
|
|||
};
|
||||
}
|
||||
|
||||
function llmProvider(): KtxLlmProvider {
|
||||
const model = { modelId: 'claude-sonnet-4-6', provider: 'anthropic' };
|
||||
function llmRuntime(output: unknown): KtxLlmRuntimePort {
|
||||
return {
|
||||
getModel: vi.fn(() => model as ReturnType<KtxLlmProvider['getModel']>),
|
||||
getModelByName: vi.fn(() => model as ReturnType<KtxLlmProvider['getModelByName']>),
|
||||
cacheMarker: vi.fn(),
|
||||
repairToolCallHandler: vi.fn(),
|
||||
thinkingProviderOptions: vi.fn(() => ({})),
|
||||
telemetryConfig: vi.fn(() => undefined),
|
||||
promptCachingConfig: vi.fn(
|
||||
() =>
|
||||
({
|
||||
enabled: false,
|
||||
systemTtl: '1h',
|
||||
toolsTtl: '1h',
|
||||
historyTtl: '5m',
|
||||
cacheSystem: true,
|
||||
cacheTools: true,
|
||||
cacheHistory: true,
|
||||
vertexFallbackTo5m: false,
|
||||
}) as ReturnType<KtxLlmProvider['promptCachingConfig']>,
|
||||
),
|
||||
activeBackend: vi.fn(() => 'anthropic' as ReturnType<KtxLlmProvider['activeBackend']>),
|
||||
generateText: vi.fn(),
|
||||
generateObject: vi.fn(async () => output) as KtxLlmRuntimePort['generateObject'],
|
||||
runAgentLoop: vi.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -505,21 +487,19 @@ describe('production relationship discovery', () => {
|
|||
INSERT INTO customers (id) VALUES (1), (2);
|
||||
INSERT INTO orders (id, buyer_ref) VALUES (10, 1), (11, 2);
|
||||
`);
|
||||
const generateText = vi.fn(async () => ({
|
||||
output: {
|
||||
pkCandidates: [{ table: 'customers', column: 'id', confidence: 0.91, rationale: 'Unique customer key.' }],
|
||||
fkCandidates: [
|
||||
{
|
||||
fromTable: 'orders',
|
||||
fromColumn: 'buyer_ref',
|
||||
toTable: 'customers',
|
||||
toColumn: 'id',
|
||||
confidence: 0.89,
|
||||
rationale: 'Buyer reference values align with customer identifiers.',
|
||||
},
|
||||
],
|
||||
},
|
||||
}));
|
||||
const llmOutput = {
|
||||
pkCandidates: [{ table: 'customers', column: 'id', confidence: 0.91, rationale: 'Unique customer key.' }],
|
||||
fkCandidates: [
|
||||
{
|
||||
fromTable: 'orders',
|
||||
fromColumn: 'buyer_ref',
|
||||
toTable: 'customers',
|
||||
toColumn: 'id',
|
||||
confidence: 0.89,
|
||||
rationale: 'Buyer reference values align with customer identifiers.',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = await discoverKtxRelationships({
|
||||
connectionId: 'warehouse',
|
||||
|
|
@ -528,8 +508,7 @@ describe('production relationship discovery', () => {
|
|||
schema: snapshotToKtxEnrichedSchema(llmOnlyRelationshipSnapshot()),
|
||||
context: { runId: 'llm-relationship-orchestrator' },
|
||||
settings: relationshipSettings(),
|
||||
llmProvider: llmProvider(),
|
||||
generateText,
|
||||
llmRuntime: llmRuntime(llmOutput),
|
||||
});
|
||||
|
||||
expect(result.llmRelationshipValidation).toBe('completed');
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { KtxLlmProvider } from '@ktx/llm';
|
||||
import type { KtxLlmRuntimePort } from '../llm/index.js';
|
||||
import type { KtxScanRelationshipConfig } from '../project/config.js';
|
||||
import type { KtxEnrichedRelationship, KtxEnrichedSchema, KtxRelationshipUpdate } from './enrichment-types.js';
|
||||
import {
|
||||
|
|
@ -15,10 +15,7 @@ import {
|
|||
type KtxResolvedRelationshipDiscoveryCandidate,
|
||||
resolveKtxRelationshipGraph,
|
||||
} from './relationship-graph-resolver.js';
|
||||
import {
|
||||
type KtxRelationshipLlmProposalGenerateText,
|
||||
proposeKtxRelationshipCandidatesWithLlm,
|
||||
} from './relationship-llm-proposal.js';
|
||||
import { proposeKtxRelationshipCandidatesWithLlm } from './relationship-llm-proposal.js';
|
||||
import {
|
||||
createKtxRelationshipProfileCache,
|
||||
type KtxRelationshipProfileArtifact,
|
||||
|
|
@ -42,8 +39,7 @@ export interface DiscoverKtxRelationshipsInput {
|
|||
schema: KtxEnrichedSchema;
|
||||
context: KtxScanContext;
|
||||
settings: KtxScanRelationshipConfig;
|
||||
llmProvider?: KtxLlmProvider | null;
|
||||
generateText?: KtxRelationshipLlmProposalGenerateText;
|
||||
llmRuntime?: KtxLlmRuntimePort | null;
|
||||
}
|
||||
|
||||
export interface DiscoverKtxRelationshipsResult {
|
||||
|
|
@ -246,11 +242,10 @@ export async function discoverKtxRelationships(
|
|||
connectionId: input.connectionId,
|
||||
schema: input.schema,
|
||||
profile,
|
||||
llmProvider: input.llmProvider ?? null,
|
||||
llmRuntime: input.llmRuntime ?? null,
|
||||
settings: {
|
||||
maxTablesPerBatch: input.settings.maxLlmTablesPerBatch,
|
||||
},
|
||||
generateText: input.generateText,
|
||||
})
|
||||
: { candidates: [], warnings: [], llmCalls: 0, summary: 'skipped' as const };
|
||||
const candidates = mergeKtxRelationshipDiscoveryCandidates([
|
||||
|
|
|
|||
|
|
@ -1,32 +1,14 @@
|
|||
import type { KtxLlmProvider } from '@ktx/llm';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import type { KtxLlmRuntimePort } from '../llm/index.js';
|
||||
import type { KtxEnrichedColumn, KtxEnrichedSchema, KtxEnrichedTable } from './enrichment-types.js';
|
||||
import type { KtxRelationshipProfileArtifact } from './relationship-profiling.js';
|
||||
import { proposeKtxRelationshipCandidatesWithLlm } from './relationship-llm-proposal.js';
|
||||
|
||||
function llmProvider(provider = 'anthropic'): KtxLlmProvider {
|
||||
const model = { modelId: 'claude-sonnet-4-6', provider };
|
||||
function llmRuntime(output?: unknown): KtxLlmRuntimePort {
|
||||
return {
|
||||
getModel: vi.fn(() => model as ReturnType<KtxLlmProvider['getModel']>),
|
||||
getModelByName: vi.fn(() => model as ReturnType<KtxLlmProvider['getModelByName']>),
|
||||
cacheMarker: vi.fn(),
|
||||
repairToolCallHandler: vi.fn(),
|
||||
thinkingProviderOptions: vi.fn(() => ({})),
|
||||
telemetryConfig: vi.fn(() => undefined),
|
||||
promptCachingConfig: vi.fn(
|
||||
() =>
|
||||
({
|
||||
enabled: false,
|
||||
systemTtl: '1h',
|
||||
toolsTtl: '1h',
|
||||
historyTtl: '5m',
|
||||
cacheSystem: true,
|
||||
cacheTools: true,
|
||||
cacheHistory: true,
|
||||
vertexFallbackTo5m: false,
|
||||
}) as ReturnType<KtxLlmProvider['promptCachingConfig']>,
|
||||
),
|
||||
activeBackend: vi.fn(() => provider as ReturnType<KtxLlmProvider['activeBackend']>),
|
||||
generateText: vi.fn(),
|
||||
generateObject: vi.fn(async () => output) as KtxLlmRuntimePort['generateObject'],
|
||||
runAgentLoop: vi.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -125,28 +107,25 @@ function profile(): KtxRelationshipProfileArtifact {
|
|||
|
||||
describe('relationship LLM proposals', () => {
|
||||
it('maps valid structured FK proposals into review candidates with rationale evidence', async () => {
|
||||
const generateText = vi.fn(async () => ({
|
||||
output: {
|
||||
pkCandidates: [{ table: 'customers', column: 'id', confidence: 0.94, rationale: 'Unique customer identifier.' }],
|
||||
fkCandidates: [
|
||||
{
|
||||
fromTable: 'orders',
|
||||
fromColumn: 'buyer_ref',
|
||||
toTable: 'customers',
|
||||
toColumn: 'id',
|
||||
confidence: 0.88,
|
||||
rationale: 'Buyer reference values match customer identifiers.',
|
||||
},
|
||||
],
|
||||
},
|
||||
}));
|
||||
const runtime = llmRuntime({
|
||||
pkCandidates: [{ table: 'customers', column: 'id', confidence: 0.94, rationale: 'Unique customer identifier.' }],
|
||||
fkCandidates: [
|
||||
{
|
||||
fromTable: 'orders',
|
||||
fromColumn: 'buyer_ref',
|
||||
toTable: 'customers',
|
||||
toColumn: 'id',
|
||||
confidence: 0.88,
|
||||
rationale: 'Buyer reference values match customer identifiers.',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const result = await proposeKtxRelationshipCandidatesWithLlm({
|
||||
connectionId: 'warehouse',
|
||||
schema: schema(),
|
||||
profile: profile(),
|
||||
llmProvider: llmProvider(),
|
||||
generateText,
|
||||
llmRuntime: runtime,
|
||||
});
|
||||
|
||||
expect(result.summary).toBe('completed');
|
||||
|
|
@ -164,42 +143,27 @@ describe('relationship LLM proposals', () => {
|
|||
reasons: ['llm_proposal', 'llm_pk_proposal'],
|
||||
},
|
||||
});
|
||||
expect(generateText).toHaveBeenCalledWith(
|
||||
expect(runtime.generateObject).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
system: expect.objectContaining({
|
||||
role: 'system',
|
||||
content: expect.stringContaining('You are helping KTX review possible SQL relationships'),
|
||||
}),
|
||||
messages: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
role: 'user',
|
||||
content: expect.stringContaining('"tables"'),
|
||||
}),
|
||||
]),
|
||||
role: 'candidateExtraction',
|
||||
system: expect.stringContaining('You are helping KTX review possible SQL relationships'),
|
||||
prompt: expect.stringContaining('"tables"'),
|
||||
}),
|
||||
);
|
||||
const call = (
|
||||
generateText.mock.calls as unknown as Array<[{ messages: Array<{ role: string; content: string }> }]>
|
||||
)[0]?.[0];
|
||||
const userMessage = call?.messages.find((m) => m.role === 'user');
|
||||
expect(userMessage?.content).not.toContain('You are helping KTX review possible SQL relationships');
|
||||
expect(call?.messages.some((m) => m.role === 'system')).toBe(false);
|
||||
const call = vi.mocked(runtime.generateObject).mock.calls[0]?.[0];
|
||||
expect(call?.prompt).not.toContain('You are helping KTX review possible SQL relationships');
|
||||
});
|
||||
|
||||
it('skips deterministic providers without calling generateText', async () => {
|
||||
const generateText = vi.fn();
|
||||
|
||||
it('skips when no runtime is configured', async () => {
|
||||
const result = await proposeKtxRelationshipCandidatesWithLlm({
|
||||
connectionId: 'warehouse',
|
||||
schema: schema(),
|
||||
profile: profile(),
|
||||
llmProvider: llmProvider('deterministic'),
|
||||
generateText,
|
||||
llmRuntime: null,
|
||||
});
|
||||
|
||||
expect(result).toMatchObject({ candidates: [], llmCalls: 0, summary: 'skipped' });
|
||||
expect(result.warnings).toEqual([]);
|
||||
expect(generateText).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns recoverable warnings for invalid references and generation failures', async () => {
|
||||
|
|
@ -207,22 +171,19 @@ describe('relationship LLM proposals', () => {
|
|||
connectionId: 'warehouse',
|
||||
schema: schema(),
|
||||
profile: profile(),
|
||||
llmProvider: llmProvider(),
|
||||
generateText: vi.fn(async () => ({
|
||||
output: {
|
||||
pkCandidates: [],
|
||||
fkCandidates: [
|
||||
{
|
||||
fromTable: 'orders',
|
||||
fromColumn: 'missing_column',
|
||||
toTable: 'customers',
|
||||
toColumn: 'id',
|
||||
confidence: 0.7,
|
||||
rationale: 'Invalid source column.',
|
||||
},
|
||||
],
|
||||
},
|
||||
})),
|
||||
llmRuntime: llmRuntime({
|
||||
pkCandidates: [],
|
||||
fkCandidates: [
|
||||
{
|
||||
fromTable: 'orders',
|
||||
fromColumn: 'missing_column',
|
||||
toTable: 'customers',
|
||||
toColumn: 'id',
|
||||
confidence: 0.7,
|
||||
rationale: 'Invalid source column.',
|
||||
},
|
||||
],
|
||||
}),
|
||||
});
|
||||
expect(invalidReference.candidates).toEqual([]);
|
||||
expect(invalidReference.summary).toBe('completed');
|
||||
|
|
@ -235,10 +196,13 @@ describe('relationship LLM proposals', () => {
|
|||
connectionId: 'warehouse',
|
||||
schema: schema(),
|
||||
profile: profile(),
|
||||
llmProvider: llmProvider(),
|
||||
generateText: vi.fn(async () => {
|
||||
throw new Error('model unavailable');
|
||||
}),
|
||||
llmRuntime: {
|
||||
generateText: vi.fn(),
|
||||
generateObject: vi.fn(async () => {
|
||||
throw new Error('model unavailable');
|
||||
}),
|
||||
runAgentLoop: vi.fn(),
|
||||
},
|
||||
});
|
||||
expect(failed).toMatchObject({ candidates: [], llmCalls: 1, summary: 'failed' });
|
||||
expect(failed.warnings[0]).toMatchObject({
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
import type { KtxLlmProvider } from '@ktx/llm';
|
||||
import type { generateText } from 'ai';
|
||||
import { z } from 'zod';
|
||||
import { generateKtxObject } from '../llm/index.js';
|
||||
import { generateKtxObject, type KtxLlmRuntimePort } from '../llm/index.js';
|
||||
import type { KtxEnrichedColumn, KtxEnrichedSchema, KtxEnrichedTable } from './enrichment-types.js';
|
||||
import {
|
||||
normalizeKtxRelationshipName,
|
||||
|
|
@ -32,10 +30,6 @@ const relationshipLlmProposalSchema = z.object({
|
|||
});
|
||||
|
||||
type KtxRelationshipLlmProposalOutput = z.infer<typeof relationshipLlmProposalSchema>;
|
||||
type GenerateTextInput = Parameters<typeof generateText>[0];
|
||||
export type KtxRelationshipLlmProposalGenerateText = (
|
||||
input: GenerateTextInput,
|
||||
) => Promise<{ text?: string; output?: unknown }>;
|
||||
|
||||
export interface KtxRelationshipLlmProposalSettings {
|
||||
maxTablesPerBatch: number;
|
||||
|
|
@ -48,9 +42,8 @@ export interface ProposeKtxRelationshipCandidatesWithLlmInput {
|
|||
connectionId: string;
|
||||
schema: KtxEnrichedSchema;
|
||||
profile: KtxRelationshipProfileArtifact;
|
||||
llmProvider: KtxLlmProvider | null;
|
||||
llmRuntime: KtxLlmRuntimePort | null;
|
||||
settings?: Partial<KtxRelationshipLlmProposalSettings>;
|
||||
generateText?: KtxRelationshipLlmProposalGenerateText;
|
||||
}
|
||||
|
||||
export interface KtxRelationshipLlmProposalResult {
|
||||
|
|
@ -77,11 +70,6 @@ function clampConfidence(value: number): number {
|
|||
return Number(Math.max(0, Math.min(1, value)).toFixed(3));
|
||||
}
|
||||
|
||||
function modelIsDeterministic(llmProvider: KtxLlmProvider): boolean {
|
||||
const model = llmProvider.getModel('candidateExtraction');
|
||||
return (model as { provider?: string }).provider === 'deterministic';
|
||||
}
|
||||
|
||||
function findTable(schema: KtxEnrichedSchema, name: string): KtxEnrichedTable | null {
|
||||
const normalized = name.toLowerCase();
|
||||
return schema.tables.find((table) => table.ref.name.toLowerCase() === normalized) ?? null;
|
||||
|
|
@ -238,7 +226,7 @@ function generationFailureWarning(error: unknown): KtxScanWarning {
|
|||
export async function proposeKtxRelationshipCandidatesWithLlm(
|
||||
input: ProposeKtxRelationshipCandidatesWithLlmInput,
|
||||
): Promise<KtxRelationshipLlmProposalResult> {
|
||||
if (!input.llmProvider || modelIsDeterministic(input.llmProvider)) {
|
||||
if (!input.llmRuntime) {
|
||||
return { candidates: [], warnings: [], llmCalls: 0, summary: 'skipped' };
|
||||
}
|
||||
|
||||
|
|
@ -256,12 +244,11 @@ export async function proposeKtxRelationshipCandidatesWithLlm(
|
|||
KtxRelationshipLlmProposalOutput,
|
||||
typeof relationshipLlmProposalSchema
|
||||
>({
|
||||
llmProvider: input.llmProvider,
|
||||
runtime: input.llmRuntime,
|
||||
role: 'candidateExtraction',
|
||||
system,
|
||||
prompt,
|
||||
schema: relationshipLlmProposalSchema,
|
||||
generateText: input.generateText,
|
||||
});
|
||||
const output = relationshipLlmProposalSchema.parse(generated);
|
||||
const mapped = mapValidProposals(input.schema, output, settings);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue