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 */
@Env('N8N_REINSTALL_MISSING_PACKAGES')
reinstallMissing: boolean = false;
/** Whether to allow community packages as tools for AI agents */
@Env('N8N_COMMUNITY_PACKAGES_ALLOW_TOOL_USAGE')
allowToolUsage: boolean = false;
}
@Config

View File

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

View File

@@ -1,4 +1,3 @@
import type { GlobalConfig } from '@n8n/config';
import { mock } from 'jest-mock-extended';
import { RoutingNode, UnrecognizedNodeTypeError } from 'n8n-core';
import type {
@@ -12,14 +11,11 @@ import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials';
import { NodeTypes } from '@/node-types';
describe('NodeTypes', () => {
const globalConfig = mock<GlobalConfig>({
nodes: { communityPackages: { allowToolUsage: false } },
});
const loadNodesAndCredentials = mock<LoadNodesAndCredentials>({
convertNodeToAiTool: LoadNodesAndCredentials.prototype.convertNodeToAiTool,
});
const nodeTypes: NodeTypes = new NodeTypes(globalConfig, loadNodesAndCredentials);
const nodeTypes: NodeTypes = new NodeTypes(loadNodesAndCredentials);
const nonVersionedNode: LoadedClass<INodeType> = {
sourcePath: '',
@@ -117,7 +113,6 @@ describe('NodeTypes', () => {
beforeEach(() => {
jest.clearAllMocks();
globalConfig.nodes.communityPackages.allowToolUsage = false;
loadNodesAndCredentials.loaded.nodes = {};
});
@@ -173,14 +168,7 @@ describe('NodeTypes', () => {
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', () => {
globalConfig.nodes.communityPackages.allowToolUsage = true;
const result = nodeTypes.getByNameAndVersion('n8n-nodes-community.testNodeTool');
expect(result).not.toEqual(toolSupportingNode.type);
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 type { NeededNodeType } from '@n8n/task-runner';
import type { Dirent } from 'fs';
@@ -14,10 +13,7 @@ import { shouldAssignExecuteMethod } from './utils';
@Service()
export class NodeTypes implements INodeTypes {
constructor(
private readonly globalConfig: GlobalConfig,
private readonly loadNodesAndCredentials: LoadNodesAndCredentials,
) {}
constructor(private readonly loadNodesAndCredentials: LoadNodesAndCredentials) {}
/**
* 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 {
const origType = nodeType;
const { communityPackages } = this.globalConfig.nodes;
const allowToolUsage = communityPackages.allowToolUsage
? true
: nodeType.startsWith('n8n-nodes-base');
const toolRequested = nodeType.endsWith('Tool');
// Make sure the nodeType to actually get from disk is the un-wrapped type
if (allowToolUsage && toolRequested) {
if (toolRequested) {
nodeType = nodeType.replace(/Tool$/, '');
}