feat(core): Expose $agentInfo variable for easy AI workflow (no-changelog) (#14445)

Co-authored-by: Mutasem Aldmour <mutasem@n8n.io>
This commit is contained in:
Charlie Kolb
2025-04-10 13:10:26 +02:00
committed by GitHub
parent d24b684a95
commit 3db47504a2
5 changed files with 466 additions and 18 deletions

View File

@@ -6,29 +6,30 @@ import * as jmespath from 'jmespath';
import { DateTime, Duration, Interval, Settings } from 'luxon'; import { DateTime, Duration, Interval, Settings } from 'luxon';
import { augmentArray, augmentObject } from './AugmentObject'; import { augmentArray, augmentObject } from './AugmentObject';
import { SCRIPTING_NODE_TYPES } from './Constants'; import { AGENT_LANGCHAIN_NODE_TYPE, SCRIPTING_NODE_TYPES } from './Constants';
import { ApplicationError } from './errors/application.error'; import { ApplicationError } from './errors/application.error';
import { ExpressionError, type ExpressionErrorOptions } from './errors/expression.error'; import { ExpressionError, type ExpressionErrorOptions } from './errors/expression.error';
import { getGlobalState } from './GlobalState'; import { getGlobalState } from './GlobalState';
import { import { NodeConnectionTypes } from './Interfaces';
type IDataObject, import type {
type IExecuteData, IDataObject,
type INodeExecutionData, IExecuteData,
type INodeParameters, INodeExecutionData,
type IPairedItemData, INodeParameters,
type IRunExecutionData, IPairedItemData,
type ISourceData, IRunExecutionData,
type ITaskData, ISourceData,
type IWorkflowDataProxyAdditionalKeys, ITaskData,
type IWorkflowDataProxyData, IWorkflowDataProxyAdditionalKeys,
type INodeParameterResourceLocator, IWorkflowDataProxyData,
type NodeParameterValueType, INodeParameterResourceLocator,
type WorkflowExecuteMode, NodeParameterValueType,
type ProxyInput, WorkflowExecuteMode,
NodeConnectionTypes, ProxyInput,
INode,
} from './Interfaces'; } from './Interfaces';
import * as NodeHelpers from './NodeHelpers'; import * as NodeHelpers from './NodeHelpers';
import { deepCopy } from './utils'; import { deepCopy, isObjectEmpty } from './utils';
import type { Workflow } from './Workflow'; import type { Workflow } from './Workflow';
import type { EnvProviderState } from './WorkflowDataProxyEnvProvider'; import type { EnvProviderState } from './WorkflowDataProxyEnvProvider';
import { createEnvProvider, createEnvProviderState } from './WorkflowDataProxyEnvProvider'; import { createEnvProvider, createEnvProviderState } from './WorkflowDataProxyEnvProvider';
@@ -159,6 +160,89 @@ export class WorkflowDataProxy {
); );
} }
private buildAgentToolInfo(node: INode) {
const nodeType = this.workflow.nodeTypes.getByNameAndVersion(node.type, node.typeVersion);
const type = nodeType.description.displayName;
const params = NodeHelpers.getNodeParameters(
nodeType.description.properties,
node.parameters,
true,
false,
node,
nodeType.description,
);
const resourceKey = params?.resource;
const operationKey = params?.operation;
const resource =
nodeType.description.properties
.find((nodeProperties) => nodeProperties.name === 'resource')
?.options?.find((option) => 'value' in option && option.value === resourceKey)?.name ??
null;
const operation =
nodeType.description.properties
.find(
(nodeProperty) =>
nodeProperty.name === 'operation' &&
nodeProperty.displayOptions?.show?.resource?.some((y) => y === resourceKey),
)
?.options?.find((y) => 'value' in y && y.value === operationKey)?.name ?? null;
const hasCredentials = !isObjectEmpty(node.credentials ?? {});
const hasValidCalendar = nodeType.description.name.includes('googleCalendar')
? isResourceLocatorValue(node.parameters.calendar) && node.parameters.calendar.value !== ''
: undefined;
const aiDefinedFields = Object.entries(node.parameters)
.map(([key, value]) => [key, isResourceLocatorValue(value) ? value.value : value] as const)
.filter(([_, value]) => value?.toString().toLowerCase().includes('$fromai'))
.map(
([key]) =>
nodeType.description.properties.find((property) => property.name === key)?.displayName,
);
return {
name: node.name,
type,
resource,
operation,
hasCredentials,
hasValidCalendar,
aiDefinedFields,
};
}
private agentInfo() {
const agentNode = this.workflow.getNode(this.activeNodeName);
if (!agentNode || agentNode.type !== AGENT_LANGCHAIN_NODE_TYPE) return undefined;
const connectedTools = this.workflow
.getParentNodes(this.activeNodeName, NodeConnectionTypes.AiTool)
.map((nodeName) => this.workflow.getNode(nodeName))
.filter((node) => node) as INode[];
const memoryConnectedToAgent =
this.workflow.getParentNodes(this.activeNodeName, NodeConnectionTypes.AiMemory).length > 0;
const allTools = this.workflow.queryNodes((nodeType) => {
return nodeType.description.name.toLowerCase().includes('tool');
});
const unconnectedTools = allTools
.filter(
(node) =>
this.workflow.getChildNodes(node.name, NodeConnectionTypes.AiTool, 1).length === 0,
)
.filter((node) => !connectedTools.includes(node));
return {
memoryConnectedToAgent,
tools: [
...connectedTools.map((node) => ({ connected: true, ...this.buildAgentToolInfo(node) })),
...unconnectedTools.map((node) => ({ connected: false, ...this.buildAgentToolInfo(node) })),
],
};
}
/** /**
* Returns a proxy which allows to query parameter data of a given node * Returns a proxy which allows to query parameter data of a given node
* *
@@ -1394,6 +1478,7 @@ export class WorkflowDataProxy {
$thisRunIndex: this.runIndex, $thisRunIndex: this.runIndex,
$nodeVersion: that.workflow.getNode(that.activeNodeName)?.typeVersion, $nodeVersion: that.workflow.getNode(that.activeNodeName)?.typeVersion,
$nodeId: that.workflow.getNode(that.activeNodeName)?.id, $nodeId: that.workflow.getNode(that.activeNodeName)?.id,
$agentInfo: this.agentInfo(),
$webhookId: that.workflow.getNode(that.activeNodeName)?.webhookId, $webhookId: that.workflow.getNode(that.activeNodeName)?.webhookId,
}; };
const throwOnMissingExecutionData = opts?.throwOnMissingExecutionData ?? true; const throwOnMissingExecutionData = opts?.throwOnMissingExecutionData ?? true;

View File

@@ -828,6 +828,179 @@ const executeWorkflowNode: LoadedClass<INodeType> = {
sourcePath: '', sourcePath: '',
}; };
const aiAgentNode: LoadedClass<INodeType> = {
sourcePath: '',
type: {
description: {
displayName: 'AI Agent',
name: '@n8n/n8n-nodes-langchain.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],
description: 'Generates an action plan and executes it. Can use external tools.',
defaults: {
name: 'AI Agent',
color: '#404040',
},
inputs: [
NodeConnectionTypes.Main,
NodeConnectionTypes.AiLanguageModel,
NodeConnectionTypes.AiTool,
NodeConnectionTypes.AiMemory,
],
outputs: [NodeConnectionTypes.Main],
properties: [],
},
},
};
const wikipediaTool: LoadedClass<INodeType> = {
sourcePath: '',
type: {
description: {
displayName: 'Wikipedia',
name: '@n8n/n8n-nodes-langchain.toolWikipedia',
icon: 'file:wikipedia.svg',
group: ['transform'],
version: 1,
description: 'Search in Wikipedia',
defaults: {
name: 'Wikipedia',
},
inputs: [],
outputs: [NodeConnectionTypes.AiTool],
properties: [],
},
},
};
const calculatorTool: LoadedClass<INodeType> = {
sourcePath: '',
type: {
description: {
displayName: 'Calculator',
name: '@n8n/n8n-nodes-langchain.toolCalculator',
icon: 'fa:calculator',
iconColor: 'black',
group: ['transform'],
version: 1,
description: 'Make it easier for AI agents to perform arithmetic',
defaults: {
name: 'Calculator',
},
inputs: [],
outputs: [NodeConnectionTypes.AiTool],
properties: [],
},
},
};
const googleCalendarTool: LoadedClass<INodeType> = {
sourcePath: '',
type: {
description: {
displayName: 'Google Calendar',
name: 'n8n-nodes-base.googleCalendarTool',
icon: 'file:googleCalendar.svg',
group: ['input'],
version: [1, 1.1, 1.2, 1.3],
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Consume Google Calendar API',
defaults: {
name: 'Google Calendar',
},
inputs: [NodeConnectionTypes.Main],
outputs: [NodeConnectionTypes.Main],
usableAsTool: true,
properties: [
{ name: 'start', type: 'dateTime', displayName: 'Start', default: '' },
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['calendar'],
},
},
options: [
{
name: 'Availability',
value: 'availability',
description: 'If a time-slot is available in a calendar',
action: 'Get availability in a calendar',
},
],
default: 'availability',
},
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['event'],
},
},
options: [
{
name: 'Create',
value: 'create',
description: 'Add a event to calendar',
action: 'Create an event',
},
{
name: 'Delete',
value: 'delete',
description: 'Delete an event',
action: 'Delete an event',
},
{
name: 'Get',
value: 'get',
description: 'Retrieve an event',
action: 'Get an event',
},
{
name: 'Get Many',
value: 'getAll',
description: 'Retrieve many events from a calendar',
action: 'Get many events',
},
{
name: 'Update',
value: 'update',
description: 'Update an event',
action: 'Update an event',
},
],
default: 'create',
},
{
displayName: 'Resource',
name: 'resource',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Calendar',
value: 'calendar',
},
{
name: 'Event',
value: 'event',
},
],
default: 'event',
},
],
},
},
};
export class NodeTypes implements INodeTypes { export class NodeTypes implements INodeTypes {
nodeTypes: INodeTypeData = { nodeTypes: INodeTypeData = {
'n8n-nodes-base.stickyNote': stickyNode, 'n8n-nodes-base.stickyNote': stickyNode,
@@ -887,6 +1060,10 @@ export class NodeTypes implements INodeTypes {
}, },
}, },
'n8n-nodes-base.manualTrigger': manualTriggerNode, 'n8n-nodes-base.manualTrigger': manualTriggerNode,
'@n8n/n8n-nodes-langchain.agent': aiAgentNode,
'n8n-nodes-base.googleCalendarTool': googleCalendarTool,
'@n8n/n8n-nodes-langchain.toolCalculator': calculatorTool,
'@n8n/n8n-nodes-langchain.toolWikipedia': wikipediaTool,
}; };
getByName(nodeType: string): INodeType | IVersionedNodeType { getByName(nodeType: string): INodeType | IVersionedNodeType {
@@ -900,6 +1077,7 @@ export class NodeTypes implements INodeTypes {
return mock<INodeType>({ return mock<INodeType>({
description: { description: {
properties: [], properties: [],
name: nodeType,
}, },
}); });
} }

View File

@@ -787,4 +787,52 @@ describe('WorkflowDataProxy', () => {
}); });
}); });
}); });
describe('$agentInfo', () => {
const fixture = loadFixture('agentInfo');
const proxy = getProxyFromFixture(fixture.workflow, fixture.run, 'AI Agent');
test('$agentInfo should return undefined for non-agent nodes', () => {
const nonAgentProxy = getProxyFromFixture(fixture.workflow, fixture.run, 'Calculator');
expect(nonAgentProxy.$agentInfo).toBeUndefined();
});
test('$agentInfo should return memoryConnectedToAgent as true if memory is connected', () => {
expect(proxy.$agentInfo.memoryConnectedToAgent).toBe(true);
});
test('$agentInfo should return memoryConnectedToAgent as false if no memory is connected', () => {
const noMemoryProxy = getProxyFromFixture(fixture.workflow, fixture.run, 'Another Agent');
expect(noMemoryProxy.$agentInfo.memoryConnectedToAgent).toBe(false);
});
test('$agentInfo.tools should include connected tools with correct details', () => {
const tools = proxy.$agentInfo.tools;
// don't show tool connected to other agent
expect(tools.length).toEqual(2);
expect(tools[0]).toMatchObject({
connected: true,
name: 'Google Calendar',
type: 'Google Calendar',
resource: 'Event',
operation: 'Create',
hasCredentials: false,
});
expect(tools[1]).toMatchObject({
connected: false,
name: 'Calculator',
type: 'Calculator',
resource: null,
operation: null,
hasCredentials: false,
});
});
test('$agentInfo.tools should correctly identify AI-defined fields', () => {
const tools = proxy.$agentInfo.tools;
expect(tools[0].name).toBe('Google Calendar');
expect(tools[0].aiDefinedFields.length).toBe(1);
expect(tools[0].aiDefinedFields).toEqual(['Start']);
});
});
}); });

View File

@@ -0,0 +1,7 @@
{
"data": {
"resultData": {
"runData": {}
}
}
}

View File

@@ -0,0 +1,130 @@
{
"nodes": [
{
"parameters": {
"options": {}
},
"type": "@n8n/n8n-nodes-langchain.agent",
"typeVersion": 1.8,
"position": [280, -160],
"id": "48d38b5e-d75f-4245-9f6b-c9ab623b1a7a",
"name": "AI Agent"
},
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [0, 0],
"id": "914a83be-5bd5-46f9-8b39-1456d12f9429",
"name": "When clicking Test workflow"
},
{
"parameters": {},
"type": "@n8n/n8n-nodes-langchain.toolCalculator",
"typeVersion": 1,
"position": [780, 60],
"id": "d939bfce-6fbd-4f13-8ba2-91d605bdb81b",
"name": "Calculator"
},
{
"parameters": {},
"type": "@n8n/n8n-nodes-langchain.toolWikipedia",
"typeVersion": 1,
"position": [680, 500],
"id": "efdb00f3-cf60-4c3a-9b18-2523d2fc3177",
"name": "Wikipedia"
},
{
"parameters": {
"calendar": {
"__rl": true,
"mode": "list",
"value": ""
},
"start": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('Start', ``, 'string') }}",
"additionalFields": {}
},
"type": "n8n-nodes-base.googleCalendarTool",
"typeVersion": 1.3,
"position": [440, 60],
"id": "a76e6696-1e19-4aa4-b5d4-c43b332c8bc8",
"name": "Google Calendar"
},
{
"parameters": {
"options": {}
},
"type": "@n8n/n8n-nodes-langchain.agent",
"typeVersion": 1.8,
"position": [280, 280],
"id": "18a77a68-10dc-486d-b179-6d787371878c",
"name": "Another Agent"
},
{
"parameters": {},
"type": "@n8n/n8n-nodes-langchain.memoryBufferWindow",
"typeVersion": 1.3,
"position": [300, 80],
"id": "a0f31ee2-14b1-4ce7-97eb-a070346db0d3",
"name": "Simple Memory"
}
],
"connections": {
"When clicking Test workflow": {
"main": [
[
{
"node": "AI Agent",
"type": "main",
"index": 0
},
{
"node": "Another Agent",
"type": "main",
"index": 0
}
]
]
},
"Calculator": {
"ai_tool": [[]]
},
"Wikipedia": {
"ai_tool": [
[
{
"node": "Another Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Google Calendar": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Simple Memory": {
"ai_memory": [
[
{
"node": "AI Agent",
"type": "ai_memory",
"index": 0
}
]
]
}
},
"pinData": {},
"meta": {
"instanceId": "866ca65bee13401b1e2b632cdf2767d28ec3301d61bdb4ceabc832d1fe22a83e"
}
}