Skip to content

Commit

Permalink
feat: Human in the loop section (#12883)
Browse files Browse the repository at this point in the history
Co-authored-by: Dana <152518854+dana-gill@users.noreply.github.com>
Co-authored-by: Jonathan Bennetts <jonathan.bennetts@gmail.com>
  • Loading branch information
3 people authored Jan 30, 2025
1 parent 0d8a544 commit 9590e5d
Show file tree
Hide file tree
Showing 15 changed files with 106 additions and 18 deletions.
9 changes: 9 additions & 0 deletions cypress/e2e/4-node-creator.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -571,4 +571,13 @@ describe('Node Creator', () => {

addVectorStoreToolToParent('In-Memory Vector Store', AGENT_NODE_NAME);
});

it('should insert node to canvas with sendAndWait operation selected', () => {
nodeCreatorFeature.getters.canvasAddButton().click();
WorkflowPage.actions.addNodeToCanvas('Manual', false);
nodeCreatorFeature.actions.openNodeCreator();
cy.contains('Human in the loop').click();
nodeCreatorFeature.getters.getCreatorItem('Slack').click();
cy.contains('Send and Wait for Response').should('exist');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
CREDENTIAL_ONLY_NODE_PREFIX,
DEFAULT_SUBCATEGORY,
DRAG_EVENT_DATA_KEY,
HITL_SUBCATEGORY,
} from '@/constants';
import { isCommunityPackageName } from '@/utils/nodeTypesUtils';
Expand Down Expand Up @@ -44,6 +45,9 @@ const draggablePosition = ref({ x: -100, y: -100 });
const draggableDataTransfer = ref(null as Element | null);
const description = computed<string>(() => {
if (isSendAndWaitCategory.value) {
return '';
}
if (
props.subcategory === DEFAULT_SUBCATEGORY &&
!props.nodeType.name.startsWith(CREDENTIAL_ONLY_NODE_PREFIX)
Expand All @@ -56,7 +60,8 @@ const description = computed<string>(() => {
fallback: props.nodeType.description,
});
});
const showActionArrow = computed(() => hasActions.value);
const showActionArrow = computed(() => hasActions.value && !isSendAndWaitCategory.value);
const isSendAndWaitCategory = computed(() => activeViewStack.subcategory === HITL_SUBCATEGORY);
const dataTestId = computed(() =>
hasActions.value ? 'node-creator-action-item' : 'node-creator-node-item',
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
<script setup lang="ts">
import { camelCase } from 'lodash-es';
import { computed } from 'vue';
import type { INodeCreateElement, NodeCreateElement, NodeFilterType } from '@/Interface';
import type {
ActionTypeDescription,
INodeCreateElement,
NodeCreateElement,
NodeFilterType,
} from '@/Interface';
import {
TRIGGER_NODE_CREATOR_VIEW,
HTTP_REQUEST_NODE_TYPE,
WEBHOOK_NODE_TYPE,
REGULAR_NODE_CREATOR_VIEW,
AI_NODE_CREATOR_VIEW,
AI_OTHERS_NODE_CREATOR_VIEW,
HITL_SUBCATEGORY,
} from '@/constants';
import type { BaseTextKey } from '@/plugins/i18n';
Expand All @@ -26,7 +32,7 @@ import { useI18n } from '@/composables/useI18n';
import { getNodeIcon, getNodeIconColor, getNodeIconUrl } from '@/utils/nodeTypesUtils';
import { useUIStore } from '@/stores/ui.store';
import { useActions } from '../composables/useActions';
import type { INodeParameters } from 'n8n-workflow';
import { SEND_AND_WAIT_OPERATION, type INodeParameters } from 'n8n-workflow';
export interface Props {
rootView: 'trigger' | 'action';
Expand All @@ -51,12 +57,19 @@ const globalSearchItemsDiff = computed(() => useViewStacks().globalSearchItemsDi
function getFilteredActions(node: NodeCreateElement) {
const nodeActions = actions?.[node.key] || [];
if (activeViewStack.value.subcategory === HITL_SUBCATEGORY) {
return getHumanInTheLoopActions(nodeActions);
}
if (activeViewStack.value.actionsFilter) {
return activeViewStack.value.actionsFilter(nodeActions);
}
return nodeActions;
}
function getHumanInTheLoopActions(nodeActions: ActionTypeDescription[]) {
return nodeActions.filter((action) => action.actionKey === SEND_AND_WAIT_OPERATION);
}
function selectNodeType(nodeTypes: string[]) {
emit('nodeTypeSelected', nodeTypes);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ describe('NodesListPanel', () => {

await nextTick();
expect(screen.getByText('What happens next?')).toBeInTheDocument();
expect(screen.queryAllByTestId('item-iterator-item')).toHaveLength(5);
expect(screen.queryAllByTestId('item-iterator-item')).toHaveLength(6);

screen.getByText('Action in an app').click();
await nextTick();
Expand Down
18 changes: 16 additions & 2 deletions packages/editor-ui/src/components/Node/NodeCreator/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
AI_TRANSFORM_NODE_TYPE,
CORE_NODES_CATEGORY,
DEFAULT_SUBCATEGORY,
HUMAN_IN_THE_LOOP_CATEGORY,
} from '@/constants';
import { v4 as uuidv4 } from 'uuid';

Expand All @@ -23,6 +24,7 @@ import { sortBy } from 'lodash-es';
import * as changeCase from 'change-case';

import { useSettingsStore } from '@/stores/settings.store';
import { SEND_AND_WAIT_OPERATION } from 'n8n-workflow';

export function transformNodeType(
node: SimplifiedNodeType,
Expand All @@ -46,7 +48,11 @@ export function transformNodeType(
}

export function subcategorizeItems(items: SimplifiedNodeType[]) {
const WHITE_LISTED_SUBCATEGORIES = [CORE_NODES_CATEGORY, AI_SUBCATEGORY];
const WHITE_LISTED_SUBCATEGORIES = [
CORE_NODES_CATEGORY,
AI_SUBCATEGORY,
HUMAN_IN_THE_LOOP_CATEGORY,
];
return items.reduce((acc: SubcategorizedNodeTypes, item) => {
// Only some subcategories are allowed
let subcategories: string[] = [DEFAULT_SUBCATEGORY];
Expand Down Expand Up @@ -174,13 +180,21 @@ export function groupItemsInSections(

return 0;
});
if (result.length <= 1) {
if (!shouldRenderSectionSubtitle(result)) {
return items;
}

return result;
}

const shouldRenderSectionSubtitle = (sections: SectionCreateElement[]) => {
if (!sections.length) return false;
if (sections.length > 1) return true;
if (sections[0].key === SEND_AND_WAIT_OPERATION) return true;

return false;
};

export const formatTriggerActionName = (actionPropertyName: string) => {
let name = actionPropertyName;
if (actionPropertyName.includes('.')) {
Expand Down
35 changes: 30 additions & 5 deletions packages/editor-ui/src/components/Node/NodeCreator/viewsData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,14 @@ import {
SPLIT_IN_BATCHES_NODE_TYPE,
HTTP_REQUEST_NODE_TYPE,
HELPERS_SUBCATEGORY,
HITL_SUBCATEGORY,
RSS_READ_NODE_TYPE,
EMAIL_SEND_NODE_TYPE,
EDIT_IMAGE_NODE_TYPE,
COMPRESSION_NODE_TYPE,
AI_CODE_TOOL_LANGCHAIN_NODE_TYPE,
AI_WORKFLOW_TOOL_LANGCHAIN_NODE_TYPE,
HUMAN_IN_THE_LOOP_CATEGORY,
} from '@/constants';
import { useI18n } from '@/composables/useI18n';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
Expand Down Expand Up @@ -442,6 +444,12 @@ export function RegularView(nodes: SimplifiedNodeType[]) {
AI_TRANSFORM_NODE_TYPE,
];

const getSendAndWaitNodes = (nodes: SimplifiedNodeType[]) => {
return (nodes ?? [])
.filter((node) => node.codex?.categories?.includes(HUMAN_IN_THE_LOOP_CATEGORY))
.map((node) => node.name);
};

const view: NodeView = {
value: REGULAR_NODE_CREATOR_VIEW,
title: i18n.baseText('nodeCreator.triggerHelperPanel.whatHappensNext'),
Expand Down Expand Up @@ -532,22 +540,39 @@ export function RegularView(nodes: SimplifiedNodeType[]) {
],
},
},
// To add node to this subcategory:
// - add "HITL" to the "categories" property of the node's codex
// - add "HITL": ["Human in the Loop"] to the "subcategories" property of the node's codex
// node has to have the "sendAndWait" operation, if a new operation needs to be included here:
// - update getHumanInTheLoopActions in packages/editor-ui/src/components/Node/NodeCreator/Modes/NodesMode.vue
{
type: 'subcategory',
key: HITL_SUBCATEGORY,
category: HUMAN_IN_THE_LOOP_CATEGORY,
properties: {
title: HITL_SUBCATEGORY,
icon: 'user-check',
sections: [
{
key: 'sendAndWait',
title: i18n.baseText('nodeCreator.sectionNames.sendAndWait'),
items: getSendAndWaitNodes(nodes),
},
],
},
},
],
};

const hasAINodes = (nodes ?? []).some((node) => node.codex?.categories?.includes(AI_SUBCATEGORY));
if (hasAINodes)
view.items.push({
view.items.unshift({
key: AI_NODE_CREATOR_VIEW,
type: 'view',
properties: {
title: i18n.baseText('nodeCreator.aiPanel.langchainAiNodes'),
icon: 'robot',
description: i18n.baseText('nodeCreator.aiPanel.nodesForAi'),
tag: {
type: 'success',
text: i18n.baseText('nodeCreator.aiPanel.newTag'),
},
borderless: true,
},
} as NodeViewItem);
Expand Down
2 changes: 2 additions & 0 deletions packages/editor-ui/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,7 @@ export const NODE_CREATOR_OPEN_SOURCES: Record<
'': '',
};
export const CORE_NODES_CATEGORY = 'Core Nodes';
export const HUMAN_IN_THE_LOOP_CATEGORY = 'HITL';
export const CUSTOM_NODES_CATEGORY = 'Custom Nodes';
export const DEFAULT_SUBCATEGORY = '*';
export const AI_OTHERS_NODE_CREATOR_VIEW = 'AI Other';
Expand All @@ -274,6 +275,7 @@ export const FILES_SUBCATEGORY = 'Files';
export const FLOWS_CONTROL_SUBCATEGORY = 'Flow';
export const AI_SUBCATEGORY = 'AI';
export const HELPERS_SUBCATEGORY = 'Helpers';
export const HITL_SUBCATEGORY = 'Human in the Loop';
export const AI_CATEGORY_AGENTS = 'Agents';
export const AI_CATEGORY_CHAINS = 'Chains';
export const AI_CATEGORY_LANGUAGE_MODELS = 'Language Models';
Expand Down
3 changes: 3 additions & 0 deletions packages/editor-ui/src/plugins/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1115,6 +1115,7 @@
"nodeCreator.subcategoryDescriptions.tools": "Utility components providing various functionalities.",
"nodeCreator.subcategoryDescriptions.vectorStores": "Handles storage and retrieval of vector representations.",
"nodeCreator.subcategoryDescriptions.miscellaneous": "Other AI related nodes.",
"nodeCreator.subcategoryDescriptions.humanInTheLoop": "Wait for approval or human input before continuing",
"nodeCreator.subcategoryInfos.languageModels": "Chat models are designed for interactive conversations and follow instructions well, while text completion models focus on generating continuations of a given text input",
"nodeCreator.subcategoryInfos.memory": "Memory allows an AI model to remember and reference past interactions with it",
"nodeCreator.subcategoryInfos.vectorStores": "Vector stores allow an AI model to reference relevant pieces of documents, useful for question answering and document search",
Expand All @@ -1137,8 +1138,10 @@
"nodeCreator.subcategoryNames.tools": "Tools",
"nodeCreator.subcategoryNames.vectorStores": "Vector Stores",
"nodeCreator.subcategoryNames.miscellaneous": "Miscellaneous",
"nodeCreator.subcategoryNames.humanInTheLoop": "Human in the loop",
"nodeCreator.sectionNames.popular": "Popular",
"nodeCreator.sectionNames.other": "Other",
"nodeCreator.sectionNames.sendAndWait": "Send and wait for response",
"nodeCreator.sectionNames.transform.combine": "Combine items",
"nodeCreator.sectionNames.transform.addOrRemove": "Add or remove items",
"nodeCreator.sectionNames.transform.convert": "Convert data",
Expand Down
2 changes: 2 additions & 0 deletions packages/editor-ui/src/plugins/icons/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ import {
faHandScissors,
faHandPointLeft,
faHandshake,
faUserCheck,
faHashtag,
faHdd,
faHistory,
Expand Down Expand Up @@ -258,6 +259,7 @@ export const FontAwesomePlugin: Plugin = {
addIcon(faHandshake);
addIcon(faHandPointLeft);
addIcon(faHashtag);
addIcon(faUserCheck);
addIcon(faHdd);
addIcon(faHistory);
addIcon(faHome);
Expand Down
5 changes: 4 additions & 1 deletion packages/nodes-base/nodes/EmailSend/EmailSend.node.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
"node": "n8n-nodes-base.emailSend",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": ["Communication", "Core Nodes"],
"categories": ["Communication", "HITL", "Core Nodes"],
"subcategories": {
"HITL": ["Human in the Loop"]
},
"resources": {
"credentialDocumentation": [
{
Expand Down
4 changes: 2 additions & 2 deletions packages/nodes-base/nodes/Google/Chat/GoogleChat.node.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
"node": "n8n-nodes-base.googleChat",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": ["Communication", "HILT"],
"categories": ["Communication", "HITL"],
"subcategories": {
"HILT": ["Human in the Loop"]
"HITL": ["Human in the Loop"]
},
"resources": {
"credentialDocumentation": [
Expand Down
5 changes: 4 additions & 1 deletion packages/nodes-base/nodes/Google/Gmail/Gmail.node.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
"node": "n8n-nodes-base.gmail",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": ["Communication"],
"categories": ["Communication", "HITL"],
"subcategories": {
"HITL": ["Human in the Loop"]
},
"resources": {
"credentialDocumentation": [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
"node": "n8n-nodes-base.microsoftOutlook",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": ["Communication"],
"categories": ["Communication", "HITL"],
"subcategories": {
"HITL": ["Human in the Loop"]
},
"resources": {
"credentialDocumentation": [
{
Expand Down
5 changes: 4 additions & 1 deletion packages/nodes-base/nodes/Slack/Slack.node.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
"node": "n8n-nodes-base.slack",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": ["Communication"],
"categories": ["Communication", "HITL"],
"subcategories": {
"HITL": ["Human in the Loop"]
},
"alias": ["human", "form", "wait"],
"resources": {
"credentialDocumentation": [
Expand Down
5 changes: 4 additions & 1 deletion packages/nodes-base/nodes/Telegram/Telegram.node.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
"node": "n8n-nodes-base.telegram",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": ["Communication"],
"categories": ["Communication", "HITL"],
"subcategories": {
"HITL": ["Human in the Loop"]
},
"resources": {
"credentialDocumentation": [
{
Expand Down

0 comments on commit 9590e5d

Please sign in to comment.