Skip to content

Commit 9702155

Browse files
authored
fix(response-format): add response format to tag dropdown, chat panel, and chat client (#637)
* add response format structure to tag dropdown * handle response format outputs for chat client and chat panel, implemented the response format handling for streamed responses * cleanup
1 parent 0f21fbf commit 9702155

17 files changed

Lines changed: 1211 additions & 151 deletions

File tree

apps/sim/app/api/chat/[subdomain]/route.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ export async function GET(
194194
description: deployment.description,
195195
customizations: deployment.customizations,
196196
authType: deployment.authType,
197+
outputConfigs: deployment.outputConfigs,
197198
}),
198199
request
199200
)
@@ -219,6 +220,7 @@ export async function GET(
219220
description: deployment.description,
220221
customizations: deployment.customizations,
221222
authType: deployment.authType,
223+
outputConfigs: deployment.outputConfigs,
222224
}),
223225
request
224226
)

apps/sim/app/api/chat/utils.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -263,17 +263,26 @@ export async function executeWorkflowForChat(
263263
let outputBlockIds: string[] = []
264264

265265
// Extract output configs from the new schema format
266+
let selectedOutputIds: string[] = []
266267
if (deployment.outputConfigs && Array.isArray(deployment.outputConfigs)) {
267-
// Extract block IDs and paths from the new outputConfigs array format
268+
// Extract output IDs in the format expected by the streaming processor
268269
logger.debug(
269270
`[${requestId}] Found ${deployment.outputConfigs.length} output configs in deployment`
270271
)
271-
deployment.outputConfigs.forEach((config) => {
272+
273+
selectedOutputIds = deployment.outputConfigs.map((config) => {
274+
const outputId = config.path
275+
? `${config.blockId}_${config.path}`
276+
: `${config.blockId}.content`
277+
272278
logger.debug(
273-
`[${requestId}] Processing output config: blockId=${config.blockId}, path=${config.path || 'none'}`
279+
`[${requestId}] Processing output config: blockId=${config.blockId}, path=${config.path || 'content'} -> outputId=${outputId}`
274280
)
281+
282+
return outputId
275283
})
276284

285+
// Also extract block IDs for legacy compatibility
277286
outputBlockIds = deployment.outputConfigs.map((config) => config.blockId)
278287
} else {
279288
// Use customizations as fallback
@@ -291,7 +300,9 @@ export async function executeWorkflowForChat(
291300
outputBlockIds = customizations.outputBlockIds
292301
}
293302

294-
logger.debug(`[${requestId}] Using ${outputBlockIds.length} output blocks for extraction`)
303+
logger.debug(
304+
`[${requestId}] Using ${outputBlockIds.length} output blocks and ${selectedOutputIds.length} selected output IDs for extraction`
305+
)
295306

296307
// Find the workflow (deployedState is NOT deprecated - needed for chat execution)
297308
const workflowResult = await db
@@ -457,7 +468,7 @@ export async function executeWorkflowForChat(
457468
workflowVariables,
458469
contextExtensions: {
459470
stream: true,
460-
selectedOutputIds: outputBlockIds,
471+
selectedOutputIds: selectedOutputIds.length > 0 ? selectedOutputIds : outputBlockIds,
461472
edges: edges.map((e: any) => ({
462473
source: e.source,
463474
target: e.target,

apps/sim/app/chat/[subdomain]/chat-client.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ interface ChatConfig {
3333
headerText?: string
3434
}
3535
authType?: 'public' | 'password' | 'email'
36+
outputConfigs?: Array<{ blockId: string; path?: string }>
3637
}
3738

3839
interface AudioStreamingOptions {
@@ -373,8 +374,16 @@ export default function ChatClient({ subdomain }: { subdomain: string }) {
373374
const json = JSON.parse(line.substring(6))
374375
const { blockId, chunk: contentChunk, event: eventType } = json
375376

376-
if (eventType === 'final') {
377+
if (eventType === 'final' && json.data) {
377378
setIsLoading(false)
379+
380+
// Process final execution result for field extraction
381+
const result = json.data
382+
const nonStreamingLogs =
383+
result.logs?.filter((log: any) => !messageIdMap.has(log.blockId)) || []
384+
385+
// Chat field extraction will be handled by the backend using deployment outputConfigs
386+
378387
return
379388
}
380389

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/chat/chat.tsx

Lines changed: 42 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ import { ArrowUp } from 'lucide-react'
55
import { Button } from '@/components/ui/button'
66
import { Input } from '@/components/ui/input'
77
import { ScrollArea } from '@/components/ui/scroll-area'
8+
import { createLogger } from '@/lib/logs/console-logger'
9+
import {
10+
extractBlockIdFromOutputId,
11+
extractPathFromOutputId,
12+
parseOutputContentSafely,
13+
} from '@/lib/response-format'
814
import type { BlockLog, ExecutionResult } from '@/executor/types'
915
import { useExecutionStore } from '@/stores/execution/store'
1016
import { useChatStore } from '@/stores/panel/chat/store'
@@ -14,6 +20,8 @@ import { useWorkflowExecution } from '../../../../hooks/use-workflow-execution'
1420
import { ChatMessage } from './components/chat-message/chat-message'
1521
import { OutputSelect } from './components/output-select/output-select'
1622

23+
const logger = createLogger('ChatPanel')
24+
1725
interface ChatProps {
1826
panelWidth: number
1927
chatMessage: string
@@ -60,8 +68,8 @@ export function Chat({ panelWidth, chatMessage, setChatMessage }: ChatProps) {
6068
const selected = selectedWorkflowOutputs[activeWorkflowId]
6169

6270
if (!selected || selected.length === 0) {
63-
const defaultSelection = outputEntries.length > 0 ? [outputEntries[0].id] : []
64-
return defaultSelection
71+
// Return empty array when nothing is explicitly selected
72+
return []
6573
}
6674

6775
// Ensure we have no duplicates in the selection
@@ -74,7 +82,7 @@ export function Chat({ panelWidth, chatMessage, setChatMessage }: ChatProps) {
7482
}
7583

7684
return selected
77-
}, [selectedWorkflowOutputs, activeWorkflowId, outputEntries, setSelectedWorkflowOutput])
85+
}, [selectedWorkflowOutputs, activeWorkflowId, setSelectedWorkflowOutput])
7886

7987
// Auto-scroll to bottom when new messages are added
8088
useEffect(() => {
@@ -141,25 +149,22 @@ export function Chat({ panelWidth, chatMessage, setChatMessage }: ChatProps) {
141149

142150
if (nonStreamingLogs.length > 0) {
143151
const outputsToRender = selectedOutputs.filter((outputId) => {
144-
// Extract block ID correctly - handle both formats:
145-
// - "blockId" (direct block ID)
146-
// - "blockId_response.result" (block ID with path)
147-
const blockIdForOutput = outputId.includes('_')
148-
? outputId.split('_')[0]
149-
: outputId.split('.')[0]
152+
const blockIdForOutput = extractBlockIdFromOutputId(outputId)
150153
return nonStreamingLogs.some((log) => log.blockId === blockIdForOutput)
151154
})
152155

153156
for (const outputId of outputsToRender) {
154-
const blockIdForOutput = outputId.includes('_')
155-
? outputId.split('_')[0]
156-
: outputId.split('.')[0]
157-
const path = outputId.substring(blockIdForOutput.length + 1)
157+
const blockIdForOutput = extractBlockIdFromOutputId(outputId)
158+
const path = extractPathFromOutputId(outputId, blockIdForOutput)
158159
const log = nonStreamingLogs.find((l) => l.blockId === blockIdForOutput)
159160

160161
if (log) {
161162
let outputValue: any = log.output
163+
162164
if (path) {
165+
// Parse JSON content safely
166+
outputValue = parseOutputContentSafely(outputValue)
167+
163168
const pathParts = path.split('.')
164169
for (const part of pathParts) {
165170
if (
@@ -211,42 +216,41 @@ export function Chat({ panelWidth, chatMessage, setChatMessage }: ChatProps) {
211216
}
212217
}
213218
} catch (e) {
214-
console.error('Error parsing stream data:', e)
219+
logger.error('Error parsing stream data:', e)
215220
}
216221
}
217222
}
218223
}
219224
}
220225

221-
processStream().catch((e) => console.error('Error processing stream:', e))
226+
processStream().catch((e) => logger.error('Error processing stream:', e))
222227
} else if (result && 'success' in result && result.success && 'logs' in result) {
223228
const finalOutputs: any[] = []
224229

225-
if (selectedOutputs && selectedOutputs.length > 0) {
230+
if (selectedOutputs?.length > 0) {
226231
for (const outputId of selectedOutputs) {
227-
// Find the log that corresponds to the start of the outputId
228-
const log = result.logs?.find(
229-
(l: BlockLog) => l.blockId === outputId || outputId.startsWith(`${l.blockId}_`)
230-
)
232+
const blockIdForOutput = extractBlockIdFromOutputId(outputId)
233+
const path = extractPathFromOutputId(outputId, blockIdForOutput)
234+
const log = result.logs?.find((l: BlockLog) => l.blockId === blockIdForOutput)
231235

232236
if (log) {
233237
let output = log.output
234-
// Check if there is a path to traverse
235-
if (outputId.length > log.blockId.length) {
236-
const path = outputId.substring(log.blockId.length + 1)
237-
if (path) {
238-
const pathParts = path.split('.')
239-
let current = output
240-
for (const part of pathParts) {
241-
if (current && typeof current === 'object' && part in current) {
242-
current = current[part]
243-
} else {
244-
current = undefined
245-
break
246-
}
238+
239+
if (path) {
240+
// Parse JSON content safely
241+
output = parseOutputContentSafely(output)
242+
243+
const pathParts = path.split('.')
244+
let current = output
245+
for (const part of pathParts) {
246+
if (current && typeof current === 'object' && part in current) {
247+
current = current[part]
248+
} else {
249+
current = undefined
250+
break
247251
}
248-
output = current
249252
}
253+
output = current
250254
}
251255
if (output !== undefined) {
252256
finalOutputs.push(output)
@@ -255,30 +259,17 @@ export function Chat({ panelWidth, chatMessage, setChatMessage }: ChatProps) {
255259
}
256260
}
257261

258-
// If no specific outputs could be resolved, fall back to the final workflow output
259-
if (finalOutputs.length === 0 && result.output) {
260-
finalOutputs.push(result.output)
261-
}
262+
// Only show outputs if something was explicitly selected
263+
// If no outputs are selected, don't show anything
262264

263265
// Add a new message for each resolved output
264266
finalOutputs.forEach((output) => {
265267
let content = ''
266268
if (typeof output === 'string') {
267269
content = output
268270
} else if (output && typeof output === 'object') {
269-
// Handle cases where output is { response: ... }
270-
const outputObj = output as Record<string, any>
271-
const response = outputObj.response
272-
if (response) {
273-
if (typeof response.content === 'string') {
274-
content = response.content
275-
} else {
276-
// Pretty print for better readability
277-
content = `\`\`\`json\n${JSON.stringify(response, null, 2)}\n\`\`\``
278-
}
279-
} else {
280-
content = `\`\`\`json\n${JSON.stringify(output, null, 2)}\n\`\`\``
281-
}
271+
// For structured responses, pretty print the JSON
272+
content = `\`\`\`json\n${JSON.stringify(output, null, 2)}\n\`\`\``
282273
}
283274

284275
if (content) {

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/chat/components/output-select/output-select.tsx

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { useEffect, useMemo, useRef, useState } from 'react'
22
import { Check, ChevronDown } from 'lucide-react'
33
import { Button } from '@/components/ui/button'
4+
import { extractFieldsFromSchema, parseResponseFormatSafely } from '@/lib/response-format'
45
import { cn } from '@/lib/utils'
56
import { getBlock } from '@/blocks'
7+
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
68
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
79

810
interface OutputSelectProps {
@@ -48,8 +50,31 @@ export function OutputSelect({
4850
? block.name.replace(/\s+/g, '').toLowerCase()
4951
: `block-${block.id}`
5052

53+
// Check for custom response format first
54+
const responseFormatValue = useSubBlockStore.getState().getValue(block.id, 'responseFormat')
55+
const responseFormat = parseResponseFormatSafely(responseFormatValue, block.id)
56+
57+
let outputsToProcess: Record<string, any> = {}
58+
59+
if (responseFormat) {
60+
// Use custom schema properties if response format is specified
61+
const schemaFields = extractFieldsFromSchema(responseFormat)
62+
if (schemaFields.length > 0) {
63+
// Convert schema fields to output structure
64+
schemaFields.forEach((field) => {
65+
outputsToProcess[field.name] = { type: field.type }
66+
})
67+
} else {
68+
// Fallback to default outputs if schema extraction failed
69+
outputsToProcess = block.outputs || {}
70+
}
71+
} else {
72+
// Use default block outputs
73+
outputsToProcess = block.outputs || {}
74+
}
75+
5176
// Add response outputs
52-
if (block.outputs && typeof block.outputs === 'object') {
77+
if (Object.keys(outputsToProcess).length > 0) {
5378
const addOutput = (path: string, outputObj: any, prefix = '') => {
5479
const fullPath = prefix ? `${prefix}.${path}` : path
5580

@@ -100,7 +125,7 @@ export function OutputSelect({
100125
}
101126

102127
// Process all output properties directly (flattened structure)
103-
Object.entries(block.outputs).forEach(([key, value]) => {
128+
Object.entries(outputsToProcess).forEach(([key, value]) => {
104129
addOutput(key, value)
105130
})
106131
}

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/console/components/console-entry/console-entry.tsx

Lines changed: 27 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -125,35 +125,33 @@ export function ConsoleEntry({ entry, consoleWidth }: ConsoleEntryProps) {
125125
<div className='flex items-start gap-2'>
126126
<Terminal className='mt-1 h-4 w-4 text-muted-foreground' />
127127
<div className='overflow-wrap-anywhere relative flex-1 whitespace-normal break-normal font-mono text-sm'>
128-
{typeof entry.output === 'object' &&
129-
entry.output !== null &&
130-
hasNestedStructure(entry.output) && (
131-
<div className='absolute top-0 right-0 z-10'>
132-
<Button
133-
variant='ghost'
134-
size='sm'
135-
className='h-6 px-2 text-muted-foreground hover:text-foreground'
136-
onClick={(e) => {
137-
e.stopPropagation()
138-
setExpandAllJson(!expandAllJson)
139-
}}
140-
>
141-
<span className='flex items-center'>
142-
{expandAllJson ? (
143-
<>
144-
<ChevronUp className='mr-1 h-3 w-3' />
145-
<span className='text-xs'>Collapse</span>
146-
</>
147-
) : (
148-
<>
149-
<ChevronDown className='mr-1 h-3 w-3' />
150-
<span className='text-xs'>Expand</span>
151-
</>
152-
)}
153-
</span>
154-
</Button>
155-
</div>
156-
)}
128+
{entry.output != null && (
129+
<div className='absolute top-0 right-0 z-10'>
130+
<Button
131+
variant='ghost'
132+
size='sm'
133+
className='h-6 px-2 text-muted-foreground hover:text-foreground'
134+
onClick={(e) => {
135+
e.stopPropagation()
136+
setExpandAllJson(!expandAllJson)
137+
}}
138+
>
139+
<span className='flex items-center'>
140+
{expandAllJson ? (
141+
<>
142+
<ChevronUp className='mr-1 h-3 w-3' />
143+
<span className='text-xs'>Collapse</span>
144+
</>
145+
) : (
146+
<>
147+
<ChevronDown className='mr-1 h-3 w-3' />
148+
<span className='text-xs'>Expand</span>
149+
</>
150+
)}
151+
</span>
152+
</Button>
153+
</div>
154+
)}
157155
<JSONView data={entry.output} initiallyExpanded={expandAllJson} />
158156
</div>
159157
</div>

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -217,10 +217,13 @@ export function useWorkflowExecution() {
217217
result.logs.forEach((log: BlockLog) => {
218218
if (streamedContent.has(log.blockId)) {
219219
const content = streamedContent.get(log.blockId) || ''
220-
if (log.output) {
221-
log.output.content = content
222-
}
223-
useConsoleStore.getState().updateConsole(log.blockId, content)
220+
// For console display, show the actual structured block output instead of formatted streaming content
221+
// This ensures console logs match the block state structure
222+
// Use replaceOutput to completely replace the output instead of merging
223+
useConsoleStore.getState().updateConsole(log.blockId, {
224+
replaceOutput: log.output,
225+
success: true,
226+
})
224227
}
225228
})
226229

0 commit comments

Comments
 (0)