From 07cb295200215a34e428a6ce77c60411fa46fb5d Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 17 Jan 2025 19:08:40 -0800 Subject: [PATCH] Modified tools directory to be more generic, updated serializer and executor accordingly --- app/w/[id]/workflow.tsx | 59 ++-- blocks/blocks/agent.ts | 31 +- blocks/blocks/api.ts | 8 +- blocks/blocks/function.ts | 3 + blocks/types.ts | 8 +- executor/__tests__/executor.test.ts | 270 +++++++++++------- executor/index.ts | 59 ++-- executor/types.ts | 23 +- serializer/__tests__/serializer.test.ts | 222 +++----------- serializer/types.ts | 32 +-- tools/anthropic/chat.ts | 84 ++++++ tools/google/chat.ts | 86 ++++++ tools/http-service/__tests__/http.test.ts | 120 -------- tools/http-service/index.ts | 108 ------- tools/http-service/types/http.ts | 26 -- tools/http/request.ts | 151 ++++++++++ tools/hubspot/contacts.ts | 143 ++++++++++ .../model-service/__tests__/providers.test.ts | 201 ------------- tools/model-service/index.ts | 62 ---- tools/model-service/providers/anthropic.ts | 47 --- tools/model-service/providers/google.ts | 48 ---- tools/model-service/providers/openai.ts | 47 --- tools/model-service/providers/xai.ts | 50 ---- tools/model-service/types/agent.ts | 17 -- tools/model-service/types/model.ts | 25 -- tools/openai/chat.ts | 95 ++++++ tools/registry.ts | 117 ++++---- tools/salesforce/opportunities.ts | 172 +++++++++++ tools/types.ts | 29 ++ tools/xai/chat.ts | 85 ++++++ 30 files changed, 1234 insertions(+), 1194 deletions(-) create mode 100644 tools/anthropic/chat.ts create mode 100644 tools/google/chat.ts delete mode 100644 tools/http-service/__tests__/http.test.ts delete mode 100644 tools/http-service/index.ts delete mode 100644 tools/http-service/types/http.ts create mode 100644 tools/http/request.ts create mode 100644 tools/hubspot/contacts.ts delete mode 100644 tools/model-service/__tests__/providers.test.ts delete mode 100644 tools/model-service/index.ts delete mode 100644 tools/model-service/providers/anthropic.ts delete mode 100644 tools/model-service/providers/google.ts delete mode 100644 tools/model-service/providers/openai.ts delete mode 100644 tools/model-service/providers/xai.ts delete mode 100644 tools/model-service/types/agent.ts delete mode 100644 tools/model-service/types/model.ts create mode 100644 tools/openai/chat.ts create mode 100644 tools/salesforce/opportunities.ts create mode 100644 tools/types.ts create mode 100644 tools/xai/chat.ts diff --git a/app/w/[id]/workflow.tsx b/app/w/[id]/workflow.tsx index 7a586d8f03..232b9f85ce 100644 --- a/app/w/[id]/workflow.tsx +++ b/app/w/[id]/workflow.tsx @@ -271,23 +271,13 @@ function WorkflowCanvas() { * @returns {Object} The initial input parameters for the block */ const getInitialInput = (block: BlockState, blockConfig: BlockConfig) => { - if (block.type === 'agent') { - return { - model: block.subBlocks?.['model']?.value || 'gpt-4o', - systemPrompt: block.subBlocks?.['systemPrompt']?.value, - temperature: block.subBlocks?.['temperature']?.value, - apiKey: block.subBlocks?.['apiKey']?.value, - prompt: block.subBlocks?.['systemPrompt']?.value, + const params: Record = {}; + Object.entries(block.subBlocks || {}).forEach(([id, subBlock]) => { + if (subBlock?.value !== undefined) { + params[id] = subBlock.value; } - } else if (block.type === 'api') { - return { - url: block.subBlocks?.['url']?.value, - method: block.subBlocks?.['method']?.value, - headers: block.subBlocks?.['headers']?.value, - body: block.subBlocks?.['body']?.value, - } - } - return {} + }); + return params; } /** @@ -302,35 +292,40 @@ function WorkflowCanvas() { throw new Error(`Block configuration not found for type: ${block.type}`) } - const tools = blockConfig.workflow.tools - if (!tools || !tools.access || tools.access.length === 0) { - throw new Error(`No tools specified for block type: ${block.type}`) + const tools = blockConfig.workflow.tools; + if (!tools?.access || tools.access.length === 0) { + throw new Error(`No tools specified for block type: ${block.type}`); } - // Get the values from subBlocks - const params: Record = {} + // Get all values from subBlocks + const params: Record = {}; Object.entries(block.subBlocks || {}).forEach(([id, subBlock]) => { - if (subBlock) { - params[id] = subBlock.value + if (subBlock?.value !== undefined) { + params[id] = subBlock.value; } }) + // Get the tool ID from the block's configuration + const toolId = tools.config?.tool?.(params) || params.tool; + if (!toolId || !tools.access.includes(toolId)) { + throw new Error(`Invalid or unauthorized tool: ${toolId}`); + } + return { id: block.id, type: 'custom', position: block.position, data: { - tool: tools.access[0], - params, + tool: toolId, + params: params, interface: { - inputs: block.type === 'agent' ? { prompt: 'string' } : {}, + inputs: blockConfig.workflow.inputs || {}, outputs: { - [block.type === 'agent' ? 'response' : 'output']: - typeof blockConfig.workflow.outputType === 'string' - ? blockConfig.workflow.outputType - : blockConfig.workflow.outputType.default, - }, - }, + output: typeof blockConfig.workflow.outputType === 'string' + ? blockConfig.workflow.outputType + : blockConfig.workflow.outputType.default + } + } }, } } diff --git a/blocks/blocks/agent.ts b/blocks/blocks/agent.ts index 88c776000b..2ac3be3dad 100644 --- a/blocks/blocks/agent.ts +++ b/blocks/blocks/agent.ts @@ -1,6 +1,15 @@ import { AgentIcon } from '@/components/icons' import { BlockConfig } from '../types' +// Map of models to their tools +const MODEL_TOOLS = { + 'gpt-4o': 'openai.chat', + 'o1-mini': 'openai.chat', + 'claude-3-5-sonnet-20241022': 'anthropic.chat', + 'gemini-pro': 'google.chat', + 'grok-2-latest': 'xai.chat' +} as const; + export const AgentBlock: BlockConfig = { type: 'agent', toolbar: { @@ -22,7 +31,23 @@ export const AgentBlock: BlockConfig = { } }, tools: { - access: ['model'] + access: ['openai.chat', 'anthropic.chat', 'google.chat', 'xai.chat'], + config: { + tool: (params: Record) => { + const model = params.model || 'gpt-4o'; + if (!model) { + throw new Error('No model selected'); + } + const tool = MODEL_TOOLS[model as keyof typeof MODEL_TOOLS]; + if (!tool) { + throw new Error(`Invalid model selected: ${model}`); + } + return tool; + } + } + }, + inputs: { + systemPrompt: 'string' }, subBlocks: [ { @@ -37,14 +62,14 @@ export const AgentBlock: BlockConfig = { title: 'Context', type: 'short-input', layout: 'full', - placeholder: 'Enter text', + placeholder: 'Enter text' }, { id: 'model', title: 'Model', type: 'dropdown', layout: 'half', - options: ['gpt-4o', 'gemini-pro', 'claude-3-5-sonnet-20241022', 'grok-2-latest', 'deepseek-v3'], + options: Object.keys(MODEL_TOOLS) }, { id: 'temperature', diff --git a/blocks/blocks/api.ts b/blocks/blocks/api.ts index a94d75fc27..0c95b27eca 100644 --- a/blocks/blocks/api.ts +++ b/blocks/blocks/api.ts @@ -13,7 +13,13 @@ export const ApiBlock: BlockConfig = { workflow: { outputType: 'json', tools: { - access: ['http'] + access: ['http.request'] + }, + inputs: { + url: 'string', + method: 'string', + headers: 'json', + body: 'json' }, subBlocks: [ { diff --git a/blocks/blocks/function.ts b/blocks/blocks/function.ts index efaea96435..b648d13e27 100644 --- a/blocks/blocks/function.ts +++ b/blocks/blocks/function.ts @@ -15,6 +15,9 @@ export const FunctionBlock: BlockConfig = { tools: { access: ['function'] }, + inputs: { + code: 'string' + }, subBlocks: [ { id: 'code', diff --git a/blocks/types.ts b/blocks/types.ts index 344bf45ba4..7a66880722 100644 --- a/blocks/types.ts +++ b/blocks/types.ts @@ -4,6 +4,7 @@ import type { JSX } from 'react' export type BlockIcon = (props: SVGProps) => JSX.Element export type BlockCategory = 'basic' | 'advanced' export type OutputType = 'string' | 'number' | 'json' | 'boolean' +export type ParamType = 'string' | 'number' | 'boolean' | 'json' export type SubBlockType = 'short-input' | 'long-input' | 'dropdown' | 'slider' | 'table' | 'code' export type SubBlockLayout = 'full' | 'half' @@ -44,9 +45,12 @@ export interface BlockConfig { workflow: { outputType: OutputTypeConfig subBlocks: SubBlockConfig[] - tools?: { + tools: { access: string[] - config?: Record + config?: { + tool: (params: Record) => string + } } + inputs?: Record } } \ No newline at end of file diff --git a/executor/__tests__/executor.test.ts b/executor/__tests__/executor.test.ts index 45a3c83013..d68c9db457 100644 --- a/executor/__tests__/executor.test.ts +++ b/executor/__tests__/executor.test.ts @@ -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) => Promise>, - private mockValidate: (params: Record) => boolean | string = () => true - ) {} - - async execute(params: Record): Promise> { - return this.mockExecute(params); - } - - validateParams(params: Record): 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: '' } ); - (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: '' }) + })); + const executor = new Executor(workflow); const result = await executor.execute('workflow-1', { input: 'test' }); expect(result.success).toBe(true); expect(result.data).toEqual({ result: '' }); - }); - - 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); }); }); }); diff --git a/executor/index.ts b/executor/index.ts index 74b15848fa..47e0923163 100644 --- a/executor/index.ts +++ b/executor/index.ts @@ -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, context: ExecutionContext ): Promise> { - // 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): 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}`); + } } } diff --git a/executor/types.ts b/executor/types.ts index c140018d56..e835ab80f8 100644 --- a/executor/types.ts +++ b/executor/types.ts @@ -1,7 +1,24 @@ -export interface Tool { +export interface Tool

{ + id: string; name: string; - execute(params: Record): Promise>; - validateParams(params: Record): 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; + body?: (params: P) => Record; + }; + transformResponse: (response: any) => R; + transformError: (error: any) => string; } export interface ToolRegistry { diff --git a/serializer/__tests__/serializer.test.ts b/serializer/__tests__/serializer.test.ts index 685dd57ae5..6af98ebfa9 100644 --- a/serializer/__tests__/serializer.test.ts +++ b/serializer/__tests__/serializer.test.ts @@ -10,14 +10,14 @@ describe('Serializer', () => { }); describe('serializeWorkflow', () => { - it('should serialize a workflow with model and http blocks', () => { + it('should serialize a workflow with agent and http blocks', () => { const blocks: Node[] = [ { - id: 'model-1', + id: 'agent-1', type: 'custom', position: { x: 100, y: 100 }, data: { - tool: 'model', + tool: 'openai.chat', params: { model: 'gpt-4o', systemPrompt: 'You are helpful', @@ -44,7 +44,7 @@ describe('Serializer', () => { type: 'custom', position: { x: 400, y: 100 }, data: { - tool: 'http', + tool: 'http.request', params: { url: 'https://api.example.com', method: 'GET' @@ -70,7 +70,7 @@ describe('Serializer', () => { const connections: Edge[] = [ { id: 'conn-1', - source: 'model-1', + source: 'agent-1', target: 'http-1', sourceHandle: 'response', targetHandle: 'body' @@ -84,16 +84,16 @@ describe('Serializer', () => { expect(serialized.blocks).toHaveLength(2); expect(serialized.connections).toHaveLength(1); - // Test model block serialization - const modelBlock = serialized.blocks.find(b => b.id === 'model-1'); - expect(modelBlock).toBeDefined(); - expect(modelBlock?.config.tool).toBe('model'); - expect(modelBlock?.config.params).toEqual({ + // 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({ model: 'gpt-4o', systemPrompt: 'You are helpful', temperature: 0.7 }); - expect(modelBlock?.metadata).toEqual({ + expect(agentBlock?.metadata).toEqual({ title: 'GPT-4o Agent', description: 'Language model block', category: 'AI', @@ -104,20 +104,11 @@ describe('Serializer', () => { // Test http block serialization const httpBlock = serialized.blocks.find(b => b.id === 'http-1'); expect(httpBlock).toBeDefined(); - expect(httpBlock?.config.tool).toBe('http'); + expect(httpBlock?.config.tool).toBe('http.request'); expect(httpBlock?.config.params).toEqual({ url: 'https://api.example.com', method: 'GET' }); - - // Test connection serialization - const connection = serialized.connections[0]; - expect(connection).toEqual({ - source: 'model-1', - target: 'http-1', - sourceHandle: 'response', - targetHandle: 'body' - }); }); it('should handle blocks with minimal required configuration', () => { @@ -126,7 +117,7 @@ describe('Serializer', () => { type: 'custom', position: { x: 0, y: 0 }, data: { - tool: 'model', + tool: 'openai.chat', params: { model: 'gpt-4o' }, @@ -141,7 +132,7 @@ describe('Serializer', () => { const block = serialized.blocks[0]; expect(block.id).toBe('minimal-1'); - expect(block.config.tool).toBe('model'); + expect(block.config.tool).toBe('openai.chat'); expect(block.config.params).toEqual({ model: 'gpt-4o' }); expect(block.metadata).toBeUndefined(); }); @@ -153,7 +144,7 @@ describe('Serializer', () => { type: 'custom', position: { x: 100, y: 100 }, data: { - tool: 'http', + tool: 'http.request', params: { url: 'https://api.data.com', method: 'GET' @@ -171,7 +162,7 @@ describe('Serializer', () => { type: 'custom', position: { x: 300, y: 100 }, data: { - tool: 'model', + tool: 'openai.chat', params: { model: 'gpt-4o', systemPrompt: 'Process this data' @@ -192,7 +183,7 @@ describe('Serializer', () => { type: 'custom', position: { x: 500, y: 100 }, data: { - tool: 'http', + tool: 'http.request', params: { url: 'https://api.output.com', method: 'POST' @@ -248,19 +239,18 @@ describe('Serializer', () => { it('should preserve tool-specific parameters', () => { const blocks: Node[] = [{ - id: 'model-1', + id: 'agent-1', type: 'custom', position: { x: 0, y: 0 }, data: { - tool: 'model', + tool: 'openai.chat', params: { model: 'gpt-4o', temperature: 0.7, maxTokens: 1000, topP: 0.9, - frequencyPenalty: 0.5, - presencePenalty: 0.5, - stop: ['###'] + frequencyPenalty: 0.1, + presencePenalty: 0.1 }, interface: { inputs: { prompt: 'string' }, @@ -271,29 +261,28 @@ describe('Serializer', () => { const serialized = serializer.serializeWorkflow(blocks, []); const block = serialized.blocks[0]; - + expect(block.config.params).toEqual({ model: 'gpt-4o', temperature: 0.7, maxTokens: 1000, topP: 0.9, - frequencyPenalty: 0.5, - presencePenalty: 0.5, - stop: ['###'] + frequencyPenalty: 0.1, + presencePenalty: 0.1 }); }); }); describe('deserializeWorkflow', () => { it('should deserialize a workflow back to ReactFlow format', () => { - const serialized: SerializedWorkflow = { + const workflow: SerializedWorkflow = { version: '1.0', blocks: [ { - id: 'model-1', + id: 'agent-1', position: { x: 100, y: 100 }, config: { - tool: 'model', + tool: 'openai.chat', params: { model: 'gpt-4o', systemPrompt: 'You are helpful' @@ -309,159 +298,18 @@ describe('Serializer', () => { } } ], - connections: [ - { - source: 'model-1', - target: 'http-1', - sourceHandle: 'response', - targetHandle: 'body' - } - ] + connections: [] }; - const deserialized = serializer.deserializeWorkflow(serialized); + const { blocks, connections } = serializer.deserializeWorkflow(workflow); - // Test blocks deserialization - expect(deserialized.blocks).toHaveLength(1); - const block = deserialized.blocks[0]; - expect(block.id).toBe('model-1'); + expect(blocks).toHaveLength(1); + const block = blocks[0]; + expect(block.id).toBe('agent-1'); expect(block.type).toBe('custom'); - expect(block.position).toEqual({ x: 100, y: 100 }); - expect(block.data).toEqual({ - tool: 'model', - params: { - model: 'gpt-4o', - systemPrompt: 'You are helpful' - }, - interface: { - inputs: { prompt: 'string' }, - outputs: { response: 'string' } - }, - title: 'GPT-4o Agent', - category: 'AI' - }); - - // Test connections deserialization - expect(deserialized.connections).toHaveLength(1); - const connection = deserialized.connections[0]; - expect(connection).toEqual({ - id: 'model-1-http-1', - source: 'model-1', - target: 'http-1', - sourceHandle: 'response', - targetHandle: 'body' - }); - }); - - it('should handle empty workflow', () => { - const serialized: SerializedWorkflow = { - version: '1.0', - blocks: [], - connections: [] - }; - - const deserialized = serializer.deserializeWorkflow(serialized); - expect(deserialized.blocks).toHaveLength(0); - expect(deserialized.connections).toHaveLength(0); - }); - - it('should handle blocks with complex interface types', () => { - const serialized: SerializedWorkflow = { - version: '1.0', - blocks: [{ - id: 'complex-1', - position: { x: 0, y: 0 }, - config: { - tool: 'model', - params: { - model: 'gpt-4o' - }, - interface: { - inputs: { - context: 'object', - options: 'array', - metadata: 'Record', - callback: 'function' - }, - outputs: { - result: 'object', - errors: 'array', - logs: 'string[]' - } - } - } - }], - connections: [] - }; - - const deserialized = serializer.deserializeWorkflow(serialized); - const block = deserialized.blocks[0]; - - expect(block.data.interface.inputs).toEqual({ - context: 'object', - options: 'array', - metadata: 'Record', - callback: 'function' - }); - expect(block.data.interface.outputs).toEqual({ - result: 'object', - errors: 'array', - logs: 'string[]' - }); - }); - - it('should handle circular connections', () => { - const serialized: SerializedWorkflow = { - version: '1.0', - blocks: [ - { - id: 'loop-1', - position: { x: 0, y: 0 }, - config: { - tool: 'model', - params: {}, - interface: { - inputs: { input: 'string' }, - outputs: { output: 'string' } - } - } - }, - { - id: 'loop-2', - position: { x: 200, y: 0 }, - config: { - tool: 'model', - params: {}, - interface: { - inputs: { input: 'string' }, - outputs: { output: 'string' } - } - } - } - ], - connections: [ - { - source: 'loop-1', - target: 'loop-2', - sourceHandle: 'output', - targetHandle: 'input' - }, - { - source: 'loop-2', - target: 'loop-1', - sourceHandle: 'output', - targetHandle: 'input' - } - ] - }; - - const deserialized = serializer.deserializeWorkflow(serialized); - - expect(deserialized.connections).toHaveLength(2); - expect(deserialized.connections[0].source).toBe('loop-1'); - expect(deserialized.connections[0].target).toBe('loop-2'); - expect(deserialized.connections[1].source).toBe('loop-2'); - expect(deserialized.connections[1].target).toBe('loop-1'); + expect(block.data.tool).toBe('openai.chat'); + expect(block.data.params.model).toBe('gpt-4o'); + expect(block.data.title).toBe('GPT-4o Agent'); }); }); }); \ No newline at end of file diff --git a/serializer/types.ts b/serializer/types.ts index 6f34b06d0f..b6ba51b83e 100644 --- a/serializer/types.ts +++ b/serializer/types.ts @@ -16,21 +16,19 @@ export interface Position { y: number; } +export interface BlockConfig { + tool: string; + params: Record; + interface: { + inputs: Record; + outputs: Record; + }; +} + export interface SerializedBlock { id: string; position: Position; - config: { - // The tool this block uses - tool: string; - // Tool-specific parameters - params: Record; - // Block's input/output interface - interface: { - inputs: Record; - outputs: Record; - }; - }; - // UI-specific metadata (optional) + config: BlockConfig; metadata?: { title?: string; description?: string; @@ -39,13 +37,3 @@ export interface SerializedBlock { color?: string; }; } - -export interface SubBlockValue { - title: string; - type: 'long-input' | 'short-input' | 'dropdown' | 'slider' | 'code'; - value: string | number | boolean; -} - -export interface BlockValues { - [key: string]: string | number | boolean; -} diff --git a/tools/anthropic/chat.ts b/tools/anthropic/chat.ts new file mode 100644 index 0000000000..95c53d076f --- /dev/null +++ b/tools/anthropic/chat.ts @@ -0,0 +1,84 @@ +import { ToolConfig } from '../types'; + +interface ChatParams { + apiKey: string; + systemPrompt: string; + model?: string; + temperature?: number; + maxTokens?: number; + topP?: number; + stream?: boolean; +} + +interface ChatResponse { + output: string; + tokens?: number; + model: string; +} + +export const chatTool: ToolConfig = { + id: 'anthropic.chat', + name: 'Anthropic Chat', + description: 'Chat with Anthropic Claude models', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + description: 'Anthropic API key' + }, + systemPrompt: { + type: 'string', + required: true, + description: 'System prompt to send to the model' + }, + model: { + type: 'string', + default: 'claude-3-5-sonnet-20241022', + description: 'Model to use' + }, + temperature: { + type: 'number', + default: 0.7, + description: 'Controls randomness in the response' + } + }, + + request: { + url: 'https://api.anthropic.com/v1/messages', + method: 'POST', + headers: (params) => ({ + 'Content-Type': 'application/json', + 'x-api-key': params.apiKey, + 'anthropic-version': '2023-06-01' + }), + body: (params) => { + const body = { + model: params.model || 'claude-3-5-sonnet-20241022', + messages: [ + { role: 'user', content: params.systemPrompt } + ], + temperature: params.temperature, + max_tokens: params.maxTokens, + top_p: params.topP, + stream: params.stream + }; + return body; + } + }, + + transformResponse: (data) => { + return { + output: data.content[0].text, + tokens: data.usage?.input_tokens + data.usage?.output_tokens, + model: data.model + }; + }, + + transformError: (error) => { + const message = error.error?.message || error.message; + const code = error.error?.type || error.code; + return `${message} (${code})`; + } +}; \ No newline at end of file diff --git a/tools/google/chat.ts b/tools/google/chat.ts new file mode 100644 index 0000000000..8bff117ffd --- /dev/null +++ b/tools/google/chat.ts @@ -0,0 +1,86 @@ +import { ToolConfig } from '../types'; + +interface ChatParams { + apiKey: string; + systemPrompt: string; + model?: string; + temperature?: number; + maxTokens?: number; + topP?: number; + topK?: number; +} + +interface ChatResponse { + output: string; + tokens?: number; + model: string; +} + +export const chatTool: ToolConfig = { + id: 'google.chat', + name: 'Google Chat', + description: 'Chat with Google Gemini models', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + description: 'Google API key' + }, + systemPrompt: { + type: 'string', + required: true, + description: 'System prompt to send to the model' + }, + model: { + type: 'string', + default: 'gemini-pro', + description: 'Model to use' + }, + temperature: { + type: 'number', + default: 0.7, + description: 'Controls randomness in the response' + } + }, + + request: { + url: 'https://generativelanguage.googleapis.com/v1/models/gemini-pro:generateContent', + method: 'POST', + headers: (params) => ({ + 'Content-Type': 'application/json', + 'x-goog-api-key': params.apiKey + }), + body: (params) => { + const body = { + contents: [ + { + role: 'user', + parts: [{ text: params.systemPrompt }] + } + ], + generationConfig: { + temperature: params.temperature, + maxOutputTokens: params.maxTokens, + topP: params.topP, + topK: params.topK + } + }; + return body; + } + }, + + transformResponse: (data) => { + return { + output: data.candidates[0].content.parts[0].text, + model: 'gemini-pro' + }; + }, + + transformError: (error) => { + const message = error.error?.message || error.message; + const code = error.error?.status || error.code; + return `${message} (${code})`; + } +}; \ No newline at end of file diff --git a/tools/http-service/__tests__/http.test.ts b/tools/http-service/__tests__/http.test.ts deleted file mode 100644 index 5cb535b699..0000000000 --- a/tools/http-service/__tests__/http.test.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { describe, expect, test, jest, beforeEach } from '@jest/globals'; -import { HttpService } from '../index'; -import { HttpRequestConfig } from '../types/http'; - -// Setup fetch mock -const mockFetch = jest.fn() as jest.MockedFunction; -global.fetch = mockFetch; - -describe('HttpService', () => { - let service: HttpService; - - beforeEach(() => { - jest.clearAllMocks(); - service = HttpService.getInstance(); - }); - - test('should make successful GET request', async () => { - const mockResponse = { message: 'Success' }; - mockFetch.mockResolvedValueOnce({ - ok: true, - status: 200, - statusText: 'OK', - headers: new Headers({ 'content-type': 'application/json' }), - json: () => Promise.resolve(mockResponse) - } as Response); - - const response = await service.get('https://api.example.com/data'); - expect(response.data).toEqual(mockResponse); - expect(response.status).toBe(200); - expect(mockFetch).toHaveBeenCalledWith( - 'https://api.example.com/data', - expect.objectContaining({ - method: 'GET' - }) - ); - }); - - test('should make successful POST request with JSON body', async () => { - const requestBody = { key: 'value' }; - const mockResponse = { id: 1 }; - mockFetch.mockResolvedValueOnce({ - ok: true, - status: 201, - statusText: 'Created', - headers: new Headers({ 'content-type': 'application/json' }), - json: () => Promise.resolve(mockResponse) - } as Response); - - const response = await service.post('https://api.example.com/data', requestBody); - expect(response.data).toEqual(mockResponse); - expect(response.status).toBe(201); - expect(mockFetch).toHaveBeenCalledWith( - 'https://api.example.com/data', - expect.objectContaining({ - method: 'POST', - body: JSON.stringify(requestBody) - }) - ); - }); - - test('should handle request with authentication', async () => { - const mockResponse = { message: 'Authenticated' }; - mockFetch.mockResolvedValueOnce({ - ok: true, - status: 200, - statusText: 'OK', - headers: new Headers({ 'content-type': 'application/json' }), - json: () => Promise.resolve(mockResponse) - } as Response); - - const config: Omit = { - auth: { - type: 'bearer', - token: 'test-token' - } - }; - - const response = await service.get('https://api.example.com/protected', config); - expect(response.data).toEqual(mockResponse); - expect(mockFetch).toHaveBeenCalledWith( - 'https://api.example.com/protected', - expect.objectContaining({ - headers: expect.any(Headers) - }) - ); - - const headers = mockFetch.mock.calls[0][1]?.headers as Headers; - expect(headers.get('Authorization')).toBe('Bearer test-token'); - }); - - test('should handle request timeout', async () => { - mockFetch.mockImplementationOnce(() => - new Promise((_, reject) => { - setTimeout(() => reject(new Error('The operation was aborted')), 50); - }) - ); - - await expect( - service.get('https://api.example.com/data', { timeout: 10 }) - ).rejects.toThrow('The operation was aborted'); - }); - - test('should handle API errors', async () => { - const errorResponse = { error: 'Not Found' }; - mockFetch.mockResolvedValueOnce({ - ok: false, - status: 404, - statusText: 'Not Found', - headers: new Headers({ 'content-type': 'application/json' }), - json: () => Promise.resolve(errorResponse) - } as Response); - - const promise = service.get('https://api.example.com/nonexistent'); - await expect(promise).rejects.toThrow('Not Found'); - await expect(promise).rejects.toMatchObject({ - status: 404, - data: errorResponse - }); - }); -}); \ No newline at end of file diff --git a/tools/http-service/index.ts b/tools/http-service/index.ts deleted file mode 100644 index 245e3adbde..0000000000 --- a/tools/http-service/index.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { HttpRequestConfig, HttpResponse, HttpError } from './types/http'; - -export class HttpService { - private static instance: HttpService; - - constructor() {} - - public static getInstance(): HttpService { - if (!HttpService.instance) { - HttpService.instance = new HttpService(); - } - return HttpService.instance; - } - - private getHeaders(config: HttpRequestConfig): Headers { - const headers = new Headers(config.headers); - - if (!headers.has('Content-Type') && config.body) { - headers.set('Content-Type', 'application/json'); - } - - if (config.auth) { - switch (config.auth.type) { - case 'bearer': - headers.set('Authorization', `Bearer ${config.auth.token}`); - break; - case 'basic': - const credentials = btoa(`${config.auth.username}:${config.auth.password}`); - headers.set('Authorization', `Basic ${credentials}`); - break; - } - } - - return headers; - } - - private async handleResponse(response: Response): Promise> { - const headers: Record = {}; - response.headers.forEach((value, key) => { - headers[key] = value; - }); - - if (!response.ok) { - const error = new Error(response.statusText) as HttpError; - error.status = response.status; - error.statusText = response.statusText; - try { - error.data = await response.json(); - } catch { - error.data = await response.text(); - } - throw error; - } - - let data: T; - const contentType = response.headers.get('content-type'); - if (contentType?.includes('application/json')) { - data = await response.json(); - } else { - data = await response.text() as T; - } - - return { - data, - status: response.status, - statusText: response.statusText, - headers - }; - } - - public async request(config: HttpRequestConfig): Promise> { - const controller = new AbortController(); - const timeoutId = config.timeout ? setTimeout(() => controller.abort(), config.timeout) : null; - - try { - const response = await fetch(config.url, { - method: config.method, - headers: this.getHeaders(config), - body: config.body ? JSON.stringify(config.body) : undefined, - signal: controller.signal - }); - - return await this.handleResponse(response); - } finally { - if (timeoutId) clearTimeout(timeoutId); - } - } - - public async get(url: string, config: Omit = {}): Promise> { - return this.request({ ...config, url, method: 'GET' }); - } - - public async post(url: string, data?: any, config: Omit = {}): Promise> { - return this.request({ ...config, url, method: 'POST', body: data }); - } - - public async put(url: string, data?: any, config: Omit = {}): Promise> { - return this.request({ ...config, url, method: 'PUT', body: data }); - } - - public async delete(url: string, config: Omit = {}): Promise> { - return this.request({ ...config, url, method: 'DELETE' }); - } - - public async patch(url: string, data?: any, config: Omit = {}): Promise> { - return this.request({ ...config, url, method: 'PATCH', body: data }); - } -} \ No newline at end of file diff --git a/tools/http-service/types/http.ts b/tools/http-service/types/http.ts deleted file mode 100644 index 866cf3a2e2..0000000000 --- a/tools/http-service/types/http.ts +++ /dev/null @@ -1,26 +0,0 @@ -export interface HttpRequestConfig { - url: string; - method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; - headers?: Record; - body?: any; - timeout?: number; - auth?: { - type: 'basic' | 'bearer'; - token?: string; - username?: string; - password?: string; - }; -} - -export interface HttpResponse { - data: T; - status: number; - headers: Record; - statusText: string; -} - -export interface HttpError extends Error { - status?: number; - statusText?: string; - data?: any; -} \ No newline at end of file diff --git a/tools/http/request.ts b/tools/http/request.ts new file mode 100644 index 0000000000..68e88aaa4a --- /dev/null +++ b/tools/http/request.ts @@ -0,0 +1,151 @@ +import { ToolConfig, HttpMethod } from '../types'; + +interface RequestParams { + url: string; + method?: HttpMethod; + headers?: Record; + body?: any; + queryParams?: Record; + pathParams?: Record; + formData?: Record; + timeout?: number; + validateStatus?: (status: number) => boolean; +} + +interface RequestResponse { + data: any; + status: number; + headers: Record; +} + +export const requestTool: ToolConfig = { + id: 'http.request', + name: 'HTTP Request', + description: 'Make HTTP requests to any endpoint with support for CRUD operations', + version: '1.0.0', + + params: { + url: { + type: 'string', + required: true, + description: 'The URL to send the request to' + }, + method: { + type: 'string', + default: 'GET', + description: 'HTTP method (GET, POST, PUT, PATCH, DELETE)' + }, + headers: { + type: 'object', + description: 'HTTP headers to include' + }, + body: { + type: 'object', + description: 'Request body (for POST, PUT, PATCH)' + }, + queryParams: { + type: 'object', + description: 'URL query parameters to append' + }, + pathParams: { + type: 'object', + description: 'URL path parameters to replace (e.g., :id in /users/:id)' + }, + formData: { + type: 'object', + description: 'Form data to send (will set appropriate Content-Type)' + }, + timeout: { + type: 'number', + default: 10000, + description: 'Request timeout in milliseconds' + }, + validateStatus: { + type: 'object', + description: 'Custom status validation function' + } + }, + + request: { + url: (params: RequestParams) => { + let url = params.url; + + // Replace path parameters + if (params.pathParams) { + Object.entries(params.pathParams).forEach(([key, value]) => { + url = url.replace(`:${key}`, encodeURIComponent(value)); + }); + } + + // Append query parameters + if (params.queryParams) { + const queryString = Object.entries(params.queryParams) + .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`) + .join('&'); + url += (url.includes('?') ? '&' : '?') + queryString; + } + + return url; + }, + method: 'POST' as HttpMethod, + headers: (params: RequestParams) => { + const headers: Record = { + ...params.headers + }; + + // Set appropriate Content-Type + if (params.formData) { + // Don't set Content-Type for FormData, browser will set it with boundary + return headers; + } else if (params.body) { + headers['Content-Type'] = 'application/json'; + } + + return headers; + }, + body: (params: RequestParams) => { + if (params.formData) { + const formData = new FormData(); + Object.entries(params.formData).forEach(([key, value]) => { + formData.append(key, value); + }); + return formData; + } + + if (params.body) { + return params.body; + } + + return undefined; + } + }, + + transformResponse: (response) => { + // Try to parse response based on content-type + const contentType = response.headers?.['content-type'] || ''; + let data = response.data; + + if (contentType.includes('application/json')) { + try { + data = typeof response.data === 'string' ? JSON.parse(response.data) : response.data; + } catch (e) { + // Keep original data if parsing fails + } + } + + return { + data, + status: response.status, + headers: response.headers + }; + }, + + transformError: (error) => { + const message = error.message || error.error?.message; + const code = error.status || error.error?.status; + const details = error.response?.data + ? `\nDetails: ${JSON.stringify(error.response.data)}` + : ''; + return `${message} (${code})${details}`; + } +}; \ No newline at end of file diff --git a/tools/hubspot/contacts.ts b/tools/hubspot/contacts.ts new file mode 100644 index 0000000000..0c2a37ad49 --- /dev/null +++ b/tools/hubspot/contacts.ts @@ -0,0 +1,143 @@ +import { ToolConfig } from '../types'; + +interface ContactParams { + apiKey: string; + email: string; + firstName?: string; + lastName?: string; + phone?: string; + company?: string; + id?: string; + properties?: Record; + limit?: number; + after?: string; +} + +interface ContactResponse { + id: string; + properties: { + email: string; + firstname?: string; + lastname?: string; + phone?: string; + company?: string; + [key: string]: any; + }; + createdAt: string; + updatedAt: string; +} + +export const contactsTool: ToolConfig = { + id: 'hubspot.contacts', + name: 'HubSpot Contacts', + description: 'Manage HubSpot contacts - create, search, and update contact records', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + description: 'HubSpot API key' + }, + email: { + type: 'string', + required: true, + description: 'Contact email address' + }, + firstName: { + type: 'string', + description: 'Contact first name' + }, + lastName: { + type: 'string', + description: 'Contact last name' + }, + phone: { + type: 'string', + description: 'Contact phone number' + }, + company: { + type: 'string', + description: 'Contact company name' + }, + id: { + type: 'string', + description: 'Contact ID (required for updates)' + }, + properties: { + type: 'object', + description: 'Additional contact properties' + }, + limit: { + type: 'number', + default: 100, + description: 'Number of records to return' + }, + after: { + type: 'string', + description: 'Pagination cursor' + } + }, + + request: { + url: (params) => { + const baseUrl = 'https://api.hubapi.com/crm/v3/objects/contacts'; + if (params.id) { + return `${baseUrl}/${params.id}`; + } + return baseUrl; + }, + method: 'POST', + headers: (params) => ({ + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${params.apiKey}` + }), + body: (params) => { + const properties = { + email: params.email, + ...(params.firstName && { firstname: params.firstName }), + ...(params.lastName && { lastname: params.lastName }), + ...(params.phone && { phone: params.phone }), + ...(params.company && { company: params.company }), + ...params.properties + }; + + if (params.id) { + // Update existing contact + return { properties }; + } + + // Create new contact or search + return { + properties, + ...(params.limit && { limit: params.limit }), + ...(params.after && { after: params.after }) + }; + } + }, + + transformResponse: (data) => { + if (Array.isArray(data.results)) { + // Search response + return data.results.map((contact: { id: string; properties: Record; createdAt: string; updatedAt: string }) => ({ + id: contact.id, + properties: contact.properties, + createdAt: contact.createdAt, + updatedAt: contact.updatedAt + })); + } + // Single contact response + return { + id: data.id, + properties: data.properties, + createdAt: data.createdAt, + updatedAt: data.updatedAt + }; + }, + + transformError: (error) => { + const message = error.message || error.error?.message; + const code = error.status || error.error?.status; + return `${message} (${code})`; + } +}; \ No newline at end of file diff --git a/tools/model-service/__tests__/providers.test.ts b/tools/model-service/__tests__/providers.test.ts deleted file mode 100644 index f7055cea4e..0000000000 --- a/tools/model-service/__tests__/providers.test.ts +++ /dev/null @@ -1,201 +0,0 @@ -import { describe, expect, test, jest, beforeEach } from '@jest/globals'; -import { OpenAIProvider } from '../providers/openai'; -import { AnthropicProvider } from '../providers/anthropic'; -import { GoogleProvider } from '../providers/google'; -import { XAIProvider } from '../providers/xai'; -import { AgentConfig } from '../types/agent'; -import { ModelRequestOptions } from '../types/model'; - -// Setup fetch mock -const mockFetch = jest.fn() as jest.MockedFunction; -global.fetch = mockFetch; - -describe('Model Providers', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - describe('OpenAI Provider', () => { - const provider = new OpenAIProvider(); - - test('should call OpenAI API successfully', async () => { - const mockResponse = { - choices: [{ message: { content: 'Test response' } }], - usage: { total_tokens: 10 } - }; - - mockFetch.mockResolvedValueOnce({ - ok: true, - json: () => Promise.resolve(mockResponse) - } as Response); - - const config: AgentConfig = { - model: 'gpt-4o', - systemPrompt: 'Test prompt', - temperature: 0.7, - apiKey: 'test-key' - }; - - const result = await provider.callModel(config, { maxTokens: 100 }); - expect(result.response).toBe('Test response'); - expect(result.tokens).toBe(10); - expect(result.model).toBe('gpt-4o'); - }); - - test('should handle API errors', async () => { - mockFetch.mockResolvedValueOnce({ - ok: false, - json: () => Promise.resolve({ error: { message: 'API Error' } }) - } as Response); - - const config: AgentConfig = { - model: 'gpt-4o', - systemPrompt: 'Test prompt', - temperature: 0.7, - apiKey: 'invalid-key' - }; - - await expect(provider.callModel(config, {})).rejects.toThrow('API Error'); - }); - }); - - describe('Anthropic Provider', () => { - const provider = new AnthropicProvider(); - - test('should call Anthropic API successfully', async () => { - const mockResponse = { - content: [{ text: 'Test response' }], - usage: { input_tokens: 5, output_tokens: 5 } - }; - - mockFetch.mockResolvedValueOnce({ - ok: true, - json: () => Promise.resolve(mockResponse) - } as Response); - - const config: AgentConfig = { - model: 'claude-3-5-sonnet-20241022', - systemPrompt: 'Test prompt', - temperature: 0.7, - apiKey: 'test-key' - }; - - const result = await provider.callModel(config, { maxTokens: 100 }); - expect(result.response).toBe('Test response'); - expect(result.tokens).toBe(10); - expect(result.model).toBe('claude-3-5-sonnet-20241022'); - }); - - test('should handle API errors', async () => { - mockFetch.mockResolvedValueOnce({ - ok: false, - json: () => Promise.resolve({ error: { message: 'API Error' } }) - } as Response); - - const config: AgentConfig = { - model: 'claude-3-5-sonnet-20241022', - systemPrompt: 'Test prompt', - temperature: 0.7, - apiKey: 'invalid-key' - }; - - await expect(provider.callModel(config, {})).rejects.toThrow('API Error'); - }); - }); - - describe('Google Provider', () => { - const provider = new GoogleProvider(); - - test('should call Google API successfully', async () => { - const mockResponse = { - candidates: [{ - content: { - parts: [{ text: 'Test response' }] - } - }], - usage: { totalTokens: 10 } - }; - - mockFetch.mockResolvedValueOnce({ - ok: true, - json: () => Promise.resolve(mockResponse) - } as Response); - - const config: AgentConfig = { - model: 'gemini-pro', - systemPrompt: 'Test prompt', - temperature: 0.7, - apiKey: 'test-key' - }; - - const result = await provider.callModel(config, { maxTokens: 100 }); - expect(result.response).toBe('Test response'); - expect(result.tokens).toBe(10); - expect(result.model).toBe('gemini-pro'); - }); - - test('should handle API errors', async () => { - mockFetch.mockResolvedValueOnce({ - ok: false, - json: () => Promise.resolve({ error: { message: 'API Error' } }) - } as Response); - - const config: AgentConfig = { - model: 'gemini-pro', - systemPrompt: 'Test prompt', - temperature: 0.7, - apiKey: 'invalid-key' - }; - - await expect(provider.callModel(config, {})).rejects.toThrow('API Error'); - }); - }); - - describe('XAI Provider', () => { - const provider = new XAIProvider(); - - test('should call XAI API successfully', async () => { - const mockResponse = { - choices: [{ - message: { - content: 'Test response' - } - }], - usage: { total_tokens: 10 } - }; - - mockFetch.mockResolvedValueOnce({ - ok: true, - json: () => Promise.resolve(mockResponse) - } as Response); - - const config: AgentConfig = { - model: 'grok-2-latest', - systemPrompt: 'Test prompt', - temperature: 0.7, - apiKey: 'test-key' - }; - - const result = await provider.callModel(config, { maxTokens: 100 }); - expect(result.response).toBe('Test response'); - expect(result.tokens).toBe(10); - expect(result.model).toBe('grok-2-latest'); - }); - - test('should handle API errors', async () => { - mockFetch.mockResolvedValueOnce({ - ok: false, - json: () => Promise.resolve({ error: { message: 'API Error' } }) - } as Response); - - const config: AgentConfig = { - model: 'grok-2-latest', - systemPrompt: 'Test prompt', - temperature: 0.7, - apiKey: 'invalid-key' - }; - - await expect(provider.callModel(config, {})).rejects.toThrow('API Error'); - }); - }); -}); \ No newline at end of file diff --git a/tools/model-service/index.ts b/tools/model-service/index.ts deleted file mode 100644 index 17d593d2f3..0000000000 --- a/tools/model-service/index.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { ModelProvider, ModelRequestOptions, ModelResponse } from './types/model'; -import { OpenAIProvider } from './providers/openai'; -import { AnthropicProvider } from './providers/anthropic'; -import { GoogleProvider } from './providers/google'; -import { XAIProvider } from './providers/xai'; -import { AgentConfig } from './types/agent'; - -export class ModelService { - private static instance: ModelService; - private providers: Map; - - constructor() { - this.providers = new Map(); - this.initializeProviders(); - } - - public static getInstance(): ModelService { - if (!ModelService.instance) { - ModelService.instance = new ModelService(); - } - return ModelService.instance; - } - - private initializeProviders() { - const openai = new OpenAIProvider(); - const anthropic = new AnthropicProvider(); - const google = new GoogleProvider(); - const xai = new XAIProvider(); - // OpenAI models - this.providers.set('gpt-4o', openai); - - // Anthropic models - this.providers.set('claude-3-5-sonnet-20241022', anthropic); - - // Google models - this.providers.set('gemini-pro', google); - - // XAI models - this.providers.set('grok-2-latest', xai); - } - - public async callModel(config: AgentConfig, options: ModelRequestOptions = {}): Promise { - const provider = this.providers.get(config.model); - if (!provider) { - throw new Error(`No provider found for model: ${config.model}`); - } - - await provider.validateConfig(config); - return provider.callModel(config, options); - } - - public setApiKey(provider: string, apiKey: string): void { - // Store API keys securely (in memory for now) - // TODO: Implement secure storage - } - - public getApiKey(provider: string): string | null { - // Retrieve API key - // TODO: Implement secure retrieval - return null; - } -} diff --git a/tools/model-service/providers/anthropic.ts b/tools/model-service/providers/anthropic.ts deleted file mode 100644 index 9c11882637..0000000000 --- a/tools/model-service/providers/anthropic.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { AgentConfig } from '../types/agent'; -import { ModelProvider, ModelRequestOptions, ModelResponse } from '../types/model'; - -export class AnthropicProvider implements ModelProvider { - private readonly SUPPORTED_MODELS = ['claude-3-5-sonnet-20241022']; - private readonly API_URL = 'https://api.anthropic.com/v1/messages'; - - async callModel(config: AgentConfig, options: ModelRequestOptions): Promise { - const response = await fetch(this.API_URL, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'x-api-key': config.apiKey, - 'anthropic-version': '2023-06-01' - }, - body: JSON.stringify({ - model: config.model, - messages: [ - { role: 'user', content: config.systemPrompt + '\n' + (config.prompt || '') } - ], - temperature: config.temperature, - max_tokens: options.maxTokens - }) - }); - - if (!response.ok) { - const error = await response.json(); - throw new Error(error.error?.message || 'Anthropic API error'); - } - - const data = await response.json(); - return { - response: data.content[0].text, - tokens: data.usage.input_tokens + data.usage.output_tokens, - model: config.model - }; - } - - async validateConfig(config: AgentConfig): Promise { - if (!config.apiKey) { - throw new Error('Anthropic API key is required'); - } - if (!this.SUPPORTED_MODELS.includes(config.model)) { - throw new Error(`Model ${config.model} is not supported. Use one of: ${this.SUPPORTED_MODELS.join(', ')}`); - } - } -} diff --git a/tools/model-service/providers/google.ts b/tools/model-service/providers/google.ts deleted file mode 100644 index 4e8441471a..0000000000 --- a/tools/model-service/providers/google.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { AgentConfig } from '../types/agent'; -import { ModelProvider, ModelRequestOptions, ModelResponse } from '../types/model'; - -export class GoogleProvider implements ModelProvider { - private readonly SUPPORTED_MODELS = ['gemini-pro']; - private readonly API_URL = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent'; - - async callModel(config: AgentConfig, options: ModelRequestOptions): Promise { - const response = await fetch(`${this.API_URL}?key=${config.apiKey}`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - contents: [{ - parts: [{ - text: config.systemPrompt + '\n' + (config.prompt || '') - }] - }], - generationConfig: { - temperature: config.temperature, - maxOutputTokens: options.maxTokens - } - }) - }); - - if (!response.ok) { - const error = await response.json(); - throw new Error(error.error?.message || 'Google API error'); - } - - const data = await response.json(); - return { - response: data.candidates[0].content.parts[0].text, - tokens: data.usage?.totalTokens || 0, - model: config.model - }; - } - - async validateConfig(config: AgentConfig): Promise { - if (!config.apiKey) { - throw new Error('Google API key is required'); - } - if (!this.SUPPORTED_MODELS.includes(config.model)) { - throw new Error(`Model ${config.model} is not supported. Use one of: ${this.SUPPORTED_MODELS.join(', ')}`); - } - } -} diff --git a/tools/model-service/providers/openai.ts b/tools/model-service/providers/openai.ts deleted file mode 100644 index deea0b4387..0000000000 --- a/tools/model-service/providers/openai.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { AgentConfig } from '../types/agent'; -import { ModelProvider, ModelRequestOptions, ModelResponse } from '../types/model'; - -export class OpenAIProvider implements ModelProvider { - private readonly SUPPORTED_MODELS = ['gpt-4o']; - private readonly API_URL = 'https://api.openai.com/v1/chat/completions'; - - async callModel(config: AgentConfig, options: ModelRequestOptions): Promise { - const response = await fetch(this.API_URL, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${config.apiKey}` - }, - body: JSON.stringify({ - model: config.model, - messages: [ - { role: 'system', content: config.systemPrompt }, - { role: 'user', content: config.prompt || '' } - ], - temperature: config.temperature, - max_tokens: options.maxTokens, - }) - }); - - if (!response.ok) { - const error = await response.json(); - throw new Error(error.error?.message || 'OpenAI API error'); - } - - const data = await response.json(); - return { - response: data.choices[0].message.content, - tokens: data.usage.total_tokens, - model: config.model - }; - } - - async validateConfig(config: AgentConfig): Promise { - if (!config.apiKey) { - throw new Error('OpenAI API key is required'); - } - if (!this.SUPPORTED_MODELS.includes(config.model)) { - throw new Error(`Model ${config.model} is not supported. Use one of: ${this.SUPPORTED_MODELS.join(', ')}`); - } - } -} diff --git a/tools/model-service/providers/xai.ts b/tools/model-service/providers/xai.ts deleted file mode 100644 index 892988058c..0000000000 --- a/tools/model-service/providers/xai.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { AgentConfig } from '../types/agent'; -import { ModelProvider, ModelRequestOptions, ModelResponse } from '../types/model'; - -export class XAIProvider implements ModelProvider { - async callModel( - config: AgentConfig, - options: ModelRequestOptions - ): Promise { - const response = await fetch('https://api.x.ai/v1/chat/completions', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${config.apiKey}` - }, - body: JSON.stringify({ - model: config.model, - messages: [ - { - role: 'system', - content: config.systemPrompt - } - ], - temperature: config.temperature, - max_tokens: options.maxTokens - }), - signal: AbortSignal.timeout(options.timeout || 10000) - }); - - if (!response.ok) { - const error = await response.json(); - throw new Error(error.error?.message || 'xAI API call failed'); - } - - const result = await response.json(); - return { - response: result.choices[0].message.content, - tokens: result.usage?.total_tokens || 0, - model: config.model - }; - } - - async validateConfig(config: AgentConfig): Promise { - if (!config.apiKey) { - throw new Error('xAI API key is required'); - } - if (!config.model.startsWith('grok')) { - throw new Error('Invalid xAI model specified'); - } - } -} \ No newline at end of file diff --git a/tools/model-service/types/agent.ts b/tools/model-service/types/agent.ts deleted file mode 100644 index e72abffe9a..0000000000 --- a/tools/model-service/types/agent.ts +++ /dev/null @@ -1,17 +0,0 @@ -export interface AgentConfig { - model: string; - systemPrompt: string; - prompt?: string; - temperature: number; - apiKey: string; - } - -export interface AgentResult { - success: boolean; - data?: { - response: string; - tokens: number; - model: string; - }; - error?: string; -} \ No newline at end of file diff --git a/tools/model-service/types/model.ts b/tools/model-service/types/model.ts deleted file mode 100644 index 060c4fd508..0000000000 --- a/tools/model-service/types/model.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { AgentConfig } from "./agent"; - -export interface ModelResponse { - response: string; - tokens: number; - model: string; - } - - export interface ModelRequestOptions { - maxTokens?: number; - timeout?: number; - } - - export interface ModelProvider { - callModel(config: AgentConfig, options: ModelRequestOptions): Promise; - validateConfig(config: AgentConfig): Promise; - } - -export const DEFAULT_MODEL_CONFIGS = { - 'gpt-4o': { provider: 'openai' }, - 'claude': { provider: 'anthropic' }, - 'gemini': { provider: 'google' }, - 'grok': { provider: 'xai' }, - 'deepseek': { provider: 'deepseek' } -} as const; diff --git a/tools/openai/chat.ts b/tools/openai/chat.ts new file mode 100644 index 0000000000..c177cf1743 --- /dev/null +++ b/tools/openai/chat.ts @@ -0,0 +1,95 @@ +import { ToolConfig } from '../types'; + +interface ChatParams { + apiKey: string; + systemPrompt: string; + model?: string; + temperature?: number; + maxTokens?: number; + topP?: number; + frequencyPenalty?: number; + presencePenalty?: number; + stream?: boolean; +} + +interface ChatResponse { + output: string; + tokens?: number; + model: string; +} + +export const chatTool: ToolConfig = { + id: 'openai.chat', + name: 'OpenAI Chat', + description: 'Chat with OpenAI models', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + description: 'OpenAI API key' + }, + systemPrompt: { + type: 'string', + required: true, + description: 'System prompt to send to the model' + }, + model: { + type: 'string', + default: 'gpt-4o', + description: 'Model to use (gpt-4o, o1-mini)' + }, + temperature: { + type: 'number', + default: 0.7, + description: 'Controls randomness in the response' + } + }, + + request: { + url: 'https://api.openai.com/v1/chat/completions', + method: 'POST', + headers: (params) => ({ + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${params.apiKey}` + }), + body: (params) => { + console.log('OpenAI Chat Tool - Request Params:', JSON.stringify(params, null, 2)); + const body = { + model: params.model || 'gpt-4o', + messages: [ + { role: 'system', content: params.systemPrompt } + ], + temperature: params.temperature, + max_tokens: params.maxTokens, + top_p: params.topP, + frequency_penalty: params.frequencyPenalty, + presence_penalty: params.presencePenalty, + stream: params.stream + }; + console.log('OpenAI Chat Tool - Request Body:', JSON.stringify(body, null, 2)); + return body; + } + }, + + transformResponse: (data) => { + if (data.choices?.[0]?.delta?.content) { + return { + output: data.choices[0].delta.content, + model: data.model + }; + } + return { + output: data.choices[0].message.content, + tokens: data.usage?.total_tokens, + model: data.model + }; + }, + + transformError: (error) => { + const message = error.error?.message || error.message; + const code = error.error?.type || error.code; + return `${message} (${code})`; + } +}; \ No newline at end of file diff --git a/tools/registry.ts b/tools/registry.ts index 8ee66ca82e..005551c8c1 100644 --- a/tools/registry.ts +++ b/tools/registry.ts @@ -1,76 +1,63 @@ -import { Tool, ToolRegistry } from '@/executor/types'; -import { ModelService } from './model-service'; -import { HttpService } from './http-service'; -import { AgentConfig } from './model-service/types/agent'; +import { ToolConfig } from './types'; +import { chatTool as openaiChat } from './openai/chat'; +import { chatTool as anthropicChat } from './anthropic/chat'; +import { chatTool as googleChat } from './google/chat'; +import { chatTool as xaiChat } from './xai/chat'; +import { requestTool as httpRequest } from './http/request'; +import { contactsTool as hubspotContacts } from './hubspot/contacts'; +import { opportunitiesTool as salesforceOpportunities } from './salesforce/opportunities'; -class ModelTool implements Tool { - name = 'model'; - private service: ModelService; +// Registry of all available tools +export const tools: Record = { + // AI Models + 'openai.chat': openaiChat, + 'anthropic.chat': anthropicChat, + 'google.chat': googleChat, + 'xai.chat': xaiChat, + // HTTP + 'http.request': httpRequest, + // CRM Tools + 'hubspot.contacts': hubspotContacts, + 'salesforce.opportunities': salesforceOpportunities +}; - constructor() { - this.service = ModelService.getInstance(); - } - - validateParams(params: Record): boolean | string { - const required = ['model', 'prompt']; - const missing = required.filter(param => !params[param]); - if (missing.length > 0) { - return `Missing required parameters: ${missing.join(', ')}`; - } - return true; - } - - async execute(params: Record): Promise> { - const config: AgentConfig = { - model: params.model, - systemPrompt: params.systemPrompt || 'You are a helpful assistant.', - prompt: params.prompt, - temperature: params.temperature || 0.7, - apiKey: params.apiKey || process.env.OPENAI_API_KEY || '' - }; - - const response = await this.service.callModel(config); - return { - response: response.response, - tokens: response.tokens, - model: response.model - }; - } +// Get a tool by its ID +export function getTool(toolId: string): ToolConfig | undefined { + return tools[toolId]; } -class HttpTool implements Tool { - name = 'http'; - private service: HttpService; +// Execute a tool with parameters +export async function executeTool( + toolId: string, + params: Record +): Promise { + const tool = getTool(toolId); - constructor() { - this.service = HttpService.getInstance(); + if (!tool) { + throw new Error(`Tool not found: ${toolId}`); } - validateParams(params: Record): boolean | string { - if (!params.url) { - return 'Missing required parameter: url'; - } - return true; - } + try { + // Get the URL (which might be a function or string) + const url = typeof tool.request.url === 'function' + ? tool.request.url(params) + : tool.request.url; - async execute(params: Record): Promise> { - const response = await this.service.request({ - url: params.url, - method: params.method || 'GET', - headers: params.headers || {}, - body: params.body, - timeout: params.timeout + // Make the HTTP request + 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 }); - return { - data: response.data, - status: response.status, - headers: response.headers - }; - } -} + if (!response.ok) { + const error = await response.json(); + throw new Error(tool.transformError(error)); + } -export const toolRegistry: ToolRegistry = { - model: new ModelTool(), - http: new HttpTool() -}; \ No newline at end of file + const data = await response.json(); + return tool.transformResponse(data); + } catch (error) { + throw new Error(tool.transformError(error)); + } +} \ No newline at end of file diff --git a/tools/salesforce/opportunities.ts b/tools/salesforce/opportunities.ts new file mode 100644 index 0000000000..5a95ba6c48 --- /dev/null +++ b/tools/salesforce/opportunities.ts @@ -0,0 +1,172 @@ +import { ToolConfig } from '../types'; + +interface OpportunityParams { + instanceUrl: string; + accessToken: string; + name: string; + accountId?: string; + stage?: string; + amount?: number; + closeDate?: string; + probability?: number; + description?: string; + id?: string; + properties?: Record; + query?: string; + limit?: number; +} + +interface OpportunityResponse { + id: string; + name: string; + accountId?: string; + stage?: string; + amount?: number; + closeDate?: string; + probability?: number; + description?: string; + createdDate: string; + lastModifiedDate: string; + [key: string]: any; +} + +export const opportunitiesTool: ToolConfig = { + id: 'salesforce.opportunities', + name: 'Salesforce Opportunities', + description: 'Manage Salesforce opportunities - create, query, and update opportunity records', + version: '1.0.0', + + params: { + instanceUrl: { + type: 'string', + required: true, + description: 'Salesforce instance URL' + }, + accessToken: { + type: 'string', + required: true, + description: 'Salesforce access token' + }, + name: { + type: 'string', + required: true, + description: 'Opportunity name' + }, + accountId: { + type: 'string', + description: 'Associated account ID' + }, + stage: { + type: 'string', + description: 'Opportunity stage' + }, + amount: { + type: 'number', + description: 'Opportunity amount' + }, + closeDate: { + type: 'string', + description: 'Expected close date (YYYY-MM-DD)' + }, + probability: { + type: 'number', + description: 'Probability of closing (%)' + }, + description: { + type: 'string', + description: 'Opportunity description' + }, + id: { + type: 'string', + description: 'Opportunity ID (required for updates)' + }, + properties: { + type: 'object', + description: 'Additional opportunity fields' + }, + query: { + type: 'string', + description: 'SOQL query for searching opportunities' + }, + limit: { + type: 'number', + default: 100, + description: 'Maximum number of records to return' + } + }, + + request: { + url: (params) => { + const baseUrl = `${params.instanceUrl}/services/data/v58.0/sobjects/Opportunity`; + if (params.query) { + return `${params.instanceUrl}/services/data/v58.0/query?q=${encodeURIComponent(params.query)}`; + } + if (params.id) { + return `${baseUrl}/${params.id}`; + } + return baseUrl; + }, + method: 'POST', + headers: (params) => ({ + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${params.accessToken}` + }), + body: (params) => { + if (params.query) { + return {}; // Empty body for queries + } + + const fields = { + Name: params.name, + ...(params.accountId && { AccountId: params.accountId }), + ...(params.stage && { StageName: params.stage }), + ...(params.amount && { Amount: params.amount }), + ...(params.closeDate && { CloseDate: params.closeDate }), + ...(params.probability && { Probability: params.probability }), + ...(params.description && { Description: params.description }), + ...params.properties + }; + + return fields; + } + }, + + transformResponse: (data) => { + if (data.records) { + // Query response + return data.records.map((record: any) => ({ + id: record.Id, + name: record.Name, + accountId: record.AccountId, + stage: record.StageName, + amount: record.Amount, + closeDate: record.CloseDate, + probability: record.Probability, + description: record.Description, + createdDate: record.CreatedDate, + lastModifiedDate: record.LastModifiedDate, + ...record + })); + } + // Single record response + return { + id: data.id || data.Id, + name: data.name || data.Name, + accountId: data.accountId || data.AccountId, + stage: data.stage || data.StageName, + amount: data.amount || data.Amount, + closeDate: data.closeDate || data.CloseDate, + probability: data.probability || data.Probability, + description: data.description || data.Description, + createdDate: data.CreatedDate, + lastModifiedDate: data.LastModifiedDate, + ...data + }; + }, + + transformError: (error) => { + const message = error.message || error.error?.message; + const code = error.errorCode || error.error?.errorCode; + return `${message} (${code})`; + } +}; \ No newline at end of file diff --git a/tools/types.ts b/tools/types.ts new file mode 100644 index 0000000000..d8fbcfe8ae --- /dev/null +++ b/tools/types.ts @@ -0,0 +1,29 @@ +export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; + +export interface ToolConfig

{ + // Basic tool identification + id: string; + name: string; + description: string; + version: string; + + // Parameter schema - what this tool accepts + params: Record; + + // Request configuration + request: { + url: string | ((params: P) => string); + method: string; + headers: (params: P) => Record; + body?: (params: P) => Record; + }; + + // Response handling + transformResponse: (data: any) => R; + transformError: (error: any) => string; +} \ No newline at end of file diff --git a/tools/xai/chat.ts b/tools/xai/chat.ts new file mode 100644 index 0000000000..c447a5ad84 --- /dev/null +++ b/tools/xai/chat.ts @@ -0,0 +1,85 @@ +import { ToolConfig } from '../types'; + +interface ChatParams { + apiKey: string; + systemPrompt: string; + model?: string; + temperature?: number; + maxTokens?: number; + topP?: number; + frequencyPenalty?: number; + presencePenalty?: number; +} + +interface ChatResponse { + output: string; + tokens?: number; + model: string; +} + +export const chatTool: ToolConfig = { + id: 'xai.chat', + name: 'xAI Chat', + description: 'Chat with xAI models', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + description: 'xAI API key' + }, + systemPrompt: { + type: 'string', + required: true, + description: 'System prompt to send to the model' + }, + model: { + type: 'string', + default: 'grok-2-latest', + description: 'Model to use' + }, + temperature: { + type: 'number', + default: 0.7, + description: 'Controls randomness in the response' + } + }, + + request: { + url: 'https://api.x.ai/v1/chat/completions', + method: 'POST', + headers: (params) => ({ + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${params.apiKey}` + }), + body: (params) => { + const body = { + model: params.model || 'grok-2-latest', + messages: [ + { role: 'system', content: params.systemPrompt } + ], + temperature: params.temperature, + max_tokens: params.maxTokens, + top_p: params.topP, + frequency_penalty: params.frequencyPenalty, + presence_penalty: params.presencePenalty + }; + return body; + } + }, + + transformResponse: (data) => { + return { + output: data.choices[0].message.content, + tokens: data.usage?.total_tokens, + model: data.model + }; + }, + + transformError: (error) => { + const message = error.error?.message || error.message; + const code = error.error?.type || error.code; + return `${message} (${code})`; + } +}; \ No newline at end of file