feat(data-recipes, recipe-studio): refactor and enhance recipe templates with updated model configurations, structure changes, and added validation logic

This commit is contained in:
Shine1i 2026-03-05 12:14:01 +01:00
commit 59995d8447
5 changed files with 297 additions and 231 deletions

View file

@ -106,6 +106,12 @@
"note_color": "#F3E8FF",
"note_opacity": "35"
},
{
"id": "seed",
"x": 484.36210245413577,
"y": 1059.99180558796,
"width": 400
},
{
"id": "provider_1",
"x": 960,
@ -123,12 +129,6 @@
"x": 960,
"y": 1077,
"width": 400
},
{
"id": "seed_1",
"x": 478.48685020808557,
"y": 1060.0000000000002,
"width": 400
}
],
"edges": [
@ -148,7 +148,7 @@
},
{
"from": "llm_structured_1",
"to": "seed_1",
"to": "seed",
"type": "canvas",
"source_handle": "data-out-left",
"target_handle": "data-in-right"

View file

@ -3,7 +3,7 @@
"model_providers": [
{
"name": "provider_column",
"endpoint": "https://openrouter.ai/api/v1",
"endpoint": "",
"provider_type": "openai",
"extra_headers": {},
"extra_body": {}
@ -13,11 +13,10 @@
"model_configs": [
{
"alias": "ministral",
"model": "mistralai/ministral-8b-2512",
"model": "",
"provider": "provider_column",
"inference_parameters": {
"temperature": 0.7,
"max_tokens": 256
"temperature": 0.7
}
}
],
@ -76,6 +75,8 @@
"drop": false,
"model_alias": "ministral",
"prompt": "Create a realistic support ticket from {{ user_full_name }} using the {{ platform }} platform. Impact scope is {{ impact_scope }}.\n",
"with_trace": "none",
"extract_reasoning_content": false,
"output_format": {
"type": "object",
"additionalProperties": false,
@ -129,6 +130,8 @@
"drop": false,
"model_alias": "ministral",
"prompt": "Write a concise support reply for ticket '{{ ticket.issue_title }}'. Category: {{ ticket.category }}. Priority: {{ ticket.priority }}. SLA target: {{ sla_target }}. {% if ticket.priority == 'P1' %}Tone must be urgent and action-first.{% else %}Tone must be calm and instructional.{% endif %}",
"with_trace": "none",
"extract_reasoning_content": false,
"output_format": {
"type": "object",
"additionalProperties": false,
@ -168,8 +171,8 @@
"nodes": [
{
"id": "note_1",
"x": 1084.767431711644,
"y": -293.4482850247655,
"x": 990.3973509933774,
"y": 1487.5768211920529,
"width": 782,
"node_type": "markdown_note",
"name": "note_1",
@ -179,8 +182,8 @@
},
{
"id": "note_2",
"x": 1944,
"y": 760.9999999999999,
"x": 3217.6543046357615,
"y": 2081.596026490066,
"width": 400,
"node_type": "markdown_note",
"name": "note_2",
@ -190,8 +193,8 @@
},
{
"id": "note_3",
"x": 2381.178207301403,
"y": 790.2196835690842,
"x": 2294.4516556291387,
"y": 2399.6099337748346,
"width": 638,
"node_type": "markdown_note",
"name": "note_3",
@ -201,8 +204,8 @@
},
{
"id": "note_4",
"x": 2405.796928768747,
"y": -362.26583299682716,
"x": 2544.684105960265,
"y": 1126.5490066225163,
"width": 399,
"node_type": "markdown_note",
"name": "note_4",
@ -212,62 +215,62 @@
},
{
"id": "provider_column",
"x": 1947.2039072039072,
"y": 32.08363858363858,
"x": 2542,
"y": 1696,
"width": 400
},
{
"id": "ministral",
"x": 1947.0573870573871,
"y": 271.94139194139194,
"x": 2542,
"y": 1890,
"width": 400
},
{
"id": "user",
"x": 0,
"y": 656.5,
"x": 191,
"y": 2423,
"width": 400
},
{
"id": "platform",
"x": 480,
"y": 656.5,
"x": 858.0384105960266,
"y": 2286.5,
"width": 400
},
{
"id": "impact_scope",
"x": 960,
"y": 656.5,
"x": 1342,
"y": 2286.5,
"width": 400
},
{
"id": "user_first_name",
"x": 1440,
"y": 895,
"x": 1822,
"y": 2505,
"width": 400
},
{
"id": "user_full_name",
"x": 1440,
"y": 269,
"x": 1822,
"y": 1959,
"width": 400
},
{
"id": "ticket",
"x": 1946.9108669108673,
"y": 657.2161172161173,
"x": 2302,
"y": 2286.5,
"width": 400
},
{
"id": "sla_target",
"x": 1440,
"y": 582,
"x": 1822,
"y": 2232,
"width": 400
},
{
"id": "agent_reply",
"x": 2384.5665445665445,
"y": 657.449938949939,
"x": 2782,
"y": 2151,
"width": 400
}
],
@ -353,10 +356,10 @@
"from": "ministral",
"to": "agent_reply",
"type": "semantic",
"source_handle": "semantic-out",
"source_handle": "semantic-out-bottom",
"target_handle": "data-in-top"
}
],
"layout_direction": "LR"
}
}
}

