mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-18 02:21:13 +00:00
fix(core): Fix support for multiple invocation of AI tools (#12141)
Co-authored-by: Oleg Ivaniv <me@olegivaniv.com>
This commit is contained in:
committed by
GitHub
parent
f4c2523419
commit
c572c0648c
@@ -945,10 +945,11 @@ export class WorkflowDataProxy {
|
||||
_type: string = 'string',
|
||||
defaultValue?: unknown,
|
||||
) => {
|
||||
const { itemIndex, runIndex } = that;
|
||||
if (!name || name === '') {
|
||||
throw new ExpressionError("Add a key, e.g. $fromAI('placeholder_name')", {
|
||||
runIndex: that.runIndex,
|
||||
itemIndex: that.itemIndex,
|
||||
runIndex,
|
||||
itemIndex,
|
||||
});
|
||||
}
|
||||
const nameValidationRegex = /^[a-zA-Z0-9_-]{0,64}$/;
|
||||
@@ -956,20 +957,20 @@ export class WorkflowDataProxy {
|
||||
throw new ExpressionError(
|
||||
'Invalid parameter key, must be between 1 and 64 characters long and only contain lowercase letters, uppercase letters, numbers, underscores, and hyphens',
|
||||
{
|
||||
runIndex: that.runIndex,
|
||||
itemIndex: that.itemIndex,
|
||||
runIndex,
|
||||
itemIndex,
|
||||
},
|
||||
);
|
||||
}
|
||||
const inputData =
|
||||
that.runExecutionData?.resultData.runData[that.activeNodeName]?.[runIndex].inputOverride;
|
||||
const placeholdersDataInputData =
|
||||
that.runExecutionData?.resultData.runData[that.activeNodeName]?.[0].inputOverride?.[
|
||||
NodeConnectionType.AiTool
|
||||
]?.[0]?.[0].json;
|
||||
inputData?.[NodeConnectionType.AiTool]?.[0]?.[itemIndex].json;
|
||||
|
||||
if (Boolean(!placeholdersDataInputData)) {
|
||||
throw new ExpressionError('No execution data available', {
|
||||
runIndex: that.runIndex,
|
||||
itemIndex: that.itemIndex,
|
||||
runIndex,
|
||||
itemIndex,
|
||||
type: 'no_execution_data',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { ExpressionError } from '@/errors/expression.error';
|
||||
import type {
|
||||
IExecuteData,
|
||||
INode,
|
||||
IPinData,
|
||||
IRun,
|
||||
IWorkflowBase,
|
||||
WorkflowExecuteMode,
|
||||
import {
|
||||
NodeConnectionType,
|
||||
type IExecuteData,
|
||||
type INode,
|
||||
type IPinData,
|
||||
type IRun,
|
||||
type IWorkflowBase,
|
||||
type WorkflowExecuteMode,
|
||||
} from '@/Interfaces';
|
||||
import { Workflow } from '@/Workflow';
|
||||
import { WorkflowDataProxy } from '@/WorkflowDataProxy';
|
||||
@@ -26,10 +27,15 @@ const getProxyFromFixture = (
|
||||
run: IRun | null,
|
||||
activeNode: string,
|
||||
mode?: WorkflowExecuteMode,
|
||||
opts?: { throwOnMissingExecutionData: boolean },
|
||||
opts?: {
|
||||
throwOnMissingExecutionData: boolean;
|
||||
connectionType?: NodeConnectionType;
|
||||
runIndex?: number;
|
||||
},
|
||||
) => {
|
||||
const taskData = run?.data.resultData.runData[activeNode]?.[0];
|
||||
const lastNodeConnectionInputData = taskData?.data?.main[0];
|
||||
const taskData = run?.data.resultData.runData[activeNode]?.[opts?.runIndex ?? 0];
|
||||
const lastNodeConnectionInputData =
|
||||
taskData?.data?.[opts?.connectionType ?? NodeConnectionType.Main]?.[0];
|
||||
|
||||
let executeData: IExecuteData | undefined;
|
||||
|
||||
@@ -38,7 +44,7 @@ const getProxyFromFixture = (
|
||||
data: taskData.data!,
|
||||
node: workflow.nodes.find((node) => node.name === activeNode) as INode,
|
||||
source: {
|
||||
main: taskData.source,
|
||||
[opts?.connectionType ?? NodeConnectionType.Main]: taskData.source,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -64,7 +70,7 @@ const getProxyFromFixture = (
|
||||
pinData,
|
||||
}),
|
||||
run?.data ?? null,
|
||||
0,
|
||||
opts?.runIndex ?? 0,
|
||||
0,
|
||||
activeNode,
|
||||
lastNodeConnectionInputData ?? [],
|
||||
@@ -443,4 +449,41 @@ describe('WorkflowDataProxy', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('$fromAI', () => {
|
||||
const fixture = loadFixture('from_ai_multiple_items');
|
||||
const getFromAIProxy = (runIndex = 0) =>
|
||||
getProxyFromFixture(fixture.workflow, fixture.run, 'Google Sheets1', 'manual', {
|
||||
connectionType: NodeConnectionType.AiTool,
|
||||
throwOnMissingExecutionData: false,
|
||||
runIndex,
|
||||
});
|
||||
|
||||
test('Retrieves values for first item', () => {
|
||||
expect(getFromAIProxy().$fromAI('full_name')).toEqual('Mr. Input 1');
|
||||
expect(getFromAIProxy().$fromAI('email')).toEqual('input1@n8n.io');
|
||||
});
|
||||
|
||||
test('Retrieves values for second item', () => {
|
||||
expect(getFromAIProxy(1).$fromAI('full_name')).toEqual('Mr. Input 2');
|
||||
expect(getFromAIProxy(1).$fromAI('email')).toEqual('input2@n8n.io');
|
||||
});
|
||||
|
||||
test('Case variants: $fromAi and $fromai', () => {
|
||||
expect(getFromAIProxy().$fromAi('full_name')).toEqual('Mr. Input 1');
|
||||
expect(getFromAIProxy().$fromai('email')).toEqual('input1@n8n.io');
|
||||
});
|
||||
|
||||
test('Returns default value when key not found', () => {
|
||||
expect(
|
||||
getFromAIProxy().$fromAI('non_existent_key', 'description', 'string', 'default_value'),
|
||||
).toEqual('default_value');
|
||||
});
|
||||
|
||||
test('Throws an error when a key is invalid (e.g. empty string)', () => {
|
||||
expect(() => getFromAIProxy().$fromAI('')).toThrow(ExpressionError);
|
||||
expect(() => getFromAIProxy().$fromAI('invalid key')).toThrow(ExpressionError);
|
||||
expect(() => getFromAIProxy().$fromAI('invalid!')).toThrow(ExpressionError);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
221
packages/workflow/test/fixtures/WorkflowDataProxy/from_ai_multiple_items_run.json
vendored
Normal file
221
packages/workflow/test/fixtures/WorkflowDataProxy/from_ai_multiple_items_run.json
vendored
Normal file
@@ -0,0 +1,221 @@
|
||||
{
|
||||
"data": {
|
||||
"startData": {},
|
||||
"resultData": {
|
||||
"runData": {
|
||||
"When clicking ‘Test workflow’": [
|
||||
{
|
||||
"hints": [],
|
||||
"startTime": 1733478795595,
|
||||
"executionTime": 0,
|
||||
"source": [],
|
||||
"executionStatus": "success",
|
||||
"data": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"json": {},
|
||||
"pairedItem": {
|
||||
"item": 0
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"Code": [
|
||||
{
|
||||
"hints": [
|
||||
{
|
||||
"message": "To make sure expressions after this node work, return the input items that produced each output item. <a target=\"_blank\" href=\"https://docs.n8n.io/data/data-mapping/data-item-linking/item-linking-code-node/\">More info</a>",
|
||||
"location": "outputPane"
|
||||
}
|
||||
],
|
||||
"startTime": 1733478795595,
|
||||
"executionTime": 2,
|
||||
"source": [
|
||||
{
|
||||
"previousNode": "When clicking ‘Test workflow’"
|
||||
}
|
||||
],
|
||||
"executionStatus": "success",
|
||||
"data": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"json": {
|
||||
"full_name": "Mr. Input 1",
|
||||
"email": "input1@n8n.io"
|
||||
},
|
||||
"pairedItem": {
|
||||
"item": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"full_name": "Mr. Input 2",
|
||||
"email": "input2@n8n.io"
|
||||
},
|
||||
"pairedItem": {
|
||||
"item": 0
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"Google Sheets1": [
|
||||
{
|
||||
"startTime": 1733478796468,
|
||||
"executionTime": 1417,
|
||||
"executionStatus": "success",
|
||||
"source": [null],
|
||||
"data": {
|
||||
"ai_tool": [
|
||||
[
|
||||
{
|
||||
"json": {
|
||||
"response": [
|
||||
{
|
||||
"full name": "Mr. Input 1",
|
||||
"email": "input1@n8n.io"
|
||||
},
|
||||
{},
|
||||
{}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"inputOverride": {
|
||||
"ai_tool": [
|
||||
[
|
||||
{
|
||||
"json": {
|
||||
"full_name": "Mr. Input 1",
|
||||
"email": "input1@n8n.io"
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"metadata": {
|
||||
"subRun": [
|
||||
{
|
||||
"node": "Google Sheets1",
|
||||
"runIndex": 0
|
||||
},
|
||||
{
|
||||
"node": "Google Sheets1",
|
||||
"runIndex": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"startTime": 1733478799915,
|
||||
"executionTime": 1271,
|
||||
"executionStatus": "success",
|
||||
"source": [null],
|
||||
"data": {
|
||||
"ai_tool": [
|
||||
[
|
||||
{
|
||||
"json": {
|
||||
"response": [
|
||||
{
|
||||
"full name": "Mr. Input 1",
|
||||
"email": "input1@n8n.io"
|
||||
},
|
||||
{},
|
||||
{}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"inputOverride": {
|
||||
"ai_tool": [
|
||||
[
|
||||
{
|
||||
"json": {
|
||||
"full_name": "Mr. Input 2",
|
||||
"email": "input2@n8n.io"
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"Agent single list with multiple tool calls": [
|
||||
{
|
||||
"hints": [],
|
||||
"startTime": 1733478795597,
|
||||
"executionTime": 9157,
|
||||
"source": [
|
||||
{
|
||||
"previousNode": "Code"
|
||||
}
|
||||
],
|
||||
"executionStatus": "success",
|
||||
"data": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"json": {
|
||||
"output": "The user \"Mr. Input 1\" with the email \"input1@n8n.io\" has been successfully added to your Users sheet."
|
||||
},
|
||||
"pairedItem": {
|
||||
"item": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"output": "The user \"Mr. Input 2\" with the email \"input2@n8n.io\" has been successfully added to your Users sheet."
|
||||
},
|
||||
"pairedItem": {
|
||||
"item": 1
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"pinData": {},
|
||||
"lastNodeExecuted": "Agent single list with multiple tool calls"
|
||||
},
|
||||
"executionData": {
|
||||
"contextData": {},
|
||||
"nodeExecutionStack": [],
|
||||
"metadata": {
|
||||
"Google Sheets1": [
|
||||
{
|
||||
"subRun": [
|
||||
{
|
||||
"node": "Google Sheets1",
|
||||
"runIndex": 0
|
||||
},
|
||||
{
|
||||
"node": "Google Sheets1",
|
||||
"runIndex": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"waitingExecution": {},
|
||||
"waitingExecutionSource": {}
|
||||
}
|
||||
},
|
||||
"mode": "manual",
|
||||
"startedAt": "2024-02-08T15:45:18.848Z",
|
||||
"stoppedAt": "2024-02-08T15:45:18.862Z",
|
||||
"status": "running"
|
||||
}
|
||||
112
packages/workflow/test/fixtures/WorkflowDataProxy/from_ai_multiple_items_workflow.json
vendored
Normal file
112
packages/workflow/test/fixtures/WorkflowDataProxy/from_ai_multiple_items_workflow.json
vendored
Normal file
@@ -0,0 +1,112 @@
|
||||
{
|
||||
"id": "8d7lUG8IdEyvIUim",
|
||||
"name": "Multiple items tool",
|
||||
"active": false,
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
"mode": "runOnceForAllItems",
|
||||
"language": "javaScript",
|
||||
"jsCode": "return [\n { \"full_name\": \"Mr. Input 1\", \"email\": \"input1@n8n.io\" }, \n { \"full_name\": \"Mr. Input 2\", \"email\": \"input2@n8n.io\" }\n]",
|
||||
"notice": ""
|
||||
},
|
||||
"id": "cb19a188-12ae-4d46-86df-4a2044ec3346",
|
||||
"name": "Code",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [-160, 480]
|
||||
},
|
||||
{
|
||||
"parameters": { "notice": "", "model": "gpt-4o-mini", "options": {} },
|
||||
"id": "c448b6b4-9e11-4044-96e5-f4138534ae52",
|
||||
"name": "OpenAI Chat Model1",
|
||||
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
|
||||
"typeVersion": 1,
|
||||
"position": [40, 700]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"descriptionType": "manual",
|
||||
"toolDescription": "Add row to Users sheet",
|
||||
"authentication": "oAuth2",
|
||||
"resource": "sheet",
|
||||
"operation": "append",
|
||||
"columns": {
|
||||
"mappingMode": "defineBelow",
|
||||
"value": {
|
||||
"full name": "={{ $fromAI('full_name') }}",
|
||||
"email": "={{ $fromAI('email') }}"
|
||||
},
|
||||
"matchingColumns": [],
|
||||
"schema": [
|
||||
{
|
||||
"id": "full name",
|
||||
"displayName": "full name",
|
||||
"required": false,
|
||||
"defaultMatch": false,
|
||||
"display": true,
|
||||
"type": "string",
|
||||
"canBeUsedToMatch": true
|
||||
},
|
||||
{
|
||||
"id": "email",
|
||||
"displayName": "email",
|
||||
"required": false,
|
||||
"defaultMatch": false,
|
||||
"display": true,
|
||||
"type": "string",
|
||||
"canBeUsedToMatch": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": { "useAppend": true }
|
||||
},
|
||||
"id": "d8b40267-9397-45b6-8a64-ee7e8f9eb8a8",
|
||||
"name": "Google Sheets1",
|
||||
"type": "n8n-nodes-base.googleSheetsTool",
|
||||
"typeVersion": 4.5,
|
||||
"position": [240, 700]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"notice_tip": "",
|
||||
"agent": "toolsAgent",
|
||||
"promptType": "define",
|
||||
"text": "=Add this user to my Users sheet:\n{{ $json.toJsonString() }}",
|
||||
"hasOutputParser": false,
|
||||
"options": {},
|
||||
"credentials": ""
|
||||
},
|
||||
"id": "0d6c1bd7-cc91-4571-8fdb-c875a1af44c7",
|
||||
"name": "Agent single list with multiple tool calls",
|
||||
"type": "@n8n/n8n-nodes-langchain.agent",
|
||||
"typeVersion": 1.7,
|
||||
"position": [40, 480]
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"When clicking ‘Test workflow’": { "main": [[{ "node": "Code", "type": "main", "index": 0 }]] },
|
||||
"Code": {
|
||||
"main": [
|
||||
[{ "node": "Agent single list with multiple tool calls", "type": "main", "index": 0 }]
|
||||
]
|
||||
},
|
||||
"OpenAI Chat Model1": {
|
||||
"ai_languageModel": [
|
||||
[
|
||||
{
|
||||
"node": "Agent single list with multiple tool calls",
|
||||
"type": "ai_languageModel",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Google Sheets1": {
|
||||
"ai_tool": [
|
||||
[{ "node": "Agent single list with multiple tool calls", "type": "ai_tool", "index": 0 }]
|
||||
]
|
||||
}
|
||||
},
|
||||
"pinData": {}
|
||||
}
|
||||
Reference in New Issue
Block a user