mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-12 15:44:06 -08:00
feat(editor): Support node-creator actions for vector store nodes (#11032)
This commit is contained in:
parent
3a9c65e1cb
commit
72b70d9d98
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue