Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 33 additions & 9 deletions apps/sim/app/api/workflows/[id]/execute/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,18 +197,42 @@ async function executeWorkflow(workflow: any, requestId: string, input?: any) {
(acc, [blockId, blockState]) => {
// Check if this block has a responseFormat that needs to be parsed
if (blockState.responseFormat && typeof blockState.responseFormat === 'string') {
try {
logger.debug(`[${requestId}] Parsing responseFormat for block ${blockId}`)
// Attempt to parse the responseFormat if it's a string
const parsedResponseFormat = JSON.parse(blockState.responseFormat)

const responseFormatValue = blockState.responseFormat.trim()

// Check for variable references like <start.input>
if (responseFormatValue.startsWith('<') && responseFormatValue.includes('>')) {
logger.debug(
`[${requestId}] Response format contains variable reference for block ${blockId}`
)
// Keep variable references as-is - they will be resolved during execution
acc[blockId] = blockState
} else if (responseFormatValue === '') {
// Empty string - remove response format
acc[blockId] = {
...blockState,
responseFormat: parsedResponseFormat,
responseFormat: undefined,
}
} else {
try {
logger.debug(`[${requestId}] Parsing responseFormat for block ${blockId}`)
// Attempt to parse the responseFormat if it's a string
const parsedResponseFormat = JSON.parse(responseFormatValue)

acc[blockId] = {
...blockState,
responseFormat: parsedResponseFormat,
}
} catch (error) {
logger.warn(
`[${requestId}] Failed to parse responseFormat for block ${blockId}, using undefined`,
error
)
// Set to undefined instead of keeping malformed JSON - this allows execution to continue
acc[blockId] = {
...blockState,
responseFormat: undefined,
}
}
} catch (error) {
logger.warn(`[${requestId}] Failed to parse responseFormat for block ${blockId}`, error)
acc[blockId] = blockState
}
} else {
acc[blockId] = blockState
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,13 @@ export function ResponseFormat({
{showPreview && (
<div className='rounded border bg-muted/30 p-2'>
<pre className='max-h-32 overflow-auto text-xs'>
{JSON.stringify(generateJSON(properties), null, 2)}
{(() => {
try {
return JSON.stringify(generateJSON(properties), null, 2)
} catch (error) {
return `Error generating preview: ${error instanceof Error ? error.message : 'Unknown error'}`
}
})()}
</pre>
</div>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,35 @@ export interface ConnectedBlock {
}
}

function parseResponseFormatSafely(responseFormatValue: any, blockId: string): any {
if (!responseFormatValue) {
return undefined
}

if (typeof responseFormatValue === 'object' && responseFormatValue !== null) {
return responseFormatValue
}

if (typeof responseFormatValue === 'string') {
const trimmedValue = responseFormatValue.trim()

if (trimmedValue.startsWith('<') && trimmedValue.includes('>')) {
return trimmedValue
}

if (trimmedValue === '') {
return undefined
}

try {
return JSON.parse(trimmedValue)
} catch (error) {
return undefined
}
}
return undefined
}

// Helper function to extract fields from JSON Schema
function extractFieldsFromSchema(schema: any): Field[] {
if (!schema || typeof schema !== 'object') {
Expand Down Expand Up @@ -77,15 +106,8 @@ export function useBlockConnections(blockId: string) {

let responseFormat

try {
responseFormat =
typeof responseFormatValue === 'string' && responseFormatValue
? JSON.parse(responseFormatValue)
: responseFormatValue // Handle case where it's already an object
} catch (e) {
logger.error('Failed to parse response format:', { e })
responseFormat = undefined
}
// Safely parse response format with proper error handling
responseFormat = parseResponseFormatSafely(responseFormatValue, sourceId)

// Get the default output type from the block's outputs
const defaultOutputs: Field[] = Object.entries(sourceBlock.outputs || {}).map(([key]) => ({
Expand Down Expand Up @@ -120,15 +142,8 @@ export function useBlockConnections(blockId: string) {

let responseFormat

try {
responseFormat =
typeof responseFormatValue === 'string' && responseFormatValue
? JSON.parse(responseFormatValue)
: responseFormatValue // Handle case where it's already an object
} catch (e) {
logger.error('Failed to parse response format:', { e })
responseFormat = undefined
}
// Safely parse response format with proper error handling
responseFormat = parseResponseFormatSafely(responseFormatValue, edge.source)

// Get the default output type from the block's outputs
const defaultOutputs: Field[] = Object.entries(sourceBlock.outputs || {}).map(([key]) => ({
Expand Down
81 changes: 77 additions & 4 deletions apps/sim/executor/handlers/agent/agent-handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -736,17 +736,90 @@ describe('AgentBlockHandler', () => {
})
})

it('should throw an error for invalid JSON in responseFormat', async () => {
it('should handle invalid JSON in responseFormat gracefully', async () => {
mockFetch.mockImplementationOnce(() => {
return Promise.resolve({
ok: true,
headers: {
get: (name: string) => {
if (name === 'Content-Type') return 'application/json'
if (name === 'X-Execution-Data') return null
return null
},
},
json: () =>
Promise.resolve({
content: 'Regular text response',
model: 'mock-model',
tokens: { prompt: 10, completion: 20, total: 30 },
timing: { total: 100 },
toolCalls: [],
cost: undefined,
}),
})
})

const inputs = {
model: 'gpt-4o',
userPrompt: 'Format this output.',
apiKey: 'test-api-key',
responseFormat: '{invalid-json',
}

await expect(handler.execute(mockBlock, inputs, mockContext)).rejects.toThrow(
'Invalid response'
)
// Should not throw an error, but continue with default behavior
const result = await handler.execute(mockBlock, inputs, mockContext)

expect(result).toEqual({
content: 'Regular text response',
model: 'mock-model',
tokens: { prompt: 10, completion: 20, total: 30 },
toolCalls: { list: [], count: 0 },
providerTiming: { total: 100 },
cost: undefined,
})
})

it('should handle variable references in responseFormat gracefully', async () => {
mockFetch.mockImplementationOnce(() => {
return Promise.resolve({
ok: true,
headers: {
get: (name: string) => {
if (name === 'Content-Type') return 'application/json'
if (name === 'X-Execution-Data') return null
return null
},
},
json: () =>
Promise.resolve({
content: 'Regular text response',
model: 'mock-model',
tokens: { prompt: 10, completion: 20, total: 30 },
timing: { total: 100 },
toolCalls: [],
cost: undefined,
}),
})
})

const inputs = {
model: 'gpt-4o',
userPrompt: 'Format this output.',
apiKey: 'test-api-key',
responseFormat: '<start.input>',
}

// Should not throw an error, but continue with default behavior
const result = await handler.execute(mockBlock, inputs, mockContext)

expect(result).toEqual({
content: 'Regular text response',
model: 'mock-model',
tokens: { prompt: 10, completion: 20, total: 30 },
toolCalls: { list: [], count: 0 },
providerTiming: { total: 100 },
cost: undefined,
})
})

it('should handle errors from the provider request', async () => {
Expand Down
61 changes: 51 additions & 10 deletions apps/sim/executor/handlers/agent/agent-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,22 +58,63 @@ export class AgentBlockHandler implements BlockHandler {
private parseResponseFormat(responseFormat?: string | object): any {
if (!responseFormat || responseFormat === '') return undefined

try {
const parsed =
typeof responseFormat === 'string' ? JSON.parse(responseFormat) : responseFormat

if (parsed && typeof parsed === 'object' && !parsed.schema && !parsed.name) {
// If already an object, process it directly
if (typeof responseFormat === 'object' && responseFormat !== null) {
const formatObj = responseFormat as any
if (!formatObj.schema && !formatObj.name) {
return {
name: 'response_schema',
schema: parsed,
schema: responseFormat,
strict: true,
}
}
return parsed
} catch (error: any) {
logger.error('Failed to parse response format:', { error })
throw new Error(`Invalid response format: ${error.message}`)
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('>')) {
logger.info('Response format contains variable reference:', {
value: trimmedValue,
})
// Variable references should have been resolved by the resolver before reaching here
// If we still have a variable reference, it means it couldn't be resolved
// Return undefined to use default behavior (no structured response)
return undefined
}

// Try to parse as JSON
try {
const parsed = JSON.parse(trimmedValue)

if (parsed && typeof parsed === 'object' && !parsed.schema && !parsed.name) {
return {
name: 'response_schema',
schema: parsed,
strict: true,
}
}
return parsed
} catch (error: any) {
logger.warn('Failed to parse response format as JSON, using default behavior:', {
error: error.message,
value: trimmedValue,
})
// Return undefined instead of throwing - this allows execution to continue
// without structured response format
return undefined
}
}

// For any other type, return undefined
logger.warn('Unexpected response format type, using default behavior:', {
type: typeof responseFormat,
value: responseFormat,
})
return undefined
}

private async formatTools(inputTools: ToolInput[], context: ExecutionContext): Promise<any[]> {
Expand Down
44 changes: 43 additions & 1 deletion apps/sim/serializer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ export class Serializer {
// Include response format fields if available
...(params.responseFormat
? {
responseFormat: JSON.parse(params.responseFormat),
responseFormat: this.parseResponseFormatSafely(params.responseFormat),
}
: {}),
},
Expand All @@ -136,6 +136,48 @@ export class Serializer {
}
}

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') {
Expand Down