mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 01:56:46 +00:00
feat(Custom n8n Workflow Tool Node): Add support for tool input schema (#9470)
Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>
This commit is contained in:
@@ -13,6 +13,7 @@ import {
|
||||
getConnectedTools,
|
||||
} from '../../../../../utils/helpers';
|
||||
import { getTracingConfig } from '../../../../../utils/tracing';
|
||||
import { throwIfToolSchema } from '../../../../../utils/schemaParsing';
|
||||
|
||||
export async function conversationalAgentExecute(
|
||||
this: IExecuteFunctions,
|
||||
@@ -111,6 +112,8 @@ export async function conversationalAgentExecute(
|
||||
|
||||
returnData.push({ json: response });
|
||||
} catch (error) {
|
||||
throwIfToolSchema(this, error);
|
||||
|
||||
if (this.continueOnFail()) {
|
||||
returnData.push({ json: { error: error.message }, pairedItem: { item: itemIndex } });
|
||||
continue;
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
getPromptInputByType,
|
||||
} from '../../../../../utils/helpers';
|
||||
import { getTracingConfig } from '../../../../../utils/tracing';
|
||||
import { throwIfToolSchema } from '../../../../../utils/schemaParsing';
|
||||
|
||||
export async function planAndExecuteAgentExecute(
|
||||
this: IExecuteFunctions,
|
||||
@@ -91,6 +92,7 @@ export async function planAndExecuteAgentExecute(
|
||||
|
||||
returnData.push({ json: response });
|
||||
} catch (error) {
|
||||
throwIfToolSchema(this, error);
|
||||
if (this.continueOnFail()) {
|
||||
returnData.push({ json: { error: error.message }, pairedItem: { item: itemIndex } });
|
||||
continue;
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
isChatInstance,
|
||||
} from '../../../../../utils/helpers';
|
||||
import { getTracingConfig } from '../../../../../utils/tracing';
|
||||
import { throwIfToolSchema } from '../../../../../utils/schemaParsing';
|
||||
|
||||
export async function reActAgentAgentExecute(
|
||||
this: IExecuteFunctions,
|
||||
@@ -112,6 +113,7 @@ export async function reActAgentAgentExecute(
|
||||
|
||||
returnData.push({ json: response });
|
||||
} catch (error) {
|
||||
throwIfToolSchema(this, error);
|
||||
if (this.continueOnFail()) {
|
||||
returnData.push({ json: { error: error.message }, pairedItem: { item: itemIndex } });
|
||||
continue;
|
||||
|
||||
@@ -92,6 +92,8 @@ function getSandbox(
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||
context.executeWorkflow = this.executeWorkflow;
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||
context.getWorkflowDataProxy = this.getWorkflowDataProxy;
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||
context.logger = this.logger;
|
||||
|
||||
if (options?.addItems) {
|
||||
|
||||
@@ -13,11 +13,15 @@ import type { JSONSchema7 } from 'json-schema';
|
||||
import { StructuredOutputParser } from 'langchain/output_parsers';
|
||||
import { OutputParserException } from '@langchain/core/output_parsers';
|
||||
import get from 'lodash/get';
|
||||
import { getSandboxContext } from 'n8n-nodes-base/dist/nodes/Code/Sandbox';
|
||||
import { JavaScriptSandbox } from 'n8n-nodes-base/dist/nodes/Code/JavaScriptSandbox';
|
||||
import { makeResolverFromLegacyOptions } from '@n8n/vm2';
|
||||
import type { JavaScriptSandbox } from 'n8n-nodes-base/dist/nodes/Code/JavaScriptSandbox';
|
||||
import { getConnectionHintNoticeField } from '../../../utils/sharedFields';
|
||||
import { logWrapper } from '../../../utils/logWrapper';
|
||||
import { generateSchema, getSandboxWithZod } from '../../../utils/schemaParsing';
|
||||
import {
|
||||
inputSchemaField,
|
||||
jsonSchemaExampleField,
|
||||
schemaTypeField,
|
||||
} from '../../../utils/descriptions';
|
||||
|
||||
const STRUCTURED_OUTPUT_KEY = '__structured__output';
|
||||
const STRUCTURED_OUTPUT_OBJECT_KEY = '__structured__output__object';
|
||||
@@ -87,8 +91,8 @@ export class OutputParserStructured implements INodeType {
|
||||
name: 'outputParserStructured',
|
||||
icon: 'fa:code',
|
||||
group: ['transform'],
|
||||
version: [1, 1.1],
|
||||
defaultVersion: 1.1,
|
||||
version: [1, 1.1, 1.2],
|
||||
defaultVersion: 1.2,
|
||||
description: 'Return data in a defined JSON format',
|
||||
defaults: {
|
||||
name: 'Structured Output Parser',
|
||||
@@ -115,6 +119,33 @@ export class OutputParserStructured implements INodeType {
|
||||
outputNames: ['Output Parser'],
|
||||
properties: [
|
||||
getConnectionHintNoticeField([NodeConnectionType.AiChain, NodeConnectionType.AiAgent]),
|
||||
{ ...schemaTypeField, displayOptions: { show: { '@version': [{ _cnd: { gte: 1.2 } }] } } },
|
||||
{
|
||||
...jsonSchemaExampleField,
|
||||
default: `{
|
||||
"state": "California",
|
||||
"cities": ["Los Angeles", "San Francisco", "San Diego"]
|
||||
}`,
|
||||
},
|
||||
{
|
||||
...inputSchemaField,
|
||||
displayName: 'JSON Schema',
|
||||
description: 'JSON Schema to structure and validate the output against',
|
||||
default: `{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"state": {
|
||||
"type": "string"
|
||||
},
|
||||
"cities": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
},
|
||||
{
|
||||
displayName: 'JSON Schema',
|
||||
name: 'jsonSchema',
|
||||
@@ -138,6 +169,11 @@ export class OutputParserStructured implements INodeType {
|
||||
rows: 10,
|
||||
},
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@version': [{ _cnd: { lte: 1.1 } }],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName:
|
||||
@@ -145,72 +181,36 @@ export class OutputParserStructured implements INodeType {
|
||||
name: 'notice',
|
||||
type: 'notice',
|
||||
default: '',
|
||||
displayOptions: {
|
||||
hide: {
|
||||
schemaType: ['fromJson'],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
async supplyData(this: IExecuteFunctions, itemIndex: number): Promise<SupplyData> {
|
||||
const schema = this.getNodeParameter('jsonSchema', itemIndex) as string;
|
||||
const schemaType = this.getNodeParameter('schemaType', itemIndex, '') as 'fromJson' | 'manual';
|
||||
// We initialize these even though one of them will always be empty
|
||||
// it makes it easer to navigate the ternary operator
|
||||
const jsonExample = this.getNodeParameter('jsonSchemaExample', itemIndex, '') as string;
|
||||
let inputSchema: string;
|
||||
|
||||
let itemSchema: JSONSchema7;
|
||||
try {
|
||||
itemSchema = jsonParse<JSONSchema7>(schema);
|
||||
|
||||
// If the type is not defined, we assume it's an object
|
||||
if (itemSchema.type === undefined) {
|
||||
itemSchema = {
|
||||
type: 'object',
|
||||
properties: itemSchema.properties ?? (itemSchema as { [key: string]: JSONSchema7 }),
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
throw new NodeOperationError(this.getNode(), 'Error during parsing of JSON Schema.');
|
||||
if (this.getNode().typeVersion <= 1.1) {
|
||||
inputSchema = this.getNodeParameter('jsonSchema', itemIndex, '') as string;
|
||||
} else {
|
||||
inputSchema = this.getNodeParameter('inputSchema', itemIndex, '') as string;
|
||||
}
|
||||
|
||||
const vmResolver = makeResolverFromLegacyOptions({
|
||||
external: {
|
||||
modules: ['json-schema-to-zod', 'zod'],
|
||||
transitive: false,
|
||||
},
|
||||
resolve(moduleName, parentDirname) {
|
||||
if (moduleName === 'json-schema-to-zod') {
|
||||
return require.resolve(
|
||||
'@n8n/n8n-nodes-langchain/node_modules/json-schema-to-zod/dist/cjs/jsonSchemaToZod.js',
|
||||
{
|
||||
paths: [parentDirname],
|
||||
},
|
||||
);
|
||||
}
|
||||
if (moduleName === 'zod') {
|
||||
return require.resolve('@n8n/n8n-nodes-langchain/node_modules/zod.cjs', {
|
||||
paths: [parentDirname],
|
||||
});
|
||||
}
|
||||
return;
|
||||
},
|
||||
builtin: [],
|
||||
});
|
||||
const context = getSandboxContext.call(this, itemIndex);
|
||||
// Make sure to remove the description from root schema
|
||||
const { description, ...restOfSchema } = itemSchema;
|
||||
const sandboxedSchema = new JavaScriptSandbox(
|
||||
context,
|
||||
`
|
||||
const { z } = require('zod');
|
||||
const { parseSchema } = require('json-schema-to-zod');
|
||||
const zodSchema = parseSchema(${JSON.stringify(restOfSchema)});
|
||||
const itemSchema = new Function('z', 'return (' + zodSchema + ')')(z)
|
||||
return itemSchema
|
||||
`,
|
||||
itemIndex,
|
||||
this.helpers,
|
||||
{ resolver: vmResolver },
|
||||
);
|
||||
const jsonSchema =
|
||||
schemaType === 'fromJson' ? generateSchema(jsonExample) : jsonParse<JSONSchema7>(inputSchema);
|
||||
|
||||
const zodSchemaSandbox = getSandboxWithZod(this, jsonSchema, 0);
|
||||
const nodeVersion = this.getNode().typeVersion;
|
||||
try {
|
||||
const parser = await N8nStructuredOutputParser.fromZedJsonSchema(
|
||||
sandboxedSchema,
|
||||
zodSchemaSandbox,
|
||||
nodeVersion,
|
||||
);
|
||||
return {
|
||||
|
||||
@@ -9,16 +9,23 @@ import type {
|
||||
ExecutionError,
|
||||
IDataObject,
|
||||
} from 'n8n-workflow';
|
||||
import { NodeConnectionType, NodeOperationError } from 'n8n-workflow';
|
||||
import { NodeConnectionType, NodeOperationError, jsonParse } from 'n8n-workflow';
|
||||
import type { SetField, SetNodeOptions } from 'n8n-nodes-base/dist/nodes/Set/v2/helpers/interfaces';
|
||||
import * as manual from 'n8n-nodes-base/dist/nodes/Set/v2/manual.mode';
|
||||
|
||||
import { DynamicTool } from '@langchain/core/tools';
|
||||
import { DynamicStructuredTool, DynamicTool } from '@langchain/core/tools';
|
||||
import get from 'lodash/get';
|
||||
import isObject from 'lodash/isObject';
|
||||
import type { CallbackManagerForToolRun } from '@langchain/core/callbacks/manager';
|
||||
import type { JSONSchema7 } from 'json-schema';
|
||||
import { getConnectionHintNoticeField } from '../../../utils/sharedFields';
|
||||
|
||||
import type { DynamicZodObject } from '../../../types/zod.types';
|
||||
import { generateSchema, getSandboxWithZod } from '../../../utils/schemaParsing';
|
||||
import {
|
||||
jsonSchemaExampleField,
|
||||
schemaTypeField,
|
||||
inputSchemaField,
|
||||
} from '../../../utils/descriptions';
|
||||
export class ToolWorkflow implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Custom n8n Workflow Tool',
|
||||
@@ -314,6 +321,21 @@ export class ToolWorkflow implements INodeType {
|
||||
},
|
||||
],
|
||||
},
|
||||
// ----------------------------------
|
||||
// Output Parsing
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Specify Input Schema',
|
||||
name: 'specifyInputSchema',
|
||||
type: 'boolean',
|
||||
description:
|
||||
'Whether to specify the schema for the function. This would require the LLM to provide the input in the correct format and would validate it against the schema.',
|
||||
noDataExpression: true,
|
||||
default: false,
|
||||
},
|
||||
{ ...schemaTypeField, displayOptions: { show: { specifyInputSchema: [true] } } },
|
||||
jsonSchemaExampleField,
|
||||
inputSchemaField,
|
||||
],
|
||||
};
|
||||
|
||||
@@ -321,8 +343,11 @@ export class ToolWorkflow implements INodeType {
|
||||
const name = this.getNodeParameter('name', itemIndex) as string;
|
||||
const description = this.getNodeParameter('description', itemIndex) as string;
|
||||
|
||||
const useSchema = this.getNodeParameter('specifyInputSchema', itemIndex) as boolean;
|
||||
let tool: DynamicTool | DynamicStructuredTool | undefined = undefined;
|
||||
|
||||
const runFunction = async (
|
||||
query: string,
|
||||
query: string | IDataObject,
|
||||
runManager?: CallbackManagerForToolRun,
|
||||
): Promise<string> => {
|
||||
const source = this.getNodeParameter('source', itemIndex) as string;
|
||||
@@ -416,50 +441,86 @@ export class ToolWorkflow implements INodeType {
|
||||
return response;
|
||||
};
|
||||
|
||||
const toolHandler = async (
|
||||
query: string | IDataObject,
|
||||
runManager?: CallbackManagerForToolRun,
|
||||
): Promise<string> => {
|
||||
const { index } = this.addInputData(NodeConnectionType.AiTool, [[{ json: { query } }]]);
|
||||
|
||||
let response: string = '';
|
||||
let executionError: ExecutionError | undefined;
|
||||
try {
|
||||
response = await runFunction(query, runManager);
|
||||
} catch (error) {
|
||||
// TODO: Do some more testing. Issues here should actually fail the workflow
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
executionError = error;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
response = `There was an error: "${error.message}"`;
|
||||
}
|
||||
|
||||
if (typeof response === 'number') {
|
||||
response = (response as number).toString();
|
||||
}
|
||||
|
||||
if (isObject(response)) {
|
||||
response = JSON.stringify(response, null, 2);
|
||||
}
|
||||
|
||||
if (typeof response !== 'string') {
|
||||
// TODO: Do some more testing. Issues here should actually fail the workflow
|
||||
executionError = new NodeOperationError(this.getNode(), 'Wrong output type returned', {
|
||||
description: `The response property should be a string, but it is an ${typeof response}`,
|
||||
});
|
||||
response = `There was an error: "${executionError.message}"`;
|
||||
}
|
||||
|
||||
if (executionError) {
|
||||
void this.addOutputData(NodeConnectionType.AiTool, index, executionError);
|
||||
} else {
|
||||
void this.addOutputData(NodeConnectionType.AiTool, index, [[{ json: { response } }]]);
|
||||
}
|
||||
return response;
|
||||
};
|
||||
|
||||
const functionBase = {
|
||||
name,
|
||||
description,
|
||||
func: toolHandler,
|
||||
};
|
||||
|
||||
if (useSchema) {
|
||||
try {
|
||||
// We initialize these even though one of them will always be empty
|
||||
// it makes it easer to navigate the ternary operator
|
||||
const jsonExample = this.getNodeParameter('jsonSchemaExample', itemIndex, '') as string;
|
||||
const inputSchema = this.getNodeParameter('inputSchema', itemIndex, '') as string;
|
||||
|
||||
const schemaType = this.getNodeParameter('schemaType', itemIndex) as 'fromJson' | 'manual';
|
||||
const jsonSchema =
|
||||
schemaType === 'fromJson'
|
||||
? generateSchema(jsonExample)
|
||||
: jsonParse<JSONSchema7>(inputSchema);
|
||||
|
||||
const zodSchemaSandbox = getSandboxWithZod(this, jsonSchema, 0);
|
||||
const zodSchema = (await zodSchemaSandbox.runCode()) as DynamicZodObject;
|
||||
|
||||
tool = new DynamicStructuredTool<typeof zodSchema>({
|
||||
schema: zodSchema,
|
||||
...functionBase,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new NodeOperationError(
|
||||
this.getNode(),
|
||||
'Error during parsing of JSON Schema. \n ' + error,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
tool = new DynamicTool(functionBase);
|
||||
}
|
||||
|
||||
return {
|
||||
response: new DynamicTool({
|
||||
name,
|
||||
description,
|
||||
|
||||
func: async (query: string, runManager?: CallbackManagerForToolRun): Promise<string> => {
|
||||
const { index } = this.addInputData(NodeConnectionType.AiTool, [[{ json: { query } }]]);
|
||||
|
||||
let response: string = '';
|
||||
let executionError: ExecutionError | undefined;
|
||||
try {
|
||||
response = await runFunction(query, runManager);
|
||||
} catch (error) {
|
||||
// TODO: Do some more testing. Issues here should actually fail the workflow
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
executionError = error;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
response = `There was an error: "${error.message}"`;
|
||||
}
|
||||
|
||||
if (typeof response === 'number') {
|
||||
response = (response as number).toString();
|
||||
}
|
||||
|
||||
if (isObject(response)) {
|
||||
response = JSON.stringify(response, null, 2);
|
||||
}
|
||||
|
||||
if (typeof response !== 'string') {
|
||||
// TODO: Do some more testing. Issues here should actually fail the workflow
|
||||
executionError = new NodeOperationError(this.getNode(), 'Wrong output type returned', {
|
||||
description: `The response property should be a string, but it is an ${typeof response}`,
|
||||
});
|
||||
response = `There was an error: "${executionError.message}"`;
|
||||
}
|
||||
|
||||
if (executionError) {
|
||||
void this.addOutputData(NodeConnectionType.AiTool, index, executionError);
|
||||
} else {
|
||||
void this.addOutputData(NodeConnectionType.AiTool, index, [[{ json: { response } }]]);
|
||||
}
|
||||
return response;
|
||||
},
|
||||
}),
|
||||
response: tool,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user