From 3850c112ca4e728201a51760e155bc4e0914dd2d Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 30 Jan 2025 13:50:35 -0800 Subject: [PATCH] Standardized output format for blocks/tools. Updated executor so we can now resolve sub-json values for tagged inputs. Updated serializer to match new block output format. --- .../connection-blocks/connection-blocks.tsx | 2 +- app/w/hooks/use-block-connections.ts | 2 +- app/w/hooks/use-workflow-execution.ts | 9 +- blocks/blocks/agent.ts | 14 +- blocks/blocks/api.ts | 8 +- blocks/blocks/crewai.ts | 24 +- blocks/blocks/firecrawl.ts | 8 +- blocks/blocks/function.ts | 7 +- blocks/types.ts | 19 +- blocks/utils.ts | 20 +- executor/__tests__/executor.test.ts | 392 ++++++++---- executor/index.ts | 140 +++-- executor/types.ts | 15 +- serializer/__tests__/serializer.test.ts | 588 +++++------------- serializer/index.ts | 27 +- serializer/types.ts | 25 +- stores/workflow/types.ts | 4 +- tools/anthropic/chat.ts | 16 +- tools/crewai/vision.ts | 22 +- tools/deepseek/chat.ts | 16 +- tools/deepseek/reasoner.ts | 18 +- tools/firecrawl/scrape.ts | 12 +- tools/function/execute.ts | 25 +- tools/google/chat.ts | 20 +- tools/http/request.ts | 16 +- tools/hubspot/contacts.ts | 22 +- tools/index.ts | 24 +- tools/openai/chat.ts | 27 +- tools/salesforce/opportunities.ts | 26 +- tools/types.ts | 5 +- tools/xai/chat.ts | 20 +- 31 files changed, 798 insertions(+), 775 deletions(-) diff --git a/app/w/components/workflow-block/components/connection-blocks/connection-blocks.tsx b/app/w/components/workflow-block/components/connection-blocks/connection-blocks.tsx index 2a3706c430..4ca5ac1076 100644 --- a/app/w/components/workflow-block/components/connection-blocks/connection-blocks.tsx +++ b/app/w/components/workflow-block/components/connection-blocks/connection-blocks.tsx @@ -52,7 +52,7 @@ export function ConnectionBlocks({ {connection.name.replace(/\s+/g, '').toLowerCase()} - .{connection.outputType === 'any' ? 'res' : connection.outputType} + .{connection.outputType} diff --git a/app/w/hooks/use-block-connections.ts b/app/w/hooks/use-block-connections.ts index 4a37f23961..3227b6743a 100644 --- a/app/w/hooks/use-block-connections.ts +++ b/app/w/hooks/use-block-connections.ts @@ -24,7 +24,7 @@ export function useBlockConnections(blockId: string) { return { id: sourceBlock.id, type: sourceBlock.type, - outputType: sourceBlock.outputs?.['response'], + outputType: 'response', name: sourceBlock.name, } }) diff --git a/app/w/hooks/use-workflow-execution.ts b/app/w/hooks/use-workflow-execution.ts index 38ef1a0ae9..7211d5cb1d 100644 --- a/app/w/hooks/use-workflow-execution.ts +++ b/app/w/hooks/use-workflow-execution.ts @@ -18,8 +18,9 @@ export function useWorkflowExecution() { try { // Extract existing block states const currentBlockStates = Object.entries(blocks).reduce((acc, [id, block]) => { - if (block.subBlocks?.response?.value !== undefined) { - acc[id] = { response: block.subBlocks.response.value } + const responseValue = block.subBlocks?.response?.value + if (responseValue !== undefined) { + acc[id] = { response: responseValue } } return acc }, {} as Record) @@ -46,7 +47,7 @@ export function useWorkflowExecution() { if (result.success) { console.group('Workflow Execution Result') console.log('Status: ✅ Success') - console.log('Data:', result.data) + console.log('Output:', result.output) if (result.metadata) { console.log('Duration:', result.metadata.duration + 'ms') console.log('Start Time:', new Date(result.metadata.startTime).toLocaleTimeString()) @@ -58,7 +59,7 @@ export function useWorkflowExecution() { const errorMessage = error instanceof Error ? error.message : 'Unknown error' setExecutionResult({ success: false, - data: {}, + output: { response: {} }, error: errorMessage }) addNotification('error', `Failed to execute workflow: ${errorMessage}`, activeWorkflowId) diff --git a/blocks/blocks/agent.ts b/blocks/blocks/agent.ts index 3069b3e79d..53892bd42d 100644 --- a/blocks/blocks/agent.ts +++ b/blocks/blocks/agent.ts @@ -52,12 +52,20 @@ export const AgentBlock: BlockConfig = { }, outputs: { response: { - type: 'string', + type: { + text: 'string', + model: 'string', + tokens: 'number' + }, dependsOn: { subBlockId: 'responseFormat', condition: { - whenEmpty: 'string', - whenFilled: 'json' + whenEmpty: { + response: { type: 'string' } + }, + whenFilled: { + response: { type: 'json' } + } } } } diff --git a/blocks/blocks/api.ts b/blocks/blocks/api.ts index c5e2844514..65a6dc0a2c 100644 --- a/blocks/blocks/api.ts +++ b/blocks/blocks/api.ts @@ -21,7 +21,13 @@ export const ApiBlock: BlockConfig = { body: { type: 'json', required: false } }, outputs: { - response: 'any' + response: { + type: { + body: 'any', + status: 'number', + headers: 'json' + } + } }, subBlocks: [ { diff --git a/blocks/blocks/crewai.ts b/blocks/blocks/crewai.ts index e69d92835d..60fd4d2b32 100644 --- a/blocks/blocks/crewai.ts +++ b/blocks/blocks/crewai.ts @@ -21,17 +21,15 @@ export const CrewAIVisionBlock: BlockConfig = { prompt: { type: 'string', required: false } }, outputs: { - response: 'any' + response: { + type: { + text: 'string', + model: 'string', + tokens: 'number' + } + } }, subBlocks: [ - { - id: 'apiKey', - title: 'API Key', - type: 'short-input', - layout: 'full', - placeholder: 'Enter your API key', - password: true - }, { id: 'imageUrl', title: 'Image URL', @@ -50,6 +48,14 @@ export const CrewAIVisionBlock: BlockConfig = { 'claude-3-sonnet-20240229' ] }, + { + id: 'apiKey', + title: 'API Key', + type: 'short-input', + layout: 'full', + placeholder: 'Enter your API key', + password: true + }, { id: 'prompt', title: 'Custom Prompt', diff --git a/blocks/blocks/firecrawl.ts b/blocks/blocks/firecrawl.ts index 33011e19fa..a04ba7fbd6 100644 --- a/blocks/blocks/firecrawl.ts +++ b/blocks/blocks/firecrawl.ts @@ -20,7 +20,13 @@ export const FirecrawlScrapeBlock: BlockConfig = { scrapeOptions: { type: 'json', required: false } }, outputs: { - response: 'any' + response: { + type: { + markdown: 'string', + html: 'string', + metadata: 'json' + } + } }, subBlocks: [ { diff --git a/blocks/blocks/function.ts b/blocks/blocks/function.ts index 2a6c190275..ec39db41b4 100644 --- a/blocks/blocks/function.ts +++ b/blocks/blocks/function.ts @@ -21,7 +21,12 @@ export const FunctionBlock: BlockConfig = { code: { type: 'string', required: true } }, outputs: { - result: 'any' + response: { + type: { + value: 'any', + stdout: 'string' + } + } }, subBlocks: [ { diff --git a/blocks/types.ts b/blocks/types.ts index 9c5933f367..90c25acf8d 100644 --- a/blocks/types.ts +++ b/blocks/types.ts @@ -3,19 +3,26 @@ import type { JSX } from 'react' export type BlockIcon = (props: SVGProps) => JSX.Element export type BlockCategory = 'basic' | 'advanced' -export type OutputType = 'string' | 'number' | 'json' | 'boolean' | 'any' + +export type PrimitiveValueType = 'string' | 'number' | 'json' | 'boolean' | 'any' +export type ValueType = PrimitiveValueType | Record + +export interface BlockOutput { + response: ValueType +} + export type ParamType = 'string' | 'number' | 'boolean' | 'json' export type SubBlockType = 'short-input' | 'long-input' | 'dropdown' | 'slider' | 'table' | 'code' | 'switch' export type SubBlockLayout = 'full' | 'half' -export type OutputConfig = OutputType | { - type: OutputType - dependsOn: { +export interface OutputConfig { + type: ValueType + dependsOn?: { subBlockId: string condition: { - whenEmpty: OutputType - whenFilled: OutputType + whenEmpty: BlockOutput + whenFilled: BlockOutput } } } diff --git a/blocks/utils.ts b/blocks/utils.ts index 69ae2f37fb..6919717a45 100644 --- a/blocks/utils.ts +++ b/blocks/utils.ts @@ -1,5 +1,5 @@ import { BlockState, SubBlockState } from '@/stores/workflow/types' -import { OutputType, OutputConfig } from '@/blocks/types' +import { BlockOutput, OutputConfig } from '@/blocks/types' interface CodeLine { id: string @@ -28,23 +28,21 @@ function isCodeEditorValue(value: any[]): value is CodeLine[] { export function resolveOutputType( outputs: Record, subBlocks: Record -): Record { - const resolvedOutputs: Record = {} +): Record { + const resolvedOutputs: Record = {} for (const [key, outputConfig] of Object.entries(outputs)) { - // If outputType is a string, use it directly - if (typeof outputConfig === 'string') { - resolvedOutputs[key] = outputConfig + // If no dependencies, use the type directly + if (!outputConfig.dependsOn) { + resolvedOutputs[key] = { response: outputConfig.type } continue } // Handle dependent output types - const { dependsOn } = outputConfig - const subBlock = subBlocks[dependsOn.subBlockId] - + const subBlock = subBlocks[outputConfig.dependsOn.subBlockId] resolvedOutputs[key] = isEmptyValue(subBlock?.value) - ? dependsOn.condition.whenEmpty - : dependsOn.condition.whenFilled + ? outputConfig.dependsOn.condition.whenEmpty + : outputConfig.dependsOn.condition.whenFilled } return resolvedOutputs diff --git a/executor/__tests__/executor.test.ts b/executor/__tests__/executor.test.ts index 277d796104..3eaf08d264 100644 --- a/executor/__tests__/executor.test.ts +++ b/executor/__tests__/executor.test.ts @@ -1,7 +1,8 @@ import { Executor } from '../index' import { SerializedWorkflow } from '@/serializer/types' import { Tool } from '../types' -import { tools } from '@/tools' +import { tools } from '@/tools' +import { BlockOutput, ValueType } from '@/blocks/types' // Mock tools const createMockTool = ( @@ -41,7 +42,13 @@ const createMockTool = ( ...(params.optionalParam !== undefined ? { optionalParam: params.optionalParam } : {}) }) }, - transformResponse: () => mockResponse, + transformResponse: async () => ({ + success: true, + output: { + text: mockResponse.result, + ...mockResponse.data + } + }), transformError: () => mockError || 'Mock error' }) @@ -60,7 +67,7 @@ describe('Executor', () => { const mockTool = createMockTool( 'test-tool', 'Test Tool', - { result: 'test processed' } + { result: 'test processed', data: { status: 200 } } ); (tools as any)['test-tool'] = mockTool @@ -71,21 +78,32 @@ describe('Executor', () => { position: { x: 0, y: 0 }, config: { tool: 'test-tool', - params: { input: 'test' }, - interface: { - inputs: { input: 'string' }, - outputs: { result: 'string' } - } + params: { input: 'test' } + }, + inputs: { input: 'string' }, + outputs: { + output: { + response: { + text: 'string', + status: 'number' + } as ValueType + } as BlockOutput } }], connections: [] - } + } // Mock fetch global.fetch = jest.fn().mockImplementation(() => Promise.resolve({ ok: true, - json: () => Promise.resolve({ result: 'test processed' }) + json: () => Promise.resolve({ + success: true, + output: { + text: 'test processed', + status: 200 + } + }) }) ) @@ -93,7 +111,12 @@ describe('Executor', () => { const result = await executor.execute('workflow-1') expect(result.success).toBe(true) - expect(result.data).toEqual({ result: 'test processed' }) + expect(result.output).toEqual({ + response: { + text: 'test processed', + status: 200 + } + }) expect(global.fetch).toHaveBeenCalledWith( 'https://api.test.com/endpoint', expect.objectContaining({ @@ -111,7 +134,7 @@ describe('Executor', () => { const mockTool = createMockTool( 'test-tool', 'Test Tool', - { result: 'test processed' }, + { result: 'test processed', data: { status: 200 } }, undefined, { optionalParam: { @@ -130,11 +153,16 @@ describe('Executor', () => { position: { x: 0, y: 0 }, config: { tool: 'test-tool', - params: { input: 'test' }, - interface: { - inputs: { input: 'string' }, - outputs: { result: 'string' } - } + params: { input: 'test' } + }, + inputs: { input: 'string' }, + outputs: { + output: { + response: { + text: 'string', + status: 'number' + } as ValueType + } as BlockOutput } }], connections: [] @@ -143,7 +171,13 @@ describe('Executor', () => { global.fetch = jest.fn().mockImplementation(() => Promise.resolve({ ok: true, - json: () => Promise.resolve({ result: 'test processed' }) + json: () => Promise.resolve({ + success: true, + output: { + text: 'test processed', + status: 200 + } + }) }) ) @@ -171,7 +205,7 @@ describe('Executor', () => { const mockTool = createMockTool( 'test-tool', 'Test Tool', - { result: 'test processed' } + { result: 'test processed', data: { status: 200 } } ); (tools as any)['test-tool'] = mockTool @@ -182,15 +216,20 @@ describe('Executor', () => { position: { x: 0, y: 0 }, config: { tool: 'test-tool', - params: {}, // Missing required 'input' parameter - interface: { - inputs: {}, - outputs: { result: 'string' } - } + params: {} // Missing required 'input' parameter + }, + inputs: {}, + outputs: { + output: { + response: { + text: 'string', + status: 'number' + } as ValueType + } as BlockOutput } }], connections: [] - } + } const executor = new Executor(workflow) const result = await executor.execute('workflow-1') @@ -206,7 +245,7 @@ describe('Executor', () => { {}, 'API Error' ); - (tools as any)['test-tool'] = mockTool + (tools as any)['test-tool'] = mockTool const workflow: SerializedWorkflow = { version: '1.0', @@ -215,15 +254,20 @@ describe('Executor', () => { position: { x: 0, y: 0 }, config: { tool: 'test-tool', - params: { input: 'test' }, - interface: { - inputs: { input: 'string' }, - outputs: { result: 'string' } - } + params: { input: 'test' } + }, + inputs: { input: 'string' }, + outputs: { + output: { + response: { + text: 'string', + status: 'number' + } as ValueType + } as BlockOutput } }], connections: [] - } + } // Mock fetch to fail global.fetch = jest.fn().mockImplementation(() => @@ -231,22 +275,22 @@ describe('Executor', () => { ok: false, json: () => Promise.resolve({ error: 'API Error' }) }) - ) + ) - const executor = new Executor(workflow) - const result = await executor.execute('workflow-1') + const executor = new Executor(workflow) + const result = await executor.execute('workflow-1') - expect(result.success).toBe(false) + expect(result.success).toBe(false) expect(result.error).toContain('API Error') - }) - }) + }) + }) describe('Interface Validation', () => { it('should validate input types', async () => { const mockTool = createMockTool( 'test-tool', 'Test Tool', - { result: 123 }, + { result: 123, data: { status: 200 } }, 'Invalid type for input' ); (tools as any)['test-tool'] = mockTool @@ -258,11 +302,16 @@ describe('Executor', () => { position: { x: 0, y: 0 }, config: { tool: 'test-tool', - params: { input: 42 }, // Wrong type for input - interface: { - inputs: { input: 'string' }, - outputs: { result: 'number' } - } + params: { input: 42 } // Wrong type for input + }, + inputs: { input: 'string' }, + outputs: { + output: { + response: { + text: 'number', + status: 'number' + } as ValueType + } as BlockOutput } }], connections: [] @@ -291,11 +340,16 @@ describe('Executor', () => { position: { x: 0, y: 0 }, config: { tool: 'test-tool', - params: { input: 'test' }, - interface: { - inputs: { input: 'string' }, - outputs: { result: 'string' } - } + params: { input: 'test' } + }, + inputs: { input: 'string' }, + outputs: { + output: { + response: { + text: 'string', + status: 'number' + } as ValueType + } as BlockOutput } }], connections: [] @@ -322,12 +376,12 @@ describe('Executor', () => { const mockTool1 = createMockTool( 'tool-1', 'Tool 1', - { response: 'test data' } + { result: 'test data', data: { status: 200 } } ); const mockTool2 = createMockTool( 'tool-2', 'Tool 2', - { response: 'processed data' } + { result: 'processed data', data: { status: 201 } } ); (tools as any)['tool-1'] = mockTool1; (tools as any)['tool-2'] = mockTool2; @@ -340,11 +394,16 @@ describe('Executor', () => { position: { x: 0, y: 0 }, config: { tool: 'tool-1', - params: { input: 'initial' }, - interface: { - inputs: {}, - outputs: { response: 'string' } - } + params: { input: 'initial' } + }, + inputs: {}, + outputs: { + output: { + response: { + text: 'string', + status: 'number' + } as ValueType + } as BlockOutput } }, { @@ -353,16 +412,28 @@ describe('Executor', () => { config: { tool: 'tool-2', params: { - input: '' - }, - interface: { - inputs: { input: 'string' }, - outputs: { response: 'string' } + input: '' } + }, + inputs: { input: 'string' }, + outputs: { + output: { + response: { + text: 'string', + status: 'number' + } as ValueType + } as BlockOutput } } ], - connections: [] + connections: [ + { + source: 'block1', + target: 'block2', + sourceHandle: 'output.response.text', + targetHandle: 'input' + } + ] }; // Mock fetch for both tools @@ -370,13 +441,25 @@ describe('Executor', () => { .mockImplementationOnce(() => Promise.resolve({ ok: true, - json: () => Promise.resolve({ response: 'test data' }) + json: () => Promise.resolve({ + success: true, + output: { + text: 'test data', + status: 200 + } + }) }) ) .mockImplementationOnce(() => Promise.resolve({ ok: true, - json: () => Promise.resolve({ response: 'processed data' }) + json: () => Promise.resolve({ + success: true, + output: { + text: 'processed data', + status: 201 + } + }) }) ); @@ -384,7 +467,12 @@ describe('Executor', () => { const result = await executor.execute('workflow-1'); expect(result.success).toBe(true); - expect(result.data).toEqual({ response: 'processed data' }); + expect(result.output).toEqual({ + response: { + text: 'processed data', + status: 201 + } + }); expect(global.fetch).toHaveBeenCalledTimes(2); }); @@ -397,11 +485,15 @@ describe('Executor', () => { position: { x: 0, y: 0 }, config: { tool: 'test-tool', - params: {}, - interface: { - inputs: {}, - outputs: {} - } + params: {} + }, + inputs: {}, + outputs: { + output: { + response: { + text: 'string' + } as ValueType + } as BlockOutput } }, { @@ -409,11 +501,15 @@ describe('Executor', () => { position: { x: 200, y: 0 }, config: { tool: 'test-tool', - params: {}, - interface: { - inputs: {}, - outputs: {} - } + params: {} + }, + inputs: {}, + outputs: { + output: { + response: { + text: 'string' + } as ValueType + } as BlockOutput } } ], @@ -421,13 +517,13 @@ describe('Executor', () => { { source: 'block-1', target: 'block-2', - sourceHandle: 'output', + sourceHandle: 'output.response.text', targetHandle: 'input' }, { source: 'block-2', target: 'block-1', - sourceHandle: 'output', + sourceHandle: 'output.response.text', targetHandle: 'input' } ] @@ -469,14 +565,18 @@ describe('Executor', () => { 'Authorization': `Bearer ${params.apiKey}` }), body: (params) => ({ - model: 'gpt-4', + model: 'gpt-4o', messages: [ { role: 'system', content: params.systemPrompt } ] }) }, transformResponse: async () => ({ - response: 'https://api.example.com/data' + success: true, + output: { + text: 'https://api.example.com/data', + model: 'gpt-4o' + } }), transformError: () => 'OpenAI error' }; @@ -506,7 +606,11 @@ describe('Executor', () => { body: (params) => ({ code: params.code, url: params.url }) }, transformResponse: async () => ({ - response: { method: 'GET', headers: { 'Accept': 'application/json' } } + success: true, + output: { + method: 'GET', + headers: { 'Accept': 'application/json' } + } }), transformError: () => 'Function execution error' }; @@ -536,7 +640,11 @@ describe('Executor', () => { body: undefined }, transformResponse: async () => ({ - response: { status: 200, data: { message: 'Success!' } } + success: true, + output: { + message: 'Success!', + status: 200 + } }), transformError: () => 'HTTP request error' }; @@ -556,16 +664,19 @@ describe('Executor', () => { params: { systemPrompt: 'Generate an API endpoint', apiKey: 'test-key' - }, - interface: { - inputs: { - systemPrompt: 'string', - apiKey: 'string' - }, - outputs: { - response: 'string' - } } + }, + inputs: { + systemPrompt: 'string', + apiKey: 'string' + }, + outputs: { + output: { + response: { + text: 'string', + model: 'string' + } as ValueType + } as BlockOutput } }, { @@ -575,17 +686,20 @@ describe('Executor', () => { tool: 'function.execute', params: { code: 'return { method: "GET", headers: { "Accept": "application/json" } }', - url: '' - }, - interface: { - inputs: { - code: 'string', - url: 'string' - }, - outputs: { - response: 'any' - } + url: '' } + }, + inputs: { + code: 'string', + url: 'string' + }, + outputs: { + output: { + response: { + method: 'string', + headers: 'json' + } as ValueType + } as BlockOutput } }, { @@ -594,31 +708,54 @@ describe('Executor', () => { config: { tool: 'http.request', params: { - url: '', - method: '' - }, - interface: { - inputs: { - url: 'string', - method: 'string' - }, - outputs: { - response: 'any' - } + url: '', + method: '' } + }, + inputs: { + url: 'string', + method: 'string' + }, + outputs: { + output: { + response: { + message: 'string', + status: 'number' + } as ValueType + } as BlockOutput } } ], - connections: [] + connections: [ + { + source: 'agent1', + target: 'function1', + sourceHandle: 'output.response.text', + targetHandle: 'url' + }, + { + source: 'function1', + target: 'api1', + sourceHandle: 'output.response.method', + targetHandle: 'method' + } + ] }; - // Mock fetch responses + // Mock fetch responses with sequential data flow + const apiEndpoint = 'https://api.example.com/data'; + const requestMethod = 'GET'; + global.fetch = jest.fn() .mockImplementationOnce(() => Promise.resolve({ ok: true, json: () => Promise.resolve({ - response: 'https://api.example.com/data' + success: true, + output: { + text: apiEndpoint, + model: 'gpt-4o' + } }) }) ) @@ -626,7 +763,11 @@ describe('Executor', () => { Promise.resolve({ ok: true, json: () => Promise.resolve({ - response: { method: 'GET', headers: { 'Accept': 'application/json' } } + success: true, + output: { + method: requestMethod, + headers: { 'Accept': 'application/json' } + } }) }) ) @@ -634,7 +775,11 @@ describe('Executor', () => { Promise.resolve({ ok: true, json: () => Promise.resolve({ - response: { status: 200, data: { message: 'Success!' } } + success: true, + output: { + message: 'Success!', + status: 200 + } }) }) ); @@ -643,8 +788,11 @@ describe('Executor', () => { const result = await executor.execute('test-workflow'); expect(result.success).toBe(true); - expect(result.data).toEqual({ - response: { status: 200, data: { message: 'Success!' } } + expect(result.output).toEqual({ + response: { + message: 'Success!', + status: 200 + } }); // Verify the execution order and data flow @@ -653,7 +801,7 @@ describe('Executor', () => { // First call - Agent generates API endpoint expect(JSON.parse(fetchCalls[0][1].body)).toEqual({ - model: 'gpt-4', + model: 'gpt-4o', messages: [ { role: 'system', content: 'Generate an API endpoint' } ] @@ -662,12 +810,12 @@ describe('Executor', () => { // Second call - Function processes the URL expect(JSON.parse(fetchCalls[1][1].body)).toEqual({ code: 'return { method: "GET", headers: { "Accept": "application/json" } }', - url: 'https://api.example.com/data' + url: "" // Should be resolved value from first call }); // Third call - API makes the request - expect(fetchCalls[2][0]).toBe('https://api.example.com/data'); - expect(fetchCalls[2][1].method).toBe('GET'); + expect(fetchCalls[2][0]).toBe(""); // Should be resolved value from first call + expect(fetchCalls[2][1].method).toBe(""); // Should be resolved value from second call }); }); }) diff --git a/executor/index.ts b/executor/index.ts index 83796b76e5..340f2b780b 100644 --- a/executor/index.ts +++ b/executor/index.ts @@ -1,18 +1,19 @@ import { SerializedWorkflow, SerializedBlock } from '@/serializer/types' import { ExecutionContext, ExecutionResult, Tool } from './types' import { tools } from '@/tools' +import { BlockOutput } from '@/blocks/types' export class Executor { constructor( private workflow: SerializedWorkflow, - private initialBlockStates: Record = {} + private initialBlockStates: Record = {} ) {} private async executeBlock( block: SerializedBlock, inputs: Record, context: ExecutionContext - ): Promise> { + ): Promise { const toolId = block.config.tool if (!toolId) throw new Error(`Block ${block.id} does not specify a tool`) @@ -38,7 +39,16 @@ export class Executor { const error = await response.json().catch(() => ({ message: response.statusText })) throw new Error(tool.transformError(error)) } - return await tool.transformResponse(response) + + const result = await tool.transformResponse(response) + + if (!result.success) { + throw new Error(tool.transformError(result)) + } + + return { + response: result.output + } } catch (error) { throw new Error(`Tool ${toolId} execution failed: ${error instanceof Error ? error.message : 'Unknown error'}`) } @@ -89,57 +99,97 @@ export class Executor { private resolveInputs(block: SerializedBlock, context: ExecutionContext): Record { const inputs = { ...block.config.params } - const blockNameMap = new Map( - this.workflow.blocks.map(b => { - const title = b.metadata?.title || ''; - const normalizedName = title.toLowerCase().replace(/\s+/g, ''); - return [normalizedName, b.id]; - }) - ); - const blockStateMap = new Map( - Object.entries(this.initialBlockStates) - .filter(([_, state]) => state !== undefined) + // Create maps for both ID and name lookups + const blockById = new Map( + this.workflow.blocks.map(b => [b.id, b]) + ) + const blockByName = new Map( + this.workflow.blocks.map(b => [ + b.metadata?.title?.toLowerCase().replace(/\s+/g, '') || '', + b + ]) ) - const connectionPattern = /<([^>]+)\.(string|number|boolean|res|any)>/g - - return Object.entries(block.config.params || {}).reduce((acc, [key, value]) => { + const resolvedInputs = Object.entries(inputs).reduce((acc, [key, value]) => { if (typeof value === 'string') { - let resolvedValue = value - Array.from(value.matchAll(connectionPattern)).forEach(match => { - const [fullMatch, blockName, type] = match + const matches = value.match(/<([^>]+)>/g) + + if (matches) { + let resolvedValue = value - // Try both the original format and normalized format - const normalizedBlockName = blockName.toLowerCase().replace(/\s+/g, ''); - const blockId = blockNameMap.get(normalizedBlockName); - - if (!blockId) { - return; - } - - const sourceOutput = context.blockStates.get(blockId) || blockStateMap.get(blockId) - - if (sourceOutput) { - const replacementValue = type === 'res' - ? sourceOutput.response - : (sourceOutput.output || sourceOutput.response) + matches.forEach(match => { + const path = match.slice(1, -1) // Remove < and > + const [blockRef, ...pathParts] = path.split('.') - if (replacementValue !== undefined) { - resolvedValue = resolvedValue.replace(fullMatch, replacementValue.toString()) + // Try to find block by ID first, then by normalized name + let sourceBlock = blockById.get(blockRef) + if (!sourceBlock) { + const normalizedName = blockRef.toLowerCase().replace(/\s+/g, '') + sourceBlock = blockByName.get(normalizedName) } + + if (!sourceBlock) { + console.warn(`Block ${blockRef} not found by ID or name`) + return + } + + const sourceState = context.blockStates.get(sourceBlock.id) + if (!sourceState) { + console.warn(`No state found for block ${sourceBlock.id}`) + return + } + + // Start with the block's state + let replacementValue: any = sourceState + + // Traverse the path parts to get the final value + for (const part of pathParts) { + if (!replacementValue || typeof replacementValue !== 'object') { + console.warn(`Invalid path part ${part} in ${path}`) + return + } + replacementValue = replacementValue[part] + } + + if (replacementValue !== undefined) { + // Replace the entire template expression with the resolved value + resolvedValue = resolvedValue.replace(match, + typeof replacementValue === 'object' + ? JSON.stringify(replacementValue) + : String(replacementValue) + ) + } else { + console.warn(`No value found at path ${path}`) + } + }) + + // Try to parse the value if it looks like JSON + try { + if (resolvedValue.startsWith('{') || resolvedValue.startsWith('[')) { + acc[key] = JSON.parse(resolvedValue) + } else { + acc[key] = resolvedValue + } + } catch { + acc[key] = resolvedValue } - }) - acc[key] = resolvedValue + } else { + acc[key] = value + } } else { acc[key] = value } + return acc - }, inputs) + }, {} as Record) + + return resolvedInputs } async execute(workflowId: string): Promise { const startTime = new Date() + const context: ExecutionContext = { workflowId, blockStates: new Map(), @@ -153,14 +203,20 @@ export class Executor { const block = this.workflow.blocks.find(b => b.id === blockId) if (!block) throw new Error(`Block ${blockId} not found in workflow`) - const result = await this.executeBlock(block, this.resolveInputs(block, context), context) - context.blockStates.set(blockId, result) + const output = await this.executeBlock(block, this.resolveInputs(block, context), context) + context.blockStates.set(blockId, output) } const endTime = new Date() + const lastOutput = context.blockStates.get(executionOrder[executionOrder.length - 1]) + + if (!lastOutput) { + throw new Error('No output from workflow execution') + } + return { success: true, - data: context.blockStates.get(executionOrder[executionOrder.length - 1]) || {}, + output: lastOutput, metadata: { duration: endTime.getTime() - startTime.getTime(), startTime: startTime.toISOString(), @@ -170,7 +226,7 @@ export class Executor { } catch (error) { return { success: false, - data: {}, + output: { response: {} }, error: error instanceof Error ? error.message : 'Unknown error' } } diff --git a/executor/types.ts b/executor/types.ts index c6acb6343a..21d0f2ebde 100644 --- a/executor/types.ts +++ b/executor/types.ts @@ -1,4 +1,6 @@ -export interface Tool

{ +import { BlockOutput } from '@/blocks/types' + +export interface Tool

> { id: string name: string description: string @@ -17,7 +19,11 @@ export interface Tool

{ headers: (params: P) => Record body?: (params: P) => Record } - transformResponse: (response: any) => R + transformResponse: (response: any) => Promise<{ + success: boolean + output: O + error?: string + }> transformError: (error: any) => string } @@ -27,14 +33,13 @@ export interface ToolRegistry { export interface ExecutionContext { workflowId: string - blockStates: Map - input?: Record + blockStates: Map metadata?: Record } export interface ExecutionResult { success: boolean - data: Record + output: BlockOutput error?: string metadata?: { duration: number diff --git a/serializer/__tests__/serializer.test.ts b/serializer/__tests__/serializer.test.ts index 83fd6cb17c..3c7896a20b 100644 --- a/serializer/__tests__/serializer.test.ts +++ b/serializer/__tests__/serializer.test.ts @@ -2,7 +2,7 @@ import { Edge } from 'reactflow' import { Serializer } from '../index' import { SerializedWorkflow } from '../types' import { BlockState } from '@/stores/workflow/types' -import { OutputType } from '@/blocks/types' +import { BlockOutput, ValueType } from '@/blocks/types' import { getBlock } from '@/blocks' // Mock icons @@ -48,7 +48,15 @@ describe('Serializer', () => { context: { type: 'string', required: false }, apiKey: { type: 'string', required: false } }, - outputs: { response: 'string' as OutputType }, + outputs: { + response: { + response: { + text: 'string', + model: 'string', + tokens: 'number' + } + } satisfies BlockOutput + }, subBlocks: [ { id: 'model', type: 'dropdown' }, { id: 'systemPrompt', type: 'long-input' }, @@ -74,7 +82,15 @@ describe('Serializer', () => { url: { type: 'string', required: true }, method: { type: 'string', required: true } }, - outputs: { response: 'any' as OutputType }, + outputs: { + response: { + response: { + body: 'any', + status: 'number', + headers: 'json' + } + } satisfies BlockOutput + }, subBlocks: [ { id: 'url', type: 'short-input' }, { id: 'method', type: 'dropdown' } @@ -95,7 +111,15 @@ describe('Serializer', () => { }, workflow: { inputs: {}, - outputs: { response: 'string' as OutputType }, + outputs: { + response: { + response: { + text: 'string', + model: 'string', + tokens: 'number' + } + } satisfies BlockOutput + }, subBlocks: [] }, toolbar: { @@ -109,13 +133,14 @@ describe('Serializer', () => { }) describe('serializeWorkflow', () => { - it('should serialize a workflow with agent and http blocks', () => { + it('should serialize a workflow with one tool', async () => { const blocks: Record = { - 'agent-1': { - id: 'agent-1', + 'block-1': { + id: 'block-1', type: 'agent', - name: 'GPT-4o Agent', - position: { x: 100, y: 100 }, + name: 'Test Agent', + position: { x: 0, y: 0 }, + enabled: true, subBlocks: { 'model': { id: 'model', @@ -125,97 +150,51 @@ describe('Serializer', () => { 'systemPrompt': { id: 'systemPrompt', type: 'long-input', - value: 'You are helpful' - }, - 'temperature': { - id: 'temperature', - type: 'slider', - value: 0.7 - }, - 'responseFormat': { - id: 'responseFormat', - type: 'code', - value: null + value: 'test' } }, outputs: { - response: 'string' - } - }, - 'http-1': { - id: 'http-1', - type: 'api', - name: 'API Call', - position: { x: 400, y: 100 }, - subBlocks: { - 'url': { - id: 'url', - type: 'short-input', - value: 'https://api.example.com' - }, - 'method': { - id: 'method', - type: 'dropdown', - value: 'GET' - } - }, - outputs: { - response: 'any' + response: { + response: { + text: 'string', + status: 'number' + } + } satisfies BlockOutput } } - } + } - const connections: Edge[] = [ - { - id: 'conn-1', - source: 'agent-1', - target: 'http-1', - sourceHandle: 'response', - targetHandle: 'body' - } - ] + const workflow = serializer.serializeWorkflow(blocks, []) + const block = workflow.blocks[0] - const serialized = serializer.serializeWorkflow(blocks, connections) - - // Test workflow structure - expect(serialized.version).toBe('1.0') - expect(serialized.blocks).toHaveLength(2) - expect(serialized.connections).toHaveLength(1) - - // Test agent block serialization - const agentBlock = serialized.blocks.find(b => b.id === 'agent-1') - expect(agentBlock).toBeDefined() - expect(agentBlock?.config.tool).toBe('openai.chat') - expect(agentBlock?.config.params).toEqual({ + expect(block.config.tool).toBe('openai.chat') + expect(block.config.params).toEqual({ model: 'gpt-4o', - systemPrompt: 'You are helpful', - temperature: 0.7, - responseFormat: null - }) - expect(agentBlock?.config.interface.outputs).toEqual({ - response: 'string' + systemPrompt: 'test' }) - - // Test http block serialization - const httpBlock = serialized.blocks.find(b => b.id === 'http-1') - expect(httpBlock).toBeDefined() - expect(httpBlock?.config.tool).toBe('http.request') - expect(httpBlock?.config.params).toEqual({ - url: 'https://api.example.com', - method: 'GET' - }) - expect(httpBlock?.config.interface.outputs).toEqual({ - response: 'any' + expect(block.inputs).toEqual({ + systemPrompt: 'string', + context: 'string', + apiKey: 'string' + }) + expect(block.outputs).toEqual({ + response: { + response: { + text: 'string', + status: 'number' + } + } satisfies BlockOutput }) }) - it('should handle blocks with minimal required configuration', () => { + it('should handle blocks with minimal configuration', () => { const blocks: Record = { 'minimal-1': { id: 'minimal-1', type: 'agent', name: 'Minimal Agent', position: { x: 0, y: 0 }, + enabled: true, subBlocks: { 'model': { id: 'model', @@ -224,29 +203,42 @@ describe('Serializer', () => { } }, outputs: { - response: 'string' + response: { + response: { + text: 'string' + } + } satisfies BlockOutput } } - } + } - const serialized = serializer.serializeWorkflow(blocks, []) - const block = serialized.blocks[0] - - expect(block.id).toBe('minimal-1') - expect(block.config.tool).toBe('openai.chat') - expect(block.config.params).toEqual({ model: 'gpt-4o' }) - expect(block.config.interface.outputs).toEqual({ - response: 'string' + const workflow = serializer.serializeWorkflow(blocks, []) + const block = workflow.blocks[0] + + expect(block.config.tool).toBe('openai.chat') + expect(block.config.params).toEqual({ model: 'gpt-4o' }) + expect(block.inputs).toEqual({ + systemPrompt: 'string', + context: 'string', + apiKey: 'string' + }) + expect(block.outputs).toEqual({ + response: { + response: { + text: 'string' + } + } satisfies BlockOutput }) }) - it('should handle complex workflow with multiple interconnected blocks', () => { + it('should handle complex workflow with multiple blocks', () => { const blocks: Record = { 'input-1': { id: 'input-1', type: 'api', name: 'Data Input', position: { x: 100, y: 100 }, + enabled: true, subBlocks: { 'url': { id: 'url', @@ -260,7 +252,13 @@ describe('Serializer', () => { } }, outputs: { - response: 'any' + response: { + response: { + body: 'json', + status: 'number', + headers: 'json' + } + } satisfies BlockOutput } }, 'process-1': { @@ -268,6 +266,7 @@ describe('Serializer', () => { type: 'agent', name: 'Data Processor', position: { x: 300, y: 100 }, + enabled: true, subBlocks: { 'model': { id: 'model', @@ -278,15 +277,16 @@ describe('Serializer', () => { id: 'systemPrompt', type: 'long-input', value: 'Process this data' - }, - 'responseFormat': { - id: 'responseFormat', - type: 'code', - value: '{ "type": "json" }' } }, outputs: { - response: 'json' + response: { + response: { + text: 'string', + model: 'string', + tokens: 'number' + } + } satisfies BlockOutput } } } @@ -301,283 +301,42 @@ describe('Serializer', () => { } ] - const serialized = serializer.serializeWorkflow(blocks, connections) + const workflow = serializer.serializeWorkflow(blocks, connections) // Verify workflow structure - expect(serialized.blocks).toHaveLength(2) - expect(serialized.connections).toHaveLength(1) + expect(workflow.blocks).toHaveLength(2) + expect(workflow.connections).toHaveLength(1) // Verify data flow chain - const conn = serialized.connections[0] + const conn = workflow.connections[0] expect(conn.source).toBe('input-1') expect(conn.target).toBe('process-1') expect(conn.sourceHandle).toBe('response') expect(conn.targetHandle).toBe('context') // Verify block outputs - const inputBlock = serialized.blocks.find(b => b.id === 'input-1') - const processBlock = serialized.blocks.find(b => b.id === 'process-1') + const inputBlock = workflow.blocks.find(b => b.id === 'input-1') + const processBlock = workflow.blocks.find(b => b.id === 'process-1') - expect(inputBlock?.config.interface.outputs).toEqual({ - response: 'any' + expect(inputBlock?.outputs).toEqual({ + response: { + response: { + body: 'json', + status: 'number', + headers: 'json' + } + } satisfies BlockOutput }) - expect(processBlock?.config.interface.outputs).toEqual({ - response: 'json' + expect(processBlock?.outputs).toEqual({ + response: { + response: { + text: 'string', + model: 'string', + tokens: 'number' + } + } satisfies BlockOutput }) }) - - it('should preserve tool-specific parameters', () => { - const blocks: Record = { - 'agent-1': { - id: 'agent-1', - type: 'agent', - name: 'Advanced Agent', - position: { x: 0, y: 0 }, - subBlocks: { - 'model': { - id: 'model', - type: 'dropdown', - value: 'gpt-4o' - }, - 'temperature': { - id: 'temperature', - type: 'slider', - value: 0.7 - }, - 'maxTokens': { - id: 'maxTokens', - type: 'slider', - value: 1000 - } - }, - outputs: { - response: 'string' - } - } - } - - const serialized = serializer.serializeWorkflow(blocks, []) - const block = serialized.blocks[0] - - expect(block.config.tool).toBe('openai.chat') - expect(block.config.params).toEqual({ - model: 'gpt-4o', - temperature: 0.7, - maxTokens: 1000 - }) - expect(block.config.interface.outputs).toEqual({ - response: 'string' - }) - }) - - it('should serialize a workflow with correct output types', () => { - // Mock block config - ;(getBlock as jest.Mock).mockReturnValue({ - tools: { - access: ['test-tool'], - config: null - }, - workflow: { - inputs: { - input: { type: 'string', required: true } - }, - outputs: { - response: { - type: 'string', - dependsOn: { - subBlockId: 'responseFormat', - condition: { - whenEmpty: 'string', - whenFilled: 'json' - } - } - } - }, - subBlocks: [ - { - id: 'input', - type: 'short-input' - }, - { - id: 'responseFormat', - type: 'code' - } - ] - }, - toolbar: { - title: 'Test Block', - description: 'A test block', - category: 'test', - bgColor: '#000000' - } - }) - - const blocks: Record = { - 'block-1': { - id: 'block-1', - type: 'agent', - name: 'Agent 1', - position: { x: 0, y: 0 }, - subBlocks: { - input: { - id: 'input', - type: 'short-input', - value: 'test input' - }, - responseFormat: { - id: 'responseFormat', - type: 'code', - value: null - } - }, - outputs: { - response: 'string' - } - } - } - - const edges: Edge[] = [] - - const serialized = serializer.serializeWorkflow(blocks, edges) - - expect(serialized.blocks[0].config.interface.outputs).toEqual({ - response: 'string' - }) - }) - - it('should handle dynamic output types based on subBlock values', () => { - // Mock block config with dynamic output type - ;(getBlock as jest.Mock).mockReturnValue({ - tools: { - access: ['test-tool'], - config: null - }, - workflow: { - inputs: { - input: { type: 'string', required: true } - }, - outputs: { - response: { - type: 'string', - dependsOn: { - subBlockId: 'responseFormat', - condition: { - whenEmpty: 'string', - whenFilled: 'json' - } - } - } - }, - subBlocks: [ - { - id: 'input', - type: 'short-input' - }, - { - id: 'responseFormat', - type: 'code' - } - ] - }, - toolbar: { - title: 'Test Block', - description: 'A test block', - category: 'test', - bgColor: '#000000' - } - }) - - const blocks: Record = { - 'block-1': { - id: 'block-1', - type: 'agent', - name: 'Agent 1', - position: { x: 0, y: 0 }, - subBlocks: { - input: { - id: 'input', - type: 'short-input', - value: 'test input' - }, - responseFormat: { - id: 'responseFormat', - type: 'code', - value: '{ "format": "json" }' // Non-empty responseFormat - } - }, - outputs: { - response: 'json' as OutputType // Should be json when responseFormat is filled - } - } - } - - const edges: Edge[] = [] - - const serialized = serializer.serializeWorkflow(blocks, edges) - - expect(serialized.blocks[0].config.interface.outputs).toEqual({ - response: 'json' as OutputType - }) - }) - - it('should preserve connection handles during serialization', () => { - // Mock block config - ;(getBlock as jest.Mock).mockReturnValue({ - tools: { - access: ['test-tool'], - config: null - }, - workflow: { - inputs: {}, - outputs: { response: 'string' as OutputType }, - subBlocks: [] - }, - toolbar: { - title: 'Test Block', - description: 'A test block', - category: 'test', - bgColor: '#000000' - } - }) - - const blocks: Record = { - 'block-1': { - id: 'block-1', - type: 'agent', - name: 'Agent 1', - position: { x: 0, y: 0 }, - subBlocks: {}, - outputs: { response: 'string' } - }, - 'block-2': { - id: 'block-2', - type: 'api', - name: 'API 1', - position: { x: 200, y: 0 }, - subBlocks: {}, - outputs: { response: 'json' } - } - } - - const edges: Edge[] = [ - { - id: 'edge-1', - source: 'block-1', - target: 'block-2', - sourceHandle: 'response', - targetHandle: 'input' - } - ] - - const serialized = serializer.serializeWorkflow(blocks, edges) - - expect(serialized.connections[0]).toEqual({ - source: 'block-1', - target: 'block-2', - sourceHandle: 'response', - targetHandle: 'input' - }) - }) }) describe('deserializeWorkflow', () => { @@ -592,20 +351,23 @@ describe('Serializer', () => { tool: 'openai.chat', params: { model: 'gpt-4o', - systemPrompt: 'You are helpful', - responseFormat: null - }, - interface: { - inputs: { - systemPrompt: 'string', - context: 'string', - apiKey: 'string' - }, - outputs: { - response: 'string' - } + systemPrompt: 'You are helpful' } }, + inputs: { + systemPrompt: 'string', + context: 'string', + apiKey: 'string' + }, + outputs: { + response: { + response: { + text: 'string', + model: 'string', + tokens: 'number' + } + } satisfies BlockOutput + }, metadata: { title: 'Agent Block', description: 'Use any LLM', @@ -615,70 +377,24 @@ describe('Serializer', () => { } ], connections: [] - } - - const { blocks } = serializer.deserializeWorkflow(workflow) - const block = blocks['agent-1'] - - expect(block.type).toBe('agent') - expect(block.subBlocks.model.value).toBe('gpt-4o') - expect(block.subBlocks.systemPrompt.value).toBe('You are helpful') - expect(block.subBlocks.responseFormat.value).toBe(null) - expect(block.outputs).toEqual({ - response: 'string' - }) - }) - - it('should deserialize a workflow with correct output types', () => { - // Mock block config - ;(getBlock as jest.Mock).mockReturnValue({ - tools: { - access: ['test-tool'], - config: null - }, - workflow: { - inputs: {}, - outputs: { response: 'string' as OutputType }, - subBlocks: [] - }, - toolbar: { - title: 'Test Block', - description: 'A test block', - category: 'test', - bgColor: '#000000' - } - }) - - const serializedWorkflow: SerializedWorkflow = { - version: '1.0', - blocks: [ - { - id: 'block-1', - position: { x: 0, y: 0 }, - config: { - tool: 'test-tool', - params: {}, - interface: { - inputs: {}, - outputs: { response: 'string' as OutputType } - } - }, - metadata: { - title: 'Test Block', - description: 'A test block', - category: 'test', - color: '#000000' - } - } - ], - connections: [] } - const { blocks } = serializer.deserializeWorkflow(serializedWorkflow) + const { blocks } = serializer.deserializeWorkflow(workflow) + const block = blocks['agent-1'] - expect(blocks['block-1'].outputs).toEqual({ - response: 'string' + expect(block.type).toBe('agent') + expect(block.enabled).toBe(true) + expect(block.subBlocks.model.value).toBe('gpt-4o') + expect(block.subBlocks.systemPrompt.value).toBe('You are helpful') + expect(block.outputs).toEqual({ + response: { + response: { + text: 'string', + model: 'string', + tokens: 'number' + } + } satisfies BlockOutput }) - }) + }) }) }) \ No newline at end of file diff --git a/serializer/index.ts b/serializer/index.ts index c666973229..92ea54bac5 100644 --- a/serializer/index.ts +++ b/serializer/index.ts @@ -1,8 +1,7 @@ import { BlockState, SubBlockState } from '@/stores/workflow/types' import { Edge } from 'reactflow' -import { SerializedBlock, SerializedConnection, SerializedWorkflow, BlockConfig, ParamType, OutputType } from './types' +import { SerializedBlock, SerializedConnection, SerializedWorkflow } from './types' import { getBlock, getBlockTypeForTool } from '@/blocks' -import { resolveOutputType } from '@/blocks/utils' export class Serializer { serializeWorkflow(blocks: Record, edges: Edge[]): SerializedWorkflow { @@ -32,30 +31,23 @@ export class Serializer { // Extract params from subBlocks const params = this.extractParams(block) - // Get input interface from block config - const inputs: Record = {} - - // Map inputs from block config + // Get inputs from block config + const inputs: Record = {} if (blockConfig.workflow.inputs) { Object.entries(blockConfig.workflow.inputs).forEach(([key, config]) => { - inputs[key] = config.type as ParamType + inputs[key] = config.type }) } - // Use the block's actual output types - const outputs = block.outputs - return { id: block.id, position: block.position, config: { tool: toolId, - params, - interface: { - inputs, - outputs - } + params }, + inputs, + outputs: block.outputs, metadata: { title: block.name, description: blockConfig.toolbar.description, @@ -117,15 +109,14 @@ export class Serializer { } }) - const outputs = resolveOutputType(blockConfig.workflow.outputs, subBlocks) - return { id: serializedBlock.id, type: blockType, name: serializedBlock.metadata?.title || blockConfig.toolbar.title, position: serializedBlock.position, subBlocks, - outputs + outputs: serializedBlock.outputs, + enabled: true } } } \ No newline at end of file diff --git a/serializer/types.ts b/serializer/types.ts index c177612311..efd2fd66bd 100644 --- a/serializer/types.ts +++ b/serializer/types.ts @@ -1,5 +1,5 @@ -export type ParamType = 'string' | 'number' | 'boolean' | 'json' -export type OutputType = 'string' | 'number' | 'json' | 'boolean' | 'any' +import { Position } from '@/stores/workflow/types' +import { BlockOutput, ParamType } from '@/blocks/types' export interface SerializedWorkflow { version: string @@ -14,24 +14,15 @@ export interface SerializedConnection { targetHandle?: string } -export interface Position { - x: number - y: number -} - -export interface BlockConfig { - tool: string - params: Record - interface: { - inputs: Record - outputs: Record - } -} - export interface SerializedBlock { id: string position: Position - config: BlockConfig + config: { + tool: string + params: Record + } + inputs: Record + outputs: Record metadata?: { title?: string description?: string diff --git a/stores/workflow/types.ts b/stores/workflow/types.ts index 1086ba54af..c0a96465f5 100644 --- a/stores/workflow/types.ts +++ b/stores/workflow/types.ts @@ -1,5 +1,5 @@ import { Node, Edge } from 'reactflow' -import { OutputType, SubBlockType } from '@/blocks/types' +import { BlockOutput, SubBlockType } from '@/blocks/types' import { WorkflowHistory } from './history-types' export interface Position { @@ -13,7 +13,7 @@ export interface BlockState { name: string position: Position subBlocks: Record - outputs: Record + outputs: Record enabled: boolean horizontalHandles?: boolean } diff --git a/tools/anthropic/chat.ts b/tools/anthropic/chat.ts index 0e1fd5f9ac..71c237307e 100644 --- a/tools/anthropic/chat.ts +++ b/tools/anthropic/chat.ts @@ -12,8 +12,11 @@ interface ChatParams { } interface ChatResponse extends ToolResponse { - tokens?: number - model: string + output: { + content: string + model: string + tokens?: number + } } export const chatTool: ToolConfig = { @@ -81,9 +84,12 @@ export const chatTool: ToolConfig = { transformResponse: async (response: Response) => { const data = await response.json() return { - output: data.completion, - tokens: data.usage?.total_tokens, - model: data.model + success: true, + output: { + content: data.completion, + model: data.model, + tokens: data.usage?.total_tokens + } } }, diff --git a/tools/crewai/vision.ts b/tools/crewai/vision.ts index c6eec60ca4..6a55e931f8 100644 --- a/tools/crewai/vision.ts +++ b/tools/crewai/vision.ts @@ -8,9 +8,11 @@ interface VisionParams { } interface VisionResponse extends ToolResponse { - response: string - tokens?: number - model?: string + output: { + content: string + model?: string + tokens?: number + } } export const visionTool: ToolConfig = { @@ -115,12 +117,14 @@ export const visionTool: ToolConfig = { } return { - output: result, - response: result, - model: data.model, - tokens: data.content - ? (data.usage?.input_tokens + data.usage?.output_tokens) - : data.usage?.total_tokens + success: true, + output: { + content: result, + model: data.model, + tokens: data.content + ? (data.usage?.input_tokens + data.usage?.output_tokens) + : data.usage?.total_tokens + } } }, diff --git a/tools/deepseek/chat.ts b/tools/deepseek/chat.ts index 91a94f473c..993129aa97 100644 --- a/tools/deepseek/chat.ts +++ b/tools/deepseek/chat.ts @@ -15,8 +15,11 @@ interface ChatParams { } interface ChatResponse extends ToolResponse { - tokens?: number - model: string + output: { + content: string + model: string + tokens?: number + } } export const chatTool: ToolConfig = { @@ -105,9 +108,12 @@ export const chatTool: ToolConfig = { const data = await response.json() return { - output: data.choices[0].message.content, - tokens: data.usage?.total_tokens, - model: data.model + success: true, + output: { + content: data.choices[0].message.content, + model: data.model, + tokens: data.usage?.total_tokens + } } }, diff --git a/tools/deepseek/reasoner.ts b/tools/deepseek/reasoner.ts index 9270ae79a9..fc00fe1e5b 100644 --- a/tools/deepseek/reasoner.ts +++ b/tools/deepseek/reasoner.ts @@ -14,9 +14,11 @@ interface ChatParams { } interface ChatResponse extends ToolResponse { - tokens?: number - model: string - reasoning_content?: string + output: { + content: string + model: string + tokens?: number + } } export const reasonerTool: ToolConfig = { @@ -99,10 +101,12 @@ export const reasonerTool: ToolConfig = { const data = await response.json() return { - output: data.choices[0].message.content, - tokens: data.usage?.total_tokens, - model: data.model, - reasoning_content: data.choices[0].message.reasoning_content + success: true, + output: { + content: data.choices[0].message.content, + model: data.model, + tokens: data.usage?.total_tokens + } } }, diff --git a/tools/firecrawl/scrape.ts b/tools/firecrawl/scrape.ts index ceec662996..06114251ca 100644 --- a/tools/firecrawl/scrape.ts +++ b/tools/firecrawl/scrape.ts @@ -10,8 +10,7 @@ interface ScrapeParams { } interface ScrapeResponse extends ToolResponse { - success: boolean - data: { + output: { markdown: string html?: string metadata: { @@ -77,9 +76,12 @@ export const scrapeTool: ToolConfig = { } return { - success: data.success, - data: data.data, - output: data.data.markdown + success: true, + output: { + markdown: data.data.markdown, + html: data.data.html, + metadata: data.data.metadata + } } }, diff --git a/tools/function/execute.ts b/tools/function/execute.ts index 37054f92c9..e4439e9376 100644 --- a/tools/function/execute.ts +++ b/tools/function/execute.ts @@ -6,7 +6,10 @@ interface CodeExecutionInput { } interface CodeExecutionOutput extends ToolResponse { - output: Record + output: { + result: any + stdout: string + } } export const functionExecuteTool: ToolConfig = { @@ -68,13 +71,21 @@ export const functionExecuteTool: ToolConfig = { @@ -88,10 +91,13 @@ export const chatTool: ToolConfig = { transformResponse: async (response: Response) => { const data = await response.json() return { - output: data.candidates[0].content.parts[0].text, - tokens: data.usage?.totalTokens, - model: data.model, - safetyRatings: data.candidates[0].safetyRatings + success: true, + output: { + content: data.candidates[0].content.parts[0].text, + model: data.model, + tokens: data.usage?.totalTokens, + safetyRatings: data.candidates[0].safetyRatings + } } }, diff --git a/tools/http/request.ts b/tools/http/request.ts index 07d33491a6..d01669d715 100644 --- a/tools/http/request.ts +++ b/tools/http/request.ts @@ -13,8 +13,11 @@ interface RequestParams { } interface RequestResponse extends ToolResponse { - status: number - headers: Record + output: { + data: any + status: number + headers: Record + } } export const requestTool: ToolConfig = { @@ -132,9 +135,12 @@ export const requestTool: ToolConfig = { : response.text()) return { - output: data, - status: response.status, - headers + success: response.ok, + output: { + data, + status: response.status, + headers + } } }, diff --git a/tools/hubspot/contacts.ts b/tools/hubspot/contacts.ts index 6897c91032..34d7e64abe 100644 --- a/tools/hubspot/contacts.ts +++ b/tools/hubspot/contacts.ts @@ -16,11 +16,14 @@ interface ContactsParams { } interface ContactsResponse extends ToolResponse { - totalResults?: number - pagination?: { - hasMore: boolean - offset: number - } + output: { + contacts: any[] + totalResults?: number + pagination?: { + hasMore: boolean + offset: number + } + } } export const contactsTool: ToolConfig = { @@ -115,9 +118,12 @@ export const contactsTool: ToolConfig = { transformResponse: async (response: Response) => { const data = await response.json() return { - output: data.results || data, - totalResults: data.total, - pagination: data.paging + success: true, + output: { + contacts: data.results || [data], + totalResults: data.total, + pagination: data.paging + } } }, diff --git a/tools/index.ts b/tools/index.ts index d31fd7ee56..af4eb8e1bf 100644 --- a/tools/index.ts +++ b/tools/index.ts @@ -1,4 +1,4 @@ -import { ToolConfig } from './types' +import { ToolConfig, ToolResponse } from './types' import { chatTool as openAIChat } from './openai/chat' import { chatTool as anthropicChat } from './anthropic/chat' import { chatTool as googleChat } from './google/chat' @@ -42,11 +42,15 @@ export function getTool(toolId: string): ToolConfig | undefined { export async function executeTool( toolId: string, params: Record -): Promise { +): Promise { const tool = getTool(toolId) if (!tool) { - throw new Error(`Tool not found: ${toolId}`) + return { + success: false, + output: {}, + error: `Tool not found: ${toolId}` + } } try { @@ -62,13 +66,15 @@ export async function executeTool( body: tool.request.body ? JSON.stringify(tool.request.body(params)) : undefined }) - if (!response.ok) { - const error = await response.json() - throw new Error(tool.transformError(error)) - } + // Transform the response + const result = await tool.transformResponse(response) + return result - return tool.transformResponse(response) } catch (error) { - throw new Error(tool.transformError(error)) + return { + success: false, + output: {}, + error: tool.transformError(error) + } } } \ No newline at end of file diff --git a/tools/openai/chat.ts b/tools/openai/chat.ts index ac5a08b1f6..d4d941c7b7 100644 --- a/tools/openai/chat.ts +++ b/tools/openai/chat.ts @@ -15,9 +15,12 @@ interface ChatParams { } interface ChatResponse extends ToolResponse { - tokens?: number - model: string - reasoning_tokens?: number + output: { + content: string + model: string + tokens?: number + reasoning_tokens?: number + } } export const chatTool: ToolConfig = { @@ -104,15 +107,21 @@ export const chatTool: ToolConfig = { const data = await response.json() if (data.choices?.[0]?.delta?.content) { return { - output: data.choices[0].delta.content, - model: data.model + success: true, + output: { + content: data.choices[0].delta.content, + model: data.model + } } } return { - output: data.choices[0].message.content, - tokens: data.usage?.total_tokens, - model: data.model, - reasoning_tokens: data.usage?.completion_tokens_details?.reasoning_tokens + success: true, + output: { + content: data.choices[0].message.content, + model: data.model, + tokens: data.usage?.total_tokens, + reasoning_tokens: data.usage?.completion_tokens_details?.reasoning_tokens + } } }, diff --git a/tools/salesforce/opportunities.ts b/tools/salesforce/opportunities.ts index c43cab0335..12c29f5b34 100644 --- a/tools/salesforce/opportunities.ts +++ b/tools/salesforce/opportunities.ts @@ -17,11 +17,14 @@ interface OpportunityParams { } interface OpportunityResponse extends ToolResponse { - totalResults?: number - pagination?: { - hasMore: boolean - offset: number - } + output: { + records: any[] + totalResults?: number + pagination?: { + hasMore: boolean + offset: number + } + } } export const opportunitiesTool: ToolConfig = { @@ -119,11 +122,14 @@ export const opportunitiesTool: ToolConfig { const data = await response.json() return { - output: data.records || data, - totalResults: data.totalSize, - pagination: { - hasMore: !data.done, - offset: data.nextRecordsUrl ? parseInt(data.nextRecordsUrl.split('-')[1]) : 0 + success: true, + output: { + records: data.records || [data], + totalResults: data.totalSize, + pagination: { + hasMore: !data.done, + offset: data.nextRecordsUrl ? parseInt(data.nextRecordsUrl.split('-')[1]) : 0 + } } } }, diff --git a/tools/types.ts b/tools/types.ts index a0ec2d3b2e..4ae8e55daa 100644 --- a/tools/types.ts +++ b/tools/types.ts @@ -1,8 +1,9 @@ export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' export interface ToolResponse { - output: any // All tools must provide an output field - [key: string]: any // Tools can include additional metadata + success: boolean // Whether the tool execution was successful + output: Record // The structured output from the tool + error?: string // Error message if success is false } export interface ToolConfig

{ diff --git a/tools/xai/chat.ts b/tools/xai/chat.ts index de8da0d2ff..69a3f17a4a 100644 --- a/tools/xai/chat.ts +++ b/tools/xai/chat.ts @@ -13,9 +13,12 @@ interface ChatParams { } interface ChatResponse extends ToolResponse { - tokens?: number - model: string - reasoning?: string + output: { + content: string + model: string + tokens?: number + reasoning?: string + } } export const chatTool: ToolConfig = { @@ -83,10 +86,13 @@ export const chatTool: ToolConfig = { transformResponse: async (response: Response) => { const data = await response.json() return { - output: data.choices[0].message.content, - tokens: data.usage?.total_tokens, - model: data.model, - reasoning: data.choices[0]?.reasoning + success: true, + output: { + content: data.choices[0].message.content, + model: data.model, + tokens: data.usage?.total_tokens, + reasoning: data.choices[0]?.reasoning + } } },