mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-18 10:31:15 +00:00
feat(core): Implement partial execution for all tool nodes (#15168)
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user