mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
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:
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
7
packages/workflow/test/fixtures/WorkflowDataProxy/agentInfo_run.json
vendored
Normal file
7
packages/workflow/test/fixtures/WorkflowDataProxy/agentInfo_run.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"data": {
|
||||
"resultData": {
|
||||
"runData": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
130
packages/workflow/test/fixtures/WorkflowDataProxy/agentInfo_workflow.json
vendored
Normal file
130
packages/workflow/test/fixtures/WorkflowDataProxy/agentInfo_workflow.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user