From a60d106ebb4fb71e80f90a17965d7fb79d7806c6 Mon Sep 17 00:00:00 2001 From: Jaakko Husso Date: Wed, 26 Feb 2025 10:23:22 +0200 Subject: [PATCH 01/47] feat(core): Make Tools Agent the default Agent type, deprecate other agent types (#13459) --- .../nodes/agents/Agent/Agent.node.ts | 39 +++++++++++++++++-- .../Agent/agents/ReActAgent/description.ts | 7 ++++ .../agents/Agent/agents/ReActAgent/execute.ts | 2 + 3 files changed, 44 insertions(+), 4 deletions(-) diff --git a/packages/@n8n/nodes-langchain/nodes/agents/Agent/Agent.node.ts b/packages/@n8n/nodes-langchain/nodes/agents/Agent/Agent.node.ts index c5a46a1192..45c9d10e8c 100644 --- a/packages/@n8n/nodes-langchain/nodes/agents/Agent/Agent.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/agents/Agent/Agent.node.ts @@ -27,7 +27,13 @@ import { toolsAgentExecute } from './agents/ToolsAgent/execute'; // Function used in the inputs expression to figure out which inputs to // display based on the agent type function getInputs( - agent: 'toolsAgent' | 'conversationalAgent' | 'openAiFunctionsAgent' | 'reActAgent' | 'sqlAgent', + agent: + | 'toolsAgent' + | 'conversationalAgent' + | 'openAiFunctionsAgent' + | 'planAndExecuteAgent' + | 'reActAgent' + | 'sqlAgent', hasOutputParser?: boolean, ): Array { interface SpecialInput { @@ -256,7 +262,7 @@ export class Agent implements INodeType { icon: 'fa:robot', iconColor: 'black', group: ['transform'], - version: [1, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7], + version: [1, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8], description: 'Generates an action plan and executes it. Can use external tools.', subtitle: "={{ { toolsAgent: 'Tools Agent', conversationalAgent: 'Conversational Agent', openAiFunctionsAgent: 'OpenAI Functions Agent', reActAgent: 'ReAct Agent', sqlAgent: 'SQL Agent', planAndExecuteAgent: 'Plan and Execute Agent' }[$parameter.agent] }}", @@ -322,6 +328,24 @@ export class Agent implements INodeType { }, }, }, + { + displayName: + "This node is using Agent that has been deprecated. Please switch to using 'Tools Agent' instead.", + name: 'deprecated', + type: 'notice', + default: '', + displayOptions: { + show: { + agent: [ + 'conversationalAgent', + 'openAiFunctionsAgent', + 'planAndExecuteAgent', + 'reActAgent', + 'sqlAgent', + ], + }, + }, + }, // Make Conversational Agent the default agent for versions 1.5 and below { ...agentTypeProperty, @@ -331,10 +355,17 @@ export class Agent implements INodeType { displayOptions: { show: { '@version': [{ _cnd: { lte: 1.5 } }] } }, default: 'conversationalAgent', }, - // Make Tools Agent the default agent for versions 1.6 and above + // Make Tools Agent the default agent for versions 1.6 and 1.7 { ...agentTypeProperty, - displayOptions: { show: { '@version': [{ _cnd: { gte: 1.6 } }] } }, + displayOptions: { show: { '@version': [{ _cnd: { between: { from: 1.6, to: 1.7 } } }] } }, + default: 'toolsAgent', + }, + // Make Tools Agent the only agent option for versions 1.8 and above + { + ...agentTypeProperty, + type: 'hidden', + displayOptions: { show: { '@version': [{ _cnd: { gte: 1.8 } }] } }, default: 'toolsAgent', }, { diff --git a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ReActAgent/description.ts b/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ReActAgent/description.ts index 890d83dc4a..20e74b07c2 100644 --- a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ReActAgent/description.ts +++ b/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ReActAgent/description.ts @@ -96,6 +96,13 @@ export const reActAgentAgentProperties: INodeProperties[] = [ rows: 6, }, }, + { + displayName: 'Max Iterations', + name: 'maxIterations', + type: 'number', + default: 10, + description: 'The maximum number of iterations the agent will run before stopping', + }, { displayName: 'Return Intermediate Steps', name: 'returnIntermediateSteps', diff --git a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ReActAgent/execute.ts b/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ReActAgent/execute.ts index 4db35634d6..32676af5ee 100644 --- a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ReActAgent/execute.ts +++ b/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ReActAgent/execute.ts @@ -38,6 +38,7 @@ export async function reActAgentAgentExecute( prefix?: string; suffix?: string; suffixChat?: string; + maxIterations?: number; humanMessageTemplate?: string; returnIntermediateSteps?: boolean; }; @@ -60,6 +61,7 @@ export async function reActAgentAgentExecute( agent, tools, returnIntermediateSteps: options?.returnIntermediateSteps === true, + maxIterations: options.maxIterations ?? 10, }); const returnData: INodeExecutionData[] = []; From 819fc2da63ce7f06d4702bce698d382eb64c45a3 Mon Sep 17 00:00:00 2001 From: Jaakko Husso Date: Wed, 26 Feb 2025 10:24:11 +0200 Subject: [PATCH 02/47] feat(editor): Rename 'Window Buffer Memory' to 'Simple Memory' (#13477) --- cypress/constants.ts | 2 +- .../memory/MemoryBufferWindow/MemoryBufferWindow.node.ts | 4 ++-- packages/frontend/@n8n/chat/resources/workflow-manual.json | 4 ++-- packages/frontend/@n8n/chat/resources/workflow.json | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cypress/constants.ts b/cypress/constants.ts index cbbf838530..8186b23db0 100644 --- a/cypress/constants.ts +++ b/cypress/constants.ts @@ -52,7 +52,7 @@ export const PIPEDRIVE_NODE_NAME = 'Pipedrive'; export const HTTP_REQUEST_NODE_NAME = 'HTTP Request'; export const AGENT_NODE_NAME = 'AI Agent'; export const BASIC_LLM_CHAIN_NODE_NAME = 'Basic LLM Chain'; -export const AI_MEMORY_WINDOW_BUFFER_MEMORY_NODE_NAME = 'Window Buffer Memory'; +export const AI_MEMORY_WINDOW_BUFFER_MEMORY_NODE_NAME = 'Simple Memory'; export const AI_TOOL_CALCULATOR_NODE_NAME = 'Calculator'; export const AI_TOOL_CODE_NODE_NAME = 'Code Tool'; export const AI_TOOL_WIKIPEDIA_NODE_NAME = 'Wikipedia'; diff --git a/packages/@n8n/nodes-langchain/nodes/memory/MemoryBufferWindow/MemoryBufferWindow.node.ts b/packages/@n8n/nodes-langchain/nodes/memory/MemoryBufferWindow/MemoryBufferWindow.node.ts index ab02339816..91282ce144 100644 --- a/packages/@n8n/nodes-langchain/nodes/memory/MemoryBufferWindow/MemoryBufferWindow.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/memory/MemoryBufferWindow/MemoryBufferWindow.node.ts @@ -75,7 +75,7 @@ class MemoryChatBufferSingleton { export class MemoryBufferWindow implements INodeType { description: INodeTypeDescription = { - displayName: 'Window Buffer Memory (easiest)', + displayName: 'Simple Memory', name: 'memoryBufferWindow', icon: 'fa:database', iconColor: 'black', @@ -83,7 +83,7 @@ export class MemoryBufferWindow implements INodeType { version: [1, 1.1, 1.2, 1.3], description: 'Stores in n8n memory, so no credentials required', defaults: { - name: 'Window Buffer Memory', + name: 'Simple Memory', }, codex: { categories: ['AI'], diff --git a/packages/frontend/@n8n/chat/resources/workflow-manual.json b/packages/frontend/@n8n/chat/resources/workflow-manual.json index 606927859e..7aa88a6831 100644 --- a/packages/frontend/@n8n/chat/resources/workflow-manual.json +++ b/packages/frontend/@n8n/chat/resources/workflow-manual.json @@ -22,7 +22,7 @@ "sessionKey": "={{ $json.sessionId }}" }, "id": "0a68a59a-8ab6-4fa5-a1ea-b7f99a93109b", - "name": "Window Buffer Memory", + "name": "Simple Memory", "type": "@n8n/n8n-nodes-langchain.memoryBufferWindow", "typeVersion": 1, "position": [640, 540] @@ -122,7 +122,7 @@ ] ] }, - "Window Buffer Memory": { + "Simple Memory": { "ai_memory": [ [ { diff --git a/packages/frontend/@n8n/chat/resources/workflow.json b/packages/frontend/@n8n/chat/resources/workflow.json index 3b2efb4a12..83e1e81285 100644 --- a/packages/frontend/@n8n/chat/resources/workflow.json +++ b/packages/frontend/@n8n/chat/resources/workflow.json @@ -22,7 +22,7 @@ "sessionKey": "={{ $json.sessionId }}" }, "id": "b416df7b-4802-462f-8f74-f0a71dc4c0be", - "name": "Window Buffer Memory", + "name": "Simple Memory", "type": "@n8n/n8n-nodes-langchain.memoryBufferWindow", "typeVersion": 1, "position": [340, 540] @@ -66,7 +66,7 @@ ] ] }, - "Window Buffer Memory": { + "Simple Memory": { "ai_memory": [ [ { From ab41fc3fb5f15e9c7ce7279b46cec90a511d0e0d Mon Sep 17 00:00:00 2001 From: Alex Grozav Date: Wed, 26 Feb 2025 12:42:49 +0200 Subject: [PATCH 03/47] fix(editor): Fix keyboard shortcuts no longer working after editing sticky note (#13502) --- .../src/components/canvas/Canvas.vue | 20 ++++++--- .../canvas/elements/nodes/CanvasNode.vue | 8 +++- .../render-types/CanvasNodeDefault.test.ts | 15 +++++++ .../nodes/render-types/CanvasNodeDefault.vue | 14 +++++- .../render-types/CanvasNodeStickyNote.test.ts | 44 +++++++++++++++++++ .../render-types/CanvasNodeStickyNote.vue | 27 +++++++----- .../src/composables/useCanvasOperations.ts | 5 +++ packages/editor-ui/src/types/canvas.ts | 2 +- packages/editor-ui/src/views/NodeView.vue | 10 ++++- 9 files changed, 122 insertions(+), 23 deletions(-) diff --git a/packages/editor-ui/src/components/canvas/Canvas.vue b/packages/editor-ui/src/components/canvas/Canvas.vue index bc4eab4407..a13c95e2d3 100644 --- a/packages/editor-ui/src/components/canvas/Canvas.vue +++ b/packages/editor-ui/src/components/canvas/Canvas.vue @@ -44,7 +44,8 @@ const emit = defineEmits<{ 'update:modelValue': [elements: CanvasNode[]]; 'update:node:position': [id: string, position: XYPosition]; 'update:nodes:position': [events: CanvasNodeMoveEvent[]]; - 'update:node:active': [id: string]; + 'update:node:activated': [id: string]; + 'update:node:deactivated': [id: string]; 'update:node:enabled': [id: string]; 'update:node:selected': [id?: string]; 'update:node:name': [id: string]; @@ -246,7 +247,7 @@ function selectUpstreamNodes(id: string) { const keyMap = computed(() => ({ ctrl_c: emitWithSelectedNodes((ids) => emit('copy:nodes', ids)), - enter: emitWithLastSelectedNode((id) => onSetNodeActive(id)), + enter: emitWithLastSelectedNode((id) => onSetNodeActivated(id)), ctrl_a: () => addSelectedNodes(graphNodes.value), // Support both key and code for zooming in and out 'shift_+|+|=|shift_Equal|Equal': async () => await onZoomIn(), @@ -337,9 +338,13 @@ function onSelectionDragStop(event: NodeDragEvent) { onUpdateNodesPosition(event.nodes.map(({ id, position }) => ({ id, position }))); } -function onSetNodeActive(id: string) { - props.eventBus.emit('nodes:action', { ids: [id], action: 'update:node:active' }); - emit('update:node:active', id); +function onSetNodeActivated(id: string) { + props.eventBus.emit('nodes:action', { ids: [id], action: 'update:node:activated' }); + emit('update:node:activated', id); +} + +function onSetNodeDeactivated(id: string) { + emit('update:node:deactivated', id); } function clearSelectedNodes() { @@ -607,7 +612,7 @@ function onContextMenuAction(action: ContextMenuAction, nodeIds: string[]) { case 'toggle_activation': return emit('update:nodes:enabled', nodeIds); case 'open': - return onSetNodeActive(nodeIds[0]); + return onSetNodeActivated(nodeIds[0]); case 'rename': return emit('update:node:name', nodeIds[0]); case 'change_color': @@ -772,7 +777,8 @@ provide(CanvasKey, { @run="onRunNode" @select="onSelectNode" @toggle="onToggleNodeEnabled" - @activate="onSetNodeActive" + @activate="onSetNodeActivated" + @deactivate="onSetNodeDeactivated" @open:contextmenu="onOpenNodeContextMenu" @update="onUpdateNodeParameters" @update:inputs="onUpdateNodeInputs" diff --git a/packages/editor-ui/src/components/canvas/elements/nodes/CanvasNode.vue b/packages/editor-ui/src/components/canvas/elements/nodes/CanvasNode.vue index feaec44b3f..3dcbdf105b 100644 --- a/packages/editor-ui/src/components/canvas/elements/nodes/CanvasNode.vue +++ b/packages/editor-ui/src/components/canvas/elements/nodes/CanvasNode.vue @@ -59,6 +59,7 @@ const emit = defineEmits<{ select: [id: string, selected: boolean]; toggle: [id: string]; activate: [id: string]; + deactivate: [id: string]; 'open:contextmenu': [id: string, event: MouseEvent, source: 'node-button' | 'node-right-click']; update: [id: string, parameters: Record]; 'update:inputs': [id: string]; @@ -258,6 +259,10 @@ function onActivate() { emit('activate', props.id); } +function onDeactivate() { + emit('deactivate', props.id); +} + function onOpenContextMenuFromToolbar(event: MouseEvent) { emit('open:contextmenu', props.id, event, 'node-button'); } @@ -395,7 +400,8 @@ onBeforeUnmount(() => { /> { expect(getByTestId('canvas-trigger-node')).toMatchSnapshot(); }); }); + + it('should emit "activate" on double click', async () => { + const { getByText, emitted } = renderComponent({ + global: { + provide: { + ...createCanvasNodeProvide(), + }, + }, + }); + + await fireEvent.dblClick(getByText('Test Node')); + + expect(emitted()).toHaveProperty('activate'); + }); }); diff --git a/packages/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeDefault.vue b/packages/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeDefault.vue index 0514965008..c30786d28c 100644 --- a/packages/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeDefault.vue +++ b/packages/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeDefault.vue @@ -12,10 +12,12 @@ const i18n = useI18n(); const emit = defineEmits<{ 'open:contextmenu': [event: MouseEvent]; + activate: [id: string]; }>(); const { initialized, viewport } = useCanvas(); const { + id, label, subtitle, inputs, @@ -122,10 +124,20 @@ watch(viewport, () => { function openContextMenu(event: MouseEvent) { emit('open:contextmenu', event); } + +function onActivate() { + emit('activate', id.value); +}