Chore/Update issue templates and add new tools (#4687)

* Enhancement: Update issue templates and add new tools

- Updated bug report template to include a default label of 'bug'.
- Updated feature request template to include a default label of 'enhancement'.
- Added new credential class for Agentflow API.
- Enhanced Agent and HTTP nodes to improve tool management and error handling.
- Added deprecation badges to several agent and chain classes.
- Introduced new tools for handling requests (GET, POST, DELETE, PUT) with improved error handling.
- Added new chatflows and agentflows for various use cases, including document QnA and translation.
- Updated UI components for better handling of agent flows and marketplace interactions.
- Refactored utility functions for improved functionality and clarity.

* Refactor: Remove beta badge and streamline template title assignment

- Removed the 'BETA' badge from the ExtractMetadataRetriever class.
- Simplified the title assignment in the agentflowv2 generator by using a variable instead of inline string manipulation.
This commit is contained in:
Henry Heng
2025-06-19 18:11:24 +01:00
committed by GitHub
parent 15dd28356b
commit a107aa7a77
86 changed files with 9942 additions and 12634 deletions
@@ -0,0 +1,23 @@
import { INodeParams, INodeCredential } from '../src/Interface'
class AgentflowApi implements INodeCredential {
label: string
name: string
version: number
inputs: INodeParams[]
constructor() {
this.label = 'Agentflow API'
this.name = 'agentflowApi'
this.version = 1.0
this.inputs = [
{
label: 'Agentflow Api Key',
name: 'agentflowApiKey',
type: 'password'
}
]
}
}
module.exports = { credClass: AgentflowApi }
@@ -496,18 +496,21 @@ class Agent_Agentflow implements INode {
}
}
const toolInstance = await newToolNodeInstance.init(newNodeData, '', options)
if (tool.agentSelectedToolRequiresHumanInput) {
toolInstance.requiresHumanInput = true
}
// toolInstance might returns a list of tools like MCP tools
if (Array.isArray(toolInstance)) {
for (const subTool of toolInstance) {
const subToolInstance = subTool as Tool
;(subToolInstance as any).agentSelectedTool = tool.agentSelectedTool
if (tool.agentSelectedToolRequiresHumanInput) {
;(subToolInstance as any).requiresHumanInput = true
}
toolsInstance.push(subToolInstance)
}
} else {
if (tool.agentSelectedToolRequiresHumanInput) {
toolInstance.requiresHumanInput = true
}
toolsInstance.push(toolInstance as Tool)
}
}
@@ -929,7 +932,14 @@ class Agent_Agentflow implements INode {
}
// Prepare final response and output object
const finalResponse = (response.content as string) ?? JSON.stringify(response, null, 2)
let finalResponse = ''
if (response.content && Array.isArray(response.content)) {
finalResponse = response.content.map((item: any) => item.text).join('\n')
} else if (response.content && typeof response.content === 'string') {
finalResponse = response.content
} else {
finalResponse = JSON.stringify(response, null, 2)
}
const output = this.prepareOutputObject(
response,
availableTools,
@@ -1374,6 +1384,7 @@ class Agent_Agentflow implements INode {
const usedTools: IUsedTool[] = []
let sourceDocuments: Array<any> = []
let artifacts: any[] = []
let isWaitingForHumanInput: boolean | undefined
// Process each tool call
for (let i = 0; i < response.tool_calls.length; i++) {
@@ -1536,7 +1547,8 @@ class Agent_Agentflow implements INode {
usedTools: recursiveUsedTools,
sourceDocuments: recursiveSourceDocuments,
artifacts: recursiveArtifacts,
totalTokens: recursiveTokens
totalTokens: recursiveTokens,
isWaitingForHumanInput: recursiveIsWaitingForHumanInput
} = await this.handleToolCalls({
response: newResponse,
messages,
@@ -1558,9 +1570,10 @@ class Agent_Agentflow implements INode {
sourceDocuments = [...sourceDocuments, ...recursiveSourceDocuments]
artifacts = [...artifacts, ...recursiveArtifacts]
totalTokens += recursiveTokens
isWaitingForHumanInput = recursiveIsWaitingForHumanInput
}
return { response: newResponse, usedTools, sourceDocuments, artifacts, totalTokens }
return { response: newResponse, usedTools, sourceDocuments, artifacts, totalTokens, isWaitingForHumanInput }
}
/**
@@ -1660,7 +1673,14 @@ class Agent_Agentflow implements INode {
if (humanInput.type === 'reject') {
messages.pop()
toolsInstance = toolsInstance.filter((tool) => tool.name !== toolCall.name)
const toBeRemovedTool = toolsInstance.find((tool) => tool.name === toolCall.name)
if (toBeRemovedTool) {
toolsInstance = toolsInstance.filter((tool) => tool.name !== toolCall.name)
// Remove other tools with the same agentSelectedTool such as MCP tools
toolsInstance = toolsInstance.filter(
(tool) => (tool as any).agentSelectedTool !== (toBeRemovedTool as any).agentSelectedTool
)
}
}
if (humanInput.type === 'proceed') {
let toolIds: ICommonObject | undefined
@@ -336,6 +336,9 @@ class HTTP_Agentflow implements INode {
} catch (error) {
console.error('HTTP Request Error:', error)
const errorMessage =
error.response?.data?.message || error.response?.data?.error || error.message || 'An error occurred during the HTTP request'
// Format error response
const errorResponse: any = {
id: nodeData.id,
@@ -353,7 +356,7 @@ class HTTP_Agentflow implements INode {
},
error: {
name: error.name || 'Error',
message: error.message || 'An error occurred during the HTTP request'
message: errorMessage
},
state
}
@@ -366,7 +369,7 @@ class HTTP_Agentflow implements INode {
errorResponse.error.headers = error.response.headers
}
throw new Error(error)
throw new Error(errorMessage)
}
}
}
+21 -3
View File
@@ -262,6 +262,7 @@ class LLM_Agentflow implements INode {
}`,
description: 'JSON schema for the structured output',
optional: true,
hideCodeExecute: true,
show: {
'llmStructuredOutput[$index].type': 'jsonArray'
}
@@ -486,8 +487,15 @@ class LLM_Agentflow implements INode {
}
// Prepare final response and output object
const finalResponse = (response.content as string) ?? JSON.stringify(response, null, 2)
const output = this.prepareOutputObject(response, finalResponse, startTime, endTime, timeDelta)
let finalResponse = ''
if (response.content && Array.isArray(response.content)) {
finalResponse = response.content.map((item: any) => item.text).join('\n')
} else if (response.content && typeof response.content === 'string') {
finalResponse = response.content
} else {
finalResponse = JSON.stringify(response, null, 2)
}
const output = this.prepareOutputObject(response, finalResponse, startTime, endTime, timeDelta, isStructuredOutput)
// End analytics tracking
if (analyticHandlers && llmIds) {
@@ -853,7 +861,8 @@ class LLM_Agentflow implements INode {
finalResponse: string,
startTime: number,
endTime: number,
timeDelta: number
timeDelta: number,
isStructuredOutput: boolean
): any {
const output: any = {
content: finalResponse,
@@ -872,6 +881,15 @@ class LLM_Agentflow implements INode {
output.usageMetadata = response.usage_metadata
}
if (isStructuredOutput && typeof response === 'object') {
const structuredOutput = response as Record<string, any>
for (const key in structuredOutput) {
if (structuredOutput[key]) {
output[key] = structuredOutput[key]
}
}
}
return output
}
@@ -23,6 +23,7 @@ class AutoGPT_Agents implements INode {
category: string
baseClasses: string[]
inputs: INodeParams[]
badge: string
constructor() {
this.label = 'AutoGPT'
@@ -30,6 +31,7 @@ class AutoGPT_Agents implements INode {
this.version = 2.0
this.type = 'AutoGPT'
this.category = 'Agents'
this.badge = 'DEPRECATING'
this.icon = 'autogpt.svg'
this.description = 'Autonomous agent with chain of thoughts for self-guided task completion'
this.baseClasses = ['AutoGPT']
@@ -15,6 +15,7 @@ class BabyAGI_Agents implements INode {
category: string
baseClasses: string[]
inputs: INodeParams[]
badge: string
constructor() {
this.label = 'BabyAGI'
@@ -23,6 +24,7 @@ class BabyAGI_Agents implements INode {
this.type = 'BabyAGI'
this.category = 'Agents'
this.icon = 'babyagi.svg'
this.badge = 'DEPRECATING'
this.description = 'Task Driven Autonomous Agent which creates new task and reprioritizes task list based on objective'
this.baseClasses = ['BabyAGI']
this.inputs = [
@@ -26,6 +26,7 @@ class GETApiChain_Chains implements INode {
baseClasses: string[]
description: string
inputs: INodeParams[]
badge: string
constructor() {
this.label = 'GET API Chain'
@@ -34,6 +35,7 @@ class GETApiChain_Chains implements INode {
this.type = 'GETApiChain'
this.icon = 'get.svg'
this.category = 'Chains'
this.badge = 'DEPRECATING'
this.description = 'Chain to run queries against GET API'
this.baseClasses = [this.type, ...getBaseClasses(APIChain)]
this.inputs = [
@@ -17,6 +17,7 @@ class OpenApiChain_Chains implements INode {
baseClasses: string[]
description: string
inputs: INodeParams[]
badge: string
constructor() {
this.label = 'OpenAPI Chain'
@@ -25,6 +26,7 @@ class OpenApiChain_Chains implements INode {
this.type = 'OpenAPIChain'
this.icon = 'openapi.svg'
this.category = 'Chains'
this.badge = 'DEPRECATING'
this.description = 'Chain that automatically select and call APIs based only on an OpenAPI spec'
this.baseClasses = [this.type, ...getBaseClasses(APIChain)]
this.inputs = [
@@ -15,6 +15,7 @@ class POSTApiChain_Chains implements INode {
baseClasses: string[]
description: string
inputs: INodeParams[]
badge: string
constructor() {
this.label = 'POST API Chain'
@@ -23,6 +24,7 @@ class POSTApiChain_Chains implements INode {
this.type = 'POSTApiChain'
this.icon = 'post.svg'
this.category = 'Chains'
this.badge = 'DEPRECATING'
this.description = 'Chain to run queries against POST API'
this.baseClasses = [this.type, ...getBaseClasses(APIChain)]
this.inputs = [
@@ -16,11 +16,13 @@ class MultiPromptChain_Chains implements INode {
baseClasses: string[]
description: string
inputs: INodeParams[]
badge: string
constructor() {
this.label = 'Multi Prompt Chain'
this.name = 'multiPromptChain'
this.version = 2.0
this.badge = 'DEPRECATING'
this.type = 'MultiPromptChain'
this.icon = 'prompt.svg'
this.category = 'Chains'
@@ -15,12 +15,14 @@ class MultiRetrievalQAChain_Chains implements INode {
category: string
baseClasses: string[]
description: string
badge: string
inputs: INodeParams[]
constructor() {
this.label = 'Multi Retrieval QA Chain'
this.name = 'multiRetrievalQAChain'
this.version = 2.0
this.badge = 'DEPRECATING'
this.type = 'MultiRetrievalQAChain'
this.icon = 'qa.svg'
this.category = 'Chains'
@@ -17,6 +17,7 @@ class RetrievalQAChain_Chains implements INode {
baseClasses: string[]
description: string
inputs: INodeParams[]
badge: string
constructor() {
this.label = 'Retrieval QA Chain'
@@ -24,6 +25,7 @@ class RetrievalQAChain_Chains implements INode {
this.version = 2.0
this.type = 'RetrievalQAChain'
this.icon = 'qa.svg'
this.badge = 'DEPRECATING'
this.category = 'Chains'
this.description = 'QA chain to answer a question based on the retrieved documents'
this.baseClasses = [this.type, ...getBaseClasses(RetrievalQAChain)]
@@ -17,6 +17,7 @@ class VectorDBQAChain_Chains implements INode {
baseClasses: string[]
description: string
inputs: INodeParams[]
badge: string
constructor() {
this.label = 'VectorDB QA Chain'
@@ -25,6 +26,7 @@ class VectorDBQAChain_Chains implements INode {
this.type = 'VectorDBQAChain'
this.icon = 'vectordb.svg'
this.category = 'Chains'
this.badge = 'DEPRECATING'
this.description = 'QA chain for vector databases'
this.baseClasses = [this.type, ...getBaseClasses(VectorDBQAChain)]
this.inputs = [
@@ -31,7 +31,6 @@ class ExtractMetadataRetriever_Retrievers implements INode {
this.category = 'Retrievers'
this.description = 'Extract keywords/metadata from the query and use it to filter documents'
this.baseClasses = [this.type, 'BaseRetriever']
this.badge = 'BETA'
this.inputs = [
{
label: 'Vector Store',
@@ -0,0 +1,381 @@
import { DataSource } from 'typeorm'
import { z } from 'zod'
import { NodeVM } from '@flowiseai/nodevm'
import { RunnableConfig } from '@langchain/core/runnables'
import { CallbackManagerForToolRun, Callbacks, CallbackManager, parseCallbackConfigArg } from '@langchain/core/callbacks/manager'
import { StructuredTool } from '@langchain/core/tools'
import { ICommonObject, IDatabaseEntity, INode, INodeData, INodeOptionsValue, INodeParams } from '../../../src/Interface'
import { availableDependencies, defaultAllowBuiltInDep, getCredentialData, getCredentialParam } from '../../../src/utils'
import { v4 as uuidv4 } from 'uuid'
class AgentAsTool_Tools implements INode {
label: string
name: string
version: number
description: string
type: string
icon: string
category: string
baseClasses: string[]
credential: INodeParams
inputs: INodeParams[]
constructor() {
this.label = 'Agent as Tool'
this.name = 'agentAsTool'
this.version = 1.0
this.type = 'AgentAsTool'
this.icon = 'agentastool.svg'
this.category = 'Tools'
this.description = 'Use as a tool to execute another agentflow'
this.baseClasses = [this.type, 'Tool']
this.credential = {
label: 'Connect Credential',
name: 'credential',
type: 'credential',
credentialNames: ['agentflowApi'],
optional: true
}
this.inputs = [
{
label: 'Select Agent',
name: 'selectedAgentflow',
type: 'asyncOptions',
loadMethod: 'listAgentflows'
},
{
label: 'Tool Name',
name: 'name',
type: 'string'
},
{
label: 'Tool Description',
name: 'description',
type: 'string',
description: 'Description of what the tool does. This is for LLM to determine when to use this tool.',
rows: 3,
placeholder:
'State of the Union QA - useful for when you need to ask questions about the most recent state of the union address.'
},
{
label: 'Return Direct',
name: 'returnDirect',
type: 'boolean',
optional: true
},
{
label: 'Override Config',
name: 'overrideConfig',
description: 'Override the config passed to the Agentflow.',
type: 'json',
optional: true,
additionalParams: true
},
{
label: 'Base URL',
name: 'baseURL',
type: 'string',
description:
'Base URL to Flowise. By default, it is the URL of the incoming request. Useful when you need to execute the Agentflow through an alternative route.',
placeholder: 'http://localhost:3000',
optional: true,
additionalParams: true
},
{
label: 'Start new session per message',
name: 'startNewSession',
type: 'boolean',
description:
'Whether to continue the session with the Agentflow tool or start a new one with each interaction. Useful for Agentflows with memory if you want to avoid it.',
default: false,
optional: true,
additionalParams: true
},
{
label: 'Use Question from Chat',
name: 'useQuestionFromChat',
type: 'boolean',
description:
'Whether to use the question from the chat as input to the agentflow. If turned on, this will override the custom input.',
optional: true,
additionalParams: true
},
{
label: 'Custom Input',
name: 'customInput',
type: 'string',
description: 'Custom input to be passed to the agentflow. Leave empty to let LLM decides the input.',
optional: true,
additionalParams: true,
show: {
useQuestionFromChat: false
}
}
]
}
//@ts-ignore
loadMethods = {
async listAgentflows(_: INodeData, options: ICommonObject): Promise<INodeOptionsValue[]> {
const returnData: INodeOptionsValue[] = []
const appDataSource = options.appDataSource as DataSource
const databaseEntities = options.databaseEntities as IDatabaseEntity
if (appDataSource === undefined || !appDataSource) {
return returnData
}
const searchOptions = options.searchOptions || {}
const agentflows = await appDataSource.getRepository(databaseEntities['ChatFlow']).findBy({
...searchOptions,
type: 'AGENTFLOW'
})
for (let i = 0; i < agentflows.length; i += 1) {
const data = {
label: agentflows[i].name,
name: agentflows[i].id
} as INodeOptionsValue
returnData.push(data)
}
return returnData
}
}
async init(nodeData: INodeData, input: string, options: ICommonObject): Promise<any> {
const selectedAgentflowId = nodeData.inputs?.selectedAgentflow as string
const _name = nodeData.inputs?.name as string
const description = nodeData.inputs?.description as string
const useQuestionFromChat = nodeData.inputs?.useQuestionFromChat as boolean
const returnDirect = nodeData.inputs?.returnDirect as boolean
const customInput = nodeData.inputs?.customInput as string
const overrideConfig =
typeof nodeData.inputs?.overrideConfig === 'string' &&
nodeData.inputs.overrideConfig.startsWith('{') &&
nodeData.inputs.overrideConfig.endsWith('}')
? JSON.parse(nodeData.inputs.overrideConfig)
: nodeData.inputs?.overrideConfig
const startNewSession = nodeData.inputs?.startNewSession as boolean
const baseURL = (nodeData.inputs?.baseURL as string) || (options.baseURL as string)
const credentialData = await getCredentialData(nodeData.credential ?? '', options)
const agentflowApiKey = getCredentialParam('agentflowApiKey', credentialData, nodeData)
if (selectedAgentflowId === options.chatflowid) throw new Error('Cannot call the same agentflow!')
let headers = {}
if (agentflowApiKey) headers = { Authorization: `Bearer ${agentflowApiKey}` }
let toolInput = ''
if (useQuestionFromChat) {
toolInput = input
} else if (customInput) {
toolInput = customInput
}
let name = _name || 'agentflow_tool'
return new AgentflowTool({
name,
baseURL,
description,
returnDirect,
agentflowid: selectedAgentflowId,
startNewSession,
headers,
input: toolInput,
overrideConfig
})
}
}
class AgentflowTool extends StructuredTool {
static lc_name() {
return 'AgentflowTool'
}
name = 'agentflow_tool'
description = 'Execute another agentflow'
input = ''
agentflowid = ''
startNewSession = false
baseURL = 'http://localhost:3000'
headers = {}
overrideConfig?: object
schema = z.object({
input: z.string().describe('input question')
// overrideConfig: z.record(z.any()).optional().describe('override config'), // This will be passed to the Agent, so comment it for now.
}) as any
constructor({
name,
description,
returnDirect,
input,
agentflowid,
startNewSession,
baseURL,
headers,
overrideConfig
}: {
name: string
description: string
returnDirect: boolean
input: string
agentflowid: string
startNewSession: boolean
baseURL: string
headers: ICommonObject
overrideConfig?: object
}) {
super()
this.name = name
this.description = description
this.input = input
this.baseURL = baseURL
this.startNewSession = startNewSession
this.headers = headers
this.agentflowid = agentflowid
this.overrideConfig = overrideConfig
this.returnDirect = returnDirect
}
async call(
arg: z.infer<typeof this.schema>,
configArg?: RunnableConfig | Callbacks,
tags?: string[],
flowConfig?: { sessionId?: string; chatId?: string; input?: string }
): 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 Error(`Received tool input did not match expected schema: ${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.infer<typeof this.schema>,
_?: CallbackManagerForToolRun,
flowConfig?: { sessionId?: string; chatId?: string; input?: string }
): Promise<string> {
const inputQuestion = this.input || arg.input
const body = {
question: inputQuestion,
chatId: this.startNewSession ? uuidv4() : flowConfig?.chatId,
overrideConfig: {
sessionId: this.startNewSession ? uuidv4() : flowConfig?.sessionId,
...(this.overrideConfig ?? {}),
...(arg.overrideConfig ?? {})
}
}
const options = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'flowise-tool': 'true',
...this.headers
},
body: JSON.stringify(body)
}
let sandbox = {
$callOptions: options,
$callBody: body,
util: undefined,
Symbol: undefined,
child_process: undefined,
fs: undefined,
process: undefined
}
const code = `
const fetch = require('node-fetch');
const url = "${this.baseURL}/api/v1/prediction/${this.agentflowid}";
const body = $callBody;
const options = $callOptions;
try {
const response = await fetch(url, options);
const resp = await response.json();
return resp.text;
} catch (error) {
console.error(error);
return '';
}
`
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 vmOptions = {
console: 'inherit',
sandbox,
require: {
external: { modules: deps },
builtin: builtinDeps
},
eval: false,
wasm: false,
timeout: 10000
} as any
const vm = new NodeVM(vmOptions)
const response = await vm.run(`module.exports = async function() {${code}}()`, __dirname)
return response
}
}
module.exports = { nodeClass: AgentAsTool_Tools }
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-users-group"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M10 13a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" /><path d="M8 21v-1a2 2 0 0 1 2 -2h4a2 2 0 0 1 2 2v1" /><path d="M15 5a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" /><path d="M17 10h2a2 2 0 0 1 2 2v1" /><path d="M5 5a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" /><path d="M3 13v-1a2 2 0 0 1 2 -2h2" /></svg>

After

Width:  |  Height:  |  Size: 588 B

@@ -0,0 +1,144 @@
import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface'
import { getBaseClasses } from '../../../src/utils'
import { ArxivParameters, desc, ArxivTool } from './core'
class Arxiv_Tools implements INode {
label: string
name: string
version: number
description: string
type: string
icon: string
category: string
baseClasses: string[]
inputs: INodeParams[]
constructor() {
this.label = 'Arxiv'
this.name = 'arxiv'
this.version = 1.0
this.type = 'Arxiv'
this.icon = 'arxiv.png'
this.category = 'Tools'
this.description = 'Search and read content from academic papers on Arxiv'
this.baseClasses = [this.type, ...getBaseClasses(ArxivTool)]
this.inputs = [
{
label: 'Name',
name: 'arxivName',
type: 'string',
default: 'arxiv_search',
description: 'Name of the tool',
additionalParams: true,
optional: true
},
{
label: 'Description',
name: 'arxivDescription',
type: 'string',
rows: 4,
default: desc,
description: 'Describe to LLM when it should use this tool',
additionalParams: true,
optional: true
},
{
label: 'Top K Results',
name: 'topKResults',
type: 'number',
description: 'Number of top results to return from Arxiv search',
default: '3',
step: 1,
optional: true,
additionalParams: true
},
{
label: 'Max Query Length',
name: 'maxQueryLength',
type: 'number',
description: 'Maximum length of the search query',
default: '300',
step: 1,
optional: true,
additionalParams: true
},
{
label: 'Max Content Length',
name: 'docContentCharsMax',
type: 'number',
description: 'Maximum length of the returned content. Set to 0 for unlimited',
default: '10000',
step: 1,
optional: true,
additionalParams: true
},
{
label: 'Load Full Content',
name: 'loadFullContent',
type: 'boolean',
description:
'Download PDFs and extract full paper content instead of just summaries. Warning: This is slower and uses more resources.',
default: false,
optional: true,
additionalParams: true
},
{
label: 'Continue On Failure',
name: 'continueOnFailure',
type: 'boolean',
description:
'Continue processing other papers if one fails to download/parse (only applies when Load Full Content is enabled)',
default: false,
optional: true,
additionalParams: true
},
{
label: 'Use Legacy Build',
name: 'legacyBuild',
type: 'boolean',
description: 'Use legacy PDF.js build for PDF parsing (only applies when Load Full Content is enabled)',
default: false,
optional: true,
additionalParams: true
}
]
}
async init(nodeData: INodeData, _: string, options: ICommonObject): Promise<any> {
const name = (nodeData.inputs?.name as string) || (nodeData.inputs?.arxivName as string)
const description = (nodeData.inputs?.description as string) || (nodeData.inputs?.arxivDescription as string)
const topKResults = nodeData.inputs?.topKResults as string
const maxQueryLength = nodeData.inputs?.maxQueryLength as string
const docContentCharsMax = nodeData.inputs?.docContentCharsMax as string
const loadFullContent = nodeData.inputs?.loadFullContent as boolean
const continueOnFailure = nodeData.inputs?.continueOnFailure as boolean
const legacyBuild = nodeData.inputs?.legacyBuild as boolean
let logger
const orgId = options.orgId
if (process.env.DEBUG === 'true') {
logger = options.logger
}
const obj: ArxivParameters = {}
if (description) obj.description = description
if (name)
obj.name = name
.toLowerCase()
.replace(/ /g, '_')
.replace(/[^a-z0-9_-]/g, '')
if (topKResults) obj.topKResults = parseInt(topKResults, 10)
if (maxQueryLength) obj.maxQueryLength = parseInt(maxQueryLength, 10)
if (docContentCharsMax) {
const maxChars = parseInt(docContentCharsMax, 10)
obj.docContentCharsMax = maxChars === 0 ? undefined : maxChars
}
if (loadFullContent !== undefined) obj.loadFullContent = loadFullContent
if (continueOnFailure !== undefined) obj.continueOnFailure = continueOnFailure
if (legacyBuild !== undefined) obj.legacyBuild = legacyBuild
return new ArxivTool(obj, logger, orgId)
}
}
module.exports = { nodeClass: Arxiv_Tools }
Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

@@ -0,0 +1,266 @@
import { z } from 'zod'
import fetch from 'node-fetch'
import { PDFLoader } from '@langchain/community/document_loaders/fs/pdf'
import { DynamicStructuredTool } from '../OpenAPIToolkit/core'
export const desc = `Use this tool to search for academic papers on Arxiv. You can search by keywords, topics, authors, or specific Arxiv IDs. The tool can return either paper summaries or download and extract full paper content.`
export interface ArxivParameters {
topKResults?: number
maxQueryLength?: number
docContentCharsMax?: number
loadFullContent?: boolean
continueOnFailure?: boolean
legacyBuild?: boolean
name?: string
description?: string
}
interface ArxivResult {
id: string
title: string
authors: string[]
summary: string
published: string
updated: string
entryId: string
}
// Schema for Arxiv search
const createArxivSchema = () => {
return z.object({
query: z
.string()
.describe('Search query for Arxiv papers. Can be keywords, topics, authors, or specific Arxiv IDs (e.g., 2301.12345)')
})
}
export class ArxivTool extends DynamicStructuredTool {
topKResults = 3
maxQueryLength = 300
docContentCharsMax = 4000
loadFullContent = false
continueOnFailure = false
legacyBuild = false
logger?: any
orgId?: string
constructor(args?: ArxivParameters, logger?: any, orgId?: string) {
const schema = createArxivSchema()
const toolInput = {
name: args?.name || 'arxiv_search',
description: args?.description || desc,
schema: schema,
baseUrl: '',
method: 'GET',
headers: {}
}
super(toolInput)
this.topKResults = args?.topKResults ?? this.topKResults
this.maxQueryLength = args?.maxQueryLength ?? this.maxQueryLength
this.docContentCharsMax = args?.docContentCharsMax ?? this.docContentCharsMax
this.loadFullContent = args?.loadFullContent ?? this.loadFullContent
this.continueOnFailure = args?.continueOnFailure ?? this.continueOnFailure
this.legacyBuild = args?.legacyBuild ?? this.legacyBuild
this.logger = logger
this.orgId = orgId
}
private isArxivIdentifier(query: string): boolean {
const arxivIdentifierPattern = /\d{2}(0[1-9]|1[0-2])\.\d{4,5}(v\d+|)|\d{7}.*/
const queryItems = query.substring(0, this.maxQueryLength).split(/\s+/)
for (const queryItem of queryItems) {
const match = queryItem.match(arxivIdentifierPattern)
if (!match || match[0] !== queryItem) {
return false
}
}
return true
}
private parseArxivResponse(xmlText: string): ArxivResult[] {
const results: ArxivResult[] = []
// Simple XML parsing for Arxiv API response
const entryRegex = /<entry>(.*?)<\/entry>/gs
const entries = xmlText.match(entryRegex) || []
for (const entry of entries) {
try {
const id = this.extractXmlValue(entry, 'id')
const title = this.extractXmlValue(entry, 'title')?.replace(/\n\s+/g, ' ').trim()
const summary = this.extractXmlValue(entry, 'summary')?.replace(/\n\s+/g, ' ').trim()
const published = this.extractXmlValue(entry, 'published')
const updated = this.extractXmlValue(entry, 'updated')
// Extract authors
const authorRegex = /<author><name>(.*?)<\/name><\/author>/g
const authors: string[] = []
let authorMatch
while ((authorMatch = authorRegex.exec(entry)) !== null) {
authors.push(authorMatch[1])
}
if (id && title && summary) {
results.push({
id,
title,
authors,
summary,
published: published || '',
updated: updated || '',
entryId: id
})
}
} catch (error) {
console.warn('Error parsing Arxiv entry:', error)
}
}
return results
}
private extractXmlValue(xml: string, tag: string): string | undefined {
const regex = new RegExp(`<${tag}[^>]*>(.*?)</${tag}>`, 's')
const match = xml.match(regex)
return match ? match[1] : undefined
}
private async fetchResults(query: string): Promise<ArxivResult[]> {
const baseUrl = 'http://export.arxiv.org/api/query'
let searchParams: URLSearchParams
if (this.isArxivIdentifier(query)) {
// Search by ID
const ids = query.split(/\s+/).join(',')
searchParams = new URLSearchParams({
id_list: ids,
max_results: this.topKResults.toString()
})
} else {
// Search by query
// Remove problematic characters that can cause search issues
const cleanedQuery = query.replace(/[:-]/g, '').substring(0, this.maxQueryLength)
searchParams = new URLSearchParams({
search_query: `all:${cleanedQuery}`,
max_results: this.topKResults.toString(),
sortBy: 'relevance',
sortOrder: 'descending'
})
}
const url = `${baseUrl}?${searchParams.toString()}`
this.logger?.info(`[${this.orgId}]: Making Arxiv API call to: ${url}`)
const response = await fetch(url)
if (!response.ok) {
throw new Error(`Arxiv API error: ${response.status} ${response.statusText}`)
}
const xmlText = await response.text()
return this.parseArxivResponse(xmlText)
}
private async downloadAndExtractPdf(arxivId: string): Promise<string> {
// Extract clean arxiv ID from full URL if needed
const cleanId = arxivId.replace('http://arxiv.org/abs/', '').replace('https://arxiv.org/abs/', '')
const pdfUrl = `https://arxiv.org/pdf/${cleanId}.pdf`
this.logger?.info(`[${this.orgId}]: Downloading PDF from: ${pdfUrl}`)
const response = await fetch(pdfUrl)
if (!response.ok) {
throw new Error(`Failed to download PDF: ${response.status} ${response.statusText}`)
}
// Get PDF buffer and create blob
const buffer = await response.buffer()
const blob = new Blob([buffer])
// Use PDFLoader to extract text (same as Pdf.ts)
const loader = new PDFLoader(blob, {
splitPages: false,
pdfjs: () =>
// @ts-ignore
this.legacyBuild ? import('pdfjs-dist/legacy/build/pdf.js') : import('pdf-parse/lib/pdf.js/v1.10.100/build/pdf.js')
})
const docs = await loader.load()
return docs.map((doc) => doc.pageContent).join('\n')
}
/** @ignore */
async _call(arg: any): Promise<string> {
const { query } = arg
if (!query) {
throw new Error('Query is required for Arxiv search')
}
try {
const results = await this.fetchResults(query)
if (results.length === 0) {
return 'No good Arxiv Result was found'
}
if (!this.loadFullContent) {
// Return summaries only (original behavior)
const docs = results.map((result) => {
const publishedDate = result.published ? new Date(result.published).toISOString().split('T')[0] : 'Unknown'
return `Published: ${publishedDate}\nTitle: ${result.title}\nAuthors: ${result.authors.join(', ')}\nSummary: ${
result.summary
}`
})
const fullText = docs.join('\n\n')
return this.docContentCharsMax ? fullText.substring(0, this.docContentCharsMax) : fullText
} else {
// Download PDFs and extract full content
const docs: string[] = []
for (const result of results) {
try {
this.logger?.info(`[${this.orgId}]: Processing paper: ${result.title}`)
// Download and extract PDF content
const fullText = await this.downloadAndExtractPdf(result.id)
const publishedDate = result.published ? new Date(result.published).toISOString().split('T')[0] : 'Unknown'
// Format with metadata and full content
const docContent = `Published: ${publishedDate}\nTitle: ${result.title}\nAuthors: ${result.authors.join(
', '
)}\nSummary: ${result.summary}\n\nFull Content:\n${fullText}`
const truncatedContent = this.docContentCharsMax ? docContent.substring(0, this.docContentCharsMax) : docContent
docs.push(truncatedContent)
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
console.error(`Error processing paper ${result.title}:`, errorMessage)
if (!this.continueOnFailure) {
throw new Error(`Failed to process paper "${result.title}": ${errorMessage}`)
} else {
// Add error notice and continue with summary only
const publishedDate = result.published ? new Date(result.published).toISOString().split('T')[0] : 'Unknown'
const fallbackContent = `Published: ${publishedDate}\nTitle: ${result.title}\nAuthors: ${result.authors.join(
', '
)}\nSummary: ${result.summary}\n\n[ERROR: Could not load full content - ${errorMessage}]`
docs.push(fallbackContent)
}
}
}
return docs.join('\n\n---\n\n')
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
console.error('Arxiv search error:', errorMessage)
throw new Error(`Failed to search Arxiv: ${errorMessage}`)
}
}
}
@@ -23,7 +23,7 @@ class ChatflowTool_Tools implements INode {
constructor() {
this.label = 'Chatflow Tool'
this.name = 'ChatflowTool'
this.version = 5.0
this.version = 5.1
this.type = 'ChatflowTool'
this.icon = 'chatflowTool.svg'
this.category = 'Tools'
@@ -106,7 +106,10 @@ class ChatflowTool_Tools implements INode {
type: 'string',
description: 'Custom input to be passed to the chatflow. Leave empty to let LLM decides the input.',
optional: true,
additionalParams: true
additionalParams: true,
show: {
useQuestionFromChat: false
}
}
]
}
@@ -126,9 +129,20 @@ class ChatflowTool_Tools implements INode {
const chatflows = await appDataSource.getRepository(databaseEntities['ChatFlow']).findBy(searchOptions)
for (let i = 0; i < chatflows.length; i += 1) {
let type = chatflows[i].type
if (type === 'AGENTFLOW') {
type = 'AgentflowV2'
} else if (type === 'MULTIAGENT') {
type = 'AgentflowV1'
} else if (type === 'ASSISTANT') {
type = 'Custom Assistant'
} else {
type = 'Chatflow'
}
const data = {
label: chatflows[i].name,
name: chatflows[i].id
name: chatflows[i].id,
description: type
} as INodeOptionsValue
returnData.push(data)
}
@@ -243,6 +243,9 @@ const getTools = (
const methods = paths[path]
for (const method in methods) {
// example of method: "get"
if (method !== 'get' && method !== 'post' && method !== 'put' && method !== 'delete' && method !== 'patch') {
continue
}
const spec = methods[method]
const toolName = spec.operationId
const toolDesc = spec.description || spec.summary || toolName
@@ -318,7 +321,7 @@ const getTools = (
dynamicStructuredTool.setVariables(variables)
dynamicStructuredTool.setFlowObject(flow)
dynamicStructuredTool.returnDirect = returnDirect
tools.push(dynamicStructuredTool)
if (toolName && toolDesc) tools.push(dynamicStructuredTool)
}
}
return tools
@@ -0,0 +1,141 @@
import { INode, INodeData, INodeParams } from '../../../src/Interface'
import { getBaseClasses, stripHTMLFromToolInput } from '../../../src/utils'
import { desc, RequestParameters, RequestsDeleteTool } from './core'
const codeExample = `{
"id": {
"type": "string",
"required": true,
"in": "path",
"description": "ID of the item to delete. /:id"
},
"force": {
"type": "string",
"in": "query",
"description": "Force delete the item. ?force=true"
}
}`
class RequestsDelete_Tools implements INode {
label: string
name: string
version: number
description: string
type: string
icon: string
category: string
baseClasses: string[]
inputs: INodeParams[]
constructor() {
this.label = 'Requests Delete'
this.name = 'requestsDelete'
this.version = 1.0
this.type = 'RequestsDelete'
this.icon = 'del.png'
this.category = 'Tools'
this.description = 'Execute HTTP DELETE requests'
this.baseClasses = [this.type, ...getBaseClasses(RequestsDeleteTool)]
this.inputs = [
{
label: 'URL',
name: 'requestsDeleteUrl',
type: 'string',
acceptVariable: true
},
{
label: 'Name',
name: 'requestsDeleteName',
type: 'string',
default: 'requests_delete',
description: 'Name of the tool',
additionalParams: true,
optional: true
},
{
label: 'Description',
name: 'requestsDeleteDescription',
type: 'string',
rows: 4,
default: desc,
description: 'Describe to LLM when it should use this tool',
additionalParams: true,
optional: true
},
{
label: 'Headers',
name: 'requestsDeleteHeaders',
type: 'string',
rows: 4,
acceptVariable: true,
additionalParams: true,
optional: true,
placeholder: `{
"Authorization": "Bearer <token>"
}`
},
{
label: 'Query Params Schema',
name: 'requestsDeleteQueryParamsSchema',
type: 'code',
description: 'Description of the available query params to enable LLM to figure out which query params to use',
placeholder: `{
"id": {
"type": "string",
"required": true,
"in": "path",
"description": "ID of the item to delete. /:id"
},
"force": {
"type": "string",
"in": "query",
"description": "Force delete the item. ?force=true"
}
}`,
optional: true,
hideCodeExecute: true,
additionalParams: true,
codeExample: codeExample
},
{
label: 'Max Output Length',
name: 'requestsDeleteMaxOutputLength',
type: 'number',
description: 'Max length of the output. Remove this if you want to return the entire response',
default: '2000',
step: 1,
optional: true,
additionalParams: true
}
]
}
async init(nodeData: INodeData): Promise<any> {
const headers = (nodeData.inputs?.headers as string) || (nodeData.inputs?.requestsDeleteHeaders as string)
const url = (nodeData.inputs?.url as string) || (nodeData.inputs?.requestsDeleteUrl as string)
const description = (nodeData.inputs?.description as string) || (nodeData.inputs?.requestsDeleteDescription as string)
const name = (nodeData.inputs?.name as string) || (nodeData.inputs?.requestsDeleteName as string)
const queryParamsSchema =
(nodeData.inputs?.queryParamsSchema as string) || (nodeData.inputs?.requestsDeleteQueryParamsSchema as string)
const maxOutputLength = nodeData.inputs?.requestsDeleteMaxOutputLength as string
const obj: RequestParameters = {}
if (url) obj.url = stripHTMLFromToolInput(url)
if (description) obj.description = description
if (name)
obj.name = name
.toLowerCase()
.replace(/ /g, '_')
.replace(/[^a-z0-9_-]/g, '')
if (queryParamsSchema) obj.queryParamsSchema = queryParamsSchema
if (maxOutputLength) obj.maxOutputLength = parseInt(maxOutputLength, 10)
if (headers) {
const parsedHeaders = typeof headers === 'object' ? headers : JSON.parse(stripHTMLFromToolInput(headers))
obj.headers = parsedHeaders
}
return new RequestsDeleteTool(obj)
}
}
module.exports = { nodeClass: RequestsDelete_Tools }
@@ -0,0 +1,184 @@
import { z } from 'zod'
import fetch from 'node-fetch'
import { DynamicStructuredTool } from '../OpenAPIToolkit/core'
export const desc = `Use this when you need to execute a DELETE request to remove data from a website.`
export interface Headers {
[key: string]: string
}
export interface RequestParameters {
headers?: Headers
url?: string
name?: string
queryParamsSchema?: string
description?: string
maxOutputLength?: number
}
// Base schema for DELETE request
const createRequestsDeleteSchema = (queryParamsSchema?: string) => {
// If queryParamsSchema is provided, parse it and add dynamic query params
if (queryParamsSchema) {
try {
const parsedSchema = JSON.parse(queryParamsSchema)
const queryParamsObject: Record<string, z.ZodTypeAny> = {}
Object.entries(parsedSchema).forEach(([key, config]: [string, any]) => {
let zodType: z.ZodTypeAny = z.string()
// Handle different types
if (config.type === 'number') {
zodType = z.string().transform((val) => Number(val))
} else if (config.type === 'boolean') {
zodType = z.string().transform((val) => val === 'true')
}
// Add description
if (config.description) {
zodType = zodType.describe(config.description)
}
// Make optional if not required
if (!config.required) {
zodType = zodType.optional()
}
queryParamsObject[key] = zodType
})
if (Object.keys(queryParamsObject).length > 0) {
return z.object({
queryParams: z.object(queryParamsObject).optional().describe('Query parameters for the request')
})
}
} catch (error) {
console.warn('Failed to parse queryParamsSchema:', error)
}
}
// Fallback to generic query params
return z.object({
queryParams: z.record(z.string()).optional().describe('Optional query parameters to include in the request')
})
}
export class RequestsDeleteTool extends DynamicStructuredTool {
url = ''
maxOutputLength = Infinity
headers = {}
queryParamsSchema?: string
constructor(args?: RequestParameters) {
const schema = createRequestsDeleteSchema(args?.queryParamsSchema)
const toolInput = {
name: args?.name || 'requests_delete',
description: args?.description || desc,
schema: schema,
baseUrl: '',
method: 'DELETE',
headers: args?.headers || {}
}
super(toolInput)
this.url = args?.url ?? this.url
this.headers = args?.headers ?? this.headers
this.maxOutputLength = args?.maxOutputLength ?? this.maxOutputLength
this.queryParamsSchema = args?.queryParamsSchema
}
/** @ignore */
async _call(arg: any): Promise<string> {
const params = { ...arg }
const inputUrl = this.url
if (!inputUrl) {
throw new Error('URL is required for DELETE request')
}
const requestHeaders = {
...(params.headers || {}),
...this.headers
}
// Process URL and query parameters based on schema
let finalUrl = inputUrl
const queryParams: Record<string, string> = {}
if (this.queryParamsSchema && params.queryParams && Object.keys(params.queryParams).length > 0) {
try {
const parsedSchema = JSON.parse(this.queryParamsSchema)
const pathParams: Array<{ key: string; value: string }> = []
Object.entries(params.queryParams).forEach(([key, value]) => {
const paramConfig = parsedSchema[key]
if (paramConfig && value !== undefined && value !== null) {
if (paramConfig.in === 'path') {
// Check if URL contains path parameter placeholder
const pathPattern = new RegExp(`:${key}\\b`, 'g')
if (finalUrl.includes(`:${key}`)) {
// Replace path parameters in URL (e.g., /:id -> /123)
finalUrl = finalUrl.replace(pathPattern, encodeURIComponent(String(value)))
} else {
// Collect path parameters to append to URL
pathParams.push({ key, value: String(value) })
}
} else if (paramConfig.in === 'query') {
// Add to query parameters
queryParams[key] = String(value)
}
}
})
// Append path parameters to URL if any exist
if (pathParams.length > 0) {
let urlPath = finalUrl
// Remove trailing slash if present
if (urlPath.endsWith('/')) {
urlPath = urlPath.slice(0, -1)
}
// Append each path parameter
pathParams.forEach(({ value }) => {
urlPath += `/${encodeURIComponent(value)}`
})
finalUrl = urlPath
}
// Add query parameters to URL if any exist
if (Object.keys(queryParams).length > 0) {
const url = new URL(finalUrl)
Object.entries(queryParams).forEach(([key, value]) => {
url.searchParams.append(key, value)
})
finalUrl = url.toString()
}
} catch (error) {
console.warn('Failed to process queryParamsSchema:', error)
}
} else if (params.queryParams && Object.keys(params.queryParams).length > 0) {
// Fallback: treat all parameters as query parameters if no schema is defined
const url = new URL(finalUrl)
Object.entries(params.queryParams).forEach(([key, value]) => {
url.searchParams.append(key, String(value))
})
finalUrl = url.toString()
}
try {
const res = await fetch(finalUrl, {
method: 'DELETE',
headers: requestHeaders
})
if (!res.ok) {
throw new Error(`HTTP Error ${res.status}: ${res.statusText}`)
}
const text = await res.text()
return text.slice(0, this.maxOutputLength)
} catch (error) {
throw new Error(`Failed to make DELETE request: ${error instanceof Error ? error.message : 'Unknown error'}`)
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

@@ -1,7 +1,21 @@
import { INode, INodeData, INodeParams } from '../../../src/Interface'
import { getBaseClasses } from '../../../src/utils'
import { getBaseClasses, stripHTMLFromToolInput } from '../../../src/utils'
import { desc, RequestParameters, RequestsGetTool } from './core'
const codeExample = `{
"id": {
"type": "string",
"required": true,
"in": "path",
"description": "ID of the item to get. /:id"
},
"limit": {
"type": "string",
"in": "query",
"description": "Limit the number of items to get. ?limit=10"
}
}`
class RequestsGet_Tools implements INode {
label: string
name: string
@@ -16,52 +30,107 @@ class RequestsGet_Tools implements INode {
constructor() {
this.label = 'Requests Get'
this.name = 'requestsGet'
this.version = 1.0
this.version = 2.0
this.type = 'RequestsGet'
this.icon = 'requestsget.svg'
this.icon = 'get.png'
this.category = 'Tools'
this.description = 'Execute HTTP GET requests'
this.baseClasses = [this.type, ...getBaseClasses(RequestsGetTool)]
this.inputs = [
{
label: 'URL',
name: 'url',
name: 'requestsGetUrl',
type: 'string',
description:
'Agent will make call to this exact URL. If not specified, agent will try to figure out itself from AIPlugin if provided',
acceptVariable: true
},
{
label: 'Name',
name: 'requestsGetName',
type: 'string',
default: 'requests_get',
description: 'Name of the tool',
additionalParams: true,
optional: true
},
{
label: 'Description',
name: 'description',
name: 'requestsGetDescription',
type: 'string',
rows: 4,
default: desc,
description: 'Acts like a prompt to tell agent when it should use this tool',
description: 'Describe to LLM when it should use this tool',
additionalParams: true,
optional: true
},
{
label: 'Headers',
name: 'headers',
type: 'json',
name: 'requestsGetHeaders',
type: 'string',
rows: 4,
acceptVariable: true,
additionalParams: true,
optional: true
optional: true,
placeholder: `{
"Authorization": "Bearer <token>"
}`
},
{
label: 'Query Params Schema',
name: 'requestsGetQueryParamsSchema',
type: 'code',
description: 'Description of the available query params to enable LLM to figure out which query params to use',
placeholder: `{
"id": {
"type": "string",
"required": true,
"in": "path",
"description": "ID of the item to get. /:id"
},
"limit": {
"type": "string",
"in": "query",
"description": "Limit the number of items to get. ?limit=10"
}
}`,
optional: true,
hideCodeExecute: true,
additionalParams: true,
codeExample: codeExample
},
{
label: 'Max Output Length',
name: 'requestsGetMaxOutputLength',
type: 'number',
description: 'Max length of the output. Remove this if you want to return the entire response',
default: '2000',
step: 1,
optional: true,
additionalParams: true
}
]
}
async init(nodeData: INodeData): Promise<any> {
const headers = nodeData.inputs?.headers as string
const url = nodeData.inputs?.url as string
const description = nodeData.inputs?.description as string
const headers = (nodeData.inputs?.headers as string) || (nodeData.inputs?.requestsGetHeaders as string)
const url = (nodeData.inputs?.url as string) || (nodeData.inputs?.requestsGetUrl as string)
const description = (nodeData.inputs?.description as string) || (nodeData.inputs?.requestsGetDescription as string)
const name = (nodeData.inputs?.name as string) || (nodeData.inputs?.requestsGetName as string)
const queryParamsSchema =
(nodeData.inputs?.queryParamsSchema as string) || (nodeData.inputs?.requestsGetQueryParamsSchema as string)
const maxOutputLength = nodeData.inputs?.requestsGetMaxOutputLength as string
const obj: RequestParameters = {}
if (url) obj.url = url
if (url) obj.url = stripHTMLFromToolInput(url)
if (description) obj.description = description
if (name)
obj.name = name
.toLowerCase()
.replace(/ /g, '_')
.replace(/[^a-z0-9_-]/g, '')
if (queryParamsSchema) obj.queryParamsSchema = queryParamsSchema
if (maxOutputLength) obj.maxOutputLength = parseInt(maxOutputLength, 10)
if (headers) {
const parsedHeaders = typeof headers === 'object' ? headers : JSON.parse(headers)
const parsedHeaders = typeof headers === 'object' ? headers : JSON.parse(stripHTMLFromToolInput(headers))
obj.headers = parsedHeaders
}
@@ -1,8 +1,8 @@
import { z } from 'zod'
import fetch from 'node-fetch'
import { Tool } from '@langchain/core/tools'
import { DynamicStructuredTool } from '../OpenAPIToolkit/core'
export const desc = `A portal to the internet. Use this when you need to get specific content from a website.
Input should be a url (i.e. https://www.google.com). The output will be the text response of the GET request.`
export const desc = `Use this when you need to execute a GET request to get data from a website.`
export interface Headers {
[key: string]: string
@@ -11,36 +11,173 @@ export interface Headers {
export interface RequestParameters {
headers?: Headers
url?: string
name?: string
queryParamsSchema?: string
description?: string
maxOutputLength?: number
}
export class RequestsGetTool extends Tool {
name = 'requests_get'
// Base schema for GET request
const createRequestsGetSchema = (queryParamsSchema?: string) => {
// If queryParamsSchema is provided, parse it and add dynamic query params
if (queryParamsSchema) {
try {
const parsedSchema = JSON.parse(queryParamsSchema)
const queryParamsObject: Record<string, z.ZodTypeAny> = {}
Object.entries(parsedSchema).forEach(([key, config]: [string, any]) => {
let zodType: z.ZodTypeAny = z.string()
// Handle different types
if (config.type === 'number') {
zodType = z.string().transform((val) => Number(val))
} else if (config.type === 'boolean') {
zodType = z.string().transform((val) => val === 'true')
}
// Add description
if (config.description) {
zodType = zodType.describe(config.description)
}
// Make optional if not required
if (!config.required) {
zodType = zodType.optional()
}
queryParamsObject[key] = zodType
})
if (Object.keys(queryParamsObject).length > 0) {
return z.object({
queryParams: z.object(queryParamsObject).optional().describe('Query parameters for the request')
})
}
} catch (error) {
console.warn('Failed to parse queryParamsSchema:', error)
}
}
// Fallback to generic query params
return z.object({
queryParams: z.record(z.string()).optional().describe('Optional query parameters to include in the request')
})
}
export class RequestsGetTool extends DynamicStructuredTool {
url = ''
description = desc
maxOutputLength = 2000
maxOutputLength = Infinity
headers = {}
queryParamsSchema?: string
constructor(args?: RequestParameters) {
super()
const schema = createRequestsGetSchema(args?.queryParamsSchema)
const toolInput = {
name: args?.name || 'requests_get',
description: args?.description || desc,
schema: schema,
baseUrl: '',
method: 'GET',
headers: args?.headers || {}
}
super(toolInput)
this.url = args?.url ?? this.url
this.headers = args?.headers ?? this.headers
this.description = args?.description ?? this.description
this.maxOutputLength = args?.maxOutputLength ?? this.maxOutputLength
this.queryParamsSchema = args?.queryParamsSchema
}
/** @ignore */
async _call(input: string) {
const inputUrl = !this.url ? input : this.url
async _call(arg: any): Promise<string> {
const params = { ...arg }
if (process.env.DEBUG === 'true') console.info(`Making GET API call to ${inputUrl}`)
const inputUrl = this.url
if (!inputUrl) {
throw new Error('URL is required for GET request')
}
const res = await fetch(inputUrl, {
headers: this.headers
})
const requestHeaders = {
...(params.headers || {}),
...this.headers
}
const text = await res.text()
return text.slice(0, this.maxOutputLength)
// Process URL and query parameters based on schema
let finalUrl = inputUrl
const queryParams: Record<string, string> = {}
if (this.queryParamsSchema && params.queryParams && Object.keys(params.queryParams).length > 0) {
try {
const parsedSchema = JSON.parse(this.queryParamsSchema)
const pathParams: Array<{ key: string; value: string }> = []
Object.entries(params.queryParams).forEach(([key, value]) => {
const paramConfig = parsedSchema[key]
if (paramConfig && value !== undefined && value !== null) {
if (paramConfig.in === 'path') {
// Check if URL contains path parameter placeholder
const pathPattern = new RegExp(`:${key}\\b`, 'g')
if (finalUrl.includes(`:${key}`)) {
// Replace path parameters in URL (e.g., /:id -> /123)
finalUrl = finalUrl.replace(pathPattern, encodeURIComponent(String(value)))
} else {
// Collect path parameters to append to URL
pathParams.push({ key, value: String(value) })
}
} else if (paramConfig.in === 'query') {
// Add to query parameters
queryParams[key] = String(value)
}
}
})
// Append path parameters to URL if any exist
if (pathParams.length > 0) {
let urlPath = finalUrl
// Remove trailing slash if present
if (urlPath.endsWith('/')) {
urlPath = urlPath.slice(0, -1)
}
// Append each path parameter
pathParams.forEach(({ value }) => {
urlPath += `/${encodeURIComponent(value)}`
})
finalUrl = urlPath
}
// Add query parameters to URL if any exist
if (Object.keys(queryParams).length > 0) {
const url = new URL(finalUrl)
Object.entries(queryParams).forEach(([key, value]) => {
url.searchParams.append(key, value)
})
finalUrl = url.toString()
}
} catch (error) {
console.warn('Failed to process queryParamsSchema:', error)
}
} else if (params.queryParams && Object.keys(params.queryParams).length > 0) {
// Fallback: treat all parameters as query parameters if no schema is defined
const url = new URL(finalUrl)
Object.entries(params.queryParams).forEach(([key, value]) => {
url.searchParams.append(key, String(value))
})
finalUrl = url.toString()
}
try {
const res = await fetch(finalUrl, {
headers: requestHeaders
})
if (!res.ok) {
throw new Error(`HTTP Error ${res.status}: ${res.statusText}`)
}
const text = await res.text()
return text.slice(0, this.maxOutputLength)
} catch (error) {
throw new Error(`Failed to make GET request: ${error instanceof Error ? error.message : 'Unknown error'}`)
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

@@ -1,6 +0,0 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.5 20.5C10.5 20.5 10 20 9 20C7.067 20 6 21.567 6 23.5C6 25.433 7.067 27 9 27C10 27 10.7037 26.4812 11 26V24H10" stroke="#110000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M18.5 20H14V27H18.5M14 23.5H17.5" stroke="black" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M23.5 27V20M21 20H26" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M19.1112 15.2076L17.482 13.3556C15.4506 14.3228 13.0464 14.0464 11.477 12.477C10.1962 11.1962 9.77656 9.35939 10.1913 7.62299C10.3492 6.9619 11.1601 6.82676 11.6407 7.30737L13.5196 9.18628C14.1962 9.86283 15.3416 9.81433 16.078 9.07795C16.8143 8.34157 16.8628 7.19616 16.1863 6.51961L14.3074 4.64071C13.8268 4.16009 13.9619 3.34916 14.623 3.19127C16.3594 2.77656 18.1962 3.19622 19.477 4.477C21.0464 6.04639 21.3228 8.45065 20.3556 10.482L22.2076 12.1112" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

@@ -1,7 +1,19 @@
import { INode, INodeData, INodeParams } from '../../../src/Interface'
import { getBaseClasses } from '../../../src/utils'
import { getBaseClasses, stripHTMLFromToolInput } from '../../../src/utils'
import { RequestParameters, desc, RequestsPostTool } from './core'
const codeExample = `{
"name": {
"type": "string",
"required": true,
"description": "Name of the item"
},
"date": {
"type": "string",
"description": "Date of the item"
}
}`
class RequestsPost_Tools implements INode {
label: string
name: string
@@ -16,62 +28,119 @@ class RequestsPost_Tools implements INode {
constructor() {
this.label = 'Requests Post'
this.name = 'requestsPost'
this.version = 1.0
this.version = 2.0
this.type = 'RequestsPost'
this.icon = 'requestspost.svg'
this.icon = 'post.png'
this.category = 'Tools'
this.description = 'Execute HTTP POST requests'
this.baseClasses = [this.type, ...getBaseClasses(RequestsPostTool)]
this.inputs = [
{
label: 'URL',
name: 'url',
name: 'requestsPostUrl',
type: 'string',
description:
'Agent will make call to this exact URL. If not specified, agent will try to figure out itself from AIPlugin if provided',
additionalParams: true,
optional: true
acceptVariable: true
},
{
label: 'Body',
name: 'body',
type: 'json',
description:
'JSON body for the POST request. If not specified, agent will try to figure out itself from AIPlugin if provided',
label: 'Name',
name: 'requestsPostName',
type: 'string',
default: 'requests_post',
description: 'Name of the tool',
additionalParams: true,
optional: true
},
{
label: 'Description',
name: 'description',
name: 'requestsPostDescription',
type: 'string',
rows: 4,
default: desc,
description: 'Acts like a prompt to tell agent when it should use this tool',
description: 'Describe to LLM when it should use this tool',
additionalParams: true,
optional: true
},
{
label: 'Headers',
name: 'headers',
type: 'json',
name: 'requestsPostHeaders',
type: 'string',
rows: 4,
acceptVariable: true,
additionalParams: true,
optional: true
optional: true,
placeholder: `{
"Authorization": "Bearer <token>"
}`
},
{
label: 'Body',
name: 'requestPostBody',
type: 'string',
rows: 4,
description: 'JSON body for the POST request. This will override the body generated by the LLM',
additionalParams: true,
acceptVariable: true,
optional: true,
placeholder: `{
"name": "John Doe",
"age": 30
}`
},
{
label: 'Body Schema',
name: 'requestsPostBodySchema',
type: 'code',
description: 'Description of the available body params to enable LLM to figure out which body params to use',
placeholder: `{
"name": {
"type": "string",
"required": true,
"description": "Name of the item"
},
"date": {
"type": "string",
"description": "Date of the item"
}
}`,
optional: true,
hideCodeExecute: true,
additionalParams: true,
codeExample: codeExample
},
{
label: 'Max Output Length',
name: 'requestsPostMaxOutputLength',
type: 'number',
description: 'Max length of the output. Remove this if you want to return the entire response',
default: '2000',
step: 1,
optional: true,
additionalParams: true
}
]
}
async init(nodeData: INodeData): Promise<any> {
const headers = nodeData.inputs?.headers as string
const url = nodeData.inputs?.url as string
const description = nodeData.inputs?.description as string
const body = nodeData.inputs?.body as string
const headers = (nodeData.inputs?.headers as string) || (nodeData.inputs?.requestsPostHeaders as string)
const url = (nodeData.inputs?.url as string) || (nodeData.inputs?.requestsPostUrl as string)
const name = (nodeData.inputs?.name as string) || (nodeData.inputs?.requestsPostName as string)
const description = (nodeData.inputs?.description as string) || (nodeData.inputs?.requestsPostDescription as string)
const body = (nodeData.inputs?.body as string) || (nodeData.inputs?.requestPostBody as string)
const bodySchema = nodeData.inputs?.requestsPostBodySchema as string
const maxOutputLength = (nodeData.inputs?.maxOutputLength as string) || (nodeData.inputs?.requestsPostMaxOutputLength as string)
const obj: RequestParameters = {}
if (url) obj.url = url
if (url) obj.url = stripHTMLFromToolInput(url)
if (description) obj.description = description
if (name)
obj.name = name
.toLowerCase()
.replace(/ /g, '_')
.replace(/[^a-z0-9_-]/g, '')
if (bodySchema) obj.bodySchema = stripHTMLFromToolInput(bodySchema)
if (maxOutputLength) obj.maxOutputLength = parseInt(maxOutputLength, 10)
if (headers) {
const parsedHeaders = typeof headers === 'object' ? headers : JSON.parse(headers)
const parsedHeaders = typeof headers === 'object' ? headers : JSON.parse(stripHTMLFromToolInput(headers))
obj.headers = parsedHeaders
}
if (body) {
@@ -1,12 +1,8 @@
import { Tool } from '@langchain/core/tools'
import { z } from 'zod'
import fetch from 'node-fetch'
import { DynamicStructuredTool } from '../OpenAPIToolkit/core'
export const desc = `Use this when you want to POST to a website.
Input should be a json string with two keys: "url" and "data".
The value of "url" should be a string, and the value of "data" should be a dictionary of
key-value pairs you want to POST to the url as a JSON body.
Be careful to always use double quotes for strings in the json string
The output will be the text response of the POST request.`
export const desc = `Use this when you want to execute a POST request to create or update a resource.`
export interface Headers {
[key: string]: string
@@ -21,52 +17,129 @@ export interface RequestParameters {
body?: Body
url?: string
description?: string
name?: string
bodySchema?: string
maxOutputLength?: number
}
export class RequestsPostTool extends Tool {
name = 'requests_post'
// Base schema for POST request
const createRequestsPostSchema = (bodySchema?: string) => {
// If bodySchema is provided, parse it and add dynamic body params
if (bodySchema) {
try {
const parsedSchema = JSON.parse(bodySchema)
const bodyParamsObject: Record<string, z.ZodTypeAny> = {}
Object.entries(parsedSchema).forEach(([key, config]: [string, any]) => {
let zodType: z.ZodTypeAny = z.string()
// Handle different types
if (config.type === 'number') {
zodType = z.number()
} else if (config.type === 'boolean') {
zodType = z.boolean()
} else if (config.type === 'object') {
zodType = z.record(z.any())
} else if (config.type === 'array') {
zodType = z.array(z.any())
}
// Add description
if (config.description) {
zodType = zodType.describe(config.description)
}
// Make optional if not required
if (!config.required) {
zodType = zodType.optional()
}
bodyParamsObject[key] = zodType
})
if (Object.keys(bodyParamsObject).length > 0) {
return z.object({
body: z.object(bodyParamsObject).describe('Request body parameters')
})
}
} catch (error) {
console.warn('Failed to parse bodySchema:', error)
}
}
// Fallback to generic body
return z.object({
body: z.record(z.any()).optional().describe('Optional body data to include in the request')
})
}
export class RequestsPostTool extends DynamicStructuredTool {
url = ''
description = desc
maxOutputLength = Infinity
headers = {}
body = {}
bodySchema?: string
constructor(args?: RequestParameters) {
super()
const schema = createRequestsPostSchema(args?.bodySchema)
const toolInput = {
name: args?.name || 'requests_post',
description: args?.description || desc,
schema: schema,
baseUrl: '',
method: 'POST',
headers: args?.headers || {}
}
super(toolInput)
this.url = args?.url ?? this.url
this.headers = args?.headers ?? this.headers
this.body = args?.body ?? this.body
this.description = args?.description ?? this.description
this.maxOutputLength = args?.maxOutputLength ?? this.maxOutputLength
this.bodySchema = args?.bodySchema
}
/** @ignore */
async _call(input: string) {
async _call(arg: any): Promise<string> {
const params = { ...arg }
try {
let inputUrl = ''
let inputBody = {}
if (Object.keys(this.body).length || this.url) {
if (this.url) inputUrl = this.url
if (Object.keys(this.body).length) inputBody = this.body
} else {
const { url, data } = JSON.parse(input)
inputUrl = url
inputBody = data
const inputUrl = this.url
if (!inputUrl) {
throw new Error('URL is required for POST request')
}
if (process.env.DEBUG === 'true') console.info(`Making POST API call to ${inputUrl} with body ${JSON.stringify(inputBody)}`)
let inputBody = {
...this.body
}
if (this.bodySchema && params.body && Object.keys(params.body).length > 0) {
inputBody = {
...inputBody,
...params.body
}
}
const requestHeaders = {
'Content-Type': 'application/json',
...(params.headers || {}),
...this.headers
}
const res = await fetch(inputUrl, {
method: 'POST',
headers: this.headers,
headers: requestHeaders,
body: JSON.stringify(inputBody)
})
if (!res.ok) {
throw new Error(`HTTP Error ${res.status}: ${res.statusText}`)
}
const text = await res.text()
return text.slice(0, this.maxOutputLength)
} catch (error) {
return `${error}`
throw new Error(`Failed to make POST request: ${error instanceof Error ? error.message : 'Unknown error'}`)
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

@@ -1,7 +0,0 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 27V20H6.5C7.60457 20 8.5 20.8954 8.5 22C8.5 23.1046 7.60457 24 6.5 24H4" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M27 27V20M25 20H29" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M22.5644 20.4399C21.6769 19.7608 19 19.6332 19 21.7961C19 24.1915 23 22.5657 23 25.0902C23 26.9875 20.33 27.5912 19 26.3537" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11 23.5C11 20.7 12.6667 20 13.5 20C14.3333 20 16 20.7 16 23.5C16 26.3 14.3333 27 13.5 27C12.6667 27 11 26.3 11 23.5Z" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M19.1112 15.2076L17.482 13.3556C15.4506 14.3228 13.0464 14.0464 11.477 12.477C10.1962 11.1962 9.77656 9.35939 10.1913 7.62299C10.3492 6.9619 11.1601 6.82676 11.6407 7.30737L13.5196 9.18628C14.1962 9.86283 15.3416 9.81433 16.078 9.07795C16.8143 8.34157 16.8628 7.19616 16.1863 6.51961L14.3074 4.64071C13.8268 4.16009 13.9619 3.34916 14.623 3.19127C16.3594 2.77656 18.1962 3.19622 19.477 4.477C21.0464 6.04639 21.3228 8.45065 20.3556 10.482L22.2076 12.1112" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

@@ -0,0 +1,155 @@
import { INode, INodeData, INodeParams } from '../../../src/Interface'
import { getBaseClasses, stripHTMLFromToolInput } from '../../../src/utils'
import { RequestParameters, desc, RequestsPutTool } from './core'
const codeExample = `{
"name": {
"type": "string",
"required": true,
"description": "Name of the item"
},
"date": {
"type": "string",
"description": "Date of the item"
}
}`
class RequestsPut_Tools implements INode {
label: string
name: string
version: number
description: string
type: string
icon: string
category: string
baseClasses: string[]
inputs: INodeParams[]
constructor() {
this.label = 'Requests Put'
this.name = 'requestsPut'
this.version = 1.0
this.type = 'RequestsPut'
this.icon = 'put.png'
this.category = 'Tools'
this.description = 'Execute HTTP PUT requests'
this.baseClasses = [this.type, ...getBaseClasses(RequestsPutTool)]
this.inputs = [
{
label: 'URL',
name: 'requestsPutUrl',
type: 'string',
acceptVariable: true
},
{
label: 'Name',
name: 'requestsPutName',
type: 'string',
default: 'requests_put',
description: 'Name of the tool',
additionalParams: true,
optional: true
},
{
label: 'Description',
name: 'requestsPutDescription',
type: 'string',
rows: 4,
default: desc,
description: 'Describe to LLM when it should use this tool',
additionalParams: true,
optional: true
},
{
label: 'Headers',
name: 'requestsPutHeaders',
type: 'string',
rows: 4,
acceptVariable: true,
additionalParams: true,
optional: true,
placeholder: `{
"Authorization": "Bearer <token>"
}`
},
{
label: 'Body',
name: 'requestPutBody',
type: 'string',
rows: 4,
description: 'JSON body for the PUT request. This will override the body generated by the LLM',
additionalParams: true,
acceptVariable: true,
optional: true,
placeholder: `{
"name": "John Doe",
"age": 30
}`
},
{
label: 'Body Schema',
name: 'requestsPutBodySchema',
type: 'code',
description: 'Description of the available body params to enable LLM to figure out which body params to use',
placeholder: `{
"name": {
"type": "string",
"required": true,
"description": "Name of the item"
},
"date": {
"type": "string",
"description": "Date of the item"
}
}`,
optional: true,
hideCodeExecute: true,
additionalParams: true,
codeExample: codeExample
},
{
label: 'Max Output Length',
name: 'requestsPutMaxOutputLength',
type: 'number',
description: 'Max length of the output. Remove this if you want to return the entire response',
default: '2000',
step: 1,
optional: true,
additionalParams: true
}
]
}
async init(nodeData: INodeData): Promise<any> {
const headers = (nodeData.inputs?.headers as string) || (nodeData.inputs?.requestsPutHeaders as string)
const url = (nodeData.inputs?.url as string) || (nodeData.inputs?.requestsPutUrl as string)
const name = (nodeData.inputs?.name as string) || (nodeData.inputs?.requestsPutName as string)
const description = (nodeData.inputs?.description as string) || (nodeData.inputs?.requestsPutDescription as string)
const body = (nodeData.inputs?.body as string) || (nodeData.inputs?.requestPutBody as string)
const bodySchema = nodeData.inputs?.requestsPutBodySchema as string
const maxOutputLength = (nodeData.inputs?.maxOutputLength as string) || (nodeData.inputs?.requestsPutMaxOutputLength as string)
const obj: RequestParameters = {}
if (url) obj.url = stripHTMLFromToolInput(url)
if (description) obj.description = description
if (name)
obj.name = name
.toLowerCase()
.replace(/ /g, '_')
.replace(/[^a-z0-9_-]/g, '')
if (bodySchema) obj.bodySchema = stripHTMLFromToolInput(bodySchema)
if (maxOutputLength) obj.maxOutputLength = parseInt(maxOutputLength, 10)
if (headers) {
const parsedHeaders = typeof headers === 'object' ? headers : JSON.parse(stripHTMLFromToolInput(headers))
obj.headers = parsedHeaders
}
if (body) {
const parsedBody = typeof body === 'object' ? body : JSON.parse(body)
obj.body = parsedBody
}
return new RequestsPutTool(obj)
}
}
module.exports = { nodeClass: RequestsPut_Tools }
@@ -0,0 +1,145 @@
import { z } from 'zod'
import fetch from 'node-fetch'
import { DynamicStructuredTool } from '../OpenAPIToolkit/core'
export const desc = `Use this when you want to execute a PUT request to update or replace a resource.`
export interface Headers {
[key: string]: string
}
export interface Body {
[key: string]: any
}
export interface RequestParameters {
headers?: Headers
body?: Body
url?: string
description?: string
name?: string
bodySchema?: string
maxOutputLength?: number
}
// Base schema for PUT request
const createRequestsPutSchema = (bodySchema?: string) => {
// If bodySchema is provided, parse it and add dynamic body params
if (bodySchema) {
try {
const parsedSchema = JSON.parse(bodySchema)
const bodyParamsObject: Record<string, z.ZodTypeAny> = {}
Object.entries(parsedSchema).forEach(([key, config]: [string, any]) => {
let zodType: z.ZodTypeAny = z.string()
// Handle different types
if (config.type === 'number') {
zodType = z.number()
} else if (config.type === 'boolean') {
zodType = z.boolean()
} else if (config.type === 'object') {
zodType = z.record(z.any())
} else if (config.type === 'array') {
zodType = z.array(z.any())
}
// Add description
if (config.description) {
zodType = zodType.describe(config.description)
}
// Make optional if not required
if (!config.required) {
zodType = zodType.optional()
}
bodyParamsObject[key] = zodType
})
if (Object.keys(bodyParamsObject).length > 0) {
return z.object({
body: z.object(bodyParamsObject).describe('Request body parameters')
})
}
} catch (error) {
console.warn('Failed to parse bodySchema:', error)
}
}
// Fallback to generic body
return z.object({
body: z.record(z.any()).optional().describe('Optional body data to include in the request')
})
}
export class RequestsPutTool extends DynamicStructuredTool {
url = ''
maxOutputLength = Infinity
headers = {}
body = {}
bodySchema?: string
constructor(args?: RequestParameters) {
const schema = createRequestsPutSchema(args?.bodySchema)
const toolInput = {
name: args?.name || 'requests_put',
description: args?.description || desc,
schema: schema,
baseUrl: '',
method: 'PUT',
headers: args?.headers || {}
}
super(toolInput)
this.url = args?.url ?? this.url
this.headers = args?.headers ?? this.headers
this.body = args?.body ?? this.body
this.maxOutputLength = args?.maxOutputLength ?? this.maxOutputLength
this.bodySchema = args?.bodySchema
}
/** @ignore */
async _call(arg: any): Promise<string> {
const params = { ...arg }
try {
const inputUrl = this.url
if (!inputUrl) {
throw new Error('URL is required for PUT request')
}
let inputBody = {
...this.body
}
if (this.bodySchema && params.body && Object.keys(params.body).length > 0) {
inputBody = {
...inputBody,
...params.body
}
}
const requestHeaders = {
'Content-Type': 'application/json',
...(params.headers || {}),
...this.headers
}
const res = await fetch(inputUrl, {
method: 'PUT',
headers: requestHeaders,
body: JSON.stringify(inputBody)
})
if (!res.ok) {
throw new Error(`HTTP Error ${res.status}: ${res.statusText}`)
}
const text = await res.text()
return text.slice(0, this.maxOutputLength)
} catch (error) {
throw new Error(`Failed to make PUT request: ${error instanceof Error ? error.message : 'Unknown error'}`)
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

+7 -3
View File
@@ -1048,17 +1048,21 @@ export const getS3Config = () => {
const customURL = process.env.S3_ENDPOINT_URL
const forcePathStyle = process.env.S3_FORCE_PATH_STYLE === 'true' ? true : false
if (!region || !Bucket) {
if (!region || region.trim() === '' || !Bucket || Bucket.trim() === '') {
throw new Error('S3 storage configuration is missing')
}
const s3Config: S3ClientConfig = {
region: region,
endpoint: customURL,
forcePathStyle: forcePathStyle
}
if (accessKeyId && secretAccessKey) {
// Only include endpoint if customURL is not empty
if (customURL && customURL.trim() !== '') {
s3Config.endpoint = customURL
}
if (accessKeyId && accessKeyId.trim() !== '' && secretAccessKey && secretAccessKey.trim() !== '') {
s3Config.credentials = {
accessKeyId: accessKeyId,
secretAccessKey: secretAccessKey
+9
View File
@@ -4,6 +4,7 @@ import * as fs from 'fs'
import * as path from 'path'
import { JSDOM } from 'jsdom'
import { z } from 'zod'
import TurndownService from 'turndown'
import { DataSource, Equal } from 'typeorm'
import { ICommonObject, IDatabaseEntity, IFileUpload, IMessage, INodeData, IVariable, MessageContentImageUrl } from './Interface'
import { AES, enc } from 'crypto-js'
@@ -1309,3 +1310,11 @@ export const refreshOAuth2Token = async (
// Token is not expired, return original data
return credentialData
}
export const stripHTMLFromToolInput = (input: string) => {
const turndownService = new TurndownService()
let cleanedInput = turndownService.turndown(input)
// After conversion, replace any escaped underscores with regular underscores
cleanedInput = cleanedInput.replace(/\\_/g, '_')
return cleanedInput
}