mirror of
https://github.com/simstudioai/sim.git
synced 2026-02-16 17:35:49 -05:00
* fix for variable format + trig * fixed slack variable * microsoft teams working * fixed outlook, plus added other minor documentation changes and fixed subblock * removed discord webhook logic * added airtable logic * bun run lint * test * test again * test again 2 * test again 3 * test again 4 * test again 4 * test again 4 * bun run lint * test 5 * test 6 * test 7 * test 7 * test 7 * test 7 * test 7 * test 7 * test 8 * test 9 * test 9 * test 9 * test 10 * test 10 * bun run lint, plus github fixed * removed some debug statements #935 * testing resolver removing * testing trig --------- Co-authored-by: Adam Gough <adamgough@Adams-MacBook-Pro.local> Co-authored-by: Adam Gough <adamgough@Mac.attlocal.net>
356 lines
12 KiB
TypeScript
356 lines
12 KiB
TypeScript
import type { Edge } from 'reactflow'
|
|
import { createLogger } from '@/lib/logs/console/logger'
|
|
import { getBlock } from '@/blocks'
|
|
import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types'
|
|
import type { BlockState, Loop, Parallel } from '@/stores/workflows/workflow/types'
|
|
import { getTool } from '@/tools/utils'
|
|
|
|
const logger = createLogger('Serializer')
|
|
|
|
export class Serializer {
|
|
serializeWorkflow(
|
|
blocks: Record<string, BlockState>,
|
|
edges: Edge[],
|
|
loops: Record<string, Loop>,
|
|
parallels?: Record<string, Parallel>,
|
|
validateRequired = false
|
|
): SerializedWorkflow {
|
|
return {
|
|
version: '1.0',
|
|
blocks: Object.values(blocks).map((block) => this.serializeBlock(block, validateRequired)),
|
|
connections: edges.map((edge) => ({
|
|
source: edge.source,
|
|
target: edge.target,
|
|
sourceHandle: edge.sourceHandle || undefined,
|
|
targetHandle: edge.targetHandle || undefined,
|
|
})),
|
|
loops,
|
|
parallels,
|
|
}
|
|
}
|
|
|
|
private serializeBlock(block: BlockState, validateRequired = false): SerializedBlock {
|
|
// Special handling for subflow blocks (loops, parallels, etc.)
|
|
if (block.type === 'loop' || block.type === 'parallel') {
|
|
return {
|
|
id: block.id,
|
|
position: block.position,
|
|
config: {
|
|
tool: '', // Loop blocks don't have tools
|
|
params: block.data || {}, // Preserve the block data (parallelType, count, etc.)
|
|
},
|
|
inputs: {},
|
|
outputs: block.outputs,
|
|
metadata: {
|
|
id: block.type,
|
|
name: block.name,
|
|
description: block.type === 'loop' ? 'Loop container' : 'Parallel container',
|
|
category: 'subflow',
|
|
color: block.type === 'loop' ? '#3b82f6' : '#8b5cf6',
|
|
},
|
|
enabled: block.enabled,
|
|
}
|
|
}
|
|
|
|
const blockConfig = getBlock(block.type)
|
|
if (!blockConfig) {
|
|
throw new Error(`Invalid block type: ${block.type}`)
|
|
}
|
|
|
|
// Extract parameters from UI state
|
|
const params = this.extractParams(block)
|
|
|
|
// Validate required fields that only users can provide (before execution starts)
|
|
if (validateRequired) {
|
|
this.validateRequiredFieldsBeforeExecution(block, blockConfig, params)
|
|
}
|
|
|
|
let toolId = ''
|
|
|
|
if (block.type === 'agent' && params.tools) {
|
|
// Process the tools in the agent block
|
|
try {
|
|
const tools = Array.isArray(params.tools) ? params.tools : JSON.parse(params.tools)
|
|
|
|
// If there are custom tools, we just keep them as is
|
|
// They'll be handled by the executor during runtime
|
|
|
|
// For non-custom tools, we determine the tool ID
|
|
const nonCustomTools = tools.filter((tool: any) => tool.type !== 'custom-tool')
|
|
if (nonCustomTools.length > 0) {
|
|
try {
|
|
toolId = blockConfig.tools.config?.tool
|
|
? blockConfig.tools.config.tool(params)
|
|
: blockConfig.tools.access[0]
|
|
} catch (error) {
|
|
logger.warn('Tool selection failed during serialization, using default:', {
|
|
error: error instanceof Error ? error.message : String(error),
|
|
})
|
|
toolId = blockConfig.tools.access[0]
|
|
}
|
|
}
|
|
} catch (error) {
|
|
logger.error('Error processing tools in agent block:', { error })
|
|
// Default to the first tool if we can't process tools
|
|
toolId = blockConfig.tools.access[0]
|
|
}
|
|
} else {
|
|
// For non-agent blocks, get tool ID from block config as usual
|
|
try {
|
|
toolId = blockConfig.tools.config?.tool
|
|
? blockConfig.tools.config.tool(params)
|
|
: blockConfig.tools.access[0]
|
|
} catch (error) {
|
|
logger.warn('Tool selection failed during serialization, using default:', {
|
|
error: error instanceof Error ? error.message : String(error),
|
|
})
|
|
toolId = blockConfig.tools.access[0]
|
|
}
|
|
}
|
|
|
|
// Get inputs from block config
|
|
const inputs: Record<string, any> = {}
|
|
if (blockConfig.inputs) {
|
|
Object.entries(blockConfig.inputs).forEach(([key, config]) => {
|
|
inputs[key] = config.type
|
|
})
|
|
}
|
|
|
|
return {
|
|
id: block.id,
|
|
position: block.position,
|
|
config: {
|
|
tool: toolId,
|
|
params,
|
|
},
|
|
inputs,
|
|
outputs: {
|
|
...block.outputs,
|
|
// Include response format fields if available
|
|
...(params.responseFormat
|
|
? {
|
|
responseFormat: this.parseResponseFormatSafely(params.responseFormat),
|
|
}
|
|
: {}),
|
|
},
|
|
metadata: {
|
|
id: block.type,
|
|
name: block.name,
|
|
description: blockConfig.description,
|
|
category: blockConfig.category,
|
|
color: blockConfig.bgColor,
|
|
},
|
|
enabled: block.enabled,
|
|
}
|
|
}
|
|
|
|
private parseResponseFormatSafely(responseFormat: any): any {
|
|
if (!responseFormat) {
|
|
return undefined
|
|
}
|
|
|
|
// If already an object, return as-is
|
|
if (typeof responseFormat === 'object' && responseFormat !== null) {
|
|
return responseFormat
|
|
}
|
|
|
|
// Handle string values
|
|
if (typeof responseFormat === 'string') {
|
|
const trimmedValue = responseFormat.trim()
|
|
|
|
// Check for variable references like <start.input>
|
|
if (trimmedValue.startsWith('<') && trimmedValue.includes('>')) {
|
|
// Keep variable references as-is
|
|
return trimmedValue
|
|
}
|
|
|
|
if (trimmedValue === '') {
|
|
return undefined
|
|
}
|
|
|
|
// Try to parse as JSON
|
|
try {
|
|
return JSON.parse(trimmedValue)
|
|
} catch (error) {
|
|
// If parsing fails, return undefined to avoid crashes
|
|
// This allows the workflow to continue without structured response format
|
|
logger.warn('Failed to parse response format as JSON in serializer, using undefined:', {
|
|
value: trimmedValue,
|
|
error: error instanceof Error ? error.message : String(error),
|
|
})
|
|
return undefined
|
|
}
|
|
}
|
|
|
|
// For any other type, return undefined
|
|
return undefined
|
|
}
|
|
|
|
private extractParams(block: BlockState): Record<string, any> {
|
|
// Special handling for subflow blocks (loops, parallels, etc.)
|
|
if (block.type === 'loop' || block.type === 'parallel') {
|
|
return {} // Loop and parallel blocks don't have traditional params
|
|
}
|
|
|
|
const blockConfig = getBlock(block.type)
|
|
if (!blockConfig) {
|
|
throw new Error(`Invalid block type: ${block.type}`)
|
|
}
|
|
|
|
const params: Record<string, any> = {}
|
|
|
|
// First collect all current values from subBlocks
|
|
Object.entries(block.subBlocks).forEach(([id, subBlock]) => {
|
|
params[id] = subBlock.value
|
|
})
|
|
|
|
// Then check for any subBlocks with default values
|
|
blockConfig.subBlocks.forEach((subBlockConfig) => {
|
|
const id = subBlockConfig.id
|
|
if (params[id] === null && subBlockConfig.value) {
|
|
// If the value is null and there's a default value function, use it
|
|
params[id] = subBlockConfig.value(params)
|
|
}
|
|
})
|
|
|
|
return params
|
|
}
|
|
|
|
private validateRequiredFieldsBeforeExecution(
|
|
block: BlockState,
|
|
blockConfig: any,
|
|
params: Record<string, any>
|
|
) {
|
|
// Validate user-only required fields before execution starts
|
|
// This catches missing API keys, credentials, and other user-provided values early
|
|
// Fields that are user-or-llm will be validated later after parameter merging
|
|
|
|
// Skip validation if the block is in trigger mode
|
|
if (block.triggerMode || blockConfig.category === 'triggers') {
|
|
logger.info('Skipping validation for block in trigger mode', {
|
|
blockId: block.id,
|
|
blockType: block.type,
|
|
})
|
|
return
|
|
}
|
|
|
|
// Get the tool configuration to check parameter visibility
|
|
const toolAccess = blockConfig.tools?.access
|
|
if (!toolAccess || toolAccess.length === 0) {
|
|
return // No tools to validate against
|
|
}
|
|
|
|
// Determine the current tool ID using the same logic as the serializer
|
|
let currentToolId = ''
|
|
try {
|
|
currentToolId = blockConfig.tools.config?.tool
|
|
? blockConfig.tools.config.tool(params)
|
|
: blockConfig.tools.access[0]
|
|
} catch (error) {
|
|
logger.warn('Tool selection failed during validation, using default:', {
|
|
error: error instanceof Error ? error.message : String(error),
|
|
})
|
|
currentToolId = blockConfig.tools.access[0]
|
|
}
|
|
|
|
// Get the specific tool to validate against
|
|
const currentTool = getTool(currentToolId)
|
|
if (!currentTool) {
|
|
return // Tool not found, skip validation
|
|
}
|
|
|
|
// Check required user-only parameters for the current tool
|
|
const missingFields: string[] = []
|
|
|
|
// Iterate through the tool's parameters, not the block's subBlocks
|
|
Object.entries(currentTool.params || {}).forEach(([paramId, paramConfig]) => {
|
|
if (paramConfig.required && paramConfig.visibility === 'user-only') {
|
|
const fieldValue = params[paramId]
|
|
if (fieldValue === undefined || fieldValue === null || fieldValue === '') {
|
|
// Find the corresponding subBlock to get the display title
|
|
const subBlockConfig = blockConfig.subBlocks?.find((sb: any) => sb.id === paramId)
|
|
const displayName = subBlockConfig?.title || paramId
|
|
missingFields.push(displayName)
|
|
}
|
|
}
|
|
})
|
|
|
|
if (missingFields.length > 0) {
|
|
const blockName = block.name || blockConfig.name || 'Block'
|
|
throw new Error(`${blockName} is missing required fields: ${missingFields.join(', ')}`)
|
|
}
|
|
}
|
|
|
|
deserializeWorkflow(workflow: SerializedWorkflow): {
|
|
blocks: Record<string, BlockState>
|
|
edges: Edge[]
|
|
} {
|
|
const blocks: Record<string, BlockState> = {}
|
|
const edges: Edge[] = []
|
|
|
|
// Deserialize blocks
|
|
workflow.blocks.forEach((serializedBlock) => {
|
|
const block = this.deserializeBlock(serializedBlock)
|
|
blocks[block.id] = block
|
|
})
|
|
|
|
// Deserialize connections
|
|
workflow.connections.forEach((connection) => {
|
|
edges.push({
|
|
id: crypto.randomUUID(),
|
|
source: connection.source,
|
|
target: connection.target,
|
|
sourceHandle: connection.sourceHandle,
|
|
targetHandle: connection.targetHandle,
|
|
})
|
|
})
|
|
|
|
return { blocks, edges }
|
|
}
|
|
|
|
private deserializeBlock(serializedBlock: SerializedBlock): BlockState {
|
|
const blockType = serializedBlock.metadata?.id
|
|
if (!blockType) {
|
|
throw new Error(`Invalid block type: ${serializedBlock.metadata?.id}`)
|
|
}
|
|
|
|
// Special handling for subflow blocks (loops, parallels, etc.)
|
|
if (blockType === 'loop' || blockType === 'parallel') {
|
|
return {
|
|
id: serializedBlock.id,
|
|
type: blockType,
|
|
name: serializedBlock.metadata?.name || (blockType === 'loop' ? 'Loop' : 'Parallel'),
|
|
position: serializedBlock.position,
|
|
subBlocks: {}, // Loops and parallels don't have traditional subBlocks
|
|
outputs: serializedBlock.outputs,
|
|
enabled: serializedBlock.enabled ?? true,
|
|
data: serializedBlock.config.params, // Preserve the data (parallelType, count, etc.)
|
|
}
|
|
}
|
|
|
|
const blockConfig = getBlock(blockType)
|
|
if (!blockConfig) {
|
|
throw new Error(`Invalid block type: ${blockType}`)
|
|
}
|
|
|
|
const subBlocks: Record<string, any> = {}
|
|
blockConfig.subBlocks.forEach((subBlock) => {
|
|
subBlocks[subBlock.id] = {
|
|
id: subBlock.id,
|
|
type: subBlock.type,
|
|
value: serializedBlock.config.params[subBlock.id] ?? null,
|
|
}
|
|
})
|
|
|
|
return {
|
|
id: serializedBlock.id,
|
|
type: blockType,
|
|
name: serializedBlock.metadata?.name || blockConfig.name,
|
|
position: serializedBlock.position,
|
|
subBlocks,
|
|
outputs: serializedBlock.outputs,
|
|
enabled: true,
|
|
}
|
|
}
|
|
}
|