mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-28 03:00:29 -04:00
511 lines
17 KiB
TypeScript
511 lines
17 KiB
TypeScript
import { useCustomToolsStore } from '@/stores/custom-tools/store'
|
|
import { useEnvironmentStore } from '@/stores/settings/environment/store'
|
|
import { driveDownloadTool, driveListTool, driveUploadTool } from './drive'
|
|
import { exaAnswerTool, exaFindSimilarLinksTool, exaGetContentsTool, exaSearchTool } from './exa'
|
|
import { scrapeTool } from './firecrawl/scrape'
|
|
import { functionExecuteTool, webcontainerExecuteTool } from './function'
|
|
import { githubCommentTool, githubPrTool, githubRepoInfoTool } from './github'
|
|
import { gmailReadTool, gmailSearchTool, gmailSendTool } from './gmail'
|
|
import { guestyGuestTool, guestyReservationTool } from './guesty'
|
|
import { requestTool as httpRequest } from './http/request'
|
|
import { contactsTool as hubspotContacts } from './hubspot/contacts'
|
|
import { readUrlTool } from './jina/reader'
|
|
import { notionReadTool, notionWriteTool } from './notion'
|
|
import { embeddingsTool as openAIEmbeddings } from './openai/embeddings'
|
|
import {
|
|
pineconeFetchTool,
|
|
pineconeGenerateEmbeddingsTool,
|
|
pineconeSearchTextTool,
|
|
pineconeSearchVectorTool,
|
|
pineconeUpsertTextTool,
|
|
} from './pinecone'
|
|
import { redditHotPostsTool } from './reddit'
|
|
import { opportunitiesTool as salesforceOpportunities } from './salesforce/opportunities'
|
|
import { searchTool as serperSearch } from './serper/search'
|
|
import { sheetsReadTool, sheetsUpdateTool, sheetsWriteTool } from './sheets'
|
|
import { slackMessageTool } from './slack/message'
|
|
import { supabaseInsertTool, supabaseQueryTool, supabaseUpdateTool } from './supabase'
|
|
import { tavilyExtractTool, tavilySearchTool } from './tavily'
|
|
import { ToolConfig, ToolResponse } from './types'
|
|
import { formatRequestParams, validateToolRequest } from './utils'
|
|
import { visionTool } from './vision/vision'
|
|
import { whatsappSendMessageTool } from './whatsapp'
|
|
import { xReadTool, xSearchTool, xUserTool, xWriteTool } from './x'
|
|
import { youtubeSearchTool } from './youtube/search'
|
|
|
|
// Registry of all available tools
|
|
export const tools: Record<string, ToolConfig> = {
|
|
openai_embeddings: openAIEmbeddings,
|
|
http_request: httpRequest,
|
|
hubspot_contacts: hubspotContacts,
|
|
salesforce_opportunities: salesforceOpportunities,
|
|
function_execute: functionExecuteTool,
|
|
webcontainer_execute: webcontainerExecuteTool,
|
|
vision_tool: visionTool,
|
|
firecrawl_scrape: scrapeTool,
|
|
jina_readurl: readUrlTool,
|
|
slack_message: slackMessageTool,
|
|
github_repoinfo: githubRepoInfoTool,
|
|
serper_search: serperSearch,
|
|
tavily_search: tavilySearchTool,
|
|
tavily_extract: tavilyExtractTool,
|
|
supabase_query: supabaseQueryTool,
|
|
supabase_insert: supabaseInsertTool,
|
|
supabase_update: supabaseUpdateTool,
|
|
youtube_search: youtubeSearchTool,
|
|
notion_read: notionReadTool,
|
|
notion_write: notionWriteTool,
|
|
gmail_send: gmailSendTool,
|
|
gmail_read: gmailReadTool,
|
|
gmail_search: gmailSearchTool,
|
|
whatsapp_send_message: whatsappSendMessageTool,
|
|
x_write: xWriteTool,
|
|
x_read: xReadTool,
|
|
x_search: xSearchTool,
|
|
x_user: xUserTool,
|
|
pinecone_fetch: pineconeFetchTool,
|
|
pinecone_generate_embeddings: pineconeGenerateEmbeddingsTool,
|
|
pinecone_search_text: pineconeSearchTextTool,
|
|
pinecone_search_vector: pineconeSearchVectorTool,
|
|
pinecone_upsert_text: pineconeUpsertTextTool,
|
|
github_pr: githubPrTool,
|
|
github_comment: githubCommentTool,
|
|
exa_search: exaSearchTool,
|
|
exa_get_contents: exaGetContentsTool,
|
|
exa_find_similar_links: exaFindSimilarLinksTool,
|
|
exa_answer: exaAnswerTool,
|
|
reddit_hot_posts: redditHotPostsTool,
|
|
google_drive_download: driveDownloadTool,
|
|
google_drive_list: driveListTool,
|
|
google_drive_upload: driveUploadTool,
|
|
google_sheets_read: sheetsReadTool,
|
|
google_sheets_write: sheetsWriteTool,
|
|
google_sheets_update: sheetsUpdateTool,
|
|
guesty_reservation: guestyReservationTool,
|
|
guesty_guest: guestyGuestTool,
|
|
}
|
|
|
|
// Get a tool by its ID
|
|
export function getTool(toolId: string): ToolConfig | undefined {
|
|
// Check for built-in tools
|
|
const builtInTool = tools[toolId]
|
|
if (builtInTool) return builtInTool
|
|
console.log('toolId', toolId)
|
|
|
|
// Check if it's a custom tool
|
|
if (toolId.startsWith('custom_')) {
|
|
return getCustomTool(toolId)
|
|
}
|
|
|
|
return undefined
|
|
}
|
|
|
|
// Check if we're running in the browser
|
|
function isBrowser(): boolean {
|
|
return typeof window !== 'undefined'
|
|
}
|
|
|
|
// Check if WebContainer is available
|
|
function isWebContainerAvailable(): boolean {
|
|
return isBrowser() && !!window.crossOriginIsolated
|
|
}
|
|
|
|
// Create a tool config from a custom tool definition
|
|
function getCustomTool(customToolId: string): ToolConfig | undefined {
|
|
// Extract the identifier part (could be UUID or title)
|
|
const identifier = customToolId.replace('custom_', '')
|
|
|
|
const customToolsStore = useCustomToolsStore.getState()
|
|
|
|
// Try to find the tool directly by ID first
|
|
let customTool = customToolsStore.getTool(identifier)
|
|
|
|
// If not found by ID, try to find by title (for backward compatibility)
|
|
if (!customTool) {
|
|
const allTools = customToolsStore.getAllTools()
|
|
customTool = allTools.find((tool) => tool.title === identifier)
|
|
}
|
|
|
|
if (!customTool) {
|
|
console.error(`Custom tool not found: ${identifier}`)
|
|
return undefined
|
|
}
|
|
|
|
// Create a parameter schema from the custom tool schema
|
|
const params: Record<string, any> = {}
|
|
|
|
if (customTool.schema.function?.parameters?.properties) {
|
|
Object.entries(customTool.schema.function.parameters.properties).forEach(([key, config]) => {
|
|
params[key] = {
|
|
type: config.type || 'string',
|
|
required: customTool.schema.function.parameters.required?.includes(key) || false,
|
|
requiredForToolCall: customTool.schema.function.parameters.required?.includes(key) || false,
|
|
description: config.description || '',
|
|
}
|
|
})
|
|
}
|
|
|
|
// Create a tool config for the custom tool
|
|
return {
|
|
id: customToolId,
|
|
name: customTool.title,
|
|
description: customTool.schema.function?.description || '',
|
|
version: '1.0.0',
|
|
params,
|
|
|
|
// Request configuration - for custom tools we'll use the execute endpoint
|
|
request: {
|
|
url: '/api/function/execute',
|
|
method: 'POST',
|
|
headers: () => ({ 'Content-Type': 'application/json' }),
|
|
body: (params: Record<string, any>) => {
|
|
// Get environment variables from the store
|
|
const envStore = useEnvironmentStore.getState()
|
|
const allEnvVars = envStore.getAllVariables()
|
|
|
|
// Convert environment variables to a simple key-value object
|
|
const envVars = Object.entries(allEnvVars).reduce(
|
|
(acc, [key, variable]) => {
|
|
acc[key] = variable.value
|
|
return acc
|
|
},
|
|
{} as Record<string, string>
|
|
)
|
|
|
|
// Include everything needed for execution
|
|
return {
|
|
code: customTool.code,
|
|
params: params, // These will be available in the VM context
|
|
schema: customTool.schema.function.parameters, // For validation on the client side
|
|
envVars: envVars, // Pass environment variables for server-side resolution
|
|
}
|
|
},
|
|
isInternalRoute: true,
|
|
},
|
|
|
|
// Direct execution support for browser environment with WebContainer
|
|
directExecution: async (params: Record<string, any>) => {
|
|
// If there's no code, we can't execute directly
|
|
if (!customTool.code) {
|
|
return {
|
|
success: false,
|
|
output: {},
|
|
error: 'No code provided for tool execution',
|
|
}
|
|
}
|
|
|
|
// If we're in a browser with WebContainer available, use it
|
|
if (isWebContainerAvailable()) {
|
|
try {
|
|
// Get environment variables from the store
|
|
const envStore = useEnvironmentStore.getState()
|
|
const envVars = envStore.getAllVariables()
|
|
|
|
// Create a merged params object that includes environment variables
|
|
const mergedParams = { ...params }
|
|
|
|
// Add environment variables to the params
|
|
Object.entries(envVars).forEach(([key, variable]) => {
|
|
if (variable.value && !mergedParams[key]) {
|
|
mergedParams[key] = variable.value
|
|
}
|
|
})
|
|
|
|
// Resolve environment variables and tags in the code
|
|
let resolvedCode = customTool.code
|
|
|
|
// Resolve environment variables with {{var_name}} syntax
|
|
const envVarMatches = resolvedCode.match(/\{\{([^}]+)\}\}/g) || []
|
|
for (const match of envVarMatches) {
|
|
const varName = match.slice(2, -2).trim()
|
|
// Look for the variable in our environment store first, then in params
|
|
const envVar = envVars[varName]
|
|
const varValue = envVar ? envVar.value : mergedParams[varName] || ''
|
|
resolvedCode = resolvedCode.replace(match, varValue)
|
|
}
|
|
|
|
// Resolve tags with <tag_name> syntax
|
|
const tagMatches = resolvedCode.match(/<([^>]+)>/g) || []
|
|
for (const match of tagMatches) {
|
|
const tagName = match.slice(1, -1).trim()
|
|
const tagValue = mergedParams[tagName] || ''
|
|
resolvedCode = resolvedCode.replace(match, tagValue)
|
|
}
|
|
|
|
// Dynamically import the executeCode function
|
|
const { executeCode } = await import('@/lib/webcontainer')
|
|
|
|
// Execute the code with resolved variables
|
|
const result = await executeCode(
|
|
resolvedCode,
|
|
mergedParams, // Use the merged params that include env vars
|
|
5000 // Default timeout
|
|
)
|
|
|
|
if (!result.success) {
|
|
throw new Error(result.error || 'WebContainer execution failed')
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
output: result.output.result || result.output,
|
|
error: undefined,
|
|
}
|
|
} catch (error: any) {
|
|
console.warn('WebContainer execution failed, falling back to API:', error.message)
|
|
// Fall back to API route if WebContainer fails
|
|
return undefined
|
|
}
|
|
}
|
|
|
|
// No WebContainer or not in browser, return undefined to use regular API route
|
|
return undefined
|
|
},
|
|
|
|
// Response handling
|
|
transformResponse: async (response: Response) => {
|
|
const data = await response.json()
|
|
|
|
if (!data.success) {
|
|
throw new Error(data.error || 'Custom tool execution failed')
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
output: data.output.result || data.output,
|
|
error: undefined,
|
|
}
|
|
},
|
|
transformError: (error: any) =>
|
|
`Custom tool execution error: ${error.message || 'Unknown error'}`,
|
|
}
|
|
}
|
|
|
|
// Execute a tool by calling either the proxy for external APIs or directly for internal routes
|
|
export async function executeTool(
|
|
toolId: string,
|
|
params: Record<string, any>,
|
|
skipProxy = false
|
|
): Promise<ToolResponse> {
|
|
try {
|
|
const tool = getTool(toolId)
|
|
console.log('tool', tool)
|
|
|
|
// Validate the tool and its parameters
|
|
validateToolRequest(toolId, tool, params)
|
|
|
|
// After validation, we know tool exists
|
|
if (!tool) {
|
|
throw new Error(`Tool not found: ${toolId}`)
|
|
}
|
|
|
|
// For custom tools, try direct execution in browser first if available
|
|
if (toolId.startsWith('custom_') && tool.directExecution) {
|
|
const directResult = await tool.directExecution(params)
|
|
if (directResult) {
|
|
return directResult
|
|
}
|
|
// If directExecution returns undefined, fall back to API route
|
|
}
|
|
|
|
// For internal routes or when skipProxy is true, call the API directly
|
|
if (tool.request.isInternalRoute || skipProxy) {
|
|
const result = await handleInternalRequest(toolId, tool, params)
|
|
return result
|
|
}
|
|
|
|
// For external APIs, use the proxy
|
|
return await handleProxyRequest(toolId, params)
|
|
} catch (error: any) {
|
|
console.error(`Error executing tool ${toolId}:`, error)
|
|
|
|
// For custom tools, provide more helpful error information
|
|
if (toolId.startsWith('custom_')) {
|
|
const identifier = toolId.replace('custom_', '')
|
|
const allTools = useCustomToolsStore.getState().getAllTools()
|
|
const availableTools = allTools.map((t) => ({ id: t.id, title: t.title }))
|
|
|
|
console.error('Available custom tools:', availableTools)
|
|
console.error(`Looking for custom tool with identifier: ${identifier}`)
|
|
}
|
|
|
|
return {
|
|
success: false,
|
|
output: {},
|
|
error: error.message || 'Unknown error',
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle an internal/direct tool request
|
|
*/
|
|
async function handleInternalRequest(
|
|
toolId: string,
|
|
tool: ToolConfig,
|
|
params: Record<string, any>
|
|
): Promise<ToolResponse> {
|
|
// Format the request parameters
|
|
const requestParams = formatRequestParams(tool, params)
|
|
|
|
try {
|
|
const baseUrl = process.env.NEXT_PUBLIC_APP_URL || ''
|
|
// Handle the case where url may be a function or string
|
|
const endpointUrl =
|
|
typeof tool.request.url === 'function' ? tool.request.url(params) : tool.request.url
|
|
|
|
const fullUrl = new URL(endpointUrl, baseUrl).toString()
|
|
|
|
// For custom tools, validate parameters on the client side before sending
|
|
if (toolId.startsWith('custom_') && tool.request.body) {
|
|
const requestBody = tool.request.body(params)
|
|
if (requestBody.schema && requestBody.params) {
|
|
validateClientSideParams(requestBody.params, requestBody.schema)
|
|
}
|
|
}
|
|
|
|
const response = await fetch(fullUrl, {
|
|
method: requestParams.method,
|
|
headers: requestParams.headers,
|
|
body: requestParams.body,
|
|
})
|
|
console.log('response', response)
|
|
|
|
if (!response.ok) {
|
|
let errorData
|
|
try {
|
|
errorData = await response.json()
|
|
} catch (e) {
|
|
errorData = { error: response.statusText }
|
|
}
|
|
|
|
throw new Error(errorData.error || `Request failed with status ${response.status}`)
|
|
}
|
|
|
|
// Use the tool's response transformer if available
|
|
if (tool.transformResponse) {
|
|
return await tool.transformResponse(response)
|
|
}
|
|
|
|
// Default response handling
|
|
const data = await response.json()
|
|
return {
|
|
success: true,
|
|
output: data.output || data,
|
|
error: undefined,
|
|
}
|
|
} catch (error: any) {
|
|
// Use the tool's error transformer if available
|
|
if (tool.transformError) {
|
|
return {
|
|
success: false,
|
|
output: {},
|
|
error: tool.transformError(error),
|
|
}
|
|
}
|
|
|
|
return {
|
|
success: false,
|
|
output: {},
|
|
error: error.message || 'Request failed',
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validates parameters on the client side before sending to the execute endpoint
|
|
*/
|
|
function validateClientSideParams(
|
|
params: Record<string, any>,
|
|
schema: { type: string; properties: Record<string, any>; required?: string[] }
|
|
) {
|
|
if (!schema || schema.type !== 'object') {
|
|
throw new Error('Invalid schema format')
|
|
}
|
|
|
|
// Check required parameters
|
|
if (schema.required) {
|
|
for (const requiredParam of schema.required) {
|
|
if (!(requiredParam in params)) {
|
|
throw new Error(`Required parameter missing: ${requiredParam}`)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check parameter types (basic validation)
|
|
for (const [paramName, paramValue] of Object.entries(params)) {
|
|
const paramSchema = schema.properties[paramName]
|
|
if (!paramSchema) {
|
|
throw new Error(`Unknown parameter: ${paramName}`)
|
|
}
|
|
|
|
// Basic type checking
|
|
const type = paramSchema.type
|
|
if (type === 'string' && typeof paramValue !== 'string') {
|
|
throw new Error(`Parameter ${paramName} should be a string`)
|
|
} else if (type === 'number' && typeof paramValue !== 'number') {
|
|
throw new Error(`Parameter ${paramName} should be a number`)
|
|
} else if (type === 'boolean' && typeof paramValue !== 'boolean') {
|
|
throw new Error(`Parameter ${paramName} should be a boolean`)
|
|
} else if (type === 'array' && !Array.isArray(paramValue)) {
|
|
throw new Error(`Parameter ${paramName} should be an array`)
|
|
} else if (type === 'object' && (typeof paramValue !== 'object' || paramValue === null)) {
|
|
throw new Error(`Parameter ${paramName} should be an object`)
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle a request via the proxy
|
|
*/
|
|
async function handleProxyRequest(
|
|
toolId: string,
|
|
params: Record<string, any>
|
|
): Promise<ToolResponse> {
|
|
const baseUrl = process.env.NEXT_PUBLIC_APP_URL
|
|
if (!baseUrl) {
|
|
throw new Error('NEXT_PUBLIC_APP_URL environment variable is not set')
|
|
}
|
|
|
|
// If we have a credential parameter, fetch the access token
|
|
if (params.credential) {
|
|
try {
|
|
const response = await fetch(`${baseUrl}/api/auth/oauth/token`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ credentialId: params.credential }),
|
|
})
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Failed to fetch access token')
|
|
}
|
|
|
|
const data = await response.json()
|
|
params.accessToken = data.accessToken
|
|
delete params.credential
|
|
} catch (error) {
|
|
console.error('Error fetching access token:', error)
|
|
throw error
|
|
}
|
|
}
|
|
|
|
const proxyUrl = new URL('/api/proxy', baseUrl).toString()
|
|
const response = await fetch(proxyUrl, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ toolId, params }),
|
|
})
|
|
|
|
const result = await response.json()
|
|
|
|
if (!result.success) {
|
|
return {
|
|
success: false,
|
|
output: {},
|
|
error: result.error,
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|