View file

@ -2,8 +2,8 @@
"recipe": {
"model_providers": [
{
"name": "provider_1",
"endpoint": "https://openrouter.ai/api/v1",
"name": "openai-compatible",
"endpoint": "",
"provider_type": "openai",
"extra_headers": {},
"extra_body": {}
@ -12,12 +12,11 @@
"mcp_providers": [],
"model_configs": [
{
"alias": "model_1",
"model": "mistralai/ministral-8b-2512",
"provider": "provider_1",
"alias": "coding-model",
"model": "",
"provider": "openai-compatible",
"inference_parameters": {
"temperature": 0.7,
"max_tokens": 2048
"temperature": 0.7
}
}
],
@ -66,24 +65,29 @@
"column_type": "llm-text",
"name": "instruction",
"drop": false,
"model_alias": "model_1",
"model_alias": "coding-model",
"prompt": "Write one clear Python coding instruction.\nDomain: {{ domain }}\nTask type: {{ task_type }}\n\nKeep it practical and specific.\nReturn only the instruction without any code.",
"with_trace": "none"
"with_trace": "none",
"extract_reasoning_content": false
},
{
"column_type": "llm-code",
"name": "code_implementation",
"drop": false,
"model_alias": "model_1",
"model_alias": "coding-model",
"prompt": "Write Python code for:\n{{ instruction }}\n\nRequirements:\n- runnable script or function\n- include needed imports\n- short comments only where useful\n- no markdown fences",
"with_trace": "none",
"extract_reasoning_content": false,
"code_lang": "python"
},
{
"column_type": "llm-judge",
"name": "code_judge_result",
"drop": false,
"model_alias": "model_1",
"model_alias": "coding-model",
"prompt": "Evaluate generated Python code against the instruction.\n\nInstruction:\n{{ instruction }}\n\nCode:\n{{ code_implementation }}",
"with_trace": "none",
"extract_reasoning_content": false,
"scores": [
{
"name": "Correctness",
@ -109,52 +113,10 @@
},
"ui": {
"nodes": [
{
"id": "provider_1",
"x": 1032.6798211423347,
"y": -450.4885376732656,
"width": 400
},
{
"id": "model_1",
"x": 1538.0273166472973,
"y": -483.2003290046642,
"width": 400
},
{
"id": "domain",
"x": 0,
"y": 24,
"width": 400
},
{
"id": "task_type",
"x": 480,
"y": 24,
"width": 400
},
{
"id": "instruction",
"x": 958.8989453654599,
"y": -9.971266983459952,
"width": 400
},
{
"id": "code_implementation",
"x": 1538.788058529745,
"y": -45.56493974435071,
"width": 400
},
{
"id": "code_judge_result",
"x": 2040.9251520522098,
"y": -13.362336454344792,
"width": 400
},
{
"id": "note_1",
"x": 1482.1328175027095,
"y": 242.4370179053253,
"x": 1526,
"y": 1790.75,
"width": 568,
"node_type": "markdown_note",
"name": "note_1",
@ -164,14 +126,56 @@
},
{
"id": "note_2",
"x": 2513.2527820497985,
"y": -235.2544980991115,
"x": 2597.376821192053,
"y": 1233.2039735099338,
"width": 471,
"node_type": "markdown_note",
"name": "note_2",
"markdown": "The **LLM Judge** block evaluates generated outputs with rubric-style scores.\n\n##### Important:\n\n- A judge can have **one or many scores**\n- Each score has:\n - a name (for example: `Correctness`)\n - a description\n - options (value + meaning)\n\n##### Example multi-score setup:\n\n- Correctness\n- Readability\n- Efficiency\n\n##### Why use multiple scores:\n\n- You get richer quality signals than a single pass/fail\n- Easier filtering and weighting later in training data prep\n\n##### Practical pattern:\n\n1. Generate code with LLM Code\n2. Judge with 2-4 focused scores\n3. Keep high-quality rows based on score thresholds\n",
"note_color": "#FEF3C7",
"note_opacity": "35"
},
{
"id": "openai-compatible",
"x": 1627.1046357615896,
"y": 921.0301324503313,
"width": 400
},
{
"id": "coding-model",
"x": 1627.1046357615894,
"y": 1138.910927152318,
"width": 400
},
{
"id": "domain",
"x": 84,
"y": 1600.5,
"width": 400
},
{
"id": "task_type",
"x": 648,
"y": 1600.5,
"width": 400
},
{
"id": "instruction",
"x": 1128,
"y": 1567,
"width": 400
},
{
"id": "code_implementation",
"x": 1627.1046357615894,
"y": 1531.4728476821192,
"width": 400
},
{
"id": "code_judge_result",
"x": 2124.617218543046,
"y": 1567.076490066225,
"width": 400
}
],
"edges": [
@ -190,11 +194,11 @@
"target_handle": "data-in"
},
{
"from": "provider_1",
"to": "model_1",
"from": "openai-compatible",
"to": "coding-model",
"type": "semantic",
"source_handle": "semantic-out",
"target_handle": "semantic-in"
"source_handle": "semantic-out-bottom",
"target_handle": "semantic-in-top"
},
{
"from": "instruction",
@ -204,14 +208,14 @@
"target_handle": "data-in"
},
{
"from": "model_1",
"from": "coding-model",
"to": "instruction",
"type": "semantic",
"source_handle": "semantic-out-bottom",
"target_handle": "data-in-top"
},
{
"from": "model_1",
"from": "coding-model",
"to": "code_implementation",
"type": "semantic",
"source_handle": "semantic-out-bottom",
@ -225,7 +229,7 @@
"target_handle": "data-in"
},
{
"from": "model_1",
"from": "coding-model",
"to": "code_judge_result",
"type": "semantic",
"source_handle": "semantic-out-bottom",
@ -234,4 +238,4 @@
],
"layout_direction": "LR"
}
}
}

View file

@ -2,8 +2,8 @@
"recipe": {
"model_providers": [
{
"name": "provider_1",
"endpoint": "https://openrouter.ai/api/v1",
"name": "vllm",
"endpoint": "",
"provider_type": "openai",
"extra_headers": {},
"extra_body": {}
@ -12,12 +12,11 @@
"mcp_providers": [],
"model_configs": [
{
"alias": "model_1",
"model": "mistralai/ministral-8b-2512",
"provider": "provider_1",
"alias": "sql-pro",
"model": "",
"provider": "vllm",
"inference_parameters": {
"temperature": 0.7,
"max_tokens": 2048
"temperature": 0.7
}
}
],
@ -93,19 +92,35 @@
"column_type": "llm-text",
"name": "sql_prompt",
"drop": false,
"model_alias": "model_1",
"model_alias": "sql-pro",
"prompt": "Generate one natural-language SQL task.\nContext:\n- Domain: {{ domain }}\n- Topic: {{ topic }}\n- Task type: {{ sql_task_type }}\nRules:\n- Must start exactly with: \"{{ instruction_phrase }}\"\n- Make it specific and practical.\n- Mention expected business outcome.\n- Keep it 1-2 sentences.\n- Do not include SQL code.\n- Output only the instruction text.",
"system_prompt": "You create clear, realistic business SQL tasks for training data.\n",
"with_trace": "none"
"with_trace": "none",
"extract_reasoning_content": false
},
{
"column_type": "llm-code",
"name": "sql",
"drop": false,
"model_alias": "model_1",
"model_alias": "sql-pro",
"prompt": "Write SQL for this instruction:\n{{ sql_prompt }}\nReturn ONE SQL script with this exact structure:\n-- SCHEMA\n[CREATE TABLE statements]\n[INSERT statements with sample rows]\n-- QUERY\n[final SELECT query solving the instruction]\nRules:\n- Use 2-3 tables max.\n- Use realistic snake_case names.\n- Include 5-8 rows of sample data per table.\n- Query must match task type \"{{ sql_task_type }}\".\n- Use only tables/columns you created.\n- No markdown fences.\n- No explanation text outside SQL comments shown above.",
"system_prompt": "You are an expert SQL engineer. Produce correct, runnable SQL only.\n",
"with_trace": "none",
"extract_reasoning_content": false,
"code_lang": "sql:ansi"
},
{
"column_type": "validation",
"name": "sql-validator",
"drop": false,
"target_columns": [
"sql"
],
"validator_type": "code",
"validator_params": {
"code_lang": "sql:ansi"
},
"batch_size": 10
}
],
"processors": []
@ -119,58 +134,10 @@
},
"ui": {
"nodes": [
{
"id": "provider_1",
"x": -1092.2003193114556,
"y": 715.157165665104,
"width": 400
},
{
"id": "model_1",
"x": -546.1001596557278,
"y": 681.8114012018752,
"width": 400
},
{
"id": "domain",
"x": -18.379173679952572,
"y": 137.70260329000595,
"width": 400
},
{
"id": "topic",
"x": -18.6022437080035,
"y": 373.0271253737222,
"width": 400
},
{
"id": "sql_task_type",
"x": 477.85851567876665,
"y": 137.4202046707293,
"width": 400
},
{
"id": "instruction_phrase",
"x": -477.8585156787667,
"y": 138.42770371608808,
"width": 400
},
{
"id": "sql_prompt",
"x": -18.188598798124787,
"y": 701.5157165665104,
"width": 400
},
{
"id": "sql",
"x": -18.188598798124758,
"y": 950.647309355259,
"width": 400
},
{
"id": "note_1",
"x": -103.00586025666547,
"y": -332.088439142397,
"x": 338,
"y": 1020,
"width": 600,
"node_type": "markdown_note",
"name": "note_1",
@ -180,8 +147,8 @@
},
{
"id": "note_2",
"x": 517.0372102151987,
"y": 600.4949327304814,
"x": 1672,
"y": 1577,
"width": 400,
"node_type": "markdown_note",
"name": "note_2",
@ -191,25 +158,79 @@
},
{
"id": "note_3",
"x": 12.635681904967385,
"y": 1224.7626182706356,
"x": 2126,
"y": 1485,
"width": 400,
"node_type": "markdown_note",
"name": "note_3",
"markdown": "The **LLM Code** block (`sql`) generates SQL script from `{{ sql_prompt }}`.\n\n##### In this recipe it returns:\n\n- schema section (`CREATE TABLE`)\n- sample seed rows (`INSERT`)\n- final query (`SELECT`)\n\n##### Current status:\n\n- SQL validation block is **not** included yet in this learning recipe\n- We will add SQL validation later",
"markdown": "The **LLM Code** block (`sql`) generates SQL script from `{{ sql_prompt }}`.\n\n##### In this recipe it returns:\n\n- schema section (`CREATE TABLE`)\n- sample seed rows (`INSERT`)\n- final query (`SELECT`)\n",
"note_color": "#DBEAFE",
"note_opacity": "35"
},
{
"id": "note_4",
"x": -1044,
"y": 108.64730935525904,
"x": 1264,
"y": 1037,
"width": 400,
"node_type": "markdown_note",
"name": "note_4",
"markdown": "Sampler columns are useful during generation, but often noisy in final output.\n\nSet helper columns to **drop=true** (like in this recipe), keep only output columns you want to export.\n\n#### Final keep we have set here:\n\n- `sql_prompt`\n- `sql`\n\n",
"note_color": "#DBEAFE",
"note_opacity": "35"
},
{
"id": "vllm",
"x": 1880,
"y": 781.25,
"width": 400
},
{
"id": "sql-pro",
"x": 1880,
"y": 975.25,
"width": 400
},
{
"id": "domain",
"x": 680,
"y": 1495,
"width": 400
},
{
"id": "topic",
"x": 1160,
"y": 1413,
"width": 400
},
{
"id": "sql_task_type",
"x": 1160,
"y": 1577,
"width": 400
},
{
"id": "instruction_phrase",
"x": 100,
"y": 1495,
"width": 400
},
{
"id": "sql_prompt",
"x": 1666.887417218543,
"y": 1378.9437086092717,
"width": 400
},
{
"id": "sql",
"x": 2120,
"y": 1236.25,
"width": 400
},
{
"id": "sql-validator",
"x": 2600,
"y": 1304.75,
"width": 400
}
],
"edges": [
@ -217,8 +238,8 @@
"from": "domain",
"to": "topic",
"type": "canvas",
"source_handle": "data-out-bottom",
"target_handle": "data-in-top"
"source_handle": "data-out",
"target_handle": "data-in"
},
{
"from": "domain",
@ -238,38 +259,52 @@
"from": "topic",
"to": "sql_prompt",
"type": "canvas",
"source_handle": "data-out-bottom",
"target_handle": "data-in-top"
"source_handle": "data-out",
"target_handle": "data-in"
},
{
"from": "sql_prompt",
"to": "sql",
"type": "canvas",
"source_handle": "data-out-bottom",
"target_handle": "data-in-top"
},
{
"from": "provider_1",
"to": "model_1",
"type": "semantic",
"source_handle": "semantic-out",
"target_handle": "semantic-in"
},
{
"from": "model_1",
"to": "sql_prompt",
"type": "semantic",
"source_handle": "semantic-out",
"source_handle": "data-out",
"target_handle": "data-in"
},
{
"from": "model_1",
"from": "vllm",
"to": "sql-pro",
"type": "semantic",
"source_handle": "semantic-out-bottom",
"target_handle": "semantic-in-top"
},
{
"from": "sql-pro",
"to": "sql",
"type": "semantic",
"source_handle": "semantic-out-bottom",
"target_handle": "data-in-top"
},
{
"from": "sql",
"to": "sql-validator",
"type": "semantic",
"source_handle": "data-out",
"target_handle": "data-in"
},
{
"from": "sql-pro",
"to": "sql_prompt",
"type": "semantic",
"source_handle": "semantic-out-bottom",
"target_handle": "data-in-top"
},
{
"from": "sql_prompt",
"to": "sql_task_type",
"type": "canvas",
"source_handle": "data-out-left",
"target_handle": "data-in-right"
}
],
"layout_direction": "LR"
}
}
}

