mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-24 20:24:05 -08: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:
parent
ef9d4aba90
commit
2fa46b6faa
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -157,6 +157,7 @@
|
|||
"d3-dsv": "2.0.0",
|
||||
"epub2": "3.0.2",
|
||||
"form-data": "4.0.0",
|
||||
"generate-schema": "^2.6.0",
|
||||
"html-to-text": "9.0.5",
|
||||
"jest-mock-extended": "^3.0.4",
|
||||
"json-schema-to-zod": "2.0.14",
|
||||
|
|
|
@ -11,7 +11,8 @@
|
|||
"credentials/**/*.ts",
|
||||
"nodes/**/*.ts",
|
||||
"nodes/**/*.json",
|
||||
"credentials/translations/**/*.json"
|
||||
"credentials/translations/**/*.json",
|
||||
"types/*.ts"
|
||||
],
|
||||
"exclude": ["nodes/**/*.test.ts", "test/**"]
|
||||
}
|
||||
|
|
|
@ -20,5 +20,5 @@
|
|||
"skipLibCheck": true,
|
||||
"outDir": "./dist/"
|
||||
},
|
||||
"include": ["credentials/**/*", "nodes/**/*", "utils/**/*.ts", "nodes/**/*.json"]
|
||||
"include": ["credentials/**/*", "nodes/**/*", "utils/**/*.ts", "nodes/**/*.json", "types/*.ts"]
|
||||
}
|
||||
|
|
27
packages/@n8n/nodes-langchain/types/generate-schema.d.ts
vendored
Normal file
27
packages/@n8n/nodes-langchain/types/generate-schema.d.ts
vendored
Normal file
|
@ -0,0 +1,27 @@
|
|||
declare module 'generate-schema' {
|
||||
export interface SchemaObject {
|
||||
$schema: string;
|
||||
title?: string;
|
||||
type: string;
|
||||
properties?: {
|
||||
[key: string]: SchemaObject | SchemaArray | SchemaProperty;
|
||||
};
|
||||
required?: string[];
|
||||
items?: SchemaObject | SchemaArray;
|
||||
}
|
||||
|
||||
export interface SchemaArray {
|
||||
type: string;
|
||||
items?: SchemaObject | SchemaArray | SchemaProperty;
|
||||
oneOf?: Array<SchemaObject | SchemaArray | SchemaProperty>;
|
||||
required?: string[];
|
||||
}
|
||||
|
||||
export interface SchemaProperty {
|
||||
type: string | string[];
|
||||
format?: string;
|
||||
}
|
||||
|
||||
export function json(title: string, schema: SchemaObject): SchemaObject;
|
||||
export function json(schema: SchemaObject): SchemaObject;
|
||||
}
|
4
packages/@n8n/nodes-langchain/types/zod.types.ts
Normal file
4
packages/@n8n/nodes-langchain/types/zod.types.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
import type { z } from 'zod';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type DynamicZodObject = z.ZodObject<any, any, any, any>;
|
|
@ -1,5 +1,70 @@
|
|||
import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
export const schemaTypeField: INodeProperties = {
|
||||
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',
|
||||
};
|
||||
|
||||
export const jsonSchemaExampleField: INodeProperties = {
|
||||
displayName: 'JSON Example',
|
||||
name: 'jsonSchemaExample',
|
||||
type: 'json',
|
||||
default: `{
|
||||
"some_input": "some_value"
|
||||
}`,
|
||||
noDataExpression: true,
|
||||
typeOptions: {
|
||||
rows: 10,
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
schemaType: ['fromJson'],
|
||||
},
|
||||
},
|
||||
description: 'Example JSON object to use to generate the schema',
|
||||
};
|
||||
|
||||
export const inputSchemaField: INodeProperties = {
|
||||
displayName: 'Input Schema',
|
||||
name: 'inputSchema',
|
||||
type: 'json',
|
||||
default: `{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"some_input": {
|
||||
"type": "string",
|
||||
"description": "Some input to the function"
|
||||
}
|
||||
}
|
||||
}`,
|
||||
noDataExpression: true,
|
||||
typeOptions: {
|
||||
rows: 10,
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
schemaType: ['manual'],
|
||||
},
|
||||
},
|
||||
description: 'Schema to use for the function',
|
||||
};
|
||||
|
||||
export const promptTypeOptions: INodeProperties = {
|
||||
displayName: 'Prompt',
|
||||
name: 'promptType',
|
||||
|
|
81
packages/@n8n/nodes-langchain/utils/schemaParsing.ts
Normal file
81
packages/@n8n/nodes-langchain/utils/schemaParsing.ts
Normal file
|
@ -0,0 +1,81 @@
|
|||
import { makeResolverFromLegacyOptions } from '@n8n/vm2';
|
||||
import { json as generateJsonSchema } from 'generate-schema';
|
||||
import type { SchemaObject } from 'generate-schema';
|
||||
import type { JSONSchema7 } from 'json-schema';
|
||||
import { JavaScriptSandbox } from 'n8n-nodes-base/dist/nodes/Code/JavaScriptSandbox';
|
||||
import { getSandboxContext } from 'n8n-nodes-base/dist/nodes/Code/Sandbox';
|
||||
import type { IExecuteFunctions } from 'n8n-workflow';
|
||||
import { NodeOperationError, jsonParse } from 'n8n-workflow';
|
||||
|
||||
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: [],
|
||||
});
|
||||
|
||||
export function getSandboxWithZod(ctx: IExecuteFunctions, schema: JSONSchema7, itemIndex: number) {
|
||||
const context = getSandboxContext.call(ctx, itemIndex);
|
||||
let itemSchema: JSONSchema7 = schema;
|
||||
try {
|
||||
// If the root 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(ctx.getNode(), 'Error during parsing of JSON Schema.');
|
||||
}
|
||||
|
||||
// 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,
|
||||
ctx.helpers,
|
||||
{ resolver: vmResolver },
|
||||
);
|
||||
return sandboxedSchema;
|
||||
}
|
||||
|
||||
export function generateSchema(schemaString: string): JSONSchema7 {
|
||||
const parsedSchema = jsonParse<SchemaObject>(schemaString);
|
||||
|
||||
return generateJsonSchema(parsedSchema) as JSONSchema7;
|
||||
}
|
||||
|
||||
export function throwIfToolSchema(ctx: IExecuteFunctions, error: Error) {
|
||||
if (error?.message?.includes('tool input did not match expected schema')) {
|
||||
throw new NodeOperationError(
|
||||
ctx.getNode(),
|
||||
`${error.message}.
|
||||
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.`,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -315,6 +315,9 @@ importers:
|
|||
form-data:
|
||||
specifier: 4.0.0
|
||||
version: 4.0.0
|
||||
generate-schema:
|
||||
specifier: ^2.6.0
|
||||
version: 2.6.0
|
||||
html-to-text:
|
||||
specifier: 9.0.5
|
||||
version: 9.0.5
|
||||
|
@ -15175,6 +15178,14 @@ packages:
|
|||
is-property: 1.0.2
|
||||
dev: false
|
||||
|
||||
/generate-schema@2.6.0:
|
||||
resolution: {integrity: sha512-EUBKfJNzT8f91xUk5X5gKtnbdejZeE065UAJ3BCzE8VEbvwKI9Pm5jaWmqVeK1MYc1g5weAVFDTSJzN7ymtTqA==}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
commander: 2.20.3
|
||||
type-of-is: 3.5.1
|
||||
dev: false
|
||||
|
||||
/generic-pool@3.9.0:
|
||||
resolution: {integrity: sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==}
|
||||
engines: {node: '>= 4'}
|
||||
|
@ -23560,6 +23571,11 @@ packages:
|
|||
media-typer: 0.3.0
|
||||
mime-types: 2.1.35
|
||||
|
||||
/type-of-is@3.5.1:
|
||||
resolution: {integrity: sha512-SOnx8xygcAh8lvDU2exnK2bomASfNjzB3Qz71s2tw9QnX8fkAo7aC+D0H7FV0HjRKj94CKV2Hi71kVkkO6nOxg==}
|
||||
engines: {node: '>=0.10.5'}
|
||||
dev: false
|
||||
|
||||
/type@1.2.0:
|
||||
resolution: {integrity: sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==}
|
||||
dev: false
|
||||
|
|
Loading…
Reference in a new issue