feat(recipe-studio): introduce validator blocks for code validation with Python and SQL engines
This commit is contained in:
parent
891739a56a
commit
761c84f92b
31 changed files with 766 additions and 107 deletions
|
|
@ -17,7 +17,12 @@ import {
|
|||
TagsIcon,
|
||||
UserAccountIcon,
|
||||
} from "@hugeicons/core-free-icons";
|
||||
import type { LlmType, NodeConfig, SamplerType, SeedSourceType } from "../types";
|
||||
import type {
|
||||
LlmType,
|
||||
NodeConfig,
|
||||
SamplerType,
|
||||
SeedSourceType,
|
||||
} from "../types";
|
||||
import {
|
||||
makeExpressionConfig,
|
||||
makeLlmConfig,
|
||||
|
|
@ -26,12 +31,21 @@ import {
|
|||
makeModelProviderConfig,
|
||||
makeSamplerConfig,
|
||||
makeSeedConfig,
|
||||
makeValidatorConfig,
|
||||
} from "../utils";
|
||||
|
||||
export type BlockKind = "sampler" | "llm" | "expression" | "seed" | "note";
|
||||
export type BlockKind =
|
||||
| "sampler"
|
||||
| "llm"
|
||||
| "validator"
|
||||
| "expression"
|
||||
| "seed"
|
||||
| "note";
|
||||
export type BlockType =
|
||||
| SamplerType
|
||||
| LlmType
|
||||
| "validator_python"
|
||||
| "validator_sql"
|
||||
| "expression"
|
||||
| "markdown_note"
|
||||
| "seed"
|
||||
|
|
@ -65,6 +79,7 @@ export type BlockDialogKey =
|
|||
| "uuid"
|
||||
| "person"
|
||||
| "llm"
|
||||
| "validator"
|
||||
| "model_provider"
|
||||
| "model_config"
|
||||
| "expression";
|
||||
|
|
@ -98,6 +113,12 @@ export const BLOCK_GROUPS: BlockGroup[] = [
|
|||
description: "Generation, providers, and model aliases.",
|
||||
icon: PencilEdit02Icon,
|
||||
},
|
||||
{
|
||||
kind: "validator",
|
||||
title: "Validators",
|
||||
description: "Validate generated code outputs with built-in engines.",
|
||||
icon: Shield02Icon,
|
||||
},
|
||||
{
|
||||
kind: "expression",
|
||||
title: "Expression",
|
||||
|
|
@ -275,6 +296,25 @@ const BLOCK_DEFINITIONS: BlockDefinition[] = [
|
|||
dialogKey: "model_config",
|
||||
createConfig: (id, existing) => makeModelConfig(id, existing),
|
||||
},
|
||||
{
|
||||
kind: "validator",
|
||||
type: "validator_python",
|
||||
title: "Python Validator",
|
||||
description: "Validate Python code columns.",
|
||||
icon: Shield02Icon,
|
||||
dialogKey: "validator",
|
||||
createConfig: (id, existing) => makeValidatorConfig(id, "python", existing),
|
||||
},
|
||||
{
|
||||
kind: "validator",
|
||||
type: "validator_sql",
|
||||
title: "SQL Validator",
|
||||
description: "Validate SQL code columns.",
|
||||
icon: Shield02Icon,
|
||||
dialogKey: "validator",
|
||||
createConfig: (id, existing) =>
|
||||
makeValidatorConfig(id, "sql:sqlite", existing),
|
||||
},
|
||||
{
|
||||
kind: "expression",
|
||||
type: "expression",
|
||||
|
|
@ -331,6 +371,13 @@ export function getBlockDefinitionForConfig(
|
|||
if (config.kind === "llm") {
|
||||
return getBlockDefinition("llm", config.llm_type);
|
||||
}
|
||||
if (config.kind === "validator") {
|
||||
const isSql = config.code_lang.startsWith("sql:");
|
||||
return getBlockDefinition(
|
||||
"validator",
|
||||
isSql ? "validator_sql" : "validator_python",
|
||||
);
|
||||
}
|
||||
if (config.kind === "model_provider") {
|
||||
return getBlockDefinition("llm", "model_provider");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import { TimedeltaDialog } from "../dialogs/samplers/timedelta-dialog";
|
|||
import { UniformDialog } from "../dialogs/samplers/uniform-dialog";
|
||||
import { UuidDialog } from "../dialogs/samplers/uuid-dialog";
|
||||
import { MarkdownNoteDialog } from "../dialogs/markdown-note/markdown-note-dialog";
|
||||
import { ValidatorDialog } from "../dialogs/validators/validator-dialog";
|
||||
|
||||
export function renderBlockDialog(
|
||||
config: NodeConfig | null,
|
||||
|
|
@ -109,6 +110,10 @@ export function renderBlockDialog(
|
|||
return config.kind === "expression" ? (
|
||||
<ExpressionDialog config={config} onUpdate={update} />
|
||||
) : null;
|
||||
case "validator":
|
||||
return config.kind === "validator" ? (
|
||||
<ValidatorDialog config={config} onUpdate={update} />
|
||||
) : null;
|
||||
case "markdown_note":
|
||||
return config.kind === "markdown_note" ? (
|
||||
<MarkdownNoteDialog config={config} onUpdate={update} />
|
||||
|
|
|
|||
|
|
@ -39,10 +39,17 @@ type SheetView =
|
|||
| "sampler"
|
||||
| "seed"
|
||||
| "llm"
|
||||
| "validator"
|
||||
| "expression"
|
||||
| "note"
|
||||
| "processor";
|
||||
type SheetKind = "sampler" | "seed" | "llm" | "expression" | "note";
|
||||
type SheetKind =
|
||||
| "sampler"
|
||||
| "seed"
|
||||
| "llm"
|
||||
| "validator"
|
||||
| "expression"
|
||||
| "note";
|
||||
type RootSheetView = Exclude<SheetView, "root">;
|
||||
type RootGroup = {
|
||||
kind: RootSheetView;
|
||||
|
|
@ -63,6 +70,7 @@ type BlockSheetProps = {
|
|||
onAddModelProvider: () => void;
|
||||
onAddModelConfig: () => void;
|
||||
onAddExpression: () => void;
|
||||
onAddValidator: (type: "validator_python" | "validator_sql") => void;
|
||||
onAddMarkdownNote: () => void;
|
||||
onOpenProcessors: () => void;
|
||||
copied: boolean;
|
||||
|
|
@ -89,6 +97,9 @@ function getSheetTitle(sheetView: SheetView): string {
|
|||
if (sheetView === "expression") {
|
||||
return "Expression blocks";
|
||||
}
|
||||
if (sheetView === "validator") {
|
||||
return "Validator blocks";
|
||||
}
|
||||
if (sheetView === "note") {
|
||||
return "Note blocks";
|
||||
}
|
||||
|
|
@ -103,6 +114,7 @@ const VIEW_KIND: Record<SheetView, SheetKind | null> = {
|
|||
sampler: "sampler",
|
||||
seed: "seed",
|
||||
llm: "llm",
|
||||
validator: "validator",
|
||||
expression: "expression",
|
||||
note: "note",
|
||||
processor: null,
|
||||
|
|
@ -121,6 +133,7 @@ const SEARCHABLE_KINDS: SheetKind[] = [
|
|||
"sampler",
|
||||
"seed",
|
||||
"llm",
|
||||
"validator",
|
||||
"expression",
|
||||
"note",
|
||||
];
|
||||
|
|
@ -193,6 +206,7 @@ export function BlockSheet({
|
|||
onAddModelProvider,
|
||||
onAddModelConfig,
|
||||
onAddExpression,
|
||||
onAddValidator,
|
||||
onAddMarkdownNote,
|
||||
onOpenProcessors,
|
||||
copied,
|
||||
|
|
@ -302,6 +316,10 @@ export function BlockSheet({
|
|||
onAddLlm(type as LlmType);
|
||||
return;
|
||||
}
|
||||
if (kind === "validator") {
|
||||
onAddValidator(type as "validator_python" | "validator_sql");
|
||||
return;
|
||||
}
|
||||
if (kind === "expression") {
|
||||
onAddExpression();
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -81,6 +81,9 @@ const NODE_META = {
|
|||
llm: {
|
||||
tone: "bg-sky-50 text-sky-600 border-sky-100",
|
||||
},
|
||||
validator: {
|
||||
tone: "bg-rose-50 text-rose-600 border-rose-100",
|
||||
},
|
||||
expression: {
|
||||
tone: "bg-indigo-50 text-indigo-600 border-indigo-100",
|
||||
},
|
||||
|
|
@ -130,6 +133,9 @@ function resolveNodeIcon(
|
|||
if (kind === "llm" && blockType in LLM_ICONS) {
|
||||
return LLM_ICONS[blockType as LlmType];
|
||||
}
|
||||
if (kind === "validator") {
|
||||
return Shield02Icon;
|
||||
}
|
||||
if (kind === "expression") {
|
||||
return FunctionIcon;
|
||||
}
|
||||
|
|
@ -200,6 +206,14 @@ function getConfigSummary(config: NodeConfig | undefined): string {
|
|||
return "Prompt/system via linked input nodes";
|
||||
}
|
||||
|
||||
if (config.kind === "validator") {
|
||||
const target = config.target_columns[0]?.trim();
|
||||
if (target) {
|
||||
return `Target: ${target}`;
|
||||
}
|
||||
return "Pick LLM code target";
|
||||
}
|
||||
|
||||
if (config.kind === "seed") {
|
||||
const seedSourceType = config.seed_source_type ?? "hf";
|
||||
if (seedSourceType === "hf" && config.hf_repo_id.trim()) {
|
||||
|
|
@ -333,12 +347,15 @@ function RecipeGraphNodeBase({
|
|||
|
||||
const showDataHandles =
|
||||
data.kind === "llm" ||
|
||||
data.kind === "validator" ||
|
||||
data.kind === "expression" ||
|
||||
data.kind === "sampler" ||
|
||||
data.kind === "seed";
|
||||
const showSemanticIn = data.kind === "model_config";
|
||||
const showSemanticIn = data.kind === "model_config" || data.kind === "validator";
|
||||
const showSemanticOut =
|
||||
data.kind === "model_config" || data.kind === "model_provider";
|
||||
data.kind === "model_config" ||
|
||||
data.kind === "model_provider" ||
|
||||
data.kind === "validator";
|
||||
const summary = getConfigSummary(config);
|
||||
const nodeBody = renderNodeBody(config, summary, updateConfig);
|
||||
const canShowLlmAux =
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ export function ConfigDialog({
|
|||
const showDropToggle =
|
||||
config?.kind === "sampler" ||
|
||||
config?.kind === "llm" ||
|
||||
config?.kind === "validator" ||
|
||||
config?.kind === "expression" ||
|
||||
(config?.kind === "seed" &&
|
||||
(config.seed_source_type ?? "hf") === "unstructured");
|
||||
|
|
|
|||
|
|
@ -105,6 +105,9 @@ export function LlmGeneralTab({
|
|||
() => Object.values(configs).find((item) => item.kind === "seed"),
|
||||
[configs],
|
||||
);
|
||||
const hasHfSeed = Boolean(
|
||||
seedConfig && (seedConfig.seed_source_type ?? "hf") === "hf",
|
||||
);
|
||||
const seedColumns = seedConfig?.seed_columns ?? [];
|
||||
const seedPreviewRows = seedConfig?.seed_preview_rows ?? [];
|
||||
const imageColumnOptions = useMemo(() => {
|
||||
|
|
@ -133,7 +136,6 @@ export function LlmGeneralTab({
|
|||
column_name: "",
|
||||
};
|
||||
const imageContextToggleId = `${config.id}-image-context-enabled`;
|
||||
const imageContextColumnId = `${config.id}-image-context-column`;
|
||||
const traceModeId = `${config.id}-trace-mode`;
|
||||
const reasoningToggleId = `${config.id}-reasoning-content`;
|
||||
const [advancedOpen, setAdvancedOpen] = useState(false);
|
||||
|
|
@ -237,20 +239,13 @@ export function LlmGeneralTab({
|
|||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-3 rounded-2xl border border-border/60 px-3 py-3">
|
||||
{hasHfSeed && (
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<FieldLabel
|
||||
label="Use image context"
|
||||
htmlFor={imageContextToggleId}
|
||||
hint="Attach one seed image column to this LLM call."
|
||||
/>
|
||||
{imageColumnOptions.length > 0 && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Suggested image columns: {imageColumnOptions.join(", ")}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<FieldLabel
|
||||
label="Use image context"
|
||||
htmlFor={imageContextToggleId}
|
||||
hint="Attach one seed image column to this LLM call."
|
||||
/>
|
||||
<Switch
|
||||
id={imageContextToggleId}
|
||||
checked={imageContext.enabled}
|
||||
|
|
@ -269,37 +264,44 @@ export function LlmGeneralTab({
|
|||
}}
|
||||
/>
|
||||
</div>
|
||||
{imageContext.enabled && (
|
||||
<div className="grid gap-2">
|
||||
<FieldLabel
|
||||
label="Image column"
|
||||
htmlFor={imageContextColumnId}
|
||||
hint="Seed column containing image values."
|
||||
/>
|
||||
<Select
|
||||
value={imageContext.column_name || ""}
|
||||
onValueChange={(value) =>
|
||||
onUpdate({
|
||||
image_context: {
|
||||
...imageContext,
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
column_name: value,
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="nodrag w-full" id={imageContextColumnId}>
|
||||
<SelectValue placeholder="Select image column" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{imageColumnOptions.map((columnName) => (
|
||||
<SelectItem key={columnName} value={columnName}>
|
||||
{columnName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
{config.llm_type === "structured" && (
|
||||
<div className="grid gap-2">
|
||||
<FieldLabel
|
||||
label="Output format (JSON schema)"
|
||||
htmlFor={outputFormatId}
|
||||
hint="Schema used to constrain structured JSON output."
|
||||
/>
|
||||
<Textarea
|
||||
id={outputFormatId}
|
||||
className="corner-squircle nodrag"
|
||||
value={config.output_format ?? ""}
|
||||
onChange={(event) =>
|
||||
onUpdate({ output_format: event.target.value })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="grid gap-2">
|
||||
<FieldLabel
|
||||
label="System prompt (optional)"
|
||||
htmlFor={systemPromptId}
|
||||
hint="Global behavior instructions prepended before prompt."
|
||||
/>
|
||||
<Textarea
|
||||
id={systemPromptId}
|
||||
className="corner-squircle nodrag max-h-[450px] overflow-auto"
|
||||
aria-invalid={invalidSystemRefs.length > 0}
|
||||
value={config.system_prompt}
|
||||
onChange={(event) => onUpdate({ system_prompt: event.target.value })}
|
||||
/>
|
||||
{invalidSystemRefs.length > 0 && (
|
||||
<p className="text-xs text-destructive">
|
||||
Unknown reference: {invalidSystemText}
|
||||
{invalidSystemRefs.length > 3
|
||||
? ` +${invalidSystemRefs.length - 3} more`
|
||||
: ""}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<Collapsible open={advancedOpen} onOpenChange={setAdvancedOpen}>
|
||||
|
|
@ -340,14 +342,12 @@ export function LlmGeneralTab({
|
|||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-3 rounded-2xl border border-border/60 px-3 py-3">
|
||||
<div>
|
||||
<FieldLabel
|
||||
label="Extract reasoning content"
|
||||
htmlFor={reasoningToggleId}
|
||||
hint="Adds {column}__reasoning_content when model provides it."
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<FieldLabel
|
||||
label="Extract reasoning content"
|
||||
htmlFor={reasoningToggleId}
|
||||
hint="Adds {column}__reasoning_content when model provides it."
|
||||
/>
|
||||
<Switch
|
||||
id={reasoningToggleId}
|
||||
checked={config.extract_reasoning_content === true}
|
||||
|
|
@ -361,45 +361,6 @@ export function LlmGeneralTab({
|
|||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
{config.llm_type === "structured" && (
|
||||
<div className="grid gap-2">
|
||||
<FieldLabel
|
||||
label="Output format (JSON schema)"
|
||||
htmlFor={outputFormatId}
|
||||
hint="Schema used to constrain structured JSON output."
|
||||
/>
|
||||
<Textarea
|
||||
id={outputFormatId}
|
||||
className="corner-squircle nodrag"
|
||||
value={config.output_format ?? ""}
|
||||
onChange={(event) =>
|
||||
onUpdate({ output_format: event.target.value })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="grid gap-2">
|
||||
<FieldLabel
|
||||
label="System prompt (optional)"
|
||||
htmlFor={systemPromptId}
|
||||
hint="Global behavior instructions prepended before prompt."
|
||||
/>
|
||||
<Textarea
|
||||
id={systemPromptId}
|
||||
className="corner-squircle nodrag max-h-[450px] overflow-auto"
|
||||
aria-invalid={invalidSystemRefs.length > 0}
|
||||
value={config.system_prompt}
|
||||
onChange={(event) => onUpdate({ system_prompt: event.target.value })}
|
||||
/>
|
||||
{invalidSystemRefs.length > 0 && (
|
||||
<p className="text-xs text-destructive">
|
||||
Unknown reference: {invalidSystemText}
|
||||
{invalidSystemRefs.length > 3
|
||||
? ` +${invalidSystemRefs.length - 3} more`
|
||||
: ""}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,126 @@
|
|||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { type ReactElement, useMemo } from "react";
|
||||
import { useRecipeStudioStore } from "../../stores/recipe-studio";
|
||||
import type { ValidatorConfig } from "../../types";
|
||||
import { isValidatorCodeLang } from "../../utils/validators/code-lang";
|
||||
import { FieldLabel } from "../shared/field-label";
|
||||
import { NameField } from "../shared/name-field";
|
||||
|
||||
type ValidatorDialogProps = {
|
||||
config: ValidatorConfig;
|
||||
onUpdate: (patch: Partial<ValidatorConfig>) => void;
|
||||
};
|
||||
|
||||
const NONE_VALUE = "__none__";
|
||||
|
||||
export function ValidatorDialog({
|
||||
config,
|
||||
onUpdate,
|
||||
}: ValidatorDialogProps): ReactElement {
|
||||
const configs = useRecipeStudioStore((state) => state.configs);
|
||||
const targetColumnId = `${config.id}-target-column`;
|
||||
const batchSizeId = `${config.id}-batch-size`;
|
||||
const codeOptions = useMemo(
|
||||
() =>
|
||||
Object.values(configs)
|
||||
.flatMap((item) => {
|
||||
if (!(item.kind === "llm" && item.llm_type === "code")) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
{
|
||||
name: item.name,
|
||||
codeLang: item.code_lang?.trim() ?? "",
|
||||
},
|
||||
];
|
||||
})
|
||||
.filter((item) => item.name.trim())
|
||||
.sort((a, b) => a.name.localeCompare(b.name)),
|
||||
[configs],
|
||||
);
|
||||
const currentTarget = config.target_columns[0] ?? "";
|
||||
const engineLabel = config.code_lang.startsWith("sql:") ? "SQL" : "Python";
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<NameField
|
||||
value={config.name}
|
||||
onChange={(value) => onUpdate({ name: value })}
|
||||
/>
|
||||
<div className="grid gap-2">
|
||||
<FieldLabel
|
||||
label="Validator engine"
|
||||
hint="Built-in validator type for this block."
|
||||
/>
|
||||
<Input value={engineLabel} disabled={true} className="nodrag" />
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<FieldLabel
|
||||
label="Target code column"
|
||||
htmlFor={targetColumnId}
|
||||
hint="Must reference an LLM Code block."
|
||||
/>
|
||||
<Select
|
||||
value={currentTarget || NONE_VALUE}
|
||||
onValueChange={(value) => {
|
||||
if (value === NONE_VALUE) {
|
||||
onUpdate({
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
target_columns: [],
|
||||
});
|
||||
return;
|
||||
}
|
||||
const targetConfig = codeOptions.find((item) => item.name === value);
|
||||
const nextCodeLang = targetConfig?.codeLang?.trim();
|
||||
onUpdate({
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
target_columns: [value],
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
code_lang:
|
||||
nextCodeLang && isValidatorCodeLang(nextCodeLang)
|
||||
? nextCodeLang
|
||||
: config.code_lang,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="nodrag w-full" id={targetColumnId}>
|
||||
<SelectValue placeholder="Select code column" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={NONE_VALUE}>None</SelectItem>
|
||||
{codeOptions.map((item) => (
|
||||
<SelectItem key={item.name} value={item.name}>
|
||||
{item.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{codeOptions.length === 0 && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Add an LLM Code block first.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<FieldLabel
|
||||
label="Batch size"
|
||||
htmlFor={batchSizeId}
|
||||
hint="Records per validation batch."
|
||||
/>
|
||||
<Input
|
||||
id={batchSizeId}
|
||||
className="nodrag"
|
||||
value={config.batch_size}
|
||||
onChange={(event) => onUpdate({ batch_size: event.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -28,6 +28,7 @@ const SUPPORTED_DRAG_KINDS: RecipeBlockDragPayload["kind"][] = [
|
|||
"sampler",
|
||||
"seed",
|
||||
"llm",
|
||||
"validator",
|
||||
"expression",
|
||||
"note",
|
||||
];
|
||||
|
|
@ -67,6 +68,11 @@ type UseRecipeEditorGraphArgs = {
|
|||
addModelProviderNode: (position?: XYPosition, openDialog?: boolean) => void;
|
||||
addModelConfigNode: (position?: XYPosition, openDialog?: boolean) => void;
|
||||
addExpressionNode: (position?: XYPosition, openDialog?: boolean) => void;
|
||||
addValidatorNode: (
|
||||
type: "validator_python" | "validator_sql",
|
||||
position?: XYPosition,
|
||||
openDialog?: boolean,
|
||||
) => void;
|
||||
addMarkdownNoteNode: (position?: XYPosition, openDialog?: boolean) => void;
|
||||
};
|
||||
|
||||
|
|
@ -85,6 +91,9 @@ type UseRecipeEditorGraphResult = {
|
|||
handleAddModelProviderFromSheet: () => void;
|
||||
handleAddModelConfigFromSheet: () => void;
|
||||
handleAddExpressionFromSheet: () => void;
|
||||
handleAddValidatorFromSheet: (
|
||||
type: "validator_python" | "validator_sql",
|
||||
) => void;
|
||||
handleAddMarkdownNoteFromSheet: () => void;
|
||||
};
|
||||
|
||||
|
|
@ -105,6 +114,7 @@ export function useRecipeEditorGraph({
|
|||
addModelProviderNode,
|
||||
addModelConfigNode,
|
||||
addExpressionNode,
|
||||
addValidatorNode,
|
||||
addMarkdownNoteNode,
|
||||
}: UseRecipeEditorGraphArgs): UseRecipeEditorGraphResult {
|
||||
const baseNodeIds = useMemo(() => new Set(nodes.map((node) => node.id)), [nodes]);
|
||||
|
|
@ -201,6 +211,14 @@ export function useRecipeEditorGraph({
|
|||
addExpressionNode(position, false);
|
||||
return;
|
||||
}
|
||||
if (payload.kind === "validator") {
|
||||
addValidatorNode(
|
||||
payload.type as "validator_python" | "validator_sql",
|
||||
position,
|
||||
false,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (payload.kind === "note") {
|
||||
addMarkdownNoteNode(position, false);
|
||||
return;
|
||||
|
|
@ -223,6 +241,7 @@ export function useRecipeEditorGraph({
|
|||
addModelProviderNode,
|
||||
addSamplerNode,
|
||||
addSeedNode,
|
||||
addValidatorNode,
|
||||
reactFlowInstance,
|
||||
],
|
||||
);
|
||||
|
|
@ -271,6 +290,13 @@ export function useRecipeEditorGraph({
|
|||
addExpressionNode(getViewportCenterPosition());
|
||||
}, [addExpressionNode, getViewportCenterPosition]);
|
||||
|
||||
const handleAddValidatorFromSheet = useCallback(
|
||||
(type: "validator_python" | "validator_sql") => {
|
||||
addValidatorNode(type, getViewportCenterPosition());
|
||||
},
|
||||
[addValidatorNode, getViewportCenterPosition],
|
||||
);
|
||||
|
||||
const handleAddMarkdownNoteFromSheet = useCallback(() => {
|
||||
addMarkdownNoteNode(getViewportCenterPosition());
|
||||
}, [addMarkdownNoteNode, getViewportCenterPosition]);
|
||||
|
|
@ -288,6 +314,7 @@ export function useRecipeEditorGraph({
|
|||
handleAddModelProviderFromSheet,
|
||||
handleAddModelConfigFromSheet,
|
||||
handleAddExpressionFromSheet,
|
||||
handleAddValidatorFromSheet,
|
||||
handleAddMarkdownNoteFromSheet,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,6 +66,9 @@ function resolveExecutionColumnIcon(config: NodeConfig | null): IconType {
|
|||
if (config.kind === "expression") {
|
||||
return FunctionIcon;
|
||||
}
|
||||
if (config.kind === "validator") {
|
||||
return Shield02Icon;
|
||||
}
|
||||
if (config.kind === "seed") {
|
||||
return Plant01Icon;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -100,6 +100,7 @@ export function RecipeStudioPage({
|
|||
addModelProviderNode,
|
||||
addModelConfigNode,
|
||||
addExpressionNode,
|
||||
addValidatorNode,
|
||||
addMarkdownNoteNode,
|
||||
selectConfig,
|
||||
openConfig,
|
||||
|
|
@ -136,6 +137,7 @@ export function RecipeStudioPage({
|
|||
addModelProviderNode: state.addModelProviderNode,
|
||||
addModelConfigNode: state.addModelConfigNode,
|
||||
addExpressionNode: state.addExpressionNode,
|
||||
addValidatorNode: state.addValidatorNode,
|
||||
addMarkdownNoteNode: state.addMarkdownNoteNode,
|
||||
selectConfig: state.selectConfig,
|
||||
openConfig: state.openConfig,
|
||||
|
|
@ -186,6 +188,7 @@ export function RecipeStudioPage({
|
|||
handleAddModelProviderFromSheet,
|
||||
handleAddModelConfigFromSheet,
|
||||
handleAddExpressionFromSheet,
|
||||
handleAddValidatorFromSheet,
|
||||
handleAddMarkdownNoteFromSheet,
|
||||
} = useRecipeEditorGraph({
|
||||
nodes,
|
||||
|
|
@ -204,6 +207,7 @@ export function RecipeStudioPage({
|
|||
addModelProviderNode,
|
||||
addModelConfigNode,
|
||||
addExpressionNode,
|
||||
addValidatorNode,
|
||||
addMarkdownNoteNode,
|
||||
});
|
||||
|
||||
|
|
@ -521,6 +525,7 @@ export function RecipeStudioPage({
|
|||
onAddModelProvider={handleAddModelProviderFromSheet}
|
||||
onAddModelConfig={handleAddModelConfigFromSheet}
|
||||
onAddExpression={handleAddExpressionFromSheet}
|
||||
onAddValidator={handleAddValidatorFromSheet}
|
||||
onAddMarkdownNote={handleAddMarkdownNoteFromSheet}
|
||||
onOpenProcessors={openProcessorsFromSheet}
|
||||
copied={copied}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import type {
|
|||
ModelConfig,
|
||||
NodeConfig,
|
||||
SamplerConfig,
|
||||
ValidatorConfig,
|
||||
} from "../../types";
|
||||
import { isCategoryConfig, isSubcategoryConfig } from "../../utils";
|
||||
import { HANDLE_IDS } from "../../utils/handles";
|
||||
|
|
@ -43,6 +44,23 @@ function addSemanticEdge(edges: Edge[], source: string, target: string): Edge[]
|
|||
);
|
||||
}
|
||||
|
||||
function addValidatorSemanticEdge(
|
||||
edges: Edge[],
|
||||
source: string,
|
||||
target: string,
|
||||
): Edge[] {
|
||||
return addEdge(
|
||||
{
|
||||
source,
|
||||
target,
|
||||
sourceHandle: HANDLE_IDS.dataOut,
|
||||
targetHandle: HANDLE_IDS.dataIn,
|
||||
type: "semantic",
|
||||
},
|
||||
edges,
|
||||
);
|
||||
}
|
||||
|
||||
function removeTargetEdges(edges: Edge[], targetId: string): Edge[] {
|
||||
return edges.filter((edge) => edge.target !== targetId);
|
||||
}
|
||||
|
|
@ -159,6 +177,42 @@ export function syncEdgesForConfigPatch(
|
|||
}
|
||||
}
|
||||
|
||||
const hasValidatorTargetsPatch = Object.prototype.hasOwnProperty.call(
|
||||
patch,
|
||||
"target_columns",
|
||||
);
|
||||
if (current.kind === "validator" && hasValidatorTargetsPatch) {
|
||||
const nextTargets =
|
||||
((patch as Partial<ValidatorConfig>).target_columns ?? [])
|
||||
.map((value) => value.trim())
|
||||
.filter(Boolean);
|
||||
nextEdges = nextEdges.filter((edge) => {
|
||||
if (edge.source !== current.id && edge.target !== current.id) {
|
||||
return true;
|
||||
}
|
||||
const otherId = edge.source === current.id ? edge.target : edge.source;
|
||||
const other = configs[otherId];
|
||||
return !(
|
||||
other &&
|
||||
other.kind === "llm" &&
|
||||
other.llm_type === "code"
|
||||
);
|
||||
});
|
||||
const nextTargetName = nextTargets[0];
|
||||
if (nextTargetName) {
|
||||
const targetId = findNodeIdByName(configs, nextTargetName);
|
||||
const target = targetId ? configs[targetId] : null;
|
||||
if (
|
||||
targetId &&
|
||||
target &&
|
||||
target.kind === "llm" &&
|
||||
target.llm_type === "code"
|
||||
) {
|
||||
nextEdges = addValidatorSemanticEdge(nextEdges, targetId, current.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nextEdges;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -83,6 +83,17 @@ export function applyRenameToConfig(
|
|||
const base = next as LlmConfig;
|
||||
next = { ...base, model_alias: to };
|
||||
}
|
||||
if (config.kind === "validator") {
|
||||
const targets = config.target_columns ?? [];
|
||||
if (targets.includes(from)) {
|
||||
const base = next as typeof config;
|
||||
next = {
|
||||
...base,
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
target_columns: targets.map((target) => (target === from ? to : target)),
|
||||
};
|
||||
}
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
|
|
@ -125,6 +136,17 @@ export function applyRemovalToConfig(
|
|||
const base = next as LlmConfig;
|
||||
next = { ...base, model_alias: "" };
|
||||
}
|
||||
if (config.kind === "validator") {
|
||||
const targets = (config.target_columns ?? []).filter((target) => target !== ref);
|
||||
if (targets.length !== (config.target_columns ?? []).length) {
|
||||
const base = next as typeof config;
|
||||
next = {
|
||||
...base,
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
target_columns: targets,
|
||||
};
|
||||
}
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -73,6 +73,19 @@ export function applyEdgeRemovals(
|
|||
}
|
||||
next[target.id] = updated;
|
||||
}
|
||||
if (
|
||||
source.kind === "validator" &&
|
||||
target.kind === "llm" &&
|
||||
target.llm_type === "code"
|
||||
) {
|
||||
const sourceUpdated = applyRemovalToConfig(source, target.name);
|
||||
if (sourceUpdated !== source) {
|
||||
if (next === configs) {
|
||||
next = { ...configs };
|
||||
}
|
||||
next[source.id] = sourceUpdated;
|
||||
}
|
||||
}
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ type SheetView =
|
|||
| "sampler"
|
||||
| "seed"
|
||||
| "llm"
|
||||
| "validator"
|
||||
| "expression"
|
||||
| "note"
|
||||
| "processor";
|
||||
|
|
@ -95,6 +96,11 @@ type RecipeStudioState = {
|
|||
addModelProviderNode: (position?: XYPosition, openDialog?: boolean) => void;
|
||||
addModelConfigNode: (position?: XYPosition, openDialog?: boolean) => void;
|
||||
addExpressionNode: (position?: XYPosition, openDialog?: boolean) => void;
|
||||
addValidatorNode: (
|
||||
type: "validator_python" | "validator_sql",
|
||||
position?: XYPosition,
|
||||
openDialog?: boolean,
|
||||
) => void;
|
||||
addMarkdownNoteNode: (position?: XYPosition, openDialog?: boolean) => void;
|
||||
updateConfig: (id: string, patch: Partial<NodeConfig>) => void;
|
||||
loadRecipe: (snapshot: RecipeSnapshot) => void;
|
||||
|
|
@ -536,6 +542,19 @@ export const useRecipeStudioStore = create<RecipeStudioState>((set, get) => ({
|
|||
openDialog,
|
||||
);
|
||||
}),
|
||||
addValidatorNode: (type, position, openDialog = true) =>
|
||||
set((state) => {
|
||||
if (state.executionLocked) {
|
||||
return state;
|
||||
}
|
||||
return buildAddedNodeState(
|
||||
state,
|
||||
"validator",
|
||||
type,
|
||||
position,
|
||||
openDialog,
|
||||
);
|
||||
}),
|
||||
addMarkdownNoteNode: (position, openDialog = true) =>
|
||||
set((state) => {
|
||||
if (state.executionLocked) {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,14 @@ export type SamplerType =
|
|||
| "person_from_faker";
|
||||
|
||||
export type LlmType = "text" | "structured" | "code" | "judge";
|
||||
export type ValidatorCodeLang =
|
||||
| "python"
|
||||
| "sql:sqlite"
|
||||
| "sql:postgres"
|
||||
| "sql:mysql"
|
||||
| "sql:tsql"
|
||||
| "sql:bigquery"
|
||||
| "sql:ansi";
|
||||
|
||||
export type ExpressionDtype = "str" | "int" | "float" | "bool";
|
||||
|
||||
|
|
@ -28,6 +36,7 @@ export type RecipeNodeData = {
|
|||
kind:
|
||||
| "sampler"
|
||||
| "llm"
|
||||
| "validator"
|
||||
| "expression"
|
||||
| "seed"
|
||||
| "note"
|
||||
|
|
@ -37,6 +46,8 @@ export type RecipeNodeData = {
|
|||
blockType:
|
||||
| SamplerType
|
||||
| LlmType
|
||||
| "validator_python"
|
||||
| "validator_sql"
|
||||
| "expression"
|
||||
| "seed"
|
||||
| "markdown_note"
|
||||
|
|
@ -234,6 +245,19 @@ export type ExpressionConfig = {
|
|||
dtype: ExpressionDtype;
|
||||
};
|
||||
|
||||
export type ValidatorConfig = {
|
||||
id: string;
|
||||
kind: "validator";
|
||||
name: string;
|
||||
drop?: boolean;
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
target_columns: string[];
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
code_lang: ValidatorCodeLang;
|
||||
// ui ergonomics (serialized to int in payload)
|
||||
batch_size: string;
|
||||
};
|
||||
|
||||
export type MarkdownNoteConfig = {
|
||||
id: string;
|
||||
kind: "markdown_note";
|
||||
|
|
@ -294,6 +318,7 @@ export type RecipeProcessorConfig = SchemaTransformProcessorConfig;
|
|||
export type NodeConfig =
|
||||
| SamplerConfig
|
||||
| LlmConfig
|
||||
| ValidatorConfig
|
||||
| ExpressionConfig
|
||||
| MarkdownNoteConfig
|
||||
| SeedConfig
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import type {
|
|||
SeedSourceType,
|
||||
SamplerConfig,
|
||||
SamplerType,
|
||||
ValidatorCodeLang,
|
||||
ValidatorConfig,
|
||||
} from "../types";
|
||||
import { nextName } from "./naming";
|
||||
|
||||
|
|
@ -288,6 +290,25 @@ export function makeExpressionConfig(
|
|||
};
|
||||
}
|
||||
|
||||
export function makeValidatorConfig(
|
||||
id: string,
|
||||
codeLang: ValidatorCodeLang,
|
||||
existing: NodeConfig[],
|
||||
): ValidatorConfig {
|
||||
const isSql = codeLang.startsWith("sql:");
|
||||
return {
|
||||
id,
|
||||
kind: "validator",
|
||||
name: nextName(existing, isSql ? "validator_sql" : "validator_python"),
|
||||
drop: false,
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
target_columns: [],
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
code_lang: codeLang,
|
||||
batch_size: "10",
|
||||
};
|
||||
}
|
||||
|
||||
export function makeMarkdownNoteConfig(
|
||||
id: string,
|
||||
existing: NodeConfig[],
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import type {
|
|||
LlmConfig,
|
||||
NodeConfig,
|
||||
SamplerConfig,
|
||||
ValidatorConfig,
|
||||
} from "../types";
|
||||
|
||||
export function isSamplerConfig(
|
||||
|
|
@ -40,3 +41,9 @@ export function isExpressionConfig(
|
|||
): config is ExpressionConfig {
|
||||
return Boolean(config && config.kind === "expression");
|
||||
}
|
||||
|
||||
export function isValidatorConfig(
|
||||
config: NodeConfig | null | undefined,
|
||||
): config is ValidatorConfig {
|
||||
return Boolean(config && config.kind === "validator");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,7 +78,8 @@ type SingleRefRelation =
|
|||
| "provider"
|
||||
| "model_alias"
|
||||
| "reference_column_name"
|
||||
| "subcategory_parent";
|
||||
| "subcategory_parent"
|
||||
| "validator_target_columns";
|
||||
|
||||
function getSingleRefRelation(
|
||||
source: NodeConfig,
|
||||
|
|
@ -101,6 +102,13 @@ function getSingleRefRelation(
|
|||
if (isCategoryConfig(source) && isSubcategoryConfig(target)) {
|
||||
return "subcategory_parent";
|
||||
}
|
||||
if (
|
||||
source.kind === "llm" &&
|
||||
source.llm_type === "code" &&
|
||||
target.kind === "validator"
|
||||
) {
|
||||
return "validator_target_columns";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -126,6 +134,9 @@ function isCompetingIncomingEdge(
|
|||
if (relation === "subcategory_parent") {
|
||||
return isCategoryConfig(source);
|
||||
}
|
||||
if (relation === "validator_target_columns") {
|
||||
return source.kind === "llm" && source.llm_type === "code";
|
||||
}
|
||||
return source.kind === "sampler" && source.sampler_type === "datetime";
|
||||
}
|
||||
|
||||
|
|
@ -220,6 +231,27 @@ function chooseModelSemanticHandles(
|
|||
};
|
||||
}
|
||||
|
||||
function normalizeValidatorSemanticConnection(
|
||||
connection: Connection,
|
||||
source: NodeConfig,
|
||||
target: NodeConfig,
|
||||
): Connection {
|
||||
if (
|
||||
source.kind === "validator" &&
|
||||
target.kind === "llm" &&
|
||||
target.llm_type === "code"
|
||||
) {
|
||||
return {
|
||||
...connection,
|
||||
source: target.id,
|
||||
target: source.id,
|
||||
sourceHandle: HANDLE_IDS.dataOut,
|
||||
targetHandle: HANDLE_IDS.dataIn,
|
||||
};
|
||||
}
|
||||
return connection;
|
||||
}
|
||||
|
||||
export function isValidRecipeConnection(
|
||||
connection: Connection,
|
||||
configs: Record<string, NodeConfig>,
|
||||
|
|
@ -253,15 +285,30 @@ export function applyRecipeConnection(
|
|||
if (!isValidRecipeConnection(connection, configs)) {
|
||||
return { edges };
|
||||
}
|
||||
const source = connection.source
|
||||
const initialSource = connection.source
|
||||
? configs[connection.source]
|
||||
: null;
|
||||
const target = connection.target
|
||||
const initialTarget = connection.target
|
||||
? configs[connection.target]
|
||||
: null;
|
||||
if (!(initialSource && initialTarget)) {
|
||||
return { edges };
|
||||
}
|
||||
const normalizedConnection = normalizeValidatorSemanticConnection(
|
||||
connection,
|
||||
initialSource,
|
||||
initialTarget,
|
||||
);
|
||||
const source = normalizedConnection.source
|
||||
? configs[normalizedConnection.source]
|
||||
: null;
|
||||
const target = normalizedConnection.target
|
||||
? configs[normalizedConnection.target]
|
||||
: null;
|
||||
if (!(source && target)) {
|
||||
return { edges };
|
||||
}
|
||||
|
||||
const semanticRelation = isSemanticRelation(source, target);
|
||||
const singleRefRelation = getSingleRefRelation(source, target);
|
||||
const nextBaseEdges = singleRefRelation
|
||||
|
|
@ -271,7 +318,7 @@ export function applyRecipeConnection(
|
|||
)
|
||||
: edges;
|
||||
const resolvedConnection = chooseModelSemanticHandles(
|
||||
connection,
|
||||
normalizedConnection,
|
||||
source,
|
||||
target,
|
||||
nextBaseEdges,
|
||||
|
|
@ -301,11 +348,29 @@ export function applyRecipeConnection(
|
|||
};
|
||||
return { edges: nextEdges, configs: { ...configs, [target.id]: next } };
|
||||
}
|
||||
if (
|
||||
source.kind === "llm" &&
|
||||
source.llm_type === "code" &&
|
||||
target.kind === "validator"
|
||||
) {
|
||||
const nextCodeLang = (source.code_lang ?? "").trim();
|
||||
const next = {
|
||||
...target,
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
target_columns: [source.name],
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
code_lang:
|
||||
(nextCodeLang || target.code_lang) as typeof target.code_lang,
|
||||
};
|
||||
return { edges: nextEdges, configs: { ...configs, [target.id]: next } };
|
||||
}
|
||||
if (
|
||||
isLlmConfig(target) &&
|
||||
!semanticRelation &&
|
||||
source.kind !== "seed" &&
|
||||
source.kind !== "model_provider" &&
|
||||
source.kind !== "model_config"
|
||||
source.kind !== "model_config" &&
|
||||
source.kind !== "validator"
|
||||
) {
|
||||
const ref = `{{ ${source.name} }}`;
|
||||
const next = {
|
||||
|
|
@ -316,9 +381,11 @@ export function applyRecipeConnection(
|
|||
}
|
||||
if (
|
||||
isExpressionConfig(target) &&
|
||||
!semanticRelation &&
|
||||
source.kind !== "seed" &&
|
||||
source.kind !== "model_provider" &&
|
||||
source.kind !== "model_config"
|
||||
source.kind !== "model_config" &&
|
||||
source.kind !== "validator"
|
||||
) {
|
||||
const ref = `{{ ${source.name} }}`;
|
||||
const next = {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,19 @@ export function isSemanticRelation(
|
|||
if (source.kind === "model_provider" && target.kind === "model_config") {
|
||||
return true;
|
||||
}
|
||||
return source.kind === "model_config" && target.kind === "llm";
|
||||
if (source.kind === "model_config" && target.kind === "llm") {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
source.kind === "llm" &&
|
||||
source.llm_type === "code" &&
|
||||
target.kind === "validator"
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return (
|
||||
source.kind === "validator" &&
|
||||
target.kind === "llm" &&
|
||||
target.llm_type === "code"
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,21 @@ function isSemanticConnection(source: NodeConfig, target: NodeConfig): boolean {
|
|||
if (source.kind === "model_provider" && target.kind === "model_config") {
|
||||
return true;
|
||||
}
|
||||
return source.kind === "model_config" && target.kind === "llm";
|
||||
if (source.kind === "model_config" && target.kind === "llm") {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
source.kind === "llm" &&
|
||||
source.llm_type === "code" &&
|
||||
target.kind === "validator"
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return (
|
||||
source.kind === "validator" &&
|
||||
target.kind === "llm" &&
|
||||
target.llm_type === "code"
|
||||
);
|
||||
}
|
||||
|
||||
export function buildEdges(
|
||||
|
|
@ -149,6 +163,13 @@ export function buildEdges(
|
|||
if (config.kind === "llm" && config.model_alias) {
|
||||
addEdgeByName(config.model_alias, config.name);
|
||||
}
|
||||
if (config.kind === "validator") {
|
||||
for (const targetColumn of config.target_columns ?? []) {
|
||||
if (targetColumn.trim()) {
|
||||
addEdgeByName(targetColumn, config.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return edges;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { parseExpression } from "./parsers/expression-parser";
|
|||
import { parseLlm } from "./parsers/llm-parser";
|
||||
export { parseModelConfig, parseModelProvider } from "./parsers/model-parser";
|
||||
import { parseSampler } from "./parsers/sampler-parser";
|
||||
import { parseValidator } from "./parsers/validator-parser";
|
||||
|
||||
type ColumnParser = (
|
||||
column: Record<string, unknown>,
|
||||
|
|
@ -20,6 +21,7 @@ const COLUMN_PARSERS: Record<string, ColumnParser> = {
|
|||
"llm-structured": (column, name, id) => parseLlm(column, name, id),
|
||||
"llm-code": (column, name, id) => parseLlm(column, name, id),
|
||||
"llm-judge": (column, name, id) => parseLlm(column, name, id),
|
||||
validation: (column, name, id) => parseValidator(column, name, id),
|
||||
};
|
||||
|
||||
export function parseColumn(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
import type { ValidatorConfig } from "../../../types";
|
||||
import { readNumberString } from "../helpers";
|
||||
import { normalizeValidatorCodeLang } from "../../validators/code-lang";
|
||||
|
||||
export function parseValidator(
|
||||
column: Record<string, unknown>,
|
||||
name: string,
|
||||
id: string,
|
||||
): ValidatorConfig {
|
||||
const targetColumns = Array.isArray(column.target_columns)
|
||||
? column.target_columns
|
||||
.filter((value): value is string => typeof value === "string")
|
||||
.map((value) => value.trim())
|
||||
.filter(Boolean)
|
||||
: [];
|
||||
const params =
|
||||
column.validator_params && typeof column.validator_params === "object"
|
||||
? (column.validator_params as Record<string, unknown>)
|
||||
: {};
|
||||
return {
|
||||
id,
|
||||
kind: "validator",
|
||||
name,
|
||||
drop: column.drop === true,
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
target_columns: targetColumns,
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
code_lang: normalizeValidatorCodeLang(params.code_lang),
|
||||
batch_size: readNumberString(column.batch_size) || "10",
|
||||
};
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ export {
|
|||
makeModelProviderConfig,
|
||||
makeSamplerConfig,
|
||||
makeSeedConfig,
|
||||
makeValidatorConfig,
|
||||
} from "./config-factories";
|
||||
export {
|
||||
labelForExpression,
|
||||
|
|
@ -18,6 +19,7 @@ export {
|
|||
isLlmConfig,
|
||||
isSamplerConfig,
|
||||
isSubcategoryConfig,
|
||||
isValidatorConfig,
|
||||
} from "./config-type-guards";
|
||||
export { nextName } from "./naming";
|
||||
export { nodeDataFromConfig } from "./node-data";
|
||||
|
|
|
|||
|
|
@ -29,6 +29,18 @@ export function nodeDataFromConfig(
|
|||
layoutDirection,
|
||||
};
|
||||
}
|
||||
if (config.kind === "validator") {
|
||||
return {
|
||||
title: "Validator",
|
||||
kind: "validator",
|
||||
subtype: config.code_lang.startsWith("sql:") ? "SQL" : "Python",
|
||||
blockType: config.code_lang.startsWith("sql:")
|
||||
? "validator_sql"
|
||||
: "validator_python",
|
||||
name: config.name,
|
||||
layoutDirection,
|
||||
};
|
||||
}
|
||||
if (config.kind === "markdown_note") {
|
||||
return {
|
||||
title: "Note",
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ import {
|
|||
buildSamplerColumn,
|
||||
buildSeedConfig,
|
||||
buildSeedDropProcessor,
|
||||
buildValidatorColumn,
|
||||
pickFirstSeedConfig,
|
||||
} from "./builders";
|
||||
import type { RecipePayloadResult } from "./types";
|
||||
|
|
@ -40,6 +41,7 @@ import {
|
|||
validateModelConfigProviders,
|
||||
validateSubcategoryConfigs,
|
||||
validateTimedeltaConfigs,
|
||||
validateValidatorConfigs,
|
||||
validateUsedProviders,
|
||||
} from "./validate";
|
||||
import { isLikelyImageValue } from "../image-preview";
|
||||
|
|
@ -179,6 +181,11 @@ export function buildRecipePayload(
|
|||
nameToConfig.set(config.name, config);
|
||||
continue;
|
||||
}
|
||||
if (config.kind === "validator") {
|
||||
columns.push(buildValidatorColumn(config, errors));
|
||||
nameToConfig.set(config.name, config);
|
||||
continue;
|
||||
}
|
||||
if (config.kind === "seed") {
|
||||
// SeedConfig is global config (seed_config); seed-dataset columns are added by DataDesigner.
|
||||
continue;
|
||||
|
|
@ -198,6 +205,7 @@ export function buildRecipePayload(
|
|||
|
||||
validateSubcategoryConfigs(configs, nameToConfig, errors);
|
||||
validateTimedeltaConfigs(configs, nameToConfig, errors);
|
||||
validateValidatorConfigs(configs, nameToConfig, errors);
|
||||
validateModelAliasLinks(modelAliases, modelConfigConfigs, errors);
|
||||
validateModelConfigProviders(
|
||||
modelConfigConfigs,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
import type { ValidatorConfig } from "../../types";
|
||||
|
||||
function parseBatchSize(value: string): number {
|
||||
const parsed = Number.parseInt(value, 10);
|
||||
if (!Number.isFinite(parsed) || parsed < 1) {
|
||||
return 10;
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
export function buildValidatorColumn(
|
||||
config: ValidatorConfig,
|
||||
errors: string[],
|
||||
): Record<string, unknown> {
|
||||
const targetColumns = (config.target_columns ?? [])
|
||||
.map((value) => value.trim())
|
||||
.filter(Boolean);
|
||||
if (targetColumns.length === 0) {
|
||||
errors.push(`Validator ${config.name}: target code column required.`);
|
||||
}
|
||||
return {
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
column_type: "validation",
|
||||
name: config.name,
|
||||
drop: config.drop ?? false,
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
target_columns: targetColumns,
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
validator_type: "code",
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
validator_params: {
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
code_lang: config.code_lang,
|
||||
},
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
batch_size: parseBatchSize(config.batch_size),
|
||||
};
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ export { buildLlmColumn, buildLlmMcpProvider, buildLlmToolConfig } from "./build
|
|||
export { buildModelConfig, buildModelProvider } from "./builders-model";
|
||||
export { buildExpressionColumn, buildProcessors } from "./builders-processors";
|
||||
export { buildSamplerColumn } from "./builders-sampler";
|
||||
export { buildValidatorColumn } from "./builders-validator";
|
||||
export {
|
||||
buildSeedConfig,
|
||||
buildSeedDropProcessor,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,9 @@
|
|||
import type { ModelConfig, ModelProviderConfig, NodeConfig } from "../../types";
|
||||
import type {
|
||||
ModelConfig,
|
||||
ModelProviderConfig,
|
||||
NodeConfig,
|
||||
ValidatorConfig,
|
||||
} from "../../types";
|
||||
|
||||
export function validateSubcategoryConfigs(
|
||||
configs: Record<string, NodeConfig>,
|
||||
|
|
@ -106,3 +111,33 @@ export function validateUsedProviders(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function validateValidatorConfigs(
|
||||
configs: Record<string, NodeConfig>,
|
||||
nameToConfig: Map<string, NodeConfig>,
|
||||
errors: string[],
|
||||
): void {
|
||||
for (const config of Object.values(configs)) {
|
||||
if (config.kind !== "validator") {
|
||||
continue;
|
||||
}
|
||||
const target = (config as ValidatorConfig).target_columns[0]?.trim();
|
||||
if (!target) {
|
||||
continue;
|
||||
}
|
||||
const targetConfig = nameToConfig.get(target);
|
||||
if (!targetConfig) {
|
||||
errors.push(`Validator ${config.name}: target '${target}' not found.`);
|
||||
continue;
|
||||
}
|
||||
if (targetConfig.kind !== "llm" || targetConfig.llm_type !== "code") {
|
||||
errors.push(`Validator ${config.name}: target '${target}' must be LLM Code.`);
|
||||
continue;
|
||||
}
|
||||
if ((targetConfig.code_lang ?? "").trim() !== config.code_lang.trim()) {
|
||||
errors.push(
|
||||
`Validator ${config.name}: code_lang '${config.code_lang}' must match target '${target}' (${targetConfig.code_lang ?? "unknown"}).`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -192,6 +192,21 @@ export function getConfigErrors(config: NodeConfig | null): string[] {
|
|||
errors.push("Expression is required.");
|
||||
}
|
||||
}
|
||||
if (config.kind === "validator") {
|
||||
const targets = (config.target_columns ?? [])
|
||||
.map((value) => value.trim())
|
||||
.filter(Boolean);
|
||||
if (targets.length === 0) {
|
||||
errors.push("Target code column is required.");
|
||||
}
|
||||
const batch = parseIntNumber(config.batch_size);
|
||||
if (batch === null || batch < 1) {
|
||||
errors.push("Batch size must be an integer >= 1.");
|
||||
}
|
||||
if (!config.code_lang.trim()) {
|
||||
errors.push("Validator code language is required.");
|
||||
}
|
||||
}
|
||||
if (config.kind === "seed") {
|
||||
const seedSourceType = config.seed_source_type ?? "hf";
|
||||
if (seedSourceType === "hf" && !config.hf_repo_id.trim()) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
import type { ValidatorCodeLang } from "../../types";
|
||||
|
||||
export const VALIDATOR_SQL_CODE_LANGS: ValidatorCodeLang[] = [
|
||||
"sql:sqlite",
|
||||
"sql:postgres",
|
||||
"sql:mysql",
|
||||
"sql:tsql",
|
||||
"sql:bigquery",
|
||||
"sql:ansi",
|
||||
];
|
||||
|
||||
const VALIDATOR_CODE_LANG_SET = new Set<ValidatorCodeLang>([
|
||||
"python",
|
||||
...VALIDATOR_SQL_CODE_LANGS,
|
||||
]);
|
||||
|
||||
export function isValidatorCodeLang(value: string): value is ValidatorCodeLang {
|
||||
return VALIDATOR_CODE_LANG_SET.has(value as ValidatorCodeLang);
|
||||
}
|
||||
|
||||
export function normalizeValidatorCodeLang(
|
||||
value: unknown,
|
||||
): ValidatorCodeLang {
|
||||
const raw = typeof value === "string" ? value.trim() : "";
|
||||
if (!raw) {
|
||||
return "python";
|
||||
}
|
||||
if (raw === "python") {
|
||||
return "python";
|
||||
}
|
||||
if (raw.startsWith("sql:")) {
|
||||
if (VALIDATOR_SQL_CODE_LANGS.includes(raw as ValidatorCodeLang)) {
|
||||
return raw as ValidatorCodeLang;
|
||||
}
|
||||
return "sql:sqlite";
|
||||
}
|
||||
return "python";
|
||||
}
|
||||
|
|
@ -43,6 +43,11 @@ export function getAvailableVariableEntries(
|
|||
continue;
|
||||
}
|
||||
|
||||
if (config.kind === "validator") {
|
||||
vars.push({ name: config.name, source: "column" });
|
||||
continue;
|
||||
}
|
||||
|
||||
if (config.kind === "seed") {
|
||||
for (const col of config.seed_columns ?? []) {
|
||||
const name = col.trim();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue