import { z } from 'zod' import { RequestInit } from 'node-fetch' import { RunnableConfig } from '@langchain/core/runnables' import { StructuredTool, ToolParams } from '@langchain/core/tools' import { CallbackManagerForToolRun, Callbacks, CallbackManager, parseCallbackConfigArg } from '@langchain/core/callbacks/manager' import { executeJavaScriptCode, createCodeExecutionSandbox } from '../../../src/utils' import { ICommonObject } from '../../../src/Interface' const removeNulls = (obj: Record) => { Object.keys(obj).forEach((key) => { if (obj[key] === null) { delete obj[key] } else if (typeof obj[key] === 'object' && obj[key] !== null) { removeNulls(obj[key]) if (Object.keys(obj[key]).length === 0) { delete obj[key] } } }) return obj } interface HttpRequestObject { PathParameters?: Record QueryParameters?: Record RequestBody?: Record } export const defaultCode = `const fetch = require('node-fetch'); const url = $url; const options = $options; try { const response = await fetch(url, options); const resp = await response.json(); return JSON.stringify(resp); } catch (error) { console.error(error); return ''; } ` export const howToUseCode = `- **Libraries:** You can use any libraries imported in Flowise. - **Tool Input Arguments:** Tool input arguments are available as the following variables: - \`$PathParameters\` - \`$QueryParameters\` - \`$RequestBody\` - **HTTP Requests:** By default, you can get the following values for making HTTP requests: - \`$url\` - \`$options\` - **Default Flow Config:** You can access the default flow configuration using these variables: - \`$flow.sessionId\` - \`$flow.chatId\` - \`$flow.chatflowId\` - \`$flow.input\` - \`$flow.state\` - **Custom Variables:** You can get custom variables using the syntax: - \`$vars.\` - **Return Value:** The function must return a **string** value at the end. \`\`\`js ${defaultCode} \`\`\` ` const getUrl = (baseUrl: string, requestObject: HttpRequestObject) => { let url = baseUrl // Add PathParameters to URL if present if (requestObject.PathParameters) { for (const [key, value] of Object.entries(requestObject.PathParameters)) { url = url.replace(`{${key}}`, encodeURIComponent(String(value))) } } // Add QueryParameters to URL if present if (requestObject.QueryParameters) { const queryParams = new URLSearchParams(requestObject.QueryParameters as Record) url += `?${queryParams.toString()}` } return url } class ToolInputParsingException extends Error { output?: string constructor(message: string, output?: string) { super(message) this.output = output } } export interface BaseDynamicToolInput extends ToolParams { name: string description: string returnDirect?: boolean } export interface DynamicStructuredToolInput< // eslint-disable-next-line T extends z.ZodObject = z.ZodObject > extends BaseDynamicToolInput { func?: (input: z.infer, runManager?: CallbackManagerForToolRun) => Promise schema: T baseUrl: string method: string headers: ICommonObject customCode?: string strict?: boolean removeNulls?: boolean } export class DynamicStructuredTool< // eslint-disable-next-line T extends z.ZodObject = z.ZodObject > extends StructuredTool { name: string description: string baseUrl: string method: string headers: ICommonObject customCode?: string strict?: boolean func: DynamicStructuredToolInput['func'] // @ts-ignore schema: T private variables: any[] private flowObj: any private removeNulls: boolean constructor(fields: DynamicStructuredToolInput) { super(fields) this.name = fields.name this.description = fields.description this.func = fields.func this.returnDirect = fields.returnDirect ?? this.returnDirect this.schema = fields.schema this.baseUrl = fields.baseUrl this.method = fields.method this.headers = fields.headers this.customCode = fields.customCode this.strict = fields.strict this.removeNulls = fields.removeNulls ?? false } async call( arg: z.output, configArg?: RunnableConfig | Callbacks, tags?: string[], flowConfig?: { sessionId?: string; chatId?: string; input?: string; state?: ICommonObject } ): Promise { const config = parseCallbackConfigArg(configArg) if (config.runName === undefined) { config.runName = this.name } let parsed try { parsed = await this.schema.parseAsync(arg) } catch (e) { throw new ToolInputParsingException(`Received tool input did not match expected schema ${e}`, JSON.stringify(arg)) } const callbackManager_ = await CallbackManager.configure( config.callbacks, this.callbacks, config.tags || tags, this.tags, config.metadata, this.metadata, { verbose: this.verbose } ) const runManager = await callbackManager_?.handleToolStart( this.toJSON(), typeof parsed === 'string' ? parsed : JSON.stringify(parsed), undefined, undefined, undefined, undefined, config.runName ) let result try { result = await this._call(parsed, runManager, flowConfig) } catch (e) { await runManager?.handleToolError(e) throw e } if (result && typeof result !== 'string') { result = JSON.stringify(result) } await runManager?.handleToolEnd(result) return result } // @ts-ignore protected async _call( arg: z.output, _?: CallbackManagerForToolRun, flowConfig?: { sessionId?: string; chatId?: string; input?: string; state?: ICommonObject } ): Promise { let processedArg = { ...arg } if (this.removeNulls && typeof processedArg === 'object' && processedArg !== null) { processedArg = removeNulls(processedArg) } // Create additional sandbox variables for tool arguments const additionalSandbox: ICommonObject = {} if (typeof processedArg === 'object' && Object.keys(processedArg).length) { for (const item in processedArg) { additionalSandbox[`$${item}`] = processedArg[item] } } // Prepare HTTP request options const callOptions: RequestInit = { method: this.method, headers: { 'Content-Type': 'application/json', ...this.headers } } if (arg.RequestBody && this.method.toUpperCase() !== 'GET') { callOptions.body = JSON.stringify(arg.RequestBody) } additionalSandbox['$options'] = callOptions // Generate complete URL const completeUrl = getUrl(this.baseUrl, arg) additionalSandbox['$url'] = completeUrl // Prepare flow object for sandbox const flow = this.flowObj ? { ...this.flowObj, ...flowConfig } : {} const sandbox = createCodeExecutionSandbox('', this.variables || [], flow, additionalSandbox) let response = await executeJavaScriptCode(this.customCode || defaultCode, sandbox) if (typeof response === 'object') { response = JSON.stringify(response) } return response } setVariables(variables: any[]) { this.variables = variables } setFlowObject(flow: any) { this.flowObj = flow } isStrict(): boolean { return this.strict === true } }