processors and drop column

This commit is contained in:
shine1i 2026-02-05 22:26:48 +01:00
commit 1ca01e5d21
13 changed files with 410 additions and 10 deletions

View file

@ -202,13 +202,21 @@ File:
model_providers: [...],
model_configs: [...],
columns: [...],
processors: [],
processors: [...],
},
run: { rows: 5, preview: true, output_formats: ["jsonl"] },
ui: { nodes: [...], edges: [...] }
}
```
Current processor UI surface:
- `schema_transform` (sheet -> `Processors` -> `Schema Transform`)
- mapped to recipe processor with `build_stage: "post_batch"` and JSON `template`.
Current drop policy:
- column dialogs (`sampler` / `llm` / `expression`) expose `drop` toggle.
- payload writes column `drop` directly (preferred over drop-columns processor in v1).
How relation is enforced:
- collect `model_alias` values used by LLM columns
- ensure each alias exists in `recipe.model_configs`
@ -253,6 +261,8 @@ Model dialogs:
`/Volumes/Expansion/projects/new-ui-prototype/studio/frontend/src/features/canvas-lab/dialogs/models/model-provider-dialog.tsx`
- model config:
`/Volumes/Expansion/projects/new-ui-prototype/studio/frontend/src/features/canvas-lab/dialogs/models/model-config-dialog.tsx`
- processors:
`/Volumes/Expansion/projects/new-ui-prototype/studio/frontend/src/features/canvas-lab/dialogs/processors-dialog.tsx`
`ModelConfigDialog` and `LlmDialog` use shadcn `Combobox` fed from store configs:
- model config `provider` suggests model-provider node names
@ -291,3 +301,36 @@ Minimal path for a new block type:
- payload/import utils = external contract boundary.
If one piece changes, keep all six in sync.
## 14) Processors roadmap (decision)
Current decision: **Option 3 (hybrid)**.
v1 scope:
- add `drop` toggle on column blocks (sampler/llm/expression).
- add processor config surface for `schema_transform`.
- keep payload builder as single mapper to `recipe.processors`.
- keep processor state separate from node graph for now.
Reason:
- fastest ship path.
- matches Data Designer column-level `drop`.
- avoids duplicate/complex processor edge logic in v1.
Future option noted: **Option 2 (processor chain in graph)**.
Option 2 structure:
- add virtual node `Dataset Output`.
- processors become graph nodes: `Schema Transform`, `Drop Columns`, future processors.
- processor order derived from chain edges:
`Dataset Output -> P1 -> P2 -> ...`
- enforce chain rules:
- no cycles
- one incoming max per processor
- one outgoing max per processor
- chain must start at `Dataset Output`
Migration from option 3 -> 2:
- keep same processor schema/payload contracts.
- move order source from list/order field to edge traversal.
- UI changes mostly in canvas rendering + validation; payload adapter stays mostly same.

View file

@ -22,11 +22,13 @@ import { CanvasEdge } from "./components/canvas-edge";
import { CanvasNode } from "./components/canvas-node";
import { ConfigDialog } from "./dialogs/config-dialog";
import { ImportDialog } from "./dialogs/import-dialog";
import { ProcessorsDialog } from "./dialogs/processors-dialog";
import { useCanvasLabStore } from "./stores/canvas-lab";
import type { CanvasNodeData, SamplerConfig } from "./types";
import { isCategoryConfig } from "./utils";
import { importCanvasPayload } from "./utils/import";
import { buildCanvasPayload } from "./utils/payload";
import { buildDefaultSchemaTransform } from "./utils/processors";
const NODE_TYPES: NodeTypes = { builder: CanvasNode };
const EDGE_TYPES: EdgeTypes = { canvas: CanvasEdge, semantic: CanvasEdge };
@ -68,6 +70,7 @@ export function CanvasLabPage(): ReactElement {
nodes,
edges,
configs,
processors,
sheetView,
activeConfigId,
dialogOpen,
@ -84,6 +87,7 @@ export function CanvasLabPage(): ReactElement {
updateConfig,
isValidConnection,
setSheetView,
setProcessors,
setDialogOpen,
loadCanvas,
setLayoutDirection,
@ -93,6 +97,7 @@ export function CanvasLabPage(): ReactElement {
nodes: state.nodes,
edges: state.edges,
configs: state.configs,
processors: state.processors,
sheetView: state.sheetView,
activeConfigId: state.activeConfigId,
dialogOpen: state.dialogOpen,
@ -109,6 +114,7 @@ export function CanvasLabPage(): ReactElement {
updateConfig: state.updateConfig,
isValidConnection: state.isValidConnection,
setSheetView: state.setSheetView,
setProcessors: state.setProcessors,
setDialogOpen: state.setDialogOpen,
loadCanvas: state.loadCanvas,
setLayoutDirection: state.setLayoutDirection,
@ -120,6 +126,7 @@ export function CanvasLabPage(): ReactElement {
);
const [previewLoading, setPreviewLoading] = useState(false);
const [importOpen, setImportOpen] = useState(false);
const [processorsOpen, setProcessorsOpen] = useState(false);
const [statusMessage, setStatusMessage] = useState<{
tone: "success" | "error";
text: string;
@ -146,7 +153,12 @@ export function CanvasLabPage(): ReactElement {
setPreviewLoading(true);
setStatusMessage(null);
try {
const { payload, errors } = buildCanvasPayload(configs, nodes, edges);
const { payload, errors } = buildCanvasPayload(
configs,
nodes,
edges,
processors,
);
if (errors.length > 0) {
setStatusMessage({
tone: "error",
@ -172,7 +184,12 @@ export function CanvasLabPage(): ReactElement {
const handleCopyRecipe = async (): Promise<void> => {
setStatusMessage(null);
const { payload, errors } = buildCanvasPayload(configs, nodes, edges);
const { payload, errors } = buildCanvasPayload(
configs,
nodes,
edges,
processors,
);
if (errors.length > 0) {
setStatusMessage({
tone: "error",
@ -211,6 +228,17 @@ export function CanvasLabPage(): ReactElement {
return null;
};
const handleOpenProcessorsFromSheet = useCallback(() => {
if (
!processors.some(
(processor) => processor.processor_type === "schema_transform",
)
) {
setProcessors([...processors, buildDefaultSchemaTransform()]);
}
setProcessorsOpen(true);
}, [processors, setProcessors]);
return (
<div className="min-h-screen bg-background">
<main className="w-full px-6 py-8">
@ -313,6 +341,7 @@ export function CanvasLabPage(): ReactElement {
onAddModelProvider={addModelProviderNode}
onAddModelConfig={addModelConfigNode}
onAddExpression={addExpressionNode}
onOpenProcessors={handleOpenProcessorsFromSheet}
/>
</Panel>
<Controls position="bottom-left" />
@ -333,6 +362,13 @@ export function CanvasLabPage(): ReactElement {
onImport={handleImport}
container={sheetContainer}
/>
<ProcessorsDialog
open={processorsOpen}
onOpenChange={setProcessorsOpen}
processors={processors}
onProcessorsChange={setProcessors}
container={sheetContainer}
/>
</div>
);
}

View file

@ -8,7 +8,9 @@ import {
} from "@/components/ui/sheet";
import {
ArrowLeft02Icon,
ArrowRight01Icon, type Database02Icon,
ArrowRight01Icon,
CodeIcon,
type Database02Icon,
PlusSignIcon,
} from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
@ -16,8 +18,15 @@ import type { ReactElement } from "react";
import type { LlmType, SamplerType } from "../types";
import { BLOCK_GROUPS, getBlocksForKind } from "../blocks/registry";
type SheetView = "root" | "sampler" | "llm" | "expression";
type SheetView = "root" | "sampler" | "llm" | "expression" | "processor";
type SheetKind = "sampler" | "llm" | "expression";
type RootSheetView = Exclude<SheetView, "root">;
type RootGroup = {
kind: RootSheetView;
title: string;
description: string;
icon: typeof Database02Icon;
};
type BlockSheetProps = {
container: HTMLDivElement | null;
@ -28,6 +37,7 @@ type BlockSheetProps = {
onAddModelProvider: () => void;
onAddModelConfig: () => void;
onAddExpression: () => void;
onOpenProcessors: () => void;
};
function getSheetTitle(view: SheetView): string {
@ -40,6 +50,9 @@ function getSheetTitle(view: SheetView): string {
if (view === "expression") {
return "Expression blocks";
}
if (view === "processor") {
return "Processor blocks";
}
return "LLM blocks";
}
@ -48,8 +61,19 @@ const VIEW_KIND: Record<SheetView, SheetKind | null> = {
sampler: "sampler",
llm: "llm",
expression: "expression",
processor: null,
};
const ROOT_GROUPS: RootGroup[] = [
...BLOCK_GROUPS,
{
kind: "processor",
title: "Processors",
description: "Output schema + post batch.",
icon: CodeIcon,
},
];
function BlockSheetButton({
icon,
title,
@ -97,6 +121,7 @@ export function BlockSheet({
onAddModelProvider,
onAddModelConfig,
onAddExpression,
onOpenProcessors,
}: BlockSheetProps): ReactElement {
const title = getSheetTitle(view);
return (
@ -138,7 +163,7 @@ export function BlockSheet({
<div className=" py-4">
<div className="mt-4 flex flex-col gap-2">
{view === "root" &&
BLOCK_GROUPS.map((item, index) => (
ROOT_GROUPS.map((item, index) => (
<BlockSheetButton
key={item.kind}
icon={item.icon}
@ -148,7 +173,17 @@ export function BlockSheet({
onClick={() => onViewChange(item.kind)}
/>
))}
{view === "processor" && (
<BlockSheetButton
icon={CodeIcon}
title="Schema Transform"
description="Transform final dataset schema."
isActive={true}
onClick={onOpenProcessors}
/>
)}
{view !== "root" &&
view !== "processor" &&
getBlocksForKind(VIEW_KIND[view] ?? "sampler").map(
(item, index) => (
<BlockSheetButton

View file

@ -1,5 +1,6 @@
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogFooter } from "@/components/ui/dialog";
import { Switch } from "@/components/ui/switch";
import type { ReactElement } from "react";
import type { NodeConfig, SamplerConfig } from "../types";
import { renderBlockDialog } from "../blocks/registry";
@ -41,6 +42,22 @@ export function ConfigDialog({
{config && (
<div className="space-y-4">
<ValidationBanner config={config} />
{(config.kind === "sampler" ||
config.kind === "llm" ||
config.kind === "expression") && (
<div className="flex items-center justify-between gap-3 rounded-2xl border border-border/60 px-3 py-2">
<div>
<p className="text-sm font-semibold">Drop from final dataset</p>
<p className="text-xs text-muted-foreground">
Keep for generation but omit from exported rows.
</p>
</div>
<Switch
checked={config.drop ?? false}
onCheckedChange={(value) => onUpdate(config.id, { drop: value })}
/>
</div>
)}
{renderBlockDialog(config, categoryOptions, onUpdate)}
</div>
)}

View file

@ -0,0 +1,134 @@
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogFooter } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
import { Textarea } from "@/components/ui/textarea";
import { type ReactElement, useMemo } from "react";
import type { CanvasProcessorConfig } from "../types";
import { buildDefaultSchemaTransform } from "../utils/processors";
type ProcessorsDialogProps = {
open: boolean;
onOpenChange: (open: boolean) => void;
processors: CanvasProcessorConfig[];
onProcessorsChange: (processors: CanvasProcessorConfig[]) => void;
container?: HTMLDivElement | null;
};
export function ProcessorsDialog({
open,
onOpenChange,
processors,
onProcessorsChange,
container,
}: ProcessorsDialogProps): ReactElement {
const schemaIndex = useMemo(
() =>
processors.findIndex(
(processor) => processor.processor_type === "schema_transform",
),
[processors],
);
const schemaProcessor = schemaIndex >= 0 ? processors[schemaIndex] : null;
const nameId = schemaProcessor ? `${schemaProcessor.id}-name` : "schema-transform-name";
const templateId = schemaProcessor
? `${schemaProcessor.id}-template`
: "schema-transform-template";
const setSchemaEnabled = (enabled: boolean) => {
if (enabled) {
if (schemaProcessor) {
return;
}
onProcessorsChange([...processors, buildDefaultSchemaTransform()]);
return;
}
onProcessorsChange(
processors.filter(
(processor) => processor.processor_type !== "schema_transform",
),
);
};
const updateSchema = (patch: Partial<CanvasProcessorConfig>) => {
if (!schemaProcessor) {
return;
}
const next = [...processors];
next[schemaIndex] = { ...schemaProcessor, ...patch };
onProcessorsChange(next);
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent
container={container}
position="absolute"
overlayPosition="absolute"
overlayClassName="bg-transparent"
className="max-h-[85vh] overflow-auto sm:max-w-2xl"
>
<div className="space-y-4">
<div className="flex items-center justify-between gap-3 rounded-2xl border border-border/60 px-3 py-2">
<div>
<p className="text-sm font-semibold">Schema transform</p>
<p className="text-xs text-muted-foreground">
Transform final rows to target schema (post-batch).
</p>
</div>
<Switch
checked={Boolean(schemaProcessor)}
onCheckedChange={setSchemaEnabled}
/>
</div>
{schemaProcessor && (
<div className="space-y-3">
<div className="grid gap-2">
<label
className="text-xs font-semibold uppercase text-muted-foreground"
htmlFor={nameId}
>
Name
</label>
<Input
id={nameId}
className="nodrag"
value={schemaProcessor.name}
onChange={(event) => updateSchema({ name: event.target.value })}
/>
</div>
<div className="grid gap-2">
<label
className="text-xs font-semibold uppercase text-muted-foreground"
htmlFor={templateId}
>
Template (JSON)
</label>
<Textarea
id={templateId}
className="nodrag min-h-[220px]"
value={schemaProcessor.template}
onChange={(event) =>
updateSchema({ template: event.target.value })
}
/>
<p className="text-xs text-muted-foreground">
Use Jinja refs like {"{{ customer_review }}"} in values.
</p>
</div>
</div>
)}
</div>
<DialogFooter>
<Button
type="button"
variant="outline"
onClick={() => onOpenChange(false)}
>
Done
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View file

@ -11,6 +11,7 @@ import {
import { create } from "zustand";
import type {
CanvasNode,
CanvasProcessorConfig,
LayoutDirection,
LlmType,
ModelConfig,
@ -34,12 +35,13 @@ import {
updateNodeData,
} from "./canvas-lab-helpers";
type SheetView = "root" | "sampler" | "llm" | "expression";
type SheetView = "root" | "sampler" | "llm" | "expression" | "processor";
type CanvasLabState = {
nodes: CanvasNode[];
edges: Edge[];
configs: Record<string, NodeConfig>;
processors: CanvasProcessorConfig[];
sheetView: SheetView;
activeConfigId: string | null;
dialogOpen: boolean;
@ -47,6 +49,7 @@ type CanvasLabState = {
nextId: number;
nextY: number;
setSheetView: (view: SheetView) => void;
setProcessors: (processors: CanvasProcessorConfig[]) => void;
setDialogOpen: (open: boolean) => void;
openConfig: (id: string) => void;
setLayoutDirection: (direction: LayoutDirection) => void;
@ -68,6 +71,7 @@ export const useCanvasLabStore = create<CanvasLabState>((set, get) => ({
nodes: [],
edges: [],
configs: {},
processors: [],
sheetView: "root",
activeConfigId: null,
dialogOpen: false,
@ -75,6 +79,7 @@ export const useCanvasLabStore = create<CanvasLabState>((set, get) => ({
nextId: 3,
nextY: 280,
setSheetView: (view) => set({ sheetView: view }),
setProcessors: (processors) => set({ processors }),
setDialogOpen: (open) => set({ dialogOpen: open }),
openConfig: (id) => set({ activeConfigId: id, dialogOpen: true }),
setLayoutDirection: (direction) =>
@ -168,6 +173,7 @@ export const useCanvasLabStore = create<CanvasLabState>((set, get) => ({
state.layoutDirection,
),
edges: snapshot.edges,
processors: snapshot.processors,
nextId: snapshot.nextId,
nextY: snapshot.nextY,
activeConfigId: null,

View file

@ -47,6 +47,7 @@ export type SamplerConfig = {
// biome-ignore lint/style/useNamingConvention: api schema
sampler_type: SamplerType;
name: string;
drop?: boolean;
// biome-ignore lint/style/useNamingConvention: api schema
convert_to?: "float" | "int" | "str";
values?: string[];
@ -107,6 +108,7 @@ export type LlmConfig = {
// biome-ignore lint/style/useNamingConvention: api schema
llm_type: LlmType;
name: string;
drop?: boolean;
// biome-ignore lint/style/useNamingConvention: api schema
model_alias: string;
prompt: string;
@ -156,10 +158,21 @@ export type ExpressionConfig = {
id: string;
kind: "expression";
name: string;
drop?: boolean;
expr: string;
dtype: ExpressionDtype;
};
export type SchemaTransformProcessorConfig = {
id: string;
// biome-ignore lint/style/useNamingConvention: api schema
processor_type: "schema_transform";
name: string;
template: string;
};
export type CanvasProcessorConfig = SchemaTransformProcessorConfig;
export type NodeConfig =
| SamplerConfig
| LlmConfig

View file

@ -1,4 +1,4 @@
import type { NodeConfig } from "../../types";
import type { CanvasProcessorConfig, NodeConfig } from "../../types";
import { buildEdges } from "./edges";
import { isRecord, parseJson, readString } from "./helpers";
import {
@ -21,6 +21,40 @@ type UiInput = {
edges?: unknown;
};
function parseProcessors(input: unknown): CanvasProcessorConfig[] {
if (!Array.isArray(input)) {
return [];
}
const processors: CanvasProcessorConfig[] = [];
input.forEach((item, index) => {
if (!isRecord(item)) {
return;
}
const type = readString(item.processor_type);
const templateRaw = item.template;
const isSchemaTransform =
type === "schema_transform" || isRecord(templateRaw);
if (!isSchemaTransform) {
return;
}
const name = readString(item.name) ?? `schema_transform_${index + 1}`;
const template =
typeof templateRaw === "string"
? templateRaw
: isRecord(templateRaw)
? JSON.stringify(templateRaw, null, 2)
: "{\n \"text\": \"{{ column_name }}\"\n}";
processors.push({
id: `p${index + 1}`,
// biome-ignore lint/style/useNamingConvention: api schema
processor_type: "schema_transform",
name,
template,
});
});
return processors;
}
export function importCanvasPayload(input: string): ImportResult {
const parsed = parseJson(input);
if (!parsed.data || !isRecord(parsed.data)) {
@ -41,6 +75,7 @@ export function importCanvasPayload(input: string): ImportResult {
const errors: string[] = [];
const configs: NodeConfig[] = [];
const processors = parseProcessors(recipe.processors);
const nameToId = new Map<string, string>();
let nextId = 1;
@ -129,6 +164,7 @@ export function importCanvasPayload(input: string): ImportResult {
configs: Object.fromEntries(configs.map((config) => [config.id, config])),
nodes,
edges,
processors,
nextId,
nextY: maxY + 140,
},

View file

@ -72,6 +72,7 @@ function parseSampler(
id: string,
errors: string[],
): SamplerConfig | null {
const drop = column.drop === true;
const samplerType = readString(column.sampler_type);
if (!samplerType || !SAMPLER_TYPES.includes(samplerType as SamplerType)) {
errors.push(`Sampler ${name}: unsupported sampler_type.`);
@ -99,6 +100,7 @@ function parseSampler(
// biome-ignore lint/style/useNamingConvention: api schema
sampler_type: "category",
name,
drop,
// biome-ignore lint/style/useNamingConvention: api schema
convert_to: normalizedConvertTo,
values,
@ -122,6 +124,7 @@ function parseSampler(
// biome-ignore lint/style/useNamingConvention: api schema
sampler_type: "subcategory",
name,
drop,
// biome-ignore lint/style/useNamingConvention: api schema
convert_to: normalizedConvertTo,
// biome-ignore lint/style/useNamingConvention: api schema
@ -137,6 +140,7 @@ function parseSampler(
// biome-ignore lint/style/useNamingConvention: api schema
sampler_type: "uniform",
name,
drop,
// biome-ignore lint/style/useNamingConvention: api schema
convert_to: normalizedConvertTo,
low: readNumberString(params.low),
@ -150,6 +154,7 @@ function parseSampler(
// biome-ignore lint/style/useNamingConvention: api schema
sampler_type: "gaussian",
name,
drop,
// biome-ignore lint/style/useNamingConvention: api schema
convert_to: normalizedConvertTo,
mean: readNumberString(params.mean),
@ -163,6 +168,7 @@ function parseSampler(
// biome-ignore lint/style/useNamingConvention: api schema
sampler_type: "bernoulli",
name,
drop,
// biome-ignore lint/style/useNamingConvention: api schema
convert_to: normalizedConvertTo,
p: readNumberString(params.p),
@ -175,6 +181,7 @@ function parseSampler(
// biome-ignore lint/style/useNamingConvention: api schema
sampler_type: "datetime",
name,
drop,
// biome-ignore lint/style/useNamingConvention: api schema
convert_to: normalizedConvertTo,
// biome-ignore lint/style/useNamingConvention: api schema
@ -197,6 +204,7 @@ function parseSampler(
// biome-ignore lint/style/useNamingConvention: api schema
sampler_type: "timedelta",
name,
drop,
// biome-ignore lint/style/useNamingConvention: api schema
convert_to: normalizedConvertTo,
// biome-ignore lint/style/useNamingConvention: api schema
@ -216,6 +224,7 @@ function parseSampler(
// biome-ignore lint/style/useNamingConvention: api schema
sampler_type: "uuid",
name,
drop,
// biome-ignore lint/style/useNamingConvention: api schema
convert_to: normalizedConvertTo,
// biome-ignore lint/style/useNamingConvention: api schema
@ -232,6 +241,7 @@ function parseSampler(
id,
kind: "sampler",
name,
drop,
// biome-ignore lint/style/useNamingConvention: api schema
sampler_type: samplerType as SamplerType,
// biome-ignore lint/style/useNamingConvention: api schema
@ -297,6 +307,7 @@ function parseLlm(
// biome-ignore lint/style/useNamingConvention: api schema
llm_type: llmType,
name,
drop: column.drop === true,
// biome-ignore lint/style/useNamingConvention: api schema
model_alias: readString(column.model_alias) ?? "",
prompt: readString(column.prompt) ?? "",
@ -378,6 +389,7 @@ function parseExpression(
id,
kind: "expression",
name,
drop: column.drop === true,
expr: readString(column.expr) ?? "",
dtype: normalized,
};

View file

@ -1,10 +1,15 @@
import type { Edge } from "@xyflow/react";
import type { CanvasNode, NodeConfig } from "../../types";
import type {
CanvasNode,
CanvasProcessorConfig,
NodeConfig,
} from "../../types";
export type CanvasSnapshot = {
configs: Record<string, NodeConfig>;
nodes: CanvasNode[];
edges: Edge[];
processors: CanvasProcessorConfig[];
nextId: number;
nextY: number;
};

View file

@ -68,6 +68,7 @@ export function makeSamplerConfig(
// biome-ignore lint/style/useNamingConvention: api schema
sampler_type: "category",
name,
drop: false,
values: ["A", "B", "C"],
weights: [null, null, null],
};
@ -79,6 +80,7 @@ export function makeSamplerConfig(
// biome-ignore lint/style/useNamingConvention: api schema
sampler_type: "subcategory",
name,
drop: false,
// biome-ignore lint/style/useNamingConvention: api schema
subcategory_parent: "",
// biome-ignore lint/style/useNamingConvention: api schema
@ -97,6 +99,7 @@ export function makeSamplerConfig(
// biome-ignore lint/style/useNamingConvention: api schema
sampler_type: "uniform",
name,
drop: false,
low: "0",
high: "1",
};
@ -108,6 +111,7 @@ export function makeSamplerConfig(
// biome-ignore lint/style/useNamingConvention: api schema
sampler_type: "gaussian",
name,
drop: false,
mean: "0",
std: "1",
};
@ -119,6 +123,7 @@ export function makeSamplerConfig(
// biome-ignore lint/style/useNamingConvention: api schema
sampler_type: "bernoulli",
name,
drop: false,
p: "0.5",
};
}
@ -129,6 +134,7 @@ export function makeSamplerConfig(
// biome-ignore lint/style/useNamingConvention: api schema
sampler_type: "datetime",
name,
drop: false,
// biome-ignore lint/style/useNamingConvention: api schema
datetime_start: "",
// biome-ignore lint/style/useNamingConvention: api schema
@ -144,6 +150,7 @@ export function makeSamplerConfig(
// biome-ignore lint/style/useNamingConvention: api schema
sampler_type: "timedelta",
name,
drop: false,
// biome-ignore lint/style/useNamingConvention: api schema
dt_min: "0",
// biome-ignore lint/style/useNamingConvention: api schema
@ -161,6 +168,7 @@ export function makeSamplerConfig(
// biome-ignore lint/style/useNamingConvention: api schema
sampler_type: "uuid",
name,
drop: false,
// biome-ignore lint/style/useNamingConvention: api schema
uuid_format: "",
};
@ -172,6 +180,7 @@ export function makeSamplerConfig(
// biome-ignore lint/style/useNamingConvention: api schema
sampler_type: "person_from_faker",
name,
drop: false,
// biome-ignore lint/style/useNamingConvention: api schema
person_locale: "",
// biome-ignore lint/style/useNamingConvention: api schema
@ -188,6 +197,7 @@ export function makeSamplerConfig(
// biome-ignore lint/style/useNamingConvention: api schema
sampler_type: "person",
name,
drop: false,
// biome-ignore lint/style/useNamingConvention: api schema
person_locale: "",
// biome-ignore lint/style/useNamingConvention: api schema
@ -221,6 +231,7 @@ export function makeLlmConfig(
// biome-ignore lint/style/useNamingConvention: api schema
llm_type: llmType,
name,
drop: false,
// biome-ignore lint/style/useNamingConvention: api schema
model_alias: "allenai/olmo-3.1-32b-instruct",
prompt:
@ -302,6 +313,7 @@ export function makeExpressionConfig(
id,
kind: "expression",
name: nextName(existing, "expr"),
drop: false,
expr: "",
dtype: "str",
};

View file

@ -1,5 +1,6 @@
import type { Edge } from "@xyflow/react";
import type {
CanvasProcessorConfig,
CategoryConditionalParams,
CanvasNode,
ExpressionConfig,
@ -316,6 +317,7 @@ function buildLlmColumn(
): Record<string, unknown> {
const base = {
name: config.name,
drop: config.drop ?? false,
// biome-ignore lint/style/useNamingConvention: api schema
model_alias: config.model_alias,
prompt: config.prompt,
@ -401,16 +403,52 @@ function buildExpressionColumn(
// biome-ignore lint/style/useNamingConvention: api schema
column_type: "expression",
name: config.name,
drop: config.drop ?? false,
expr: config.expr,
dtype: config.dtype,
};
}
function buildProcessors(
processors: CanvasProcessorConfig[],
errors: string[],
): Record<string, unknown>[] {
const output: Record<string, unknown>[] = [];
for (const processor of processors) {
if (processor.processor_type !== "schema_transform") {
continue;
}
const name = processor.name.trim();
if (!name) {
errors.push("Schema transform: name is required.");
continue;
}
const template = parseJsonObject(
processor.template,
`Schema transform ${name} template`,
errors,
);
if (!template) {
continue;
}
output.push({
// biome-ignore lint/style/useNamingConvention: api schema
processor_type: "schema_transform",
name,
// biome-ignore lint/style/useNamingConvention: api schema
build_stage: "post_batch",
template,
});
}
return output;
}
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: payload build
export function buildCanvasPayload(
configs: Record<string, NodeConfig>,
nodes: CanvasNode[],
edges: Edge[],
processors: CanvasProcessorConfig[] = [],
): CanvasPayloadResult {
const errors: string[] = [];
const columns: Record<string, unknown>[] = [];
@ -442,6 +480,7 @@ export function buildCanvasPayload(
// biome-ignore lint/style/useNamingConvention: api schema
column_type: "sampler",
name: config.name,
drop: config.drop ?? false,
// biome-ignore lint/style/useNamingConvention: api schema
sampler_type: config.sampler_type,
params: buildSamplerParams(config, errors),
@ -585,6 +624,7 @@ export function buildCanvasPayload(
},
];
});
const recipeProcessors = buildProcessors(processors, errors);
return {
errors,
@ -595,7 +635,7 @@ export function buildCanvasPayload(
// biome-ignore lint/style/useNamingConvention: api schema
model_configs: modelConfigs,
columns,
processors: [],
processors: recipeProcessors,
},
run: {
rows: 5,

View file

@ -0,0 +1,11 @@
import type { CanvasProcessorConfig } from "../types";
export function buildDefaultSchemaTransform(): CanvasProcessorConfig {
return {
id: "schema-transform-1",
// biome-ignore lint/style/useNamingConvention: api schema
processor_type: "schema_transform",
name: "schema_transform",
template: '{\n "text": "{{ column_name }}"\n}',
};
}