mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
Added support for structured response format from agent block in the target block's connectio tags & connection dropdown menu. Tested, resolves values when response format is specified, and when it isn't
This commit is contained in:
+57
-18
@@ -1,17 +1,27 @@
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { useBlockConnections } from '@/app/w/hooks/use-block-connections'
|
||||
import { ConnectedBlock, useBlockConnections } from '@/app/w/hooks/use-block-connections'
|
||||
|
||||
interface ConnectionBlocksProps {
|
||||
blockId: string
|
||||
setIsConnecting: (isConnecting: boolean) => void
|
||||
}
|
||||
|
||||
interface ResponseField {
|
||||
name: string
|
||||
type: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
export function ConnectionBlocks({ blockId, setIsConnecting }: ConnectionBlocksProps) {
|
||||
const { incomingConnections, hasIncomingConnections } = useBlockConnections(blockId)
|
||||
|
||||
if (!hasIncomingConnections) return null
|
||||
|
||||
const handleDragStart = (e: React.DragEvent, connection: any) => {
|
||||
const handleDragStart = (
|
||||
e: React.DragEvent<HTMLDivElement>,
|
||||
connection: ConnectedBlock,
|
||||
field?: ResponseField
|
||||
) => {
|
||||
e.stopPropagation() // Prevent parent drag handlers from firing
|
||||
setIsConnecting(true)
|
||||
e.dataTransfer.setData(
|
||||
@@ -21,35 +31,64 @@ export function ConnectionBlocks({ blockId, setIsConnecting }: ConnectionBlocksP
|
||||
connectionData: {
|
||||
id: connection.id,
|
||||
name: connection.name,
|
||||
outputType: connection.outputType,
|
||||
outputType: field ? field.name : connection.outputType,
|
||||
sourceBlockId: connection.id,
|
||||
fieldType: field?.type,
|
||||
},
|
||||
})
|
||||
)
|
||||
e.dataTransfer.effectAllowed = 'copy'
|
||||
}
|
||||
|
||||
const handleDragEnd = (e: React.DragEvent) => {
|
||||
const handleDragEnd = (e: React.DragEvent<HTMLDivElement>) => {
|
||||
setIsConnecting(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="absolute -left-[180px] top-0 space-y-2 flex flex-col items-end w-[160px]">
|
||||
{incomingConnections.map((connection) => (
|
||||
<Card
|
||||
key={connection.id}
|
||||
draggable
|
||||
onDragStart={(e) => handleDragStart(e, connection)}
|
||||
onDragEnd={handleDragEnd}
|
||||
className="group flex items-center rounded-lg border bg-card p-2 shadow-sm transition-colors hover:bg-accent/50 cursor-grab active:cursor-grabbing w-fit"
|
||||
>
|
||||
<div className="text-sm">
|
||||
<span className="font-medium leading-none">
|
||||
{connection.name.replace(/\s+/g, '').toLowerCase()}
|
||||
</span>
|
||||
<span className="text-muted-foreground">.{connection.outputType}</span>
|
||||
</div>
|
||||
</Card>
|
||||
<div key={connection.id} className="space-y-2">
|
||||
{Array.isArray(connection.outputType) ? (
|
||||
connection.outputType.map((fieldName) => {
|
||||
const field = connection.responseFormat?.fields.find(
|
||||
(f: ResponseField) => f.name === fieldName
|
||||
) || {
|
||||
name: fieldName,
|
||||
type: 'string',
|
||||
}
|
||||
return (
|
||||
<Card
|
||||
key={field.name}
|
||||
draggable
|
||||
onDragStart={(e) => handleDragStart(e, connection, field)}
|
||||
onDragEnd={handleDragEnd}
|
||||
className="group flex items-center rounded-lg border bg-card p-2 shadow-sm transition-colors hover:bg-accent/50 cursor-grab active:cursor-grabbing w-fit"
|
||||
>
|
||||
<div className="text-sm">
|
||||
<span className="font-medium leading-none">
|
||||
{connection.name.replace(/\s+/g, '').toLowerCase()}
|
||||
</span>
|
||||
<span className="text-muted-foreground">.{field.name}</span>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
})
|
||||
) : (
|
||||
<Card
|
||||
draggable
|
||||
onDragStart={(e) => handleDragStart(e, connection)}
|
||||
onDragEnd={handleDragEnd}
|
||||
className="group flex items-center rounded-lg border bg-card p-2 shadow-sm transition-colors hover:bg-accent/50 cursor-grab active:cursor-grabbing w-fit"
|
||||
>
|
||||
<div className="text-sm">
|
||||
<span className="font-medium leading-none">
|
||||
{connection.name.replace(/\s+/g, '').toLowerCase()}
|
||||
</span>
|
||||
<span className="text-muted-foreground">.{connection.outputType}</span>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
import { shallow } from 'zustand/shallow'
|
||||
import { useWorkflowStore } from '@/stores/workflow/store'
|
||||
|
||||
interface Field {
|
||||
name: string
|
||||
type: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
export interface ConnectedBlock {
|
||||
id: string
|
||||
type: string
|
||||
outputType: string
|
||||
outputType: string | string[]
|
||||
name: string
|
||||
responseFormat?: {
|
||||
fields: Field[]
|
||||
}
|
||||
}
|
||||
|
||||
export function useBlockConnections(blockId: string) {
|
||||
@@ -21,11 +30,34 @@ export function useBlockConnections(blockId: string) {
|
||||
.filter((edge) => edge.target === blockId)
|
||||
.map((edge) => {
|
||||
const sourceBlock = blocks[edge.source]
|
||||
const responseFormatValue = sourceBlock.subBlocks?.responseFormat?.value
|
||||
let responseFormat
|
||||
|
||||
try {
|
||||
responseFormat =
|
||||
typeof responseFormatValue === 'string' && responseFormatValue
|
||||
? JSON.parse(responseFormatValue)
|
||||
: undefined
|
||||
} catch (e) {
|
||||
console.error('Failed to parse response format:', e)
|
||||
responseFormat = undefined
|
||||
}
|
||||
|
||||
// Get the default output type from the block's outputs
|
||||
const defaultOutputs: Field[] = Object.entries(sourceBlock.outputs || {}).map(([key]) => ({
|
||||
name: key,
|
||||
type: 'string',
|
||||
}))
|
||||
|
||||
// If we have a valid response format, use its fields as the output types
|
||||
const outputFields = responseFormat?.fields || defaultOutputs
|
||||
|
||||
return {
|
||||
id: sourceBlock.id,
|
||||
type: sourceBlock.type,
|
||||
outputType: Object.keys(sourceBlock.outputs || {}),
|
||||
outputType: outputFields.map((field: Field) => field.name),
|
||||
name: sourceBlock.name,
|
||||
responseFormat,
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -106,6 +106,18 @@ export const AgentBlock: BlockConfig<AgentResponse> = {
|
||||
tokens: 'any',
|
||||
toolCalls: 'any',
|
||||
},
|
||||
dependsOn: {
|
||||
subBlockId: 'responseFormat',
|
||||
condition: {
|
||||
whenEmpty: {
|
||||
content: 'string',
|
||||
model: 'string',
|
||||
tokens: 'any',
|
||||
toolCalls: 'any',
|
||||
},
|
||||
whenFilled: 'json',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
subBlocks: [
|
||||
|
||||
@@ -101,6 +101,13 @@ export interface BlockConfig<T extends ToolResponse = ToolResponse> {
|
||||
outputs: {
|
||||
response: {
|
||||
type: ToolOutputToValueType<ExtractToolOutput<T>>
|
||||
dependsOn?: {
|
||||
subBlockId: string
|
||||
condition: {
|
||||
whenEmpty: ToolOutputToValueType<ExtractToolOutput<T>>
|
||||
whenFilled: 'json'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,12 @@ import React, { useEffect, useMemo, useState } from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useWorkflowStore } from '@/stores/workflow/store'
|
||||
|
||||
interface Field {
|
||||
name: string
|
||||
type: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
interface TagDropdownProps {
|
||||
visible: boolean
|
||||
onSelect: (newValue: string) => void
|
||||
@@ -38,31 +44,52 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
|
||||
// Get source block and compute tags
|
||||
const { tags } = useMemo(() => {
|
||||
// Helper function to get output paths
|
||||
const getOutputPaths = (obj: any, prefix = ''): string[] => {
|
||||
if (typeof obj !== 'object' || obj === null) {
|
||||
return prefix ? [prefix] : []
|
||||
}
|
||||
|
||||
if ('type' in obj && typeof obj.type === 'string') {
|
||||
return [prefix]
|
||||
}
|
||||
|
||||
return Object.entries(obj).flatMap(([key, value]) => {
|
||||
const newPrefix = prefix ? `${prefix}.${key}` : key
|
||||
return getOutputPaths(value, newPrefix)
|
||||
})
|
||||
}
|
||||
|
||||
// If we have an active source block ID from a drop, use that specific block only
|
||||
if (activeSourceBlockId) {
|
||||
const sourceBlock = blocks[activeSourceBlockId]
|
||||
if (!sourceBlock) return { tags: [] }
|
||||
|
||||
const getOutputPaths = (obj: any, prefix = ''): string[] => {
|
||||
if (typeof obj !== 'object' || obj === null) {
|
||||
return prefix ? [prefix] : []
|
||||
}
|
||||
const blockName = sourceBlock.name || sourceBlock.type
|
||||
const normalizedBlockName = blockName.replace(/\s+/g, '').toLowerCase()
|
||||
|
||||
if ('type' in obj) {
|
||||
return getOutputPaths(obj.type, prefix)
|
||||
// Check for response format first
|
||||
try {
|
||||
const responseFormatValue = sourceBlock.subBlocks?.responseFormat?.value
|
||||
if (typeof responseFormatValue === 'string' && responseFormatValue) {
|
||||
const responseFormat = JSON.parse(responseFormatValue)
|
||||
if (responseFormat?.fields) {
|
||||
return {
|
||||
tags: responseFormat.fields.map(
|
||||
(field: Field) => `${normalizedBlockName}.${field.name}`
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Object.entries(obj).flatMap(([key, value]) => {
|
||||
const newPrefix = prefix ? `${prefix}.${key}` : key
|
||||
return getOutputPaths(value, newPrefix)
|
||||
})
|
||||
} catch (e) {
|
||||
console.error('Error parsing response format:', e)
|
||||
}
|
||||
|
||||
// Fall back to default outputs if no response format
|
||||
const outputPaths = getOutputPaths(sourceBlock.outputs)
|
||||
const blockName = sourceBlock.name || sourceBlock.type
|
||||
|
||||
return {
|
||||
tags: outputPaths.map((path) => `${blockName.replace(/\s+/g, '').toLowerCase()}.${path}`),
|
||||
tags: outputPaths.map((path) => `${normalizedBlockName}.${path}`),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,25 +99,27 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
const sourceBlock = blocks[edge.source]
|
||||
if (!sourceBlock) return []
|
||||
|
||||
const getOutputPaths = (obj: any, prefix = ''): string[] => {
|
||||
if (typeof obj !== 'object' || obj === null) {
|
||||
return prefix ? [prefix] : []
|
||||
}
|
||||
const blockName = sourceBlock.name || sourceBlock.type
|
||||
const normalizedBlockName = blockName.replace(/\s+/g, '').toLowerCase()
|
||||
|
||||
if ('type' in obj) {
|
||||
return getOutputPaths(obj.type, prefix)
|
||||
// Check for response format first
|
||||
try {
|
||||
const responseFormatValue = sourceBlock.subBlocks?.responseFormat?.value
|
||||
if (typeof responseFormatValue === 'string' && responseFormatValue) {
|
||||
const responseFormat = JSON.parse(responseFormatValue)
|
||||
if (responseFormat?.fields) {
|
||||
return responseFormat.fields.map(
|
||||
(field: Field) => `${normalizedBlockName}.${field.name}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return Object.entries(obj).flatMap(([key, value]) => {
|
||||
const newPrefix = prefix ? `${prefix}.${key}` : key
|
||||
return getOutputPaths(value, newPrefix)
|
||||
})
|
||||
} catch (e) {
|
||||
console.error('Error parsing response format:', e)
|
||||
}
|
||||
|
||||
// Fall back to default outputs if no response format
|
||||
const outputPaths = getOutputPaths(sourceBlock.outputs)
|
||||
const blockName = sourceBlock.name || sourceBlock.type
|
||||
|
||||
return outputPaths.map((path) => `${blockName.replace(/\s+/g, '').toLowerCase()}.${path}`)
|
||||
return outputPaths.map((path) => `${normalizedBlockName}.${path}`)
|
||||
})
|
||||
|
||||
return { tags: sourceTags }
|
||||
@@ -99,7 +128,7 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
// Filter tags based on search term
|
||||
const filteredTags = useMemo(() => {
|
||||
if (!searchTerm) return tags
|
||||
return tags.filter((tag) => tag.toLowerCase().includes(searchTerm))
|
||||
return tags.filter((tag: string) => tag.toLowerCase().includes(searchTerm))
|
||||
}, [tags, searchTerm])
|
||||
|
||||
// Reset selection when filtered results change
|
||||
@@ -168,7 +197,7 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
{filteredTags.length === 0 ? (
|
||||
<div className="px-3 py-2 text-sm text-muted-foreground">No matching tags found</div>
|
||||
) : (
|
||||
filteredTags.map((tag, index) => (
|
||||
filteredTags.map((tag: string, index: number) => (
|
||||
<button
|
||||
key={tag}
|
||||
className={cn(
|
||||
|
||||
+35
-66
@@ -205,6 +205,7 @@ export class Executor {
|
||||
): Promise<BlockOutput> {
|
||||
// Start timing
|
||||
const startTime = new Date()
|
||||
|
||||
const blockLog: BlockLog = {
|
||||
blockId: block.id,
|
||||
blockTitle: block.metadata?.title,
|
||||
@@ -216,17 +217,12 @@ export class Executor {
|
||||
}
|
||||
|
||||
try {
|
||||
let output: BlockOutput
|
||||
|
||||
// Handle router blocks differently
|
||||
if (block.metadata?.type === 'router') {
|
||||
const routerOutput = await this.executeRouterBlock(block, context)
|
||||
// console.log('Router output:', routerOutput);
|
||||
|
||||
// Filter workflow to only include blocks in the chosen path
|
||||
this.workflow.blocks = this.workflow.blocks.filter((b) =>
|
||||
this.isInChosenPath(b.id, routerOutput.selectedPath.blockId, block.id)
|
||||
)
|
||||
|
||||
const output = {
|
||||
output = {
|
||||
response: {
|
||||
content: routerOutput.content,
|
||||
model: routerOutput.model,
|
||||
@@ -234,26 +230,8 @@ export class Executor {
|
||||
selectedPath: routerOutput.selectedPath,
|
||||
},
|
||||
}
|
||||
|
||||
blockLog.success = true
|
||||
blockLog.output = output
|
||||
|
||||
// Compute timing
|
||||
const endTime = new Date()
|
||||
blockLog.endedAt = endTime.toISOString()
|
||||
blockLog.durationMs = endTime.getTime() - startTime.getTime()
|
||||
|
||||
// Add log entry
|
||||
context.blockLogs.push(blockLog)
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
// Special handling for agent blocks that use providers
|
||||
if (block.metadata?.type === 'agent') {
|
||||
console.log('Executing agent block with inputs:', inputs)
|
||||
|
||||
// Get response format from inputs if provided
|
||||
} else if (block.metadata?.type === 'agent') {
|
||||
// Special handling for agent blocks that use providers
|
||||
let responseFormat = undefined
|
||||
if (inputs.responseFormat) {
|
||||
try {
|
||||
@@ -264,7 +242,6 @@ export class Executor {
|
||||
// If it's somehow already an object, use it directly
|
||||
responseFormat = inputs.responseFormat
|
||||
}
|
||||
console.log('Parsed responseFormat:', responseFormat)
|
||||
} catch (error: any) {
|
||||
console.error('Error parsing responseFormat:', error)
|
||||
throw new Error('Invalid response format: ' + error.message)
|
||||
@@ -327,11 +304,9 @@ export class Executor {
|
||||
responseFormat,
|
||||
})
|
||||
|
||||
// If responseFormat was specified, return the content directly as the response
|
||||
// with metadata as additional fields
|
||||
const output = responseFormat
|
||||
output = responseFormat
|
||||
? {
|
||||
...JSON.parse(response.content), // The formatted response as the root
|
||||
...JSON.parse(response.content),
|
||||
tokens: response.tokens || {
|
||||
prompt: 0,
|
||||
completion: 0,
|
||||
@@ -345,7 +320,6 @@ export class Executor {
|
||||
: undefined,
|
||||
}
|
||||
: {
|
||||
// Default format when no responseFormat specified
|
||||
response: {
|
||||
content: response.content,
|
||||
model: response.model,
|
||||
@@ -360,36 +334,22 @@ export class Executor {
|
||||
},
|
||||
},
|
||||
}
|
||||
} else {
|
||||
// Regular tool execution
|
||||
const tool = getTool(block.config.tool)
|
||||
if (!tool) {
|
||||
throw new Error(`Tool ${block.config.tool} not found`)
|
||||
}
|
||||
|
||||
blockLog.success = true
|
||||
blockLog.output = output
|
||||
const result = await executeTool(block.config.tool, inputs)
|
||||
if (!result.success) {
|
||||
console.error('Tool execution failed:', result.error)
|
||||
throw new Error(result.error || `Tool ${block.config.tool} failed with no error message`)
|
||||
}
|
||||
|
||||
// Compute timing
|
||||
const endTime = new Date()
|
||||
blockLog.endedAt = endTime.toISOString()
|
||||
blockLog.durationMs = endTime.getTime() - startTime.getTime()
|
||||
|
||||
// Add log entry
|
||||
context.blockLogs.push(blockLog)
|
||||
|
||||
return output
|
||||
output = { response: result.output }
|
||||
}
|
||||
|
||||
// Regular tool execution
|
||||
const tool = getTool(block.config.tool)
|
||||
if (!tool) {
|
||||
throw new Error(`Tool ${block.config.tool} not found`)
|
||||
}
|
||||
|
||||
const result = await executeTool(block.config.tool, inputs)
|
||||
|
||||
if (!result.success) {
|
||||
console.error('Tool execution failed:', result.error)
|
||||
throw new Error(result.error || `Tool ${block.config.tool} failed with no error message`)
|
||||
}
|
||||
|
||||
const output = { response: result.output }
|
||||
|
||||
blockLog.success = true
|
||||
blockLog.output = output
|
||||
|
||||
@@ -403,12 +363,6 @@ export class Executor {
|
||||
|
||||
return output
|
||||
} catch (error: any) {
|
||||
console.error('Block execution failed:', {
|
||||
blockId: block.id,
|
||||
blockTitle: block.metadata?.title,
|
||||
error: error.message,
|
||||
})
|
||||
|
||||
// Update block log with error
|
||||
blockLog.success = false
|
||||
blockLog.error = error.message || `Block execution failed`
|
||||
@@ -538,6 +492,21 @@ export class Executor {
|
||||
`Invalid path part "${part}" in "${path}" for block "${block.metadata?.title}".`
|
||||
)
|
||||
}
|
||||
|
||||
// Check if we have a response format and this is a field from it
|
||||
const responseFormat = sourceBlock.config.params?.responseFormat
|
||||
if (responseFormat && typeof responseFormat === 'string') {
|
||||
try {
|
||||
const parsedFormat = JSON.parse(responseFormat)
|
||||
if (parsedFormat?.fields?.some((f: any) => f.name === part)) {
|
||||
replacementValue = replacementValue[part]
|
||||
continue
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error parsing response format:', e)
|
||||
}
|
||||
}
|
||||
|
||||
replacementValue = replacementValue[part]
|
||||
}
|
||||
|
||||
|
||||
+9
-1
@@ -47,7 +47,15 @@ export class Serializer {
|
||||
params,
|
||||
},
|
||||
inputs,
|
||||
outputs: block.outputs,
|
||||
outputs: {
|
||||
...block.outputs,
|
||||
// Include response format fields if available
|
||||
...(params.responseFormat
|
||||
? {
|
||||
responseFormat: JSON.parse(params.responseFormat),
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
metadata: {
|
||||
title: block.name,
|
||||
description: blockConfig.toolbar.description,
|
||||
|
||||
Reference in New Issue
Block a user