mirror of
http://112.124.100.131/huang.ze/ebiz-dify-ai.git
synced 2025-12-10 19:36:53 +08:00
Feature/newnew workflow loop node (#14863)
Co-authored-by: arkunzz <4873204@qq.com>
This commit is contained in:
@@ -7,6 +7,7 @@ import { useBoolean } from 'ahooks'
|
||||
import type {
|
||||
AgentLogItemWithChildren,
|
||||
IterationDurationMap,
|
||||
LoopDurationMap,
|
||||
NodeTracing,
|
||||
} from '@/types/workflow'
|
||||
|
||||
@@ -33,6 +34,18 @@ export const useLogs = () => {
|
||||
setIterationResultDurationMap(iterDurationMap)
|
||||
}, [setShowIteratingDetailTrue, setIterationResultList, setIterationResultDurationMap])
|
||||
|
||||
const [showLoopingDetail, {
|
||||
setTrue: setShowLoopingDetailTrue,
|
||||
setFalse: setShowLoopingDetailFalse,
|
||||
}] = useBoolean(false)
|
||||
const [loopResultList, setLoopResultList] = useState<NodeTracing[][]>([])
|
||||
const [loopResultDurationMap, setLoopResultDurationMap] = useState<LoopDurationMap>({})
|
||||
const handleShowLoopResultList = useCallback((detail: NodeTracing[][], loopDurationMap: LoopDurationMap) => {
|
||||
setShowLoopingDetailTrue()
|
||||
setLoopResultList(detail)
|
||||
setLoopResultDurationMap(loopDurationMap)
|
||||
}, [setShowLoopingDetailTrue, setLoopResultList, setLoopResultDurationMap])
|
||||
|
||||
const [agentOrToolLogItemStack, setAgentOrToolLogItemStack] = useState<AgentLogItemWithChildren[]>([])
|
||||
const agentOrToolLogItemStackRef = useRef(agentOrToolLogItemStack)
|
||||
const [agentOrToolLogListMap, setAgentOrToolLogListMap] = useState<Record<string, AgentLogItemWithChildren[]>>({})
|
||||
@@ -64,7 +77,7 @@ export const useLogs = () => {
|
||||
}, [setAgentOrToolLogItemStack, setAgentOrToolLogListMap])
|
||||
|
||||
return {
|
||||
showSpecialResultPanel: showRetryDetail || showIteratingDetail || !!agentOrToolLogItemStack.length,
|
||||
showSpecialResultPanel: showRetryDetail || showIteratingDetail || showLoopingDetail || !!agentOrToolLogItemStack.length,
|
||||
showRetryDetail,
|
||||
setShowRetryDetailTrue,
|
||||
setShowRetryDetailFalse,
|
||||
@@ -81,6 +94,15 @@ export const useLogs = () => {
|
||||
setIterationResultDurationMap,
|
||||
handleShowIterationResultList,
|
||||
|
||||
showLoopingDetail,
|
||||
setShowLoopingDetailTrue,
|
||||
setShowLoopingDetailFalse,
|
||||
loopResultList,
|
||||
setLoopResultList,
|
||||
loopResultDurationMap,
|
||||
setLoopResultDurationMap,
|
||||
handleShowLoopResultList,
|
||||
|
||||
agentOrToolLogItemStack,
|
||||
agentOrToolLogListMap,
|
||||
handleShowAgentOrToolLog,
|
||||
|
||||
2
web/app/components/workflow/run/loop-log/index.tsx
Normal file
2
web/app/components/workflow/run/loop-log/index.tsx
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as LoopLogTrigger } from './loop-log-trigger'
|
||||
export { default as LoopResultPanel } from './loop-result-panel'
|
||||
@@ -0,0 +1,57 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiArrowRightSLine } from '@remixicon/react'
|
||||
import Button from '@/app/components/base/button'
|
||||
import type {
|
||||
LoopDurationMap,
|
||||
NodeTracing,
|
||||
} from '@/types/workflow'
|
||||
import { Loop } from '@/app/components/base/icons/src/vender/workflow'
|
||||
|
||||
type LoopLogTriggerProps = {
|
||||
nodeInfo: NodeTracing
|
||||
onShowLoopResultList: (loopResultList: NodeTracing[][], loopResultDurationMap: LoopDurationMap) => void
|
||||
}
|
||||
const LoopLogTrigger = ({
|
||||
nodeInfo,
|
||||
onShowLoopResultList,
|
||||
}: LoopLogTriggerProps) => {
|
||||
const { t } = useTranslation()
|
||||
const getErrorCount = (details: NodeTracing[][] | undefined) => {
|
||||
if (!details || details.length === 0)
|
||||
return 0
|
||||
|
||||
return details.reduce((acc, loop) => {
|
||||
if (loop.some(item => item.status === 'failed'))
|
||||
acc++
|
||||
return acc
|
||||
}, 0)
|
||||
}
|
||||
const getCount = (loop_curr_length: number | undefined, loop_length: number) => {
|
||||
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 || {})
|
||||
}
|
||||
return (
|
||||
<Button
|
||||
className='flex items-center w-full self-stretch gap-2 px-3 py-2 bg-components-button-tertiary-bg-hover hover:bg-components-button-tertiary-bg-hover rounded-lg cursor-pointer border-none'
|
||||
onClick={handleOnShowLoopDetail}
|
||||
>
|
||||
<Loop className='w-4 h-4 text-components-button-tertiary-text shrink-0' />
|
||||
<div className='flex-1 text-left system-sm-medium text-components-button-tertiary-text'>{t('workflow.nodes.loop.loop', { count: getCount(nodeInfo.details?.length, nodeInfo.metadata?.loop_length) })}{getErrorCount(nodeInfo.details) > 0 && (
|
||||
<>
|
||||
{t('workflow.nodes.loop.comma')}
|
||||
{t('workflow.nodes.loop.error', { count: getErrorCount(nodeInfo.details) })}
|
||||
</>
|
||||
)}</div>
|
||||
<RiArrowRightSLine className='w-4 h-4 text-components-button-tertiary-text shrink-0' />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
export default LoopLogTrigger
|
||||
128
web/app/components/workflow/run/loop-log/loop-result-panel.tsx
Normal file
128
web/app/components/workflow/run/loop-log/loop-result-panel.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiArrowLeftLine,
|
||||
RiArrowRightSLine,
|
||||
RiErrorWarningLine,
|
||||
RiLoader2Line,
|
||||
} from '@remixicon/react'
|
||||
import { NodeRunningStatus } from '@/app/components/workflow/types'
|
||||
import TracingPanel from '@/app/components/workflow/run/tracing-panel'
|
||||
import { Loop } from '@/app/components/base/icons/src/vender/workflow'
|
||||
import cn from '@/utils/classnames'
|
||||
import type { LoopDurationMap, NodeTracing } from '@/types/workflow'
|
||||
const i18nPrefix = 'workflow.singleRun'
|
||||
|
||||
type Props = {
|
||||
list: NodeTracing[][]
|
||||
onBack: () => void
|
||||
loopDurationMap?: LoopDurationMap
|
||||
}
|
||||
|
||||
const LoopResultPanel: FC<Props> = ({
|
||||
list,
|
||||
onBack,
|
||||
loopDurationMap,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [expandedLoops, setExpandedLoops] = useState<Record<number, boolean>>({})
|
||||
|
||||
const toggleLoop = useCallback((index: number) => {
|
||||
setExpandedLoops(prev => ({
|
||||
...prev,
|
||||
[index]: !prev[index],
|
||||
}))
|
||||
}, [])
|
||||
|
||||
const countLoopDuration = (loop: NodeTracing[], loopDurationMap: LoopDurationMap): string => {
|
||||
const loopRunIndex = loop[0]?.execution_metadata?.loop_index as number
|
||||
const loopRunId = loop[0]?.execution_metadata?.parallel_mode_run_id
|
||||
const loopItem = loopDurationMap[loopRunId || loopRunIndex]
|
||||
const duration = loopItem
|
||||
return `${(duration && duration > 0.01) ? duration.toFixed(2) : 0.01}s`
|
||||
}
|
||||
|
||||
const loopStatusShow = (index: number, loop: NodeTracing[], loopDurationMap?: LoopDurationMap) => {
|
||||
const hasFailed = loop.some(item => item.status === NodeRunningStatus.Failed)
|
||||
const isRunning = loop.some(item => item.status === NodeRunningStatus.Running)
|
||||
const hasDurationMap = loopDurationMap && Object.keys(loopDurationMap).length !== 0
|
||||
|
||||
if (hasFailed)
|
||||
return <RiErrorWarningLine className='w-4 h-4 text-text-destructive' />
|
||||
|
||||
if (isRunning)
|
||||
return <RiLoader2Line className='w-3.5 h-3.5 text-primary-600 animate-spin' />
|
||||
|
||||
return (
|
||||
<>
|
||||
{hasDurationMap && (
|
||||
<div className='system-xs-regular text-text-tertiary'>
|
||||
{countLoopDuration(loop, loopDurationMap)}
|
||||
</div>
|
||||
)}
|
||||
<RiArrowRightSLine
|
||||
className={cn(
|
||||
'w-4 h-4 text-text-tertiary transition-transform duration-200 flex-shrink-0',
|
||||
expandedLoops[index] && 'transform rotate-90',
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='bg-components-panel-bg'>
|
||||
<div
|
||||
className='flex items-center px-4 h-8 text-text-accent-secondary cursor-pointer border-b-[0.5px] border-b-divider-regular'
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
e.nativeEvent.stopImmediatePropagation()
|
||||
onBack()
|
||||
}}
|
||||
>
|
||||
<RiArrowLeftLine className='mr-1 w-4 h-4' />
|
||||
<div className='system-sm-medium'>{t(`${i18nPrefix}.back`)}</div>
|
||||
</div>
|
||||
{/* List */}
|
||||
<div className='p-2 bg-components-panel-bg'>
|
||||
{list.map((loop, index) => (
|
||||
<div key={index} className={cn('mb-1 overflow-hidden rounded-xl bg-background-section-burn border-none')}>
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center justify-between w-full px-3 cursor-pointer',
|
||||
expandedLoops[index] ? 'pt-3 pb-2' : 'py-3',
|
||||
'rounded-xl text-left',
|
||||
)}
|
||||
onClick={() => toggleLoop(index)}
|
||||
>
|
||||
<div className={cn('flex items-center gap-2 flex-grow')}>
|
||||
<div className='flex items-center justify-center w-4 h-4 rounded-[5px] border-divider-subtle bg-util-colors-cyan-cyan-500 shrink-0'>
|
||||
<Loop className='w-3 h-3 text-text-primary-on-surface' />
|
||||
</div>
|
||||
<span className='system-sm-semibold-uppercase text-text-primary grow'>
|
||||
{t(`${i18nPrefix}.loop`)} {index + 1}
|
||||
</span>
|
||||
{loopStatusShow(index, loop, loopDurationMap)}
|
||||
</div>
|
||||
</div>
|
||||
{expandedLoops[index] && <div
|
||||
className="grow h-px bg-divider-subtle"
|
||||
></div>}
|
||||
<div className={cn(
|
||||
'overflow-hidden transition-all duration-200',
|
||||
expandedLoops[index] ? 'max-h-[1000px] opacity-100' : 'max-h-0 opacity-0',
|
||||
)}>
|
||||
<TracingPanel
|
||||
list={loop}
|
||||
className='bg-background-section-burn'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(LoopResultPanel)
|
||||
122
web/app/components/workflow/run/loop-result-panel.tsx
Normal file
122
web/app/components/workflow/run/loop-result-panel.tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiArrowRightSLine,
|
||||
RiCloseLine,
|
||||
} from '@remixicon/react'
|
||||
import { ArrowNarrowLeft } from '../../base/icons/src/vender/line/arrows'
|
||||
import TracingPanel from './tracing-panel'
|
||||
import { Loop } from '@/app/components/base/icons/src/vender/workflow'
|
||||
import cn from '@/utils/classnames'
|
||||
import type { NodeTracing } from '@/types/workflow'
|
||||
|
||||
const i18nPrefix = 'workflow.singleRun'
|
||||
|
||||
type Props = {
|
||||
list: NodeTracing[][]
|
||||
onHide: () => void
|
||||
onBack: () => void
|
||||
noWrap?: boolean
|
||||
}
|
||||
|
||||
const LoopResultPanel: FC<Props> = ({
|
||||
list,
|
||||
onHide,
|
||||
onBack,
|
||||
noWrap,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [expandedLoops, setExpandedLoops] = useState<Record<number, boolean>>([])
|
||||
|
||||
const toggleLoop = useCallback((index: number) => {
|
||||
setExpandedLoops(prev => ({
|
||||
...prev,
|
||||
[index]: !prev[index],
|
||||
}))
|
||||
}, [])
|
||||
|
||||
const main = (
|
||||
<>
|
||||
<div className={cn(!noWrap && 'shrink-0 ', 'px-4 pt-3')}>
|
||||
<div className='shrink-0 flex justify-between items-center h-8'>
|
||||
<div className='system-xl-semibold text-text-primary truncate'>
|
||||
{t(`${i18nPrefix}.testRunLoop`)}
|
||||
</div>
|
||||
<div className='ml-2 shrink-0 p-1 cursor-pointer' onClick={onHide}>
|
||||
<RiCloseLine className='w-4 h-4 text-text-tertiary' />
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex items-center py-2 space-x-1 text-text-accent-secondary cursor-pointer' onClick={onBack}>
|
||||
<ArrowNarrowLeft className='w-4 h-4' />
|
||||
<div className='system-sm-medium'>{t(`${i18nPrefix}.back`)}</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* List */}
|
||||
<div className={cn(!noWrap ? 'flex-grow overflow-auto' : 'max-h-full', 'p-2 bg-components-panel-bg')}>
|
||||
{list.map((loop, index) => (
|
||||
<div key={index} className={cn('mb-1 overflow-hidden rounded-xl bg-background-section-burn border-none')}>
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center justify-between w-full px-3 cursor-pointer',
|
||||
expandedLoops[index] ? 'pt-3 pb-2' : 'py-3',
|
||||
'rounded-xl text-left',
|
||||
)}
|
||||
onClick={() => toggleLoop(index)}
|
||||
>
|
||||
<div className={cn('flex items-center gap-2 flex-grow')}>
|
||||
<div className='flex items-center justify-center w-4 h-4 rounded-[5px] border-divider-subtle bg-util-colors-cyan-cyan-500 shrink-0'>
|
||||
<Loop className='w-3 h-3 text-text-primary-on-surface' />
|
||||
</div>
|
||||
<span className='system-sm-semibold-uppercase text-text-primary grow'>
|
||||
{t(`${i18nPrefix}.loop`)} {index + 1}
|
||||
</span>
|
||||
<RiArrowRightSLine className={cn(
|
||||
'w-4 h-4 text-text-tertiary transition-transform duration-200 flex-shrink-0',
|
||||
expandedLoops[index] && 'transform rotate-90',
|
||||
)} />
|
||||
</div>
|
||||
</div>
|
||||
{expandedLoops[index] && <div
|
||||
className="grow h-px bg-divider-subtle"
|
||||
></div>}
|
||||
<div className={cn(
|
||||
'overflow-hidden transition-all duration-200',
|
||||
expandedLoops[index] ? 'max-h-[1000px] opacity-100' : 'max-h-0 opacity-0',
|
||||
)}>
|
||||
<TracingPanel
|
||||
list={loop}
|
||||
className='bg-background-section-burn'
|
||||
/>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
const handleNotBubble = useCallback((e: React.MouseEvent) => {
|
||||
// if not do this, it will trigger the message log modal disappear(useClickAway)
|
||||
e.stopPropagation()
|
||||
e.nativeEvent.stopImmediatePropagation()
|
||||
}, [])
|
||||
|
||||
if (noWrap)
|
||||
return main
|
||||
|
||||
return (
|
||||
<div
|
||||
className='absolute inset-0 z-10 rounded-2xl pt-10'
|
||||
style={{
|
||||
backgroundColor: 'rgba(16, 24, 40, 0.20)',
|
||||
}}
|
||||
onClick={handleNotBubble}
|
||||
>
|
||||
<div className='h-full rounded-2xl bg-components-panel-bg flex flex-col'>
|
||||
{main}
|
||||
</div>
|
||||
</div >
|
||||
)
|
||||
}
|
||||
export default React.memo(LoopResultPanel)
|
||||
@@ -13,6 +13,7 @@ import BlockIcon from '../block-icon'
|
||||
import { BlockEnum } from '../types'
|
||||
import { RetryLogTrigger } from './retry-log'
|
||||
import { IterationLogTrigger } from './iteration-log'
|
||||
import { LoopLogTrigger } from './loop-log'
|
||||
import { AgentLogTrigger } from './agent-log'
|
||||
import cn from '@/utils/classnames'
|
||||
import StatusContainer from '@/app/components/workflow/run/status-container'
|
||||
@@ -21,6 +22,7 @@ import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
|
||||
import type {
|
||||
AgentLogItemWithChildren,
|
||||
IterationDurationMap,
|
||||
LoopDurationMap,
|
||||
NodeTracing,
|
||||
} from '@/types/workflow'
|
||||
import ErrorHandleTip from '@/app/components/workflow/nodes/_base/components/error-handle/error-handle-tip'
|
||||
@@ -33,9 +35,11 @@ type Props = {
|
||||
hideInfo?: boolean
|
||||
hideProcessDetail?: boolean
|
||||
onShowIterationDetail?: (detail: NodeTracing[][], iterDurationMap: IterationDurationMap) => void
|
||||
onShowLoopDetail?: (detail: NodeTracing[][], loopDurationMap: LoopDurationMap) => void
|
||||
onShowRetryDetail?: (detail: NodeTracing[]) => void
|
||||
onShowAgentOrToolLog?: (detail?: AgentLogItemWithChildren) => void
|
||||
notShowIterationNav?: boolean
|
||||
notShowLoopNav?: boolean
|
||||
}
|
||||
|
||||
const NodePanel: FC<Props> = ({
|
||||
@@ -45,9 +49,11 @@ const NodePanel: FC<Props> = ({
|
||||
hideInfo = false,
|
||||
hideProcessDetail,
|
||||
onShowIterationDetail,
|
||||
onShowLoopDetail,
|
||||
onShowRetryDetail,
|
||||
onShowAgentOrToolLog,
|
||||
notShowIterationNav,
|
||||
notShowLoopNav,
|
||||
}) => {
|
||||
const [collapseState, doSetCollapseState] = useState<boolean>(true)
|
||||
const setCollapseState = useCallback((state: boolean) => {
|
||||
@@ -79,6 +85,7 @@ const NodePanel: FC<Props> = ({
|
||||
}, [nodeInfo.expand, setCollapseState])
|
||||
|
||||
const isIterationNode = nodeInfo.node_type === BlockEnum.Iteration && !!nodeInfo.details?.length
|
||||
const isLoopNode = nodeInfo.node_type === BlockEnum.Loop && !!nodeInfo.details?.length
|
||||
const isRetryNode = hasRetryNode(nodeInfo.node_type) && !!nodeInfo.retryDetail?.length
|
||||
const isAgentNode = nodeInfo.node_type === BlockEnum.Agent && !!nodeInfo.agentLog?.length
|
||||
const isToolNode = nodeInfo.node_type === BlockEnum.Tool && !!nodeInfo.agentLog?.length
|
||||
@@ -138,6 +145,13 @@ const NodePanel: FC<Props> = ({
|
||||
onShowIterationResultList={onShowIterationDetail}
|
||||
/>
|
||||
)}
|
||||
{/* The nav to the Loop detail */}
|
||||
{isLoopNode && !notShowLoopNav && onShowLoopDetail && (
|
||||
<LoopLogTrigger
|
||||
nodeInfo={nodeInfo}
|
||||
onShowLoopResultList={onShowLoopDetail}
|
||||
/>
|
||||
)}
|
||||
{isRetryNode && onShowRetryDetail && (
|
||||
<RetryLogTrigger
|
||||
nodeInfo={nodeInfo}
|
||||
|
||||
@@ -13,6 +13,7 @@ import type {
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { hasRetryNode } from '@/app/components/workflow/utils'
|
||||
import { IterationLogTrigger } from '@/app/components/workflow/run/iteration-log'
|
||||
import { LoopLogTrigger } from '@/app/components/workflow/run/loop-log'
|
||||
import { RetryLogTrigger } from '@/app/components/workflow/run/retry-log'
|
||||
import { AgentLogTrigger } from '@/app/components/workflow/run/agent-log'
|
||||
|
||||
@@ -33,6 +34,7 @@ type ResultPanelProps = {
|
||||
exceptionCounts?: number
|
||||
execution_metadata?: any
|
||||
handleShowIterationResultList?: (detail: NodeTracing[][], iterDurationMap: any) => void
|
||||
handleShowLoopResultList?: (detail: NodeTracing[][], loopDurationMap: any) => void
|
||||
onShowRetryDetail?: (detail: NodeTracing[]) => void
|
||||
handleShowAgentOrToolLog?: (detail?: AgentLogItemWithChildren) => void
|
||||
}
|
||||
@@ -53,11 +55,13 @@ const ResultPanel: FC<ResultPanelProps> = ({
|
||||
exceptionCounts,
|
||||
execution_metadata,
|
||||
handleShowIterationResultList,
|
||||
handleShowLoopResultList,
|
||||
onShowRetryDetail,
|
||||
handleShowAgentOrToolLog,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const isIterationNode = nodeInfo?.node_type === BlockEnum.Iteration && !!nodeInfo?.details?.length
|
||||
const isLoopNode = nodeInfo?.node_type === BlockEnum.Loop && !!nodeInfo?.details?.length
|
||||
const isRetryNode = hasRetryNode(nodeInfo?.node_type) && !!nodeInfo?.retryDetail?.length
|
||||
const isAgentNode = nodeInfo?.node_type === BlockEnum.Agent && !!nodeInfo?.agentLog?.length
|
||||
const isToolNode = nodeInfo?.node_type === BlockEnum.Tool && !!nodeInfo?.agentLog?.length
|
||||
@@ -82,6 +86,14 @@ const ResultPanel: FC<ResultPanelProps> = ({
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
isLoopNode && handleShowLoopResultList && (
|
||||
<LoopLogTrigger
|
||||
nodeInfo={nodeInfo}
|
||||
onShowLoopResultList={handleShowLoopResultList}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
isRetryNode && onShowRetryDetail && (
|
||||
<RetryLogTrigger
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { RetryResultPanel } from './retry-log'
|
||||
import { IterationResultPanel } from './iteration-log'
|
||||
import { LoopResultPanel } from './loop-log'
|
||||
import { AgentResultPanel } from './agent-log'
|
||||
import type {
|
||||
AgentLogItemWithChildren,
|
||||
IterationDurationMap,
|
||||
LoopDurationMap,
|
||||
NodeTracing,
|
||||
} from '@/types/workflow'
|
||||
|
||||
@@ -17,6 +19,11 @@ export type SpecialResultPanelProps = {
|
||||
iterationResultList?: NodeTracing[][]
|
||||
iterationResultDurationMap?: IterationDurationMap
|
||||
|
||||
showLoopingDetail?: boolean
|
||||
setShowLoopingDetailFalse?: () => void
|
||||
loopResultList?: NodeTracing[][]
|
||||
loopResultDurationMap?: LoopDurationMap
|
||||
|
||||
agentOrToolLogItemStack?: AgentLogItemWithChildren[]
|
||||
agentOrToolLogListMap?: Record<string, AgentLogItemWithChildren[]>
|
||||
handleShowAgentOrToolLog?: (detail?: AgentLogItemWithChildren) => void
|
||||
@@ -31,6 +38,11 @@ const SpecialResultPanel = ({
|
||||
iterationResultList,
|
||||
iterationResultDurationMap,
|
||||
|
||||
showLoopingDetail,
|
||||
setShowLoopingDetailFalse,
|
||||
loopResultList,
|
||||
loopResultDurationMap,
|
||||
|
||||
agentOrToolLogItemStack,
|
||||
agentOrToolLogListMap,
|
||||
handleShowAgentOrToolLog,
|
||||
@@ -57,6 +69,15 @@ const SpecialResultPanel = ({
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
showLoopingDetail && !!loopResultList?.length && setShowLoopingDetailFalse && (
|
||||
<LoopResultPanel
|
||||
list={loopResultList}
|
||||
onBack={setShowLoopingDetailFalse}
|
||||
loopDurationMap={loopResultDurationMap}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
!!agentOrToolLogItemStack?.length && agentOrToolLogListMap && handleShowAgentOrToolLog && (
|
||||
<AgentResultPanel
|
||||
|
||||
@@ -82,6 +82,12 @@ const TracingPanel: FC<TracingPanelProps> = ({
|
||||
iterationResultDurationMap,
|
||||
handleShowIterationResultList,
|
||||
|
||||
showLoopingDetail,
|
||||
setShowLoopingDetailFalse,
|
||||
loopResultList,
|
||||
loopResultDurationMap,
|
||||
handleShowLoopResultList,
|
||||
|
||||
agentOrToolLogItemStack,
|
||||
agentOrToolLogListMap,
|
||||
handleShowAgentOrToolLog,
|
||||
@@ -139,6 +145,7 @@ const TracingPanel: FC<TracingPanelProps> = ({
|
||||
<NodePanel
|
||||
nodeInfo={node!}
|
||||
onShowIterationDetail={handleShowIterationResultList}
|
||||
onShowLoopDetail={handleShowLoopResultList}
|
||||
onShowRetryDetail={handleShowRetryResultList}
|
||||
onShowAgentOrToolLog={handleShowAgentOrToolLog}
|
||||
hideInfo={hideNodeInfo}
|
||||
@@ -161,6 +168,11 @@ const TracingPanel: FC<TracingPanelProps> = ({
|
||||
iterationResultList={iterationResultList}
|
||||
iterationResultDurationMap={iterationResultDurationMap}
|
||||
|
||||
showLoopingDetail={showLoopingDetail}
|
||||
setShowLoopingDetailFalse={setShowLoopingDetailFalse}
|
||||
loopResultList={loopResultList}
|
||||
loopResultDurationMap={loopResultDurationMap}
|
||||
|
||||
agentOrToolLogItemStack={agentOrToolLogItemStack}
|
||||
agentOrToolLogListMap={agentOrToolLogListMap}
|
||||
handleShowAgentOrToolLog={handleShowAgentOrToolLog}
|
||||
|
||||
@@ -31,6 +31,16 @@ describe('parseDSL', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('should parse loop nodes correctly', () => {
|
||||
const dsl = '(loop, loopNode, plainNode1 -> plainNode2)'
|
||||
const result = parseDSL(dsl)
|
||||
expect(result).toEqual([
|
||||
{ id: 'loopNode', node_id: 'loopNode', title: 'loopNode', node_type: 'loop', execution_metadata: {}, status: 'succeeded' },
|
||||
{ id: 'plainNode1', node_id: 'plainNode1', title: 'plainNode1', execution_metadata: { loop_id: 'loopNode', loop_index: 0 }, status: 'succeeded' },
|
||||
{ id: 'plainNode2', node_id: 'plainNode2', title: 'plainNode2', execution_metadata: { loop_id: 'loopNode', loop_index: 0 }, status: 'succeeded' },
|
||||
])
|
||||
})
|
||||
|
||||
it('should parse parallel nodes correctly', () => {
|
||||
const dsl = '(parallel, parallelNode, nodeA, nodeB -> nodeC)'
|
||||
const result = parseDSL(dsl)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
type IterationInfo = { iterationId: string; iterationIndex: number }
|
||||
type NodePlain = { nodeType: 'plain'; nodeId: string; } & Partial<IterationInfo>
|
||||
type NodeComplex = { nodeType: string; nodeId: string; params: (NodePlain | (NodeComplex & Partial<IterationInfo>) | Node[] | number)[] } & Partial<IterationInfo>
|
||||
type LoopInfo = { loopId: string; loopIndex: number }
|
||||
type NodePlain = { nodeType: 'plain'; nodeId: string; } & (Partial<IterationInfo> & Partial<LoopInfo>)
|
||||
type NodeComplex = { nodeType: string; nodeId: string; params: (NodePlain | (NodeComplex & (Partial<IterationInfo> & Partial<LoopInfo>)) | Node[] | number)[] } & (Partial<IterationInfo> & Partial<LoopInfo>)
|
||||
type Node = NodePlain | NodeComplex
|
||||
|
||||
/**
|
||||
@@ -46,9 +47,10 @@ function parseTopLevelFlow(dsl: string): string[] {
|
||||
* If the node is complex (e.g., has parentheses), it extracts the node type, node ID, and parameters.
|
||||
* @param nodeStr - The node string to parse.
|
||||
* @param parentIterationId - The ID of the parent iteration node (if applicable).
|
||||
* @param parentLoopId - The ID of the parent loop node (if applicable).
|
||||
* @returns A parsed node object.
|
||||
*/
|
||||
function parseNode(nodeStr: string, parentIterationId?: string): Node {
|
||||
function parseNode(nodeStr: string, parentIterationId?: string, parentLoopId?: string): Node {
|
||||
// Check if the node is a complex node
|
||||
if (nodeStr.startsWith('(') && nodeStr.endsWith(')')) {
|
||||
const innerContent = nodeStr.slice(1, -1).trim() // Remove outer parentheses
|
||||
@@ -74,7 +76,7 @@ function parseNode(nodeStr: string, parentIterationId?: string): Node {
|
||||
|
||||
// Extract nodeType, nodeId, and params
|
||||
const [nodeType, nodeId, ...paramsRaw] = parts
|
||||
const params = parseParams(paramsRaw, nodeType === 'iteration' ? nodeId.trim() : parentIterationId)
|
||||
const params = parseParams(paramsRaw, nodeType === 'iteration' ? nodeId.trim() : parentIterationId, nodeType === 'loop' ? nodeId.trim() : parentLoopId)
|
||||
const complexNode = {
|
||||
nodeType: nodeType.trim(),
|
||||
nodeId: nodeId.trim(),
|
||||
@@ -84,6 +86,10 @@ function parseNode(nodeStr: string, parentIterationId?: string): Node {
|
||||
(complexNode as any).iterationId = parentIterationId;
|
||||
(complexNode as any).iterationIndex = 0 // Fixed as 0
|
||||
}
|
||||
if (parentLoopId) {
|
||||
(complexNode as any).loopId = parentLoopId;
|
||||
(complexNode as any).loopIndex = 0 // Fixed as 0
|
||||
}
|
||||
return complexNode
|
||||
}
|
||||
|
||||
@@ -93,6 +99,10 @@ function parseNode(nodeStr: string, parentIterationId?: string): Node {
|
||||
plainNode.iterationId = parentIterationId
|
||||
plainNode.iterationIndex = 0 // Fixed as 0
|
||||
}
|
||||
if (parentLoopId) {
|
||||
plainNode.loopId = parentLoopId
|
||||
plainNode.loopIndex = 0 // Fixed as 0
|
||||
}
|
||||
return plainNode
|
||||
}
|
||||
|
||||
@@ -101,18 +111,19 @@ function parseNode(nodeStr: string, parentIterationId?: string): Node {
|
||||
* Supports nested flows and complex sub-nodes.
|
||||
* Adds iteration-specific metadata recursively.
|
||||
* @param paramParts - The parameters string split by commas.
|
||||
* @param iterationId - The ID of the iteration node, if applicable.
|
||||
* @param parentIterationId - The ID of the parent iteration node (if applicable).
|
||||
* @param parentLoopId - The ID of the parent loop node (if applicable).
|
||||
* @returns An array of parsed parameters (plain nodes, nested nodes, or flows).
|
||||
*/
|
||||
function parseParams(paramParts: string[], iterationId?: string): (Node | Node[] | number)[] {
|
||||
function parseParams(paramParts: string[], parentIteration?: string, parentLoopId?: string): (Node | Node[] | number)[] {
|
||||
return paramParts.map((part) => {
|
||||
if (part.includes('->')) {
|
||||
// Parse as a flow and return an array of nodes
|
||||
return parseTopLevelFlow(part).map(node => parseNode(node, iterationId))
|
||||
return parseTopLevelFlow(part).map(node => parseNode(node, parentIteration || undefined, parentLoopId || undefined))
|
||||
}
|
||||
else if (part.startsWith('(')) {
|
||||
// Parse as a nested complex node
|
||||
return parseNode(part, iterationId)
|
||||
return parseNode(part, parentIteration || undefined, parentLoopId || undefined)
|
||||
}
|
||||
else if (!Number.isNaN(Number(part.trim()))) {
|
||||
// Parse as a numeric parameter
|
||||
@@ -120,7 +131,7 @@ function parseParams(paramParts: string[], iterationId?: string): (Node | Node[]
|
||||
}
|
||||
else {
|
||||
// Parse as a plain node
|
||||
return parseNode(part, iterationId)
|
||||
return parseNode(part, parentIteration || undefined, parentLoopId || undefined)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -153,7 +164,7 @@ function convertPlainNode(node: Node): NodeData[] {
|
||||
* Converts a retry node to node data.
|
||||
*/
|
||||
function convertRetryNode(node: Node): NodeData[] {
|
||||
const { nodeId, iterationId, iterationIndex, params } = node as NodeComplex
|
||||
const { nodeId, iterationId, iterationIndex, loopId, loopIndex, params } = node as NodeComplex
|
||||
const retryCount = params ? Number.parseInt(params[0] as unknown as string, 10) : 0
|
||||
const result: NodeData[] = [
|
||||
{
|
||||
@@ -173,6 +184,9 @@ function convertRetryNode(node: Node): NodeData[] {
|
||||
execution_metadata: iterationId ? {
|
||||
iteration_id: iterationId,
|
||||
iteration_index: iterationIndex || 0,
|
||||
} : loopId ? {
|
||||
loop_id: loopId,
|
||||
loop_index: loopIndex || 0,
|
||||
} : {},
|
||||
status: 'retry',
|
||||
})
|
||||
@@ -216,6 +230,41 @@ function convertIterationNode(node: Node): NodeData[] {
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an loop node to node data.
|
||||
*/
|
||||
function convertLoopNode(node: Node): NodeData[] {
|
||||
const { nodeId, params } = node as NodeComplex
|
||||
const result: NodeData[] = [
|
||||
{
|
||||
id: nodeId,
|
||||
node_id: nodeId,
|
||||
title: nodeId,
|
||||
node_type: 'loop',
|
||||
status: 'succeeded',
|
||||
execution_metadata: {},
|
||||
},
|
||||
]
|
||||
|
||||
params?.forEach((param: any) => {
|
||||
if (Array.isArray(param)) {
|
||||
param.forEach((childNode: Node) => {
|
||||
const childData = convertToNodeData([childNode])
|
||||
childData.forEach((data) => {
|
||||
data.execution_metadata = {
|
||||
...data.execution_metadata,
|
||||
loop_id: nodeId,
|
||||
loop_index: 0,
|
||||
}
|
||||
})
|
||||
result.push(...childData)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a parallel node to node data.
|
||||
*/
|
||||
@@ -290,6 +339,9 @@ function convertToNodeData(nodes: Node[], parentParallelId?: string, parentStart
|
||||
case 'iteration':
|
||||
result.push(...convertIterationNode(node))
|
||||
break
|
||||
case 'loop':
|
||||
result.push(...convertLoopNode(node))
|
||||
break
|
||||
case 'parallel':
|
||||
result.push(...convertParallelNode(node, parentParallelId, parentStartNodeId))
|
||||
break
|
||||
|
||||
@@ -1,9 +1,80 @@
|
||||
import type { NodeTracing } from '@/types/workflow'
|
||||
import formatIterationNode from './iteration'
|
||||
import { addChildrenToIterationNode } from './iteration'
|
||||
import { addChildrenToLoopNode } from './loop'
|
||||
import formatParallelNode from './parallel'
|
||||
import formatRetryNode from './retry'
|
||||
import formatAgentNode from './agent'
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
import { BlockEnum } from '../../../types'
|
||||
|
||||
const formatIterationAndLoopNode = (list: NodeTracing[], t: any) => {
|
||||
const clonedList = cloneDeep(list)
|
||||
|
||||
// Identify all loop and iteration nodes
|
||||
const loopNodeIds = clonedList
|
||||
.filter(item => item.node_type === BlockEnum.Loop)
|
||||
.map(item => item.node_id)
|
||||
|
||||
const iterationNodeIds = clonedList
|
||||
.filter(item => item.node_type === BlockEnum.Iteration)
|
||||
.map(item => item.node_id)
|
||||
|
||||
// Identify all child nodes for both loop and iteration
|
||||
const loopChildrenNodeIds = clonedList
|
||||
.filter(item => item.execution_metadata?.loop_id && loopNodeIds.includes(item.execution_metadata.loop_id))
|
||||
.map(item => item.node_id)
|
||||
|
||||
const iterationChildrenNodeIds = clonedList
|
||||
.filter(item => item.execution_metadata?.iteration_id && iterationNodeIds.includes(item.execution_metadata.iteration_id))
|
||||
.map(item => item.node_id)
|
||||
|
||||
// Filter out child nodes as they will be included in their parent nodes
|
||||
const result = clonedList
|
||||
.filter(item => !loopChildrenNodeIds.includes(item.node_id) && !iterationChildrenNodeIds.includes(item.node_id))
|
||||
.map((item) => {
|
||||
// Process Loop nodes
|
||||
if (item.node_type === BlockEnum.Loop) {
|
||||
const childrenNodes = clonedList.filter(child => child.execution_metadata?.loop_id === item.node_id)
|
||||
const error = childrenNodes.find(child => child.status === 'failed')
|
||||
if (error) {
|
||||
item.status = 'failed'
|
||||
item.error = error.error
|
||||
}
|
||||
const addedChildrenList = addChildrenToLoopNode(item, childrenNodes)
|
||||
|
||||
// Handle parallel nodes in loop node
|
||||
if (addedChildrenList.details && addedChildrenList.details.length > 0) {
|
||||
addedChildrenList.details = addedChildrenList.details.map((row) => {
|
||||
return formatParallelNode(row, t)
|
||||
})
|
||||
}
|
||||
return addedChildrenList
|
||||
}
|
||||
|
||||
// Process Iteration nodes
|
||||
if (item.node_type === BlockEnum.Iteration) {
|
||||
const childrenNodes = clonedList.filter(child => child.execution_metadata?.iteration_id === item.node_id)
|
||||
const error = childrenNodes.find(child => child.status === 'failed')
|
||||
if (error) {
|
||||
item.status = 'failed'
|
||||
item.error = error.error
|
||||
}
|
||||
const addedChildrenList = addChildrenToIterationNode(item, childrenNodes)
|
||||
|
||||
// Handle parallel nodes in iteration node
|
||||
if (addedChildrenList.details && addedChildrenList.details.length > 0) {
|
||||
addedChildrenList.details = addedChildrenList.details.map((row) => {
|
||||
return formatParallelNode(row, t)
|
||||
})
|
||||
}
|
||||
return addedChildrenList
|
||||
}
|
||||
|
||||
return item
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
const formatToTracingNodeList = (list: NodeTracing[], t: any) => {
|
||||
const allItems = cloneDeep([...list]).sort((a, b) => a.index - b.index)
|
||||
@@ -14,8 +85,8 @@ const formatToTracingNodeList = (list: NodeTracing[], t: any) => {
|
||||
const formattedAgentList = formatAgentNode(allItems)
|
||||
const formattedRetryList = formatRetryNode(formattedAgentList) // retry one node
|
||||
// would change the structure of the list. Iteration and parallel can include each other.
|
||||
const formattedIterationList = formatIterationNode(formattedRetryList, t)
|
||||
const formattedParallelList = formatParallelNode(formattedIterationList, t)
|
||||
const formattedLoopAndIterationList = formatIterationAndLoopNode(formattedRetryList, t)
|
||||
const formattedParallelList = formatParallelNode(formattedLoopAndIterationList, t)
|
||||
|
||||
const result = formattedParallelList
|
||||
// console.log(allItems)
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import type { NodeTracing } from '@/types/workflow'
|
||||
import formatParallelNode from '../parallel'
|
||||
function addChildrenToIterationNode(iterationNode: NodeTracing, childrenNodes: NodeTracing[]): NodeTracing {
|
||||
|
||||
export function addChildrenToIterationNode(iterationNode: NodeTracing, childrenNodes: NodeTracing[]): NodeTracing {
|
||||
const details: NodeTracing[][] = []
|
||||
childrenNodes.forEach((item, index) => {
|
||||
if (!item.execution_metadata) return
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import format from '.'
|
||||
import graphToLogStruct from '../graph-to-log-struct'
|
||||
|
||||
describe('loop', () => {
|
||||
const list = graphToLogStruct('start -> (loop, loopNode, plainNode1 -> plainNode2)')
|
||||
const [startNode, loopNode, ...loops] = list
|
||||
const result = format(list as any, () => { })
|
||||
test('result should have no nodes in loop node', () => {
|
||||
expect((result as any).find((item: any) => !!item.execution_metadata?.loop_id)).toBeUndefined()
|
||||
})
|
||||
test('loop should put nodes in details', () => {
|
||||
expect(result as any).toEqual([
|
||||
startNode,
|
||||
{
|
||||
...loopNode,
|
||||
details: [
|
||||
[loops[0], loops[1]],
|
||||
],
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,56 @@
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import type { NodeTracing } from '@/types/workflow'
|
||||
import formatParallelNode from '../parallel'
|
||||
|
||||
export function addChildrenToLoopNode(loopNode: NodeTracing, childrenNodes: NodeTracing[]): NodeTracing {
|
||||
const details: NodeTracing[][] = []
|
||||
childrenNodes.forEach((item) => {
|
||||
if (!item.execution_metadata) return
|
||||
const { parallel_mode_run_id, loop_index = 0 } = item.execution_metadata
|
||||
const runIndex: number = (parallel_mode_run_id || loop_index) as number
|
||||
if (!details[runIndex])
|
||||
details[runIndex] = []
|
||||
|
||||
details[runIndex].push(item)
|
||||
})
|
||||
return {
|
||||
...loopNode,
|
||||
details,
|
||||
}
|
||||
}
|
||||
|
||||
const format = (list: NodeTracing[], t: any): NodeTracing[] => {
|
||||
const loopNodeIds = list
|
||||
.filter(item => item.node_type === BlockEnum.Loop)
|
||||
.map(item => item.node_id)
|
||||
const loopChildrenNodeIds = list
|
||||
.filter(item => item.execution_metadata?.loop_id && loopNodeIds.includes(item.execution_metadata.loop_id))
|
||||
.map(item => item.node_id)
|
||||
// move loop children nodes to loop node's details field
|
||||
const result = list
|
||||
.filter(item => !loopChildrenNodeIds.includes(item.node_id))
|
||||
.map((item) => {
|
||||
if (item.node_type === BlockEnum.Loop) {
|
||||
const childrenNodes = list.filter(child => child.execution_metadata?.loop_id === item.node_id)
|
||||
const error = childrenNodes.find(child => child.status === 'failed')
|
||||
if (error) {
|
||||
item.status = 'failed'
|
||||
item.error = error.error
|
||||
}
|
||||
const addedChildrenList = addChildrenToLoopNode(item, childrenNodes)
|
||||
// handle parallel node in loop node
|
||||
if (addedChildrenList.details && addedChildrenList.details.length > 0) {
|
||||
addedChildrenList.details = addedChildrenList.details.map((row) => {
|
||||
return formatParallelNode(row, t)
|
||||
})
|
||||
}
|
||||
return addedChildrenList
|
||||
}
|
||||
|
||||
return item
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export default format
|
||||
@@ -12,6 +12,7 @@ const format = (list: NodeTracing[]): NodeTracing[] => {
|
||||
}).map((item) => {
|
||||
const { execution_metadata } = item
|
||||
const isInIteration = !!execution_metadata?.iteration_id
|
||||
const isInLoop = !!execution_metadata?.loop_id
|
||||
const nodeId = item.node_id
|
||||
const isRetryBelongNode = retryNodeIds.includes(nodeId)
|
||||
|
||||
@@ -19,11 +20,18 @@ const format = (list: NodeTracing[]): NodeTracing[] => {
|
||||
return {
|
||||
...item,
|
||||
retryDetail: retryNodes.filter((node) => {
|
||||
if (!isInIteration)
|
||||
if (!isInIteration && !isInLoop)
|
||||
return node.node_id === nodeId
|
||||
|
||||
// retry node in iteration
|
||||
return node.node_id === nodeId && node.execution_metadata?.iteration_index === execution_metadata?.iteration_index
|
||||
if (isInIteration)
|
||||
return node.node_id === nodeId && node.execution_metadata?.iteration_index === execution_metadata?.iteration_index
|
||||
|
||||
// retry node in loop
|
||||
if (isInLoop)
|
||||
return node.node_id === nodeId && node.execution_metadata?.loop_index === execution_metadata?.loop_index
|
||||
|
||||
return false
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user