mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
fix response format json extraction issues + add warning for invalid json
This commit is contained in:
+25
-3
@@ -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<HTMLDivElement>(null)
|
||||
|
||||
// Function to toggle collapsed state
|
||||
@@ -343,9 +363,11 @@ export function Code({
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
'group relative min-h-[100px] rounded-md border bg-background font-mono text-sm',
|
||||
isConnecting && 'ring-2 ring-blue-500 ring-offset-2'
|
||||
'group relative min-h-[100px] rounded-md border bg-background font-mono text-sm transition-colors',
|
||||
isConnecting && 'ring-2 ring-blue-500 ring-offset-2',
|
||||
!isValidJson && 'border-destructive border-2 bg-destructive/10'
|
||||
)}
|
||||
title={!isValidJson ? 'Invalid JSON' : undefined}
|
||||
onDragOver={(e) => e.preventDefault()}
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
|
||||
+19
-1
@@ -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({
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
{config.id === 'responseFormat' && !isValidJson && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<AlertTriangle className='h-4 w-4 cursor-pointer text-destructive' />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side='top'>
|
||||
<p>Invalid JSON</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
{config.description && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user