mirror of
https://github.com/simstudioai/sim.git
synced 2026-02-08 21:54:57 -05:00
360 lines
8.6 KiB
TypeScript
360 lines
8.6 KiB
TypeScript
import {
|
|
createAgentBlock,
|
|
createBlock,
|
|
createFunctionBlock,
|
|
createStarterBlock,
|
|
} from '../factories/block.factory'
|
|
import type { Position } from '../types'
|
|
|
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
|
|
/**
|
|
* Fluent builder for creating complex workflow states.
|
|
*
|
|
* Use this when you need fine-grained control over workflow construction,
|
|
* especially for testing edge cases or complex scenarios.
|
|
*
|
|
* @example
|
|
* ```ts
|
|
* // Simple linear workflow
|
|
* const workflow = new WorkflowBuilder()
|
|
* .addStarter('start')
|
|
* .addAgent('agent', { x: 200, y: 0 })
|
|
* .addFunction('end', { x: 400, y: 0 })
|
|
* .connect('start', 'agent')
|
|
* .connect('agent', 'end')
|
|
* .build()
|
|
*
|
|
* // Using static presets
|
|
* const workflow = WorkflowBuilder.linear(5).build()
|
|
* const workflow = WorkflowBuilder.branching().build()
|
|
* ```
|
|
*/
|
|
export class WorkflowBuilder {
|
|
private blocks: Record<string, any> = {}
|
|
private edges: any[] = []
|
|
private loops: Record<string, any> = {}
|
|
private parallels: Record<string, any> = {}
|
|
private variables: any[] = []
|
|
private isDeployed = false
|
|
|
|
/**
|
|
* Adds a generic block to the workflow.
|
|
*/
|
|
addBlock(id: string, type: string, position?: Position, name?: string): this {
|
|
this.blocks[id] = createBlock({
|
|
id,
|
|
type,
|
|
name: name ?? id,
|
|
position: position ?? { x: 0, y: 0 },
|
|
})
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* Adds a starter block (workflow entry point).
|
|
*/
|
|
addStarter(id = 'start', position?: Position): this {
|
|
this.blocks[id] = createStarterBlock({
|
|
id,
|
|
position: position ?? { x: 0, y: 0 },
|
|
})
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* Adds a function block.
|
|
*/
|
|
addFunction(id: string, position?: Position, name?: string): this {
|
|
this.blocks[id] = createFunctionBlock({
|
|
id,
|
|
name: name ?? id,
|
|
position: position ?? { x: 0, y: 0 },
|
|
})
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* Adds an agent block.
|
|
*/
|
|
addAgent(id: string, position?: Position, name?: string): this {
|
|
this.blocks[id] = createAgentBlock({
|
|
id,
|
|
name: name ?? id,
|
|
position: position ?? { x: 0, y: 0 },
|
|
})
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* Adds a condition block.
|
|
*/
|
|
addCondition(id: string, position?: Position, name?: string): this {
|
|
this.blocks[id] = createBlock({
|
|
id,
|
|
type: 'condition',
|
|
name: name ?? id,
|
|
position: position ?? { x: 0, y: 0 },
|
|
})
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* Adds a loop container block.
|
|
*/
|
|
addLoop(
|
|
id: string,
|
|
position?: Position,
|
|
config?: {
|
|
iterations?: number
|
|
loopType?: 'for' | 'forEach' | 'while' | 'doWhile'
|
|
}
|
|
): this {
|
|
this.blocks[id] = createBlock({
|
|
id,
|
|
type: 'loop',
|
|
name: 'Loop',
|
|
position: position ?? { x: 0, y: 0 },
|
|
data: {
|
|
loopType: config?.loopType ?? 'for',
|
|
count: config?.iterations ?? 3,
|
|
type: 'loop',
|
|
},
|
|
})
|
|
this.loops[id] = {
|
|
id,
|
|
nodes: [],
|
|
iterations: config?.iterations ?? 3,
|
|
loopType: config?.loopType ?? 'for',
|
|
}
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* Adds a block as a child of a loop container.
|
|
*/
|
|
addLoopChild(loopId: string, childId: string, type = 'function', position?: Position): this {
|
|
if (!this.loops[loopId]) {
|
|
throw new Error(`Loop ${loopId} does not exist. Call addLoop first.`)
|
|
}
|
|
|
|
this.blocks[childId] = createBlock({
|
|
id: childId,
|
|
type,
|
|
name: childId,
|
|
position: position ?? { x: 50, y: 50 },
|
|
parentId: loopId,
|
|
})
|
|
|
|
this.loops[loopId].nodes.push(childId)
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* Adds a parallel container block.
|
|
*/
|
|
addParallel(
|
|
id: string,
|
|
position?: Position,
|
|
config?: {
|
|
count?: number
|
|
parallelType?: 'count' | 'collection'
|
|
}
|
|
): this {
|
|
this.blocks[id] = createBlock({
|
|
id,
|
|
type: 'parallel',
|
|
name: 'Parallel',
|
|
position: position ?? { x: 0, y: 0 },
|
|
data: {
|
|
parallelType: config?.parallelType ?? 'count',
|
|
count: config?.count ?? 2,
|
|
type: 'parallel',
|
|
},
|
|
})
|
|
this.parallels[id] = {
|
|
id,
|
|
nodes: [],
|
|
count: config?.count ?? 2,
|
|
parallelType: config?.parallelType ?? 'count',
|
|
}
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* Adds a block as a child of a parallel container.
|
|
*/
|
|
addParallelChild(
|
|
parallelId: string,
|
|
childId: string,
|
|
type = 'function',
|
|
position?: Position
|
|
): this {
|
|
if (!this.parallels[parallelId]) {
|
|
throw new Error(`Parallel ${parallelId} does not exist. Call addParallel first.`)
|
|
}
|
|
|
|
this.blocks[childId] = createBlock({
|
|
id: childId,
|
|
type,
|
|
name: childId,
|
|
position: position ?? { x: 50, y: 50 },
|
|
parentId: parallelId,
|
|
})
|
|
|
|
this.parallels[parallelId].nodes.push(childId)
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* Creates an edge connecting two blocks.
|
|
*/
|
|
connect(sourceId: string, targetId: string, sourceHandle?: string, targetHandle?: string): this {
|
|
this.edges.push({
|
|
id: `${sourceId}-${targetId}`,
|
|
source: sourceId,
|
|
target: targetId,
|
|
sourceHandle,
|
|
targetHandle,
|
|
})
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* Adds a workflow variable.
|
|
*/
|
|
addVariable(
|
|
name: string,
|
|
type: 'string' | 'number' | 'boolean' | 'object' | 'array' | 'plain',
|
|
value: any
|
|
): this {
|
|
this.variables?.push({
|
|
id: `var-${Math.random().toString(36).substring(2, 8)}`,
|
|
name,
|
|
type,
|
|
value,
|
|
})
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* Sets the workflow as deployed.
|
|
*/
|
|
setDeployed(deployed = true): this {
|
|
this.isDeployed = deployed
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* Builds and returns the workflow state.
|
|
* Returns `any` to be assignable to any app's workflow type.
|
|
*/
|
|
build(): any {
|
|
return {
|
|
blocks: this.blocks,
|
|
edges: this.edges,
|
|
loops: this.loops,
|
|
parallels: this.parallels,
|
|
lastSaved: Date.now(),
|
|
isDeployed: this.isDeployed,
|
|
variables: this.variables?.length ? this.variables : undefined,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates a workflow with the specified blocks and connects them linearly.
|
|
*/
|
|
static chain(...blockConfigs: Array<{ id: string; type: string }>): WorkflowBuilder {
|
|
const builder = new WorkflowBuilder()
|
|
let x = 0
|
|
const spacing = 200
|
|
|
|
blockConfigs.forEach((config, index) => {
|
|
builder.addBlock(config.id, config.type, { x, y: 0 })
|
|
x += spacing
|
|
|
|
if (index > 0) {
|
|
builder.connect(blockConfigs[index - 1].id, config.id)
|
|
}
|
|
})
|
|
|
|
return builder
|
|
}
|
|
|
|
/**
|
|
* Creates a linear workflow with N blocks.
|
|
* First block is a starter, rest are function blocks.
|
|
*/
|
|
static linear(blockCount: number): WorkflowBuilder {
|
|
const builder = new WorkflowBuilder()
|
|
const spacing = 200
|
|
|
|
for (let i = 0; i < blockCount; i++) {
|
|
const id = `block-${i}`
|
|
const position = { x: i * spacing, y: 0 }
|
|
|
|
if (i === 0) {
|
|
builder.addStarter(id, position)
|
|
} else {
|
|
builder.addFunction(id, position, `Step ${i}`)
|
|
}
|
|
|
|
if (i > 0) {
|
|
builder.connect(`block-${i - 1}`, id)
|
|
}
|
|
}
|
|
|
|
return builder
|
|
}
|
|
|
|
/**
|
|
* Creates a branching workflow with a condition.
|
|
*
|
|
* Structure:
|
|
* ```
|
|
* ┌─→ true ─┐
|
|
* start ─→ cond ├─→ end
|
|
* └─→ false ┘
|
|
* ```
|
|
*/
|
|
static branching(): WorkflowBuilder {
|
|
return new WorkflowBuilder()
|
|
.addStarter('start', { x: 0, y: 0 })
|
|
.addCondition('condition', { x: 200, y: 0 })
|
|
.addFunction('true-branch', { x: 400, y: -100 }, 'If True')
|
|
.addFunction('false-branch', { x: 400, y: 100 }, 'If False')
|
|
.addFunction('end', { x: 600, y: 0 }, 'End')
|
|
.connect('start', 'condition')
|
|
.connect('condition', 'true-branch', 'condition-if')
|
|
.connect('condition', 'false-branch', 'condition-else')
|
|
.connect('true-branch', 'end')
|
|
.connect('false-branch', 'end')
|
|
}
|
|
|
|
/**
|
|
* Creates a workflow with a loop.
|
|
*/
|
|
static withLoop(iterations = 3): WorkflowBuilder {
|
|
return new WorkflowBuilder()
|
|
.addStarter('start', { x: 0, y: 0 })
|
|
.addLoop('loop', { x: 200, y: 0 }, { iterations })
|
|
.addLoopChild('loop', 'loop-body', 'function', { x: 50, y: 50 })
|
|
.addFunction('end', { x: 500, y: 0 })
|
|
.connect('start', 'loop')
|
|
.connect('loop', 'end')
|
|
}
|
|
|
|
/**
|
|
* Creates a workflow with parallel execution.
|
|
*/
|
|
static withParallel(count = 2): WorkflowBuilder {
|
|
return new WorkflowBuilder()
|
|
.addStarter('start', { x: 0, y: 0 })
|
|
.addParallel('parallel', { x: 200, y: 0 }, { count })
|
|
.addParallelChild('parallel', 'parallel-task', 'function', { x: 50, y: 50 })
|
|
.addFunction('end', { x: 500, y: 0 })
|
|
.connect('start', 'parallel')
|
|
.connect('parallel', 'end')
|
|
}
|
|
}
|