mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
feat(editor): Send workflow context to assistant store (#11135)
This commit is contained in:
parent
8e6ddfe028
commit
fade9e43c8
|
@ -59,7 +59,7 @@ export function setCredentialByName(name: string) {
|
||||||
|
|
||||||
export function clickCreateNewCredential() {
|
export function clickCreateNewCredential() {
|
||||||
openCredentialSelect();
|
openCredentialSelect();
|
||||||
getCreateNewCredentialOption().click();
|
getCreateNewCredentialOption().click({ force: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function clickGetBackToCanvas() {
|
export function clickGetBackToCanvas() {
|
||||||
|
|
|
@ -80,9 +80,9 @@ describe('AI Assistant::enabled', () => {
|
||||||
it('should start chat session from node error view', () => {
|
it('should start chat session from node error view', () => {
|
||||||
cy.intercept('POST', '/rest/ai/chat', {
|
cy.intercept('POST', '/rest/ai/chat', {
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
fixture: 'aiAssistant/simple_message_response.json',
|
fixture: 'aiAssistant/responses/simple_message_response.json',
|
||||||
}).as('chatRequest');
|
}).as('chatRequest');
|
||||||
cy.createFixtureWorkflow('aiAssistant/test_workflow.json');
|
cy.createFixtureWorkflow('aiAssistant/workflows/test_workflow.json');
|
||||||
wf.actions.openNode('Stop and Error');
|
wf.actions.openNode('Stop and Error');
|
||||||
ndv.getters.nodeExecuteButton().click();
|
ndv.getters.nodeExecuteButton().click();
|
||||||
aiAssistant.getters.nodeErrorViewAssistantButton().click();
|
aiAssistant.getters.nodeErrorViewAssistantButton().click();
|
||||||
|
@ -98,9 +98,9 @@ describe('AI Assistant::enabled', () => {
|
||||||
it('should render chat input correctly', () => {
|
it('should render chat input correctly', () => {
|
||||||
cy.intercept('POST', '/rest/ai/chat', {
|
cy.intercept('POST', '/rest/ai/chat', {
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
fixture: 'aiAssistant/simple_message_response.json',
|
fixture: 'aiAssistant/responses/simple_message_response.json',
|
||||||
}).as('chatRequest');
|
}).as('chatRequest');
|
||||||
cy.createFixtureWorkflow('aiAssistant/test_workflow.json');
|
cy.createFixtureWorkflow('aiAssistant/workflows/test_workflow.json');
|
||||||
wf.actions.openNode('Stop and Error');
|
wf.actions.openNode('Stop and Error');
|
||||||
ndv.getters.nodeExecuteButton().click();
|
ndv.getters.nodeExecuteButton().click();
|
||||||
aiAssistant.getters.nodeErrorViewAssistantButton().click();
|
aiAssistant.getters.nodeErrorViewAssistantButton().click();
|
||||||
|
@ -131,9 +131,9 @@ describe('AI Assistant::enabled', () => {
|
||||||
it('should render and handle quick replies', () => {
|
it('should render and handle quick replies', () => {
|
||||||
cy.intercept('POST', '/rest/ai/chat', {
|
cy.intercept('POST', '/rest/ai/chat', {
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
fixture: 'aiAssistant/quick_reply_message_response.json',
|
fixture: 'aiAssistant/responses/quick_reply_message_response.json',
|
||||||
}).as('chatRequest');
|
}).as('chatRequest');
|
||||||
cy.createFixtureWorkflow('aiAssistant/test_workflow.json');
|
cy.createFixtureWorkflow('aiAssistant/workflows/test_workflow.json');
|
||||||
wf.actions.openNode('Stop and Error');
|
wf.actions.openNode('Stop and Error');
|
||||||
ndv.getters.nodeExecuteButton().click();
|
ndv.getters.nodeExecuteButton().click();
|
||||||
aiAssistant.getters.nodeErrorViewAssistantButton().click();
|
aiAssistant.getters.nodeErrorViewAssistantButton().click();
|
||||||
|
@ -149,15 +149,21 @@ describe('AI Assistant::enabled', () => {
|
||||||
cy.intercept('POST', '/rest/ai/chat', (req) => {
|
cy.intercept('POST', '/rest/ai/chat', (req) => {
|
||||||
req.reply((res) => {
|
req.reply((res) => {
|
||||||
if (['init-error-helper', 'message'].includes(req.body.payload.type)) {
|
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') {
|
} 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 {
|
} else {
|
||||||
res.send({ statusCode: 500 });
|
res.send({ statusCode: 500 });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}).as('chatRequest');
|
}).as('chatRequest');
|
||||||
cy.createFixtureWorkflow('aiAssistant/test_workflow.json');
|
cy.createFixtureWorkflow('aiAssistant/workflows/test_workflow.json');
|
||||||
wf.actions.openNode('Edit Fields');
|
wf.actions.openNode('Edit Fields');
|
||||||
ndv.getters.nodeExecuteButton().click();
|
ndv.getters.nodeExecuteButton().click();
|
||||||
aiAssistant.getters.nodeErrorViewAssistantButton().click();
|
aiAssistant.getters.nodeErrorViewAssistantButton().click();
|
||||||
|
@ -172,16 +178,15 @@ describe('AI Assistant::enabled', () => {
|
||||||
aiAssistant.getters.quickReplies().should('not.exist');
|
aiAssistant.getters.quickReplies().should('not.exist');
|
||||||
ndv.getters.nodeExecuteButton().click();
|
ndv.getters.nodeExecuteButton().click();
|
||||||
// But after executing the node again, quick replies should be shown
|
// 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);
|
aiAssistant.getters.quickReplies().should('have.length', 2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should warn before starting a new session', () => {
|
it('should warn before starting a new session', () => {
|
||||||
cy.intercept('POST', '/rest/ai/chat', {
|
cy.intercept('POST', '/rest/ai/chat', {
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
fixture: 'aiAssistant/simple_message_response.json',
|
fixture: 'aiAssistant/responses/simple_message_response.json',
|
||||||
}).as('chatRequest');
|
}).as('chatRequest');
|
||||||
cy.createFixtureWorkflow('aiAssistant/test_workflow.json');
|
cy.createFixtureWorkflow('aiAssistant/workflows/test_workflow.json');
|
||||||
wf.actions.openNode('Edit Fields');
|
wf.actions.openNode('Edit Fields');
|
||||||
ndv.getters.nodeExecuteButton().click();
|
ndv.getters.nodeExecuteButton().click();
|
||||||
aiAssistant.getters.nodeErrorViewAssistantButton().click({ force: true });
|
aiAssistant.getters.nodeErrorViewAssistantButton().click({ force: true });
|
||||||
|
@ -206,13 +211,13 @@ describe('AI Assistant::enabled', () => {
|
||||||
it('should apply code diff to code node', () => {
|
it('should apply code diff to code node', () => {
|
||||||
cy.intercept('POST', '/rest/ai/chat', {
|
cy.intercept('POST', '/rest/ai/chat', {
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
fixture: 'aiAssistant/code_diff_suggestion_response.json',
|
fixture: 'aiAssistant/responses/code_diff_suggestion_response.json',
|
||||||
}).as('chatRequest');
|
}).as('chatRequest');
|
||||||
cy.intercept('POST', '/rest/ai/chat/apply-suggestion', {
|
cy.intercept('POST', '/rest/ai/chat/apply-suggestion', {
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
fixture: 'aiAssistant/apply_code_diff_response.json',
|
fixture: 'aiAssistant/responses/apply_code_diff_response.json',
|
||||||
}).as('applySuggestion');
|
}).as('applySuggestion');
|
||||||
cy.createFixtureWorkflow('aiAssistant/test_workflow.json');
|
cy.createFixtureWorkflow('aiAssistant/workflows/test_workflow.json');
|
||||||
wf.actions.openNode('Code');
|
wf.actions.openNode('Code');
|
||||||
ndv.getters.nodeExecuteButton().click();
|
ndv.getters.nodeExecuteButton().click();
|
||||||
aiAssistant.getters.nodeErrorViewAssistantButton().click({ force: true });
|
aiAssistant.getters.nodeErrorViewAssistantButton().click({ force: true });
|
||||||
|
@ -256,9 +261,9 @@ describe('AI Assistant::enabled', () => {
|
||||||
it('should end chat session when `end_session` event is received', () => {
|
it('should end chat session when `end_session` event is received', () => {
|
||||||
cy.intercept('POST', '/rest/ai/chat', {
|
cy.intercept('POST', '/rest/ai/chat', {
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
fixture: 'aiAssistant/end_session_response.json',
|
fixture: 'aiAssistant/responses/end_session_response.json',
|
||||||
}).as('chatRequest');
|
}).as('chatRequest');
|
||||||
cy.createFixtureWorkflow('aiAssistant/test_workflow.json');
|
cy.createFixtureWorkflow('aiAssistant/workflows/test_workflow.json');
|
||||||
wf.actions.openNode('Stop and Error');
|
wf.actions.openNode('Stop and Error');
|
||||||
ndv.getters.nodeExecuteButton().click();
|
ndv.getters.nodeExecuteButton().click();
|
||||||
aiAssistant.getters.nodeErrorViewAssistantButton().click();
|
aiAssistant.getters.nodeErrorViewAssistantButton().click();
|
||||||
|
@ -271,9 +276,12 @@ describe('AI Assistant::enabled', () => {
|
||||||
cy.intercept('POST', '/rest/ai/chat', (req) => {
|
cy.intercept('POST', '/rest/ai/chat', (req) => {
|
||||||
req.reply((res) => {
|
req.reply((res) => {
|
||||||
if (['init-support-chat'].includes(req.body.payload.type)) {
|
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 {
|
} else {
|
||||||
res.send({ statusCode: 200, fixture: 'aiAssistant/end_session_response.json' });
|
res.send({ statusCode: 200, fixture: 'aiAssistant/responses/end_session_response.json' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}).as('chatRequest');
|
}).as('chatRequest');
|
||||||
|
@ -298,7 +306,7 @@ describe('AI Assistant::enabled', () => {
|
||||||
it('Should not reset assistant session when workflow is saved', () => {
|
it('Should not reset assistant session when workflow is saved', () => {
|
||||||
cy.intercept('POST', '/rest/ai/chat', {
|
cy.intercept('POST', '/rest/ai/chat', {
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
fixture: 'aiAssistant/simple_message_response.json',
|
fixture: 'aiAssistant/responses/simple_message_response.json',
|
||||||
}).as('chatRequest');
|
}).as('chatRequest');
|
||||||
wf.actions.addInitialNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
wf.actions.addInitialNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
||||||
aiAssistant.actions.openChat();
|
aiAssistant.actions.openChat();
|
||||||
|
@ -323,7 +331,7 @@ describe('AI Assistant Credential Help', () => {
|
||||||
it('should start credential help from node credential', () => {
|
it('should start credential help from node credential', () => {
|
||||||
cy.intercept('POST', '/rest/ai/chat', {
|
cy.intercept('POST', '/rest/ai/chat', {
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
fixture: 'aiAssistant/simple_message_response.json',
|
fixture: 'aiAssistant/responses/simple_message_response.json',
|
||||||
}).as('chatRequest');
|
}).as('chatRequest');
|
||||||
wf.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
wf.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
||||||
wf.actions.addNodeToCanvas(GMAIL_NODE_NAME);
|
wf.actions.addNodeToCanvas(GMAIL_NODE_NAME);
|
||||||
|
@ -349,7 +357,7 @@ describe('AI Assistant Credential Help', () => {
|
||||||
it('should start credential help from credential list', () => {
|
it('should start credential help from credential list', () => {
|
||||||
cy.intercept('POST', '/rest/ai/chat', {
|
cy.intercept('POST', '/rest/ai/chat', {
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
fixture: 'aiAssistant/simple_message_response.json',
|
fixture: 'aiAssistant/responses/simple_message_response.json',
|
||||||
}).as('chatRequest');
|
}).as('chatRequest');
|
||||||
|
|
||||||
cy.visit(credentialsPage.url);
|
cy.visit(credentialsPage.url);
|
||||||
|
@ -448,7 +456,7 @@ describe('General help', () => {
|
||||||
it('assistant returns code snippet', () => {
|
it('assistant returns code snippet', () => {
|
||||||
cy.intercept('POST', '/rest/ai/chat', {
|
cy.intercept('POST', '/rest/ai/chat', {
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
fixture: 'aiAssistant/code_snippet_response.json',
|
fixture: 'aiAssistant/responses/code_snippet_response.json',
|
||||||
}).as('chatRequest');
|
}).as('chatRequest');
|
||||||
|
|
||||||
aiAssistant.getters.askAssistantFloatingButton().should('be.visible');
|
aiAssistant.getters.askAssistantFloatingButton().should('be.visible');
|
||||||
|
@ -492,4 +500,65 @@ describe('General help', () => {
|
||||||
);
|
);
|
||||||
aiAssistant.getters.codeSnippet().should('have.text', '{{$json.body.city}}');
|
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;
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -156,7 +156,7 @@ export class NDV extends BasePage {
|
||||||
this.getters.nodeExecuteButton().first().click();
|
this.getters.nodeExecuteButton().first().click();
|
||||||
},
|
},
|
||||||
close: () => {
|
close: () => {
|
||||||
this.getters.backToCanvas().click();
|
this.getters.backToCanvas().click({ force: true });
|
||||||
},
|
},
|
||||||
openInlineExpressionEditor: () => {
|
openInlineExpressionEditor: () => {
|
||||||
cy.contains('Expression').invoke('show').click();
|
cy.contains('Expression').invoke('show').click();
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
{
|
|
||||||
"folders": [
|
|
||||||
{
|
|
||||||
"path": "."
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -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 { deepCopy, type INode } from 'n8n-workflow';
|
||||||
import { useWorkflowHelpers } from './useWorkflowHelpers';
|
import { useWorkflowHelpers } from './useWorkflowHelpers';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
|
@ -203,6 +209,39 @@ export const useAIAssistantHelpers = () => {
|
||||||
return undefined;
|
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 {
|
return {
|
||||||
processNodeForAssistant,
|
processNodeForAssistant,
|
||||||
|
@ -212,5 +251,6 @@ export const useAIAssistantHelpers = () => {
|
||||||
getNodesSchemas,
|
getNodesSchemas,
|
||||||
getCurrentViewDescription,
|
getCurrentViewDescription,
|
||||||
getReferencedNodes,
|
getReferencedNodes,
|
||||||
|
simplifyResultData,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -83,6 +83,9 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
|
||||||
// We use streaming for assistants that support it, and this for agents
|
// We use streaming for assistants that support it, and this for agents
|
||||||
const assistantThinkingMessage = ref<string | undefined>();
|
const assistantThinkingMessage = ref<string | undefined>();
|
||||||
const chatSessionTask = ref<'error' | 'support' | 'credentials' | undefined>();
|
const chatSessionTask = ref<'error' | 'support' | 'credentials' | undefined>();
|
||||||
|
// Indicate if last sent workflow and execution data is stale
|
||||||
|
const workflowDataStale = ref<boolean>(true);
|
||||||
|
const workflowExecutionDataStale = ref<boolean>(true);
|
||||||
|
|
||||||
const isExperimentEnabled = computed(
|
const isExperimentEnabled = computed(
|
||||||
() => getVariant(AI_ASSISTANT_EXPERIMENT.name) === AI_ASSISTANT_EXPERIMENT.variant,
|
() => getVariant(AI_ASSISTANT_EXPERIMENT.name) === AI_ASSISTANT_EXPERIMENT.variant,
|
||||||
|
@ -124,18 +127,6 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
|
||||||
).length,
|
).length,
|
||||||
);
|
);
|
||||||
|
|
||||||
watch(route, () => {
|
|
||||||
const activeWorkflowId = workflowsStore.workflowId;
|
|
||||||
if (
|
|
||||||
!currentSessionId.value ||
|
|
||||||
currentSessionWorkflowId.value === PLACEHOLDER_EMPTY_WORKFLOW_ID ||
|
|
||||||
currentSessionWorkflowId.value === activeWorkflowId
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
resetAssistantChat();
|
|
||||||
});
|
|
||||||
|
|
||||||
function resetAssistantChat() {
|
function resetAssistantChat() {
|
||||||
clearMessages();
|
clearMessages();
|
||||||
currentSessionId.value = undefined;
|
currentSessionId.value = undefined;
|
||||||
|
@ -305,11 +296,7 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
|
||||||
{ withPostHog: true },
|
{ withPostHog: true },
|
||||||
);
|
);
|
||||||
// Track first user message in support chat now that we have a session id
|
// Track first user message in support chat now that we have a session id
|
||||||
if (
|
if (usersMessages.value.length === 1 && chatSessionTask.value === 'support') {
|
||||||
usersMessages.value.length === 1 &&
|
|
||||||
!currentSessionId.value &&
|
|
||||||
chatSessionTask.value === 'support'
|
|
||||||
) {
|
|
||||||
const firstUserMessage = usersMessages.value[0] as ChatUI.TextMessage;
|
const firstUserMessage = usersMessages.value[0] as ChatUI.TextMessage;
|
||||||
trackUserMessage(firstUserMessage.content, false);
|
trackUserMessage(firstUserMessage.content, false);
|
||||||
}
|
}
|
||||||
|
@ -325,6 +312,8 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
|
||||||
|
|
||||||
function onDoneStreaming(id: string) {
|
function onDoneStreaming(id: string) {
|
||||||
stopStreaming();
|
stopStreaming();
|
||||||
|
workflowDataStale.value = false;
|
||||||
|
workflowExecutionDataStale.value = false;
|
||||||
lastUnread.value = chatMessages.value.find(
|
lastUnread.value = chatMessages.value.find(
|
||||||
(msg) =>
|
(msg) =>
|
||||||
msg.id === id && !msg.read && msg.role === 'assistant' && READABLE_TYPES.includes(msg.type),
|
msg.id === id && !msg.read && msg.role === 'assistant' && READABLE_TYPES.includes(msg.type),
|
||||||
|
@ -353,14 +342,16 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
|
||||||
/**
|
/**
|
||||||
* Gets information about the current view and active node to provide context to the assistant
|
* Gets information about the current view and active node to provide context to the assistant
|
||||||
*/
|
*/
|
||||||
function getVisualContext(nodeInfo?: ChatRequest.NodeInfo): ChatRequest.UserContext | undefined {
|
function getVisualContext(
|
||||||
|
nodeInfo?: ChatRequest.NodeInfo,
|
||||||
|
): ChatRequest.AssistantContext | undefined {
|
||||||
if (chatSessionTask.value === 'error') {
|
if (chatSessionTask.value === 'error') {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
const currentView = route.name as VIEWS;
|
const currentView = route.name as VIEWS;
|
||||||
const activeNode = workflowsStore.activeNode();
|
const activeNode = workflowsStore.activeNode();
|
||||||
const activeNodeForLLM = activeNode
|
const activeNodeForLLM = activeNode
|
||||||
? assistantHelpers.processNodeForAssistant(activeNode, ['position'])
|
? assistantHelpers.processNodeForAssistant(activeNode, ['position', 'parameters.notice'])
|
||||||
: null;
|
: null;
|
||||||
const activeModals = uiStore.activeModals;
|
const activeModals = uiStore.activeModals;
|
||||||
const isCredentialModalActive = activeModals.includes(CREDENTIAL_EDIT_MODAL_KEY);
|
const isCredentialModalActive = activeModals.includes(CREDENTIAL_EDIT_MODAL_KEY);
|
||||||
|
@ -401,6 +392,11 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
|
||||||
authType: nodeInfo?.authType?.name,
|
authType: nodeInfo?.authType?.name,
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
|
currentWorkflow: workflowDataStale.value ? workflowsStore.workflow : undefined,
|
||||||
|
executionData:
|
||||||
|
workflowExecutionDataStale.value && executionResult
|
||||||
|
? assistantHelpers.simplifyResultData(executionResult)
|
||||||
|
: undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -491,7 +487,10 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
|
||||||
firstName: usersStore.currentUser?.firstName ?? '',
|
firstName: usersStore.currentUser?.firstName ?? '',
|
||||||
},
|
},
|
||||||
error: context.error,
|
error: context.error,
|
||||||
node: assistantHelpers.processNodeForAssistant(context.node, ['position']),
|
node: assistantHelpers.processNodeForAssistant(context.node, [
|
||||||
|
'position',
|
||||||
|
'parameters.notice',
|
||||||
|
]),
|
||||||
nodeInputData,
|
nodeInputData,
|
||||||
executionSchema: schemas,
|
executionSchema: schemas,
|
||||||
authType,
|
authType,
|
||||||
|
@ -577,7 +576,10 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
|
||||||
) {
|
) {
|
||||||
nodeExecutionStatus.value = 'not_executed';
|
nodeExecutionStatus.value = 'not_executed';
|
||||||
}
|
}
|
||||||
const userContext = getVisualContext();
|
const activeNode = workflowsStore.activeNode() as INode;
|
||||||
|
const nodeInfo = assistantHelpers.getNodeInfoForAssistant(activeNode);
|
||||||
|
const userContext = getVisualContext(nodeInfo);
|
||||||
|
|
||||||
chatWithAssistant(
|
chatWithAssistant(
|
||||||
rootStore.restApiContext,
|
rootStore.restApiContext,
|
||||||
{
|
{
|
||||||
|
@ -764,6 +766,33 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watch(route, () => {
|
||||||
|
const activeWorkflowId = workflowsStore.workflowId;
|
||||||
|
if (
|
||||||
|
!currentSessionId.value ||
|
||||||
|
currentSessionWorkflowId.value === PLACEHOLDER_EMPTY_WORKFLOW_ID ||
|
||||||
|
currentSessionWorkflowId.value === activeWorkflowId
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resetAssistantChat();
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => uiStore.stateIsDirty,
|
||||||
|
() => {
|
||||||
|
workflowDataStale.value = true;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => workflowsStore.workflowExecutionData?.data?.resultData ?? {},
|
||||||
|
() => {
|
||||||
|
workflowExecutionDataStale.value = true;
|
||||||
|
},
|
||||||
|
{ deep: true, immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isAssistantEnabled,
|
isAssistantEnabled,
|
||||||
canShowAssistantButtonsOnCanvas,
|
canShowAssistantButtonsOnCanvas,
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
import type { VIEWS } from '@/constants';
|
import type { VIEWS } from '@/constants';
|
||||||
import type { NodeAuthenticationOption, Schema } from '@/Interface';
|
import type { IWorkflowDb, NodeAuthenticationOption, Schema } from '@/Interface';
|
||||||
import type {
|
import type {
|
||||||
|
ExecutionError,
|
||||||
ICredentialType,
|
ICredentialType,
|
||||||
IDataObject,
|
IDataObject,
|
||||||
INode,
|
INode,
|
||||||
INodeIssues,
|
INodeIssues,
|
||||||
INodeParameters,
|
INodeParameters,
|
||||||
|
IRunExecutionData,
|
||||||
|
ITaskData,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
export namespace ChatRequest {
|
export namespace ChatRequest {
|
||||||
|
@ -16,6 +19,15 @@ export namespace ChatRequest {
|
||||||
|
|
||||||
export interface WorkflowContext {
|
export interface WorkflowContext {
|
||||||
executionSchema?: NodeExecutionSchema[];
|
executionSchema?: NodeExecutionSchema[];
|
||||||
|
currentWorkflow?: IWorkflowDb;
|
||||||
|
executionData?: IRunExecutionData['resultData'];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExecutionResultData {
|
||||||
|
error?: ExecutionError;
|
||||||
|
runData: Record<string, Array<Omit<ITaskData, 'data'>>>;
|
||||||
|
lastNodeExecuted?: string;
|
||||||
|
metadata?: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ErrorContext {
|
export interface ErrorContext {
|
||||||
|
@ -47,6 +59,7 @@ export namespace ChatRequest {
|
||||||
firstName: string;
|
firstName: string;
|
||||||
};
|
};
|
||||||
context?: UserContext;
|
context?: UserContext;
|
||||||
|
workflowContext?: WorkflowContext;
|
||||||
question: string;
|
question: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,6 +91,7 @@ export namespace ChatRequest {
|
||||||
text: string;
|
text: string;
|
||||||
quickReplyType?: string;
|
quickReplyType?: string;
|
||||||
context?: UserContext;
|
context?: UserContext;
|
||||||
|
workflowContext?: WorkflowContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserContext {
|
export interface UserContext {
|
||||||
|
@ -98,6 +112,8 @@ export namespace ChatRequest {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type AssistantContext = UserContext & WorkflowContext;
|
||||||
|
|
||||||
export type RequestPayload =
|
export type RequestPayload =
|
||||||
| {
|
| {
|
||||||
payload: InitErrorHelper | InitSupportChat | InitCredHelp;
|
payload: InitErrorHelper | InitSupportChat | InitCredHelp;
|
||||||
|
|
Loading…
Reference in a new issue