Skip to content

Commit 209d822

Browse files
Vikhyath MondretiVikhyath Mondreti
authored andcommitted
fix response format json extraction issues + add warning for invalid json
1 parent 31d9e2a commit 209d822

File tree

3 files changed

+154
-10
lines changed

3 files changed

+154
-10
lines changed

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/code.tsx

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { ReactElement } from 'react'
22
import { useEffect, useMemo, useRef, useState } from 'react'
3-
import { Wand2 } from 'lucide-react'
3+
import { Wand2, AlertTriangle } from 'lucide-react'
44
import { highlight, languages } from 'prismjs'
55
import 'prismjs/components/prism-javascript'
66
import 'prismjs/themes/prism.css'
@@ -29,6 +29,7 @@ interface CodeProps {
2929
isPreview?: boolean
3030
previewValue?: string | null
3131
disabled?: boolean
32+
onValidationChange?: (isValid: boolean) => void
3233
}
3334

3435
if (typeof document !== 'undefined') {
@@ -60,6 +61,7 @@ export function Code({
6061
isPreview = false,
6162
previewValue,
6263
disabled = false,
64+
onValidationChange,
6365
}: CodeProps) {
6466
// Determine the AI prompt placeholder based on language
6567
const aiPromptPlaceholder = useMemo(() => {
@@ -90,6 +92,24 @@ export function Code({
9092
const showCollapseButton =
9193
(subBlockId === 'responseFormat' || subBlockId === 'code') && code.split('\n').length > 5
9294

95+
const isValidJson = useMemo(() => {
96+
if (subBlockId !== 'responseFormat' || !code.trim()) {
97+
return true
98+
}
99+
try {
100+
JSON.parse(code)
101+
return true
102+
} catch {
103+
return false
104+
}
105+
}, [subBlockId, code])
106+
107+
useEffect(() => {
108+
if (onValidationChange && subBlockId === 'responseFormat') {
109+
onValidationChange(isValidJson)
110+
}
111+
}, [isValidJson, onValidationChange, subBlockId])
112+
93113
const editorRef = useRef<HTMLDivElement>(null)
94114

95115
// Function to toggle collapsed state
@@ -343,9 +363,11 @@ export function Code({
343363

344364
<div
345365
className={cn(
346-
'group relative min-h-[100px] rounded-md border bg-background font-mono text-sm',
347-
isConnecting && 'ring-2 ring-blue-500 ring-offset-2'
366+
'group relative min-h-[100px] rounded-md border bg-background font-mono text-sm transition-colors',
367+
isConnecting && 'ring-2 ring-blue-500 ring-offset-2',
368+
!isValidJson && 'border-destructive border-2 bg-destructive/10'
348369
)}
370+
title={!isValidJson ? 'Invalid JSON' : undefined}
349371
onDragOver={(e) => e.preventDefault()}
350372
onDrop={handleDrop}
351373
>

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/sub-block.tsx

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { Info } from 'lucide-react'
1+
import { useState } from 'react'
2+
import { Info, AlertTriangle } from 'lucide-react'
23
import { Label } from '@/components/ui/label'
34
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
45
import { getBlock } from '@/blocks/index'
@@ -48,10 +49,16 @@ export function SubBlock({
4849
subBlockValues,
4950
disabled = false,
5051
}: SubBlockProps) {
52+
const [isValidJson, setIsValidJson] = useState(true)
53+
5154
const handleMouseDown = (e: React.MouseEvent) => {
5255
e.stopPropagation()
5356
}
5457

58+
const handleValidationChange = (isValid: boolean) => {
59+
setIsValidJson(isValid)
60+
}
61+
5562
const isFieldRequired = () => {
5663
const blockType = useWorkflowStore.getState().blocks[blockId]?.type
5764
if (!blockType) return false
@@ -169,6 +176,7 @@ export function SubBlock({
169176
isPreview={isPreview}
170177
previewValue={previewValue}
171178
disabled={isDisabled}
179+
onValidationChange={handleValidationChange}
172180
/>
173181
)
174182
case 'switch':
@@ -406,6 +414,16 @@ export function SubBlock({
406414
</TooltipContent>
407415
</Tooltip>
408416
)}
417+
{config.id === 'responseFormat' && !isValidJson && (
418+
<Tooltip>
419+
<TooltipTrigger asChild>
420+
<AlertTriangle className='h-4 w-4 cursor-pointer text-destructive' />
421+
</TooltipTrigger>
422+
<TooltipContent side='top'>
423+
<p>Invalid JSON</p>
424+
</TooltipContent>
425+
</Tooltip>
426+
)}
409427
{config.description && (
410428
<Tooltip>
411429
<TooltipTrigger asChild>

apps/sim/executor/handlers/agent/agent-handler.ts

Lines changed: 110 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -703,16 +703,120 @@ export class AgentBlockHandler implements BlockHandler {
703703
}
704704

705705
private processStructuredResponse(result: any, responseFormat: any): BlockOutput {
706-
try {
707-
const parsedContent = JSON.parse(result.content)
706+
const content = result.content
707+
708+
const extractedJson = this.extractJsonFromContent(content)
709+
710+
if (extractedJson !== null) {
711+
logger.info('Successfully parsed structured response content')
708712
return {
709-
...parsedContent,
713+
...extractedJson,
710714
...this.createResponseMetadata(result),
711715
}
712-
} catch (error) {
713-
logger.error('Failed to parse response content:', { error })
714-
return this.processStandardResponse(result)
715716
}
717+
718+
// All parsing attempts failed
719+
logger.error('Failed to parse response content as JSON:', {
720+
content: content.substring(0, 200) + (content.length > 200 ? '...' : ''),
721+
responseFormat: responseFormat
722+
})
723+
724+
// Return standard response but include a warning
725+
const standardResponse = this.processStandardResponse(result)
726+
return Object.assign(standardResponse, {
727+
_responseFormatWarning: 'Response format was specified but content could not be parsed as JSON. Falling back to standard format.',
728+
})
729+
}
730+
731+
private extractJsonFromContent(content: string): any | null {
732+
// Strategy 1: Direct JSON parsing
733+
try {
734+
return JSON.parse(content.trim())
735+
} catch {
736+
// Continue to next strategy
737+
}
738+
739+
// Strategy 2: Extract from markdown code blocks (most common case)
740+
// Matches ```json ... ``` or ``` ... ```
741+
const codeBlockRegex = /```(?:json)?\s*([\s\S]*?)\s*```/
742+
const codeBlockMatch = content.match(codeBlockRegex)
743+
if (codeBlockMatch?.[1]) {
744+
try {
745+
return JSON.parse(codeBlockMatch[1].trim())
746+
} catch {
747+
// Continue to next strategy
748+
}
749+
}
750+
751+
// Strategy 3: Find first complete JSON object/array in the text
752+
// Look for { ... } or [ ... ] with proper bracket matching
753+
const jsonObjectMatch = this.findCompleteJsonInText(content)
754+
if (jsonObjectMatch) {
755+
try {
756+
return JSON.parse(jsonObjectMatch)
757+
} catch {
758+
// Continue to next strategy
759+
}
760+
}
761+
762+
return null
763+
}
764+
765+
private findCompleteJsonInText(text: string): string | null {
766+
const trimmed = text.trim()
767+
768+
// Find first { or [
769+
let startIndex = -1
770+
let startChar = ''
771+
772+
for (let i = 0; i < trimmed.length; i++) {
773+
if (trimmed[i] === '{' || trimmed[i] === '[') {
774+
startIndex = i
775+
startChar = trimmed[i]
776+
break
777+
}
778+
}
779+
780+
if (startIndex === -1) {
781+
return null
782+
}
783+
784+
const endChar = startChar === '{' ? '}' : ']'
785+
let depth = 0
786+
let inString = false
787+
let escaped = false
788+
789+
for (let i = startIndex; i < trimmed.length; i++) {
790+
const char = trimmed[i]
791+
792+
if (escaped) {
793+
escaped = false
794+
continue
795+
}
796+
797+
if (char === '\\') {
798+
escaped = true
799+
continue
800+
}
801+
802+
if (char === '"' && !escaped) {
803+
inString = !inString
804+
continue
805+
}
806+
807+
if (!inString) {
808+
if (char === startChar) {
809+
depth++
810+
} else if (char === endChar) {
811+
depth--
812+
if (depth === 0) {
813+
return trimmed.substring(startIndex, i + 1)
814+
}
815+
}
816+
}
817+
}
818+
819+
return null
716820
}
717821

718822
private processStandardResponse(result: any): BlockOutput {

0 commit comments

Comments
 (0)