View file

@ -68,45 +68,51 @@ function findNonOverlappingPosition(
return preferred;
}
function isProviderToConfigEdge(edge: Edge, configs: Record<string, NodeConfig>): boolean {
function isProviderToConfigEdge(
edge: Edge,
configs: Record<string, NodeConfig>,
): boolean {
const source = configs[edge.source];
const target = configs[edge.target];
return source?.kind === "model_provider" && target?.kind === "model_config";
}
function isConfigToLlmEdge(edge: Edge, configs: Record<string, NodeConfig>): boolean {
function isConfigToLlmEdge(
edge: Edge,
configs: Record<string, NodeConfig>,
): boolean {
const source = configs[edge.source];
const target = configs[edge.target];
return source?.kind === "model_config" && target?.kind === "llm";
}
function isDataToLlmEdge(edge: Edge, configs: Record<string, NodeConfig>): boolean {
const source = configs[edge.source];
const target = configs[edge.target];
return Boolean(
source &&
target &&
source.kind !== "model_config" &&
target.kind === "llm" &&
edge.type !== "semantic",
);
}
function usageKey(nodeId: string, handleId: string): string {
return `${nodeId}::${handleId}`;
}
function incrementUsage(map: Map<string, number>, nodeId: string, handleId: string): void {
function incrementUsage(
map: Map<string, number>,
nodeId: string,
handleId: string,
): void {
const key = usageKey(nodeId, handleId);
map.set(key, (map.get(key) ?? 0) + 1);
}
function decrementUsage(map: Map<string, number>, nodeId: string, handleId: string): void {
function decrementUsage(
map: Map<string, number>,
nodeId: string,
handleId: string,
): void {
const key = usageKey(nodeId, handleId);
map.set(key, Math.max(0, (map.get(key) ?? 0) - 1));
}
function getUsage(map: Map<string, number>, nodeId: string, handleId: string): number {
function getUsage(
map: Map<string, number>,
nodeId: string,
handleId: string,
): number {
return map.get(usageKey(nodeId, handleId)) ?? 0;
}
@ -115,7 +121,9 @@ function pickHandleByUsage(
nodeId: string,
usageMap: Map<string, number>,
): string {
const free = candidates.filter((handleId) => getUsage(usageMap, nodeId, handleId) === 0);
const free = candidates.filter(
(handleId) => getUsage(usageMap, nodeId, handleId) === 0,
);
if (free.length > 0) {
return free[0];
}
@ -152,7 +160,10 @@ function getNodeCenter(node: RecipeNode): { x: number; y: number } {
};
}
function collectBounds(ids: string[], nodesById: Map<string, RecipeNode>): Bounds | null {
function collectBounds(
ids: string[],
nodesById: Map<string, RecipeNode>,
): Bounds | null {
const rects = ids
.map((id) => nodesById.get(id))
.flatMap((node) => (node ? [toRect(node)] : []));
@ -198,20 +209,26 @@ function sortPreferredLlmTargetHandles(
return [...verticalFirst, HANDLE_IDS.dataIn, HANDLE_IDS.dataInRight];
}
function getProviderSourceHandleCandidates(direction: LayoutDirection): string[] {
function getProviderSourceHandleCandidates(
direction: LayoutDirection,
): string[] {
return direction === "TB"
? [HANDLE_IDS.semanticOut, HANDLE_IDS.semanticOutBottom]
: [HANDLE_IDS.semanticOutBottom, HANDLE_IDS.semanticOut];
}
function getProviderTargetHandleCandidates(direction: LayoutDirection): string[] {
function getProviderTargetHandleCandidates(
direction: LayoutDirection,
): string[] {
return direction === "TB"
? [HANDLE_IDS.semanticIn, HANDLE_IDS.semanticInTop]
: [HANDLE_IDS.semanticInTop, HANDLE_IDS.semanticIn];
}
function getConfigSourceHandleCandidates(direction: LayoutDirection): string[] {
return direction === "TB" ? [HANDLE_IDS.semanticOut] : [HANDLE_IDS.semanticOutBottom];
return direction === "TB"
? [HANDLE_IDS.semanticOut]
: [HANDLE_IDS.semanticOutBottom];
}
export function optimizeModelInfraEdgeHandles(
@ -248,28 +265,31 @@ export function optimizeModelInfraEdgeHandles(
const targetHandleBefore = normalizeRecipeHandleId(edge.targetHandle);
const isModelSemantic =
isProviderToConfigEdge(edge, configs) || isConfigToLlmEdge(edge, configs);
const isLlmDataTarget = isDataToLlmEdge(edge, configs);
if (!isModelSemantic && !isLlmDataTarget) {
if (!isModelSemantic) {
nextEdges.push(edge);
continue;
}
if (isModelSemantic) {
if (sourceHandleBefore) {
decrementUsage(sourceUsage, edge.source, sourceHandleBefore);
}
if (targetHandleBefore) {
decrementUsage(targetUsage, edge.target, targetHandleBefore);
}
} else if (targetHandleBefore) {
if (sourceHandleBefore) {
decrementUsage(sourceUsage, edge.source, sourceHandleBefore);
}
if (targetHandleBefore) {
decrementUsage(targetUsage, edge.target, targetHandleBefore);
}
if (isProviderToConfigEdge(edge, configs)) {
const sourceCandidates = getProviderSourceHandleCandidates(direction);
const targetCandidates = getProviderTargetHandleCandidates(direction);
const sourceHandle = pickHandleByUsage(sourceCandidates, edge.source, sourceUsage);
const targetHandle = pickHandleByUsage(targetCandidates, edge.target, targetUsage);
const sourceHandle = pickHandleByUsage(
sourceCandidates,
edge.source,
sourceUsage,
);
const targetHandle = pickHandleByUsage(
targetCandidates,
edge.target,
targetUsage,
);
nextEdges.push(
applyEdgeWithHandles(
edge,
@ -287,19 +307,17 @@ export function optimizeModelInfraEdgeHandles(
nodesById.get(edge.source),
nodesById.get(edge.target),
);
if (isLlmDataTarget) {
const targetHandle = pickHandleByUsage(targetCandidates, edge.target, targetUsage);
incrementUsage(targetUsage, edge.target, targetHandle);
nextEdges.push({
...edge,
targetHandle,
});
continue;
}
const sourceCandidates = getConfigSourceHandleCandidates(direction);
const sourceHandle = pickHandleByUsage(sourceCandidates, edge.source, sourceUsage);
const targetHandle = pickHandleByUsage(targetCandidates, edge.target, targetUsage);
const sourceHandle = pickHandleByUsage(
sourceCandidates,
edge.source,
sourceUsage,
);
const targetHandle = pickHandleByUsage(
targetCandidates,
edge.target,
targetUsage,
);
nextEdges.push(
applyEdgeWithHandles(
edge,
@ -343,13 +361,19 @@ export function centerModelInfraNodes(
}
const modelConfigIds = Object.values(configs)
.filter((config) => config.kind === "model_config" && nodesById.has(config.id))
.filter(
(config) => config.kind === "model_config" && nodesById.has(config.id),
)
.map((config) => config.id);
const modelProviderIds = Object.values(configs)
.filter((config) => config.kind === "model_provider" && nodesById.has(config.id))
.filter(
(config) => config.kind === "model_provider" && nodesById.has(config.id),
)
.map((config) => config.id);
const occupiedById = new Map(nodes.map((node) => [node.id, toRect(node)] as const));
const occupiedById = new Map(
nodes.map((node) => [node.id, toRect(node)] as const),
);
const clusterGap = 72;
const placeNode = (nodeId: string, preferred: XYPosition): void => {