diff --git a/cypress/constants.ts b/cypress/constants.ts index 8fdb03ef9cfba..6f7e7b978d317 100644 --- a/cypress/constants.ts +++ b/cypress/constants.ts @@ -37,7 +37,7 @@ export const INSTANCE_MEMBERS = [ export const MANUAL_TRIGGER_NODE_NAME = 'Manual Trigger'; export const MANUAL_TRIGGER_NODE_DISPLAY_NAME = 'When clicking ‘Test workflow’'; export const MANUAL_CHAT_TRIGGER_NODE_NAME = 'Chat Trigger'; -export const MANUAL_CHAT_TRIGGER_NODE_DISPLAY_NAME = 'When chat message received'; +export const CHAT_TRIGGER_NODE_DISPLAY_NAME = 'When chat message received'; export const SCHEDULE_TRIGGER_NODE_NAME = 'Schedule Trigger'; export const CODE_NODE_NAME = 'Code'; export const SET_NODE_NAME = 'Set'; diff --git a/cypress/e2e/233-AI-switch-to-logs-on-error.cy.ts b/cypress/e2e/233-AI-switch-to-logs-on-error.cy.ts index 78d4b61449c23..4c733df90dc48 100644 --- a/cypress/e2e/233-AI-switch-to-logs-on-error.cy.ts +++ b/cypress/e2e/233-AI-switch-to-logs-on-error.cy.ts @@ -13,7 +13,7 @@ import { AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, AI_MEMORY_POSTGRES_NODE_NAME, AI_TOOL_CALCULATOR_NODE_NAME, - MANUAL_CHAT_TRIGGER_NODE_DISPLAY_NAME, + CHAT_TRIGGER_NODE_DISPLAY_NAME, MANUAL_CHAT_TRIGGER_NODE_NAME, MANUAL_TRIGGER_NODE_DISPLAY_NAME, MANUAL_TRIGGER_NODE_NAME, @@ -148,7 +148,7 @@ function setupTestWorkflow(chatTrigger: boolean = false) { if (!chatTrigger) { // Remove chat trigger WorkflowPage.getters - .canvasNodeByName(MANUAL_CHAT_TRIGGER_NODE_DISPLAY_NAME) + .canvasNodeByName(CHAT_TRIGGER_NODE_DISPLAY_NAME) .find('[data-test-id="delete-node-button"]') .click({ force: true }); diff --git a/cypress/e2e/30-langchain.cy.ts b/cypress/e2e/30-langchain.cy.ts index c1409a34f379b..c6d0f4ab4d3e7 100644 --- a/cypress/e2e/30-langchain.cy.ts +++ b/cypress/e2e/30-langchain.cy.ts @@ -10,7 +10,9 @@ import { disableNode, getExecuteWorkflowButton, navigateToNewWorkflowPage, + getNodes, openNode, + getConnectionBySourceAndTarget, } from '../composables/workflow'; import { clickCreateNewCredential, @@ -41,6 +43,7 @@ import { AI_TOOL_WIKIPEDIA_NODE_NAME, BASIC_LLM_CHAIN_NODE_NAME, EDIT_FIELDS_SET_NODE_NAME, + CHAT_TRIGGER_NODE_DISPLAY_NAME, } from './../constants'; describe('Langchain Integration', () => { @@ -331,4 +334,27 @@ describe('Langchain Integration', () => { closeManualChatModal(); }); + + it('should auto-add chat trigger and basic LLM chain when adding LLM node', () => { + addNodeToCanvas(AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, true); + + getConnectionBySourceAndTarget( + CHAT_TRIGGER_NODE_DISPLAY_NAME, + BASIC_LLM_CHAIN_NODE_NAME, + ).should('exist'); + + getConnectionBySourceAndTarget( + AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, + BASIC_LLM_CHAIN_NODE_NAME, + ).should('exist'); + getNodes().should('have.length', 3); + }); + + it('should not auto-add nodes if AI nodes are already present', () => { + addNodeToCanvas(AGENT_NODE_NAME, true); + + addNodeToCanvas(AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, true); + getConnectionBySourceAndTarget(CHAT_TRIGGER_NODE_DISPLAY_NAME, AGENT_NODE_NAME).should('exist'); + getNodes().should('have.length', 3); + }); }); diff --git a/packages/@n8n/nodes-langchain/nodes/llms/LMChatAnthropic/LmChatAnthropic.node.ts b/packages/@n8n/nodes-langchain/nodes/llms/LMChatAnthropic/LmChatAnthropic.node.ts index 241efadb6d392..ecc14e1344341 100644 --- a/packages/@n8n/nodes-langchain/nodes/llms/LMChatAnthropic/LmChatAnthropic.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/llms/LMChatAnthropic/LmChatAnthropic.node.ts @@ -74,7 +74,7 @@ export class LmChatAnthropic implements INodeType { codex: { categories: ['AI'], subcategories: { - AI: ['Language Models'], + AI: ['Language Models', 'Root Nodes'], 'Language Models': ['Chat Models (Recommended)'], }, resources: { diff --git a/packages/@n8n/nodes-langchain/nodes/llms/LMChatOllama/LmChatOllama.node.ts b/packages/@n8n/nodes-langchain/nodes/llms/LMChatOllama/LmChatOllama.node.ts index f0f9e5f630e29..b4fc474dd2ecf 100644 --- a/packages/@n8n/nodes-langchain/nodes/llms/LMChatOllama/LmChatOllama.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/llms/LMChatOllama/LmChatOllama.node.ts @@ -28,7 +28,7 @@ export class LmChatOllama implements INodeType { codex: { categories: ['AI'], subcategories: { - AI: ['Language Models'], + AI: ['Language Models', 'Root Nodes'], 'Language Models': ['Chat Models (Recommended)'], }, resources: { diff --git a/packages/@n8n/nodes-langchain/nodes/llms/LMChatOpenAi/LmChatOpenAi.node.ts b/packages/@n8n/nodes-langchain/nodes/llms/LMChatOpenAi/LmChatOpenAi.node.ts index 264c594d1bd87..1f39c082290e4 100644 --- a/packages/@n8n/nodes-langchain/nodes/llms/LMChatOpenAi/LmChatOpenAi.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/llms/LMChatOpenAi/LmChatOpenAi.node.ts @@ -26,7 +26,7 @@ export class LmChatOpenAi implements INodeType { codex: { categories: ['AI'], subcategories: { - AI: ['Language Models'], + AI: ['Language Models', 'Root Nodes'], 'Language Models': ['Chat Models (Recommended)'], }, resources: { diff --git a/packages/@n8n/nodes-langchain/nodes/llms/LMCohere/LmCohere.node.ts b/packages/@n8n/nodes-langchain/nodes/llms/LMCohere/LmCohere.node.ts index ee5163a78f1a3..191209bb3363b 100644 --- a/packages/@n8n/nodes-langchain/nodes/llms/LMCohere/LmCohere.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/llms/LMCohere/LmCohere.node.ts @@ -26,7 +26,7 @@ export class LmCohere implements INodeType { codex: { categories: ['AI'], subcategories: { - AI: ['Language Models'], + AI: ['Language Models', 'Root Nodes'], 'Language Models': ['Text Completion Models'], }, resources: { diff --git a/packages/@n8n/nodes-langchain/nodes/llms/LMOllama/LmOllama.node.ts b/packages/@n8n/nodes-langchain/nodes/llms/LMOllama/LmOllama.node.ts index 95024ad4b2f39..5492a51a97914 100644 --- a/packages/@n8n/nodes-langchain/nodes/llms/LMOllama/LmOllama.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/llms/LMOllama/LmOllama.node.ts @@ -27,7 +27,7 @@ export class LmOllama implements INodeType { codex: { categories: ['AI'], subcategories: { - AI: ['Language Models'], + AI: ['Language Models', 'Root Nodes'], 'Language Models': ['Text Completion Models'], }, resources: { diff --git a/packages/@n8n/nodes-langchain/nodes/llms/LMOpenAi/LmOpenAi.node.ts b/packages/@n8n/nodes-langchain/nodes/llms/LMOpenAi/LmOpenAi.node.ts index 2f0e3480d8701..a46ad429a2963 100644 --- a/packages/@n8n/nodes-langchain/nodes/llms/LMOpenAi/LmOpenAi.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/llms/LMOpenAi/LmOpenAi.node.ts @@ -38,7 +38,7 @@ export class LmOpenAi implements INodeType { codex: { categories: ['AI'], subcategories: { - AI: ['Language Models'], + AI: ['Language Models', 'Root Nodes'], 'Language Models': ['Text Completion Models'], }, resources: { diff --git a/packages/@n8n/nodes-langchain/nodes/llms/LMOpenHuggingFaceInference/LmOpenHuggingFaceInference.node.ts b/packages/@n8n/nodes-langchain/nodes/llms/LMOpenHuggingFaceInference/LmOpenHuggingFaceInference.node.ts index 9a30ef74d73ae..7b2c821f9c34c 100644 --- a/packages/@n8n/nodes-langchain/nodes/llms/LMOpenHuggingFaceInference/LmOpenHuggingFaceInference.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/llms/LMOpenHuggingFaceInference/LmOpenHuggingFaceInference.node.ts @@ -26,7 +26,7 @@ export class LmOpenHuggingFaceInference implements INodeType { codex: { categories: ['AI'], subcategories: { - AI: ['Language Models'], + AI: ['Language Models', 'Root Nodes'], 'Language Models': ['Text Completion Models'], }, resources: { diff --git a/packages/@n8n/nodes-langchain/nodes/llms/LmChatAwsBedrock/LmChatAwsBedrock.node.ts b/packages/@n8n/nodes-langchain/nodes/llms/LmChatAwsBedrock/LmChatAwsBedrock.node.ts index d313ba53b54e4..4e1c27bfde8c2 100644 --- a/packages/@n8n/nodes-langchain/nodes/llms/LmChatAwsBedrock/LmChatAwsBedrock.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/llms/LmChatAwsBedrock/LmChatAwsBedrock.node.ts @@ -29,7 +29,7 @@ export class LmChatAwsBedrock implements INodeType { codex: { categories: ['AI'], subcategories: { - AI: ['Language Models'], + AI: ['Language Models', 'Root Nodes'], 'Language Models': ['Chat Models (Recommended)'], }, resources: { diff --git a/packages/@n8n/nodes-langchain/nodes/llms/LmChatAzureOpenAi/LmChatAzureOpenAi.node.ts b/packages/@n8n/nodes-langchain/nodes/llms/LmChatAzureOpenAi/LmChatAzureOpenAi.node.ts index 2e05b4770d99d..5770387158b1e 100644 --- a/packages/@n8n/nodes-langchain/nodes/llms/LmChatAzureOpenAi/LmChatAzureOpenAi.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/llms/LmChatAzureOpenAi/LmChatAzureOpenAi.node.ts @@ -27,7 +27,7 @@ export class LmChatAzureOpenAi implements INodeType { codex: { categories: ['AI'], subcategories: { - AI: ['Language Models'], + AI: ['Language Models', 'Root Nodes'], 'Language Models': ['Chat Models (Recommended)'], }, resources: { diff --git a/packages/@n8n/nodes-langchain/nodes/llms/LmChatGoogleGemini/LmChatGoogleGemini.node.ts b/packages/@n8n/nodes-langchain/nodes/llms/LmChatGoogleGemini/LmChatGoogleGemini.node.ts index d3d3d1ea2c218..ce08a650f203f 100644 --- a/packages/@n8n/nodes-langchain/nodes/llms/LmChatGoogleGemini/LmChatGoogleGemini.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/llms/LmChatGoogleGemini/LmChatGoogleGemini.node.ts @@ -27,7 +27,7 @@ export class LmChatGoogleGemini implements INodeType { codex: { categories: ['AI'], subcategories: { - AI: ['Language Models'], + AI: ['Language Models', 'Root Nodes'], 'Language Models': ['Chat Models (Recommended)'], }, resources: { diff --git a/packages/@n8n/nodes-langchain/nodes/llms/LmChatGooglePalm/LmChatGooglePalm.node.ts b/packages/@n8n/nodes-langchain/nodes/llms/LmChatGooglePalm/LmChatGooglePalm.node.ts index a32a5e959cdb5..6195d4d98763b 100644 --- a/packages/@n8n/nodes-langchain/nodes/llms/LmChatGooglePalm/LmChatGooglePalm.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/llms/LmChatGooglePalm/LmChatGooglePalm.node.ts @@ -25,7 +25,7 @@ export class LmChatGooglePalm implements INodeType { codex: { categories: ['AI'], subcategories: { - AI: ['Language Models'], + AI: ['Language Models', 'Root Nodes'], 'Language Models': ['Chat Models (Recommended)'], }, resources: { diff --git a/packages/@n8n/nodes-langchain/nodes/llms/LmChatGoogleVertex/LmChatGoogleVertex.node.ts b/packages/@n8n/nodes-langchain/nodes/llms/LmChatGoogleVertex/LmChatGoogleVertex.node.ts index 1e3837818fb5b..044428c01adc3 100644 --- a/packages/@n8n/nodes-langchain/nodes/llms/LmChatGoogleVertex/LmChatGoogleVertex.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/llms/LmChatGoogleVertex/LmChatGoogleVertex.node.ts @@ -32,7 +32,7 @@ export class LmChatGoogleVertex implements INodeType { codex: { categories: ['AI'], subcategories: { - AI: ['Language Models'], + AI: ['Language Models', 'Root Nodes'], 'Language Models': ['Chat Models (Recommended)'], }, resources: { diff --git a/packages/@n8n/nodes-langchain/nodes/llms/LmChatGroq/LmChatGroq.node.ts b/packages/@n8n/nodes-langchain/nodes/llms/LmChatGroq/LmChatGroq.node.ts index 3354ac030f404..d0a28715e1010 100644 --- a/packages/@n8n/nodes-langchain/nodes/llms/LmChatGroq/LmChatGroq.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/llms/LmChatGroq/LmChatGroq.node.ts @@ -26,7 +26,7 @@ export class LmChatGroq implements INodeType { codex: { categories: ['AI'], subcategories: { - AI: ['Language Models'], + AI: ['Language Models', 'Root Nodes'], 'Language Models': ['Chat Models (Recommended)'], }, resources: { diff --git a/packages/@n8n/nodes-langchain/nodes/llms/LmChatMistralCloud/LmChatMistralCloud.node.ts b/packages/@n8n/nodes-langchain/nodes/llms/LmChatMistralCloud/LmChatMistralCloud.node.ts index 32364545d2073..129beeadfe5eb 100644 --- a/packages/@n8n/nodes-langchain/nodes/llms/LmChatMistralCloud/LmChatMistralCloud.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/llms/LmChatMistralCloud/LmChatMistralCloud.node.ts @@ -27,7 +27,7 @@ export class LmChatMistralCloud implements INodeType { codex: { categories: ['AI'], subcategories: { - AI: ['Language Models'], + AI: ['Language Models', 'Root Nodes'], 'Language Models': ['Chat Models (Recommended)'], }, resources: { diff --git a/packages/@n8n/nodes-langchain/nodes/llms/LmGooglePalm/LmGooglePalm.node.ts b/packages/@n8n/nodes-langchain/nodes/llms/LmGooglePalm/LmGooglePalm.node.ts index d79f74be02fe6..e4681803fe59a 100644 --- a/packages/@n8n/nodes-langchain/nodes/llms/LmGooglePalm/LmGooglePalm.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/llms/LmGooglePalm/LmGooglePalm.node.ts @@ -25,7 +25,7 @@ export class LmGooglePalm implements INodeType { codex: { categories: ['AI'], subcategories: { - AI: ['Language Models'], + AI: ['Language Models', 'Root Nodes'], 'Language Models': ['Text Completion Models'], }, resources: { diff --git a/packages/editor-ui/src/Interface.ts b/packages/editor-ui/src/Interface.ts index 770cfe2298238..8c93c847ba540 100644 --- a/packages/editor-ui/src/Interface.ts +++ b/packages/editor-ui/src/Interface.ts @@ -1808,8 +1808,8 @@ export type AddedNode = { } & Partial; export type AddedNodeConnection = { - from: { nodeIndex: number; outputIndex?: number }; - to: { nodeIndex: number; inputIndex?: number }; + from: { nodeIndex: number; outputIndex?: number; type?: NodeConnectionType }; + to: { nodeIndex: number; inputIndex?: number; type?: NodeConnectionType }; }; export type AddedNodesAndConnections = { diff --git a/packages/editor-ui/src/components/Node/NodeCreator/composables/useActions.ts b/packages/editor-ui/src/components/Node/NodeCreator/composables/useActions.ts index 2346968f798ab..e9f7917385eec 100644 --- a/packages/editor-ui/src/components/Node/NodeCreator/composables/useActions.ts +++ b/packages/editor-ui/src/components/Node/NodeCreator/composables/useActions.ts @@ -1,5 +1,10 @@ import { computed } from 'vue'; -import type { IDataObject, INodeParameters } from 'n8n-workflow'; +import { + CHAIN_LLM_LANGCHAIN_NODE_TYPE, + NodeConnectionType, + type IDataObject, + type INodeParameters, +} from 'n8n-workflow'; import type { ActionTypeDescription, AddedNode, @@ -11,6 +16,7 @@ import type { } from '@/Interface'; import { AGENT_NODE_TYPE, + AI_CATEGORY_LANGUAGE_MODELS, BASIC_CHAIN_NODE_TYPE, CHAT_TRIGGER_NODE_TYPE, MANUAL_CHAT_TRIGGER_NODE_TYPE, @@ -37,11 +43,12 @@ import { useExternalHooks } from '@/composables/useExternalHooks'; import { sortNodeCreateElements, transformNodeType } from '../utils'; import { useI18n } from '@/composables/useI18n'; +import { useCanvasStore } from '@/stores/canvas.store'; export const useActions = () => { const nodeCreatorStore = useNodeCreatorStore(); + const nodeTypesStore = useNodeTypesStore(); const i18n = useI18n(); - const singleNodeOpenSources = [ NODE_CREATOR_OPEN_SOURCES.PLUS_ENDPOINT, NODE_CREATOR_OPEN_SOURCES.NODE_CONNECTION_ACTION, @@ -216,6 +223,19 @@ export const useActions = () => { return isCompatibleNode && isChatTriggerMissing; } + // AI-226: Prepend LLM Chain node when adding a language model + function shouldPrependLLMChain(addedNodes: AddedNode[]): boolean { + const canvasHasAINodes = useCanvasStore().aiNodes.length > 0; + if (canvasHasAINodes) return false; + + return addedNodes.some((node) => { + const nodeType = nodeTypesStore.getNodeType(node.type); + return Object.keys(nodeType?.codex?.subcategories ?? {}).includes( + AI_CATEGORY_LANGUAGE_MODELS, + ); + }); + } + function getAddedNodesAndConnections(addedNodes: AddedNode[]): AddedNodesAndConnections { if (addedNodes.length === 0) { return { nodes: [], connections: [] }; @@ -230,7 +250,14 @@ export const useActions = () => { nodeToAutoOpen.openDetail = true; } - if (shouldPrependChatTrigger(addedNodes)) { + if (shouldPrependLLMChain(addedNodes) || shouldPrependChatTrigger(addedNodes)) { + if (shouldPrependLLMChain(addedNodes)) { + addedNodes.unshift({ type: CHAIN_LLM_LANGCHAIN_NODE_TYPE, isAutoAdd: true }); + connections.push({ + from: { nodeIndex: 2, type: NodeConnectionType.AiLanguageModel }, + to: { nodeIndex: 1 }, + }); + } addedNodes.unshift({ type: CHAT_TRIGGER_NODE_TYPE, isAutoAdd: true }); connections.push({ from: { nodeIndex: 0 }, diff --git a/packages/editor-ui/src/views/NodeView.vue b/packages/editor-ui/src/views/NodeView.vue index 91ebdf1228b9f..5650be3b7acae 100644 --- a/packages/editor-ui/src/views/NodeView.vue +++ b/packages/editor-ui/src/views/NodeView.vue @@ -610,7 +610,7 @@ export default defineComponent({ return this.workflowsStore.getWorkflowExecution; }, workflowRunning(): boolean { - return this.uiStore.isActionActive['workflowRunning']; + return this.uiStore.isActionActive.workflowRunning; }, currentWorkflow(): string { return this.$route.params.name?.toString() || this.workflowsStore.workflowId; @@ -4428,7 +4428,7 @@ export default defineComponent({ from.outputIndex ?? 0, toNode.name, to.inputIndex ?? 0, - NodeConnectionType.Main, + from.type ?? NodeConnectionType.Main, ); } @@ -4449,6 +4449,22 @@ export default defineComponent({ }); } + const lastNodeType = this.nodeTypesStore.getNodeType(lastAddedNode.type); + const isSubNode = NodeHelpers.isSubNodeType(lastNodeType); + + // When adding a sub-node and there's more than one node added at the time, it must mean that it's + // connected to a root node, so we adjust the position of the sub-node to make it appear in the correct + // in relation to the root node + if (isSubNode && nodes.length > 1) { + this.onMoveNode({ + nodeName: lastAddedNode.name, + position: [ + lastAddedNode.position[0] - NodeViewUtils.NODE_SIZE * 2.5, + lastAddedNode.position[1] + NodeViewUtils.NODE_SIZE * 1.5, + ], + }); + } + this.nodeHelpers.addPinDataConnections(this.workflowsStore.pinnedWorkflowData); },