processors and drop column
This commit is contained in:
parent
d67efb8516
commit
1ca01e5d21
13 changed files with 410 additions and 10 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
11
studio/frontend/src/features/canvas-lab/utils/processors.ts
Normal file
11
studio/frontend/src/features/canvas-lab/utils/processors.ts
Normal 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}',
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue