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();
|
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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
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.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",
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user