Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 5 commits into from
Oct 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions cypress/e2e/233-AI-switch-to-logs-on-error.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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,
},
],
},
}),
];
}
Expand Down
6 changes: 3 additions & 3 deletions cypress/e2e/30-langchain.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,9 @@ describe('Langchain Integration', () => {
},
},
},
metadata: {
subRun: [{ node: AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, runIndex: 0 }],
},
inputOverride: {
ai_languageModel: [
[
Expand Down Expand Up @@ -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,
Expand Down
16 changes: 9 additions & 7 deletions packages/editor-ui/src/components/OutputPanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -100,14 +100,15 @@ const isTriggerNode = computed(() => {
});

const hasAiMetadata = computed(() => {
if (node.value) {
const resultData = workflowsStore.getWorkflowResultDataByNodeName(node.value.name);
if (isNodeRunning.value || !workflowRunData.value) {
return false;
}

if (!resultData || !Array.isArray(resultData) || resultData.length === 0) {
return false;
}
if (node.value) {
const connectedSubNodes = props.workflow.getParentNodes(node.value.name, 'ALL_NON_MAIN');
const resultData = connectedSubNodes.map(workflowsStore.getWorkflowResultDataByNodeName);

return !!resultData[resultData.length - 1].metadata;
return resultData && Array.isArray(resultData) && resultData.length > 0;
}
return false;
});
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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>
Expand Down
5 changes: 5 additions & 0 deletions packages/editor-ui/src/components/RunData.vue
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,10 @@ export default defineComponent({
type: Boolean,
default: false,
},
hidePagination: {
type: Boolean,
default: false,
},
},
setup(props) {
const ndvStore = useNDVStore();
Expand Down Expand Up @@ -1743,6 +1747,7 @@ export default defineComponent({
</div>
<div
v-if="
hidePagination === false &&
hasNodeRun &&
!hasRunError &&
displayMode !== 'binary' &&
Expand Down
100 changes: 52 additions & 48 deletions packages/editor-ui/src/components/RunDataAi/RunDataAi.vue
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 [];
}
Expand Down Expand Up @@ -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),
},
];
}
Expand Down Expand Up @@ -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);

Expand All @@ -170,43 +160,57 @@ 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 || [];
});

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"
Expand Down
Loading
Loading