n8n/packages/@n8n/nodes-langchain/nodes/ToolExecutor/ToolExecutor.node.ts
Mutasem Aldmour 9729c2a5da
feat(ai-builder): Add code-base workflow builder (#24535)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-10 12:24:27 +00:00

200 lines
6.1 KiB
TypeScript

import type { Toolkit } from '@langchain/classic/agents';
import { StructuredTool, Tool } from '@langchain/core/tools';
import { buildResponseMetadata, processHitlResponses } from '@utils/agent-execution';
import {
extractHitlMetadata,
hasGatedToolNodeName,
} from '@utils/agent-execution/createEngineRequests';
import type { RequestResponseMetadata } from '@utils/agent-execution/types';
import get from 'lodash/get';
import type {
EngineRequest,
EngineResponse,
IDataObject,
IExecuteFunctions,
INodeExecutionData,
INodeType,
INodeTypeDescription,
NodeOutput,
} from 'n8n-workflow';
import { NodeConnectionTypes, NodeOperationError } from 'n8n-workflow';
import { executeTool } from './utils/executeTool';
import { convertValueBySchema } from './utils/convertToSchema';
import { ZodObject } from 'zod';
export class ToolExecutor implements INodeType {
description: INodeTypeDescription = {
displayName: 'Tool Executor',
name: 'toolExecutor',
version: 1,
defaults: {
name: 'Tool Executor',
},
hidden: true,
inputs: [NodeConnectionTypes.Main, NodeConnectionTypes.AiTool],
outputs: [NodeConnectionTypes.Main],
builderHint: {
inputs: {
ai_tool: { required: true },
},
},
properties: [
{
displayName: 'Query',
name: 'query',
type: 'json',
default: '{}',
description:
'Key-value pairs, where key is the name of the tool name and value is the parameters to pass to the tool',
},
{
displayName: 'Tool Name',
name: 'toolName',
type: 'string',
default: '',
description: 'Name of the tool to execute if the connected tool is a toolkit',
},
{
displayName: 'Node',
name: 'node',
type: 'string',
default: '',
description: 'Name of the node that is being executed',
},
],
group: ['transform'],
description: 'Node to execute tools without an AI Agent',
};
async execute(
this: IExecuteFunctions,
response?: EngineResponse<RequestResponseMetadata>,
): Promise<NodeOutput> {
// Process HITL (Human-in-the-Loop) tool responses before running the agent
// If there are approved HITL tools, we need to execute the gated tools first
const hitlResult = processHitlResponses(response, 0);
if (hitlResult.hasApprovedHitlTools && hitlResult.pendingGatedToolRequest) {
// Return the gated tool request immediately
// The Agent will resume after the gated tool executes
return hitlResult.pendingGatedToolRequest;
}
const query = this.getNodeParameter('query', 0, {}) as string | object;
const toolName = this.getNodeParameter('toolName', 0, '') as string;
const node = this.getNodeParameter('node', 0, '') as string;
let parsedQuery: Record<string, unknown>;
try {
parsedQuery = typeof query === 'string' ? JSON.parse(query) : query;
} catch (error) {
throw new NodeOperationError(
this.getNode(),
`Failed to parse query: ${(error as Error).message}`,
);
}
const getQueryData = (name: string) => {
// node names in query may have underscores in place of spaces, use it for accessing the query data.
return (get(parsedQuery, name, null) ?? get(parsedQuery, name.replaceAll(' ', '_'), null)) as
| Record<string, unknown>
| string
| null;
};
const resultData: INodeExecutionData[] = [];
const toolInputs = await this.getInputConnectionData(NodeConnectionTypes.AiTool, 0);
if (!toolInputs || !Array.isArray(toolInputs)) {
throw new NodeOperationError(this.getNode(), 'No tool inputs found');
}
try {
for (const tool of toolInputs) {
// Handle toolkits
if (tool && typeof (tool as Toolkit).getTools === 'function') {
const toolsInToolkit = (tool as Toolkit).getTools();
for (const toolkitTool of toolsInToolkit) {
if (!(toolkitTool instanceof Tool || toolkitTool instanceof StructuredTool)) {
continue;
}
if (toolName === toolkitTool.name) {
if (hasGatedToolNodeName(toolkitTool.metadata) && node) {
const toolInput: { toolParameters: unknown } = {
toolParameters: getQueryData(toolName) ?? {},
};
const hitlInput = getQueryData(node);
if (typeof hitlInput === 'string') {
throw new NodeOperationError(
this.getNode(),
`Invalid hitl input for tool ${toolkitTool.name}`,
);
}
// handle code tool which uses a string input, but it should be converted to an object
const requiresObjectInput =
toolkitTool.metadata.originalSchema &&
toolkitTool.metadata.originalSchema instanceof ZodObject;
if (typeof toolInput.toolParameters === 'string' && requiresObjectInput) {
toolInput.toolParameters = convertValueBySchema(
toolInput.toolParameters,
toolkitTool.metadata.originalSchema,
);
}
const hitlMetadata = extractHitlMetadata(
toolkitTool.metadata,
toolkitTool.name,
toolInput as IDataObject,
);
// prepare request for execution engine to execute the HITL node
const engineRequest: EngineRequest<RequestResponseMetadata>['actions'] = [
{
actionType: 'ExecutionNodeAction' as const,
nodeName: node,
input: {
tool: toolName,
toolParameters: toolInput.toolParameters as IDataObject,
...hitlInput,
},
type: 'ai_tool',
id: crypto.randomUUID(),
metadata: {
itemIndex: 0,
hitl: hitlMetadata,
},
},
];
return {
actions: engineRequest,
metadata: buildResponseMetadata(response, 0),
};
}
const result = await executeTool(toolkitTool, getQueryData(toolName) ?? {});
resultData.push(result);
}
}
} else {
// Handle single tool
if (!toolName || toolName === tool.name) {
const toolInput = getQueryData(toolName || tool.name);
const result = await executeTool(tool, toolInput ?? {});
resultData.push(result);
}
}
}
} catch (error) {
throw new NodeOperationError(
this.getNode(),
`Error executing tool: ${(error as Error).message}`,
);
}
return [resultData];
}
}