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 { 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 { ExpressionError, type ExpressionErrorOptions } from './errors/expression.error';
import { getGlobalState } from './GlobalState';
import {
type IDataObject,
type IExecuteData,
type INodeExecutionData,
type INodeParameters,
type IPairedItemData,
type IRunExecutionData,
type ISourceData,
type ITaskData,
type IWorkflowDataProxyAdditionalKeys,
type IWorkflowDataProxyData,
type INodeParameterResourceLocator,
type NodeParameterValueType,
type WorkflowExecuteMode,
type ProxyInput,
NodeConnectionTypes,
import { NodeConnectionTypes } from './Interfaces';
import type {
IDataObject,
IExecuteData,
INodeExecutionData,
INodeParameters,
IPairedItemData,
IRunExecutionData,
ISourceData,
ITaskData,
IWorkflowDataProxyAdditionalKeys,
IWorkflowDataProxyData,
INodeParameterResourceLocator,
NodeParameterValueType,
WorkflowExecuteMode,
ProxyInput,
INode,
} from './Interfaces';
import * as NodeHelpers from './NodeHelpers';
import { deepCopy } from './utils';
import { deepCopy, isObjectEmpty } from './utils';
import type { Workflow } from './Workflow';
import type { EnvProviderState } 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
*
@@ -1394,6 +1478,7 @@ export class WorkflowDataProxy {
$thisRunIndex: this.runIndex,
$nodeVersion: that.workflow.getNode(that.activeNodeName)?.typeVersion,
$nodeId: that.workflow.getNode(that.activeNodeName)?.id,
$agentInfo: this.agentInfo(),
$webhookId: that.workflow.getNode(that.activeNodeName)?.webhookId,
};
const throwOnMissingExecutionData = opts?.throwOnMissingExecutionData ?? true;

View File

@@ -828,6 +828,179 @@ const executeWorkflowNode: LoadedClass<INodeType> = {
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 {
nodeTypes: INodeTypeData = {
'n8n-nodes-base.stickyNote': stickyNode,
@@ -887,6 +1060,10 @@ export class NodeTypes implements INodeTypes {
},
},
'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 {
@@ -900,6 +1077,7 @@ export class NodeTypes implements INodeTypes {
return mock<INodeType>({
description: {
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"
}
}