mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-12 13:27:31 -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,
|
getConnectedTools,
|
||||||
} from '../../../../../utils/helpers';
|
} from '../../../../../utils/helpers';
|
||||||
import { getTracingConfig } from '../../../../../utils/tracing';
|
import { getTracingConfig } from '../../../../../utils/tracing';
|
||||||
|
import { throwIfToolSchema } from '../../../../../utils/schemaParsing';
|
||||||
|
|
||||||
export async function conversationalAgentExecute(
|
export async function conversationalAgentExecute(
|
||||||
this: IExecuteFunctions,
|
this: IExecuteFunctions,
|
||||||
|
@ -111,6 +112,8 @@ export async function conversationalAgentExecute(
|
||||||
|
|
||||||
returnData.push({ json: response });
|
returnData.push({ json: response });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
throwIfToolSchema(this, error);
|
||||||
|
|
||||||
if (this.continueOnFail()) {
|
if (this.continueOnFail()) {
|
||||||
returnData.push({ json: { error: error.message }, pairedItem: { item: itemIndex } });
|
returnData.push({ json: { error: error.message }, pairedItem: { item: itemIndex } });
|
||||||
continue;
|
continue;
|
||||||
|
|
|
@ -16,6 +16,7 @@ import {
|
||||||
getPromptInputByType,
|
getPromptInputByType,
|
||||||
} from '../../../../../utils/helpers';
|
} from '../../../../../utils/helpers';
|
||||||
import { getTracingConfig } from '../../../../../utils/tracing';
|
import { getTracingConfig } from '../../../../../utils/tracing';
|
||||||
|
import { throwIfToolSchema } from '../../../../../utils/schemaParsing';
|
||||||
|
|
||||||
export async function planAndExecuteAgentExecute(
|
export async function planAndExecuteAgentExecute(
|
||||||
this: IExecuteFunctions,
|
this: IExecuteFunctions,
|
||||||
|
@ -91,6 +92,7 @@ export async function planAndExecuteAgentExecute(
|
||||||
|
|
||||||
returnData.push({ json: response });
|
returnData.push({ json: response });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
throwIfToolSchema(this, error);
|
||||||
if (this.continueOnFail()) {
|
if (this.continueOnFail()) {
|
||||||
returnData.push({ json: { error: error.message }, pairedItem: { item: itemIndex } });
|
returnData.push({ json: { error: error.message }, pairedItem: { item: itemIndex } });
|
||||||
continue;
|
continue;
|
||||||
|
|
|
@ -18,6 +18,7 @@ import {
|
||||||
isChatInstance,
|
isChatInstance,
|
||||||
} from '../../../../../utils/helpers';
|
} from '../../../../../utils/helpers';
|
||||||
import { getTracingConfig } from '../../../../../utils/tracing';
|
import { getTracingConfig } from '../../../../../utils/tracing';
|
||||||
|
import { throwIfToolSchema } from '../../../../../utils/schemaParsing';
|
||||||
|
|
||||||
export async function reActAgentAgentExecute(
|
export async function reActAgentAgentExecute(
|
||||||
this: IExecuteFunctions,
|
this: IExecuteFunctions,
|
||||||
|
@ -112,6 +113,7 @@ export async function reActAgentAgentExecute(
|
||||||
|
|
||||||
returnData.push({ json: response });
|
returnData.push({ json: response });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
throwIfToolSchema(this, error);
|
||||||
if (this.continueOnFail()) {
|
if (this.continueOnFail()) {
|
||||||
returnData.push({ json: { error: error.message }, pairedItem: { item: itemIndex } });
|
returnData.push({ json: { error: error.message }, pairedItem: { item: itemIndex } });
|
||||||
continue;
|
continue;
|
||||||
|
|
|
@ -92,6 +92,8 @@ function getSandbox(
|
||||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||||
context.executeWorkflow = this.executeWorkflow;
|
context.executeWorkflow = this.executeWorkflow;
|
||||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||||
|
context.getWorkflowDataProxy = this.getWorkflowDataProxy;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||||
context.logger = this.logger;
|
context.logger = this.logger;
|
||||||
|
|
||||||
if (options?.addItems) {
|
if (options?.addItems) {
|
||||||
|
|
|
@ -13,11 +13,15 @@ import type { JSONSchema7 } from 'json-schema';
|
||||||
import { StructuredOutputParser } from 'langchain/output_parsers';
|
import { StructuredOutputParser } from 'langchain/output_parsers';
|
||||||
import { OutputParserException } from '@langchain/core/output_parsers';
|
import { OutputParserException } from '@langchain/core/output_parsers';
|
||||||
import get from 'lodash/get';
|
import get from 'lodash/get';
|
||||||
import { getSandboxContext } from 'n8n-nodes-base/dist/nodes/Code/Sandbox';
|
import type { JavaScriptSandbox } from 'n8n-nodes-base/dist/nodes/Code/JavaScriptSandbox';
|
||||||
import { JavaScriptSandbox } from 'n8n-nodes-base/dist/nodes/Code/JavaScriptSandbox';
|
|
||||||
import { makeResolverFromLegacyOptions } from '@n8n/vm2';
|
|
||||||
import { getConnectionHintNoticeField } from '../../../utils/sharedFields';
|
import { getConnectionHintNoticeField } from '../../../utils/sharedFields';
|
||||||
import { logWrapper } from '../../../utils/logWrapper';
|
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_KEY = '__structured__output';
|
||||||
const STRUCTURED_OUTPUT_OBJECT_KEY = '__structured__output__object';
|
const STRUCTURED_OUTPUT_OBJECT_KEY = '__structured__output__object';
|
||||||
|
@ -87,8 +91,8 @@ export class OutputParserStructured implements INodeType {
|
||||||
name: 'outputParserStructured',
|
name: 'outputParserStructured',
|
||||||
icon: 'fa:code',
|
icon: 'fa:code',
|
||||||
group: ['transform'],
|
group: ['transform'],
|
||||||
version: [1, 1.1],
|
version: [1, 1.1, 1.2],
|
||||||
defaultVersion: 1.1,
|
defaultVersion: 1.2,
|
||||||
description: 'Return data in a defined JSON format',
|
description: 'Return data in a defined JSON format',
|
||||||
defaults: {
|
defaults: {
|
||||||
name: 'Structured Output Parser',
|
name: 'Structured Output Parser',
|
||||||
|
@ -115,6 +119,33 @@ export class OutputParserStructured implements INodeType {
|
||||||
outputNames: ['Output Parser'],
|
outputNames: ['Output Parser'],
|
||||||
properties: [
|
properties: [
|
||||||
getConnectionHintNoticeField([NodeConnectionType.AiChain, NodeConnectionType.AiAgent]),
|
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',
|
displayName: 'JSON Schema',
|
||||||
name: 'jsonSchema',
|
name: 'jsonSchema',
|
||||||
|
@ -138,6 +169,11 @@ export class OutputParserStructured implements INodeType {
|
||||||
rows: 10,
|
rows: 10,
|
||||||
},
|
},
|
||||||
required: true,
|
required: true,
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
'@version': [{ _cnd: { lte: 1.1 } }],
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
displayName:
|
displayName:
|
||||||
|
@ -145,72 +181,36 @@ export class OutputParserStructured implements INodeType {
|
||||||
name: 'notice',
|
name: 'notice',
|
||||||
type: 'notice',
|
type: 'notice',
|
||||||
default: '',
|
default: '',
|
||||||
|
displayOptions: {
|
||||||
|
hide: {
|
||||||
|
schemaType: ['fromJson'],
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
async supplyData(this: IExecuteFunctions, itemIndex: number): Promise<SupplyData> {
|
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;
|
if (this.getNode().typeVersion <= 1.1) {
|
||||||
try {
|
inputSchema = this.getNodeParameter('jsonSchema', itemIndex, '') as string;
|
||||||
itemSchema = jsonParse<JSONSchema7>(schema);
|
} else {
|
||||||
|
inputSchema = this.getNodeParameter('inputSchema', itemIndex, '') as 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.');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const vmResolver = makeResolverFromLegacyOptions({
|
const jsonSchema =
|
||||||
external: {
|
schemaType === 'fromJson' ? generateSchema(jsonExample) : jsonParse<JSONSchema7>(inputSchema);
|
||||||
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 zodSchemaSandbox = getSandboxWithZod(this, jsonSchema, 0);
|
||||||
const nodeVersion = this.getNode().typeVersion;
|
const nodeVersion = this.getNode().typeVersion;
|
||||||
try {
|
try {
|
||||||
const parser = await N8nStructuredOutputParser.fromZedJsonSchema(
|
const parser = await N8nStructuredOutputParser.fromZedJsonSchema(
|
||||||
sandboxedSchema,
|
zodSchemaSandbox,
|
||||||
nodeVersion,
|
nodeVersion,
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -9,16 +9,23 @@ import type {
|
||||||
ExecutionError,
|
ExecutionError,
|
||||||
IDataObject,
|
IDataObject,
|
||||||
} from 'n8n-workflow';
|
} 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 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 * 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 get from 'lodash/get';
|
||||||
import isObject from 'lodash/isObject';
|
import isObject from 'lodash/isObject';
|
||||||
import type { CallbackManagerForToolRun } from '@langchain/core/callbacks/manager';
|
import type { CallbackManagerForToolRun } from '@langchain/core/callbacks/manager';
|
||||||
|
import type { JSONSchema7 } from 'json-schema';
|
||||||
import { getConnectionHintNoticeField } from '../../../utils/sharedFields';
|
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 {
|
export class ToolWorkflow implements INodeType {
|
||||||
description: INodeTypeDescription = {
|
description: INodeTypeDescription = {
|
||||||
displayName: 'Custom n8n Workflow Tool',
|
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 name = this.getNodeParameter('name', itemIndex) as string;
|
||||||
const description = this.getNodeParameter('description', 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 (
|
const runFunction = async (
|
||||||
query: string,
|
query: string | IDataObject,
|
||||||
runManager?: CallbackManagerForToolRun,
|
runManager?: CallbackManagerForToolRun,
|
||||||
): Promise<string> => {
|
): Promise<string> => {
|
||||||
const source = this.getNodeParameter('source', itemIndex) as string;
|
const source = this.getNodeParameter('source', itemIndex) as string;
|
||||||
|
@ -416,50 +441,86 @@ export class ToolWorkflow implements INodeType {
|
||||||
return response;
|
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 {
|
return {
|
||||||
response: new DynamicTool({
|
response: tool,
|
||||||
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;
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -157,6 +157,7 @@
|
||||||
"d3-dsv": "2.0.0",
|
"d3-dsv": "2.0.0",
|
||||||
"epub2": "3.0.2",
|
"epub2": "3.0.2",
|
||||||
"form-data": "4.0.0",
|
"form-data": "4.0.0",
|
||||||
|
"generate-schema": "^2.6.0",
|
||||||
"html-to-text": "9.0.5",
|
"html-to-text": "9.0.5",
|
||||||
"jest-mock-extended": "^3.0.4",
|
"jest-mock-extended": "^3.0.4",
|
||||||
"json-schema-to-zod": "2.0.14",
|
"json-schema-to-zod": "2.0.14",
|
||||||
|
|
|
@ -11,7 +11,8 @@
|
||||||
"credentials/**/*.ts",
|
"credentials/**/*.ts",
|
||||||
"nodes/**/*.ts",
|
"nodes/**/*.ts",
|
||||||
"nodes/**/*.json",
|
"nodes/**/*.json",
|
||||||
"credentials/translations/**/*.json"
|
"credentials/translations/**/*.json",
|
||||||
|
"types/*.ts"
|
||||||
],
|
],
|
||||||
"exclude": ["nodes/**/*.test.ts", "test/**"]
|
"exclude": ["nodes/**/*.test.ts", "test/**"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,5 +20,5 @@
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"outDir": "./dist/"
|
"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';
|
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 = {
|
export const promptTypeOptions: INodeProperties = {
|
||||||
displayName: 'Prompt',
|
displayName: 'Prompt',
|
||||||
name: 'promptType',
|
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:
|
form-data:
|
||||||
specifier: 4.0.0
|
specifier: 4.0.0
|
||||||
version: 4.0.0
|
version: 4.0.0
|
||||||
|
generate-schema:
|
||||||
|
specifier: ^2.6.0
|
||||||
|
version: 2.6.0
|
||||||
html-to-text:
|
html-to-text:
|
||||||
specifier: 9.0.5
|
specifier: 9.0.5
|
||||||
version: 9.0.5
|
version: 9.0.5
|
||||||
|
@ -15175,6 +15178,14 @@ packages:
|
||||||
is-property: 1.0.2
|
is-property: 1.0.2
|
||||||
dev: false
|
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:
|
/generic-pool@3.9.0:
|
||||||
resolution: {integrity: sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==}
|
resolution: {integrity: sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==}
|
||||||
engines: {node: '>= 4'}
|
engines: {node: '>= 4'}
|
||||||
|
@ -23560,6 +23571,11 @@ packages:
|
||||||
media-typer: 0.3.0
|
media-typer: 0.3.0
|
||||||
mime-types: 2.1.35
|
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:
|
/type@1.2.0:
|
||||||
resolution: {integrity: sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==}
|
resolution: {integrity: sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
Loading…
Reference in a new issue