fix(editor): Fix rendering of AI logs (#11450)

This commit is contained in:
oleg 2024-10-31 14:20:04 +01:00 committed by GitHub
parent 5d19e8f2b4
commit 73b0a80ac9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 97 additions and 83 deletions

View file

@ -90,6 +90,14 @@ function createRunDataWithError(inputMessage: string) {
routine: 'InitPostgres', routine: 'InitPostgres',
} as unknown as Error, } as unknown as Error,
} as ExecutionError, } as ExecutionError,
metadata: {
subRun: [
{
node: 'Postgres Chat Memory',
runIndex: 0,
},
],
},
}), }),
createMockNodeExecutionData(AGENT_NODE_NAME, { createMockNodeExecutionData(AGENT_NODE_NAME, {
executionStatus: 'error', executionStatus: 'error',
@ -124,14 +132,6 @@ function createRunDataWithError(inputMessage: string) {
description: 'Internal error', description: 'Internal error',
message: 'Internal error', message: 'Internal error',
} as unknown as ExecutionError, } 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: { inputOverride: {
ai_languageModel: [ ai_languageModel: [
[ [
@ -316,9 +319,6 @@ describe('Langchain Integration', () => {
jsonData: { jsonData: {
main: { output: 'Hi there! How can I assist you today?' }, 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, lastNodeExecuted: AGENT_NODE_NAME,

View file

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

View file

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

View file

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

View file

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