Merge branch 'feat/plugins' into dev/plugin-deploy

fix/switch-strategy-clean-param
JzoNg 1 year ago
commit 6d8b54f1e5

@ -46,7 +46,7 @@ export default function ChartView({ appId }: IChartViewProps) {
return (
<div>
<div className='flex flex-row items-center mt-8 mb-4 text-gray-900 text-base'>
<div className='flex flex-row items-center mt-8 mb-4 system-xl-semibold text-text-primary'>
<span className='mr-3'>{t('appOverview.analysis.title')}</span>
<SimpleSelect
items={Object.entries(TIME_PERIOD_MAPPING).map(([k, v]) => ({ value: k, name: t(`appLog.filter.period.${v.name}`) }))}

@ -12,7 +12,7 @@ const Overview = async ({
params: { appId },
}: IDevelopProps) => {
return (
<div className="h-full px-4 sm:px-16 py-6 overflow-scroll">
<div className="h-full px-4 sm:px-12 py-6 overflow-scroll bg-chatbot-bg">
<ApikeyInfoPanel />
<TracingPanel />
<CardView appId={appId} />

@ -60,18 +60,18 @@ export default function AppBasic({ icon, icon_background, name, isExternal, type
return (
<div className="flex items-start p-1">
{icon && icon_background && iconType === 'app' && (
<div className='flex-shrink-0 mr-3'>
<div className='shrink-0 mr-3'>
<AppIcon icon={icon} background={icon_background} />
</div>
)}
{iconType !== 'app'
&& <div className='flex-shrink-0 mr-3'>
&& <div className='shrink-0 mr-3'>
{ICON_MAP[iconType]}
</div>
}
{mode === 'expand' && <div className="group">
<div className={`flex flex-row items-center text-sm font-semibold text-gray-700 group-hover:text-gray-900 break-all ${textStyle?.main ?? ''}`}>
<div className={`flex flex-row items-center text-sm font-semibold text-text-secondary group-hover:text-text-primary break-all ${textStyle?.main ?? ''}`}>
{name}
{hoverTip
&& <Tooltip
@ -86,7 +86,7 @@ export default function AppBasic({ icon, icon_background, name, isExternal, type
/>
}
</div>
<div className={`text-xs font-normal text-gray-500 group-hover:text-gray-700 break-all ${textStyle?.extra ?? ''}`}>{type}</div>
<div className={`text-xs font-normal text-text-tertiary group-hover:text-text-secondary break-all ${textStyle?.extra ?? ''}`}>{type}</div>
<div className='text-text-tertiary system-2xs-medium-uppercase'>{isExternal ? t('dataset.externalTag') : ''}</div>
</div>}
</div>

@ -231,7 +231,7 @@ const Chart: React.FC<IChartProps> = ({
const sumData = isAvg ? (sum(yData) / yData.length) : sum(yData)
return (
<div className={`flex flex-col w-full px-6 py-4 border-[0.5px] rounded-lg border-gray-200 shadow-xs ${className ?? ''}`}>
<div className={`flex flex-col w-full px-6 py-4 rounded-xl bg-components-chart-bg shadow-xs ${className ?? ''}`}>
<div className='mb-3'>
<Basic name={title} type={timePeriod} hoverTip={explanation} />
</div>
@ -242,11 +242,11 @@ const Chart: React.FC<IChartProps> = ({
type={!CHART_TYPE_CONFIG[chartType].showTokens
? ''
: <span>{t('appOverview.analysis.tokenUsage.consumed')} Tokens<span className='text-sm'>
<span className='ml-1 text-gray-500'>(</span>
<span className='ml-1 text-text-tertiary'>(</span>
<span className='text-orange-400'>~{sum(statistics.map(item => Number.parseFloat(get(item, 'total_price', '0')))).toLocaleString('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 4 })}</span>
<span className='text-gray-500'>)</span>
<span className='text-text-tertiary'>)</span>
</span></span>}
textStyle={{ main: `!text-3xl !font-normal ${sumData === 0 ? '!text-gray-300' : ''}` }} />
textStyle={{ main: `!text-3xl !font-normal ${sumData === 0 ? '!text-text-quaternary' : ''}` }} />
</div>
<ReactECharts option={options} style={{ height: 160 }} />
</div>

@ -1,10 +1,11 @@
import type { ReactNode } from 'react'
import { memo } from 'react'
import cn from '@/utils/classnames'
type BadgeProps = {
className?: string
text?: string
children?: React.ReactNode
text?: ReactNode
children?: ReactNode
uppercase?: boolean
hasRedCornerMark?: boolean
}

@ -29,7 +29,7 @@ export default function Modal({
}: IModal) {
return (
<Transition appear show={isShow} as={Fragment}>
<Dialog as="div" className={classNames('relative z-50', wrapperClassName)} onClose={onClose}>
<Dialog as="div" className={classNames('relative z-[60]', wrapperClassName)} onClose={onClose}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"

@ -245,7 +245,7 @@ const SimpleSelect: FC<ISelectProps> = ({
leaveTo="opacity-0"
>
<Listbox.Options className={classNames('absolute z-10 mt-1 px-1 max-h-60 w-full overflow-auto rounded-md bg-components-panel-bg-blur py-1 text-base shadow-lg border-components-panel-border border-[0.5px] focus:outline-none sm:text-sm', optionWrapClassName)}>
<Listbox.Options className={classNames('absolute z-10 mt-1 px-1 max-h-60 w-full overflow-auto rounded-md bg-components-panel-bg-blur backdrop-blur-sm py-1 text-base shadow-lg border-components-panel-border border-[0.5px] focus:outline-none sm:text-sm', optionWrapClassName)}>
{items.map((item: Item) => (
<Listbox.Option
key={item.value}

@ -28,7 +28,6 @@ const ModelIcon: FC<ModelIconProps> = ({
if (provider?.icon_small) {
return (
<div className={`flex items-center justify-center ${isDeprecated ? 'opacity-50' : ''}`}>
<img
alt='model-icon'
@ -44,8 +43,8 @@ const ModelIcon: FC<ModelIconProps> = ({
'flex items-center justify-center rounded-md border-[0.5px] border-components-panel-border-subtle bg-background-default-subtle',
className,
)}>
<div className='flex w-5 h5 items-center justify-center opacity-35'>
<Group className='text-text-tertiary' />
<div className='flex w-5 h-5 items-center justify-center opacity-35'>
<Group className='text-text-tertiary w-3 h-3' />
</div>
</div>
)

@ -91,7 +91,7 @@ const PopupItem: FC<PopupItemProps> = ({
popupClassName='p-3 !w-[206px] bg-components-panel-bg-blur backdrop-blur-sm border-[0.5px] border-components-panel-border rounded-xl'
popupContent={
<div className='flex flex-col gap-1'>
<div className='flex flex-col gap-2'>
<div className='flex flex-col items-start gap-2'>
<ModelIcon
className={cn('shrink-0 w-5 h-5')}
provider={model}

@ -106,7 +106,7 @@ const Popup: FC<PopupProps> = ({
)
}
</div>
<div className='sticky bottom-0 px-4 py-2 flex items-center border-t border-divider-subtle cursor-pointer text-text-accent-light-mode-only' onClick={() => {
<div className='sticky bottom-0 px-4 py-2 flex items-center border-t border-divider-subtle cursor-pointer text-text-accent-light-mode-only bg-components-panel-bg rounded-b-lg' onClick={() => {
onHide()
setShowAccountSettingModal({ payload: 'provider' })
}}>

@ -139,7 +139,7 @@ const SystemModel: FC<SystemModelSelectorProps> = ({
{t('common.modelProvider.systemModelSettings')}
</Button>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-50'>
<PortalToFollowElemContent className='z-[60]'>
<div className='pt-4 w-[360px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-xl'>
<div className='px-6 py-1'>
<div className='flex items-center h-8 text-[13px] font-medium text-text-primary'>

@ -0,0 +1,43 @@
import { useUpdateModelProviders } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { useProviderContext } from '@/context/provider-context'
import { useInvalidateInstalledPluginList } from '@/service/use-plugins'
import { useInvalidateAllBuiltInTools, useInvalidateAllToolProviders } from '@/service/use-tools'
import { useInvalidateStrategyProviders } from '@/service/use-strategy'
import type { Plugin, PluginManifestInMarket } from '../../types'
import { PluginType } from '../../types'
const useRefreshPluginList = () => {
const invalidateInstalledPluginList = useInvalidateInstalledPluginList()
const updateModelProviders = useUpdateModelProviders()
const { refreshModelProviders } = useProviderContext()
const invalidateAllToolProviders = useInvalidateAllToolProviders()
const invalidateAllBuiltInTools = useInvalidateAllBuiltInTools()
const invalidateStrategyProviders = useInvalidateStrategyProviders()
return {
refreshPluginList: (manifest: PluginManifestInMarket | Plugin) => {
// installed list
invalidateInstalledPluginList()
// tool page, tool select
if (PluginType.tool.includes(manifest.category)) {
invalidateAllToolProviders()
invalidateAllBuiltInTools()
// TODO: update suggested tools. It's a function in hook useMarketplacePlugins,handleUpdatePlugins
}
// model select
if (PluginType.model.includes(manifest.category)) {
updateModelProviders()
refreshModelProviders()
}
// agent select
if (PluginType.agent.includes(manifest.category))
invalidateStrategyProviders()
},
}
}
export default useRefreshPluginList

@ -3,13 +3,11 @@
import React, { useCallback, useState } from 'react'
import Modal from '@/app/components/base/modal'
import type { Dependency, Plugin, PluginManifestInMarket } from '../../types'
import { InstallStep, PluginType } from '../../types'
import { InstallStep } from '../../types'
import Install from './steps/install'
import Installed from '../base/installed'
import { useTranslation } from 'react-i18next'
import { useUpdateModelProviders } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { useInvalidateInstalledPluginList } from '@/service/use-plugins'
import { useInvalidateAllToolProviders } from '@/service/use-tools'
import useRefreshPluginList from '../hooks/use-refresh-plugin-list'
import ReadyToInstallBundle from '../install-bundle/ready-to-install'
const i18nPrefix = 'plugin.installModal'
@ -35,9 +33,7 @@ const InstallFromMarketplace: React.FC<InstallFromMarketplaceProps> = ({
// readyToInstall -> check installed -> installed/failed
const [step, setStep] = useState<InstallStep>(InstallStep.readyToInstall)
const [errorMsg, setErrorMsg] = useState<string | null>(null)
const updateModelProviders = useUpdateModelProviders()
const invalidateAllToolProviders = useInvalidateAllToolProviders()
const invalidateInstalledPluginList = useInvalidateInstalledPluginList()
const { refreshPluginList } = useRefreshPluginList()
const getTitle = useCallback(() => {
if (isBundle && step === InstallStep.installed)
@ -51,12 +47,8 @@ const InstallFromMarketplace: React.FC<InstallFromMarketplaceProps> = ({
const handleInstalled = useCallback(() => {
setStep(InstallStep.installed)
invalidateInstalledPluginList()
if (PluginType.model.includes(manifest.category))
updateModelProviders()
if (PluginType.tool.includes(manifest.category))
invalidateAllToolProviders()
}, [invalidateAllToolProviders, invalidateInstalledPluginList, manifest, updateModelProviders])
refreshPluginList(manifest)
}, [manifest, refreshPluginList])
const handleFailed = useCallback((errorMsg?: string) => {
setStep(InstallStep.installFailed)

@ -8,7 +8,7 @@ import Button from '@/app/components/base/button'
import Drawer from '@/app/components/base/drawer'
import Form from '@/app/components/header/account-setting/model-provider-page/model-modal/Form'
import Toast from '@/app/components/base/toast'
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { useRenderI18nObject } from '@/hooks/use-i18n'
import cn from '@/utils/classnames'
type Props = {
@ -24,14 +24,14 @@ const EndpointModal: FC<Props> = ({
onCancel,
onSaved,
}) => {
const getValueFromI18nObject = useRenderI18nObject()
const { t } = useTranslation()
const language = useLanguage()
const [tempCredential, setTempCredential] = React.useState<any>(defaultValues)
const handleSave = () => {
for (const field of formSchemas) {
if (field.required && !tempCredential[field.name]) {
Toast.notify({ type: 'error', message: t('common.errorMsg.fieldRequired', { field: field.label[language] || field.label.en_US }) })
Toast.notify({ type: 'error', message: t('common.errorMsg.fieldRequired', { field: getValueFromI18nObject(field.label) }) })
return
}
}

@ -2,7 +2,6 @@
import type { FC } from 'react'
import React, { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import {
RiArrowLeftLine,
RiCloseLine,
@ -16,8 +15,7 @@ import type {
StrategyDetail,
} from '@/app/components/plugins/types'
import type { Locale } from '@/i18n'
import I18n from '@/context/i18n'
import { getLanguage } from '@/i18n/language'
import { useRenderI18nObject } from '@/hooks/use-i18n'
import cn from '@/utils/classnames'
type Props = {
@ -38,8 +36,7 @@ const StrategyDetail: FC<Props> = ({
detail,
onHide,
}) => {
const { locale } = useContext(I18n)
const language = getLanguage(locale)
const getValueFromI18nObject = useRenderI18nObject()
const { t } = useTranslation()
const outputSchema = useMemo(() => {
@ -98,10 +95,10 @@ const StrategyDetail: FC<Props> = ({
</div>
<div className='flex items-center gap-1'>
<Icon size='tiny' className='w-6 h-6' src={provider.icon} />
<div className=''>{provider.label[language]}</div>
<div className=''>{getValueFromI18nObject(provider.label)}</div>
</div>
<div className='mt-1 text-text-primary system-md-semibold'>{detail.identity.label[language]}</div>
<Description className='mt-3' text={detail.description[language]} descriptionLineRows={2}></Description>
<div className='mt-1 text-text-primary system-md-semibold'>{getValueFromI18nObject(detail.identity.label)}</div>
<Description className='mt-3' text={getValueFromI18nObject(detail.description)} descriptionLineRows={2}></Description>
</div>
{/* form */}
<div className='h-full'>
@ -113,7 +110,7 @@ const StrategyDetail: FC<Props> = ({
{detail.parameters.map((item: any, index) => (
<div key={index} className='py-1'>
<div className='flex items-center gap-2'>
<div className='text-text-secondary code-sm-semibold'>{item.label[language]}</div>
<div className='text-text-secondary code-sm-semibold'>{getValueFromI18nObject(item.label)}</div>
<div className='text-text-tertiary system-xs-regular'>
{getType(item.type)}
</div>
@ -123,7 +120,7 @@ const StrategyDetail: FC<Props> = ({
</div>
{item.human_description && (
<div className='mt-0.5 text-text-tertiary system-xs-regular'>
{item.human_description?.[language]}
{getValueFromI18nObject(item.human_description)}
</div>
)}
</div>

@ -1,13 +1,11 @@
'use client'
import React, { useState } from 'react'
import { useContext } from 'use-context-selector'
import StrategyDetailPanel from './strategy-detail'
import type {
StrategyDetail,
} from '@/app/components/plugins/types'
import type { Locale } from '@/i18n'
import I18n from '@/context/i18n'
import { getLanguage } from '@/i18n/language'
import { useRenderI18nObject } from '@/hooks/use-i18n'
import cn from '@/utils/classnames'
type Props = {
@ -26,8 +24,7 @@ const StrategyItem = ({
provider,
detail,
}: Props) => {
const { locale } = useContext(I18n)
const language = getLanguage(locale)
const getValueFromI18nObject = useRenderI18nObject()
const [showDetail, setShowDetail] = useState(false)
return (
@ -36,8 +33,8 @@ const StrategyItem = ({
className={cn('mb-2 px-4 py-3 bg-components-panel-item-bg rounded-xl border-[0.5px] border-components-panel-border-subtle shadow-xs cursor-pointer hover:bg-components-panel-on-panel-item-bg-hover')}
onClick={() => setShowDetail(true)}
>
<div className='pb-0.5 text-text-secondary system-md-semibold'>{detail.identity.label[language]}</div>
<div className='text-text-tertiary system-xs-regular line-clamp-2' title={detail.description[language]}>{detail.description[language]}</div>
<div className='pb-0.5 text-text-secondary system-md-semibold'>{getValueFromI18nObject(detail.identity.label)}</div>
<div className='text-text-tertiary system-xs-regular line-clamp-2' title={getValueFromI18nObject(detail.description)}>{getValueFromI18nObject(detail.description)}</div>
</div>
{showDetail && (
<StrategyDetailPanel

@ -12,7 +12,7 @@ import Toast from '@/app/components/base/toast'
import { fetchBuiltInToolCredential, fetchBuiltInToolCredentialSchema } from '@/service/tools'
import Loading from '@/app/components/base/loading'
import Form from '@/app/components/header/account-setting/model-provider-page/model-modal/Form'
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { useRenderI18nObject } from '@/hooks/use-i18n'
import cn from '@/utils/classnames'
type Props = {
@ -26,8 +26,8 @@ const ToolCredentialForm: FC<Props> = ({
onCancel,
onSaved,
}) => {
const getValueFromI18nObject = useRenderI18nObject()
const { t } = useTranslation()
const language = useLanguage()
const [credentialSchema, setCredentialSchema] = useState<any>(null)
const { name: collectionName } = collection
const [tempCredential, setTempCredential] = React.useState<any>({})
@ -45,7 +45,7 @@ const ToolCredentialForm: FC<Props> = ({
const handleSave = () => {
for (const field of credentialSchema) {
if (field.required && !tempCredential[field.name]) {
Toast.notify({ type: 'error', message: t('common.errorMsg.fieldRequired', { field: field.label[language] || field.label.en_US }) })
Toast.notify({ type: 'error', message: t('common.errorMsg.fieldRequired', { field: getValueFromI18nObject(field.label) }) })
return
}
}

@ -5,6 +5,8 @@ import {
RiDeleteBinLine,
RiEqualizer2Line,
RiErrorWarningFill,
RiInstallLine,
RiLoader2Line,
} from '@remixicon/react'
import { Group } from '@/app/components/base/icons/src/vender/other'
import AppIcon from '@/app/components/base/app-icon'
@ -13,7 +15,6 @@ import Button from '@/app/components/base/button'
import Indicator from '@/app/components/header/indicator'
import ActionButton from '@/app/components/base/action-button'
import Tooltip from '@/app/components/base/tooltip'
import { InstallPluginButton } from '@/app/components/workflow/nodes/_base/components/install-plugin-button'
import cn from '@/utils/classnames'
type Props = {
@ -115,10 +116,19 @@ const ToolItem = ({
</Button>
)}
{!isError && uninstalled && (
<InstallPluginButton size={'small'} loading={isInstalling} onClick={(e) => {
e.stopPropagation()
onInstall?.()
}} />
<Button
className={cn('flex items-center')}
size='small'
variant='secondary'
disabled={isInstalling}
onClick={(e) => {
e.stopPropagation()
onInstall?.()
}}
>
{!isInstalling ? t('workflow.nodes.agent.pluginInstaller.install') : t('workflow.nodes.agent.pluginInstaller.installing')}
{!isInstalling ? <RiInstallLine className='size-4 ml-1' /> : <RiLoader2Line className='size-4 ml-1 animate-spin' />}
</Button>
)}
{isError && (
<Tooltip

@ -20,11 +20,11 @@ import Title from '../card/base/title'
import Action from './action'
import cn from '@/utils/classnames'
import { API_PREFIX, MARKETPLACE_URL_PREFIX } from '@/config'
import { useLanguage } from '../../header/account-setting/model-provider-page/hooks'
import { useInvalidateInstalledPluginList } from '@/service/use-plugins'
import { useInvalidateAllToolProviders } from '@/service/use-tools'
import { useCategories } from '../hooks'
import { useProviderContext } from '@/context/provider-context'
import { useRenderI18nObject } from '@/hooks/use-i18n'
type Props = {
className?: string
@ -35,7 +35,6 @@ const PluginItem: FC<Props> = ({
className,
plugin,
}) => {
const locale = useLanguage()
const { t } = useTranslation()
const { categoriesMap } = useCategories()
const currentPluginID = usePluginPageContext(v => v.currentPluginID)
@ -66,6 +65,10 @@ const PluginItem: FC<Props> = ({
if (PluginType.tool.includes(category))
invalidateAllToolProviders()
}
const getValueFromI18nObject = useRenderI18nObject()
const title = getValueFromI18nObject(label)
const descriptionText = getValueFromI18nObject(description)
return (
<div
className={cn(
@ -92,12 +95,12 @@ const PluginItem: FC<Props> = ({
</div>
<div className="ml-3 w-0 grow">
<div className="flex items-center h-5">
<Title title={label[locale]} />
<Title title={title} />
{verified && <RiVerifiedBadgeLine className="shrink-0 ml-0.5 w-4 h-4 text-text-accent" />}
<Badge className='shrink-0 ml-1' text={source === PluginSource.github ? plugin.meta!.version : plugin.version} />
</div>
<div className='flex items-center justify-between'>
<Description text={description[locale]} descriptionLineRows={1}></Description>
<Description text={descriptionText} descriptionLineRows={1}></Description>
<div onClick={e => e.stopPropagation()}>
<Action
pluginUniqueIdentifier={plugin_unique_identifier}

@ -10,12 +10,12 @@ import Icon from './card/base/card-icon'
import Title from './card/base/title'
import DownloadCount from './card/base/download-count'
import Button from '@/app/components/base/button'
import { useGetLanguage } from '@/context/i18n'
import InstallFromMarketplace from '@/app/components/plugins/install-plugin/install-from-marketplace'
import cn from '@/utils/classnames'
import { useBoolean } from 'ahooks'
import { getPluginLinkInMarketplace } from '@/app/components/plugins/marketplace/utils'
import { useI18N } from '@/context/i18n'
import { useRenderI18nObject } from '@/hooks/use-i18n'
type Props = {
className?: string
@ -26,12 +26,12 @@ const ProviderCard: FC<Props> = ({
className,
payload,
}) => {
const getValueFromI18nObject = useRenderI18nObject()
const { t } = useTranslation()
const [isShowInstallFromMarketplace, {
setTrue: showInstallFromMarketplace,
setFalse: hideInstallFromMarketplace,
}] = useBoolean(false)
const language = useGetLanguage()
const { org, label } = payload
const { locale } = useI18N()
@ -42,7 +42,7 @@ const ProviderCard: FC<Props> = ({
<Icon src={payload.icon} />
<div className="ml-3 w-0 grow">
<div className="flex items-center h-5">
<Title title={label[language] || label.en_US} />
<Title title={getValueFromI18nObject(label)} />
{/* <RiVerifiedBadgeLine className="shrink-0 ml-0.5 w-4 h-4 text-text-accent" /> */}
</div>
<div className='mb-1 flex justify-between items-center h-4'>
@ -54,7 +54,7 @@ const ProviderCard: FC<Props> = ({
</div>
</div>
</div>
<Description className='mt-3' text={payload.brief[language] || payload.brief.en_US} descriptionLineRows={2}></Description>
<Description className='mt-3' text={getValueFromI18nObject(payload.brief)} descriptionLineRows={2}></Description>
<div className='mt-3 flex space-x-0.5'>
{payload.tags.map(tag => (
<Badge key={tag.name} text={tag.name} />

@ -25,6 +25,7 @@ import { useToastContext } from '@/app/components/base/toast'
import { CollectionType } from '@/app/components/tools/types'
import { useGetLanguage } from '@/context/i18n'
import type { AgentNodeType } from '../nodes/agent/types'
import { useStrategyProviders } from '@/service/use-strategy'
export const useChecklist = (nodes: Node[], edges: Edge[]) => {
const { t } = useTranslation()
@ -34,7 +35,7 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => {
const buildInTools = useStore(s => s.buildInTools)
const customTools = useStore(s => s.customTools)
const workflowTools = useStore(s => s.workflowTools)
const agentStrategies = useStore(s => s.agentStrategies)
const { data: agentStrategies } = useStrategyProviders()
const needWarningNodes = useMemo(() => {
const list = []
@ -61,9 +62,8 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => {
if (node.data.type === BlockEnum.Agent) {
const data = node.data as AgentNodeType
const provider = agentStrategies.find(s => s.plugin_unique_identifier === data.plugin_unique_identifier)
const strategy = provider?.declaration.strategies.find(s => s.identity.name === data.agent_strategy_name)
// debugger
const provider = agentStrategies?.find(s => s.plugin_unique_identifier === data.plugin_unique_identifier)
const strategy = provider?.declaration.strategies?.find(s => s.identity.name === data.agent_strategy_name)
moreDataForCheckValid = {
provider,
strategy,

@ -58,7 +58,6 @@ import I18n from '@/context/i18n'
import { CollectionType } from '@/app/components/tools/types'
import { CUSTOM_ITERATION_START_NODE } from '@/app/components/workflow/nodes/iteration-start/constants'
import { useWorkflowConfig } from '@/service/use-workflow'
import { fetchStrategyList } from '@/service/strategy'
export const useIsChatMode = () => {
const appDetail = useAppStore(s => s.appDetail)
@ -460,21 +459,6 @@ export const useFetchToolsData = () => {
}
}
export const useFetchAgentStrategy = () => {
const workflowStore = useWorkflowStore()
const handleFetchAllAgentStrategies = useCallback(async () => {
const agentStrategies = await fetchStrategyList()
workflowStore.setState({
agentStrategies: agentStrategies || [],
})
}, [workflowStore])
return {
handleFetchAllAgentStrategies,
}
}
export const useWorkflowInit = () => {
const workflowStore = useWorkflowStore()
const {
@ -482,7 +466,6 @@ export const useWorkflowInit = () => {
edges: edgesTemplate,
} = useWorkflowTemplate()
const { handleFetchAllTools } = useFetchToolsData()
const { handleFetchAllAgentStrategies } = useFetchAgentStrategy()
const appDetail = useAppStore(state => state.appDetail)!
const setSyncWorkflowDraftHash = useStore(s => s.setSyncWorkflowDraftHash)
const [data, setData] = useState<FetchWorkflowDraftResponse>()
@ -562,8 +545,7 @@ export const useWorkflowInit = () => {
handleFetchAllTools('builtin')
handleFetchAllTools('custom')
handleFetchAllTools('workflow')
handleFetchAllAgentStrategies()
}, [handleFetchPreloadData, handleFetchAllTools, handleFetchAllAgentStrategies])
}, [handleFetchPreloadData, handleFetchAllTools])
useEffect(() => {
if (data) {

@ -1,4 +1,5 @@
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem'
import type { ReactNode } from 'react'
import { memo, useMemo, useState } from 'react'
import type { Strategy } from './agent-strategy'
import classNames from '@/utils/classnames'
@ -16,17 +17,32 @@ import type { StrategyPluginDetail } from '@/app/components/plugins/types'
import type { ToolWithProvider } from '../../../types'
import { CollectionType } from '@/app/components/tools/types'
import useGetIcon from '@/app/components/plugins/install-plugin/base/use-get-icon'
import { useStrategyInfo } from '../../agent/use-config'
import { SwitchPluginVersion } from './switch-plugin-version'
const NotFoundWarn = (props: {
title: ReactNode,
description: ReactNode
}) => {
const { title, description } = props
const ExternalNotInstallWarn = () => {
const { t } = useTranslation()
return <Tooltip
popupContent={<div className='space-y-1 text-xs'>
<h3 className='text-text-primary font-semibold'>{t('workflow.nodes.agent.pluginNotInstalled')}</h3>
<p className='text-text-secondary tracking-tight'>{t('workflow.nodes.agent.pluginNotInstalledDesc')}</p>
<p>
<Link href={'/plugins'} className='text-text-accent tracking-tight'>{t('workflow.nodes.agent.linkToPlugin')}</Link>
</p>
</div>}
popupContent={
<div className='space-y-1 text-xs'>
<h3 className='text-text-primary font-semibold'>
{title}
</h3>
<p className='text-text-secondary tracking-tight'>
{description}
</p>
<p>
<Link href={'/plugins'} className='text-text-accent tracking-tight'>
{t('workflow.nodes.agent.linkToPlugin')}
</Link>
</p>
</div>
}
needsDelay
>
<div>
@ -81,15 +97,34 @@ export const AgentStrategySelector = memo((props: AgentStrategySelectorProps) =>
if (!list) return []
return list.filter(tool => tool.name.toLowerCase().includes(query.toLowerCase()))
}, [query, list])
// TODO: should be replaced by real data
const isExternalInstalled = true
const { strategyStatus } = useStrategyInfo(
value?.agent_strategy_provider_name,
value?.agent_strategy_name,
)
const showPluginNotInstalledWarn = strategyStatus?.plugin?.source === 'external'
&& !strategyStatus.plugin.installed
const showUnsupportedStrategy = strategyStatus?.plugin.source === 'external'
&& !strategyStatus?.isExistInPlugin
const showSwitchVersion = !strategyStatus?.isExistInPlugin
&& strategyStatus?.plugin.source === 'marketplace' && strategyStatus.plugin.installed
const showInstallButton = !strategyStatus?.isExistInPlugin
&& strategyStatus?.plugin.source === 'marketplace' && !strategyStatus.plugin.installed
const icon = list?.find(
coll => coll.tools?.find(tool => tool.name === value?.agent_strategy_name),
)?.icon as string | undefined
const { t } = useTranslation()
return <PortalToFollowElem open={open} onOpenChange={setOpen} placement='bottom'>
<PortalToFollowElemTrigger className='w-full'>
<div className='h-8 p-1 gap-0.5 flex items-center rounded-lg bg-components-input-bg-normal w-full hover:bg-state-base-hover-alt select-none' onClick={() => setOpen(o => !o)}>
<div
className='h-8 p-1 gap-0.5 flex items-center rounded-lg bg-components-input-bg-normal w-full hover:bg-state-base-hover-alt select-none'
onClick={() => setOpen(o => !o)}
>
{/* eslint-disable-next-line @next/next/no-img-element */}
{icon && <div className='flex items-center justify-center w-6 h-6'><img
src={icon}
@ -104,8 +139,30 @@ export const AgentStrategySelector = memo((props: AgentStrategySelectorProps) =>
{value?.agent_strategy_label || t('workflow.nodes.agent.strategy.selectTip')}
</p>
{value && <div className='ml-auto flex items-center gap-1'>
<InstallPluginButton onClick={e => e.stopPropagation()} size={'small'} />
{isExternalInstalled ? <ExternalNotInstallWarn /> : <RiArrowDownSLine className='size-4 text-text-tertiary' />}
{showInstallButton && <InstallPluginButton
onClick={e => e.stopPropagation()}
size={'small'}
uniqueIdentifier={value.plugin_unique_identifier}
/>}
{showPluginNotInstalledWarn
? <NotFoundWarn
title={t('workflow.nodes.agent.pluginNotInstalled')}
description={t('workflow.nodes.agent.pluginNotInstalledDesc')}
/>
: showUnsupportedStrategy
? <NotFoundWarn
title={t('workflow.nodes.agent.unsupportedStrategy')}
description={t('workflow.nodes.agent.strategyNotFoundDesc')}
/>
: <RiArrowDownSLine className='size-4 text-text-tertiary' />
}
{showSwitchVersion && <SwitchPluginVersion
uniqueIdentifier={'langgenius/openai:12'}
tooltip={t('workflow.nodes.agent.switchToNewVersion')}
onChange={() => {
// TODO: refresh all strategies
}}
/>}
</div>}
</div>
</PortalToFollowElemTrigger>
@ -143,9 +200,6 @@ export const AgentStrategySelector = memo((props: AgentStrategySelectorProps) =>
</div>
</main>
</div>
{/* <div>
aaa
</div> */}
</PortalToFollowElemContent>
</PortalToFollowElem>
})

@ -1,16 +1,43 @@
import Button from '@/app/components/base/button'
import { RiInstallLine, RiLoader2Line } from '@remixicon/react'
import type { ComponentProps } from 'react'
import type { ComponentProps, MouseEventHandler } from 'react'
import classNames from '@/utils/classnames'
import { useTranslation } from 'react-i18next'
import { useCheckInstalled, useInstallPackageFromMarketPlace } from '@/service/use-plugins'
type InstallPluginButtonProps = Omit<ComponentProps<typeof Button>, 'children'>
type InstallPluginButtonProps = Omit<ComponentProps<typeof Button>, 'children' | 'loading'> & {
uniqueIdentifier: string
onSuccess?: () => void
}
export const InstallPluginButton = (props: InstallPluginButtonProps) => {
const { loading, className, ...rest } = props
const { className, uniqueIdentifier, onSuccess, ...rest } = props
const { t } = useTranslation()
return <Button variant={'secondary'} disabled={loading} className={classNames('flex items-center', className)} {...rest}>
{loading ? t('workflow.nodes.agent.pluginInstaller.install') : t('workflow.nodes.agent.pluginInstaller.installing')}
{!loading ? <RiInstallLine className='size-4 ml-1' /> : <RiLoader2Line className='size-4 ml-1 animate-spin' />}
const manifest = useCheckInstalled({
pluginIds: [uniqueIdentifier],
enabled: !!uniqueIdentifier,
})
const install = useInstallPackageFromMarketPlace({
onSuccess() {
manifest.refetch()
onSuccess?.()
},
})
const handleInstall: MouseEventHandler = (e) => {
e.stopPropagation()
install.mutate(uniqueIdentifier)
}
const isLoading = manifest.isLoading || install.isPending
if (!manifest.data) return null
if (manifest.data.plugins.some(plugin => plugin.id === uniqueIdentifier)) return null
return <Button
variant={'secondary'}
disabled={isLoading}
{...rest}
onClick={handleInstall}
className={classNames('flex items-center', className)}
>
{!isLoading ? t('workflow.nodes.agent.pluginInstaller.install') : t('workflow.nodes.agent.pluginInstaller.installing')}
{!isLoading ? <RiInstallLine className='size-4 ml-1' /> : <RiLoader2Line className='size-4 ml-1 animate-spin' />}
</Button>
}

@ -0,0 +1,80 @@
'use client'
import Badge from '@/app/components/base/badge'
import Tooltip from '@/app/components/base/tooltip'
import PluginVersionPicker from '@/app/components/plugins/update-plugin/plugin-version-picker'
import { RiArrowLeftRightLine } from '@remixicon/react'
import { type FC, useCallback, useState } from 'react'
import cn from '@/utils/classnames'
import UpdateFromMarketplace from '@/app/components/plugins/update-plugin/from-market-place'
import { useBoolean } from 'ahooks'
import { useCheckInstalled } from '@/service/use-plugins'
export type SwitchPluginVersionProps = {
uniqueIdentifier: string
tooltip?: string
onChange?: (version: string) => void
}
export const SwitchPluginVersion: FC<SwitchPluginVersionProps> = (props) => {
const { uniqueIdentifier, tooltip, onChange } = props
const [pluginId] = uniqueIdentifier.split(':')
const [isShow, setIsShow] = useState(false)
const [isShowUpdateModal, { setTrue: showUpdateModal, setFalse: hideUpdateModal }] = useBoolean(false)
const [targetVersion, setTargetVersion] = useState<string>()
const pluginDetails = useCheckInstalled({
pluginIds: [pluginId],
enabled: true,
})
const pluginDetail = pluginDetails.data?.plugins.at(0)
const handleUpdatedFromMarketplace = useCallback(() => {
hideUpdateModal()
pluginDetails.refetch()
onChange?.(targetVersion!)
}, [hideUpdateModal, onChange, pluginDetails, targetVersion])
return <Tooltip popupContent={!isShow && !isShowUpdateModal && tooltip} triggerMethod='hover'>
<div className='w-fit'>
{isShowUpdateModal && pluginDetail && <UpdateFromMarketplace
payload={{
originalPackageInfo: {
id: uniqueIdentifier,
payload: pluginDetail.declaration,
},
targetPackageInfo: {
id: uniqueIdentifier,
version: targetVersion!,
},
}}
onCancel={hideUpdateModal}
onSave={handleUpdatedFromMarketplace}
/>}
{pluginDetail && <PluginVersionPicker
isShow={isShow}
onShowChange={setIsShow}
pluginID={pluginId}
currentVersion={pluginDetail.version}
onSelect={(state) => {
setTargetVersion(state.version)
showUpdateModal()
}}
trigger={
<Badge
className={cn(
'mx-1 hover:bg-state-base-hover flex',
isShow && 'bg-state-base-hover',
)}
uppercase={true}
text={
<>
<div>{pluginDetail.version}</div>
<RiArrowLeftRightLine className='ml-1 w-3 h-3 text-text-tertiary' />
</>
}
hasRedCornerMark={true}
/>
}
/>}
</div>
</Tooltip>
}

@ -18,8 +18,8 @@ const nodeDefault: NodeDefault<AgentNodeType> = {
: ALL_COMPLETION_AVAILABLE_BLOCKS
},
checkValid(payload, t, moreDataForCheckValid: {
strategyProvider: StrategyPluginDetail | undefined,
strategy: StrategyDetail | undefined
strategyProvider?: StrategyPluginDetail,
strategy?: StrategyDetail
language: string
}) {
const { strategy, language } = moreDataForCheckValid

@ -89,17 +89,15 @@ const AgentNode: FC<NodeProps<AgentNodeType>> = (props) => {
{inputs.agent_strategy_name
? <SettingItem
label={t('workflow.nodes.agent.strategy.shortLabel')}
status={
['plugin-not-found', 'strategy-not-found'].includes(currentStrategyStatus)
? 'error'
: undefined
status={!currentStrategyStatus?.isExistInPlugin ? 'error' : undefined}
tooltip={
!currentStrategyStatus?.isExistInPlugin ? t('workflow.nodes.agent.strategyNotInstallTooltip', {
plugin: pluginDetail?.declaration.label
? renderI18nObject(pluginDetail?.declaration.label)
: undefined,
strategy: inputs.agent_strategy_label,
}) : undefined
}
tooltip={t(`workflow.nodes.agent.${currentStrategyStatus === 'plugin-not-found' ? 'strategyNotInstallTooltip' : 'strategyNotFoundInPlugin'}`, {
strategy: inputs.agent_strategy_label,
plugin: pluginDetail?.declaration.label
? renderI18nObject(pluginDetail?.declaration.label)
: undefined,
})}
>
{inputs.agent_strategy_label}
</SettingItem>

@ -8,11 +8,54 @@ import {
} from '@/app/components/workflow/hooks'
import { useCallback, useMemo } from 'react'
import { type ToolVarInputs, VarType } from '../tool/types'
import { useCheckInstalled } from '@/service/use-plugins'
import { useCheckInstalled, useFetchPluginsInMarketPlaceByIds } from '@/service/use-plugins'
import type { Var } from '../../types'
import { VarType as VarKindType } from '../../types'
import useAvailableVarList from '../_base/hooks/use-available-var-list'
export type StrategyStatus = {
plugin: {
source: 'external' | 'marketplace'
installed: boolean
}
isExistInPlugin: boolean
}
export const useStrategyInfo = (
strategyProviderName?: string,
strategyName?: string,
) => {
const strategyProvider = useStrategyProviderDetail(
strategyProviderName || '',
{ retry: false },
)
const strategy = strategyProvider.data?.declaration.strategies.find(
str => str.identity.name === strategyName,
)
const marketplace = useFetchPluginsInMarketPlaceByIds([strategyProviderName!], {
retry: false,
})
const strategyStatus: StrategyStatus | undefined = useMemo(() => {
if (strategyProvider.isLoading || marketplace.isLoading)
return undefined
const strategyExist = !!strategy
const isPluginInstalled = !strategyProvider.isError
const isInMarketplace = !!marketplace.data?.data.plugins.at(0)
return {
plugin: {
source: isInMarketplace ? 'marketplace' : 'external',
installed: isPluginInstalled,
},
isExistInPlugin: strategyExist,
}
}, [strategy, marketplace, strategyProvider.isError, strategyProvider.isLoading])
return {
strategyProvider,
strategy,
strategyStatus,
}
}
const useConfig = (id: string, payload: AgentNodeType) => {
const { nodesReadOnly: readOnly } = useNodesReadOnly()
const { inputs, setInputs } = useNodeCrud<AgentNodeType>(id, payload)
@ -21,21 +64,17 @@ const useConfig = (id: string, payload: AgentNodeType) => {
inputs,
setInputs,
})
const strategyProvider = useStrategyProviderDetail(
inputs.agent_strategy_provider_name || '',
)
const currentStrategy = strategyProvider.data?.declaration.strategies.find(
str => str.identity.name === inputs.agent_strategy_name,
const {
strategyStatus: currentStrategyStatus,
strategy: currentStrategy,
strategyProvider,
} = useStrategyInfo(
inputs.agent_strategy_provider_name,
inputs.agent_strategy_name,
)
const currentStrategyStatus: 'loading' | 'plugin-not-found' | 'strategy-not-found' | 'success' = useMemo(() => {
if (strategyProvider.isLoading) return 'loading'
if (strategyProvider.isError) return 'plugin-not-found'
if (!currentStrategy) return 'strategy-not-found'
return 'success'
}, [currentStrategy, strategyProvider])
const pluginId = inputs.agent_strategy_provider_name?.split('/').splice(0, 2).join('/')
const pluginDetail = useCheckInstalled({
pluginIds: [pluginId || ''],
pluginIds: [pluginId!],
enabled: Boolean(pluginId),
})
const formData = useMemo(() => {

@ -78,10 +78,10 @@ const NodePanel: FC<Props> = ({
setCollapseState(!nodeInfo.expand)
}, [nodeInfo.expand, setCollapseState])
const isIterationNode = nodeInfo.node_type === BlockEnum.Iteration && nodeInfo.details?.length
const isRetryNode = hasRetryNode(nodeInfo.node_type) && nodeInfo.retryDetail?.length
const isAgentNode = nodeInfo.node_type === BlockEnum.Agent && nodeInfo.agentLog?.length
const isToolNode = nodeInfo.node_type === BlockEnum.Tool && nodeInfo.agentLog?.length
const isIterationNode = nodeInfo.node_type === BlockEnum.Iteration && !!nodeInfo.details?.length
const isRetryNode = hasRetryNode(nodeInfo.node_type) && !!nodeInfo.retryDetail?.length
const isAgentNode = nodeInfo.node_type === BlockEnum.Agent && !!nodeInfo.agentLog?.length
const isToolNode = nodeInfo.node_type === BlockEnum.Tool && !!nodeInfo.agentLog?.length
return (
<div className={cn('px-2 py-1', className)}>

@ -57,10 +57,10 @@ const ResultPanel: FC<ResultPanelProps> = ({
handleShowAgentOrToolLog,
}) => {
const { t } = useTranslation()
const isIterationNode = nodeInfo?.node_type === BlockEnum.Iteration && nodeInfo?.details?.length
const isRetryNode = hasRetryNode(nodeInfo?.node_type) && nodeInfo?.retryDetail?.length
const isAgentNode = nodeInfo?.node_type === BlockEnum.Agent && nodeInfo?.agentLog?.length
const isToolNode = nodeInfo?.node_type === BlockEnum.Tool && nodeInfo?.agentLog?.length
const isIterationNode = nodeInfo?.node_type === BlockEnum.Iteration && !!nodeInfo?.details?.length
const isRetryNode = hasRetryNode(nodeInfo?.node_type) && !!nodeInfo?.retryDetail?.length
const isAgentNode = nodeInfo?.node_type === BlockEnum.Agent && !!nodeInfo?.agentLog?.length
const isToolNode = nodeInfo?.node_type === BlockEnum.Tool && !!nodeInfo?.agentLog?.length
return (
<div className='bg-components-panel-bg py-2'>

@ -0,0 +1,128 @@
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')
})
})

@ -0,0 +1,304 @@
type IterationInfo = { iterationId: string; iterationIndex: number }
type NodePlain = { nodeType: 'plain'; nodeId: string; } & Partial<IterationInfo>
type NodeComplex = { nodeType: string; nodeId: string; params: (NodePlain | (NodeComplex & Partial<IterationInfo>) | Node[] | number)[] } & Partial<IterationInfo>
type Node = NodePlain | NodeComplex
/**
* Parses a DSL string into an array of node objects.
* @param dsl - The input DSL string.
* @returns An array of parsed nodes.
*/
function parseDSL(dsl: string): NodeData[] {
return convertToNodeData(parseTopLevelFlow(dsl).map(nodeStr => parseNode(nodeStr)))
}
/**
* Splits a top-level flow string by "->", respecting nested structures.
* @param dsl - The DSL string to split.
* @returns An array of top-level segments.
*/
function parseTopLevelFlow(dsl: string): string[] {
const segments: string[] = []
let buffer = ''
let nested = 0
for (let i = 0; i < dsl.length; i++) {
const char = dsl[i]
if (char === '(') nested++
if (char === ')') nested--
if (char === '-' && dsl[i + 1] === '>' && nested === 0) {
segments.push(buffer.trim())
buffer = ''
i++ // Skip the ">" character
}
else {
buffer += char
}
}
if (buffer.trim())
segments.push(buffer.trim())
return segments
}
/**
* Parses a single node string.
* If the node is complex (e.g., has parentheses), it extracts the node type, node ID, and parameters.
* @param nodeStr - The node string to parse.
* @param parentIterationId - The ID of the parent iteration node (if applicable).
* @returns A parsed node object.
*/
function parseNode(nodeStr: string, parentIterationId?: string): Node {
// Check if the node is a complex node
if (nodeStr.startsWith('(') && nodeStr.endsWith(')')) {
const innerContent = nodeStr.slice(1, -1).trim() // Remove outer parentheses
let nested = 0
let buffer = ''
const parts: string[] = []
// Split the inner content by commas, respecting nested parentheses
for (let i = 0; i < innerContent.length; i++) {
const char = innerContent[i]
if (char === '(') nested++
if (char === ')') nested--
if (char === ',' && nested === 0) {
parts.push(buffer.trim())
buffer = ''
}
else {
buffer += char
}
}
parts.push(buffer.trim())
// Extract nodeType, nodeId, and params
const [nodeType, nodeId, ...paramsRaw] = parts
const params = parseParams(paramsRaw, nodeType === 'iteration' ? nodeId.trim() : parentIterationId)
const complexNode = {
nodeType: nodeType.trim(),
nodeId: nodeId.trim(),
params,
}
if (parentIterationId) {
(complexNode as any).iterationId = parentIterationId;
(complexNode as any).iterationIndex = 0 // Fixed as 0
}
return complexNode
}
// If it's not a complex node, treat it as a plain node
const plainNode: NodePlain = { nodeType: 'plain', nodeId: nodeStr.trim() }
if (parentIterationId) {
plainNode.iterationId = parentIterationId
plainNode.iterationIndex = 0 // Fixed as 0
}
return plainNode
}
/**
* Parses parameters of a complex node.
* Supports nested flows and complex sub-nodes.
* Adds iteration-specific metadata recursively.
* @param paramParts - The parameters string split by commas.
* @param iterationId - The ID of the iteration node, if applicable.
* @returns An array of parsed parameters (plain nodes, nested nodes, or flows).
*/
function parseParams(paramParts: string[], iterationId?: string): (Node | Node[] | number)[] {
return paramParts.map((part) => {
if (part.includes('->')) {
// Parse as a flow and return an array of nodes
return parseTopLevelFlow(part).map(node => parseNode(node, iterationId))
}
else if (part.startsWith('(')) {
// Parse as a nested complex node
return parseNode(part, iterationId)
}
else if (!Number.isNaN(Number(part.trim()))) {
// Parse as a numeric parameter
return Number(part.trim())
}
else {
// Parse as a plain node
return parseNode(part, iterationId)
}
})
}
type NodeData = {
id: string;
node_id: string;
title: string;
node_type?: string;
execution_metadata: Record<string, any>;
status: string;
}
/**
* Converts a plain node to node data.
*/
function convertPlainNode(node: Node): NodeData[] {
return [
{
id: node.nodeId,
node_id: node.nodeId,
title: node.nodeId,
execution_metadata: {},
status: 'succeeded',
},
]
}
/**
* Converts a retry node to node data.
*/
function convertRetryNode(node: Node): NodeData[] {
const { nodeId, iterationId, iterationIndex, params } = node as NodeComplex
const retryCount = params ? Number.parseInt(params[0] as unknown as string, 10) : 0
const result: NodeData[] = [
{
id: nodeId,
node_id: nodeId,
title: nodeId,
execution_metadata: {},
status: 'succeeded',
},
]
for (let i = 0; i < retryCount; i++) {
result.push({
id: nodeId,
node_id: nodeId,
title: nodeId,
execution_metadata: iterationId ? {
iteration_id: iterationId,
iteration_index: iterationIndex || 0,
} : {},
status: 'retry',
})
}
return result
}
/**
* Converts an iteration node to node data.
*/
function convertIterationNode(node: Node): NodeData[] {
const { nodeId, params } = node as NodeComplex
const result: NodeData[] = [
{
id: nodeId,
node_id: nodeId,
title: nodeId,
node_type: 'iteration',
status: 'succeeded',
execution_metadata: {},
},
]
params?.forEach((param: any) => {
if (Array.isArray(param)) {
param.forEach((childNode: Node) => {
const childData = convertToNodeData([childNode])
childData.forEach((data) => {
data.execution_metadata = {
...data.execution_metadata,
iteration_id: nodeId,
iteration_index: 0,
}
})
result.push(...childData)
})
}
})
return result
}
/**
* Converts a parallel node to node data.
*/
function convertParallelNode(node: Node, parentParallelId?: string, parentStartNodeId?: string): NodeData[] {
const { nodeId, params } = node as NodeComplex
const result: NodeData[] = [
{
id: nodeId,
node_id: nodeId,
title: nodeId,
execution_metadata: {
parallel_id: nodeId,
},
status: 'succeeded',
},
]
params?.forEach((param) => {
if (Array.isArray(param)) {
const startNodeId = param[0]?.nodeId
param.forEach((childNode: Node) => {
const childData = convertToNodeData([childNode])
childData.forEach((data) => {
data.execution_metadata = {
...data.execution_metadata,
parallel_id: nodeId,
parallel_start_node_id: startNodeId,
...(parentParallelId && {
parent_parallel_id: parentParallelId,
parent_parallel_start_node_id: parentStartNodeId,
}),
}
})
result.push(...childData)
})
}
else if (param && typeof param === 'object') {
const startNodeId = param.nodeId
const childData = convertToNodeData([param])
childData.forEach((data) => {
data.execution_metadata = {
...data.execution_metadata,
parallel_id: nodeId,
parallel_start_node_id: startNodeId,
...(parentParallelId && {
parent_parallel_id: parentParallelId,
parent_parallel_start_node_id: parentStartNodeId,
}),
}
})
result.push(...childData)
}
})
return result
}
/**
* Main function to convert nodes to node data.
*/
function convertToNodeData(nodes: Node[], parentParallelId?: string, parentStartNodeId?: string): NodeData[] {
const result: NodeData[] = []
nodes.forEach((node) => {
switch (node.nodeType) {
case 'plain':
result.push(...convertPlainNode(node))
break
case 'retry':
result.push(...convertRetryNode(node))
break
case 'iteration':
result.push(...convertIterationNode(node))
break
case 'parallel':
result.push(...convertParallelNode(node, parentParallelId, parentStartNodeId))
break
default:
throw new Error(`Unknown nodeType: ${node.nodeType}`)
}
})
return result
}
export { parseDSL }

@ -0,0 +1,97 @@
import graphToLogStruct, { parseNodeString } 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',
],
},
],
},
'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 },
status: 'succeeded',
},
{
id: '3',
node_id: '3',
title: '3',
execution_metadata: { iteration_id: '1', iteration_index: 1 },
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',
},
])
})
})

@ -0,0 +1,174 @@
const STEP_SPLIT = '->'
const toNodeData = (step: string, info: Record<string, any> = {}): any => {
const [nodeId, title] = step.split('@')
const data: Record<string, any> = {
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
}
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
}
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<string | NodeStructure>;
}
export function parseNodeString(input: string): NodeStructure {
input = input.trim()
if (input.startsWith('(') && input.endsWith(')'))
input = input.slice(1, -1)
const parts: Array<string | NodeStructure> = []
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 = ''
}
else {
current += char
}
}
if (current)
parts.push(current.trim())
const result: NodeStructure = {
node: '',
params: [],
}
for (let i = 0; i < parts.length; i++) {
const part = parts[i]
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))
}
}
else if (i === 0) {
result.node = part as unknown as string
}
else {
result.params.push(part as unknown as string)
}
}
return result
}
const toNodes = (input: string): any[] => {
const list = input.split(STEP_SPLIT)
.map(step => step.trim())
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
}
})
return res
}
/*
* : 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
}
export default graphToLogStruct

@ -1,105 +0,0 @@
import type { NodeTracing } from '@/types/workflow'
import { BlockEnum } from '../../../types'
type IterationNodeId = string
type RunIndex = string
type IterationGroupMap = Map<IterationNodeId, Map<RunIndex, NodeTracing[]>>
const processIterationNode = (item: NodeTracing) => {
return {
...item,
details: [], // to add the sub nodes in the iteration
}
}
const updateParallelModeGroup = (nodeGroupMap: IterationGroupMap, runIndex: string, item: NodeTracing, iterationNode: NodeTracing) => {
if (!nodeGroupMap.has(iterationNode.node_id))
nodeGroupMap.set(iterationNode.node_id, new Map())
const groupMap = nodeGroupMap.get(iterationNode.node_id)!
if (!groupMap.has(runIndex))
groupMap.set(runIndex, [item])
else
groupMap.get(runIndex)!.push(item)
if (item.status === 'failed') {
iterationNode.status = 'failed'
iterationNode.error = item.error
}
iterationNode.details = Array.from(groupMap.values())
}
const updateSequentialModeGroup = (runIndex: number, item: NodeTracing, iterationNode: NodeTracing) => {
const { details } = iterationNode
if (details) {
if (!details[runIndex])
details[runIndex] = [item]
else
details[runIndex].push(item)
}
if (item.status === 'failed') {
iterationNode.status = 'failed'
iterationNode.error = item.error
}
}
const addRetryDetail = (result: NodeTracing[], item: NodeTracing) => {
const retryNode = result.find(node => node.node_id === item.node_id)
if (retryNode) {
if (retryNode?.retryDetail)
retryNode.retryDetail.push(item)
else
retryNode.retryDetail = [item]
}
}
const processNonIterationNode = (result: NodeTracing[], nodeGroupMap: IterationGroupMap, item: NodeTracing) => {
const { execution_metadata } = item
if (!execution_metadata?.iteration_id) {
if (item.status === 'retry') {
addRetryDetail(result, item)
return
}
result.push(item)
return
}
const parentIterationNode = result.find(node => node.node_id === execution_metadata.iteration_id)
const isInIteration = !!parentIterationNode && Array.isArray(parentIterationNode.details)
if (!isInIteration)
return
// the parallel in the iteration in mode.
const { parallel_mode_run_id, iteration_index = 0 } = execution_metadata
const isInParallel = !!parallel_mode_run_id
if (isInParallel)
updateParallelModeGroup(nodeGroupMap, parallel_mode_run_id, item, parentIterationNode)
else
updateSequentialModeGroup(iteration_index, item, parentIterationNode)
}
// list => tree. Put the iteration node's children into the details field.
const formatToTracingNodeList = (list: NodeTracing[]) => {
const allItems = [...list].reverse()
const result: NodeTracing[] = []
const iterationGroupMap = new Map<string, Map<string, NodeTracing[]>>()
allItems.forEach((item) => {
item.node_type === BlockEnum.Iteration
? result.push(processIterationNode(item))
: processNonIterationNode(result, iterationGroupMap, item)
})
// console.log(allItems)
// console.log(result)
return result
}
export default formatToTracingNodeList

@ -1,190 +0,0 @@
export const simpleIterationData = (() => {
// start -> code(output: [1, 2, 3]) -> iteration(output: ['aaa', 'aaa', 'aaa']) -> end(output: ['aaa', 'aaa', 'aaa'])
const startNode = {
id: '36c9860a-39e6-4107-b750-655b07895f47',
index: 1,
predecessor_node_id: null,
node_id: '1735023354069',
node_type: 'start',
title: 'Start',
inputs: {
'sys.files': [],
'sys.user_id': '5ee03762-1d1a-46e8-ba0b-5f419a77da96',
'sys.app_id': '8a5e87f8-6433-40f4-a67a-4be78a558dc7',
'sys.workflow_id': 'bb5e2b89-40ac-45c9-9ccb-4f2cd926e080',
'sys.workflow_run_id': '76adf675-a7d3-4cc1-9282-ed7ecfe4f65d',
},
process_data: null,
outputs: {
'sys.files': [],
'sys.user_id': '5ee03762-1d1a-46e8-ba0b-5f419a77da96',
'sys.app_id': '8a5e87f8-6433-40f4-a67a-4be78a558dc7',
'sys.workflow_id': 'bb5e2b89-40ac-45c9-9ccb-4f2cd926e080',
'sys.workflow_run_id': '76adf675-a7d3-4cc1-9282-ed7ecfe4f65d',
},
status: 'succeeded',
error: null,
elapsed_time: 0.011458,
execution_metadata: null,
extras: {},
created_by_end_user: null,
finished_at: 1735023510,
}
const outputArrayNode = {
id: 'a3105c5d-ff9e-44ea-9f4c-ab428958af20',
index: 2,
predecessor_node_id: '1735023354069',
node_id: '1735023361224',
node_type: 'code',
title: 'Code',
inputs: null,
process_data: null,
outputs: {
result: [
1,
2,
3,
],
},
status: 'succeeded',
error: null,
elapsed_time: 0.103333,
execution_metadata: null,
extras: {},
finished_at: 1735023511,
}
const iterationNode = {
id: 'a823134d-9f1a-45a4-8977-db838d076316',
index: 3,
predecessor_node_id: '1735023361224',
node_id: '1735023391914',
node_type: 'iteration',
title: 'Iteration',
inputs: null,
process_data: null,
outputs: {
output: [
'aaa',
'aaa',
'aaa',
],
},
}
const iterations = [
{
id: 'a84a22d8-0f08-4006-bee2-fa7a7aef0420',
index: 4,
predecessor_node_id: '1735023391914start',
node_id: '1735023409906',
node_type: 'code',
title: 'Code 2',
inputs: null,
process_data: null,
outputs: {
result: 'aaa',
},
status: 'succeeded',
error: null,
elapsed_time: 0.112688,
execution_metadata: {
iteration_id: '1735023391914',
iteration_index: 0,
},
extras: {},
created_at: 1735023511,
finished_at: 1735023511,
},
{
id: 'ff71d773-a916-4513-960f-d7dcc4fadd86',
index: 5,
predecessor_node_id: '1735023391914start',
node_id: '1735023409906',
node_type: 'code',
title: 'Code 2',
inputs: null,
process_data: null,
outputs: {
result: 'aaa',
},
status: 'succeeded',
error: null,
elapsed_time: 0.126034,
execution_metadata: {
iteration_id: '1735023391914',
iteration_index: 1,
},
extras: {},
created_at: 1735023511,
finished_at: 1735023511,
},
{
id: 'd91c3ef9-0162-4013-9272-d4cc7fb1f188',
index: 6,
predecessor_node_id: '1735023391914start',
node_id: '1735023409906',
node_type: 'code',
title: 'Code 2',
inputs: null,
process_data: null,
outputs: {
result: 'aaa',
},
status: 'succeeded',
error: null,
elapsed_time: 0.122716,
execution_metadata: {
iteration_id: '1735023391914',
iteration_index: 2,
},
extras: {},
created_at: 1735023511,
finished_at: 1735023511,
},
]
const endNode = {
id: 'e6ad6560-1aa3-43f3-89e3-e5287c9ea272',
index: 7,
predecessor_node_id: '1735023391914',
node_id: '1735023417757',
node_type: 'end',
title: 'End',
inputs: {
output: [
'aaa',
'aaa',
'aaa',
],
},
process_data: null,
outputs: {
output: [
'aaa',
'aaa',
'aaa',
],
},
status: 'succeeded',
error: null,
elapsed_time: 0.017552,
execution_metadata: null,
extras: {},
finished_at: 1735023511,
}
return {
in: [startNode, outputArrayNode, iterationNode, ...iterations, endNode],
expect: [startNode, outputArrayNode, {
...iterationNode,
details: [
[iterations[0]],
[iterations[1]],
[iterations[2]],
],
}, endNode],
}
})()

@ -1,11 +1,23 @@
import format from '.'
import { simpleIterationData } from './data'
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 result = format(list as any, () => { })
test('result should have no nodes in iteration node', () => {
expect(format(simpleIterationData.in as any).find(item => !!(item as any).execution_metadata?.iteration_id)).toBeUndefined()
expect((result as any).find((item: any) => !!item.execution_metadata?.iteration_id)).toBeUndefined()
})
test('iteration should put nodes in details', () => {
expect(format(simpleIterationData.in as any)).toEqual(simpleIterationData.expect)
expect(result as any).toEqual([
startNode,
{
...iterationNode,
details: [
[iterations[0]],
[iterations[1]],
],
},
])
})
})

@ -1,133 +0,0 @@
export const simpleRetryData = (() => {
const startNode = {
id: 'f7938b2b-77cd-43f0-814c-2f0ade7cbc60',
index: 1,
predecessor_node_id: null,
node_id: '1735112903395',
node_type: 'start',
title: 'Start',
inputs: {
'sys.files': [],
'sys.user_id': '6d8ad01f-edf9-43a6-b863-a034b1828ac7',
'sys.app_id': '6180ead7-2190-4a61-975c-ec3bf29653da',
'sys.workflow_id': 'eef6da45-244b-4c79-958e-f3573f7c12bb',
'sys.workflow_run_id': 'fc8970ef-1406-484e-afde-8567dc22f34c',
},
process_data: null,
outputs: {
'sys.files': [],
'sys.user_id': '6d8ad01f-edf9-43a6-b863-a034b1828ac7',
'sys.app_id': '6180ead7-2190-4a61-975c-ec3bf29653da',
'sys.workflow_id': 'eef6da45-244b-4c79-958e-f3573f7c12bb',
'sys.workflow_run_id': 'fc8970ef-1406-484e-afde-8567dc22f34c',
},
status: 'succeeded',
error: null,
elapsed_time: 0.008715,
execution_metadata: null,
extras: {},
created_at: 1735112940,
created_by_role: 'account',
created_by_account: {
id: '6d8ad01f-edf9-43a6-b863-a034b1828ac7',
name: '九彩拼盘',
email: 'iamjoel007@gmail.com',
},
created_by_end_user: null,
finished_at: 1735112940,
}
const httpNode = {
id: '50220407-3420-4ad4-89da-c6959710d1aa',
index: 2,
predecessor_node_id: '1735112903395',
node_id: '1735112908006',
node_type: 'http-request',
title: 'HTTP Request',
inputs: null,
process_data: {
request: 'GET / HTTP/1.1\r\nHost: 404\r\n\r\n',
},
outputs: null,
status: 'failed',
error: 'timed out',
elapsed_time: 30.247757,
execution_metadata: null,
extras: {},
created_at: 1735112940,
created_by_role: 'account',
created_by_account: {
id: '6d8ad01f-edf9-43a6-b863-a034b1828ac7',
name: '九彩拼盘',
email: 'iamjoel007@gmail.com',
},
created_by_end_user: null,
finished_at: 1735112970,
}
const retry1 = {
id: 'ed352b36-27fb-49c6-9e8f-cc755bfc25fc',
index: 3,
predecessor_node_id: '1735112903395',
node_id: '1735112908006',
node_type: 'http-request',
title: 'HTTP Request',
inputs: null,
process_data: null,
outputs: null,
status: 'retry',
error: 'timed out',
elapsed_time: 10.011833,
execution_metadata: {
iteration_id: null,
parallel_mode_run_id: null,
},
extras: {},
created_at: 1735112940,
created_by_role: 'account',
created_by_account: {
id: '6d8ad01f-edf9-43a6-b863-a034b1828ac7',
name: '九彩拼盘',
email: 'iamjoel007@gmail.com',
},
created_by_end_user: null,
finished_at: 1735112950,
}
const retry2 = {
id: '74dfb3d3-dacf-44f2-8784-e36bfa2d6c4e',
index: 4,
predecessor_node_id: '1735112903395',
node_id: '1735112908006',
node_type: 'http-request',
title: 'HTTP Request',
inputs: null,
process_data: null,
outputs: null,
status: 'retry',
error: 'timed out',
elapsed_time: 10.010368,
execution_metadata: {
iteration_id: null,
parallel_mode_run_id: null,
},
extras: {},
created_at: 1735112950,
created_by_role: 'account',
created_by_account: {
id: '6d8ad01f-edf9-43a6-b863-a034b1828ac7',
name: '九彩拼盘',
email: 'iamjoel007@gmail.com',
},
created_by_end_user: null,
finished_at: 1735112960,
}
return {
in: [startNode, httpNode, retry1, retry2],
expect: [startNode, {
...httpNode,
retryDetail: [retry1, retry2],
}],
}
})()

@ -1,11 +1,21 @@
import format from '.'
import { simpleRetryData } from './data'
import graphToLogStruct from '../graph-to-log-struct'
describe('retry', () => {
// retry nodeId:1 3 times.
const steps = graphToLogStruct('start -> (retry, 1, 3)')
const [startNode, retryNode, ...retryDetail] = steps
const result = format(steps)
test('should have no retry status nodes', () => {
expect(format(simpleRetryData.in as any).find(item => (item as any).status === 'retry')).toBeUndefined()
expect(result.find(item => (item as any).status === 'retry')).toBeUndefined()
})
test('should put retry nodes in retryDetail', () => {
expect(format(simpleRetryData.in as any)).toEqual(simpleRetryData.expect)
expect(result).toEqual([
startNode,
{
...retryNode,
retryDetail,
},
])
})
})

@ -1,14 +0,0 @@
const STEP_SPLIT = '->'
/*
* : 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, [2,3]). 1 is parent, [2, 3] is retry nodes
*/
const simpleGraphToLogStruct = (input: string): any[] => {
const list = input.split(STEP_SPLIT)
return list
}
export default simpleGraphToLogStruct

@ -22,9 +22,6 @@ import type {
} from './types'
import { WorkflowContext } from './context'
import type { NodeTracing, VersionHistory } from '@/types/workflow'
import type {
StrategyPluginDetail,
} from '@/app/components/plugins/types'
// #TODO chatVar#
// const MOCK_DATA = [
@ -101,7 +98,6 @@ type Shape = {
setCustomTools: (tools: ToolWithProvider[]) => void
workflowTools: ToolWithProvider[]
setWorkflowTools: (tools: ToolWithProvider[]) => void
agentStrategies: StrategyPluginDetail[],
clipboardElements: Node[]
setClipboardElements: (clipboardElements: Node[]) => void
showDebugAndPreviewPanel: boolean
@ -234,7 +230,6 @@ export const createWorkflowStore = () => {
setCustomTools: customTools => set(() => ({ customTools })),
workflowTools: [],
setWorkflowTools: workflowTools => set(() => ({ workflowTools })),
agentStrategies: [],
clipboardElements: [],
setClipboardElements: clipboardElements => set(() => ({ clipboardElements })),
showDebugAndPreviewPanel: false,

@ -1,76 +1,14 @@
'use client'
import { useState } from 'react'
import { FormattedText } from '../components/datasets/formatted-text/formatted'
import { PreviewSlice } from '../components/datasets/formatted-text/flavours/preview-slice'
import { PreviewContainer } from '../components/datasets/preview/container'
import { PreviewHeader } from '../components/datasets/preview/header'
import FileIcon from '../components/base/file-icon'
import { ChevronDown } from '../components/base/icons/src/vender/solid/arrows'
import Badge from '../components/base/badge'
import { DividerWithLabel } from '../components/base/divider/with-label'
import Button from '../components/base/button'
import { ChunkContainer, QAPreview } from '../components/datasets/chunk'
import classNames from '@/utils/classnames'
import { SwitchPluginVersion } from '../components/workflow/nodes/_base/components/switch-plugin-version'
import { useTranslation } from 'react-i18next'
export default function Page() {
const [parentChild, setParentChild] = useState(false)
const [vertical, setVertical] = useState(false)
const [qa, setQa] = useState(false)
return <div className='p-4'>
<div className='flex gap-2 my-4'>
<Button onClick={() => setParentChild(!parentChild)}>
Parent-Child
</Button>
<Button onClick={() => setVertical(!vertical)}>Vertical</Button>
<Button onClick={() => setQa(!qa)}>QA</Button>
</div>
<PreviewContainer header={
<PreviewHeader title='Preview'>
<div className='flex items-center'>
<FileIcon type='pdf' className='size-4' />
<p
className='text-text-primary text-sm font-semibold mx-1'
>EOS R3 Tech Sheet.pdf</p>
<ChevronDown className='size-[18px]' />
<Badge text='276 Estimated chunks' className='ml-1' />
</div>
</PreviewHeader>
}>
<div className='space-y-6'>{parentChild
? Array.from({ length: 4 }, (_, i) => {
return <ChunkContainer
label='Parent-Chunk-01'
characterCount={521}
key={i}
>
<FormattedText className={classNames(
'w-full',
vertical && 'flex flex-col gap-2',
)}>
{Array.from({ length: 4 }, (_, i) => {
return <PreviewSlice
key={i}
label='C-1'
text='lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.' tooltip={'Child-chunk-2 · 268 Characters'} />
})}
</FormattedText>
</ChunkContainer>
})
: Array.from({ length: 2 }, (_, i) => {
return <ChunkContainer label='Chunk-01' characterCount={521} key={i}>
{
qa
? <QAPreview qa={{
question: 'What is the author\'s unconventional approach to writing this book, and how does it challenge the traditional academic mindset of \'publish or perish\'?',
answer: 'It is quite natural for academics who are continuously told to “publish or perish” to want to always create something from scratch that is their own fresh creation. This book is an experiment in not starting from scratch, but instead “re-mixing” the book titled Think Python: How to Think Like a Computer Scientist written by Allen B. Downey, Jeff Elkner and others.',
}} />
: 'In December of 2009, I was preparing to teach SI502 - Networked Programming at the University of Michigan for the fifth semester in a row and decided it was time to write a Python textbook that focused on exploring data instead of understanding algorithms and abstractions. My goal in SI502 is to teach people life-long data handling skills using Python. Few of my students were planning to be professional computer programmers. Instead, they planned be librarians, managers, lawyers, biologists, economists, etc. who happened to want to skillfully use technology in their chosen field.'
}
</ChunkContainer>
})
}</div>
<DividerWithLabel label='Display previews of up to 10 paragraphs' />
</PreviewContainer>
const { t } = useTranslation()
return <div className="p-20">
<SwitchPluginVersion
uniqueIdentifier={'langgenius/openai:12'}
tooltip={t('workflow.nodes.agent.switchToNewVersion')}
/>
</div>
}

@ -733,7 +733,10 @@ const translation = {
toolNotInstallTooltip: '{{tool}} is not installed',
toolNotAuthorizedTooltip: '{{tool}} Not Authorized',
strategyNotInstallTooltip: '{{strategy}} is not installed',
strategyNotFoundInPlugin: '{{strategy}} is not found in {{plugin}}',
unsupportedStrategy: 'Unsupported strategy',
pluginNotFoundDesc: 'This plugin is installed from GitHub. Please go to Plugins to reinstall',
strategyNotFoundDesc: 'The installed plugin version does not provide this strategy.',
strategyNotFoundDescAndSwitchVersion: 'The installed plugin version does not provide this strategy. Click to switch version.',
modelSelectorTooltips: {
deprecated: 'This model is deprecated',
},
@ -752,6 +755,7 @@ const translation = {
checkList: {
strategyNotSelected: 'Strategy not selected',
},
switchToNewVersion: 'Switch to new version',
},
tracing: {
stopBy: 'Stop by {{user}}',

@ -733,7 +733,10 @@ const translation = {
toolNotInstallTooltip: '{{tool}} 未安装',
toolNotAuthorizedTooltip: '{{tool}} 未授权',
strategyNotInstallTooltip: '{{strategy}} 未安装',
strategyNotFoundInPlugin: '在 {{plugin}} 中未找到 {{strategy}}',
unsupportedStrategy: '不支持的策略',
strategyNotFoundDesc: '安装的插件版本不提供此策略。',
pluginNotFoundDesc: '此插件安装自 GitHub。请转到插件重新安装。',
strategyNotFoundDescAndSwitchVersion: '安装的插件版本不提供此策略。点击切换版本。',
modelSelectorTooltips: {
deprecated: '此模型已弃用',
},
@ -751,6 +754,7 @@ const translation = {
checkList: {
strategyNotSelected: '未选择策略',
},
switchToNewVersion: '切换到新版',
},
},
tracing: {

@ -22,6 +22,7 @@ import type {
PluginsSearchParams,
} from '@/app/components/plugins/marketplace/types'
import { get, getMarketplace, post, postMarketplace } from './base'
import type { MutateOptions, QueryOptions } from '@tanstack/react-query'
import {
useMutation,
useQuery,
@ -72,8 +73,9 @@ export const useInvalidateInstalledPluginList = () => {
}
}
export const useInstallPackageFromMarketPlace = () => {
export const useInstallPackageFromMarketPlace = (options?: MutateOptions<InstallPackageResponse, Error, string>) => {
return useMutation({
...options,
mutationFn: (uniqueIdentifier: string) => {
return post<InstallPackageResponse>('/workspaces/current/plugin/install/marketplace', { body: { plugin_unique_identifiers: [uniqueIdentifier] } })
},
@ -319,8 +321,9 @@ export const useMutationPluginsFromMarketplace = () => {
})
}
export const useFetchPluginsInMarketPlaceByIds = (unique_identifiers: string[]) => {
export const useFetchPluginsInMarketPlaceByIds = (unique_identifiers: string[], options?: QueryOptions<{ data: PluginsFromMarketplaceResponse }>) => {
return useQuery({
...options,
queryKey: [NAME_SPACE, 'fetchPluginsInMarketPlaceByIds', unique_identifiers],
queryFn: () => postMarketplace<{ data: PluginsFromMarketplaceResponse }>('/plugins/identifier/batch', {
body: {

@ -2,6 +2,7 @@ import type {
StrategyPluginDetail,
} from '@/app/components/plugins/types'
import { useInvalid } from './use-base'
import type { QueryOptions } from '@tanstack/react-query'
import {
useQuery,
} from '@tanstack/react-query'
@ -21,8 +22,9 @@ export const useInvalidateStrategyProviders = () => {
return useInvalid(useStrategyListKey)
}
export const useStrategyProviderDetail = (agentProvider: string) => {
export const useStrategyProviderDetail = (agentProvider: string, options?: QueryOptions<StrategyPluginDetail>) => {
return useQuery<StrategyPluginDetail>({
...options,
queryKey: [NAME_SPACE, 'detail', agentProvider],
queryFn: () => fetchStrategyDetail(agentProvider),
enabled: !!agentProvider,

Loading…
Cancel
Save