Skip to content

Commit

Permalink
feat(editor): Send workflow context to assistant store (#11135)
Browse files Browse the repository at this point in the history
  • Loading branch information
MiloradFilipovic authored Oct 10, 2024
1 parent 8e6ddfe commit fade9e4
Show file tree
Hide file tree
Showing 16 changed files with 237 additions and 55 deletions.
2 changes: 1 addition & 1 deletion cypress/composables/ndv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export function setCredentialByName(name: string) {

export function clickCreateNewCredential() {
openCredentialSelect();
getCreateNewCredentialOption().click();
getCreateNewCredentialOption().click({ force: true });
}

export function clickGetBackToCanvas() {
Expand Down
115 changes: 92 additions & 23 deletions cypress/e2e/45-ai-assistant.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,9 @@ describe('AI Assistant::enabled', () => {
it('should start chat session from node error view', () => {
cy.intercept('POST', '/rest/ai/chat', {
statusCode: 200,
fixture: 'aiAssistant/simple_message_response.json',
fixture: 'aiAssistant/responses/simple_message_response.json',
}).as('chatRequest');
cy.createFixtureWorkflow('aiAssistant/test_workflow.json');
cy.createFixtureWorkflow('aiAssistant/workflows/test_workflow.json');
wf.actions.openNode('Stop and Error');
ndv.getters.nodeExecuteButton().click();
aiAssistant.getters.nodeErrorViewAssistantButton().click();
Expand All @@ -98,9 +98,9 @@ describe('AI Assistant::enabled', () => {
it('should render chat input correctly', () => {
cy.intercept('POST', '/rest/ai/chat', {
statusCode: 200,
fixture: 'aiAssistant/simple_message_response.json',
fixture: 'aiAssistant/responses/simple_message_response.json',
}).as('chatRequest');
cy.createFixtureWorkflow('aiAssistant/test_workflow.json');
cy.createFixtureWorkflow('aiAssistant/workflows/test_workflow.json');
wf.actions.openNode('Stop and Error');
ndv.getters.nodeExecuteButton().click();
aiAssistant.getters.nodeErrorViewAssistantButton().click();
Expand Down Expand Up @@ -131,9 +131,9 @@ describe('AI Assistant::enabled', () => {
it('should render and handle quick replies', () => {
cy.intercept('POST', '/rest/ai/chat', {
statusCode: 200,
fixture: 'aiAssistant/quick_reply_message_response.json',
fixture: 'aiAssistant/responses/quick_reply_message_response.json',
}).as('chatRequest');
cy.createFixtureWorkflow('aiAssistant/test_workflow.json');
cy.createFixtureWorkflow('aiAssistant/workflows/test_workflow.json');
wf.actions.openNode('Stop and Error');
ndv.getters.nodeExecuteButton().click();
aiAssistant.getters.nodeErrorViewAssistantButton().click();
Expand All @@ -149,15 +149,21 @@ describe('AI Assistant::enabled', () => {
cy.intercept('POST', '/rest/ai/chat', (req) => {
req.reply((res) => {
if (['init-error-helper', 'message'].includes(req.body.payload.type)) {
res.send({ statusCode: 200, fixture: 'aiAssistant/simple_message_response.json' });
res.send({
statusCode: 200,
fixture: 'aiAssistant/responses/simple_message_response.json',
});
} else if (req.body.payload.type === 'event') {
res.send({ statusCode: 200, fixture: 'aiAssistant/node_execution_error_response.json' });
res.send({
statusCode: 200,
fixture: 'aiAssistant/responses/node_execution_error_response.json',
});
} else {
res.send({ statusCode: 500 });
}
});
}).as('chatRequest');
cy.createFixtureWorkflow('aiAssistant/test_workflow.json');
cy.createFixtureWorkflow('aiAssistant/workflows/test_workflow.json');
wf.actions.openNode('Edit Fields');
ndv.getters.nodeExecuteButton().click();
aiAssistant.getters.nodeErrorViewAssistantButton().click();
Expand All @@ -172,16 +178,15 @@ describe('AI Assistant::enabled', () => {
aiAssistant.getters.quickReplies().should('not.exist');
ndv.getters.nodeExecuteButton().click();
// But after executing the node again, quick replies should be shown
aiAssistant.getters.chatMessagesAssistant().should('have.length', 4);
aiAssistant.getters.quickReplies().should('have.length', 2);
});

it('should warn before starting a new session', () => {
cy.intercept('POST', '/rest/ai/chat', {
statusCode: 200,
fixture: 'aiAssistant/simple_message_response.json',
fixture: 'aiAssistant/responses/simple_message_response.json',
}).as('chatRequest');
cy.createFixtureWorkflow('aiAssistant/test_workflow.json');
cy.createFixtureWorkflow('aiAssistant/workflows/test_workflow.json');
wf.actions.openNode('Edit Fields');
ndv.getters.nodeExecuteButton().click();
aiAssistant.getters.nodeErrorViewAssistantButton().click({ force: true });
Expand All @@ -206,13 +211,13 @@ describe('AI Assistant::enabled', () => {
it('should apply code diff to code node', () => {
cy.intercept('POST', '/rest/ai/chat', {
statusCode: 200,
fixture: 'aiAssistant/code_diff_suggestion_response.json',
fixture: 'aiAssistant/responses/code_diff_suggestion_response.json',
}).as('chatRequest');
cy.intercept('POST', '/rest/ai/chat/apply-suggestion', {
statusCode: 200,
fixture: 'aiAssistant/apply_code_diff_response.json',
fixture: 'aiAssistant/responses/apply_code_diff_response.json',
}).as('applySuggestion');
cy.createFixtureWorkflow('aiAssistant/test_workflow.json');
cy.createFixtureWorkflow('aiAssistant/workflows/test_workflow.json');
wf.actions.openNode('Code');
ndv.getters.nodeExecuteButton().click();
aiAssistant.getters.nodeErrorViewAssistantButton().click({ force: true });
Expand Down Expand Up @@ -256,9 +261,9 @@ describe('AI Assistant::enabled', () => {
it('should end chat session when `end_session` event is received', () => {
cy.intercept('POST', '/rest/ai/chat', {
statusCode: 200,
fixture: 'aiAssistant/end_session_response.json',
fixture: 'aiAssistant/responses/end_session_response.json',
}).as('chatRequest');
cy.createFixtureWorkflow('aiAssistant/test_workflow.json');
cy.createFixtureWorkflow('aiAssistant/workflows/test_workflow.json');
wf.actions.openNode('Stop and Error');
ndv.getters.nodeExecuteButton().click();
aiAssistant.getters.nodeErrorViewAssistantButton().click();
Expand All @@ -271,9 +276,12 @@ describe('AI Assistant::enabled', () => {
cy.intercept('POST', '/rest/ai/chat', (req) => {
req.reply((res) => {
if (['init-support-chat'].includes(req.body.payload.type)) {
res.send({ statusCode: 200, fixture: 'aiAssistant/simple_message_response.json' });
res.send({
statusCode: 200,
fixture: 'aiAssistant/responses/simple_message_response.json',
});
} else {
res.send({ statusCode: 200, fixture: 'aiAssistant/end_session_response.json' });
res.send({ statusCode: 200, fixture: 'aiAssistant/responses/end_session_response.json' });
}
});
}).as('chatRequest');
Expand All @@ -298,7 +306,7 @@ describe('AI Assistant::enabled', () => {
it('Should not reset assistant session when workflow is saved', () => {
cy.intercept('POST', '/rest/ai/chat', {
statusCode: 200,
fixture: 'aiAssistant/simple_message_response.json',
fixture: 'aiAssistant/responses/simple_message_response.json',
}).as('chatRequest');
wf.actions.addInitialNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
aiAssistant.actions.openChat();
Expand All @@ -323,7 +331,7 @@ describe('AI Assistant Credential Help', () => {
it('should start credential help from node credential', () => {
cy.intercept('POST', '/rest/ai/chat', {
statusCode: 200,
fixture: 'aiAssistant/simple_message_response.json',
fixture: 'aiAssistant/responses/simple_message_response.json',
}).as('chatRequest');
wf.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
wf.actions.addNodeToCanvas(GMAIL_NODE_NAME);
Expand All @@ -349,7 +357,7 @@ describe('AI Assistant Credential Help', () => {
it('should start credential help from credential list', () => {
cy.intercept('POST', '/rest/ai/chat', {
statusCode: 200,
fixture: 'aiAssistant/simple_message_response.json',
fixture: 'aiAssistant/responses/simple_message_response.json',
}).as('chatRequest');

cy.visit(credentialsPage.url);
Expand Down Expand Up @@ -448,7 +456,7 @@ describe('General help', () => {
it('assistant returns code snippet', () => {
cy.intercept('POST', '/rest/ai/chat', {
statusCode: 200,
fixture: 'aiAssistant/code_snippet_response.json',
fixture: 'aiAssistant/responses/code_snippet_response.json',
}).as('chatRequest');

aiAssistant.getters.askAssistantFloatingButton().should('be.visible');
Expand Down Expand Up @@ -492,4 +500,65 @@ describe('General help', () => {
);
aiAssistant.getters.codeSnippet().should('have.text', '{{$json.body.city}}');
});

it('should send current context to support chat', () => {
cy.createFixtureWorkflow('aiAssistant/workflows/simple_http_request_workflow.json');
cy.intercept('POST', '/rest/ai/chat', {
statusCode: 200,
fixture: 'aiAssistant/responses/simple_message_response.json',
}).as('chatRequest');

aiAssistant.getters.askAssistantFloatingButton().click();
aiAssistant.actions.sendMessage('What is wrong with this workflow?');

cy.wait('@chatRequest').then((interception) => {
const { body } = interception.request;
// Body should contain the current workflow context
expect(body.payload).to.have.property('context');
expect(body.payload.context).to.have.property('currentView');
expect(body.payload.context.currentView.name).to.equal('NodeViewExisting');
expect(body.payload.context).to.have.property('currentWorkflow');
});
});

it('should not send workflow context if nothing changed', () => {
cy.createFixtureWorkflow('aiAssistant/workflows/simple_http_request_workflow.json');
cy.intercept('POST', '/rest/ai/chat', {
statusCode: 200,
fixture: 'aiAssistant/responses/simple_message_response.json',
}).as('chatRequest');

aiAssistant.getters.askAssistantFloatingButton().click();
aiAssistant.actions.sendMessage('What is wrong with this workflow?');
cy.wait('@chatRequest');

// Send another message without changing workflow or executing any node
aiAssistant.actions.sendMessage('And now?');

cy.wait('@chatRequest').then((interception) => {
const { body } = interception.request;
// Workflow context should be empty
expect(body.payload).to.have.property('context');
expect(body.payload.context).not.to.have.property('currentWorkflow');
});

// Update http request node url
wf.actions.openNode('HTTP Request');
ndv.actions.typeIntoParameterInput('url', 'https://example.com');
ndv.actions.close();
// Also execute the workflow
wf.actions.executeWorkflow();

// Send another message
aiAssistant.actions.sendMessage('What about now?');
cy.wait('@chatRequest').then((interception) => {
const { body } = interception.request;
// Both workflow and execution context should be sent
expect(body.payload).to.have.property('context');
expect(body.payload.context).to.have.property('currentWorkflow');
expect(body.payload.context.currentWorkflow).not.to.be.empty;
expect(body.payload.context).to.have.property('executionData');
expect(body.payload.context.executionData).not.to.be.empty;
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"nodes": [
{
"parameters": {},
"id": "298d3dc9-5e99-4b3f-919e-05fdcdfbe2d0",
"name": "When clicking ‘Test workflow’",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [360, 220]
},
{
"parameters": {
"options": {}
},
"id": "65c32346-e939-4ec7-88a9-1f9184e2258d",
"name": "HTTP Request",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [580, 220]
}
],
"connections": {
"When clicking ‘Test workflow’": {
"main": [
[
{
"node": "HTTP Request",
"type": "main",
"index": 0
}
]
]
}
}
}
2 changes: 1 addition & 1 deletion cypress/pages/ndv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ export class NDV extends BasePage {
this.getters.nodeExecuteButton().first().click();
},
close: () => {
this.getters.backToCanvas().click();
this.getters.backToCanvas().click({ force: true });
},
openInlineExpressionEditor: () => {
cy.contains('Expression').invoke('show').click();
Expand Down
7 changes: 0 additions & 7 deletions n8n.code-workspace

This file was deleted.

42 changes: 41 additions & 1 deletion packages/editor-ui/src/composables/useAIAssistantHelpers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import type { IDataObject, NodeApiError, NodeError, NodeOperationError } from 'n8n-workflow';
import type {
IDataObject,
IRunExecutionData,
NodeApiError,
NodeError,
NodeOperationError,
} from 'n8n-workflow';
import { deepCopy, type INode } from 'n8n-workflow';
import { useWorkflowHelpers } from './useWorkflowHelpers';
import { useRouter } from 'vue-router';
Expand Down Expand Up @@ -203,6 +209,39 @@ export const useAIAssistantHelpers = () => {
return undefined;
}
}
/**
* Prepare workflow execution result data for the AI assistant
* by removing data from nodes
**/
function simplifyResultData(
data: IRunExecutionData['resultData'],
): ChatRequest.ExecutionResultData {
const simplifiedResultData: ChatRequest.ExecutionResultData = {
runData: {},
};

// Handle optional error
if (data.error) {
simplifiedResultData.error = data.error;
}
// Map runData, excluding the `data` field from ITaskData
Object.keys(data.runData).forEach((key) => {
const taskDataArray = data.runData[key];
simplifiedResultData.runData[key] = taskDataArray.map((taskData) => {
const { data: taskDataContent, ...taskDataWithoutData } = taskData;
return taskDataWithoutData;
});
});
// Handle lastNodeExecuted if it exists
if (data.lastNodeExecuted) {
simplifiedResultData.lastNodeExecuted = data.lastNodeExecuted;
}
// Handle metadata if it exists
if (data.metadata) {
simplifiedResultData.metadata = data.metadata;
}
return simplifiedResultData;
}

return {
processNodeForAssistant,
Expand All @@ -212,5 +251,6 @@ export const useAIAssistantHelpers = () => {
getNodesSchemas,
getCurrentViewDescription,
getReferencedNodes,
simplifyResultData,
};
};
Loading

0 comments on commit fade9e4

Please sign in to comment.