mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
Modified tools directory to be more generic, updated serializer and executor accordingly
This commit is contained in:
@@ -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})`;
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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}`;
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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})`;
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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