feat: Add more context to support chat (#11014)

This commit is contained in:
Milorad FIlipović
2024-10-02 10:00:20 +02:00
committed by GitHub
parent 5903592a23
commit 8a30f92156
10 changed files with 365 additions and 184 deletions

View File

@@ -83,10 +83,10 @@ const i18n = useI18n();
const telemetry = useTelemetry(); const telemetry = useTelemetry();
onBeforeMount(async () => { onBeforeMount(async () => {
if (rootStore.defaultLocale === 'en') return;
uiStore.activeCredentialType = props.credentialType.name; uiStore.activeCredentialType = props.credentialType.name;
if (rootStore.defaultLocale === 'en') return;
const key = `n8n-nodes-base.credentials.${props.credentialType.name}`; const key = `n8n-nodes-base.credentials.${props.credentialType.name}`;
if (i18n.exists(key)) return; if (i18n.exists(key)) return;

View File

@@ -420,6 +420,7 @@ async function beforeClose() {
} }
if (!keepEditing) { if (!keepEditing) {
uiStore.activeCredentialType = null;
return true; return true;
} else if (!requiredPropertiesFilled.value) { } else if (!requiredPropertiesFilled.value) {
showValidationWarning.value = true; showValidationWarning.value = true;
@@ -986,6 +987,7 @@ async function onAuthTypeChanged(type: string): Promise<void> {
const credentialsForType = getNodeCredentialForSelectedAuthType(activeNodeType.value, type); const credentialsForType = getNodeCredentialForSelectedAuthType(activeNodeType.value, type);
if (credentialsForType) { if (credentialsForType) {
selectedCredential.value = credentialsForType.name; selectedCredential.value = credentialsForType.name;
uiStore.activeCredentialType = credentialsForType.name;
resetCredentialData(); resetCredentialData();
// Update current node auth type so credentials dropdown can be displayed properly // Update current node auth type so credentials dropdown can be displayed properly
updateNodeAuthType(ndvStore.activeNode, type); updateNodeAuthType(ndvStore.activeNode, type);

View File

@@ -23,6 +23,7 @@ import type { ChatRequest } from '@/types/assistant.types';
import InlineAskAssistantButton from 'n8n-design-system/components/InlineAskAssistantButton/InlineAskAssistantButton.vue'; import InlineAskAssistantButton from 'n8n-design-system/components/InlineAskAssistantButton/InlineAskAssistantButton.vue';
import { useUIStore } from '@/stores/ui.store'; import { useUIStore } from '@/stores/ui.store';
import { isCommunityPackageName } from '@/utils/nodeTypesUtils'; import { isCommunityPackageName } from '@/utils/nodeTypesUtils';
import { useAIAssistantHelpers } from '@/composables/useAIAssistantHelpers';
type Props = { type Props = {
// TODO: .node can be undefined // TODO: .node can be undefined
@@ -34,6 +35,7 @@ const props = defineProps<Props>();
const clipboard = useClipboard(); const clipboard = useClipboard();
const toast = useToast(); const toast = useToast();
const i18n = useI18n(); const i18n = useI18n();
const assistantHelpers = useAIAssistantHelpers();
const nodeTypesStore = useNodeTypesStore(); const nodeTypesStore = useNodeTypesStore();
const ndvStore = useNDVStore(); const ndvStore = useNDVStore();
@@ -124,33 +126,11 @@ const isAskAssistantAvailable = computed(() => {
const assistantAlreadyAsked = computed(() => { const assistantAlreadyAsked = computed(() => {
return assistantStore.isNodeErrorActive({ return assistantStore.isNodeErrorActive({
error: simplifyErrorForAssistant(props.error), error: assistantHelpers.simplifyErrorForAssistant(props.error),
node: props.error.node || ndvStore.activeNode, node: props.error.node || ndvStore.activeNode,
}); });
}); });
function simplifyErrorForAssistant(
error: NodeError | NodeApiError | NodeOperationError,
): ChatRequest.ErrorContext['error'] {
const simple: ChatRequest.ErrorContext['error'] = {
name: error.name,
message: error.message,
};
if ('type' in error) {
simple.type = error.type;
}
if ('description' in error && error.description) {
simple.description = error.description;
}
if (error.stack) {
simple.stack = error.stack;
}
if ('lineNumber' in error) {
simple.lineNumber = error.lineNumber;
}
return simple;
}
function nodeVersionTag(nodeType: NodeError['node']): string { function nodeVersionTag(nodeType: NodeError['node']): string {
if (!nodeType || ('hidden' in nodeType && nodeType.hidden)) { if (!nodeType || ('hidden' in nodeType && nodeType.hidden)) {
return i18n.baseText('nodeSettings.deprecated'); return i18n.baseText('nodeSettings.deprecated');

View File

@@ -1,6 +1,8 @@
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
import { getReferencedNodes } from '../nodeTypesUtils';
import type { INode } from 'n8n-workflow'; import type { INode } from 'n8n-workflow';
import { useAIAssistantHelpers } from '../useAIAssistantHelpers';
import { createTestingPinia } from '@pinia/testing';
import { setActivePinia } from 'pinia';
const referencedNodesTestCases: Array<{ caseName: string; node: INode; expected: string[] }> = [ const referencedNodesTestCases: Array<{ caseName: string; node: INode; expected: string[] }> = [
{ {
@@ -376,8 +378,15 @@ const referencedNodesTestCases: Array<{ caseName: string; node: INode; expected:
]; ];
describe.each(referencedNodesTestCases)('getReferencedNodes', (testCase) => { describe.each(referencedNodesTestCases)('getReferencedNodes', (testCase) => {
let aiAssistantHelpers: ReturnType<typeof useAIAssistantHelpers>;
beforeEach(() => {
setActivePinia(createTestingPinia());
aiAssistantHelpers = useAIAssistantHelpers();
});
const caseName = testCase.caseName; const caseName = testCase.caseName;
it(`${caseName}`, () => { it(`${caseName}`, () => {
expect(getReferencedNodes(testCase.node)).toEqual(testCase.expected); expect(aiAssistantHelpers.getReferencedNodes(testCase.node)).toEqual(testCase.expected);
}); });
}); });

View File

@@ -0,0 +1,216 @@
import type { IDataObject, NodeApiError, NodeError, NodeOperationError } from 'n8n-workflow';
import { deepCopy, type INode } from 'n8n-workflow';
import { useWorkflowHelpers } from './useWorkflowHelpers';
import { useRouter } from 'vue-router';
import { useNDVStore } from '@/stores/ndv.store';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { executionDataToJson, getMainAuthField, getNodeAuthOptions } from '@/utils/nodeTypesUtils';
import type { ChatRequest } from '@/types/assistant.types';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { useDataSchema } from './useDataSchema';
import { VIEWS } from '@/constants';
import { useI18n } from './useI18n';
const CANVAS_VIEWS = [VIEWS.NEW_WORKFLOW, VIEWS.WORKFLOW, VIEWS.EXECUTION_DEBUG];
const EXECUTION_VIEWS = [VIEWS.EXECUTION_PREVIEW];
const WORKFLOW_LIST_VIEWS = [VIEWS.WORKFLOWS, VIEWS.PROJECTS_WORKFLOWS];
const CREDENTIALS_LIST_VIEWS = [VIEWS.CREDENTIALS, VIEWS.PROJECTS_CREDENTIALS];
export const useAIAssistantHelpers = () => {
const ndvStore = useNDVStore();
const nodeTypesStore = useNodeTypesStore();
const workflowsStore = useWorkflowsStore();
const workflowHelpers = useWorkflowHelpers({ router: useRouter() });
const locale = useI18n();
/**
Regular expression to extract the node names from the expressions in the template.
Supports single quotes, double quotes, and backticks.
*/
const entityRegex = /\$\(\s*(\\?["'`])((?:\\.|(?!\1)[^\\])*)\1\s*\)/g;
/**
* Extract the node names from the expressions in the template.
*/
function extractNodeNames(template: string): string[] {
let matches;
const nodeNames: string[] = [];
while ((matches = entityRegex.exec(template)) !== null) {
nodeNames.push(matches[2]);
}
return nodeNames;
}
/**
* Unescape quotes in the string. Supports single quotes, double quotes, and backticks.
*/
function unescapeQuotes(str: string): string {
return str.replace(/\\(['"`])/g, '$1');
}
/**
* Extract the node names from the expressions in the node parameters.
*/
function getReferencedNodes(node: INode): string[] {
const referencedNodes: Set<string> = new Set();
if (!node) {
return [];
}
// Go through all parameters and check if they contain expressions on any level
for (const key in node.parameters) {
let names: string[] = [];
if (
node.parameters[key] &&
typeof node.parameters[key] === 'object' &&
Object.keys(node.parameters[key]).length
) {
names = extractNodeNames(JSON.stringify(node.parameters[key]));
} else if (typeof node.parameters[key] === 'string' && node.parameters[key]) {
names = extractNodeNames(node.parameters[key]);
}
if (names.length) {
names
.map((name) => unescapeQuotes(name))
.forEach((name) => {
referencedNodes.add(name);
});
}
}
return referencedNodes.size ? Array.from(referencedNodes) : [];
}
/**
* Processes node object before sending it to AI assistant
* - Removes unnecessary properties
* - Extracts expressions from the parameters and resolves them
* @param node original node object
* @param propsToRemove properties to remove from the node object
* @returns processed node
*/
function processNodeForAssistant(node: INode, propsToRemove: string[]): INode {
// Make a copy of the node object so we don't modify the original
const nodeForLLM = deepCopy(node);
propsToRemove.forEach((key) => {
delete nodeForLLM[key as keyof INode];
});
const resolvedParameters = workflowHelpers.getNodeParametersWithResolvedExpressions(
nodeForLLM.parameters,
);
nodeForLLM.parameters = resolvedParameters;
return nodeForLLM;
}
function getNodeInfoForAssistant(node: INode): ChatRequest.NodeInfo {
if (!node) {
return {};
}
// Get all referenced nodes and their schemas
const referencedNodeNames = getReferencedNodes(node);
const schemas = getNodesSchemas(referencedNodeNames);
const nodeType = nodeTypesStore.getNodeType(node.type);
// Get node credentials details for the ai assistant
let authType = undefined;
if (nodeType) {
const authField = getMainAuthField(nodeType);
const credentialInUse = node.parameters[authField?.name ?? ''];
const availableAuthOptions = getNodeAuthOptions(nodeType);
authType = availableAuthOptions.find((option) => option.value === credentialInUse);
}
let nodeInputData: { inputNodeName?: string; inputData?: IDataObject } | undefined = undefined;
const ndvInput = ndvStore.ndvInputData;
if (isNodeReferencingInputData(node) && ndvInput?.length) {
const inputData = ndvStore.ndvInputData[0].json;
const inputNodeName = ndvStore.input.nodeName;
nodeInputData = {
inputNodeName,
inputData,
};
}
return {
authType,
schemas,
nodeInputData,
};
}
/**
* Simplify node error object for AI assistant
*/
function simplifyErrorForAssistant(
error: NodeError | NodeApiError | NodeOperationError,
): ChatRequest.ErrorContext['error'] {
const simple: ChatRequest.ErrorContext['error'] = {
name: error.name,
message: error.message,
};
if ('type' in error) {
simple.type = error.type;
}
if ('description' in error && error.description) {
simple.description = error.description;
}
if (error.stack) {
simple.stack = error.stack;
}
if ('lineNumber' in error) {
simple.lineNumber = error.lineNumber;
}
return simple;
}
function isNodeReferencingInputData(node: INode): boolean {
const parametersString = JSON.stringify(node.parameters);
const references = ['$json', '$input', '$binary'];
return references.some((ref) => parametersString.includes(ref));
}
/**
* Get the schema for the referenced nodes as expected by the AI assistant
* @param nodeNames The names of the nodes to get the schema for
* @returns An array of NodeExecutionSchema objects
*/
function getNodesSchemas(nodeNames: string[]) {
const schemas: ChatRequest.NodeExecutionSchema[] = [];
for (const name of nodeNames) {
const node = workflowsStore.getNodeByName(name);
if (!node) {
continue;
}
const { getSchemaForExecutionData, getInputDataWithPinned } = useDataSchema();
const schema = getSchemaForExecutionData(executionDataToJson(getInputDataWithPinned(node)));
schemas.push({
nodeName: node.name,
schema,
});
}
return schemas;
}
function getCurrentViewDescription(view: VIEWS) {
switch (true) {
case WORKFLOW_LIST_VIEWS.includes(view):
return locale.baseText('aiAssistant.prompts.currentView.workflowList');
case CREDENTIALS_LIST_VIEWS.includes(view):
return locale.baseText('aiAssistant.prompts.currentView.credentialsList');
case EXECUTION_VIEWS.includes(view):
return locale.baseText('aiAssistant.prompts.currentView.executionsView');
case CANVAS_VIEWS.includes(view):
return locale.baseText('aiAssistant.prompts.currentView.workflowEditor');
default:
return undefined;
}
}
return {
processNodeForAssistant,
getNodeInfoForAssistant,
simplifyErrorForAssistant,
isNodeReferencingInputData,
getNodesSchemas,
getCurrentViewDescription,
getReferencedNodes,
};
};

View File

@@ -150,6 +150,10 @@
"aiAssistant.codeUpdated.message.body2": "node to see the changes", "aiAssistant.codeUpdated.message.body2": "node to see the changes",
"aiAssistant.thinkingSteps.analyzingError": "Analyzing the error...", "aiAssistant.thinkingSteps.analyzingError": "Analyzing the error...",
"aiAssistant.thinkingSteps.thinking": "Thinking...", "aiAssistant.thinkingSteps.thinking": "Thinking...",
"aiAssistant.prompts.currentView.workflowList": "The user is currently looking at the list of workflows.",
"aiAssistant.prompts.currentView.credentialsList": "The user is currently looking at the list of credentials.",
"aiAssistant.prompts.currentView.executionsView": "The user is currently looking at the list of executions for the currently open workflow.",
"aiAssistant.prompts.currentView.workflowEditor": "The user is currently looking at the current workflow in n8n editor, without any specific node selected.",
"banners.confirmEmail.message.1": "To secure your account and prevent future access issues, please confirm your", "banners.confirmEmail.message.1": "To secure your account and prevent future access issues, please confirm your",
"banners.confirmEmail.message.2": "email address.", "banners.confirmEmail.message.2": "email address.",
"banners.confirmEmail.button": "Confirm email", "banners.confirmEmail.button": "Confirm email",

View File

@@ -5,6 +5,7 @@ import {
STORES, STORES,
AI_ASSISTANT_EXPERIMENT, AI_ASSISTANT_EXPERIMENT,
PLACEHOLDER_EMPTY_WORKFLOW_ID, PLACEHOLDER_EMPTY_WORKFLOW_ID,
CREDENTIAL_EDIT_MODAL_KEY,
} from '@/constants'; } from '@/constants';
import type { ChatRequest } from '@/types/assistant.types'; import type { ChatRequest } from '@/types/assistant.types';
import type { ChatUI } from 'n8n-design-system/types/assistant'; import type { ChatUI } from 'n8n-design-system/types/assistant';
@@ -17,26 +18,19 @@ import { useRoute } from 'vue-router';
import { useSettingsStore } from './settings.store'; import { useSettingsStore } from './settings.store';
import { assert } from '@/utils/assert'; import { assert } from '@/utils/assert';
import { useWorkflowsStore } from './workflows.store'; import { useWorkflowsStore } from './workflows.store';
import type { IDataObject, ICredentialType, INodeParameters } from 'n8n-workflow'; import type { ICredentialType, INodeParameters, NodeError, INode } from 'n8n-workflow';
import { deepCopy } from 'n8n-workflow'; import { deepCopy } from 'n8n-workflow';
import { ndvEventBus, codeNodeEditorEventBus } from '@/event-bus'; import { ndvEventBus, codeNodeEditorEventBus } from '@/event-bus';
import { useNDVStore } from './ndv.store'; import { useNDVStore } from './ndv.store';
import type { IUpdateInformation } from '@/Interface'; import type { IUpdateInformation } from '@/Interface';
import {
getMainAuthField,
getNodeAuthOptions,
getReferencedNodes,
getNodesSchemas,
processNodeForAssistant,
isNodeReferencingInputData,
} from '@/utils/nodeTypesUtils';
import { useNodeTypesStore } from './nodeTypes.store';
import { usePostHog } from './posthog.store'; import { usePostHog } from './posthog.store';
import { useI18n } from '@/composables/useI18n'; import { useI18n } from '@/composables/useI18n';
import { useTelemetry } from '@/composables/useTelemetry'; import { useTelemetry } from '@/composables/useTelemetry';
import { useToast } from '@/composables/useToast'; import { useToast } from '@/composables/useToast';
import { useUIStore } from './ui.store'; import { useUIStore } from './ui.store';
import AiUpdatedCodeMessage from '@/components/AiUpdatedCodeMessage.vue'; import AiUpdatedCodeMessage from '@/components/AiUpdatedCodeMessage.vue';
import { useCredentialsStore } from './credentials.store';
import { useAIAssistantHelpers } from '@/composables/useAIAssistantHelpers';
export const MAX_CHAT_WIDTH = 425; export const MAX_CHAT_WIDTH = 425;
export const MIN_CHAT_WIDTH = 250; export const MIN_CHAT_WIDTH = 250;
@@ -69,6 +63,7 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
const { getVariant } = usePostHog(); const { getVariant } = usePostHog();
const locale = useI18n(); const locale = useI18n();
const telemetry = useTelemetry(); const telemetry = useTelemetry();
const assistantHelpers = useAIAssistantHelpers();
const suggestions = ref<{ const suggestions = ref<{
[suggestionId: string]: { [suggestionId: string]: {
@@ -355,10 +350,74 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
}); });
} }
/**
* Gets information about the current view and active node to provide context to the assistant
*/
function getVisualContext(nodeInfo?: ChatRequest.NodeInfo): ChatRequest.UserContext | undefined {
if (chatSessionTask.value === 'error') {
return undefined;
}
const currentView = route.name as VIEWS;
const activeNode = workflowsStore.activeNode();
const activeNodeForLLM = activeNode
? assistantHelpers.processNodeForAssistant(activeNode, ['position'])
: null;
const activeModals = uiStore.activeModals;
const isCredentialModalActive = activeModals.includes(CREDENTIAL_EDIT_MODAL_KEY);
const activeCredential = isCredentialModalActive
? useCredentialsStore().getCredentialTypeByName(uiStore.activeCredentialType ?? '')
: undefined;
const executionResult = workflowsStore.workflowExecutionData?.data?.resultData;
const isCurrentNodeExecuted = Boolean(
executionResult?.runData?.hasOwnProperty(activeNode?.name ?? ''),
);
const currentNodeHasError =
executionResult?.error &&
'node' in executionResult.error &&
executionResult.error.node?.name === activeNode?.name;
const nodeError = currentNodeHasError ? (executionResult.error as NodeError) : undefined;
const executionStatus = isCurrentNodeExecuted
? {
status: nodeError ? 'error' : 'success',
error: nodeError ? assistantHelpers.simplifyErrorForAssistant(nodeError) : undefined,
}
: undefined;
return {
currentView: {
name: currentView,
description: assistantHelpers.getCurrentViewDescription(currentView),
},
activeNodeInfo: {
node: activeNodeForLLM ?? undefined,
nodeIssues: !isCurrentNodeExecuted ? activeNode?.issues : undefined,
executionStatus,
nodeInputData: nodeInfo?.nodeInputData,
referencedNodes: nodeInfo?.schemas,
},
activeCredentials: activeCredential
? {
name: activeCredential?.name,
displayName: activeCredential?.displayName,
authType: nodeInfo?.authType?.name,
}
: undefined,
};
}
async function initSupportChat(userMessage: string, credentialType?: ICredentialType) { async function initSupportChat(userMessage: string, credentialType?: ICredentialType) {
const id = getRandomId();
resetAssistantChat(); resetAssistantChat();
chatSessionTask.value = credentialType ? 'credentials' : 'support'; chatSessionTask.value = credentialType ? 'credentials' : 'support';
const activeNode = workflowsStore.activeNode() as INode;
const nodeInfo = assistantHelpers.getNodeInfoForAssistant(activeNode);
// For the initial message, only provide visual context if the task is support
const visualContext =
chatSessionTask.value === 'support' ? getVisualContext(nodeInfo) : undefined;
if (nodeInfo.authType && chatSessionTask.value === 'credentials') {
userMessage += ` I am using ${nodeInfo.authType.name}.`;
}
const id = getRandomId();
chatSessionCredType.value = credentialType; chatSessionCredType.value = credentialType;
addUserMessage(userMessage, id); addUserMessage(userMessage, id);
addLoadingAssistantMessage(locale.baseText('aiAssistant.thinkingSteps.thinking')); addLoadingAssistantMessage(locale.baseText('aiAssistant.thinkingSteps.thinking'));
@@ -371,6 +430,7 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
user: { user: {
firstName: usersStore.currentUser?.firstName ?? '', firstName: usersStore.currentUser?.firstName ?? '',
}, },
context: visualContext,
question: userMessage, question: userMessage,
}; };
if (credentialType) { if (credentialType) {
@@ -413,29 +473,10 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
currentSessionActiveExecutionId.value = workflowsStore.activeExecutionId; currentSessionActiveExecutionId.value = workflowsStore.activeExecutionId;
} }
// Get all referenced nodes and their schemas const { authType, nodeInputData, schemas } = assistantHelpers.getNodeInfoForAssistant(
const referencedNodeNames = getReferencedNodes(context.node); context.node,
const schemas = getNodesSchemas(referencedNodeNames); );
// Get node credentials details for the ai assistant
const nodeType = useNodeTypesStore().getNodeType(context.node.type);
let authType = undefined;
if (nodeType) {
const authField = getMainAuthField(nodeType);
const credentialInUse = context.node.parameters[authField?.name ?? ''];
const availableAuthOptions = getNodeAuthOptions(nodeType);
authType = availableAuthOptions.find((option) => option.value === credentialInUse);
}
let nodeInputData: { inputNodeName?: string; inputData?: IDataObject } | undefined = undefined;
const ndvInput = ndvStore.ndvInputData;
if (isNodeReferencingInputData(context.node) && ndvInput?.length) {
const inputData = ndvStore.ndvInputData[0].json;
const inputNodeName = ndvStore.input.nodeName;
nodeInputData = {
inputNodeName,
inputData,
};
}
addLoadingAssistantMessage(locale.baseText('aiAssistant.thinkingSteps.analyzingError')); addLoadingAssistantMessage(locale.baseText('aiAssistant.thinkingSteps.analyzingError'));
openChat(); openChat();
@@ -450,7 +491,7 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
firstName: usersStore.currentUser?.firstName ?? '', firstName: usersStore.currentUser?.firstName ?? '',
}, },
error: context.error, error: context.error,
node: processNodeForAssistant(context.node, ['position']), node: assistantHelpers.processNodeForAssistant(context.node, ['position']),
nodeInputData, nodeInputData,
executionSchema: schemas, executionSchema: schemas,
authType, authType,
@@ -536,6 +577,7 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
) { ) {
nodeExecutionStatus.value = 'not_executed'; nodeExecutionStatus.value = 'not_executed';
} }
const userContext = getVisualContext();
chatWithAssistant( chatWithAssistant(
rootStore.restApiContext, rootStore.restApiContext,
{ {
@@ -544,6 +586,7 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
type: 'message', type: 'message',
text: chatMessage.text, text: chatMessage.text,
quickReplyType: chatMessage.quickReplyType, quickReplyType: chatMessage.quickReplyType,
context: userContext,
}, },
sessionId: currentSessionId.value, sessionId: currentSessionId.value,
}, },

View File

@@ -303,6 +303,8 @@ export const useUIStore = defineStore(STORES.UI, () => {
}, {}), }, {}),
); );
const activeModals = computed(() => modalStack.value.map((modalName) => modalName));
const fakeDoorsByLocation = computed(() => const fakeDoorsByLocation = computed(() =>
fakeDoorFeatures.value.reduce((acc: { [uiLocation: string]: IFakeDoor }, fakeDoor) => { fakeDoorFeatures.value.reduce((acc: { [uiLocation: string]: IFakeDoor }, fakeDoor) => {
fakeDoor.uiLocations.forEach((uiLocation: IFakeDoorLocation) => { fakeDoor.uiLocations.forEach((uiLocation: IFakeDoorLocation) => {
@@ -700,6 +702,7 @@ export const useUIStore = defineStore(STORES.UI, () => {
setNotificationsForView, setNotificationsForView,
deleteNotificationsForView, deleteNotificationsForView,
resetLastInteractedWith, resetLastInteractedWith,
activeModals,
}; };
}); });

View File

@@ -1,5 +1,12 @@
import type { Schema } from '@/Interface'; import type { VIEWS } from '@/constants';
import type { IDataObject, INode, INodeParameters } from 'n8n-workflow'; import type { NodeAuthenticationOption, Schema } from '@/Interface';
import type {
ICredentialType,
IDataObject,
INode,
INodeIssues,
INodeParameters,
} from 'n8n-workflow';
export namespace ChatRequest { export namespace ChatRequest {
export interface NodeExecutionSchema { export interface NodeExecutionSchema {
@@ -39,6 +46,7 @@ export namespace ChatRequest {
user: { user: {
firstName: string; firstName: string;
}; };
context?: UserContext;
question: string; question: string;
} }
@@ -69,6 +77,25 @@ export namespace ChatRequest {
type: 'message'; type: 'message';
text: string; text: string;
quickReplyType?: string; quickReplyType?: string;
context?: UserContext;
}
export interface UserContext {
activeNodeInfo?: {
node?: INode;
nodeIssues?: INodeIssues;
nodeInputData?: IDataObject;
referencedNodes?: NodeExecutionSchema[];
executionStatus?: {
status: string;
error?: ErrorContext['error'];
};
};
activeCredentials?: Pick<ICredentialType, 'name' | 'displayName'> & { authType?: string };
currentView?: {
name: VIEWS;
description?: string;
};
} }
export type RequestPayload = export type RequestPayload =
@@ -146,6 +173,15 @@ export namespace ChatRequest {
sessionId?: string; sessionId?: string;
messages: MessageResponse[]; messages: MessageResponse[];
} }
export interface NodeInfo {
authType?: NodeAuthenticationOption;
schemas?: NodeExecutionSchema[];
nodeInputData?: {
inputNodeName?: string;
inputData?: IDataObject;
};
}
} }
export namespace ReplaceCodeRequest { export namespace ReplaceCodeRequest {

View File

@@ -7,8 +7,6 @@ import type {
NodeAuthenticationOption, NodeAuthenticationOption,
SimplifiedNodeType, SimplifiedNodeType,
} from '@/Interface'; } from '@/Interface';
import { useDataSchema } from '@/composables/useDataSchema';
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
import { import {
CORE_NODES_CATEGORY, CORE_NODES_CATEGORY,
MAIN_AUTH_FIELD_NAME, MAIN_AUTH_FIELD_NAME,
@@ -20,22 +18,18 @@ import { i18n as locale } from '@/plugins/i18n';
import { useCredentialsStore } from '@/stores/credentials.store'; import { useCredentialsStore } from '@/stores/credentials.store';
import { useNodeTypesStore } from '@/stores/nodeTypes.store'; import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { useWorkflowsStore } from '@/stores/workflows.store'; import { useWorkflowsStore } from '@/stores/workflows.store';
import type { ChatRequest } from '@/types/assistant.types';
import { isResourceLocatorValue } from '@/utils/typeGuards'; import { isResourceLocatorValue } from '@/utils/typeGuards';
import { isJsonKeyObject } from '@/utils/typesUtils'; import { isJsonKeyObject } from '@/utils/typesUtils';
import { import type {
deepCopy, IDataObject,
type IDataObject, INodeCredentialDescription,
type INode, INodeExecutionData,
type INodeCredentialDescription, INodeProperties,
type INodeExecutionData, INodeTypeDescription,
type INodeProperties, NodeParameterValueType,
type INodeTypeDescription, ResourceMapperField,
type NodeParameterValueType, Themed,
type ResourceMapperField,
type Themed,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { useRouter } from 'vue-router';
/* /*
Constants and utility functions mainly used to get information about Constants and utility functions mainly used to get information about
@@ -502,109 +496,3 @@ export const getNodeIconColor = (
} }
return nodeType?.defaults?.color?.toString(); return nodeType?.defaults?.color?.toString();
}; };
/**
Regular expression to extract the node names from the expressions in the template.
Supports single quotes, double quotes, and backticks.
*/
const entityRegex = /\$\(\s*(\\?["'`])((?:\\.|(?!\1)[^\\])*)\1\s*\)/g;
/**
* Extract the node names from the expressions in the template.
*/
function extractNodeNames(template: string): string[] {
let matches;
const nodeNames: string[] = [];
while ((matches = entityRegex.exec(template)) !== null) {
nodeNames.push(matches[2]);
}
return nodeNames;
}
/**
* Unescape quotes in the string. Supports single quotes, double quotes, and backticks.
*/
export function unescapeQuotes(str: string): string {
return str.replace(/\\(['"`])/g, '$1');
}
/**
* Extract the node names from the expressions in the node parameters.
*/
export function getReferencedNodes(node: INode): string[] {
const referencedNodes: Set<string> = new Set();
if (!node) {
return [];
}
// Go through all parameters and check if they contain expressions on any level
for (const key in node.parameters) {
let names: string[] = [];
if (
node.parameters[key] &&
typeof node.parameters[key] === 'object' &&
Object.keys(node.parameters[key]).length
) {
names = extractNodeNames(JSON.stringify(node.parameters[key]));
} else if (typeof node.parameters[key] === 'string' && node.parameters[key]) {
names = extractNodeNames(node.parameters[key]);
}
if (names.length) {
names
.map((name) => unescapeQuotes(name))
.forEach((name) => {
referencedNodes.add(name);
});
}
}
return referencedNodes.size ? Array.from(referencedNodes) : [];
}
/**
* Processes node object before sending it to AI assistant
* - Removes unnecessary properties
* - Extracts expressions from the parameters and resolves them
* @param node original node object
* @param propsToRemove properties to remove from the node object
* @returns processed node
*/
export function processNodeForAssistant(node: INode, propsToRemove: string[]): INode {
// Make a copy of the node object so we don't modify the original
const nodeForLLM = deepCopy(node);
propsToRemove.forEach((key) => {
delete nodeForLLM[key as keyof INode];
});
const workflowHelpers = useWorkflowHelpers({ router: useRouter() });
const resolvedParameters = workflowHelpers.getNodeParametersWithResolvedExpressions(
nodeForLLM.parameters,
);
nodeForLLM.parameters = resolvedParameters;
return nodeForLLM;
}
export function isNodeReferencingInputData(node: INode): boolean {
const parametersString = JSON.stringify(node.parameters);
const references = ['$json', '$input', '$binary'];
return references.some((ref) => parametersString.includes(ref));
}
/**
* Get the schema for the referenced nodes as expected by the AI assistant
* @param nodeNames The names of the nodes to get the schema for
* @returns An array of NodeExecutionSchema objects
*/
export function getNodesSchemas(nodeNames: string[]) {
const schemas: ChatRequest.NodeExecutionSchema[] = [];
for (const name of nodeNames) {
const node = useWorkflowsStore().getNodeByName(name);
if (!node) {
continue;
}
const { getSchemaForExecutionData, getInputDataWithPinned } = useDataSchema();
const schema = getSchemaForExecutionData(executionDataToJson(getInputDataWithPinned(node)));
schemas.push({
nodeName: node.name,
schema,
});
}
return schemas;
}