diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/code.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/code.tsx index 8306e1e8f8..35f65b4a2b 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/code.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/code.tsx @@ -1,6 +1,6 @@ import type { ReactElement } from 'react' import { useEffect, useMemo, useRef, useState } from 'react' -import { Wand2 } from 'lucide-react' +import { Wand2, AlertTriangle } from 'lucide-react' import { highlight, languages } from 'prismjs' import 'prismjs/components/prism-javascript' import 'prismjs/themes/prism.css' @@ -29,6 +29,7 @@ interface CodeProps { isPreview?: boolean previewValue?: string | null disabled?: boolean + onValidationChange?: (isValid: boolean) => void } if (typeof document !== 'undefined') { @@ -60,6 +61,7 @@ export function Code({ isPreview = false, previewValue, disabled = false, + onValidationChange, }: CodeProps) { // Determine the AI prompt placeholder based on language const aiPromptPlaceholder = useMemo(() => { @@ -90,6 +92,24 @@ export function Code({ const showCollapseButton = (subBlockId === 'responseFormat' || subBlockId === 'code') && code.split('\n').length > 5 + const isValidJson = useMemo(() => { + if (subBlockId !== 'responseFormat' || !code.trim()) { + return true + } + try { + JSON.parse(code) + return true + } catch { + return false + } + }, [subBlockId, code]) + + useEffect(() => { + if (onValidationChange && subBlockId === 'responseFormat') { + onValidationChange(isValidJson) + } + }, [isValidJson, onValidationChange, subBlockId]) + const editorRef = useRef(null) // Function to toggle collapsed state @@ -343,9 +363,11 @@ export function Code({
e.preventDefault()} onDrop={handleDrop} > diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/sub-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/sub-block.tsx index e821aedd96..c3bfd9ed87 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/sub-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/sub-block.tsx @@ -1,4 +1,5 @@ -import { Info } from 'lucide-react' +import { useState } from 'react' +import { Info, AlertTriangle } from 'lucide-react' import { Label } from '@/components/ui/label' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' import { getBlock } from '@/blocks/index' @@ -48,10 +49,16 @@ export function SubBlock({ subBlockValues, disabled = false, }: SubBlockProps) { + const [isValidJson, setIsValidJson] = useState(true) + const handleMouseDown = (e: React.MouseEvent) => { e.stopPropagation() } + const handleValidationChange = (isValid: boolean) => { + setIsValidJson(isValid) + } + const isFieldRequired = () => { const blockType = useWorkflowStore.getState().blocks[blockId]?.type if (!blockType) return false @@ -169,6 +176,7 @@ export function SubBlock({ isPreview={isPreview} previewValue={previewValue} disabled={isDisabled} + onValidationChange={handleValidationChange} /> ) case 'switch': @@ -406,6 +414,16 @@ export function SubBlock({ )} + {config.id === 'responseFormat' && !isValidJson && ( + + + + + +

Invalid JSON

+
+
+ )} {config.description && ( diff --git a/apps/sim/executor/handlers/agent/agent-handler.ts b/apps/sim/executor/handlers/agent/agent-handler.ts index 80382f5fff..e76d2ba33e 100644 --- a/apps/sim/executor/handlers/agent/agent-handler.ts +++ b/apps/sim/executor/handlers/agent/agent-handler.ts @@ -703,16 +703,120 @@ export class AgentBlockHandler implements BlockHandler { } private processStructuredResponse(result: any, responseFormat: any): BlockOutput { - try { - const parsedContent = JSON.parse(result.content) + const content = result.content + + const extractedJson = this.extractJsonFromContent(content) + + if (extractedJson !== null) { + logger.info('Successfully parsed structured response content') return { - ...parsedContent, + ...extractedJson, ...this.createResponseMetadata(result), } - } catch (error) { - logger.error('Failed to parse response content:', { error }) - return this.processStandardResponse(result) } + + // All parsing attempts failed + logger.error('Failed to parse response content as JSON:', { + content: content.substring(0, 200) + (content.length > 200 ? '...' : ''), + responseFormat: responseFormat + }) + + // Return standard response but include a warning + const standardResponse = this.processStandardResponse(result) + return Object.assign(standardResponse, { + _responseFormatWarning: 'Response format was specified but content could not be parsed as JSON. Falling back to standard format.', + }) + } + + private extractJsonFromContent(content: string): any | null { + // Strategy 1: Direct JSON parsing + try { + return JSON.parse(content.trim()) + } catch { + // Continue to next strategy + } + + // Strategy 2: Extract from markdown code blocks (most common case) + // Matches ```json ... ``` or ``` ... ``` + const codeBlockRegex = /```(?:json)?\s*([\s\S]*?)\s*```/ + const codeBlockMatch = content.match(codeBlockRegex) + if (codeBlockMatch?.[1]) { + try { + return JSON.parse(codeBlockMatch[1].trim()) + } catch { + // Continue to next strategy + } + } + + // Strategy 3: Find first complete JSON object/array in the text + // Look for { ... } or [ ... ] with proper bracket matching + const jsonObjectMatch = this.findCompleteJsonInText(content) + if (jsonObjectMatch) { + try { + return JSON.parse(jsonObjectMatch) + } catch { + // Continue to next strategy + } + } + + return null + } + + private findCompleteJsonInText(text: string): string | null { + const trimmed = text.trim() + + // Find first { or [ + let startIndex = -1 + let startChar = '' + + for (let i = 0; i < trimmed.length; i++) { + if (trimmed[i] === '{' || trimmed[i] === '[') { + startIndex = i + startChar = trimmed[i] + break + } + } + + if (startIndex === -1) { + return null + } + + const endChar = startChar === '{' ? '}' : ']' + let depth = 0 + let inString = false + let escaped = false + + for (let i = startIndex; i < trimmed.length; i++) { + const char = trimmed[i] + + if (escaped) { + escaped = false + continue + } + + if (char === '\\') { + escaped = true + continue + } + + if (char === '"' && !escaped) { + inString = !inString + continue + } + + if (!inString) { + if (char === startChar) { + depth++ + } else if (char === endChar) { + depth-- + if (depth === 0) { + return trimmed.substring(startIndex, i + 1) + } + } + } + } + + return null } private processStandardResponse(result: any): BlockOutput {