Skip to content

Commit af06c4f

Browse files
committed
Fix router block for copilot
1 parent 40a7825 commit af06c4f

File tree

2 files changed

+223
-4
lines changed

2 files changed

+223
-4
lines changed

apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -878,6 +878,25 @@ function validateSourceHandleForBlock(
878878
error: `Invalid source handle "${sourceHandle}" for router block. Valid handles: source, ${EDGE.ROUTER_PREFIX}{targetId}, error`,
879879
}
880880

881+
case 'router_v2': {
882+
if (!sourceHandle.startsWith(EDGE.ROUTER_PREFIX)) {
883+
return {
884+
valid: false,
885+
error: `Invalid source handle "${sourceHandle}" for router_v2 block. Must start with "${EDGE.ROUTER_PREFIX}"`,
886+
}
887+
}
888+
889+
const routesValue = sourceBlock?.subBlocks?.routes?.value
890+
if (!routesValue) {
891+
return {
892+
valid: false,
893+
error: `Invalid router handle "${sourceHandle}" - no routes defined`,
894+
}
895+
}
896+
897+
return validateRouterHandle(sourceHandle, sourceBlock.id, routesValue)
898+
}
899+
881900
default:
882901
if (sourceHandle === 'source') {
883902
return { valid: true }
@@ -963,6 +982,85 @@ function validateConditionHandle(
963982
}
964983
}
965984

985+
/**
986+
* Validates router handle references a valid route in the block.
987+
* Accepts both internal IDs (router-{routeId}) and semantic keys (router-{blockId}-route-1)
988+
*/
989+
function validateRouterHandle(
990+
sourceHandle: string,
991+
blockId: string,
992+
routesValue: string | any[]
993+
): EdgeHandleValidationResult {
994+
let routes: any[]
995+
if (typeof routesValue === 'string') {
996+
try {
997+
routes = JSON.parse(routesValue)
998+
} catch {
999+
return {
1000+
valid: false,
1001+
error: `Cannot validate router handle "${sourceHandle}" - routes is not valid JSON`,
1002+
}
1003+
}
1004+
} else if (Array.isArray(routesValue)) {
1005+
routes = routesValue
1006+
} else {
1007+
return {
1008+
valid: false,
1009+
error: `Cannot validate router handle "${sourceHandle}" - routes is not an array`,
1010+
}
1011+
}
1012+
1013+
if (!Array.isArray(routes) || routes.length === 0) {
1014+
return {
1015+
valid: false,
1016+
error: `Invalid router handle "${sourceHandle}" - no routes defined`,
1017+
}
1018+
}
1019+
1020+
const validHandles = new Set<string>()
1021+
const semanticPrefix = `router-${blockId}-`
1022+
1023+
for (let i = 0; i < routes.length; i++) {
1024+
const route = routes[i]
1025+
1026+
// Accept internal ID format: router-{uuid}
1027+
if (route.id) {
1028+
validHandles.add(`router-${route.id}`)
1029+
}
1030+
1031+
// Accept 1-indexed route number format: router-{blockId}-route-1, router-{blockId}-route-2, etc.
1032+
validHandles.add(`${semanticPrefix}route-${i + 1}`)
1033+
1034+
// Accept normalized title format: router-{blockId}-{normalized-title}
1035+
// Normalize: lowercase, replace spaces with dashes, remove special chars
1036+
if (route.title && typeof route.title === 'string') {
1037+
const normalizedTitle = route.title
1038+
.toLowerCase()
1039+
.replace(/\s+/g, '-')
1040+
.replace(/[^a-z0-9-]/g, '')
1041+
if (normalizedTitle) {
1042+
validHandles.add(`${semanticPrefix}${normalizedTitle}`)
1043+
}
1044+
}
1045+
}
1046+
1047+
if (validHandles.has(sourceHandle)) {
1048+
return { valid: true }
1049+
}
1050+
1051+
const validOptions = Array.from(validHandles).slice(0, 5)
1052+
const moreCount = validHandles.size - validOptions.length
1053+
let validOptionsStr = validOptions.join(', ')
1054+
if (moreCount > 0) {
1055+
validOptionsStr += `, ... and ${moreCount} more`
1056+
}
1057+
1058+
return {
1059+
valid: false,
1060+
error: `Invalid router handle "${sourceHandle}". Valid handles: ${validOptionsStr}`,
1061+
}
1062+
}
1063+
9661064
/**
9671065
* Validates target handle is valid (must be 'target')
9681066
*/

apps/sim/lib/workflows/sanitization/json-sanitizer.ts

Lines changed: 125 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -268,12 +268,130 @@ function sanitizeSubBlocks(
268268
return sanitized
269269
}
270270

