mirror of
https://github.com/n8n-io/n8n.git
synced 2025-02-21 02:56:40 -08:00
feat(HTTP Request Tool Node): Use DynamicStructuredTool with models supporting it (no-changelog) (#10246)
This commit is contained in:
parent
fa17391dbd
commit
a936680768
|
@ -38,7 +38,7 @@ export async function openAiFunctionsAgentExecute(
|
||||||
const memory = (await this.getInputConnectionData(NodeConnectionType.AiMemory, 0)) as
|
const memory = (await this.getInputConnectionData(NodeConnectionType.AiMemory, 0)) as
|
||||||
| BaseChatMemory
|
| BaseChatMemory
|
||||||
| undefined;
|
| undefined;
|
||||||
const tools = await getConnectedTools(this, nodeVersion >= 1.5);
|
const tools = await getConnectedTools(this, nodeVersion >= 1.5, false);
|
||||||
const outputParsers = await getOptionalOutputParsers(this);
|
const outputParsers = await getOptionalOutputParsers(this);
|
||||||
const options = this.getNodeParameter('options', 0, {}) as {
|
const options = this.getNodeParameter('options', 0, {}) as {
|
||||||
systemMessage?: string;
|
systemMessage?: string;
|
||||||
|
|
|
@ -90,7 +90,7 @@ export async function toolsAgentExecute(this: IExecuteFunctions): Promise<INodeE
|
||||||
| BaseChatMemory
|
| BaseChatMemory
|
||||||
| undefined;
|
| undefined;
|
||||||
|
|
||||||
const tools = (await getConnectedTools(this, true)) as Array<DynamicStructuredTool | Tool>;
|
const tools = (await getConnectedTools(this, true, false)) as Array<DynamicStructuredTool | Tool>;
|
||||||
const outputParser = (await getOptionalOutputParsers(this))?.[0];
|
const outputParser = (await getOptionalOutputParsers(this))?.[0];
|
||||||
let structuredOutputParserTool: DynamicStructuredTool | undefined;
|
let structuredOutputParserTool: DynamicStructuredTool | undefined;
|
||||||
|
|
||||||
|
|
|
@ -313,7 +313,7 @@ export class OpenAiAssistant implements INodeType {
|
||||||
|
|
||||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||||
const nodeVersion = this.getNode().typeVersion;
|
const nodeVersion = this.getNode().typeVersion;
|
||||||
const tools = await getConnectedTools(this, nodeVersion > 1);
|
const tools = await getConnectedTools(this, nodeVersion > 1, false);
|
||||||
const credentials = await this.getCredentials('openAiApi');
|
const credentials = await this.getCredentials('openAiApi');
|
||||||
|
|
||||||
const items = this.getInputData();
|
const items = this.getInputData();
|
||||||
|
|
|
@ -12,6 +12,7 @@ import { NodeConnectionType, NodeOperationError, tryToParseAlphanumericString }
|
||||||
import { DynamicTool } from '@langchain/core/tools';
|
import { DynamicTool } from '@langchain/core/tools';
|
||||||
import { getConnectionHintNoticeField } from '../../../utils/sharedFields';
|
import { getConnectionHintNoticeField } from '../../../utils/sharedFields';
|
||||||
|
|
||||||
|
import { N8nTool } from '../../../utils/N8nTool';
|
||||||
import {
|
import {
|
||||||
configureHttpRequestFunction,
|
configureHttpRequestFunction,
|
||||||
configureResponseOptimizer,
|
configureResponseOptimizer,
|
||||||
|
@ -19,6 +20,7 @@ import {
|
||||||
prepareToolDescription,
|
prepareToolDescription,
|
||||||
configureToolFunction,
|
configureToolFunction,
|
||||||
updateParametersAndOptions,
|
updateParametersAndOptions,
|
||||||
|
makeToolInputSchema,
|
||||||
} from './utils';
|
} from './utils';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -38,7 +40,7 @@ export class ToolHttpRequest implements INodeType {
|
||||||
name: 'toolHttpRequest',
|
name: 'toolHttpRequest',
|
||||||
icon: { light: 'file:httprequest.svg', dark: 'file:httprequest.dark.svg' },
|
icon: { light: 'file:httprequest.svg', dark: 'file:httprequest.dark.svg' },
|
||||||
group: ['output'],
|
group: ['output'],
|
||||||
version: 1,
|
version: [1, 1.1],
|
||||||
description: 'Makes an HTTP request and returns the response data',
|
description: 'Makes an HTTP request and returns the response data',
|
||||||
subtitle: '={{ $parameter.toolDescription }}',
|
subtitle: '={{ $parameter.toolDescription }}',
|
||||||
defaults: {
|
defaults: {
|
||||||
|
@ -394,9 +396,24 @@ export class ToolHttpRequest implements INodeType {
|
||||||
optimizeResponse,
|
optimizeResponse,
|
||||||
);
|
);
|
||||||
|
|
||||||
const description = prepareToolDescription(toolDescription, toolParameters);
|
let tool: DynamicTool | N8nTool;
|
||||||
|
|
||||||
const tool = new DynamicTool({ name, description, func });
|
// If the node version is 1.1 or higher, we use the N8nTool wrapper:
|
||||||
|
// it allows to use tool as a DynamicStructuredTool and have a fallback to DynamicTool
|
||||||
|
if (this.getNode().typeVersion >= 1.1) {
|
||||||
|
const schema = makeToolInputSchema(toolParameters);
|
||||||
|
|
||||||
|
tool = new N8nTool(this, {
|
||||||
|
name,
|
||||||
|
description: toolDescription,
|
||||||
|
func,
|
||||||
|
schema,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Keep the old behavior for nodes with version 1.0
|
||||||
|
const description = prepareToolDescription(toolDescription, toolParameters);
|
||||||
|
tool = new DynamicTool({ name, description, func });
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
response: tool,
|
response: tool,
|
||||||
|
|
|
@ -27,6 +27,8 @@ import type {
|
||||||
SendIn,
|
SendIn,
|
||||||
ToolParameter,
|
ToolParameter,
|
||||||
} from './interfaces';
|
} from './interfaces';
|
||||||
|
import type { DynamicZodObject } from '../../../types/zod.types';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
const genericCredentialRequest = async (ctx: IExecuteFunctions, itemIndex: number) => {
|
const genericCredentialRequest = async (ctx: IExecuteFunctions, itemIndex: number) => {
|
||||||
const genericType = ctx.getNodeParameter('genericAuthType', itemIndex) as string;
|
const genericType = ctx.getNodeParameter('genericAuthType', itemIndex) as string;
|
||||||
|
@ -566,7 +568,7 @@ export const configureToolFunction = (
|
||||||
httpRequest: (options: IHttpRequestOptions) => Promise<any>,
|
httpRequest: (options: IHttpRequestOptions) => Promise<any>,
|
||||||
optimizeResponse: (response: string) => string,
|
optimizeResponse: (response: string) => string,
|
||||||
) => {
|
) => {
|
||||||
return async (query: string): Promise<string> => {
|
return async (query: string | IDataObject): Promise<string> => {
|
||||||
const { index } = ctx.addInputData(NodeConnectionType.AiTool, [[{ json: { query } }]]);
|
const { index } = ctx.addInputData(NodeConnectionType.AiTool, [[{ json: { query } }]]);
|
||||||
|
|
||||||
let response: string = '';
|
let response: string = '';
|
||||||
|
@ -581,6 +583,7 @@ export const configureToolFunction = (
|
||||||
if (query) {
|
if (query) {
|
||||||
let dataFromModel;
|
let dataFromModel;
|
||||||
|
|
||||||
|
if (typeof query === 'string') {
|
||||||
try {
|
try {
|
||||||
dataFromModel = jsonParse<IDataObject>(query);
|
dataFromModel = jsonParse<IDataObject>(query);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -594,6 +597,9 @@ export const configureToolFunction = (
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
dataFromModel = query;
|
||||||
|
}
|
||||||
|
|
||||||
for (const parameter of toolParameters) {
|
for (const parameter of toolParameters) {
|
||||||
if (
|
if (
|
||||||
|
@ -727,6 +733,8 @@ export const configureToolFunction = (
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
|
||||||
const errorMessage = 'Input provided by model is not valid';
|
const errorMessage = 'Input provided by model is not valid';
|
||||||
|
|
||||||
if (error instanceof NodeOperationError) {
|
if (error instanceof NodeOperationError) {
|
||||||
|
@ -765,3 +773,38 @@ export const configureToolFunction = (
|
||||||
return response;
|
return response;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function makeParameterZodSchema(parameter: ToolParameter) {
|
||||||
|
let schema: z.ZodTypeAny;
|
||||||
|
|
||||||
|
if (parameter.type === 'string') {
|
||||||
|
schema = z.string();
|
||||||
|
} else if (parameter.type === 'number') {
|
||||||
|
schema = z.number();
|
||||||
|
} else if (parameter.type === 'boolean') {
|
||||||
|
schema = z.boolean();
|
||||||
|
} else if (parameter.type === 'json') {
|
||||||
|
schema = z.record(z.any());
|
||||||
|
} else {
|
||||||
|
schema = z.string();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!parameter.required) {
|
||||||
|
schema = schema.optional();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parameter.description) {
|
||||||
|
schema = schema.describe(parameter.description);
|
||||||
|
}
|
||||||
|
|
||||||
|
return schema;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeToolInputSchema(parameters: ToolParameter[]): DynamicZodObject {
|
||||||
|
const schemaEntries = parameters.map((parameter) => [
|
||||||
|
parameter.name,
|
||||||
|
makeParameterZodSchema(parameter),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return z.object(Object.fromEntries(schemaEntries));
|
||||||
|
}
|
||||||
|
|
|
@ -493,7 +493,7 @@ export class ToolWorkflow implements INodeType {
|
||||||
if (useSchema) {
|
if (useSchema) {
|
||||||
try {
|
try {
|
||||||
// We initialize these even though one of them will always be empty
|
// We initialize these even though one of them will always be empty
|
||||||
// it makes it easer to navigate the ternary operator
|
// it makes it easier to navigate the ternary operator
|
||||||
const jsonExample = this.getNodeParameter('jsonSchemaExample', itemIndex, '') as string;
|
const jsonExample = this.getNodeParameter('jsonSchemaExample', itemIndex, '') as string;
|
||||||
const inputSchema = this.getNodeParameter('inputSchema', itemIndex, '') as string;
|
const inputSchema = this.getNodeParameter('inputSchema', itemIndex, '') as string;
|
||||||
|
|
||||||
|
|
|
@ -163,7 +163,7 @@ export async function execute(this: IExecuteFunctions, i: number): Promise<INode
|
||||||
|
|
||||||
const agent = new OpenAIAssistantRunnable({ assistantId, client, asAgent: true });
|
const agent = new OpenAIAssistantRunnable({ assistantId, client, asAgent: true });
|
||||||
|
|
||||||
const tools = await getConnectedTools(this, nodeVersion > 1);
|
const tools = await getConnectedTools(this, nodeVersion > 1, false);
|
||||||
let assistantTools;
|
let assistantTools;
|
||||||
|
|
||||||
if (tools.length) {
|
if (tools.length) {
|
||||||
|
|
|
@ -219,7 +219,7 @@ export async function execute(this: IExecuteFunctions, i: number): Promise<INode
|
||||||
|
|
||||||
if (hideTools !== 'hide') {
|
if (hideTools !== 'hide') {
|
||||||
const enforceUniqueNames = nodeVersion > 1;
|
const enforceUniqueNames = nodeVersion > 1;
|
||||||
externalTools = await getConnectedTools(this, enforceUniqueNames);
|
externalTools = await getConnectedTools(this, enforceUniqueNames, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (externalTools.length) {
|
if (externalTools.length) {
|
||||||
|
|
169
packages/@n8n/nodes-langchain/utils/N8nTool.test.ts
Normal file
169
packages/@n8n/nodes-langchain/utils/N8nTool.test.ts
Normal file
|
@ -0,0 +1,169 @@
|
||||||
|
import { N8nTool } from './N8nTool';
|
||||||
|
import { createMockExecuteFunction } from 'n8n-nodes-base/test/nodes/Helpers';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import type { INode } from 'n8n-workflow';
|
||||||
|
import { DynamicStructuredTool, DynamicTool } from '@langchain/core/tools';
|
||||||
|
|
||||||
|
const mockNode: INode = {
|
||||||
|
id: '1',
|
||||||
|
name: 'Mock node',
|
||||||
|
typeVersion: 2,
|
||||||
|
type: 'n8n-nodes-base.mock',
|
||||||
|
position: [60, 760],
|
||||||
|
parameters: {
|
||||||
|
operation: 'test',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Test N8nTool wrapper as DynamicStructuredTool', () => {
|
||||||
|
it('should wrap a tool', () => {
|
||||||
|
const func = jest.fn();
|
||||||
|
|
||||||
|
const ctx = createMockExecuteFunction({}, mockNode);
|
||||||
|
|
||||||
|
const tool = new N8nTool(ctx, {
|
||||||
|
name: 'Dummy Tool',
|
||||||
|
description: 'A dummy tool for testing',
|
||||||
|
func,
|
||||||
|
schema: z.object({
|
||||||
|
foo: z.string(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(tool).toBeInstanceOf(DynamicStructuredTool);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Test N8nTool wrapper - DynamicTool fallback', () => {
|
||||||
|
it('should convert the tool to a dynamic tool', () => {
|
||||||
|
const func = jest.fn();
|
||||||
|
|
||||||
|
const ctx = createMockExecuteFunction({}, mockNode);
|
||||||
|
|
||||||
|
const tool = new N8nTool(ctx, {
|
||||||
|
name: 'Dummy Tool',
|
||||||
|
description: 'A dummy tool for testing',
|
||||||
|
func,
|
||||||
|
schema: z.object({
|
||||||
|
foo: z.string(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const dynamicTool = tool.asDynamicTool();
|
||||||
|
|
||||||
|
expect(dynamicTool).toBeInstanceOf(DynamicTool);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should format fallback description correctly', () => {
|
||||||
|
const func = jest.fn();
|
||||||
|
|
||||||
|
const ctx = createMockExecuteFunction({}, mockNode);
|
||||||
|
|
||||||
|
const tool = new N8nTool(ctx, {
|
||||||
|
name: 'Dummy Tool',
|
||||||
|
description: 'A dummy tool for testing',
|
||||||
|
func,
|
||||||
|
schema: z.object({
|
||||||
|
foo: z.string(),
|
||||||
|
bar: z.number().optional(),
|
||||||
|
qwe: z.boolean().describe('Boolean description'),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const dynamicTool = tool.asDynamicTool();
|
||||||
|
|
||||||
|
expect(dynamicTool.description).toContain('foo: (description: , type: string, required: true)');
|
||||||
|
expect(dynamicTool.description).toContain(
|
||||||
|
'bar: (description: , type: number, required: false)',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(dynamicTool.description).toContain(
|
||||||
|
'qwe: (description: Boolean description, type: boolean, required: true)',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty parameter list correctly', () => {
|
||||||
|
const func = jest.fn();
|
||||||
|
|
||||||
|
const ctx = createMockExecuteFunction({}, mockNode);
|
||||||
|
|
||||||
|
const tool = new N8nTool(ctx, {
|
||||||
|
name: 'Dummy Tool',
|
||||||
|
description: 'A dummy tool for testing',
|
||||||
|
func,
|
||||||
|
schema: z.object({}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const dynamicTool = tool.asDynamicTool();
|
||||||
|
|
||||||
|
expect(dynamicTool.description).toEqual('A dummy tool for testing');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse correct parameters', async () => {
|
||||||
|
const func = jest.fn();
|
||||||
|
|
||||||
|
const ctx = createMockExecuteFunction({}, mockNode);
|
||||||
|
|
||||||
|
const tool = new N8nTool(ctx, {
|
||||||
|
name: 'Dummy Tool',
|
||||||
|
description: 'A dummy tool for testing',
|
||||||
|
func,
|
||||||
|
schema: z.object({
|
||||||
|
foo: z.string().describe('Foo description'),
|
||||||
|
bar: z.number().optional(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const dynamicTool = tool.asDynamicTool();
|
||||||
|
|
||||||
|
const testParameters = { foo: 'some value' };
|
||||||
|
|
||||||
|
await dynamicTool.func(JSON.stringify(testParameters));
|
||||||
|
|
||||||
|
expect(func).toHaveBeenCalledWith(testParameters);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should recover when 1 parameter is passed directly', async () => {
|
||||||
|
const func = jest.fn();
|
||||||
|
|
||||||
|
const ctx = createMockExecuteFunction({}, mockNode);
|
||||||
|
|
||||||
|
const tool = new N8nTool(ctx, {
|
||||||
|
name: 'Dummy Tool',
|
||||||
|
description: 'A dummy tool for testing',
|
||||||
|
func,
|
||||||
|
schema: z.object({
|
||||||
|
foo: z.string().describe('Foo description'),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const dynamicTool = tool.asDynamicTool();
|
||||||
|
|
||||||
|
const testParameter = 'some value';
|
||||||
|
|
||||||
|
await dynamicTool.func(testParameter);
|
||||||
|
|
||||||
|
expect(func).toHaveBeenCalledWith({ foo: testParameter });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should recover when JS object is passed instead of JSON', async () => {
|
||||||
|
const func = jest.fn();
|
||||||
|
|
||||||
|
const ctx = createMockExecuteFunction({}, mockNode);
|
||||||
|
|
||||||
|
const tool = new N8nTool(ctx, {
|
||||||
|
name: 'Dummy Tool',
|
||||||
|
description: 'A dummy tool for testing',
|
||||||
|
func,
|
||||||
|
schema: z.object({
|
||||||
|
foo: z.string().describe('Foo description'),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const dynamicTool = tool.asDynamicTool();
|
||||||
|
|
||||||
|
await dynamicTool.func('{ foo: "some value" }');
|
||||||
|
|
||||||
|
expect(func).toHaveBeenCalledWith({ foo: 'some value' });
|
||||||
|
});
|
||||||
|
});
|
113
packages/@n8n/nodes-langchain/utils/N8nTool.ts
Normal file
113
packages/@n8n/nodes-langchain/utils/N8nTool.ts
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,17 +1,19 @@
|
||||||
import { NodeConnectionType, NodeOperationError, jsonStringify } from 'n8n-workflow';
|
|
||||||
import type {
|
import type {
|
||||||
EventNamesAiNodesType,
|
EventNamesAiNodesType,
|
||||||
IDataObject,
|
IDataObject,
|
||||||
IExecuteFunctions,
|
IExecuteFunctions,
|
||||||
IWebhookFunctions,
|
IWebhookFunctions,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
import { NodeConnectionType, NodeOperationError, jsonStringify } from 'n8n-workflow';
|
||||||
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
||||||
import type { BaseOutputParser } from '@langchain/core/output_parsers';
|
import type { BaseOutputParser } from '@langchain/core/output_parsers';
|
||||||
import type { BaseMessage } from '@langchain/core/messages';
|
import type { BaseMessage } from '@langchain/core/messages';
|
||||||
import { DynamicTool, type Tool } from '@langchain/core/tools';
|
import type { Tool } from '@langchain/core/tools';
|
||||||
import type { BaseLLM } from '@langchain/core/language_models/llms';
|
import type { BaseLLM } from '@langchain/core/language_models/llms';
|
||||||
import type { BaseChatMemory } from 'langchain/memory';
|
import type { BaseChatMemory } from 'langchain/memory';
|
||||||
import type { BaseChatMessageHistory } from '@langchain/core/chat_history';
|
import type { BaseChatMessageHistory } from '@langchain/core/chat_history';
|
||||||
|
import { N8nTool } from './N8nTool';
|
||||||
|
import { DynamicTool } from '@langchain/core/tools';
|
||||||
|
|
||||||
function hasMethods<T>(obj: unknown, ...methodNames: Array<string | symbol>): obj is T {
|
function hasMethods<T>(obj: unknown, ...methodNames: Array<string | symbol>): obj is T {
|
||||||
return methodNames.every(
|
return methodNames.every(
|
||||||
|
@ -178,7 +180,11 @@ export function serializeChatHistory(chatHistory: BaseMessage[]): string {
|
||||||
.join('\n');
|
.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getConnectedTools = async (ctx: IExecuteFunctions, enforceUniqueNames: boolean) => {
|
export const getConnectedTools = async (
|
||||||
|
ctx: IExecuteFunctions,
|
||||||
|
enforceUniqueNames: boolean,
|
||||||
|
convertStructuredTool: boolean = true,
|
||||||
|
) => {
|
||||||
const connectedTools =
|
const connectedTools =
|
||||||
((await ctx.getInputConnectionData(NodeConnectionType.AiTool, 0)) as Tool[]) || [];
|
((await ctx.getInputConnectionData(NodeConnectionType.AiTool, 0)) as Tool[]) || [];
|
||||||
|
|
||||||
|
@ -186,8 +192,10 @@ export const getConnectedTools = async (ctx: IExecuteFunctions, enforceUniqueNam
|
||||||
|
|
||||||
const seenNames = new Set<string>();
|
const seenNames = new Set<string>();
|
||||||
|
|
||||||
|
const finalTools = [];
|
||||||
|
|
||||||
for (const tool of connectedTools) {
|
for (const tool of connectedTools) {
|
||||||
if (!(tool instanceof DynamicTool)) continue;
|
if (!(tool instanceof DynamicTool) && !(tool instanceof N8nTool)) continue;
|
||||||
|
|
||||||
const { name } = tool;
|
const { name } = tool;
|
||||||
if (seenNames.has(name)) {
|
if (seenNames.has(name)) {
|
||||||
|
@ -197,7 +205,13 @@ export const getConnectedTools = async (ctx: IExecuteFunctions, enforceUniqueNam
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
seenNames.add(name);
|
seenNames.add(name);
|
||||||
|
|
||||||
|
if (convertStructuredTool && tool instanceof N8nTool) {
|
||||||
|
finalTools.push(tool.asDynamicTool());
|
||||||
|
} else {
|
||||||
|
finalTools.push(tool);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return connectedTools;
|
return finalTools;
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue