mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 01:56:46 +00:00
feat: Add AI tool building capabilities (#7336)
Github issue / Community forum post (link here to close automatically): https://community.n8n.io/t/langchain-memory-chat/23733 --------- Signed-off-by: Oleg Ivaniv <me@olegivaniv.com> Co-authored-by: Oleg Ivaniv <me@olegivaniv.com> Co-authored-by: Val <68596159+valya@users.noreply.github.com> Co-authored-by: Alex Grozav <alex@grozav.com> Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <aditya@netroy.in> Co-authored-by: Deborah <deborah@starfallprojects.co.uk> Co-authored-by: Jesper Bylund <mail@jesperbylund.com> Co-authored-by: Jon <jonathan.bennetts@gmail.com> Co-authored-by: Michael Kret <88898367+michael-radency@users.noreply.github.com> Co-authored-by: Giulio Andreini <andreini@netseven.it> Co-authored-by: Mason Geloso <Mason.geloso@gmail.com> Co-authored-by: Mason Geloso <hone@Masons-Mac-mini.local> Co-authored-by: Mutasem Aldmour <mutasem@n8n.io>
This commit is contained in:
48
cypress/composables/modals/chat-modal.ts
Normal file
48
cypress/composables/modals/chat-modal.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Getters
|
||||
*/
|
||||
|
||||
export function getManualChatModal() {
|
||||
return cy.getByTestId('lmChat-modal');
|
||||
}
|
||||
|
||||
export function getManualChatInput() {
|
||||
return cy.getByTestId('workflow-chat-input');
|
||||
}
|
||||
|
||||
export function getManualChatSendButton() {
|
||||
return getManualChatModal().getByTestId('workflow-chat-send-button');
|
||||
}
|
||||
|
||||
export function getManualChatMessages() {
|
||||
return getManualChatModal().get('.messages .message');
|
||||
}
|
||||
|
||||
export function getManualChatModalCloseButton() {
|
||||
return getManualChatModal().get('.el-dialog__close');
|
||||
}
|
||||
|
||||
export function getManualChatModalLogs() {
|
||||
return getManualChatModal().getByTestId('lm-chat-logs');
|
||||
}
|
||||
|
||||
export function getManualChatModalLogsTree() {
|
||||
return getManualChatModalLogs().getByTestId('lm-chat-logs-tree');
|
||||
}
|
||||
|
||||
export function getManualChatModalLogsEntries() {
|
||||
return getManualChatModalLogs().getByTestId('lm-chat-logs-entry');
|
||||
}
|
||||
|
||||
/**
|
||||
* Actions
|
||||
*/
|
||||
|
||||
export function sendManualChatMessage(message: string) {
|
||||
getManualChatInput().type(message);
|
||||
getManualChatSendButton().click();
|
||||
}
|
||||
|
||||
export function closeManualChatModal() {
|
||||
getManualChatModalCloseButton().click();
|
||||
}
|
||||
54
cypress/composables/modals/credential-modal.ts
Normal file
54
cypress/composables/modals/credential-modal.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Getters
|
||||
*/
|
||||
|
||||
export function getCredentialConnectionParameterInputs() {
|
||||
return cy.getByTestId('credential-connection-parameter');
|
||||
}
|
||||
|
||||
export function getCredentialConnectionParameterInputByName(name: string) {
|
||||
return cy.getByTestId(`parameter-input-${name}`);
|
||||
}
|
||||
|
||||
export function getEditCredentialModal() {
|
||||
return cy.getByTestId('editCredential-modal', { timeout: 5000 });
|
||||
}
|
||||
|
||||
export function getCredentialSaveButton() {
|
||||
return cy.getByTestId('credential-save-button', { timeout: 5000 });
|
||||
}
|
||||
|
||||
export function getCredentialDeleteButton() {
|
||||
return cy.getByTestId('credential-delete-button');
|
||||
}
|
||||
|
||||
export function getCredentialModalCloseButton() {
|
||||
return getEditCredentialModal().find('.el-dialog__close').first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Actions
|
||||
*/
|
||||
|
||||
export function setCredentialConnectionParameterInputByName(name: string, value: string) {
|
||||
getCredentialConnectionParameterInputByName(name).type(value);
|
||||
}
|
||||
|
||||
export function saveCredential() {
|
||||
getCredentialSaveButton().click({ force: true });
|
||||
}
|
||||
|
||||
export function closeCredentialModal() {
|
||||
getCredentialModalCloseButton().click();
|
||||
}
|
||||
|
||||
export function setCredentialValues(values: Record<string, any>, save = true) {
|
||||
Object.entries(values).forEach(([key, value]) => {
|
||||
setCredentialConnectionParameterInputByName(key, value);
|
||||
});
|
||||
|
||||
if (save) {
|
||||
saveCredential();
|
||||
closeCredentialModal();
|
||||
}
|
||||
}
|
||||
73
cypress/composables/ndv.ts
Normal file
73
cypress/composables/ndv.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* Getters
|
||||
*/
|
||||
|
||||
export function getCredentialSelect(eq = 0) {
|
||||
return cy.getByTestId('node-credentials-select').eq(eq);
|
||||
}
|
||||
|
||||
export function getCreateNewCredentialOption() {
|
||||
return cy.getByTestId('node-credentials-select-item-new');
|
||||
}
|
||||
|
||||
export function getBackToCanvasButton() {
|
||||
return cy.getByTestId('back-to-canvas');
|
||||
}
|
||||
|
||||
export function getExecuteNodeButton() {
|
||||
return cy.getByTestId('node-execute-button');
|
||||
}
|
||||
|
||||
export function getParameterInputByName(name: string) {
|
||||
return cy.getByTestId(`parameter-input-${name}`);
|
||||
}
|
||||
|
||||
export function getInputPanel() {
|
||||
return cy.getByTestId('input-panel');
|
||||
}
|
||||
|
||||
export function getMainPanel() {
|
||||
return cy.getByTestId('node-parameters');
|
||||
}
|
||||
|
||||
export function getOutputPanel() {
|
||||
return cy.getByTestId('output-panel');
|
||||
}
|
||||
|
||||
export function getOutputPanelDataContainer() {
|
||||
return getOutputPanel().getByTestId('ndv-data-container');
|
||||
}
|
||||
|
||||
export function getOutputPanelTable() {
|
||||
return getOutputPanelDataContainer().get('table');
|
||||
}
|
||||
|
||||
/**
|
||||
* Actions
|
||||
*/
|
||||
|
||||
export function openCredentialSelect(eq = 0) {
|
||||
getCredentialSelect(eq).click();
|
||||
}
|
||||
|
||||
export function setCredentialByName(name: string) {
|
||||
openCredentialSelect();
|
||||
getCredentialSelect().contains(name).click();
|
||||
}
|
||||
|
||||
export function clickCreateNewCredential() {
|
||||
openCredentialSelect();
|
||||
getCreateNewCredentialOption().click();
|
||||
}
|
||||
|
||||
export function clickGetBackToCanvas() {
|
||||
getBackToCanvasButton().click();
|
||||
}
|
||||
|
||||
export function clickExecuteNode() {
|
||||
getExecuteNodeButton().click();
|
||||
}
|
||||
|
||||
export function setParameterInputByName(name: string, value: string) {
|
||||
getParameterInputByName(name).clear().type(value);
|
||||
}
|
||||
142
cypress/composables/workflow.ts
Normal file
142
cypress/composables/workflow.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
import { ROUTES } from '../constants';
|
||||
import { getManualChatModal } from './modals/chat-modal';
|
||||
|
||||
/**
|
||||
* Types
|
||||
*/
|
||||
|
||||
export type EndpointType =
|
||||
| 'ai_chain'
|
||||
| 'ai_document'
|
||||
| 'ai_embedding'
|
||||
| 'ai_languageModel'
|
||||
| 'ai_memory'
|
||||
| 'ai_outputParser'
|
||||
| 'ai_tool'
|
||||
| 'ai_retriever'
|
||||
| 'ai_textSplitter'
|
||||
| 'ai_vectorRetriever'
|
||||
| 'ai_vectorStore';
|
||||
|
||||
/**
|
||||
* Getters
|
||||
*/
|
||||
|
||||
export function getAddInputEndpointByType(nodeName: string, endpointType: EndpointType) {
|
||||
return cy.get(
|
||||
`.add-input-endpoint[data-jtk-scope-${endpointType}][data-endpoint-name="${nodeName}"]`,
|
||||
);
|
||||
}
|
||||
|
||||
export function getNodeCreatorItems() {
|
||||
return cy.getByTestId('item-iterator-item');
|
||||
}
|
||||
|
||||
export function getExecuteWorkflowButton() {
|
||||
return cy.getByTestId('execute-workflow-button');
|
||||
}
|
||||
|
||||
export function getManualChatButton() {
|
||||
return cy.getByTestId('workflow-chat-button');
|
||||
}
|
||||
|
||||
export function getNodes() {
|
||||
return cy.getByTestId('canvas-node');
|
||||
}
|
||||
|
||||
export function getNodeByName(name: string) {
|
||||
return cy.getByTestId('canvas-node').filter(`[data-name="${name}"]`).eq(0);
|
||||
}
|
||||
|
||||
export function getConnectionBySourceAndTarget(source: string, target: string) {
|
||||
return cy
|
||||
.get('.jtk-connector')
|
||||
.filter(`[data-source-node="${source}"][data-target-node="${target}"]`)
|
||||
.eq(0);
|
||||
}
|
||||
|
||||
export function getNodeCreatorSearchBar() {
|
||||
return cy.getByTestId('node-creator-search-bar');
|
||||
}
|
||||
|
||||
export function getNodeCreatorPlusButton() {
|
||||
return cy.getByTestId('node-creator-plus-button');
|
||||
}
|
||||
|
||||
/**
|
||||
* Actions
|
||||
*/
|
||||
|
||||
export function addNodeToCanvas(
|
||||
nodeDisplayName: string,
|
||||
plusButtonClick = true,
|
||||
preventNdvClose?: boolean,
|
||||
action?: string,
|
||||
) {
|
||||
if (plusButtonClick) {
|
||||
getNodeCreatorPlusButton().click();
|
||||
}
|
||||
|
||||
getNodeCreatorSearchBar().type(nodeDisplayName);
|
||||
getNodeCreatorSearchBar().type('{enter}');
|
||||
cy.wait(500);
|
||||
cy.get('body').then((body) => {
|
||||
if (body.find('[data-test-id=node-creator]').length > 0) {
|
||||
if (action) {
|
||||
cy.contains(action).click();
|
||||
} else {
|
||||
// Select the first action
|
||||
cy.get('[data-keyboard-nav-type="action"]').eq(0).click();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!preventNdvClose) cy.get('body').type('{esc}');
|
||||
}
|
||||
|
||||
export function navigateToNewWorkflowPage(preventNodeViewUnload = true) {
|
||||
cy.visit(ROUTES.NEW_WORKFLOW_PAGE);
|
||||
cy.waitForLoad();
|
||||
cy.window().then((win) => {
|
||||
win.preventNodeViewBeforeUnload = preventNodeViewUnload;
|
||||
});
|
||||
}
|
||||
|
||||
export function addSupplementalNodeToParent(
|
||||
nodeName: string,
|
||||
endpointType: EndpointType,
|
||||
parentNodeName: string,
|
||||
) {
|
||||
getAddInputEndpointByType(parentNodeName, endpointType).click({ force: true });
|
||||
getNodeCreatorItems().contains(nodeName).click();
|
||||
getConnectionBySourceAndTarget(parentNodeName, nodeName).should('exist');
|
||||
}
|
||||
|
||||
export function addLanguageModelNodeToParent(nodeName: string, parentNodeName: string) {
|
||||
addSupplementalNodeToParent(nodeName, 'ai_languageModel', parentNodeName);
|
||||
}
|
||||
|
||||
export function addMemoryNodeToParent(nodeName: string, parentNodeName: string) {
|
||||
addSupplementalNodeToParent(nodeName, 'ai_memory', parentNodeName);
|
||||
}
|
||||
|
||||
export function addToolNodeToParent(nodeName: string, parentNodeName: string) {
|
||||
addSupplementalNodeToParent(nodeName, 'ai_tool', parentNodeName);
|
||||
}
|
||||
|
||||
export function addOutputParserNodeToParent(nodeName: string, parentNodeName: string) {
|
||||
addSupplementalNodeToParent(nodeName, 'ai_outputParser', parentNodeName);
|
||||
}
|
||||
|
||||
export function clickExecuteWorkflowButton() {
|
||||
getExecuteWorkflowButton().click();
|
||||
}
|
||||
|
||||
export function clickManualChatButton() {
|
||||
getManualChatButton().click();
|
||||
getManualChatModal().should('be.visible');
|
||||
}
|
||||
|
||||
export function openNode(nodeName: string) {
|
||||
getNodeByName(nodeName).dblclick();
|
||||
}
|
||||
@@ -29,6 +29,7 @@ export const INSTANCE_MEMBERS = [
|
||||
|
||||
export const MANUAL_TRIGGER_NODE_NAME = 'Manual Trigger';
|
||||
export const MANUAL_TRIGGER_NODE_DISPLAY_NAME = 'When clicking "Execute Workflow"';
|
||||
export const MANUAL_CHAT_TRIGGER_NODE_NAME = 'Manual Chat Trigger';
|
||||
export const SCHEDULE_TRIGGER_NODE_NAME = 'Schedule Trigger';
|
||||
export const CODE_NODE_NAME = 'Code';
|
||||
export const SET_NODE_NAME = 'Set';
|
||||
@@ -41,6 +42,14 @@ export const TRELLO_NODE_NAME = 'Trello';
|
||||
export const NOTION_NODE_NAME = 'Notion';
|
||||
export const PIPEDRIVE_NODE_NAME = 'Pipedrive';
|
||||
export const HTTP_REQUEST_NODE_NAME = 'HTTP Request';
|
||||
export const AGENT_NODE_NAME = 'AI Agent';
|
||||
export const BASIC_LLM_CHAIN_NODE_NAME = 'Basic LLM Chain';
|
||||
export const AI_MEMORY_WINDOW_BUFFER_MEMORY_NODE_NAME = 'Window Buffer Memory';
|
||||
export const AI_TOOL_CALCULATOR_NODE_NAME = 'Calculator';
|
||||
export const AI_TOOL_CODE_NODE_NAME = 'Custom Code Tool';
|
||||
export const AI_TOOL_WIKIPEDIA_NODE_NAME = 'Wikipedia';
|
||||
export const AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME = 'OpenAI Chat Model';
|
||||
export const AI_OUTPUT_PARSER_AUTO_FIXING_NODE_NAME = 'Auto-fixing Output Parser';
|
||||
|
||||
export const META_KEY = Cypress.platform === 'darwin' ? '{meta}' : '{ctrl}';
|
||||
|
||||
@@ -48,3 +57,7 @@ export const NEW_GOOGLE_ACCOUNT_NAME = 'Gmail account';
|
||||
export const NEW_TRELLO_ACCOUNT_NAME = 'Trello account';
|
||||
export const NEW_NOTION_ACCOUNT_NAME = 'Notion account';
|
||||
export const NEW_QUERY_AUTH_ACCOUNT_NAME = 'Query Auth account';
|
||||
|
||||
export const ROUTES = {
|
||||
NEW_WORKFLOW_PAGE: '/workflow/new',
|
||||
};
|
||||
|
||||
278
cypress/e2e/30-langchain.cy.ts
Normal file
278
cypress/e2e/30-langchain.cy.ts
Normal file
@@ -0,0 +1,278 @@
|
||||
import {
|
||||
AGENT_NODE_NAME,
|
||||
MANUAL_CHAT_TRIGGER_NODE_NAME,
|
||||
AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME,
|
||||
MANUAL_TRIGGER_NODE_NAME,
|
||||
AI_MEMORY_WINDOW_BUFFER_MEMORY_NODE_NAME,
|
||||
AI_TOOL_CALCULATOR_NODE_NAME,
|
||||
AI_OUTPUT_PARSER_AUTO_FIXING_NODE_NAME,
|
||||
AI_TOOL_CODE_NODE_NAME,
|
||||
AI_TOOL_WIKIPEDIA_NODE_NAME,
|
||||
BASIC_LLM_CHAIN_NODE_NAME,
|
||||
} from './../constants';
|
||||
import { createMockNodeExecutionData, runMockWorkflowExcution } from '../utils';
|
||||
import {
|
||||
addLanguageModelNodeToParent,
|
||||
addMemoryNodeToParent,
|
||||
addNodeToCanvas,
|
||||
addOutputParserNodeToParent,
|
||||
addToolNodeToParent,
|
||||
clickManualChatButton,
|
||||
navigateToNewWorkflowPage,
|
||||
openNode,
|
||||
} from '../composables/workflow';
|
||||
import {
|
||||
clickCreateNewCredential,
|
||||
clickExecuteNode,
|
||||
clickGetBackToCanvas,
|
||||
getOutputPanelTable,
|
||||
setParameterInputByName,
|
||||
} from '../composables/ndv';
|
||||
import { setCredentialValues } from '../composables/modals/credential-modal';
|
||||
import {
|
||||
closeManualChatModal,
|
||||
getManualChatMessages,
|
||||
getManualChatModalLogs,
|
||||
getManualChatModalLogsEntries,
|
||||
getManualChatModalLogsTree,
|
||||
sendManualChatMessage,
|
||||
} from '../composables/modals/chat-modal';
|
||||
|
||||
describe('Langchain Integration', () => {
|
||||
beforeEach(() => {
|
||||
navigateToNewWorkflowPage();
|
||||
});
|
||||
|
||||
it('should add nodes to all Agent node input types', () => {
|
||||
addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME, true);
|
||||
addNodeToCanvas(AGENT_NODE_NAME, true);
|
||||
|
||||
addLanguageModelNodeToParent(AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, AGENT_NODE_NAME);
|
||||
clickGetBackToCanvas();
|
||||
|
||||
addMemoryNodeToParent(AI_MEMORY_WINDOW_BUFFER_MEMORY_NODE_NAME, AGENT_NODE_NAME);
|
||||
clickGetBackToCanvas();
|
||||
|
||||
addToolNodeToParent(AI_TOOL_CALCULATOR_NODE_NAME, AGENT_NODE_NAME);
|
||||
clickGetBackToCanvas();
|
||||
|
||||
addOutputParserNodeToParent(AI_OUTPUT_PARSER_AUTO_FIXING_NODE_NAME, AGENT_NODE_NAME);
|
||||
clickGetBackToCanvas();
|
||||
});
|
||||
|
||||
it('should add multiple tool nodes to Agent node tool input type', () => {
|
||||
addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME, true);
|
||||
addNodeToCanvas(AGENT_NODE_NAME, true);
|
||||
|
||||
[
|
||||
AI_TOOL_CALCULATOR_NODE_NAME,
|
||||
AI_TOOL_CODE_NODE_NAME,
|
||||
AI_TOOL_CODE_NODE_NAME,
|
||||
AI_TOOL_CODE_NODE_NAME,
|
||||
AI_TOOL_WIKIPEDIA_NODE_NAME,
|
||||
].forEach((tool) => {
|
||||
addToolNodeToParent(tool, AGENT_NODE_NAME);
|
||||
clickGetBackToCanvas();
|
||||
});
|
||||
});
|
||||
|
||||
it('should be able to open and execute Basic LLM Chain node', () => {
|
||||
addNodeToCanvas(MANUAL_CHAT_TRIGGER_NODE_NAME, true);
|
||||
addNodeToCanvas(BASIC_LLM_CHAIN_NODE_NAME, true);
|
||||
|
||||
addLanguageModelNodeToParent(
|
||||
AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME,
|
||||
BASIC_LLM_CHAIN_NODE_NAME,
|
||||
);
|
||||
|
||||
clickCreateNewCredential();
|
||||
setCredentialValues({
|
||||
apiKey: 'sk_test_123',
|
||||
});
|
||||
clickGetBackToCanvas();
|
||||
|
||||
openNode(BASIC_LLM_CHAIN_NODE_NAME);
|
||||
|
||||
const inputMessage = 'Hello!';
|
||||
const outputMessage = 'Hi there! How can I assist you today?';
|
||||
|
||||
setParameterInputByName('prompt', inputMessage);
|
||||
|
||||
runMockWorkflowExcution({
|
||||
trigger: () => clickExecuteNode(),
|
||||
runData: [
|
||||
createMockNodeExecutionData(BASIC_LLM_CHAIN_NODE_NAME, {
|
||||
jsonData: {
|
||||
main: { output: outputMessage },
|
||||
},
|
||||
metadata: {
|
||||
subRun: [{ node: AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, runIndex: 0 }],
|
||||
},
|
||||
}),
|
||||
],
|
||||
lastNodeExecuted: BASIC_LLM_CHAIN_NODE_NAME,
|
||||
});
|
||||
|
||||
getOutputPanelTable().should('contain', 'output');
|
||||
getOutputPanelTable().should('contain', outputMessage);
|
||||
});
|
||||
|
||||
it('should be able to open and execute Agent node', () => {
|
||||
addNodeToCanvas(MANUAL_CHAT_TRIGGER_NODE_NAME, true);
|
||||
addNodeToCanvas(AGENT_NODE_NAME, true);
|
||||
|
||||
addLanguageModelNodeToParent(AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, AGENT_NODE_NAME);
|
||||
|
||||
clickCreateNewCredential();
|
||||
setCredentialValues({
|
||||
apiKey: 'sk_test_123',
|
||||
});
|
||||
clickGetBackToCanvas();
|
||||
|
||||
openNode(AGENT_NODE_NAME);
|
||||
|
||||
const inputMessage = 'Hello!';
|
||||
const outputMessage = 'Hi there! How can I assist you today?';
|
||||
|
||||
setParameterInputByName('text', inputMessage);
|
||||
|
||||
runMockWorkflowExcution({
|
||||
trigger: () => clickExecuteNode(),
|
||||
runData: [
|
||||
createMockNodeExecutionData(AGENT_NODE_NAME, {
|
||||
jsonData: {
|
||||
main: { output: outputMessage },
|
||||
},
|
||||
metadata: {
|
||||
subRun: [{ node: AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, runIndex: 0 }],
|
||||
},
|
||||
}),
|
||||
],
|
||||
lastNodeExecuted: AGENT_NODE_NAME,
|
||||
});
|
||||
|
||||
getOutputPanelTable().should('contain', 'output');
|
||||
getOutputPanelTable().should('contain', outputMessage);
|
||||
});
|
||||
|
||||
it('should add and use Manual Chat Trigger node together with Agent node', () => {
|
||||
addNodeToCanvas(MANUAL_CHAT_TRIGGER_NODE_NAME, true);
|
||||
addNodeToCanvas(AGENT_NODE_NAME, true);
|
||||
|
||||
addLanguageModelNodeToParent(AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, AGENT_NODE_NAME);
|
||||
|
||||
clickCreateNewCredential();
|
||||
setCredentialValues({
|
||||
apiKey: 'sk_test_123',
|
||||
});
|
||||
clickGetBackToCanvas();
|
||||
|
||||
clickManualChatButton();
|
||||
|
||||
getManualChatModalLogs().should('not.exist');
|
||||
|
||||
const inputMessage = 'Hello!';
|
||||
const outputMessage = 'Hi there! How can I assist you today?';
|
||||
|
||||
runMockWorkflowExcution({
|
||||
trigger: () => {
|
||||
sendManualChatMessage(inputMessage);
|
||||
},
|
||||
runData: [
|
||||
createMockNodeExecutionData(MANUAL_CHAT_TRIGGER_NODE_NAME, {
|
||||
jsonData: {
|
||||
main: { input: inputMessage },
|
||||
},
|
||||
}),
|
||||
createMockNodeExecutionData(AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, {
|
||||
jsonData: {
|
||||
ai_languageModel: {
|
||||
response: {
|
||||
generations: [
|
||||
{
|
||||
text: `{
|
||||
"action": "Final Answer",
|
||||
"action_input": "${outputMessage}"
|
||||
}`,
|
||||
message: {
|
||||
lc: 1,
|
||||
type: 'constructor',
|
||||
id: ['langchain', 'schema', 'AIMessage'],
|
||||
kwargs: {
|
||||
content: `{
|
||||
"action": "Final Answer",
|
||||
"action_input": "${outputMessage}"
|
||||
}`,
|
||||
additional_kwargs: {},
|
||||
},
|
||||
},
|
||||
generationInfo: { finish_reason: 'stop' },
|
||||
},
|
||||
],
|
||||
llmOutput: {
|
||||
tokenUsage: {
|
||||
completionTokens: 26,
|
||||
promptTokens: 519,
|
||||
totalTokens: 545,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
inputOverride: {
|
||||
ai_languageModel: [
|
||||
[
|
||||
{
|
||||
json: {
|
||||
messages: [
|
||||
{
|
||||
lc: 1,
|
||||
type: 'constructor',
|
||||
id: ['langchain', 'schema', 'SystemMessage'],
|
||||
kwargs: {
|
||||
content:
|
||||
'Assistant is a large language model trained by OpenAI.\n\nAssistant is designed to be able to assist with a wide range of tasks, from answering simple questions to providing in-depth explanations and discussions on a wide range of topics. As a language model, Assistant is able to generate human-like text based on the input it receives, allowing it to engage in natural-sounding conversations and provide responses that are coherent and relevant to the topic at hand.\n\nAssistant is constantly learning and improving, and its capabilities are constantly evolving. It is able to process and understand large amounts of text, and can use this knowledge to provide accurate and informative responses to a wide range of questions. Additionally, Assistant is able to generate its own text based on the input it receives, allowing it to engage in discussions and provide explanations and descriptions on a wide range of topics.\n\nOverall, Assistant is a powerful system that can help with a wide range of tasks and provide valuable insights and information on a wide range of topics. Whether you need help with a specific question or just want to have a conversation about a particular topic, Assistant is here to assist. However, above all else, all responses must adhere to the format of RESPONSE FORMAT INSTRUCTIONS.',
|
||||
additional_kwargs: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
lc: 1,
|
||||
type: 'constructor',
|
||||
id: ['langchain', 'schema', 'HumanMessage'],
|
||||
kwargs: {
|
||||
content:
|
||||
'TOOLS\n------\nAssistant can ask the user to use tools to look up information that may be helpful in answering the users original question. The tools the human can use are:\n\n\n\nRESPONSE FORMAT INSTRUCTIONS\n----------------------------\n\nOutput a JSON markdown code snippet containing a valid JSON object in one of two formats:\n\n**Option 1:**\nUse this if you want the human to use a tool.\nMarkdown code snippet formatted in the following schema:\n\n```json\n{\n "action": string, // The action to take. Must be one of []\n "action_input": string // The input to the action. May be a stringified object.\n}\n```\n\n**Option #2:**\nUse this if you want to respond directly and conversationally to the human. Markdown code snippet formatted in the following schema:\n\n```json\n{\n "action": "Final Answer",\n "action_input": string // You should put what you want to return to use here and make sure to use valid json newline characters.\n}\n```\n\nFor both options, remember to always include the surrounding markdown code snippet delimiters (begin with "```json" and end with "```")!\n\n\nUSER\'S INPUT\n--------------------\nHere is the user\'s input (remember to respond with a markdown code snippet of a json blob with a single action, and NOTHING else):\n\nHello!',
|
||||
additional_kwargs: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
options: { stop: ['Observation:'], promptIndex: 0 },
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
}),
|
||||
createMockNodeExecutionData(AGENT_NODE_NAME, {
|
||||
jsonData: {
|
||||
main: { output: 'Hi there! How can I assist you today?' },
|
||||
},
|
||||
metadata: {
|
||||
subRun: [{ node: AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, runIndex: 0 }],
|
||||
},
|
||||
}),
|
||||
],
|
||||
lastNodeExecuted: AGENT_NODE_NAME,
|
||||
});
|
||||
|
||||
const messages = getManualChatMessages();
|
||||
messages.should('have.length', 2);
|
||||
messages.should('contain', inputMessage);
|
||||
messages.should('contain', outputMessage);
|
||||
|
||||
getManualChatModalLogsTree().should('be.visible');
|
||||
getManualChatModalLogsEntries().should('have.length', 1);
|
||||
|
||||
closeManualChatModal();
|
||||
});
|
||||
});
|
||||
@@ -34,7 +34,7 @@ describe('Node Creator', () => {
|
||||
nodeCreatorFeature.actions.openNodeCreator();
|
||||
|
||||
nodeCreatorFeature.getters.searchBar().find('input').type('manual');
|
||||
nodeCreatorFeature.getters.creatorItem().should('have.length', 1);
|
||||
nodeCreatorFeature.getters.creatorItem().should('have.length', 3);
|
||||
nodeCreatorFeature.getters.searchBar().find('input').clear().type('manual123');
|
||||
nodeCreatorFeature.getters.creatorItem().should('have.length', 0);
|
||||
nodeCreatorFeature.getters
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { NDV, WorkflowPage } from '../pages';
|
||||
import { getVisibleSelect } from '../utils';
|
||||
import { MANUAL_TRIGGER_NODE_DISPLAY_NAME } from '../constants';
|
||||
import { NDV, WorkflowPage } from '../pages';
|
||||
|
||||
const workflowPage = new WorkflowPage();
|
||||
const ndv = new NDV();
|
||||
@@ -368,6 +369,109 @@ describe('NDV', () => {
|
||||
cy.get('@fetchParameterOptions').should('have.been.calledOnce');
|
||||
});
|
||||
|
||||
describe('floating nodes', () => {
|
||||
function getFloatingNodeByPosition(position: 'inputMain' | 'outputMain' | 'outputSub'| 'inputSub') {
|
||||
return cy.get(`[data-node-placement=${position}]`);
|
||||
}
|
||||
beforeEach(() => {
|
||||
cy.createFixtureWorkflow('Floating_Nodes.json', `Floating Nodes`);
|
||||
workflowPage.getters.canvasNodes().first().dblclick()
|
||||
getFloatingNodeByPosition("inputMain").should('not.exist');
|
||||
getFloatingNodeByPosition("outputMain").should('exist');
|
||||
});
|
||||
|
||||
it('should traverse floating nodes with mouse', () => {
|
||||
// Traverse 4 connected node forwards
|
||||
Array.from(Array(4).keys()).forEach(i => {
|
||||
getFloatingNodeByPosition("outputMain").click({ force: true});
|
||||
ndv.getters.nodeNameContainer().should('contain', `Node ${i + 1}`);
|
||||
getFloatingNodeByPosition("inputMain").should('exist');
|
||||
getFloatingNodeByPosition("outputMain").should('exist');
|
||||
ndv.actions.close();
|
||||
workflowPage.getters.selectedNodes().should('have.length', 1);
|
||||
workflowPage.getters.selectedNodes().first().should('contain', `Node ${i + 1}`);
|
||||
workflowPage.getters.selectedNodes().first().dblclick();
|
||||
})
|
||||
|
||||
getFloatingNodeByPosition("outputMain").click({ force: true});
|
||||
ndv.getters.nodeNameContainer().should('contain', 'Chain');
|
||||
getFloatingNodeByPosition("inputSub").should('exist');
|
||||
getFloatingNodeByPosition("inputSub").click({ force: true});
|
||||
ndv.getters.nodeNameContainer().should('contain', 'Model');
|
||||
getFloatingNodeByPosition("inputSub").should('not.exist');
|
||||
getFloatingNodeByPosition("inputMain").should('not.exist');
|
||||
getFloatingNodeByPosition("outputMain").should('not.exist');
|
||||
getFloatingNodeByPosition("outputSub").should('exist');
|
||||
ndv.actions.close();
|
||||
workflowPage.getters.selectedNodes().should('have.length', 1);
|
||||
workflowPage.getters.selectedNodes().first().should('contain', 'Model');
|
||||
workflowPage.getters.selectedNodes().first().dblclick();
|
||||
getFloatingNodeByPosition("outputSub").click({ force: true});
|
||||
ndv.getters.nodeNameContainer().should('contain', 'Chain');
|
||||
|
||||
// Traverse 4 connected node backwards
|
||||
Array.from(Array(4).keys()).forEach(i => {
|
||||
getFloatingNodeByPosition("inputMain").click({ force: true});
|
||||
ndv.getters.nodeNameContainer().should('contain', `Node ${4 - (i)}`);
|
||||
getFloatingNodeByPosition("outputMain").should('exist');
|
||||
getFloatingNodeByPosition("inputMain").should('exist');
|
||||
})
|
||||
getFloatingNodeByPosition("inputMain").click({ force: true});
|
||||
workflowPage.getters.selectedNodes().first().should('contain', MANUAL_TRIGGER_NODE_DISPLAY_NAME);
|
||||
getFloatingNodeByPosition("inputMain").should('not.exist');
|
||||
getFloatingNodeByPosition("inputSub").should('not.exist');
|
||||
getFloatingNodeByPosition("outputSub").should('not.exist');
|
||||
ndv.actions.close();
|
||||
workflowPage.getters.selectedNodes().should('have.length', 1);
|
||||
workflowPage.getters.selectedNodes().first().should('contain', MANUAL_TRIGGER_NODE_DISPLAY_NAME);
|
||||
});
|
||||
|
||||
it('should traverse floating nodes with mouse', () => {
|
||||
// Traverse 4 connected node forwards
|
||||
Array.from(Array(4).keys()).forEach(i => {
|
||||
cy.realPress(['ShiftLeft', 'Meta', 'AltLeft', 'ArrowRight'])
|
||||
ndv.getters.nodeNameContainer().should('contain', `Node ${i + 1}`);
|
||||
getFloatingNodeByPosition("inputMain").should('exist');
|
||||
getFloatingNodeByPosition("outputMain").should('exist');
|
||||
ndv.actions.close();
|
||||
workflowPage.getters.selectedNodes().should('have.length', 1);
|
||||
workflowPage.getters.selectedNodes().first().should('contain', `Node ${i + 1}`);
|
||||
workflowPage.getters.selectedNodes().first().dblclick();
|
||||
})
|
||||
|
||||
cy.realPress(['ShiftLeft', 'Meta', 'AltLeft', 'ArrowRight'])
|
||||
ndv.getters.nodeNameContainer().should('contain', 'Chain');
|
||||
getFloatingNodeByPosition("inputSub").should('exist');
|
||||
cy.realPress(['ShiftLeft', 'Meta', 'AltLeft', 'ArrowDown'])
|
||||
ndv.getters.nodeNameContainer().should('contain', 'Model');
|
||||
getFloatingNodeByPosition("inputSub").should('not.exist');
|
||||
getFloatingNodeByPosition("inputMain").should('not.exist');
|
||||
getFloatingNodeByPosition("outputMain").should('not.exist');
|
||||
getFloatingNodeByPosition("outputSub").should('exist');
|
||||
ndv.actions.close();
|
||||
workflowPage.getters.selectedNodes().should('have.length', 1);
|
||||
workflowPage.getters.selectedNodes().first().should('contain', 'Model');
|
||||
workflowPage.getters.selectedNodes().first().dblclick();
|
||||
cy.realPress(['ShiftLeft', 'Meta', 'AltLeft', 'ArrowUp'])
|
||||
ndv.getters.nodeNameContainer().should('contain', 'Chain');
|
||||
|
||||
// Traverse 4 connected node backwards
|
||||
Array.from(Array(4).keys()).forEach(i => {
|
||||
cy.realPress(['ShiftLeft', 'Meta', 'AltLeft', 'ArrowLeft'])
|
||||
ndv.getters.nodeNameContainer().should('contain', `Node ${4 - (i)}`);
|
||||
getFloatingNodeByPosition("outputMain").should('exist');
|
||||
getFloatingNodeByPosition("inputMain").should('exist');
|
||||
})
|
||||
cy.realPress(['ShiftLeft', 'Meta', 'AltLeft', 'ArrowLeft'])
|
||||
workflowPage.getters.selectedNodes().first().should('contain', MANUAL_TRIGGER_NODE_DISPLAY_NAME);
|
||||
getFloatingNodeByPosition("inputMain").should('not.exist');
|
||||
getFloatingNodeByPosition("inputSub").should('not.exist');
|
||||
getFloatingNodeByPosition("outputSub").should('not.exist');
|
||||
ndv.actions.close();
|
||||
workflowPage.getters.selectedNodes().should('have.length', 1);
|
||||
workflowPage.getters.selectedNodes().first().should('contain', MANUAL_TRIGGER_NODE_DISPLAY_NAME);
|
||||
});
|
||||
})
|
||||
it('should show node name and version in settings', () => {
|
||||
cy.createFixtureWorkflow('Test_workflow_ndv_version.json', `NDV test version ${uuid()}`);
|
||||
|
||||
|
||||
176
cypress/fixtures/Floating_Nodes.json
Normal file
176
cypress/fixtures/Floating_Nodes.json
Normal file
@@ -0,0 +1,176 @@
|
||||
{
|
||||
"name": "Floating Nodes",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "d0eda550-2526-42a1-aa19-dee411c8acf9",
|
||||
"name": "When clicking \"Execute Workflow\"",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
700,
|
||||
560
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"options": {}
|
||||
},
|
||||
"id": "30412165-1229-4b21-9890-05bfbd9952ab",
|
||||
"name": "Node 1",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.2,
|
||||
"position": [
|
||||
920,
|
||||
560
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"options": {}
|
||||
},
|
||||
"id": "201cc8fc-3124-47a3-bc08-b3917c1ddcd9",
|
||||
"name": "Node 2",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.2,
|
||||
"position": [
|
||||
1100,
|
||||
560
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"options": {}
|
||||
},
|
||||
"id": "a29802bb-a284-495d-9917-6c6e42fef01e",
|
||||
"name": "Node 3",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.2,
|
||||
"position": [
|
||||
1280,
|
||||
560
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"options": {}
|
||||
},
|
||||
"id": "a95a72b3-8b39-44e2-a05b-d8d677741c80",
|
||||
"name": "Node 4",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.2,
|
||||
"position": [
|
||||
1440,
|
||||
560
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "4674f10d-6144-4a17-bbbb-350c3974438e",
|
||||
"name": "Chain",
|
||||
"type": "@n8n/n8n-nodes-langchain.chainLlm",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
1580,
|
||||
560
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"options": {}
|
||||
},
|
||||
"id": "58e12ea5-bd3e-4abf-abec-fcfb5c0a7955",
|
||||
"name": "Model",
|
||||
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
1600,
|
||||
740
|
||||
]
|
||||
}
|
||||
],
|
||||
"pinData": {},
|
||||
"connections": {
|
||||
"When clicking \"Execute Workflow\"": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Node 1",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Node 1": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Node 2",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Node 3": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Node 4",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Node 2": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Node 3",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Chain": {
|
||||
"main": [
|
||||
[]
|
||||
]
|
||||
},
|
||||
"Model": {
|
||||
"ai_languageModel": [
|
||||
[
|
||||
{
|
||||
"node": "Chain",
|
||||
"type": "ai_languageModel",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Node 4": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Chain",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
},
|
||||
"versionId": "2730d156-a98a-4ac8-b481-5c16361fdba2",
|
||||
"id": "6bzXMGxHuxeEaqsA",
|
||||
"meta": {
|
||||
"instanceId": "1838be0fa0389fbaf5e2e4aaedab4ddc79abc4175b433401abb22a281001b853"
|
||||
},
|
||||
"tags": []
|
||||
}
|
||||
@@ -2,7 +2,6 @@ import { META_KEY } from '../constants';
|
||||
import { BasePage } from './base';
|
||||
import { getVisibleSelect } from '../utils';
|
||||
import { NodeCreator } from './features/node-creator';
|
||||
import Chainable = Cypress.Chainable;
|
||||
|
||||
const nodeCreator = new NodeCreator();
|
||||
export class WorkflowPage extends BasePage {
|
||||
|
||||
135
cypress/utils/executions.ts
Normal file
135
cypress/utils/executions.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
import { ITaskData } from '../../packages/workflow/src';
|
||||
import { IPinData } from '../../packages/workflow';
|
||||
import { clickExecuteWorkflowButton } from '../composables/workflow';
|
||||
|
||||
export function createMockNodeExecutionData(
|
||||
name: string,
|
||||
{
|
||||
data,
|
||||
inputOverride,
|
||||
executionStatus = 'success',
|
||||
jsonData,
|
||||
...rest
|
||||
}: Partial<ITaskData> & { jsonData?: Record<string, object> },
|
||||
): Record<string, ITaskData> {
|
||||
return {
|
||||
[name]: {
|
||||
startTime: new Date().getTime(),
|
||||
executionTime: 0,
|
||||
executionStatus,
|
||||
data: jsonData
|
||||
? Object.keys(jsonData).reduce((acc, key) => {
|
||||
acc[key] = [
|
||||
[
|
||||
{
|
||||
json: jsonData[key],
|
||||
pairedItem: { item: 0 },
|
||||
},
|
||||
],
|
||||
];
|
||||
|
||||
return acc;
|
||||
}, {})
|
||||
: data,
|
||||
source: [null],
|
||||
...rest,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function createMockWorkflowExecutionData({
|
||||
executionId,
|
||||
runData,
|
||||
pinData = {},
|
||||
lastNodeExecuted,
|
||||
}: {
|
||||
executionId: string;
|
||||
runData: Record<string, ITaskData | ITaskData[]>;
|
||||
pinData?: IPinData;
|
||||
lastNodeExecuted: string;
|
||||
}) {
|
||||
return {
|
||||
executionId,
|
||||
data: {
|
||||
data: {
|
||||
startData: {},
|
||||
resultData: {
|
||||
runData,
|
||||
pinData,
|
||||
lastNodeExecuted,
|
||||
},
|
||||
executionData: {
|
||||
contextData: {},
|
||||
nodeExecutionStack: [],
|
||||
metadata: {},
|
||||
waitingExecution: {},
|
||||
waitingExecutionSource: {},
|
||||
},
|
||||
},
|
||||
mode: 'manual',
|
||||
startedAt: new Date().toISOString(),
|
||||
stoppedAt: new Date().toISOString(),
|
||||
status: 'success',
|
||||
finished: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function runMockWorkflowExcution({
|
||||
trigger,
|
||||
lastNodeExecuted,
|
||||
runData,
|
||||
workflowExecutionData,
|
||||
}: {
|
||||
trigger?: () => void;
|
||||
lastNodeExecuted: string;
|
||||
runData: Array<ReturnType<typeof createMockNodeExecutionData>>;
|
||||
workflowExecutionData?: ReturnType<typeof createMockWorkflowExecutionData>;
|
||||
}) {
|
||||
const executionId = Math.random().toString(36).substring(4);
|
||||
|
||||
cy.intercept('POST', '/rest/workflows/run', {
|
||||
statusCode: 201,
|
||||
body: {
|
||||
data: {
|
||||
executionId,
|
||||
},
|
||||
},
|
||||
}).as('runWorkflow');
|
||||
|
||||
if (trigger) {
|
||||
trigger();
|
||||
} else {
|
||||
clickExecuteWorkflowButton();
|
||||
}
|
||||
|
||||
cy.wait('@runWorkflow');
|
||||
|
||||
const resolvedRunData = {};
|
||||
runData.forEach((nodeExecution) => {
|
||||
const nodeName = Object.keys(nodeExecution)[0];
|
||||
const nodeRunData = nodeExecution[nodeName];
|
||||
|
||||
cy.push('nodeExecuteBefore', {
|
||||
executionId,
|
||||
nodeName,
|
||||
});
|
||||
cy.push('nodeExecuteAfter', {
|
||||
executionId,
|
||||
nodeName,
|
||||
data: nodeRunData,
|
||||
});
|
||||
|
||||
resolvedRunData[nodeName] = nodeExecution[nodeName];
|
||||
});
|
||||
|
||||
cy.push(
|
||||
'executionFinished',
|
||||
createMockWorkflowExecutionData({
|
||||
executionId,
|
||||
lastNodeExecuted,
|
||||
runData: resolvedRunData,
|
||||
...workflowExecutionData,
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -1 +1,3 @@
|
||||
export * from './executions';
|
||||
export * from './modal';
|
||||
export * from './popper';
|
||||
|
||||
Reference in New Issue
Block a user