mirror of
https://github.com/n8n-io/n8n.git
synced 2026-06-02 01:37:07 +02:00
feat: Add execute function to tools (no-changelog) (#19997)
This commit is contained in:
parent
0789364838
commit
1291399b88
|
|
@ -0,0 +1,105 @@
|
|||
import { Calculator } from '@langchain/community/tools/calculator';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import type {
|
||||
IExecuteFunctions,
|
||||
INode,
|
||||
INodeExecutionData,
|
||||
ISupplyDataFunctions,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { ToolCalculator } from './ToolCalculator.node';
|
||||
|
||||
describe('ToolCalculator', () => {
|
||||
describe('supplyData', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should return Calculator tool instance', async () => {
|
||||
const node = new ToolCalculator();
|
||||
|
||||
const supplyDataResult = await node.supplyData.call(
|
||||
mock<ISupplyDataFunctions>({
|
||||
getNode: jest.fn(() => mock<INode>({ name: 'test calculator' })),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(supplyDataResult.response).toBeInstanceOf(Calculator);
|
||||
});
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should execute calculator and return result', async () => {
|
||||
const node = new ToolCalculator();
|
||||
const inputData: INodeExecutionData[] = [
|
||||
{
|
||||
json: { input: '2 + 2' },
|
||||
},
|
||||
];
|
||||
|
||||
const mockExecute = mock<IExecuteFunctions>({
|
||||
getInputData: jest.fn(() => inputData),
|
||||
getNode: jest.fn(() => mock<INode>({ name: 'test calculator' })),
|
||||
});
|
||||
|
||||
const result = await node.execute.call(mockExecute);
|
||||
|
||||
expect(result).toEqual([
|
||||
[
|
||||
{
|
||||
json: {
|
||||
response: '4',
|
||||
},
|
||||
pairedItem: {
|
||||
item: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle multiple input items', async () => {
|
||||
const node = new ToolCalculator();
|
||||
const inputData: INodeExecutionData[] = [
|
||||
{
|
||||
json: { input: '2 + 2' },
|
||||
},
|
||||
{
|
||||
json: { input: '5 * 3' },
|
||||
},
|
||||
];
|
||||
|
||||
const mockExecute = mock<IExecuteFunctions>({
|
||||
getInputData: jest.fn(() => inputData),
|
||||
getNode: jest.fn(() => mock<INode>({ name: 'test calculator' })),
|
||||
});
|
||||
|
||||
const result = await node.execute.call(mockExecute);
|
||||
|
||||
expect(result).toEqual([
|
||||
[
|
||||
{
|
||||
json: {
|
||||
response: '4',
|
||||
},
|
||||
pairedItem: {
|
||||
item: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
json: {
|
||||
response: '15',
|
||||
},
|
||||
pairedItem: {
|
||||
item: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
import { Calculator } from '@langchain/community/tools/calculator';
|
||||
import {
|
||||
type IExecuteFunctions,
|
||||
type INodeExecutionData,
|
||||
NodeConnectionTypes,
|
||||
type INodeType,
|
||||
type INodeTypeDescription,
|
||||
|
|
@ -10,6 +12,12 @@ import {
|
|||
import { logWrapper } from '@utils/logWrapper';
|
||||
import { getConnectionHintNoticeField } from '@utils/sharedFields';
|
||||
|
||||
function getTool(ctx: ISupplyDataFunctions | IExecuteFunctions): Calculator {
|
||||
const calculator = new Calculator();
|
||||
calculator.name = ctx.getNode().name;
|
||||
return calculator;
|
||||
}
|
||||
|
||||
export class ToolCalculator implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Calculator',
|
||||
|
|
@ -46,7 +54,27 @@ export class ToolCalculator implements INodeType {
|
|||
|
||||
async supplyData(this: ISupplyDataFunctions): Promise<SupplyData> {
|
||||
return {
|
||||
response: logWrapper(new Calculator(), this),
|
||||
response: logWrapper(getTool(this), this),
|
||||
};
|
||||
}
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const calculator = getTool(this);
|
||||
const input = this.getInputData();
|
||||
const response: INodeExecutionData[] = [];
|
||||
for (let i = 0; i < input.length; i++) {
|
||||
const inputItem = input[i];
|
||||
const result = await calculator.invoke(inputItem.json);
|
||||
response.push({
|
||||
json: {
|
||||
response: result,
|
||||
},
|
||||
pairedItem: {
|
||||
item: i,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return [response];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
import { mock } from 'jest-mock-extended';
|
||||
import { DynamicTool } from 'langchain/tools';
|
||||
import { type INode, type ISupplyDataFunctions } from 'n8n-workflow';
|
||||
import {
|
||||
type IExecuteFunctions,
|
||||
type INode,
|
||||
type INodeExecutionData,
|
||||
type ISupplyDataFunctions,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { ToolCode } from './ToolCode.node';
|
||||
|
||||
|
|
@ -78,4 +83,125 @@ describe('ToolCode', () => {
|
|||
expect(tool.func).toBeInstanceOf(Function);
|
||||
});
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should execute code tool and return result', async () => {
|
||||
const node = new ToolCode();
|
||||
const inputData: INodeExecutionData[] = [
|
||||
{
|
||||
json: { query: 'test query' },
|
||||
},
|
||||
];
|
||||
|
||||
const mockExecute = mock<IExecuteFunctions>({
|
||||
getInputData: jest.fn(() => inputData),
|
||||
getNode: jest.fn(() => mock<INode>({ typeVersion: 1.2, name: 'test tool' })),
|
||||
getNodeParameter: jest.fn().mockImplementation((paramName, _itemIndex) => {
|
||||
switch (paramName) {
|
||||
case 'description':
|
||||
return 'description text';
|
||||
case 'name':
|
||||
return 'wrong_field';
|
||||
case 'specifyInputSchema':
|
||||
return false;
|
||||
case 'language':
|
||||
return 'javaScript';
|
||||
case 'jsCode':
|
||||
return 'return "test result";';
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}),
|
||||
getMode: jest.fn(() => 'manual'),
|
||||
});
|
||||
|
||||
// Mock the DynamicTool.invoke method
|
||||
const mockResult = 'test result';
|
||||
DynamicTool.prototype.invoke = jest.fn().mockResolvedValue(mockResult);
|
||||
|
||||
const result = await node.execute.call(mockExecute);
|
||||
|
||||
expect(result).toEqual([
|
||||
[
|
||||
{
|
||||
json: {
|
||||
response: mockResult,
|
||||
},
|
||||
pairedItem: {
|
||||
item: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
]);
|
||||
expect(DynamicTool.prototype.invoke).toHaveBeenCalledWith({ query: 'test query' });
|
||||
});
|
||||
|
||||
it('should handle multiple input items', async () => {
|
||||
const node = new ToolCode();
|
||||
const inputData: INodeExecutionData[] = [
|
||||
{
|
||||
json: { query: 'first query' },
|
||||
},
|
||||
{
|
||||
json: { query: 'second query' },
|
||||
},
|
||||
];
|
||||
|
||||
const mockExecute = mock<IExecuteFunctions>({
|
||||
getInputData: jest.fn(() => inputData),
|
||||
getNode: jest.fn(() => mock<INode>({ typeVersion: 1.2, name: 'test tool' })),
|
||||
getNodeParameter: jest.fn().mockImplementation((paramName, _itemIndex) => {
|
||||
switch (paramName) {
|
||||
case 'description':
|
||||
return 'description text';
|
||||
case 'name':
|
||||
return 'wrong_field';
|
||||
case 'specifyInputSchema':
|
||||
return false;
|
||||
case 'language':
|
||||
return 'javaScript';
|
||||
case 'jsCode':
|
||||
return 'return "result for " + query;';
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}),
|
||||
getMode: jest.fn(() => 'manual'),
|
||||
});
|
||||
|
||||
// Mock the DynamicTool.invoke method
|
||||
DynamicTool.prototype.invoke = jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce('result for first query')
|
||||
.mockResolvedValueOnce('result for second query');
|
||||
|
||||
const result = await node.execute.call(mockExecute);
|
||||
|
||||
expect(result).toEqual([
|
||||
[
|
||||
{
|
||||
json: {
|
||||
response: 'result for first query',
|
||||
},
|
||||
pairedItem: {
|
||||
item: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
json: {
|
||||
response: 'result for second query',
|
||||
},
|
||||
pairedItem: {
|
||||
item: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
]);
|
||||
expect(DynamicTool.prototype.invoke).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -10,18 +10,20 @@ import { getSandboxContext } from 'n8n-nodes-base/dist/nodes/Code/Sandbox';
|
|||
import type {
|
||||
ExecutionError,
|
||||
IDataObject,
|
||||
IExecuteFunctions,
|
||||
INodeExecutionData,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
ISupplyDataFunctions,
|
||||
SupplyData,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
jsonParse,
|
||||
NodeConnectionTypes,
|
||||
nodeNameToToolName,
|
||||
NodeOperationError,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
buildInputSchemaField,
|
||||
buildJsonSchemaExampleField,
|
||||
|
|
@ -46,6 +48,154 @@ const jsonSchemaExampleNotice = buildJsonSchemaExampleNotice({
|
|||
|
||||
const jsonSchemaField = buildInputSchemaField({ showExtraProps: { specifyInputSchema: [true] } });
|
||||
|
||||
function getTool(
|
||||
ctx: ISupplyDataFunctions | IExecuteFunctions,
|
||||
itemIndex: number,
|
||||
log: boolean = true,
|
||||
) {
|
||||
const node = ctx.getNode();
|
||||
const workflowMode = ctx.getMode();
|
||||
|
||||
const runnersConfig = Container.get(TaskRunnersConfig);
|
||||
const isRunnerEnabled = runnersConfig.enabled;
|
||||
|
||||
const { typeVersion } = node;
|
||||
const name =
|
||||
typeVersion <= 1.1
|
||||
? (ctx.getNodeParameter('name', itemIndex) as string)
|
||||
: nodeNameToToolName(node);
|
||||
|
||||
const description = ctx.getNodeParameter('description', itemIndex) as string;
|
||||
|
||||
const useSchema = ctx.getNodeParameter('specifyInputSchema', itemIndex) as boolean;
|
||||
|
||||
const language = ctx.getNodeParameter('language', itemIndex) as string;
|
||||
let code = '';
|
||||
if (language === 'javaScript') {
|
||||
code = ctx.getNodeParameter('jsCode', itemIndex) as string;
|
||||
} else {
|
||||
code = ctx.getNodeParameter('pythonCode', itemIndex) as string;
|
||||
}
|
||||
|
||||
// @deprecated - TODO: Remove this after a new python runner is implemented
|
||||
const getSandbox = (query: string | IDataObject, index = 0) => {
|
||||
const context = getSandboxContext.call(ctx, index);
|
||||
context.query = query;
|
||||
|
||||
let sandbox: Sandbox;
|
||||
if (language === 'javaScript') {
|
||||
sandbox = new JavaScriptSandbox(context, code, ctx.helpers);
|
||||
} else {
|
||||
sandbox = new PythonSandbox(context, code, ctx.helpers);
|
||||
}
|
||||
|
||||
sandbox.on(
|
||||
'output',
|
||||
workflowMode === 'manual'
|
||||
? ctx.sendMessageToUI.bind(ctx)
|
||||
: (...args: unknown[]) =>
|
||||
console.log(`[Workflow "${ctx.getWorkflow().id}"][Node "${node.name}"]`, ...args),
|
||||
);
|
||||
return sandbox;
|
||||
};
|
||||
|
||||
const runFunction = async (query: string | IDataObject): Promise<unknown> => {
|
||||
if (language === 'javaScript' && isRunnerEnabled) {
|
||||
const sandbox = new JsTaskRunnerSandbox(
|
||||
code,
|
||||
'runOnceForAllItems',
|
||||
workflowMode,
|
||||
ctx,
|
||||
undefined,
|
||||
{
|
||||
query,
|
||||
},
|
||||
);
|
||||
const executionData = await sandbox.runCodeForTool();
|
||||
return executionData;
|
||||
} else {
|
||||
// use old vm2-based sandbox for python or when without runner enabled
|
||||
const sandbox = getSandbox(query, itemIndex);
|
||||
return await sandbox.runCode<string>();
|
||||
}
|
||||
};
|
||||
|
||||
const toolHandler = async (query: string | IDataObject): Promise<string> => {
|
||||
const { index } = log
|
||||
? ctx.addInputData(NodeConnectionTypes.AiTool, [[{ json: { query } }]])
|
||||
: { index: 0 };
|
||||
|
||||
let response: any = '';
|
||||
let executionError: ExecutionError | undefined;
|
||||
try {
|
||||
response = await runFunction(query);
|
||||
} catch (error: unknown) {
|
||||
executionError = new NodeOperationError(ctx.getNode(), error as ExecutionError);
|
||||
response = `There was an error: "${executionError.message}"`;
|
||||
}
|
||||
|
||||
if (typeof response === 'number') {
|
||||
response = (response as number).toString();
|
||||
}
|
||||
|
||||
if (typeof response !== 'string') {
|
||||
// TODO: Do some more testing. Issues here should actually fail the workflow
|
||||
executionError = new NodeOperationError(ctx.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 && log) {
|
||||
void ctx.addOutputData(NodeConnectionTypes.AiTool, index, executionError);
|
||||
} else if (log) {
|
||||
void ctx.addOutputData(NodeConnectionTypes.AiTool, index, [[{ json: { response } }]]);
|
||||
}
|
||||
|
||||
return response;
|
||||
};
|
||||
|
||||
const commonToolOptions = {
|
||||
name,
|
||||
description,
|
||||
func: toolHandler,
|
||||
};
|
||||
|
||||
let tool: DynamicTool | DynamicStructuredTool | undefined = undefined;
|
||||
|
||||
if (useSchema) {
|
||||
try {
|
||||
// We initialize these even though one of them will always be empty
|
||||
// it makes it easier to navigate the ternary operator
|
||||
const jsonExample = ctx.getNodeParameter('jsonSchemaExample', itemIndex, '') as string;
|
||||
const inputSchema = ctx.getNodeParameter('inputSchema', itemIndex, '') as string;
|
||||
|
||||
const schemaType = ctx.getNodeParameter('schemaType', itemIndex) as 'fromJson' | 'manual';
|
||||
|
||||
const jsonSchema =
|
||||
schemaType === 'fromJson'
|
||||
? generateSchemaFromExample(jsonExample, ctx.getNode().typeVersion >= 1.3)
|
||||
: jsonParse<JSONSchema7>(inputSchema);
|
||||
|
||||
const zodSchema = convertJsonSchemaToZod<DynamicZodObject>(jsonSchema);
|
||||
|
||||
tool = new DynamicStructuredTool({
|
||||
schema: zodSchema,
|
||||
...commonToolOptions,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new NodeOperationError(
|
||||
ctx.getNode(),
|
||||
'Error during parsing of JSON Schema. \n ' + error,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
tool = new DynamicTool(commonToolOptions);
|
||||
}
|
||||
|
||||
return tool;
|
||||
}
|
||||
|
||||
export class ToolCode implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Code Tool',
|
||||
|
|
@ -200,146 +350,26 @@ export class ToolCode implements INodeType {
|
|||
};
|
||||
|
||||
async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
|
||||
const node = this.getNode();
|
||||
const workflowMode = this.getMode();
|
||||
|
||||
const runnersConfig = Container.get(TaskRunnersConfig);
|
||||
const isRunnerEnabled = runnersConfig.enabled;
|
||||
|
||||
const { typeVersion } = node;
|
||||
const name =
|
||||
typeVersion <= 1.1
|
||||
? (this.getNodeParameter('name', itemIndex) as string)
|
||||
: nodeNameToToolName(node);
|
||||
|
||||
const description = this.getNodeParameter('description', itemIndex) as string;
|
||||
|
||||
const useSchema = this.getNodeParameter('specifyInputSchema', itemIndex) as boolean;
|
||||
|
||||
const language = this.getNodeParameter('language', itemIndex) as string;
|
||||
let code = '';
|
||||
if (language === 'javaScript') {
|
||||
code = this.getNodeParameter('jsCode', itemIndex) as string;
|
||||
} else {
|
||||
code = this.getNodeParameter('pythonCode', itemIndex) as string;
|
||||
}
|
||||
|
||||
// @deprecated - TODO: Remove this after a new python runner is implemented
|
||||
const getSandbox = (query: string | IDataObject, index = 0) => {
|
||||
const context = getSandboxContext.call(this, index);
|
||||
context.query = query;
|
||||
|
||||
let sandbox: Sandbox;
|
||||
if (language === 'javaScript') {
|
||||
sandbox = new JavaScriptSandbox(context, code, this.helpers);
|
||||
} else {
|
||||
sandbox = new PythonSandbox(context, code, this.helpers);
|
||||
}
|
||||
|
||||
sandbox.on(
|
||||
'output',
|
||||
workflowMode === 'manual'
|
||||
? this.sendMessageToUI.bind(this)
|
||||
: (...args: unknown[]) =>
|
||||
console.log(`[Workflow "${this.getWorkflow().id}"][Node "${node.name}"]`, ...args),
|
||||
);
|
||||
return sandbox;
|
||||
};
|
||||
|
||||
const runFunction = async (query: string | IDataObject): Promise<unknown> => {
|
||||
if (language === 'javaScript' && isRunnerEnabled) {
|
||||
const sandbox = new JsTaskRunnerSandbox(
|
||||
code,
|
||||
'runOnceForAllItems',
|
||||
workflowMode,
|
||||
this,
|
||||
undefined,
|
||||
{
|
||||
query,
|
||||
},
|
||||
);
|
||||
const executionData = await sandbox.runCodeForTool();
|
||||
return executionData;
|
||||
} else {
|
||||
// use old vm2-based sandbox for python or when without runner enabled
|
||||
const sandbox = getSandbox(query, itemIndex);
|
||||
return await sandbox.runCode<string>();
|
||||
}
|
||||
};
|
||||
|
||||
const toolHandler = async (query: string | IDataObject): Promise<string> => {
|
||||
const { index } = this.addInputData(NodeConnectionTypes.AiTool, [[{ json: { query } }]]);
|
||||
|
||||
let response: any = '';
|
||||
let executionError: ExecutionError | undefined;
|
||||
try {
|
||||
response = await runFunction(query);
|
||||
} catch (error: unknown) {
|
||||
executionError = new NodeOperationError(this.getNode(), error as ExecutionError);
|
||||
response = `There was an error: "${executionError.message}"`;
|
||||
}
|
||||
|
||||
if (typeof response === 'number') {
|
||||
response = (response as number).toString();
|
||||
}
|
||||
|
||||
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(NodeConnectionTypes.AiTool, index, executionError);
|
||||
} else {
|
||||
void this.addOutputData(NodeConnectionTypes.AiTool, index, [[{ json: { response } }]]);
|
||||
}
|
||||
|
||||
return response;
|
||||
};
|
||||
|
||||
const commonToolOptions = {
|
||||
name,
|
||||
description,
|
||||
func: toolHandler,
|
||||
};
|
||||
|
||||
let tool: DynamicTool | DynamicStructuredTool | undefined = undefined;
|
||||
|
||||
if (useSchema) {
|
||||
try {
|
||||
// We initialize these even though one of them will always be empty
|
||||
// it makes it easier 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'
|
||||
? generateSchemaFromExample(jsonExample, this.getNode().typeVersion >= 1.3)
|
||||
: jsonParse<JSONSchema7>(inputSchema);
|
||||
|
||||
const zodSchema = convertJsonSchemaToZod<DynamicZodObject>(jsonSchema);
|
||||
|
||||
tool = new DynamicStructuredTool({
|
||||
schema: zodSchema,
|
||||
...commonToolOptions,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new NodeOperationError(
|
||||
this.getNode(),
|
||||
'Error during parsing of JSON Schema. \n ' + error,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
tool = new DynamicTool(commonToolOptions);
|
||||
}
|
||||
|
||||
return {
|
||||
response: tool,
|
||||
response: getTool(this, itemIndex),
|
||||
};
|
||||
}
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const result: INodeExecutionData[] = [];
|
||||
const input = this.getInputData();
|
||||
for (let i = 0; i < input.length; i++) {
|
||||
const item = input[i];
|
||||
const tool = getTool(this, i, false);
|
||||
result.push({
|
||||
json: {
|
||||
response: await tool.invoke(item.json),
|
||||
},
|
||||
pairedItem: {
|
||||
item: i,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return [result];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,150 @@
|
|||
import { SearxngSearch } from '@langchain/community/tools/searxng_search';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import type {
|
||||
IExecuteFunctions,
|
||||
INode,
|
||||
INodeExecutionData,
|
||||
ISupplyDataFunctions,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { ToolSearXng } from './ToolSearXng.node';
|
||||
|
||||
describe('ToolSearXng', () => {
|
||||
describe('supplyData', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should return SearXNG tool instance', async () => {
|
||||
const node = new ToolSearXng();
|
||||
|
||||
const supplyDataResult = await node.supplyData.call(
|
||||
mock<ISupplyDataFunctions>({
|
||||
getNode: jest.fn(() => mock<INode>({ name: 'test searxng' })),
|
||||
getCredentials: jest.fn().mockResolvedValue({ apiUrl: 'https://searx.example.com' }),
|
||||
getNodeParameter: jest.fn().mockReturnValue({}),
|
||||
}),
|
||||
0,
|
||||
);
|
||||
|
||||
expect(supplyDataResult.response).toBeInstanceOf(SearxngSearch);
|
||||
});
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should execute SearXNG search and return result', async () => {
|
||||
const node = new ToolSearXng();
|
||||
const inputData: INodeExecutionData[] = [
|
||||
{
|
||||
json: { query: 'artificial intelligence' },
|
||||
},
|
||||
];
|
||||
|
||||
const mockExecute = mock<IExecuteFunctions>({
|
||||
getInputData: jest.fn(() => inputData),
|
||||
getNode: jest.fn(() => mock<INode>({ name: 'test searxng' })),
|
||||
getCredentials: jest.fn().mockResolvedValue({ apiUrl: 'https://searx.example.com' }),
|
||||
getNodeParameter: jest.fn().mockReturnValue({}),
|
||||
});
|
||||
|
||||
// Mock the SearxngSearch.invoke method
|
||||
const mockResult = 'Search results for artificial intelligence...';
|
||||
SearxngSearch.prototype.invoke = jest.fn().mockResolvedValue(mockResult);
|
||||
|
||||
const result = await node.execute.call(mockExecute);
|
||||
|
||||
expect(result).toEqual([
|
||||
[
|
||||
{
|
||||
json: {
|
||||
response: mockResult,
|
||||
},
|
||||
pairedItem: {
|
||||
item: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
]);
|
||||
expect(SearxngSearch.prototype.invoke).toHaveBeenCalledWith({
|
||||
query: 'artificial intelligence',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle multiple input items', async () => {
|
||||
const node = new ToolSearXng();
|
||||
const inputData: INodeExecutionData[] = [
|
||||
{
|
||||
json: { query: 'machine learning' },
|
||||
},
|
||||
{
|
||||
json: { query: 'deep learning' },
|
||||
},
|
||||
];
|
||||
|
||||
const mockExecute = mock<IExecuteFunctions>({
|
||||
getInputData: jest.fn(() => inputData),
|
||||
getNode: jest.fn(() => mock<INode>({ name: 'test searxng' })),
|
||||
getCredentials: jest.fn().mockResolvedValue({ apiUrl: 'https://searx.example.com' }),
|
||||
getNodeParameter: jest.fn().mockReturnValue({}),
|
||||
});
|
||||
|
||||
// Mock the SearxngSearch.invoke method
|
||||
SearxngSearch.prototype.invoke = jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce('Machine learning search results')
|
||||
.mockResolvedValueOnce('Deep learning search results');
|
||||
|
||||
const result = await node.execute.call(mockExecute);
|
||||
|
||||
expect(result).toEqual([
|
||||
[
|
||||
{
|
||||
json: {
|
||||
response: 'Machine learning search results',
|
||||
},
|
||||
pairedItem: {
|
||||
item: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
json: {
|
||||
response: 'Deep learning search results',
|
||||
},
|
||||
pairedItem: {
|
||||
item: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
]);
|
||||
expect(SearxngSearch.prototype.invoke).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should handle credentials and options correctly', async () => {
|
||||
const node = new ToolSearXng();
|
||||
const inputData: INodeExecutionData[] = [
|
||||
{
|
||||
json: { query: 'test query' },
|
||||
},
|
||||
];
|
||||
|
||||
const testOptions = { engines: ['google'], safesearch: 1 };
|
||||
const mockExecute = mock<IExecuteFunctions>({
|
||||
getInputData: jest.fn(() => inputData),
|
||||
getNode: jest.fn(() => mock<INode>({ name: 'test searxng' })),
|
||||
getCredentials: jest.fn().mockResolvedValue({ apiUrl: 'https://searx.test.com' }),
|
||||
getNodeParameter: jest.fn().mockReturnValue(testOptions),
|
||||
});
|
||||
|
||||
SearxngSearch.prototype.invoke = jest.fn().mockResolvedValue('test result');
|
||||
|
||||
await node.execute.call(mockExecute);
|
||||
|
||||
expect(mockExecute.getCredentials).toHaveBeenCalledWith('searXngApi');
|
||||
expect(mockExecute.getNodeParameter).toHaveBeenCalledWith('options', 0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
import { SearxngSearch } from '@langchain/community/tools/searxng_search';
|
||||
import { NodeConnectionTypes } from 'n8n-workflow';
|
||||
import type {
|
||||
IExecuteFunctions,
|
||||
INodeExecutionData,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
ISupplyDataFunctions,
|
||||
|
|
@ -17,6 +19,19 @@ type Options = {
|
|||
safesearch: 0 | 1 | 2;
|
||||
};
|
||||
|
||||
async function getTool(ctx: ISupplyDataFunctions | IExecuteFunctions, itemIndex: number) {
|
||||
const credentials = await ctx.getCredentials<{ apiUrl: string }>('searXngApi');
|
||||
const options = ctx.getNodeParameter('options', itemIndex) as Options;
|
||||
|
||||
return new SearxngSearch({
|
||||
apiBase: credentials.apiUrl,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
},
|
||||
params: options,
|
||||
});
|
||||
}
|
||||
|
||||
export class ToolSearXng implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'SearXNG',
|
||||
|
|
@ -107,19 +122,27 @@ export class ToolSearXng implements INodeType {
|
|||
};
|
||||
|
||||
async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
|
||||
const credentials = await this.getCredentials<{ apiUrl: string }>('searXngApi');
|
||||
const options = this.getNodeParameter('options', itemIndex) as Options;
|
||||
|
||||
const tool = new SearxngSearch({
|
||||
apiBase: credentials.apiUrl,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
},
|
||||
params: options,
|
||||
});
|
||||
|
||||
return {
|
||||
response: logWrapper(tool, this),
|
||||
response: logWrapper(await getTool(this, itemIndex), this),
|
||||
};
|
||||
}
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const result: INodeExecutionData[] = [];
|
||||
const input = this.getInputData();
|
||||
for (let i = 0; i < input.length; i++) {
|
||||
const item = input[i];
|
||||
const tool = await getTool(this, i);
|
||||
result.push({
|
||||
json: {
|
||||
response: await tool.invoke(item.json),
|
||||
},
|
||||
pairedItem: {
|
||||
item: i,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return [result];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,148 @@
|
|||
import { SerpAPI } from '@langchain/community/tools/serpapi';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import type {
|
||||
IExecuteFunctions,
|
||||
INode,
|
||||
INodeExecutionData,
|
||||
ISupplyDataFunctions,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { ToolSerpApi } from './ToolSerpApi.node';
|
||||
|
||||
describe('ToolSerpApi', () => {
|
||||
describe('supplyData', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should return SerpAPI tool instance', async () => {
|
||||
const node = new ToolSerpApi();
|
||||
|
||||
const supplyDataResult = await node.supplyData.call(
|
||||
mock<ISupplyDataFunctions>({
|
||||
getNode: jest.fn(() => mock<INode>({ name: 'test serpapi' })),
|
||||
getCredentials: jest.fn().mockResolvedValue({ apiKey: 'test-api-key' }),
|
||||
getNodeParameter: jest.fn().mockReturnValue({}),
|
||||
}),
|
||||
0,
|
||||
);
|
||||
|
||||
expect(supplyDataResult.response).toBeInstanceOf(SerpAPI);
|
||||
});
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should execute SerpAPI search and return result', async () => {
|
||||
const node = new ToolSerpApi();
|
||||
const inputData: INodeExecutionData[] = [
|
||||
{
|
||||
json: { query: 'artificial intelligence news' },
|
||||
},
|
||||
];
|
||||
|
||||
const mockExecute = mock<IExecuteFunctions>({
|
||||
getInputData: jest.fn(() => inputData),
|
||||
getNode: jest.fn(() => mock<INode>({ name: 'test serpapi' })),
|
||||
getCredentials: jest.fn().mockResolvedValue({ apiKey: 'test-api-key' }),
|
||||
getNodeParameter: jest.fn().mockReturnValue({}),
|
||||
});
|
||||
|
||||
// Mock the SerpAPI.invoke method
|
||||
const mockResult = 'Latest news about artificial intelligence...';
|
||||
SerpAPI.prototype.invoke = jest.fn().mockResolvedValue(mockResult);
|
||||
|
||||
const result = await node.execute.call(mockExecute);
|
||||
|
||||
expect(result).toEqual([
|
||||
[
|
||||
{
|
||||
json: {
|
||||
response: mockResult,
|
||||
},
|
||||
pairedItem: {
|
||||
item: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
]);
|
||||
expect(SerpAPI.prototype.invoke).toHaveBeenCalledWith(inputData[0]);
|
||||
});
|
||||
|
||||
it('should handle multiple input items', async () => {
|
||||
const node = new ToolSerpApi();
|
||||
const inputData: INodeExecutionData[] = [
|
||||
{
|
||||
json: { query: 'machine learning' },
|
||||
},
|
||||
{
|
||||
json: { query: 'deep learning' },
|
||||
},
|
||||
];
|
||||
|
||||
const mockExecute = mock<IExecuteFunctions>({
|
||||
getInputData: jest.fn(() => inputData),
|
||||
getNode: jest.fn(() => mock<INode>({ name: 'test serpapi' })),
|
||||
getCredentials: jest.fn().mockResolvedValue({ apiKey: 'test-api-key' }),
|
||||
getNodeParameter: jest.fn().mockReturnValue({}),
|
||||
});
|
||||
|
||||
// Mock the SerpAPI.invoke method
|
||||
SerpAPI.prototype.invoke = jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce('Machine learning search results')
|
||||
.mockResolvedValueOnce('Deep learning search results');
|
||||
|
||||
const result = await node.execute.call(mockExecute);
|
||||
|
||||
expect(result).toEqual([
|
||||
[
|
||||
{
|
||||
json: {
|
||||
response: 'Machine learning search results',
|
||||
},
|
||||
pairedItem: {
|
||||
item: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
json: {
|
||||
response: 'Deep learning search results',
|
||||
},
|
||||
pairedItem: {
|
||||
item: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
]);
|
||||
expect(SerpAPI.prototype.invoke).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should handle credentials and options correctly', async () => {
|
||||
const node = new ToolSerpApi();
|
||||
const inputData: INodeExecutionData[] = [
|
||||
{
|
||||
json: { query: 'test query' },
|
||||
},
|
||||
];
|
||||
|
||||
const testOptions = { engine: 'google', location: 'US' };
|
||||
const mockExecute = mock<IExecuteFunctions>({
|
||||
getInputData: jest.fn(() => inputData),
|
||||
getNode: jest.fn(() => mock<INode>({ name: 'test serpapi' })),
|
||||
getCredentials: jest.fn().mockResolvedValue({ apiKey: 'secret-api-key' }),
|
||||
getNodeParameter: jest.fn().mockReturnValue(testOptions),
|
||||
});
|
||||
|
||||
SerpAPI.prototype.invoke = jest.fn().mockResolvedValue('test result');
|
||||
|
||||
await node.execute.call(mockExecute);
|
||||
|
||||
expect(mockExecute.getCredentials).toHaveBeenCalledWith('serpApi');
|
||||
expect(mockExecute.getNodeParameter).toHaveBeenCalledWith('options', 0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,14 +1,23 @@
|
|||
import { SerpAPI } from '@langchain/community/tools/serpapi';
|
||||
import {
|
||||
type IExecuteFunctions,
|
||||
NodeConnectionTypes,
|
||||
type INodeType,
|
||||
type INodeTypeDescription,
|
||||
type ISupplyDataFunctions,
|
||||
type SupplyData,
|
||||
type INodeExecutionData,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { logWrapper } from '@utils/logWrapper';
|
||||
import { getConnectionHintNoticeField } from '@utils/sharedFields';
|
||||
async function getTool(ctx: ISupplyDataFunctions | IExecuteFunctions, itemIndex: number) {
|
||||
const credentials = await ctx.getCredentials('serpApi');
|
||||
|
||||
const options = ctx.getNodeParameter('options', itemIndex) as object;
|
||||
|
||||
return new SerpAPI(credentials.apiKey as string, options);
|
||||
}
|
||||
|
||||
export class ToolSerpApi implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
|
|
@ -114,12 +123,26 @@ export class ToolSerpApi implements INodeType {
|
|||
};
|
||||
|
||||
async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
|
||||
const credentials = await this.getCredentials('serpApi');
|
||||
|
||||
const options = this.getNodeParameter('options', itemIndex) as object;
|
||||
|
||||
return {
|
||||
response: logWrapper(new SerpAPI(credentials.apiKey as string, options), this),
|
||||
response: logWrapper(await getTool(this, itemIndex), this),
|
||||
};
|
||||
}
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const inputData = this.getInputData();
|
||||
const returnData: INodeExecutionData[] = [];
|
||||
for (let itemIndex = 0; itemIndex < inputData.length; itemIndex++) {
|
||||
const tool = await getTool(this, itemIndex);
|
||||
const query = inputData[itemIndex];
|
||||
const result = await tool.invoke(query);
|
||||
returnData.push({
|
||||
json: {
|
||||
response: result,
|
||||
},
|
||||
pairedItem: { item: itemIndex },
|
||||
});
|
||||
}
|
||||
|
||||
return [returnData];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,37 @@
|
|||
import { DynamicTool } from 'langchain/tools';
|
||||
import {
|
||||
type IExecuteFunctions,
|
||||
NodeConnectionTypes,
|
||||
nodeNameToToolName,
|
||||
type INodeType,
|
||||
type INodeTypeDescription,
|
||||
type ISupplyDataFunctions,
|
||||
type SupplyData,
|
||||
type INodeExecutionData,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { logWrapper } from '@utils/logWrapper';
|
||||
import { getConnectionHintNoticeField } from '@utils/sharedFields';
|
||||
|
||||
async function getTool(
|
||||
ctx: ISupplyDataFunctions | IExecuteFunctions,
|
||||
itemIndex: number,
|
||||
): Promise<DynamicTool> {
|
||||
const node = ctx.getNode();
|
||||
const { typeVersion } = node;
|
||||
|
||||
const name = typeVersion === 1 ? 'thinking_tool' : nodeNameToToolName(node);
|
||||
const description = ctx.getNodeParameter('description', itemIndex) as string;
|
||||
|
||||
return new DynamicTool({
|
||||
name,
|
||||
description,
|
||||
func: async (subject: string) => {
|
||||
return subject;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// A thinking tool, see https://www.anthropic.com/engineering/claude-think-tool
|
||||
|
||||
const defaultToolDescription =
|
||||
|
|
@ -63,22 +84,30 @@ export class ToolThink implements INodeType {
|
|||
};
|
||||
|
||||
async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
|
||||
const node = this.getNode();
|
||||
const { typeVersion } = node;
|
||||
|
||||
const name = typeVersion === 1 ? 'thinking_tool' : nodeNameToToolName(node);
|
||||
const description = this.getNodeParameter('description', itemIndex) as string;
|
||||
|
||||
const tool = new DynamicTool({
|
||||
name,
|
||||
description,
|
||||
func: async (subject: string) => {
|
||||
return subject;
|
||||
},
|
||||
});
|
||||
const tool = await getTool(this, itemIndex);
|
||||
|
||||
return {
|
||||
response: logWrapper(tool, this),
|
||||
};
|
||||
}
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const input = this.getInputData();
|
||||
const response: INodeExecutionData[] = [];
|
||||
for (let i = 0; i < input.length; i++) {
|
||||
const inputItem = input[i];
|
||||
const tool = await getTool(this, i);
|
||||
const result = await tool.invoke(inputItem.json);
|
||||
response.push({
|
||||
json: {
|
||||
response: result,
|
||||
},
|
||||
pairedItem: {
|
||||
item: i,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return [response];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
import { mock } from 'jest-mock-extended';
|
||||
import { DynamicTool } from 'langchain/tools';
|
||||
import type { ISupplyDataFunctions, INode } from 'n8n-workflow';
|
||||
import type {
|
||||
IExecuteFunctions,
|
||||
INodeExecutionData,
|
||||
ISupplyDataFunctions,
|
||||
INode,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { ToolThink } from '../ToolThink.node';
|
||||
|
||||
|
|
@ -59,4 +64,135 @@ describe('ToolThink', () => {
|
|||
expect(response.name).toEqual('My_Thinking_Tool');
|
||||
});
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should execute think tool and return input as result', async () => {
|
||||
const node = new ToolThink();
|
||||
const inputData: INodeExecutionData[] = [
|
||||
{
|
||||
json: { input: 'thinking about this problem' },
|
||||
},
|
||||
];
|
||||
|
||||
const mockExecute = mock<IExecuteFunctions>({
|
||||
getInputData: jest.fn(() => inputData),
|
||||
getNode: jest.fn(() => mock<INode>({ typeVersion: 1.1, name: 'test think tool' })),
|
||||
getNodeParameter: jest.fn().mockImplementation((paramName, _itemIndex) => {
|
||||
switch (paramName) {
|
||||
case 'description':
|
||||
return 'Tool for thinking';
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await node.execute.call(mockExecute);
|
||||
|
||||
expect(result).toEqual([
|
||||
[
|
||||
{
|
||||
json: {
|
||||
response: 'thinking about this problem',
|
||||
},
|
||||
pairedItem: {
|
||||
item: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle multiple input items', async () => {
|
||||
const node = new ToolThink();
|
||||
const inputData: INodeExecutionData[] = [
|
||||
{
|
||||
json: { input: 'first thought' },
|
||||
},
|
||||
{
|
||||
json: { input: 'second thought' },
|
||||
},
|
||||
];
|
||||
|
||||
const mockExecute = mock<IExecuteFunctions>({
|
||||
getInputData: jest.fn(() => inputData),
|
||||
getNode: jest.fn(() => mock<INode>({ typeVersion: 1.1, name: 'test think tool' })),
|
||||
getNodeParameter: jest.fn().mockImplementation((paramName, _itemIndex) => {
|
||||
switch (paramName) {
|
||||
case 'description':
|
||||
return 'Tool for thinking';
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await node.execute.call(mockExecute);
|
||||
|
||||
expect(result).toEqual([
|
||||
[
|
||||
{
|
||||
json: {
|
||||
response: 'first thought',
|
||||
},
|
||||
pairedItem: {
|
||||
item: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
json: {
|
||||
response: 'second thought',
|
||||
},
|
||||
pairedItem: {
|
||||
item: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should use hardcoded name for version 1', async () => {
|
||||
const node = new ToolThink();
|
||||
const inputData: INodeExecutionData[] = [
|
||||
{
|
||||
json: { input: 'test' },
|
||||
},
|
||||
];
|
||||
|
||||
const mockExecute = mock<IExecuteFunctions>({
|
||||
getInputData: jest.fn(() => inputData),
|
||||
getNode: jest.fn(() => mock<INode>({ typeVersion: 1, name: 'My Thinking Tool' })),
|
||||
getNodeParameter: jest.fn().mockImplementation((paramName, _itemIndex) => {
|
||||
switch (paramName) {
|
||||
case 'description':
|
||||
return 'Tool for thinking';
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await node.execute.call(mockExecute);
|
||||
|
||||
expect(result).toEqual([
|
||||
[
|
||||
{
|
||||
json: {
|
||||
response: 'test',
|
||||
},
|
||||
pairedItem: {
|
||||
item: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
]);
|
||||
// The tool should be created with the hardcoded name for version 1
|
||||
// This is tested indirectly through the getTool function usage
|
||||
expect(mockExecute.getNode).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,12 @@
|
|||
import { mock } from 'jest-mock-extended';
|
||||
import { VectorStoreQATool } from 'langchain/tools';
|
||||
import { NodeConnectionTypes, type INode, type ISupplyDataFunctions } from 'n8n-workflow';
|
||||
import {
|
||||
NodeConnectionTypes,
|
||||
type IExecuteFunctions,
|
||||
type INode,
|
||||
type INodeExecutionData,
|
||||
type ISupplyDataFunctions,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { ToolVectorStore } from './ToolVectorStore.node';
|
||||
|
||||
|
|
@ -88,4 +94,135 @@ describe('ToolVectorStore', () => {
|
|||
expect(tool.description).toContain('test_tool');
|
||||
});
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should execute vector store tool and return result', async () => {
|
||||
const node = new ToolVectorStore();
|
||||
const inputData: INodeExecutionData[] = [
|
||||
{
|
||||
json: { query: 'test question' },
|
||||
},
|
||||
];
|
||||
|
||||
const mockExecute = mock<IExecuteFunctions>({
|
||||
getInputData: jest.fn(() => inputData),
|
||||
getNode: jest.fn(() => mock<INode>({ typeVersion: 1.2, name: 'test tool' })),
|
||||
getNodeParameter: jest.fn().mockImplementation((paramName, _itemIndex) => {
|
||||
switch (paramName) {
|
||||
case 'description':
|
||||
return 'test description';
|
||||
case 'topK':
|
||||
return 4;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}),
|
||||
getInputConnectionData: jest.fn().mockImplementation(async (inputName, _itemIndex) => {
|
||||
switch (inputName) {
|
||||
case NodeConnectionTypes.AiVectorStore:
|
||||
return jest.fn();
|
||||
case NodeConnectionTypes.AiLanguageModel:
|
||||
return {
|
||||
_modelType: jest.fn(),
|
||||
};
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}),
|
||||
});
|
||||
|
||||
// Mock the VectorStoreQATool.invoke method
|
||||
const mockResult = 'This is the answer from vector store';
|
||||
VectorStoreQATool.prototype.invoke = jest.fn().mockResolvedValue(mockResult);
|
||||
|
||||
const result = await node.execute.call(mockExecute);
|
||||
|
||||
expect(result).toEqual([
|
||||
[
|
||||
{
|
||||
json: {
|
||||
response: mockResult,
|
||||
},
|
||||
pairedItem: {
|
||||
item: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
]);
|
||||
expect(VectorStoreQATool.prototype.invoke).toHaveBeenCalledWith(inputData[0].json);
|
||||
});
|
||||
|
||||
it('should handle multiple input items', async () => {
|
||||
const node = new ToolVectorStore();
|
||||
const inputData: INodeExecutionData[] = [
|
||||
{
|
||||
json: { query: 'first question' },
|
||||
},
|
||||
{
|
||||
json: { query: 'second question' },
|
||||
},
|
||||
];
|
||||
|
||||
const mockExecute = mock<IExecuteFunctions>({
|
||||
getInputData: jest.fn(() => inputData),
|
||||
getNode: jest.fn(() => mock<INode>({ typeVersion: 1.2, name: 'test tool' })),
|
||||
getNodeParameter: jest.fn().mockImplementation((paramName, _itemIndex) => {
|
||||
switch (paramName) {
|
||||
case 'description':
|
||||
return 'test description';
|
||||
case 'topK':
|
||||
return 4;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}),
|
||||
getInputConnectionData: jest.fn().mockImplementation(async (inputName, _itemIndex) => {
|
||||
switch (inputName) {
|
||||
case NodeConnectionTypes.AiVectorStore:
|
||||
return jest.fn();
|
||||
case NodeConnectionTypes.AiLanguageModel:
|
||||
return {
|
||||
_modelType: jest.fn(),
|
||||
};
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}),
|
||||
});
|
||||
|
||||
// Mock the VectorStoreQATool.invoke method
|
||||
VectorStoreQATool.prototype.invoke = jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce('Answer to first question')
|
||||
.mockResolvedValueOnce('Answer to second question');
|
||||
|
||||
const result = await node.execute.call(mockExecute);
|
||||
|
||||
expect(result).toEqual([
|
||||
[
|
||||
{
|
||||
json: {
|
||||
response: 'Answer to first question',
|
||||
},
|
||||
pairedItem: {
|
||||
item: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
json: {
|
||||
response: 'Answer to second question',
|
||||
},
|
||||
pairedItem: {
|
||||
item: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
]);
|
||||
expect(VectorStoreQATool.prototype.invoke).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import type { VectorStore } from '@langchain/core/vectorstores';
|
|||
import { VectorDBQAChain } from 'langchain/chains';
|
||||
import { VectorStoreQATool } from 'langchain/tools';
|
||||
import type {
|
||||
IExecuteFunctions,
|
||||
INodeExecutionData,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
ISupplyDataFunctions,
|
||||
|
|
@ -13,6 +15,40 @@ import { NodeConnectionTypes, nodeNameToToolName } from 'n8n-workflow';
|
|||
import { logWrapper } from '@utils/logWrapper';
|
||||
import { getConnectionHintNoticeField } from '@utils/sharedFields';
|
||||
|
||||
async function getTool(
|
||||
ctx: ISupplyDataFunctions | IExecuteFunctions,
|
||||
itemIndex: number,
|
||||
): Promise<VectorStoreQATool> {
|
||||
const node = ctx.getNode();
|
||||
const { typeVersion } = node;
|
||||
const name =
|
||||
typeVersion <= 1
|
||||
? (ctx.getNodeParameter('name', itemIndex) as string)
|
||||
: nodeNameToToolName(node);
|
||||
const toolDescription = ctx.getNodeParameter('description', itemIndex) as string;
|
||||
const topK = ctx.getNodeParameter('topK', itemIndex, 4) as number;
|
||||
const description = VectorStoreQATool.getDescription(name, toolDescription);
|
||||
const vectorStore = (await ctx.getInputConnectionData(
|
||||
NodeConnectionTypes.AiVectorStore,
|
||||
itemIndex,
|
||||
)) as VectorStore;
|
||||
const llm = (await ctx.getInputConnectionData(
|
||||
NodeConnectionTypes.AiLanguageModel,
|
||||
itemIndex,
|
||||
)) as BaseLanguageModel;
|
||||
|
||||
const vectorStoreTool = new VectorStoreQATool(name, description, {
|
||||
llm,
|
||||
vectorStore,
|
||||
});
|
||||
|
||||
vectorStoreTool.chain = VectorDBQAChain.fromLLM(llm, vectorStore, {
|
||||
k: topK,
|
||||
});
|
||||
|
||||
return vectorStoreTool;
|
||||
}
|
||||
|
||||
export class ToolVectorStore implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Vector Store Question Answer Tool',
|
||||
|
|
@ -97,37 +133,29 @@ export class ToolVectorStore implements INodeType {
|
|||
};
|
||||
|
||||
async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
|
||||
const node = this.getNode();
|
||||
const { typeVersion } = node;
|
||||
const name =
|
||||
typeVersion <= 1
|
||||
? (this.getNodeParameter('name', itemIndex) as string)
|
||||
: nodeNameToToolName(node);
|
||||
const toolDescription = this.getNodeParameter('description', itemIndex) as string;
|
||||
const topK = this.getNodeParameter('topK', itemIndex, 4) as number;
|
||||
|
||||
const vectorStore = (await this.getInputConnectionData(
|
||||
NodeConnectionTypes.AiVectorStore,
|
||||
itemIndex,
|
||||
)) as VectorStore;
|
||||
|
||||
const llm = (await this.getInputConnectionData(
|
||||
NodeConnectionTypes.AiLanguageModel,
|
||||
0,
|
||||
)) as BaseLanguageModel;
|
||||
|
||||
const description = VectorStoreQATool.getDescription(name, toolDescription);
|
||||
const vectorStoreTool = new VectorStoreQATool(name, description, {
|
||||
llm,
|
||||
vectorStore,
|
||||
});
|
||||
|
||||
vectorStoreTool.chain = VectorDBQAChain.fromLLM(llm, vectorStore, {
|
||||
k: topK,
|
||||
});
|
||||
const vectorStoreTool = await getTool(this, itemIndex);
|
||||
|
||||
return {
|
||||
response: logWrapper(vectorStoreTool, this),
|
||||
};
|
||||
}
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const inputData = this.getInputData();
|
||||
const result: INodeExecutionData[] = [];
|
||||
for (let itemIndex = 0; itemIndex < inputData.length; itemIndex++) {
|
||||
const tool = await getTool(this, itemIndex);
|
||||
const outputData = await tool.invoke(inputData[itemIndex].json);
|
||||
result.push({
|
||||
json: {
|
||||
response: outputData,
|
||||
},
|
||||
pairedItem: {
|
||||
item: itemIndex,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return [result];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,154 @@
|
|||
import { WikipediaQueryRun } from '@langchain/community/tools/wikipedia_query_run';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import type {
|
||||
IExecuteFunctions,
|
||||
INode,
|
||||
INodeExecutionData,
|
||||
ISupplyDataFunctions,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { ToolWikipedia } from './ToolWikipedia.node';
|
||||
|
||||
describe('ToolWikipedia', () => {
|
||||
describe('supplyData', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should return Wikipedia tool instance', async () => {
|
||||
const node = new ToolWikipedia();
|
||||
|
||||
const supplyDataResult = await node.supplyData.call(
|
||||
mock<ISupplyDataFunctions>({
|
||||
getNode: jest.fn(() => mock<INode>({ name: 'test wikipedia' })),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(supplyDataResult.response).toBeInstanceOf(WikipediaQueryRun);
|
||||
});
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should execute wikipedia search and return result', async () => {
|
||||
const node = new ToolWikipedia();
|
||||
const inputData: INodeExecutionData[] = [
|
||||
{
|
||||
json: { query: 'artificial intelligence' },
|
||||
},
|
||||
];
|
||||
|
||||
const mockExecute = mock<IExecuteFunctions>({
|
||||
getInputData: jest.fn(() => inputData),
|
||||
getNode: jest.fn(() => mock<INode>({ name: 'test wikipedia' })),
|
||||
});
|
||||
|
||||
// Mock the WikipediaQueryRun.invoke method
|
||||
const mockResult = 'Artificial intelligence (AI) is intelligence demonstrated by machines...';
|
||||
WikipediaQueryRun.prototype.invoke = jest.fn().mockResolvedValue(mockResult);
|
||||
|
||||
const result = await node.execute.call(mockExecute);
|
||||
|
||||
expect(result).toEqual([
|
||||
[
|
||||
{
|
||||
json: {
|
||||
response: mockResult,
|
||||
},
|
||||
pairedItem: {
|
||||
item: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
]);
|
||||
expect(WikipediaQueryRun.prototype.invoke).toHaveBeenCalledWith({
|
||||
query: 'artificial intelligence',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle multiple input items', async () => {
|
||||
const node = new ToolWikipedia();
|
||||
const inputData: INodeExecutionData[] = [
|
||||
{
|
||||
json: { query: 'machine learning' },
|
||||
},
|
||||
{
|
||||
json: { query: 'deep learning' },
|
||||
},
|
||||
];
|
||||
|
||||
const mockExecute = mock<IExecuteFunctions>({
|
||||
getInputData: jest.fn(() => inputData),
|
||||
getNode: jest.fn(() => mock<INode>({ name: 'test wikipedia' })),
|
||||
});
|
||||
|
||||
// Mock the WikipediaQueryRun.invoke method
|
||||
WikipediaQueryRun.prototype.invoke = jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce('Machine learning (ML) is a field of artificial intelligence...')
|
||||
.mockResolvedValueOnce('Deep learning (also known as deep structured learning...');
|
||||
|
||||
const result = await node.execute.call(mockExecute);
|
||||
|
||||
expect(result).toEqual([
|
||||
[
|
||||
{
|
||||
json: {
|
||||
response: 'Machine learning (ML) is a field of artificial intelligence...',
|
||||
},
|
||||
pairedItem: {
|
||||
item: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
json: {
|
||||
response: 'Deep learning (also known as deep structured learning...',
|
||||
},
|
||||
pairedItem: {
|
||||
item: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
]);
|
||||
expect(WikipediaQueryRun.prototype.invoke).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should skip undefined items', async () => {
|
||||
const node = new ToolWikipedia();
|
||||
const inputData: INodeExecutionData[] = [
|
||||
{
|
||||
json: { query: 'test' },
|
||||
},
|
||||
];
|
||||
// Simulate undefined item by mocking getInputData to return array with undefined
|
||||
inputData.push(undefined as any);
|
||||
|
||||
const mockExecute = mock<IExecuteFunctions>({
|
||||
getInputData: jest.fn(() => inputData),
|
||||
getNode: jest.fn(() => mock<INode>({ name: 'test wikipedia' })),
|
||||
});
|
||||
|
||||
// Mock the WikipediaQueryRun.invoke method
|
||||
WikipediaQueryRun.prototype.invoke = jest.fn().mockResolvedValue('test result');
|
||||
|
||||
const result = await node.execute.call(mockExecute);
|
||||
|
||||
expect(result).toEqual([
|
||||
[
|
||||
{
|
||||
json: {
|
||||
response: 'test result',
|
||||
},
|
||||
pairedItem: {
|
||||
item: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
]);
|
||||
expect(WikipediaQueryRun.prototype.invoke).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,15 +1,25 @@
|
|||
import { WikipediaQueryRun } from '@langchain/community/tools/wikipedia_query_run';
|
||||
import {
|
||||
type IExecuteFunctions,
|
||||
NodeConnectionTypes,
|
||||
type INodeType,
|
||||
type INodeTypeDescription,
|
||||
type ISupplyDataFunctions,
|
||||
type SupplyData,
|
||||
type INodeExecutionData,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { logWrapper } from '@utils/logWrapper';
|
||||
import { getConnectionHintNoticeField } from '@utils/sharedFields';
|
||||
|
||||
function getTool(ctx: ISupplyDataFunctions | IExecuteFunctions): WikipediaQueryRun {
|
||||
const WikiTool = new WikipediaQueryRun();
|
||||
WikiTool.name = ctx.getNode().name;
|
||||
WikiTool.description =
|
||||
'A tool for interacting with and fetching data from the Wikipedia API. The input should always be a string query.';
|
||||
return WikiTool;
|
||||
}
|
||||
|
||||
export class ToolWikipedia implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Wikipedia',
|
||||
|
|
@ -44,13 +54,29 @@ export class ToolWikipedia implements INodeType {
|
|||
};
|
||||
|
||||
async supplyData(this: ISupplyDataFunctions): Promise<SupplyData> {
|
||||
const WikiTool = new WikipediaQueryRun();
|
||||
|
||||
WikiTool.description =
|
||||
'A tool for interacting with and fetching data from the Wikipedia API. The input should always be a string query.';
|
||||
|
||||
return {
|
||||
response: logWrapper(WikiTool, this),
|
||||
response: logWrapper(getTool(this), this),
|
||||
};
|
||||
}
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const WikiTool = getTool(this);
|
||||
|
||||
const items = this.getInputData();
|
||||
|
||||
const response: INodeExecutionData[] = [];
|
||||
for (let itemIndex = 0; itemIndex < this.getInputData().length; itemIndex++) {
|
||||
const item = items[itemIndex];
|
||||
if (item === undefined) {
|
||||
continue;
|
||||
}
|
||||
const result = await WikiTool.invoke(item.json);
|
||||
response.push({
|
||||
json: { response: result },
|
||||
pairedItem: { item: itemIndex },
|
||||
});
|
||||
}
|
||||
|
||||
return [response];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,141 @@
|
|||
import { WolframAlphaTool } from '@langchain/community/tools/wolframalpha';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import type {
|
||||
IExecuteFunctions,
|
||||
INode,
|
||||
INodeExecutionData,
|
||||
ISupplyDataFunctions,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { ToolWolframAlpha } from './ToolWolframAlpha.node';
|
||||
|
||||
describe('ToolWolframAlpha', () => {
|
||||
describe('supplyData', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should return WolframAlpha tool instance', async () => {
|
||||
const node = new ToolWolframAlpha();
|
||||
|
||||
const supplyDataResult = await node.supplyData.call(
|
||||
mock<ISupplyDataFunctions>({
|
||||
getNode: jest.fn(() => mock<INode>({ name: 'test wolfram' })),
|
||||
getCredentials: jest.fn().mockResolvedValue({ appId: 'test-app-id' }),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(supplyDataResult.response).toBeInstanceOf(WolframAlphaTool);
|
||||
});
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should execute WolframAlpha query and return result', async () => {
|
||||
const node = new ToolWolframAlpha();
|
||||
const inputData: INodeExecutionData[] = [
|
||||
{
|
||||
json: { query: 'what is 2+2?' },
|
||||
},
|
||||
];
|
||||
|
||||
const mockExecute = mock<IExecuteFunctions>({
|
||||
getInputData: jest.fn(() => inputData),
|
||||
getNode: jest.fn(() => mock<INode>({ name: 'test wolfram' })),
|
||||
getCredentials: jest.fn().mockResolvedValue({ appId: 'test-app-id' }),
|
||||
});
|
||||
|
||||
// Mock the WolframAlphaTool.invoke method
|
||||
const mockResult = '4';
|
||||
WolframAlphaTool.prototype.invoke = jest.fn().mockResolvedValue(mockResult);
|
||||
|
||||
const result = await node.execute.call(mockExecute);
|
||||
|
||||
expect(result).toEqual([
|
||||
[
|
||||
{
|
||||
json: {
|
||||
response: mockResult,
|
||||
},
|
||||
pairedItem: {
|
||||
item: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
]);
|
||||
expect(WolframAlphaTool.prototype.invoke).toHaveBeenCalledWith(inputData[0].json);
|
||||
});
|
||||
|
||||
it('should handle multiple input items', async () => {
|
||||
const node = new ToolWolframAlpha();
|
||||
const inputData: INodeExecutionData[] = [
|
||||
{
|
||||
json: { query: 'what is 5*3?' },
|
||||
},
|
||||
{
|
||||
json: { query: 'what is the square root of 16?' },
|
||||
},
|
||||
];
|
||||
|
||||
const mockExecute = mock<IExecuteFunctions>({
|
||||
getInputData: jest.fn(() => inputData),
|
||||
getNode: jest.fn(() => mock<INode>({ name: 'test wolfram' })),
|
||||
getCredentials: jest.fn().mockResolvedValue({ appId: 'test-app-id' }),
|
||||
});
|
||||
|
||||
// Mock the WolframAlphaTool.invoke method
|
||||
WolframAlphaTool.prototype.invoke = jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce('15')
|
||||
.mockResolvedValueOnce('4');
|
||||
|
||||
const result = await node.execute.call(mockExecute);
|
||||
|
||||
expect(result).toEqual([
|
||||
[
|
||||
{
|
||||
json: {
|
||||
response: '15',
|
||||
},
|
||||
pairedItem: {
|
||||
item: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
json: {
|
||||
response: '4',
|
||||
},
|
||||
pairedItem: {
|
||||
item: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
]);
|
||||
expect(WolframAlphaTool.prototype.invoke).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should handle credentials correctly', async () => {
|
||||
const node = new ToolWolframAlpha();
|
||||
const inputData: INodeExecutionData[] = [
|
||||
{
|
||||
json: { query: 'test query' },
|
||||
},
|
||||
];
|
||||
|
||||
const mockExecute = mock<IExecuteFunctions>({
|
||||
getInputData: jest.fn(() => inputData),
|
||||
getNode: jest.fn(() => mock<INode>({ name: 'test wolfram' })),
|
||||
getCredentials: jest.fn().mockResolvedValue({ appId: 'secret-app-id' }),
|
||||
});
|
||||
|
||||
WolframAlphaTool.prototype.invoke = jest.fn().mockResolvedValue('test result');
|
||||
|
||||
await node.execute.call(mockExecute);
|
||||
|
||||
expect(mockExecute.getCredentials).toHaveBeenCalledWith('wolframAlphaApi');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
import { WolframAlphaTool } from '@langchain/community/tools/wolframalpha';
|
||||
import {
|
||||
NodeConnectionTypes,
|
||||
type IExecuteFunctions,
|
||||
type INodeExecutionData,
|
||||
type INodeType,
|
||||
type INodeTypeDescription,
|
||||
type ISupplyDataFunctions,
|
||||
|
|
@ -56,4 +58,25 @@ export class ToolWolframAlpha implements INodeType {
|
|||
response: logWrapper(new WolframAlphaTool({ appid: credentials.appId as string }), this),
|
||||
};
|
||||
}
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const credentials = await this.getCredentials('wolframAlphaApi');
|
||||
const input = this.getInputData();
|
||||
const result: INodeExecutionData[] = [];
|
||||
|
||||
for (let i = 0; i < input.length; i++) {
|
||||
const item = input[i];
|
||||
const tool = new WolframAlphaTool({ appid: credentials.appId as string });
|
||||
result.push({
|
||||
json: {
|
||||
response: await tool.invoke(item.json),
|
||||
},
|
||||
pairedItem: {
|
||||
item: i,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return [result];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,43 @@
|
|||
import type { DynamicStructuredTool, DynamicTool } from '@langchain/core/tools';
|
||||
|
||||
import type {
|
||||
INodeTypeBaseDescription,
|
||||
ISupplyDataFunctions,
|
||||
SupplyData,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
IExecuteFunctions,
|
||||
INodeExecutionData,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { nodeNameToToolName } from 'n8n-workflow';
|
||||
|
||||
import { localResourceMapping } from './methods';
|
||||
import { WorkflowToolService } from './utils/WorkflowToolService';
|
||||
import { versionDescription } from './versionDescription';
|
||||
|
||||
async function getTool(
|
||||
ctx: ISupplyDataFunctions | IExecuteFunctions,
|
||||
enableLogging: boolean,
|
||||
itemIndex: number,
|
||||
): Promise<DynamicTool | DynamicStructuredTool> {
|
||||
const node = ctx.getNode();
|
||||
const { typeVersion } = node;
|
||||
const returnAllItems = typeVersion > 2;
|
||||
|
||||
const workflowToolService = new WorkflowToolService(ctx, { returnAllItems });
|
||||
const name =
|
||||
typeVersion <= 2.1 ? (ctx.getNodeParameter('name', 0) as string) : nodeNameToToolName(node);
|
||||
const description = ctx.getNodeParameter('description', 0) as string;
|
||||
|
||||
return await workflowToolService.createTool({
|
||||
ctx,
|
||||
name,
|
||||
description,
|
||||
itemIndex,
|
||||
manualLogging: enableLogging,
|
||||
});
|
||||
}
|
||||
|
||||
export class ToolWorkflowV2 implements INodeType {
|
||||
description: INodeTypeDescription;
|
||||
|
||||
|
|
@ -27,24 +53,24 @@ export class ToolWorkflowV2 implements INodeType {
|
|||
};
|
||||
|
||||
async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
|
||||
const node = this.getNode();
|
||||
const { typeVersion } = node;
|
||||
const returnAllItems = typeVersion > 2;
|
||||
return { response: await getTool(this, true, itemIndex) };
|
||||
}
|
||||
|
||||
const workflowToolService = new WorkflowToolService(this, { returnAllItems });
|
||||
const name =
|
||||
typeVersion <= 2.1
|
||||
? (this.getNodeParameter('name', itemIndex) as string)
|
||||
: nodeNameToToolName(node);
|
||||
const description = this.getNodeParameter('description', itemIndex) as string;
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const items = this.getInputData();
|
||||
|
||||
const tool = await workflowToolService.createTool({
|
||||
ctx: this,
|
||||
name,
|
||||
description,
|
||||
itemIndex,
|
||||
});
|
||||
const response: INodeExecutionData[] = [];
|
||||
for (let itemIndex = 0; itemIndex < this.getInputData().length; itemIndex++) {
|
||||
const item = items[itemIndex];
|
||||
const tool = await getTool(this, false, itemIndex);
|
||||
|
||||
return { response: tool };
|
||||
if (item === undefined) {
|
||||
continue;
|
||||
}
|
||||
const result = await tool.invoke(item.json);
|
||||
response.push(result);
|
||||
}
|
||||
|
||||
return [response];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import type {
|
|||
ExecutionError,
|
||||
FromAIArgument,
|
||||
IDataObject,
|
||||
IExecuteFunctions,
|
||||
IExecuteWorkflowInfo,
|
||||
INodeExecutionData,
|
||||
INodeParameterResourceLocator,
|
||||
|
|
@ -51,7 +52,7 @@ export class WorkflowToolService {
|
|||
private returnAllItems: boolean = false;
|
||||
|
||||
constructor(
|
||||
private baseContext: ISupplyDataFunctions,
|
||||
private baseContext: ISupplyDataFunctions | IExecuteFunctions,
|
||||
options?: { returnAllItems: boolean },
|
||||
) {
|
||||
const subWorkflowInputs = this.baseContext.getNode().parameters
|
||||
|
|
@ -66,11 +67,13 @@ export class WorkflowToolService {
|
|||
name,
|
||||
description,
|
||||
itemIndex,
|
||||
manualLogging = true,
|
||||
}: {
|
||||
ctx: ISupplyDataFunctions;
|
||||
ctx: ISupplyDataFunctions | IExecuteFunctions;
|
||||
name: string;
|
||||
description: string;
|
||||
itemIndex: number;
|
||||
manualLogging?: boolean;
|
||||
}): Promise<DynamicTool | DynamicStructuredTool> {
|
||||
// Handler for the tool execution, will be called when the tool is executed
|
||||
// This function will execute the sub-workflow and return the response
|
||||
|
|
@ -78,7 +81,7 @@ export class WorkflowToolService {
|
|||
// of the same tool when the tool is used in a loop or in a parallel execution.
|
||||
const node = ctx.getNode();
|
||||
|
||||
let runIndex: number = ctx.getNextRunIndex();
|
||||
let runIndex: number = 'getNextRunIndex' in ctx ? ctx.getNextRunIndex() : 0;
|
||||
const toolHandler = async (
|
||||
query: string | IDataObject,
|
||||
runManager?: CallbackManagerForToolRun,
|
||||
|
|
@ -97,13 +100,17 @@ export class WorkflowToolService {
|
|||
|
||||
for (let tryIndex = 0; tryIndex < maxTries; tryIndex++) {
|
||||
const localRunIndex = runIndex++;
|
||||
|
||||
let context = this.baseContext;
|
||||
// We need to clone the context here to handle runIndex correctly
|
||||
// Otherwise the runIndex will be shared between different executions
|
||||
// Causing incorrect data to be passed to the sub-workflow and via $fromAI
|
||||
const context = this.baseContext.cloneWith({
|
||||
runIndex: localRunIndex,
|
||||
inputData: [[{ json: { query } }]],
|
||||
});
|
||||
if ('cloneWith' in this.baseContext) {
|
||||
context = this.baseContext.cloneWith({
|
||||
runIndex: localRunIndex,
|
||||
inputData: [[{ json: { query } }]],
|
||||
});
|
||||
}
|
||||
|
||||
// Get abort signal from context for cancellation support
|
||||
const abortSignal = context.getExecutionCancelSignal?.();
|
||||
|
|
@ -153,12 +160,14 @@ export class WorkflowToolService {
|
|||
};
|
||||
}
|
||||
|
||||
void context.addOutputData(
|
||||
NodeConnectionTypes.AiTool,
|
||||
localRunIndex,
|
||||
[responseData],
|
||||
metadata,
|
||||
);
|
||||
if (manualLogging) {
|
||||
void context.addOutputData(
|
||||
NodeConnectionTypes.AiTool,
|
||||
localRunIndex,
|
||||
[responseData],
|
||||
metadata,
|
||||
);
|
||||
}
|
||||
|
||||
return processedResponse;
|
||||
} catch (error) {
|
||||
|
|
@ -171,13 +180,15 @@ export class WorkflowToolService {
|
|||
lastError = executionError;
|
||||
const errorResponse = `There was an error: "${executionError.message}"`;
|
||||
|
||||
const metadata = parseErrorMetadata(error);
|
||||
void context.addOutputData(
|
||||
NodeConnectionTypes.AiTool,
|
||||
localRunIndex,
|
||||
executionError,
|
||||
metadata,
|
||||
);
|
||||
if (manualLogging) {
|
||||
const metadata = parseErrorMetadata(error);
|
||||
void context.addOutputData(
|
||||
NodeConnectionTypes.AiTool,
|
||||
localRunIndex,
|
||||
executionError,
|
||||
metadata,
|
||||
);
|
||||
}
|
||||
|
||||
if (tryIndex === maxTries - 1) {
|
||||
return errorResponse;
|
||||
|
|
@ -224,7 +235,7 @@ export class WorkflowToolService {
|
|||
* Executes specified sub-workflow with provided inputs
|
||||
*/
|
||||
private async executeSubWorkflow(
|
||||
context: ISupplyDataFunctions,
|
||||
context: ISupplyDataFunctions | IExecuteFunctions,
|
||||
workflowInfo: IExecuteWorkflowInfo,
|
||||
items: INodeExecutionData[],
|
||||
workflowProxy: IWorkflowDataProxyData,
|
||||
|
|
@ -265,7 +276,7 @@ export class WorkflowToolService {
|
|||
* This function will be called as part of the tool execution (from the toolHandler)
|
||||
*/
|
||||
private async runFunction(
|
||||
context: ISupplyDataFunctions,
|
||||
context: ISupplyDataFunctions | IExecuteFunctions,
|
||||
query: string | IDataObject,
|
||||
itemIndex: number,
|
||||
runManager?: CallbackManagerForToolRun,
|
||||
|
|
@ -298,7 +309,7 @@ export class WorkflowToolService {
|
|||
* Gets the sub-workflow info based on the source (database or parameter)
|
||||
*/
|
||||
private async getSubWorkflowInfo(
|
||||
context: ISupplyDataFunctions,
|
||||
context: ISupplyDataFunctions | IExecuteFunctions,
|
||||
source: string,
|
||||
itemIndex: number,
|
||||
workflowProxy: IWorkflowDataProxyData,
|
||||
|
|
@ -336,7 +347,7 @@ export class WorkflowToolService {
|
|||
}
|
||||
|
||||
private prepareRawData(
|
||||
context: ISupplyDataFunctions,
|
||||
context: ISupplyDataFunctions | IExecuteFunctions,
|
||||
query: string | IDataObject,
|
||||
itemIndex: number,
|
||||
): IDataObject {
|
||||
|
|
@ -359,7 +370,7 @@ export class WorkflowToolService {
|
|||
* Prepares the sub-workflow items for execution
|
||||
*/
|
||||
private async prepareWorkflowItems(
|
||||
context: ISupplyDataFunctions,
|
||||
context: ISupplyDataFunctions | IExecuteFunctions,
|
||||
query: string | IDataObject,
|
||||
itemIndex: number,
|
||||
rawData: IDataObject,
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user