Merge branch 'master' of https://github.com/n8n-io/n8n into node-1669-switch-node-adding-routing-rule-reconnects-fallback-route

This commit is contained in:
Michael Kret 2024-10-31 15:26:26 +02:00
commit 97b7a5bd16
6 changed files with 97 additions and 83 deletions

View file

@ -90,6 +90,14 @@ function createRunDataWithError(inputMessage: string) {
routine: 'InitPostgres',
} as unknown as Error,
} as ExecutionError,
metadata: {
subRun: [
{
node: 'Postgres Chat Memory',
runIndex: 0,
},
],
},
}),
createMockNodeExecutionData(AGENT_NODE_NAME, {
executionStatus: 'error',
@ -124,14 +132,6 @@ function createRunDataWithError(inputMessage: string) {
description: 'Internal error',
message: 'Internal error',
} as unknown as ExecutionError,
metadata: {
subRun: [
{
node: 'Postgres Chat Memory',
runIndex: 0,
},
],
},
}),
];
}

View file

@ -278,6 +278,9 @@ describe('Langchain Integration', () => {
},
},
},
metadata: {
subRun: [{ node: AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, runIndex: 0 }],
},
inputOverride: {
ai_languageModel: [
[
@ -316,9 +319,6 @@ describe('Langchain Integration', () => {
jsonData: {
main: { output: 'Hi there! How can I assist you today?' },
},
metadata: {
subRun: [{ node: AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, runIndex: 0 }],
},
}),
],
lastNodeExecuted: AGENT_NODE_NAME,

View file

@ -100,14 +100,15 @@ const isTriggerNode = computed(() => {
});
const hasAiMetadata = computed(() => {
if (node.value) {
const resultData = workflowsStore.getWorkflowResultDataByNodeName(node.value.name);
if (!resultData || !Array.isArray(resultData) || resultData.length === 0) {
if (isNodeRunning.value || !workflowRunData.value) {
return false;
}
return !!resultData[resultData.length - 1].metadata;
if (node.value) {
const connectedSubNodes = props.workflow.getParentNodes(node.value.name, 'ALL_NON_MAIN');
const resultData = connectedSubNodes.map(workflowsStore.getWorkflowResultDataByNodeName);
return resultData && Array.isArray(resultData) && resultData.length > 0;
}
return false;
});
@ -295,6 +296,7 @@ const activatePane = () => {
:block-u-i="blockUI"
:is-production-execution-preview="isProductionExecutionPreview"
:is-pane-active="isPaneActive"
:hide-pagination="outputMode === 'logs'"
pane-type="output"
:data-output-type="outputMode"
@activate-pane="activatePane"
@ -368,7 +370,7 @@ const activatePane = () => {
</template>
<template v-if="outputMode === 'logs' && node" #content>
<RunDataAi :node="node" :run-index="runIndex" />
<RunDataAi :node="node" :run-index="runIndex" :workflow="workflow" />
</template>
<template #recovered-artificial-output-data>

View file

@ -162,6 +162,10 @@ export default defineComponent({
type: Boolean,
default: false,
},
hidePagination: {
type: Boolean,
default: false,
},
},
setup(props) {
const ndvStore = useNDVStore();
@ -1743,6 +1747,7 @@ export default defineComponent({
</div>
<div
v-if="
hidePagination === false &&
hasNodeRun &&
!hasRunError &&
displayMode !== 'binary' &&

View file

@ -1,8 +1,7 @@
<script lang="ts" setup>
import type { Ref } from 'vue';
import { computed, ref, watch } from 'vue';
import type { ITaskSubRunMetadata, ITaskDataConnections } from 'n8n-workflow';
import { NodeConnectionType } from 'n8n-workflow';
import type { ITaskDataConnections, NodeConnectionType, Workflow, ITaskData } from 'n8n-workflow';
import type { IAiData, IAiDataContent, INodeUi } from '@/Interface';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { useWorkflowsStore } from '@/stores/workflows.store';
@ -28,29 +27,21 @@ export interface Props {
runIndex?: number;
hideTitle?: boolean;
slim?: boolean;
workflow: Workflow;
}
const props = withDefaults(defineProps<Props>(), { runIndex: 0 });
const workflowsStore = useWorkflowsStore();
const nodeTypesStore = useNodeTypesStore();
const selectedRun: Ref<IAiData[]> = ref([]);
function isTreeNodeSelected(node: TreeNode) {
return selectedRun.value.some((run) => run.node === node.node && run.runIndex === node.runIndex);
}
function getReferencedData(
reference: ITaskSubRunMetadata,
taskData: ITaskData,
withInput: boolean,
withOutput: boolean,
): IAiDataContent[] {
const resultData = workflowsStore.getWorkflowResultDataByNodeName(reference.node);
if (!resultData?.[reference.runIndex]) {
return [];
}
const taskData = resultData[reference.runIndex];
if (!taskData) {
return [];
}
@ -98,18 +89,18 @@ function onItemClick(data: TreeNode) {
return;
}
const selectedNodeRun = workflowsStore.getWorkflowResultDataByNodeName(data.node)?.[
data.runIndex
];
if (!selectedNodeRun) {
return;
}
selectedRun.value = [
{
node: data.node,
runIndex: data.runIndex,
data: getReferencedData(
{
node: data.node,
runIndex: data.runIndex,
},
true,
true,
),
data: getReferencedData(selectedNodeRun, true, true),
},
];
}
@ -145,21 +136,20 @@ const createNode = (
});
function getTreeNodeData(nodeName: string, currentDepth: number): TreeNode[] {
const { connectionsByDestinationNode } = workflowsStore.getCurrentWorkflow();
const connections = connectionsByDestinationNode[nodeName];
// eslint-disable-next-line @typescript-eslint/no-use-before-define
const connections = props.workflow.connectionsByDestinationNode[nodeName];
const resultData = aiData.value?.filter((data) => data.node === nodeName) ?? [];
if (!connections) {
return resultData.map((d) => createNode(nodeName, currentDepth, d));
}
const nonMainConnectionsKeys = Object.keys(connections).filter(
(key) => key !== NodeConnectionType.Main,
);
const children = nonMainConnectionsKeys.flatMap((key) =>
connections[key][0].flatMap((node) => getTreeNodeData(node.node, currentDepth + 1)),
);
// Get the first level of children
const connectedSubNodes = props.workflow.getParentNodes(nodeName, 'ALL_NON_MAIN', 1);
const children = connectedSubNodes
// Only include sub-nodes which have data
.filter((name) => aiData.value?.find((data) => data.node === name))
.flatMap((name) => getTreeNodeData(name, currentDepth + 1));
children.sort((a, b) => a.startTime - b.startTime);
@ -170,35 +160,49 @@ function getTreeNodeData(nodeName: string, currentDepth: number): TreeNode[] {
return [createNode(nodeName, currentDepth, undefined, children)];
}
const aiData = computed<AIResult[] | undefined>(() => {
const resultData = workflowsStore.getWorkflowResultDataByNodeName(props.node.name);
const aiData = computed<AIResult[]>(() => {
const result: AIResult[] = [];
const connectedSubNodes = props.workflow.getParentNodes(props.node.name, 'ALL_NON_MAIN');
const rootNodeResult = workflowsStore.getWorkflowResultDataByNodeName(props.node.name);
const rootNodeStartTime = rootNodeResult?.[0]?.startTime ?? 0;
const rootNodeEndTime = rootNodeStartTime + (rootNodeResult?.[0]?.executionTime ?? 0);
if (!resultData || !Array.isArray(resultData)) {
return;
}
connectedSubNodes.forEach((nodeName) => {
const nodeRunData = workflowsStore.getWorkflowResultDataByNodeName(nodeName) ?? [];
const subRun = resultData[props.runIndex].metadata?.subRun;
if (!Array.isArray(subRun)) {
return;
}
// Extend the subRun with the data and sort by adding execution time + startTime and comparing them
const subRunWithData = subRun.flatMap((run) =>
getReferencedData(run, false, true).map((data) => ({ ...run, data })),
);
nodeRunData.forEach((run, runIndex) => {
const referenceData = {
data: getReferencedData(run, false, true)[0],
node: nodeName,
runIndex,
};
subRunWithData.sort((a, b) => {
const aTime = a.data?.metadata?.startTime || 0;
const bTime = b.data?.metadata?.startTime || 0;
result.push(referenceData);
});
});
// Sort the data by start time
result.sort((a, b) => {
const aTime = a.data?.metadata?.startTime ?? 0;
const bTime = b.data?.metadata?.startTime ?? 0;
return aTime - bTime;
});
return subRunWithData;
// Only show data that is within the root node's execution time
// This is because sub-node could be connected to multiple root nodes
const currentNodeResult = result.filter((r) => {
const startTime = r.data?.metadata?.startTime ?? 0;
return startTime >= rootNodeStartTime && startTime <= rootNodeEndTime;
});
return currentNodeResult;
});
const executionTree = computed<TreeNode[]>(() => {
const rootNode = props.node;
const tree = getTreeNodeData(rootNode.name, 0);
const tree = getTreeNodeData(rootNode.name, 1);
return tree || [];
});
@ -206,7 +210,7 @@ watch(() => props.runIndex, selectFirst, { immediate: true });
</script>
<template>
<div v-if="aiData" :class="$style.container">
<div v-if="aiData.length > 0" :class="$style.container">
<div :class="{ [$style.tree]: true, [$style.slim]: slim }">
<ElTree
:data="executionTree"

View file

@ -22,7 +22,6 @@ import { useUsersStore } from '@/stores/users.store';
import MessagesList from '@n8n/chat/components/MessagesList.vue';
import type { ArrowKeyDownPayload } from '@n8n/chat/components/Input.vue';
import ChatInput from '@n8n/chat/components/Input.vue';
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
import { useRouter } from 'vue-router';
import { useRunWorkflow } from '@/composables/useRunWorkflow';
import type { Chat, ChatMessage, ChatMessageText, ChatOptions } from '@n8n/chat/types';
@ -78,7 +77,6 @@ interface MemoryOutput {
}
const router = useRouter();
const workflowHelpers = useWorkflowHelpers({ router });
const { runWorkflow } = useRunWorkflow({ router });
const workflowsStore = useWorkflowsStore();
const nodeTypesStore = useNodeTypesStore();
@ -145,9 +143,9 @@ const messageVars = {
'--chat--color-typing': 'var(--color-text-dark)',
};
const workflow = computed(() => workflowsStore.getCurrentWorkflow());
function getTriggerNode() {
const workflow = workflowHelpers.getCurrentWorkflow();
const triggerNode = workflow.queryNodes((nodeType: INodeType) =>
const triggerNode = workflow.value.queryNodes((nodeType: INodeType) =>
[CHAT_TRIGGER_NODE_TYPE, MANUAL_CHAT_TRIGGER_NODE_TYPE].includes(nodeType.description.name),
);
@ -164,19 +162,19 @@ function setNode() {
return;
}
const workflow = workflowHelpers.getCurrentWorkflow();
const childNodes = workflow.getChildNodes(triggerNode.name);
const childNodes = workflow.value.getChildNodes(triggerNode.name);
for (const childNode of childNodes) {
// Look for the first connected node with metadata
// TODO: Allow later users to change that in the UI
const resultData = workflowsStore.getWorkflowResultDataByNodeName(childNode);
const connectedSubNodes = workflow.value.getParentNodes(childNode, 'ALL_NON_MAIN');
const resultData = connectedSubNodes.map(workflowsStore.getWorkflowResultDataByNodeName);
if (!resultData && !Array.isArray(resultData)) {
continue;
}
if (resultData[resultData.length - 1].metadata) {
if (resultData.some((data) => data?.[0].metadata)) {
node.value = workflowsStore.getNodeByName(childNode);
break;
}
@ -190,7 +188,6 @@ function setConnectedNode() {
showError(new Error('Chat Trigger Node could not be found!'), 'Trigger Node not found');
return;
}
const workflow = workflowHelpers.getCurrentWorkflow();
const chatNode = workflowsStore.getNodes().find((storeNode: INodeUi): boolean => {
if (storeNode.type === CHAIN_SUMMARIZATION_LANGCHAIN_NODE_TYPE) return false;
@ -202,10 +199,10 @@ function setConnectedNode() {
let isCustomChainOrAgent = false;
if (nodeType.name === AI_CODE_NODE_TYPE) {
const inputs = NodeHelpers.getNodeInputs(workflow, storeNode, nodeType);
const inputs = NodeHelpers.getNodeInputs(workflow.value, storeNode, nodeType);
const inputTypes = NodeHelpers.getConnectionTypes(inputs);
const outputs = NodeHelpers.getNodeOutputs(workflow, storeNode, nodeType);
const outputs = NodeHelpers.getNodeOutputs(workflow.value, storeNode, nodeType);
const outputTypes = NodeHelpers.getConnectionTypes(outputs);
if (
@ -219,7 +216,7 @@ function setConnectedNode() {
if (!isAgent && !isChain && !isCustomChainOrAgent) return false;
const parentNodes = workflow.getParentNodes(storeNode.name);
const parentNodes = workflow.value.getParentNodes(storeNode.name);
const isChatChild = parentNodes.some((parentNodeName) => parentNodeName === triggerNode.name);
return Boolean(isChatChild && (isAgent || isChain || isCustomChainOrAgent));
@ -431,10 +428,9 @@ async function sendMessage(message: string, files?: File[]) {
}
function displayExecution(executionId: string) {
const workflow = workflowHelpers.getCurrentWorkflow();
const route = router.resolve({
name: VIEWS.EXECUTION_PREVIEW,
params: { name: workflow.id, executionId },
params: { name: workflow.value.id, executionId },
});
window.open(route.href, '_blank');
}
@ -452,9 +448,10 @@ function reuseMessage(message: ChatMessageText) {
function getChatMessages(): ChatMessageText[] {
if (!connectedNode.value) return [];
const workflow = workflowHelpers.getCurrentWorkflow();
const connectedMemoryInputs =
workflow.connectionsByDestinationNode[connectedNode.value.name][NodeConnectionType.AiMemory];
workflow.value.connectionsByDestinationNode[connectedNode.value.name][
NodeConnectionType.AiMemory
];
if (!connectedMemoryInputs) return [];
const memoryConnection = (connectedMemoryInputs ?? []).find((i) => i.length > 0)?.[0];
@ -573,7 +570,13 @@ onMounted(() => {
locale.baseText('chat.window.logs')
}}</n8n-text>
<div :class="$style.logs">
<LazyRunDataAi :key="messages.length" :node="node" hide-title slim />
<LazyRunDataAi
:key="messages.length"
:node="node"
hide-title
slim
:workflow="workflow"
/>
</div>
</div>
</div>