| @@ -361,6 +361,7 @@ class WorkflowBasedAppRunner(AppRunner): | |||
| node_run_index=workflow_entry.graph_engine.graph_runtime_state.node_run_steps, | |||
| output=event.pre_iteration_output, | |||
| parallel_mode_run_id=event.parallel_mode_run_id, | |||
| duration=event.duration, | |||
| ) | |||
| ) | |||
| elif isinstance(event, (IterationRunSucceededEvent | IterationRunFailedEvent)): | |||
| @@ -111,6 +111,7 @@ class QueueIterationNextEvent(AppQueueEvent): | |||
| """iteratoin run in parallel mode run id""" | |||
| node_run_index: int | |||
| output: Optional[Any] = None # output for the current iteration | |||
| duration: Optional[float] = None | |||
| @field_validator("output", mode="before") | |||
| @classmethod | |||
| @@ -307,6 +308,8 @@ class QueueNodeSucceededEvent(AppQueueEvent): | |||
| execution_metadata: Optional[dict[NodeRunMetadataKey, Any]] = None | |||
| error: Optional[str] = None | |||
| """single iteration duration map""" | |||
| iteration_duration_map: Optional[dict[str, float]] = None | |||
| class QueueNodeInIterationFailedEvent(AppQueueEvent): | |||
| @@ -434,6 +434,7 @@ class IterationNodeNextStreamResponse(StreamResponse): | |||
| parallel_id: Optional[str] = None | |||
| parallel_start_node_id: Optional[str] = None | |||
| parallel_mode_run_id: Optional[str] = None | |||
| duration: Optional[float] = None | |||
| event: StreamEvent = StreamEvent.ITERATION_NEXT | |||
| workflow_run_id: str | |||
| @@ -624,6 +624,7 @@ class WorkflowCycleManage: | |||
| parallel_id=event.parallel_id, | |||
| parallel_start_node_id=event.parallel_start_node_id, | |||
| parallel_mode_run_id=event.parallel_mode_run_id, | |||
| duration=event.duration, | |||
| ), | |||
| ) | |||
| @@ -24,6 +24,7 @@ class NodeRunMetadataKey(str, Enum): | |||
| PARENT_PARALLEL_ID = "parent_parallel_id" | |||
| PARENT_PARALLEL_START_NODE_ID = "parent_parallel_start_node_id" | |||
| PARALLEL_MODE_RUN_ID = "parallel_mode_run_id" | |||
| ITERATION_DURATION_MAP = "iteration_duration_map" # single iteration duration if iteration node runs | |||
| class NodeRunResult(BaseModel): | |||
| @@ -148,6 +148,7 @@ class IterationRunStartedEvent(BaseIterationEvent): | |||
| class IterationRunNextEvent(BaseIterationEvent): | |||
| index: int = Field(..., description="index") | |||
| pre_iteration_output: Optional[Any] = Field(None, description="pre iteration output") | |||
| duration: Optional[float] = Field(None, description="duration") | |||
| class IterationRunSucceededEvent(BaseIterationEvent): | |||
| @@ -156,6 +157,7 @@ class IterationRunSucceededEvent(BaseIterationEvent): | |||
| outputs: Optional[dict[str, Any]] = None | |||
| metadata: Optional[dict[str, Any]] = None | |||
| steps: int = 0 | |||
| iteration_duration_map: Optional[dict[str, float]] = None | |||
| class IterationRunFailedEvent(BaseIterationEvent): | |||
| @@ -156,6 +156,7 @@ class IterationNode(BaseNode[IterationNodeData]): | |||
| index=0, | |||
| pre_iteration_output=None, | |||
| ) | |||
| iter_run_map: dict[str, float] = {} | |||
| outputs: list[Any] = [None] * len(iterator_list_value) | |||
| try: | |||
| if self.node_data.is_parallel: | |||
| @@ -175,6 +176,7 @@ class IterationNode(BaseNode[IterationNodeData]): | |||
| iteration_graph, | |||
| index, | |||
| item, | |||
| iter_run_map, | |||
| ) | |||
| future.add_done_callback(thread_pool.task_done_callback) | |||
| futures.append(future) | |||
| @@ -213,6 +215,7 @@ class IterationNode(BaseNode[IterationNodeData]): | |||
| start_at, | |||
| graph_engine, | |||
| iteration_graph, | |||
| iter_run_map, | |||
| ) | |||
| if self.node_data.error_handle_mode == ErrorHandleMode.REMOVE_ABNORMAL_OUTPUT: | |||
| outputs = [output for output in outputs if output is not None] | |||
| @@ -230,7 +233,9 @@ class IterationNode(BaseNode[IterationNodeData]): | |||
| yield RunCompletedEvent( | |||
| run_result=NodeRunResult( | |||
| status=WorkflowNodeExecutionStatus.SUCCEEDED, outputs={"output": jsonable_encoder(outputs)} | |||
| status=WorkflowNodeExecutionStatus.SUCCEEDED, | |||
| outputs={"output": jsonable_encoder(outputs)}, | |||
| metadata={NodeRunMetadataKey.ITERATION_DURATION_MAP: iter_run_map}, | |||
| ) | |||
| ) | |||
| except IterationNodeError as e: | |||
| @@ -356,15 +361,19 @@ class IterationNode(BaseNode[IterationNodeData]): | |||
| start_at: datetime, | |||
| graph_engine: "GraphEngine", | |||
| iteration_graph: Graph, | |||
| iter_run_map: dict[str, float], | |||
| parallel_mode_run_id: Optional[str] = None, | |||
| ) -> Generator[NodeEvent | InNodeEvent, None, None]: | |||
| """ | |||
| run single iteration | |||
| """ | |||
| iter_start_at = datetime.now(timezone.utc).replace(tzinfo=None) | |||
| try: | |||
| rst = graph_engine.run() | |||
| # get current iteration index | |||
| current_index = variable_pool.get([self.node_id, "index"]).value | |||
| iteration_run_id = parallel_mode_run_id if parallel_mode_run_id is not None else f"{current_index}" | |||
| next_index = int(current_index) + 1 | |||
| if current_index is None: | |||
| @@ -431,6 +440,8 @@ class IterationNode(BaseNode[IterationNodeData]): | |||
| variable_pool.add([self.node_id, "index"], next_index) | |||
| if next_index < len(iterator_list_value): | |||
| variable_pool.add([self.node_id, "item"], iterator_list_value[next_index]) | |||
| duration = (datetime.now(timezone.utc).replace(tzinfo=None) - iter_start_at).total_seconds() | |||
| iter_run_map[iteration_run_id] = duration | |||
| yield IterationRunNextEvent( | |||
| iteration_id=self.id, | |||
| iteration_node_id=self.node_id, | |||
| @@ -439,6 +450,7 @@ class IterationNode(BaseNode[IterationNodeData]): | |||
| index=next_index, | |||
| parallel_mode_run_id=parallel_mode_run_id, | |||
| pre_iteration_output=None, | |||
| duration=duration, | |||
| ) | |||
| return | |||
| elif self.node_data.error_handle_mode == ErrorHandleMode.REMOVE_ABNORMAL_OUTPUT: | |||
| @@ -449,6 +461,8 @@ class IterationNode(BaseNode[IterationNodeData]): | |||
| if next_index < len(iterator_list_value): | |||
| variable_pool.add([self.node_id, "item"], iterator_list_value[next_index]) | |||
| duration = (datetime.now(timezone.utc).replace(tzinfo=None) - iter_start_at).total_seconds() | |||
| iter_run_map[iteration_run_id] = duration | |||
| yield IterationRunNextEvent( | |||
| iteration_id=self.id, | |||
| iteration_node_id=self.node_id, | |||
| @@ -457,6 +471,7 @@ class IterationNode(BaseNode[IterationNodeData]): | |||
| index=next_index, | |||
| parallel_mode_run_id=parallel_mode_run_id, | |||
| pre_iteration_output=None, | |||
| duration=duration, | |||
| ) | |||
| return | |||
| elif self.node_data.error_handle_mode == ErrorHandleMode.TERMINATED: | |||
| @@ -485,6 +500,8 @@ class IterationNode(BaseNode[IterationNodeData]): | |||
| if next_index < len(iterator_list_value): | |||
| variable_pool.add([self.node_id, "item"], iterator_list_value[next_index]) | |||
| duration = (datetime.now(timezone.utc).replace(tzinfo=None) - iter_start_at).total_seconds() | |||
| iter_run_map[iteration_run_id] = duration | |||
| yield IterationRunNextEvent( | |||
| iteration_id=self.id, | |||
| iteration_node_id=self.node_id, | |||
| @@ -493,6 +510,7 @@ class IterationNode(BaseNode[IterationNodeData]): | |||
| index=next_index, | |||
| parallel_mode_run_id=parallel_mode_run_id, | |||
| pre_iteration_output=jsonable_encoder(current_iteration_output) if current_iteration_output else None, | |||
| duration=duration, | |||
| ) | |||
| except IterationNodeError as e: | |||
| @@ -528,6 +546,7 @@ class IterationNode(BaseNode[IterationNodeData]): | |||
| iteration_graph: Graph, | |||
| index: int, | |||
| item: Any, | |||
| iter_run_map: dict[str, float], | |||
| ) -> Generator[NodeEvent | InNodeEvent, None, None]: | |||
| """ | |||
| run single iteration in parallel mode | |||
| @@ -546,6 +565,7 @@ class IterationNode(BaseNode[IterationNodeData]): | |||
| start_at=start_at, | |||
| graph_engine=graph_engine_copy, | |||
| iteration_graph=iteration_graph, | |||
| iter_run_map=iter_run_map, | |||
| parallel_mode_run_id=parallel_mode_run_id, | |||
| ): | |||
| q.put(event) | |||
| @@ -445,6 +445,7 @@ export const useWorkflowRun = () => { | |||
| ...data, | |||
| status: NodeRunningStatus.Running, | |||
| details: [], | |||
| iterDurationMap: {}, | |||
| } as any) | |||
| })) | |||
| @@ -496,6 +497,8 @@ export const useWorkflowRun = () => { | |||
| setWorkflowRunningData(produce(workflowRunningData!, (draft) => { | |||
| const iteration = draft.tracing!.find(trace => trace.node_id === data.node_id) | |||
| if (iteration) { | |||
| if (iteration.iterDurationMap && data.duration) | |||
| iteration.iterDurationMap[data.parallel_mode_run_id ?? `${data.index - 1}`] = data.duration | |||
| if (iteration.details!.length >= iteration.metadata.iterator_length!) | |||
| return | |||
| } | |||
| @@ -193,7 +193,7 @@ const BaseNode: FC<BaseNodeProps> = ({ | |||
| { | |||
| data._iterationLength && data._iterationIndex && data._runningStatus === NodeRunningStatus.Running && ( | |||
| <div className='mr-1.5 text-xs font-medium text-primary-600'> | |||
| {data._iterationIndex}/{data._iterationLength} | |||
| {data._iterationIndex > data._iterationLength ? data._iterationLength : data._iterationIndex}/{data._iterationLength} | |||
| </div> | |||
| ) | |||
| } | |||
| @@ -28,7 +28,7 @@ import IterationResultPanel from '../run/iteration-result-panel' | |||
| import InputsPanel from './inputs-panel' | |||
| import cn from '@/utils/classnames' | |||
| import Loading from '@/app/components/base/loading' | |||
| import type { NodeTracing } from '@/types/workflow' | |||
| import type { IterationDurationMap, NodeTracing } from '@/types/workflow' | |||
| const WorkflowPreview = () => { | |||
| const { t } = useTranslation() | |||
| @@ -53,12 +53,14 @@ const WorkflowPreview = () => { | |||
| }, [workflowRunningData]) | |||
| const [iterationRunResult, setIterationRunResult] = useState<NodeTracing[][]>([]) | |||
| const [iterDurationMap, setIterDurationMap] = useState<IterationDurationMap>({}) | |||
| const [isShowIterationDetail, { | |||
| setTrue: doShowIterationDetail, | |||
| setFalse: doHideIterationDetail, | |||
| }] = useBoolean(false) | |||
| const handleShowIterationDetail = useCallback((detail: NodeTracing[][]) => { | |||
| const handleShowIterationDetail = useCallback((detail: NodeTracing[][], iterationDurationMap: IterationDurationMap) => { | |||
| setIterDurationMap(iterationDurationMap) | |||
| setIterationRunResult(detail) | |||
| doShowIterationDetail() | |||
| }, [doShowIterationDetail]) | |||
| @@ -72,6 +74,7 @@ const WorkflowPreview = () => { | |||
| list={iterationRunResult} | |||
| onHide={doHideIterationDetail} | |||
| onBack={doHideIterationDetail} | |||
| iterDurationMap={iterDurationMap} | |||
| /> | |||
| </div> | |||
| ) | |||
| @@ -94,6 +97,7 @@ const WorkflowPreview = () => { | |||
| list={iterationRunResult} | |||
| onHide={doHideIterationDetail} | |||
| onBack={doHideIterationDetail} | |||
| iterDurationMap={iterDurationMap} | |||
| /> | |||
| ) | |||
| : ( | |||
| @@ -13,7 +13,7 @@ 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 { NodeTracing } from '@/types/workflow' | |||
| import type { IterationDurationMap, NodeTracing } from '@/types/workflow' | |||
| import type { WorkflowRunDetailResponse } from '@/models/log' | |||
| import { useStore as useAppStore } from '@/app/components/app/store' | |||
| @@ -172,15 +172,17 @@ const RunPanel: FC<RunProps> = ({ hideResult, activeTab = 'RESULT', runID, getRe | |||
| }, [loading]) | |||
| const [iterationRunResult, setIterationRunResult] = useState<NodeTracing[][]>([]) | |||
| const [iterDurationMap, setIterDurationMap] = useState<IterationDurationMap>({}) | |||
| const [isShowIterationDetail, { | |||
| setTrue: doShowIterationDetail, | |||
| setFalse: doHideIterationDetail, | |||
| }] = useBoolean(false) | |||
| const handleShowIterationDetail = useCallback((detail: NodeTracing[][]) => { | |||
| const handleShowIterationDetail = useCallback((detail: NodeTracing[][], iterDurationMap: IterationDurationMap) => { | |||
| setIterationRunResult(detail) | |||
| doShowIterationDetail() | |||
| }, [doShowIterationDetail]) | |||
| setIterDurationMap(iterDurationMap) | |||
| }, [doShowIterationDetail, setIterationRunResult, setIterDurationMap]) | |||
| if (isShowIterationDetail) { | |||
| return ( | |||
| @@ -189,6 +191,7 @@ const RunPanel: FC<RunProps> = ({ hideResult, activeTab = 'RESULT', runID, getRe | |||
| list={iterationRunResult} | |||
| onHide={doHideIterationDetail} | |||
| onBack={doHideIterationDetail} | |||
| iterDurationMap={iterDurationMap} | |||
| /> | |||
| </div> | |||
| ) | |||
| @@ -6,12 +6,14 @@ import { | |||
| 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 { Iteration } from '@/app/components/base/icons/src/vender/workflow' | |||
| import cn from '@/utils/classnames' | |||
| import type { NodeTracing } from '@/types/workflow' | |||
| import type { IterationDurationMap, NodeTracing } from '@/types/workflow' | |||
| const i18nPrefix = 'workflow.singleRun' | |||
| type Props = { | |||
| @@ -19,6 +21,7 @@ type Props = { | |||
| onHide: () => void | |||
| onBack: () => void | |||
| noWrap?: boolean | |||
| iterDurationMap?: IterationDurationMap | |||
| } | |||
| const IterationResultPanel: FC<Props> = ({ | |||
| @@ -26,6 +29,7 @@ const IterationResultPanel: FC<Props> = ({ | |||
| onHide, | |||
| onBack, | |||
| noWrap, | |||
| iterDurationMap, | |||
| }) => { | |||
| const { t } = useTranslation() | |||
| const [expandedIterations, setExpandedIterations] = useState<Record<number, boolean>>({}) | |||
| @@ -36,6 +40,40 @@ const IterationResultPanel: FC<Props> = ({ | |||
| [index]: !prev[index], | |||
| })) | |||
| }, []) | |||
| const countIterDuration = (iteration: NodeTracing[], iterDurationMap: IterationDurationMap): string => { | |||
| const IterRunIndex = iteration[0].execution_metadata.iteration_index as number | |||
| const iterRunId = iteration[0].execution_metadata.parallel_mode_run_id | |||
| const iterItem = iterDurationMap[iterRunId || IterRunIndex] | |||
| const duration = iterItem | |||
| return `${(duration && duration > 0.01) ? duration.toFixed(2) : 0.01}s` | |||
| } | |||
| const iterationStatusShow = (index: number, iteration: NodeTracing[], iterDurationMap?: IterationDurationMap) => { | |||
| const hasFailed = iteration.some(item => item.status === NodeRunningStatus.Failed) | |||
| const isRunning = iteration.some(item => item.status === NodeRunningStatus.Running) | |||
| const hasDurationMap = iterDurationMap && Object.keys(iterDurationMap).length !== 0 | |||
| if (hasFailed) | |||
| return <RiErrorWarningLine className='w-4 h-4 text-text-destructive' /> | |||
| if (isRunning) | |||
| return <RiLoader2Line className='w-3.5 h-3.5 text-primary-600 animate-spin' /> | |||
| return ( | |||
| <> | |||
| {hasDurationMap && ( | |||
| <div className='system-xs-regular text-text-tertiary'> | |||
| {countIterDuration(iteration, iterDurationMap)} | |||
| </div> | |||
| )} | |||
| <RiArrowRightSLine | |||
| className={cn( | |||
| 'w-4 h-4 text-text-tertiary transition-transform duration-200 flex-shrink-0', | |||
| expandedIterations[index] && 'transform rotate-90', | |||
| )} | |||
| /> | |||
| </> | |||
| ) | |||
| } | |||
| const main = ( | |||
| <> | |||
| @@ -72,19 +110,7 @@ const IterationResultPanel: FC<Props> = ({ | |||
| <span className='system-sm-semibold-uppercase text-text-primary flex-grow'> | |||
| {t(`${i18nPrefix}.iteration`)} {index + 1} | |||
| </span> | |||
| { | |||
| iteration.some(item => item.status === 'failed') | |||
| ? ( | |||
| <RiErrorWarningLine className='w-4 h-4 text-text-destructive' /> | |||
| ) | |||
| : (< RiArrowRightSLine className={ | |||
| cn( | |||
| 'w-4 h-4 text-text-tertiary transition-transform duration-200 flex-shrink-0', | |||
| expandedIterations[index] && 'transform rotate-90', | |||
| )} /> | |||
| ) | |||
| } | |||
| {iterationStatusShow(index, iteration, iterDurationMap)} | |||
| </div> | |||
| </div> | |||
| {expandedIterations[index] && <div | |||
| @@ -18,7 +18,7 @@ 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 { NodeTracing } from '@/types/workflow' | |||
| import type { IterationDurationMap, NodeTracing } from '@/types/workflow' | |||
| type Props = { | |||
| className?: string | |||
| @@ -26,7 +26,7 @@ type Props = { | |||
| inMessage?: boolean | |||
| hideInfo?: boolean | |||
| hideProcessDetail?: boolean | |||
| onShowIterationDetail?: (detail: NodeTracing[][]) => void | |||
| onShowIterationDetail?: (detail: NodeTracing[][], iterDurationMap: IterationDurationMap) => void | |||
| notShowIterationNav?: boolean | |||
| justShowIterationNavArrow?: boolean | |||
| } | |||
| @@ -90,7 +90,7 @@ const NodePanel: FC<Props> = ({ | |||
| const handleOnShowIterationDetail = (e: React.MouseEvent<HTMLButtonElement>) => { | |||
| e.stopPropagation() | |||
| e.nativeEvent.stopImmediatePropagation() | |||
| onShowIterationDetail?.(nodeInfo.details || []) | |||
| onShowIterationDetail?.(nodeInfo.details || [], nodeInfo?.iterDurationMap || nodeInfo.execution_metadata?.iteration_duration_map || {}) | |||
| } | |||
| return ( | |||
| <div className={cn('px-2 py-1', className)}> | |||
| @@ -16,11 +16,11 @@ import NodePanel from './node' | |||
| import { | |||
| BlockEnum, | |||
| } from '@/app/components/workflow/types' | |||
| import type { NodeTracing } from '@/types/workflow' | |||
| import type { IterationDurationMap, NodeTracing } from '@/types/workflow' | |||
| type TracingPanelProps = { | |||
| list: NodeTracing[] | |||
| onShowIterationDetail?: (detail: NodeTracing[][]) => void | |||
| onShowIterationDetail?: (detail: NodeTracing[][], iterDurationMap: IterationDurationMap) => void | |||
| className?: string | |||
| hideNodeInfo?: boolean | |||
| hideNodeProcessDetail?: boolean | |||
| @@ -569,9 +569,9 @@ const translation = { | |||
| MaxParallelismDesc: 'The maximum parallelism is used to control the number of tasks executed simultaneously in a single iteration.', | |||
| errorResponseMethod: 'Error response method', | |||
| ErrorMethod: { | |||
| operationTerminated: 'terminated', | |||
| continueOnError: 'continue on error', | |||
| removeAbnormalOutput: 'remove abnormal output', | |||
| operationTerminated: 'Terminated', | |||
| continueOnError: 'Continue on Error', | |||
| removeAbnormalOutput: 'Remove Abnormal Output', | |||
| }, | |||
| answerNodeWarningDesc: 'Parallel mode warning: Answer nodes, conversation variable assignments, and persistent read/write operations within iterations may cause exceptions.', | |||
| }, | |||
| @@ -33,6 +33,7 @@ export type NodeTracing = { | |||
| parent_parallel_id?: string | |||
| parent_parallel_start_node_id?: string | |||
| parallel_mode_run_id?: string | |||
| iteration_duration_map?: IterationDurationMap | |||
| } | |||
| metadata: { | |||
| iterator_length: number | |||
| @@ -44,6 +45,7 @@ export type NodeTracing = { | |||
| name: string | |||
| email: string | |||
| } | |||
| iterDurationMap?: IterationDurationMap | |||
| finished_at: number | |||
| extras?: any | |||
| expand?: boolean // for UI | |||
| @@ -207,7 +209,10 @@ export type IterationNextResponse = { | |||
| parallel_mode_run_id: string | |||
| execution_metadata: { | |||
| parallel_id?: string | |||
| iteration_index: number | |||
| parallel_mode_run_id?: string | |||
| } | |||
| duration?: number | |||
| } | |||
| } | |||
| @@ -323,3 +328,5 @@ export type ConversationVariableResponse = { | |||
| total: number | |||
| page: number | |||
| } | |||
| export type IterationDurationMap = Record<string, number> | |||