mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
feat: Add more context to support chat (#11014)
This commit is contained in:
committed by
GitHub
parent
5903592a23
commit
8a30f92156
@@ -83,10 +83,10 @@ const i18n = useI18n();
|
||||
const telemetry = useTelemetry();
|
||||
|
||||
onBeforeMount(async () => {
|
||||
if (rootStore.defaultLocale === 'en') return;
|
||||
|
||||
uiStore.activeCredentialType = props.credentialType.name;
|
||||
|
||||
if (rootStore.defaultLocale === 'en') return;
|
||||
|
||||
const key = `n8n-nodes-base.credentials.${props.credentialType.name}`;
|
||||
|
||||
if (i18n.exists(key)) return;
|
||||
|
||||
@@ -420,6 +420,7 @@ async function beforeClose() {
|
||||
}
|
||||
|
||||
if (!keepEditing) {
|
||||
uiStore.activeCredentialType = null;
|
||||
return true;
|
||||
} else if (!requiredPropertiesFilled.value) {
|
||||
showValidationWarning.value = true;
|
||||
@@ -986,6 +987,7 @@ async function onAuthTypeChanged(type: string): Promise<void> {
|
||||
const credentialsForType = getNodeCredentialForSelectedAuthType(activeNodeType.value, type);
|
||||
if (credentialsForType) {
|
||||
selectedCredential.value = credentialsForType.name;
|
||||
uiStore.activeCredentialType = credentialsForType.name;
|
||||
resetCredentialData();
|
||||
// Update current node auth type so credentials dropdown can be displayed properly
|
||||
updateNodeAuthType(ndvStore.activeNode, type);
|
||||
|
||||
@@ -23,6 +23,7 @@ import type { ChatRequest } from '@/types/assistant.types';
|
||||
import InlineAskAssistantButton from 'n8n-design-system/components/InlineAskAssistantButton/InlineAskAssistantButton.vue';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import { isCommunityPackageName } from '@/utils/nodeTypesUtils';
|
||||
import { useAIAssistantHelpers } from '@/composables/useAIAssistantHelpers';
|
||||
|
||||
type Props = {
|
||||
// TODO: .node can be undefined
|
||||
@@ -34,6 +35,7 @@ const props = defineProps<Props>();
|
||||
const clipboard = useClipboard();
|
||||
const toast = useToast();
|
||||
const i18n = useI18n();
|
||||
const assistantHelpers = useAIAssistantHelpers();
|
||||
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
const ndvStore = useNDVStore();
|
||||
@@ -124,33 +126,11 @@ const isAskAssistantAvailable = computed(() => {
|
||||
|
||||
const assistantAlreadyAsked = computed(() => {
|
||||
return assistantStore.isNodeErrorActive({
|
||||
error: simplifyErrorForAssistant(props.error),
|
||||
error: assistantHelpers.simplifyErrorForAssistant(props.error),
|
||||
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 {
|
||||
if (!nodeType || ('hidden' in nodeType && nodeType.hidden)) {
|
||||
return i18n.baseText('nodeSettings.deprecated');
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { getReferencedNodes } from '../nodeTypesUtils';
|
||||
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[] }> = [
|
||||
{
|
||||
@@ -376,8 +378,15 @@ const referencedNodesTestCases: Array<{ caseName: string; node: INode; expected:
|
||||
];
|
||||
|
||||
describe.each(referencedNodesTestCases)('getReferencedNodes', (testCase) => {
|
||||
let aiAssistantHelpers: ReturnType<typeof useAIAssistantHelpers>;
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createTestingPinia());
|
||||
aiAssistantHelpers = useAIAssistantHelpers();
|
||||
});
|
||||
|
||||
const caseName = testCase.caseName;
|
||||
it(`${caseName}`, () => {
|
||||
expect(getReferencedNodes(testCase.node)).toEqual(testCase.expected);
|
||||
expect(aiAssistantHelpers.getReferencedNodes(testCase.node)).toEqual(testCase.expected);
|
||||
});
|
||||
});
|
||||
216
packages/editor-ui/src/composables/useAIAssistantHelpers.ts
Normal file
216
packages/editor-ui/src/composables/useAIAssistantHelpers.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
@@ -150,6 +150,10 @@
|
||||
"aiAssistant.codeUpdated.message.body2": "node to see the changes",
|
||||
"aiAssistant.thinkingSteps.analyzingError": "Analyzing the error...",
|
||||
"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.2": "email address.",
|
||||
"banners.confirmEmail.button": "Confirm email",
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
STORES,
|
||||
AI_ASSISTANT_EXPERIMENT,
|
||||
PLACEHOLDER_EMPTY_WORKFLOW_ID,
|
||||
CREDENTIAL_EDIT_MODAL_KEY,
|
||||
} from '@/constants';
|
||||
import type { ChatRequest } from '@/types/assistant.types';
|
||||
import type { ChatUI } from 'n8n-design-system/types/assistant';
|
||||
@@ -17,26 +18,19 @@ import { useRoute } from 'vue-router';
|
||||
import { useSettingsStore } from './settings.store';
|
||||
import { assert } from '@/utils/assert';
|
||||
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 { ndvEventBus, codeNodeEditorEventBus } from '@/event-bus';
|
||||
import { useNDVStore } from './ndv.store';
|
||||
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 { useI18n } from '@/composables/useI18n';
|
||||
import { useTelemetry } from '@/composables/useTelemetry';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import { useUIStore } from './ui.store';
|
||||
import AiUpdatedCodeMessage from '@/components/AiUpdatedCodeMessage.vue';
|
||||
import { useCredentialsStore } from './credentials.store';
|
||||
import { useAIAssistantHelpers } from '@/composables/useAIAssistantHelpers';
|
||||
|
||||
export const MAX_CHAT_WIDTH = 425;
|
||||
export const MIN_CHAT_WIDTH = 250;
|
||||
@@ -69,6 +63,7 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
|
||||
const { getVariant } = usePostHog();
|
||||
const locale = useI18n();
|
||||
const telemetry = useTelemetry();
|
||||
const assistantHelpers = useAIAssistantHelpers();
|
||||
|
||||
const suggestions = ref<{
|
||||
[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) {
|
||||
const id = getRandomId();
|
||||
resetAssistantChat();
|
||||
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;
|
||||
addUserMessage(userMessage, id);
|
||||
addLoadingAssistantMessage(locale.baseText('aiAssistant.thinkingSteps.thinking'));
|
||||
@@ -371,6 +430,7 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
|
||||
user: {
|
||||
firstName: usersStore.currentUser?.firstName ?? '',
|
||||
},
|
||||
context: visualContext,
|
||||
question: userMessage,
|
||||
};
|
||||
if (credentialType) {
|
||||
@@ -413,29 +473,10 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
|
||||
currentSessionActiveExecutionId.value = workflowsStore.activeExecutionId;
|
||||
}
|
||||
|
||||
// Get all referenced nodes and their schemas
|
||||
const referencedNodeNames = getReferencedNodes(context.node);
|
||||
const schemas = getNodesSchemas(referencedNodeNames);
|
||||
const { authType, nodeInputData, schemas } = assistantHelpers.getNodeInfoForAssistant(
|
||||
context.node,
|
||||
);
|
||||
|
||||
// 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'));
|
||||
openChat();
|
||||
|
||||
@@ -450,7 +491,7 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
|
||||
firstName: usersStore.currentUser?.firstName ?? '',
|
||||
},
|
||||
error: context.error,
|
||||
node: processNodeForAssistant(context.node, ['position']),
|
||||
node: assistantHelpers.processNodeForAssistant(context.node, ['position']),
|
||||
nodeInputData,
|
||||
executionSchema: schemas,
|
||||
authType,
|
||||
@@ -536,6 +577,7 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
|
||||
) {
|
||||
nodeExecutionStatus.value = 'not_executed';
|
||||
}
|
||||
const userContext = getVisualContext();
|
||||
chatWithAssistant(
|
||||
rootStore.restApiContext,
|
||||
{
|
||||
@@ -544,6 +586,7 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
|
||||
type: 'message',
|
||||
text: chatMessage.text,
|
||||
quickReplyType: chatMessage.quickReplyType,
|
||||
context: userContext,
|
||||
},
|
||||
sessionId: currentSessionId.value,
|
||||
},
|
||||
|
||||
@@ -303,6 +303,8 @@ export const useUIStore = defineStore(STORES.UI, () => {
|
||||
}, {}),
|
||||
);
|
||||
|
||||
const activeModals = computed(() => modalStack.value.map((modalName) => modalName));
|
||||
|
||||
const fakeDoorsByLocation = computed(() =>
|
||||
fakeDoorFeatures.value.reduce((acc: { [uiLocation: string]: IFakeDoor }, fakeDoor) => {
|
||||
fakeDoor.uiLocations.forEach((uiLocation: IFakeDoorLocation) => {
|
||||
@@ -700,6 +702,7 @@ export const useUIStore = defineStore(STORES.UI, () => {
|
||||
setNotificationsForView,
|
||||
deleteNotificationsForView,
|
||||
resetLastInteractedWith,
|
||||
activeModals,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import type { Schema } from '@/Interface';
|
||||
import type { IDataObject, INode, INodeParameters } from 'n8n-workflow';
|
||||
import type { VIEWS } from '@/constants';
|
||||
import type { NodeAuthenticationOption, Schema } from '@/Interface';
|
||||
import type {
|
||||
ICredentialType,
|
||||
IDataObject,
|
||||
INode,
|
||||
INodeIssues,
|
||||
INodeParameters,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
export namespace ChatRequest {
|
||||
export interface NodeExecutionSchema {
|
||||
@@ -39,6 +46,7 @@ export namespace ChatRequest {
|
||||
user: {
|
||||
firstName: string;
|
||||
};
|
||||
context?: UserContext;
|
||||
question: string;
|
||||
}
|
||||
|
||||
@@ -69,6 +77,25 @@ export namespace ChatRequest {
|
||||
type: 'message';
|
||||
text: 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 =
|
||||
@@ -146,6 +173,15 @@ export namespace ChatRequest {
|
||||
sessionId?: string;
|
||||
messages: MessageResponse[];
|
||||
}
|
||||
|
||||
export interface NodeInfo {
|
||||
authType?: NodeAuthenticationOption;
|
||||
schemas?: NodeExecutionSchema[];
|
||||
nodeInputData?: {
|
||||
inputNodeName?: string;
|
||||
inputData?: IDataObject;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export namespace ReplaceCodeRequest {
|
||||
|
||||
@@ -7,8 +7,6 @@ import type {
|
||||
NodeAuthenticationOption,
|
||||
SimplifiedNodeType,
|
||||
} from '@/Interface';
|
||||
import { useDataSchema } from '@/composables/useDataSchema';
|
||||
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
||||
import {
|
||||
CORE_NODES_CATEGORY,
|
||||
MAIN_AUTH_FIELD_NAME,
|
||||
@@ -20,22 +18,18 @@ import { i18n as locale } from '@/plugins/i18n';
|
||||
import { useCredentialsStore } from '@/stores/credentials.store';
|
||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import type { ChatRequest } from '@/types/assistant.types';
|
||||
import { isResourceLocatorValue } from '@/utils/typeGuards';
|
||||
import { isJsonKeyObject } from '@/utils/typesUtils';
|
||||
import {
|
||||
deepCopy,
|
||||
type IDataObject,
|
||||
type INode,
|
||||
type INodeCredentialDescription,
|
||||
type INodeExecutionData,
|
||||
type INodeProperties,
|
||||
type INodeTypeDescription,
|
||||
type NodeParameterValueType,
|
||||
type ResourceMapperField,
|
||||
type Themed,
|
||||
import type {
|
||||
IDataObject,
|
||||
INodeCredentialDescription,
|
||||
INodeExecutionData,
|
||||
INodeProperties,
|
||||
INodeTypeDescription,
|
||||
NodeParameterValueType,
|
||||
ResourceMapperField,
|
||||
Themed,
|
||||
} from 'n8n-workflow';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
/*
|
||||
Constants and utility functions mainly used to get information about
|
||||
@@ -502,109 +496,3 @@ export const getNodeIconColor = (
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user