n8n/packages/@n8n/nodes-langchain/utils/N8nTool.ts

114 lines
3.7 KiB
TypeScript

import type { DynamicStructuredToolInput } from '@langchain/core/tools';
import { DynamicStructuredTool, DynamicTool } from '@langchain/core/tools';
import type { IExecuteFunctions, IDataObject } from 'n8n-workflow';
import { NodeConnectionType, jsonParse, NodeOperationError } from 'n8n-workflow';
import { StructuredOutputParser } from 'langchain/output_parsers';
import type { ZodTypeAny } from 'zod';
import { ZodBoolean, ZodNullable, ZodNumber, ZodObject, ZodOptional } from 'zod';
const getSimplifiedType = (schema: ZodTypeAny) => {
if (schema instanceof ZodObject) {
return 'object';
} else if (schema instanceof ZodNumber) {
return 'number';
} else if (schema instanceof ZodBoolean) {
return 'boolean';
} else if (schema instanceof ZodNullable || schema instanceof ZodOptional) {
return getSimplifiedType(schema.unwrap());
}
return 'string';
};
const getParametersDescription = (parameters: Array<[string, ZodTypeAny]>) =>
parameters
.map(
([name, schema]) =>
`${name}: (description: ${schema.description ?? ''}, type: ${getSimplifiedType(schema)}, required: ${!schema.isOptional()})`,
)
.join(',\n ');
export const prepareFallbackToolDescription = (toolDescription: string, schema: ZodObject<any>) => {
let description = `${toolDescription}`;
const toolParameters = Object.entries<ZodTypeAny>(schema.shape);
if (toolParameters.length) {
description += `
Tool expects valid stringified JSON object with ${toolParameters.length} properties.
Property names with description, type and required status:
${getParametersDescription(toolParameters)}
ALL parameters marked as required must be provided`;
}
return description;
};
export class N8nTool extends DynamicStructuredTool {
private context: IExecuteFunctions;
constructor(context: IExecuteFunctions, fields: DynamicStructuredToolInput) {
super(fields);
this.context = context;
}
asDynamicTool(): DynamicTool {
const { name, func, schema, context, description } = this;
const parser = new StructuredOutputParser(schema);
const wrappedFunc = async function (query: string) {
let parsedQuery: object;
// First we try to parse the query using the structured parser (Zod schema)
try {
parsedQuery = await parser.parse(query);
} catch (e) {
// If we were unable to parse the query using the schema, we try to gracefully handle it
let dataFromModel;
try {
// First we try to parse a JSON with more relaxed rules
dataFromModel = jsonParse<IDataObject>(query, { acceptJSObject: true });
} catch (error) {
// In case of error,
// If model supplied a simple string instead of an object AND only one parameter expected, we try to recover the object structure
if (Object.keys(schema.shape).length === 1) {
const parameterName = Object.keys(schema.shape)[0];
dataFromModel = { [parameterName]: query };
} else {
// Finally throw an error if we were unable to parse the query
throw new NodeOperationError(
context.getNode(),
`Input is not a valid JSON: ${error.message}`,
);
}
}
// If we were able to parse the query with a fallback, we try to validate it using the schema
// Here we will throw an error if the data still does not match the schema
parsedQuery = schema.parse(dataFromModel);
}
try {
// Call tool function with parsed query
const result = await func(parsedQuery);
return result;
} catch (e) {
const { index } = context.addInputData(NodeConnectionType.AiTool, [[{ json: { query } }]]);
void context.addOutputData(NodeConnectionType.AiTool, index, e);
return e.toString();
}
};
return new DynamicTool({
name,
description: prepareFallbackToolDescription(description, schema),
func: wrappedFunc,
});
}
}