mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 01:56:46 +00:00
feat: Optimise langchain calls in batching mode (#15243)
This commit is contained in:
@@ -1,475 +1,49 @@
|
||||
import { NodeConnectionTypes, NodeOperationError } from 'n8n-workflow';
|
||||
import type {
|
||||
INodeInputConfiguration,
|
||||
INodeInputFilter,
|
||||
IExecuteFunctions,
|
||||
INodeExecutionData,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
INodeProperties,
|
||||
NodeConnectionType,
|
||||
} from 'n8n-workflow';
|
||||
import type { INodeTypeBaseDescription, IVersionedNodeType } from 'n8n-workflow';
|
||||
import { VersionedNodeType } from 'n8n-workflow';
|
||||
|
||||
import { promptTypeOptions, textFromPreviousNode, textInput } from '@utils/descriptions';
|
||||
import { AgentV1 } from './V1/AgentV1.node';
|
||||
import { AgentV2 } from './V2/AgentV2.node';
|
||||
|
||||
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';
|
||||
|
||||
// Function used in the inputs expression to figure out which inputs to
|
||||
// display based on the agent type
|
||||
function getInputs(
|
||||
agent:
|
||||
| 'toolsAgent'
|
||||
| 'conversationalAgent'
|
||||
| 'openAiFunctionsAgent'
|
||||
| 'planAndExecuteAgent'
|
||||
| 'reActAgent'
|
||||
| 'sqlAgent',
|
||||
hasOutputParser?: boolean,
|
||||
): Array<NodeConnectionType | INodeInputConfiguration> {
|
||||
interface SpecialInput {
|
||||
type: NodeConnectionType;
|
||||
filter?: INodeInputFilter;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
const getInputData = (
|
||||
inputs: SpecialInput[],
|
||||
): Array<NodeConnectionType | INodeInputConfiguration> => {
|
||||
const displayNames: { [key: string]: string } = {
|
||||
ai_languageModel: 'Model',
|
||||
ai_memory: 'Memory',
|
||||
ai_tool: 'Tool',
|
||||
ai_outputParser: 'Output Parser',
|
||||
export class Agent extends VersionedNodeType {
|
||||
constructor() {
|
||||
const baseDescription: INodeTypeBaseDescription = {
|
||||
displayName: 'AI Agent',
|
||||
name: 'agent',
|
||||
icon: 'fa:robot',
|
||||
iconColor: 'black',
|
||||
group: ['transform'],
|
||||
description: 'Generates an action plan and executes it. Can use external tools.',
|
||||
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/',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
defaultVersion: 2,
|
||||
};
|
||||
|
||||
return inputs.map(({ type, filter }) => {
|
||||
const isModelType = type === ('ai_languageModel' as NodeConnectionType);
|
||||
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: ['ai_languageModel', 'ai_memory', 'ai_outputParser'].includes(
|
||||
type as NodeConnectionType,
|
||||
)
|
||||
? 1
|
||||
: undefined,
|
||||
};
|
||||
const nodeVersions: IVersionedNodeType['nodeVersions'] = {
|
||||
1: new AgentV1(baseDescription),
|
||||
1.1: new AgentV1(baseDescription),
|
||||
1.2: new AgentV1(baseDescription),
|
||||
1.3: new AgentV1(baseDescription),
|
||||
1.4: new AgentV1(baseDescription),
|
||||
1.5: new AgentV1(baseDescription),
|
||||
1.6: new AgentV1(baseDescription),
|
||||
1.7: new AgentV1(baseDescription),
|
||||
1.8: new AgentV1(baseDescription),
|
||||
1.9: new AgentV1(baseDescription),
|
||||
2: new AgentV2(baseDescription),
|
||||
};
|
||||
|
||||
if (filter) {
|
||||
input.filter = filter;
|
||||
}
|
||||
|
||||
return input;
|
||||
});
|
||||
};
|
||||
|
||||
let specialInputs: SpecialInput[] = [];
|
||||
|
||||
if (agent === 'conversationalAgent') {
|
||||
specialInputs = [
|
||||
{
|
||||
type: 'ai_languageModel',
|
||||
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',
|
||||
'@n8n/n8n-nodes-langchain.lmChatDeepSeek',
|
||||
'@n8n/n8n-nodes-langchain.lmChatOpenRouter',
|
||||
'@n8n/n8n-nodes-langchain.lmChatXAiGrok',
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'ai_memory',
|
||||
},
|
||||
{
|
||||
type: 'ai_tool',
|
||||
},
|
||||
{
|
||||
type: 'ai_outputParser',
|
||||
},
|
||||
];
|
||||
} else if (agent === 'toolsAgent') {
|
||||
specialInputs = [
|
||||
{
|
||||
type: 'ai_languageModel',
|
||||
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',
|
||||
'@n8n/n8n-nodes-langchain.lmChatDeepSeek',
|
||||
'@n8n/n8n-nodes-langchain.lmChatOpenRouter',
|
||||
'@n8n/n8n-nodes-langchain.lmChatXAiGrok',
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'ai_memory',
|
||||
},
|
||||
{
|
||||
type: 'ai_tool',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
type: 'ai_outputParser',
|
||||
},
|
||||
];
|
||||
} else if (agent === 'openAiFunctionsAgent') {
|
||||
specialInputs = [
|
||||
{
|
||||
type: 'ai_languageModel',
|
||||
filter: {
|
||||
nodes: [
|
||||
'@n8n/n8n-nodes-langchain.lmChatOpenAi',
|
||||
'@n8n/n8n-nodes-langchain.lmChatAzureOpenAi',
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'ai_memory',
|
||||
},
|
||||
{
|
||||
type: 'ai_tool',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
type: 'ai_outputParser',
|
||||
},
|
||||
];
|
||||
} else if (agent === 'reActAgent') {
|
||||
specialInputs = [
|
||||
{
|
||||
type: 'ai_languageModel',
|
||||
},
|
||||
{
|
||||
type: 'ai_tool',
|
||||
},
|
||||
{
|
||||
type: 'ai_outputParser',
|
||||
},
|
||||
];
|
||||
} else if (agent === 'sqlAgent') {
|
||||
specialInputs = [
|
||||
{
|
||||
type: 'ai_languageModel',
|
||||
},
|
||||
{
|
||||
type: 'ai_memory',
|
||||
},
|
||||
];
|
||||
} else if (agent === 'planAndExecuteAgent') {
|
||||
specialInputs = [
|
||||
{
|
||||
type: 'ai_languageModel',
|
||||
},
|
||||
{
|
||||
type: 'ai_tool',
|
||||
},
|
||||
{
|
||||
type: 'ai_outputParser',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (hasOutputParser === false) {
|
||||
specialInputs = specialInputs.filter((input) => input.type !== 'ai_outputParser');
|
||||
}
|
||||
return ['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: '',
|
||||
};
|
||||
|
||||
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, 1.8, 1.9],
|
||||
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: [NodeConnectionTypes.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 <a href="https://docs.n8n.io/advanced-ai/intro-tutorial/" target="_blank">tutorial</a> or see an <a href="/templates/1954" target="_blank">example</a> of how this node works',
|
||||
name: 'notice_tip',
|
||||
type: 'notice',
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
agent: ['conversationalAgent', 'toolsAgent'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName:
|
||||
"This node is using Agent that has been deprecated. Please switch to using 'Tools Agent' instead.",
|
||||
name: 'deprecated',
|
||||
type: 'notice',
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
agent: [
|
||||
'conversationalAgent',
|
||||
'openAiFunctionsAgent',
|
||||
'planAndExecuteAgent',
|
||||
'reActAgent',
|
||||
'sqlAgent',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
// 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 1.7
|
||||
{
|
||||
...agentTypeProperty,
|
||||
displayOptions: { show: { '@version': [{ _cnd: { between: { from: 1.6, to: 1.7 } } }] } },
|
||||
default: 'toolsAgent',
|
||||
},
|
||||
// Make Tools Agent the only agent option for versions 1.8 and above
|
||||
{
|
||||
...agentTypeProperty,
|
||||
type: 'hidden',
|
||||
displayOptions: { show: { '@version': [{ _cnd: { gte: 1.8 } }] } },
|
||||
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 <a data-action='openSelectiveNodeCreator' data-action-parameter-connectiontype='${NodeConnectionTypes.AiOutputParser}'>output parser</a> 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<INodeExecutionData[][]> {
|
||||
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`);
|
||||
super(nodeVersions, baseDescription);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,460 @@
|
||||
import { NodeConnectionTypes, NodeOperationError } from 'n8n-workflow';
|
||||
import type {
|
||||
INodeInputConfiguration,
|
||||
INodeInputFilter,
|
||||
IExecuteFunctions,
|
||||
INodeExecutionData,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
INodeProperties,
|
||||
NodeConnectionType,
|
||||
INodeTypeBaseDescription,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { promptTypeOptions, textFromPreviousNode, textInput } from '@utils/descriptions';
|
||||
|
||||
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/V1/description';
|
||||
import { toolsAgentExecute } from '../agents/ToolsAgent/V1/execute';
|
||||
|
||||
// Function used in the inputs expression to figure out which inputs to
|
||||
// display based on the agent type
|
||||
function getInputs(
|
||||
agent:
|
||||
| 'toolsAgent'
|
||||
| 'conversationalAgent'
|
||||
| 'openAiFunctionsAgent'
|
||||
| 'planAndExecuteAgent'
|
||||
| 'reActAgent'
|
||||
| 'sqlAgent',
|
||||
hasOutputParser?: boolean,
|
||||
): Array<NodeConnectionType | INodeInputConfiguration> {
|
||||
interface SpecialInput {
|
||||
type: NodeConnectionType;
|
||||
filter?: INodeInputFilter;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
const getInputData = (
|
||||
inputs: SpecialInput[],
|
||||
): Array<NodeConnectionType | INodeInputConfiguration> => {
|
||||
const displayNames: { [key: string]: string } = {
|
||||
ai_languageModel: 'Model',
|
||||
ai_memory: 'Memory',
|
||||
ai_tool: 'Tool',
|
||||
ai_outputParser: 'Output Parser',
|
||||
};
|
||||
|
||||
return inputs.map(({ type, filter }) => {
|
||||
const isModelType = type === ('ai_languageModel' as NodeConnectionType);
|
||||
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: ['ai_languageModel', 'ai_memory', 'ai_outputParser'].includes(
|
||||
type as NodeConnectionType,
|
||||
)
|
||||
? 1
|
||||
: undefined,
|
||||
};
|
||||
|
||||
if (filter) {
|
||||
input.filter = filter;
|
||||
}
|
||||
|
||||
return input;
|
||||
});
|
||||
};
|
||||
|
||||
let specialInputs: SpecialInput[] = [];
|
||||
|
||||
if (agent === 'conversationalAgent') {
|
||||
specialInputs = [
|
||||
{
|
||||
type: 'ai_languageModel',
|
||||
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',
|
||||
'@n8n/n8n-nodes-langchain.lmChatDeepSeek',
|
||||
'@n8n/n8n-nodes-langchain.lmChatOpenRouter',
|
||||
'@n8n/n8n-nodes-langchain.lmChatXAiGrok',
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'ai_memory',
|
||||
},
|
||||
{
|
||||
type: 'ai_tool',
|
||||
},
|
||||
{
|
||||
type: 'ai_outputParser',
|
||||
},
|
||||
];
|
||||
} else if (agent === 'toolsAgent') {
|
||||
specialInputs = [
|
||||
{
|
||||
type: 'ai_languageModel',
|
||||
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',
|
||||
'@n8n/n8n-nodes-langchain.lmChatDeepSeek',
|
||||
'@n8n/n8n-nodes-langchain.lmChatOpenRouter',
|
||||
'@n8n/n8n-nodes-langchain.lmChatXAiGrok',
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'ai_memory',
|
||||
},
|
||||
{
|
||||
type: 'ai_tool',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
type: 'ai_outputParser',
|
||||
},
|
||||
];
|
||||
} else if (agent === 'openAiFunctionsAgent') {
|
||||
specialInputs = [
|
||||
{
|
||||
type: 'ai_languageModel',
|
||||
filter: {
|
||||
nodes: [
|
||||
'@n8n/n8n-nodes-langchain.lmChatOpenAi',
|
||||
'@n8n/n8n-nodes-langchain.lmChatAzureOpenAi',
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'ai_memory',
|
||||
},
|
||||
{
|
||||
type: 'ai_tool',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
type: 'ai_outputParser',
|
||||
},
|
||||
];
|
||||
} else if (agent === 'reActAgent') {
|
||||
specialInputs = [
|
||||
{
|
||||
type: 'ai_languageModel',
|
||||
},
|
||||
{
|
||||
type: 'ai_tool',
|
||||
},
|
||||
{
|
||||
type: 'ai_outputParser',
|
||||
},
|
||||
];
|
||||
} else if (agent === 'sqlAgent') {
|
||||
specialInputs = [
|
||||
{
|
||||
type: 'ai_languageModel',
|
||||
},
|
||||
{
|
||||
type: 'ai_memory',
|
||||
},
|
||||
];
|
||||
} else if (agent === 'planAndExecuteAgent') {
|
||||
specialInputs = [
|
||||
{
|
||||
type: 'ai_languageModel',
|
||||
},
|
||||
{
|
||||
type: 'ai_tool',
|
||||
},
|
||||
{
|
||||
type: 'ai_outputParser',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (hasOutputParser === false) {
|
||||
specialInputs = specialInputs.filter((input) => input.type !== 'ai_outputParser');
|
||||
}
|
||||
return ['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: '',
|
||||
};
|
||||
|
||||
export class AgentV1 implements INodeType {
|
||||
description: INodeTypeDescription;
|
||||
|
||||
constructor(baseDescription: INodeTypeBaseDescription) {
|
||||
this.description = {
|
||||
version: [1, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9],
|
||||
...baseDescription,
|
||||
defaults: {
|
||||
name: 'AI Agent',
|
||||
color: '#404040',
|
||||
},
|
||||
inputs: `={{
|
||||
((agent, hasOutputParser) => {
|
||||
${getInputs.toString()};
|
||||
return getInputs(agent, hasOutputParser)
|
||||
})($parameter.agent, $parameter.hasOutputParser === undefined || $parameter.hasOutputParser === true)
|
||||
}}`,
|
||||
outputs: [NodeConnectionTypes.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 <a href="https://docs.n8n.io/advanced-ai/intro-tutorial/" target="_blank">tutorial</a> or see an <a href="/templates/1954" target="_blank">example</a> of how this node works',
|
||||
name: 'notice_tip',
|
||||
type: 'notice',
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
agent: ['conversationalAgent', 'toolsAgent'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName:
|
||||
"This node is using Agent that has been deprecated. Please switch to using 'Tools Agent' instead.",
|
||||
name: 'deprecated',
|
||||
type: 'notice',
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
agent: [
|
||||
'conversationalAgent',
|
||||
'openAiFunctionsAgent',
|
||||
'planAndExecuteAgent',
|
||||
'reActAgent',
|
||||
'sqlAgent',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
// 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 1.7
|
||||
{
|
||||
...agentTypeProperty,
|
||||
displayOptions: { show: { '@version': [{ _cnd: { between: { from: 1.6, to: 1.7 } } }] } },
|
||||
default: 'toolsAgent',
|
||||
},
|
||||
// Make Tools Agent the only agent option for versions 1.8 and above
|
||||
{
|
||||
...agentTypeProperty,
|
||||
type: 'hidden',
|
||||
displayOptions: { show: { '@version': [{ _cnd: { gte: 1.8 } }] } },
|
||||
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 <a data-action='openSelectiveNodeCreator' data-action-parameter-connectiontype='${NodeConnectionTypes.AiOutputParser}'>output parser</a> 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<INodeExecutionData[][]> {
|
||||
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`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
import { NodeConnectionTypes } from 'n8n-workflow';
|
||||
import type {
|
||||
INodeInputConfiguration,
|
||||
INodeInputFilter,
|
||||
IExecuteFunctions,
|
||||
INodeExecutionData,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
NodeConnectionType,
|
||||
INodeTypeBaseDescription,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { promptTypeOptions, textFromPreviousNode, textInput } from '@utils/descriptions';
|
||||
|
||||
import { toolsAgentProperties } from '../agents/ToolsAgent/V2/description';
|
||||
import { toolsAgentExecute } from '../agents/ToolsAgent/V2/execute';
|
||||
|
||||
// Function used in the inputs expression to figure out which inputs to
|
||||
// display based on the agent type
|
||||
function getInputs(hasOutputParser?: boolean): Array<NodeConnectionType | INodeInputConfiguration> {
|
||||
interface SpecialInput {
|
||||
type: NodeConnectionType;
|
||||
filter?: INodeInputFilter;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
const getInputData = (
|
||||
inputs: SpecialInput[],
|
||||
): Array<NodeConnectionType | INodeInputConfiguration> => {
|
||||
const displayNames: { [key: string]: string } = {
|
||||
ai_languageModel: 'Model',
|
||||
ai_memory: 'Memory',
|
||||
ai_tool: 'Tool',
|
||||
ai_outputParser: 'Output Parser',
|
||||
};
|
||||
|
||||
return inputs.map(({ type, filter }) => {
|
||||
const isModelType = type === 'ai_languageModel';
|
||||
let displayName = type in displayNames ? displayNames[type] : undefined;
|
||||
if (isModelType) {
|
||||
displayName = 'Chat Model';
|
||||
}
|
||||
const input: INodeInputConfiguration = {
|
||||
type,
|
||||
displayName,
|
||||
required: isModelType,
|
||||
maxConnections: ['ai_languageModel', 'ai_memory', 'ai_outputParser'].includes(
|
||||
type as NodeConnectionType,
|
||||
)
|
||||
? 1
|
||||
: undefined,
|
||||
};
|
||||
|
||||
if (filter) {
|
||||
input.filter = filter;
|
||||
}
|
||||
|
||||
return input;
|
||||
});
|
||||
};
|
||||
|
||||
let specialInputs: SpecialInput[] = [
|
||||
{
|
||||
type: 'ai_languageModel',
|
||||
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',
|
||||
'@n8n/n8n-nodes-langchain.lmChatDeepSeek',
|
||||
'@n8n/n8n-nodes-langchain.lmChatOpenRouter',
|
||||
'@n8n/n8n-nodes-langchain.lmChatXAiGrok',
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'ai_memory',
|
||||
},
|
||||
{
|
||||
type: 'ai_tool',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
type: 'ai_outputParser',
|
||||
},
|
||||
];
|
||||
|
||||
if (hasOutputParser === false) {
|
||||
specialInputs = specialInputs.filter((input) => input.type !== 'ai_outputParser');
|
||||
}
|
||||
return ['main', ...getInputData(specialInputs)];
|
||||
}
|
||||
|
||||
export class AgentV2 implements INodeType {
|
||||
description: INodeTypeDescription;
|
||||
|
||||
constructor(baseDescription: INodeTypeBaseDescription) {
|
||||
this.description = {
|
||||
...baseDescription,
|
||||
version: 2,
|
||||
defaults: {
|
||||
name: 'AI Agent',
|
||||
color: '#404040',
|
||||
},
|
||||
inputs: `={{
|
||||
((hasOutputParser) => {
|
||||
${getInputs.toString()};
|
||||
return getInputs(hasOutputParser)
|
||||
})($parameter.hasOutputParser === undefined || $parameter.hasOutputParser === true)
|
||||
}}`,
|
||||
outputs: [NodeConnectionTypes.Main],
|
||||
properties: [
|
||||
{
|
||||
displayName:
|
||||
'Tip: Get a feel for agents with our quick <a href="https://docs.n8n.io/advanced-ai/intro-tutorial/" target="_blank">tutorial</a> or see an <a href="/templates/1954" target="_blank">example</a> of how this node works',
|
||||
name: 'notice_tip',
|
||||
type: 'notice',
|
||||
default: '',
|
||||
},
|
||||
promptTypeOptions,
|
||||
{
|
||||
...textFromPreviousNode,
|
||||
displayOptions: {
|
||||
show: {
|
||||
promptType: ['auto'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
...textInput,
|
||||
displayOptions: {
|
||||
show: {
|
||||
promptType: ['define'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Require Specific Output Format',
|
||||
name: 'hasOutputParser',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
noDataExpression: true,
|
||||
},
|
||||
{
|
||||
displayName: `Connect an <a data-action='openSelectiveNodeCreator' data-action-parameter-connectiontype='${NodeConnectionTypes.AiOutputParser}'>output parser</a> on the canvas to specify the output format you require`,
|
||||
name: 'notice',
|
||||
type: 'notice',
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
hasOutputParser: [true],
|
||||
},
|
||||
},
|
||||
},
|
||||
...toolsAgentProperties,
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
return await toolsAgentExecute.call(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
import { commonOptions } from '../options';
|
||||
|
||||
export const toolsAgentProperties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
type: 'collection',
|
||||
displayOptions: {
|
||||
show: {
|
||||
agent: ['toolsAgent'],
|
||||
},
|
||||
},
|
||||
default: {},
|
||||
placeholder: 'Add Option',
|
||||
options: [...commonOptions],
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,138 @@
|
||||
import { RunnableSequence } from '@langchain/core/runnables';
|
||||
import { AgentExecutor, createToolCallingAgent } from 'langchain/agents';
|
||||
import { omit } from 'lodash';
|
||||
import { jsonParse, NodeOperationError } from 'n8n-workflow';
|
||||
import type { IExecuteFunctions, INodeExecutionData } from 'n8n-workflow';
|
||||
|
||||
import { getPromptInputByType } from '@utils/helpers';
|
||||
import { getOptionalOutputParser } from '@utils/output_parsers/N8nOutputParser';
|
||||
|
||||
import {
|
||||
fixEmptyContentMessage,
|
||||
getAgentStepsParser,
|
||||
getChatModel,
|
||||
getOptionalMemory,
|
||||
getTools,
|
||||
prepareMessages,
|
||||
preparePrompt,
|
||||
} from '../common';
|
||||
import { SYSTEM_MESSAGE } from '../prompt';
|
||||
|
||||
/* -----------------------------------------------------------
|
||||
Main Executor Function
|
||||
----------------------------------------------------------- */
|
||||
/**
|
||||
* The main executor method for the Tools Agent.
|
||||
*
|
||||
* This function retrieves necessary components (model, memory, tools), prepares the prompt,
|
||||
* creates the agent, and processes each input item. The error handling for each item is also
|
||||
* managed here based on the node's continueOnFail setting.
|
||||
*
|
||||
* @returns The array of execution data for all processed items
|
||||
*/
|
||||
export async function toolsAgentExecute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
this.logger.debug('Executing Tools Agent');
|
||||
|
||||
const returnData: INodeExecutionData[] = [];
|
||||
const items = this.getInputData();
|
||||
const outputParser = await getOptionalOutputParser(this);
|
||||
const tools = await getTools(this, outputParser);
|
||||
|
||||
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
|
||||
try {
|
||||
const model = await getChatModel(this);
|
||||
const memory = await getOptionalMemory(this);
|
||||
|
||||
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 options = this.getNodeParameter('options', itemIndex, {}) as {
|
||||
systemMessage?: string;
|
||||
maxIterations?: number;
|
||||
returnIntermediateSteps?: boolean;
|
||||
passthroughBinaryImages?: boolean;
|
||||
};
|
||||
|
||||
// Prepare the prompt messages and prompt template.
|
||||
const messages = await prepareMessages(this, itemIndex, {
|
||||
systemMessage: options.systemMessage,
|
||||
passthroughBinaryImages: options.passthroughBinaryImages ?? true,
|
||||
outputParser,
|
||||
});
|
||||
const prompt = preparePrompt(messages);
|
||||
|
||||
// Create the base agent that calls tools.
|
||||
const agent = createToolCallingAgent({
|
||||
llm: model,
|
||||
tools,
|
||||
prompt,
|
||||
streamRunnable: false,
|
||||
});
|
||||
agent.streamRunnable = false;
|
||||
// Wrap the agent with parsers and fixes.
|
||||
const runnableAgent = RunnableSequence.from([
|
||||
agent,
|
||||
getAgentStepsParser(outputParser, memory),
|
||||
fixEmptyContentMessage,
|
||||
]);
|
||||
const executor = AgentExecutor.fromAgentAndTools({
|
||||
agent: runnableAgent,
|
||||
memory,
|
||||
tools,
|
||||
returnIntermediateSteps: options.returnIntermediateSteps === true,
|
||||
maxIterations: options.maxIterations ?? 10,
|
||||
});
|
||||
|
||||
// Invoke the executor with the given input and system message.
|
||||
const response = await executor.invoke(
|
||||
{
|
||||
input,
|
||||
system_message: options.systemMessage ?? SYSTEM_MESSAGE,
|
||||
formatting_instructions:
|
||||
'IMPORTANT: For your response to user, you MUST use the `format_final_json_response` tool with your complete answer formatted according to the required schema. Do not attempt to format the JSON manually - always use this tool. Your response will be rejected if it is not properly formatted through this tool. Only use this tool once you are ready to provide your final answer.',
|
||||
},
|
||||
{ signal: this.getExecutionCancelSignal() },
|
||||
);
|
||||
|
||||
// If memory and outputParser are connected, parse the output.
|
||||
if (memory && outputParser) {
|
||||
const parsedOutput = jsonParse<{ output: Record<string, unknown> }>(
|
||||
response.output as string,
|
||||
);
|
||||
response.output = parsedOutput?.output ?? parsedOutput;
|
||||
}
|
||||
|
||||
// Omit internal keys before returning the result.
|
||||
const itemResult = {
|
||||
json: omit(
|
||||
response,
|
||||
'system_message',
|
||||
'formatting_instructions',
|
||||
'input',
|
||||
'chat_history',
|
||||
'agent_scratchpad',
|
||||
),
|
||||
};
|
||||
|
||||
returnData.push(itemResult);
|
||||
} catch (error) {
|
||||
if (this.continueOnFail()) {
|
||||
returnData.push({
|
||||
json: { error: error.message },
|
||||
pairedItem: { item: itemIndex },
|
||||
});
|
||||
continue;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
return [returnData];
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
import { getBatchingOptionFields } from '@utils/sharedFields';
|
||||
|
||||
import { commonOptions } from '../options';
|
||||
|
||||
export const toolsAgentProperties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
type: 'collection',
|
||||
default: {},
|
||||
placeholder: 'Add Option',
|
||||
options: [...commonOptions, getBatchingOptionFields(undefined, 1)],
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,161 @@
|
||||
import type { ChatPromptTemplate } from '@langchain/core/prompts';
|
||||
import { RunnableSequence } from '@langchain/core/runnables';
|
||||
import { AgentExecutor, createToolCallingAgent } from 'langchain/agents';
|
||||
import { omit } from 'lodash';
|
||||
import { jsonParse, NodeOperationError, sleep } from 'n8n-workflow';
|
||||
import type { IExecuteFunctions, INodeExecutionData } from 'n8n-workflow';
|
||||
|
||||
import { getPromptInputByType } from '@utils/helpers';
|
||||
import { getOptionalOutputParser } from '@utils/output_parsers/N8nOutputParser';
|
||||
|
||||
import {
|
||||
fixEmptyContentMessage,
|
||||
getAgentStepsParser,
|
||||
getChatModel,
|
||||
getOptionalMemory,
|
||||
getTools,
|
||||
prepareMessages,
|
||||
preparePrompt,
|
||||
} from '../common';
|
||||
import { SYSTEM_MESSAGE } from '../prompt';
|
||||
|
||||
/* -----------------------------------------------------------
|
||||
Main Executor Function
|
||||
----------------------------------------------------------- */
|
||||
/**
|
||||
* The main executor method for the Tools Agent.
|
||||
*
|
||||
* This function retrieves necessary components (model, memory, tools), prepares the prompt,
|
||||
* creates the agent, and processes each input item. The error handling for each item is also
|
||||
* managed here based on the node's continueOnFail setting.
|
||||
*
|
||||
* @returns The array of execution data for all processed items
|
||||
*/
|
||||
export async function toolsAgentExecute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
this.logger.debug('Executing Tools Agent V2');
|
||||
|
||||
const returnData: INodeExecutionData[] = [];
|
||||
const items = this.getInputData();
|
||||
const outputParser = await getOptionalOutputParser(this);
|
||||
const tools = await getTools(this, outputParser);
|
||||
const batchSize = this.getNodeParameter('options.batching.batchSize', 0, 1) as number;
|
||||
const delayBetweenBatches = this.getNodeParameter(
|
||||
'options.batching.delayBetweenBatches',
|
||||
0,
|
||||
0,
|
||||
) as number;
|
||||
const memory = await getOptionalMemory(this);
|
||||
const model = await getChatModel(this);
|
||||
|
||||
for (let i = 0; i < items.length; i += batchSize) {
|
||||
const batch = items.slice(i, i + batchSize);
|
||||
const batchPromises = batch.map(async (_item, batchItemIndex) => {
|
||||
const itemIndex = i + batchItemIndex;
|
||||
|
||||
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 options = this.getNodeParameter('options', itemIndex, {}) as {
|
||||
systemMessage?: string;
|
||||
maxIterations?: number;
|
||||
returnIntermediateSteps?: boolean;
|
||||
passthroughBinaryImages?: boolean;
|
||||
};
|
||||
|
||||
// Prepare the prompt messages and prompt template.
|
||||
const messages = await prepareMessages(this, itemIndex, {
|
||||
systemMessage: options.systemMessage,
|
||||
passthroughBinaryImages: options.passthroughBinaryImages ?? true,
|
||||
outputParser,
|
||||
});
|
||||
const prompt: ChatPromptTemplate = preparePrompt(messages);
|
||||
|
||||
// Create the base agent that calls tools.
|
||||
const agent = createToolCallingAgent({
|
||||
llm: model,
|
||||
tools,
|
||||
prompt,
|
||||
streamRunnable: false,
|
||||
});
|
||||
agent.streamRunnable = false;
|
||||
// Wrap the agent with parsers and fixes.
|
||||
const runnableAgent = RunnableSequence.from([
|
||||
agent,
|
||||
getAgentStepsParser(outputParser, memory),
|
||||
fixEmptyContentMessage,
|
||||
]);
|
||||
const executor = AgentExecutor.fromAgentAndTools({
|
||||
agent: runnableAgent,
|
||||
memory,
|
||||
tools,
|
||||
returnIntermediateSteps: options.returnIntermediateSteps === true,
|
||||
maxIterations: options.maxIterations ?? 10,
|
||||
});
|
||||
|
||||
// Invoke the executor with the given input and system message.
|
||||
return await executor.invoke(
|
||||
{
|
||||
input,
|
||||
system_message: options.systemMessage ?? SYSTEM_MESSAGE,
|
||||
formatting_instructions:
|
||||
'IMPORTANT: For your response to user, you MUST use the `format_final_json_response` tool with your complete answer formatted according to the required schema. Do not attempt to format the JSON manually - always use this tool. Your response will be rejected if it is not properly formatted through this tool. Only use this tool once you are ready to provide your final answer.',
|
||||
},
|
||||
{ signal: this.getExecutionCancelSignal() },
|
||||
);
|
||||
});
|
||||
|
||||
const batchResults = await Promise.allSettled(batchPromises);
|
||||
|
||||
batchResults.forEach((result, index) => {
|
||||
const itemIndex = i + index;
|
||||
if (result.status === 'rejected') {
|
||||
const error = result.reason as Error;
|
||||
if (this.continueOnFail()) {
|
||||
returnData.push({
|
||||
json: { error: error.message },
|
||||
pairedItem: { item: itemIndex },
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
throw new NodeOperationError(this.getNode(), error);
|
||||
}
|
||||
}
|
||||
const response = result.value;
|
||||
// If memory and outputParser are connected, parse the output.
|
||||
if (memory && outputParser) {
|
||||
const parsedOutput = jsonParse<{ output: Record<string, unknown> }>(
|
||||
response.output as string,
|
||||
);
|
||||
response.output = parsedOutput?.output ?? parsedOutput;
|
||||
}
|
||||
|
||||
// Omit internal keys before returning the result.
|
||||
const itemResult = {
|
||||
json: omit(
|
||||
response,
|
||||
'system_message',
|
||||
'formatting_instructions',
|
||||
'input',
|
||||
'chat_history',
|
||||
'agent_scratchpad',
|
||||
),
|
||||
pairedItem: { item: itemIndex },
|
||||
};
|
||||
|
||||
returnData.push(itemResult);
|
||||
});
|
||||
|
||||
if (i + batchSize < items.length && delayBetweenBatches > 0) {
|
||||
await sleep(delayBetweenBatches);
|
||||
}
|
||||
}
|
||||
|
||||
return [returnData];
|
||||
}
|
||||
@@ -1,29 +1,18 @@
|
||||
import type { BaseChatMemory } from '@langchain/community/memory/chat_memory';
|
||||
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
||||
import { HumanMessage } from '@langchain/core/messages';
|
||||
import type { BaseMessage } from '@langchain/core/messages';
|
||||
import type { BaseMessagePromptTemplateLike } from '@langchain/core/prompts';
|
||||
import { ChatPromptTemplate } from '@langchain/core/prompts';
|
||||
import { RunnableSequence } from '@langchain/core/runnables';
|
||||
import type { Tool } from '@langchain/core/tools';
|
||||
import { DynamicStructuredTool } from '@langchain/core/tools';
|
||||
import { ChatPromptTemplate, type BaseMessagePromptTemplateLike } from '@langchain/core/prompts';
|
||||
import type { AgentAction, AgentFinish } from 'langchain/agents';
|
||||
import { AgentExecutor, createToolCallingAgent } from 'langchain/agents';
|
||||
import type { ToolsAgentAction } from 'langchain/dist/agents/tool_calling/output_parser';
|
||||
import { omit } from 'lodash';
|
||||
import type { BaseChatMemory } from 'langchain/memory';
|
||||
import { DynamicStructuredTool, type Tool } from 'langchain/tools';
|
||||
import { BINARY_ENCODING, jsonParse, NodeConnectionTypes, NodeOperationError } from 'n8n-workflow';
|
||||
import type { IExecuteFunctions, INodeExecutionData } from 'n8n-workflow';
|
||||
import type { IExecuteFunctions } from 'n8n-workflow';
|
||||
import type { ZodObject } from 'zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { isChatInstance, getPromptInputByType, getConnectedTools } from '@utils/helpers';
|
||||
import {
|
||||
getOptionalOutputParser,
|
||||
type N8nOutputParser,
|
||||
} from '@utils/output_parsers/N8nOutputParser';
|
||||
|
||||
import { SYSTEM_MESSAGE } from './prompt';
|
||||
|
||||
import { isChatInstance, getConnectedTools } from '@utils/helpers';
|
||||
import { type N8nOutputParser } from '@utils/output_parsers/N8nOutputParser';
|
||||
/* -----------------------------------------------------------
|
||||
Output Parser Helper
|
||||
----------------------------------------------------------- */
|
||||
@@ -387,122 +376,3 @@ export async function prepareMessages(
|
||||
export function preparePrompt(messages: BaseMessagePromptTemplateLike[]): ChatPromptTemplate {
|
||||
return ChatPromptTemplate.fromMessages(messages);
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------------
|
||||
Main Executor Function
|
||||
----------------------------------------------------------- */
|
||||
/**
|
||||
* The main executor method for the Tools Agent.
|
||||
*
|
||||
* This function retrieves necessary components (model, memory, tools), prepares the prompt,
|
||||
* creates the agent, and processes each input item. The error handling for each item is also
|
||||
* managed here based on the node's continueOnFail setting.
|
||||
*
|
||||
* @returns The array of execution data for all processed items
|
||||
*/
|
||||
export async function toolsAgentExecute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
this.logger.debug('Executing Tools Agent');
|
||||
|
||||
const returnData: INodeExecutionData[] = [];
|
||||
const items = this.getInputData();
|
||||
const outputParser = await getOptionalOutputParser(this);
|
||||
const tools = await getTools(this, outputParser);
|
||||
|
||||
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
|
||||
try {
|
||||
const model = await getChatModel(this);
|
||||
const memory = await getOptionalMemory(this);
|
||||
|
||||
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 options = this.getNodeParameter('options', itemIndex, {}) as {
|
||||
systemMessage?: string;
|
||||
maxIterations?: number;
|
||||
returnIntermediateSteps?: boolean;
|
||||
passthroughBinaryImages?: boolean;
|
||||
};
|
||||
|
||||
// Prepare the prompt messages and prompt template.
|
||||
const messages = await prepareMessages(this, itemIndex, {
|
||||
systemMessage: options.systemMessage,
|
||||
passthroughBinaryImages: options.passthroughBinaryImages ?? true,
|
||||
outputParser,
|
||||
});
|
||||
const prompt = preparePrompt(messages);
|
||||
|
||||
// Create the base agent that calls tools.
|
||||
const agent = createToolCallingAgent({
|
||||
llm: model,
|
||||
tools,
|
||||
prompt,
|
||||
streamRunnable: false,
|
||||
});
|
||||
agent.streamRunnable = false;
|
||||
// Wrap the agent with parsers and fixes.
|
||||
const runnableAgent = RunnableSequence.from([
|
||||
agent,
|
||||
getAgentStepsParser(outputParser, memory),
|
||||
fixEmptyContentMessage,
|
||||
]);
|
||||
const executor = AgentExecutor.fromAgentAndTools({
|
||||
agent: runnableAgent,
|
||||
memory,
|
||||
tools,
|
||||
returnIntermediateSteps: options.returnIntermediateSteps === true,
|
||||
maxIterations: options.maxIterations ?? 10,
|
||||
});
|
||||
|
||||
// Invoke the executor with the given input and system message.
|
||||
const response = await executor.invoke(
|
||||
{
|
||||
input,
|
||||
system_message: options.systemMessage ?? SYSTEM_MESSAGE,
|
||||
formatting_instructions:
|
||||
'IMPORTANT: For your response to user, you MUST use the `format_final_json_response` tool with your complete answer formatted according to the required schema. Do not attempt to format the JSON manually - always use this tool. Your response will be rejected if it is not properly formatted through this tool. Only use this tool once you are ready to provide your final answer.',
|
||||
},
|
||||
{ signal: this.getExecutionCancelSignal() },
|
||||
);
|
||||
|
||||
// If memory and outputParser are connected, parse the output.
|
||||
if (memory && outputParser) {
|
||||
const parsedOutput = jsonParse<{ output: Record<string, unknown> }>(
|
||||
response.output as string,
|
||||
);
|
||||
response.output = parsedOutput?.output ?? parsedOutput;
|
||||
}
|
||||
|
||||
// Omit internal keys before returning the result.
|
||||
const itemResult = {
|
||||
json: omit(
|
||||
response,
|
||||
'system_message',
|
||||
'formatting_instructions',
|
||||
'input',
|
||||
'chat_history',
|
||||
'agent_scratchpad',
|
||||
),
|
||||
};
|
||||
|
||||
returnData.push(itemResult);
|
||||
} catch (error) {
|
||||
if (this.continueOnFail()) {
|
||||
returnData.push({
|
||||
json: { error: error.message },
|
||||
pairedItem: { item: itemIndex },
|
||||
});
|
||||
continue;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
return [returnData];
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
import { SYSTEM_MESSAGE } from './prompt';
|
||||
|
||||
export const toolsAgentProperties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
type: 'collection',
|
||||
displayOptions: {
|
||||
show: {
|
||||
agent: ['toolsAgent'],
|
||||
},
|
||||
},
|
||||
default: {},
|
||||
placeholder: 'Add Option',
|
||||
options: [
|
||||
{
|
||||
displayName: 'System Message',
|
||||
name: 'systemMessage',
|
||||
type: 'string',
|
||||
default: SYSTEM_MESSAGE,
|
||||
description: 'The message that will be sent to the agent before the conversation starts',
|
||||
typeOptions: {
|
||||
rows: 6,
|
||||
},
|
||||
},
|
||||
{
|
||||
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',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,38 @@
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
import { SYSTEM_MESSAGE } from './prompt';
|
||||
|
||||
export const commonOptions: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'System Message',
|
||||
name: 'systemMessage',
|
||||
type: 'string',
|
||||
default: SYSTEM_MESSAGE,
|
||||
description: 'The message that will be sent to the agent before the conversation starts',
|
||||
typeOptions: {
|
||||
rows: 6,
|
||||
},
|
||||
},
|
||||
{
|
||||
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',
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,159 @@
|
||||
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import { AgentExecutor } from 'langchain/agents';
|
||||
import type { Tool } from 'langchain/tools';
|
||||
import type { IExecuteFunctions, INode } from 'n8n-workflow';
|
||||
|
||||
import * as helpers from '../../../../../utils/helpers';
|
||||
import { toolsAgentExecute } from '../../agents/ToolsAgent/V1/execute';
|
||||
|
||||
const mockHelpers = mock<IExecuteFunctions['helpers']>();
|
||||
const mockContext = mock<IExecuteFunctions>({ helpers: mockHelpers });
|
||||
|
||||
beforeEach(() => jest.resetAllMocks());
|
||||
|
||||
describe('toolsAgentExecute', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockContext.logger = {
|
||||
debug: jest.fn(),
|
||||
info: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
error: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
it('should process items', async () => {
|
||||
const mockNode = mock<INode>();
|
||||
mockContext.getNode.mockReturnValue(mockNode);
|
||||
mockContext.getInputData.mockReturnValue([
|
||||
{ json: { text: 'test input 1' } },
|
||||
{ json: { text: 'test input 2' } },
|
||||
]);
|
||||
|
||||
const mockModel = mock<BaseChatModel>();
|
||||
mockModel.bindTools = jest.fn();
|
||||
mockModel.lc_namespace = ['chat_models'];
|
||||
mockContext.getInputConnectionData.mockResolvedValue(mockModel);
|
||||
|
||||
const mockTools = [mock<Tool>()];
|
||||
jest.spyOn(helpers, 'getConnectedTools').mockResolvedValue(mockTools);
|
||||
|
||||
// Mock getNodeParameter to return default values
|
||||
mockContext.getNodeParameter.mockImplementation((param, _i, defaultValue) => {
|
||||
if (param === 'text') return 'test input';
|
||||
if (param === 'options')
|
||||
return {
|
||||
systemMessage: 'You are a helpful assistant',
|
||||
maxIterations: 10,
|
||||
returnIntermediateSteps: false,
|
||||
passthroughBinaryImages: true,
|
||||
};
|
||||
return defaultValue;
|
||||
});
|
||||
|
||||
const mockExecutor = {
|
||||
invoke: jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce({ output: JSON.stringify({ text: 'success 1' }) })
|
||||
.mockResolvedValueOnce({ output: JSON.stringify({ text: 'success 2' }) }),
|
||||
};
|
||||
|
||||
jest.spyOn(AgentExecutor, 'fromAgentAndTools').mockReturnValue(mockExecutor as any);
|
||||
|
||||
const result = await toolsAgentExecute.call(mockContext);
|
||||
|
||||
expect(mockExecutor.invoke).toHaveBeenCalledTimes(2);
|
||||
expect(result[0]).toHaveLength(2);
|
||||
expect(result[0][0].json).toEqual({ output: { text: 'success 1' } });
|
||||
expect(result[0][1].json).toEqual({ output: { text: 'success 2' } });
|
||||
});
|
||||
|
||||
it('should handle errors when continueOnFail is true', async () => {
|
||||
const mockNode = mock<INode>();
|
||||
mockContext.getNode.mockReturnValue(mockNode);
|
||||
mockContext.getInputData.mockReturnValue([
|
||||
{ json: { text: 'test input 1' } },
|
||||
{ json: { text: 'test input 2' } },
|
||||
]);
|
||||
|
||||
const mockModel = mock<BaseChatModel>();
|
||||
mockModel.bindTools = jest.fn();
|
||||
mockModel.lc_namespace = ['chat_models'];
|
||||
mockContext.getInputConnectionData.mockResolvedValue(mockModel);
|
||||
|
||||
const mockTools = [mock<Tool>()];
|
||||
jest.spyOn(helpers, 'getConnectedTools').mockResolvedValue(mockTools);
|
||||
|
||||
mockContext.getNodeParameter.mockImplementation((param, _i, defaultValue) => {
|
||||
if (param === 'text') return 'test input';
|
||||
if (param === 'options')
|
||||
return {
|
||||
systemMessage: 'You are a helpful assistant',
|
||||
maxIterations: 10,
|
||||
returnIntermediateSteps: false,
|
||||
passthroughBinaryImages: true,
|
||||
};
|
||||
return defaultValue;
|
||||
});
|
||||
|
||||
mockContext.continueOnFail.mockReturnValue(true);
|
||||
|
||||
const mockExecutor = {
|
||||
invoke: jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce({ output: '{ "text": "success" }' })
|
||||
.mockRejectedValueOnce(new Error('Test error')),
|
||||
};
|
||||
|
||||
jest.spyOn(AgentExecutor, 'fromAgentAndTools').mockReturnValue(mockExecutor as any);
|
||||
|
||||
const result = await toolsAgentExecute.call(mockContext);
|
||||
|
||||
expect(result[0]).toHaveLength(2);
|
||||
expect(result[0][0].json).toEqual({ output: { text: 'success' } });
|
||||
expect(result[0][1].json).toEqual({ error: 'Test error' });
|
||||
});
|
||||
|
||||
it('should throw error in when continueOnFail is false', async () => {
|
||||
const mockNode = mock<INode>();
|
||||
mockContext.getNode.mockReturnValue(mockNode);
|
||||
mockContext.getInputData.mockReturnValue([
|
||||
{ json: { text: 'test input 1' } },
|
||||
{ json: { text: 'test input 2' } },
|
||||
]);
|
||||
|
||||
const mockModel = mock<BaseChatModel>();
|
||||
mockModel.bindTools = jest.fn();
|
||||
mockModel.lc_namespace = ['chat_models'];
|
||||
mockContext.getInputConnectionData.mockResolvedValue(mockModel);
|
||||
|
||||
const mockTools = [mock<Tool>()];
|
||||
jest.spyOn(helpers, 'getConnectedTools').mockResolvedValue(mockTools);
|
||||
|
||||
mockContext.getNodeParameter.mockImplementation((param, _i, defaultValue) => {
|
||||
if (param === 'text') return 'test input';
|
||||
if (param === 'options')
|
||||
return {
|
||||
systemMessage: 'You are a helpful assistant',
|
||||
maxIterations: 10,
|
||||
returnIntermediateSteps: false,
|
||||
passthroughBinaryImages: true,
|
||||
};
|
||||
return defaultValue;
|
||||
});
|
||||
|
||||
mockContext.continueOnFail.mockReturnValue(false);
|
||||
|
||||
const mockExecutor = {
|
||||
invoke: jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce({ output: JSON.stringify({ text: 'success' }) })
|
||||
.mockRejectedValueOnce(new Error('Test error')),
|
||||
};
|
||||
|
||||
jest.spyOn(AgentExecutor, 'fromAgentAndTools').mockReturnValue(mockExecutor as any);
|
||||
|
||||
await expect(toolsAgentExecute.call(mockContext)).rejects.toThrow('Test error');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,223 @@
|
||||
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import { AgentExecutor } from 'langchain/agents';
|
||||
import type { Tool } from 'langchain/tools';
|
||||
import type { IExecuteFunctions, INode } from 'n8n-workflow';
|
||||
|
||||
import * as helpers from '../../../../../utils/helpers';
|
||||
import { toolsAgentExecute } from '../../agents/ToolsAgent/V2/execute';
|
||||
|
||||
const mockHelpers = mock<IExecuteFunctions['helpers']>();
|
||||
const mockContext = mock<IExecuteFunctions>({ helpers: mockHelpers });
|
||||
|
||||
beforeEach(() => jest.resetAllMocks());
|
||||
|
||||
describe('toolsAgentExecute', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockContext.logger = {
|
||||
debug: jest.fn(),
|
||||
info: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
error: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
it('should process items sequentially when batchSize is not set', async () => {
|
||||
const mockNode = mock<INode>();
|
||||
mockNode.typeVersion = 2;
|
||||
mockContext.getNode.mockReturnValue(mockNode);
|
||||
mockContext.getInputData.mockReturnValue([
|
||||
{ json: { text: 'test input 1' } },
|
||||
{ json: { text: 'test input 2' } },
|
||||
]);
|
||||
|
||||
const mockModel = mock<BaseChatModel>();
|
||||
mockModel.bindTools = jest.fn();
|
||||
mockModel.lc_namespace = ['chat_models'];
|
||||
mockContext.getInputConnectionData.mockResolvedValue(mockModel);
|
||||
|
||||
const mockTools = [mock<Tool>()];
|
||||
jest.spyOn(helpers, 'getConnectedTools').mockResolvedValue(mockTools);
|
||||
|
||||
// Mock getNodeParameter to return default values
|
||||
mockContext.getNodeParameter.mockImplementation((param, _i, defaultValue) => {
|
||||
if (param === 'text') return 'test input';
|
||||
if (param === 'options.batching.batchSize') return defaultValue;
|
||||
if (param === 'options.batching.delayBetweenBatches') return defaultValue;
|
||||
if (param === 'options')
|
||||
return {
|
||||
systemMessage: 'You are a helpful assistant',
|
||||
maxIterations: 10,
|
||||
returnIntermediateSteps: false,
|
||||
passthroughBinaryImages: true,
|
||||
};
|
||||
return defaultValue;
|
||||
});
|
||||
|
||||
const mockExecutor = {
|
||||
invoke: jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce({ output: JSON.stringify({ text: 'success 1' }) })
|
||||
.mockResolvedValueOnce({ output: JSON.stringify({ text: 'success 2' }) }),
|
||||
};
|
||||
|
||||
jest.spyOn(AgentExecutor, 'fromAgentAndTools').mockReturnValue(mockExecutor as any);
|
||||
|
||||
const result = await toolsAgentExecute.call(mockContext);
|
||||
|
||||
expect(mockExecutor.invoke).toHaveBeenCalledTimes(2);
|
||||
expect(result[0]).toHaveLength(2);
|
||||
expect(result[0][0].json).toEqual({ output: { text: 'success 1' } });
|
||||
expect(result[0][1].json).toEqual({ output: { text: 'success 2' } });
|
||||
});
|
||||
|
||||
it('should process items in parallel within batches when batchSize > 1', async () => {
|
||||
const mockNode = mock<INode>();
|
||||
mockNode.typeVersion = 2;
|
||||
mockContext.getNode.mockReturnValue(mockNode);
|
||||
mockContext.getInputData.mockReturnValue([
|
||||
{ json: { text: 'test input 1' } },
|
||||
{ json: { text: 'test input 2' } },
|
||||
{ json: { text: 'test input 3' } },
|
||||
{ json: { text: 'test input 4' } },
|
||||
]);
|
||||
|
||||
const mockModel = mock<BaseChatModel>();
|
||||
mockModel.bindTools = jest.fn();
|
||||
mockModel.lc_namespace = ['chat_models'];
|
||||
mockContext.getInputConnectionData.mockResolvedValue(mockModel);
|
||||
|
||||
const mockTools = [mock<Tool>()];
|
||||
jest.spyOn(helpers, 'getConnectedTools').mockResolvedValue(mockTools);
|
||||
|
||||
mockContext.getNodeParameter.mockImplementation((param, _i, defaultValue) => {
|
||||
if (param === 'options.batching.batchSize') return 2;
|
||||
if (param === 'options.batching.delayBetweenBatches') return 100;
|
||||
if (param === 'text') return 'test input';
|
||||
if (param === 'options')
|
||||
return {
|
||||
systemMessage: 'You are a helpful assistant',
|
||||
maxIterations: 10,
|
||||
returnIntermediateSteps: false,
|
||||
passthroughBinaryImages: true,
|
||||
};
|
||||
return defaultValue;
|
||||
});
|
||||
|
||||
const mockExecutor = {
|
||||
invoke: jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce({ output: JSON.stringify({ text: 'success 1' }) })
|
||||
.mockResolvedValueOnce({ output: JSON.stringify({ text: 'success 2' }) })
|
||||
.mockResolvedValueOnce({ output: JSON.stringify({ text: 'success 3' }) })
|
||||
.mockResolvedValueOnce({ output: JSON.stringify({ text: 'success 4' }) }),
|
||||
};
|
||||
|
||||
jest.spyOn(AgentExecutor, 'fromAgentAndTools').mockReturnValue(mockExecutor as any);
|
||||
|
||||
const result = await toolsAgentExecute.call(mockContext);
|
||||
|
||||
expect(mockExecutor.invoke).toHaveBeenCalledTimes(4); // Each item is processed individually
|
||||
expect(result[0]).toHaveLength(4);
|
||||
|
||||
expect(result[0][0].json).toEqual({ output: { text: 'success 1' } });
|
||||
expect(result[0][1].json).toEqual({ output: { text: 'success 2' } });
|
||||
expect(result[0][2].json).toEqual({ output: { text: 'success 3' } });
|
||||
expect(result[0][3].json).toEqual({ output: { text: 'success 4' } });
|
||||
});
|
||||
|
||||
it('should handle errors in batch processing when continueOnFail is true', async () => {
|
||||
const mockNode = mock<INode>();
|
||||
mockNode.typeVersion = 2;
|
||||
mockContext.getNode.mockReturnValue(mockNode);
|
||||
mockContext.getInputData.mockReturnValue([
|
||||
{ json: { text: 'test input 1' } },
|
||||
{ json: { text: 'test input 2' } },
|
||||
]);
|
||||
|
||||
const mockModel = mock<BaseChatModel>();
|
||||
mockModel.bindTools = jest.fn();
|
||||
mockModel.lc_namespace = ['chat_models'];
|
||||
mockContext.getInputConnectionData.mockResolvedValue(mockModel);
|
||||
|
||||
const mockTools = [mock<Tool>()];
|
||||
jest.spyOn(helpers, 'getConnectedTools').mockResolvedValue(mockTools);
|
||||
|
||||
mockContext.getNodeParameter.mockImplementation((param, _i, defaultValue) => {
|
||||
if (param === 'options.batching.batchSize') return 2;
|
||||
if (param === 'options.batching.delayBetweenBatches') return 0;
|
||||
if (param === 'text') return 'test input';
|
||||
if (param === 'options')
|
||||
return {
|
||||
systemMessage: 'You are a helpful assistant',
|
||||
maxIterations: 10,
|
||||
returnIntermediateSteps: false,
|
||||
passthroughBinaryImages: true,
|
||||
};
|
||||
return defaultValue;
|
||||
});
|
||||
|
||||
mockContext.continueOnFail.mockReturnValue(true);
|
||||
|
||||
const mockExecutor = {
|
||||
invoke: jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce({ output: '{ "text": "success" }' })
|
||||
.mockRejectedValueOnce(new Error('Test error')),
|
||||
};
|
||||
|
||||
jest.spyOn(AgentExecutor, 'fromAgentAndTools').mockReturnValue(mockExecutor as any);
|
||||
|
||||
const result = await toolsAgentExecute.call(mockContext);
|
||||
|
||||
expect(result[0]).toHaveLength(2);
|
||||
expect(result[0][0].json).toEqual({ output: { text: 'success' } });
|
||||
expect(result[0][1].json).toEqual({ error: 'Test error' });
|
||||
});
|
||||
|
||||
it('should throw error in batch processing when continueOnFail is false', async () => {
|
||||
const mockNode = mock<INode>();
|
||||
mockNode.typeVersion = 2;
|
||||
mockContext.getNode.mockReturnValue(mockNode);
|
||||
mockContext.getInputData.mockReturnValue([
|
||||
{ json: { text: 'test input 1' } },
|
||||
{ json: { text: 'test input 2' } },
|
||||
]);
|
||||
|
||||
const mockModel = mock<BaseChatModel>();
|
||||
mockModel.bindTools = jest.fn();
|
||||
mockModel.lc_namespace = ['chat_models'];
|
||||
mockContext.getInputConnectionData.mockResolvedValue(mockModel);
|
||||
|
||||
const mockTools = [mock<Tool>()];
|
||||
jest.spyOn(helpers, 'getConnectedTools').mockResolvedValue(mockTools);
|
||||
|
||||
mockContext.getNodeParameter.mockImplementation((param, _i, defaultValue) => {
|
||||
if (param === 'options.batching.batchSize') return 2;
|
||||
if (param === 'options.batching.delayBetweenBatches') return 0;
|
||||
if (param === 'text') return 'test input';
|
||||
if (param === 'options')
|
||||
return {
|
||||
systemMessage: 'You are a helpful assistant',
|
||||
maxIterations: 10,
|
||||
returnIntermediateSteps: false,
|
||||
passthroughBinaryImages: true,
|
||||
};
|
||||
return defaultValue;
|
||||
});
|
||||
|
||||
mockContext.continueOnFail.mockReturnValue(false);
|
||||
|
||||
const mockExecutor = {
|
||||
invoke: jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce({ output: JSON.stringify({ text: 'success' }) })
|
||||
.mockRejectedValueOnce(new Error('Test error')),
|
||||
};
|
||||
|
||||
jest.spyOn(AgentExecutor, 'fromAgentAndTools').mockReturnValue(mockExecutor as any);
|
||||
|
||||
await expect(toolsAgentExecute.call(mockContext)).rejects.toThrow('Test error');
|
||||
});
|
||||
});
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
prepareMessages,
|
||||
preparePrompt,
|
||||
getTools,
|
||||
} from '../agents/ToolsAgent/execute';
|
||||
} from '../../agents/ToolsAgent/common';
|
||||
|
||||
function getFakeOutputParser(returnSchema?: ZodType): N8nOutputParser {
|
||||
const fakeOutputParser = mock<N8nOutputParser>();
|
||||
Reference in New Issue
Block a user