Feat/workflow retry (#11885)

This commit is contained in:
zxhlyh
2024-12-20 15:44:37 +08:00
committed by GitHub
parent dacd457478
commit 0c0120ef27
31 changed files with 690 additions and 51 deletions

View File

@@ -17,17 +17,25 @@ import ResultPanel from '@/app/components/workflow/run/result-panel'
import Toast from '@/app/components/base/toast'
import { TransferMethod } from '@/types/app'
import { getProcessedFiles } from '@/app/components/base/file-uploader/utils'
import type { NodeTracing } from '@/types/workflow'
import RetryResultPanel from '@/app/components/workflow/run/retry-result-panel'
import type { BlockEnum } from '@/app/components/workflow/types'
import type { Emoji } from '@/app/components/tools/types'
const i18nPrefix = 'workflow.singleRun'
type BeforeRunFormProps = {
nodeName: string
nodeType?: BlockEnum
toolIcon?: string | Emoji
onHide: () => void
onRun: (submitData: Record<string, any>) => void
onStop: () => void
runningStatus: NodeRunningStatus
result?: JSX.Element
forms: FormProps[]
retryDetails?: NodeTracing[]
onRetryDetailBack?: any
}
function formatValue(value: string | any, type: InputVarType) {
@@ -50,12 +58,16 @@ function formatValue(value: string | any, type: InputVarType) {
}
const BeforeRunForm: FC<BeforeRunFormProps> = ({
nodeName,
nodeType,
toolIcon,
onHide,
onRun,
onStop,
runningStatus,
result,
forms,
retryDetails,
onRetryDetailBack = () => { },
}) => {
const { t } = useTranslation()
@@ -122,48 +134,69 @@ const BeforeRunForm: FC<BeforeRunFormProps> = ({
<div className='text-base font-semibold text-gray-900 truncate'>
{t(`${i18nPrefix}.testRun`)} {nodeName}
</div>
<div className='ml-2 shrink-0 p-1 cursor-pointer' onClick={onHide}>
<div className='ml-2 shrink-0 p-1 cursor-pointer' onClick={() => {
onHide()
}}>
<RiCloseLine className='w-4 h-4 text-gray-500 ' />
</div>
</div>
<div className='h-0 grow overflow-y-auto pb-4'>
<div className='mt-3 px-4 space-y-4'>
{forms.map((form, index) => (
<div key={index}>
<Form
key={index}
className={cn(index < forms.length - 1 && 'mb-4')}
{...form}
/>
{index < forms.length - 1 && <Split />}
{
retryDetails?.length && (
<div className='h-0 grow overflow-y-auto pb-4'>
<RetryResultPanel
list={retryDetails.map((item, index) => ({
...item,
title: `${t('workflow.nodes.common.retry.retry')} ${index + 1}`,
node_type: nodeType!,
extras: {
icon: toolIcon!,
},
}))}
onBack={onRetryDetailBack}
/>
</div>
)
}
{
!retryDetails?.length && (
<div className='h-0 grow overflow-y-auto pb-4'>
<div className='mt-3 px-4 space-y-4'>
{forms.map((form, index) => (
<div key={index}>
<Form
key={index}
className={cn(index < forms.length - 1 && 'mb-4')}
{...form}
/>
{index < forms.length - 1 && <Split />}
</div>
))}
</div>
))}
</div>
<div className='mt-4 flex justify-between space-x-2 px-4' >
{isRunning && (
<div
className='p-2 rounded-lg border border-gray-200 bg-white shadow-xs cursor-pointer'
onClick={onStop}
>
<StopCircle className='w-4 h-4 text-gray-500' />
<div className='mt-4 flex justify-between space-x-2 px-4' >
{isRunning && (
<div
className='p-2 rounded-lg border border-gray-200 bg-white shadow-xs cursor-pointer'
onClick={onStop}
>
<StopCircle className='w-4 h-4 text-gray-500' />
</div>
)}
<Button disabled={!isFileLoaded || isRunning} variant='primary' className='w-0 grow space-x-2' onClick={handleRun}>
{isRunning && <RiLoader2Line className='animate-spin w-4 h-4 text-white' />}
<div>{t(`${i18nPrefix}.${isRunning ? 'running' : 'startRun'}`)}</div>
</Button>
</div>
)}
<Button disabled={!isFileLoaded || isRunning} variant='primary' className='w-0 grow space-x-2' onClick={handleRun}>
{isRunning && <RiLoader2Line className='animate-spin w-4 h-4 text-white' />}
<div>{t(`${i18nPrefix}.${isRunning ? 'running' : 'startRun'}`)}</div>
</Button>
</div>
{isRunning && (
<ResultPanel status='running' showSteps={false} />
)}
{isFinished && (
<>
{result}
</>
)}
</div>
{isRunning && (
<ResultPanel status='running' showSteps={false} />
)}
{isFinished && (
<>
{result}
</>
)}
</div>
)
}
</div>
</div>
)

View File

@@ -14,7 +14,6 @@ import type {
CommonNodeType,
Node,
} from '@/app/components/workflow/types'
import Split from '@/app/components/workflow/nodes/_base/components/split'
import Tooltip from '@/app/components/base/tooltip'
type ErrorHandleProps = Pick<Node, 'id' | 'data'>
@@ -45,7 +44,6 @@ const ErrorHandle = ({
return (
<>
<Split />
<div className='py-4'>
<Collapse
disabled={!error_strategy}

View File

@@ -0,0 +1,41 @@
import {
useCallback,
useState,
} from 'react'
import type { WorkflowRetryConfig } from './types'
import {
useNodeDataUpdate,
} from '@/app/components/workflow/hooks'
import type { NodeTracing } from '@/types/workflow'
export const useRetryConfig = (
id: string,
) => {
const { handleNodeDataUpdateWithSyncDraft } = useNodeDataUpdate()
const handleRetryConfigChange = useCallback((value?: WorkflowRetryConfig) => {
handleNodeDataUpdateWithSyncDraft({
id,
data: {
retry_config: value,
},
})
}, [id, handleNodeDataUpdateWithSyncDraft])
return {
handleRetryConfigChange,
}
}
export const useRetryDetailShowInSingleRun = () => {
const [retryDetails, setRetryDetails] = useState<NodeTracing[] | undefined>()
const handleRetryDetailsChange = useCallback((details: NodeTracing[] | undefined) => {
setRetryDetails(details)
}, [])
return {
retryDetails,
handleRetryDetailsChange,
}
}

View File

@@ -0,0 +1,88 @@
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import {
RiAlertFill,
RiCheckboxCircleFill,
RiLoader2Line,
} from '@remixicon/react'
import type { Node } from '@/app/components/workflow/types'
import { NodeRunningStatus } from '@/app/components/workflow/types'
import cn from '@/utils/classnames'
type RetryOnNodeProps = Pick<Node, 'id' | 'data'>
const RetryOnNode = ({
data,
}: RetryOnNodeProps) => {
const { t } = useTranslation()
const { retry_config } = data
const showSelectedBorder = data.selected || data._isBundled || data._isEntering
const {
isRunning,
isSuccessful,
isException,
isFailed,
} = useMemo(() => {
return {
isRunning: data._runningStatus === NodeRunningStatus.Running && !showSelectedBorder,
isSuccessful: data._runningStatus === NodeRunningStatus.Succeeded && !showSelectedBorder,
isFailed: data._runningStatus === NodeRunningStatus.Failed && !showSelectedBorder,
isException: data._runningStatus === NodeRunningStatus.Exception && !showSelectedBorder,
}
}, [data._runningStatus, showSelectedBorder])
const showDefault = !isRunning && !isSuccessful && !isException && !isFailed
if (!retry_config)
return null
return (
<div className='px-3'>
<div className={cn(
'flex items-center justify-between px-[5px] py-1 bg-workflow-block-parma-bg border-[0.5px] border-transparent rounded-md system-xs-medium-uppercase text-text-tertiary',
isRunning && 'bg-state-accent-hover border-state-accent-active text-text-accent',
isSuccessful && 'bg-state-success-hover border-state-success-active text-text-success',
(isException || isFailed) && 'bg-state-warning-hover border-state-warning-active text-text-warning',
)}>
<div className='flex items-center'>
{
showDefault && (
t('workflow.nodes.common.retry.retryTimes', { times: retry_config.max_retries })
)
}
{
isRunning && (
<>
<RiLoader2Line className='animate-spin mr-1 w-3.5 h-3.5' />
{t('workflow.nodes.common.retry.retrying')}
</>
)
}
{
isSuccessful && (
<>
<RiCheckboxCircleFill className='mr-1 w-3.5 h-3.5' />
{t('workflow.nodes.common.retry.retrySuccessful')}
</>
)
}
{
(isFailed || isException) && (
<>
<RiAlertFill className='mr-1 w-3.5 h-3.5' />
{t('workflow.nodes.common.retry.retryFailed')}
</>
)
}
</div>
{
!showDefault && (
<div>
{data._retryIndex}/{data.retry_config?.max_retries}
</div>
)
}
</div>
</div>
)
}
export default RetryOnNode

View File

@@ -0,0 +1,117 @@
import { useTranslation } from 'react-i18next'
import { useRetryConfig } from './hooks'
import s from './style.module.css'
import Switch from '@/app/components/base/switch'
import Slider from '@/app/components/base/slider'
import Input from '@/app/components/base/input'
import type {
Node,
} from '@/app/components/workflow/types'
import Split from '@/app/components/workflow/nodes/_base/components/split'
type RetryOnPanelProps = Pick<Node, 'id' | 'data'>
const RetryOnPanel = ({
id,
data,
}: RetryOnPanelProps) => {
const { t } = useTranslation()
const { handleRetryConfigChange } = useRetryConfig(id)
const { retry_config } = data
const handleRetryEnabledChange = (value: boolean) => {
handleRetryConfigChange({
retry_enabled: value,
max_retries: retry_config?.max_retries || 3,
retry_interval: retry_config?.retry_interval || 1000,
})
}
const handleMaxRetriesChange = (value: number) => {
if (value > 10)
value = 10
else if (value < 1)
value = 1
handleRetryConfigChange({
retry_enabled: true,
max_retries: value,
retry_interval: retry_config?.retry_interval || 1000,
})
}
const handleRetryIntervalChange = (value: number) => {
if (value > 5000)
value = 5000
else if (value < 100)
value = 100
handleRetryConfigChange({
retry_enabled: true,
max_retries: retry_config?.max_retries || 3,
retry_interval: value,
})
}
return (
<>
<div className='pt-2'>
<div className='flex items-center justify-between px-4 py-2 h-10'>
<div className='flex items-center'>
<div className='mr-0.5 system-sm-semibold-uppercase text-text-secondary'>{t('workflow.nodes.common.retry.retryOnFailure')}</div>
</div>
<Switch
defaultValue={retry_config?.retry_enabled}
onChange={v => handleRetryEnabledChange(v)}
/>
</div>
{
retry_config?.retry_enabled && (
<div className='px-4 pb-2'>
<div className='flex items-center mb-1 w-full'>
<div className='grow mr-2 system-xs-medium-uppercase'>{t('workflow.nodes.common.retry.maxRetries')}</div>
<Slider
className='mr-3 w-[108px]'
value={retry_config?.max_retries || 3}
onChange={handleMaxRetriesChange}
min={1}
max={10}
/>
<Input
type='number'
wrapperClassName='w-[80px]'
value={retry_config?.max_retries || 3}
onChange={e => handleMaxRetriesChange(e.target.value as any)}
min={1}
max={10}
unit={t('workflow.nodes.common.retry.times') || ''}
className={s.input}
/>
</div>
<div className='flex items-center'>
<div className='grow mr-2 system-xs-medium-uppercase'>{t('workflow.nodes.common.retry.retryInterval')}</div>
<Slider
className='mr-3 w-[108px]'
value={retry_config?.retry_interval || 1000}
onChange={handleRetryIntervalChange}
min={100}
max={5000}
/>
<Input
type='number'
wrapperClassName='w-[80px]'
value={retry_config?.retry_interval || 1000}
onChange={e => handleRetryIntervalChange(e.target.value as any)}
min={100}
max={5000}
unit={t('workflow.nodes.common.retry.ms') || ''}
className={s.input}
/>
</div>
</div>
)
}
</div>
<Split className='mx-4 mt-2' />
</>
)
}
export default RetryOnPanel

View File

@@ -0,0 +1,5 @@
.input::-webkit-inner-spin-button,
.input::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}

View File

@@ -0,0 +1,5 @@
export type WorkflowRetryConfig = {
max_retries: number
retry_interval: number
retry_enabled: boolean
}

View File

@@ -25,7 +25,10 @@ import {
useNodesReadOnly,
useToolIcon,
} from '../../hooks'
import { hasErrorHandleNode } from '../../utils'
import {
hasErrorHandleNode,
hasRetryNode,
} from '../../utils'
import { useNodeIterationInteractions } from '../iteration/use-interactions'
import type { IterationNodeType } from '../iteration/types'
import {
@@ -35,6 +38,7 @@ import {
import NodeResizer from './components/node-resizer'
import NodeControl from './components/node-control'
import ErrorHandleOnNode from './components/error-handle/error-handle-on-node'
import RetryOnNode from './components/retry/retry-on-node'
import AddVariablePopupWithPosition from './components/add-variable-popup-with-position'
import cn from '@/utils/classnames'
import BlockIcon from '@/app/components/workflow/block-icon'
@@ -237,6 +241,14 @@ const BaseNode: FC<BaseNodeProps> = ({
</div>
)
}
{
hasRetryNode(data.type) && (
<RetryOnNode
id={id}
data={data}
/>
)
}
{
hasErrorHandleNode(data.type) && (
<ErrorHandleOnNode

View File

@@ -21,9 +21,11 @@ import {
TitleInput,
} from './components/title-description-input'
import ErrorHandleOnPanel from './components/error-handle/error-handle-on-panel'
import RetryOnPanel from './components/retry/retry-on-panel'
import { useResizePanel } from './hooks/use-resize-panel'
import cn from '@/utils/classnames'
import BlockIcon from '@/app/components/workflow/block-icon'
import Split from '@/app/components/workflow/nodes/_base/components/split'
import {
WorkflowHistoryEvent,
useAvailableBlocks,
@@ -38,6 +40,7 @@ import {
import {
canRunBySingle,
hasErrorHandleNode,
hasRetryNode,
} from '@/app/components/workflow/utils'
import Tooltip from '@/app/components/base/tooltip'
import type { Node } from '@/app/components/workflow/types'
@@ -168,6 +171,15 @@ const BasePanel: FC<BasePanelProps> = ({
<div>
{cloneElement(children, { id, data })}
</div>
<Split />
{
hasRetryNode(data.type) && (
<RetryOnPanel
id={id}
data={data}
/>
)
}
{
hasErrorHandleNode(data.type) && (
<ErrorHandleOnPanel

View File

@@ -2,7 +2,10 @@ import { BlockEnum } from '../../types'
import type { NodeDefault } from '../../types'
import { AuthorizationType, BodyType, Method } from './types'
import type { BodyPayload, HttpNodeType } from './types'
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/constants'
import {
ALL_CHAT_AVAILABLE_BLOCKS,
ALL_COMPLETION_AVAILABLE_BLOCKS,
} from '@/app/components/workflow/constants'
const nodeDefault: NodeDefault<HttpNodeType> = {
defaultValue: {
@@ -24,6 +27,11 @@ const nodeDefault: NodeDefault<HttpNodeType> = {
max_read_timeout: 0,
max_write_timeout: 0,
},
retry_config: {
retry_enabled: true,
max_retries: 3,
retry_interval: 100,
},
},
getAvailablePrevNodes(isChatMode: boolean) {
const nodes = isChatMode

View File

@@ -1,5 +1,5 @@
import type { FC } from 'react'
import React from 'react'
import { memo } from 'react'
import { useTranslation } from 'react-i18next'
import useConfig from './use-config'
import ApiInput from './components/api-input'
@@ -18,6 +18,7 @@ import { FileArrow01 } from '@/app/components/base/icons/src/vender/line/files'
import type { NodePanelProps } from '@/app/components/workflow/types'
import BeforeRunForm from '@/app/components/workflow/nodes/_base/components/before-run-form'
import ResultPanel from '@/app/components/workflow/run/result-panel'
import { useRetryDetailShowInSingleRun } from '@/app/components/workflow/nodes/_base/components/retry/hooks'
const i18nPrefix = 'workflow.nodes.http'
@@ -60,6 +61,10 @@ const Panel: FC<NodePanelProps<HttpNodeType>> = ({
hideCurlPanel,
handleCurlImport,
} = useConfig(id, data)
const {
retryDetails,
handleRetryDetailsChange,
} = useRetryDetailShowInSingleRun()
// To prevent prompt editor in body not update data.
if (!isDataReady)
return null
@@ -181,6 +186,7 @@ const Panel: FC<NodePanelProps<HttpNodeType>> = ({
{isShowSingleRun && (
<BeforeRunForm
nodeName={inputs.title}
nodeType={inputs.type}
onHide={hideSingleRun}
forms={[
{
@@ -192,7 +198,9 @@ const Panel: FC<NodePanelProps<HttpNodeType>> = ({
runningStatus={runningStatus}
onRun={handleRun}
onStop={handleStop}
result={<ResultPanel {...runResult} showSteps={false} />}
retryDetails={retryDetails}
onRetryDetailBack={handleRetryDetailsChange}
result={<ResultPanel {...runResult} showSteps={false} onShowRetryDetail={handleRetryDetailsChange} />}
/>
)}
{(isShowCurlPanel && !readOnly) && (
@@ -207,4 +215,4 @@ const Panel: FC<NodePanelProps<HttpNodeType>> = ({
)
}
export default React.memo(Panel)
export default memo(Panel)

View File

@@ -19,6 +19,7 @@ import type { Props as FormProps } from '@/app/components/workflow/nodes/_base/c
import ResultPanel from '@/app/components/workflow/run/result-panel'
import Tooltip from '@/app/components/base/tooltip'
import Editor from '@/app/components/workflow/nodes/_base/components/prompt/editor'
import { useRetryDetailShowInSingleRun } from '@/app/components/workflow/nodes/_base/components/retry/hooks'
const i18nPrefix = 'workflow.nodes.llm'
@@ -69,6 +70,10 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({
runResult,
filterJinjia2InputVar,
} = useConfig(id, data)
const {
retryDetails,
handleRetryDetailsChange,
} = useRetryDetailShowInSingleRun()
const model = inputs.model
@@ -282,12 +287,15 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({
{isShowSingleRun && (
<BeforeRunForm
nodeName={inputs.title}
nodeType={inputs.type}
onHide={hideSingleRun}
forms={singleRunForms}
runningStatus={runningStatus}
onRun={handleRun}
onStop={handleStop}
result={<ResultPanel {...runResult} showSteps={false} />}
retryDetails={retryDetails}
onRetryDetailBack={handleRetryDetailsChange}
result={<ResultPanel {...runResult} showSteps={false} onShowRetryDetail={handleRetryDetailsChange} />}
/>
)}
</div>

View File

@@ -14,6 +14,8 @@ import Loading from '@/app/components/base/loading'
import BeforeRunForm from '@/app/components/workflow/nodes/_base/components/before-run-form'
import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars'
import ResultPanel from '@/app/components/workflow/run/result-panel'
import { useRetryDetailShowInSingleRun } from '@/app/components/workflow/nodes/_base/components/retry/hooks'
import { useToolIcon } from '@/app/components/workflow/hooks'
const i18nPrefix = 'workflow.nodes.tool'
@@ -48,6 +50,11 @@ const Panel: FC<NodePanelProps<ToolNodeType>> = ({
handleStop,
runResult,
} = useConfig(id, data)
const toolIcon = useToolIcon(data)
const {
retryDetails,
handleRetryDetailsChange,
} = useRetryDetailShowInSingleRun()
if (isLoading) {
return <div className='flex h-[200px] items-center justify-center'>
@@ -143,12 +150,16 @@ const Panel: FC<NodePanelProps<ToolNodeType>> = ({
{isShowSingleRun && (
<BeforeRunForm
nodeName={inputs.title}
nodeType={inputs.type}
toolIcon={toolIcon}
onHide={hideSingleRun}
forms={singleRunForms}
runningStatus={runningStatus}
onRun={handleRun}
onStop={handleStop}
result={<ResultPanel {...runResult} showSteps={false} />}
retryDetails={retryDetails}
onRetryDetailBack={handleRetryDetailsChange}
result={<ResultPanel {...runResult} showSteps={false} onShowRetryDetail={handleRetryDetailsChange} />}
/>
)}
</div>