feat(n8n Evaluation Trigger Node): Add Evaluation Trigger and Evaluation Node (#15194)

This commit is contained in:
Dana
2025-05-16 11:16:00 +02:00
committed by GitHub
parent 840a3bee4b
commit 570d1e7aad
24 changed files with 1778 additions and 6 deletions

View File

@@ -76,7 +76,7 @@ describe('NodesListPanel', () => {
await fireEvent.click(container.querySelector('.backButton')!);
await nextTick();
expect(screen.queryAllByTestId('item-iterator-item')).toHaveLength(8);
expect(screen.queryAllByTestId('item-iterator-item')).toHaveLength(9);
});
it('should render regular nodes', async () => {

View File

@@ -132,3 +132,63 @@ exports[`viewsData > AIView > should return ai view without ai transform node if
"value": "AI",
}
`;
exports[`viewsData > AIView > should return ai view without ai transform node if ask ai is not enabled and node is not in the list 1`] = `
{
"items": [
{
"key": "ai_templates_root",
"properties": {
"description": "See what's possible and get started 5x faster",
"icon": "box-open",
"name": "ai_templates_root",
"tag": {
"text": "Recommended",
"type": "info",
},
"title": "AI Templates",
"url": "template-repository-url.n8n.io?test=value&utm_user_role=AdvancedAI",
},
"type": "link",
},
{
"key": "agent",
"properties": {
"description": "example mock agent node",
"displayName": "agent",
"group": [],
"icon": "fa:pen",
"iconUrl": "nodes/test-node/icon.svg",
"name": "agent",
"title": "agent",
},
"type": "node",
},
{
"key": "chain",
"properties": {
"description": "example mock chain node",
"displayName": "chain",
"group": [],
"icon": "fa:pen",
"iconUrl": "nodes/test-node/icon.svg",
"name": "chain",
"title": "chain",
},
"type": "node",
},
{
"key": "AI Other",
"properties": {
"description": "Embeddings, Vector Stores, LLMs and other AI nodes",
"icon": "robot",
"title": "Other AI Nodes",
},
"type": "view",
},
],
"subtitle": "Select an AI Node to add to your workflow",
"title": "AI Nodes",
"value": "AI",
}
`;

View File

@@ -4,6 +4,7 @@ import {
AI_CATEGORY_TOOLS,
AI_SUBCATEGORY,
CUSTOM_API_CALL_KEY,
EVALUATION_TRIGGER,
HTTP_REQUEST_NODE_TYPE,
} from '@/constants';
import { memoize, startCase } from 'lodash-es';
@@ -19,6 +20,7 @@ import { i18n } from '@/plugins/i18n';
import { getCredentialOnlyNodeType } from '@/utils/credentialOnlyNodes';
import { formatTriggerActionName } from '../utils';
import { usePostHog } from '@/stores/posthog.store';
const PLACEHOLDER_RECOMMENDED_ACTION_KEY = 'placeholder_recommended';
@@ -330,7 +332,23 @@ export function useActionsGenerator() {
nodeTypes: INodeTypeDescription[],
httpOnlyCredentials: ICredentialType[],
) {
const visibleNodeTypes = [...nodeTypes];
const posthogStore = usePostHog();
const isEvaluationVariantEnabled = posthogStore.isVariantEnabled(
EVALUATION_TRIGGER.name,
EVALUATION_TRIGGER.variant,
);
const visibleNodeTypes = nodeTypes.filter((node) => {
if (isEvaluationVariantEnabled) {
return true;
}
return (
node.name !== 'n8n-nodes-base.evaluation' &&
node.name !== 'n8n-nodes-base.evaluationTrigger'
);
});
const actions: ActionsRecord<typeof mergedNodes> = {};
const mergedNodes: SimplifiedNodeType[] = [];
visibleNodeTypes

View File

@@ -1,5 +1,10 @@
import { NodeConnectionTypes, type INodeProperties, type INodeTypeDescription } from 'n8n-workflow';
import { useActionsGenerator } from './composables/useActionsGeneration';
import { usePostHog } from '@/stores/posthog.store';
import { createTestingPinia } from '@pinia/testing';
import { setActivePinia } from 'pinia';
let posthogStore: ReturnType<typeof usePostHog>;
describe('useActionsGenerator', () => {
const { generateMergedNodesAndActions } = useActionsGenerator();
@@ -19,6 +24,17 @@ describe('useActionsGenerator', () => {
properties: [],
};
beforeEach(() => {
vi.clearAllMocks();
const pinia = createTestingPinia({ stubActions: false });
setActivePinia(pinia);
posthogStore = usePostHog();
vi.spyOn(posthogStore, 'isVariantEnabled').mockReturnValue(true);
});
describe('App actions for resource category', () => {
const resourcePropertyWithUser: INodeProperties = {
displayName: 'Resource',
@@ -386,5 +402,48 @@ describe('useActionsGenerator', () => {
],
});
});
it('should not return evaluation or evaluation trigger node if variant is not enabled', () => {
vi.spyOn(posthogStore, 'isVariantEnabled').mockReturnValue(false);
const node: INodeTypeDescription = {
...baseV2NodeWoProps,
properties: [
resourcePropertyWithUser,
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {},
options: [
{
name: 'Get',
value: 'get',
description: 'Get description',
},
],
default: 'get',
},
],
};
const evalNode: INodeTypeDescription = {
...baseV2NodeWoProps,
name: 'n8n-nodes-base.evaluation',
};
const evalNodeTrigger: INodeTypeDescription = {
...baseV2NodeWoProps,
name: 'n8n-nodes-base.evaluationTrigger',
};
const { mergedNodes } = generateMergedNodesAndActions([node, evalNode, evalNodeTrigger], []);
mergedNodes.forEach((mergedNode) => {
expect(mergedNode.name).not.toEqual('n8n-nodes-base.evaluation');
expect(mergedNode.name).not.toEqual('n8n-nodes-base.evaluationTrigger');
});
});
});
});

View File

@@ -7,6 +7,9 @@ import { useSettingsStore } from '@/stores/settings.store';
import { AIView } from './viewsData';
import { mockNodeTypeDescription } from '@/__tests__/mocks';
import { useTemplatesStore } from '@/stores/templates.store';
import { usePostHog } from '@/stores/posthog.store';
let posthogStore: ReturnType<typeof usePostHog>;
const getNodeType = vi.fn();
@@ -51,6 +54,9 @@ describe('viewsData', () => {
beforeAll(() => {
setActivePinia(createTestingPinia());
posthogStore = usePostHog();
vi.spyOn(posthogStore, 'isVariantEnabled').mockReturnValue(true);
const templatesStore = useTemplatesStore();
vi.spyOn(templatesStore, 'websiteTemplateRepositoryParameters', 'get').mockImplementation(
@@ -86,5 +92,14 @@ describe('viewsData', () => {
expect(AIView([])).toMatchSnapshot();
});
test('should return ai view without ai transform node if ask ai is not enabled and node is not in the list', () => {
vi.spyOn(posthogStore, 'isVariantEnabled').mockReturnValue(false);
const settingsStore = useSettingsStore();
vi.spyOn(settingsStore, 'isAskAiEnabled', 'get').mockReturnValue(false);
expect(AIView([])).toMatchSnapshot();
});
});
});

View File

@@ -57,17 +57,19 @@ import {
AI_CODE_TOOL_LANGCHAIN_NODE_TYPE,
AI_WORKFLOW_TOOL_LANGCHAIN_NODE_TYPE,
HUMAN_IN_THE_LOOP_CATEGORY,
EVALUATION_TRIGGER,
} from '@/constants';
import { useI18n } from '@/composables/useI18n';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import type { SimplifiedNodeType } from '@/Interface';
import type { INodeTypeDescription, Themed } from 'n8n-workflow';
import { NodeConnectionTypes } from 'n8n-workflow';
import { EVALUATION_TRIGGER_NODE_TYPE, NodeConnectionTypes } from 'n8n-workflow';
import type { NodeConnectionType } from 'n8n-workflow';
import { useTemplatesStore } from '@/stores/templates.store';
import type { BaseTextKey } from '@/plugins/i18n';
import { camelCase } from 'lodash-es';
import { useSettingsStore } from '@/stores/settings.store';
import { usePostHog } from '@/stores/posthog.store';
export interface NodeViewItemSection {
key: string;
@@ -141,6 +143,16 @@ export function AIView(_nodes: SimplifiedNodeType[]): NodeView {
const i18n = useI18n();
const nodeTypesStore = useNodeTypesStore();
const templatesStore = useTemplatesStore();
const posthogStore = usePostHog();
const isEvaluationVariantEnabled = posthogStore.isVariantEnabled(
EVALUATION_TRIGGER.name,
EVALUATION_TRIGGER.variant,
);
const evaluationNodeStore = nodeTypesStore.getNodeType('n8n-nodes-base.evaluation');
const evaluationNode =
isEvaluationVariantEnabled && evaluationNodeStore ? [getNodeView(evaluationNodeStore)] : [];
const chainNodes = getAiNodesBySubcategory(nodeTypesStore.allLatestNodeTypes, AI_CATEGORY_CHAINS);
const agentNodes = getAiNodesBySubcategory(nodeTypesStore.allLatestNodeTypes, AI_CATEGORY_AGENTS);
@@ -177,6 +189,7 @@ export function AIView(_nodes: SimplifiedNodeType[]): NodeView {
...agentNodes,
...chainNodes,
...transformNode,
...evaluationNode,
{
key: AI_OTHERS_NODE_CREATOR_VIEW,
type: 'view',
@@ -424,6 +437,18 @@ export function TriggerView() {
icon: 'fa:comments',
},
},
{
key: EVALUATION_TRIGGER_NODE_TYPE,
type: 'node',
category: [CORE_NODES_CATEGORY],
properties: {
group: [],
name: EVALUATION_TRIGGER_NODE_TYPE,
displayName: 'Evaluation Trigger',
description: 'Run a dataset through your workflow to test performance',
icon: 'fa:check-double',
},
},
{
type: 'subcategory',
key: OTHER_TRIGGER_NODES_SUBCATEGORY,