mirror of
https://github.com/Kaelio/ktx.git
synced 2026-07-04 10:52:13 +02:00
fix(context): make scan description generation resilient and quiet
A transient sampleTable failure during ingest used to take out every table in a connection: generateTableDescription returned a hardcoded 'Table not found' string into descriptions.ai, and KtxDescriptionGenerator was constructed without a logger, so the failure left no trail anywhere. - sampleTable / sampleColumn calls retry 3x with 200/400/800ms backoff, honouring KtxScanContext.signal via a new KtxAbortedError. - On retry exhaustion or missing capability, table generation falls back to a metadata-only prompt built from column name / native type / comment / rawDescriptions. The column path follows the same rule -- call the LLM when any of samples or rawDescriptions are available; skip only when both are absent. - Logger is now threaded from KtxScanContext into the generator. Failures emit structured KtxScanWarning entries (new description_fallback_used code, plus existing sampling_failed / enrichment_failed / connector_capability_missing). ktx scan groups warnings by code so a batch of identical failures collapses to one summary line plus sample. - Returns null on failure instead of the 'Table not found' sentinel; the manifest writer's existing guard already skips empty descriptions, so schema YAML no longer carries misleading text. SCAN_MANAGED_DESCRIPTION_KEYS already strips stale 'ai' on merge, so existing YAML clears on next run. Also suppress AI SDK v6 'system in messages' warning: pull system messages out of KtxMessageBuilder.wrapSimple's output via a new splitKtxSystemMessages helper and pass them top-level to generateText (preserves cacheControl providerOptions on the SystemModelMessage). Agent-runner's local splitSystemPromptMessages dedupes onto the shared helper.
This commit is contained in:
parent
f561bfa850
commit
81b67a96cb
14 changed files with 761 additions and 82 deletions
|
|
@ -1,6 +1,7 @@
|
|||
export { createKtxEmbeddingProvider } from './embedding-provider.js';
|
||||
export { runKtxEmbeddingHealthCheck } from './embedding-health.js';
|
||||
export { KtxMessageBuilder } from './message-builder.js';
|
||||
export { KtxMessageBuilder, splitKtxSystemMessages } from './message-builder.js';
|
||||
export type { KtxSplitSystemMessagesResult } from './message-builder.js';
|
||||
export type { KtxEmbeddingHealthCheckOptions, KtxEmbeddingHealthCheckResult } from './embedding-health.js';
|
||||
export type { KtxEmbeddingProviderDeps } from './embedding-provider.js';
|
||||
export type { KtxLlmHealthCheckDeps, KtxLlmHealthCheckOptions, KtxLlmHealthCheckResult } from './model-health.js';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { ModelMessage } from 'ai';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { KtxMessageBuilder } from './message-builder.js';
|
||||
import { KtxMessageBuilder, splitKtxSystemMessages } from './message-builder.js';
|
||||
import { createKtxLlmProvider } from './model-provider.js';
|
||||
|
||||
function makeBuilder(overrides: Parameters<typeof createKtxLlmProvider>[0]['promptCaching'] = {}) {
|
||||
|
|
@ -111,3 +111,36 @@ describe('KtxMessageBuilder.build', () => {
|
|||
expect((out.tools.z as { providerOptions: any }).providerOptions.anthropic.cacheControl.ttl).toBe('5m');
|
||||
});
|
||||
});
|
||||
|
||||
describe('splitKtxSystemMessages', () => {
|
||||
it('returns undefined system when no system messages are present', () => {
|
||||
const split = splitKtxSystemMessages([
|
||||
{ role: 'user', content: 'hello' },
|
||||
{ role: 'assistant', content: 'hi' },
|
||||
]);
|
||||
expect(split.system).toBeUndefined();
|
||||
expect(split.messages).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('returns a single system message object when one system message is present, preserving providerOptions', () => {
|
||||
const systemMessage = {
|
||||
role: 'system' as const,
|
||||
content: 'You are helpful.',
|
||||
providerOptions: { anthropic: { cacheControl: { type: 'ephemeral' } } },
|
||||
};
|
||||
const split = splitKtxSystemMessages([systemMessage, { role: 'user', content: 'hello' }]);
|
||||
expect(split.system).toBe(systemMessage);
|
||||
expect(split.messages).toEqual([{ role: 'user', content: 'hello' }]);
|
||||
});
|
||||
|
||||
it('returns an array of system messages when multiple are present, in order', () => {
|
||||
const split = splitKtxSystemMessages([
|
||||
{ role: 'system', content: 'cached' },
|
||||
{ role: 'system', content: 'fresh' },
|
||||
{ role: 'user', content: 'hello' },
|
||||
]);
|
||||
expect(Array.isArray(split.system)).toBe(true);
|
||||
expect(split.system).toHaveLength(2);
|
||||
expect(split.messages).toEqual([{ role: 'user', content: 'hello' }]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,29 @@
|
|||
import type { LanguageModel, ModelMessage, ToolSet } from 'ai';
|
||||
import type { LanguageModel, ModelMessage, SystemModelMessage, ToolSet } from 'ai';
|
||||
import { isAnthropicProtocolModel } from './model-provider.js';
|
||||
import type { KtxLlmProvider, KtxPromptCacheTtl, KtxPromptParts } from './types.js';
|
||||
|
||||
export interface KtxSplitSystemMessagesResult {
|
||||
system: SystemModelMessage | SystemModelMessage[] | undefined;
|
||||
messages: ModelMessage[];
|
||||
}
|
||||
|
||||
export function splitKtxSystemMessages(messages: readonly ModelMessage[]): KtxSplitSystemMessagesResult {
|
||||
const systemMessages: SystemModelMessage[] = [];
|
||||
const otherMessages: ModelMessage[] = [];
|
||||
for (const message of messages) {
|
||||
if (message.role === 'system') {
|
||||
systemMessages.push(message);
|
||||
} else {
|
||||
otherMessages.push(message);
|
||||
}
|
||||
}
|
||||
return {
|
||||
system:
|
||||
systemMessages.length === 0 ? undefined : systemMessages.length === 1 ? systemMessages[0] : systemMessages,
|
||||
messages: otherMessages,
|
||||
};
|
||||
}
|
||||
|
||||
type ToolMap = ToolSet | Record<string, Record<string, unknown>>;
|
||||
|
||||
interface KtxMessageBuilderOptions {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue