Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Custom n8n Workflow Tool Node): Add support for tool input schema #9470

Merged
merged 9 commits into from
May 22, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,15 @@

returnData.push({ json: response });
} catch (error) {
if (error.message.includes('tool input did not match expected schema')) {

Check warning on line 114 in packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ConversationalAgent/execute.ts

View workflow job for this annotation

GitHub Actions / Lint changes

Unsafe call of an `any` typed value

Check warning on line 114 in packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ConversationalAgent/execute.ts

View workflow job for this annotation

GitHub Actions / Lint changes

Unsafe member access .message on an `any` value
throw new NodeOperationError(
this.getNode(),
`${error.message}.

Check warning on line 117 in packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ConversationalAgent/execute.ts

View workflow job for this annotation

GitHub Actions / Lint changes

Unsafe member access .message on an `any` value
This is most likely because some of your tools are configured to require a specific schema. This is not supported by Conversational Agent. Remove the schema from the tool configuration or use Tools agent instead.`,
);
}
if (this.continueOnFail()) {
returnData.push({ json: { error: error.message }, pairedItem: { item: itemIndex } });

Check warning on line 122 in packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ConversationalAgent/execute.ts

View workflow job for this annotation

GitHub Actions / Lint changes

Unsafe assignment of an `any` value

Check warning on line 122 in packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ConversationalAgent/execute.ts

View workflow job for this annotation

GitHub Actions / Lint changes

Unsafe member access .message on an `any` value
continue;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,13 @@

returnData.push({ json: response });
} catch (error) {
if (error.message.includes('tool input did not match expected schema')) {

Check warning on line 94 in packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/PlanAndExecuteAgent/execute.ts

View workflow job for this annotation

GitHub Actions / Lint changes

Unsafe call of an `any` typed value

Check warning on line 94 in packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/PlanAndExecuteAgent/execute.ts

View workflow job for this annotation

GitHub Actions / Lint changes

Unsafe member access .message on an `any` value
throw new NodeOperationError(
this.getNode(),
`${error.message}.

Check warning on line 97 in packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/PlanAndExecuteAgent/execute.ts

View workflow job for this annotation

GitHub Actions / Lint changes

Unsafe member access .message on an `any` value
This is most likely because some of your tools are configured to require a specific schema. This is not supported by Plan And Execute agent. Remove the schema from the tool configuration or use Tools agent instead.`,
);
}
if (this.continueOnFail()) {
returnData.push({ json: { error: error.message }, pairedItem: { item: itemIndex } });
continue;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,13 @@ export async function reActAgentAgentExecute(

returnData.push({ json: response });
} catch (error) {
if (error.message.includes('tool input did not match expected schema')) {
throw new NodeOperationError(
this.getNode(),
`${error.message}.
This is most likely because some of your tools are configured to require a specific schema. This is not supported by ReAct Agent. Remove the schema from the tool configuration or use Tools agent instead.`,
);
}
OlegIvaniv marked this conversation as resolved.
Show resolved Hide resolved
if (this.continueOnFail()) {
returnData.push({ json: { error: error.message }, pairedItem: { item: itemIndex } });
continue;
Expand Down
2 changes: 2 additions & 0 deletions packages/@n8n/nodes-langchain/nodes/code/Code.node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,10 @@ 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';

const STRUCTURED_OUTPUT_KEY = '__structured__output';
const STRUCTURED_OUTPUT_OBJECT_KEY = '__structured__output__object';
Expand Down Expand Up @@ -87,8 +86,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',
Expand All @@ -115,6 +114,79 @@ export class OutputParserStructured implements INodeType {
outputNames: ['Output Parser'],
properties: [
getConnectionHintNoticeField([NodeConnectionType.AiChain, NodeConnectionType.AiAgent]),
{
displayName: 'Schema Type',
name: 'schemaType',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Generate From JSON Example',
value: 'fromJson',
description: 'Generate a schema from an example JSON object',
},
{
name: 'Define Below',
value: 'manual',
description: 'Define the JSON schema manually',
},
],
default: 'fromJson',
description: 'How to specify the schema for the function',
displayOptions: {
show: {
'@version': [{ _cnd: { gte: 1.2 } }],
},
},
},
{
displayName: 'JSON Example',
name: 'jsonSchemaExample',
type: 'json',
default: `{
"state": "California",
"cities": ["Los Angeles", "San Francisco", "San Diego"]
}`,
noDataExpression: true,
typeOptions: {
rows: 10,
},
displayOptions: {
show: {
schemaType: ['fromJson'],
},
},
description: 'Example JSON object to use to generate the schema',
},
{
displayName: 'Input Schema',
name: 'inputSchema',
type: 'json',
default: `{
"type": "object",
"properties": {
"state": {
"type": "string"
},
"cities": {
"type": "array",
"items": {
"type": "string"
}
}
}
}`,
noDataExpression: true,
typeOptions: {
rows: 10,
},
displayOptions: {
show: {
schemaType: ['manual'],
},
},
description: 'Schema to use for the function',
},
{
displayName: 'JSON Schema',
name: 'jsonSchema',
Expand All @@ -138,79 +210,48 @@ export class OutputParserStructured implements INodeType {
rows: 10,
},
required: true,
displayOptions: {
show: {
'@version': [{ _cnd: { lte: 1.1 } }],
},
},
},
{
displayName:
'The schema has to be defined in the <a target="_blank" href="https://json-schema.org/">JSON Schema</a> format. Look at <a target="_blank" href="https://json-schema.org/learn/miscellaneous-examples.html">this</a> page for examples.',
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;

let itemSchema: JSONSchema7;
try {
itemSchema = jsonParse<JSONSchema7>(schema);
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;

// 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 {
Expand Down
Loading
Loading