first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
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];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user