refactor(workflow): revamp logging module for loop & iteration nodes (#19484)

This commit is contained in:
LeanDeR 2025-05-12 09:32:41 +08:00 committed by GitHub
parent 87da155477
commit c720e0dd04
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 149 additions and 46 deletions

View File

@ -353,27 +353,26 @@ class IterationNode(BaseNode[IterationNodeData]):
) -> NodeRunStartedEvent | BaseNodeEvent | InNodeEvent: ) -> NodeRunStartedEvent | BaseNodeEvent | InNodeEvent:
""" """
add iteration metadata to event. add iteration metadata to event.
ensures iteration context (ID, index/parallel_run_id) is added to metadata,
""" """
if not isinstance(event, BaseNodeEvent): if not isinstance(event, BaseNodeEvent):
return event return event
if self.node_data.is_parallel and isinstance(event, NodeRunStartedEvent): if self.node_data.is_parallel and isinstance(event, NodeRunStartedEvent):
event.parallel_mode_run_id = parallel_mode_run_id event.parallel_mode_run_id = parallel_mode_run_id
return event
iter_metadata = {
NodeRunMetadataKey.ITERATION_ID: self.node_id,
NodeRunMetadataKey.ITERATION_INDEX: iter_run_index,
}
if parallel_mode_run_id:
# for parallel, the specific branch ID is more important than the sequential index
iter_metadata[NodeRunMetadataKey.PARALLEL_MODE_RUN_ID] = parallel_mode_run_id
if event.route_node_state.node_run_result: if event.route_node_state.node_run_result:
metadata = event.route_node_state.node_run_result.metadata current_metadata = event.route_node_state.node_run_result.metadata or {}
if not metadata: if NodeRunMetadataKey.ITERATION_ID not in current_metadata:
metadata = {} event.route_node_state.node_run_result.metadata = {**current_metadata, **iter_metadata}
if NodeRunMetadataKey.ITERATION_ID not in metadata:
metadata = {
**metadata,
NodeRunMetadataKey.ITERATION_ID: self.node_id,
NodeRunMetadataKey.PARALLEL_MODE_RUN_ID
if self.node_data.is_parallel
else NodeRunMetadataKey.ITERATION_INDEX: parallel_mode_run_id
if self.node_data.is_parallel
else iter_run_index,
}
event.route_node_state.node_run_result.metadata = metadata
return event return event
def _run_single_iter( def _run_single_iter(

View File

@ -337,7 +337,7 @@ class LoopNode(BaseNode[LoopNodeData]):
return {"check_break_result": True} return {"check_break_result": True}
elif isinstance(event, NodeRunFailedEvent): elif isinstance(event, NodeRunFailedEvent):
# Loop run failed # Loop run failed
yield event yield self._handle_event_metadata(event=event, iter_run_index=current_index)
yield LoopRunFailedEvent( yield LoopRunFailedEvent(
loop_id=self.id, loop_id=self.id,
loop_node_id=self.node_id, loop_node_id=self.node_id,

View File

@ -9,44 +9,90 @@ import { Iteration } from '@/app/components/base/icons/src/vender/workflow'
type IterationLogTriggerProps = { type IterationLogTriggerProps = {
nodeInfo: NodeTracing nodeInfo: NodeTracing
allExecutions?: NodeTracing[]
onShowIterationResultList: (iterationResultList: NodeTracing[][], iterationResultDurationMap: IterationDurationMap) => void onShowIterationResultList: (iterationResultList: NodeTracing[][], iterationResultDurationMap: IterationDurationMap) => void
} }
const IterationLogTrigger = ({ const IterationLogTrigger = ({
nodeInfo, nodeInfo,
allExecutions,
onShowIterationResultList, onShowIterationResultList,
}: IterationLogTriggerProps) => { }: IterationLogTriggerProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const filterNodesForInstance = (key: string): NodeTracing[] => {
if (!allExecutions) return []
const parallelNodes = allExecutions.filter(exec =>
exec.execution_metadata?.parallel_mode_run_id === key,
)
if (parallelNodes.length > 0)
return parallelNodes
const serialIndex = parseInt(key, 10)
if (!isNaN(serialIndex)) {
const serialNodes = allExecutions.filter(exec =>
exec.execution_metadata?.iteration_id === nodeInfo.node_id
&& exec.execution_metadata?.iteration_index === serialIndex,
)
if (serialNodes.length > 0)
return serialNodes
}
return []
}
const handleOnShowIterationDetail = (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation()
e.nativeEvent.stopImmediatePropagation()
const iterationNodeMeta = nodeInfo.execution_metadata
const iterDurationMap = nodeInfo?.iterDurationMap || iterationNodeMeta?.iteration_duration_map || {}
let structuredList: NodeTracing[][] = []
if (iterationNodeMeta?.iteration_duration_map) {
const instanceKeys = Object.keys(iterationNodeMeta.iteration_duration_map)
structuredList = instanceKeys
.map(key => filterNodesForInstance(key))
.filter(branchNodes => branchNodes.length > 0)
}
else if (nodeInfo.details?.length) {
structuredList = nodeInfo.details
}
onShowIterationResultList(structuredList, iterDurationMap)
}
let displayIterationCount = 0
const iterMap = nodeInfo.execution_metadata?.iteration_duration_map
if (iterMap)
displayIterationCount = Object.keys(iterMap).length
else if (nodeInfo.details?.length)
displayIterationCount = nodeInfo.details.length
else if (nodeInfo.metadata?.iterator_length)
displayIterationCount = nodeInfo.metadata.iterator_length
const getErrorCount = (details: NodeTracing[][] | undefined) => { const getErrorCount = (details: NodeTracing[][] | undefined) => {
if (!details || details.length === 0) if (!details || details.length === 0)
return 0 return 0
return details.reduce((acc, iteration) => { return details.reduce((acc, iteration) => {
if (iteration.some(item => item.status === 'failed')) if (iteration.some(item => item.status === 'failed'))
acc++ acc++
return acc return acc
}, 0) }, 0)
} }
const getCount = (iteration_curr_length: number | undefined, iteration_length: number) => { const errorCount = getErrorCount(nodeInfo.details)
if ((iteration_curr_length && iteration_curr_length < iteration_length) || !iteration_length)
return iteration_curr_length
return iteration_length
}
const handleOnShowIterationDetail = (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation()
e.nativeEvent.stopImmediatePropagation()
onShowIterationResultList(nodeInfo.details || [], nodeInfo?.iterDurationMap || nodeInfo.execution_metadata?.iteration_duration_map || {})
}
return ( return (
<Button <Button
className='flex w-full cursor-pointer items-center gap-2 self-stretch rounded-lg border-none bg-components-button-tertiary-bg-hover px-3 py-2 hover:bg-components-button-tertiary-bg-hover' className='flex w-full cursor-pointer items-center gap-2 self-stretch rounded-lg border-none bg-components-button-tertiary-bg-hover px-3 py-2 hover:bg-components-button-tertiary-bg-hover'
onClick={handleOnShowIterationDetail} onClick={handleOnShowIterationDetail}
> >
<Iteration className='h-4 w-4 shrink-0 text-components-button-tertiary-text' /> <Iteration className='h-4 w-4 shrink-0 text-components-button-tertiary-text' />
<div className='system-sm-medium flex-1 text-left text-components-button-tertiary-text'>{t('workflow.nodes.iteration.iteration', { count: getCount(nodeInfo.details?.length, nodeInfo.metadata?.iterator_length) })}{getErrorCount(nodeInfo.details) > 0 && ( <div className='system-sm-medium flex-1 text-left text-components-button-tertiary-text'>{t('workflow.nodes.iteration.iteration', { count: displayIterationCount })}{errorCount > 0 && (
<> <>
{t('workflow.nodes.iteration.comma')} {t('workflow.nodes.iteration.comma')}
{t('workflow.nodes.iteration.error', { count: getErrorCount(nodeInfo.details) })} {t('workflow.nodes.iteration.error', { count: errorCount })}
</> </>
)}</div> )}</div>
<RiArrowRightSLine className='h-4 w-4 shrink-0 text-components-button-tertiary-text' /> <RiArrowRightSLine className='h-4 w-4 shrink-0 text-components-button-tertiary-text' />

View File

@ -10,48 +10,95 @@ import { Loop } from '@/app/components/base/icons/src/vender/workflow'
type LoopLogTriggerProps = { type LoopLogTriggerProps = {
nodeInfo: NodeTracing nodeInfo: NodeTracing
allExecutions?: NodeTracing[]
onShowLoopResultList: (loopResultList: NodeTracing[][], loopResultDurationMap: LoopDurationMap, loopVariableMap: LoopVariableMap) => void onShowLoopResultList: (loopResultList: NodeTracing[][], loopResultDurationMap: LoopDurationMap, loopVariableMap: LoopVariableMap) => void
} }
const LoopLogTrigger = ({ const LoopLogTrigger = ({
nodeInfo, nodeInfo,
allExecutions,
onShowLoopResultList, onShowLoopResultList,
}: LoopLogTriggerProps) => { }: LoopLogTriggerProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const filterNodesForInstance = (key: string): NodeTracing[] => {
if (!allExecutions) return []
const parallelNodes = allExecutions.filter(exec =>
exec.execution_metadata?.parallel_mode_run_id === key,
)
if (parallelNodes.length > 0)
return parallelNodes
const serialIndex = parseInt(key, 10)
if (!isNaN(serialIndex)) {
const serialNodes = allExecutions.filter(exec =>
exec.execution_metadata?.loop_id === nodeInfo.node_id
&& exec.execution_metadata?.loop_index === serialIndex,
)
if (serialNodes.length > 0)
return serialNodes
}
return []
}
const handleOnShowLoopDetail = (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation()
e.nativeEvent.stopImmediatePropagation()
const loopNodeMeta = nodeInfo.execution_metadata
const loopDurMap = nodeInfo?.loopDurationMap || loopNodeMeta?.loop_duration_map || {}
const loopVarMap = loopNodeMeta?.loop_variable_map || {}
let structuredList: NodeTracing[][] = []
if (loopNodeMeta?.loop_duration_map) {
const instanceKeys = Object.keys(loopNodeMeta.loop_duration_map)
structuredList = instanceKeys
.map(key => filterNodesForInstance(key))
.filter(branchNodes => branchNodes.length > 0)
}
else if (nodeInfo.details?.length) {
structuredList = nodeInfo.details
}
onShowLoopResultList(
structuredList,
loopDurMap,
loopVarMap,
)
}
let displayLoopCount = 0
const loopMap = nodeInfo.execution_metadata?.loop_duration_map
if (loopMap)
displayLoopCount = Object.keys(loopMap).length
else if (nodeInfo.details?.length)
displayLoopCount = nodeInfo.details.length
else if (nodeInfo.metadata?.loop_length)
displayLoopCount = nodeInfo.metadata.loop_length
const getErrorCount = (details: NodeTracing[][] | undefined) => { const getErrorCount = (details: NodeTracing[][] | undefined) => {
if (!details || details.length === 0) if (!details || details.length === 0)
return 0 return 0
return details.reduce((acc, loop) => { return details.reduce((acc, loop) => {
if (loop.some(item => item.status === 'failed')) if (loop.some(item => item.status === 'failed'))
acc++ acc++
return acc return acc
}, 0) }, 0)
} }
const getCount = (loop_curr_length: number | undefined, loop_length: number) => { const errorCount = getErrorCount(nodeInfo.details)
if ((loop_curr_length && loop_curr_length < loop_length) || !loop_length)
return loop_curr_length
return loop_length
}
const handleOnShowLoopDetail = (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation()
e.nativeEvent.stopImmediatePropagation()
onShowLoopResultList(
nodeInfo.details || [],
nodeInfo?.loopDurationMap || nodeInfo.execution_metadata?.loop_duration_map || {},
nodeInfo.execution_metadata?.loop_variable_map || {},
)
}
return ( return (
<Button <Button
className='flex w-full cursor-pointer items-center gap-2 self-stretch rounded-lg border-none bg-components-button-tertiary-bg-hover px-3 py-2 hover:bg-components-button-tertiary-bg-hover' className='flex w-full cursor-pointer items-center gap-2 self-stretch rounded-lg border-none bg-components-button-tertiary-bg-hover px-3 py-2 hover:bg-components-button-tertiary-bg-hover'
onClick={handleOnShowLoopDetail} onClick={handleOnShowLoopDetail}
> >
<Loop className='h-4 w-4 shrink-0 text-components-button-tertiary-text' /> <Loop className='h-4 w-4 shrink-0 text-components-button-tertiary-text' />
<div className='system-sm-medium flex-1 text-left text-components-button-tertiary-text'>{t('workflow.nodes.loop.loop', { count: getCount(nodeInfo.details?.length, nodeInfo.metadata?.loop_length) })}{getErrorCount(nodeInfo.details) > 0 && ( <div className='system-sm-medium flex-1 text-left text-components-button-tertiary-text'>{t('workflow.nodes.loop.loop', { count: displayLoopCount })}{errorCount > 0 && (
<> <>
{t('workflow.nodes.loop.comma')} {t('workflow.nodes.loop.comma')}
{t('workflow.nodes.loop.error', { count: getErrorCount(nodeInfo.details) })} {t('workflow.nodes.loop.error', { count: errorCount })}
</> </>
)}</div> )}</div>
<RiArrowRightSLine className='h-4 w-4 shrink-0 text-components-button-tertiary-text' /> <RiArrowRightSLine className='h-4 w-4 shrink-0 text-components-button-tertiary-text' />

View File

@ -32,6 +32,7 @@ import { hasRetryNode } from '@/app/components/workflow/utils'
type Props = { type Props = {
className?: string className?: string
nodeInfo: NodeTracing nodeInfo: NodeTracing
allExecutions?: NodeTracing[]
inMessage?: boolean inMessage?: boolean
hideInfo?: boolean hideInfo?: boolean
hideProcessDetail?: boolean hideProcessDetail?: boolean
@ -46,6 +47,7 @@ type Props = {
const NodePanel: FC<Props> = ({ const NodePanel: FC<Props> = ({
className, className,
nodeInfo, nodeInfo,
allExecutions,
inMessage = false, inMessage = false,
hideInfo = false, hideInfo = false,
hideProcessDetail, hideProcessDetail,
@ -157,6 +159,7 @@ const NodePanel: FC<Props> = ({
{isIterationNode && !notShowIterationNav && onShowIterationDetail && ( {isIterationNode && !notShowIterationNav && onShowIterationDetail && (
<IterationLogTrigger <IterationLogTrigger
nodeInfo={nodeInfo} nodeInfo={nodeInfo}
allExecutions={allExecutions}
onShowIterationResultList={onShowIterationDetail} onShowIterationResultList={onShowIterationDetail}
/> />
)} )}
@ -164,6 +167,7 @@ const NodePanel: FC<Props> = ({
{isLoopNode && !notShowLoopNav && onShowLoopDetail && ( {isLoopNode && !notShowLoopNav && onShowLoopDetail && (
<LoopLogTrigger <LoopLogTrigger
nodeInfo={nodeInfo} nodeInfo={nodeInfo}
allExecutions={allExecutions}
onShowLoopResultList={onShowLoopDetail} onShowLoopResultList={onShowLoopDetail}
/> />
)} )}

View File

@ -145,6 +145,7 @@ const TracingPanel: FC<TracingPanelProps> = ({
</div> </div>
<NodePanel <NodePanel
nodeInfo={node!} nodeInfo={node!}
allExecutions={list}
onShowIterationDetail={handleShowIterationResultList} onShowIterationDetail={handleShowIterationResultList}
onShowLoopDetail={handleShowLoopResultList} onShowLoopDetail={handleShowLoopResultList}
onShowRetryDetail={handleShowRetryResultList} onShowRetryDetail={handleShowRetryResultList}

View File

@ -738,6 +738,9 @@ const translation = {
loop_one: '{{count}} Loop', loop_one: '{{count}} Loop',
loop_other: '{{count}} Loops', loop_other: '{{count}} Loops',
currentLoop: 'Current Loop', currentLoop: 'Current Loop',
comma: ', ',
error_one: '{{count}} Error',
error_other: '{{count}} Errors',
breakCondition: 'Loop Termination Condition', breakCondition: 'Loop Termination Condition',
breakConditionTip: 'Only variables within loops with termination conditions and conversation variables can be referenced.', breakConditionTip: 'Only variables within loops with termination conditions and conversation variables can be referenced.',
loopMaxCount: 'Maximum Loop Count', loopMaxCount: 'Maximum Loop Count',

View File

@ -739,6 +739,9 @@ const translation = {
loop_one: '{{count}} 个循环', loop_one: '{{count}} 个循环',
loop_other: '{{count}} 个循环', loop_other: '{{count}} 个循环',
currentLoop: '当前循环', currentLoop: '当前循环',
comma: '',
error_one: '{{count}}个失败',
error_other: '{{count}}个失败',
breakCondition: '循环终止条件', breakCondition: '循环终止条件',
breakConditionTip: '支持引用终止条件循环内的变量和会话变量。', breakConditionTip: '支持引用终止条件循环内的变量和会话变量。',
loopMaxCount: '最大循环次数', loopMaxCount: '最大循环次数',