mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
feat(custom-functions): add support for custom functions, tested with & without code provided
This commit is contained in:
@@ -12,7 +12,7 @@ export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const body = await req.json()
|
||||
|
||||
const { code, timeout = 3000 } = body
|
||||
const { code, params = {}, timeout = 3000 } = body
|
||||
|
||||
// Check if code contains unresolved template variables
|
||||
if (code.includes('<') && code.includes('>')) {
|
||||
@@ -23,6 +23,7 @@ export async function POST(req: NextRequest) {
|
||||
|
||||
// Create a secure context with console logging
|
||||
const context = createContext({
|
||||
params,
|
||||
console: {
|
||||
log: (...args: any[]) => {
|
||||
const logMessage =
|
||||
@@ -42,7 +43,6 @@ export async function POST(req: NextRequest) {
|
||||
},
|
||||
})
|
||||
|
||||
// Create and run the script
|
||||
const script = new Script(`
|
||||
(async () => {
|
||||
try {
|
||||
|
||||
@@ -69,6 +69,22 @@ export class AgentBlockHandler implements BlockHandler {
|
||||
const formattedTools = Array.isArray(inputs.tools)
|
||||
? inputs.tools
|
||||
.map((tool: any) => {
|
||||
// Handle custom tools
|
||||
if (tool.type === 'custom-tool' && tool.schema) {
|
||||
return {
|
||||
id: `custom_${tool.title}`,
|
||||
name: tool.schema.function.name,
|
||||
description: tool.schema.function.description || '',
|
||||
params: tool.params || {},
|
||||
parameters: {
|
||||
type: tool.schema.function.parameters.type,
|
||||
properties: tool.schema.function.parameters.properties,
|
||||
required: tool.schema.function.parameters.required || [],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Handle regular block tools
|
||||
const blockFound = getAllBlocks().find((b) => b.type === tool.type)
|
||||
const toolId = blockFound?.tools.access[0]
|
||||
if (!toolId) return null
|
||||
|
||||
+36
-1
@@ -1,9 +1,10 @@
|
||||
import { useCustomToolsStore } from '@/stores/custom-tools/store'
|
||||
import { anthropicProvider } from './anthropic'
|
||||
import { cerebrasProvider } from './cerebras'
|
||||
import { deepseekProvider } from './deepseek'
|
||||
import { googleProvider } from './google'
|
||||
import { openaiProvider } from './openai'
|
||||
import { ProviderConfig, ProviderId } from './types'
|
||||
import { ProviderConfig, ProviderId, ProviderToolConfig } from './types'
|
||||
import { xAIProvider } from './xai'
|
||||
|
||||
/**
|
||||
@@ -192,3 +193,37 @@ export function extractAndParseJSON(content: string): any {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms a custom tool schema into a provider tool config
|
||||
*/
|
||||
export function transformCustomTool(customTool: any): ProviderToolConfig {
|
||||
const schema = customTool.schema
|
||||
|
||||
if (!schema || !schema.function) {
|
||||
throw new Error('Invalid custom tool schema')
|
||||
}
|
||||
|
||||
return {
|
||||
id: `custom_${customTool.id}`, // Prefix with 'custom_' to identify custom tools
|
||||
name: schema.function.name,
|
||||
description: schema.function.description || '',
|
||||
params: {}, // This will be derived from parameters
|
||||
parameters: {
|
||||
type: schema.function.parameters.type,
|
||||
properties: schema.function.parameters.properties,
|
||||
required: schema.function.parameters.required || [],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all available custom tools as provider tool configs
|
||||
*/
|
||||
export function getCustomTools(): ProviderToolConfig[] {
|
||||
// Get custom tools from the store
|
||||
const customTools = useCustomToolsStore.getState().getAllTools()
|
||||
|
||||
// Transform each custom tool into a provider tool config
|
||||
return customTools.map(transformCustomTool)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import OpenAI from 'openai'
|
||||
import { executeTool } from '@/tools'
|
||||
import { ProviderConfig, ProviderRequest, ProviderResponse } from '../types'
|
||||
|
||||
export const xAIProvider: ProviderConfig = {
|
||||
@@ -141,13 +142,7 @@ export const xAIProvider: ProviderConfig = {
|
||||
const tool = request.tools?.find((t) => t.id === toolName)
|
||||
if (!tool) continue
|
||||
|
||||
const { executeTool } = await import('@/tools')
|
||||
const mergedArgs = { ...tool.params, ...toolArgs }
|
||||
console.log(`Merged tool args for ${toolName}:`, {
|
||||
toolParams: tool.params,
|
||||
llmArgs: toolArgs,
|
||||
mergedArgs,
|
||||
})
|
||||
const result = await executeTool(toolName, mergedArgs, true)
|
||||
|
||||
if (!result.success) continue
|
||||
|
||||
+29
-6
@@ -28,13 +28,36 @@ export class Serializer {
|
||||
throw new Error(`Invalid block type: ${block.type}`)
|
||||
}
|
||||
|
||||
// Get tool ID from block config
|
||||
const toolId = blockConfig.tools.config?.tool
|
||||
? blockConfig.tools.config.tool(this.extractParams(block))
|
||||
: blockConfig.tools.access[0]
|
||||
|
||||
// Extract params from subBlocks
|
||||
// Check if this is an agent block with custom tools
|
||||
const params = this.extractParams(block)
|
||||
let toolId = ''
|
||||
|
||||
if (block.type === 'agent' && params.tools) {
|
||||
// Process the tools in the agent block
|
||||
try {
|
||||
const tools = Array.isArray(params.tools) ? params.tools : JSON.parse(params.tools)
|
||||
|
||||
// If there are custom tools, we just keep them as is
|
||||
// They'll be handled by the executor during runtime
|
||||
|
||||
// For non-custom tools, we determine the tool ID
|
||||
const nonCustomTools = tools.filter((tool: any) => tool.type !== 'custom-tool')
|
||||
if (nonCustomTools.length > 0) {
|
||||
toolId = blockConfig.tools.config?.tool
|
||||
? blockConfig.tools.config.tool(params)
|
||||
: blockConfig.tools.access[0]
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error processing tools in agent block:', error)
|
||||
// Default to the first tool if we can't process tools
|
||||
toolId = blockConfig.tools.access[0]
|
||||
}
|
||||
} else {
|
||||
// For non-agent blocks, get tool ID from block config as usual
|
||||
toolId = blockConfig.tools.config?.tool
|
||||
? blockConfig.tools.config.tool(params)
|
||||
: blockConfig.tools.access[0]
|
||||
}
|
||||
|
||||
// Get inputs from block config
|
||||
const inputs: Record<string, any> = {}
|
||||
|
||||
+204
-20
@@ -1,3 +1,4 @@
|
||||
import { useCustomToolsStore } from '@/stores/custom-tools/store'
|
||||
import { visionTool as crewAIVision } from './crewai/vision'
|
||||
import { scrapeTool } from './firecrawl/scrape'
|
||||
import { functionExecuteTool as functionExecute } from './function/execute'
|
||||
@@ -67,7 +68,94 @@ export const tools: Record<string, ToolConfig> = {
|
||||
|
||||
// Get a tool by its ID
|
||||
export function getTool(toolId: string): ToolConfig | undefined {
|
||||
return tools[toolId]
|
||||
// Check for built-in tools
|
||||
const builtInTool = tools[toolId]
|
||||
if (builtInTool) return builtInTool
|
||||
|
||||
// Check if it's a custom tool
|
||||
if (toolId.startsWith('custom_')) {
|
||||
return getCustomTool(toolId)
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
// 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/execute',
|
||||
method: 'POST',
|
||||
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||
body: (params: Record<string, any>) => {
|
||||
// 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
|
||||
}
|
||||
},
|
||||
isInternalRoute: true,
|
||||
},
|
||||
|
||||
// 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
|
||||
@@ -78,10 +166,6 @@ export async function executeTool(
|
||||
): Promise<ToolResponse> {
|
||||
try {
|
||||
const tool = getTool(toolId)
|
||||
console.log(`Tool being called: ${toolId}`, {
|
||||
params: { ...params, apiKey: params.apiKey ? '[REDACTED]' : undefined },
|
||||
skipProxy,
|
||||
})
|
||||
|
||||
// Validate the tool and its parameters
|
||||
validateToolRequest(toolId, tool, params)
|
||||
@@ -93,21 +177,25 @@ export async function executeTool(
|
||||
|
||||
// For internal routes or when skipProxy is true, call the API directly
|
||||
if (tool.request.isInternalRoute || skipProxy) {
|
||||
console.log(`Calling internal request for ${toolId}`)
|
||||
const result = await handleInternalRequest(toolId, tool, params)
|
||||
console.log(`Tool ${toolId} execution result:`, {
|
||||
success: result.success,
|
||||
outputKeys: result.success ? Object.keys(result.output) : [],
|
||||
error: result.error,
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
// For external APIs, use the proxy
|
||||
console.log(`Calling proxy request for ${toolId}`)
|
||||
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: {},
|
||||
@@ -124,17 +212,113 @@ async function handleInternalRequest(
|
||||
tool: ToolConfig,
|
||||
params: Record<string, any>
|
||||
): Promise<ToolResponse> {
|
||||
// Log the request for debugging
|
||||
console.log(`Executing tool ${toolId} with params:`, {
|
||||
toolId,
|
||||
params: { ...params, apiKey: params.apiKey ? '[REDACTED]' : undefined },
|
||||
})
|
||||
|
||||
// Format the request parameters
|
||||
const requestParams = formatRequestParams(tool, params)
|
||||
|
||||
// Execute the request
|
||||
return await executeRequest(toolId, tool, requestParams)
|
||||
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,
|
||||
})
|
||||
|
||||
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`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -66,14 +66,8 @@ export async function executeRequest(
|
||||
try {
|
||||
const { url, method, headers, body } = requestParams
|
||||
|
||||
// Log the request for debugging
|
||||
console.log(`Executing tool ${toolId}:`, { url, method })
|
||||
|
||||
const externalResponse = await fetch(url, { method, headers, body })
|
||||
|
||||
// Log response status
|
||||
console.log(`${toolId} response status:`, externalResponse.status, externalResponse.statusText)
|
||||
|
||||
if (!externalResponse.ok) {
|
||||
let errorContent
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user