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:
Waleed Latif
2025-02-07 13:01:38 -08:00
parent 660b8887b0
commit 01d2a8acd5
7 changed files with 212 additions and 116 deletions
@@ -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>
)
+34 -2
View File
@@ -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,
}
})
+12
View File
@@ -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: [
+7
View File
@@ -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'
}
}
}
}
}
+58 -29
View File
@@ -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
View File
@@ -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
View File
@@ -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,