From 761c84f92beaa480e4fde8929d771bac539aa45e Mon Sep 17 00:00:00 2001 From: Shine1i Date: Sun, 1 Mar 2026 13:01:00 +0100 Subject: [PATCH] feat(recipe-studio): introduce validator blocks for code validation with Python and SQL engines --- .../recipe-studio/blocks/definitions.ts | 51 +++++- .../recipe-studio/blocks/render-dialog.tsx | 5 + .../recipe-studio/components/block-sheet.tsx | 20 ++- .../components/recipe-graph-node.tsx | 21 ++- .../recipe-studio/dialogs/config-dialog.tsx | 1 + .../recipe-studio/dialogs/llm/general-tab.tsx | 145 +++++++----------- .../dialogs/validators/validator-dialog.tsx | 126 +++++++++++++++ .../hooks/use-recipe-editor-graph.ts | 27 ++++ .../hooks/use-recipe-runtime-visuals.ts | 3 + .../recipe-studio/recipe-studio-page.tsx | 5 + .../recipe-studio/stores/helpers/edge-sync.ts | 54 +++++++ .../stores/helpers/reference-sync.ts | 22 +++ .../recipe-studio/stores/helpers/removals.ts | 13 ++ .../recipe-studio/stores/recipe-studio.ts | 19 +++ .../src/features/recipe-studio/types/index.ts | 25 +++ .../recipe-studio/utils/config-factories.ts | 21 +++ .../recipe-studio/utils/config-type-guards.ts | 7 + .../utils/graph/recipe-graph-connection.ts | 79 +++++++++- .../recipe-studio/utils/graph/relations.ts | 17 +- .../recipe-studio/utils/import/edges.ts | 23 ++- .../recipe-studio/utils/import/parsers.ts | 2 + .../utils/import/parsers/validator-parser.ts | 31 ++++ .../src/features/recipe-studio/utils/index.ts | 2 + .../features/recipe-studio/utils/node-data.ts | 12 ++ .../utils/payload/build-payload.ts | 8 + .../utils/payload/builders-validator.ts | 38 +++++ .../recipe-studio/utils/payload/builders.ts | 1 + .../recipe-studio/utils/payload/validate.ts | 37 ++++- .../recipe-studio/utils/validation.ts | 15 ++ .../utils/validators/code-lang.ts | 38 +++++ .../features/recipe-studio/utils/variables.ts | 5 + 31 files changed, 766 insertions(+), 107 deletions(-) create mode 100644 studio/frontend/src/features/recipe-studio/dialogs/validators/validator-dialog.tsx create mode 100644 studio/frontend/src/features/recipe-studio/utils/import/parsers/validator-parser.ts create mode 100644 studio/frontend/src/features/recipe-studio/utils/payload/builders-validator.ts create mode 100644 studio/frontend/src/features/recipe-studio/utils/validators/code-lang.ts diff --git a/studio/frontend/src/features/recipe-studio/blocks/definitions.ts b/studio/frontend/src/features/recipe-studio/blocks/definitions.ts index 073cce928e..a37a24ad3b 100644 --- a/studio/frontend/src/features/recipe-studio/blocks/definitions.ts +++ b/studio/frontend/src/features/recipe-studio/blocks/definitions.ts @@ -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"); } diff --git a/studio/frontend/src/features/recipe-studio/blocks/render-dialog.tsx b/studio/frontend/src/features/recipe-studio/blocks/render-dialog.tsx index f99c1c78da..d07aa6b627 100644 --- a/studio/frontend/src/features/recipe-studio/blocks/render-dialog.tsx +++ b/studio/frontend/src/features/recipe-studio/blocks/render-dialog.tsx @@ -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" ? ( ) : null; + case "validator": + return config.kind === "validator" ? ( + + ) : null; case "markdown_note": return config.kind === "markdown_note" ? ( diff --git a/studio/frontend/src/features/recipe-studio/components/block-sheet.tsx b/studio/frontend/src/features/recipe-studio/components/block-sheet.tsx index be301a36fd..f80f309f90 100644 --- a/studio/frontend/src/features/recipe-studio/components/block-sheet.tsx +++ b/studio/frontend/src/features/recipe-studio/components/block-sheet.tsx @@ -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; 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 = { 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; diff --git a/studio/frontend/src/features/recipe-studio/components/recipe-graph-node.tsx b/studio/frontend/src/features/recipe-studio/components/recipe-graph-node.tsx index 1effe96fc4..35b52b75c5 100644 --- a/studio/frontend/src/features/recipe-studio/components/recipe-graph-node.tsx +++ b/studio/frontend/src/features/recipe-studio/components/recipe-graph-node.tsx @@ -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 = diff --git a/studio/frontend/src/features/recipe-studio/dialogs/config-dialog.tsx b/studio/frontend/src/features/recipe-studio/dialogs/config-dialog.tsx index 6f9c20c58b..34b8ea95c3 100644 --- a/studio/frontend/src/features/recipe-studio/dialogs/config-dialog.tsx +++ b/studio/frontend/src/features/recipe-studio/dialogs/config-dialog.tsx @@ -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"); diff --git a/studio/frontend/src/features/recipe-studio/dialogs/llm/general-tab.tsx b/studio/frontend/src/features/recipe-studio/dialogs/llm/general-tab.tsx index c974944491..c4b92489de 100644 --- a/studio/frontend/src/features/recipe-studio/dialogs/llm/general-tab.tsx +++ b/studio/frontend/src/features/recipe-studio/dialogs/llm/general-tab.tsx @@ -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({

)} -
+ {hasHfSeed && (
-
- - {imageColumnOptions.length > 0 && ( -

- Suggested image columns: {imageColumnOptions.join(", ")} -

- )} -
+
- {imageContext.enabled && ( -
- - -
+ )} + {config.llm_type === "structured" && ( +
+ +