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();
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;

View File

@@ -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);

View File

@@ -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');

View File

@@ -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);
});
});

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.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",

View File

@@ -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,
},

View File

@@ -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,
};
});

View File

@@ -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 {

View File

@@ -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;
}