feat(core): Implement wrapping of regular nodes as AI Tools (#10641)

Co-authored-by: JP van Oosten <jp@n8n.io>
This commit is contained in:
oleg
2024-09-04 12:06:17 +02:00
committed by GitHub
parent f114035a6b
commit da44fe4b89
13 changed files with 615 additions and 18 deletions

View File

@@ -13,11 +13,12 @@ import {
} from 'n8n-core';
import type {
KnownNodesAndCredentials,
INodeTypeBaseDescription,
INodeTypeDescription,
INodeTypeData,
ICredentialTypeData,
} from 'n8n-workflow';
import { ApplicationError, ErrorReporterProxy as ErrorReporter } from 'n8n-workflow';
import { NodeHelpers, ApplicationError, ErrorReporterProxy as ErrorReporter } from 'n8n-workflow';
import {
CUSTOM_API_CALL_KEY,
@@ -38,8 +39,11 @@ interface LoadedNodesAndCredentials {
export class LoadNodesAndCredentials {
private known: KnownNodesAndCredentials = { nodes: {}, credentials: {} };
// This contains the actually loaded objects, and their source paths
loaded: LoadedNodesAndCredentials = { nodes: {}, credentials: {} };
// For nodes, this only contains the descriptions, loaded from either the
// actual file, or the lazy loaded json
types: Types = { nodes: [], credentials: [] };
loaders: Record<string, DirectoryLoader> = {};
@@ -260,6 +264,34 @@ export class LoadNodesAndCredentials {
return loader;
}
/**
* This creates all AI Agent tools by duplicating the node descriptions for
* all nodes that are marked as `usableAsTool`. It basically modifies the
* description. The actual wrapping happens in the langchain code for getting
* the connected tools.
*/
createAiTools() {
const usableNodes: Array<INodeTypeBaseDescription | INodeTypeDescription> =
this.types.nodes.filter((nodetype) => nodetype.usableAsTool === true);
for (const usableNode of usableNodes) {
const description: INodeTypeBaseDescription | INodeTypeDescription =
structuredClone(usableNode);
const wrapped = NodeHelpers.convertNodeToAiTool({ description }).description;
this.types.nodes.push(wrapped);
this.known.nodes[wrapped.name] = structuredClone(this.known.nodes[usableNode.name]);
const credentialNames = Object.entries(this.known.credentials)
.filter(([_, credential]) => credential?.supportedNodes?.includes(usableNode.name))
.map(([credentialName]) => credentialName);
credentialNames.forEach((name) =>
this.known.credentials[name]?.supportedNodes?.push(wrapped.name),
);
}
}
async postProcessLoaders() {
this.known = { nodes: {}, credentials: {} };
this.loaded = { nodes: {}, credentials: {} };
@@ -307,6 +339,8 @@ export class LoadNodesAndCredentials {
}
}
this.createAiTools();
this.injectCustomApiCallOptions();
for (const postProcessor of this.postProcessors) {

View File

@@ -43,7 +43,15 @@ export class NodeTypes implements INodeTypes {
}
getByNameAndVersion(nodeType: string, version?: number): INodeType {
return NodeHelpers.getVersionedNodeType(this.getNode(nodeType).type, version);
const versionedNodeType = NodeHelpers.getVersionedNodeType(
this.getNode(nodeType).type,
version,
);
if (versionedNodeType.description.usableAsTool) {
return NodeHelpers.convertNodeToAiTool(versionedNodeType);
}
return versionedNodeType;
}
/* Some nodeTypes need to get special parameters applied like the polling nodes the polling times */
@@ -66,8 +74,9 @@ export class NodeTypes implements INodeTypes {
if (type in knownNodes) {
const { className, sourcePath } = knownNodes[type];
const loaded: INodeType = loadClassInIsolation(sourcePath, className);
NodeHelpers.applySpecialNodeParameters(loaded);
const loaded: INodeType | IVersionedNodeType = loadClassInIsolation(sourcePath, className);
if (NodeHelpers.isINodeType(loaded)) NodeHelpers.applySpecialNodeParameters(loaded);
loadedNodes[type] = { sourcePath, type: loaded };
return loadedNodes[type];
}