Files
n8n-enterprise-unlocked/packages/@n8n/ai-workflow-builder.ee/src/tools/node-details.tool.ts
T

205 lines
5.7 KiB
TypeScript

import { tool } from '@langchain/core/tools';
import type { INodeTypeDescription } from 'n8n-workflow';
import { z } from 'zod';
import { ValidationError, ToolExecutionError } from '../errors';
import { createProgressReporter, reportProgress } from './helpers/progress';
import { createSuccessResponse, createErrorResponse } from './helpers/response';
import { findNodeType, createNodeTypeNotFoundError } from './helpers/validation';
import type { NodeDetails } from '../types/nodes';
import type { NodeDetailsOutput } from '../types/tools';
/**
* Schema for node details tool input
*/
const nodeDetailsSchema = z.object({
nodeName: z.string().describe('The exact node type name (e.g., n8n-nodes-base.httpRequest)'),
withParameters: z
.boolean()
.optional()
.default(false)
.describe('Whether to include node parameters in the output'),
withConnections: z
.boolean()
.optional()
.default(true)
.describe('Whether to include node supported connections in the output'),
});
/**
* Format node inputs
*/
function formatInputs(inputs: INodeTypeDescription['inputs']): string {
if (!inputs || inputs.length === 0) {
return '<inputs>none</inputs>';
}
if (typeof inputs === 'string') {
return `<input>${inputs}</input>`;
}
const formattedInputs = inputs.map((input) => {
if (typeof input === 'string') {
return `<input>${input}</input>`;
}
return `<input>${JSON.stringify(input)}</input>`;
});
return formattedInputs.join('\n');
}
/**
* Format node outputs
*/
function formatOutputs(outputs: INodeTypeDescription['outputs']): string {
if (!outputs || outputs.length === 0) {
return '<outputs>none</outputs>';
}
if (typeof outputs === 'string') {
return `<output>${outputs}</output>`;
}
const formattedOutputs = outputs.map((output) => {
if (typeof output === 'string') {
return `<output>${output}</output>`;
}
return `<output>${JSON.stringify(output)}</output>`;
});
return formattedOutputs.join('\n');
}
/**
* Format node details into a structured message
*/
function formatNodeDetails(
details: NodeDetails,
withParameters: boolean = false,
withConnections: boolean = true,
): string {
const parts: string[] = [];
// Basic details
parts.push('<node_details>');
parts.push(`<name>${details.name}</name>`);
parts.push(`<display_name>${details.displayName}</display_name>`);
parts.push(`<description>${details.description}</description>`);
if (details.subtitle) {
parts.push(`<subtitle>${details.subtitle}</subtitle>`);
}
// Parameters
if (withParameters && details.properties.length > 0) {
const stringifiedProperties = JSON.stringify(details.properties, null, 2);
parts.push(`<properties>
${stringifiedProperties.length > 1000 ? stringifiedProperties.slice(0, 1000) + '... Rest of properties omitted' : stringifiedProperties}
</properties>`);
}
// Connections
if (withConnections) {
parts.push('<connections>');
parts.push(formatInputs(details.inputs));
parts.push(formatOutputs(details.outputs));
parts.push('</connections>');
}
parts.push('</node_details>');
return parts.join('\n');
}
/**
* Helper to extract node details from a node type description
*/
function extractNodeDetails(nodeType: INodeTypeDescription): NodeDetails {
return {
name: nodeType.name,
displayName: nodeType.displayName,
description: nodeType.description,
properties: nodeType.properties,
subtitle: nodeType.subtitle,
inputs: nodeType.inputs,
outputs: nodeType.outputs,
};
}
/**
* Factory function to create the node details tool
*/
export function createNodeDetailsTool(nodeTypes: INodeTypeDescription[]) {
const DISPLAY_TITLE = 'Getting node details';
const dynamicTool = tool(
(input: unknown, config) => {
const reporter = createProgressReporter(config, 'get_node_details', DISPLAY_TITLE);
try {
// Validate input using Zod schema
const validatedInput = nodeDetailsSchema.parse(input);
const { nodeName, withParameters, withConnections } = validatedInput;
// Report tool start
reporter.start(validatedInput);
// Report progress
reportProgress(reporter, `Looking up details for ${nodeName}...`);
// Find the node type
const nodeType = findNodeType(nodeName, nodeTypes);
if (!nodeType) {
const error = createNodeTypeNotFoundError(nodeName);
reporter.error(error);
return createErrorResponse(config, error);
}
// Extract node details
const details = extractNodeDetails(nodeType);
// Format the output message
const message = formatNodeDetails(details, withParameters, withConnections);
// Report completion
const output: NodeDetailsOutput = {
details,
found: true,
message,
};
reporter.complete(output);
// Return success response
return createSuccessResponse(config, message);
} catch (error) {
// Handle validation or unexpected errors
if (error instanceof z.ZodError) {
const validationError = new ValidationError('Invalid input parameters', {
extra: { errors: error.errors },
});
reporter.error(validationError);
return createErrorResponse(config, validationError);
}
const toolError = new ToolExecutionError(
error instanceof Error ? error.message : 'Unknown error occurred',
{
toolName: 'get_node_details',
cause: error instanceof Error ? error : undefined,
},
);
reporter.error(toolError);
return createErrorResponse(config, toolError);
}
},
{
name: 'get_node_details',
description:
'Get detailed information about a specific n8n node type including properties and available connections. Use this before adding nodes to understand their input/output structure.',
schema: nodeDetailsSchema,
},
);
return {
tool: dynamicTool,
displayTitle: DISPLAY_TITLE,
};
}