diff --git a/web/app/components/header/account-setting/model-provider-page/model-selector/deprecated-model-trigger.tsx b/web/app/components/header/account-setting/model-provider-page/model-selector/deprecated-model-trigger.tsx index bcc573b06a..069e026b64 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-selector/deprecated-model-trigger.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-selector/deprecated-model-trigger.tsx @@ -10,11 +10,15 @@ type ModelTriggerProps = { modelName: string providerName: string className?: string + showWarnIcon?: boolean + contentClassName?: string } const ModelTrigger: FC = ({ modelName, providerName, className, + showWarnIcon, + contentClassName, }) => { const { t } = useTranslation() const { modelProviders } = useProviderContext() @@ -24,7 +28,7 @@ const ModelTrigger: FC = ({
-
+
= ({
- - - + {showWarnIcon && ( + + + + )}
diff --git a/web/app/components/header/account-setting/model-provider-page/model-selector/index.tsx b/web/app/components/header/account-setting/model-provider-page/model-selector/index.tsx index da31cdeee5..d28959a509 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-selector/index.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-selector/index.tsx @@ -15,6 +15,7 @@ import { PortalToFollowElemContent, PortalToFollowElemTrigger, } from '@/app/components/base/portal-to-follow-elem' +import classNames from '@/utils/classnames' type ModelSelectorProps = { defaultModel?: DefaultModel @@ -24,6 +25,8 @@ type ModelSelectorProps = { onSelect?: (model: DefaultModel) => void readonly?: boolean scopeFeatures?: string[] + deprecatedClassName?: string + showDeprecatedWarnIcon?: boolean } const ModelSelector: FC = ({ defaultModel, @@ -33,6 +36,8 @@ const ModelSelector: FC = ({ onSelect, readonly, scopeFeatures = [], + deprecatedClassName, + showDeprecatedWarnIcon = false, }) => { const [open, setOpen] = useState(false) const { @@ -64,7 +69,7 @@ const ModelSelector: FC = ({ placement='bottom-start' offset={4} > -
+
= ({ modelName={defaultModel?.model || ''} providerName={defaultModel?.provider || ''} className={triggerClassName} + showWarnIcon={showDeprecatedWarnIcon} + contentClassName={deprecatedClassName} /> ) } diff --git a/web/app/components/plugins/marketplace/hooks.ts b/web/app/components/plugins/marketplace/hooks.ts index be7554a847..8f811d1b3e 100644 --- a/web/app/components/plugins/marketplace/hooks.ts +++ b/web/app/components/plugins/marketplace/hooks.ts @@ -151,7 +151,7 @@ export const useSearchBoxAutoAnimate = (searchBoxAutoAnimate?: boolean) => { if (!searchBoxAutoAnimate) { const clientWidth = document.documentElement.clientWidth - if (clientWidth < 1350) + if (clientWidth < 1400) setSearchBoxCanAnimate(false) else setSearchBoxCanAnimate(true) diff --git a/web/app/components/plugins/marketplace/list/list-wrapper.tsx b/web/app/components/plugins/marketplace/list/list-wrapper.tsx index 155dfe397a..2dc83ee831 100644 --- a/web/app/components/plugins/marketplace/list/list-wrapper.tsx +++ b/web/app/components/plugins/marketplace/list/list-wrapper.tsx @@ -37,10 +37,10 @@ const ListWrapper = ({ }, [handleQueryPlugins, marketplaceCollections, marketplaceCollectionsFromClient, isSuccessCollections]) return ( -
+
{ plugins && ( -
+
{t('plugin.marketplace.pluginsResult', { num: pluginsTotal })}
diff --git a/web/app/components/workflow/nodes/_base/components/agent-strategy-selector.tsx b/web/app/components/workflow/nodes/_base/components/agent-strategy-selector.tsx index db684ea2df..a96654c4d9 100644 --- a/web/app/components/workflow/nodes/_base/components/agent-strategy-selector.tsx +++ b/web/app/components/workflow/nodes/_base/components/agent-strategy-selector.tsx @@ -99,22 +99,22 @@ export const AgentStrategySelector = memo((props: AgentStrategySelectorProps) => if (!list) return [] return list.filter(tool => tool.name.toLowerCase().includes(query.toLowerCase())) }, [query, list]) - const { strategyStatus } = useStrategyInfo( + const { strategyStatus, refetch: refetchStrategyInfo } = useStrategyInfo( value?.agent_strategy_provider_name, value?.agent_strategy_name, ) const showPluginNotInstalledWarn = strategyStatus?.plugin?.source === 'external' - && !strategyStatus.plugin.installed + && !strategyStatus.plugin.installed && !!value const showUnsupportedStrategy = strategyStatus?.plugin.source === 'external' - && !strategyStatus?.isExistInPlugin + && !strategyStatus?.isExistInPlugin && !!value const showSwitchVersion = !strategyStatus?.isExistInPlugin - && strategyStatus?.plugin.source === 'marketplace' && strategyStatus.plugin.installed + && strategyStatus?.plugin.source === 'marketplace' && strategyStatus.plugin.installed && !!value const showInstallButton = !strategyStatus?.isExistInPlugin - && strategyStatus?.plugin.source === 'marketplace' && !strategyStatus.plugin.installed + && strategyStatus?.plugin.source === 'marketplace' && !strategyStatus.plugin.installed && !!value const icon = list?.find( coll => coll.tools?.find(tool => tool.name === value?.agent_strategy_name), @@ -159,8 +159,8 @@ export const AgentStrategySelector = memo((props: AgentStrategySelectorProps) => > {value?.agent_strategy_label || t('workflow.nodes.agent.strategy.selectTip')}

- {value &&
- {showInstallButton && + {showInstallButton && value && e.stopPropagation()} size={'small'} uniqueIdentifier={value.plugin_unique_identifier} @@ -178,16 +178,16 @@ export const AgentStrategySelector = memo((props: AgentStrategySelectorProps) => : } {showSwitchVersion && {t('workflow.nodes.agent.strategyNotFoundDescAndSwitchVersion')} } onChange={() => { - // TODO: refresh all strategies + refetchStrategyInfo() }} />} -
} +
diff --git a/web/app/components/workflow/nodes/_base/components/agent-strategy.tsx b/web/app/components/workflow/nodes/_base/components/agent-strategy.tsx index 295c0a6252..01a1aba24e 100644 --- a/web/app/components/workflow/nodes/_base/components/agent-strategy.tsx +++ b/web/app/components/workflow/nodes/_base/components/agent-strategy.tsx @@ -82,6 +82,7 @@ export const AgentStrategy = memo((props: AgentStrategyProps) => { availableNodes={availableNodes} nodesOutputVars={nodeOutputVars} isSupportJinja={def.template?.enabled} + required={def.required} varList={[]} modelConfig={ defaultModel.data @@ -107,7 +108,13 @@ export const AgentStrategy = memo((props: AgentStrategyProps) => { const onChange = (value: number) => { props.onChange({ ...props.value, [schema.variable]: value }) } - return + return + {renderI18nObject(def.label)} {def.required && *} + } + tooltip={def.tooltip && renderI18nObject(def.tooltip)} + inline + >
{ props.onChange({ ...props.value, [schema.variable]: value }) } return ( - + + {renderI18nObject(schema.label)} {schema.required && *} + } + tooltip={schema.tooltip && renderI18nObject(schema.tooltip)} + > { tooltip={schema.tooltip && renderI18nObject(schema.tooltip)} onChange={onChange} supportCollapse + required={schema.required} /> ) } diff --git a/web/app/components/workflow/nodes/_base/components/field.tsx b/web/app/components/workflow/nodes/_base/components/field.tsx index eb8976f78d..d5d5ed7394 100644 --- a/web/app/components/workflow/nodes/_base/components/field.tsx +++ b/web/app/components/workflow/nodes/_base/components/field.tsx @@ -1,18 +1,17 @@ 'use client' -import type { FC } from 'react' +import type { FC, ReactNode } from 'react' import React from 'react' import { RiArrowDownSLine, } from '@remixicon/react' import { useBoolean } from 'ahooks' -import type { DefaultTFuncReturn } from 'i18next' import cn from '@/utils/classnames' import Tooltip from '@/app/components/base/tooltip' type Props = { className?: string - title: JSX.Element | string | DefaultTFuncReturn - tooltip?: React.ReactNode + title: ReactNode + tooltip?: ReactNode isSubTitle?: boolean supportFold?: boolean children?: JSX.Element | string | null diff --git a/web/app/components/workflow/nodes/_base/components/prompt/editor.tsx b/web/app/components/workflow/nodes/_base/components/prompt/editor.tsx index 43db8a276e..9b7c63d862 100644 --- a/web/app/components/workflow/nodes/_base/components/prompt/editor.tsx +++ b/web/app/components/workflow/nodes/_base/components/prompt/editor.tsx @@ -75,6 +75,7 @@ type Props = { editorContainerClassName?: string placeholderClassName?: string titleClassName?: string + required?: boolean } const Editor: FC = ({ @@ -110,6 +111,7 @@ const Editor: FC = ({ placeholderClassName, titleClassName, editorContainerClassName, + required, }) => { const { t } = useTranslation() const { eventEmitter } = useEventEmitterContextContext() @@ -147,7 +149,7 @@ const Editor: FC = ({
-
{title}
+
{title} {required && *}
{titleTooltip && }
diff --git a/web/app/components/workflow/nodes/agent/components/model-bar.tsx b/web/app/components/workflow/nodes/agent/components/model-bar.tsx new file mode 100644 index 0000000000..dffbf40d36 --- /dev/null +++ b/web/app/components/workflow/nodes/agent/components/model-bar.tsx @@ -0,0 +1,67 @@ +import Tooltip from '@/app/components/base/tooltip' +import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' +import { useModelList } from '@/app/components/header/account-setting/model-provider-page/hooks' +import ModelSelector from '@/app/components/header/account-setting/model-provider-page/model-selector' +import Indicator from '@/app/components/header/indicator' +import { type FC, useMemo } from 'react' +import { useTranslation } from 'react-i18next' + +export type ModelBarProps = { + provider: string + model: string +} | {} + +const useAllModel = () => { + const { data: textGeneration } = useModelList(ModelTypeEnum.textGeneration) + const { data: moderation } = useModelList(ModelTypeEnum.moderation) + const { data: rerank } = useModelList(ModelTypeEnum.rerank) + const { data: speech2text } = useModelList(ModelTypeEnum.speech2text) + const { data: textEmbedding } = useModelList(ModelTypeEnum.textEmbedding) + const { data: tts } = useModelList(ModelTypeEnum.tts) + const models = useMemo(() => { + return textGeneration + .concat(moderation) + .concat(rerank) + .concat(speech2text) + .concat(textEmbedding) + .concat(tts) + }, [textGeneration, moderation, rerank, speech2text, textEmbedding, tts]) + if (!textGeneration || !moderation || !rerank || !speech2text || !textEmbedding || !tts) + return undefined + return models +} + +export const ModelBar: FC = (props) => { + const { t } = useTranslation() + const modelList = useAllModel() + if (!('provider' in props)) { + return + } + const modelInstalled = modelList?.some( + provider => provider.provider === props.provider && provider.models.some(model => model.model === props.model)) + const showWarn = modelList && !modelInstalled + return modelList && +
+ + {showWarn && } +
+
+} diff --git a/web/app/components/workflow/nodes/agent/components/tool-icon.tsx b/web/app/components/workflow/nodes/agent/components/tool-icon.tsx index 99dc45d46c..0d2d3c2837 100644 --- a/web/app/components/workflow/nodes/agent/components/tool-icon.tsx +++ b/web/app/components/workflow/nodes/agent/components/tool-icon.tsx @@ -1,10 +1,11 @@ import Tooltip from '@/app/components/base/tooltip' import Indicator from '@/app/components/header/indicator' import classNames from '@/utils/classnames' -import { memo, useMemo, useRef } from 'react' +import { memo, useMemo, useRef, useState } from 'react' import { useAllBuiltInTools, useAllCustomTools, useAllWorkflowTools } from '@/service/use-tools' import { getIconFromMarketPlace } from '@/utils/get-icon' import { useTranslation } from 'react-i18next' +import { Group } from '@/app/components/base/icons/src/vender/other' type Status = 'not-installed' | 'not-authorized' | undefined @@ -45,21 +46,31 @@ export const ToolIcon = memo(({ providerName }: ToolIconProps) => { if (status === 'not-authorized') return t('workflow.nodes.agent.toolNotAuthorizedTooltip', { tool: name }) throw new Error('Unknown status') }, [name, notSuccess, status, t]) - return -
+
- {/* eslint-disable-next-line @next/next/no-img-element */} - tool icon + {!iconFetchError + // eslint-disable-next-line @next/next/no-img-element + ? tool icon setIconFetchError(true)} + /> + : + } {indicator && }
diff --git a/web/app/components/workflow/nodes/agent/node.tsx b/web/app/components/workflow/nodes/agent/node.tsx index f2c93cfc4d..0bc2fb4caf 100644 --- a/web/app/components/workflow/nodes/agent/node.tsx +++ b/web/app/components/workflow/nodes/agent/node.tsx @@ -2,41 +2,19 @@ import { type FC, memo, useMemo } from 'react' import type { NodeProps } from '../../types' import type { AgentNodeType } from './types' import { SettingItem } from '../_base/components/setting-item' -import ModelSelector from '@/app/components/header/account-setting/model-provider-page/model-selector' import { Group, GroupLabel } from '../_base/components/group' import type { ToolIconProps } from './components/tool-icon' import { ToolIcon } from './components/tool-icon' import useConfig from './use-config' import { useTranslation } from 'react-i18next' -import { FormTypeEnum, ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' -import { useModelList } from '@/app/components/header/account-setting/model-provider-page/hooks' +import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import { useRenderI18nObject } from '@/hooks/use-i18n' - -const useAllModel = () => { - const { data: textGeneration } = useModelList(ModelTypeEnum.textGeneration) - const { data: moderation } = useModelList(ModelTypeEnum.moderation) - const { data: rerank } = useModelList(ModelTypeEnum.rerank) - const { data: speech2text } = useModelList(ModelTypeEnum.speech2text) - const { data: textEmbedding } = useModelList(ModelTypeEnum.textEmbedding) - const { data: tts } = useModelList(ModelTypeEnum.tts) - const models = useMemo(() => { - return textGeneration - .concat(moderation) - .concat(rerank) - .concat(speech2text) - .concat(textEmbedding) - .concat(tts) - }, [textGeneration, moderation, rerank, speech2text, textEmbedding, tts]) - if (!textGeneration || !moderation || !rerank || !speech2text || !textEmbedding || !tts) - return undefined - return models -} +import { ModelBar } from './components/model-bar' const AgentNode: FC> = (props) => { const { inputs, currentStrategy, currentStrategyStatus, pluginDetail } = useConfig(props.id, props.data) const renderI18nObject = useRenderI18nObject() const { t } = useTranslation() - const modelList = useAllModel() const models = useMemo(() => { if (!inputs) return [] // if selected, show in node @@ -46,6 +24,7 @@ const AgentNode: FC> = (props) => { .filter(param => param.type === FormTypeEnum.modelSelector) .reduce((acc, param) => { const item = inputs.agent_parameters?.[param.name]?.value + console.log({ item }) if (!item) { if (param.required) { acc.push({ param: param.name }) @@ -102,27 +81,18 @@ const AgentNode: FC> = (props) => { {inputs.agent_strategy_label} : } - {models.length > 0 && modelList && {t('workflow.nodes.agent.model')} } > {models.map((model) => { - return })} - } + {tools.length > 0 && {t('workflow.nodes.agent.toolbox')} }> diff --git a/web/app/components/workflow/nodes/agent/panel.tsx b/web/app/components/workflow/nodes/agent/panel.tsx index 2b1f3827b3..a0210d0363 100644 --- a/web/app/components/workflow/nodes/agent/panel.tsx +++ b/web/app/components/workflow/nodes/agent/panel.tsx @@ -73,7 +73,7 @@ const AgentPanel: FC> = (props) => { })() return
- + { + strategyProvider.refetch() + marketplace.refetch() + }, [marketplace, strategyProvider]) return { strategyProvider, strategy, strategyStatus, + refetch, } } diff --git a/web/app/components/workflow/run/agent-log/agent-log-item.tsx b/web/app/components/workflow/run/agent-log/agent-log-item.tsx index 36b1d78bc6..49c279d58a 100644 --- a/web/app/components/workflow/run/agent-log/agent-log-item.tsx +++ b/web/app/components/workflow/run/agent-log/agent-log-item.tsx @@ -23,6 +23,7 @@ const AgentLogItem = ({ status, children, data, + metadata, } = item const [expanded, setExpanded] = useState(false) @@ -41,8 +42,17 @@ const AgentLogItem = ({ : }
-
{label}
- {/*
0.02s
*/} +
+ {label} +
+ { + metadata?.elapsed_time && ( +
{metadata?.elapsed_time?.toFixed(3)}s
+ ) + }
{ diff --git a/web/app/components/workflow/run/agent-log/agent-log-nav.tsx b/web/app/components/workflow/run/agent-log/agent-log-nav.tsx index 0506f5ded0..ccfc6da8cf 100644 --- a/web/app/components/workflow/run/agent-log/agent-log-nav.tsx +++ b/web/app/components/workflow/run/agent-log/agent-log-nav.tsx @@ -62,7 +62,7 @@ const AgentLogNav = ({ ) } { - !!end && agentOrToolLogItemStackLength > 2 && ( + !!end && agentOrToolLogItemStackLength > 1 && ( <>
/
diff --git a/web/app/components/workflow/run/utils/format-log/graph-to-log-struct-2.spec.ts b/web/app/components/workflow/run/utils/format-log/graph-to-log-struct-2.spec.ts deleted file mode 100644 index 5e00cd8ca7..0000000000 --- a/web/app/components/workflow/run/utils/format-log/graph-to-log-struct-2.spec.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { parseDSL } from './graph-to-log-struct-2' - -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') - }) -}) diff --git a/web/app/components/workflow/run/utils/format-log/graph-to-log-struct-2.ts b/web/app/components/workflow/run/utils/format-log/graph-to-log-struct-2.ts deleted file mode 100644 index 9b5a830e98..0000000000 --- a/web/app/components/workflow/run/utils/format-log/graph-to-log-struct-2.ts +++ /dev/null @@ -1,304 +0,0 @@ -type IterationInfo = { iterationId: string; iterationIndex: number } -type NodePlain = { nodeType: 'plain'; nodeId: string; } & Partial -type NodeComplex = { nodeType: string; nodeId: string; params: (NodePlain | (NodeComplex & Partial) | Node[] | number)[] } & Partial -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; - 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 { parseDSL } diff --git a/web/app/components/workflow/run/utils/format-log/graph-to-log-struct.spec.ts b/web/app/components/workflow/run/utils/format-log/graph-to-log-struct.spec.ts index f4d78b62f2..18758404ff 100644 --- a/web/app/components/workflow/run/utils/format-log/graph-to-log-struct.spec.ts +++ b/web/app/components/workflow/run/utils/format-log/graph-to-log-struct.spec.ts @@ -1,97 +1,128 @@ -import graphToLogStruct, { parseNodeString } from './graph-to-log-struct' +import parseDSL from './graph-to-log-struct' -describe('graphToLogStruct', () => { - test('parseNodeString', () => { - expect(parseNodeString('(node1, param1, (node2, param2, (node3, param1)), param4)')).toEqual({ - node: 'node1', - params: [ - 'param1', - { - node: 'node2', - params: [ - 'param2', - { - node: 'node3', - params: [ - 'param1', - ], - }, - ], +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', }, - 'param4', - ], - }) - }) - test('iteration nodes', () => { - expect(graphToLogStruct('start -> (iteration, 1, [2, 3])')).toEqual([ - { - id: 'start', - node_id: 'start', - title: 'start', - execution_metadata: {}, status: 'succeeded', }, { - id: '1', - node_id: '1', - title: '1', - execution_metadata: {}, - status: 'succeeded', - node_type: 'iteration', - }, - { - id: '2', - node_id: '2', - title: '2', - execution_metadata: { iteration_id: '1', iteration_index: 0 }, + 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: '3', - node_id: '3', - title: '3', - execution_metadata: { iteration_id: '1', iteration_index: 1 }, + id: 'plainNode3', + node_id: 'plainNode3', + title: 'plainNode3', + execution_metadata: { + parallel_id: 'outerParallel', + parallel_start_node_id: 'plainNode3', + }, status: 'succeeded', }, ]) }) - test('retry nodes', () => { - expect(graphToLogStruct('start -> (retry, 1, 3)')).toEqual([ - { - id: 'start', - node_id: 'start', - title: 'start', - execution_metadata: {}, - status: 'succeeded', - }, - { - id: '1', - node_id: '1', - title: '1', - execution_metadata: {}, - status: 'succeeded', - }, - { - id: '1', - node_id: '1', - title: '1', - execution_metadata: {}, - status: 'retry', - }, - { - id: '1', - node_id: '1', - title: '1', - execution_metadata: {}, - status: 'retry', - }, - { - id: '1', - node_id: '1', - title: '1', - execution_metadata: {}, - status: 'retry', - }, + + // 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') + }) }) diff --git a/web/app/components/workflow/run/utils/format-log/graph-to-log-struct.ts b/web/app/components/workflow/run/utils/format-log/graph-to-log-struct.ts index 0a3a04da09..a6b20b0565 100644 --- a/web/app/components/workflow/run/utils/format-log/graph-to-log-struct.ts +++ b/web/app/components/workflow/run/utils/format-log/graph-to-log-struct.ts @@ -1,174 +1,304 @@ -const STEP_SPLIT = '->' +type IterationInfo = { iterationId: string; iterationIndex: number } +type NodePlain = { nodeType: 'plain'; nodeId: string; } & Partial +type NodeComplex = { nodeType: string; nodeId: string; params: (NodePlain | (NodeComplex & Partial) | Node[] | number)[] } & Partial +type Node = NodePlain | NodeComplex -const toNodeData = (step: string, info: Record = {}): any => { - const [nodeId, title] = step.split('@') - - const data: Record = { - id: nodeId, - node_id: nodeId, - title: title || nodeId, - execution_metadata: {}, - status: 'succeeded', - } - - const executionMetadata = data.execution_metadata - const { isRetry, isIteration, inIterationInfo } = info - if (isRetry) - data.status = 'retry' - - if (isIteration) - data.node_type = 'iteration' - - if (inIterationInfo) { - executionMetadata.iteration_id = inIterationInfo.iterationId - executionMetadata.iteration_index = inIterationInfo.iterationIndex - } - - return data +/** + * 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))) } -const toRetryNodeData = ({ - nodeId, - repeatTimes, -}: { - nodeId: string, - repeatTimes: number, -}): any => { - const res = [toNodeData(nodeId)] - for (let i = 0; i < repeatTimes; i++) - res.push(toNodeData(nodeId, { isRetry: true })) - return res -} +/** + * 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 -const toIterationNodeData = ({ - nodeId, - children, -}: { - nodeId: string, - children: number[], -}) => { - const res = [toNodeData(nodeId, { isIteration: true })] - // TODO: handle inner node structure - for (let i = 0; i < children.length; i++) { - const step = `${children[i]}` - res.push(toNodeData(step, { inIterationInfo: { iterationId: nodeId, iterationIndex: i } })) - } - - return res -} - -type NodeStructure = { - node: string; - params: Array; -} - -export function parseNodeString(input: string): NodeStructure { - input = input.trim() - if (input.startsWith('(') && input.endsWith(')')) - input = input.slice(1, -1) - - const parts: Array = [] - let current = '' - let depth = 0 - let inArrayDepth = 0 - - for (let i = 0; i < input.length; i++) { - const char = input[i] - - if (char === '(') - depth++ - else if (char === ')') - depth-- - - if (char === '[') - inArrayDepth++ - else if (char === ']') - inArrayDepth-- - - const isInArray = inArrayDepth > 0 - - if (char === ',' && depth === 0 && !isInArray) { - parts.push(current.trim()) - current = '' + 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 { - current += char + buffer += char } } + if (buffer.trim()) + segments.push(buffer.trim()) - if (current) - parts.push(current.trim()) + return segments +} - const result: NodeStructure = { - node: '', - params: [], - } +/** + * 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[] = [] - for (let i = 0; i < parts.length; i++) { - const part = parts[i] + // 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 (typeof part === 'string') { - if (part.startsWith('(')) - result.params.push(parseNodeString(part)) - - if (part.startsWith('[')) { - const content = part.slice(1, -1) - result.params.push(parseNodeString(content)) + if (char === ',' && nested === 0) { + parts.push(buffer.trim()) + buffer = '' + } + else { + buffer += char } } - else if (i === 0) { - result.node = part as unknown as string + 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 { - result.params.push(part as unknown as string) + // Parse as a plain node + return parseNode(part, iterationId) } + }) +} + +type NodeData = { + id: string; + node_id: string; + title: string; + node_type?: string; + execution_metadata: Record; + 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 } -const toNodes = (input: string): any[] => { - const list = input.split(STEP_SPLIT) - .map(step => step.trim()) +/** + * 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: {}, + }, + ] - const res: any[] = [] - list.forEach((step) => { - const isPlainStep = !step.includes('(') - if (isPlainStep) { - res.push(toNodeData(step)) - return - } - - const { node, params } = parseNodeString(step) - switch (node) { - case 'iteration': - console.log(params) - break - res.push(...toIterationNodeData({ - nodeId: params[0] as string, - children: JSON.parse(params[1] as string) as number[], - })) - break - case 'retry': - res.push(...toRetryNodeData({ - nodeId: params[0] as string, - repeatTimes: Number.parseInt(params[1] as string), - })) - break + 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 res + + return result } -/* -* : 1 -> 2 -> 3 -* iteration: (iteration, 1, [2, 3]) -> 4. (1, [2, 3]) means 1 is parent, [2, 3] is children -* parallel: 1 -> (parallel, [1,2,3], [4, (parallel: (6,7))]). -* retry: (retry, 1, 3). 1 is parent, 3 is retry times -*/ -const graphToLogStruct = (input: string): any[] => { - const list = toNodes(input) - return list +/** + * 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 } -export default graphToLogStruct +/** + * 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 diff --git a/web/app/components/workflow/run/utils/format-log/iteration/index.spec.ts b/web/app/components/workflow/run/utils/format-log/iteration/index.spec.ts index 4c49c41420..ca23c4d9ee 100644 --- a/web/app/components/workflow/run/utils/format-log/iteration/index.spec.ts +++ b/web/app/components/workflow/run/utils/format-log/iteration/index.spec.ts @@ -2,8 +2,8 @@ import format from '.' import graphToLogStruct from '../graph-to-log-struct' describe('iteration', () => { - const list = graphToLogStruct('start -> (iteration, 1, [2, 3])') - const [startNode, iterationNode, ...iterations] = graphToLogStruct('start -> (iteration, 1, [2, 3])') + 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() @@ -14,8 +14,7 @@ describe('iteration', () => { { ...iterationNode, details: [ - [iterations[0]], - [iterations[1]], + [iterations[0], iterations[1]], ], }, ]) diff --git a/web/app/components/workflow/run/utils/format-log/parallel/index.spec.ts b/web/app/components/workflow/run/utils/format-log/parallel/index.spec.ts new file mode 100644 index 0000000000..d1ce052ee8 --- /dev/null +++ b/web/app/components/workflow/run/utils/format-log/parallel/index.spec.ts @@ -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], + ], + }, + }, + ]) + }) +}) diff --git a/web/app/components/workflow/run/utils/format-log/retry/index.spec.ts b/web/app/components/workflow/run/utils/format-log/retry/index.spec.ts index 099b987843..a8f46e96b1 100644 --- a/web/app/components/workflow/run/utils/format-log/retry/index.spec.ts +++ b/web/app/components/workflow/run/utils/format-log/retry/index.spec.ts @@ -3,9 +3,9 @@ import graphToLogStruct from '../graph-to-log-struct' describe('retry', () => { // retry nodeId:1 3 times. - const steps = graphToLogStruct('start -> (retry, 1, 3)') + const steps = graphToLogStruct('start -> (retry, retryNode, 3)') const [startNode, retryNode, ...retryDetail] = steps - const result = format(steps) + const result = format(steps as any) test('should have no retry status nodes', () => { expect(result.find(item => (item as any).status === 'retry')).toBeUndefined() }) diff --git a/web/i18n/en-US/plugin.ts b/web/i18n/en-US/plugin.ts index d5bc3ecbf8..ea93b8812d 100644 --- a/web/i18n/en-US/plugin.ts +++ b/web/i18n/en-US/plugin.ts @@ -162,12 +162,12 @@ const translation = { selectPackagePlaceholder: 'Please select a package', }, upgrade: { - title: 'Upgrade Plugin', - successfulTitle: 'Upgrade successful', - description: 'About to upgrade the following plugin', + title: 'Install Plugin', + successfulTitle: 'Install successful', + description: 'About to install the following plugin', usedInApps: 'Used in {{num}} apps', - upgrade: 'Upgrade', - upgrading: 'Upgrading...', + upgrade: 'Install', + upgrading: 'Installing...', close: 'Close', }, error: { diff --git a/web/i18n/en-US/workflow.ts b/web/i18n/en-US/workflow.ts index e3f9c78ba9..bbdcac19ac 100644 --- a/web/i18n/en-US/workflow.ts +++ b/web/i18n/en-US/workflow.ts @@ -703,6 +703,7 @@ const translation = { agent: { strategy: { label: 'Agentic Strategy', + tooltip: 'Different Agentic strategies determine how the system plans and executes multi-step tool calls', shortLabel: 'Strategy', configureTip: 'Please configure agentic strategy.', configureTipDesc: 'After configuring the agentic strategy, this node will automatically load the remaining configurations. The strategy will affect the mechanism of multi-step tool reasoning. ', diff --git a/web/i18n/zh-Hans/plugin.ts b/web/i18n/zh-Hans/plugin.ts index 50219dc322..53f2ef8e5c 100644 --- a/web/i18n/zh-Hans/plugin.ts +++ b/web/i18n/zh-Hans/plugin.ts @@ -162,12 +162,12 @@ const translation = { selectPackagePlaceholder: '请选择一个包', }, upgrade: { - title: '升级插件', - successfulTitle: '升级成功', - description: '即将升级以下插件', + title: '安装插件', + successfulTitle: '安装成功', + description: '即将安装以下插件', usedInApps: '在 {{num}} 个应用中使用', - upgrade: '升级', - upgrading: '升级中...', + upgrade: '安装', + upgrading: '安装中...', close: '关闭', }, error: { diff --git a/web/i18n/zh-Hans/workflow.ts b/web/i18n/zh-Hans/workflow.ts index 9bb8337972..ea4ae6654a 100644 --- a/web/i18n/zh-Hans/workflow.ts +++ b/web/i18n/zh-Hans/workflow.ts @@ -703,6 +703,7 @@ const translation = { agent: { strategy: { label: 'Agent 策略', + tooltip: '不同的 Agent 策略决定了系统如何规划和执行多步工具调用', shortLabel: '策略', configureTip: '请配置 Agent 策略。', configureTipDesc: '配置完成后,此节点将自动加载剩余配置。策略将影响多步工具推理的机制。', diff --git a/web/types/workflow.ts b/web/types/workflow.ts index b152c3615b..9cc1cbac15 100644 --- a/web/types/workflow.ts +++ b/web/types/workflow.ts @@ -11,6 +11,10 @@ export type AgentLogItem = { data: object, // debug data error?: string, status: string, + metadata?: { + elapsed_time?: number + provider?: string + }, } export type AgentLogItemWithChildren = AgentLogItem & {