mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
fix(resp format): non-json input was crashing (#631)
* fix response format non-json input crash bug * fix lint --------- Co-authored-by: Vikhyath Mondreti <vikhyathmondreti@Vikhyaths-Air.attlocal.net>
This commit is contained in:
co-authored by
Vikhyath Mondreti
parent
02b7899861
commit
5167deb75c
@@ -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
|
||||
|
||||
+7
-1
@@ -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>
|
||||
)}
|
||||
|
||||
+33
-18
@@ -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') {
|
||||
@@ -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]) => ({
|
||||
@@ -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]) => ({
|
||||
|
||||
@@ -736,7 +736,29 @@ 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.',
|
||||
@@ -744,9 +766,60 @@ describe('AgentBlockHandler', () => {
|
||||
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 () => {
|
||||
|
||||
@@ -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[]> {
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
@@ -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') {
|
||||
|
||||
Reference in New Issue
Block a user