feat(core): Allow community nodes to be used as tools (#14042)

This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™
2025-03-19 12:26:05 +01:00
committed by GitHub
parent 91b27964d8
commit 9d698edceb
4 changed files with 3 additions and 28 deletions

View File

@@ -33,10 +33,6 @@ class CommunityPackagesConfig {
/** Whether to reinstall any missing community packages */ /** Whether to reinstall any missing community packages */
@Env('N8N_REINSTALL_MISSING_PACKAGES') @Env('N8N_REINSTALL_MISSING_PACKAGES')
reinstallMissing: boolean = false; reinstallMissing: boolean = false;
/** Whether to allow community packages as tools for AI agents */
@Env('N8N_COMMUNITY_PACKAGES_ALLOW_TOOL_USAGE')
allowToolUsage: boolean = false;
} }
@Config @Config

View File

@@ -118,7 +118,6 @@ describe('GlobalConfig', () => {
enabled: true, enabled: true,
registry: 'https://registry.npmjs.org', registry: 'https://registry.npmjs.org',
reinstallMissing: false, reinstallMissing: false,
allowToolUsage: false,
}, },
errorTriggerType: 'n8n-nodes-base.errorTrigger', errorTriggerType: 'n8n-nodes-base.errorTrigger',
include: [], include: [],

View File

@@ -1,4 +1,3 @@
import type { GlobalConfig } from '@n8n/config';
import { mock } from 'jest-mock-extended'; import { mock } from 'jest-mock-extended';
import { RoutingNode, UnrecognizedNodeTypeError } from 'n8n-core'; import { RoutingNode, UnrecognizedNodeTypeError } from 'n8n-core';
import type { import type {
@@ -12,14 +11,11 @@ import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials';
import { NodeTypes } from '@/node-types'; import { NodeTypes } from '@/node-types';
describe('NodeTypes', () => { describe('NodeTypes', () => {
const globalConfig = mock<GlobalConfig>({
nodes: { communityPackages: { allowToolUsage: false } },
});
const loadNodesAndCredentials = mock<LoadNodesAndCredentials>({ const loadNodesAndCredentials = mock<LoadNodesAndCredentials>({
convertNodeToAiTool: LoadNodesAndCredentials.prototype.convertNodeToAiTool, convertNodeToAiTool: LoadNodesAndCredentials.prototype.convertNodeToAiTool,
}); });
const nodeTypes: NodeTypes = new NodeTypes(globalConfig, loadNodesAndCredentials); const nodeTypes: NodeTypes = new NodeTypes(loadNodesAndCredentials);
const nonVersionedNode: LoadedClass<INodeType> = { const nonVersionedNode: LoadedClass<INodeType> = {
sourcePath: '', sourcePath: '',
@@ -117,7 +113,6 @@ describe('NodeTypes', () => {
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks(); jest.clearAllMocks();
globalConfig.nodes.communityPackages.allowToolUsage = false;
loadNodesAndCredentials.loaded.nodes = {}; loadNodesAndCredentials.loaded.nodes = {};
}); });
@@ -173,14 +168,7 @@ describe('NodeTypes', () => {
expect(result.description.outputs).toEqual(['ai_tool']); expect(result.description.outputs).toEqual(['ai_tool']);
}); });
it('should throw when a node-type is requested as tool, but is a community package', () => {
expect(() => nodeTypes.getByNameAndVersion('n8n-nodes-community.testNodeTool')).toThrow(
'Unrecognized node type: n8n-nodes-community.testNodeTool',
);
});
it('should return a tool node-type from a community node, when requested as tool', () => { it('should return a tool node-type from a community node, when requested as tool', () => {
globalConfig.nodes.communityPackages.allowToolUsage = true;
const result = nodeTypes.getByNameAndVersion('n8n-nodes-community.testNodeTool'); const result = nodeTypes.getByNameAndVersion('n8n-nodes-community.testNodeTool');
expect(result).not.toEqual(toolSupportingNode.type); expect(result).not.toEqual(toolSupportingNode.type);
expect(result.description.name).toEqual('n8n-nodes-community.testNodeTool'); expect(result.description.name).toEqual('n8n-nodes-community.testNodeTool');

View File

@@ -1,4 +1,3 @@
import { GlobalConfig } from '@n8n/config';
import { Service } from '@n8n/di'; import { Service } from '@n8n/di';
import type { NeededNodeType } from '@n8n/task-runner'; import type { NeededNodeType } from '@n8n/task-runner';
import type { Dirent } from 'fs'; import type { Dirent } from 'fs';
@@ -14,10 +13,7 @@ import { shouldAssignExecuteMethod } from './utils';
@Service() @Service()
export class NodeTypes implements INodeTypes { export class NodeTypes implements INodeTypes {
constructor( constructor(private readonly loadNodesAndCredentials: LoadNodesAndCredentials) {}
private readonly globalConfig: GlobalConfig,
private readonly loadNodesAndCredentials: LoadNodesAndCredentials,
) {}
/** /**
* Variant of `getByNameAndVersion` that includes the node's source path, used to locate a node's translations. * Variant of `getByNameAndVersion` that includes the node's source path, used to locate a node's translations.
@@ -39,14 +35,10 @@ export class NodeTypes implements INodeTypes {
getByNameAndVersion(nodeType: string, version?: number): INodeType { getByNameAndVersion(nodeType: string, version?: number): INodeType {
const origType = nodeType; const origType = nodeType;
const { communityPackages } = this.globalConfig.nodes;
const allowToolUsage = communityPackages.allowToolUsage
? true
: nodeType.startsWith('n8n-nodes-base');
const toolRequested = nodeType.endsWith('Tool'); const toolRequested = nodeType.endsWith('Tool');
// Make sure the nodeType to actually get from disk is the un-wrapped type // Make sure the nodeType to actually get from disk is the un-wrapped type
if (allowToolUsage && toolRequested) { if (toolRequested) {
nodeType = nodeType.replace(/Tool$/, ''); nodeType = nodeType.replace(/Tool$/, '');
} }