Skip to content

Commit

Permalink
feat(Custom n8n Workflow Tool Node): Add support for tool input schema (
Browse files Browse the repository at this point in the history
#9470)

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>
  • Loading branch information
OlegIvaniv authored May 22, 2024
1 parent ef9d4ab commit 2fa46b6
Show file tree
Hide file tree
Showing 14 changed files with 368 additions and 103 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -111,6 +112,8 @@ export async function conversationalAgentExecute(

returnData.push({ json: response });
} catch (error) {
throwIfToolSchema(this, error);

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

View workflow job for this annotation

GitHub Actions / Lint changes (18.x)

Unsafe argument of type `any` assigned to a parameter of type `Error`

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

View workflow job for this annotation

GitHub Actions / Lint changes (20.x)

Unsafe argument of type `any` assigned to a parameter of type `Error`

if (this.continueOnFail()) {
returnData.push({ json: { error: error.message }, pairedItem: { item: itemIndex } });

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

View workflow job for this annotation

GitHub Actions / Lint changes (18.x)

Unsafe assignment of an `any` value

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

View workflow job for this annotation

GitHub Actions / Lint changes (18.x)

Unsafe member access .message on an `any` value

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

View workflow job for this annotation

GitHub Actions / Lint changes (20.x)

Unsafe assignment of an `any` value

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

View workflow job for this annotation

GitHub Actions / Lint changes (20.x)

Unsafe member access .message on an `any` value
continue;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -91,6 +92,7 @@ export async function planAndExecuteAgentExecute(

returnData.push({ json: response });
} catch (error) {
throwIfToolSchema(this, error);

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

View workflow job for this annotation

GitHub Actions / Lint changes (18.x)

Unsafe argument of type `any` assigned to a parameter of type `Error`

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

View workflow job for this annotation

GitHub Actions / Lint changes (20.x)

Unsafe argument of type `any` assigned to a parameter of type `Error`
if (this.continueOnFail()) {
returnData.push({ json: { error: error.message }, pairedItem: { item: itemIndex } });

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 (18.x)

Unsafe assignment of an `any` value

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 (18.x)

Unsafe member access .message on an `any` value

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 (20.x)

Unsafe assignment of an `any` value

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 (20.x)

Unsafe member access .message on an `any` value
continue;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -112,6 +113,7 @@ export async function reActAgentAgentExecute(

returnData.push({ json: response });
} catch (error) {
throwIfToolSchema(this, error);

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

View workflow job for this annotation

GitHub Actions / Lint changes (18.x)

Unsafe argument of type `any` assigned to a parameter of type `Error`

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

View workflow job for this annotation

GitHub Actions / Lint changes (20.x)

Unsafe argument of type `any` assigned to a parameter of type `Error`
if (this.continueOnFail()) {
returnData.push({ json: { error: error.message }, pairedItem: { item: itemIndex } });

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

View workflow job for this annotation

GitHub Actions / Lint changes (18.x)

Unsafe assignment of an `any` value

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

View workflow job for this annotation

GitHub Actions / Lint changes (20.x)

Unsafe assignment of an `any` value
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,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';
Expand Down Expand Up @@ -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',
Expand All @@ -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',
Expand All @@ -138,79 +169,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;
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 {
Expand Down
Loading

0 comments on commit 2fa46b6

Please sign in to comment.