Flowise/packages/components/nodes/tools/OpenAPIToolkit/core.ts

279 lines
8.0 KiB
TypeScript

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<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 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, {
timeout: 10000
})
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
}
}