-
-
-
-
Create Data Recipe
-
- Design synthetic-data pipelines with Data Designer.
-
- {statusMessage && (
-
- {statusMessage.text}
-
- )}
-
-
-
-
-
-
+
,
+ name: string,
+): string | null {
+ const entry = Object.entries(configs).find(
+ ([, config]) => config.name === name,
+ );
+ return entry ? entry[0] : null;
+}
+
+function addRecipeEdge(edges: Edge[], source: string, target: string): Edge[] {
+ return addEdge(
+ {
+ source,
+ target,
+ sourceHandle: HANDLE_IDS.dataOut,
+ targetHandle: HANDLE_IDS.dataIn,
+ type: "canvas",
+ },
+ edges,
+ );
+}
+
+function addSemanticEdge(edges: Edge[], source: string, target: string): Edge[] {
+ return addEdge(
+ {
+ source,
+ target,
+ sourceHandle: HANDLE_IDS.semanticOut,
+ targetHandle: HANDLE_IDS.semanticIn,
+ type: "semantic",
+ },
+ edges,
+ );
+}
+
+function removeTargetEdges(edges: Edge[], targetId: string): Edge[] {
+ return edges.filter((edge) => edge.target !== targetId);
+}
+
+function removeTargetEdgesBySource(
+ edges: Edge[],
+ configs: Record,
+ targetId: string,
+ shouldRemove: (source: NodeConfig | undefined) => boolean,
+): Edge[] {
+ return edges.filter((edge) => {
+ if (edge.target !== targetId) {
+ return true;
+ }
+ return !shouldRemove(configs[edge.source]);
+ });
+}
+
+export function syncEdgesForConfigPatch(
+ current: NodeConfig,
+ patch: Partial,
+ configs: Record,
+ edges: Edge[],
+): Edge[] {
+ let nextEdges = edges;
+
+ const hasParentPatch = Object.prototype.hasOwnProperty.call(
+ patch,
+ "subcategory_parent",
+ );
+ if (isSubcategoryConfig(current) && hasParentPatch) {
+ const nextParent = (patch as Partial).subcategory_parent ?? "";
+ const parentId = nextParent ? findNodeIdByName(configs, nextParent) : null;
+ nextEdges = removeTargetEdges(nextEdges, current.id);
+ if (parentId) {
+ nextEdges = addRecipeEdge(nextEdges, parentId, current.id);
+ }
+ }
+
+ const hasProviderPatch = Object.prototype.hasOwnProperty.call(
+ patch,
+ "provider",
+ );
+ if (current.kind === "model_config" && hasProviderPatch) {
+ const nextProvider = (patch as Partial).provider ?? "";
+ nextEdges = removeTargetEdgesBySource(
+ nextEdges,
+ configs,
+ current.id,
+ (source) => Boolean(source && source.kind === "model_provider"),
+ );
+ if (nextProvider) {
+ const providerId = findNodeIdByName(configs, nextProvider);
+ if (providerId) {
+ nextEdges = addSemanticEdge(nextEdges, providerId, current.id);
+ }
+ }
+ }
+
+ const hasReferencePatch = Object.prototype.hasOwnProperty.call(
+ patch,
+ "reference_column_name",
+ );
+ if (
+ current.kind === "sampler" &&
+ current.sampler_type === "timedelta" &&
+ hasReferencePatch
+ ) {
+ const nextReference =
+ (patch as Partial).reference_column_name ?? "";
+ nextEdges = removeTargetEdgesBySource(
+ nextEdges,
+ configs,
+ current.id,
+ (source) =>
+ Boolean(
+ source &&
+ source.kind === "sampler" &&
+ source.sampler_type === "datetime",
+ ),
+ );
+ if (nextReference) {
+ const referenceId = findNodeIdByName(configs, nextReference);
+ const source = referenceId ? configs[referenceId] : null;
+ if (
+ referenceId &&
+ source &&
+ source.kind === "sampler" &&
+ source.sampler_type === "datetime"
+ ) {
+ nextEdges = addRecipeEdge(nextEdges, referenceId, current.id);
+ }
+ }
+ }
+
+ const hasModelAliasPatch = Object.prototype.hasOwnProperty.call(
+ patch,
+ "model_alias",
+ );
+ if (current.kind === "llm" && hasModelAliasPatch) {
+ const nextAlias =
+ (patch as Partial & { model_alias?: string }).model_alias ?? "";
+ nextEdges = removeTargetEdgesBySource(
+ nextEdges,
+ configs,
+ current.id,
+ (source) => Boolean(source && source.kind === "model_config"),
+ );
+ if (nextAlias) {
+ const modelConfigId = findNodeIdByName(configs, nextAlias);
+ if (modelConfigId) {
+ nextEdges = addSemanticEdge(nextEdges, modelConfigId, current.id);
+ }
+ }
+ }
+
+ return nextEdges;
+}
+
+export function syncSubcategoryConfigsForCategoryUpdate(
+ current: NodeConfig,
+ next: NodeConfig,
+ configs: Record,
+ oldName: string,
+ newName: string,
+ nameChanged: boolean,
+): Record {
+ if (!isCategoryConfig(current)) {
+ return configs;
+ }
+ const nextCategory = isCategoryConfig(next) ? next : current;
+ const oldValues = current.values ?? [];
+ const newValues = nextCategory.values ?? [];
+ const valuesChanged =
+ oldValues.length !== newValues.length ||
+ oldValues.some((value, index) => value !== newValues[index]);
+
+ let nextConfigs = configs;
+ for (const config of Object.values(configs)) {
+ if (!isSubcategoryConfig(config)) {
+ continue;
+ }
+ if (config.subcategory_parent !== oldName) {
+ continue;
+ }
+ const mapping = config.subcategory_mapping ?? {};
+ const nextMapping: Record = {};
+ for (const value of newValues) {
+ nextMapping[value] = mapping[value] ?? [];
+ }
+ const updated: NodeConfig = {
+ ...config,
+ // biome-ignore lint/style/useNamingConvention: api schema
+ subcategory_parent: nameChanged ? newName : config.subcategory_parent,
+ // biome-ignore lint/style/useNamingConvention: api schema
+ subcategory_mapping: valuesChanged ? nextMapping : mapping,
+ };
+ nextConfigs = { ...nextConfigs, [config.id]: updated };
+ }
+ return nextConfigs;
+}
diff --git a/studio/frontend/src/features/recipe-studio/stores/helpers/node-updates.ts b/studio/frontend/src/features/recipe-studio/stores/helpers/node-updates.ts
new file mode 100644
index 0000000000..6d17986595
--- /dev/null
+++ b/studio/frontend/src/features/recipe-studio/stores/helpers/node-updates.ts
@@ -0,0 +1,78 @@
+import { DEFAULT_NODE_WIDTH } from "../../constants";
+import type {
+ RecipeNode,
+ LayoutDirection,
+ NodeConfig,
+} from "../../types";
+import { nodeDataFromConfig } from "../../utils";
+import { getConfigUiMode } from "../../components/inline/inline-policy";
+
+export type NodeUpdateState = {
+ configs: Record;
+ nodes: RecipeNode[];
+ nextId: number;
+ nextY: number;
+};
+
+export type NodeUpdateResult = {
+ configs: Record;
+ nodes: RecipeNode[];
+ nextId: number;
+ nextY: number;
+ activeConfigId: string;
+ dialogOpen: boolean;
+};
+
+export function updateNodeData(
+ nodes: RecipeNode[],
+ id: string,
+ config: NodeConfig,
+ layoutDirection: LayoutDirection,
+): RecipeNode[] {
+ return nodes.map((node) =>
+ node.id === id
+ ? { ...node, data: nodeDataFromConfig(config, layoutDirection) }
+ : node,
+ );
+}
+
+export function buildNodeUpdate(
+ state: NodeUpdateState,
+ config: NodeConfig,
+ layoutDirection: LayoutDirection,
+): NodeUpdateResult {
+ const node: RecipeNode = {
+ id: config.id,
+ type: "builder",
+ position: { x: 0, y: state.nextY },
+ data: nodeDataFromConfig(config, layoutDirection),
+ style: { width: DEFAULT_NODE_WIDTH },
+ selected: true,
+ };
+ const mode = getConfigUiMode(config);
+ return {
+ configs: { ...state.configs, [config.id]: config },
+ nodes: [...state.nodes.map((item) => ({ ...item, selected: false })), node],
+ nextId: state.nextId + 1,
+ nextY: state.nextY + 140,
+ activeConfigId: config.id,
+ dialogOpen: mode === "dialog",
+ };
+}
+
+export function applyLayoutDirectionToNodes(
+ nodes: RecipeNode[],
+ configs: Record,
+ layoutDirection: LayoutDirection,
+): RecipeNode[] {
+ return nodes.map((node) => {
+ const config = configs[node.id];
+ if (config) {
+ return { ...node, data: nodeDataFromConfig(config, layoutDirection) };
+ }
+ return {
+ ...node,
+ data: { ...node.data, layoutDirection },
+ };
+ });
+}
diff --git a/studio/frontend/src/features/recipe-studio/stores/helpers/reference-sync.ts b/studio/frontend/src/features/recipe-studio/stores/helpers/reference-sync.ts
new file mode 100644
index 0000000000..d3245e513f
--- /dev/null
+++ b/studio/frontend/src/features/recipe-studio/stores/helpers/reference-sync.ts
@@ -0,0 +1,169 @@
+import type {
+ LlmConfig,
+ ModelConfig,
+ NodeConfig,
+ SamplerConfig,
+} from "../../types";
+import { removeRef, replaceRef } from "../../utils/refs";
+
+function updateTemplateFields(
+ config: NodeConfig,
+ updater: (value: string) => string,
+): NodeConfig {
+ if (config.kind === "llm") {
+ const nextPrompt = updater(config.prompt);
+ const nextSystem = updater(config.system_prompt);
+ const nextOutput =
+ typeof config.output_format === "string"
+ ? updater(config.output_format)
+ : config.output_format;
+ if (
+ nextPrompt === config.prompt &&
+ nextSystem === config.system_prompt &&
+ nextOutput === config.output_format
+ ) {
+ return config;
+ }
+ return {
+ ...config,
+ prompt: nextPrompt,
+ // biome-ignore lint/style/useNamingConvention: api schema
+ system_prompt: nextSystem,
+ // biome-ignore lint/style/useNamingConvention: api schema
+ output_format: nextOutput,
+ };
+ }
+ if (config.kind === "expression") {
+ const nextExpr = updater(config.expr);
+ if (nextExpr === config.expr) {
+ return config;
+ }
+ return { ...config, expr: nextExpr };
+ }
+ return config;
+}
+
+export function applyRenameToConfig(
+ config: NodeConfig,
+ from: string,
+ to: string,
+): NodeConfig {
+ let next = updateTemplateFields(config, (value) =>
+ replaceRef(value, from, to),
+ );
+ if (
+ config.kind === "sampler" &&
+ config.sampler_type === "subcategory" &&
+ config.subcategory_parent === from
+ ) {
+ const base = next as SamplerConfig;
+ next = {
+ ...base,
+ // biome-ignore lint/style/useNamingConvention: api schema
+ subcategory_parent: to,
+ };
+ }
+ if (
+ config.kind === "sampler" &&
+ config.sampler_type === "timedelta" &&
+ config.reference_column_name === from
+ ) {
+ const base = next as SamplerConfig;
+ next = {
+ ...base,
+ // biome-ignore lint/style/useNamingConvention: api schema
+ reference_column_name: to,
+ };
+ }
+ if (config.kind === "model_config" && config.provider === from) {
+ const base = next as ModelConfig;
+ next = { ...base, provider: to };
+ }
+ if (config.kind === "llm" && config.model_alias === from) {
+ const base = next as LlmConfig;
+ next = { ...base, model_alias: to };
+ }
+ return next;
+}
+
+export function applyRemovalToConfig(
+ config: NodeConfig,
+ ref: string,
+): NodeConfig {
+ let next = updateTemplateFields(config, (value) => removeRef(value, ref));
+ if (
+ config.kind === "sampler" &&
+ config.sampler_type === "subcategory" &&
+ config.subcategory_parent === ref
+ ) {
+ const base = next as SamplerConfig;
+ next = {
+ ...base,
+ // biome-ignore lint/style/useNamingConvention: api schema
+ subcategory_parent: "",
+ // biome-ignore lint/style/useNamingConvention: api schema
+ subcategory_mapping: {},
+ };
+ }
+ if (
+ config.kind === "sampler" &&
+ config.sampler_type === "timedelta" &&
+ config.reference_column_name === ref
+ ) {
+ const base = next as SamplerConfig;
+ next = {
+ ...base,
+ // biome-ignore lint/style/useNamingConvention: api schema
+ reference_column_name: "",
+ };
+ }
+ if (config.kind === "model_config" && config.provider === ref) {
+ const base = next as ModelConfig;
+ next = { ...base, provider: "" };
+ }
+ if (config.kind === "llm" && config.model_alias === ref) {
+ const base = next as LlmConfig;
+ next = { ...base, model_alias: "" };
+ }
+ return next;
+}
+
+function applyConfigTransform(
+ configs: Record,
+ transform: (config: NodeConfig) => NodeConfig,
+): Record {
+ let next = configs;
+ for (const [id, config] of Object.entries(configs)) {
+ const updated = transform(config);
+ if (updated !== config) {
+ if (next === configs) {
+ next = { ...configs };
+ }
+ next[id] = updated;
+ }
+ }
+ return next;
+}
+
+export function applyRenameToConfigs(
+ configs: Record,
+ from: string,
+ to: string,
+): Record {
+ if (!from || from === to) {
+ return configs;
+ }
+ return applyConfigTransform(configs, (config) =>
+ applyRenameToConfig(config, from, to),
+ );
+}
+
+export function applyRemovalToConfigs(
+ configs: Record,
+ ref: string,
+): Record {
+ if (!ref) {
+ return configs;
+ }
+ return applyConfigTransform(configs, (config) => applyRemovalToConfig(config, ref));
+}
diff --git a/studio/frontend/src/features/recipe-studio/stores/recipe-studio-helpers.ts b/studio/frontend/src/features/recipe-studio/stores/recipe-studio-helpers.ts
index 9a14791156..4515ba1e69 100644
--- a/studio/frontend/src/features/recipe-studio/stores/recipe-studio-helpers.ts
+++ b/studio/frontend/src/features/recipe-studio/stores/recipe-studio-helpers.ts
@@ -1,444 +1,17 @@
-import { type Edge, addEdge } from "@xyflow/react";
-import { DEFAULT_NODE_WIDTH } from "../constants";
-import type {
- RecipeNode,
- LayoutDirection,
- LlmConfig,
- ModelConfig,
- NodeConfig,
- SamplerConfig,
-} from "../types";
-import { isCategoryConfig, isSubcategoryConfig, nodeDataFromConfig } from "../utils";
-import { HANDLE_IDS } from "../utils/handles";
-import { removeRef, replaceRef } from "../utils/refs";
-import { getConfigUiMode } from "../components/inline/inline-policy";
-
-type NodeUpdateState = {
- configs: Record;
- nodes: RecipeNode[];
- nextId: number;
- nextY: number;
-};
-
-type NodeUpdateResult = {
- configs: Record;
- nodes: RecipeNode[];
- nextId: number;
- nextY: number;
- activeConfigId: string;
- dialogOpen: boolean;
-};
-
-export function updateNodeData(
- nodes: RecipeNode[],
- id: string,
- config: NodeConfig,
- layoutDirection: LayoutDirection,
-): RecipeNode[] {
- return nodes.map((node) =>
- node.id === id
- ? { ...node, data: nodeDataFromConfig(config, layoutDirection) }
- : node,
- );
-}
-
-function findNodeIdByName(
- configs: Record,
- name: string,
-): string | null {
- const entry = Object.entries(configs).find(
- ([, config]) => config.name === name,
- );
- return entry ? entry[0] : null;
-}
-
-function addRecipeEdge(edges: Edge[], source: string, target: string): Edge[] {
- return addEdge(
- {
- source,
- target,
- sourceHandle: HANDLE_IDS.dataOut,
- targetHandle: HANDLE_IDS.dataIn,
- type: "canvas",
- },
- edges,
- );
-}
-
-function addSemanticEdge(edges: Edge[], source: string, target: string): Edge[] {
- return addEdge(
- {
- source,
- target,
- sourceHandle: HANDLE_IDS.semanticOut,
- targetHandle: HANDLE_IDS.semanticIn,
- type: "semantic",
- },
- edges,
- );
-}
-
-function removeTargetEdges(edges: Edge[], targetId: string): Edge[] {
- return edges.filter((edge) => edge.target !== targetId);
-}
-
-function removeTargetEdgesBySource(
- edges: Edge[],
- configs: Record,
- targetId: string,
- shouldRemove: (source: NodeConfig | undefined) => boolean,
-): Edge[] {
- return edges.filter((edge) => {
- if (edge.target !== targetId) {
- return true;
- }
- return !shouldRemove(configs[edge.source]);
- });
-}
-
-export function buildNodeUpdate(
- state: NodeUpdateState,
- config: NodeConfig,
- layoutDirection: LayoutDirection,
-): NodeUpdateResult {
- const node: RecipeNode = {
- id: config.id,
- type: "builder",
- position: { x: 0, y: state.nextY },
- data: nodeDataFromConfig(config, layoutDirection),
- style: { width: DEFAULT_NODE_WIDTH },
- selected: true,
- };
- const mode = getConfigUiMode(config);
- return {
- configs: { ...state.configs, [config.id]: config },
- nodes: [...state.nodes.map((item) => ({ ...item, selected: false })), node],
- nextId: state.nextId + 1,
- nextY: state.nextY + 140,
- activeConfigId: config.id,
- dialogOpen: mode === "dialog",
- };
-}
-
-export function applyLayoutDirectionToNodes(
- nodes: RecipeNode[],
- configs: Record,
- layoutDirection: LayoutDirection,
-): RecipeNode[] {
- return nodes.map((node) => {
- const config = configs[node.id];
- if (config) {
- return { ...node, data: nodeDataFromConfig(config, layoutDirection) };
- }
- return {
- ...node,
- data: { ...node.data, layoutDirection },
- };
- });
-}
-
-export function syncEdgesForConfigPatch(
- current: NodeConfig,
- patch: Partial,
- configs: Record,
- edges: Edge[],
-): Edge[] {
- let nextEdges = edges;
-
- const hasParentPatch = Object.prototype.hasOwnProperty.call(
- patch,
- "subcategory_parent",
- );
- if (isSubcategoryConfig(current) && hasParentPatch) {
- const nextParent = (patch as Partial).subcategory_parent ?? "";
- const parentId = nextParent ? findNodeIdByName(configs, nextParent) : null;
- nextEdges = removeTargetEdges(nextEdges, current.id);
- if (parentId) {
- nextEdges = addRecipeEdge(nextEdges, parentId, current.id);
- }
- }
-
- const hasProviderPatch = Object.prototype.hasOwnProperty.call(
- patch,
- "provider",
- );
- if (current.kind === "model_config" && hasProviderPatch) {
- const nextProvider = (patch as Partial).provider ?? "";
- nextEdges = removeTargetEdgesBySource(
- nextEdges,
- configs,
- current.id,
- (source) => Boolean(source && source.kind === "model_provider"),
- );
- if (nextProvider) {
- const providerId = findNodeIdByName(configs, nextProvider);
- if (providerId) {
- nextEdges = addSemanticEdge(nextEdges, providerId, current.id);
- }
- }
- }
-
- const hasReferencePatch = Object.prototype.hasOwnProperty.call(
- patch,
- "reference_column_name",
- );
- if (
- current.kind === "sampler" &&
- current.sampler_type === "timedelta" &&
- hasReferencePatch
- ) {
- const nextReference =
- (patch as Partial).reference_column_name ?? "";
- nextEdges = removeTargetEdgesBySource(
- nextEdges,
- configs,
- current.id,
- (source) =>
- Boolean(
- source &&
- source.kind === "sampler" &&
- source.sampler_type === "datetime",
- ),
- );
- if (nextReference) {
- const referenceId = findNodeIdByName(configs, nextReference);
- const source = referenceId ? configs[referenceId] : null;
- if (
- referenceId &&
- source &&
- source.kind === "sampler" &&
- source.sampler_type === "datetime"
- ) {
- nextEdges = addRecipeEdge(nextEdges, referenceId, current.id);
- }
- }
- }
-
- const hasModelAliasPatch = Object.prototype.hasOwnProperty.call(
- patch,
- "model_alias",
- );
- if (current.kind === "llm" && hasModelAliasPatch) {
- const nextAlias =
- (patch as Partial & { model_alias?: string }).model_alias ?? "";
- nextEdges = removeTargetEdgesBySource(
- nextEdges,
- configs,
- current.id,
- (source) => Boolean(source && source.kind === "model_config"),
- );
- if (nextAlias) {
- const modelConfigId = findNodeIdByName(configs, nextAlias);
- if (modelConfigId) {
- nextEdges = addSemanticEdge(nextEdges, modelConfigId, current.id);
- }
- }
- }
-
- return nextEdges;
-}
-
-export function syncSubcategoryConfigsForCategoryUpdate(
- current: NodeConfig,
- next: NodeConfig,
- configs: Record,
- oldName: string,
- newName: string,
- nameChanged: boolean,
-): Record {
- if (!isCategoryConfig(current)) {
- return configs;
- }
- const nextCategory = isCategoryConfig(next) ? next : current;
- const oldValues = current.values ?? [];
- const newValues = nextCategory.values ?? [];
- const valuesChanged =
- oldValues.length !== newValues.length ||
- oldValues.some((value, index) => value !== newValues[index]);
-
- let nextConfigs = configs;
- for (const config of Object.values(configs)) {
- if (!isSubcategoryConfig(config)) {
- continue;
- }
- if (config.subcategory_parent !== oldName) {
- continue;
- }
- const mapping = config.subcategory_mapping ?? {};
- const nextMapping: Record = {};
- for (const value of newValues) {
- nextMapping[value] = mapping[value] ?? [];
- }
- const updated: NodeConfig = {
- ...config,
- // biome-ignore lint/style/useNamingConvention: api schema
- subcategory_parent: nameChanged ? newName : config.subcategory_parent,
- // biome-ignore lint/style/useNamingConvention: api schema
- subcategory_mapping: valuesChanged ? nextMapping : mapping,
- };
- nextConfigs = { ...nextConfigs, [config.id]: updated };
- }
- return nextConfigs;
-}
-
-function updateTemplateFields(
- config: NodeConfig,
- updater: (value: string) => string,
-): NodeConfig {
- if (config.kind === "llm") {
- const nextPrompt = updater(config.prompt);
- const nextSystem = updater(config.system_prompt);
- const nextOutput =
- typeof config.output_format === "string"
- ? updater(config.output_format)
- : config.output_format;
- if (
- nextPrompt === config.prompt &&
- nextSystem === config.system_prompt &&
- nextOutput === config.output_format
- ) {
- return config;
- }
- return {
- ...config,
- prompt: nextPrompt,
- // biome-ignore lint/style/useNamingConvention: api schema
- system_prompt: nextSystem,
- // biome-ignore lint/style/useNamingConvention: api schema
- output_format: nextOutput,
- };
- }
- if (config.kind === "expression") {
- const nextExpr = updater(config.expr);
- if (nextExpr === config.expr) {
- return config;
- }
- return { ...config, expr: nextExpr };
- }
- return config;
-}
-
-export function applyRenameToConfig(
- config: NodeConfig,
- from: string,
- to: string,
-): NodeConfig {
- let next = updateTemplateFields(config, (value) =>
- replaceRef(value, from, to),
- );
- if (
- config.kind === "sampler" &&
- config.sampler_type === "subcategory" &&
- config.subcategory_parent === from
- ) {
- const base = next as SamplerConfig;
- next = {
- ...base,
- // biome-ignore lint/style/useNamingConvention: api schema
- subcategory_parent: to,
- };
- }
- if (
- config.kind === "sampler" &&
- config.sampler_type === "timedelta" &&
- config.reference_column_name === from
- ) {
- const base = next as SamplerConfig;
- next = {
- ...base,
- // biome-ignore lint/style/useNamingConvention: api schema
- reference_column_name: to,
- };
- }
- if (config.kind === "model_config" && config.provider === from) {
- const base = next as ModelConfig;
- next = { ...base, provider: to };
- }
- if (config.kind === "llm" && config.model_alias === from) {
- const base = next as LlmConfig;
- next = { ...base, model_alias: to };
- }
- return next;
-}
-
-export function applyRemovalToConfig(
- config: NodeConfig,
- ref: string,
-): NodeConfig {
- let next = updateTemplateFields(config, (value) => removeRef(value, ref));
- if (
- config.kind === "sampler" &&
- config.sampler_type === "subcategory" &&
- config.subcategory_parent === ref
- ) {
- const base = next as SamplerConfig;
- next = {
- ...base,
- // biome-ignore lint/style/useNamingConvention: api schema
- subcategory_parent: "",
- // biome-ignore lint/style/useNamingConvention: api schema
- subcategory_mapping: {},
- };
- }
- if (
- config.kind === "sampler" &&
- config.sampler_type === "timedelta" &&
- config.reference_column_name === ref
- ) {
- const base = next as SamplerConfig;
- next = {
- ...base,
- // biome-ignore lint/style/useNamingConvention: api schema
- reference_column_name: "",
- };
- }
- if (config.kind === "model_config" && config.provider === ref) {
- const base = next as ModelConfig;
- next = { ...base, provider: "" };
- }
- if (config.kind === "llm" && config.model_alias === ref) {
- const base = next as LlmConfig;
- next = { ...base, model_alias: "" };
- }
- return next;
-}
-
-export function applyRenameToConfigs(
- configs: Record,
- from: string,
- to: string,
-): Record {
- if (!from || from === to) {
- return configs;
- }
- return applyConfigTransform(configs, (config) =>
- applyRenameToConfig(config, from, to),
- );
-}
-
-function applyConfigTransform(
- configs: Record,
- transform: (config: NodeConfig) => NodeConfig,
-): Record {
- let next = configs;
- for (const [id, config] of Object.entries(configs)) {
- const updated = transform(config);
- if (updated !== config) {
- if (next === configs) {
- next = { ...configs };
- }
- next[id] = updated;
- }
- }
- return next;
-}
-
-export function applyRemovalToConfigs(
- configs: Record,
- ref: string,
-): Record {
- if (!ref) {
- return configs;
- }
- return applyConfigTransform(configs, (config) => applyRemovalToConfig(config, ref));
-}
+export {
+ applyLayoutDirectionToNodes,
+ buildNodeUpdate,
+ type NodeUpdateResult,
+ type NodeUpdateState,
+ updateNodeData,
+} from "./helpers/node-updates";
+export {
+ syncEdgesForConfigPatch,
+ syncSubcategoryConfigsForCategoryUpdate,
+} from "./helpers/edge-sync";
+export {
+ applyRemovalToConfig,
+ applyRemovalToConfigs,
+ applyRenameToConfig,
+ applyRenameToConfigs,
+} from "./helpers/reference-sync";
diff --git a/studio/frontend/src/features/recipe-studio/utils/config-factories.ts b/studio/frontend/src/features/recipe-studio/utils/config-factories.ts
new file mode 100644
index 0000000000..7b8f0814a9
--- /dev/null
+++ b/studio/frontend/src/features/recipe-studio/utils/config-factories.ts
@@ -0,0 +1,277 @@
+import type {
+ ExpressionConfig,
+ LlmConfig,
+ LlmType,
+ ModelConfig,
+ ModelProviderConfig,
+ NodeConfig,
+ SamplerConfig,
+ SamplerType,
+} from "../types";
+import { nextName } from "./naming";
+
+export function makeSamplerConfig(
+ id: string,
+ samplerType: SamplerType,
+ existing: NodeConfig[],
+): SamplerConfig {
+ const namePrefix =
+ samplerType === "subcategory" ? "subcategory" : samplerType;
+ const name = nextName(existing, namePrefix);
+ if (samplerType === "category") {
+ return {
+ id,
+ kind: "sampler",
+ // biome-ignore lint/style/useNamingConvention: api schema
+ sampler_type: "category",
+ name,
+ drop: false,
+ values: ["A", "B", "C"],
+ weights: [null, null, null],
+ };
+ }
+ if (samplerType === "subcategory") {
+ return {
+ id,
+ kind: "sampler",
+ // 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
+ subcategory_mapping: {
+ // biome-ignore lint/style/useNamingConvention: sample values
+ A: ["A1", "A2"],
+ // biome-ignore lint/style/useNamingConvention: sample values
+ B: ["B1", "B2"],
+ },
+ };
+ }
+ if (samplerType === "uniform") {
+ return {
+ id,
+ kind: "sampler",
+ // biome-ignore lint/style/useNamingConvention: api schema
+ sampler_type: "uniform",
+ name,
+ drop: false,
+ low: "0",
+ high: "1",
+ };
+ }
+ if (samplerType === "gaussian") {
+ return {
+ id,
+ kind: "sampler",
+ // biome-ignore lint/style/useNamingConvention: api schema
+ sampler_type: "gaussian",
+ name,
+ drop: false,
+ mean: "0",
+ std: "1",
+ };
+ }
+ if (samplerType === "bernoulli") {
+ return {
+ id,
+ kind: "sampler",
+ // biome-ignore lint/style/useNamingConvention: api schema
+ sampler_type: "bernoulli",
+ name,
+ drop: false,
+ p: "0.5",
+ };
+ }
+ if (samplerType === "datetime") {
+ return {
+ id,
+ kind: "sampler",
+ // 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
+ datetime_end: "",
+ // biome-ignore lint/style/useNamingConvention: api schema
+ datetime_unit: "day",
+ };
+ }
+ if (samplerType === "timedelta") {
+ return {
+ id,
+ kind: "sampler",
+ // 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
+ dt_max: "1",
+ // biome-ignore lint/style/useNamingConvention: api schema
+ reference_column_name: "",
+ // biome-ignore lint/style/useNamingConvention: api schema
+ timedelta_unit: "D",
+ };
+ }
+ if (samplerType === "uuid") {
+ return {
+ id,
+ kind: "sampler",
+ // biome-ignore lint/style/useNamingConvention: api schema
+ sampler_type: "uuid",
+ name,
+ drop: false,
+ // biome-ignore lint/style/useNamingConvention: api schema
+ uuid_format: "",
+ };
+ }
+ if (samplerType === "person_from_faker") {
+ return {
+ id,
+ kind: "sampler",
+ // 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
+ person_sex: "",
+ // biome-ignore lint/style/useNamingConvention: api schema
+ person_age_range: "",
+ // biome-ignore lint/style/useNamingConvention: api schema
+ person_city: "",
+ };
+ }
+ return {
+ id,
+ kind: "sampler",
+ // 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
+ person_sex: "",
+ // biome-ignore lint/style/useNamingConvention: api schema
+ person_age_range: "",
+ // biome-ignore lint/style/useNamingConvention: api schema
+ person_city: "",
+ // biome-ignore lint/style/useNamingConvention: api schema
+ person_with_synthetic_personas: false,
+ };
+}
+
+export function makeLlmConfig(
+ id: string,
+ llmType: LlmType,
+ existing: NodeConfig[],
+): LlmConfig {
+ let namePrefix = "llm_text";
+ if (llmType === "structured") {
+ namePrefix = "llm_structured";
+ } else if (llmType === "code") {
+ namePrefix = "llm_code";
+ } else if (llmType === "judge") {
+ namePrefix = "llm_judge";
+ }
+ const name = nextName(existing, namePrefix);
+ return {
+ id,
+ kind: "llm",
+ // 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:
+ llmType === "judge"
+ ? "Evaluate the content using the scoring criteria below."
+ : "Write a response.",
+ // biome-ignore lint/style/useNamingConvention: api schema
+ system_prompt: "",
+ // biome-ignore lint/style/useNamingConvention: api schema
+ code_lang: llmType === "code" ? "python" : undefined,
+ // biome-ignore lint/style/useNamingConvention: api schema
+ output_format:
+ llmType === "structured" ? '{\n "field": "string"\n}' : undefined,
+ scores:
+ llmType === "judge"
+ ? [
+ {
+ name: "Quality",
+ description: "Overall quality based on the criteria.",
+ options: [
+ { value: "1", description: "Poor" },
+ { value: "3", description: "Acceptable" },
+ { value: "5", description: "Excellent" },
+ ],
+ },
+ ]
+ : undefined,
+ };
+}
+
+export function makeModelProviderConfig(
+ id: string,
+ existing: NodeConfig[],
+): ModelProviderConfig {
+ return {
+ id,
+ kind: "model_provider",
+ name: nextName(existing, "provider"),
+ endpoint: "",
+ // biome-ignore lint/style/useNamingConvention: api schema
+ provider_type: "openai",
+ // biome-ignore lint/style/useNamingConvention: api schema
+ api_key_env: "",
+ // biome-ignore lint/style/useNamingConvention: api schema
+ api_key: "",
+ // biome-ignore lint/style/useNamingConvention: api schema
+ extra_headers: "",
+ // biome-ignore lint/style/useNamingConvention: api schema
+ extra_body: "",
+ };
+}
+
+export function makeModelConfig(
+ id: string,
+ existing: NodeConfig[],
+): ModelConfig {
+ return {
+ id,
+ kind: "model_config",
+ name: nextName(existing, "model"),
+ model: "",
+ provider: "",
+ // biome-ignore lint/style/useNamingConvention: api schema
+ inference_temperature: "0.7",
+ // biome-ignore lint/style/useNamingConvention: api schema
+ inference_max_tokens: "256",
+ // biome-ignore lint/style/useNamingConvention: api schema
+ inference_top_p: "",
+ // biome-ignore lint/style/useNamingConvention: api schema
+ skip_health_check: false,
+ };
+}
+
+export function makeExpressionConfig(
+ id: string,
+ existing: NodeConfig[],
+): ExpressionConfig {
+ return {
+ id,
+ kind: "expression",
+ name: nextName(existing, "expr"),
+ drop: false,
+ expr: "",
+ dtype: "str",
+ };
+}
diff --git a/studio/frontend/src/features/recipe-studio/utils/config-labels.ts b/studio/frontend/src/features/recipe-studio/utils/config-labels.ts
new file mode 100644
index 0000000000..b70d7566b1
--- /dev/null
+++ b/studio/frontend/src/features/recipe-studio/utils/config-labels.ts
@@ -0,0 +1,44 @@
+import type {
+ ExpressionDtype,
+ LlmType,
+ SamplerType,
+} from "../types";
+
+const SAMPLER_LABELS: Record = {
+ category: "Category",
+ subcategory: "Subcategory",
+ uniform: "Uniform",
+ gaussian: "Gaussian",
+ bernoulli: "Bernoulli",
+ datetime: "Datetime",
+ timedelta: "Timedelta",
+ uuid: "UUID",
+ person: "Person",
+ person_from_faker: "Person (Faker)",
+};
+
+const LLM_LABELS: Record = {
+ text: "LLM Text",
+ structured: "LLM Structured",
+ code: "LLM Code",
+ judge: "LLM Judge",
+};
+
+const EXPRESSION_LABELS: Record = {
+ str: "Text",
+ int: "Int",
+ float: "Float",
+ bool: "Bool",
+};
+
+export function labelForSampler(type: SamplerType): string {
+ return SAMPLER_LABELS[type] ?? "Sampler";
+}
+
+export function labelForLlm(type: LlmType): string {
+ return LLM_LABELS[type] ?? "LLM";
+}
+
+export function labelForExpression(type: ExpressionDtype): string {
+ return EXPRESSION_LABELS[type] ?? "Expression";
+}
diff --git a/studio/frontend/src/features/recipe-studio/utils/config-type-guards.ts b/studio/frontend/src/features/recipe-studio/utils/config-type-guards.ts
new file mode 100644
index 0000000000..855cca8c6a
--- /dev/null
+++ b/studio/frontend/src/features/recipe-studio/utils/config-type-guards.ts
@@ -0,0 +1,42 @@
+import type {
+ ExpressionConfig,
+ LlmConfig,
+ NodeConfig,
+ SamplerConfig,
+} from "../types";
+
+export function isSamplerConfig(
+ config: NodeConfig | null | undefined,
+): config is SamplerConfig {
+ return Boolean(config && config.kind === "sampler");
+}
+
+export function isCategoryConfig(
+ config: NodeConfig | null | undefined,
+): config is SamplerConfig {
+ return Boolean(
+ config && config.kind === "sampler" && config.sampler_type === "category",
+ );
+}
+
+export function isSubcategoryConfig(
+ config: NodeConfig | null | undefined,
+): config is SamplerConfig {
+ return Boolean(
+ config &&
+ config.kind === "sampler" &&
+ config.sampler_type === "subcategory",
+ );
+}
+
+export function isLlmConfig(
+ config: NodeConfig | null | undefined,
+): config is LlmConfig {
+ return Boolean(config && config.kind === "llm");
+}
+
+export function isExpressionConfig(
+ config: NodeConfig | null | undefined,
+): config is ExpressionConfig {
+ return Boolean(config && config.kind === "expression");
+}
diff --git a/studio/frontend/src/features/recipe-studio/utils/import/parsers.ts b/studio/frontend/src/features/recipe-studio/utils/import/parsers.ts
index 349da4e9a8..edc3580aa1 100644
--- a/studio/frontend/src/features/recipe-studio/utils/import/parsers.ts
+++ b/studio/frontend/src/features/recipe-studio/utils/import/parsers.ts
@@ -1,399 +1,9 @@
-import type {
- ExpressionConfig,
- ExpressionDtype,
- LlmConfig,
- ModelConfig,
- ModelProviderConfig,
- NodeConfig,
- SamplerConfig,
- SamplerType,
- Score,
- ScoreOption,
-} from "../../types";
-import {
- isRecord,
- normalizeOutputFormat,
- readNumberString,
- readString,
-} from "./helpers";
-
-const SAMPLER_TYPES: SamplerType[] = [
- "category",
- "subcategory",
- "uniform",
- "gaussian",
- "bernoulli",
- "datetime",
- "timedelta",
- "uuid",
- "person",
- "person_from_faker",
-];
-
-const EXPRESSION_DTYPES: ExpressionDtype[] = ["str", "int", "float", "bool"];
-const TIMEDELTA_UNITS = new Set(["D", "h", "m", "s"]);
-
-function parseCategoryConditionalParams(
- column: Record,
-): SamplerConfig["conditional_params"] {
- if (!isRecord(column.conditional_params)) {
- return undefined;
- }
- const conditional: NonNullable = {};
- for (const [condition, rawParams] of Object.entries(column.conditional_params)) {
- if (!isRecord(rawParams)) {
- continue;
- }
- if (readString(rawParams.sampler_type) !== "category") {
- continue;
- }
- const values = Array.isArray(rawParams.values)
- ? rawParams.values.filter((item) => typeof item === "string")
- : [];
- if (values.length === 0) {
- continue;
- }
- const weights = Array.isArray(rawParams.weights)
- ? rawParams.weights.map((item) => (typeof item === "number" ? item : null))
- : undefined;
- conditional[condition] = {
- // biome-ignore lint/style/useNamingConvention: api schema
- sampler_type: "category",
- values,
- weights,
- };
- }
- return Object.keys(conditional).length > 0 ? conditional : undefined;
-}
-
-function parseSampler(
- column: Record,
- name: string,
- 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.`);
- return null;
- }
- const convertTo = readString(column.convert_to);
- const normalizedConvertTo =
- convertTo && ["float", "int", "str"].includes(convertTo)
- ? (convertTo as "float" | "int" | "str")
- : undefined;
- const params =
- typeof column.params === "object" && column.params
- ? (column.params as Record)
- : {};
- if (samplerType === "category") {
- const values = Array.isArray(params.values)
- ? params.values.filter((item) => typeof item === "string")
- : [];
- const weights = Array.isArray(params.weights)
- ? params.weights.map((item) => (typeof item === "number" ? item : null))
- : [];
- return {
- id,
- kind: "sampler",
- // biome-ignore lint/style/useNamingConvention: api schema
- sampler_type: "category",
- name,
- drop,
- // biome-ignore lint/style/useNamingConvention: api schema
- convert_to: normalizedConvertTo,
- values,
- weights,
- // biome-ignore lint/style/useNamingConvention: api schema
- conditional_params: parseCategoryConditionalParams(column),
- };
- }
- if (samplerType === "subcategory") {
- const mapping: Record = {};
- if (params.values && typeof params.values === "object") {
- for (const [key, value] of Object.entries(params.values)) {
- if (Array.isArray(value)) {
- mapping[key] = value.filter((item) => typeof item === "string");
- }
- }
- }
- return {
- id,
- kind: "sampler",
- // 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
- subcategory_parent: readString(params.category) ?? "",
- // biome-ignore lint/style/useNamingConvention: api schema
- subcategory_mapping: mapping,
- };
- }
- if (samplerType === "uniform") {
- return {
- id,
- kind: "sampler",
- // 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),
- high: readNumberString(params.high),
- };
- }
- if (samplerType === "gaussian") {
- return {
- id,
- kind: "sampler",
- // 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),
- std: readNumberString(params.std),
- };
- }
- if (samplerType === "bernoulli") {
- return {
- id,
- kind: "sampler",
- // 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),
- };
- }
- if (samplerType === "datetime") {
- return {
- id,
- kind: "sampler",
- // 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
- datetime_start: readString(params.start) ?? "",
- // biome-ignore lint/style/useNamingConvention: api schema
- datetime_end: readString(params.end) ?? "",
- // biome-ignore lint/style/useNamingConvention: api schema
- datetime_unit: readString(params.unit) ?? "",
- };
- }
- if (samplerType === "timedelta") {
- const rawUnit = readString(params.unit);
- const unit =
- rawUnit && TIMEDELTA_UNITS.has(rawUnit)
- ? (rawUnit as "D" | "h" | "m" | "s")
- : "D";
- return {
- id,
- kind: "sampler",
- // 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
- dt_min: readNumberString(params.dt_min),
- // biome-ignore lint/style/useNamingConvention: api schema
- dt_max: readNumberString(params.dt_max),
- // biome-ignore lint/style/useNamingConvention: api schema
- reference_column_name: readString(params.reference_column_name) ?? "",
- // biome-ignore lint/style/useNamingConvention: api schema
- timedelta_unit: unit,
- };
- }
- if (samplerType === "uuid") {
- return {
- id,
- kind: "sampler",
- // 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
- uuid_format: readString(params.format) ?? "",
- };
- }
- const ageRange =
- Array.isArray(params.age_range) &&
- params.age_range.length === 2 &&
- params.age_range.every((item) => typeof item === "number")
- ? `${params.age_range[0]}-${params.age_range[1]}`
- : readString(params.age_range) ?? "";
- const base: SamplerConfig = {
- id,
- kind: "sampler",
- name,
- drop,
- // biome-ignore lint/style/useNamingConvention: api schema
- sampler_type: samplerType as SamplerType,
- // biome-ignore lint/style/useNamingConvention: api schema
- convert_to: normalizedConvertTo,
- // biome-ignore lint/style/useNamingConvention: api schema
- person_locale: readString(params.locale) ?? "",
- // biome-ignore lint/style/useNamingConvention: api schema
- person_sex: readString(params.sex) ?? "",
- // biome-ignore lint/style/useNamingConvention: api schema
- person_age_range: ageRange,
- // biome-ignore lint/style/useNamingConvention: api schema
- person_city: readString(params.city) ?? "",
- };
- if (samplerType === "person") {
- return {
- ...base,
- // biome-ignore lint/style/useNamingConvention: api schema
- person_with_synthetic_personas:
- typeof params.with_synthetic_personas === "boolean"
- ? params.with_synthetic_personas
- : false,
- };
- }
- return base;
-}
-
-function parseLlm(
- column: Record,
- name: string,
- id: string,
-): LlmConfig {
- const columnType = readString(column.column_type) ?? "llm-text";
- let llmType: LlmConfig["llm_type"] = "text";
- if (columnType === "llm-structured") {
- llmType = "structured";
- } else if (columnType === "llm-code") {
- llmType = "code";
- } else if (columnType === "llm-judge") {
- llmType = "judge";
- }
- const scores: Score[] =
- columnType === "llm-judge" && Array.isArray(column.scores)
- ? column.scores
- .filter((score) => isRecord(score))
- .map((score) => {
- const options: ScoreOption[] = [];
- const rawOptions = isRecord(score.options) ? score.options : {};
- for (const [key, value] of Object.entries(rawOptions)) {
- const description =
- typeof value === "string" ? value : JSON.stringify(value);
- options.push({ value: String(key), description });
- }
- return {
- name: readString(score.name) ?? "",
- description: readString(score.description) ?? "",
- options,
- };
- })
- : [];
- return {
- id,
- kind: "llm",
- // 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) ?? "",
- // biome-ignore lint/style/useNamingConvention: api schema
- system_prompt: readString(column.system_prompt) ?? "",
- // biome-ignore lint/style/useNamingConvention: api schema
- code_lang: readString(column.code_lang) ?? "",
- // biome-ignore lint/style/useNamingConvention: api schema
- output_format: normalizeOutputFormat(column.output_format),
- scores: llmType === "judge" ? scores : undefined,
- };
-}
-
-export function parseModelProvider(
- provider: Record,
- name: string,
- id: string,
-): ModelProviderConfig {
- return {
- id,
- kind: "model_provider",
- name,
- endpoint: readString(provider.endpoint) ?? "",
- // biome-ignore lint/style/useNamingConvention: api schema
- provider_type: readString(provider.provider_type) ?? "openai",
- // biome-ignore lint/style/useNamingConvention: api schema
- api_key_env: readString(provider.api_key_env) ?? "",
- // biome-ignore lint/style/useNamingConvention: api schema
- api_key: readString(provider.api_key) ?? "",
- // biome-ignore lint/style/useNamingConvention: api schema
- extra_headers: isRecord(provider.extra_headers)
- ? JSON.stringify(provider.extra_headers, null, 2)
- : "",
- // biome-ignore lint/style/useNamingConvention: api schema
- extra_body: isRecord(provider.extra_body)
- ? JSON.stringify(provider.extra_body, null, 2)
- : "",
- };
-}
-
-export function parseModelConfig(
- model: Record,
- name: string,
- id: string,
-): ModelConfig {
- const inference = isRecord(model.inference_parameters)
- ? (model.inference_parameters as Record)
- : {};
- return {
- id,
- kind: "model_config",
- name,
- model: readString(model.model) ?? "",
- provider: readString(model.provider) ?? "",
- // biome-ignore lint/style/useNamingConvention: api schema
- inference_temperature: readNumberString(inference.temperature),
- // biome-ignore lint/style/useNamingConvention: api schema
- inference_top_p: readNumberString(inference.top_p),
- // biome-ignore lint/style/useNamingConvention: api schema
- inference_max_tokens: readNumberString(inference.max_tokens),
- // biome-ignore lint/style/useNamingConvention: api schema
- skip_health_check:
- typeof model.skip_health_check === "boolean"
- ? model.skip_health_check
- : false,
- };
-}
-
-function parseExpression(
- column: Record,
- name: string,
- id: string,
-): ExpressionConfig {
- const dtype = readString(column.dtype);
- const normalized = EXPRESSION_DTYPES.includes(dtype as ExpressionDtype)
- ? (dtype as ExpressionDtype)
- : "str";
- return {
- id,
- kind: "expression",
- name,
- drop: column.drop === true,
- expr: readString(column.expr) ?? "",
- dtype: normalized,
- };
-}
+import type { NodeConfig } from "../../types";
+import { readString } from "./helpers";
+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";
type ColumnParser = (
column: Record,
diff --git a/studio/frontend/src/features/recipe-studio/utils/import/parsers/expression-parser.ts b/studio/frontend/src/features/recipe-studio/utils/import/parsers/expression-parser.ts
new file mode 100644
index 0000000000..03585a1f11
--- /dev/null
+++ b/studio/frontend/src/features/recipe-studio/utils/import/parsers/expression-parser.ts
@@ -0,0 +1,26 @@
+import type {
+ ExpressionConfig,
+ ExpressionDtype,
+} from "../../../types";
+import { readString } from "../helpers";
+
+const EXPRESSION_DTYPES: ExpressionDtype[] = ["str", "int", "float", "bool"];
+
+export function parseExpression(
+ column: Record,
+ name: string,
+ id: string,
+): ExpressionConfig {
+ const dtype = readString(column.dtype);
+ const normalized = EXPRESSION_DTYPES.includes(dtype as ExpressionDtype)
+ ? (dtype as ExpressionDtype)
+ : "str";
+ return {
+ id,
+ kind: "expression",
+ name,
+ drop: column.drop === true,
+ expr: readString(column.expr) ?? "",
+ dtype: normalized,
+ };
+}
diff --git a/studio/frontend/src/features/recipe-studio/utils/import/parsers/llm-parser.ts b/studio/frontend/src/features/recipe-studio/utils/import/parsers/llm-parser.ts
new file mode 100644
index 0000000000..6d37cb832c
--- /dev/null
+++ b/studio/frontend/src/features/recipe-studio/utils/import/parsers/llm-parser.ts
@@ -0,0 +1,65 @@
+import type {
+ LlmConfig,
+ Score,
+ ScoreOption,
+} from "../../../types";
+import {
+ isRecord,
+ normalizeOutputFormat,
+ readString,
+} from "../helpers";
+
+export function parseLlm(
+ column: Record,
+ name: string,
+ id: string,
+): LlmConfig {
+ const columnType = readString(column.column_type) ?? "llm-text";
+ let llmType: LlmConfig["llm_type"] = "text";
+ if (columnType === "llm-structured") {
+ llmType = "structured";
+ } else if (columnType === "llm-code") {
+ llmType = "code";
+ } else if (columnType === "llm-judge") {
+ llmType = "judge";
+ }
+
+ const scores: Score[] =
+ columnType === "llm-judge" && Array.isArray(column.scores)
+ ? column.scores
+ .filter((score) => isRecord(score))
+ .map((score) => {
+ const options: ScoreOption[] = [];
+ const rawOptions = isRecord(score.options) ? score.options : {};
+ for (const [key, value] of Object.entries(rawOptions)) {
+ const description =
+ typeof value === "string" ? value : JSON.stringify(value);
+ options.push({ value: String(key), description });
+ }
+ return {
+ name: readString(score.name) ?? "",
+ description: readString(score.description) ?? "",
+ options,
+ };
+ })
+ : [];
+
+ return {
+ id,
+ kind: "llm",
+ // 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) ?? "",
+ // biome-ignore lint/style/useNamingConvention: api schema
+ system_prompt: readString(column.system_prompt) ?? "",
+ // biome-ignore lint/style/useNamingConvention: api schema
+ code_lang: readString(column.code_lang) ?? "",
+ // biome-ignore lint/style/useNamingConvention: api schema
+ output_format: normalizeOutputFormat(column.output_format),
+ scores: llmType === "judge" ? scores : undefined,
+ };
+}
diff --git a/studio/frontend/src/features/recipe-studio/utils/import/parsers/model-parser.ts b/studio/frontend/src/features/recipe-studio/utils/import/parsers/model-parser.ts
new file mode 100644
index 0000000000..18632d5c03
--- /dev/null
+++ b/studio/frontend/src/features/recipe-studio/utils/import/parsers/model-parser.ts
@@ -0,0 +1,64 @@
+import type {
+ ModelConfig,
+ ModelProviderConfig,
+} from "../../../types";
+import {
+ isRecord,
+ readNumberString,
+ readString,
+} from "../helpers";
+
+export function parseModelProvider(
+ provider: Record,
+ name: string,
+ id: string,
+): ModelProviderConfig {
+ return {
+ id,
+ kind: "model_provider",
+ name,
+ endpoint: readString(provider.endpoint) ?? "",
+ // biome-ignore lint/style/useNamingConvention: api schema
+ provider_type: readString(provider.provider_type) ?? "openai",
+ // biome-ignore lint/style/useNamingConvention: api schema
+ api_key_env: readString(provider.api_key_env) ?? "",
+ // biome-ignore lint/style/useNamingConvention: api schema
+ api_key: readString(provider.api_key) ?? "",
+ // biome-ignore lint/style/useNamingConvention: api schema
+ extra_headers: isRecord(provider.extra_headers)
+ ? JSON.stringify(provider.extra_headers, null, 2)
+ : "",
+ // biome-ignore lint/style/useNamingConvention: api schema
+ extra_body: isRecord(provider.extra_body)
+ ? JSON.stringify(provider.extra_body, null, 2)
+ : "",
+ };
+}
+
+export function parseModelConfig(
+ model: Record,
+ name: string,
+ id: string,
+): ModelConfig {
+ const inference = isRecord(model.inference_parameters)
+ ? (model.inference_parameters as Record)
+ : {};
+ return {
+ id,
+ kind: "model_config",
+ name,
+ model: readString(model.model) ?? "",
+ provider: readString(model.provider) ?? "",
+ // biome-ignore lint/style/useNamingConvention: api schema
+ inference_temperature: readNumberString(inference.temperature),
+ // biome-ignore lint/style/useNamingConvention: api schema
+ inference_top_p: readNumberString(inference.top_p),
+ // biome-ignore lint/style/useNamingConvention: api schema
+ inference_max_tokens: readNumberString(inference.max_tokens),
+ // biome-ignore lint/style/useNamingConvention: api schema
+ skip_health_check:
+ typeof model.skip_health_check === "boolean"
+ ? model.skip_health_check
+ : false,
+ };
+}
diff --git a/studio/frontend/src/features/recipe-studio/utils/import/parsers/sampler-parser.ts b/studio/frontend/src/features/recipe-studio/utils/import/parsers/sampler-parser.ts
new file mode 100644
index 0000000000..067216af1a
--- /dev/null
+++ b/studio/frontend/src/features/recipe-studio/utils/import/parsers/sampler-parser.ts
@@ -0,0 +1,271 @@
+import type {
+ SamplerConfig,
+ SamplerType,
+} from "../../../types";
+import {
+ isRecord,
+ readNumberString,
+ readString,
+} from "../helpers";
+
+const SAMPLER_TYPES: SamplerType[] = [
+ "category",
+ "subcategory",
+ "uniform",
+ "gaussian",
+ "bernoulli",
+ "datetime",
+ "timedelta",
+ "uuid",
+ "person",
+ "person_from_faker",
+];
+
+const TIMEDELTA_UNITS = new Set(["D", "h", "m", "s"]);
+
+function parseCategoryConditionalParams(
+ column: Record,
+): SamplerConfig["conditional_params"] {
+ if (!isRecord(column.conditional_params)) {
+ return undefined;
+ }
+ const conditional: NonNullable = {};
+ for (const [condition, rawParams] of Object.entries(column.conditional_params)) {
+ if (!isRecord(rawParams)) {
+ continue;
+ }
+ if (readString(rawParams.sampler_type) !== "category") {
+ continue;
+ }
+ const values = Array.isArray(rawParams.values)
+ ? rawParams.values.filter((item) => typeof item === "string")
+ : [];
+ if (values.length === 0) {
+ continue;
+ }
+ const weights = Array.isArray(rawParams.weights)
+ ? rawParams.weights.map((item) => (typeof item === "number" ? item : null))
+ : undefined;
+ conditional[condition] = {
+ // biome-ignore lint/style/useNamingConvention: api schema
+ sampler_type: "category",
+ values,
+ weights,
+ };
+ }
+ return Object.keys(conditional).length > 0 ? conditional : undefined;
+}
+
+export function parseSampler(
+ column: Record,
+ name: string,
+ 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.`);
+ return null;
+ }
+ const convertTo = readString(column.convert_to);
+ const normalizedConvertTo =
+ convertTo && ["float", "int", "str"].includes(convertTo)
+ ? (convertTo as "float" | "int" | "str")
+ : undefined;
+ const params =
+ typeof column.params === "object" && column.params
+ ? (column.params as Record)
+ : {};
+
+ if (samplerType === "category") {
+ const values = Array.isArray(params.values)
+ ? params.values.filter((item) => typeof item === "string")
+ : [];
+ const weights = Array.isArray(params.weights)
+ ? params.weights.map((item) => (typeof item === "number" ? item : null))
+ : [];
+ return {
+ id,
+ kind: "sampler",
+ // biome-ignore lint/style/useNamingConvention: api schema
+ sampler_type: "category",
+ name,
+ drop,
+ // biome-ignore lint/style/useNamingConvention: api schema
+ convert_to: normalizedConvertTo,
+ values,
+ weights,
+ // biome-ignore lint/style/useNamingConvention: api schema
+ conditional_params: parseCategoryConditionalParams(column),
+ };
+ }
+
+ if (samplerType === "subcategory") {
+ const mapping: Record = {};
+ if (params.values && typeof params.values === "object") {
+ for (const [key, value] of Object.entries(params.values)) {
+ if (Array.isArray(value)) {
+ mapping[key] = value.filter((item) => typeof item === "string");
+ }
+ }
+ }
+ return {
+ id,
+ kind: "sampler",
+ // 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
+ subcategory_parent: readString(params.category) ?? "",
+ // biome-ignore lint/style/useNamingConvention: api schema
+ subcategory_mapping: mapping,
+ };
+ }
+
+ if (samplerType === "uniform") {
+ return {
+ id,
+ kind: "sampler",
+ // 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),
+ high: readNumberString(params.high),
+ };
+ }
+
+ if (samplerType === "gaussian") {
+ return {
+ id,
+ kind: "sampler",
+ // 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),
+ std: readNumberString(params.std),
+ };
+ }
+
+ if (samplerType === "bernoulli") {
+ return {
+ id,
+ kind: "sampler",
+ // 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),
+ };
+ }
+
+ if (samplerType === "datetime") {
+ return {
+ id,
+ kind: "sampler",
+ // 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
+ datetime_start: readString(params.start) ?? "",
+ // biome-ignore lint/style/useNamingConvention: api schema
+ datetime_end: readString(params.end) ?? "",
+ // biome-ignore lint/style/useNamingConvention: api schema
+ datetime_unit: readString(params.unit) ?? "",
+ };
+ }
+
+ if (samplerType === "timedelta") {
+ const rawUnit = readString(params.unit);
+ const unit =
+ rawUnit && TIMEDELTA_UNITS.has(rawUnit)
+ ? (rawUnit as "D" | "h" | "m" | "s")
+ : "D";
+ return {
+ id,
+ kind: "sampler",
+ // 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
+ dt_min: readNumberString(params.dt_min),
+ // biome-ignore lint/style/useNamingConvention: api schema
+ dt_max: readNumberString(params.dt_max),
+ // biome-ignore lint/style/useNamingConvention: api schema
+ reference_column_name: readString(params.reference_column_name) ?? "",
+ // biome-ignore lint/style/useNamingConvention: api schema
+ timedelta_unit: unit,
+ };
+ }
+
+ if (samplerType === "uuid") {
+ return {
+ id,
+ kind: "sampler",
+ // 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
+ uuid_format: readString(params.format) ?? "",
+ };
+ }
+
+ const ageRange =
+ Array.isArray(params.age_range) &&
+ params.age_range.length === 2 &&
+ params.age_range.every((item) => typeof item === "number")
+ ? `${params.age_range[0]}-${params.age_range[1]}`
+ : readString(params.age_range) ?? "";
+
+ const base: SamplerConfig = {
+ id,
+ kind: "sampler",
+ name,
+ drop,
+ // biome-ignore lint/style/useNamingConvention: api schema
+ sampler_type: samplerType as SamplerType,
+ // biome-ignore lint/style/useNamingConvention: api schema
+ convert_to: normalizedConvertTo,
+ // biome-ignore lint/style/useNamingConvention: api schema
+ person_locale: readString(params.locale) ?? "",
+ // biome-ignore lint/style/useNamingConvention: api schema
+ person_sex: readString(params.sex) ?? "",
+ // biome-ignore lint/style/useNamingConvention: api schema
+ person_age_range: ageRange,
+ // biome-ignore lint/style/useNamingConvention: api schema
+ person_city: readString(params.city) ?? "",
+ };
+
+ if (samplerType === "person") {
+ return {
+ ...base,
+ // biome-ignore lint/style/useNamingConvention: api schema
+ person_with_synthetic_personas:
+ typeof params.with_synthetic_personas === "boolean"
+ ? params.with_synthetic_personas
+ : false,
+ };
+ }
+
+ return base;
+}
diff --git a/studio/frontend/src/features/recipe-studio/utils/index.ts b/studio/frontend/src/features/recipe-studio/utils/index.ts
index 5a58103683..f4256047d8 100644
--- a/studio/frontend/src/features/recipe-studio/utils/index.ts
+++ b/studio/frontend/src/features/recipe-studio/utils/index.ts
@@ -1,422 +1,22 @@
-import type {
- RecipeNodeData,
- ExpressionConfig,
- ExpressionDtype,
- LayoutDirection,
- LlmConfig,
- LlmType,
- ModelConfig,
- ModelProviderConfig,
- NodeConfig,
- SamplerConfig,
- SamplerType,
-} from "../types";
+export {
+ makeExpressionConfig,
+ makeLlmConfig,
+ makeModelConfig,
+ makeModelProviderConfig,
+ makeSamplerConfig,
+} from "./config-factories";
+export {
+ labelForExpression,
+ labelForLlm,
+ labelForSampler,
+} from "./config-labels";
+export {
+ isCategoryConfig,
+ isExpressionConfig,
+ isLlmConfig,
+ isSamplerConfig,
+ isSubcategoryConfig,
+} from "./config-type-guards";
+export { nextName } from "./naming";
+export { nodeDataFromConfig } from "./node-data";
export { getConfigErrors } from "./validation";
-
-const SAMPLER_LABELS: Record = {
- category: "Category",
- subcategory: "Subcategory",
- uniform: "Uniform",
- gaussian: "Gaussian",
- bernoulli: "Bernoulli",
- datetime: "Datetime",
- timedelta: "Timedelta",
- uuid: "UUID",
- person: "Person",
- person_from_faker: "Person (Faker)",
-};
-
-const LLM_LABELS: Record = {
- text: "LLM Text",
- structured: "LLM Structured",
- code: "LLM Code",
- judge: "LLM Judge",
-};
-
-const EXPRESSION_LABELS: Record = {
- str: "Text",
- int: "Int",
- float: "Float",
- bool: "Bool",
-};
-
-export function nextName(existing: NodeConfig[], prefix: string): string {
- const counts = existing
- .map((item) => item.name)
- .filter((name) => name.startsWith(prefix))
- .map((name) => {
- const suffix = name.slice(prefix.length);
- const num = Number.parseInt(suffix.replace("_", ""), 10);
- return Number.isNaN(num) ? 0 : num;
- });
- const next = counts.length > 0 ? Math.max(...counts) + 1 : 1;
- return `${prefix}_${next}`;
-}
-
-export function makeSamplerConfig(
- id: string,
- samplerType: SamplerType,
- existing: NodeConfig[],
-): SamplerConfig {
- const namePrefix =
- samplerType === "subcategory" ? "subcategory" : samplerType;
- const name = nextName(existing, namePrefix);
- if (samplerType === "category") {
- return {
- id,
- kind: "sampler",
- // biome-ignore lint/style/useNamingConvention: api schema
- sampler_type: "category",
- name,
- drop: false,
- values: ["A", "B", "C"],
- weights: [null, null, null],
- };
- }
- if (samplerType === "subcategory") {
- return {
- id,
- kind: "sampler",
- // 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
- subcategory_mapping: {
- // biome-ignore lint/style/useNamingConvention: sample values
- A: ["A1", "A2"],
- // biome-ignore lint/style/useNamingConvention: sample values
- B: ["B1", "B2"],
- },
- };
- }
- if (samplerType === "uniform") {
- return {
- id,
- kind: "sampler",
- // biome-ignore lint/style/useNamingConvention: api schema
- sampler_type: "uniform",
- name,
- drop: false,
- low: "0",
- high: "1",
- };
- }
- if (samplerType === "gaussian") {
- return {
- id,
- kind: "sampler",
- // biome-ignore lint/style/useNamingConvention: api schema
- sampler_type: "gaussian",
- name,
- drop: false,
- mean: "0",
- std: "1",
- };
- }
- if (samplerType === "bernoulli") {
- return {
- id,
- kind: "sampler",
- // biome-ignore lint/style/useNamingConvention: api schema
- sampler_type: "bernoulli",
- name,
- drop: false,
- p: "0.5",
- };
- }
- if (samplerType === "datetime") {
- return {
- id,
- kind: "sampler",
- // 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
- datetime_end: "",
- // biome-ignore lint/style/useNamingConvention: api schema
- datetime_unit: "day",
- };
- }
- if (samplerType === "timedelta") {
- return {
- id,
- kind: "sampler",
- // 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
- dt_max: "1",
- // biome-ignore lint/style/useNamingConvention: api schema
- reference_column_name: "",
- // biome-ignore lint/style/useNamingConvention: api schema
- timedelta_unit: "D",
- };
- }
- if (samplerType === "uuid") {
- return {
- id,
- kind: "sampler",
- // biome-ignore lint/style/useNamingConvention: api schema
- sampler_type: "uuid",
- name,
- drop: false,
- // biome-ignore lint/style/useNamingConvention: api schema
- uuid_format: "",
- };
- }
- if (samplerType === "person_from_faker") {
- return {
- id,
- kind: "sampler",
- // 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
- person_sex: "",
- // biome-ignore lint/style/useNamingConvention: api schema
- person_age_range: "",
- // biome-ignore lint/style/useNamingConvention: api schema
- person_city: "",
- };
- }
- return {
- id,
- kind: "sampler",
- // 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
- person_sex: "",
- // biome-ignore lint/style/useNamingConvention: api schema
- person_age_range: "",
- // biome-ignore lint/style/useNamingConvention: api schema
- person_city: "",
- // biome-ignore lint/style/useNamingConvention: api schema
- person_with_synthetic_personas: false,
- };
-}
-
-export function makeLlmConfig(
- id: string,
- llmType: LlmType,
- existing: NodeConfig[],
-): LlmConfig {
- let namePrefix = "llm_text";
- if (llmType === "structured") {
- namePrefix = "llm_structured";
- } else if (llmType === "code") {
- namePrefix = "llm_code";
- } else if (llmType === "judge") {
- namePrefix = "llm_judge";
- }
- const name = nextName(existing, namePrefix);
- return {
- id,
- kind: "llm",
- // 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:
- llmType === "judge"
- ? "Evaluate the content using the scoring criteria below."
- : "Write a response.",
- // biome-ignore lint/style/useNamingConvention: api schema
- system_prompt: "",
- // biome-ignore lint/style/useNamingConvention: api schema
- code_lang: llmType === "code" ? "python" : undefined,
- // biome-ignore lint/style/useNamingConvention: api schema
- output_format:
- llmType === "structured" ? '{\n "field": "string"\n}' : undefined,
- scores:
- llmType === "judge"
- ? [
- {
- name: "Quality",
- description: "Overall quality based on the criteria.",
- options: [
- { value: "1", description: "Poor" },
- { value: "3", description: "Acceptable" },
- { value: "5", description: "Excellent" },
- ],
- },
- ]
- : undefined,
- };
-}
-
-export function makeModelProviderConfig(
- id: string,
- existing: NodeConfig[],
-): ModelProviderConfig {
- return {
- id,
- kind: "model_provider",
- name: nextName(existing, "provider"),
- endpoint: "",
- // biome-ignore lint/style/useNamingConvention: api schema
- provider_type: "openai",
- // biome-ignore lint/style/useNamingConvention: api schema
- api_key_env: "",
- // biome-ignore lint/style/useNamingConvention: api schema
- api_key: "",
- // biome-ignore lint/style/useNamingConvention: api schema
- extra_headers: "",
- // biome-ignore lint/style/useNamingConvention: api schema
- extra_body: "",
- };
-}
-
-export function makeModelConfig(
- id: string,
- existing: NodeConfig[],
-): ModelConfig {
- return {
- id,
- kind: "model_config",
- name: nextName(existing, "model"),
- model: "",
- provider: "",
- // biome-ignore lint/style/useNamingConvention: api schema
- inference_temperature: "0.7",
- // biome-ignore lint/style/useNamingConvention: api schema
- inference_max_tokens: "256",
- // biome-ignore lint/style/useNamingConvention: api schema
- inference_top_p: "",
- // biome-ignore lint/style/useNamingConvention: api schema
- skip_health_check: false,
- };
-}
-
-export function makeExpressionConfig(
- id: string,
- existing: NodeConfig[],
-): ExpressionConfig {
- return {
- id,
- kind: "expression",
- name: nextName(existing, "expr"),
- drop: false,
- expr: "",
- dtype: "str",
- };
-}
-
-export function labelForSampler(type: SamplerType): string {
- return SAMPLER_LABELS[type] ?? "Sampler";
-}
-
-export function labelForLlm(type: LlmType): string {
- return LLM_LABELS[type] ?? "LLM";
-}
-
-export function labelForExpression(type: ExpressionDtype): string {
- return EXPRESSION_LABELS[type] ?? "Expression";
-}
-
-export function nodeDataFromConfig(
- config: NodeConfig,
- layoutDirection: LayoutDirection = "LR",
-): RecipeNodeData {
- if (config.kind === "sampler") {
- return {
- title: "Sampler",
- kind: "sampler",
- subtype: labelForSampler(config.sampler_type),
- blockType: config.sampler_type,
- name: config.name,
- layoutDirection,
- };
- }
- if (config.kind === "expression") {
- return {
- title: "Expression",
- kind: "expression",
- subtype: labelForExpression(config.dtype),
- blockType: "expression",
- name: config.name,
- layoutDirection,
- };
- }
- if (config.kind === "model_provider") {
- return {
- title: "Model Provider",
- kind: "model_provider",
- subtype: config.provider_type || "Provider",
- blockType: "model_provider",
- name: config.name,
- layoutDirection,
- };
- }
- if (config.kind === "model_config") {
- return {
- title: "Model Config",
- kind: "model_config",
- subtype: config.model || "Model",
- blockType: "model_config",
- name: config.name,
- layoutDirection,
- };
- }
- return {
- title: "LLM",
- kind: "llm",
- subtype: labelForLlm(config.llm_type),
- blockType: config.llm_type,
- name: config.name,
- layoutDirection,
- };
-}
-
-export function isSamplerConfig(
- config: NodeConfig | null | undefined,
-): config is SamplerConfig {
- return Boolean(config && config.kind === "sampler");
-}
-
-export function isCategoryConfig(
- config: NodeConfig | null | undefined,
-): config is SamplerConfig {
- return Boolean(
- config && config.kind === "sampler" && config.sampler_type === "category",
- );
-}
-
-export function isSubcategoryConfig(
- config: NodeConfig | null | undefined,
-): config is SamplerConfig {
- return Boolean(
- config &&
- config.kind === "sampler" &&
- config.sampler_type === "subcategory",
- );
-}
-
-export function isLlmConfig(
- config: NodeConfig | null | undefined,
-): config is LlmConfig {
- return Boolean(config && config.kind === "llm");
-}
-
-export function isExpressionConfig(
- config: NodeConfig | null | undefined,
-): config is ExpressionConfig {
- return Boolean(config && config.kind === "expression");
-}
diff --git a/studio/frontend/src/features/recipe-studio/utils/naming.ts b/studio/frontend/src/features/recipe-studio/utils/naming.ts
new file mode 100644
index 0000000000..664ed62c08
--- /dev/null
+++ b/studio/frontend/src/features/recipe-studio/utils/naming.ts
@@ -0,0 +1,14 @@
+import type { NodeConfig } from "../types";
+
+export function nextName(existing: NodeConfig[], prefix: string): string {
+ const counts = existing
+ .map((item) => item.name)
+ .filter((name) => name.startsWith(prefix))
+ .map((name) => {
+ const suffix = name.slice(prefix.length);
+ const num = Number.parseInt(suffix.replace("_", ""), 10);
+ return Number.isNaN(num) ? 0 : num;
+ });
+ const next = counts.length > 0 ? Math.max(...counts) + 1 : 1;
+ return `${prefix}_${next}`;
+}
diff --git a/studio/frontend/src/features/recipe-studio/utils/node-data.ts b/studio/frontend/src/features/recipe-studio/utils/node-data.ts
new file mode 100644
index 0000000000..8b0bf5cf35
--- /dev/null
+++ b/studio/frontend/src/features/recipe-studio/utils/node-data.ts
@@ -0,0 +1,60 @@
+import type { RecipeNodeData, LayoutDirection, NodeConfig } from "../types";
+import {
+ labelForExpression,
+ labelForLlm,
+ labelForSampler,
+} from "./config-labels";
+
+export function nodeDataFromConfig(
+ config: NodeConfig,
+ layoutDirection: LayoutDirection = "LR",
+): RecipeNodeData {
+ if (config.kind === "sampler") {
+ return {
+ title: "Sampler",
+ kind: "sampler",
+ subtype: labelForSampler(config.sampler_type),
+ blockType: config.sampler_type,
+ name: config.name,
+ layoutDirection,
+ };
+ }
+ if (config.kind === "expression") {
+ return {
+ title: "Expression",
+ kind: "expression",
+ subtype: labelForExpression(config.dtype),
+ blockType: "expression",
+ name: config.name,
+ layoutDirection,
+ };
+ }
+ if (config.kind === "model_provider") {
+ return {
+ title: "Model Provider",
+ kind: "model_provider",
+ subtype: config.provider_type || "Provider",
+ blockType: "model_provider",
+ name: config.name,
+ layoutDirection,
+ };
+ }
+ if (config.kind === "model_config") {
+ return {
+ title: "Model Config",
+ kind: "model_config",
+ subtype: config.model || "Model",
+ blockType: "model_config",
+ name: config.name,
+ layoutDirection,
+ };
+ }
+ return {
+ title: "LLM",
+ kind: "llm",
+ subtype: labelForLlm(config.llm_type),
+ blockType: config.llm_type,
+ name: config.name,
+ layoutDirection,
+ };
+}