mirror of
http://112.124.100.131/huang.ze/ebiz-dify-ai.git
synced 2025-12-24 18:23:07 +08:00
Introduce Plugins (#13836)
Signed-off-by: yihong0618 <zouzou0208@gmail.com> Signed-off-by: -LAN- <laipz8200@outlook.com> Signed-off-by: xhe <xw897002528@gmail.com> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: takatost <takatost@gmail.com> Co-authored-by: kurokobo <kuro664@gmail.com> Co-authored-by: Novice Lee <novicelee@NoviPro.local> Co-authored-by: zxhlyh <jasonapring2015@outlook.com> Co-authored-by: AkaraChen <akarachen@outlook.com> Co-authored-by: Yi <yxiaoisme@gmail.com> Co-authored-by: Joel <iamjoel007@gmail.com> Co-authored-by: JzoNg <jzongcode@gmail.com> Co-authored-by: twwu <twwu@dify.ai> Co-authored-by: Hiroshi Fujita <fujita-h@users.noreply.github.com> Co-authored-by: AkaraChen <85140972+AkaraChen@users.noreply.github.com> Co-authored-by: NFish <douxc512@gmail.com> Co-authored-by: Wu Tianwei <30284043+WTW0313@users.noreply.github.com> Co-authored-by: 非法操作 <hjlarry@163.com> Co-authored-by: Novice <857526207@qq.com> Co-authored-by: Hiroki Nagai <82458324+nagaihiroki-git@users.noreply.github.com> Co-authored-by: Gen Sato <52241300+halogen22@users.noreply.github.com> Co-authored-by: eux <euxuuu@gmail.com> Co-authored-by: huangzhuo1949 <167434202+huangzhuo1949@users.noreply.github.com> Co-authored-by: huangzhuo <huangzhuo1@xiaomi.com> Co-authored-by: lotsik <lotsik@mail.ru> Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com> Co-authored-by: nite-knite <nkCoding@gmail.com> Co-authored-by: Jyong <76649700+JohnJyong@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: gakkiyomi <gakkiyomi@aliyun.com> Co-authored-by: CN-P5 <heibai2006@gmail.com> Co-authored-by: CN-P5 <heibai2006@qq.com> Co-authored-by: Chuehnone <1897025+chuehnone@users.noreply.github.com> Co-authored-by: yihong <zouzou0208@gmail.com> Co-authored-by: Kevin9703 <51311316+Kevin9703@users.noreply.github.com> Co-authored-by: -LAN- <laipz8200@outlook.com> Co-authored-by: Boris Feld <lothiraldan@gmail.com> Co-authored-by: mbo <himabo@gmail.com> Co-authored-by: mabo <mabo@aeyes.ai> Co-authored-by: Warren Chen <warren.chen830@gmail.com> Co-authored-by: JzoNgKVO <27049666+JzoNgKVO@users.noreply.github.com> Co-authored-by: jiandanfeng <chenjh3@wangsu.com> Co-authored-by: zhu-an <70234959+xhdd123321@users.noreply.github.com> Co-authored-by: zhaoqingyu.1075 <zhaoqingyu.1075@bytedance.com> Co-authored-by: 海狸大師 <86974027+yenslife@users.noreply.github.com> Co-authored-by: Xu Song <xusong.vip@gmail.com> Co-authored-by: rayshaw001 <396301947@163.com> Co-authored-by: Ding Jiatong <dingjiatong@gmail.com> Co-authored-by: Bowen Liang <liangbowen@gf.com.cn> Co-authored-by: JasonVV <jasonwangiii@outlook.com> Co-authored-by: le0zh <newlight@qq.com> Co-authored-by: zhuxinliang <zhuxinliang@didiglobal.com> Co-authored-by: k-zaku <zaku99@outlook.jp> Co-authored-by: luckylhb90 <luckylhb90@gmail.com> Co-authored-by: hobo.l <hobo.l@binance.com> Co-authored-by: jiangbo721 <365065261@qq.com> Co-authored-by: 刘江波 <jiangbo721@163.com> Co-authored-by: Shun Miyazawa <34241526+miya@users.noreply.github.com> Co-authored-by: EricPan <30651140+Egfly@users.noreply.github.com> Co-authored-by: crazywoola <427733928@qq.com> Co-authored-by: sino <sino2322@gmail.com> Co-authored-by: Jhvcc <37662342+Jhvcc@users.noreply.github.com> Co-authored-by: lowell <lowell.hu@zkteco.in> Co-authored-by: Boris Polonsky <BorisPolonsky@users.noreply.github.com> Co-authored-by: Ademílson Tonato <ademilsonft@outlook.com> Co-authored-by: Ademílson Tonato <ademilson.tonato@refurbed.com> Co-authored-by: IWAI, Masaharu <iwaim.sub@gmail.com> Co-authored-by: Yueh-Po Peng (Yabi) <94939112+y10ab1@users.noreply.github.com> Co-authored-by: Jason <ggbbddjm@gmail.com> Co-authored-by: Xin Zhang <sjhpzx@gmail.com> Co-authored-by: yjc980121 <3898524+yjc980121@users.noreply.github.com> Co-authored-by: heyszt <36215648+hieheihei@users.noreply.github.com> Co-authored-by: Abdullah AlOsaimi <osaimiacc@gmail.com> Co-authored-by: Abdullah AlOsaimi <189027247+osaimi@users.noreply.github.com> Co-authored-by: Yingchun Lai <laiyingchun@apache.org> Co-authored-by: Hash Brown <hi@xzd.me> Co-authored-by: zuodongxu <192560071+zuodongxu@users.noreply.github.com> Co-authored-by: Masashi Tomooka <tmokmss@users.noreply.github.com> Co-authored-by: aplio <ryo.091219@gmail.com> Co-authored-by: Obada Khalili <54270856+obadakhalili@users.noreply.github.com> Co-authored-by: Nam Vu <zuzoovn@gmail.com> Co-authored-by: Kei YAMAZAKI <1715090+kei-yamazaki@users.noreply.github.com> Co-authored-by: TechnoHouse <13776377+deephbz@users.noreply.github.com> Co-authored-by: Riddhimaan-Senapati <114703025+Riddhimaan-Senapati@users.noreply.github.com> Co-authored-by: MaFee921 <31881301+2284730142@users.noreply.github.com> Co-authored-by: te-chan <t-nakanome@sakura-is.co.jp> Co-authored-by: HQidea <HQidea@users.noreply.github.com> Co-authored-by: Joshbly <36315710+Joshbly@users.noreply.github.com> Co-authored-by: xhe <xw897002528@gmail.com> Co-authored-by: weiwenyan-dev <154779315+weiwenyan-dev@users.noreply.github.com> Co-authored-by: ex_wenyan.wei <ex_wenyan.wei@tcl.com> Co-authored-by: engchina <12236799+engchina@users.noreply.github.com> Co-authored-by: engchina <atjapan2015@gmail.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: 呆萌闷油瓶 <253605712@qq.com> Co-authored-by: Kemal <kemalmeler@outlook.com> Co-authored-by: Lazy_Frog <4590648+lazyFrogLOL@users.noreply.github.com> Co-authored-by: Yi Xiao <54782454+YIXIAO0@users.noreply.github.com> Co-authored-by: Steven sun <98230804+Tuyohai@users.noreply.github.com> Co-authored-by: steven <sunzwj@digitalchina.com> Co-authored-by: Kalo Chin <91766386+fdb02983rhy@users.noreply.github.com> Co-authored-by: Katy Tao <34019945+KatyTao@users.noreply.github.com> Co-authored-by: depy <42985524+h4ckdepy@users.noreply.github.com> Co-authored-by: 胡春东 <gycm520@gmail.com> Co-authored-by: Junjie.M <118170653@qq.com> Co-authored-by: MuYu <mr.muzea@gmail.com> Co-authored-by: Naoki Takashima <39912547+takatea@users.noreply.github.com> Co-authored-by: Summer-Gu <37869445+gubinjie@users.noreply.github.com> Co-authored-by: Fei He <droxer.he@gmail.com> Co-authored-by: ybalbert001 <120714773+ybalbert001@users.noreply.github.com> Co-authored-by: Yuanbo Li <ybalbert@amazon.com> Co-authored-by: douxc <7553076+douxc@users.noreply.github.com> Co-authored-by: liuzhenghua <1090179900@qq.com> Co-authored-by: Wu Jiayang <62842862+Wu-Jiayang@users.noreply.github.com> Co-authored-by: Your Name <you@example.com> Co-authored-by: kimjion <45935338+kimjion@users.noreply.github.com> Co-authored-by: AugNSo <song.tiankai@icloud.com> Co-authored-by: llinvokerl <38915183+llinvokerl@users.noreply.github.com> Co-authored-by: liusurong.lsr <liusurong.lsr@alibaba-inc.com> Co-authored-by: Vasu Negi <vasu-negi@users.noreply.github.com> Co-authored-by: Hundredwz <1808096180@qq.com> Co-authored-by: Xiyuan Chen <52963600+GareArc@users.noreply.github.com>
This commit is contained in:
126
web/app/components/workflow/run/agent-log/agent-log-item.tsx
Normal file
126
web/app/components/workflow/run/agent-log/agent-log-item.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
import {
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import {
|
||||
RiArrowRightSLine,
|
||||
RiListView,
|
||||
} from '@remixicon/react'
|
||||
import cn from '@/utils/classnames'
|
||||
import Button from '@/app/components/base/button'
|
||||
import type { AgentLogItemWithChildren } from '@/types/workflow'
|
||||
import NodeStatusIcon from '@/app/components/workflow/nodes/_base/components/node-status-icon'
|
||||
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
|
||||
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
|
||||
import BlockIcon from '@/app/components/workflow/block-icon'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import useGetIcon from '@/app/components/plugins/install-plugin/base/use-get-icon'
|
||||
|
||||
type AgentLogItemProps = {
|
||||
item: AgentLogItemWithChildren
|
||||
onShowAgentOrToolLog: (detail: AgentLogItemWithChildren) => void
|
||||
}
|
||||
const AgentLogItem = ({
|
||||
item,
|
||||
onShowAgentOrToolLog,
|
||||
}: AgentLogItemProps) => {
|
||||
const {
|
||||
label,
|
||||
status,
|
||||
children,
|
||||
data,
|
||||
metadata,
|
||||
} = item
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
const { getIconUrl } = useGetIcon()
|
||||
const toolIcon = useMemo(() => {
|
||||
const icon = metadata?.icon
|
||||
|
||||
if (icon) {
|
||||
if (icon.includes('http'))
|
||||
return icon
|
||||
|
||||
return getIconUrl(icon)
|
||||
}
|
||||
|
||||
return ''
|
||||
}, [getIconUrl, metadata?.icon])
|
||||
|
||||
const mergeStatus = useMemo(() => {
|
||||
if (status === 'start')
|
||||
return 'running'
|
||||
|
||||
return status
|
||||
}, [status])
|
||||
|
||||
return (
|
||||
<div className='bg-background-default border-[0.5px] border-components-panel-border rounded-[10px]'>
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center pl-1.5 pt-2 pr-3 pb-2 cursor-pointer',
|
||||
expanded && 'pb-1',
|
||||
)}
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
>
|
||||
{
|
||||
expanded
|
||||
? <RiArrowRightSLine className='shrink-0 w-4 h-4 rotate-90 text-text-quaternary' />
|
||||
: <RiArrowRightSLine className='shrink-0 w-4 h-4 text-text-quaternary' />
|
||||
}
|
||||
<BlockIcon
|
||||
className='shrink-0 mr-1.5'
|
||||
type={toolIcon ? BlockEnum.Tool : BlockEnum.Agent}
|
||||
toolIcon={toolIcon}
|
||||
/>
|
||||
<div
|
||||
className='grow system-sm-semibold-uppercase text-text-secondary truncate'
|
||||
title={label}
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
{
|
||||
metadata?.elapsed_time && (
|
||||
<div className='shrink-0 mr-2 system-xs-regular text-text-tertiary'>{metadata?.elapsed_time?.toFixed(3)}s</div>
|
||||
)
|
||||
}
|
||||
<NodeStatusIcon status={mergeStatus} />
|
||||
</div>
|
||||
{
|
||||
expanded && (
|
||||
<div className='p-1 pt-0'>
|
||||
{
|
||||
!!children?.length && (
|
||||
<Button
|
||||
className='flex items-center justify-between mb-1 w-full'
|
||||
variant='tertiary'
|
||||
onClick={() => onShowAgentOrToolLog(item)}
|
||||
>
|
||||
<div className='flex items-center'>
|
||||
<RiListView className='mr-1 w-4 h-4 text-components-button-tertiary-text shrink-0' />
|
||||
{`${children.length} Action Logs`}
|
||||
</div>
|
||||
<div className='flex'>
|
||||
<RiArrowRightSLine className='w-4 h-4 text-components-button-tertiary-text shrink-0' />
|
||||
</div>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
{
|
||||
data && (
|
||||
<CodeEditor
|
||||
readOnly
|
||||
title={<div>{'data'.toLocaleUpperCase()}</div>}
|
||||
language={CodeLanguage.json}
|
||||
value={data}
|
||||
isJSONStringifyBeauty
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AgentLogItem
|
||||
@@ -0,0 +1,61 @@
|
||||
import { useState } from 'react'
|
||||
import { RiMoreLine } from '@remixicon/react'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import Button from '@/app/components/base/button'
|
||||
import type { AgentLogItemWithChildren } from '@/types/workflow'
|
||||
|
||||
type AgentLogNavMoreProps = {
|
||||
options: { id: string; label: string }[]
|
||||
onShowAgentOrToolLog: (detail?: AgentLogItemWithChildren) => void
|
||||
}
|
||||
const AgentLogNavMore = ({
|
||||
options,
|
||||
onShowAgentOrToolLog,
|
||||
}: AgentLogNavMoreProps) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
placement='bottom-start'
|
||||
offset={{
|
||||
mainAxis: 2,
|
||||
crossAxis: -54,
|
||||
}}
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
>
|
||||
<PortalToFollowElemTrigger>
|
||||
<Button
|
||||
className='w-6 h-6'
|
||||
variant='ghost-accent'
|
||||
>
|
||||
<RiMoreLine className='w-4 h-4' />
|
||||
</Button>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent>
|
||||
<div className='p-1 w-[136px] bg-components-panel-bg-blur border-[0.5px] border-components-panel-border rounded-xl shadow-lg'>
|
||||
{
|
||||
options.map(option => (
|
||||
<div
|
||||
key={option.id}
|
||||
className='flex items-center px-2 h-8 rounded-lg system-md-regular text-text-secondary hover:bg-state-base-hover cursor-pointer'
|
||||
onClick={() => {
|
||||
onShowAgentOrToolLog(option as AgentLogItemWithChildren)
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
{option.label}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
}
|
||||
|
||||
export default AgentLogNavMore
|
||||
78
web/app/components/workflow/run/agent-log/agent-log-nav.tsx
Normal file
78
web/app/components/workflow/run/agent-log/agent-log-nav.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import { RiArrowLeftLine } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import AgentLogNavMore from './agent-log-nav-more'
|
||||
import Button from '@/app/components/base/button'
|
||||
import type { AgentLogItemWithChildren } from '@/types/workflow'
|
||||
|
||||
type AgentLogNavProps = {
|
||||
agentOrToolLogItemStack: AgentLogItemWithChildren[]
|
||||
onShowAgentOrToolLog: (detail?: AgentLogItemWithChildren) => void
|
||||
}
|
||||
const AgentLogNav = ({
|
||||
agentOrToolLogItemStack,
|
||||
onShowAgentOrToolLog,
|
||||
}: AgentLogNavProps) => {
|
||||
const { t } = useTranslation()
|
||||
const agentOrToolLogItemStackLength = agentOrToolLogItemStack.length
|
||||
const first = agentOrToolLogItemStack[0]
|
||||
const mid = agentOrToolLogItemStack.slice(1, -1)
|
||||
const end = agentOrToolLogItemStack.at(-1)
|
||||
|
||||
return (
|
||||
<div className='flex items-center p-1 pr-3 h-8 bg-components-panel-bg'>
|
||||
<Button
|
||||
className='shrink-0 px-[5px]'
|
||||
size='small'
|
||||
variant='ghost-accent'
|
||||
onClick={() => {
|
||||
onShowAgentOrToolLog()
|
||||
}}
|
||||
>
|
||||
<RiArrowLeftLine className='mr-1 w-3.5 h-3.5' />
|
||||
AGENT
|
||||
</Button>
|
||||
<div className='shrink-0 mx-0.5 system-xs-regular text-divider-deep'>/</div>
|
||||
{
|
||||
agentOrToolLogItemStackLength > 1
|
||||
? (
|
||||
<Button
|
||||
className='shrink-0 px-[5px]'
|
||||
size='small'
|
||||
variant='ghost-accent'
|
||||
onClick={() => onShowAgentOrToolLog(first)}
|
||||
>
|
||||
{t('workflow.nodes.agent.strategy.label')}
|
||||
</Button>
|
||||
)
|
||||
: (
|
||||
<div className='flex items-center px-[5px] system-xs-medium-uppercase text-text-tertiary'>
|
||||
{t('workflow.nodes.agent.strategy.label')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
!!mid.length && (
|
||||
<>
|
||||
<div className='shrink-0 mx-0.5 system-xs-regular text-divider-deep'>/</div>
|
||||
<AgentLogNavMore
|
||||
options={mid}
|
||||
onShowAgentOrToolLog={onShowAgentOrToolLog}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
!!end && agentOrToolLogItemStackLength > 1 && (
|
||||
<>
|
||||
<div className='shrink-0 mx-0.5 system-xs-regular text-divider-deep'>/</div>
|
||||
<div className='flex items-center px-[5px] system-xs-medium-uppercase text-text-tertiary'>
|
||||
{end.label}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AgentLogNav
|
||||
@@ -0,0 +1,49 @@
|
||||
import { RiArrowRightLine } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type {
|
||||
AgentLogItemWithChildren,
|
||||
NodeTracing,
|
||||
} from '@/types/workflow'
|
||||
|
||||
type AgentLogTriggerProps = {
|
||||
nodeInfo: NodeTracing
|
||||
onShowAgentOrToolLog: (detail?: AgentLogItemWithChildren) => void
|
||||
}
|
||||
const AgentLogTrigger = ({
|
||||
nodeInfo,
|
||||
onShowAgentOrToolLog,
|
||||
}: AgentLogTriggerProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { agentLog, execution_metadata } = nodeInfo
|
||||
const agentStrategy = execution_metadata?.tool_info?.agent_strategy
|
||||
|
||||
return (
|
||||
<div
|
||||
className='bg-components-button-tertiary-bg rounded-[10px] cursor-pointer'
|
||||
onClick={() => {
|
||||
onShowAgentOrToolLog({ id: nodeInfo.id, children: agentLog || [] } as AgentLogItemWithChildren)
|
||||
}}
|
||||
>
|
||||
<div className='flex items-center px-3 pt-2 system-2xs-medium-uppercase text-text-tertiary'>
|
||||
{t('workflow.nodes.agent.strategy.label')}
|
||||
</div>
|
||||
<div className='flex items-center pl-3 pt-1 pr-2 pb-1.5'>
|
||||
{
|
||||
agentStrategy && (
|
||||
<div className='grow system-xs-medium text-text-secondary'>
|
||||
{agentStrategy}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<div
|
||||
className='shrink-0 flex items-center px-[1px] system-xs-regular-uppercase text-text-tertiary cursor-pointer'
|
||||
>
|
||||
{t('runLog.detail')}
|
||||
<RiArrowRightLine className='ml-0.5 w-3.5 h-3.5' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AgentLogTrigger
|
||||
@@ -0,0 +1,60 @@
|
||||
import { RiAlertFill } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import AgentLogItem from './agent-log-item'
|
||||
import AgentLogNav from './agent-log-nav'
|
||||
import type { AgentLogItemWithChildren } from '@/types/workflow'
|
||||
|
||||
type AgentResultPanelProps = {
|
||||
agentOrToolLogItemStack: AgentLogItemWithChildren[]
|
||||
agentOrToolLogListMap: Record<string, AgentLogItemWithChildren[]>
|
||||
onShowAgentOrToolLog: (detail?: AgentLogItemWithChildren) => void
|
||||
}
|
||||
const AgentResultPanel = ({
|
||||
agentOrToolLogItemStack,
|
||||
agentOrToolLogListMap,
|
||||
onShowAgentOrToolLog,
|
||||
}: AgentResultPanelProps) => {
|
||||
const { t } = useTranslation()
|
||||
const top = agentOrToolLogItemStack[agentOrToolLogItemStack.length - 1]
|
||||
const list = agentOrToolLogListMap[top.id]
|
||||
|
||||
return (
|
||||
<div className='bg-background-section overflow-y-auto'>
|
||||
<AgentLogNav
|
||||
agentOrToolLogItemStack={agentOrToolLogItemStack}
|
||||
onShowAgentOrToolLog={onShowAgentOrToolLog}
|
||||
/>
|
||||
{
|
||||
<div className='p-2 space-y-1'>
|
||||
{
|
||||
list.map(item => (
|
||||
<AgentLogItem
|
||||
key={item.id}
|
||||
item={item}
|
||||
onShowAgentOrToolLog={onShowAgentOrToolLog}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
}
|
||||
{
|
||||
top.hasCircle && (
|
||||
<div className='flex items-center mt-1 rounded-xl px-3 pr-2 border border-components-panel-border bg-components-panel-bg-blur shadow-md'>
|
||||
<div
|
||||
className='absolute inset-0 opacity-[0.4] rounded-xl'
|
||||
style={{
|
||||
background: 'linear-gradient(92deg, rgba(247, 144, 9, 0.25) 0%, rgba(255, 255, 255, 0.00) 100%)',
|
||||
}}
|
||||
></div>
|
||||
<RiAlertFill className='mr-1.5 w-4 h-4 text-text-warning-secondary' />
|
||||
<div className='system-xs-medium text-text-primary'>
|
||||
{t('runLog.circularInvocationTip')}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AgentResultPanel
|
||||
2
web/app/components/workflow/run/agent-log/index.tsx
Normal file
2
web/app/components/workflow/run/agent-log/index.tsx
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as AgentLogTrigger } from './agent-log-trigger'
|
||||
export { default as AgentResultPanel } from './agent-result-panel'
|
||||
@@ -0,0 +1,9 @@
|
||||
<svg width="237" height="50" viewBox="0 0 237 50" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path opacity="0.5" d="M0 8C0 3.58172 3.58172 0 8 0H237L215.033 50H8C3.58172 50 0 46.4183 0 42V8Z" fill="url(#paint0_linear_3552_29170)"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_3552_29170" x1="-4.89158e-08" y1="4.62963" x2="168.013" y2="23.1752" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white" stop-opacity="0.03"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0.05"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 517 B |
88
web/app/components/workflow/run/hooks.ts
Normal file
88
web/app/components/workflow/run/hooks.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import {
|
||||
useCallback,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import type {
|
||||
AgentLogItemWithChildren,
|
||||
IterationDurationMap,
|
||||
NodeTracing,
|
||||
} from '@/types/workflow'
|
||||
|
||||
export const useLogs = () => {
|
||||
const [showRetryDetail, {
|
||||
setTrue: setShowRetryDetailTrue,
|
||||
setFalse: setShowRetryDetailFalse,
|
||||
}] = useBoolean(false)
|
||||
const [retryResultList, setRetryResultList] = useState<NodeTracing[]>([])
|
||||
const handleShowRetryResultList = useCallback((detail: NodeTracing[]) => {
|
||||
setShowRetryDetailTrue()
|
||||
setRetryResultList(detail)
|
||||
}, [setShowRetryDetailTrue, setRetryResultList])
|
||||
|
||||
const [showIteratingDetail, {
|
||||
setTrue: setShowIteratingDetailTrue,
|
||||
setFalse: setShowIteratingDetailFalse,
|
||||
}] = useBoolean(false)
|
||||
const [iterationResultList, setIterationResultList] = useState<NodeTracing[][]>([])
|
||||
const [iterationResultDurationMap, setIterationResultDurationMap] = useState<IterationDurationMap>({})
|
||||
const handleShowIterationResultList = useCallback((detail: NodeTracing[][], iterDurationMap: IterationDurationMap) => {
|
||||
setShowIteratingDetailTrue()
|
||||
setIterationResultList(detail)
|
||||
setIterationResultDurationMap(iterDurationMap)
|
||||
}, [setShowIteratingDetailTrue, setIterationResultList, setIterationResultDurationMap])
|
||||
|
||||
const [agentOrToolLogItemStack, setAgentOrToolLogItemStack] = useState<AgentLogItemWithChildren[]>([])
|
||||
const agentOrToolLogItemStackRef = useRef(agentOrToolLogItemStack)
|
||||
const [agentOrToolLogListMap, setAgentOrToolLogListMap] = useState<Record<string, AgentLogItemWithChildren[]>>({})
|
||||
const agentOrToolLogListMapRef = useRef(agentOrToolLogListMap)
|
||||
const handleShowAgentOrToolLog = useCallback((detail?: AgentLogItemWithChildren) => {
|
||||
if (!detail) {
|
||||
setAgentOrToolLogItemStack([])
|
||||
agentOrToolLogItemStackRef.current = []
|
||||
return
|
||||
}
|
||||
const { id, children } = detail
|
||||
let currentAgentOrToolLogItemStack = agentOrToolLogItemStackRef.current.slice()
|
||||
const index = currentAgentOrToolLogItemStack.findIndex(logItem => logItem.id === id)
|
||||
|
||||
if (index > -1)
|
||||
currentAgentOrToolLogItemStack = currentAgentOrToolLogItemStack.slice(0, index + 1)
|
||||
else
|
||||
currentAgentOrToolLogItemStack = [...currentAgentOrToolLogItemStack.slice(), detail]
|
||||
|
||||
setAgentOrToolLogItemStack(currentAgentOrToolLogItemStack)
|
||||
agentOrToolLogItemStackRef.current = currentAgentOrToolLogItemStack
|
||||
|
||||
if (children) {
|
||||
setAgentOrToolLogListMap({
|
||||
...agentOrToolLogListMapRef.current,
|
||||
[id]: children,
|
||||
})
|
||||
}
|
||||
}, [setAgentOrToolLogItemStack, setAgentOrToolLogListMap])
|
||||
|
||||
return {
|
||||
showSpecialResultPanel: showRetryDetail || showIteratingDetail || !!agentOrToolLogItemStack.length,
|
||||
showRetryDetail,
|
||||
setShowRetryDetailTrue,
|
||||
setShowRetryDetailFalse,
|
||||
retryResultList,
|
||||
setRetryResultList,
|
||||
handleShowRetryResultList,
|
||||
|
||||
showIteratingDetail,
|
||||
setShowIteratingDetailTrue,
|
||||
setShowIteratingDetailFalse,
|
||||
iterationResultList,
|
||||
setIterationResultList,
|
||||
iterationResultDurationMap,
|
||||
setIterationResultDurationMap,
|
||||
handleShowIterationResultList,
|
||||
|
||||
agentOrToolLogItemStack,
|
||||
agentOrToolLogListMap,
|
||||
handleShowAgentOrToolLog,
|
||||
}
|
||||
}
|
||||
@@ -3,21 +3,16 @@ import type { FC } from 'react'
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import { BlockEnum } from '../types'
|
||||
import OutputPanel from './output-panel'
|
||||
import ResultPanel from './result-panel'
|
||||
import TracingPanel from './tracing-panel'
|
||||
import IterationResultPanel from './iteration-result-panel'
|
||||
import RetryResultPanel from './retry-result-panel'
|
||||
import cn from '@/utils/classnames'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { fetchRunDetail, fetchTracingList } from '@/service/log'
|
||||
import type { IterationDurationMap, NodeTracing } from '@/types/workflow'
|
||||
import type { NodeTracing } from '@/types/workflow'
|
||||
import type { WorkflowRunDetailResponse } from '@/models/log'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
|
||||
export type RunProps = {
|
||||
hideResult?: boolean
|
||||
activeTab?: 'RESULT' | 'DETAIL' | 'TRACING'
|
||||
@@ -60,124 +55,12 @@ const RunPanel: FC<RunProps> = ({ hideResult, activeTab = 'RESULT', runID, getRe
|
||||
}
|
||||
}, [notify, getResultCallback])
|
||||
|
||||
const formatNodeList = useCallback((list: NodeTracing[]) => {
|
||||
const allItems = [...list].reverse()
|
||||
const result: NodeTracing[] = []
|
||||
const nodeGroupMap = new Map<string, Map<string, NodeTracing[]>>()
|
||||
|
||||
const processIterationNode = (item: NodeTracing) => {
|
||||
result.push({
|
||||
...item,
|
||||
details: [],
|
||||
})
|
||||
}
|
||||
|
||||
const updateParallelModeGroup = (runId: string, item: NodeTracing, iterationNode: NodeTracing) => {
|
||||
if (!nodeGroupMap.has(iterationNode.node_id))
|
||||
nodeGroupMap.set(iterationNode.node_id, new Map())
|
||||
|
||||
const groupMap = nodeGroupMap.get(iterationNode.node_id)!
|
||||
|
||||
if (!groupMap.has(runId)) {
|
||||
groupMap.set(runId, [item])
|
||||
}
|
||||
else {
|
||||
if (item.status === 'retry') {
|
||||
const retryNode = groupMap.get(runId)!.find(node => node.node_id === item.node_id)
|
||||
|
||||
if (retryNode) {
|
||||
if (retryNode?.retryDetail)
|
||||
retryNode.retryDetail.push(item)
|
||||
else
|
||||
retryNode.retryDetail = [item]
|
||||
}
|
||||
}
|
||||
else {
|
||||
groupMap.get(runId)!.push(item)
|
||||
}
|
||||
}
|
||||
|
||||
if (item.status === 'failed') {
|
||||
iterationNode.status = 'failed'
|
||||
iterationNode.error = item.error
|
||||
}
|
||||
|
||||
iterationNode.details = Array.from(groupMap.values())
|
||||
}
|
||||
const updateSequentialModeGroup = (index: number, item: NodeTracing, iterationNode: NodeTracing) => {
|
||||
const { details } = iterationNode
|
||||
if (details) {
|
||||
if (!details[index]) {
|
||||
details[index] = [item]
|
||||
}
|
||||
else {
|
||||
if (item.status === 'retry') {
|
||||
const retryNode = details[index].find(node => node.node_id === item.node_id)
|
||||
|
||||
if (retryNode) {
|
||||
if (retryNode?.retryDetail)
|
||||
retryNode.retryDetail.push(item)
|
||||
else
|
||||
retryNode.retryDetail = [item]
|
||||
}
|
||||
}
|
||||
else {
|
||||
details[index].push(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (item.status === 'failed') {
|
||||
iterationNode.status = 'failed'
|
||||
iterationNode.error = item.error
|
||||
}
|
||||
}
|
||||
const processNonIterationNode = (item: NodeTracing) => {
|
||||
const { execution_metadata } = item
|
||||
if (!execution_metadata?.iteration_id) {
|
||||
if (item.status === 'retry') {
|
||||
const retryNode = result.find(node => node.node_id === item.node_id)
|
||||
|
||||
if (retryNode) {
|
||||
if (retryNode?.retryDetail)
|
||||
retryNode.retryDetail.push(item)
|
||||
else
|
||||
retryNode.retryDetail = [item]
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
result.push(item)
|
||||
return
|
||||
}
|
||||
|
||||
const iterationNode = result.find(node => node.node_id === execution_metadata.iteration_id)
|
||||
if (!iterationNode || !Array.isArray(iterationNode.details))
|
||||
return
|
||||
|
||||
const { parallel_mode_run_id, iteration_index = 0 } = execution_metadata
|
||||
|
||||
if (parallel_mode_run_id)
|
||||
updateParallelModeGroup(parallel_mode_run_id, item, iterationNode)
|
||||
else
|
||||
updateSequentialModeGroup(iteration_index, item, iterationNode)
|
||||
}
|
||||
|
||||
allItems.forEach((item) => {
|
||||
item.node_type === BlockEnum.Iteration
|
||||
? processIterationNode(item)
|
||||
: processNonIterationNode(item)
|
||||
})
|
||||
|
||||
return result
|
||||
}, [])
|
||||
|
||||
const getTracingList = useCallback(async (appID: string, runID: string) => {
|
||||
try {
|
||||
const { data: nodeList } = await fetchTracingList({
|
||||
url: `/apps/${appID}/workflow-runs/${runID}/node-executions`,
|
||||
})
|
||||
setList(formatNodeList(nodeList))
|
||||
setList(nodeList)
|
||||
}
|
||||
catch (err) {
|
||||
notify({
|
||||
@@ -219,42 +102,6 @@ const RunPanel: FC<RunProps> = ({ hideResult, activeTab = 'RESULT', runID, getRe
|
||||
adjustResultHeight()
|
||||
}, [loading])
|
||||
|
||||
const [iterationRunResult, setIterationRunResult] = useState<NodeTracing[][]>([])
|
||||
const [iterDurationMap, setIterDurationMap] = useState<IterationDurationMap>({})
|
||||
const [retryRunResult, setRetryRunResult] = useState<NodeTracing[]>([])
|
||||
const [isShowIterationDetail, {
|
||||
setTrue: doShowIterationDetail,
|
||||
setFalse: doHideIterationDetail,
|
||||
}] = useBoolean(false)
|
||||
const [isShowRetryDetail, {
|
||||
setTrue: doShowRetryDetail,
|
||||
setFalse: doHideRetryDetail,
|
||||
}] = useBoolean(false)
|
||||
|
||||
const handleShowIterationDetail = useCallback((detail: NodeTracing[][], iterDurationMap: IterationDurationMap) => {
|
||||
setIterationRunResult(detail)
|
||||
doShowIterationDetail()
|
||||
setIterDurationMap(iterDurationMap)
|
||||
}, [doShowIterationDetail, setIterationRunResult, setIterDurationMap])
|
||||
|
||||
const handleShowRetryDetail = useCallback((detail: NodeTracing[]) => {
|
||||
setRetryRunResult(detail)
|
||||
doShowRetryDetail()
|
||||
}, [doShowRetryDetail, setRetryRunResult])
|
||||
|
||||
if (isShowIterationDetail) {
|
||||
return (
|
||||
<div className='grow relative flex flex-col'>
|
||||
<IterationResultPanel
|
||||
list={iterationRunResult}
|
||||
onHide={doHideIterationDetail}
|
||||
onBack={doHideIterationDetail}
|
||||
iterDurationMap={iterDurationMap}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='grow relative flex flex-col'>
|
||||
{/* tab */}
|
||||
@@ -284,7 +131,7 @@ const RunPanel: FC<RunProps> = ({ hideResult, activeTab = 'RESULT', runID, getRe
|
||||
>{t('runLog.tracing')}</div>
|
||||
</div>
|
||||
{/* panel detail */}
|
||||
<div ref={ref} className={cn('grow bg-components-panel-bg h-0 overflow-y-auto rounded-b-2xl', currentTab !== 'DETAIL' && '!bg-background-section-burn')}>
|
||||
<div ref={ref} className={cn('relative grow bg-components-panel-bg h-0 overflow-y-auto rounded-b-2xl')}>
|
||||
{loading && (
|
||||
<div className='flex h-full items-center justify-center bg-components-panel-bg'>
|
||||
<Loading />
|
||||
@@ -311,22 +158,12 @@ const RunPanel: FC<RunProps> = ({ hideResult, activeTab = 'RESULT', runID, getRe
|
||||
exceptionCounts={runDetail.exceptions_count}
|
||||
/>
|
||||
)}
|
||||
{!loading && currentTab === 'TRACING' && !isShowRetryDetail && (
|
||||
{!loading && currentTab === 'TRACING' && (
|
||||
<TracingPanel
|
||||
className='bg-background-section-burn'
|
||||
list={list}
|
||||
onShowIterationDetail={handleShowIterationDetail}
|
||||
onShowRetryDetail={handleShowRetryDetail}
|
||||
/>
|
||||
)}
|
||||
{
|
||||
!loading && currentTab === 'TRACING' && isShowRetryDetail && (
|
||||
<RetryResultPanel
|
||||
list={retryRunResult}
|
||||
onBack={doHideRetryDetail}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
2
web/app/components/workflow/run/iteration-log/index.tsx
Normal file
2
web/app/components/workflow/run/iteration-log/index.tsx
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as IterationLogTrigger } from './iteration-log-trigger'
|
||||
export { default as IterationResultPanel } from './iteration-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 {
|
||||
IterationDurationMap,
|
||||
NodeTracing,
|
||||
} from '@/types/workflow'
|
||||
import { Iteration } from '@/app/components/base/icons/src/vender/workflow'
|
||||
|
||||
type IterationLogTriggerProps = {
|
||||
nodeInfo: NodeTracing
|
||||
onShowIterationResultList: (iterationResultList: NodeTracing[][], iterationResultDurationMap: IterationDurationMap) => void
|
||||
}
|
||||
const IterationLogTrigger = ({
|
||||
nodeInfo,
|
||||
onShowIterationResultList,
|
||||
}: IterationLogTriggerProps) => {
|
||||
const { t } = useTranslation()
|
||||
const getErrorCount = (details: NodeTracing[][] | undefined) => {
|
||||
if (!details || details.length === 0)
|
||||
return 0
|
||||
|
||||
return details.reduce((acc, iteration) => {
|
||||
if (iteration.some(item => item.status === 'failed'))
|
||||
acc++
|
||||
return acc
|
||||
}, 0)
|
||||
}
|
||||
const getCount = (iteration_curr_length: number | undefined, iteration_length: number) => {
|
||||
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 (
|
||||
<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={handleOnShowIterationDetail}
|
||||
>
|
||||
<Iteration 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.iteration.iteration', { count: getCount(nodeInfo.details?.length, nodeInfo.metadata?.iterator_length) })}{getErrorCount(nodeInfo.details) > 0 && (
|
||||
<>
|
||||
{t('workflow.nodes.iteration.comma')}
|
||||
{t('workflow.nodes.iteration.error', { count: getErrorCount(nodeInfo.details) })}
|
||||
</>
|
||||
)}</div>
|
||||
<RiArrowRightSLine className='w-4 h-4 text-components-button-tertiary-text shrink-0' />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
export default IterationLogTrigger
|
||||
@@ -3,15 +3,13 @@ import type { FC } from 'react'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiArrowLeftLine,
|
||||
RiArrowRightSLine,
|
||||
RiCloseLine,
|
||||
RiErrorWarningLine,
|
||||
RiLoader2Line,
|
||||
} from '@remixicon/react'
|
||||
import { ArrowNarrowLeft } from '../../base/icons/src/vender/line/arrows'
|
||||
import { NodeRunningStatus } from '../types'
|
||||
import TracingPanel from './tracing-panel'
|
||||
import RetryResultPanel from './retry-result-panel'
|
||||
import { NodeRunningStatus } from '@/app/components/workflow/types'
|
||||
import TracingPanel from '@/app/components/workflow/run/tracing-panel'
|
||||
import { Iteration } from '@/app/components/base/icons/src/vender/workflow'
|
||||
import cn from '@/utils/classnames'
|
||||
import type { IterationDurationMap, NodeTracing } from '@/types/workflow'
|
||||
@@ -19,17 +17,13 @@ const i18nPrefix = 'workflow.singleRun'
|
||||
|
||||
type Props = {
|
||||
list: NodeTracing[][]
|
||||
onHide: () => void
|
||||
onBack: () => void
|
||||
noWrap?: boolean
|
||||
iterDurationMap?: IterationDurationMap
|
||||
}
|
||||
|
||||
const IterationResultPanel: FC<Props> = ({
|
||||
list,
|
||||
onHide,
|
||||
onBack,
|
||||
noWrap,
|
||||
iterDurationMap,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
@@ -75,29 +69,22 @@ const IterationResultPanel: FC<Props> = ({
|
||||
</>
|
||||
)
|
||||
}
|
||||
const [retryRunResult, setRetryRunResult] = useState<Record<string, NodeTracing[]> | undefined>()
|
||||
const handleRetryDetail = (v: number, detail?: NodeTracing[]) => {
|
||||
setRetryRunResult({ ...retryRunResult, [v]: detail })
|
||||
}
|
||||
|
||||
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}.testRunIteration`)}
|
||||
</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>
|
||||
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={cn(!noWrap ? 'flex-grow overflow-auto' : 'max-h-full', 'p-2 bg-components-panel-bg')}>
|
||||
<div className='p-2 bg-components-panel-bg'>
|
||||
{list.map((iteration, index) => (
|
||||
<div key={index} className={cn('mb-1 overflow-hidden rounded-xl bg-background-section-burn border-none')}>
|
||||
<div
|
||||
@@ -109,66 +96,31 @@ const IterationResultPanel: FC<Props> = ({
|
||||
onClick={() => toggleIteration(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 flex-shrink-0'>
|
||||
<div className='flex items-center justify-center w-4 h-4 rounded-[5px] border-divider-subtle bg-util-colors-cyan-cyan-500 shrink-0'>
|
||||
<Iteration className='w-3 h-3 text-text-primary-on-surface' />
|
||||
</div>
|
||||
<span className='system-sm-semibold-uppercase text-text-primary flex-grow'>
|
||||
<span className='system-sm-semibold-uppercase text-text-primary grow'>
|
||||
{t(`${i18nPrefix}.iteration`)} {index + 1}
|
||||
</span>
|
||||
{iterationStatusShow(index, iteration, iterDurationMap)}
|
||||
</div>
|
||||
</div>
|
||||
{expandedIterations[index] && <div
|
||||
className="flex-grow h-px bg-divider-subtle"
|
||||
className="grow h-px bg-divider-subtle"
|
||||
></div>}
|
||||
{
|
||||
!retryRunResult?.[index] && (
|
||||
<div className={cn(
|
||||
'overflow-hidden transition-all duration-200',
|
||||
expandedIterations[index] ? 'max-h-[1000px] opacity-100' : 'max-h-0 opacity-0',
|
||||
)}>
|
||||
<TracingPanel
|
||||
list={iteration}
|
||||
className='bg-background-section-burn'
|
||||
onShowRetryDetail={v => handleRetryDetail(index, v)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
retryRunResult?.[index] && (
|
||||
<RetryResultPanel
|
||||
list={retryRunResult[index]}
|
||||
onBack={() => handleRetryDetail(index, undefined)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
<div className={cn(
|
||||
'overflow-hidden transition-all duration-200',
|
||||
expandedIterations[index] ? 'max-h-[1000px] opacity-100' : 'max-h-0 opacity-0',
|
||||
)}>
|
||||
<TracingPanel
|
||||
list={iteration}
|
||||
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 >
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(IterationResultPanel)
|
||||
@@ -8,18 +8,21 @@ import {
|
||||
RiCheckboxCircleFill,
|
||||
RiErrorWarningLine,
|
||||
RiLoader2Line,
|
||||
RiRestartFill,
|
||||
} from '@remixicon/react'
|
||||
import BlockIcon from '../block-icon'
|
||||
import { BlockEnum } from '../types'
|
||||
import Split from '../nodes/_base/components/split'
|
||||
import { Iteration } from '@/app/components/base/icons/src/vender/workflow'
|
||||
import { RetryLogTrigger } from './retry-log'
|
||||
import { IterationLogTrigger } from './iteration-log'
|
||||
import { AgentLogTrigger } from './agent-log'
|
||||
import cn from '@/utils/classnames'
|
||||
import StatusContainer from '@/app/components/workflow/run/status-container'
|
||||
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
|
||||
import type { IterationDurationMap, NodeTracing } from '@/types/workflow'
|
||||
import type {
|
||||
AgentLogItemWithChildren,
|
||||
IterationDurationMap,
|
||||
NodeTracing,
|
||||
} from '@/types/workflow'
|
||||
import ErrorHandleTip from '@/app/components/workflow/nodes/_base/components/error-handle/error-handle-tip'
|
||||
import { hasRetryNode } from '@/app/components/workflow/utils'
|
||||
|
||||
@@ -31,9 +34,8 @@ type Props = {
|
||||
hideProcessDetail?: boolean
|
||||
onShowIterationDetail?: (detail: NodeTracing[][], iterDurationMap: IterationDurationMap) => void
|
||||
onShowRetryDetail?: (detail: NodeTracing[]) => void
|
||||
onShowAgentOrToolLog?: (detail?: AgentLogItemWithChildren) => void
|
||||
notShowIterationNav?: boolean
|
||||
justShowIterationNavArrow?: boolean
|
||||
justShowRetryNavArrow?: boolean
|
||||
}
|
||||
|
||||
const NodePanel: FC<Props> = ({
|
||||
@@ -44,8 +46,8 @@ const NodePanel: FC<Props> = ({
|
||||
hideProcessDetail,
|
||||
onShowIterationDetail,
|
||||
onShowRetryDetail,
|
||||
onShowAgentOrToolLog,
|
||||
notShowIterationNav,
|
||||
justShowIterationNavArrow,
|
||||
}) => {
|
||||
const [collapseState, doSetCollapseState] = useState<boolean>(true)
|
||||
const setCollapseState = useCallback((state: boolean) => {
|
||||
@@ -59,7 +61,7 @@ const NodePanel: FC<Props> = ({
|
||||
if (time < 1)
|
||||
return `${(time * 1000).toFixed(3)} ms`
|
||||
if (time > 60)
|
||||
return `${parseInt(Math.round(time / 60).toString())} m ${(time % 60).toFixed(3)} s`
|
||||
return `${Number.parseInt(Math.round(time / 60).toString())} m ${(time % 60).toFixed(3)} s`
|
||||
return `${time.toFixed(3)} s`
|
||||
}
|
||||
|
||||
@@ -67,43 +69,20 @@ const NodePanel: FC<Props> = ({
|
||||
if (tokens < 1000)
|
||||
return tokens
|
||||
if (tokens >= 1000 && tokens < 1000000)
|
||||
return `${parseFloat((tokens / 1000).toFixed(3))}K`
|
||||
return `${Number.parseFloat((tokens / 1000).toFixed(3))}K`
|
||||
if (tokens >= 1000000)
|
||||
return `${parseFloat((tokens / 1000000).toFixed(3))}M`
|
||||
return `${Number.parseFloat((tokens / 1000000).toFixed(3))}M`
|
||||
}
|
||||
|
||||
const getCount = (iteration_curr_length: number | undefined, iteration_length: number) => {
|
||||
if ((iteration_curr_length && iteration_curr_length < iteration_length) || !iteration_length)
|
||||
return iteration_curr_length
|
||||
|
||||
return iteration_length
|
||||
}
|
||||
const getErrorCount = (details: NodeTracing[][] | undefined) => {
|
||||
if (!details || details.length === 0)
|
||||
return 0
|
||||
|
||||
return details.reduce((acc, iteration) => {
|
||||
if (iteration.some(item => item.status === 'failed'))
|
||||
acc++
|
||||
return acc
|
||||
}, 0)
|
||||
}
|
||||
useEffect(() => {
|
||||
setCollapseState(!nodeInfo.expand)
|
||||
}, [nodeInfo.expand, setCollapseState])
|
||||
|
||||
const isIterationNode = nodeInfo.node_type === BlockEnum.Iteration
|
||||
const isRetryNode = hasRetryNode(nodeInfo.node_type) && nodeInfo.retryDetail
|
||||
const handleOnShowIterationDetail = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation()
|
||||
e.nativeEvent.stopImmediatePropagation()
|
||||
onShowIterationDetail?.(nodeInfo.details || [], nodeInfo?.iterDurationMap || nodeInfo.execution_metadata?.iteration_duration_map || {})
|
||||
}
|
||||
const handleOnShowRetryDetail = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation()
|
||||
e.nativeEvent.stopImmediatePropagation()
|
||||
onShowRetryDetail?.(nodeInfo.retryDetail || [])
|
||||
}
|
||||
const isIterationNode = nodeInfo.node_type === BlockEnum.Iteration && !!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
|
||||
|
||||
return (
|
||||
<div className={cn('px-2 py-1', className)}>
|
||||
<div className='group transition-all bg-background-default border border-components-panel-border rounded-[10px] shadow-xs hover:shadow-md'>
|
||||
@@ -153,46 +132,26 @@ const NodePanel: FC<Props> = ({
|
||||
{!collapseState && !hideProcessDetail && (
|
||||
<div className='px-1 pb-1'>
|
||||
{/* The nav to the iteration detail */}
|
||||
{isIterationNode && !notShowIterationNav && (
|
||||
<div className='mt-2 mb-1 !px-2'>
|
||||
<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={handleOnShowIterationDetail}
|
||||
>
|
||||
<Iteration className='w-4 h-4 text-components-button-tertiary-text flex-shrink-0' />
|
||||
<div className='flex-1 text-left system-sm-medium text-components-button-tertiary-text'>{t('workflow.nodes.iteration.iteration', { count: getCount(nodeInfo.details?.length, nodeInfo.metadata?.iterator_length) })}{getErrorCount(nodeInfo.details) > 0 && (
|
||||
<>
|
||||
{t('workflow.nodes.iteration.comma')}
|
||||
{t('workflow.nodes.iteration.error', { count: getErrorCount(nodeInfo.details) })}
|
||||
</>
|
||||
)}</div>
|
||||
{justShowIterationNavArrow
|
||||
? (
|
||||
<RiArrowRightSLine className='w-4 h-4 text-components-button-tertiary-text flex-shrink-0' />
|
||||
)
|
||||
: (
|
||||
<div className='flex items-center space-x-1 text-[#155EEF]'>
|
||||
<div className='text-[13px] font-normal '>{t('workflow.common.viewDetailInTracingPanel')}</div>
|
||||
<RiArrowRightSLine className='w-4 h-4 text-components-button-tertiary-text flex-shrink-0' />
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
<Split className='mt-2' />
|
||||
</div>
|
||||
{isIterationNode && !notShowIterationNav && onShowIterationDetail && (
|
||||
<IterationLogTrigger
|
||||
nodeInfo={nodeInfo}
|
||||
onShowIterationResultList={onShowIterationDetail}
|
||||
/>
|
||||
)}
|
||||
{isRetryNode && (
|
||||
<Button
|
||||
className='flex items-center justify-between mb-1 w-full'
|
||||
variant='tertiary'
|
||||
onClick={handleOnShowRetryDetail}
|
||||
>
|
||||
<div className='flex items-center'>
|
||||
<RiRestartFill className='mr-0.5 w-4 h-4 text-components-button-tertiary-text flex-shrink-0' />
|
||||
{t('workflow.nodes.common.retry.retries', { num: nodeInfo.retryDetail?.length })}
|
||||
</div>
|
||||
<RiArrowRightSLine className='w-4 h-4 text-components-button-tertiary-text flex-shrink-0' />
|
||||
</Button>
|
||||
{isRetryNode && onShowRetryDetail && (
|
||||
<RetryLogTrigger
|
||||
nodeInfo={nodeInfo}
|
||||
onShowRetryResultList={onShowRetryDetail}
|
||||
/>
|
||||
)}
|
||||
{
|
||||
(isAgentNode || isToolNode) && onShowAgentOrToolLog && (
|
||||
<AgentLogTrigger
|
||||
nodeInfo={nodeInfo}
|
||||
onShowAgentOrToolLog={onShowAgentOrToolLog}
|
||||
/>
|
||||
)
|
||||
}
|
||||
<div className={cn('mb-1', hideInfo && '!px-2 !py-0.5')}>
|
||||
{(nodeInfo.status === 'stopped') && (
|
||||
<StatusContainer status='stopped'>
|
||||
|
||||
@@ -54,7 +54,7 @@ const OutputPanel: FC<OutputPanelProps> = ({
|
||||
return getProcessedFilesFromResponse(fileList)
|
||||
}, [outputs])
|
||||
return (
|
||||
<div className='py-2'>
|
||||
<div className='p-2'>
|
||||
{isRunning && (
|
||||
<div className='pt-4 pl-[26px]'>
|
||||
<LoadingAnim type='text' />
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiArrowRightSLine,
|
||||
RiRestartFill,
|
||||
} from '@remixicon/react'
|
||||
import StatusPanel from './status'
|
||||
import MetaData from './meta'
|
||||
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
|
||||
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
|
||||
import ErrorHandleTip from '@/app/components/workflow/nodes/_base/components/error-handle/error-handle-tip'
|
||||
import type { NodeTracing } from '@/types/workflow'
|
||||
import Button from '@/app/components/base/button'
|
||||
import type {
|
||||
AgentLogItemWithChildren,
|
||||
NodeTracing,
|
||||
} from '@/types/workflow'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { hasRetryNode } from '@/app/components/workflow/utils'
|
||||
import { IterationLogTrigger } from '@/app/components/workflow/run/iteration-log'
|
||||
import { RetryLogTrigger } from '@/app/components/workflow/run/retry-log'
|
||||
import { AgentLogTrigger } from '@/app/components/workflow/run/agent-log'
|
||||
|
||||
type ResultPanelProps = {
|
||||
nodeInfo?: NodeTracing
|
||||
inputs?: string
|
||||
process_data?: string
|
||||
outputs?: string
|
||||
@@ -28,11 +32,13 @@ type ResultPanelProps = {
|
||||
showSteps?: boolean
|
||||
exceptionCounts?: number
|
||||
execution_metadata?: any
|
||||
retry_events?: NodeTracing[]
|
||||
onShowRetryDetail?: (retries: NodeTracing[]) => void
|
||||
handleShowIterationResultList?: (detail: NodeTracing[][], iterDurationMap: any) => void
|
||||
onShowRetryDetail?: (detail: NodeTracing[]) => void
|
||||
handleShowAgentOrToolLog?: (detail?: AgentLogItemWithChildren) => void
|
||||
}
|
||||
|
||||
const ResultPanel: FC<ResultPanelProps> = ({
|
||||
nodeInfo,
|
||||
inputs,
|
||||
process_data,
|
||||
outputs,
|
||||
@@ -46,10 +52,15 @@ const ResultPanel: FC<ResultPanelProps> = ({
|
||||
showSteps,
|
||||
exceptionCounts,
|
||||
execution_metadata,
|
||||
retry_events,
|
||||
handleShowIterationResultList,
|
||||
onShowRetryDetail,
|
||||
handleShowAgentOrToolLog,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const isIterationNode = nodeInfo?.node_type === BlockEnum.Iteration && !!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
|
||||
|
||||
return (
|
||||
<div className='bg-components-panel-bg py-2'>
|
||||
@@ -62,23 +73,32 @@ const ResultPanel: FC<ResultPanelProps> = ({
|
||||
exceptionCounts={exceptionCounts}
|
||||
/>
|
||||
</div>
|
||||
{
|
||||
retry_events?.length && onShowRetryDetail && (
|
||||
<div className='px-4'>
|
||||
<Button
|
||||
className='flex items-center justify-between w-full'
|
||||
variant='tertiary'
|
||||
onClick={() => onShowRetryDetail(retry_events)}
|
||||
>
|
||||
<div className='flex items-center'>
|
||||
<RiRestartFill className='mr-0.5 w-4 h-4 text-components-button-tertiary-text flex-shrink-0' />
|
||||
{t('workflow.nodes.common.retry.retries', { num: retry_events?.length })}
|
||||
</div>
|
||||
<RiArrowRightSLine className='w-4 h-4 text-components-button-tertiary-text flex-shrink-0' />
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<div className='px-4'>
|
||||
{
|
||||
isIterationNode && handleShowIterationResultList && (
|
||||
<IterationLogTrigger
|
||||
nodeInfo={nodeInfo}
|
||||
onShowIterationResultList={handleShowIterationResultList}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
isRetryNode && onShowRetryDetail && (
|
||||
<RetryLogTrigger
|
||||
nodeInfo={nodeInfo}
|
||||
onShowRetryResultList={onShowRetryDetail}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
(isAgentNode || isToolNode) && handleShowAgentOrToolLog && (
|
||||
<AgentLogTrigger
|
||||
nodeInfo={nodeInfo}
|
||||
onShowAgentOrToolLog={handleShowAgentOrToolLog}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<div className='px-4 py-2 flex flex-col gap-2'>
|
||||
<CodeEditor
|
||||
readOnly
|
||||
|
||||
@@ -7,7 +7,7 @@ import LoadingAnim from '@/app/components/base/chat/chat/loading-anim'
|
||||
import StatusContainer from '@/app/components/workflow/run/status-container'
|
||||
import { FileList } from '@/app/components/base/file-uploader'
|
||||
|
||||
type ResultTextProps = {
|
||||
interface ResultTextProps {
|
||||
isRunning?: boolean
|
||||
outputs?: any
|
||||
error?: string
|
||||
|
||||
2
web/app/components/workflow/run/retry-log/index.tsx
Normal file
2
web/app/components/workflow/run/retry-log/index.tsx
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as RetryLogTrigger } from './retry-log-trigger'
|
||||
export { default as RetryResultPanel } from './retry-result-panel'
|
||||
@@ -0,0 +1,41 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiArrowRightSLine,
|
||||
RiRestartFill,
|
||||
} from '@remixicon/react'
|
||||
import Button from '@/app/components/base/button'
|
||||
import type { NodeTracing } from '@/types/workflow'
|
||||
|
||||
type RetryLogTriggerProps = {
|
||||
nodeInfo: NodeTracing
|
||||
onShowRetryResultList: (detail: NodeTracing[]) => void
|
||||
}
|
||||
const RetryLogTrigger = ({
|
||||
nodeInfo,
|
||||
onShowRetryResultList,
|
||||
}: RetryLogTriggerProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { retryDetail } = nodeInfo
|
||||
|
||||
const handleShowRetryResultList = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation()
|
||||
e.nativeEvent.stopImmediatePropagation()
|
||||
onShowRetryResultList(retryDetail || [])
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
className='flex items-center justify-between mb-1 w-full'
|
||||
variant='tertiary'
|
||||
onClick={handleShowRetryResultList}
|
||||
>
|
||||
<div className='flex items-center'>
|
||||
<RiRestartFill className='mr-0.5 w-4 h-4 text-components-button-tertiary-text shrink-0' />
|
||||
{t('workflow.nodes.common.retry.retries', { num: retryDetail?.length })}
|
||||
</div>
|
||||
<RiArrowRightSLine className='w-4 h-4 text-components-button-tertiary-text shrink-0' />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
export default RetryLogTrigger
|
||||
@@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiArrowLeftLine,
|
||||
} from '@remixicon/react'
|
||||
import TracingPanel from './tracing-panel'
|
||||
import TracingPanel from '../tracing-panel'
|
||||
import type { NodeTracing } from '@/types/workflow'
|
||||
|
||||
type Props = {
|
||||
73
web/app/components/workflow/run/special-result-panel.tsx
Normal file
73
web/app/components/workflow/run/special-result-panel.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import { RetryResultPanel } from './retry-log'
|
||||
import { IterationResultPanel } from './iteration-log'
|
||||
import { AgentResultPanel } from './agent-log'
|
||||
import type {
|
||||
AgentLogItemWithChildren,
|
||||
IterationDurationMap,
|
||||
NodeTracing,
|
||||
} from '@/types/workflow'
|
||||
|
||||
export type SpecialResultPanelProps = {
|
||||
showRetryDetail?: boolean
|
||||
setShowRetryDetailFalse?: () => void
|
||||
retryResultList?: NodeTracing[]
|
||||
|
||||
showIteratingDetail?: boolean
|
||||
setShowIteratingDetailFalse?: () => void
|
||||
iterationResultList?: NodeTracing[][]
|
||||
iterationResultDurationMap?: IterationDurationMap
|
||||
|
||||
agentOrToolLogItemStack?: AgentLogItemWithChildren[]
|
||||
agentOrToolLogListMap?: Record<string, AgentLogItemWithChildren[]>
|
||||
handleShowAgentOrToolLog?: (detail?: AgentLogItemWithChildren) => void
|
||||
}
|
||||
const SpecialResultPanel = ({
|
||||
showRetryDetail,
|
||||
setShowRetryDetailFalse,
|
||||
retryResultList,
|
||||
|
||||
showIteratingDetail,
|
||||
setShowIteratingDetailFalse,
|
||||
iterationResultList,
|
||||
iterationResultDurationMap,
|
||||
|
||||
agentOrToolLogItemStack,
|
||||
agentOrToolLogListMap,
|
||||
handleShowAgentOrToolLog,
|
||||
}: SpecialResultPanelProps) => {
|
||||
return (
|
||||
<div onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
e.nativeEvent.stopImmediatePropagation()
|
||||
}}>
|
||||
{
|
||||
!!showRetryDetail && !!retryResultList?.length && setShowRetryDetailFalse && (
|
||||
<RetryResultPanel
|
||||
list={retryResultList}
|
||||
onBack={setShowRetryDetailFalse}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
showIteratingDetail && !!iterationResultList?.length && setShowIteratingDetailFalse && (
|
||||
<IterationResultPanel
|
||||
list={iterationResultList}
|
||||
onBack={setShowIteratingDetailFalse}
|
||||
iterDurationMap={iterationResultDurationMap}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
!!agentOrToolLogItemStack?.length && agentOrToolLogListMap && handleShowAgentOrToolLog && (
|
||||
<AgentResultPanel
|
||||
agentOrToolLogItemStack={agentOrToolLogItemStack}
|
||||
agentOrToolLogListMap={agentOrToolLogListMap}
|
||||
onShowAgentOrToolLog={handleShowAgentOrToolLog}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SpecialResultPanel
|
||||
@@ -1,5 +1,7 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { Theme } from '@/types/app'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type Props = {
|
||||
@@ -11,19 +13,36 @@ const StatusContainer: FC<Props> = ({
|
||||
status,
|
||||
children,
|
||||
}) => {
|
||||
const { theme } = useAppContext()
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'relative px-3 py-2.5 rounded-lg border system-xs-regular break-all',
|
||||
status === 'succeeded' && 'border-[rgba(23,178,106,0.8)] bg-workflow-display-success-bg bg-[url(~@/app/components/workflow/run/assets/bg-line-success.svg)] shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.5),inset_0_1px_3px_0_rgba(0,0,0,0.12),inset_0_2px_24px_0_rgba(23,178,106,0.2),0_1px_2px_0_rgba(9,9,11,0.05),0_0_0_1px_rgba(0,0,0,0.05)] text-text-success',
|
||||
status === 'partial-succeeded' && 'border-[rgba(23,178,106,0.8)] bg-workflow-display-success-bg bg-[url(~@/app/components/workflow/run/assets/bg-line-success.svg)] shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.5),inset_0_1px_3px_0_rgba(0,0,0,0.12),inset_0_2px_24px_0_rgba(23,178,106,0.2),0_1px_2px_0_rgba(9,9,11,0.05),0_0_0_1px_rgba(0,0,0,0.05)] text-text-success',
|
||||
status === 'failed' && 'border-[rgba(240,68,56,0.8)] bg-workflow-display-error-bg bg-[url(~@/app/components/workflow/run/assets/bg-line-error.svg)] shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.5),inset_0_1px_3px_0_rgba(0,0,0,0.12),inset_0_2px_24px_0_rgba(240,68,56,0.2),0_1px_2px_0_rgba(9,9,11,0.05),0_0_0_1px_rgba(0,0,0,0.05)] text-text-warning',
|
||||
status === 'stopped' && 'border-[rgba(247,144,9,0.8)] bg-workflow-display-warning-bg bg-[url(~@/app/components/workflow/run/assets/bg-line-warning.svg)] shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.5),inset_0_1px_3px_0_rgba(0,0,0,0.12),inset_0_2px_24px_0_rgba(247,144,9,0.2),0_1px_2px_0_rgba(9,9,11,0.05),0_0_0_1px_rgba(0,0,0,0.05)] text-text-destructive',
|
||||
status === 'exception' && 'border-[rgba(247,144,9,0.8)] bg-workflow-display-warning-bg bg-[url(~@/app/components/workflow/run/assets/bg-line-warning.svg)] shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.5),inset_0_1px_3px_0_rgba(0,0,0,0.12),inset_0_2px_24px_0_rgba(247,144,9,0.2),0_1px_2px_0_rgba(9,9,11,0.05),0_0_0_1px_rgba(0,0,0,0.05)] text-text-destructive',
|
||||
status === 'running' && 'border-[rgba(11,165,236,0.8)] bg-workflow-display-normal-bg bg-[url(~@/app/components/workflow/run/assets/bg-line-running.svg)] shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.5),inset_0_1px_3px_0_rgba(0,0,0,0.12),inset_0_2px_24px_0_rgba(11,165,236,0.2),0_1px_2px_0_rgba(9,9,11,0.05),0_0_0_1px_rgba(0,0,0,0.05)] text-util-colors-blue-light-blue-light-600',
|
||||
status === 'succeeded' && 'border-[rgba(23,178,106,0.8)] bg-workflow-display-success-bg bg-[url(~@/app/components/workflow/run/assets/bg-line-success.svg)] text-text-success',
|
||||
status === 'succeeded' && theme === Theme.light && 'shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.5),inset_0_1px_3px_0_rgba(0,0,0,0.12),inset_0_2px_24px_0_rgba(23,178,106,0.2),0_1px_2px_0_rgba(9,9,11,0.05),0_0_0_1px_rgba(0,0,0,0.05)]',
|
||||
status === 'succeeded' && theme === Theme.dark && 'shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.12),inset_0_1px_3px_0_rgba(0,0,0,0.4),inset_0_2px_24px_0_rgba(23,178,106,0.25),0_1px_2px_0_rgba(0,0,0,0.1),0_0_0_1px_rgba(24, 24, 27, 0.95)]',
|
||||
status === 'partial-succeeded' && 'border-[rgba(23,178,106,0.8)] bg-workflow-display-success-bg bg-[url(~@/app/components/workflow/run/assets/bg-line-success.svg)] text-text-success',
|
||||
status === 'partial-succeeded' && theme === Theme.light && 'shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.5),inset_0_1px_3px_0_rgba(0,0,0,0.12),inset_0_2px_24px_0_rgba(23,178,106,0.2),0_1px_2px_0_rgba(9,9,11,0.05),0_0_0_1px_rgba(0,0,0,0.05)]',
|
||||
status === 'partial-succeeded' && theme === Theme.dark && 'shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.12),inset_0_1px_3px_0_rgba(0,0,0,0.4),inset_0_2px_24px_0_rgba(23,178,106,0.25),0_1px_2px_0_rgba(0,0,0,0.1),0_0_0_1px_rgba(24, 24, 27, 0.95)]',
|
||||
status === 'failed' && 'border-[rgba(240,68,56,0.8)] bg-workflow-display-error-bg bg-[url(~@/app/components/workflow/run/assets/bg-line-error.svg)] text-text-warning',
|
||||
status === 'failed' && theme === Theme.light && 'shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.5),inset_0_1px_3px_0_rgba(0,0,0,0.12),inset_0_2px_24px_0_rgba(240,68,56,0.2),0_1px_2px_0_rgba(9,9,11,0.05),0_0_0_1px_rgba(0,0,0,0.05)]',
|
||||
status === 'failed' && theme === Theme.dark && 'shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.12),inset_0_1px_3px_0_rgba(0,0,0,0.4),inset_0_2px_24px_0_rgba(240,68,56,0.25),0_1px_2px_0_rgba(0,0,0,0.1),0_0_0_1px_rgba(24, 24, 27, 0.95)]',
|
||||
status === 'stopped' && 'border-[rgba(247,144,9,0.8)] bg-workflow-display-warning-bg bg-[url(~@/app/components/workflow/run/assets/bg-line-warning.svg)] text-text-destructive',
|
||||
status === 'stopped' && theme === Theme.light && 'shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.5),inset_0_1px_3px_0_rgba(0,0,0,0.12),inset_0_2px_24px_0_rgba(247,144,9,0.2),0_1px_2px_0_rgba(9,9,11,0.05),0_0_0_1px_rgba(0,0,0,0.05)]',
|
||||
status === 'stopped' && theme === Theme.dark && 'shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.12),inset_0_1px_3px_0_rgba(0,0,0,0.4),inset_0_2px_24px_0_rgba(247,144,9,0.25),0_1px_2px_0_rgba(0,0,0,0.1),0_0_0_1px_rgba(24, 24, 27, 0.95)]',
|
||||
status === 'exception' && 'border-[rgba(247,144,9,0.8)] bg-workflow-display-warning-bg bg-[url(~@/app/components/workflow/run/assets/bg-line-warning.svg)] text-text-destructive',
|
||||
status === 'exception' && theme === Theme.light && 'shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.5),inset_0_1px_3px_0_rgba(0,0,0,0.12),inset_0_2px_24px_0_rgba(247,144,9,0.2),0_1px_2px_0_rgba(9,9,11,0.05),0_0_0_1px_rgba(0,0,0,0.05)]',
|
||||
status === 'exception' && theme === Theme.dark && 'shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.12),inset_0_1px_3px_0_rgba(0,0,0,0.4),inset_0_2px_24px_0_rgba(247,144,9,0.25),0_1px_2px_0_rgba(0,0,0,0.1),0_0_0_1px_rgba(24, 24, 27, 0.95)]',
|
||||
status === 'running' && 'border-[rgba(11,165,236,0.8)] bg-workflow-display-normal-bg bg-[url(~@/app/components/workflow/run/assets/bg-line-running.svg)] text-util-colors-blue-light-blue-light-600',
|
||||
status === 'running' && theme === Theme.light && 'shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.5),inset_0_1px_3px_0_rgba(0,0,0,0.12),inset_0_2px_24px_0_rgba(11,165,236,0.2),0_1px_2px_0_rgba(9,9,11,0.05),0_0_0_1px_rgba(0,0,0,0.05)]',
|
||||
status === 'running' && theme === Theme.dark && 'shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.12),inset_0_1px_3px_0_rgba(0,0,0,0.4),inset_0_2px_24px_0_rgba(11,165,236,0.25),0_1px_2px_0_rgba(0,0,0,0.1),0_0_0_1px_rgba(24, 24, 27, 0.95)]',
|
||||
)}
|
||||
>
|
||||
<div className='absolute top-0 left-0 w-[65%] h-[50px] bg-[url(~@/app/components/workflow/run/assets/highlight.svg)]'></div>
|
||||
<div className={cn(
|
||||
'absolute top-0 left-0 w-[65%] h-[50px] bg-no-repeat',
|
||||
theme === Theme.light && 'bg-[url(~@/app/components/workflow/run/assets/highlight.svg)]',
|
||||
theme === Theme.dark && 'bg-[url(~@/app/components/workflow/run/assets/highlight-dark.svg)]',
|
||||
)}></div>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -12,162 +12,27 @@ import {
|
||||
RiMenu4Line,
|
||||
} from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useLogs } from './hooks'
|
||||
import NodePanel from './node'
|
||||
import {
|
||||
BlockEnum,
|
||||
} from '@/app/components/workflow/types'
|
||||
import type { IterationDurationMap, NodeTracing } from '@/types/workflow'
|
||||
import SpecialResultPanel from './special-result-panel'
|
||||
import type { NodeTracing } from '@/types/workflow'
|
||||
import formatNodeList from '@/app/components/workflow/run/utils/format-log'
|
||||
|
||||
type TracingPanelProps = {
|
||||
list: NodeTracing[]
|
||||
onShowIterationDetail?: (detail: NodeTracing[][], iterDurationMap: IterationDurationMap) => void
|
||||
onShowRetryDetail?: (detail: NodeTracing[]) => void
|
||||
className?: string
|
||||
hideNodeInfo?: boolean
|
||||
hideNodeProcessDetail?: boolean
|
||||
}
|
||||
|
||||
type TracingNodeProps = {
|
||||
id: string
|
||||
uniqueId: string
|
||||
isParallel: boolean
|
||||
data: NodeTracing | null
|
||||
children: TracingNodeProps[]
|
||||
parallelTitle?: string
|
||||
branchTitle?: string
|
||||
hideNodeInfo?: boolean
|
||||
hideNodeProcessDetail?: boolean
|
||||
}
|
||||
|
||||
function buildLogTree(nodes: NodeTracing[], t: (key: string) => string): TracingNodeProps[] {
|
||||
const rootNodes: TracingNodeProps[] = []
|
||||
const parallelStacks: { [key: string]: TracingNodeProps } = {}
|
||||
const levelCounts: { [key: string]: number } = {}
|
||||
const parallelChildCounts: { [key: string]: Set<string> } = {}
|
||||
let uniqueIdCounter = 0
|
||||
const getUniqueId = () => {
|
||||
uniqueIdCounter++
|
||||
return `unique-${uniqueIdCounter}`
|
||||
}
|
||||
|
||||
const getParallelTitle = (parentId: string | null): string => {
|
||||
const levelKey = parentId || 'root'
|
||||
if (!levelCounts[levelKey])
|
||||
levelCounts[levelKey] = 0
|
||||
|
||||
levelCounts[levelKey]++
|
||||
|
||||
const parentTitle = parentId ? parallelStacks[parentId]?.parallelTitle : ''
|
||||
const levelNumber = parentTitle ? parseInt(parentTitle.split('-')[1]) + 1 : 1
|
||||
const letter = parallelChildCounts[levelKey]?.size > 1 ? String.fromCharCode(64 + levelCounts[levelKey]) : ''
|
||||
return `${t('workflow.common.parallel')}-${levelNumber}${letter}`
|
||||
}
|
||||
|
||||
const getBranchTitle = (parentId: string | null, branchNum: number): string => {
|
||||
const levelKey = parentId || 'root'
|
||||
const parentTitle = parentId ? parallelStacks[parentId]?.parallelTitle : ''
|
||||
const levelNumber = parentTitle ? parseInt(parentTitle.split('-')[1]) + 1 : 1
|
||||
const letter = parallelChildCounts[levelKey]?.size > 1 ? String.fromCharCode(64 + levelCounts[levelKey]) : ''
|
||||
const branchLetter = String.fromCharCode(64 + branchNum)
|
||||
return `${t('workflow.common.branch')}-${levelNumber}${letter}-${branchLetter}`
|
||||
}
|
||||
|
||||
// Count parallel children (for figuring out if we need to use letters)
|
||||
for (const node of nodes) {
|
||||
const parent_parallel_id = node.parent_parallel_id ?? node.execution_metadata?.parent_parallel_id ?? null
|
||||
const parallel_id = node.parallel_id ?? node.execution_metadata?.parallel_id ?? null
|
||||
|
||||
if (parallel_id) {
|
||||
const parentKey = parent_parallel_id || 'root'
|
||||
if (!parallelChildCounts[parentKey])
|
||||
parallelChildCounts[parentKey] = new Set()
|
||||
|
||||
parallelChildCounts[parentKey].add(parallel_id)
|
||||
}
|
||||
}
|
||||
|
||||
for (const node of nodes) {
|
||||
const parallel_id = node.parallel_id ?? node.execution_metadata?.parallel_id ?? null
|
||||
const parent_parallel_id = node.parent_parallel_id ?? node.execution_metadata?.parent_parallel_id ?? null
|
||||
const parallel_start_node_id = node.parallel_start_node_id ?? node.execution_metadata?.parallel_start_node_id ?? null
|
||||
const parent_parallel_start_node_id = node.parent_parallel_start_node_id ?? node.execution_metadata?.parent_parallel_start_node_id ?? null
|
||||
|
||||
if (!parallel_id || node.node_type === BlockEnum.End) {
|
||||
rootNodes.push({
|
||||
id: node.id,
|
||||
uniqueId: getUniqueId(),
|
||||
isParallel: false,
|
||||
data: node,
|
||||
children: [],
|
||||
})
|
||||
}
|
||||
else {
|
||||
if (!parallelStacks[parallel_id]) {
|
||||
const newParallelGroup: TracingNodeProps = {
|
||||
id: parallel_id,
|
||||
uniqueId: getUniqueId(),
|
||||
isParallel: true,
|
||||
data: null,
|
||||
children: [],
|
||||
parallelTitle: '',
|
||||
}
|
||||
parallelStacks[parallel_id] = newParallelGroup
|
||||
|
||||
if (parent_parallel_id && parallelStacks[parent_parallel_id]) {
|
||||
const sameBranchIndex = parallelStacks[parent_parallel_id].children.findLastIndex(c =>
|
||||
c.data?.execution_metadata?.parallel_start_node_id === parent_parallel_start_node_id || c.data?.parallel_start_node_id === parent_parallel_start_node_id,
|
||||
)
|
||||
parallelStacks[parent_parallel_id].children.splice(sameBranchIndex + 1, 0, newParallelGroup)
|
||||
newParallelGroup.parallelTitle = getParallelTitle(parent_parallel_id)
|
||||
}
|
||||
else {
|
||||
newParallelGroup.parallelTitle = getParallelTitle(parent_parallel_id)
|
||||
rootNodes.push(newParallelGroup)
|
||||
}
|
||||
}
|
||||
const branchTitle = parallel_start_node_id === node.node_id ? getBranchTitle(parent_parallel_id, parallelStacks[parallel_id].children.length + 1) : ''
|
||||
if (branchTitle) {
|
||||
parallelStacks[parallel_id].children.push({
|
||||
id: node.id,
|
||||
uniqueId: getUniqueId(),
|
||||
isParallel: false,
|
||||
data: node,
|
||||
children: [],
|
||||
branchTitle,
|
||||
})
|
||||
}
|
||||
else {
|
||||
let sameBranchIndex = parallelStacks[parallel_id].children.findLastIndex(c =>
|
||||
c.data?.execution_metadata?.parallel_start_node_id === parallel_start_node_id || c.data?.parallel_start_node_id === parallel_start_node_id,
|
||||
)
|
||||
if (parallelStacks[parallel_id].children[sameBranchIndex + 1]?.isParallel)
|
||||
sameBranchIndex++
|
||||
|
||||
parallelStacks[parallel_id].children.splice(sameBranchIndex + 1, 0, {
|
||||
id: node.id,
|
||||
uniqueId: getUniqueId(),
|
||||
isParallel: false,
|
||||
data: node,
|
||||
children: [],
|
||||
branchTitle,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return rootNodes
|
||||
}
|
||||
|
||||
const TracingPanel: FC<TracingPanelProps> = ({
|
||||
list,
|
||||
onShowIterationDetail,
|
||||
onShowRetryDetail,
|
||||
className,
|
||||
hideNodeInfo = false,
|
||||
hideNodeProcessDetail = false,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const treeNodes = buildLogTree(list, t)
|
||||
const treeNodes = formatNodeList(list, t)
|
||||
const [collapsedNodes, setCollapsedNodes] = useState<Set<string>>(new Set())
|
||||
const [hoveredParallel, setHoveredParallel] = useState<string | null>(null)
|
||||
|
||||
@@ -203,13 +68,34 @@ const TracingPanel: FC<TracingPanelProps> = ({
|
||||
}
|
||||
}, [])
|
||||
|
||||
const renderNode = (node: TracingNodeProps) => {
|
||||
if (node.isParallel) {
|
||||
const {
|
||||
showSpecialResultPanel,
|
||||
|
||||
showRetryDetail,
|
||||
setShowRetryDetailFalse,
|
||||
retryResultList,
|
||||
handleShowRetryResultList,
|
||||
|
||||
showIteratingDetail,
|
||||
setShowIteratingDetailFalse,
|
||||
iterationResultList,
|
||||
iterationResultDurationMap,
|
||||
handleShowIterationResultList,
|
||||
|
||||
agentOrToolLogItemStack,
|
||||
agentOrToolLogListMap,
|
||||
handleShowAgentOrToolLog,
|
||||
} = useLogs()
|
||||
|
||||
const renderNode = (node: NodeTracing) => {
|
||||
const isParallelFirstNode = !!node.parallelDetail?.isParallelStartNode
|
||||
if (isParallelFirstNode) {
|
||||
const parallelDetail = node.parallelDetail!
|
||||
const isCollapsed = collapsedNodes.has(node.id)
|
||||
const isHovered = hoveredParallel === node.id
|
||||
return (
|
||||
<div
|
||||
key={node.uniqueId}
|
||||
key={node.id}
|
||||
className="ml-4 mb-2 relative"
|
||||
data-parallel-id={node.id}
|
||||
onMouseEnter={() => handleParallelMouseEnter(node.id)}
|
||||
@@ -226,10 +112,10 @@ const TracingPanel: FC<TracingPanelProps> = ({
|
||||
{isHovered ? <RiArrowDownSLine className="w-3 h-3" /> : <RiMenu4Line className="w-3 h-3 text-text-tertiary" />}
|
||||
</button>
|
||||
<div className="system-xs-semibold-uppercase text-text-secondary flex items-center">
|
||||
<span>{node.parallelTitle}</span>
|
||||
<span>{parallelDetail.parallelTitle}</span>
|
||||
</div>
|
||||
<div
|
||||
className="mx-2 flex-grow h-px bg-divider-subtle"
|
||||
className="mx-2 grow h-px bg-divider-subtle"
|
||||
style={{ background: 'linear-gradient(to right, rgba(16, 24, 40, 0.08), rgba(255, 255, 255, 0)' }}
|
||||
></div>
|
||||
</div>
|
||||
@@ -238,7 +124,7 @@ const TracingPanel: FC<TracingPanelProps> = ({
|
||||
'absolute top-0 bottom-0 left-[5px] w-[2px]',
|
||||
isHovered ? 'bg-text-accent-secondary' : 'bg-divider-subtle',
|
||||
)}></div>
|
||||
{node.children.map(renderNode)}
|
||||
{parallelDetail.children!.map(renderNode)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -246,16 +132,15 @@ const TracingPanel: FC<TracingPanelProps> = ({
|
||||
else {
|
||||
const isHovered = hoveredParallel === node.id
|
||||
return (
|
||||
<div key={node.uniqueId}>
|
||||
<div key={node.id}>
|
||||
<div className={cn('pl-4 -mb-1.5 system-2xs-medium-uppercase', isHovered ? 'text-text-tertiary' : 'text-text-quaternary')}>
|
||||
{node.branchTitle}
|
||||
{node?.parallelDetail?.branchTitle}
|
||||
</div>
|
||||
<NodePanel
|
||||
nodeInfo={node.data!}
|
||||
onShowIterationDetail={onShowIterationDetail}
|
||||
onShowRetryDetail={onShowRetryDetail}
|
||||
justShowIterationNavArrow={true}
|
||||
justShowRetryNavArrow={true}
|
||||
nodeInfo={node!}
|
||||
onShowIterationDetail={handleShowIterationResultList}
|
||||
onShowRetryDetail={handleShowRetryResultList}
|
||||
onShowAgentOrToolLog={handleShowAgentOrToolLog}
|
||||
hideInfo={hideNodeInfo}
|
||||
hideProcessDetail={hideNodeProcessDetail}
|
||||
/>
|
||||
@@ -264,8 +149,33 @@ const TracingPanel: FC<TracingPanelProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
if (showSpecialResultPanel) {
|
||||
return (
|
||||
<SpecialResultPanel
|
||||
showRetryDetail={showRetryDetail}
|
||||
setShowRetryDetailFalse={setShowRetryDetailFalse}
|
||||
retryResultList={retryResultList}
|
||||
|
||||
showIteratingDetail={showIteratingDetail}
|
||||
setShowIteratingDetailFalse={setShowIteratingDetailFalse}
|
||||
iterationResultList={iterationResultList}
|
||||
iterationResultDurationMap={iterationResultDurationMap}
|
||||
|
||||
agentOrToolLogItemStack={agentOrToolLogItemStack}
|
||||
agentOrToolLogListMap={agentOrToolLogListMap}
|
||||
handleShowAgentOrToolLog={handleShowAgentOrToolLog}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn(className || 'bg-components-panel-bg', 'py-2')}>
|
||||
<div
|
||||
className={cn(className || 'bg-components-panel-bg', 'py-2')}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
e.nativeEvent.stopImmediatePropagation()
|
||||
}}
|
||||
>
|
||||
{treeNodes.map(renderNode)}
|
||||
</div>
|
||||
)
|
||||
|
||||
182
web/app/components/workflow/run/utils/format-log/agent/data.ts
Normal file
182
web/app/components/workflow/run/utils/format-log/agent/data.ts
Normal file
@@ -0,0 +1,182 @@
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
|
||||
export const agentNodeData = (() => {
|
||||
const node = {
|
||||
node_type: BlockEnum.Agent,
|
||||
execution_metadata: {
|
||||
agent_log: [
|
||||
{ id: '1', label: 'Root 1' },
|
||||
{ id: '2', parent_id: '1', label: 'Child 1.2' },
|
||||
{ id: '3', parent_id: '1', label: 'Child 1.3' },
|
||||
{ id: '4', parent_id: '2', label: 'Child 2.4' },
|
||||
{ id: '5', parent_id: '2', label: 'Child 2.5' },
|
||||
{ id: '6', parent_id: '3', label: 'Child 3.6' },
|
||||
{ id: '7', parent_id: '4', label: 'Child 4.7' },
|
||||
{ id: '8', parent_id: '4', label: 'Child 4.8' },
|
||||
{ id: '9', parent_id: '5', label: 'Child 5.9' },
|
||||
{ id: '10', parent_id: '5', label: 'Child 5.10' },
|
||||
{ id: '11', parent_id: '7', label: 'Child 7.11' },
|
||||
{ id: '12', parent_id: '7', label: 'Child 7.12' },
|
||||
{ id: '13', parent_id: '9', label: 'Child 9.13' },
|
||||
{ id: '14', parent_id: '9', label: 'Child 9.14' },
|
||||
{ id: '15', parent_id: '9', label: 'Child 9.15' },
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
in: [node],
|
||||
expect: [{
|
||||
...node,
|
||||
agentLog: [
|
||||
{
|
||||
id: '1',
|
||||
label: 'Root 1',
|
||||
children: [
|
||||
{
|
||||
id: '2',
|
||||
parent_id: '1',
|
||||
label: 'Child 1.2',
|
||||
children: [
|
||||
{
|
||||
id: '4',
|
||||
parent_id: '2',
|
||||
label: 'Child 2.4',
|
||||
children: [
|
||||
{
|
||||
id: '7',
|
||||
parent_id: '4',
|
||||
label: 'Child 4.7',
|
||||
children: [
|
||||
{ id: '11', parent_id: '7', label: 'Child 7.11' },
|
||||
{ id: '12', parent_id: '7', label: 'Child 7.12' },
|
||||
],
|
||||
},
|
||||
{ id: '8', parent_id: '4', label: 'Child 4.8' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
parent_id: '2',
|
||||
label: 'Child 2.5',
|
||||
children: [
|
||||
{
|
||||
id: '9',
|
||||
parent_id: '5',
|
||||
label: 'Child 5.9',
|
||||
children: [
|
||||
{ id: '13', parent_id: '9', label: 'Child 9.13' },
|
||||
{ id: '14', parent_id: '9', label: 'Child 9.14' },
|
||||
{ id: '15', parent_id: '9', label: 'Child 9.15' },
|
||||
],
|
||||
},
|
||||
{ id: '10', parent_id: '5', label: 'Child 5.10' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
parent_id: '1',
|
||||
label: 'Child 1.3',
|
||||
children: [
|
||||
{ id: '6', parent_id: '3', label: 'Child 3.6' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}],
|
||||
}
|
||||
})()
|
||||
|
||||
export const oneStepCircle = (() => {
|
||||
const node = {
|
||||
node_type: BlockEnum.Agent,
|
||||
execution_metadata: {
|
||||
agent_log: [
|
||||
{ id: '1', label: 'Node 1' },
|
||||
{ id: '1', parent_id: '1', label: 'Node 1' },
|
||||
{ id: '1', parent_id: '1', label: 'Node 1' },
|
||||
{ id: '1', parent_id: '1', label: 'Node 1' },
|
||||
{ id: '1', parent_id: '1', label: 'Node 1' },
|
||||
{ id: '1', parent_id: '1', label: 'Node 1' },
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
in: [node],
|
||||
expect: [{
|
||||
...node,
|
||||
agentLog: [
|
||||
{
|
||||
id: '1',
|
||||
label: 'Node 1',
|
||||
hasCircle: true,
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
}],
|
||||
}
|
||||
})()
|
||||
|
||||
export const multiStepsCircle = (() => {
|
||||
const node = {
|
||||
node_type: BlockEnum.Agent,
|
||||
execution_metadata: {
|
||||
agent_log: [
|
||||
// 1 -> [2 -> 4 -> 1, 3]
|
||||
{ id: '1', label: 'Node 1' },
|
||||
{ id: '2', parent_id: '1', label: 'Node 2' },
|
||||
{ id: '3', parent_id: '1', label: 'Node 3' },
|
||||
{ id: '4', parent_id: '2', label: 'Node 4' },
|
||||
|
||||
// Loop
|
||||
{ id: '1', parent_id: '4', label: 'Node 1' },
|
||||
{ id: '2', parent_id: '1', label: 'Node 2' },
|
||||
{ id: '4', parent_id: '2', label: 'Node 4' },
|
||||
{ id: '1', parent_id: '4', label: 'Node 1' },
|
||||
{ id: '2', parent_id: '1', label: 'Node 2' },
|
||||
{ id: '4', parent_id: '2', label: 'Node 4' },
|
||||
],
|
||||
},
|
||||
}
|
||||
// 1 -> [2(4(1(2(4...)))), 3]
|
||||
return {
|
||||
in: [node],
|
||||
expect: [{
|
||||
...node,
|
||||
agentLog: [
|
||||
{
|
||||
id: '1',
|
||||
label: 'Node 1',
|
||||
children: [
|
||||
{
|
||||
id: '2',
|
||||
parent_id: '1',
|
||||
label: 'Node 2',
|
||||
children: [
|
||||
{
|
||||
id: '4',
|
||||
parent_id: '2',
|
||||
label: 'Node 4',
|
||||
children: [],
|
||||
hasCircle: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
parent_id: '1',
|
||||
label: 'Node 3',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}],
|
||||
}
|
||||
})()
|
||||
|
||||
export const CircleNestCircle = (() => {
|
||||
})()
|
||||
@@ -0,0 +1,15 @@
|
||||
import format from '.'
|
||||
import { agentNodeData, multiStepsCircle, oneStepCircle } from './data'
|
||||
|
||||
describe('agent', () => {
|
||||
test('list should transform to tree', () => {
|
||||
// console.log(format(agentNodeData.in as any))
|
||||
expect(format(agentNodeData.in as any)).toEqual(agentNodeData.expect)
|
||||
})
|
||||
|
||||
test('list should remove circle log item', () => {
|
||||
// format(oneStepCircle.in as any)
|
||||
expect(format(oneStepCircle.in as any)).toEqual(oneStepCircle.expect)
|
||||
expect(format(multiStepsCircle.in as any)).toEqual(multiStepsCircle.expect)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,99 @@
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import type { AgentLogItem, AgentLogItemWithChildren, NodeTracing } from '@/types/workflow'
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
|
||||
const supportedAgentLogNodes = [BlockEnum.Agent, BlockEnum.Tool]
|
||||
|
||||
const remove = (node: AgentLogItemWithChildren, removeId: string) => {
|
||||
let { children } = node
|
||||
if (!children || children.length === 0)
|
||||
return
|
||||
|
||||
const hasCircle = !!children.find(c => c.id === removeId)
|
||||
if (hasCircle) {
|
||||
node.hasCircle = true
|
||||
node.children = node.children.filter(c => c.id !== removeId)
|
||||
children = node.children
|
||||
}
|
||||
|
||||
children.forEach((child) => {
|
||||
remove(child, removeId)
|
||||
})
|
||||
}
|
||||
|
||||
const removeRepeatedSiblings = (list: AgentLogItemWithChildren[]) => {
|
||||
if (!list || list.length === 0)
|
||||
return []
|
||||
|
||||
const result: AgentLogItemWithChildren[] = []
|
||||
const addedItemIds: string[] = []
|
||||
list.forEach((item) => {
|
||||
if (!addedItemIds.includes(item.id)) {
|
||||
result.push(item)
|
||||
addedItemIds.push(item.id)
|
||||
}
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
const removeCircleLogItem = (log: AgentLogItemWithChildren) => {
|
||||
const newLog = cloneDeep(log)
|
||||
newLog.children = removeRepeatedSiblings(newLog.children)
|
||||
let { id, children } = newLog
|
||||
if (!children || children.length === 0)
|
||||
return log
|
||||
|
||||
// check one step circle
|
||||
const hasOneStepCircle = !!children.find(c => c.id === id)
|
||||
if (hasOneStepCircle) {
|
||||
newLog.hasCircle = true
|
||||
newLog.children = newLog.children.filter(c => c.id !== id)
|
||||
children = newLog.children
|
||||
}
|
||||
|
||||
children.forEach((child, index) => {
|
||||
remove(child, id) // check multi steps circle
|
||||
children[index] = removeCircleLogItem(child)
|
||||
})
|
||||
return newLog
|
||||
}
|
||||
|
||||
const listToTree = (logs: AgentLogItem[]) => {
|
||||
if (!logs || logs.length === 0)
|
||||
return []
|
||||
|
||||
const tree: AgentLogItemWithChildren[] = []
|
||||
logs.forEach((log) => {
|
||||
const hasParent = !!log.parent_id
|
||||
if (hasParent) {
|
||||
const parent = logs.find(item => item.id === log.parent_id) as AgentLogItemWithChildren
|
||||
if (parent) {
|
||||
if (!parent.children)
|
||||
parent.children = []
|
||||
parent.children.push(log as AgentLogItemWithChildren)
|
||||
}
|
||||
}
|
||||
else {
|
||||
tree.push(log as AgentLogItemWithChildren)
|
||||
}
|
||||
})
|
||||
return tree
|
||||
}
|
||||
|
||||
const format = (list: NodeTracing[]): NodeTracing[] => {
|
||||
const result: NodeTracing[] = list.map((item) => {
|
||||
let treeList: AgentLogItemWithChildren[] = []
|
||||
let removedCircleTree: AgentLogItemWithChildren[] = []
|
||||
if (supportedAgentLogNodes.includes(item.node_type) && item.execution_metadata?.agent_log && item.execution_metadata?.agent_log.length > 0)
|
||||
treeList = listToTree(item.execution_metadata.agent_log)
|
||||
// console.log(JSON.stringify(treeList))
|
||||
removedCircleTree = treeList.length > 0 ? treeList.map(t => removeCircleLogItem(t)) : []
|
||||
item.agentLog = removedCircleTree
|
||||
|
||||
return item
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export default format
|
||||
@@ -0,0 +1,128 @@
|
||||
import parseDSL from './graph-to-log-struct'
|
||||
|
||||
describe('parseDSL', () => {
|
||||
it('should parse plain nodes correctly', () => {
|
||||
const dsl = 'plainNode1 -> plainNode2'
|
||||
const result = parseDSL(dsl)
|
||||
expect(result).toEqual([
|
||||
{ id: 'plainNode1', node_id: 'plainNode1', title: 'plainNode1', execution_metadata: {}, status: 'succeeded' },
|
||||
{ id: 'plainNode2', node_id: 'plainNode2', title: 'plainNode2', execution_metadata: {}, status: 'succeeded' },
|
||||
])
|
||||
})
|
||||
|
||||
it('should parse retry nodes correctly', () => {
|
||||
const dsl = '(retry, retryNode, 3)'
|
||||
const result = parseDSL(dsl)
|
||||
expect(result).toEqual([
|
||||
{ id: 'retryNode', node_id: 'retryNode', title: 'retryNode', execution_metadata: {}, status: 'succeeded' },
|
||||
{ id: 'retryNode', node_id: 'retryNode', title: 'retryNode', execution_metadata: {}, status: 'retry' },
|
||||
{ id: 'retryNode', node_id: 'retryNode', title: 'retryNode', execution_metadata: {}, status: 'retry' },
|
||||
{ id: 'retryNode', node_id: 'retryNode', title: 'retryNode', execution_metadata: {}, status: 'retry' },
|
||||
])
|
||||
})
|
||||
|
||||
it('should parse iteration nodes correctly', () => {
|
||||
const dsl = '(iteration, iterationNode, plainNode1 -> plainNode2)'
|
||||
const result = parseDSL(dsl)
|
||||
expect(result).toEqual([
|
||||
{ id: 'iterationNode', node_id: 'iterationNode', title: 'iterationNode', node_type: 'iteration', execution_metadata: {}, status: 'succeeded' },
|
||||
{ id: 'plainNode1', node_id: 'plainNode1', title: 'plainNode1', execution_metadata: { iteration_id: 'iterationNode', iteration_index: 0 }, status: 'succeeded' },
|
||||
{ id: 'plainNode2', node_id: 'plainNode2', title: 'plainNode2', execution_metadata: { iteration_id: 'iterationNode', iteration_index: 0 }, status: 'succeeded' },
|
||||
])
|
||||
})
|
||||
|
||||
it('should parse parallel nodes correctly', () => {
|
||||
const dsl = '(parallel, parallelNode, nodeA, nodeB -> nodeC)'
|
||||
const result = parseDSL(dsl)
|
||||
expect(result).toEqual([
|
||||
{ id: 'parallelNode', node_id: 'parallelNode', title: 'parallelNode', execution_metadata: { parallel_id: 'parallelNode' }, status: 'succeeded' },
|
||||
{ id: 'nodeA', node_id: 'nodeA', title: 'nodeA', execution_metadata: { parallel_id: 'parallelNode', parallel_start_node_id: 'nodeA' }, status: 'succeeded' },
|
||||
{ id: 'nodeB', node_id: 'nodeB', title: 'nodeB', execution_metadata: { parallel_id: 'parallelNode', parallel_start_node_id: 'nodeB' }, status: 'succeeded' },
|
||||
{ id: 'nodeC', node_id: 'nodeC', title: 'nodeC', execution_metadata: { parallel_id: 'parallelNode', parallel_start_node_id: 'nodeB' }, status: 'succeeded' },
|
||||
])
|
||||
})
|
||||
|
||||
// TODO
|
||||
it('should handle nested parallel nodes', () => {
|
||||
const dsl = '(parallel, outerParallel, (parallel, innerParallel, plainNode1 -> plainNode2) -> plainNode3)'
|
||||
const result = parseDSL(dsl)
|
||||
expect(result).toEqual([
|
||||
{
|
||||
id: 'outerParallel',
|
||||
node_id: 'outerParallel',
|
||||
title: 'outerParallel',
|
||||
execution_metadata: { parallel_id: 'outerParallel' },
|
||||
status: 'succeeded',
|
||||
},
|
||||
{
|
||||
id: 'innerParallel',
|
||||
node_id: 'innerParallel',
|
||||
title: 'innerParallel',
|
||||
execution_metadata: { parallel_id: 'outerParallel', parallel_start_node_id: 'innerParallel' },
|
||||
status: 'succeeded',
|
||||
},
|
||||
{
|
||||
id: 'plainNode1',
|
||||
node_id: 'plainNode1',
|
||||
title: 'plainNode1',
|
||||
execution_metadata: {
|
||||
parallel_id: 'innerParallel',
|
||||
parallel_start_node_id: 'plainNode1',
|
||||
parent_parallel_id: 'outerParallel',
|
||||
parent_parallel_start_node_id: 'innerParallel',
|
||||
},
|
||||
status: 'succeeded',
|
||||
},
|
||||
{
|
||||
id: 'plainNode2',
|
||||
node_id: 'plainNode2',
|
||||
title: 'plainNode2',
|
||||
execution_metadata: {
|
||||
parallel_id: 'innerParallel',
|
||||
parallel_start_node_id: 'plainNode1',
|
||||
parent_parallel_id: 'outerParallel',
|
||||
parent_parallel_start_node_id: 'innerParallel',
|
||||
},
|
||||
status: 'succeeded',
|
||||
},
|
||||
{
|
||||
id: 'plainNode3',
|
||||
node_id: 'plainNode3',
|
||||
title: 'plainNode3',
|
||||
execution_metadata: {
|
||||
parallel_id: 'outerParallel',
|
||||
parallel_start_node_id: 'plainNode3',
|
||||
},
|
||||
status: 'succeeded',
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
// iterations not support nested iterations
|
||||
// it('should handle nested iterations', () => {
|
||||
// const dsl = '(iteration, outerIteration, (iteration, innerIteration -> plainNode1 -> plainNode2))'
|
||||
// const result = parseDSL(dsl)
|
||||
// expect(result).toEqual([
|
||||
// { id: 'outerIteration', node_id: 'outerIteration', title: 'outerIteration', node_type: 'iteration', execution_metadata: {}, status: 'succeeded' },
|
||||
// { id: 'innerIteration', node_id: 'innerIteration', title: 'innerIteration', node_type: 'iteration', execution_metadata: { iteration_id: 'outerIteration', iteration_index: 0 }, status: 'succeeded' },
|
||||
// { id: 'plainNode1', node_id: 'plainNode1', title: 'plainNode1', execution_metadata: { iteration_id: 'innerIteration', iteration_index: 0 }, status: 'succeeded' },
|
||||
// { id: 'plainNode2', node_id: 'plainNode2', title: 'plainNode2', execution_metadata: { iteration_id: 'innerIteration', iteration_index: 0 }, status: 'succeeded' },
|
||||
// ])
|
||||
// })
|
||||
|
||||
it('should handle nested iterations within parallel nodes', () => {
|
||||
const dsl = '(parallel, parallelNode, (iteration, iterationNode, plainNode1, plainNode2))'
|
||||
const result = parseDSL(dsl)
|
||||
expect(result).toEqual([
|
||||
{ id: 'parallelNode', node_id: 'parallelNode', title: 'parallelNode', execution_metadata: { parallel_id: 'parallelNode' }, status: 'succeeded' },
|
||||
{ id: 'iterationNode', node_id: 'iterationNode', title: 'iterationNode', node_type: 'iteration', execution_metadata: { parallel_id: 'parallelNode', parallel_start_node_id: 'iterationNode' }, status: 'succeeded' },
|
||||
{ id: 'plainNode1', node_id: 'plainNode1', title: 'plainNode1', execution_metadata: { iteration_id: 'iterationNode', iteration_index: 0, parallel_id: 'parallelNode', parallel_start_node_id: 'iterationNode' }, status: 'succeeded' },
|
||||
{ id: 'plainNode2', node_id: 'plainNode2', title: 'plainNode2', execution_metadata: { iteration_id: 'iterationNode', iteration_index: 0, parallel_id: 'parallelNode', parallel_start_node_id: 'iterationNode' }, status: 'succeeded' },
|
||||
])
|
||||
})
|
||||
|
||||
it('should throw an error for unknown node types', () => {
|
||||
const dsl = '(unknown, nodeId)'
|
||||
expect(() => parseDSL(dsl)).toThrowError('Unknown nodeType: unknown')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,304 @@
|
||||
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 Node = NodePlain | NodeComplex
|
||||
|
||||
/**
|
||||
* Parses a DSL string into an array of node objects.
|
||||
* @param dsl - The input DSL string.
|
||||
* @returns An array of parsed nodes.
|
||||
*/
|
||||
function parseDSL(dsl: string): NodeData[] {
|
||||
return convertToNodeData(parseTopLevelFlow(dsl).map(nodeStr => parseNode(nodeStr)))
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits a top-level flow string by "->", respecting nested structures.
|
||||
* @param dsl - The DSL string to split.
|
||||
* @returns An array of top-level segments.
|
||||
*/
|
||||
function parseTopLevelFlow(dsl: string): string[] {
|
||||
const segments: string[] = []
|
||||
let buffer = ''
|
||||
let nested = 0
|
||||
|
||||
for (let i = 0; i < dsl.length; i++) {
|
||||
const char = dsl[i]
|
||||
if (char === '(') nested++
|
||||
if (char === ')') nested--
|
||||
if (char === '-' && dsl[i + 1] === '>' && nested === 0) {
|
||||
segments.push(buffer.trim())
|
||||
buffer = ''
|
||||
i++ // Skip the ">" character
|
||||
}
|
||||
else {
|
||||
buffer += char
|
||||
}
|
||||
}
|
||||
if (buffer.trim())
|
||||
segments.push(buffer.trim())
|
||||
|
||||
return segments
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a single node 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).
|
||||
* @returns A parsed node object.
|
||||
*/
|
||||
function parseNode(nodeStr: string, parentIterationId?: 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
|
||||
let nested = 0
|
||||
let buffer = ''
|
||||
const parts: string[] = []
|
||||
|
||||
// Split the inner content by commas, respecting nested parentheses
|
||||
for (let i = 0; i < innerContent.length; i++) {
|
||||
const char = innerContent[i]
|
||||
if (char === '(') nested++
|
||||
if (char === ')') nested--
|
||||
|
||||
if (char === ',' && nested === 0) {
|
||||
parts.push(buffer.trim())
|
||||
buffer = ''
|
||||
}
|
||||
else {
|
||||
buffer += char
|
||||
}
|
||||
}
|
||||
parts.push(buffer.trim())
|
||||
|
||||
// Extract nodeType, nodeId, and params
|
||||
const [nodeType, nodeId, ...paramsRaw] = parts
|
||||
const params = parseParams(paramsRaw, nodeType === 'iteration' ? nodeId.trim() : parentIterationId)
|
||||
const complexNode = {
|
||||
nodeType: nodeType.trim(),
|
||||
nodeId: nodeId.trim(),
|
||||
params,
|
||||
}
|
||||
if (parentIterationId) {
|
||||
(complexNode as any).iterationId = parentIterationId;
|
||||
(complexNode as any).iterationIndex = 0 // Fixed as 0
|
||||
}
|
||||
return complexNode
|
||||
}
|
||||
|
||||
// If it's not a complex node, treat it as a plain node
|
||||
const plainNode: NodePlain = { nodeType: 'plain', nodeId: nodeStr.trim() }
|
||||
if (parentIterationId) {
|
||||
plainNode.iterationId = parentIterationId
|
||||
plainNode.iterationIndex = 0 // Fixed as 0
|
||||
}
|
||||
return plainNode
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses parameters of a complex 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.
|
||||
* @returns An array of parsed parameters (plain nodes, nested nodes, or flows).
|
||||
*/
|
||||
function parseParams(paramParts: string[], iterationId?: 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))
|
||||
}
|
||||
else if (part.startsWith('(')) {
|
||||
// Parse as a nested complex node
|
||||
return parseNode(part, iterationId)
|
||||
}
|
||||
else if (!Number.isNaN(Number(part.trim()))) {
|
||||
// Parse as a numeric parameter
|
||||
return Number(part.trim())
|
||||
}
|
||||
else {
|
||||
// Parse as a plain node
|
||||
return parseNode(part, iterationId)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
type NodeData = {
|
||||
id: string;
|
||||
node_id: string;
|
||||
title: string;
|
||||
node_type?: string;
|
||||
execution_metadata: Record<string, any>;
|
||||
status: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a plain node to node data.
|
||||
*/
|
||||
function convertPlainNode(node: Node): NodeData[] {
|
||||
return [
|
||||
{
|
||||
id: node.nodeId,
|
||||
node_id: node.nodeId,
|
||||
title: node.nodeId,
|
||||
execution_metadata: {},
|
||||
status: 'succeeded',
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a retry node to node data.
|
||||
*/
|
||||
function convertRetryNode(node: Node): NodeData[] {
|
||||
const { nodeId, iterationId, iterationIndex, params } = node as NodeComplex
|
||||
const retryCount = params ? Number.parseInt(params[0] as unknown as string, 10) : 0
|
||||
const result: NodeData[] = [
|
||||
{
|
||||
id: nodeId,
|
||||
node_id: nodeId,
|
||||
title: nodeId,
|
||||
execution_metadata: {},
|
||||
status: 'succeeded',
|
||||
},
|
||||
]
|
||||
|
||||
for (let i = 0; i < retryCount; i++) {
|
||||
result.push({
|
||||
id: nodeId,
|
||||
node_id: nodeId,
|
||||
title: nodeId,
|
||||
execution_metadata: iterationId ? {
|
||||
iteration_id: iterationId,
|
||||
iteration_index: iterationIndex || 0,
|
||||
} : {},
|
||||
status: 'retry',
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an iteration node to node data.
|
||||
*/
|
||||
function convertIterationNode(node: Node): NodeData[] {
|
||||
const { nodeId, params } = node as NodeComplex
|
||||
const result: NodeData[] = [
|
||||
{
|
||||
id: nodeId,
|
||||
node_id: nodeId,
|
||||
title: nodeId,
|
||||
node_type: 'iteration',
|
||||
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,
|
||||
iteration_id: nodeId,
|
||||
iteration_index: 0,
|
||||
}
|
||||
})
|
||||
result.push(...childData)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a parallel node to node data.
|
||||
*/
|
||||
function convertParallelNode(node: Node, parentParallelId?: string, parentStartNodeId?: string): NodeData[] {
|
||||
const { nodeId, params } = node as NodeComplex
|
||||
const result: NodeData[] = [
|
||||
{
|
||||
id: nodeId,
|
||||
node_id: nodeId,
|
||||
title: nodeId,
|
||||
execution_metadata: {
|
||||
parallel_id: nodeId,
|
||||
},
|
||||
status: 'succeeded',
|
||||
},
|
||||
]
|
||||
|
||||
params?.forEach((param) => {
|
||||
if (Array.isArray(param)) {
|
||||
const startNodeId = param[0]?.nodeId
|
||||
param.forEach((childNode: Node) => {
|
||||
const childData = convertToNodeData([childNode])
|
||||
childData.forEach((data) => {
|
||||
data.execution_metadata = {
|
||||
...data.execution_metadata,
|
||||
parallel_id: nodeId,
|
||||
parallel_start_node_id: startNodeId,
|
||||
...(parentParallelId && {
|
||||
parent_parallel_id: parentParallelId,
|
||||
parent_parallel_start_node_id: parentStartNodeId,
|
||||
}),
|
||||
}
|
||||
})
|
||||
result.push(...childData)
|
||||
})
|
||||
}
|
||||
else if (param && typeof param === 'object') {
|
||||
const startNodeId = param.nodeId
|
||||
const childData = convertToNodeData([param])
|
||||
childData.forEach((data) => {
|
||||
data.execution_metadata = {
|
||||
...data.execution_metadata,
|
||||
parallel_id: nodeId,
|
||||
parallel_start_node_id: startNodeId,
|
||||
...(parentParallelId && {
|
||||
parent_parallel_id: parentParallelId,
|
||||
parent_parallel_start_node_id: parentStartNodeId,
|
||||
}),
|
||||
}
|
||||
})
|
||||
result.push(...childData)
|
||||
}
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Main function to convert nodes to node data.
|
||||
*/
|
||||
function convertToNodeData(nodes: Node[], parentParallelId?: string, parentStartNodeId?: string): NodeData[] {
|
||||
const result: NodeData[] = []
|
||||
|
||||
nodes.forEach((node) => {
|
||||
switch (node.nodeType) {
|
||||
case 'plain':
|
||||
result.push(...convertPlainNode(node))
|
||||
break
|
||||
case 'retry':
|
||||
result.push(...convertRetryNode(node))
|
||||
break
|
||||
case 'iteration':
|
||||
result.push(...convertIterationNode(node))
|
||||
break
|
||||
case 'parallel':
|
||||
result.push(...convertParallelNode(node, parentParallelId, parentStartNodeId))
|
||||
break
|
||||
default:
|
||||
throw new Error(`Unknown nodeType: ${node.nodeType}`)
|
||||
}
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export default parseDSL
|
||||
@@ -0,0 +1,10 @@
|
||||
import formatToTracingNodeList from '.'
|
||||
import { simpleIterationData } from './iteration/data'
|
||||
import { simpleRetryData } from './retry/data'
|
||||
|
||||
describe('format api data to tracing panel data', () => {
|
||||
test('integration', () => {
|
||||
expect(formatToTracingNodeList(simpleIterationData.in.reverse() as any)).toEqual(simpleIterationData.expect)
|
||||
expect(formatToTracingNodeList(simpleRetryData.in.reverse() as any)).toEqual(simpleRetryData.expect)
|
||||
})
|
||||
})
|
||||
27
web/app/components/workflow/run/utils/format-log/index.ts
Normal file
27
web/app/components/workflow/run/utils/format-log/index.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import type { NodeTracing } from '@/types/workflow'
|
||||
import formatIterationNode from './iteration'
|
||||
import formatParallelNode from './parallel'
|
||||
import formatRetryNode from './retry'
|
||||
import formatAgentNode from './agent'
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
|
||||
const formatToTracingNodeList = (list: NodeTracing[], t: any) => {
|
||||
const allItems = cloneDeep([...list]).sort((a, b) => a.index - b.index)
|
||||
/*
|
||||
* First handle not change list structure node
|
||||
* Because Handle struct node will put the node in different
|
||||
*/
|
||||
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 result = formattedParallelList
|
||||
// console.log(allItems)
|
||||
// console.log(result)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export default formatToTracingNodeList
|
||||
@@ -0,0 +1,22 @@
|
||||
import format from '.'
|
||||
import graphToLogStruct from '../graph-to-log-struct'
|
||||
|
||||
describe('iteration', () => {
|
||||
const list = graphToLogStruct('start -> (iteration, iterationNode, plainNode1 -> plainNode2)')
|
||||
const [startNode, iterationNode, ...iterations] = list
|
||||
const result = format(list as any, () => { })
|
||||
test('result should have no nodes in iteration node', () => {
|
||||
expect((result as any).find((item: any) => !!item.execution_metadata?.iteration_id)).toBeUndefined()
|
||||
})
|
||||
test('iteration should put nodes in details', () => {
|
||||
expect(result as any).toEqual([
|
||||
startNode,
|
||||
{
|
||||
...iterationNode,
|
||||
details: [
|
||||
[iterations[0], iterations[1]],
|
||||
],
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,55 @@
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import type { NodeTracing } from '@/types/workflow'
|
||||
import formatParallelNode from '../parallel'
|
||||
function addChildrenToIterationNode(iterationNode: NodeTracing, childrenNodes: NodeTracing[]): NodeTracing {
|
||||
const details: NodeTracing[][] = []
|
||||
childrenNodes.forEach((item, index) => {
|
||||
if (!item.execution_metadata) return
|
||||
const { iteration_index = 0 } = item.execution_metadata
|
||||
const runIndex: number = iteration_index || index
|
||||
if (!details[runIndex])
|
||||
details[runIndex] = []
|
||||
|
||||
details[runIndex].push(item)
|
||||
})
|
||||
return {
|
||||
...iterationNode,
|
||||
details,
|
||||
}
|
||||
}
|
||||
|
||||
const format = (list: NodeTracing[], t: any): NodeTracing[] => {
|
||||
const iterationNodeIds = list
|
||||
.filter(item => item.node_type === BlockEnum.Iteration)
|
||||
.map(item => item.node_id)
|
||||
const iterationChildrenNodeIds = list
|
||||
.filter(item => item.execution_metadata?.iteration_id && iterationNodeIds.includes(item.execution_metadata.iteration_id))
|
||||
.map(item => item.node_id)
|
||||
// move iteration children nodes to iteration node's details field
|
||||
const result = list
|
||||
.filter(item => !iterationChildrenNodeIds.includes(item.node_id))
|
||||
.map((item) => {
|
||||
if (item.node_type === BlockEnum.Iteration) {
|
||||
const childrenNodes = list.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 node 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
|
||||
}
|
||||
|
||||
export default format
|
||||
@@ -0,0 +1,39 @@
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
import format from '.'
|
||||
import graphToLogStruct from '../graph-to-log-struct'
|
||||
|
||||
describe('parallel', () => {
|
||||
const list = graphToLogStruct('(parallel, parallelNode, nodeA, nodeB -> nodeC)')
|
||||
const [parallelNode, ...parallelDetail] = list
|
||||
const parallelI18n = 'PARALLEL'
|
||||
// format will change the list...
|
||||
const result = format(cloneDeep(list) as any, () => parallelI18n)
|
||||
|
||||
test('parallel should put nodes in details', () => {
|
||||
expect(result as any).toEqual([
|
||||
{
|
||||
...parallelNode,
|
||||
parallelDetail: {
|
||||
isParallelStartNode: true,
|
||||
parallelTitle: `${parallelI18n}-1`,
|
||||
children: [
|
||||
parallelNode,
|
||||
{
|
||||
...parallelDetail[0],
|
||||
parallelDetail: {
|
||||
branchTitle: `${parallelI18n}-1-A`,
|
||||
},
|
||||
},
|
||||
{
|
||||
...parallelDetail[1],
|
||||
parallelDetail: {
|
||||
branchTitle: `${parallelI18n}-1-B`,
|
||||
},
|
||||
},
|
||||
parallelDetail[2],
|
||||
],
|
||||
},
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,175 @@
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import type { NodeTracing } from '@/types/workflow'
|
||||
|
||||
function printNodeStructure(node: NodeTracing, depth: number) {
|
||||
const indent = ' '.repeat(depth)
|
||||
console.log(`${indent}${node.title}`)
|
||||
if (node.parallelDetail?.children) {
|
||||
node.parallelDetail.children.forEach((child) => {
|
||||
printNodeStructure(child, depth + 1)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function addTitle({
|
||||
list, depth, belongParallelIndexInfo,
|
||||
}: {
|
||||
list: NodeTracing[],
|
||||
depth: number,
|
||||
belongParallelIndexInfo?: string,
|
||||
}, t: any) {
|
||||
let branchIndex = 0
|
||||
const hasMoreThanOneParallel = list.filter(node => node.parallelDetail?.isParallelStartNode).length > 1
|
||||
list.forEach((node) => {
|
||||
const parallel_id = node.parallel_id ?? node.execution_metadata?.parallel_id ?? null
|
||||
const parallel_start_node_id = node.parallel_start_node_id ?? node.execution_metadata?.parallel_start_node_id ?? null
|
||||
|
||||
const isNotInParallel = !parallel_id || node.node_type === BlockEnum.End
|
||||
if (isNotInParallel)
|
||||
return
|
||||
|
||||
const isParallelStartNode = node.parallelDetail?.isParallelStartNode
|
||||
|
||||
const parallelIndexLetter = (() => {
|
||||
if (!isParallelStartNode || !hasMoreThanOneParallel)
|
||||
return ''
|
||||
|
||||
const index = 1 + list.filter(node => node.parallelDetail?.isParallelStartNode).findIndex(item => item.node_id === node.node_id)
|
||||
return String.fromCharCode(64 + index)
|
||||
})()
|
||||
|
||||
const parallelIndexInfo = `${depth}${parallelIndexLetter}`
|
||||
|
||||
if (isParallelStartNode) {
|
||||
node.parallelDetail!.isParallelStartNode = true
|
||||
node.parallelDetail!.parallelTitle = `${t('workflow.common.parallel')}-${parallelIndexInfo}`
|
||||
}
|
||||
|
||||
const isBrachStartNode = parallel_start_node_id === node.node_id
|
||||
if (isBrachStartNode) {
|
||||
branchIndex++
|
||||
const branchLetter = String.fromCharCode(64 + branchIndex)
|
||||
if (!node.parallelDetail) {
|
||||
node.parallelDetail = {
|
||||
branchTitle: '',
|
||||
}
|
||||
}
|
||||
|
||||
node.parallelDetail!.branchTitle = `${t('workflow.common.branch')}-${belongParallelIndexInfo}-${branchLetter}`
|
||||
}
|
||||
|
||||
if (node.parallelDetail?.children && node.parallelDetail.children.length > 0) {
|
||||
addTitle({
|
||||
list: node.parallelDetail.children,
|
||||
depth: depth + 1,
|
||||
belongParallelIndexInfo: parallelIndexInfo,
|
||||
}, t)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// list => group by parallel_id(parallel tree).
|
||||
const format = (list: NodeTracing[], t: any, isPrint?: boolean): NodeTracing[] => {
|
||||
if (isPrint)
|
||||
console.log(list)
|
||||
|
||||
const result: NodeTracing[] = [...list]
|
||||
const parallelFirstNodeMap: Record<string, string> = {}
|
||||
// list to tree by parent_parallel_start_node_id and branch by parallel_start_node_id. Each parallel may has more than one branch.
|
||||
result.forEach((node) => {
|
||||
const parallel_id = node.parallel_id ?? node.execution_metadata?.parallel_id ?? null
|
||||
const parent_parallel_id = node.parent_parallel_id ?? node.execution_metadata?.parent_parallel_id ?? null
|
||||
const branchStartNodeId = node.parallel_start_node_id ?? node.execution_metadata?.parallel_start_node_id ?? null
|
||||
const parentParallelBranchStartNodeId = node.parent_parallel_start_node_id ?? node.execution_metadata?.parent_parallel_start_node_id ?? null
|
||||
const isNotInParallel = !parallel_id || node.node_type === BlockEnum.End
|
||||
if (isNotInParallel)
|
||||
return
|
||||
|
||||
const isParallelStartNode = !parallelFirstNodeMap[parallel_id]
|
||||
if (isParallelStartNode) {
|
||||
const selfNode = { ...node, parallelDetail: undefined }
|
||||
node.parallelDetail = {
|
||||
isParallelStartNode: true,
|
||||
children: [selfNode],
|
||||
}
|
||||
parallelFirstNodeMap[parallel_id] = node.node_id
|
||||
const isRootLevel = !parent_parallel_id
|
||||
if (isRootLevel)
|
||||
return
|
||||
|
||||
const parentParallelStartNode = result.find(item => item.node_id === parentParallelBranchStartNodeId)
|
||||
// append to parent parallel start node and after the same branch
|
||||
if (parentParallelStartNode) {
|
||||
if (!parentParallelStartNode?.parallelDetail) {
|
||||
parentParallelStartNode!.parallelDetail = {
|
||||
children: [],
|
||||
}
|
||||
}
|
||||
if (parentParallelStartNode!.parallelDetail.children) {
|
||||
const sameBranchNodesLastIndex = parentParallelStartNode.parallelDetail.children.findLastIndex((node) => {
|
||||
const currStartNodeId = node.parallel_start_node_id ?? node.execution_metadata?.parallel_start_node_id ?? null
|
||||
return currStartNodeId === parentParallelBranchStartNodeId
|
||||
})
|
||||
if (sameBranchNodesLastIndex !== -1)
|
||||
parentParallelStartNode!.parallelDetail.children.splice(sameBranchNodesLastIndex + 1, 0, node)
|
||||
else
|
||||
parentParallelStartNode!.parallelDetail.children.push(node)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// append to parallel start node and after the same branch
|
||||
const parallelStartNode = result.find(item => item.node_id === parallelFirstNodeMap[parallel_id])
|
||||
|
||||
if (parallelStartNode && parallelStartNode.parallelDetail && parallelStartNode!.parallelDetail!.children) {
|
||||
const sameBranchNodesLastIndex = parallelStartNode.parallelDetail.children.findLastIndex((node) => {
|
||||
const currStartNodeId = node.parallel_start_node_id ?? node.execution_metadata?.parallel_start_node_id ?? null
|
||||
return currStartNodeId === branchStartNodeId
|
||||
})
|
||||
if (sameBranchNodesLastIndex !== -1) {
|
||||
parallelStartNode.parallelDetail.children.splice(sameBranchNodesLastIndex + 1, 0, node)
|
||||
}
|
||||
else { // new branch
|
||||
parallelStartNode.parallelDetail.children.push(node)
|
||||
}
|
||||
}
|
||||
// parallelStartNode!.parallelDetail!.children.push(node)
|
||||
})
|
||||
|
||||
const filteredInParallelSubNodes = result.filter((node) => {
|
||||
const parallel_id = node.parallel_id ?? node.execution_metadata?.parallel_id ?? null
|
||||
const isNotInParallel = !parallel_id || node.node_type === BlockEnum.End
|
||||
if (isNotInParallel)
|
||||
return true
|
||||
|
||||
const parent_parallel_id = node.parent_parallel_id ?? node.execution_metadata?.parent_parallel_id ?? null
|
||||
|
||||
if (parent_parallel_id)
|
||||
return false
|
||||
|
||||
const isParallelStartNode = node.parallelDetail?.isParallelStartNode
|
||||
if (!isParallelStartNode)
|
||||
return false
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
// print node structure for debug
|
||||
if (isPrint) {
|
||||
filteredInParallelSubNodes.forEach((node) => {
|
||||
const now = Date.now()
|
||||
console.log(`----- p: ${now} start -----`)
|
||||
printNodeStructure(node, 0)
|
||||
console.log(`----- p: ${now} end -----`)
|
||||
})
|
||||
}
|
||||
|
||||
addTitle({
|
||||
list: filteredInParallelSubNodes,
|
||||
depth: 1,
|
||||
}, t)
|
||||
|
||||
return filteredInParallelSubNodes
|
||||
}
|
||||
export default format
|
||||
@@ -0,0 +1,21 @@
|
||||
import format from '.'
|
||||
import graphToLogStruct from '../graph-to-log-struct'
|
||||
|
||||
describe('retry', () => {
|
||||
// retry nodeId:1 3 times.
|
||||
const steps = graphToLogStruct('start -> (retry, retryNode, 3)')
|
||||
const [startNode, retryNode, ...retryDetail] = steps
|
||||
const result = format(steps as any)
|
||||
test('should have no retry status nodes', () => {
|
||||
expect(result.find(item => (item as any).status === 'retry')).toBeUndefined()
|
||||
})
|
||||
test('should put retry nodes in retryDetail', () => {
|
||||
expect(result).toEqual([
|
||||
startNode,
|
||||
{
|
||||
...retryNode,
|
||||
retryDetail,
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { NodeTracing } from '@/types/workflow'
|
||||
|
||||
const format = (list: NodeTracing[]): NodeTracing[] => {
|
||||
const retryNodes = list.filter((item) => {
|
||||
return item.status === 'retry'
|
||||
})
|
||||
|
||||
const retryNodeIds = retryNodes.map(item => item.node_id)
|
||||
// move retry nodes to retryDetail
|
||||
const result = list.filter((item) => {
|
||||
return item.status !== 'retry'
|
||||
}).map((item) => {
|
||||
const { execution_metadata } = item
|
||||
const isInIteration = !!execution_metadata?.iteration_id
|
||||
const nodeId = item.node_id
|
||||
const isRetryBelongNode = retryNodeIds.includes(nodeId)
|
||||
|
||||
if (isRetryBelongNode) {
|
||||
return {
|
||||
...item,
|
||||
retryDetail: retryNodes.filter((node) => {
|
||||
if (!isInIteration)
|
||||
return node.node_id === nodeId
|
||||
|
||||
// retry node in iteration
|
||||
return node.node_id === nodeId && node.execution_metadata?.iteration_index === execution_metadata?.iteration_index
|
||||
}),
|
||||
}
|
||||
}
|
||||
return item
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
export default format
|
||||
Reference in New Issue
Block a user