Handle loops/parallel

This commit is contained in:
Siddharth Ganesan
2025-07-09 10:23:31 -07:00
parent a2827a52c0
commit 218041dba3
2 changed files with 134 additions and 6 deletions
+38 -2
View File
@@ -12,6 +12,7 @@ interface YamlBlock {
inputs?: Record<string, any>
preceding?: string[]
following?: string[]
parentId?: string // Add parentId for nested blocks
}
interface YamlWorkflow {
@@ -34,8 +35,38 @@ function extractBlockInputs(
// Get subblock values for this block (if provided)
const blockSubBlockValues = subBlockValues?.[blockId] || {}
// Special handling for loop and parallel blocks
if (blockState.type === 'loop' || blockState.type === 'parallel') {
// Extract configuration from blockState.data instead of subBlocks
if (blockState.data) {
Object.entries(blockState.data).forEach(([key, value]) => {
// Include relevant configuration properties
if (key === 'count' || key === 'loopType' || key === 'collection' ||
key === 'parallelType' || key === 'distribution') {
if (value !== undefined && value !== null && value !== '') {
inputs[key] = value
}
}
// Also include any override values from subBlockValues if they exist
const overrideValue = blockSubBlockValues[key]
if (overrideValue !== undefined && overrideValue !== null && overrideValue !== '') {
inputs[key] = overrideValue
}
})
}
// Include any additional values from subBlockValues that might not be in data
Object.entries(blockSubBlockValues).forEach(([key, value]) => {
if (value !== undefined && value !== null && value !== '' && !inputs.hasOwnProperty(key)) {
inputs[key] = value
}
})
return inputs
}
if (!blockConfig) {
// For custom blocks like loops/parallels, extract available subBlock values
// For other custom blocks without config, extract available subBlock values
Object.entries(blockState.subBlocks || {}).forEach(([subBlockId, subBlockState]) => {
const value = blockSubBlockValues[subBlockId] ?? subBlockState.value
if (value !== undefined && value !== null && value !== '') {
@@ -45,7 +76,7 @@ function extractBlockInputs(
return inputs
}
// Process each subBlock configuration
// Process each subBlock configuration for regular blocks
blockConfig.subBlocks.forEach((subBlockConfig: SubBlockConfig) => {
const subBlockId = subBlockConfig.id
@@ -175,6 +206,11 @@ export function generateWorkflowYaml(
yamlBlock.following = following
}
// Include parent-child relationship for nested blocks
if (blockState.data?.parentId) {
yamlBlock.parentId = blockState.data.parentId
}
yamlWorkflow.blocks[blockId] = yamlBlock
})
+96 -4
View File
@@ -11,6 +11,7 @@ interface YamlBlock {
inputs?: Record<string, any>
preceding?: string[]
following?: string[]
parentId?: string // Add parentId for nested blocks
}
interface YamlWorkflow {
@@ -257,6 +258,55 @@ function calculateBlockPositions(
return positions
}
/**
* Sort blocks to ensure parents are processed before children
* This ensures proper creation order for nested blocks
*/
function sortBlocksByParentChildOrder(blocks: ImportedBlock[]): ImportedBlock[] {
const sorted: ImportedBlock[] = []
const processed = new Set<string>()
const visiting = new Set<string>() // Track blocks currently being processed to detect cycles
// Create a map for quick lookup
const blockMap = new Map<string, ImportedBlock>()
blocks.forEach(block => blockMap.set(block.id, block))
// Process blocks recursively, ensuring parents are added first
function processBlock(block: ImportedBlock) {
if (processed.has(block.id)) {
return // Already processed
}
if (visiting.has(block.id)) {
// Circular dependency detected - break the cycle by processing this block without its parent
logger.warn(`Circular parent-child dependency detected for block ${block.id}, breaking cycle`)
sorted.push(block)
processed.add(block.id)
return
}
visiting.add(block.id)
// If this block has a parent, ensure the parent is processed first
if (block.parentId) {
const parentBlock = blockMap.get(block.parentId)
if (parentBlock && !processed.has(block.parentId)) {
processBlock(parentBlock)
}
}
// Now process this block
visiting.delete(block.id)
sorted.push(block)
processed.add(block.id)
}
// Process all blocks
blocks.forEach(block => processBlock(block))
return sorted
}
/**
* Convert YAML workflow to importable format
*/
@@ -296,11 +346,28 @@ export function convertYamlToWorkflow(yamlWorkflow: YamlWorkflow): ImportResult
// Add container-specific data
if (yamlBlock.type === 'loop' || yamlBlock.type === 'parallel') {
// For loop/parallel blocks, map the inputs to the data field since they don't use subBlocks
importedBlock.data = {
width: 500,
height: 300,
type: yamlBlock.type === 'loop' ? 'loopNode' : 'parallelNode',
// Map YAML inputs to data properties for loop/parallel blocks
...(yamlBlock.inputs || {}),
}
// Clear inputs since they're now in data
importedBlock.inputs = {}
}
// Handle parent-child relationships for nested blocks
if (yamlBlock.parentId) {
importedBlock.parentId = yamlBlock.parentId
importedBlock.extent = 'parent'
// Also add to data for consistency with how the system works
if (!importedBlock.data) {
importedBlock.data = {}
}
importedBlock.data.parentId = yamlBlock.parentId
importedBlock.data.extent = 'parent'
}
blocks.push(importedBlock)
@@ -326,7 +393,10 @@ export function convertYamlToWorkflow(yamlWorkflow: YamlWorkflow): ImportResult
}
})
return { blocks, edges, errors, warnings }
// Sort blocks to ensure parents are created before children
const sortedBlocks = sortBlocksByParentChildOrder(blocks)
return { blocks: sortedBlocks, edges, errors, warnings }
}
/**
@@ -474,6 +544,8 @@ export async function importWorkflowFromYaml(
}
// Create all other blocks
// Note: blocks are now sorted to ensure parents come before children,
// but we still need the two-phase approach because we're generating new UUIDs
let blocksProcessed = 0
for (const block of blocks) {
if (block.type === 'starter') {
@@ -499,10 +571,11 @@ export async function importWorkflowFromYaml(
horizontalHandles: true,
isWide: false,
height: 0,
data: block.data || {},
data: block.data || {}, // Configuration is already in block.data from convertYamlToWorkflow
}
completeSubBlockValues[blockId] = { ...block.inputs }
// Loop/parallel blocks don't use subBlocks, their config is in data
// No need to set completeSubBlockValues since they don't have subBlocks
blocksProcessed++
} else if (blockConfig) {
// Handle regular blocks
@@ -526,7 +599,7 @@ export async function importWorkflowFromYaml(
horizontalHandles: true,
isWide: false,
height: 0,
data: block.data || {},
data: block.data || {}, // This already includes parentId and extent from convertYamlToWorkflow
}
// Set block input values
@@ -537,6 +610,25 @@ export async function importWorkflowFromYaml(
}
}
// Update parent-child relationships with mapped IDs
// This two-phase approach is necessary because:
// 1. We generate new UUIDs for all blocks (can't reuse YAML IDs)
// 2. Parent references in YAML use the original IDs, need to map to new UUIDs
// 3. All blocks must exist before we can map their parent references
for (const [blockId, blockData] of Object.entries(completeBlocks)) {
if (blockData.data?.parentId) {
const mappedParentId = yamlIdToActualId.get(blockData.data.parentId)
if (mappedParentId) {
blockData.data.parentId = mappedParentId
} else {
logger.warn(`Parent block not found for mapping: ${blockData.data.parentId}`)
// Remove invalid parent reference
delete blockData.data.parentId
delete blockData.data.extent
}
}
}
// Create complete edges using the ID mapping
const completeEdges: any[] = []
for (const edge of edges) {