diff --git a/cypress/composables/workflow.ts b/cypress/composables/workflow.ts index 8d37d5f2a..394a35af1 100644 --- a/cypress/composables/workflow.ts +++ b/cypress/composables/workflow.ts @@ -144,6 +144,12 @@ export function addToolNodeToParent(nodeName: string, parentNodeName: string) { export function addOutputParserNodeToParent(nodeName: string, parentNodeName: string) { addSupplementalNodeToParent(nodeName, 'ai_outputParser', parentNodeName); } +export function addVectorStoreNodeToParent(nodeName: string, parentNodeName: string) { + addSupplementalNodeToParent(nodeName, 'ai_vectorStore', parentNodeName); +} +export function addRetrieverNodeToParent(nodeName: string, parentNodeName: string) { + addSupplementalNodeToParent(nodeName, 'ai_retriever', parentNodeName); +} export function clickExecuteWorkflowButton() { getExecuteWorkflowButton().click(); diff --git a/cypress/e2e/4-node-creator.cy.ts b/cypress/e2e/4-node-creator.cy.ts index 9dfe12832..a2cd5968d 100644 --- a/cypress/e2e/4-node-creator.cy.ts +++ b/cypress/e2e/4-node-creator.cy.ts @@ -1,3 +1,9 @@ +import { + addNodeToCanvas, + addRetrieverNodeToParent, + addVectorStoreNodeToParent, + getNodeCreatorItems, +} from '../composables/workflow'; import { IF_NODE_NAME } from '../constants'; import { NodeCreator } from '../pages/features/node-creator'; import { NDV } from '../pages/ndv'; @@ -504,4 +510,38 @@ describe('Node Creator', () => { nodeCreatorFeature.getters.searchBar().find('input').clear().type('gith'); nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'GitHub'); }); + + it('should show vector stores actions', () => { + const actions = [ + 'Get ranked documents from vector store', + 'Add documents to vector store', + 'Retrieve documents for AI processing', + ]; + + nodeCreatorFeature.actions.openNodeCreator(); + + nodeCreatorFeature.getters.searchBar().find('input').clear().type('Vector Store'); + + getNodeCreatorItems().then((items) => { + const vectorStores = items.map((_i, el) => el.innerText); + + // Loop over all vector stores and check if they have the three actions + vectorStores.each((_i, vectorStore) => { + nodeCreatorFeature.getters.getCreatorItem(vectorStore).click(); + actions.forEach((action) => { + nodeCreatorFeature.getters.getCreatorItem(action).should('be.visible'); + }); + cy.realPress('ArrowLeft'); + }); + }); + }); + + it('should add node directly for sub-connection', () => { + addNodeToCanvas('Question and Answer Chain', true); + addRetrieverNodeToParent('Vector Store Retriever', 'Question and Answer Chain'); + cy.realPress('Escape'); + addVectorStoreNodeToParent('In-Memory Vector Store', 'Vector Store Retriever'); + cy.realPress('Escape'); + WorkflowPage.getters.canvasNodes().should('have.length', 4); + }); }); diff --git a/packages/@n8n/nodes-langchain/nodes/vector_store/shared/createVectorStoreNode.ts b/packages/@n8n/nodes-langchain/nodes/vector_store/shared/createVectorStoreNode.ts index 10ea879bd..d48796907 100644 --- a/packages/@n8n/nodes-langchain/nodes/vector_store/shared/createVectorStoreNode.ts +++ b/packages/@n8n/nodes-langchain/nodes/vector_store/shared/createVectorStoreNode.ts @@ -88,25 +88,25 @@ function getOperationModeOptions(args: VectorStoreNodeConstructorArgs): INodePro name: 'Get Many', value: 'load', description: 'Get many ranked documents from vector store for query', - action: 'Get many ranked documents from vector store for query', + action: 'Get ranked documents from vector store', }, { name: 'Insert Documents', value: 'insert', description: 'Insert documents into vector store', - action: 'Insert documents into vector store', + action: 'Add documents to vector store', }, { name: 'Retrieve Documents (For Agent/Chain)', value: 'retrieve', description: 'Retrieve documents from vector store to be used with AI nodes', - action: 'Retrieve documents from vector store to be used with AI nodes', + action: 'Retrieve documents for AI processing', }, { name: 'Update Documents', value: 'update', description: 'Update documents in vector store by ID', - action: 'Update documents in vector store by ID', + action: 'Update vector store documents', }, ]; diff --git a/packages/editor-ui/src/components/Node/NodeCreator/ItemTypes/NodeItem.vue b/packages/editor-ui/src/components/Node/NodeCreator/ItemTypes/NodeItem.vue index 499163817..9b4a8614d 100644 --- a/packages/editor-ui/src/components/Node/NodeCreator/ItemTypes/NodeItem.vue +++ b/packages/editor-ui/src/components/Node/NodeCreator/ItemTypes/NodeItem.vue @@ -14,6 +14,7 @@ import { useNodeCreatorStore } from '@/stores/nodeCreator.store'; import NodeIcon from '@/components/NodeIcon.vue'; import { useActions } from '../composables/useActions'; +import { useViewStacks } from '../composables/useViewStacks'; import { useI18n } from '@/composables/useI18n'; import { useTelemetry } from '@/composables/useTelemetry'; import { useNodeType } from '@/composables/useNodeType'; @@ -34,6 +35,7 @@ const telemetry = useTelemetry(); const { actions } = useNodeCreatorStore(); const { getAddedNodesAndConnections } = useActions(); +const { activeViewStack } = useViewStacks(); const { isSubNodeType } = useNodeType({ nodeType: props.nodeType, }); @@ -61,7 +63,7 @@ const dataTestId = computed(() => ); const hasActions = computed(() => { - return nodeActions.value.length > 1; + return nodeActions.value.length > 1 && !activeViewStack.hideActions; }); const nodeActions = computed(() => { diff --git a/packages/editor-ui/src/components/Node/NodeCreator/Modes/NodesMode.vue b/packages/editor-ui/src/components/Node/NodeCreator/Modes/NodesMode.vue index 346d67f15..08f8d14cb 100644 --- a/packages/editor-ui/src/components/Node/NodeCreator/Modes/NodesMode.vue +++ b/packages/editor-ui/src/components/Node/NodeCreator/Modes/NodesMode.vue @@ -90,7 +90,8 @@ function onSelected(item: INodeCreateElement) { if (item.type === 'node') { const nodeActions = actions?.[item.key] || []; - if (nodeActions.length <= 1) { + // Only show actions if there are more than one or if the view is not an AI subcategory + if (nodeActions.length <= 1 || activeViewStack.value.hideActions) { selectNodeType([item.key]); return; } diff --git a/packages/editor-ui/src/components/Node/NodeCreator/composables/useActionsGeneration.ts b/packages/editor-ui/src/components/Node/NodeCreator/composables/useActionsGeneration.ts index 8ada34e30..b7f0d46cf 100644 --- a/packages/editor-ui/src/components/Node/NodeCreator/composables/useActionsGeneration.ts +++ b/packages/editor-ui/src/components/Node/NodeCreator/composables/useActionsGeneration.ts @@ -1,5 +1,5 @@ import type { ActionTypeDescription, ActionsRecord, SimplifiedNodeType } from '@/Interface'; -import { CUSTOM_API_CALL_KEY, HTTP_REQUEST_NODE_TYPE } from '@/constants'; +import { AI_SUBCATEGORY, CUSTOM_API_CALL_KEY, HTTP_REQUEST_NODE_TYPE } from '@/constants'; import { memoize, startCase } from 'lodash-es'; import type { ICredentialType, @@ -97,6 +97,36 @@ function operationsCategory(nodeTypeDescription: INodeTypeDescription): ActionTy return items; } +function modeCategory(nodeTypeDescription: INodeTypeDescription): ActionTypeDescription[] { + // Mode actions should only be available for AI nodes + const isAINode = nodeTypeDescription.codex?.categories?.includes(AI_SUBCATEGORY); + if (!isAINode) return []; + + const matchedProperty = nodeTypeDescription.properties.find( + (property) => property.name?.toLowerCase() === 'mode', + ); + + if (!matchedProperty?.options) return []; + + const modeOptions = matchedProperty.options as INodePropertyOptions[]; + + const items = modeOptions.map((item: INodePropertyOptions) => ({ + ...getNodeTypeBase(nodeTypeDescription), + actionKey: item.value as string, + displayName: item.action ?? startCase(item.name), + description: item.description ?? '', + displayOptions: matchedProperty.displayOptions, + values: { + [matchedProperty.name]: item.value, + }, + })); + + // Do not return empty category + if (items.length === 0) return []; + + return items; +} + function triggersCategory(nodeTypeDescription: INodeTypeDescription): ActionTypeDescription[] { const matchingKeys = ['event', 'events', 'trigger on']; const isTrigger = nodeTypeDescription.displayName?.toLowerCase().includes('trigger'); @@ -231,7 +261,12 @@ export function useActionsGenerator() { function generateNodeActions(node: INodeTypeDescription | undefined) { if (!node) return []; if (node.codex?.subcategories?.AI?.includes('Tools')) return []; - return [...triggersCategory(node), ...operationsCategory(node), ...resourceCategories(node)]; + return [ + ...triggersCategory(node), + ...operationsCategory(node), + ...resourceCategories(node), + ...modeCategory(node), + ]; } function filterActions(actions: ActionTypeDescription[]) { // Do not show single action nodes diff --git a/packages/editor-ui/src/components/Node/NodeCreator/composables/useViewStacks.ts b/packages/editor-ui/src/components/Node/NodeCreator/composables/useViewStacks.ts index 2ce7aad64..4eb22fd85 100644 --- a/packages/editor-ui/src/components/Node/NodeCreator/composables/useViewStacks.ts +++ b/packages/editor-ui/src/components/Node/NodeCreator/composables/useViewStacks.ts @@ -68,6 +68,7 @@ interface ViewStack { searchItems?: SimplifiedNodeType[]; forceIncludeNodes?: string[]; mode?: 'actions' | 'nodes'; + hideActions?: boolean; baseFilter?: (item: INodeCreateElement) => boolean; itemsMapper?: (item: INodeCreateElement) => INodeCreateElement; panelClass?: string; @@ -344,6 +345,7 @@ export const useViewStacks = defineStore('nodeCreatorViewStacks', () => { subcategory: connectionType, }; }, + hideActions: true, preventBack: true, }); }