mirror of
https://github.com/n8n-io/n8n.git
synced 2026-05-31 08:46:58 +02:00
200 lines
6.1 KiB
TypeScript
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];
|
|
}
|
|
}
|