mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-22 20:29:08 +00:00
feat(core): Implement partial execution for all tool nodes (#15168)
This commit is contained in:
@@ -217,6 +217,12 @@ export interface IStartRunData {
|
||||
name: string;
|
||||
data?: ITaskData;
|
||||
};
|
||||
agentRequest?: {
|
||||
query: NodeParameterValueType;
|
||||
tool: {
|
||||
name: NodeParameterValueType;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface ITableData {
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
import FromAiParametersModal from '@/components/FromAiParametersModal.vue';
|
||||
import { FROM_AI_PARAMETERS_MODAL_KEY, STORES } from '@/constants';
|
||||
import { FROM_AI_PARAMETERS_MODAL_KEY, STORES, AI_MCP_TOOL_NODE_TYPE } from '@/constants';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { useParameterOverridesStore } from '@/stores/parameterOverrides.store';
|
||||
import { useAgentRequestStore } from '@/stores/agentRequest.store';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { NodeConnectionTypes } from 'n8n-workflow';
|
||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||
import { nextTick } from 'vue';
|
||||
|
||||
const ModalStub = {
|
||||
template: `
|
||||
@@ -26,12 +28,20 @@ vi.mocked(useRouter);
|
||||
const mockNode = {
|
||||
id: 'id1',
|
||||
name: 'Test Node',
|
||||
type: 'n8n-nodes-base.ai-tool',
|
||||
parameters: {
|
||||
testBoolean: "={{ $fromAI('testBoolean', ``, 'boolean') }}",
|
||||
testParam: "={{ $fromAi('testParam', ``, 'string') }}",
|
||||
},
|
||||
};
|
||||
|
||||
const mockMcpNode = {
|
||||
id: 'id1',
|
||||
name: 'Test MCP Node',
|
||||
type: AI_MCP_TOOL_NODE_TYPE,
|
||||
parameters: {},
|
||||
};
|
||||
|
||||
const mockParentNode = {
|
||||
name: 'Parent Node',
|
||||
};
|
||||
@@ -43,7 +53,7 @@ const mockRunData = {
|
||||
['Test Node']: [
|
||||
{
|
||||
inputOverride: {
|
||||
[NodeConnectionTypes.AiTool]: [[{ json: { testParam: 'override' } }]],
|
||||
[NodeConnectionTypes.AiTool]: [[{ json: { query: { testParam: 'override' } } }]],
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -57,10 +67,27 @@ const mockWorkflow = {
|
||||
getChildNodes: () => ['Parent Node'],
|
||||
};
|
||||
|
||||
const mockTools = [
|
||||
{
|
||||
name: 'Test Tool',
|
||||
value: 'test-tool',
|
||||
inputSchema: {
|
||||
properties: {
|
||||
query: {
|
||||
type: 'string',
|
||||
description: 'Test query',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const renderModal = createComponentRenderer(FromAiParametersModal);
|
||||
let pinia: ReturnType<typeof createTestingPinia>;
|
||||
let workflowsStore: ReturnType<typeof useWorkflowsStore>;
|
||||
let parameterOverridesStore: ReturnType<typeof useParameterOverridesStore>;
|
||||
let agentRequestStore: ReturnType<typeof useAgentRequestStore>;
|
||||
let nodeTypesStore: ReturnType<typeof useNodeTypesStore>;
|
||||
|
||||
describe('FromAiParametersModal', () => {
|
||||
beforeEach(() => {
|
||||
pinia = createTestingPinia({
|
||||
@@ -83,14 +110,23 @@ describe('FromAiParametersModal', () => {
|
||||
},
|
||||
});
|
||||
workflowsStore = useWorkflowsStore();
|
||||
workflowsStore.getNodeByName = vi
|
||||
.fn()
|
||||
.mockImplementation((name: string) => (name === 'Test Node' ? mockNode : mockParentNode));
|
||||
workflowsStore.getNodeByName = vi.fn().mockImplementation((name: string) => {
|
||||
switch (name) {
|
||||
case 'Test Node':
|
||||
return mockNode;
|
||||
case 'Test MCP Node':
|
||||
return mockMcpNode;
|
||||
default:
|
||||
return mockParentNode;
|
||||
}
|
||||
});
|
||||
workflowsStore.getCurrentWorkflow = vi.fn().mockReturnValue(mockWorkflow);
|
||||
parameterOverridesStore = useParameterOverridesStore();
|
||||
parameterOverridesStore.clearParameterOverrides = vi.fn();
|
||||
parameterOverridesStore.addParameterOverrides = vi.fn();
|
||||
parameterOverridesStore.substituteParameters = vi.fn();
|
||||
agentRequestStore = useAgentRequestStore();
|
||||
agentRequestStore.clearAgentRequests = vi.fn();
|
||||
agentRequestStore.addAgentRequests = vi.fn();
|
||||
agentRequestStore.generateAgentRequest = vi.fn();
|
||||
nodeTypesStore = useNodeTypesStore();
|
||||
nodeTypesStore.getNodeParameterOptions = vi.fn().mockResolvedValue(mockTools);
|
||||
});
|
||||
|
||||
it('renders correctly with node data', () => {
|
||||
@@ -112,6 +148,53 @@ describe('FromAiParametersModal', () => {
|
||||
expect(getByTitle('Test Test Node')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('shows tool selection for AI tool nodes', async () => {
|
||||
const { findByRole } = renderModal({
|
||||
props: {
|
||||
modalName: FROM_AI_PARAMETERS_MODAL_KEY,
|
||||
data: {
|
||||
nodeName: 'Test MCP Node',
|
||||
},
|
||||
},
|
||||
global: {
|
||||
stubs: {
|
||||
Modal: ModalStub,
|
||||
},
|
||||
},
|
||||
pinia,
|
||||
});
|
||||
|
||||
const toolSelect = await findByRole('combobox');
|
||||
expect(toolSelect).toBeTruthy();
|
||||
});
|
||||
|
||||
it('shows tool parameters after tool selection', async () => {
|
||||
const { getByTestId, findByRole, findByText } = renderModal({
|
||||
props: {
|
||||
modalName: FROM_AI_PARAMETERS_MODAL_KEY,
|
||||
data: {
|
||||
nodeName: 'Test MCP Node',
|
||||
},
|
||||
},
|
||||
global: {
|
||||
stubs: {
|
||||
Modal: ModalStub,
|
||||
},
|
||||
},
|
||||
pinia,
|
||||
});
|
||||
|
||||
const toolSelect = await findByRole('combobox');
|
||||
await userEvent.click(toolSelect);
|
||||
|
||||
const toolOption = await findByText('Test Tool');
|
||||
await userEvent.click(toolOption);
|
||||
await nextTick();
|
||||
const inputs = getByTestId('from-ai-parameters-modal-inputs');
|
||||
const inputByName = inputs.querySelector('input[name="query.query"]');
|
||||
expect(inputByName).toBeTruthy();
|
||||
});
|
||||
|
||||
it('uses run data when available as initial values', async () => {
|
||||
const { getByTestId } = renderModal({
|
||||
props: {
|
||||
@@ -130,14 +213,10 @@ describe('FromAiParametersModal', () => {
|
||||
|
||||
await userEvent.click(getByTestId('execute-workflow-button'));
|
||||
|
||||
expect(parameterOverridesStore.addParameterOverrides).toHaveBeenCalledWith(
|
||||
'test-workflow',
|
||||
'id1',
|
||||
{
|
||||
testBoolean: true,
|
||||
testParam: 'override',
|
||||
},
|
||||
);
|
||||
expect(agentRequestStore.addAgentRequests).toHaveBeenCalledWith('test-workflow', 'id1', {
|
||||
'query.testBoolean': true,
|
||||
'query.testParam': 'override',
|
||||
});
|
||||
});
|
||||
|
||||
it('clears parameter overrides when modal is executed', async () => {
|
||||
@@ -158,13 +237,10 @@ describe('FromAiParametersModal', () => {
|
||||
|
||||
await userEvent.click(getByTestId('execute-workflow-button'));
|
||||
|
||||
expect(parameterOverridesStore.clearParameterOverrides).toHaveBeenCalledWith(
|
||||
'test-workflow',
|
||||
'id1',
|
||||
);
|
||||
expect(agentRequestStore.clearAgentRequests).toHaveBeenCalledWith('test-workflow', 'id1');
|
||||
});
|
||||
|
||||
it('adds substitutes for parameters when executed', async () => {
|
||||
it('adds agent request with given parameters when executed', async () => {
|
||||
const { getByTestId } = renderModal({
|
||||
props: {
|
||||
modalName: FROM_AI_PARAMETERS_MODAL_KEY,
|
||||
@@ -182,17 +258,16 @@ describe('FromAiParametersModal', () => {
|
||||
|
||||
const inputs = getByTestId('from-ai-parameters-modal-inputs');
|
||||
await userEvent.click(inputs.querySelector('input[value="testBoolean"]') as Element);
|
||||
await userEvent.clear(inputs.querySelector('input[name="testParam"]') as Element);
|
||||
await userEvent.type(inputs.querySelector('input[name="testParam"]') as Element, 'given value');
|
||||
await userEvent.clear(inputs.querySelector('input[name="query.testParam"]') as Element);
|
||||
await userEvent.type(
|
||||
inputs.querySelector('input[name="query.testParam"]') as Element,
|
||||
'given value',
|
||||
);
|
||||
await userEvent.click(getByTestId('execute-workflow-button'));
|
||||
|
||||
expect(parameterOverridesStore.addParameterOverrides).toHaveBeenCalledWith(
|
||||
'test-workflow',
|
||||
'id1',
|
||||
{
|
||||
testBoolean: false,
|
||||
testParam: 'given value',
|
||||
},
|
||||
);
|
||||
expect(agentRequestStore.addAgentRequests).toHaveBeenCalledWith('test-workflow', 'id1', {
|
||||
'query.testBoolean': false,
|
||||
'query.testParam': 'given value',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import { useRunWorkflow } from '@/composables/useRunWorkflow';
|
||||
import { FROM_AI_PARAMETERS_MODAL_KEY } from '@/constants';
|
||||
import { useParameterOverridesStore } from '@/stores/parameterOverrides.store';
|
||||
import { FROM_AI_PARAMETERS_MODAL_KEY, AI_MCP_TOOL_NODE_TYPE } from '@/constants';
|
||||
import { useAgentRequestStore } from '@/stores/agentRequest.store';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { createEventBus } from '@n8n/utils/event-bus';
|
||||
import {
|
||||
type FromAIArgument,
|
||||
type IDataObject,
|
||||
NodeConnectionTypes,
|
||||
traverseNodeParametersWithParamNames,
|
||||
traverseNodeParameters,
|
||||
} from 'n8n-workflow';
|
||||
import { computed, ref } from 'vue';
|
||||
import type { IFormInput } from '@n8n/design-system';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { type IFormInput } from '@n8n/design-system';
|
||||
import { useTelemetry } from '@/composables/useTelemetry';
|
||||
import { useNDVStore } from '@/stores/ndv.store';
|
||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||
import { type JSONSchema7 } from 'json-schema';
|
||||
|
||||
type Value = string | number | boolean | null | undefined;
|
||||
|
||||
@@ -31,9 +34,10 @@ const telemetry = useTelemetry();
|
||||
const ndvStore = useNDVStore();
|
||||
const modalBus = createEventBus();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
const router = useRouter();
|
||||
const { runWorkflow } = useRunWorkflow({ router });
|
||||
const parameterOverridesStore = useParameterOverridesStore();
|
||||
const agentRequestStore = useAgentRequestStore();
|
||||
|
||||
const node = computed(() =>
|
||||
props.data.nodeName ? workflowsStore.getNodeByName(props.data.nodeName) : undefined,
|
||||
@@ -47,6 +51,9 @@ const parentNode = computed(() => {
|
||||
return workflowsStore.getNodeByName(parentNodes[0])?.name;
|
||||
});
|
||||
|
||||
const parameters = ref<IFormInput[]>([]);
|
||||
const selectedTool = ref<string>('');
|
||||
|
||||
const nodeRunData = computed(() => {
|
||||
if (!node.value) return undefined;
|
||||
|
||||
@@ -80,38 +87,125 @@ const mapTypes: {
|
||||
},
|
||||
};
|
||||
|
||||
const parameters = computed(() => {
|
||||
if (!node.value) return [];
|
||||
watch(
|
||||
[node, selectedTool],
|
||||
async ([newNode, newSelectedTool]) => {
|
||||
if (!newNode) {
|
||||
parameters.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const result: IFormInput[] = [];
|
||||
const params = node.value.parameters;
|
||||
const collectedArgs: Map<string, FromAIArgument> = new Map();
|
||||
traverseNodeParametersWithParamNames(params, collectedArgs);
|
||||
const inputOverrides =
|
||||
nodeRunData.value?.inputOverride?.[NodeConnectionTypes.AiTool]?.[0]?.[0].json;
|
||||
const result: IFormInput[] = [];
|
||||
|
||||
collectedArgs.forEach((value: FromAIArgument, paramName: string) => {
|
||||
const type = value.type ?? 'string';
|
||||
const initialValue = inputOverrides?.[value.key]
|
||||
? inputOverrides[value.key]
|
||||
: (parameterOverridesStore.getParameterOverride(
|
||||
workflowsStore.workflowId,
|
||||
node.value!.id,
|
||||
paramName,
|
||||
) ?? mapTypes[type]?.defaultValue);
|
||||
// Handle MCPClientTool nodes differently
|
||||
if (newNode.type === AI_MCP_TOOL_NODE_TYPE) {
|
||||
const tools = await nodeTypesStore.getNodeParameterOptions({
|
||||
nodeTypeAndVersion: {
|
||||
name: newNode.type,
|
||||
version: newNode.typeVersion,
|
||||
},
|
||||
path: 'parmeters.includedTools',
|
||||
methodName: 'getTools',
|
||||
currentNodeParameters: newNode.parameters,
|
||||
});
|
||||
|
||||
result.push({
|
||||
name: paramName,
|
||||
initialValue: initialValue as string | number | boolean | null | undefined,
|
||||
properties: {
|
||||
label: value.key,
|
||||
type: mapTypes[type].inputType,
|
||||
required: true,
|
||||
},
|
||||
// Load available tools
|
||||
const toolOptions = tools?.map((tool) => ({
|
||||
label: tool.name,
|
||||
value: String(tool.value),
|
||||
disabled: false,
|
||||
}));
|
||||
|
||||
result.push({
|
||||
name: 'toolName',
|
||||
initialValue: '',
|
||||
properties: {
|
||||
label: 'Tool name',
|
||||
type: 'select',
|
||||
options: toolOptions,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Only show parameters for selected tool
|
||||
if (newSelectedTool) {
|
||||
const selectedToolData = tools?.find((tool) => String(tool.value) === newSelectedTool);
|
||||
const schema = selectedToolData?.inputSchema as JSONSchema7;
|
||||
if (schema.properties) {
|
||||
for (const [propertyName, value] of Object.entries(schema.properties)) {
|
||||
const typedValue = value as {
|
||||
type: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
result.push({
|
||||
name: 'query.' + propertyName,
|
||||
initialValue: '',
|
||||
properties: {
|
||||
label: propertyName,
|
||||
type: mapTypes[typedValue.type ?? 'text'].inputType,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
parameters.value = result;
|
||||
}
|
||||
|
||||
// Handle regular tool nodes
|
||||
const params = newNode.parameters;
|
||||
const collectedArgs: FromAIArgument[] = [];
|
||||
traverseNodeParameters(params, collectedArgs);
|
||||
const inputOverrides =
|
||||
nodeRunData.value?.inputOverride?.[NodeConnectionTypes.AiTool]?.[0]?.[0].json;
|
||||
|
||||
collectedArgs.forEach((value: FromAIArgument) => {
|
||||
const type = value.type ?? 'string';
|
||||
const inputQuery = inputOverrides?.query as IDataObject;
|
||||
const initialValue = inputQuery?.[value.key]
|
||||
? inputQuery[value.key]
|
||||
: (agentRequestStore.getAgentRequest(
|
||||
workflowsStore.workflowId,
|
||||
newNode.id,
|
||||
'query.' + value.key,
|
||||
) ?? mapTypes[type]?.defaultValue);
|
||||
|
||||
result.push({
|
||||
name: 'query.' + value.key,
|
||||
initialValue: initialValue as string | number | boolean | null | undefined,
|
||||
properties: {
|
||||
label: value.key,
|
||||
type: mapTypes[value.type ?? 'string'].inputType,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
return result;
|
||||
});
|
||||
if (result.length === 0) {
|
||||
let inputQuery = inputOverrides?.query;
|
||||
if (typeof inputQuery === 'object') {
|
||||
inputQuery = JSON.stringify(inputQuery);
|
||||
}
|
||||
const queryValue =
|
||||
inputQuery ??
|
||||
agentRequestStore.getAgentRequest(workflowsStore.workflowId, newNode.id, 'query') ??
|
||||
'';
|
||||
|
||||
result.push({
|
||||
name: 'query',
|
||||
initialValue: (queryValue as string) ?? '',
|
||||
properties: {
|
||||
label: 'Query',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
parameters.value = result;
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
const onClose = () => {
|
||||
modalBus.emit('close');
|
||||
@@ -119,14 +213,10 @@ const onClose = () => {
|
||||
|
||||
const onExecute = async () => {
|
||||
if (!node.value) return;
|
||||
const inputValues = inputs.value!.getValues();
|
||||
const inputValues = inputs.value?.getValues() ?? {};
|
||||
|
||||
parameterOverridesStore.clearParameterOverrides(workflowsStore.workflowId, node.value.id);
|
||||
parameterOverridesStore.addParameterOverrides(
|
||||
workflowsStore.workflowId,
|
||||
node.value.id,
|
||||
inputValues,
|
||||
);
|
||||
agentRequestStore.clearAgentRequests(workflowsStore.workflowId, node.value.id);
|
||||
agentRequestStore.addAgentRequests(workflowsStore.workflowId, node.value.id, inputValues);
|
||||
|
||||
const telemetryPayload = {
|
||||
node_type: node.value.type,
|
||||
@@ -139,11 +229,16 @@ const onExecute = async () => {
|
||||
|
||||
await runWorkflow({
|
||||
destinationNode: node.value.name,
|
||||
source: 'RunData.TestExecuteModal',
|
||||
});
|
||||
|
||||
onClose();
|
||||
};
|
||||
|
||||
// Add handler for tool selection change
|
||||
const onUpdate = (change: { name: string; value: string }) => {
|
||||
if (change.name !== 'toolName') return;
|
||||
selectedTool.value = change.value;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -177,6 +272,7 @@ const onExecute = async () => {
|
||||
:column-view="true"
|
||||
data-test-id="from-ai-parameters-modal-inputs"
|
||||
@submit="onExecute"
|
||||
@update="onUpdate"
|
||||
></N8nFormInputs>
|
||||
</el-row>
|
||||
</el-col>
|
||||
|
||||
@@ -28,7 +28,7 @@ import { useI18n } from '@/composables/useI18n';
|
||||
import { useTelemetry } from '@/composables/useTelemetry';
|
||||
import { type IUpdateInformation } from '@/Interface';
|
||||
import { generateCodeForAiTransform } from '@/components/ButtonParameter/utils';
|
||||
import { hasFromAiExpressions } from '@/utils/nodes/nodeTransforms';
|
||||
import { needsAgentInput } from '@/utils/nodes/nodeTransforms';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
|
||||
const NODE_TEST_STEP_POPUP_COUNT_KEY = 'N8N_NODE_TEST_STEP_POPUP_COUNT';
|
||||
@@ -345,7 +345,7 @@ async function onClick() {
|
||||
}
|
||||
|
||||
if (!pinnedData.hasData.value || shouldUnpinAndExecute) {
|
||||
if (node.value && hasFromAiExpressions(node.value)) {
|
||||
if (node.value && needsAgentInput(node.value)) {
|
||||
uiStore.openModalWithData({
|
||||
name: FROM_AI_PARAMETERS_MODAL_KEY,
|
||||
data: {
|
||||
|
||||
@@ -130,9 +130,7 @@ const node = computed(() => ndvStore.activeNode);
|
||||
|
||||
const isTriggerNode = computed(() => !!node.value && nodeTypesStore.isTriggerNode(node.value.type));
|
||||
|
||||
const isNodesAsToolNode = computed(
|
||||
() => !!node.value && nodeTypesStore.isNodesAsToolNode(node.value.type),
|
||||
);
|
||||
const isToolNode = computed(() => !!node.value && nodeTypesStore.isToolNode(node.value.type));
|
||||
|
||||
const isExecutable = computed(() => {
|
||||
if (props.nodeType && node.value) {
|
||||
@@ -146,7 +144,7 @@ const isExecutable = computed(() => {
|
||||
|
||||
if (
|
||||
!inputNames.includes(NodeConnectionTypes.Main) &&
|
||||
!isNodesAsToolNode.value &&
|
||||
!isToolNode.value &&
|
||||
!isTriggerNode.value
|
||||
) {
|
||||
return false;
|
||||
|
||||
@@ -29,9 +29,7 @@ const workflowsStore = useWorkflowsStore();
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
|
||||
const node = computed(() => !!name.value && workflowsStore.getNodeByName(name.value));
|
||||
const isNodesAsToolNode = computed(
|
||||
() => !!node.value && nodeTypesStore.isNodesAsToolNode(node.value.type),
|
||||
);
|
||||
const isToolNode = computed(() => !!node.value && nodeTypesStore.isToolNode(node.value.type));
|
||||
|
||||
const nodeDisabledTitle = computed(() => {
|
||||
return isDisabled.value ? i18n.baseText('node.enable') : i18n.baseText('node.disable');
|
||||
@@ -51,7 +49,7 @@ const isExecuteNodeVisible = computed(() => {
|
||||
!props.readOnly &&
|
||||
render.value.type === CanvasNodeRenderType.Default &&
|
||||
'configuration' in render.value.options &&
|
||||
(!render.value.options.configuration || isNodesAsToolNode.value)
|
||||
(!render.value.options.configuration || isToolNode.value)
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ import { useSettingsStore } from '@/stores/settings.store';
|
||||
import { usePushConnectionStore } from '@/stores/pushConnection.store';
|
||||
import { createTestNode, createTestWorkflow } from '@/__tests__/mocks';
|
||||
import { waitFor } from '@testing-library/vue';
|
||||
import { useParameterOverridesStore } from '@/stores/parameterOverrides.store';
|
||||
import { useAgentRequestStore } from '@/stores/agentRequest.store';
|
||||
|
||||
vi.mock('@/stores/workflows.store', () => {
|
||||
const storeState: Partial<ReturnType<typeof useWorkflowsStore>> & {
|
||||
@@ -66,12 +66,12 @@ vi.mock('@/stores/workflows.store', () => {
|
||||
});
|
||||
|
||||
vi.mock('@/stores/parameterOverrides.store', () => {
|
||||
const storeState: Partial<ReturnType<typeof useParameterOverridesStore>> & {} = {
|
||||
parameterOverrides: {},
|
||||
substituteParameters: vi.fn(),
|
||||
const storeState: Partial<ReturnType<typeof useAgentRequestStore>> & {} = {
|
||||
agentRequests: {},
|
||||
generateAgentRequest: vi.fn(),
|
||||
};
|
||||
return {
|
||||
useParameterOverridesStore: vi.fn().mockReturnValue(storeState),
|
||||
useAgentRequestStore: vi.fn().mockReturnValue(storeState),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -138,7 +138,7 @@ describe('useRunWorkflow({ router })', () => {
|
||||
let router: ReturnType<typeof useRouter>;
|
||||
let workflowHelpers: ReturnType<typeof useWorkflowHelpers>;
|
||||
let settingsStore: ReturnType<typeof useSettingsStore>;
|
||||
let parameterOverridesStore: ReturnType<typeof useParameterOverridesStore>;
|
||||
let agentRequestStore: ReturnType<typeof useAgentRequestStore>;
|
||||
|
||||
beforeEach(() => {
|
||||
const pinia = createTestingPinia({ stubActions: false });
|
||||
@@ -149,7 +149,7 @@ describe('useRunWorkflow({ router })', () => {
|
||||
uiStore = useUIStore();
|
||||
workflowsStore = useWorkflowsStore();
|
||||
settingsStore = useSettingsStore();
|
||||
parameterOverridesStore = useParameterOverridesStore();
|
||||
agentRequestStore = useAgentRequestStore();
|
||||
|
||||
router = useRouter();
|
||||
workflowHelpers = useWorkflowHelpers({ router });
|
||||
@@ -512,12 +512,16 @@ describe('useRunWorkflow({ router })', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('does substituteParameters on partial execution if `partialExecutionVersion` is set to 2', async () => {
|
||||
it('sends agentRequest on partial execution if `partialExecutionVersion` is set to 2', async () => {
|
||||
// ARRANGE
|
||||
const mockExecutionResponse = { executionId: '123' };
|
||||
const mockRunData = { nodeName: [] };
|
||||
const { runWorkflow } = useRunWorkflow({ router });
|
||||
const dataCaptor = captor();
|
||||
const agentRequest = {
|
||||
query: 'query',
|
||||
toolName: 'tool',
|
||||
};
|
||||
|
||||
const workflow = mock<Workflow>({
|
||||
name: 'Test Workflow',
|
||||
@@ -559,16 +563,31 @@ describe('useRunWorkflow({ router })', () => {
|
||||
vi.mocked(workflowHelpers).getCurrentWorkflow.mockReturnValue(workflow);
|
||||
vi.mocked(workflowHelpers).getWorkflowDataToSave.mockResolvedValue(workflowData);
|
||||
vi.mocked(workflowsStore).getWorkflowRunData = mockRunData;
|
||||
|
||||
vi.mocked(agentRequestStore).generateAgentRequest.mockReturnValue(agentRequest);
|
||||
// ACT
|
||||
const result = await runWorkflow({ destinationNode: 'Test node' });
|
||||
|
||||
// ASSERT
|
||||
expect(parameterOverridesStore.substituteParameters).toHaveBeenCalledWith(
|
||||
'WorkflowId',
|
||||
'Test id',
|
||||
{ param: '0' },
|
||||
);
|
||||
expect(agentRequestStore.generateAgentRequest).toHaveBeenCalledWith('WorkflowId', 'Test id');
|
||||
expect(workflowsStore.runWorkflow).toHaveBeenCalledWith({
|
||||
agentRequest: {
|
||||
query: 'query',
|
||||
tool: {
|
||||
name: 'tool',
|
||||
},
|
||||
},
|
||||
destinationNode: 'Test node',
|
||||
dirtyNodeNames: undefined,
|
||||
runData: mockRunData,
|
||||
startNodes: [
|
||||
{
|
||||
name: 'Test node',
|
||||
sourceData: null,
|
||||
},
|
||||
],
|
||||
triggerToStartFrom: undefined,
|
||||
workflowData,
|
||||
});
|
||||
expect(result).toEqual(mockExecutionResponse);
|
||||
expect(workflowsStore.setWorkflowExecutionData).toHaveBeenCalledTimes(1);
|
||||
expect(workflowsStore.setWorkflowExecutionData).toHaveBeenCalledWith(dataCaptor);
|
||||
|
||||
@@ -42,7 +42,7 @@ import { useTelemetry } from './useTelemetry';
|
||||
import { useSettingsStore } from '@/stores/settings.store';
|
||||
import { usePushConnectionStore } from '@/stores/pushConnection.store';
|
||||
import { useNodeDirtiness } from '@/composables/useNodeDirtiness';
|
||||
import { useParameterOverridesStore } from '@/stores/parameterOverrides.store';
|
||||
import { useAgentRequestStore } from '@/stores/agentRequest.store';
|
||||
|
||||
export function useRunWorkflow(useRunWorkflowOpts: { router: ReturnType<typeof useRouter> }) {
|
||||
const nodeHelpers = useNodeHelpers();
|
||||
@@ -52,7 +52,7 @@ export function useRunWorkflow(useRunWorkflowOpts: { router: ReturnType<typeof u
|
||||
const telemetry = useTelemetry();
|
||||
const externalHooks = useExternalHooks();
|
||||
const settingsStore = useSettingsStore();
|
||||
const parameterOverridesStore = useParameterOverridesStore();
|
||||
const agentRequestStore = useAgentRequestStore();
|
||||
|
||||
const rootStore = useRootStore();
|
||||
const pushConnectionStore = usePushConnectionStore();
|
||||
@@ -295,14 +295,16 @@ export function useRunWorkflow(useRunWorkflowOpts: { router: ReturnType<typeof u
|
||||
if ('destinationNode' in options) {
|
||||
startRunData.destinationNode = options.destinationNode;
|
||||
const nodeId = workflowsStore.getNodeByName(options.destinationNode as string)?.id;
|
||||
if (nodeId && version === 2) {
|
||||
const node = workflowData.nodes.find((nodeData) => nodeData.id === nodeId);
|
||||
if (node?.parameters) {
|
||||
node.parameters = parameterOverridesStore.substituteParameters(
|
||||
workflow.id,
|
||||
nodeId,
|
||||
node?.parameters,
|
||||
);
|
||||
if (workflow.id && nodeId && version === 2) {
|
||||
const agentRequest = agentRequestStore.generateAgentRequest(workflow.id, nodeId);
|
||||
|
||||
if (agentRequest) {
|
||||
startRunData.agentRequest = {
|
||||
query: agentRequest.query ?? {},
|
||||
tool: {
|
||||
name: agentRequest.toolName ?? '',
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,6 +125,7 @@ export const CALENDLY_TRIGGER_NODE_TYPE = 'n8n-nodes-base.calendlyTrigger';
|
||||
export const CODE_NODE_TYPE = 'n8n-nodes-base.code';
|
||||
export const AI_CODE_NODE_TYPE = '@n8n/n8n-nodes-langchain.code';
|
||||
export const AI_MCP_TOOL_NODE_TYPE = '@n8n/n8n-nodes-langchain.mcpClientTool';
|
||||
export const WIKIPEDIA_TOOL_NODE_TYPE = '@n8n/n8n-nodes-langchain.toolWikipedia';
|
||||
export const CRON_NODE_TYPE = 'n8n-nodes-base.cron';
|
||||
export const CLEARBIT_NODE_TYPE = 'n8n-nodes-base.clearbit';
|
||||
export const FILTER_NODE_TYPE = 'n8n-nodes-base.filter';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { setActivePinia, createPinia } from 'pinia';
|
||||
import { useParameterOverridesStore } from './parameterOverrides.store';
|
||||
import { useAgentRequestStore } from './agentRequest.store';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { nextTick } from 'vue';
|
||||
|
||||
@@ -26,8 +26,8 @@ describe('parameterOverrides.store', () => {
|
||||
describe('Initialization', () => {
|
||||
it('initializes with empty state when localStorage is empty', () => {
|
||||
localStorageMock.getItem.mockReturnValue(null);
|
||||
const store = useParameterOverridesStore();
|
||||
expect(store.parameterOverrides).toEqual({});
|
||||
const store = useAgentRequestStore();
|
||||
expect(store.agentRequests).toEqual({});
|
||||
});
|
||||
|
||||
it('initializes with data from localStorage', () => {
|
||||
@@ -37,16 +37,16 @@ describe('parameterOverrides.store', () => {
|
||||
},
|
||||
};
|
||||
localStorageMock.getItem.mockReturnValue(JSON.stringify(mockData));
|
||||
const store = useParameterOverridesStore();
|
||||
expect(store.parameterOverrides).toEqual(mockData);
|
||||
const store = useAgentRequestStore();
|
||||
expect(store.agentRequests).toEqual(mockData);
|
||||
});
|
||||
|
||||
it('handles localStorage errors gracefully', () => {
|
||||
localStorageMock.getItem.mockImplementation(() => {
|
||||
throw new Error('Storage error');
|
||||
});
|
||||
const store = useParameterOverridesStore();
|
||||
expect(store.parameterOverrides).toEqual({});
|
||||
const store = useAgentRequestStore();
|
||||
expect(store.agentRequests).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -58,16 +58,16 @@ describe('parameterOverrides.store', () => {
|
||||
},
|
||||
};
|
||||
localStorageMock.getItem.mockReturnValue(JSON.stringify(mockData));
|
||||
const store = useParameterOverridesStore();
|
||||
const store = useAgentRequestStore();
|
||||
|
||||
const overrides = store.getParameterOverrides('workflow-1', 'node-1');
|
||||
const overrides = store.getAgentRequests('workflow-1', 'node-1');
|
||||
expect(overrides).toEqual({ param1: 'value1', param2: 'value2' });
|
||||
});
|
||||
|
||||
it('returns empty object for non-existent workflow/node', () => {
|
||||
const store = useParameterOverridesStore();
|
||||
const store = useAgentRequestStore();
|
||||
|
||||
const overrides = store.getParameterOverrides('non-existent', 'node-1');
|
||||
const overrides = store.getAgentRequests('non-existent', 'node-1');
|
||||
expect(overrides).toEqual({});
|
||||
});
|
||||
|
||||
@@ -78,9 +78,9 @@ describe('parameterOverrides.store', () => {
|
||||
},
|
||||
};
|
||||
localStorageMock.getItem.mockReturnValue(JSON.stringify(mockData));
|
||||
const store = useParameterOverridesStore();
|
||||
const store = useAgentRequestStore();
|
||||
|
||||
const override = store.getParameterOverride('workflow-1', 'node-1', 'param1');
|
||||
const override = store.getAgentRequest('workflow-1', 'node-1', 'param1');
|
||||
expect(override).toBe('value1');
|
||||
});
|
||||
|
||||
@@ -91,31 +91,31 @@ describe('parameterOverrides.store', () => {
|
||||
},
|
||||
};
|
||||
localStorageMock.getItem.mockReturnValue(JSON.stringify(mockData));
|
||||
const store = useParameterOverridesStore();
|
||||
const store = useAgentRequestStore();
|
||||
|
||||
const override = store.getParameterOverride('workflow-1', 'node-1', 'non-existent');
|
||||
const override = store.getAgentRequest('workflow-1', 'node-1', 'non-existent');
|
||||
expect(override).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Actions', () => {
|
||||
it('adds a parameter override', () => {
|
||||
const store = useParameterOverridesStore();
|
||||
const store = useAgentRequestStore();
|
||||
|
||||
store.addParameterOverride('workflow-1', 'node-1', 'param1', 'value1');
|
||||
store.addAgentRequest('workflow-1', 'node-1', 'param1', 'value1');
|
||||
|
||||
expect(store.parameterOverrides['workflow-1']['node-1']['param1']).toBe('value1');
|
||||
expect(store.agentRequests['workflow-1']['node-1']['param1']).toBe('value1');
|
||||
});
|
||||
|
||||
it('adds multiple parameter overrides', () => {
|
||||
const store = useParameterOverridesStore();
|
||||
const store = useAgentRequestStore();
|
||||
|
||||
store.addParameterOverrides('workflow-1', 'node-1', {
|
||||
store.addAgentRequests('workflow-1', 'node-1', {
|
||||
param1: 'value1',
|
||||
param2: 'value2',
|
||||
});
|
||||
|
||||
expect(store.parameterOverrides['workflow-1']['node-1']).toEqual({
|
||||
expect(store.agentRequests['workflow-1']['node-1']).toEqual({
|
||||
param1: 'value1',
|
||||
param2: 'value2',
|
||||
});
|
||||
@@ -129,12 +129,12 @@ describe('parameterOverrides.store', () => {
|
||||
},
|
||||
};
|
||||
localStorageMock.getItem.mockReturnValue(JSON.stringify(mockData));
|
||||
const store = useParameterOverridesStore();
|
||||
const store = useAgentRequestStore();
|
||||
|
||||
store.clearParameterOverrides('workflow-1', 'node-1');
|
||||
store.clearAgentRequests('workflow-1', 'node-1');
|
||||
|
||||
expect(store.parameterOverrides['workflow-1']['node-1']).toEqual({});
|
||||
expect(store.parameterOverrides['workflow-1']['node-2']).toEqual({ param3: 'value3' });
|
||||
expect(store.agentRequests['workflow-1']['node-1']).toEqual({});
|
||||
expect(store.agentRequests['workflow-1']['node-2']).toEqual({ param3: 'value3' });
|
||||
});
|
||||
|
||||
it('clears all parameter overrides for a workflow', () => {
|
||||
@@ -148,12 +148,12 @@ describe('parameterOverrides.store', () => {
|
||||
},
|
||||
};
|
||||
localStorageMock.getItem.mockReturnValue(JSON.stringify(mockData));
|
||||
const store = useParameterOverridesStore();
|
||||
const store = useAgentRequestStore();
|
||||
|
||||
store.clearAllParameterOverrides('workflow-1');
|
||||
store.clearAllAgentRequests('workflow-1');
|
||||
|
||||
expect(store.parameterOverrides['workflow-1']).toEqual({});
|
||||
expect(store.parameterOverrides['workflow-2']).toEqual({
|
||||
expect(store.agentRequests['workflow-1']).toEqual({});
|
||||
expect(store.agentRequests['workflow-2']).toEqual({
|
||||
'node-3': { param3: 'value3' },
|
||||
});
|
||||
});
|
||||
@@ -168,43 +168,26 @@ describe('parameterOverrides.store', () => {
|
||||
},
|
||||
};
|
||||
localStorageMock.getItem.mockReturnValue(JSON.stringify(mockData));
|
||||
const store = useParameterOverridesStore();
|
||||
const store = useAgentRequestStore();
|
||||
|
||||
store.clearAllParameterOverrides();
|
||||
store.clearAllAgentRequests();
|
||||
|
||||
expect(store.parameterOverrides).toEqual({});
|
||||
expect(store.agentRequests).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('substituteParameters', () => {
|
||||
it('substitutes parameters in a node', () => {
|
||||
const store = useParameterOverridesStore();
|
||||
describe('generateAgentRequest', () => {
|
||||
it('generateAgentRequest', () => {
|
||||
const store = useAgentRequestStore();
|
||||
|
||||
store.addParameterOverrides('workflow-1', 'id1', {
|
||||
store.addAgentRequests('workflow-1', 'id1', {
|
||||
param1: 'override1',
|
||||
'parent.child': 'override2',
|
||||
'parent.array[0].value': 'overrideArray1',
|
||||
'parent.array[1].value': 'overrideArray2',
|
||||
});
|
||||
|
||||
const nodeParameters = {
|
||||
param1: 'original1',
|
||||
parent: {
|
||||
child: 'original2',
|
||||
array: [
|
||||
{
|
||||
name: 'name',
|
||||
value: 'original1',
|
||||
},
|
||||
{
|
||||
name: 'name2',
|
||||
value: 'original2',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const result = store.substituteParameters('workflow-1', 'id1', nodeParameters);
|
||||
const result = store.generateAgentRequest('workflow-1', 'id1');
|
||||
|
||||
expect(result).toEqual({
|
||||
param1: 'override1',
|
||||
@@ -212,11 +195,9 @@ describe('parameterOverrides.store', () => {
|
||||
child: 'override2',
|
||||
array: [
|
||||
{
|
||||
name: 'name',
|
||||
value: 'overrideArray1',
|
||||
},
|
||||
{
|
||||
name: 'name2',
|
||||
value: 'overrideArray2',
|
||||
},
|
||||
],
|
||||
@@ -227,17 +208,17 @@ describe('parameterOverrides.store', () => {
|
||||
|
||||
describe('Persistence', () => {
|
||||
it('saves to localStorage when state changes', async () => {
|
||||
const store = useParameterOverridesStore();
|
||||
const store = useAgentRequestStore();
|
||||
|
||||
localStorageMock.setItem.mockReset();
|
||||
|
||||
store.addParameterOverride('workflow-1', 'node-1', 'param1', 'value1');
|
||||
store.addAgentRequest('workflow-1', 'node-1', 'param1', 'value1');
|
||||
|
||||
// Wait for the next tick to allow the watch to execute
|
||||
await nextTick();
|
||||
|
||||
expect(localStorageMock.setItem).toHaveBeenCalledWith(
|
||||
'n8n-parameter-overrides',
|
||||
'n8n-agent-requests',
|
||||
JSON.stringify({
|
||||
'workflow-1': {
|
||||
'node-1': { param1: 'value1' },
|
||||
@@ -247,7 +228,7 @@ describe('parameterOverrides.store', () => {
|
||||
});
|
||||
|
||||
it('should handle localStorage errors when saving', async () => {
|
||||
const store = useParameterOverridesStore();
|
||||
const store = useAgentRequestStore();
|
||||
|
||||
localStorageMock.setItem.mockReset();
|
||||
|
||||
@@ -255,11 +236,11 @@ describe('parameterOverrides.store', () => {
|
||||
throw new Error('Storage error');
|
||||
});
|
||||
|
||||
store.addParameterOverride('workflow-1', 'node-1', 'param1', 'value1');
|
||||
store.addAgentRequest('workflow-1', 'node-1', 'param1', 'value1');
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(store.parameterOverrides['workflow-1']['node-1'].param1).toBe('value1');
|
||||
expect(store.agentRequests['workflow-1']['node-1'].param1).toBe('value1');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -2,20 +2,20 @@ import { type INodeParameters, type NodeParameterValueType } from 'n8n-workflow'
|
||||
import { defineStore } from 'pinia';
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
interface IParameterOverridesStoreState {
|
||||
interface IAgentRequestStoreState {
|
||||
[workflowId: string]: {
|
||||
[nodeName: string]: INodeParameters;
|
||||
};
|
||||
}
|
||||
|
||||
const STORAGE_KEY = 'n8n-parameter-overrides';
|
||||
const STORAGE_KEY = 'n8n-agent-requests';
|
||||
|
||||
export const useParameterOverridesStore = defineStore('parameterOverrides', () => {
|
||||
export const useAgentRequestStore = defineStore('agentRequest', () => {
|
||||
// State
|
||||
const parameterOverrides = ref<IParameterOverridesStoreState>(loadFromLocalStorage());
|
||||
const agentRequests = ref<IAgentRequestStoreState>(loadFromLocalStorage());
|
||||
|
||||
// Load initial state from localStorage
|
||||
function loadFromLocalStorage(): IParameterOverridesStoreState {
|
||||
function loadFromLocalStorage(): IAgentRequestStoreState {
|
||||
try {
|
||||
const storedData = localStorage.getItem(STORAGE_KEY);
|
||||
return storedData ? JSON.parse(storedData) : {};
|
||||
@@ -26,12 +26,12 @@ export const useParameterOverridesStore = defineStore('parameterOverrides', () =
|
||||
|
||||
// Save state to localStorage whenever it changes
|
||||
watch(
|
||||
parameterOverrides,
|
||||
agentRequests,
|
||||
(newValue) => {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(newValue));
|
||||
} catch (error) {
|
||||
console.error('Failed to save parameter overrides to localStorage:', error);
|
||||
console.error('Failed to save agent requests to localStorage:', error);
|
||||
}
|
||||
},
|
||||
{ deep: true },
|
||||
@@ -39,30 +39,30 @@ export const useParameterOverridesStore = defineStore('parameterOverrides', () =
|
||||
|
||||
// Helper function to ensure workflow and node entries exist
|
||||
const ensureWorkflowAndNodeExist = (workflowId: string, nodeId: string): void => {
|
||||
if (!parameterOverrides.value[workflowId]) {
|
||||
parameterOverrides.value[workflowId] = {};
|
||||
if (!agentRequests.value[workflowId]) {
|
||||
agentRequests.value[workflowId] = {};
|
||||
}
|
||||
|
||||
if (!parameterOverrides.value[workflowId][nodeId]) {
|
||||
parameterOverrides.value[workflowId][nodeId] = {};
|
||||
if (!agentRequests.value[workflowId][nodeId]) {
|
||||
agentRequests.value[workflowId][nodeId] = {};
|
||||
}
|
||||
};
|
||||
|
||||
// Getters
|
||||
const getParameterOverrides = (workflowId: string, nodeId: string): INodeParameters => {
|
||||
return parameterOverrides.value[workflowId]?.[nodeId] || {};
|
||||
const getAgentRequests = (workflowId: string, nodeId: string): INodeParameters => {
|
||||
return agentRequests.value[workflowId]?.[nodeId] || {};
|
||||
};
|
||||
|
||||
const getParameterOverride = (
|
||||
const getAgentRequest = (
|
||||
workflowId: string,
|
||||
nodeId: string,
|
||||
paramName: string,
|
||||
): NodeParameterValueType | undefined => {
|
||||
return parameterOverrides.value[workflowId]?.[nodeId]?.[paramName];
|
||||
return agentRequests.value[workflowId]?.[nodeId]?.[paramName];
|
||||
};
|
||||
|
||||
// Actions
|
||||
const addParameterOverride = (
|
||||
const addAgentRequest = (
|
||||
workflowId: string,
|
||||
nodeId: string,
|
||||
paramName: string,
|
||||
@@ -70,40 +70,36 @@ export const useParameterOverridesStore = defineStore('parameterOverrides', () =
|
||||
): INodeParameters => {
|
||||
ensureWorkflowAndNodeExist(workflowId, nodeId);
|
||||
|
||||
parameterOverrides.value[workflowId][nodeId] = {
|
||||
...parameterOverrides.value[workflowId][nodeId],
|
||||
agentRequests.value[workflowId][nodeId] = {
|
||||
...agentRequests.value[workflowId][nodeId],
|
||||
[paramName]: paramValues,
|
||||
};
|
||||
|
||||
return parameterOverrides.value[workflowId][nodeId];
|
||||
return agentRequests.value[workflowId][nodeId];
|
||||
};
|
||||
|
||||
const addParameterOverrides = (
|
||||
workflowId: string,
|
||||
nodeId: string,
|
||||
params: INodeParameters,
|
||||
): void => {
|
||||
const addAgentRequests = (workflowId: string, nodeId: string, params: INodeParameters): void => {
|
||||
ensureWorkflowAndNodeExist(workflowId, nodeId);
|
||||
|
||||
parameterOverrides.value[workflowId][nodeId] = {
|
||||
...parameterOverrides.value[workflowId][nodeId],
|
||||
agentRequests.value[workflowId][nodeId] = {
|
||||
...agentRequests.value[workflowId][nodeId],
|
||||
...params,
|
||||
};
|
||||
};
|
||||
|
||||
const clearParameterOverrides = (workflowId: string, nodeId: string): void => {
|
||||
if (parameterOverrides.value[workflowId]) {
|
||||
parameterOverrides.value[workflowId][nodeId] = {};
|
||||
const clearAgentRequests = (workflowId: string, nodeId: string): void => {
|
||||
if (agentRequests.value[workflowId]) {
|
||||
agentRequests.value[workflowId][nodeId] = {};
|
||||
}
|
||||
};
|
||||
|
||||
const clearAllParameterOverrides = (workflowId?: string): void => {
|
||||
const clearAllAgentRequests = (workflowId?: string): void => {
|
||||
if (workflowId) {
|
||||
// Clear overrides for a specific workflow
|
||||
parameterOverrides.value[workflowId] = {};
|
||||
// Clear requests for a specific workflow
|
||||
agentRequests.value[workflowId] = {};
|
||||
} else {
|
||||
// Clear all overrides
|
||||
parameterOverrides.value = {};
|
||||
// Clear all requests
|
||||
agentRequests.value = {};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -120,7 +116,7 @@ export const useParameterOverridesStore = defineStore('parameterOverrides', () =
|
||||
}, []);
|
||||
}
|
||||
|
||||
function buildOverrideObject(path: string[], value: NodeParameterValueType): INodeParameters {
|
||||
function buildRequestObject(path: string[], value: NodeParameterValueType): INodeParameters {
|
||||
const result: INodeParameters = {};
|
||||
let current = result;
|
||||
|
||||
@@ -192,31 +188,23 @@ export const useParameterOverridesStore = defineStore('parameterOverrides', () =
|
||||
return result;
|
||||
}
|
||||
|
||||
const substituteParameters = (
|
||||
workflowId: string,
|
||||
nodeId: string,
|
||||
nodeParameters: INodeParameters,
|
||||
): INodeParameters => {
|
||||
if (!nodeParameters) return {};
|
||||
const generateAgentRequest = (workflowId: string, nodeId: string): INodeParameters => {
|
||||
const nodeRequests = agentRequests.value[workflowId]?.[nodeId] || {};
|
||||
|
||||
const nodeOverrides = parameterOverrides.value[workflowId]?.[nodeId] || {};
|
||||
|
||||
const overrideParams = Object.entries(nodeOverrides).reduce(
|
||||
(acc, [path, value]) => deepMerge(acc, buildOverrideObject(parsePath(path), value)),
|
||||
return Object.entries(nodeRequests).reduce(
|
||||
(acc, [path, value]) => deepMerge(acc, buildRequestObject(parsePath(path), value)),
|
||||
{} as INodeParameters,
|
||||
);
|
||||
|
||||
return deepMerge(nodeParameters, overrideParams);
|
||||
};
|
||||
|
||||
return {
|
||||
parameterOverrides,
|
||||
getParameterOverrides,
|
||||
getParameterOverride,
|
||||
addParameterOverride,
|
||||
addParameterOverrides,
|
||||
clearParameterOverrides,
|
||||
clearAllParameterOverrides,
|
||||
substituteParameters,
|
||||
agentRequests,
|
||||
getAgentRequests,
|
||||
getAgentRequest,
|
||||
addAgentRequest,
|
||||
addAgentRequests,
|
||||
clearAgentRequests,
|
||||
clearAllAgentRequests,
|
||||
generateAgentRequest,
|
||||
};
|
||||
});
|
||||
@@ -135,14 +135,19 @@ export const useNodeTypesStore = defineStore(STORES.NODE_TYPES, () => {
|
||||
};
|
||||
});
|
||||
|
||||
const isNodesAsToolNode = computed(() => {
|
||||
const isToolNode = computed(() => {
|
||||
return (nodeTypeName: string) => {
|
||||
const nodeType = getNodeType.value(nodeTypeName);
|
||||
return !!(
|
||||
nodeType &&
|
||||
nodeType.outputs.includes(NodeConnectionTypes.AiTool) &&
|
||||
nodeType.usableAsTool
|
||||
);
|
||||
if (nodeType?.outputs && Array.isArray(nodeType.outputs)) {
|
||||
const outputTypes = nodeType.outputs.map(
|
||||
(output: NodeConnectionType | INodeOutputConfiguration) =>
|
||||
typeof output === 'string' ? output : output.type,
|
||||
);
|
||||
|
||||
return outputTypes.includes(NodeConnectionTypes.AiTool);
|
||||
} else {
|
||||
return nodeType?.outputs.includes(NodeConnectionTypes.AiTool);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
@@ -388,7 +393,7 @@ export const useNodeTypesStore = defineStore(STORES.NODE_TYPES, () => {
|
||||
getCredentialOnlyNodeType,
|
||||
isConfigNode,
|
||||
isTriggerNode,
|
||||
isNodesAsToolNode,
|
||||
isToolNode,
|
||||
isCoreNodeType,
|
||||
visibleNodeTypes,
|
||||
nativelyNumberSuffixedDefaults,
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
import {
|
||||
AI_CODE_TOOL_LANGCHAIN_NODE_TYPE,
|
||||
AI_MCP_TOOL_NODE_TYPE,
|
||||
WIKIPEDIA_TOOL_NODE_TYPE,
|
||||
} from '@/constants';
|
||||
import type { INodeUi } from '@/Interface';
|
||||
import type { NodeTypeProvider } from '@/utils/nodeTypes/nodeTypeTransforms';
|
||||
import type { INodeCredentialDescription, FromAIArgument } from 'n8n-workflow';
|
||||
@@ -79,10 +84,19 @@ export function doesNodeHaveAllCredentialsFilled(
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given node has any fromAi expressions in its parameters.
|
||||
* Checks if the given node needs agentInput
|
||||
*/
|
||||
export function hasFromAiExpressions(node: Pick<INodeUi, 'parameters'>) {
|
||||
export function needsAgentInput(node: Pick<INodeUi, 'parameters' | 'type'>) {
|
||||
const nodeTypesNeedModal = [
|
||||
WIKIPEDIA_TOOL_NODE_TYPE,
|
||||
AI_MCP_TOOL_NODE_TYPE,
|
||||
AI_CODE_TOOL_LANGCHAIN_NODE_TYPE,
|
||||
];
|
||||
const collectedArgs: FromAIArgument[] = [];
|
||||
traverseNodeParameters(node.parameters, collectedArgs);
|
||||
return collectedArgs.length > 0;
|
||||
return (
|
||||
collectedArgs.length > 0 ||
|
||||
nodeTypesNeedModal.includes(node.type) ||
|
||||
(node.type.includes('vectorStore') && node.parameters?.mode === 'retrieve-as-tool')
|
||||
);
|
||||
}
|
||||
|
||||
@@ -118,8 +118,8 @@ import { LOGS_PANEL_STATE } from '@/components/CanvasChat/types/logs';
|
||||
import { useWorkflowSaving } from '@/composables/useWorkflowSaving';
|
||||
import { useBuilderStore } from '@/stores/builder.store';
|
||||
import { useFoldersStore } from '@/stores/folders.store';
|
||||
import { useParameterOverridesStore } from '@/stores/parameterOverrides.store';
|
||||
import { hasFromAiExpressions } from '@/utils/nodes/nodeTransforms';
|
||||
import { useAgentRequestStore } from '@/stores/agentRequest.store';
|
||||
import { needsAgentInput } from '@/utils/nodes/nodeTransforms';
|
||||
|
||||
defineOptions({
|
||||
name: 'NodeView',
|
||||
@@ -172,7 +172,7 @@ const ndvStore = useNDVStore();
|
||||
const templatesStore = useTemplatesStore();
|
||||
const builderStore = useBuilderStore();
|
||||
const foldersStore = useFoldersStore();
|
||||
const parameterOverridesStore = useParameterOverridesStore();
|
||||
const agentRequestStore = useAgentRequestStore();
|
||||
|
||||
const canvasEventBus = createEventBus<CanvasEventBusEvents>();
|
||||
|
||||
@@ -1166,7 +1166,7 @@ async function onRunWorkflowToNode(id: string) {
|
||||
const node = workflowsStore.getNodeById(id);
|
||||
if (!node) return;
|
||||
|
||||
if (hasFromAiExpressions(node) && nodeTypesStore.isNodesAsToolNode(node.type)) {
|
||||
if (needsAgentInput(node) && nodeTypesStore.isToolNode(node.type)) {
|
||||
uiStore.openModalWithData({
|
||||
name: FROM_AI_PARAMETERS_MODAL_KEY,
|
||||
data: {
|
||||
@@ -1175,7 +1175,7 @@ async function onRunWorkflowToNode(id: string) {
|
||||
});
|
||||
} else {
|
||||
trackRunWorkflowToNode(node);
|
||||
parameterOverridesStore.clearParameterOverrides(workflowsStore.workflowId, node.id);
|
||||
agentRequestStore.clearAgentRequests(workflowsStore.workflowId, node.id);
|
||||
|
||||
void runWorkflow({ destinationNode: node.name, source: 'Node.executeNode' });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user