Modified tools directory to be more generic, updated serializer and executor accordingly

This commit is contained in:
Waleed Latif
2025-01-17 19:08:40 -08:00
parent 85a2528e4d
commit 07cb295200
30 changed files with 1234 additions and 1194 deletions
+84
View 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
View 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})`;
}
};
-120
View File
@@ -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
});
});
});
-108
View File
@@ -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 });
}
}
-26
View File
@@ -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
View 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
View 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');
});
});
});
-62
View File
@@ -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(', ')}`);
}
}
}
-48
View File
@@ -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(', ')}`);
}
}
}
-47
View File
@@ -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(', ')}`);
}
}
}
-50
View File
@@ -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');
}
}
}
-17
View File
@@ -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;
}
-25
View File
@@ -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
View 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})`;
}
};
+52 -65
View File
@@ -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
View 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
View 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
View 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})`;
}
};