mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-23 14:02:24 +08:00
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.
This commit is contained in:
@@ -52,7 +52,7 @@ export function ConnectionBlocks({
|
||||
{connection.name.replace(/\s+/g, '').toLowerCase()}
|
||||
</span>
|
||||
<span className="text-muted-foreground">
|
||||
.{connection.outputType === 'any' ? 'res' : connection.outputType}
|
||||
.{connection.outputType}
|
||||
</span>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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<string, any>)
|
||||
@@ -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)
|
||||
|
||||
+11
-3
@@ -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' }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: [
|
||||
{
|
||||
|
||||
+15
-9
@@ -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',
|
||||
|
||||
@@ -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: [
|
||||
{
|
||||
|
||||
@@ -21,7 +21,12 @@ export const FunctionBlock: BlockConfig = {
|
||||
code: { type: 'string', required: true }
|
||||
},
|
||||
outputs: {
|
||||
result: 'any'
|
||||
response: {
|
||||
type: {
|
||||
value: 'any',
|
||||
stdout: 'string'
|
||||
}
|
||||
}
|
||||
},
|
||||
subBlocks: [
|
||||
{
|
||||
|
||||
+13
-6
@@ -3,19 +3,26 @@ import type { JSX } from 'react'
|
||||
|
||||
export type BlockIcon = (props: SVGProps<SVGSVGElement>) => 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<string, PrimitiveValueType>
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+9
-11
@@ -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<string, OutputConfig>,
|
||||
subBlocks: Record<string, SubBlockState>
|
||||
): Record<string, OutputType> {
|
||||
const resolvedOutputs: Record<string, OutputType> = {}
|
||||
): Record<string, BlockOutput> {
|
||||
const resolvedOutputs: Record<string, BlockOutput> = {}
|
||||
|
||||
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
|
||||
|
||||
+270
-122
@@ -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: '<block1.string>'
|
||||
},
|
||||
interface: {
|
||||
inputs: { input: 'string' },
|
||||
outputs: { response: 'string' }
|
||||
input: '<block1.output.response.text>'
|
||||
}
|
||||
},
|
||||
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: '<agent1.string>'
|
||||
},
|
||||
interface: {
|
||||
inputs: {
|
||||
code: 'string',
|
||||
url: 'string'
|
||||
},
|
||||
outputs: {
|
||||
response: 'any'
|
||||
}
|
||||
url: '<agent1.output.response.text>'
|
||||
}
|
||||
},
|
||||
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: '<agent1.string>',
|
||||
method: '<function1.res>'
|
||||
},
|
||||
interface: {
|
||||
inputs: {
|
||||
url: 'string',
|
||||
method: 'string'
|
||||
},
|
||||
outputs: {
|
||||
response: 'any'
|
||||
}
|
||||
url: '<agent1.output.response.text>',
|
||||
method: '<function1.output.response.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: "<agent1.output.response.text>" // 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("<agent1.output.response.text>"); // Should be resolved value from first call
|
||||
expect(fetchCalls[2][1].method).toBe("<function1.output.response.method>"); // Should be resolved value from second call
|
||||
});
|
||||
});
|
||||
})
|
||||
|
||||
+98
-42
@@ -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<string, any> = {}
|
||||
private initialBlockStates: Record<string, BlockOutput> = {}
|
||||
) {}
|
||||
|
||||
private async executeBlock(
|
||||
block: SerializedBlock,
|
||||
inputs: Record<string, any>,
|
||||
context: ExecutionContext
|
||||
): Promise<Record<string, any>> {
|
||||
): Promise<BlockOutput> {
|
||||
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<string, any> {
|
||||
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<string, any>)
|
||||
|
||||
return resolvedInputs
|
||||
}
|
||||
|
||||
async execute(workflowId: string): Promise<ExecutionResult> {
|
||||
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'
|
||||
}
|
||||
}
|
||||
|
||||
+10
-5
@@ -1,4 +1,6 @@
|
||||
export interface Tool<P = any, R = any> {
|
||||
import { BlockOutput } from '@/blocks/types'
|
||||
|
||||
export interface Tool<P = any, O = Record<string, any>> {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
@@ -17,7 +19,11 @@ export interface Tool<P = any, R = any> {
|
||||
headers: (params: P) => Record<string, string>
|
||||
body?: (params: P) => Record<string, any>
|
||||
}
|
||||
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<string, any>
|
||||
input?: Record<string, any>
|
||||
blockStates: Map<string, BlockOutput>
|
||||
metadata?: Record<string, any>
|
||||
}
|
||||
|
||||
export interface ExecutionResult {
|
||||
success: boolean
|
||||
data: Record<string, any>
|
||||
output: BlockOutput
|
||||
error?: string
|
||||
metadata?: {
|
||||
duration: number
|
||||
|
||||
@@ -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<string, BlockState> = {
|
||||
'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<string, BlockState> = {
|
||||
'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<string, BlockState> = {
|
||||
'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<string, BlockState> = {
|
||||
'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<string, BlockState> = {
|
||||
'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<string, BlockState> = {
|
||||
'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<string, BlockState> = {
|
||||
'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
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
+9
-18
@@ -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<string, BlockState>, 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<string, ParamType> = {}
|
||||
|
||||
// Map inputs from block config
|
||||
// Get inputs from block config
|
||||
const inputs: Record<string, any> = {}
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
+8
-17
@@ -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<string, any>
|
||||
interface: {
|
||||
inputs: Record<string, ParamType>
|
||||
outputs: Record<string, OutputType>
|
||||
}
|
||||
}
|
||||
|
||||
export interface SerializedBlock {
|
||||
id: string
|
||||
position: Position
|
||||
config: BlockConfig
|
||||
config: {
|
||||
tool: string
|
||||
params: Record<string, any>
|
||||
}
|
||||
inputs: Record<string, ParamType>
|
||||
outputs: Record<string, BlockOutput>
|
||||
metadata?: {
|
||||
title?: string
|
||||
description?: string
|
||||
|
||||
@@ -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<string, SubBlockState>
|
||||
outputs: Record<string, OutputType>
|
||||
outputs: Record<string, BlockOutput>
|
||||
enabled: boolean
|
||||
horizontalHandles?: boolean
|
||||
}
|
||||
|
||||
+11
-5
@@ -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<ChatParams, ChatResponse> = {
|
||||
@@ -81,9 +84,12 @@ export const chatTool: ToolConfig<ChatParams, ChatResponse> = {
|
||||
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
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
+13
-9
@@ -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<VisionParams, VisionResponse> = {
|
||||
@@ -115,12 +117,14 @@ export const visionTool: ToolConfig<VisionParams, VisionResponse> = {
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
+11
-5
@@ -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<ChatParams, ChatResponse> = {
|
||||
@@ -105,9 +108,12 @@ export const chatTool: ToolConfig<ChatParams, ChatResponse> = {
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -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<ChatParams, ChatResponse> = {
|
||||
@@ -99,10 +101,12 @@ export const reasonerTool: ToolConfig<ChatParams, ChatResponse> = {
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -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<ScrapeParams, ScrapeResponse> = {
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -6,7 +6,10 @@ interface CodeExecutionInput {
|
||||
}
|
||||
|
||||
interface CodeExecutionOutput extends ToolResponse {
|
||||
output: Record<string, any>
|
||||
output: {
|
||||
result: any
|
||||
stdout: string
|
||||
}
|
||||
}
|
||||
|
||||
export const functionExecuteTool: ToolConfig<CodeExecutionInput, CodeExecutionOutput> = {
|
||||
@@ -68,13 +71,21 @@ export const functionExecuteTool: ToolConfig<CodeExecutionInput, CodeExecutionOu
|
||||
try {
|
||||
// Try parsing the output as JSON
|
||||
const parsed = JSON.parse(stdout)
|
||||
return { output: parsed }
|
||||
} catch {
|
||||
// If not JSON, wrap it in a JSON object
|
||||
return {
|
||||
output: {
|
||||
result: stdout
|
||||
}
|
||||
success: true,
|
||||
output: {
|
||||
result: parsed,
|
||||
stdout
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// If not JSON, return as string
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
result: stdout,
|
||||
stdout
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
+13
-7
@@ -12,9 +12,12 @@ interface ChatParams {
|
||||
}
|
||||
|
||||
interface ChatResponse extends ToolResponse {
|
||||
tokens?: number
|
||||
model: string
|
||||
safetyRatings?: any[]
|
||||
output: {
|
||||
content: string
|
||||
model: string
|
||||
tokens?: number
|
||||
safetyRatings?: any[]
|
||||
}
|
||||
}
|
||||
|
||||
export const chatTool: ToolConfig<ChatParams, ChatResponse> = {
|
||||
@@ -88,10 +91,13 @@ export const chatTool: ToolConfig<ChatParams, ChatResponse> = {
|
||||
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
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
+11
-5
@@ -13,8 +13,11 @@ interface RequestParams {
|
||||
}
|
||||
|
||||
interface RequestResponse extends ToolResponse {
|
||||
status: number
|
||||
headers: Record<string, string>
|
||||
output: {
|
||||
data: any
|
||||
status: number
|
||||
headers: Record<string, string>
|
||||
}
|
||||
}
|
||||
|
||||
export const requestTool: ToolConfig<RequestParams, RequestResponse> = {
|
||||
@@ -132,9 +135,12 @@ export const requestTool: ToolConfig<RequestParams, RequestResponse> = {
|
||||
: response.text())
|
||||
|
||||
return {
|
||||
output: data,
|
||||
status: response.status,
|
||||
headers
|
||||
success: response.ok,
|
||||
output: {
|
||||
data,
|
||||
status: response.status,
|
||||
headers
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -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<ContactsParams, ContactsResponse> = {
|
||||
@@ -115,9 +118,12 @@ export const contactsTool: ToolConfig<ContactsParams, ContactsResponse> = {
|
||||
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
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
+15
-9
@@ -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<string, any>
|
||||
): Promise<any> {
|
||||
): Promise<ToolResponse> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+18
-9
@@ -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<ChatParams, ChatResponse> = {
|
||||
@@ -104,15 +107,21 @@ export const chatTool: ToolConfig<ChatParams, ChatResponse> = {
|
||||
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
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -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<OpportunityParams, OpportunityResponse> = {
|
||||
@@ -119,11 +122,14 @@ export const opportunitiesTool: ToolConfig<OpportunityParams, OpportunityRespons
|
||||
transformResponse: async (response: Response) => {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
+3
-2
@@ -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<string, any> // The structured output from the tool
|
||||
error?: string // Error message if success is false
|
||||
}
|
||||
|
||||
export interface ToolConfig<P = any, R extends ToolResponse = ToolResponse> {
|
||||
|
||||
+13
-7
@@ -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<ChatParams, ChatResponse> = {
|
||||
@@ -83,10 +86,13 @@ export const chatTool: ToolConfig<ChatParams, ChatResponse> = {
|
||||
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
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user