feat: refine metadata catalog workflows
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { z } from "zod";
|
||||
import type { WorkspaceRegistry } from "../workspaces/registry.js";
|
||||
import type { MetadataGenerationModels, ResolvedMetadataGenerationModel } from "./metadata-generation-models.js";
|
||||
import type { ModelCompleter, ModelCompletionMessage } from "./model-completer.js";
|
||||
import type { ModelCompleter, ModelCompletionMessage, ModelCompletionResult } from "./model-completer.js";
|
||||
import {
|
||||
ModelCompletionCancelledError,
|
||||
ModelCompletionProviderError,
|
||||
@@ -38,6 +38,7 @@ const MAX_SAMPLE_FIELDS_PER_ROW = 4;
|
||||
const MAX_SAMPLE_COLUMNS = 4;
|
||||
const MAX_REPRESENTATIVE_VALUES_PER_REQUEST = 5;
|
||||
const MAX_TARGET_SAMPLE_JSON_BYTES = 8 * 1024;
|
||||
const MAX_COMPLETION_ATTEMPTS_PER_BATCH = 2;
|
||||
const generatedOutcomeSchema = z.object({
|
||||
targetId: z.uuid(),
|
||||
outcome: z.literal("generated"),
|
||||
@@ -55,7 +56,9 @@ const completionResponseSchema = z.object({
|
||||
results: z.array(outcomeSchema).min(1).max(MAX_TARGETS_PER_BATCH),
|
||||
}).strict();
|
||||
|
||||
class InvalidModelOutcomeError extends Error {}
|
||||
class InvalidModelJsonError extends Error {}
|
||||
class InvalidModelSchemaError extends Error {}
|
||||
class MissingModelTargetsError extends Error {}
|
||||
class DescriptionGenerationBatchError extends Error {
|
||||
constructor(
|
||||
readonly failure: unknown,
|
||||
@@ -143,6 +146,9 @@ interface DescriptionGenerationCounters {
|
||||
generated: number;
|
||||
nonGeneratable: number;
|
||||
failed: number;
|
||||
inputTokens: number;
|
||||
cacheReadTokens: number;
|
||||
outputTokens: number;
|
||||
consecutiveTechnicalFailures: number;
|
||||
}
|
||||
|
||||
@@ -152,6 +158,9 @@ function persistedCounters(counters: DescriptionGenerationCounters) {
|
||||
generated: counters.generated,
|
||||
nonGeneratable: counters.nonGeneratable,
|
||||
failed: counters.failed,
|
||||
inputTokens: counters.inputTokens,
|
||||
cacheReadTokens: counters.cacheReadTokens,
|
||||
outputTokens: counters.outputTokens,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -625,36 +634,47 @@ function messagesFor(
|
||||
}
|
||||
|
||||
function parseOutcomes(content: string, expectedTargetIds: readonly string[]): Map<string, ParsedOutcome> {
|
||||
const trimmed = content.trim();
|
||||
const fenced = /^```(?:json)?[ \t]*\r?\n([\s\S]*?)\r?\n```$/iu.exec(trimmed);
|
||||
let parsed: unknown;
|
||||
try {
|
||||
const trimmed = content.trim();
|
||||
const fenced = /^```(?:json)?[ \t]*\r?\n([\s\S]*?)\r?\n```$/iu.exec(trimmed);
|
||||
const outcomes = completionResponseSchema.parse(JSON.parse(fenced?.[1] ?? trimmed)).results;
|
||||
const expected = new Set(expectedTargetIds);
|
||||
if (outcomes.length !== expectedTargetIds.length || expected.size !== expectedTargetIds.length) {
|
||||
throw new InvalidModelOutcomeError();
|
||||
}
|
||||
const mapped = new Map<string, ParsedOutcome>();
|
||||
for (const outcome of outcomes) {
|
||||
if (!expected.has(outcome.targetId) || mapped.has(outcome.targetId)) {
|
||||
throw new InvalidModelOutcomeError();
|
||||
}
|
||||
mapped.set(outcome.targetId, outcome.outcome === "generated"
|
||||
? { ...outcome, description: outcome.description.trim() }
|
||||
: outcome);
|
||||
}
|
||||
if (mapped.size !== expected.size) throw new InvalidModelOutcomeError();
|
||||
return mapped;
|
||||
} catch (error) {
|
||||
if (error instanceof InvalidModelOutcomeError) throw error;
|
||||
throw new InvalidModelOutcomeError();
|
||||
parsed = JSON.parse(fenced?.[1] ?? trimmed);
|
||||
} catch {
|
||||
throw new InvalidModelJsonError();
|
||||
}
|
||||
|
||||
const response = completionResponseSchema.safeParse(parsed);
|
||||
if (!response.success) throw new InvalidModelSchemaError();
|
||||
|
||||
const outcomes = response.data.results;
|
||||
const expected = new Set(expectedTargetIds);
|
||||
if (outcomes.length !== expectedTargetIds.length || expected.size !== expectedTargetIds.length) {
|
||||
throw new MissingModelTargetsError();
|
||||
}
|
||||
const mapped = new Map<string, ParsedOutcome>();
|
||||
for (const outcome of outcomes) {
|
||||
if (!expected.has(outcome.targetId) || mapped.has(outcome.targetId)) {
|
||||
throw new MissingModelTargetsError();
|
||||
}
|
||||
mapped.set(outcome.targetId, outcome.outcome === "generated"
|
||||
? { ...outcome, description: outcome.description.trim() }
|
||||
: outcome);
|
||||
}
|
||||
if (mapped.size !== expected.size) throw new MissingModelTargetsError();
|
||||
return mapped;
|
||||
}
|
||||
|
||||
function safeFailure(error: unknown): string {
|
||||
if (error instanceof DescriptionGenerationFailureStreakError) return error.message;
|
||||
const failure = error instanceof DescriptionGenerationBatchError ? error.failure : error;
|
||||
if (failure instanceof ModelCompletionProviderError) return "The model provider request failed.";
|
||||
if (failure instanceof InvalidModelOutcomeError) return "The model response was invalid.";
|
||||
if (failure instanceof InvalidModelJsonError) return "The model response was not valid JSON.";
|
||||
if (failure instanceof InvalidModelSchemaError) {
|
||||
return "The model response did not match the required schema.";
|
||||
}
|
||||
if (failure instanceof MissingModelTargetsError) {
|
||||
return "The model response was missing one or more requested targets.";
|
||||
}
|
||||
return "Description generation failed.";
|
||||
}
|
||||
|
||||
@@ -907,6 +927,9 @@ export class DescriptionGenerationWorker {
|
||||
generated: 0,
|
||||
nonGeneratable: 0,
|
||||
failed: 0,
|
||||
inputTokens: 0,
|
||||
cacheReadTokens: 0,
|
||||
outputTokens: 0,
|
||||
consecutiveTechnicalFailures: 0,
|
||||
};
|
||||
await this.processTargets(run, database, plan.columnTargets, model, counters, signal);
|
||||
@@ -973,20 +996,41 @@ export class DescriptionGenerationWorker {
|
||||
);
|
||||
}
|
||||
throwIfCancelled(signal);
|
||||
let outcomes: Map<string, ParsedOutcome>;
|
||||
try {
|
||||
const content = await this.completer.complete({
|
||||
model,
|
||||
messages: messagesFor(database, batch, run.language, sourceSamples),
|
||||
signal,
|
||||
});
|
||||
throwIfCancelled(signal);
|
||||
outcomes = parseOutcomes(content, batch.map((target) => (
|
||||
target.kind === "column" ? target.column.id : target.table.id
|
||||
)));
|
||||
} catch (error) {
|
||||
if (error instanceof ModelCompletionCancelledError) throw error;
|
||||
const batchError = new DescriptionGenerationBatchError(error, batch.map(failureTarget));
|
||||
const expectedTargetIds = batch.map((target) => (
|
||||
target.kind === "column" ? target.column.id : target.table.id
|
||||
));
|
||||
const messages = messagesFor(database, batch, run.language, sourceSamples);
|
||||
let outcomes: Map<string, ParsedOutcome> | undefined;
|
||||
let terminalFailure: unknown;
|
||||
for (let attempt = 1; attempt <= MAX_COMPLETION_ATTEMPTS_PER_BATCH; attempt += 1) {
|
||||
try {
|
||||
const completion = await this.completer.complete({ model, messages, signal });
|
||||
const result: ModelCompletionResult = typeof completion === "string"
|
||||
? { content: completion, usage: { input: 0, cacheRead: 0, output: 0 } }
|
||||
: completion;
|
||||
counters.inputTokens += result.usage.input;
|
||||
counters.cacheReadTokens += result.usage.cacheRead;
|
||||
counters.outputTokens += result.usage.output;
|
||||
throwIfCancelled(signal);
|
||||
outcomes = parseOutcomes(result.content, expectedTargetIds);
|
||||
break;
|
||||
} catch (error) {
|
||||
if (error instanceof ModelCompletionCancelledError) throw error;
|
||||
terminalFailure = error;
|
||||
if (attempt < MAX_COMPLETION_ATTEMPTS_PER_BATCH) {
|
||||
await this.appendEvent(
|
||||
run.id,
|
||||
"warning",
|
||||
`${safeFailure(error)} Retrying batch (attempt ${attempt + 1} of ${MAX_COMPLETION_ATTEMPTS_PER_BATCH}).`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!outcomes) {
|
||||
const batchError = new DescriptionGenerationBatchError(
|
||||
terminalFailure,
|
||||
batch.map(failureTarget),
|
||||
);
|
||||
counters.processed += batch.length;
|
||||
counters.failed += batch.length;
|
||||
counters.consecutiveTechnicalFailures += 1;
|
||||
@@ -1009,7 +1053,10 @@ export class DescriptionGenerationWorker {
|
||||
const targetId = target.kind === "column" ? target.column.id : target.table.id;
|
||||
const outcome = outcomes.get(targetId);
|
||||
if (!outcome) {
|
||||
throw new DescriptionGenerationBatchError(new InvalidModelOutcomeError(), [failureTarget(target)]);
|
||||
throw new DescriptionGenerationBatchError(
|
||||
new MissingModelTargetsError(),
|
||||
[failureTarget(target)],
|
||||
);
|
||||
}
|
||||
const generatedDescription = outcome.outcome === "generated"
|
||||
? outcome.description
|
||||
|
||||
Reference in New Issue
Block a user