From 281b70be044b3fd70a26a16d14eebf38bab739a6 Mon Sep 17 00:00:00 2001 From: Yiorgis Gozadinos Date: Thu, 3 Apr 2025 12:06:23 +0300 Subject: [PATCH] feat(Think Tool Node): ToolThink, a simple tool to force the AI agent to do some thinking (#14351) --- .../nodes/tools/ToolThink/ToolThink.node.ts | 79 +++++++++++++++++++ .../ToolThink/test/ToolThink.node.test.ts | 34 ++++++++ packages/@n8n/nodes-langchain/package.json | 1 + 3 files changed, 114 insertions(+) create mode 100644 packages/@n8n/nodes-langchain/nodes/tools/ToolThink/ToolThink.node.ts create mode 100644 packages/@n8n/nodes-langchain/nodes/tools/ToolThink/test/ToolThink.node.test.ts diff --git a/packages/@n8n/nodes-langchain/nodes/tools/ToolThink/ToolThink.node.ts b/packages/@n8n/nodes-langchain/nodes/tools/ToolThink/ToolThink.node.ts new file mode 100644 index 0000000000..9515156a62 --- /dev/null +++ b/packages/@n8n/nodes-langchain/nodes/tools/ToolThink/ToolThink.node.ts @@ -0,0 +1,79 @@ +/* eslint-disable n8n-nodes-base/node-dirname-against-convention */ +import { DynamicTool } from 'langchain/tools'; +import { + NodeConnectionTypes, + type INodeType, + type INodeTypeDescription, + type ISupplyDataFunctions, + type SupplyData, +} from 'n8n-workflow'; + +import { logWrapper } from '@utils/logWrapper'; +import { getConnectionHintNoticeField } from '@utils/sharedFields'; + +// A thinking tool, see https://www.anthropic.com/engineering/claude-think-tool + +const defaultToolDescription = + 'Use the tool to think about something. It will not obtain new information or change the database, but just append the thought to the log. Use it when complex reasoning or some cache memory is needed.'; + +export class ToolThink implements INodeType { + description: INodeTypeDescription = { + displayName: 'Think Tool', + name: 'toolThink', + icon: 'fa:brain', + iconColor: 'black', + group: ['transform'], + version: 1, + description: 'Invite the AI agent to do some thinking', + defaults: { + name: 'Think', + }, + codex: { + categories: ['AI'], + subcategories: { + AI: ['Tools'], + Tools: ['Other Tools'], + }, + resources: { + primaryDocumentation: [ + { + url: 'https://docs.n8n.io/integrations/builtin/cluster-nodes/sub-nodes/n8n-nodes-langchain.toolthink/', + }, + ], + }, + }, + inputs: [], + outputs: [NodeConnectionTypes.AiTool], + outputNames: ['Tool'], + properties: [ + getConnectionHintNoticeField([NodeConnectionTypes.AiAgent]), + { + displayName: 'Think Tool Description', + name: 'description', + type: 'string', + default: defaultToolDescription, + placeholder: '[Describe your thinking tool here, explaining how it will help the AI think]', + description: "The thinking tool's description", + typeOptions: { + rows: 3, + }, + required: true, + }, + ], + }; + + async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise { + const description = this.getNodeParameter('description', itemIndex) as string; + const tool = new DynamicTool({ + name: 'thinking_tool', + description, + func: async (subject: string) => { + return subject; + }, + }); + + return { + response: logWrapper(tool, this), + }; + } +} diff --git a/packages/@n8n/nodes-langchain/nodes/tools/ToolThink/test/ToolThink.node.test.ts b/packages/@n8n/nodes-langchain/nodes/tools/ToolThink/test/ToolThink.node.test.ts new file mode 100644 index 0000000000..cc80b406a9 --- /dev/null +++ b/packages/@n8n/nodes-langchain/nodes/tools/ToolThink/test/ToolThink.node.test.ts @@ -0,0 +1,34 @@ +import { mock } from 'jest-mock-extended'; +import { DynamicTool } from 'langchain/tools'; +import type { ISupplyDataFunctions } from 'n8n-workflow'; + +import { ToolThink } from '../ToolThink.node'; + +describe('ToolThink', () => { + const thinkTool = new ToolThink(); + const helpers = mock(); + const executeFunctions = mock({ + helpers, + }); + executeFunctions.addInputData.mockReturnValue({ index: 0 }); + executeFunctions.getNodeParameter.mockImplementation((paramName: string) => { + switch (paramName) { + case 'description': + return 'Tool description'; + default: + return undefined; + } + }); + + describe('Tool response', () => { + it('should return the same text as response when receiving a text input', async () => { + const { response } = (await thinkTool.supplyData.call(executeFunctions, 0)) as { + response: DynamicTool; + }; + expect(response).toBeInstanceOf(DynamicTool); + expect(response.description).toEqual('Tool description'); + const res = (await response.invoke('foo')) as string; + expect(res).toEqual('foo'); + }); + }); +}); diff --git a/packages/@n8n/nodes-langchain/package.json b/packages/@n8n/nodes-langchain/package.json index 91f4407870..299ba4db72 100644 --- a/packages/@n8n/nodes-langchain/package.json +++ b/packages/@n8n/nodes-langchain/package.json @@ -104,6 +104,7 @@ "dist/nodes/tools/ToolCode/ToolCode.node.js", "dist/nodes/tools/ToolHttpRequest/ToolHttpRequest.node.js", "dist/nodes/tools/ToolSerpApi/ToolSerpApi.node.js", + "dist/nodes/tools/ToolThink/ToolThink.node.js", "dist/nodes/tools/ToolVectorStore/ToolVectorStore.node.js", "dist/nodes/tools/ToolWikipedia/ToolWikipedia.node.js", "dist/nodes/tools/ToolWolframAlpha/ToolWolframAlpha.node.js",