fix(core): Fix support for multiple invocation of AI tools (#12141)

Co-authored-by: Oleg Ivaniv <me@olegivaniv.com>
This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™
2024-12-11 13:47:13 +01:00
committed by GitHub
parent f4c2523419
commit c572c0648c
10 changed files with 605 additions and 226 deletions

View File

@@ -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',
});
}

View File

@@ -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);
});
});
});

View 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"
}

View 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": {}
}