mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-28 03:00:29 -04:00
Modified tools directory to be more generic, updated serializer and executor accordingly
This commit is contained in:
@@ -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<string, any> = {};
|
||||
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<string, any> = {}
|
||||
// Get all values from subBlocks
|
||||
const params: Record<string, any> = {};
|
||||
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
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<string, any>) => {
|
||||
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',
|
||||
|
||||
@@ -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: [
|
||||
{
|
||||
|
||||
@@ -15,6 +15,9 @@ export const FunctionBlock: BlockConfig = {
|
||||
tools: {
|
||||
access: ['function']
|
||||
},
|
||||
inputs: {
|
||||
code: 'string'
|
||||
},
|
||||
subBlocks: [
|
||||
{
|
||||
id: 'code',
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { JSX } from 'react'
|
||||
export type BlockIcon = (props: SVGProps<SVGSVGElement>) => JSX.Element
|
||||
export type BlockCategory = 'basic' | 'advanced'
|
||||
export type OutputType = 'string' | 'number' | 'json' | 'boolean'
|
||||
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<string, any>
|
||||
config?: {
|
||||
tool: (params: Record<string, any>) => string
|
||||
}
|
||||
}
|
||||
inputs?: Record<string, ParamType>
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<string, any>',
|
||||
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<string, any>',
|
||||
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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -16,21 +16,19 @@ export interface Position {
|
||||
y: number;
|
||||
}
|
||||
|
||||
export interface BlockConfig {
|
||||
tool: string;
|
||||
params: Record<string, any>;
|
||||
interface: {
|
||||
inputs: Record<string, string>;
|
||||
outputs: Record<string, string>;
|
||||
};
|
||||
}
|
||||
|
||||
export interface SerializedBlock {
|
||||
id: string;
|
||||
position: Position;
|
||||
config: {
|
||||
// The tool this block uses
|
||||
tool: string;
|
||||
// Tool-specific parameters
|
||||
params: Record<string, any>;
|
||||
// Block's input/output interface
|
||||
interface: {
|
||||
inputs: Record<string, string>;
|
||||
outputs: Record<string, string>;
|
||||
};
|
||||
};
|
||||
// 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;
|
||||
}
|
||||
|
||||
84
tools/anthropic/chat.ts
Normal file
84
tools/anthropic/chat.ts
Normal file
@@ -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<ChatParams, ChatResponse> = {
|
||||
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})`;
|
||||
}
|
||||
};
|
||||
86
tools/google/chat.ts
Normal file
86
tools/google/chat.ts
Normal file
@@ -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<ChatParams, ChatResponse> = {
|
||||
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})`;
|
||||
}
|
||||
};
|
||||
@@ -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<typeof fetch>;
|
||||
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<HttpRequestConfig, 'url' | 'method'> = {
|
||||
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
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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<T>(response: Response): Promise<HttpResponse<T>> {
|
||||
const headers: Record<string, string> = {};
|
||||
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<T = any>(config: HttpRequestConfig): Promise<HttpResponse<T>> {
|
||||
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<T>(response);
|
||||
} finally {
|
||||
if (timeoutId) clearTimeout(timeoutId);
|
||||
}
|
||||
}
|
||||
|
||||
public async get<T = any>(url: string, config: Omit<HttpRequestConfig, 'url' | 'method'> = {}): Promise<HttpResponse<T>> {
|
||||
return this.request<T>({ ...config, url, method: 'GET' });
|
||||
}
|
||||
|
||||
public async post<T = any>(url: string, data?: any, config: Omit<HttpRequestConfig, 'url' | 'method' | 'body'> = {}): Promise<HttpResponse<T>> {
|
||||
return this.request<T>({ ...config, url, method: 'POST', body: data });
|
||||
}
|
||||
|
||||
public async put<T = any>(url: string, data?: any, config: Omit<HttpRequestConfig, 'url' | 'method' | 'body'> = {}): Promise<HttpResponse<T>> {
|
||||
return this.request<T>({ ...config, url, method: 'PUT', body: data });
|
||||
}
|
||||
|
||||
public async delete<T = any>(url: string, config: Omit<HttpRequestConfig, 'url' | 'method'> = {}): Promise<HttpResponse<T>> {
|
||||
return this.request<T>({ ...config, url, method: 'DELETE' });
|
||||
}
|
||||
|
||||
public async patch<T = any>(url: string, data?: any, config: Omit<HttpRequestConfig, 'url' | 'method' | 'body'> = {}): Promise<HttpResponse<T>> {
|
||||
return this.request<T>({ ...config, url, method: 'PATCH', body: data });
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
export interface HttpRequestConfig {
|
||||
url: string;
|
||||
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
|
||||
headers?: Record<string, string>;
|
||||
body?: any;
|
||||
timeout?: number;
|
||||
auth?: {
|
||||
type: 'basic' | 'bearer';
|
||||
token?: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface HttpResponse<T = any> {
|
||||
data: T;
|
||||
status: number;
|
||||
headers: Record<string, string>;
|
||||
statusText: string;
|
||||
}
|
||||
|
||||
export interface HttpError extends Error {
|
||||
status?: number;
|
||||
statusText?: string;
|
||||
data?: any;
|
||||
}
|
||||
151
tools/http/request.ts
Normal file
151
tools/http/request.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import { ToolConfig, HttpMethod } from '../types';
|
||||
|
||||
interface RequestParams {
|
||||
url: string;
|
||||
method?: HttpMethod;
|
||||
headers?: Record<string, string>;
|
||||
body?: any;
|
||||
queryParams?: Record<string, string>;
|
||||
pathParams?: Record<string, string>;
|
||||
formData?: Record<string, string | Blob>;
|
||||
timeout?: number;
|
||||
validateStatus?: (status: number) => boolean;
|
||||
}
|
||||
|
||||
interface RequestResponse {
|
||||
data: any;
|
||||
status: number;
|
||||
headers: Record<string, string>;
|
||||
}
|
||||
|
||||
export const requestTool: ToolConfig<RequestParams, RequestResponse> = {
|
||||
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<string, string> = {
|
||||
...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}`;
|
||||
}
|
||||
};
|
||||
143
tools/hubspot/contacts.ts
Normal file
143
tools/hubspot/contacts.ts
Normal file
@@ -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<string, string>;
|
||||
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<ContactParams, ContactResponse | ContactResponse[]> = {
|
||||
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<string, any>; 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})`;
|
||||
}
|
||||
};
|
||||
@@ -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<typeof fetch>;
|
||||
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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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<string, ModelProvider>;
|
||||
|
||||
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<ModelResponse> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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<ModelResponse> {
|
||||
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<void> {
|
||||
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(', ')}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<ModelResponse> {
|
||||
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<void> {
|
||||
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(', ')}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<ModelResponse> {
|
||||
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<void> {
|
||||
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(', ')}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<ModelResponse> {
|
||||
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<void> {
|
||||
if (!config.apiKey) {
|
||||
throw new Error('xAI API key is required');
|
||||
}
|
||||
if (!config.model.startsWith('grok')) {
|
||||
throw new Error('Invalid xAI model specified');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<ModelResponse>;
|
||||
validateConfig(config: AgentConfig): Promise<void>;
|
||||
}
|
||||
|
||||
export const DEFAULT_MODEL_CONFIGS = {
|
||||
'gpt-4o': { provider: 'openai' },
|
||||
'claude': { provider: 'anthropic' },
|
||||
'gemini': { provider: 'google' },
|
||||
'grok': { provider: 'xai' },
|
||||
'deepseek': { provider: 'deepseek' }
|
||||
} as const;
|
||||
95
tools/openai/chat.ts
Normal file
95
tools/openai/chat.ts
Normal file
@@ -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<ChatParams, ChatResponse> = {
|
||||
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})`;
|
||||
}
|
||||
};
|
||||
@@ -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<string, ToolConfig> = {
|
||||
// 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<string, any>): 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<string, any>): Promise<Record<string, any>> {
|
||||
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<string, any>
|
||||
): Promise<any> {
|
||||
const tool = getTool(toolId);
|
||||
|
||||
constructor() {
|
||||
this.service = HttpService.getInstance();
|
||||
if (!tool) {
|
||||
throw new Error(`Tool not found: ${toolId}`);
|
||||
}
|
||||
|
||||
validateParams(params: Record<string, any>): 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<string, any>): Promise<Record<string, any>> {
|
||||
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()
|
||||
};
|
||||
const data = await response.json();
|
||||
return tool.transformResponse(data);
|
||||
} catch (error) {
|
||||
throw new Error(tool.transformError(error));
|
||||
}
|
||||
}
|
||||
172
tools/salesforce/opportunities.ts
Normal file
172
tools/salesforce/opportunities.ts
Normal file
@@ -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<string, any>;
|
||||
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<OpportunityParams, OpportunityResponse | OpportunityResponse[]> = {
|
||||
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})`;
|
||||
}
|
||||
};
|
||||
29
tools/types.ts
Normal file
29
tools/types.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
|
||||
|
||||
export interface ToolConfig<P = any, R = any> {
|
||||
// Basic tool identification
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
version: string;
|
||||
|
||||
// Parameter schema - what this tool accepts
|
||||
params: Record<string, {
|
||||
type: string;
|
||||
required?: boolean;
|
||||
default?: any;
|
||||
description?: string;
|
||||
}>;
|
||||
|
||||
// Request configuration
|
||||
request: {
|
||||
url: string | ((params: P) => string);
|
||||
method: string;
|
||||
headers: (params: P) => Record<string, string>;
|
||||
body?: (params: P) => Record<string, any>;
|
||||
};
|
||||
|
||||
// Response handling
|
||||
transformResponse: (data: any) => R;
|
||||
transformError: (error: any) => string;
|
||||
}
|
||||
85
tools/xai/chat.ts
Normal file
85
tools/xai/chat.ts
Normal file
@@ -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<ChatParams, ChatResponse> = {
|
||||
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})`;
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user