mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-18 02:21:13 +00:00
fix(editor): Logs not shown when tools are partially executed (#16274)
This commit is contained in:
1
packages/@n8n/constants/src/execution.ts
Normal file
1
packages/@n8n/constants/src/execution.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export const TOOL_EXECUTOR_NODE_NAME = 'PartialExecutionToolExecutor';
|
||||||
@@ -2,6 +2,7 @@ export * from './api';
|
|||||||
export * from './browser';
|
export * from './browser';
|
||||||
export * from './community-nodes';
|
export * from './community-nodes';
|
||||||
export * from './instance';
|
export * from './instance';
|
||||||
|
export * from './execution';
|
||||||
|
|
||||||
export const LICENSE_FEATURES = {
|
export const LICENSE_FEATURES = {
|
||||||
SHARING: 'feat:sharing',
|
SHARING: 'feat:sharing',
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
|
import { TOOL_EXECUTOR_NODE_NAME } from '@n8n/constants';
|
||||||
import * as a from 'assert/strict';
|
import * as a from 'assert/strict';
|
||||||
import { type AiAgentRequest, type INode, NodeConnectionTypes } from 'n8n-workflow';
|
import { type AiAgentRequest, type INode, NodeConnectionTypes } from 'n8n-workflow';
|
||||||
|
|
||||||
import { type DirectedGraph } from './directed-graph';
|
import { type DirectedGraph } from './directed-graph';
|
||||||
|
|
||||||
export const TOOL_EXECUTOR_NODE_NAME = 'PartialExecutionToolExecutor';
|
|
||||||
|
|
||||||
export function rewireGraph(
|
export function rewireGraph(
|
||||||
tool: INode,
|
tool: INode,
|
||||||
graph: DirectedGraph,
|
graph: DirectedGraph,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */
|
/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */
|
||||||
|
import { TOOL_EXECUTOR_NODE_NAME } from '@n8n/constants';
|
||||||
import { Container } from '@n8n/di';
|
import { Container } from '@n8n/di';
|
||||||
import * as assert from 'assert/strict';
|
import * as assert from 'assert/strict';
|
||||||
import { setMaxListeners } from 'events';
|
import { setMaxListeners } from 'events';
|
||||||
@@ -74,7 +75,6 @@ import {
|
|||||||
rewireGraph,
|
rewireGraph,
|
||||||
getNextExecutionIndex,
|
getNextExecutionIndex,
|
||||||
} from './partial-execution-utils';
|
} from './partial-execution-utils';
|
||||||
import { TOOL_EXECUTOR_NODE_NAME } from './partial-execution-utils/rewire-graph';
|
|
||||||
import { RoutingNode } from './routing-node';
|
import { RoutingNode } from './routing-node';
|
||||||
import { TriggersAndPollers } from './triggers-and-pollers';
|
import { TriggersAndPollers } from './triggers-and-pollers';
|
||||||
|
|
||||||
|
|||||||
@@ -12,11 +12,12 @@ import NodeIcon from '@/components/NodeIcon.vue';
|
|||||||
import { useI18n } from '@n8n/i18n';
|
import { useI18n } from '@n8n/i18n';
|
||||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
import LogsViewNodeName from '@/features/logs/components/LogsViewNodeName.vue';
|
import LogsViewNodeName from '@/features/logs/components/LogsViewNodeName.vue';
|
||||||
import { N8nButton, N8nResizeWrapper } from '@n8n/design-system';
|
import { N8nButton, N8nResizeWrapper, N8nText } from '@n8n/design-system';
|
||||||
import { computed, useTemplateRef } from 'vue';
|
import { computed, useTemplateRef } from 'vue';
|
||||||
import KeyboardShortcutTooltip from '@/components/KeyboardShortcutTooltip.vue';
|
import KeyboardShortcutTooltip from '@/components/KeyboardShortcutTooltip.vue';
|
||||||
import { getSubtreeTotalConsumedTokens } from '@/features/logs/logs.utils';
|
import { getSubtreeTotalConsumedTokens } from '@/features/logs/logs.utils';
|
||||||
import { LOG_DETAILS_PANEL_STATE } from '@/features/logs/logs.constants';
|
import { LOG_DETAILS_PANEL_STATE } from '@/features/logs/logs.constants';
|
||||||
|
import { isPlaceholderLog } from '@/features/logs/logs.utils';
|
||||||
|
|
||||||
const MIN_IO_PANEL_WIDTH = 200;
|
const MIN_IO_PANEL_WIDTH = 200;
|
||||||
|
|
||||||
@@ -82,7 +83,7 @@ function handleResizeEnd() {
|
|||||||
:is-deleted="latestInfo?.deleted ?? false"
|
:is-deleted="latestInfo?.deleted ?? false"
|
||||||
/>
|
/>
|
||||||
<LogsViewExecutionSummary
|
<LogsViewExecutionSummary
|
||||||
v-if="isOpen"
|
v-if="isOpen && logEntry.runData !== undefined"
|
||||||
:class="$style.executionSummary"
|
:class="$style.executionSummary"
|
||||||
:status="logEntry.runData.executionStatus ?? 'unknown'"
|
:status="logEntry.runData.executionStatus ?? 'unknown'"
|
||||||
:consumed-tokens="consumedTokens"
|
:consumed-tokens="consumedTokens"
|
||||||
@@ -92,7 +93,7 @@ function handleResizeEnd() {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<div v-if="isOpen && !isTriggerNode" :class="$style.actions">
|
<div v-if="isOpen && !isTriggerNode && !isPlaceholderLog(logEntry)" :class="$style.actions">
|
||||||
<KeyboardShortcutTooltip
|
<KeyboardShortcutTooltip
|
||||||
:label="locale.baseText('generic.shortcutHint')"
|
:label="locale.baseText('generic.shortcutHint')"
|
||||||
:shortcut="{ keys: ['i'] }"
|
:shortcut="{ keys: ['i'] }"
|
||||||
@@ -124,36 +125,43 @@ function handleResizeEnd() {
|
|||||||
</template>
|
</template>
|
||||||
</LogsPanelHeader>
|
</LogsPanelHeader>
|
||||||
<div v-if="isOpen" :class="$style.content" data-test-id="logs-details-body">
|
<div v-if="isOpen" :class="$style.content" data-test-id="logs-details-body">
|
||||||
<N8nResizeWrapper
|
<div v-if="isPlaceholderLog(logEntry)" :class="$style.placeholder">
|
||||||
v-if="!isTriggerNode && panels !== LOG_DETAILS_PANEL_STATE.OUTPUT"
|
<N8nText color="text-base">
|
||||||
:class="{
|
{{ locale.baseText('ndv.output.runNodeHint') }}
|
||||||
[$style.inputResizer]: true,
|
</N8nText>
|
||||||
[$style.collapsed]: resizer.isCollapsed.value,
|
</div>
|
||||||
[$style.full]: resizer.isFullSize.value,
|
<template v-else>
|
||||||
}"
|
<N8nResizeWrapper
|
||||||
:width="resizer.size.value"
|
v-if="!isTriggerNode && panels !== LOG_DETAILS_PANEL_STATE.OUTPUT"
|
||||||
:style="shouldResize ? { width: `${resizer.size.value ?? 0}px` } : undefined"
|
:class="{
|
||||||
:supported-directions="['right']"
|
[$style.inputResizer]: true,
|
||||||
:is-resizing-enabled="shouldResize"
|
[$style.collapsed]: resizer.isCollapsed.value,
|
||||||
:window="window"
|
[$style.full]: resizer.isFullSize.value,
|
||||||
@resize="resizer.onResize"
|
}"
|
||||||
@resizeend="handleResizeEnd"
|
:width="resizer.size.value"
|
||||||
>
|
:style="shouldResize ? { width: `${resizer.size.value ?? 0}px` } : undefined"
|
||||||
|
:supported-directions="['right']"
|
||||||
|
:is-resizing-enabled="shouldResize"
|
||||||
|
:window="window"
|
||||||
|
@resize="resizer.onResize"
|
||||||
|
@resizeend="handleResizeEnd"
|
||||||
|
>
|
||||||
|
<LogsViewRunData
|
||||||
|
data-test-id="log-details-input"
|
||||||
|
pane-type="input"
|
||||||
|
:title="locale.baseText('logs.details.header.actions.input')"
|
||||||
|
:log-entry="logEntry"
|
||||||
|
/>
|
||||||
|
</N8nResizeWrapper>
|
||||||
<LogsViewRunData
|
<LogsViewRunData
|
||||||
data-test-id="log-details-input"
|
v-if="isTriggerNode || panels !== LOG_DETAILS_PANEL_STATE.INPUT"
|
||||||
pane-type="input"
|
data-test-id="log-details-output"
|
||||||
:title="locale.baseText('logs.details.header.actions.input')"
|
pane-type="output"
|
||||||
|
:class="$style.outputPanel"
|
||||||
|
:title="locale.baseText('logs.details.header.actions.output')"
|
||||||
:log-entry="logEntry"
|
:log-entry="logEntry"
|
||||||
/>
|
/>
|
||||||
</N8nResizeWrapper>
|
</template>
|
||||||
<LogsViewRunData
|
|
||||||
v-if="isTriggerNode || panels !== LOG_DETAILS_PANEL_STATE.INPUT"
|
|
||||||
data-test-id="log-details-output"
|
|
||||||
pane-type="output"
|
|
||||||
:class="$style.outputPanel"
|
|
||||||
:title="locale.baseText('logs.details.header.actions.output')"
|
|
||||||
:log-entry="logEntry"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -218,4 +226,11 @@ function handleResizeEnd() {
|
|||||||
border-right: var(--border-base);
|
border-right: var(--border-base);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.placeholder {
|
||||||
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -37,11 +37,15 @@ const nodeTypeStore = useNodeTypesStore();
|
|||||||
const type = computed(() => nodeTypeStore.getNodeType(props.data.node.type));
|
const type = computed(() => nodeTypeStore.getNodeType(props.data.node.type));
|
||||||
const isSettled = computed(
|
const isSettled = computed(
|
||||||
() =>
|
() =>
|
||||||
props.data.runData.executionStatus &&
|
props.data.runData?.executionStatus &&
|
||||||
!['running', 'waiting'].includes(props.data.runData.executionStatus),
|
!['running', 'waiting'].includes(props.data.runData.executionStatus),
|
||||||
);
|
);
|
||||||
const isError = computed(() => !!props.data.runData.error);
|
const isError = computed(() => !!props.data.runData?.error);
|
||||||
const startedAtText = computed(() => {
|
const startedAtText = computed(() => {
|
||||||
|
if (props.data.runData === undefined) {
|
||||||
|
return '—';
|
||||||
|
}
|
||||||
|
|
||||||
const time = new Date(props.data.runData.startTime);
|
const time = new Date(props.data.runData.startTime);
|
||||||
|
|
||||||
return locale.baseText('logs.overview.body.started', {
|
return locale.baseText('logs.overview.body.started', {
|
||||||
@@ -50,14 +54,16 @@ const startedAtText = computed(() => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
const statusText = computed(() => upperFirst(props.data.runData.executionStatus));
|
const statusText = computed(() => upperFirst(props.data.runData?.executionStatus ?? ''));
|
||||||
const timeText = computed(() =>
|
const timeText = computed(() =>
|
||||||
locale.displayTimer(
|
props.data.runData
|
||||||
isSettled.value
|
? locale.displayTimer(
|
||||||
? props.data.runData.executionTime
|
isSettled.value
|
||||||
: Math.floor((now.value - props.data.runData.startTime) / 1000) * 1000,
|
? props.data.runData.executionTime
|
||||||
true,
|
: Math.floor((now.value - props.data.runData.startTime) / 1000) * 1000,
|
||||||
),
|
true,
|
||||||
|
)
|
||||||
|
: undefined,
|
||||||
);
|
);
|
||||||
|
|
||||||
const subtreeConsumedTokens = computed(() =>
|
const subtreeConsumedTokens = computed(() =>
|
||||||
@@ -65,7 +71,7 @@ const subtreeConsumedTokens = computed(() =>
|
|||||||
);
|
);
|
||||||
|
|
||||||
const hasChildren = computed(
|
const hasChildren = computed(
|
||||||
() => props.data.children.length > 0 || !!props.data.runData.metadata?.subExecution,
|
() => props.data.children.length > 0 || !!props.data.runData?.metadata?.subExecution,
|
||||||
);
|
);
|
||||||
|
|
||||||
function isLastChild(level: number) {
|
function isLastChild(level: number) {
|
||||||
@@ -144,13 +150,14 @@ watch(
|
|||||||
</template>
|
</template>
|
||||||
<template #time>{{ timeText }}</template>
|
<template #time>{{ timeText }}</template>
|
||||||
</I18nT>
|
</I18nT>
|
||||||
<template v-else>
|
<template v-else-if="timeText !== undefined">
|
||||||
{{
|
{{
|
||||||
locale.baseText('logs.overview.body.summaryText.for', {
|
locale.baseText('logs.overview.body.summaryText.for', {
|
||||||
interpolate: { status: statusText, time: timeText },
|
interpolate: { status: statusText, time: timeText },
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
</template>
|
</template>
|
||||||
|
<template v-else>—</template>
|
||||||
</N8nText>
|
</N8nText>
|
||||||
<N8nText
|
<N8nText
|
||||||
v-if="!isCompact"
|
v-if="!isCompact"
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ async function handleOpenNdv(treeNode: LogEntry) {
|
|||||||
ndvStore.setActiveNodeName(treeNode.node.name);
|
ndvStore.setActiveNodeName(treeNode.node.name);
|
||||||
|
|
||||||
await nextTick(() => {
|
await nextTick(() => {
|
||||||
const source = treeNode.runData.source[0];
|
const source = treeNode.runData?.source[0];
|
||||||
const inputBranch = source?.previousNodeOutput ?? 0;
|
const inputBranch = source?.previousNodeOutput ?? 0;
|
||||||
|
|
||||||
ndvEventBus.emit('updateInputNodeName', source?.previousNode);
|
ndvEventBus.emit('updateInputNodeName', source?.previousNode);
|
||||||
|
|||||||
@@ -19,7 +19,9 @@ const locale = useI18n();
|
|||||||
const ndvStore = useNDVStore();
|
const ndvStore = useNDVStore();
|
||||||
|
|
||||||
const displayMode = ref<IRunDataDisplayMode>(paneType === 'input' ? 'schema' : 'table');
|
const displayMode = ref<IRunDataDisplayMode>(paneType === 'input' ? 'schema' : 'table');
|
||||||
const isMultipleInput = computed(() => paneType === 'input' && logEntry.runData.source.length > 1);
|
const isMultipleInput = computed(
|
||||||
|
() => paneType === 'input' && (logEntry.runData?.source.length ?? 0) > 1,
|
||||||
|
);
|
||||||
const runDataProps = computed<
|
const runDataProps = computed<
|
||||||
Pick<InstanceType<typeof RunData>['$props'], 'node' | 'runIndex' | 'overrideOutputs'> | undefined
|
Pick<InstanceType<typeof RunData>['$props'], 'node' | 'runIndex' | 'overrideOutputs'> | undefined
|
||||||
>(() => {
|
>(() => {
|
||||||
@@ -27,7 +29,7 @@ const runDataProps = computed<
|
|||||||
return { node: logEntry.node, runIndex: logEntry.runIndex };
|
return { node: logEntry.node, runIndex: logEntry.runIndex };
|
||||||
}
|
}
|
||||||
|
|
||||||
const source = logEntry.runData.source[0];
|
const source = logEntry.runData?.source[0];
|
||||||
const node = source && logEntry.workflow.getNode(source.previousNode);
|
const node = source && logEntry.workflow.getNode(source.previousNode);
|
||||||
|
|
||||||
if (!source || !node) {
|
if (!source || !node) {
|
||||||
@@ -46,8 +48,8 @@ const runDataProps = computed<
|
|||||||
const isExecuting = computed(
|
const isExecuting = computed(
|
||||||
() =>
|
() =>
|
||||||
paneType === 'output' &&
|
paneType === 'output' &&
|
||||||
(logEntry.runData.executionStatus === 'running' ||
|
(logEntry.runData?.executionStatus === 'running' ||
|
||||||
logEntry.runData.executionStatus === 'waiting'),
|
logEntry.runData?.executionStatus === 'waiting'),
|
||||||
);
|
);
|
||||||
|
|
||||||
function handleClickOpenNdv() {
|
function handleClickOpenNdv() {
|
||||||
|
|||||||
@@ -68,8 +68,8 @@ export function useLogsExecutionData() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadSubExecution(logEntry: LogEntry) {
|
async function loadSubExecution(logEntry: LogEntry) {
|
||||||
const executionId = logEntry.runData.metadata?.subExecution?.executionId;
|
const executionId = logEntry.runData?.metadata?.subExecution?.executionId;
|
||||||
const workflowId = logEntry.runData.metadata?.subExecution?.workflowId;
|
const workflowId = logEntry.runData?.metadata?.subExecution?.workflowId;
|
||||||
|
|
||||||
if (!execData.value?.data || !executionId || !workflowId) {
|
if (!execData.value?.data || !executionId || !workflowId) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export interface LogEntry {
|
|||||||
children: LogEntry[];
|
children: LogEntry[];
|
||||||
depth: number;
|
depth: number;
|
||||||
runIndex: number;
|
runIndex: number;
|
||||||
runData: ITaskData;
|
runData: ITaskData | undefined;
|
||||||
consumedTokens: LlmTokenUsageData;
|
consumedTokens: LlmTokenUsageData;
|
||||||
workflow: Workflow;
|
workflow: Workflow;
|
||||||
executionId: string;
|
executionId: string;
|
||||||
|
|||||||
@@ -20,7 +20,12 @@ import {
|
|||||||
type ExecutionError,
|
type ExecutionError,
|
||||||
type ITaskStartedData,
|
type ITaskStartedData,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { createTestLogTreeCreationContext } from './__test__/data';
|
import {
|
||||||
|
aiAgentNode,
|
||||||
|
aiChatWorkflow,
|
||||||
|
aiModelNode,
|
||||||
|
createTestLogTreeCreationContext,
|
||||||
|
} from './__test__/data';
|
||||||
import type { LogEntrySelection } from './logs.types';
|
import type { LogEntrySelection } from './logs.types';
|
||||||
import type { IExecutionResponse } from '@/Interface';
|
import type { IExecutionResponse } from '@/Interface';
|
||||||
import { isReactive, reactive } from 'vue';
|
import { isReactive, reactive } from 'vue';
|
||||||
@@ -29,10 +34,11 @@ import { AGENT_NODE_TYPE, CHAT_TRIGGER_NODE_TYPE } from '@/constants';
|
|||||||
|
|
||||||
describe(getTreeNodeData, () => {
|
describe(getTreeNodeData, () => {
|
||||||
it('should generate one node per execution', () => {
|
it('should generate one node per execution', () => {
|
||||||
|
const nodeA = createTestNode({ name: 'A', id: 'test-node-id-a' });
|
||||||
const workflow = createTestWorkflowObject({
|
const workflow = createTestWorkflowObject({
|
||||||
id: 'test-wf-id',
|
id: 'test-wf-id',
|
||||||
nodes: [
|
nodes: [
|
||||||
createTestNode({ name: 'A', id: 'test-node-id-a' }),
|
nodeA,
|
||||||
createTestNode({ name: 'B', id: 'test-node-id-b' }),
|
createTestNode({ name: 'B', id: 'test-node-id-b' }),
|
||||||
createTestNode({ name: 'C', id: 'test-node-id-c' }),
|
createTestNode({ name: 'C', id: 'test-node-id-c' }),
|
||||||
],
|
],
|
||||||
@@ -68,7 +74,7 @@ describe(getTreeNodeData, () => {
|
|||||||
createTestTaskData({ startTime: 1740528000004 }),
|
createTestTaskData({ startTime: 1740528000004 }),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
const logTree = getTreeNodeData('A', ctx.data.resultData.runData.A[0], undefined, ctx);
|
const logTree = getTreeNodeData(nodeA, ctx.data.resultData.runData.A[0], undefined, ctx);
|
||||||
|
|
||||||
expect(logTree.length).toBe(1);
|
expect(logTree.length).toBe(1);
|
||||||
|
|
||||||
@@ -76,14 +82,14 @@ describe(getTreeNodeData, () => {
|
|||||||
expect(logTree[0].depth).toBe(0);
|
expect(logTree[0].depth).toBe(0);
|
||||||
expect(logTree[0].runIndex).toBe(0);
|
expect(logTree[0].runIndex).toBe(0);
|
||||||
expect(logTree[0].parent).toBe(undefined);
|
expect(logTree[0].parent).toBe(undefined);
|
||||||
expect(logTree[0].runData.startTime).toBe(1740528000000);
|
expect(logTree[0].runData?.startTime).toBe(1740528000000);
|
||||||
expect(logTree[0].children.length).toBe(2);
|
expect(logTree[0].children.length).toBe(2);
|
||||||
|
|
||||||
expect(logTree[0].children[0].id).toBe('test-wf-id:B:test-execution-id:0');
|
expect(logTree[0].children[0].id).toBe('test-wf-id:B:test-execution-id:0');
|
||||||
expect(logTree[0].children[0].depth).toBe(1);
|
expect(logTree[0].children[0].depth).toBe(1);
|
||||||
expect(logTree[0].children[0].runIndex).toBe(0);
|
expect(logTree[0].children[0].runIndex).toBe(0);
|
||||||
expect(logTree[0].children[0].parent?.node.name).toBe('A');
|
expect(logTree[0].children[0].parent?.node.name).toBe('A');
|
||||||
expect(logTree[0].children[0].runData.startTime).toBe(1740528000001);
|
expect(logTree[0].children[0].runData?.startTime).toBe(1740528000001);
|
||||||
expect(logTree[0].children[0].consumedTokens.isEstimate).toBe(false);
|
expect(logTree[0].children[0].consumedTokens.isEstimate).toBe(false);
|
||||||
expect(logTree[0].children[0].consumedTokens.completionTokens).toBe(1);
|
expect(logTree[0].children[0].consumedTokens.completionTokens).toBe(1);
|
||||||
expect(logTree[0].children[0].children.length).toBe(1);
|
expect(logTree[0].children[0].children.length).toBe(1);
|
||||||
@@ -111,12 +117,10 @@ describe(getTreeNodeData, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should filter node executions based on source node', () => {
|
it('should filter node executions based on source node', () => {
|
||||||
|
const rootNode1 = createTestNode({ name: 'RootNode1' });
|
||||||
|
const rootNode2 = createTestNode({ name: 'RootNode2' });
|
||||||
const workflow = createTestWorkflowObject({
|
const workflow = createTestWorkflowObject({
|
||||||
nodes: [
|
nodes: [rootNode1, rootNode2, createTestNode({ name: 'SharedSubNode' })],
|
||||||
createTestNode({ name: 'RootNode1' }),
|
|
||||||
createTestNode({ name: 'RootNode2' }),
|
|
||||||
createTestNode({ name: 'SharedSubNode' }),
|
|
||||||
],
|
|
||||||
connections: {
|
connections: {
|
||||||
SharedSubNode: {
|
SharedSubNode: {
|
||||||
ai_tool: [
|
ai_tool: [
|
||||||
@@ -159,7 +163,7 @@ describe(getTreeNodeData, () => {
|
|||||||
|
|
||||||
// Test for RootNode1 - should only show SharedSubNode with source RootNode1
|
// Test for RootNode1 - should only show SharedSubNode with source RootNode1
|
||||||
const rootNode1Tree = getTreeNodeData(
|
const rootNode1Tree = getTreeNodeData(
|
||||||
'RootNode1',
|
rootNode1,
|
||||||
runData.RootNode1[0],
|
runData.RootNode1[0],
|
||||||
undefined,
|
undefined,
|
||||||
createTestLogTreeCreationContext(workflow, runData),
|
createTestLogTreeCreationContext(workflow, runData),
|
||||||
@@ -170,7 +174,7 @@ describe(getTreeNodeData, () => {
|
|||||||
|
|
||||||
// Test for RootNode2 - should only show SharedSubNode with source RootNode2
|
// Test for RootNode2 - should only show SharedSubNode with source RootNode2
|
||||||
const rootNode2Tree = getTreeNodeData(
|
const rootNode2Tree = getTreeNodeData(
|
||||||
'RootNode2',
|
rootNode2,
|
||||||
runData.RootNode2[0],
|
runData.RootNode2[0],
|
||||||
undefined,
|
undefined,
|
||||||
createTestLogTreeCreationContext(workflow, runData),
|
createTestLogTreeCreationContext(workflow, runData),
|
||||||
@@ -181,8 +185,9 @@ describe(getTreeNodeData, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should filter node executions based on source run index', () => {
|
it('should filter node executions based on source run index', () => {
|
||||||
|
const rootNode = createTestNode({ name: 'RootNode' });
|
||||||
const workflow = createTestWorkflowObject({
|
const workflow = createTestWorkflowObject({
|
||||||
nodes: [createTestNode({ name: 'RootNode' }), createTestNode({ name: 'SubNode' })],
|
nodes: [rootNode, createTestNode({ name: 'SubNode' })],
|
||||||
connections: {
|
connections: {
|
||||||
SubNode: {
|
SubNode: {
|
||||||
ai_tool: [[{ node: 'RootNode', type: NodeConnectionTypes.AiTool, index: 0 }]],
|
ai_tool: [[{ node: 'RootNode', type: NodeConnectionTypes.AiTool, index: 0 }]],
|
||||||
@@ -216,7 +221,7 @@ describe(getTreeNodeData, () => {
|
|||||||
|
|
||||||
// Test for run #1 of RootNode - should only show SubNode with source run index 0
|
// Test for run #1 of RootNode - should only show SubNode with source run index 0
|
||||||
const rootNode1Tree = getTreeNodeData(
|
const rootNode1Tree = getTreeNodeData(
|
||||||
'RootNode',
|
rootNode,
|
||||||
runData.RootNode[0],
|
runData.RootNode[0],
|
||||||
0,
|
0,
|
||||||
createTestLogTreeCreationContext(workflow, runData),
|
createTestLogTreeCreationContext(workflow, runData),
|
||||||
@@ -227,7 +232,7 @@ describe(getTreeNodeData, () => {
|
|||||||
|
|
||||||
// Test for run #2 of RootNode - should only show SubNode with source run index 1
|
// Test for run #2 of RootNode - should only show SubNode with source run index 1
|
||||||
const rootNode2Tree = getTreeNodeData(
|
const rootNode2Tree = getTreeNodeData(
|
||||||
'RootNode',
|
rootNode,
|
||||||
runData.RootNode[1],
|
runData.RootNode[1],
|
||||||
1,
|
1,
|
||||||
createTestLogTreeCreationContext(workflow, runData),
|
createTestLogTreeCreationContext(workflow, runData),
|
||||||
@@ -238,8 +243,9 @@ describe(getTreeNodeData, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should include nodes without source information', () => {
|
it('should include nodes without source information', () => {
|
||||||
|
const rootNode = createTestNode({ name: 'RootNode' });
|
||||||
const workflow = createTestWorkflowObject({
|
const workflow = createTestWorkflowObject({
|
||||||
nodes: [createTestNode({ name: 'RootNode' }), createTestNode({ name: 'SubNode' })],
|
nodes: [rootNode, createTestNode({ name: 'SubNode' })],
|
||||||
connections: {
|
connections: {
|
||||||
SubNode: {
|
SubNode: {
|
||||||
ai_tool: [[{ node: 'RootNode', type: NodeConnectionTypes.AiTool, index: 0 }]],
|
ai_tool: [[{ node: 'RootNode', type: NodeConnectionTypes.AiTool, index: 0 }]],
|
||||||
@@ -267,7 +273,7 @@ describe(getTreeNodeData, () => {
|
|||||||
|
|
||||||
// Test for RootNode - should still show SubNode even without source info
|
// Test for RootNode - should still show SubNode even without source info
|
||||||
const rootNodeTree = getTreeNodeData(
|
const rootNodeTree = getTreeNodeData(
|
||||||
'RootNode',
|
rootNode,
|
||||||
runData.RootNode[0],
|
runData.RootNode[0],
|
||||||
undefined,
|
undefined,
|
||||||
createTestLogTreeCreationContext(workflow, runData),
|
createTestLogTreeCreationContext(workflow, runData),
|
||||||
@@ -278,8 +284,9 @@ describe(getTreeNodeData, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should include nodes with empty source array', () => {
|
it('should include nodes with empty source array', () => {
|
||||||
|
const rootNode = createTestNode({ name: 'RootNode' });
|
||||||
const workflow = createTestWorkflowObject({
|
const workflow = createTestWorkflowObject({
|
||||||
nodes: [createTestNode({ name: 'RootNode' }), createTestNode({ name: 'SubNode' })],
|
nodes: [rootNode, createTestNode({ name: 'SubNode' })],
|
||||||
connections: {
|
connections: {
|
||||||
SubNode: {
|
SubNode: {
|
||||||
ai_tool: [[{ node: 'RootNode', type: NodeConnectionTypes.AiTool, index: 0 }]],
|
ai_tool: [[{ node: 'RootNode', type: NodeConnectionTypes.AiTool, index: 0 }]],
|
||||||
@@ -307,7 +314,7 @@ describe(getTreeNodeData, () => {
|
|||||||
|
|
||||||
// Test for RootNode - should still show SubNode even with empty source array
|
// Test for RootNode - should still show SubNode even with empty source array
|
||||||
const rootNodeTree = getTreeNodeData(
|
const rootNodeTree = getTreeNodeData(
|
||||||
'RootNode',
|
rootNode,
|
||||||
runData.RootNode[0],
|
runData.RootNode[0],
|
||||||
undefined,
|
undefined,
|
||||||
createTestLogTreeCreationContext(workflow, runData),
|
createTestLogTreeCreationContext(workflow, runData),
|
||||||
@@ -318,8 +325,9 @@ describe(getTreeNodeData, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should include nodes with source array without previous node', () => {
|
it('should include nodes with source array without previous node', () => {
|
||||||
|
const rootNode = createTestNode({ name: 'RootNode' });
|
||||||
const workflow = createTestWorkflowObject({
|
const workflow = createTestWorkflowObject({
|
||||||
nodes: [createTestNode({ name: 'RootNode' }), createTestNode({ name: 'SubNode' })],
|
nodes: [rootNode, createTestNode({ name: 'SubNode' })],
|
||||||
connections: {
|
connections: {
|
||||||
SubNode: {
|
SubNode: {
|
||||||
ai_tool: [[{ node: 'RootNode', type: NodeConnectionTypes.AiTool, index: 0 }]],
|
ai_tool: [[{ node: 'RootNode', type: NodeConnectionTypes.AiTool, index: 0 }]],
|
||||||
@@ -334,7 +342,7 @@ describe(getTreeNodeData, () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const rootNodeTree = getTreeNodeData(
|
const rootNodeTree = getTreeNodeData(
|
||||||
'RootNode',
|
rootNode,
|
||||||
runData.RootNode[0],
|
runData.RootNode[0],
|
||||||
undefined,
|
undefined,
|
||||||
createTestLogTreeCreationContext(workflow, runData),
|
createTestLogTreeCreationContext(workflow, runData),
|
||||||
@@ -345,10 +353,12 @@ describe(getTreeNodeData, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should filter deeper nested nodes based on source', () => {
|
it('should filter deeper nested nodes based on source', () => {
|
||||||
|
const rootNode1 = createTestNode({ name: 'RootNode1' });
|
||||||
|
const rootNode2 = createTestNode({ name: 'RootNode2' });
|
||||||
const workflow = createTestWorkflowObject({
|
const workflow = createTestWorkflowObject({
|
||||||
nodes: [
|
nodes: [
|
||||||
createTestNode({ name: 'RootNode1' }),
|
rootNode1,
|
||||||
createTestNode({ name: 'RootNode2' }),
|
rootNode2,
|
||||||
createTestNode({ name: 'SharedSubNode' }),
|
createTestNode({ name: 'SharedSubNode' }),
|
||||||
createTestNode({ name: 'DeepSubNode' }),
|
createTestNode({ name: 'DeepSubNode' }),
|
||||||
],
|
],
|
||||||
@@ -411,7 +421,7 @@ describe(getTreeNodeData, () => {
|
|||||||
|
|
||||||
// Test filtering for RootNode1
|
// Test filtering for RootNode1
|
||||||
const rootNode1Tree = getTreeNodeData(
|
const rootNode1Tree = getTreeNodeData(
|
||||||
'RootNode1',
|
rootNode1,
|
||||||
runData.RootNode1[0],
|
runData.RootNode1[0],
|
||||||
undefined,
|
undefined,
|
||||||
createTestLogTreeCreationContext(workflow, runData),
|
createTestLogTreeCreationContext(workflow, runData),
|
||||||
@@ -425,7 +435,7 @@ describe(getTreeNodeData, () => {
|
|||||||
|
|
||||||
// Test filtering for RootNode2
|
// Test filtering for RootNode2
|
||||||
const rootNode2Tree = getTreeNodeData(
|
const rootNode2Tree = getTreeNodeData(
|
||||||
'RootNode2',
|
rootNode2,
|
||||||
runData.RootNode2[0],
|
runData.RootNode2[0],
|
||||||
undefined,
|
undefined,
|
||||||
createTestLogTreeCreationContext(workflow, runData),
|
createTestLogTreeCreationContext(workflow, runData),
|
||||||
@@ -439,10 +449,12 @@ describe(getTreeNodeData, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should handle complex tree with multiple branches and filters correctly', () => {
|
it('should handle complex tree with multiple branches and filters correctly', () => {
|
||||||
|
const rootNode1 = createTestNode({ name: 'RootNode1' });
|
||||||
|
const rootNode2 = createTestNode({ name: 'RootNode2' });
|
||||||
const workflow = createTestWorkflowObject({
|
const workflow = createTestWorkflowObject({
|
||||||
nodes: [
|
nodes: [
|
||||||
createTestNode({ name: 'RootNode1' }),
|
rootNode1,
|
||||||
createTestNode({ name: 'RootNode2' }),
|
rootNode2,
|
||||||
createTestNode({ name: 'SubNodeA' }),
|
createTestNode({ name: 'SubNodeA' }),
|
||||||
createTestNode({ name: 'SubNodeB' }),
|
createTestNode({ name: 'SubNodeB' }),
|
||||||
createTestNode({ name: 'DeepNode' }),
|
createTestNode({ name: 'DeepNode' }),
|
||||||
@@ -511,7 +523,7 @@ describe(getTreeNodeData, () => {
|
|||||||
|
|
||||||
// Test filtering for RootNode1 -> SubNodeA -> DeepNode
|
// Test filtering for RootNode1 -> SubNodeA -> DeepNode
|
||||||
const rootNode1Tree = getTreeNodeData(
|
const rootNode1Tree = getTreeNodeData(
|
||||||
'RootNode1',
|
rootNode1,
|
||||||
runData.RootNode1[0],
|
runData.RootNode1[0],
|
||||||
undefined,
|
undefined,
|
||||||
createTestLogTreeCreationContext(workflow, runData),
|
createTestLogTreeCreationContext(workflow, runData),
|
||||||
@@ -524,7 +536,7 @@ describe(getTreeNodeData, () => {
|
|||||||
|
|
||||||
// Test filtering for RootNode2 -> SubNodeB -> DeepNode
|
// Test filtering for RootNode2 -> SubNodeB -> DeepNode
|
||||||
const rootNode2Tree = getTreeNodeData(
|
const rootNode2Tree = getTreeNodeData(
|
||||||
'RootNode2',
|
rootNode2,
|
||||||
runData.RootNode2[0],
|
runData.RootNode2[0],
|
||||||
undefined,
|
undefined,
|
||||||
createTestLogTreeCreationContext(workflow, runData),
|
createTestLogTreeCreationContext(workflow, runData),
|
||||||
@@ -986,6 +998,88 @@ describe(createLogTree, () => {
|
|||||||
expect(logs[0].children[1].children[0].node.name).toBe('C');
|
expect(logs[0].children[1].children[0].node.name).toBe('C');
|
||||||
expect(logs[0].children[1].children[1].node.name).toBe('C');
|
expect(logs[0].children[1].children[1].node.name).toBe('C');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not include nodes without run data', () => {
|
||||||
|
const logs = createLogTree(
|
||||||
|
createTestWorkflowObject(aiChatWorkflow),
|
||||||
|
createTestWorkflowExecutionResponse({ data: { resultData: { runData: {} } } }),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(logs).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include sub node log without run data in its root node', () => {
|
||||||
|
const taskData = createTestTaskData({
|
||||||
|
source: [{ previousNode: 'PartialExecutionToolExecutor' }],
|
||||||
|
});
|
||||||
|
const logs = createLogTree(
|
||||||
|
createTestWorkflowObject(aiChatWorkflow),
|
||||||
|
createTestWorkflowExecutionResponse({
|
||||||
|
data: { resultData: { runData: { [aiModelNode.name]: [taskData] } } },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(logs).toHaveLength(1);
|
||||||
|
expect(logs[0].node.name).toBe(aiAgentNode.name);
|
||||||
|
expect(logs[0].runData).toBe(undefined);
|
||||||
|
expect(logs[0].children).toHaveLength(1);
|
||||||
|
expect(logs[0].children[0].node.name).toBe(aiModelNode.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include sub node log with its root node disabled', () => {
|
||||||
|
const taskData = createTestTaskData({
|
||||||
|
source: [{ previousNode: 'PartialExecutionToolExecutor' }],
|
||||||
|
});
|
||||||
|
const logs = createLogTree(
|
||||||
|
createTestWorkflowObject({
|
||||||
|
...aiChatWorkflow,
|
||||||
|
nodes: [{ ...aiAgentNode, disabled: true }, aiModelNode],
|
||||||
|
}),
|
||||||
|
createTestWorkflowExecutionResponse({
|
||||||
|
data: { resultData: { runData: { [aiModelNode.name]: [taskData] } } },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(logs).toHaveLength(1);
|
||||||
|
expect(logs[0].node.name).toBe(aiAgentNode.name);
|
||||||
|
expect(logs[0].runData).toBe(undefined);
|
||||||
|
expect(logs[0].children).toHaveLength(1);
|
||||||
|
expect(logs[0].children[0].node.name).toBe(aiModelNode.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not include duplicate sub node log when the node belongs to multiple root nodes with no run data', () => {
|
||||||
|
const taskData = createTestTaskData({
|
||||||
|
source: [{ previousNode: 'PartialExecutionToolExecutor' }],
|
||||||
|
});
|
||||||
|
const logs = createLogTree(
|
||||||
|
createTestWorkflowObject({
|
||||||
|
nodes: [
|
||||||
|
{ ...aiAgentNode, name: 'Agent A' },
|
||||||
|
{ ...aiAgentNode, name: 'Agent B' },
|
||||||
|
aiModelNode,
|
||||||
|
],
|
||||||
|
connections: {
|
||||||
|
[aiModelNode.name]: {
|
||||||
|
[NodeConnectionTypes.AiLanguageModel]: [
|
||||||
|
[
|
||||||
|
{ node: 'Agent A', index: 0, type: NodeConnectionTypes.AiLanguageModel },
|
||||||
|
{ node: 'Agent B', index: 0, type: NodeConnectionTypes.AiLanguageModel },
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
createTestWorkflowExecutionResponse({
|
||||||
|
data: { resultData: { runData: { [aiModelNode.name]: [taskData] } } },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(logs).toHaveLength(1);
|
||||||
|
expect(logs[0].node.name).toBe('Agent B');
|
||||||
|
expect(logs[0].runData).toBe(undefined);
|
||||||
|
expect(logs[0].children).toHaveLength(1);
|
||||||
|
expect(logs[0].children[0].node.name).toBe(aiModelNode.name);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe(deepToRaw, () => {
|
describe(deepToRaw, () => {
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import {
|
|||||||
type ITaskData,
|
type ITaskData,
|
||||||
type ITaskStartedData,
|
type ITaskStartedData,
|
||||||
type Workflow,
|
type Workflow,
|
||||||
|
type INode,
|
||||||
|
type ISourceData,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import type { LogEntry, LogEntrySelection, LogTreeCreationContext } from './logs.types';
|
import type { LogEntry, LogEntrySelection, LogTreeCreationContext } from './logs.types';
|
||||||
import { isProxy, isReactive, isRef, toRaw } from 'vue';
|
import { isProxy, isReactive, isRef, toRaw } from 'vue';
|
||||||
@@ -16,6 +18,7 @@ import { type ChatMessage } from '@n8n/chat/types';
|
|||||||
import get from 'lodash-es/get';
|
import get from 'lodash-es/get';
|
||||||
import isEmpty from 'lodash-es/isEmpty';
|
import isEmpty from 'lodash-es/isEmpty';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
|
import { TOOL_EXECUTOR_NODE_NAME } from '@n8n/constants';
|
||||||
|
|
||||||
function getConsumedTokens(task: ITaskData): LlmTokenUsageData {
|
function getConsumedTokens(task: ITaskData): LlmTokenUsageData {
|
||||||
if (!task.data) {
|
if (!task.data) {
|
||||||
@@ -43,7 +46,7 @@ function createNode(
|
|||||||
node: INodeUi,
|
node: INodeUi,
|
||||||
context: LogTreeCreationContext,
|
context: LogTreeCreationContext,
|
||||||
runIndex: number,
|
runIndex: number,
|
||||||
runData: ITaskData,
|
runData: ITaskData | undefined,
|
||||||
children: LogEntry[] = [],
|
children: LogEntry[] = [],
|
||||||
): LogEntry {
|
): LogEntry {
|
||||||
return {
|
return {
|
||||||
@@ -54,24 +57,13 @@ function createNode(
|
|||||||
runIndex,
|
runIndex,
|
||||||
runData,
|
runData,
|
||||||
children,
|
children,
|
||||||
consumedTokens: getConsumedTokens(runData),
|
consumedTokens: runData ? getConsumedTokens(runData) : emptyTokenUsageData,
|
||||||
workflow: context.workflow,
|
workflow: context.workflow,
|
||||||
executionId: context.executionId,
|
executionId: context.executionId,
|
||||||
execution: context.data,
|
execution: context.data,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTreeNodeData(
|
|
||||||
nodeName: string,
|
|
||||||
runData: ITaskData,
|
|
||||||
runIndex: number | undefined,
|
|
||||||
context: LogTreeCreationContext,
|
|
||||||
): LogEntry[] {
|
|
||||||
const node = context.workflow.getNode(nodeName);
|
|
||||||
|
|
||||||
return node ? getTreeNodeDataRec(node, runData, context, runIndex) : [];
|
|
||||||
}
|
|
||||||
|
|
||||||
function getChildNodes(
|
function getChildNodes(
|
||||||
treeNode: LogEntry,
|
treeNode: LogEntry,
|
||||||
node: INodeUi,
|
node: INodeUi,
|
||||||
@@ -79,8 +71,8 @@ function getChildNodes(
|
|||||||
context: LogTreeCreationContext,
|
context: LogTreeCreationContext,
|
||||||
) {
|
) {
|
||||||
if (hasSubExecution(treeNode)) {
|
if (hasSubExecution(treeNode)) {
|
||||||
const workflowId = treeNode.runData.metadata?.subExecution?.workflowId;
|
const workflowId = treeNode.runData?.metadata?.subExecution?.workflowId;
|
||||||
const executionId = treeNode.runData.metadata?.subExecution?.executionId;
|
const executionId = treeNode.runData?.metadata?.subExecution?.executionId;
|
||||||
const workflow = workflowId ? context.workflows[workflowId] : undefined;
|
const workflow = workflowId ? context.workflows[workflowId] : undefined;
|
||||||
const subWorkflowRunData = executionId ? context.subWorkflowData[executionId] : undefined;
|
const subWorkflowRunData = executionId ? context.subWorkflowData[executionId] : undefined;
|
||||||
|
|
||||||
@@ -102,6 +94,14 @@ function getChildNodes(
|
|||||||
const connectedSubNodes = context.workflow.getParentNodes(node.name, 'ALL_NON_MAIN', 1);
|
const connectedSubNodes = context.workflow.getParentNodes(node.name, 'ALL_NON_MAIN', 1);
|
||||||
const isExecutionRoot = !isSubNodeLog(treeNode);
|
const isExecutionRoot = !isSubNodeLog(treeNode);
|
||||||
|
|
||||||
|
function isMatchedSource(source: ISourceData | null): boolean {
|
||||||
|
return (
|
||||||
|
(source?.previousNode === node.name ||
|
||||||
|
(isPlaceholderLog(treeNode) && source?.previousNode === TOOL_EXECUTOR_NODE_NAME)) &&
|
||||||
|
(runIndex === undefined || source.previousNodeRun === runIndex)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return connectedSubNodes.flatMap((subNodeName) =>
|
return connectedSubNodes.flatMap((subNodeName) =>
|
||||||
(context.data.resultData.runData[subNodeName] ?? []).flatMap((t, index) => {
|
(context.data.resultData.runData[subNodeName] ?? []).flatMap((t, index) => {
|
||||||
// At root depth, filter out node executions that weren't triggered by this node
|
// At root depth, filter out node executions that weren't triggered by this node
|
||||||
@@ -109,11 +109,7 @@ function getChildNodes(
|
|||||||
// Only filter nodes that have source information with valid previousNode references
|
// Only filter nodes that have source information with valid previousNode references
|
||||||
const isMatched =
|
const isMatched =
|
||||||
isExecutionRoot && t.source.some((source) => source !== null)
|
isExecutionRoot && t.source.some((source) => source !== null)
|
||||||
? t.source.some(
|
? t.source.some(isMatchedSource)
|
||||||
(source) =>
|
|
||||||
source?.previousNode === node.name &&
|
|
||||||
(runIndex === undefined || source.previousNodeRun === runIndex),
|
|
||||||
)
|
|
||||||
: runIndex === undefined || index === runIndex;
|
: runIndex === undefined || index === runIndex;
|
||||||
|
|
||||||
if (!isMatched) {
|
if (!isMatched) {
|
||||||
@@ -123,26 +119,29 @@ function getChildNodes(
|
|||||||
const subNode = context.workflow.getNode(subNodeName);
|
const subNode = context.workflow.getNode(subNodeName);
|
||||||
|
|
||||||
return subNode
|
return subNode
|
||||||
? getTreeNodeDataRec(
|
? getTreeNodeData(subNode, t, index, {
|
||||||
subNode,
|
...context,
|
||||||
t,
|
depth: context.depth + 1,
|
||||||
{ ...context, depth: context.depth + 1, parent: treeNode },
|
parent: treeNode,
|
||||||
index,
|
})
|
||||||
)
|
|
||||||
: [];
|
: [];
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTreeNodeDataRec(
|
export function getTreeNodeData(
|
||||||
node: INodeUi,
|
node: INodeUi,
|
||||||
runData: ITaskData,
|
runData: ITaskData | undefined,
|
||||||
context: LogTreeCreationContext,
|
|
||||||
runIndex: number | undefined,
|
runIndex: number | undefined,
|
||||||
|
context: LogTreeCreationContext,
|
||||||
): LogEntry[] {
|
): LogEntry[] {
|
||||||
const treeNode = createNode(node, context, runIndex ?? 0, runData);
|
const treeNode = createNode(node, context, runIndex ?? 0, runData);
|
||||||
const children = getChildNodes(treeNode, node, runIndex, context).sort(sortLogEntries);
|
const children = getChildNodes(treeNode, node, runIndex, context).sort(sortLogEntries);
|
||||||
|
|
||||||
|
if ((runData === undefined || node.disabled) && children.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
treeNode.children = children;
|
treeNode.children = children;
|
||||||
|
|
||||||
return [treeNode];
|
return [treeNode];
|
||||||
@@ -185,6 +184,10 @@ function findLogEntryToAutoSelectRec(subTree: LogEntry[], depth: number): LogEnt
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (entry.node.type === AGENT_LANGCHAIN_NODE_TYPE) {
|
if (entry.node.type === AGENT_LANGCHAIN_NODE_TYPE) {
|
||||||
|
if (isPlaceholderLog(entry) && entry.children.length > 0) {
|
||||||
|
return entry.children[0];
|
||||||
|
}
|
||||||
|
|
||||||
return entry;
|
return entry;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -210,23 +213,52 @@ export function createLogTree(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createLogTreeRec(context: LogTreeCreationContext) {
|
function createLogTreeRec(context: LogTreeCreationContext) {
|
||||||
const runs = Object.entries(context.data.resultData.runData)
|
const runData = context.data.resultData.runData;
|
||||||
.flatMap(([nodeName, taskData]) =>
|
|
||||||
context.workflow.getChildNodes(nodeName, 'ALL_NON_MAIN').length > 0 ||
|
return Object.entries(runData)
|
||||||
context.workflow.getNode(nodeName)?.disabled
|
.flatMap<{
|
||||||
? [] // skip sub nodes and disabled nodes
|
node: INode;
|
||||||
: taskData.map((task, runIndex) => ({
|
task?: ITaskData;
|
||||||
nodeName,
|
runIndex?: number;
|
||||||
runData: task,
|
nodeHasMultipleRuns: boolean;
|
||||||
runIndex,
|
}>(([nodeName, taskData]) => {
|
||||||
nodeHasMultipleRuns: taskData.length > 1,
|
const node = context.workflow.getNode(nodeName);
|
||||||
})),
|
|
||||||
|
if (node === null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const childNodes = context.workflow.getChildNodes(nodeName, 'ALL_NON_MAIN');
|
||||||
|
|
||||||
|
if (childNodes.length === 0) {
|
||||||
|
// The node is root node
|
||||||
|
return taskData.map((task, runIndex) => ({
|
||||||
|
node,
|
||||||
|
task,
|
||||||
|
runIndex,
|
||||||
|
nodeHasMultipleRuns: taskData.length > 1,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The node is sub node
|
||||||
|
if (childNodes.some((child) => (runData[child] ?? []).length > 0)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// The sub node has data but its children don't: this can happen for partial execution of tools.
|
||||||
|
// In this case, we insert first child as placeholder so that the node is included in the tree.
|
||||||
|
const firstChild = context.workflow.getNode(childNodes[0]);
|
||||||
|
|
||||||
|
if (firstChild === null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [{ node: firstChild, nodeHasMultipleRuns: false }];
|
||||||
|
})
|
||||||
|
.flatMap(({ node, runIndex, task, nodeHasMultipleRuns }) =>
|
||||||
|
getTreeNodeData(node, task, nodeHasMultipleRuns ? runIndex : undefined, context),
|
||||||
)
|
)
|
||||||
.sort(sortLogEntries);
|
.sort(sortLogEntries);
|
||||||
|
|
||||||
return runs.flatMap(({ nodeName, runIndex, runData, nodeHasMultipleRuns }) =>
|
|
||||||
getTreeNodeData(nodeName, runData, nodeHasMultipleRuns ? runIndex : undefined, context),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function findLogEntryRec(
|
export function findLogEntryRec(
|
||||||
@@ -334,7 +366,15 @@ export function getEntryAtRelativeIndex(
|
|||||||
return offset === -1 ? undefined : entries[offset + relativeIndex];
|
return offset === -1 ? undefined : entries[offset + relativeIndex];
|
||||||
}
|
}
|
||||||
|
|
||||||
function sortLogEntries<T extends { runData: ITaskData }>(a: T, b: T) {
|
function sortLogEntries(a: LogEntry, b: LogEntry): number {
|
||||||
|
if (a.runData === undefined) {
|
||||||
|
return a.children.length > 0 ? sortLogEntries(a.children[0], b) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (b.runData === undefined) {
|
||||||
|
return b.children.length > 0 ? sortLogEntries(a, b.children[0]) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
// We rely on execution index only when startTime is different
|
// We rely on execution index only when startTime is different
|
||||||
// Because it is reset to 0 when execution is waited, and therefore not necessarily unique
|
// Because it is reset to 0 when execution is waited, and therefore not necessarily unique
|
||||||
if (a.runData.startTime === b.runData.startTime) {
|
if (a.runData.startTime === b.runData.startTime) {
|
||||||
@@ -394,7 +434,7 @@ export function mergeStartData(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function hasSubExecution(entry: LogEntry): boolean {
|
export function hasSubExecution(entry: LogEntry): boolean {
|
||||||
return !!entry.runData.metadata?.subExecution;
|
return !!entry.runData?.metadata?.subExecution;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getDefaultCollapsedEntries(entries: LogEntry[]): Record<string, boolean> {
|
export function getDefaultCollapsedEntries(entries: LogEntry[]): Record<string, boolean> {
|
||||||
@@ -542,3 +582,7 @@ export function restoreChatHistory(
|
|||||||
export function isSubNodeLog(logEntry: LogEntry): boolean {
|
export function isSubNodeLog(logEntry: LogEntry): boolean {
|
||||||
return logEntry.parent !== undefined && logEntry.parent.executionId === logEntry.executionId;
|
return logEntry.parent !== undefined && logEntry.parent.executionId === logEntry.executionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isPlaceholderLog(treeNode: LogEntry): boolean {
|
||||||
|
return treeNode.runData === undefined;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user