From f28fda5f7205734043ae7ff45f122dc38f41e2c6 Mon Sep 17 00:00:00 2001 From: Oleg Ivaniv Date: Tue, 10 Dec 2024 07:54:16 +0100 Subject: [PATCH] WIP: Agent V2 using langraph --- .../nodes/agents/Agent/Agent.node.ts | 458 ++---------------- .../nodes/agents/Agent/v1/AgentV1.node.ts | 445 +++++++++++++++++ .../agents/ConversationalAgent/description.ts | 0 .../agents/ConversationalAgent/execute.ts | 8 +- .../agents/ConversationalAgent/prompt.ts | 0 .../OpenAiFunctionsAgent/description.ts | 0 .../agents/OpenAiFunctionsAgent/execute.ts | 6 +- .../agents/OpenAiFunctionsAgent/prompt.ts | 0 .../agents/PlanAndExecuteAgent/description.ts | 0 .../agents/PlanAndExecuteAgent/execute.ts | 8 +- .../agents/PlanAndExecuteAgent/prompt.ts | 0 .../{ => v1}/agents/ReActAgent/description.ts | 0 .../{ => v1}/agents/ReActAgent/execute.ts | 8 +- .../{ => v1}/agents/ReActAgent/prompt.ts | 0 .../{ => v1}/agents/SqlAgent/description.ts | 2 +- .../Agent/{ => v1}/agents/SqlAgent/execute.ts | 4 +- .../agents/SqlAgent/other/handlers/mysql.ts | 0 .../SqlAgent/other/handlers/postgres.ts | 0 .../agents/SqlAgent/other/handlers/sqlite.ts | 0 .../{ => v1}/agents/SqlAgent/other/prompts.ts | 0 .../{ => v1}/agents/ToolsAgent/description.ts | 0 .../{ => v1}/agents/ToolsAgent/execute.ts | 4 +- .../{ => v1}/agents/ToolsAgent/prompt.ts | 0 .../agents/Agent/{ => v1}/agents/utils.ts | 2 +- .../agents/Agent/{ => v1}/test/utils.test.ts | 0 .../nodes/agents/Agent/v2/AgentV2.node.ts | 424 ++++++++++++++++ packages/@n8n/nodes-langchain/package.json | 1 + .../nodes-base/nodes/Set/v2/SetV2.node.ts | 1 + pnpm-lock.yaml | 50 +- 29 files changed, 965 insertions(+), 456 deletions(-) create mode 100644 packages/@n8n/nodes-langchain/nodes/agents/Agent/v1/AgentV1.node.ts rename packages/@n8n/nodes-langchain/nodes/agents/Agent/{ => v1}/agents/ConversationalAgent/description.ts (100%) rename packages/@n8n/nodes-langchain/nodes/agents/Agent/{ => v1}/agents/ConversationalAgent/execute.ts (93%) rename packages/@n8n/nodes-langchain/nodes/agents/Agent/{ => v1}/agents/ConversationalAgent/prompt.ts (100%) rename packages/@n8n/nodes-langchain/nodes/agents/Agent/{ => v1}/agents/OpenAiFunctionsAgent/description.ts (100%) rename packages/@n8n/nodes-langchain/nodes/agents/Agent/{ => v1}/agents/OpenAiFunctionsAgent/execute.ts (94%) rename packages/@n8n/nodes-langchain/nodes/agents/Agent/{ => v1}/agents/OpenAiFunctionsAgent/prompt.ts (100%) rename packages/@n8n/nodes-langchain/nodes/agents/Agent/{ => v1}/agents/PlanAndExecuteAgent/description.ts (100%) rename packages/@n8n/nodes-langchain/nodes/agents/Agent/{ => v1}/agents/PlanAndExecuteAgent/execute.ts (91%) rename packages/@n8n/nodes-langchain/nodes/agents/Agent/{ => v1}/agents/PlanAndExecuteAgent/prompt.ts (100%) rename packages/@n8n/nodes-langchain/nodes/agents/Agent/{ => v1}/agents/ReActAgent/description.ts (100%) rename packages/@n8n/nodes-langchain/nodes/agents/Agent/{ => v1}/agents/ReActAgent/execute.ts (92%) rename packages/@n8n/nodes-langchain/nodes/agents/Agent/{ => v1}/agents/ReActAgent/prompt.ts (100%) rename packages/@n8n/nodes-langchain/nodes/agents/Agent/{ => v1}/agents/SqlAgent/description.ts (98%) rename packages/@n8n/nodes-langchain/nodes/agents/Agent/{ => v1}/agents/SqlAgent/execute.ts (98%) rename packages/@n8n/nodes-langchain/nodes/agents/Agent/{ => v1}/agents/SqlAgent/other/handlers/mysql.ts (100%) rename packages/@n8n/nodes-langchain/nodes/agents/Agent/{ => v1}/agents/SqlAgent/other/handlers/postgres.ts (100%) rename packages/@n8n/nodes-langchain/nodes/agents/Agent/{ => v1}/agents/SqlAgent/other/handlers/sqlite.ts (100%) rename packages/@n8n/nodes-langchain/nodes/agents/Agent/{ => v1}/agents/SqlAgent/other/prompts.ts (100%) rename packages/@n8n/nodes-langchain/nodes/agents/Agent/{ => v1}/agents/ToolsAgent/description.ts (100%) rename packages/@n8n/nodes-langchain/nodes/agents/Agent/{ => v1}/agents/ToolsAgent/execute.ts (99%) rename packages/@n8n/nodes-langchain/nodes/agents/Agent/{ => v1}/agents/ToolsAgent/prompt.ts (100%) rename packages/@n8n/nodes-langchain/nodes/agents/Agent/{ => v1}/agents/utils.ts (100%) rename packages/@n8n/nodes-langchain/nodes/agents/Agent/{ => v1}/test/utils.test.ts (100%) create mode 100644 packages/@n8n/nodes-langchain/nodes/agents/Agent/v2/AgentV2.node.ts diff --git a/packages/@n8n/nodes-langchain/nodes/agents/Agent/Agent.node.ts b/packages/@n8n/nodes-langchain/nodes/agents/Agent/Agent.node.ts index 80e5da9cfa..788f82cccc 100644 --- a/packages/@n8n/nodes-langchain/nodes/agents/Agent/Agent.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/agents/Agent/Agent.node.ts @@ -1,438 +1,38 @@ -import { NodeConnectionType, NodeOperationError } from 'n8n-workflow'; -import type { - INodeInputConfiguration, - INodeInputFilter, - IExecuteFunctions, - INodeExecutionData, - INodeType, - INodeTypeDescription, - INodeProperties, -} from 'n8n-workflow'; +import type { INodeTypeBaseDescription } from 'n8n-workflow'; +import { VersionedNodeType } from 'n8n-workflow'; -import { conversationalAgentProperties } from './agents/ConversationalAgent/description'; -import { conversationalAgentExecute } from './agents/ConversationalAgent/execute'; -import { openAiFunctionsAgentProperties } from './agents/OpenAiFunctionsAgent/description'; -import { openAiFunctionsAgentExecute } from './agents/OpenAiFunctionsAgent/execute'; -import { planAndExecuteAgentProperties } from './agents/PlanAndExecuteAgent/description'; -import { planAndExecuteAgentExecute } from './agents/PlanAndExecuteAgent/execute'; -import { reActAgentAgentProperties } from './agents/ReActAgent/description'; -import { reActAgentAgentExecute } from './agents/ReActAgent/execute'; -import { sqlAgentAgentProperties } from './agents/SqlAgent/description'; -import { sqlAgentAgentExecute } from './agents/SqlAgent/execute'; -import { toolsAgentProperties } from './agents/ToolsAgent/description'; -import { toolsAgentExecute } from './agents/ToolsAgent/execute'; -import { promptTypeOptions, textFromPreviousNode, textInput } from '../../../utils/descriptions'; +import { AgentV1 } from './v1/AgentV1.node'; +import { AgentV2 } from './v2/AgentV2.node'; -// Function used in the inputs expression to figure out which inputs to -// display based on the agent type -function getInputs( - agent: 'toolsAgent' | 'conversationalAgent' | 'openAiFunctionsAgent' | 'reActAgent' | 'sqlAgent', - hasOutputParser?: boolean, -): Array { - interface SpecialInput { - type: NodeConnectionType; - filter?: INodeInputFilter; - required?: boolean; - } - - const getInputData = ( - inputs: SpecialInput[], - ): Array => { - const displayNames: { [key: string]: string } = { - [NodeConnectionType.AiLanguageModel]: 'Model', - [NodeConnectionType.AiMemory]: 'Memory', - [NodeConnectionType.AiTool]: 'Tool', - [NodeConnectionType.AiOutputParser]: 'Output Parser', - }; - - return inputs.map(({ type, filter }) => { - const isModelType = type === NodeConnectionType.AiLanguageModel; - let displayName = type in displayNames ? displayNames[type] : undefined; - if ( - isModelType && - ['openAiFunctionsAgent', 'toolsAgent', 'conversationalAgent'].includes(agent) - ) { - displayName = 'Chat Model'; - } - const input: INodeInputConfiguration = { - type, - displayName, - required: isModelType, - maxConnections: [NodeConnectionType.AiLanguageModel, NodeConnectionType.AiMemory].includes( - type as NodeConnectionType, - ) - ? 1 - : undefined, - }; - - if (filter) { - input.filter = filter; - } - - return input; - }); - }; - - let specialInputs: SpecialInput[] = []; - - if (agent === 'conversationalAgent') { - specialInputs = [ - { - type: NodeConnectionType.AiLanguageModel, - filter: { - nodes: [ - '@n8n/n8n-nodes-langchain.lmChatAnthropic', - '@n8n/n8n-nodes-langchain.lmChatAwsBedrock', - '@n8n/n8n-nodes-langchain.lmChatGroq', - '@n8n/n8n-nodes-langchain.lmChatOllama', - '@n8n/n8n-nodes-langchain.lmChatOpenAi', - '@n8n/n8n-nodes-langchain.lmChatGoogleGemini', - '@n8n/n8n-nodes-langchain.lmChatGoogleVertex', - '@n8n/n8n-nodes-langchain.lmChatMistralCloud', - '@n8n/n8n-nodes-langchain.lmChatAzureOpenAi', - ], - }, - }, - { - type: NodeConnectionType.AiMemory, - }, - { - type: NodeConnectionType.AiTool, - }, - { - type: NodeConnectionType.AiOutputParser, - }, - ]; - } else if (agent === 'toolsAgent') { - specialInputs = [ - { - type: NodeConnectionType.AiLanguageModel, - filter: { - nodes: [ - '@n8n/n8n-nodes-langchain.lmChatAnthropic', - '@n8n/n8n-nodes-langchain.lmChatAzureOpenAi', - '@n8n/n8n-nodes-langchain.lmChatAwsBedrock', - '@n8n/n8n-nodes-langchain.lmChatMistralCloud', - '@n8n/n8n-nodes-langchain.lmChatOllama', - '@n8n/n8n-nodes-langchain.lmChatOpenAi', - '@n8n/n8n-nodes-langchain.lmChatGroq', - '@n8n/n8n-nodes-langchain.lmChatGoogleVertex', - '@n8n/n8n-nodes-langchain.lmChatGoogleGemini', - ], - }, - }, - { - type: NodeConnectionType.AiMemory, - }, - { - type: NodeConnectionType.AiTool, - required: true, - }, - { - type: NodeConnectionType.AiOutputParser, - }, - ]; - } else if (agent === 'openAiFunctionsAgent') { - specialInputs = [ - { - type: NodeConnectionType.AiLanguageModel, - filter: { - nodes: [ - '@n8n/n8n-nodes-langchain.lmChatOpenAi', - '@n8n/n8n-nodes-langchain.lmChatAzureOpenAi', - ], - }, - }, - { - type: NodeConnectionType.AiMemory, - }, - { - type: NodeConnectionType.AiTool, - required: true, - }, - { - type: NodeConnectionType.AiOutputParser, - }, - ]; - } else if (agent === 'reActAgent') { - specialInputs = [ - { - type: NodeConnectionType.AiLanguageModel, - }, - { - type: NodeConnectionType.AiTool, - }, - { - type: NodeConnectionType.AiOutputParser, - }, - ]; - } else if (agent === 'sqlAgent') { - specialInputs = [ - { - type: NodeConnectionType.AiLanguageModel, - }, - { - type: NodeConnectionType.AiMemory, - }, - ]; - } else if (agent === 'planAndExecuteAgent') { - specialInputs = [ - { - type: NodeConnectionType.AiLanguageModel, - }, - { - type: NodeConnectionType.AiTool, - }, - { - type: NodeConnectionType.AiOutputParser, - }, - ]; - } - - if (hasOutputParser === false) { - specialInputs = specialInputs.filter( - (input) => input.type !== NodeConnectionType.AiOutputParser, - ); - } - return [NodeConnectionType.Main, ...getInputData(specialInputs)]; -} - -const agentTypeProperty: INodeProperties = { - displayName: 'Agent', +const baseDescription: INodeTypeBaseDescription = { + displayName: 'AI Agent', + description: 'Generates an action plan and executes it. Can use external tools.', name: 'agent', - type: 'options', - noDataExpression: true, - // eslint-disable-next-line n8n-nodes-base/node-param-options-type-unsorted-items - options: [ - { - name: 'Tools Agent', - value: 'toolsAgent', - description: - 'Utilizes structured tool schemas for precise and reliable tool selection and execution. Recommended for complex tasks requiring accurate and consistent tool usage, but only usable with models that support tool calling.', + icon: 'fa:robot', + group: ['transform'], + codex: { + alias: ['LangChain', 'Chat', 'Conversational', 'Plan and Execute', 'ReAct', 'Tools'], + categories: ['AI'], + subcategories: { + AI: ['Agents', 'Root Nodes'], }, - { - name: 'Conversational Agent', - value: 'conversationalAgent', - description: - 'Describes tools in the system prompt and parses JSON responses for tool calls. More flexible but potentially less reliable than the Tools Agent. Suitable for simpler interactions or with models not supporting structured schemas.', + resources: { + primaryDocumentation: [ + { + url: 'https://docs.n8n.io/integrations/builtin/cluster-nodes/root-nodes/n8n-nodes-langchain.agent/', + }, + ], }, - { - name: 'OpenAI Functions Agent', - value: 'openAiFunctionsAgent', - description: - "Leverages OpenAI's function calling capabilities to precisely select and execute tools. Excellent for tasks requiring structured outputs when working with OpenAI models.", - }, - { - name: 'Plan and Execute Agent', - value: 'planAndExecuteAgent', - description: - 'Creates a high-level plan for complex tasks and then executes each step. Suitable for multi-stage problems or when a strategic approach is needed.', - }, - { - name: 'ReAct Agent', - value: 'reActAgent', - description: - 'Combines reasoning and action in an iterative process. Effective for tasks that require careful analysis and step-by-step problem-solving.', - }, - { - name: 'SQL Agent', - value: 'sqlAgent', - description: - 'Specializes in interacting with SQL databases. Ideal for data analysis tasks, generating queries, or extracting insights from structured data.', - }, - ], - default: '', + }, + defaultVersion: 2, }; -export class Agent implements INodeType { - description: INodeTypeDescription = { - displayName: 'AI Agent', - name: 'agent', - icon: 'fa:robot', - iconColor: 'black', - group: ['transform'], - version: [1, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7], - description: 'Generates an action plan and executes it. Can use external tools.', - subtitle: - "={{ { toolsAgent: 'Tools Agent', conversationalAgent: 'Conversational Agent', openAiFunctionsAgent: 'OpenAI Functions Agent', reActAgent: 'ReAct Agent', sqlAgent: 'SQL Agent', planAndExecuteAgent: 'Plan and Execute Agent' }[$parameter.agent] }}", - defaults: { - name: 'AI Agent', - color: '#404040', - }, - codex: { - alias: ['LangChain', 'Chat', 'Conversational', 'Plan and Execute', 'ReAct', 'Tools'], - categories: ['AI'], - subcategories: { - AI: ['Agents', 'Root Nodes'], - }, - resources: { - primaryDocumentation: [ - { - url: 'https://docs.n8n.io/integrations/builtin/cluster-nodes/root-nodes/n8n-nodes-langchain.agent/', - }, - ], - }, - }, - inputs: `={{ - ((agent, hasOutputParser) => { - ${getInputs.toString()}; - return getInputs(agent, hasOutputParser) - })($parameter.agent, $parameter.hasOutputParser === undefined || $parameter.hasOutputParser === true) - }}`, - outputs: [NodeConnectionType.Main], - credentials: [ - { - // eslint-disable-next-line n8n-nodes-base/node-class-description-credentials-name-unsuffixed - name: 'mySql', - required: true, - testedBy: 'mysqlConnectionTest', - displayOptions: { - show: { - agent: ['sqlAgent'], - '/dataSource': ['mysql'], - }, - }, - }, - { - name: 'postgres', - required: true, - displayOptions: { - show: { - agent: ['sqlAgent'], - '/dataSource': ['postgres'], - }, - }, - }, - ], - properties: [ - { - displayName: - 'Tip: Get a feel for agents with our quick tutorial or see an example of how this node works', - name: 'notice_tip', - type: 'notice', - default: '', - displayOptions: { - show: { - agent: ['conversationalAgent', 'toolsAgent'], - }, - }, - }, - // Make Conversational Agent the default agent for versions 1.5 and below - { - ...agentTypeProperty, - options: agentTypeProperty?.options?.filter( - (o) => 'value' in o && o.value !== 'toolsAgent', - ), - displayOptions: { show: { '@version': [{ _cnd: { lte: 1.5 } }] } }, - default: 'conversationalAgent', - }, - // Make Tools Agent the default agent for versions 1.6 and above - { - ...agentTypeProperty, - displayOptions: { show: { '@version': [{ _cnd: { gte: 1.6 } }] } }, - default: 'toolsAgent', - }, - { - ...promptTypeOptions, - displayOptions: { - hide: { - '@version': [{ _cnd: { lte: 1.2 } }], - agent: ['sqlAgent'], - }, - }, - }, - { - ...textFromPreviousNode, - displayOptions: { - show: { promptType: ['auto'], '@version': [{ _cnd: { gte: 1.7 } }] }, - // SQL Agent has data source and credentials parameters so we need to include this input there manually - // to preserve the order - hide: { - agent: ['sqlAgent'], - }, - }, - }, - { - ...textInput, - displayOptions: { - show: { - promptType: ['define'], - }, - hide: { - agent: ['sqlAgent'], - }, - }, - }, - { - displayName: 'For more reliable structured output parsing, consider using the Tools agent', - name: 'notice', - type: 'notice', - default: '', - displayOptions: { - show: { - hasOutputParser: [true], - agent: [ - 'conversationalAgent', - 'reActAgent', - 'planAndExecuteAgent', - 'openAiFunctionsAgent', - ], - }, - }, - }, - { - displayName: 'Require Specific Output Format', - name: 'hasOutputParser', - type: 'boolean', - default: false, - noDataExpression: true, - displayOptions: { - hide: { - '@version': [{ _cnd: { lte: 1.2 } }], - agent: ['sqlAgent'], - }, - }, - }, - { - displayName: `Connect an output parser on the canvas to specify the output format you require`, - name: 'notice', - type: 'notice', - default: '', - displayOptions: { - show: { - hasOutputParser: [true], - agent: ['toolsAgent'], - }, - }, - }, - - ...toolsAgentProperties, - ...conversationalAgentProperties, - ...openAiFunctionsAgentProperties, - ...reActAgentAgentProperties, - ...sqlAgentAgentProperties, - ...planAndExecuteAgentProperties, - ], - }; - - async execute(this: IExecuteFunctions): Promise { - const agentType = this.getNodeParameter('agent', 0, '') as string; - const nodeVersion = this.getNode().typeVersion; - - if (agentType === 'conversationalAgent') { - return await conversationalAgentExecute.call(this, nodeVersion); - } else if (agentType === 'toolsAgent') { - return await toolsAgentExecute.call(this); - } else if (agentType === 'openAiFunctionsAgent') { - return await openAiFunctionsAgentExecute.call(this, nodeVersion); - } else if (agentType === 'reActAgent') { - return await reActAgentAgentExecute.call(this, nodeVersion); - } else if (agentType === 'sqlAgent') { - return await sqlAgentAgentExecute.call(this); - } else if (agentType === 'planAndExecuteAgent') { - return await planAndExecuteAgentExecute.call(this, nodeVersion); - } - - throw new NodeOperationError(this.getNode(), `The agent type "${agentType}" is not supported`); +export class Agent extends VersionedNodeType { + constructor() { + const nodeVersions = { + 1: new AgentV1(baseDescription), + 2: new AgentV2(baseDescription), + }; + super(nodeVersions, baseDescription); } } diff --git a/packages/@n8n/nodes-langchain/nodes/agents/Agent/v1/AgentV1.node.ts b/packages/@n8n/nodes-langchain/nodes/agents/Agent/v1/AgentV1.node.ts new file mode 100644 index 0000000000..c4e4a19af3 --- /dev/null +++ b/packages/@n8n/nodes-langchain/nodes/agents/Agent/v1/AgentV1.node.ts @@ -0,0 +1,445 @@ +import { NodeConnectionType, NodeOperationError } from 'n8n-workflow'; +import type { + INodeInputConfiguration, + INodeInputFilter, + IExecuteFunctions, + INodeExecutionData, + INodeType, + INodeTypeDescription, + INodeProperties, + INodeTypeBaseDescription, +} from 'n8n-workflow'; + +import { conversationalAgentProperties } from './agents/ConversationalAgent/description'; +import { conversationalAgentExecute } from './agents/ConversationalAgent/execute'; +import { openAiFunctionsAgentProperties } from './agents/OpenAiFunctionsAgent/description'; +import { openAiFunctionsAgentExecute } from './agents/OpenAiFunctionsAgent/execute'; +import { planAndExecuteAgentProperties } from './agents/PlanAndExecuteAgent/description'; +import { planAndExecuteAgentExecute } from './agents/PlanAndExecuteAgent/execute'; +import { reActAgentAgentProperties } from './agents/ReActAgent/description'; +import { reActAgentAgentExecute } from './agents/ReActAgent/execute'; +import { sqlAgentAgentProperties } from './agents/SqlAgent/description'; +import { sqlAgentAgentExecute } from './agents/SqlAgent/execute'; +import { toolsAgentProperties } from './agents/ToolsAgent/description'; +import { toolsAgentExecute } from './agents/ToolsAgent/execute'; +import { promptTypeOptions, textFromPreviousNode, textInput } from '../../../../utils/descriptions'; + +// Function used in the inputs expression to figure out which inputs to +// display based on the agent type +function getInputs( + agent: 'toolsAgent' | 'conversationalAgent' | 'openAiFunctionsAgent' | 'reActAgent' | 'sqlAgent', + hasOutputParser?: boolean, +): Array { + interface SpecialInput { + type: NodeConnectionType; + filter?: INodeInputFilter; + required?: boolean; + } + + const getInputData = ( + inputs: SpecialInput[], + ): Array => { + const displayNames: { [key: string]: string } = { + [NodeConnectionType.AiLanguageModel]: 'Model', + [NodeConnectionType.AiMemory]: 'Memory', + [NodeConnectionType.AiTool]: 'Tool', + [NodeConnectionType.AiOutputParser]: 'Output Parser', + }; + + return inputs.map(({ type, filter }) => { + const isModelType = type === NodeConnectionType.AiLanguageModel; + let displayName = type in displayNames ? displayNames[type] : undefined; + if ( + isModelType && + ['openAiFunctionsAgent', 'toolsAgent', 'conversationalAgent'].includes(agent) + ) { + displayName = 'Chat Model'; + } + const input: INodeInputConfiguration = { + type, + displayName, + required: isModelType, + maxConnections: [NodeConnectionType.AiLanguageModel, NodeConnectionType.AiMemory].includes( + type as NodeConnectionType, + ) + ? 1 + : undefined, + }; + + if (filter) { + input.filter = filter; + } + + return input; + }); + }; + + let specialInputs: SpecialInput[] = []; + + if (agent === 'conversationalAgent') { + specialInputs = [ + { + type: NodeConnectionType.AiLanguageModel, + filter: { + nodes: [ + '@n8n/n8n-nodes-langchain.lmChatAnthropic', + '@n8n/n8n-nodes-langchain.lmChatAwsBedrock', + '@n8n/n8n-nodes-langchain.lmChatGroq', + '@n8n/n8n-nodes-langchain.lmChatOllama', + '@n8n/n8n-nodes-langchain.lmChatOpenAi', + '@n8n/n8n-nodes-langchain.lmChatGoogleGemini', + '@n8n/n8n-nodes-langchain.lmChatGoogleVertex', + '@n8n/n8n-nodes-langchain.lmChatMistralCloud', + '@n8n/n8n-nodes-langchain.lmChatAzureOpenAi', + ], + }, + }, + { + type: NodeConnectionType.AiMemory, + }, + { + type: NodeConnectionType.AiTool, + }, + { + type: NodeConnectionType.AiOutputParser, + }, + ]; + } else if (agent === 'toolsAgent') { + specialInputs = [ + { + type: NodeConnectionType.AiLanguageModel, + filter: { + nodes: [ + '@n8n/n8n-nodes-langchain.lmChatAnthropic', + '@n8n/n8n-nodes-langchain.lmChatAzureOpenAi', + '@n8n/n8n-nodes-langchain.lmChatAwsBedrock', + '@n8n/n8n-nodes-langchain.lmChatMistralCloud', + '@n8n/n8n-nodes-langchain.lmChatOllama', + '@n8n/n8n-nodes-langchain.lmChatOpenAi', + '@n8n/n8n-nodes-langchain.lmChatGroq', + '@n8n/n8n-nodes-langchain.lmChatGoogleVertex', + '@n8n/n8n-nodes-langchain.lmChatGoogleGemini', + ], + }, + }, + { + type: NodeConnectionType.AiMemory, + }, + { + type: NodeConnectionType.AiTool, + required: true, + }, + { + type: NodeConnectionType.AiOutputParser, + }, + ]; + } else if (agent === 'openAiFunctionsAgent') { + specialInputs = [ + { + type: NodeConnectionType.AiLanguageModel, + filter: { + nodes: [ + '@n8n/n8n-nodes-langchain.lmChatOpenAi', + '@n8n/n8n-nodes-langchain.lmChatAzureOpenAi', + ], + }, + }, + { + type: NodeConnectionType.AiMemory, + }, + { + type: NodeConnectionType.AiTool, + required: true, + }, + { + type: NodeConnectionType.AiOutputParser, + }, + ]; + } else if (agent === 'reActAgent') { + specialInputs = [ + { + type: NodeConnectionType.AiLanguageModel, + }, + { + type: NodeConnectionType.AiTool, + }, + { + type: NodeConnectionType.AiOutputParser, + }, + ]; + } else if (agent === 'sqlAgent') { + specialInputs = [ + { + type: NodeConnectionType.AiLanguageModel, + }, + { + type: NodeConnectionType.AiMemory, + }, + ]; + } else if (agent === 'planAndExecuteAgent') { + specialInputs = [ + { + type: NodeConnectionType.AiLanguageModel, + }, + { + type: NodeConnectionType.AiTool, + }, + { + type: NodeConnectionType.AiOutputParser, + }, + ]; + } + + if (hasOutputParser === false) { + specialInputs = specialInputs.filter( + (input) => input.type !== NodeConnectionType.AiOutputParser, + ); + } + return [NodeConnectionType.Main, ...getInputData(specialInputs)]; +} + +const agentTypeProperty: INodeProperties = { + displayName: 'Agent', + name: 'agent', + type: 'options', + noDataExpression: true, + // eslint-disable-next-line n8n-nodes-base/node-param-options-type-unsorted-items + options: [ + { + name: 'Tools Agent', + value: 'toolsAgent', + description: + 'Utilizes structured tool schemas for precise and reliable tool selection and execution. Recommended for complex tasks requiring accurate and consistent tool usage, but only usable with models that support tool calling.', + }, + { + name: 'Conversational Agent', + value: 'conversationalAgent', + description: + 'Describes tools in the system prompt and parses JSON responses for tool calls. More flexible but potentially less reliable than the Tools Agent. Suitable for simpler interactions or with models not supporting structured schemas.', + }, + { + name: 'OpenAI Functions Agent', + value: 'openAiFunctionsAgent', + description: + "Leverages OpenAI's function calling capabilities to precisely select and execute tools. Excellent for tasks requiring structured outputs when working with OpenAI models.", + }, + { + name: 'Plan and Execute Agent', + value: 'planAndExecuteAgent', + description: + 'Creates a high-level plan for complex tasks and then executes each step. Suitable for multi-stage problems or when a strategic approach is needed.', + }, + { + name: 'ReAct Agent', + value: 'reActAgent', + description: + 'Combines reasoning and action in an iterative process. Effective for tasks that require careful analysis and step-by-step problem-solving.', + }, + { + name: 'SQL Agent', + value: 'sqlAgent', + description: + 'Specializes in interacting with SQL databases. Ideal for data analysis tasks, generating queries, or extracting insights from structured data.', + }, + ], + default: '', +}; + +const versionDescription: INodeTypeDescription = { + displayName: 'AI Agent', + name: 'agent', + icon: 'fa:robot', + iconColor: 'black', + group: ['transform'], + version: [1, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7], + description: 'Generates an action plan and executes it. Can use external tools.', + subtitle: + "={{ { toolsAgent: 'Tools Agent', conversationalAgent: 'Conversational Agent', openAiFunctionsAgent: 'OpenAI Functions Agent', reActAgent: 'ReAct Agent', sqlAgent: 'SQL Agent', planAndExecuteAgent: 'Plan and Execute Agent' }[$parameter.agent] }}", + defaults: { + name: 'AI Agent', + color: '#404040', + }, + codex: { + alias: ['LangChain', 'Chat', 'Conversational', 'Plan and Execute', 'ReAct', 'Tools'], + categories: ['AI'], + subcategories: { + AI: ['Agents', 'Root Nodes'], + }, + resources: { + primaryDocumentation: [ + { + url: 'https://docs.n8n.io/integrations/builtin/cluster-nodes/root-nodes/n8n-nodes-langchain.agent/', + }, + ], + }, + }, + inputs: `={{ + ((agent, hasOutputParser) => { + ${getInputs.toString()}; + return getInputs(agent, hasOutputParser) + })($parameter.agent, $parameter.hasOutputParser === undefined || $parameter.hasOutputParser === true) + }}`, + outputs: [NodeConnectionType.Main], + credentials: [ + { + // eslint-disable-next-line n8n-nodes-base/node-class-description-credentials-name-unsuffixed + name: 'mySql', + required: true, + testedBy: 'mysqlConnectionTest', + displayOptions: { + show: { + agent: ['sqlAgent'], + '/dataSource': ['mysql'], + }, + }, + }, + { + name: 'postgres', + required: true, + displayOptions: { + show: { + agent: ['sqlAgent'], + '/dataSource': ['postgres'], + }, + }, + }, + ], + properties: [ + { + displayName: + 'Tip: Get a feel for agents with our quick tutorial or see an example of how this node works', + name: 'notice_tip', + type: 'notice', + default: '', + displayOptions: { + show: { + agent: ['conversationalAgent', 'toolsAgent'], + }, + }, + }, + // Make Conversational Agent the default agent for versions 1.5 and below + { + ...agentTypeProperty, + options: agentTypeProperty?.options?.filter((o) => 'value' in o && o.value !== 'toolsAgent'), + displayOptions: { show: { '@version': [{ _cnd: { lte: 1.5 } }] } }, + default: 'conversationalAgent', + }, + // Make Tools Agent the default agent for versions 1.6 and above + { + ...agentTypeProperty, + displayOptions: { show: { '@version': [{ _cnd: { gte: 1.6 } }] } }, + default: 'toolsAgent', + }, + { + ...promptTypeOptions, + displayOptions: { + hide: { + '@version': [{ _cnd: { lte: 1.2 } }], + agent: ['sqlAgent'], + }, + }, + }, + { + ...textFromPreviousNode, + displayOptions: { + show: { promptType: ['auto'], '@version': [{ _cnd: { gte: 1.7 } }] }, + // SQL Agent has data source and credentials parameters so we need to include this input there manually + // to preserve the order + hide: { + agent: ['sqlAgent'], + }, + }, + }, + { + ...textInput, + displayOptions: { + show: { + promptType: ['define'], + }, + hide: { + agent: ['sqlAgent'], + }, + }, + }, + { + displayName: 'For more reliable structured output parsing, consider using the Tools agent', + name: 'notice', + type: 'notice', + default: '', + displayOptions: { + show: { + hasOutputParser: [true], + agent: [ + 'conversationalAgent', + 'reActAgent', + 'planAndExecuteAgent', + 'openAiFunctionsAgent', + ], + }, + }, + }, + { + displayName: 'Require Specific Output Format', + name: 'hasOutputParser', + type: 'boolean', + default: false, + noDataExpression: true, + displayOptions: { + hide: { + '@version': [{ _cnd: { lte: 1.2 } }], + agent: ['sqlAgent'], + }, + }, + }, + { + displayName: `Connect an output parser on the canvas to specify the output format you require`, + name: 'notice', + type: 'notice', + default: '', + displayOptions: { + show: { + hasOutputParser: [true], + agent: ['toolsAgent'], + }, + }, + }, + + ...toolsAgentProperties, + ...conversationalAgentProperties, + ...openAiFunctionsAgentProperties, + ...reActAgentAgentProperties, + ...sqlAgentAgentProperties, + ...planAndExecuteAgentProperties, + ], +}; +export class AgentV1 implements INodeType { + description: INodeTypeDescription; + + constructor(baseDescription: INodeTypeBaseDescription) { + this.description = { + ...baseDescription, + ...versionDescription, + }; + } + + async execute(this: IExecuteFunctions): Promise { + const agentType = this.getNodeParameter('agent', 0, '') as string; + const nodeVersion = this.getNode().typeVersion; + + if (agentType === 'conversationalAgent') { + return await conversationalAgentExecute.call(this, nodeVersion); + } else if (agentType === 'toolsAgent') { + return await toolsAgentExecute.call(this); + } else if (agentType === 'openAiFunctionsAgent') { + return await openAiFunctionsAgentExecute.call(this, nodeVersion); + } else if (agentType === 'reActAgent') { + return await reActAgentAgentExecute.call(this, nodeVersion); + } else if (agentType === 'sqlAgent') { + return await sqlAgentAgentExecute.call(this); + } else if (agentType === 'planAndExecuteAgent') { + return await planAndExecuteAgentExecute.call(this, nodeVersion); + } + + throw new NodeOperationError(this.getNode(), `The agent type "${agentType}" is not supported`); + } +} diff --git a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ConversationalAgent/description.ts b/packages/@n8n/nodes-langchain/nodes/agents/Agent/v1/agents/ConversationalAgent/description.ts similarity index 100% rename from packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ConversationalAgent/description.ts rename to packages/@n8n/nodes-langchain/nodes/agents/Agent/v1/agents/ConversationalAgent/description.ts diff --git a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ConversationalAgent/execute.ts b/packages/@n8n/nodes-langchain/nodes/agents/Agent/v1/agents/ConversationalAgent/execute.ts similarity index 93% rename from packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ConversationalAgent/execute.ts rename to packages/@n8n/nodes-langchain/nodes/agents/Agent/v1/agents/ConversationalAgent/execute.ts index 09e04c0b76..d7ffee6bc8 100644 --- a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ConversationalAgent/execute.ts +++ b/packages/@n8n/nodes-langchain/nodes/agents/Agent/v1/agents/ConversationalAgent/execute.ts @@ -10,10 +10,10 @@ import { isChatInstance, getPromptInputByType, getConnectedTools, -} from '../../../../../utils/helpers'; -import { getOptionalOutputParsers } from '../../../../../utils/output_parsers/N8nOutputParser'; -import { throwIfToolSchema } from '../../../../../utils/schemaParsing'; -import { getTracingConfig } from '../../../../../utils/tracing'; +} from '../../../../../../utils/helpers'; +import { getOptionalOutputParsers } from '../../../../../../utils/output_parsers/N8nOutputParser'; +import { throwIfToolSchema } from '../../../../../../utils/schemaParsing'; +import { getTracingConfig } from '../../../../../../utils/tracing'; import { checkForStructuredTools, extractParsedOutput } from '../utils'; export async function conversationalAgentExecute( diff --git a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ConversationalAgent/prompt.ts b/packages/@n8n/nodes-langchain/nodes/agents/Agent/v1/agents/ConversationalAgent/prompt.ts similarity index 100% rename from packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ConversationalAgent/prompt.ts rename to packages/@n8n/nodes-langchain/nodes/agents/Agent/v1/agents/ConversationalAgent/prompt.ts diff --git a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/OpenAiFunctionsAgent/description.ts b/packages/@n8n/nodes-langchain/nodes/agents/Agent/v1/agents/OpenAiFunctionsAgent/description.ts similarity index 100% rename from packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/OpenAiFunctionsAgent/description.ts rename to packages/@n8n/nodes-langchain/nodes/agents/Agent/v1/agents/OpenAiFunctionsAgent/description.ts diff --git a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/OpenAiFunctionsAgent/execute.ts b/packages/@n8n/nodes-langchain/nodes/agents/Agent/v1/agents/OpenAiFunctionsAgent/execute.ts similarity index 94% rename from packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/OpenAiFunctionsAgent/execute.ts rename to packages/@n8n/nodes-langchain/nodes/agents/Agent/v1/agents/OpenAiFunctionsAgent/execute.ts index a9b324678c..1eabf56fa2 100644 --- a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/OpenAiFunctionsAgent/execute.ts +++ b/packages/@n8n/nodes-langchain/nodes/agents/Agent/v1/agents/OpenAiFunctionsAgent/execute.ts @@ -12,9 +12,9 @@ import { NodeOperationError, } from 'n8n-workflow'; -import { getConnectedTools, getPromptInputByType } from '../../../../../utils/helpers'; -import { getOptionalOutputParsers } from '../../../../../utils/output_parsers/N8nOutputParser'; -import { getTracingConfig } from '../../../../../utils/tracing'; +import { getConnectedTools, getPromptInputByType } from '../../../../../../utils/helpers'; +import { getOptionalOutputParsers } from '../../../../../../utils/output_parsers/N8nOutputParser'; +import { getTracingConfig } from '../../../../../../utils/tracing'; import { extractParsedOutput } from '../utils'; export async function openAiFunctionsAgentExecute( diff --git a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/OpenAiFunctionsAgent/prompt.ts b/packages/@n8n/nodes-langchain/nodes/agents/Agent/v1/agents/OpenAiFunctionsAgent/prompt.ts similarity index 100% rename from packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/OpenAiFunctionsAgent/prompt.ts rename to packages/@n8n/nodes-langchain/nodes/agents/Agent/v1/agents/OpenAiFunctionsAgent/prompt.ts diff --git a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/PlanAndExecuteAgent/description.ts b/packages/@n8n/nodes-langchain/nodes/agents/Agent/v1/agents/PlanAndExecuteAgent/description.ts similarity index 100% rename from packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/PlanAndExecuteAgent/description.ts rename to packages/@n8n/nodes-langchain/nodes/agents/Agent/v1/agents/PlanAndExecuteAgent/description.ts diff --git a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/PlanAndExecuteAgent/execute.ts b/packages/@n8n/nodes-langchain/nodes/agents/Agent/v1/agents/PlanAndExecuteAgent/execute.ts similarity index 91% rename from packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/PlanAndExecuteAgent/execute.ts rename to packages/@n8n/nodes-langchain/nodes/agents/Agent/v1/agents/PlanAndExecuteAgent/execute.ts index d2dc152ebb..5387aadb36 100644 --- a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/PlanAndExecuteAgent/execute.ts +++ b/packages/@n8n/nodes-langchain/nodes/agents/Agent/v1/agents/PlanAndExecuteAgent/execute.ts @@ -10,10 +10,10 @@ import { NodeOperationError, } from 'n8n-workflow'; -import { getConnectedTools, getPromptInputByType } from '../../../../../utils/helpers'; -import { getOptionalOutputParsers } from '../../../../../utils/output_parsers/N8nOutputParser'; -import { throwIfToolSchema } from '../../../../../utils/schemaParsing'; -import { getTracingConfig } from '../../../../../utils/tracing'; +import { getConnectedTools, getPromptInputByType } from '../../../../../../utils/helpers'; +import { getOptionalOutputParsers } from '../../../../../../utils/output_parsers/N8nOutputParser'; +import { throwIfToolSchema } from '../../../../../../utils/schemaParsing'; +import { getTracingConfig } from '../../../../../../utils/tracing'; import { checkForStructuredTools, extractParsedOutput } from '../utils'; export async function planAndExecuteAgentExecute( diff --git a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/PlanAndExecuteAgent/prompt.ts b/packages/@n8n/nodes-langchain/nodes/agents/Agent/v1/agents/PlanAndExecuteAgent/prompt.ts similarity index 100% rename from packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/PlanAndExecuteAgent/prompt.ts rename to packages/@n8n/nodes-langchain/nodes/agents/Agent/v1/agents/PlanAndExecuteAgent/prompt.ts diff --git a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ReActAgent/description.ts b/packages/@n8n/nodes-langchain/nodes/agents/Agent/v1/agents/ReActAgent/description.ts similarity index 100% rename from packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ReActAgent/description.ts rename to packages/@n8n/nodes-langchain/nodes/agents/Agent/v1/agents/ReActAgent/description.ts diff --git a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ReActAgent/execute.ts b/packages/@n8n/nodes-langchain/nodes/agents/Agent/v1/agents/ReActAgent/execute.ts similarity index 92% rename from packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ReActAgent/execute.ts rename to packages/@n8n/nodes-langchain/nodes/agents/Agent/v1/agents/ReActAgent/execute.ts index b671a8189c..94b18a1f34 100644 --- a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ReActAgent/execute.ts +++ b/packages/@n8n/nodes-langchain/nodes/agents/Agent/v1/agents/ReActAgent/execute.ts @@ -15,10 +15,10 @@ import { getConnectedTools, getPromptInputByType, isChatInstance, -} from '../../../../../utils/helpers'; -import { getOptionalOutputParsers } from '../../../../../utils/output_parsers/N8nOutputParser'; -import { throwIfToolSchema } from '../../../../../utils/schemaParsing'; -import { getTracingConfig } from '../../../../../utils/tracing'; +} from '../../../../../../utils/helpers'; +import { getOptionalOutputParsers } from '../../../../../../utils/output_parsers/N8nOutputParser'; +import { throwIfToolSchema } from '../../../../../../utils/schemaParsing'; +import { getTracingConfig } from '../../../../../../utils/tracing'; import { checkForStructuredTools, extractParsedOutput } from '../utils'; export async function reActAgentAgentExecute( diff --git a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ReActAgent/prompt.ts b/packages/@n8n/nodes-langchain/nodes/agents/Agent/v1/agents/ReActAgent/prompt.ts similarity index 100% rename from packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ReActAgent/prompt.ts rename to packages/@n8n/nodes-langchain/nodes/agents/Agent/v1/agents/ReActAgent/prompt.ts diff --git a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/SqlAgent/description.ts b/packages/@n8n/nodes-langchain/nodes/agents/Agent/v1/agents/SqlAgent/description.ts similarity index 98% rename from packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/SqlAgent/description.ts rename to packages/@n8n/nodes-langchain/nodes/agents/Agent/v1/agents/SqlAgent/description.ts index bed547ba6d..945ca32520 100644 --- a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/SqlAgent/description.ts +++ b/packages/@n8n/nodes-langchain/nodes/agents/Agent/v1/agents/SqlAgent/description.ts @@ -5,7 +5,7 @@ import { promptTypeOptions, textFromPreviousNode, textInput, -} from '../../../../../utils/descriptions'; +} from '../../../../../../utils/descriptions'; const dataSourceOptions: INodeProperties = { displayName: 'Data Source', diff --git a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/SqlAgent/execute.ts b/packages/@n8n/nodes-langchain/nodes/agents/Agent/v1/agents/SqlAgent/execute.ts similarity index 98% rename from packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/SqlAgent/execute.ts rename to packages/@n8n/nodes-langchain/nodes/agents/Agent/v1/agents/SqlAgent/execute.ts index b9c0f3db8e..288588dc3d 100644 --- a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/SqlAgent/execute.ts +++ b/packages/@n8n/nodes-langchain/nodes/agents/Agent/v1/agents/SqlAgent/execute.ts @@ -16,8 +16,8 @@ import { getMysqlDataSource } from './other/handlers/mysql'; import { getPostgresDataSource } from './other/handlers/postgres'; import { getSqliteDataSource } from './other/handlers/sqlite'; import { SQL_PREFIX, SQL_SUFFIX } from './other/prompts'; -import { getPromptInputByType, serializeChatHistory } from '../../../../../utils/helpers'; -import { getTracingConfig } from '../../../../../utils/tracing'; +import { getPromptInputByType, serializeChatHistory } from '../../../../../../utils/helpers'; +import { getTracingConfig } from '../../../../../../utils/tracing'; const parseTablesString = (tablesString: string) => tablesString diff --git a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/SqlAgent/other/handlers/mysql.ts b/packages/@n8n/nodes-langchain/nodes/agents/Agent/v1/agents/SqlAgent/other/handlers/mysql.ts similarity index 100% rename from packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/SqlAgent/other/handlers/mysql.ts rename to packages/@n8n/nodes-langchain/nodes/agents/Agent/v1/agents/SqlAgent/other/handlers/mysql.ts diff --git a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/SqlAgent/other/handlers/postgres.ts b/packages/@n8n/nodes-langchain/nodes/agents/Agent/v1/agents/SqlAgent/other/handlers/postgres.ts similarity index 100% rename from packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/SqlAgent/other/handlers/postgres.ts rename to packages/@n8n/nodes-langchain/nodes/agents/Agent/v1/agents/SqlAgent/other/handlers/postgres.ts diff --git a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/SqlAgent/other/handlers/sqlite.ts b/packages/@n8n/nodes-langchain/nodes/agents/Agent/v1/agents/SqlAgent/other/handlers/sqlite.ts similarity index 100% rename from packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/SqlAgent/other/handlers/sqlite.ts rename to packages/@n8n/nodes-langchain/nodes/agents/Agent/v1/agents/SqlAgent/other/handlers/sqlite.ts diff --git a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/SqlAgent/other/prompts.ts b/packages/@n8n/nodes-langchain/nodes/agents/Agent/v1/agents/SqlAgent/other/prompts.ts similarity index 100% rename from packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/SqlAgent/other/prompts.ts rename to packages/@n8n/nodes-langchain/nodes/agents/Agent/v1/agents/SqlAgent/other/prompts.ts diff --git a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ToolsAgent/description.ts b/packages/@n8n/nodes-langchain/nodes/agents/Agent/v1/agents/ToolsAgent/description.ts similarity index 100% rename from packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ToolsAgent/description.ts rename to packages/@n8n/nodes-langchain/nodes/agents/Agent/v1/agents/ToolsAgent/description.ts diff --git a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ToolsAgent/execute.ts b/packages/@n8n/nodes-langchain/nodes/agents/Agent/v1/agents/ToolsAgent/execute.ts similarity index 99% rename from packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ToolsAgent/execute.ts rename to packages/@n8n/nodes-langchain/nodes/agents/Agent/v1/agents/ToolsAgent/execute.ts index 74d6819961..831a247ff8 100644 --- a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ToolsAgent/execute.ts +++ b/packages/@n8n/nodes-langchain/nodes/agents/Agent/v1/agents/ToolsAgent/execute.ts @@ -19,11 +19,11 @@ import { isChatInstance, getPromptInputByType, getConnectedTools, -} from '../../../../../utils/helpers'; +} from '../../../../../../utils/helpers'; import { getOptionalOutputParsers, type N8nOutputParser, -} from '../../../../../utils/output_parsers/N8nOutputParser'; +} from '../../../../../../utils/output_parsers/N8nOutputParser'; function getOutputParserSchema(outputParser: N8nOutputParser): ZodObject { const schema = diff --git a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ToolsAgent/prompt.ts b/packages/@n8n/nodes-langchain/nodes/agents/Agent/v1/agents/ToolsAgent/prompt.ts similarity index 100% rename from packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ToolsAgent/prompt.ts rename to packages/@n8n/nodes-langchain/nodes/agents/Agent/v1/agents/ToolsAgent/prompt.ts diff --git a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/utils.ts b/packages/@n8n/nodes-langchain/nodes/agents/Agent/v1/agents/utils.ts similarity index 100% rename from packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/utils.ts rename to packages/@n8n/nodes-langchain/nodes/agents/Agent/v1/agents/utils.ts index 144b622c76..991b5895a7 100644 --- a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/utils.ts +++ b/packages/@n8n/nodes-langchain/nodes/agents/Agent/v1/agents/utils.ts @@ -1,7 +1,7 @@ -import type { z } from 'zod'; import type { BaseOutputParser } from '@langchain/core/output_parsers'; import type { DynamicStructuredTool, Tool } from 'langchain/tools'; import { NodeOperationError, type IExecuteFunctions, type INode } from 'n8n-workflow'; +import type { z } from 'zod'; type ZodObjectAny = z.ZodObject; diff --git a/packages/@n8n/nodes-langchain/nodes/agents/Agent/test/utils.test.ts b/packages/@n8n/nodes-langchain/nodes/agents/Agent/v1/test/utils.test.ts similarity index 100% rename from packages/@n8n/nodes-langchain/nodes/agents/Agent/test/utils.test.ts rename to packages/@n8n/nodes-langchain/nodes/agents/Agent/v1/test/utils.test.ts diff --git a/packages/@n8n/nodes-langchain/nodes/agents/Agent/v2/AgentV2.node.ts b/packages/@n8n/nodes-langchain/nodes/agents/Agent/v2/AgentV2.node.ts new file mode 100644 index 0000000000..0e2bb49ac4 --- /dev/null +++ b/packages/@n8n/nodes-langchain/nodes/agents/Agent/v2/AgentV2.node.ts @@ -0,0 +1,424 @@ +import type { BaseChatModel } from '@langchain/core/language_models/chat_models'; +import { + HumanMessage, + SystemMessage, + type AIMessage, + type BaseMessage, + trimMessages, +} from '@langchain/core/messages'; +import type { RunnableConfig } from '@langchain/core/runnables'; +import { tool } from '@langchain/core/tools'; +import { StateGraph, Annotation, END, MemorySaver } from '@langchain/langgraph'; +import { ToolNode } from '@langchain/langgraph/prebuilt'; +import type { BaseChatMemory } from 'langchain/memory'; +import type { DynamicStructuredTool, Tool } from 'langchain/tools'; +import type { + IExecuteFunctions, + INodeType, + INodeTypeDescription, + INodeTypeBaseDescription, + INodeExecutionData, +} from 'n8n-workflow'; +import { NodeConnectionType, NodeOperationError } from 'n8n-workflow'; +import { v4 as uuidv4 } from 'uuid'; +import { z } from 'zod'; + +import { promptTypeOptions, textFromPreviousNode, textInput } from '../../../../utils/descriptions'; +import { getConnectedTools, getPromptInputByType, isChatInstance } from '../../../../utils/helpers'; + +const versionDescription: INodeTypeDescription = { + displayName: 'AI Agent', + name: 'agent', + icon: 'fa:robot', + group: ['transform'], + version: 2, + description: 'Generates an action plan and executes it. Can use external tools.', + defaults: { + name: 'AI Agent', + color: '#404040', + }, + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + inputs: [ + NodeConnectionType.Main, + { + type: NodeConnectionType.AiLanguageModel, + displayName: 'Chat Model', + required: true, + maxConnections: 1, + filter: { + nodes: [ + '@n8n/n8n-nodes-langchain.lmChatAnthropic', + '@n8n/n8n-nodes-langchain.lmChatAzureOpenAi', + '@n8n/n8n-nodes-langchain.lmChatAwsBedrock', + '@n8n/n8n-nodes-langchain.lmChatMistralCloud', + '@n8n/n8n-nodes-langchain.lmChatOllama', + '@n8n/n8n-nodes-langchain.lmChatOpenAi', + '@n8n/n8n-nodes-langchain.lmChatGroq', + '@n8n/n8n-nodes-langchain.lmChatGoogleVertex', + '@n8n/n8n-nodes-langchain.lmChatGoogleGemini', + ], + }, + }, + { + type: NodeConnectionType.AiMemory, + displayName: 'Memory', + required: false, + maxConnections: 1, + }, + { + type: NodeConnectionType.AiTool, + displayName: 'Tools', + required: false, + }, + ], + outputs: [NodeConnectionType.Main], + properties: [ + promptTypeOptions, + { ...textInput, displayOptions: { show: { promptType: ['define'] } } }, + { ...textFromPreviousNode, displayOptions: { show: { promptType: ['auto'] } } }, + { + displayName: 'System Message', + name: 'systemMessage', + type: 'string', + default: 'You are a helpful assistant', + description: 'The message that will be sent to the agent before the conversation starts', + typeOptions: { + rows: 10, + }, + }, + { + displayName: 'Conversation Routes', + name: 'routes', + placeholder: 'Add Route', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + sortable: true, + }, + default: {}, + options: [ + { + name: 'values', + displayName: 'Route', + values: [ + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + placeholder: 'e.g. Technical Support', + description: 'Name of this conversation route', + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + placeholder: 'e.g. Handle technical questions about our product', + description: 'Description of when this route should be used', + }, + { + displayName: 'Conditions', + name: 'conditions', + placeholder: 'Add Condition', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + default: {}, + options: [ + { + name: 'conditions', + displayName: 'Conditions', + values: [ + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '={{ $json.text }}', + description: 'The value to evaluate', + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + options: [ + { + name: 'Contains', + value: 'contains', + }, + { + name: 'Ends With', + value: 'endsWith', + }, + { + name: 'Equals', + value: 'equals', + }, + { + name: 'Not Contains', + value: 'notContains', + }, + { + name: 'Not Equals', + value: 'notEquals', + }, + { + name: 'Regex', + value: 'regex', + }, + { + name: 'Starts With', + value: 'startsWith', + }, + ], + default: 'contains', + }, + { + displayName: 'Match', + name: 'match', + type: 'string', + default: '', + placeholder: 'e.g. technical', + description: 'The value to match against', + }, + ], + }, + ], + }, + ], + }, + ], + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + default: {}, + placeholder: 'Add Option', + options: [ + { + displayName: 'Max Iterations', + name: 'maxIterations', + type: 'number', + default: 10, + description: 'The maximum number of iterations the agent will run before stopping', + }, + { + displayName: 'Return Intermediate Steps', + name: 'returnIntermediateSteps', + type: 'boolean', + default: false, + description: 'Whether or not the output should include intermediate steps the agent took', + }, + { + displayName: 'Automatically Passthrough Binary Images', + name: 'passthroughBinaryImages', + type: 'boolean', + default: true, + description: + 'Whether or not binary images should be automatically passed through to the agent as image type messages', + }, + ], + }, + ], +}; + +export class AgentV2 implements INodeType { + description: INodeTypeDescription; + + constructor(baseDescription: INodeTypeBaseDescription) { + this.description = { + ...baseDescription, + ...versionDescription, + }; + } + + async execute(this: IExecuteFunctions): Promise { + const model = (await this.getInputConnectionData( + NodeConnectionType.AiLanguageModel, + 0, + )) as BaseChatModel; + + if (!isChatInstance(model) || !model.bindTools) { + throw new NodeOperationError( + this.getNode(), + 'Langraph Agent requires Chat Model which supports Tools calling', + ); + } + + const memory = (await this.getInputConnectionData(NodeConnectionType.AiMemory, 0)) as + | BaseChatMemory + | undefined; + + const tools = (await getConnectedTools(this, true, false)) as Array< + DynamicStructuredTool | Tool + >; + const responseSchema = z.object({ + regular_answer: z.string().describe('Regular answer'), + pirate_voice_answer: z.string().describe('Answer as pirate'), + }); + const finalResponseTool = tool(async () => 'mocked value', { + name: 'format_final_response', + description: 'Always respond to the user using this tool.', + schema: responseSchema, + }); + + const StateAnnotation = Annotation.Root({ + input: Annotation<{ + prompt: string; + workflowName?: string; + }>({ + reducer: (x, y) => y ?? x ?? { prompt: '' }, + }), + messages: Annotation({ + reducer: (x, y) => x.concat(y), + }), + parsedOutput: Annotation>({ + reducer: (x, y) => y ?? x ?? { output: {} }, + }), + }); + + const toolNode = new ToolNode(tools); + + const modelWithTools = model.bindTools(tools); + + const stateModifier = async (messages: BaseMessage[]): Promise => { + return await trimMessages(messages, { + tokenCounter: (msgs) => msgs.length, + maxTokens: 10, // Adjust this number based on needs + strategy: 'last', + startOn: 'human', + includeSystem: false, + allowPartial: false, + }); + }; + + const memorySaver = new MemorySaver(); + + async function shouldContinue(state: typeof StateAnnotation.State) { + const { messages } = state; + const lastMessage = messages[messages.length - 1] as AIMessage; + + if (!lastMessage.tool_calls || lastMessage.tool_calls.length === 0) { + if (memory) { + const processedMessages = await stateModifier(messages); + const lastHumanMsg = processedMessages.find((m) => m.getType() === 'human'); + const lastAIMsg = processedMessages.find((m) => m.getType() === 'ai'); + + if (lastHumanMsg && lastAIMsg) { + await memory.saveContext( + { input: lastHumanMsg.content }, + { output: lastAIMsg.content }, + ); + } + } + return END; + } + return 'tools'; + } + + async function callModel(state: typeof StateAnnotation.State, config?: RunnableConfig) { + // console.log('Calling model with state: ', state) + const messages = state.messages; + const response = await modelWithTools.invoke(messages, config); + // console.log('Response from model: ', JSON.stringify(response, null, 2)) + // We return a list, because this will get added to the existing list + return { messages: [response] }; + } + + const workflow = new StateGraph(StateAnnotation) + .addNode('agent', callModel) + // .addNode('formatResponse', formatResponse) + .addNode('tools', toolNode) + .addEdge('__start__', 'agent') + .addConditionalEdges( + // First, we define the start node. We use `agent`. + // This means these are the edges taken after the `agent` node is called. + 'agent', + // Next, we pass in the function that will determine which node is called next. + shouldContinue, + // We supply a map of possible response values to the conditional edge + // to make it possible to draw a visualization of the graph. + { + [END]: END, + tools: 'tools', + // formatResponse: 'formatResponse', + }, + ) + // We now add a normal edge from `tools` to `agent`. + // This means that after `tools` is called, `agent` node is called next. + .addEdge('tools', 'agent'); + + const app = workflow.compile({ + checkpointer: memorySaver, + }); + + const returnData: INodeExecutionData[] = []; + const items = this.getInputData(); + + for (let itemIndex = 0; itemIndex < items.length; itemIndex++) { + try { + const systemMessage = this.getNodeParameter('systemMessage', itemIndex, '') as string; + const options = this.getNodeParameter('options', itemIndex, {}) as { + maxIterations?: number; + returnIntermediateSteps?: boolean; + }; + const input = getPromptInputByType({ + ctx: this, + i: itemIndex, + inputKey: 'text', + promptTypeKey: 'promptType', + }); + + if (input === undefined) { + throw new NodeOperationError(this.getNode(), 'The text parameter is empty.'); + } + + const chatHistory = (await memory?.chatHistory.getMessages()) ?? []; + const threadId = uuidv4(); + + const processedHistory = await stateModifier(chatHistory); + + const response = await app.invoke( + { + messages: [ + new SystemMessage(systemMessage), + ...processedHistory, + new HumanMessage(input), + ], + }, + { + configurable: { + thread_id: threadId, + maxIterations: options.maxIterations ?? 10, + }, + }, + ); + + // Get the final AI message from the response + const messages = response.messages ?? []; + const lastAIMessage = messages[messages.length - 1] as AIMessage; + + returnData.push({ + json: { + output: lastAIMessage?.content ?? '', + threadId, + }, + }); + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ + json: { error: error.message }, + pairedItem: { item: itemIndex }, + }); + continue; + } + throw error; + } + } + + return [returnData]; + } +} diff --git a/packages/@n8n/nodes-langchain/package.json b/packages/@n8n/nodes-langchain/package.json index f086f2e82f..f5ac7f44f3 100644 --- a/packages/@n8n/nodes-langchain/package.json +++ b/packages/@n8n/nodes-langchain/package.json @@ -145,6 +145,7 @@ "@langchain/google-genai": "0.1.4", "@langchain/google-vertexai": "0.1.3", "@langchain/groq": "0.1.2", + "@langchain/langgraph": "^0.2.27", "@langchain/mistralai": "0.2.0", "@langchain/ollama": "0.1.2", "@langchain/openai": "0.3.14", diff --git a/packages/nodes-base/nodes/Set/v2/SetV2.node.ts b/packages/nodes-base/nodes/Set/v2/SetV2.node.ts index b1dd055590..ca8fe7f96f 100644 --- a/packages/nodes-base/nodes/Set/v2/SetV2.node.ts +++ b/packages/nodes-base/nodes/Set/v2/SetV2.node.ts @@ -29,6 +29,7 @@ const versionDescription: INodeTypeDescription = { }, inputs: [NodeConnectionType.Main], outputs: [NodeConnectionType.Main], + usableAsTool: true, properties: [ { displayName: 'Mode', diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 55c52b25ca..60de8df7b7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -471,6 +471,9 @@ importers: '@langchain/groq': specifier: 0.1.2 version: 0.1.2(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)))(encoding@0.1.13) + '@langchain/langgraph': + specifier: ^0.2.27 + version: 0.2.27(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8))) '@langchain/mistralai': specifier: 0.2.0 version: 0.2.0(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8))) @@ -3585,6 +3588,21 @@ packages: peerDependencies: '@langchain/core': '>=0.2.21 <0.4.0' + '@langchain/langgraph-checkpoint@0.0.13': + resolution: {integrity: sha512-amdmBcNT8a9xP2VwcEWxqArng4gtRDcnVyVI4DsQIo1Aaz8e8+hH17zSwrUF3pt1pIYztngIfYnBOim31mtKMg==} + engines: {node: '>=18'} + peerDependencies: + '@langchain/core': '>=0.2.31 <0.4.0' + + '@langchain/langgraph-sdk@0.0.31': + resolution: {integrity: sha512-oYZWoC3x7vH9bAL1Y30XjtuWnic1j3knXD4BbldsY0chFLxwIT5i6/GMThNy3Oiwb4SB+c6gvaSuxBNDkp7dkw==} + + '@langchain/langgraph@0.2.27': + resolution: {integrity: sha512-7+PlVXlNpswzXzZp/k8O99YBN3zBkUdusfyxISkZ/gdXz1p5RySQEpKQ4EVIZnzBrZ98zZ3FArj4OWOgeF0EeA==} + engines: {node: '>=18'} + peerDependencies: + '@langchain/core': '>=0.2.36 <0.3.0 || >=0.3.9 < 0.4.0' + '@langchain/mistralai@0.2.0': resolution: {integrity: sha512-VdfbKZopAuSXf/vlXbriGWLK3c7j5s47DoB3S31xpprY2BMSKZZiX9vE9TsgxMfAPuIDPIYcfgU7p1upvTYt8g==} engines: {node: '>=18'} @@ -14755,6 +14773,26 @@ snapshots: - encoding - supports-color + '@langchain/langgraph-checkpoint@0.0.13(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)))': + dependencies: + '@langchain/core': 0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)) + uuid: 10.0.0 + + '@langchain/langgraph-sdk@0.0.31': + dependencies: + '@types/json-schema': 7.0.15 + p-queue: 6.6.2 + p-retry: 4.6.2 + uuid: 9.0.1 + + '@langchain/langgraph@0.2.27(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)))': + dependencies: + '@langchain/core': 0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)) + '@langchain/langgraph-checkpoint': 0.0.13(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8))) + '@langchain/langgraph-sdk': 0.0.31 + uuid: 10.0.0 + zod: 3.23.8 + '@langchain/mistralai@0.2.0(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)))': dependencies: '@langchain/core': 0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)) @@ -19228,7 +19266,7 @@ snapshots: eslint-import-resolver-node@0.3.9: dependencies: - debug: 3.2.7(supports-color@5.5.0) + debug: 3.2.7(supports-color@8.1.1) is-core-module: 2.13.1 resolve: 1.22.8 transitivePeerDependencies: @@ -19253,7 +19291,7 @@ snapshots: eslint-module-utils@2.8.0(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.7.2))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0): dependencies: - debug: 3.2.7(supports-color@5.5.0) + debug: 3.2.7(supports-color@8.1.1) optionalDependencies: '@typescript-eslint/parser': 7.2.0(eslint@8.57.0)(typescript@5.7.2) eslint: 8.57.0 @@ -19273,7 +19311,7 @@ snapshots: array.prototype.findlastindex: 1.2.3 array.prototype.flat: 1.3.2 array.prototype.flatmap: 1.3.2 - debug: 3.2.7(supports-color@5.5.0) + debug: 3.2.7(supports-color@8.1.1) doctrine: 2.1.0 eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 @@ -20052,7 +20090,7 @@ snapshots: array-parallel: 0.1.3 array-series: 0.1.5 cross-spawn: 4.0.2 - debug: 3.2.7(supports-color@5.5.0) + debug: 3.2.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -22955,7 +22993,7 @@ snapshots: pdf-parse@1.1.1: dependencies: - debug: 3.2.7(supports-color@5.5.0) + debug: 3.2.7(supports-color@8.1.1) node-ensure: 0.0.0 transitivePeerDependencies: - supports-color @@ -23764,7 +23802,7 @@ snapshots: rhea@1.0.24: dependencies: - debug: 3.2.7(supports-color@5.5.0) + debug: 3.2.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color