271+
/**
272+
* Convert internal condition handle (condition-{uuid}) to semantic format (condition-{blockId}-if)
273+
*/
274+
function convertConditionHandleToSemantic(
275+
handle: string,
276+
blockId: string,
277+
block: BlockState
278+
): string {
279+
if (!handle.startsWith('condition-')) {
280+
return handle
281+
}
282+
283+
// Extract the condition UUID from the handle
284+
const conditionId = handle.substring('condition-'.length)
285+
286+
// Get conditions from block subBlocks
287+
const conditionsValue = block.subBlocks?.conditions?.value
288+
if (!conditionsValue || typeof conditionsValue !== 'string') {
289+
return handle
290+
}
291+
292+
let conditions: Array<{ id: string; title: string }>
293+
try {
294+
conditions = JSON.parse(conditionsValue)
295+
} catch {
296+
return handle
297+
}
298+
299+
if (!Array.isArray(conditions)) {
300+
return handle
301+
}
302+
303+
// Find the condition by ID and generate semantic handle
304+
let elseIfCount = 0
305+
for (const condition of conditions) {
306+
const title = condition.title?.toLowerCase()
307+
if (condition.id === conditionId) {
308+
if (title === 'if') {
309+
return `condition-${blockId}-if`
310+
} else if (title === 'else if') {
311+
elseIfCount++
312+
return elseIfCount === 1
313+
? `condition-${blockId}-else-if`
314+
: `condition-${blockId}-else-if-${elseIfCount}`
315+
} else if (title === 'else') {
316+
return `condition-${blockId}-else`
317+
}
318+
}
319+
// Count else-ifs as we iterate
320+
if (title === 'else if') {
321+
elseIfCount++
322+
}
323+
}
324+
325+
// Fallback: return original handle if condition not found
326+
return handle
327+
}
328+
329+
/**
330+
* Convert internal router handle (router-{uuid}) to semantic format (router-{blockId}-route-N)
331+
*/
332+
function convertRouterHandleToSemantic(
333+
handle: string,
334+
blockId: string,
335+
block: BlockState
336+
): string {
337+
if (!handle.startsWith('router-')) {
338+
return handle
339+
}
340+
341+
// Extract the route UUID from the handle
342+
const routeId = handle.substring('router-'.length)
343+
344+
// Get routes from block subBlocks
345+
const routesValue = block.subBlocks?.routes?.value
346+
if (!routesValue || typeof routesValue !== 'string') {
347+
return handle
348+
}
349+
350+
let routes: Array<{ id: string; title?: string }>
351+
try {
352+
routes = JSON.parse(routesValue)
353+
} catch {
354+
return handle
355+
}
356+
357+
if (!Array.isArray(routes)) {
358+
return handle
359+
}
360+
361+
// Find the route by ID and generate semantic handle (1-indexed)
362+
for (let i = 0; i < routes.length; i++) {
363+
if (routes[i].id === routeId) {
364+
return `router-${blockId}-route-${i + 1}`
365+
}
366+
}
367+
368+
// Fallback: return original handle if route not found
369+
return handle
370+
}
371+
372+
/**
373+
* Convert source handle to semantic format for condition and router blocks
374+
*/
375+
function convertToSemanticHandle(handle: string, blockId: string, block: BlockState): string {
376+
if (handle.startsWith('condition-') && block.type === 'condition') {
377+
return convertConditionHandleToSemantic(handle, blockId, block)
378+
}
379+
380+
if (handle.startsWith('router-') && block.type === 'router_v2') {
381+
return convertRouterHandleToSemantic(handle, blockId, block)
382+
}
383+
384+
return handle
385+
}
386+
271387
/**
272388
* Extract connections for a block from edges and format as operations-style connections
389+
* Converts internal UUID handles to semantic format for training data
273390
*/
274391
function extractConnectionsForBlock(
275392
blockId: string,
276-
edges: WorkflowState['edges']
393+
edges: WorkflowState['edges'],
394+
block: BlockState
277395
): Record<string, string | string[]> | undefined {
278396
const connections: Record<string, string[]> = {}
279397

@@ -284,9 +402,12 @@ function extractConnectionsForBlock(
284402
return undefined
285403
}
286404

287-
// Group by source handle
405+
// Group by source handle (converting to semantic format)
288406
for (const edge of outgoingEdges) {
289-
const handle = edge.sourceHandle || 'source'
407+
let handle = edge.sourceHandle || 'source'
408+
409+
// Convert internal UUID handles to semantic format
410+
handle = convertToSemanticHandle(handle, blockId, block)
290411

291412
if (!connections[handle]) {
292413
connections[handle] = []
@@ -321,7 +442,7 @@ export function sanitizeForCopilot(state: WorkflowState): CopilotWorkflowState {
321442

322443
// Helper to recursively sanitize a block and its children
323444
const sanitizeBlock = (blockId: string, block: BlockState): CopilotBlockState => {
324-
const connections = extractConnectionsForBlock(blockId, state.edges)
445+
const connections = extractConnectionsForBlock(blockId, state.edges, block)
325446

326447
// For loop/parallel blocks, extract config from block.data instead of subBlocks
327448
let inputs: Record<string, string | number | string[][] | object>

0 commit comments

Comments
 (0)