mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-09 06:58:07 -05:00
fix(vars): local and api execution
This commit is contained in:
@@ -320,11 +320,33 @@ export async function GET(req: NextRequest) {
|
||||
)
|
||||
|
||||
logger.info(`[${requestId}] Executing workflow ${schedule.workflowId}`)
|
||||
|
||||
// Get workflow variables
|
||||
let workflowVariables = {}
|
||||
if (workflowRecord.variables) {
|
||||
try {
|
||||
// Parse workflow variables if they're stored as a string
|
||||
if (typeof workflowRecord.variables === 'string') {
|
||||
workflowVariables = JSON.parse(workflowRecord.variables)
|
||||
} else {
|
||||
// Otherwise use as is (already parsed JSON)
|
||||
workflowVariables = workflowRecord.variables
|
||||
}
|
||||
logger.debug(`[${requestId}] Loaded ${Object.keys(workflowVariables).length} workflow variables for: ${schedule.workflowId}`)
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Failed to parse workflow variables: ${schedule.workflowId}`, error)
|
||||
// Continue execution even if variables can't be parsed
|
||||
}
|
||||
} else {
|
||||
logger.debug(`[${requestId}] No workflow variables found for: ${schedule.workflowId}`)
|
||||
}
|
||||
|
||||
const executor = new Executor(
|
||||
serializedWorkflow,
|
||||
processedBlockStates, // Use the processed block states
|
||||
decryptedEnvVars,
|
||||
input
|
||||
input,
|
||||
workflowVariables
|
||||
)
|
||||
const result = await executor.execute(schedule.workflowId)
|
||||
|
||||
|
||||
@@ -620,11 +620,32 @@ async function processWebhook(
|
||||
`[${requestId}] Starting workflow execution with ${Object.keys(processedBlockStates).length} blocks`
|
||||
)
|
||||
|
||||
// Get workflow variables
|
||||
let workflowVariables = {}
|
||||
if (foundWorkflow.variables) {
|
||||
try {
|
||||
// Parse workflow variables if they're stored as a string
|
||||
if (typeof foundWorkflow.variables === 'string') {
|
||||
workflowVariables = JSON.parse(foundWorkflow.variables)
|
||||
} else {
|
||||
// Otherwise use as is (already parsed JSON)
|
||||
workflowVariables = foundWorkflow.variables
|
||||
}
|
||||
logger.debug(`[${requestId}] Loaded ${Object.keys(workflowVariables).length} workflow variables for: ${foundWorkflow.id}`)
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Failed to parse workflow variables: ${foundWorkflow.id}`, error)
|
||||
// Continue execution even if variables can't be parsed
|
||||
}
|
||||
} else {
|
||||
logger.debug(`[${requestId}] No workflow variables found for: ${foundWorkflow.id}`)
|
||||
}
|
||||
|
||||
const executor = new Executor(
|
||||
serializedWorkflow,
|
||||
processedBlockStates,
|
||||
decryptedEnvVars,
|
||||
enrichedInput
|
||||
enrichedInput,
|
||||
workflowVariables
|
||||
)
|
||||
const result = await executor.execute(foundWorkflow.id)
|
||||
|
||||
|
||||
@@ -265,7 +265,8 @@ describe('Workflow Execution API Route', () => {
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
requestBody
|
||||
requestBody,
|
||||
expect.anything() // Expect workflow variables (5th parameter)
|
||||
)
|
||||
})
|
||||
|
||||
@@ -308,7 +309,8 @@ describe('Workflow Execution API Route', () => {
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
structuredInput
|
||||
structuredInput,
|
||||
expect.anything() // Expect workflow variables (5th parameter)
|
||||
)
|
||||
})
|
||||
|
||||
@@ -342,7 +344,8 @@ describe('Workflow Execution API Route', () => {
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
{}
|
||||
{},
|
||||
expect.anything() // Expect workflow variables (5th parameter)
|
||||
)
|
||||
})
|
||||
|
||||
@@ -447,4 +450,99 @@ describe('Workflow Execution API Route', () => {
|
||||
.persistExecutionError
|
||||
expect(persistExecutionError).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
/**
|
||||
* Test that workflow variables are properly passed to the Executor
|
||||
*/
|
||||
it('should pass workflow variables to the Executor', async () => {
|
||||
// Create mock variables for the workflow
|
||||
const workflowVariables = {
|
||||
'variable1': { id: 'var1', name: 'variable1', type: 'string', value: '"test value"' },
|
||||
'variable2': { id: 'var2', name: 'variable2', type: 'boolean', value: 'true' }
|
||||
}
|
||||
|
||||
// Mock workflow with variables
|
||||
vi.doMock('@/app/api/workflows/middleware', () => ({
|
||||
validateWorkflowAccess: vi.fn().mockResolvedValue({
|
||||
workflow: {
|
||||
id: 'workflow-with-vars-id',
|
||||
userId: 'user-id',
|
||||
state: {
|
||||
blocks: {
|
||||
'starter-id': {
|
||||
id: 'starter-id',
|
||||
type: 'starter',
|
||||
name: 'Start',
|
||||
position: { x: 100, y: 100 },
|
||||
enabled: true,
|
||||
},
|
||||
'agent-id': {
|
||||
id: 'agent-id',
|
||||
type: 'agent',
|
||||
name: 'Agent',
|
||||
position: { x: 300, y: 100 },
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
edges: [
|
||||
{
|
||||
id: 'edge-1',
|
||||
source: 'starter-id',
|
||||
target: 'agent-id',
|
||||
sourceHandle: 'source',
|
||||
targetHandle: 'target',
|
||||
},
|
||||
],
|
||||
loops: {},
|
||||
},
|
||||
variables: workflowVariables,
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
// Create a constructor mock to capture the arguments
|
||||
const executorConstructorMock = vi.fn().mockImplementation(() => ({
|
||||
execute: vi.fn().mockResolvedValue({
|
||||
success: true,
|
||||
output: { response: 'Execution completed with variables' },
|
||||
logs: [],
|
||||
metadata: {
|
||||
duration: 100,
|
||||
startTime: new Date().toISOString(),
|
||||
endTime: new Date().toISOString(),
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
// Override the executor mock
|
||||
vi.doMock('@/executor', () => ({
|
||||
Executor: executorConstructorMock,
|
||||
}))
|
||||
|
||||
// Create a mock request
|
||||
const req = createMockRequest('POST', { testInput: 'value' })
|
||||
|
||||
// Create params similar to what Next.js would provide
|
||||
const params = Promise.resolve({ id: 'workflow-with-vars-id' })
|
||||
|
||||
// Import the handler after mocks are set up
|
||||
const { POST } = await import('./route')
|
||||
|
||||
// Call the handler
|
||||
await POST(req, { params })
|
||||
|
||||
// Verify the Executor was constructed with workflow variables
|
||||
expect(executorConstructorMock).toHaveBeenCalled()
|
||||
|
||||
// Check that the 5th parameter (workflow variables) was passed
|
||||
const executorCalls = executorConstructorMock.mock.calls
|
||||
expect(executorCalls.length).toBeGreaterThan(0)
|
||||
|
||||
// Each call to the constructor should have at least 5 parameters
|
||||
const lastCall = executorCalls[executorCalls.length - 1]
|
||||
expect(lastCall.length).toBeGreaterThanOrEqual(5)
|
||||
|
||||
// The 5th parameter should be the workflow variables
|
||||
expect(lastCall[4]).toEqual(workflowVariables)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -148,11 +148,38 @@ async function executeWorkflow(workflow: any, requestId: string, input?: any) {
|
||||
{} as Record<string, Record<string, any>>
|
||||
)
|
||||
|
||||
// Get workflow variables
|
||||
let workflowVariables = {}
|
||||
if (workflow.variables) {
|
||||
try {
|
||||
// Parse workflow variables if they're stored as a string
|
||||
if (typeof workflow.variables === 'string') {
|
||||
workflowVariables = JSON.parse(workflow.variables)
|
||||
} else {
|
||||
// Otherwise use as is (already parsed JSON)
|
||||
workflowVariables = workflow.variables
|
||||
}
|
||||
logger.debug(`[${requestId}] Loaded ${Object.keys(workflowVariables).length} workflow variables for: ${workflowId}`)
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Failed to parse workflow variables: ${workflowId}`, error)
|
||||
// Continue execution even if variables can't be parsed
|
||||
}
|
||||
} else {
|
||||
logger.debug(`[${requestId}] No workflow variables found for: ${workflowId}`)
|
||||
}
|
||||
|
||||
// Serialize and execute the workflow
|
||||
logger.debug(`[${requestId}] Serializing workflow: ${workflowId}`)
|
||||
const serializedWorkflow = new Serializer().serializeWorkflow(mergedStates, edges, loops)
|
||||
|
||||
const executor = new Executor(serializedWorkflow, processedBlockStates, decryptedEnvVars, input)
|
||||
const executor = new Executor(
|
||||
serializedWorkflow,
|
||||
processedBlockStates,
|
||||
decryptedEnvVars,
|
||||
input,
|
||||
workflowVariables
|
||||
)
|
||||
|
||||
const result = await executor.execute(workflowId)
|
||||
|
||||
logger.info(`[${requestId}] Workflow execution completed: ${workflowId}`, {
|
||||
|
||||
@@ -140,6 +140,7 @@ export function useWorkflowExecution() {
|
||||
workflow,
|
||||
currentBlockStates,
|
||||
envVarValues,
|
||||
undefined,
|
||||
workflowVariables
|
||||
)
|
||||
setExecutor(newExecutor)
|
||||
|
||||
@@ -51,6 +51,7 @@ export class InputResolver {
|
||||
resolveInputs(block: SerializedBlock, context: ExecutionContext): Record<string, any> {
|
||||
const inputs = { ...block.config.params }
|
||||
const result: Record<string, any> = {}
|
||||
const isFunctionBlock = block.metadata?.id === 'function'
|
||||
|
||||
// Process each input parameter
|
||||
for (const [key, value] of Object.entries(inputs)) {
|
||||
@@ -63,7 +64,7 @@ export class InputResolver {
|
||||
// Handle string values that may contain references
|
||||
if (typeof value === 'string') {
|
||||
// First check for variable references
|
||||
let resolvedValue = this.resolveVariableReferences(value)
|
||||
let resolvedValue = this.resolveVariableReferences(value, block)
|
||||
|
||||
// Then resolve block references
|
||||
resolvedValue = this.resolveBlockReferences(resolvedValue, context, block)
|
||||
@@ -77,16 +78,23 @@ export class InputResolver {
|
||||
// Resolve environment variables
|
||||
resolvedValue = this.resolveEnvVariables(resolvedValue, isApiKey)
|
||||
|
||||
// Convert JSON strings to objects if possible
|
||||
try {
|
||||
if (resolvedValue.startsWith('{') || resolvedValue.startsWith('[')) {
|
||||
result[key] = JSON.parse(resolvedValue)
|
||||
} else {
|
||||
// For function blocks, we need special handling for code input
|
||||
if (isFunctionBlock && key === 'code') {
|
||||
// For code input in function blocks, we don't want to parse JSON
|
||||
result[key] = resolvedValue
|
||||
}
|
||||
// For other inputs, try to convert JSON strings to objects
|
||||
else {
|
||||
try {
|
||||
if (resolvedValue.startsWith('{') || resolvedValue.startsWith('[')) {
|
||||
result[key] = JSON.parse(resolvedValue)
|
||||
} else {
|
||||
result[key] = resolvedValue
|
||||
}
|
||||
} catch {
|
||||
// If it's not valid JSON, keep it as a string
|
||||
result[key] = resolvedValue
|
||||
}
|
||||
} catch {
|
||||
// If it's not valid JSON, keep it as a string
|
||||
result[key] = resolvedValue
|
||||
}
|
||||
}
|
||||
// Handle objects and arrays recursively
|
||||
@@ -106,9 +114,10 @@ export class InputResolver {
|
||||
* Resolves workflow variable references in a string (<variable.name>).
|
||||
*
|
||||
* @param value - String containing variable references
|
||||
* @param currentBlock - The current block, used to determine context
|
||||
* @returns String with resolved variable references
|
||||
*/
|
||||
resolveVariableReferences(value: string): string {
|
||||
resolveVariableReferences(value: string, currentBlock?: SerializedBlock): string {
|
||||
const variableMatches = value.match(/<variable\.([^>]+)>/g)
|
||||
if (!variableMatches) return value
|
||||
|
||||
@@ -125,11 +134,57 @@ export class InputResolver {
|
||||
|
||||
if (foundVariable) {
|
||||
const [_, variable] = foundVariable
|
||||
// Format the value appropriately
|
||||
const formattedValue =
|
||||
typeof variable.value === 'object'
|
||||
? JSON.stringify(variable.value)
|
||||
: String(variable.value)
|
||||
|
||||
// Process variable value based on its type
|
||||
let processedValue = variable.value
|
||||
|
||||
// Handle string values that could be stored with quotes
|
||||
if (variable.type === 'string' && typeof processedValue === 'string') {
|
||||
// If the string value starts and ends with quotes, remove them
|
||||
const trimmed = processedValue.trim()
|
||||
if ((trimmed.startsWith('"') && trimmed.endsWith('"')) ||
|
||||
(trimmed.startsWith("'") && trimmed.endsWith("'"))) {
|
||||
// Remove the quotes and unescape any escaped quotes
|
||||
processedValue = trimmed.slice(1, -1).replace(/\\"/g, '"').replace(/\\'/g, "'")
|
||||
}
|
||||
}
|
||||
// Handle boolean values that might be stored as strings
|
||||
else if (variable.type === 'boolean' && typeof processedValue === 'string') {
|
||||
processedValue = processedValue.trim().toLowerCase() === 'true'
|
||||
}
|
||||
// Handle number values that might be stored as strings
|
||||
else if (variable.type === 'number' && typeof processedValue === 'string') {
|
||||
const parsed = Number(processedValue)
|
||||
if (!isNaN(parsed)) {
|
||||
processedValue = parsed
|
||||
}
|
||||
}
|
||||
// Handle object/array values that might be stored as JSON strings
|
||||
else if ((variable.type === 'object' || variable.type === 'array') &&
|
||||
typeof processedValue === 'string') {
|
||||
try {
|
||||
processedValue = JSON.parse(processedValue)
|
||||
} catch (e) {
|
||||
// Keep as string if parsing fails
|
||||
}
|
||||
}
|
||||
|
||||
// Determine if this needs to be a code-compatible string literal
|
||||
const needsCodeStringLiteral = this.needsCodeStringLiteral(currentBlock, value);
|
||||
|
||||
// Format the processed value for insertion into the string based on context
|
||||
let formattedValue: string;
|
||||
|
||||
if (variable.type === 'string' && needsCodeStringLiteral) {
|
||||
// For code contexts like function and condition blocks, properly quote strings
|
||||
formattedValue = JSON.stringify(processedValue);
|
||||
} else if (typeof processedValue === 'object' && processedValue !== null) {
|
||||
// For objects, always stringify
|
||||
formattedValue = JSON.stringify(processedValue);
|
||||
} else {
|
||||
// For other types in normal contexts, use simple string conversion
|
||||
formattedValue = String(processedValue);
|
||||
}
|
||||
|
||||
resolvedValue = resolvedValue.replace(match, formattedValue)
|
||||
}
|
||||
@@ -138,6 +193,56 @@ export class InputResolver {
|
||||
return resolvedValue
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a value needs to be formatted as a code-compatible string literal
|
||||
* based on the block type and context. Handles JavaScript and other code contexts.
|
||||
*
|
||||
* @param block - The block where the value is being used
|
||||
* @param expression - The expression containing the value
|
||||
* @returns Whether the value should be formatted as a string literal
|
||||
*/
|
||||
private needsCodeStringLiteral(block?: SerializedBlock, expression?: string): boolean {
|
||||
if (!block) return false;
|
||||
|
||||
// These block types execute code and need properly formatted string literals
|
||||
const codeExecutionBlocks = ['function', 'condition'];
|
||||
|
||||
// Check if this is a block that executes code
|
||||
if (block.metadata?.id && codeExecutionBlocks.includes(block.metadata.id)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if the expression is likely part of code
|
||||
if (expression) {
|
||||
const codeIndicators = [
|
||||
// Function/method calls
|
||||
/\(\s*$/, // Function call
|
||||
/\.\w+\s*\(/, // Method call
|
||||
|
||||
// JavaScript/Python operators
|
||||
/[=<>!+\-*/%](?:==?)?/, // Common operators
|
||||
/\+=|-=|\*=|\/=|%=|\*\*=?/, // Assignment operators
|
||||
|
||||
// JavaScript keywords
|
||||
/\b(if|else|for|while|return|var|let|const|function)\b/,
|
||||
|
||||
// Python keywords
|
||||
/\b(if|else|elif|for|while|def|return|import|from|as|class|with|try|except)\b/,
|
||||
|
||||
// Common code patterns
|
||||
/^['"]use strict['"];?$/, // JS strict mode
|
||||
/\$\{.+?\}/, // JS template literals
|
||||
/f['"].*?['"]/, // Python f-strings
|
||||
/\bprint\s*\(/, // Python print
|
||||
/\bconsole\.\w+\(/ // JS console methods
|
||||
];
|
||||
|
||||
return codeIndicators.some(pattern => pattern.test(expression));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves block references in a string (<blockId.property> or <blockName.property>).
|
||||
* Handles inactive paths, missing blocks, and formats values appropriately.
|
||||
@@ -387,8 +492,8 @@ export class InputResolver {
|
||||
|
||||
if (currentBlock.metadata?.id === 'condition') {
|
||||
formattedValue = this.stringifyForCondition(replacementValue)
|
||||
} else if (currentBlock.metadata?.id === 'function' && typeof replacementValue === 'string') {
|
||||
// For function blocks, we need to properly quote string values to avoid syntax errors
|
||||
} else if (typeof replacementValue === 'string' && this.needsCodeStringLiteral(currentBlock, value)) {
|
||||
// For code blocks, quote string values properly for the given language
|
||||
formattedValue = JSON.stringify(replacementValue)
|
||||
} else {
|
||||
formattedValue =
|
||||
@@ -518,7 +623,7 @@ export class InputResolver {
|
||||
// Handle strings
|
||||
if (typeof value === 'string') {
|
||||
// First resolve variable references
|
||||
const resolvedVars = this.resolveVariableReferences(value)
|
||||
const resolvedVars = this.resolveVariableReferences(value, currentBlock)
|
||||
|
||||
// Then resolve block references
|
||||
const resolvedReferences = this.resolveBlockReferences(resolvedVars, context, currentBlock)
|
||||
|
||||
Reference in New Issue
Block a user