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 @@ -13,6 +13,7 @@
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,8 +112,10 @@

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

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

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

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 @@
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,8 +92,9 @@

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

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

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

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 @@
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,8 +113,9 @@

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

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

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
Loading