diff --git a/app/w/[id]/components/workflow-block/components/connection-blocks/connection-blocks.tsx b/app/w/[id]/components/workflow-block/components/connection-blocks/connection-blocks.tsx index a7b90fcf5b..75b18558a2 100644 --- a/app/w/[id]/components/workflow-block/components/connection-blocks/connection-blocks.tsx +++ b/app/w/[id]/components/workflow-block/components/connection-blocks/connection-blocks.tsx @@ -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, + 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) => { setIsConnecting(false) } return (
{incomingConnections.map((connection) => ( - 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" - > -
- - {connection.name.replace(/\s+/g, '').toLowerCase()} - - .{connection.outputType} -
-
+
+ {Array.isArray(connection.outputType) ? ( + connection.outputType.map((fieldName) => { + const field = connection.responseFormat?.fields.find( + (f: ResponseField) => f.name === fieldName + ) || { + name: fieldName, + type: 'string', + } + return ( + 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" + > +
+ + {connection.name.replace(/\s+/g, '').toLowerCase()} + + .{field.name} +
+
+ ) + }) + ) : ( + 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" + > +
+ + {connection.name.replace(/\s+/g, '').toLowerCase()} + + .{connection.outputType} +
+
+ )} +
))}
) diff --git a/app/w/hooks/use-block-connections.ts b/app/w/hooks/use-block-connections.ts index ea45903e02..5f3bd3583b 100644 --- a/app/w/hooks/use-block-connections.ts +++ b/app/w/hooks/use-block-connections.ts @@ -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, } }) diff --git a/blocks/blocks/agent.ts b/blocks/blocks/agent.ts index e2f624de7b..d6fc2273f2 100644 --- a/blocks/blocks/agent.ts +++ b/blocks/blocks/agent.ts @@ -106,6 +106,18 @@ export const AgentBlock: BlockConfig = { tokens: 'any', toolCalls: 'any', }, + dependsOn: { + subBlockId: 'responseFormat', + condition: { + whenEmpty: { + content: 'string', + model: 'string', + tokens: 'any', + toolCalls: 'any', + }, + whenFilled: 'json', + }, + }, }, }, subBlocks: [ diff --git a/blocks/types.ts b/blocks/types.ts index 7473067487..3302e7730a 100644 --- a/blocks/types.ts +++ b/blocks/types.ts @@ -101,6 +101,13 @@ export interface BlockConfig { outputs: { response: { type: ToolOutputToValueType> + dependsOn?: { + subBlockId: string + condition: { + whenEmpty: ToolOutputToValueType> + whenFilled: 'json' + } + } } } } diff --git a/components/ui/tag-dropdown.tsx b/components/ui/tag-dropdown.tsx index b31c8e9219..467b49e316 100644 --- a/components/ui/tag-dropdown.tsx +++ b/components/ui/tag-dropdown.tsx @@ -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 = ({ // 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 = ({ 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 = ({ // 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 = ({ {filteredTags.length === 0 ? (
No matching tags found
) : ( - filteredTags.map((tag, index) => ( + filteredTags.map((tag: string, index: number) => (