mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
Modified tools directory to be more generic, updated serializer and executor accordingly
This commit is contained in:
+159
-111
@@ -1,38 +1,60 @@
|
||||
import { Executor } from '../index';
|
||||
import { SerializedWorkflow } from '@/serializer/types';
|
||||
import { Tool } from '../types';
|
||||
import { toolRegistry } from '@/tools/registry';
|
||||
import { tools } from '@/tools/registry';
|
||||
|
||||
// Mock tools
|
||||
class MockTool implements Tool {
|
||||
constructor(
|
||||
public name: string,
|
||||
private mockExecute: (params: Record<string, any>) => Promise<Record<string, any>>,
|
||||
private mockValidate: (params: Record<string, any>) => boolean | string = () => true
|
||||
) {}
|
||||
|
||||
async execute(params: Record<string, any>): Promise<Record<string, any>> {
|
||||
return this.mockExecute(params);
|
||||
}
|
||||
|
||||
validateParams(params: Record<string, any>): boolean | string {
|
||||
return this.mockValidate(params);
|
||||
}
|
||||
}
|
||||
const createMockTool = (
|
||||
id: string,
|
||||
name: string,
|
||||
mockResponse: any,
|
||||
mockError?: string
|
||||
): Tool => ({
|
||||
id,
|
||||
name,
|
||||
description: 'Mock tool for testing',
|
||||
version: '1.0.0',
|
||||
params: {
|
||||
input: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'Input to process'
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
description: 'API key for authentication'
|
||||
}
|
||||
},
|
||||
request: {
|
||||
url: 'https://api.test.com/endpoint',
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': params.apiKey || 'test-key'
|
||||
}),
|
||||
body: (params) => ({
|
||||
input: params.input
|
||||
})
|
||||
},
|
||||
transformResponse: () => mockResponse,
|
||||
transformError: () => mockError || 'Mock error'
|
||||
});
|
||||
|
||||
describe('Executor', () => {
|
||||
beforeEach(() => {
|
||||
// Reset toolRegistry mock
|
||||
(toolRegistry as any) = {};
|
||||
// Reset tools mock
|
||||
(tools as any) = {};
|
||||
});
|
||||
|
||||
describe('Tool Execution', () => {
|
||||
it('should execute a simple workflow with one tool', async () => {
|
||||
const mockTool = new MockTool(
|
||||
const mockTool = createMockTool(
|
||||
'test-tool',
|
||||
async (params) => ({ result: params.input + ' processed' })
|
||||
'Test Tool',
|
||||
{ result: 'test processed' }
|
||||
);
|
||||
(toolRegistry as any)['test-tool'] = mockTool;
|
||||
(tools as any)['test-tool'] = mockTool;
|
||||
|
||||
const workflow: SerializedWorkflow = {
|
||||
version: '1.0',
|
||||
@@ -41,7 +63,7 @@ describe('Executor', () => {
|
||||
position: { x: 0, y: 0 },
|
||||
config: {
|
||||
tool: 'test-tool',
|
||||
params: {},
|
||||
params: { input: 'test' },
|
||||
interface: {
|
||||
inputs: { input: 'string' },
|
||||
outputs: { result: 'string' }
|
||||
@@ -51,20 +73,39 @@ describe('Executor', () => {
|
||||
connections: []
|
||||
};
|
||||
|
||||
// Mock fetch
|
||||
global.fetch = jest.fn().mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ result: 'test processed' })
|
||||
})
|
||||
);
|
||||
|
||||
const executor = new Executor(workflow);
|
||||
const result = await executor.execute('workflow-1', { input: 'test' });
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data).toEqual({ result: 'test processed' });
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://api.test.com/endpoint',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'test-key'
|
||||
},
|
||||
body: JSON.stringify({ input: 'test' })
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should validate tool parameters', async () => {
|
||||
const mockTool = new MockTool(
|
||||
it('should validate required parameters', async () => {
|
||||
const mockTool = createMockTool(
|
||||
'test-tool',
|
||||
async () => ({}),
|
||||
(params) => params.required ? true : 'Missing required parameter'
|
||||
'Test Tool',
|
||||
{ result: 'test processed' }
|
||||
);
|
||||
(toolRegistry as any)['test-tool'] = mockTool;
|
||||
(tools as any)['test-tool'] = mockTool;
|
||||
|
||||
const workflow: SerializedWorkflow = {
|
||||
version: '1.0',
|
||||
@@ -73,10 +114,10 @@ describe('Executor', () => {
|
||||
position: { x: 0, y: 0 },
|
||||
config: {
|
||||
tool: 'test-tool',
|
||||
params: {},
|
||||
params: {}, // Missing required 'input' parameter
|
||||
interface: {
|
||||
inputs: {},
|
||||
outputs: {}
|
||||
outputs: { result: 'string' }
|
||||
}
|
||||
}
|
||||
}],
|
||||
@@ -89,15 +130,15 @@ describe('Executor', () => {
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain('Missing required parameter');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Interface Validation', () => {
|
||||
it('should validate input types', async () => {
|
||||
const mockTool = new MockTool(
|
||||
it('should handle tool execution errors', async () => {
|
||||
const mockTool = createMockTool(
|
||||
'test-tool',
|
||||
async (params) => ({ result: params.input })
|
||||
'Test Tool',
|
||||
{},
|
||||
'API Error'
|
||||
);
|
||||
(toolRegistry as any)['test-tool'] = mockTool;
|
||||
(tools as any)['test-tool'] = mockTool;
|
||||
|
||||
const workflow: SerializedWorkflow = {
|
||||
version: '1.0',
|
||||
@@ -106,9 +147,51 @@ describe('Executor', () => {
|
||||
position: { x: 0, y: 0 },
|
||||
config: {
|
||||
tool: 'test-tool',
|
||||
params: {},
|
||||
params: { input: 'test' },
|
||||
interface: {
|
||||
inputs: { input: 'number' },
|
||||
inputs: { input: 'string' },
|
||||
outputs: { result: 'string' }
|
||||
}
|
||||
}
|
||||
}],
|
||||
connections: []
|
||||
};
|
||||
|
||||
// Mock fetch to fail
|
||||
global.fetch = jest.fn().mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
ok: false,
|
||||
json: () => Promise.resolve({ error: 'API Error' })
|
||||
})
|
||||
);
|
||||
|
||||
const executor = new Executor(workflow);
|
||||
const result = await executor.execute('workflow-1', { input: 'test' });
|
||||
|
||||
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 }
|
||||
);
|
||||
(tools as any)['test-tool'] = mockTool;
|
||||
|
||||
const workflow: SerializedWorkflow = {
|
||||
version: '1.0',
|
||||
blocks: [{
|
||||
id: 'block-1',
|
||||
position: { x: 0, y: 0 },
|
||||
config: {
|
||||
tool: 'test-tool',
|
||||
params: { input: 42 }, // Wrong type for input
|
||||
interface: {
|
||||
inputs: { input: 'string' },
|
||||
outputs: { result: 'number' }
|
||||
}
|
||||
}
|
||||
@@ -117,18 +200,19 @@ describe('Executor', () => {
|
||||
};
|
||||
|
||||
const executor = new Executor(workflow);
|
||||
const result = await executor.execute('workflow-1', { input: 'not a number' });
|
||||
const result = await executor.execute('workflow-1', { input: 42 });
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain('Invalid type for input');
|
||||
});
|
||||
|
||||
it('should validate tool output against interface', async () => {
|
||||
const mockTool = new MockTool(
|
||||
const mockTool = createMockTool(
|
||||
'test-tool',
|
||||
async () => ({ wrongField: 'wrong type' })
|
||||
'Test Tool',
|
||||
{ wrongField: 'wrong type' }
|
||||
);
|
||||
(toolRegistry as any)['test-tool'] = mockTool;
|
||||
(tools as any)['test-tool'] = mockTool;
|
||||
|
||||
const workflow: SerializedWorkflow = {
|
||||
version: '1.0',
|
||||
@@ -137,9 +221,9 @@ describe('Executor', () => {
|
||||
position: { x: 0, y: 0 },
|
||||
config: {
|
||||
tool: 'test-tool',
|
||||
params: {},
|
||||
params: { input: 'test' },
|
||||
interface: {
|
||||
inputs: {},
|
||||
inputs: { input: 'string' },
|
||||
outputs: { result: 'string' }
|
||||
}
|
||||
}
|
||||
@@ -147,8 +231,16 @@ describe('Executor', () => {
|
||||
connections: []
|
||||
};
|
||||
|
||||
// Mock fetch
|
||||
global.fetch = jest.fn().mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ wrongField: 'wrong type' })
|
||||
})
|
||||
);
|
||||
|
||||
const executor = new Executor(workflow);
|
||||
const result = await executor.execute('workflow-1', {});
|
||||
const result = await executor.execute('workflow-1', { input: 'test' });
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain('Tool output missing required field');
|
||||
@@ -157,16 +249,18 @@ describe('Executor', () => {
|
||||
|
||||
describe('Complex Workflows', () => {
|
||||
it('should execute a workflow with multiple connected blocks', async () => {
|
||||
const processorTool = new MockTool(
|
||||
const processorTool = createMockTool(
|
||||
'processor',
|
||||
async (params) => ({ processed: params.input.toUpperCase() })
|
||||
'Processor Tool',
|
||||
{ processed: 'TEST' }
|
||||
);
|
||||
const formatterTool = new MockTool(
|
||||
const formatterTool = createMockTool(
|
||||
'formatter',
|
||||
async (params) => ({ result: `<${params.processed}>` })
|
||||
'Formatter Tool',
|
||||
{ result: '<TEST>' }
|
||||
);
|
||||
(toolRegistry as any)['processor'] = processorTool;
|
||||
(toolRegistry as any)['formatter'] = formatterTool;
|
||||
(tools as any)['processor'] = processorTool;
|
||||
(tools as any)['formatter'] = formatterTool;
|
||||
|
||||
const workflow: SerializedWorkflow = {
|
||||
version: '1.0',
|
||||
@@ -176,7 +270,7 @@ describe('Executor', () => {
|
||||
position: { x: 0, y: 0 },
|
||||
config: {
|
||||
tool: 'processor',
|
||||
params: {},
|
||||
params: { input: 'test' },
|
||||
interface: {
|
||||
inputs: { input: 'string' },
|
||||
outputs: { processed: 'string' }
|
||||
@@ -190,7 +284,7 @@ describe('Executor', () => {
|
||||
tool: 'formatter',
|
||||
params: {},
|
||||
interface: {
|
||||
inputs: { processed: 'string' },
|
||||
inputs: { input: 'string' },
|
||||
outputs: { result: 'string' }
|
||||
}
|
||||
}
|
||||
@@ -200,73 +294,27 @@ describe('Executor', () => {
|
||||
source: 'process',
|
||||
target: 'format',
|
||||
sourceHandle: 'processed',
|
||||
targetHandle: 'processed'
|
||||
targetHandle: 'input'
|
||||
}]
|
||||
};
|
||||
|
||||
// Mock fetch for both tools
|
||||
global.fetch = jest.fn()
|
||||
.mockImplementationOnce(() => Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ processed: 'TEST' })
|
||||
}))
|
||||
.mockImplementationOnce(() => Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ result: '<TEST>' })
|
||||
}));
|
||||
|
||||
const executor = new Executor(workflow);
|
||||
const result = await executor.execute('workflow-1', { input: 'test' });
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data).toEqual({ result: '<TEST>' });
|
||||
});
|
||||
|
||||
it('should handle circular dependencies', async () => {
|
||||
const mockTool = new MockTool(
|
||||
'test-tool',
|
||||
async () => ({ output: 'test' })
|
||||
);
|
||||
(toolRegistry as any)['test-tool'] = mockTool;
|
||||
|
||||
const workflow: SerializedWorkflow = {
|
||||
version: '1.0',
|
||||
blocks: [
|
||||
{
|
||||
id: 'block-1',
|
||||
position: { x: 0, y: 0 },
|
||||
config: {
|
||||
tool: 'test-tool',
|
||||
params: {},
|
||||
interface: {
|
||||
inputs: { input: 'string' },
|
||||
outputs: { output: 'string' }
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'block-2',
|
||||
position: { x: 100, y: 0 },
|
||||
config: {
|
||||
tool: 'test-tool',
|
||||
params: {},
|
||||
interface: {
|
||||
inputs: { input: 'string' },
|
||||
outputs: { output: 'string' }
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
connections: [
|
||||
{
|
||||
source: 'block-1',
|
||||
target: 'block-2',
|
||||
sourceHandle: 'output',
|
||||
targetHandle: 'input'
|
||||
},
|
||||
{
|
||||
source: 'block-2',
|
||||
target: 'block-1',
|
||||
sourceHandle: 'output',
|
||||
targetHandle: 'input'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const executor = new Executor(workflow);
|
||||
const result = await executor.execute('workflow-1', {});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain('Workflow contains cycles');
|
||||
expect(global.fetch).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+42
-17
@@ -1,6 +1,6 @@
|
||||
import { SerializedWorkflow, SerializedBlock } from '@/serializer/types';
|
||||
import { SerializedWorkflow, SerializedBlock, BlockConfig } from '@/serializer/types';
|
||||
import { ExecutionContext, ExecutionResult, Tool } from './types';
|
||||
import { toolRegistry } from '@/tools/registry';
|
||||
import { tools } from '@/tools/registry';
|
||||
|
||||
export class Executor {
|
||||
private workflow: SerializedWorkflow;
|
||||
@@ -14,39 +14,64 @@ export class Executor {
|
||||
inputs: Record<string, any>,
|
||||
context: ExecutionContext
|
||||
): Promise<Record<string, any>> {
|
||||
// Get the tool specified by the block's tool property
|
||||
const toolName = block.config.tool;
|
||||
if (!toolName) {
|
||||
const config = block.config as BlockConfig;
|
||||
const toolId = config.tool;
|
||||
|
||||
if (!toolId) {
|
||||
throw new Error(`Block ${block.id} does not specify a tool`);
|
||||
}
|
||||
|
||||
const tool = toolRegistry[toolName];
|
||||
const tool = tools[toolId];
|
||||
if (!tool) {
|
||||
throw new Error(`Tool not found: ${toolName}`);
|
||||
throw new Error(`Tool not found: ${toolId}`);
|
||||
}
|
||||
|
||||
// Validate interface compatibility
|
||||
this.validateInterface(block, inputs);
|
||||
|
||||
// Merge tool parameters with runtime inputs
|
||||
// Merge block parameters with runtime inputs
|
||||
const params = {
|
||||
...block.config.params,
|
||||
...config.params,
|
||||
...inputs
|
||||
};
|
||||
|
||||
// Validate the parameters against tool requirements
|
||||
const validationResult = tool.validateParams(params);
|
||||
if (typeof validationResult === 'string') {
|
||||
throw new Error(`Invalid parameters for tool ${toolName}: ${validationResult}`);
|
||||
}
|
||||
// Validate tool parameters
|
||||
this.validateToolParams(tool, params);
|
||||
|
||||
try {
|
||||
// Execute the tool and validate its output matches the interface
|
||||
const result = await tool.execute(params);
|
||||
// Make the HTTP request
|
||||
const url = typeof tool.request.url === 'function'
|
||||
? tool.request.url(params)
|
||||
: tool.request.url;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: tool.request.method,
|
||||
headers: tool.request.headers(params),
|
||||
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));
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const result = tool.transformResponse(data);
|
||||
|
||||
// Validate the output matches the interface
|
||||
this.validateToolOutput(block, result);
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw new Error(`Tool ${toolName} execution failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
throw new Error(`Tool ${toolId} execution failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
}
|
||||
|
||||
private validateToolParams(tool: Tool, params: Record<string, any>): void {
|
||||
// Check required parameters
|
||||
for (const [paramName, paramConfig] of Object.entries(tool.params)) {
|
||||
if (paramConfig.required && !(paramName in params)) {
|
||||
throw new Error(`Missing required parameter '${paramName}' for tool ${tool.id}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+20
-3
@@ -1,7 +1,24 @@
|
||||
export interface Tool {
|
||||
export interface Tool<P = any, R = any> {
|
||||
id: string;
|
||||
name: string;
|
||||
execute(params: Record<string, any>): Promise<Record<string, any>>;
|
||||
validateParams(params: Record<string, any>): boolean | string;
|
||||
description: string;
|
||||
version: string;
|
||||
params: {
|
||||
[key: string]: {
|
||||
type: string;
|
||||
required?: boolean;
|
||||
description?: string;
|
||||
default?: any;
|
||||
};
|
||||
};
|
||||
request: {
|
||||
url: string | ((params: P) => string);
|
||||
method: string;
|
||||
headers: (params: P) => Record<string, string>;
|
||||
body?: (params: P) => Record<string, any>;
|
||||
};
|
||||
transformResponse: (response: any) => R;
|
||||
transformError: (error: any) => string;
|
||||
}
|
||||
|
||||
export interface ToolRegistry {
|
||||
|
||||
Reference in New Issue
Block a user