Files
Flowise/packages/components/nodes/tools/OpenAPIToolkit/core.ts
T
owengo e58c8b953d Several features for OpenAPI toolkit and OpenAI Assistants (#3989)
* Allows 'x-strict' attribute in OpenAPI spec tool and other json spec objects, this allows the OpenAI Assistant to have function calls with 'strict' mode. Also allows the OpenAI assistant to call several tools in the same run. And adds a checkbox 'remove Nulls' for the OpenAPI toolkit so that parameters with null values are not passed to the backend api.

* fix lint errors

---------

Co-authored-by: Olivier Schiavo <olivier.schiavo@wengo.com>
2025-02-28 12:05:57 +00:00

297 lines
8.6 KiB
TypeScript

import { z } from 'zod'
import { RequestInit } from 'node-fetch'
import { NodeVM } from '@flowiseai/nodevm'
import { RunnableConfig } from '@langchain/core/runnables'
import { StructuredTool, ToolParams } from '@langchain/core/tools'
import { CallbackManagerForToolRun, Callbacks, CallbackManager, parseCallbackConfigArg } from '@langchain/core/callbacks/manager'
import { availableDependencies, defaultAllowBuiltInDep, prepareSandboxVars } from '../../../src/utils'
import { ICommonObject } from '../../../src/Interface'
const removeNulls = (obj: Record<string, any>) => {
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<string, any>
QueryParameters?: Record<string, any>
RequestBody?: Record<string, any>
}
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.<variable-name>\`
- **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<string, string>)
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<any, any, any, any> = z.ZodObject<any, any, any, any>
> extends BaseDynamicToolInput {
func?: (input: z.infer<T>, runManager?: CallbackManagerForToolRun) => Promise<string>
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<any, any, any, any> = z.ZodObject<any, any, any, any>
> 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<T>) {
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<T>,
configArg?: RunnableConfig | Callbacks,
tags?: string[],
flowConfig?: { sessionId?: string; chatId?: string; input?: string; state?: ICommonObject }
): Promise<string> {
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<T>,
_?: CallbackManagerForToolRun,
flowConfig?: { sessionId?: string; chatId?: string; input?: string; state?: ICommonObject }
): Promise<string> {
let sandbox: any = {
util: undefined,
Symbol: undefined,
child_process: undefined,
fs: undefined,
process: undefined
}
let processedArg = { ...arg }
if (this.removeNulls && typeof processedArg === 'object' && processedArg !== null) {
processedArg = removeNulls(processedArg)
}
if (typeof processedArg === 'object' && Object.keys(processedArg).length) {
for (const item in processedArg) {
sandbox[`$${item}`] = processedArg[item]
}
}
sandbox['$vars'] = prepareSandboxVars(this.variables)
// inject flow properties
if (this.flowObj) {
sandbox['$flow'] = { ...this.flowObj, ...flowConfig }
}
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)
}
sandbox['$options'] = callOptions
const completeUrl = getUrl(this.baseUrl, arg)
sandbox['$url'] = completeUrl
const builtinDeps = process.env.TOOL_FUNCTION_BUILTIN_DEP
? defaultAllowBuiltInDep.concat(process.env.TOOL_FUNCTION_BUILTIN_DEP.split(','))
: defaultAllowBuiltInDep
const externalDeps = process.env.TOOL_FUNCTION_EXTERNAL_DEP ? process.env.TOOL_FUNCTION_EXTERNAL_DEP.split(',') : []
const deps = availableDependencies.concat(externalDeps)
const options = {
console: 'inherit',
sandbox,
require: {
external: { modules: deps },
builtin: builtinDeps
},
eval: false,
wasm: false,
timeout: 10000
} as any
const vm = new NodeVM(options)
const response = await vm.run(`module.exports = async function() {${this.customCode || defaultCode}}()`, __dirname)
return response
}
setVariables(variables: any[]) {
this.variables = variables
}
setFlowObject(flow: any) {
this.flowObj = flow
}
isStrict(): boolean {
return this.strict === true
}
}