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

This commit is contained in:
zxhlyh 2024-12-31 14:09:11 +08:00
commit a66d92054a
40 changed files with 570 additions and 185 deletions

View File

@ -3,7 +3,7 @@ import { RiInstallLine, RiLoader2Line } from '@remixicon/react'
type InstallButtonProps = {
loading: boolean
onInstall: () => void
onInstall: (e: React.MouseEvent) => void
t: any
}

View File

@ -32,7 +32,7 @@ const MenuDialog = ({
return (
<Transition appear show={show} as={Fragment}>
<Dialog as="div" className="relative z-40" onClose={() => {}}>
<Dialog as="div" className="relative z-[60]" onClose={() => {}}>
<div className="fixed inset-0">
<div className="flex flex-col items-center justify-center min-h-full">
<Transition.Child

View File

@ -22,18 +22,18 @@ const ModelIcon: FC<ModelIconProps> = ({
}) => {
const language = useLanguage()
if (provider?.provider.includes('openai') && modelName?.includes('gpt-4o'))
return <OpenaiBlue className={cn('w-5 h-5', className)}/>
return <div className='flex w-6 h-6 items-center justify-center'><OpenaiBlue className={cn('w-5 h-5', className)}/></div>
if (provider?.provider.includes('openai') && modelName?.startsWith('gpt-4'))
return <OpenaiViolet className={cn('w-5 h-5', className)}/>
return <div className='flex w-6 h-6 items-center justify-center'><OpenaiViolet className={cn('w-5 h-5', className)}/></div>
if (provider?.icon_small) {
return (
<div className={isDeprecated ? 'opacity-50' : ''}>
<div className={`flex w-6 h-6 items-center justify-center ${isDeprecated ? 'opacity-50' : ''}`}>
<img
alt='model-icon'
src={`${provider.icon_small[language] || provider.icon_small.en_US}`}
className={cn('w-4 h-4', className)}
className={cn('w-5 h-5', className)}
/>
</div>
)
@ -41,10 +41,10 @@ const ModelIcon: FC<ModelIconProps> = ({
return (
<div className={cn(
'flex items-center justify-center w-5 h-5 rounded-md border-[0.5px] border-components-panel-border-subtle bg-background-default-subtle',
'flex items-center justify-center w-6 h-6 rounded-md border-[0.5px] border-components-panel-border-subtle bg-background-default-subtle',
className,
)}>
<div className='flex w-3 h-3 items-center justify-center opacity-35'>
<div className='flex w-5 h5 items-center justify-center opacity-35'>
<Group className='text-text-tertiary' />
</div>
</div>

View File

@ -1,5 +1,5 @@
import type { FC } from 'react'
import { useEffect, useState } from 'react'
import { useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import type {
CustomConfigurationModelFixedFields,
@ -9,6 +9,7 @@ import type {
import {
ConfigurationMethodEnum,
CustomConfigurationStatusEnum,
ModelTypeEnum,
} from '../declarations'
import { UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST } from '../provider-added-card'
import type { PluginInfoFromMarketPlace } from '@/app/components/plugins/types'
@ -38,6 +39,7 @@ export type AgentModelTriggerProps = {
providerName?: string
modelId?: string
hasDeprecated?: boolean
scope?: string
}
const AgentModelTrigger: FC<AgentModelTriggerProps> = ({
@ -47,6 +49,7 @@ const AgentModelTrigger: FC<AgentModelTriggerProps> = ({
providerName,
modelId,
hasDeprecated,
scope,
}) => {
const { t } = useTranslation()
const { modelProviders } = useProviderContext()
@ -54,13 +57,19 @@ const AgentModelTrigger: FC<AgentModelTriggerProps> = ({
const updateModelProviders = useUpdateModelProviders()
const updateModelList = useUpdateModelList()
const { eventEmitter } = useEventEmitterContextContext()
const modelProvider = modelProviders.find(item => item.provider === providerName)
const needsConfiguration = modelProvider?.custom_configuration.status === CustomConfigurationStatusEnum.noConfigure && !(
modelProvider.system_configuration.enabled === true
&& modelProvider.system_configuration.quota_configurations.find(
item => item.quota_type === modelProvider.system_configuration.current_quota_type,
const { modelProvider, needsConfiguration } = useMemo(() => {
const modelProvider = modelProviders.find(item => item.provider === providerName)
const needsConfiguration = modelProvider?.custom_configuration.status === CustomConfigurationStatusEnum.noConfigure && !(
modelProvider.system_configuration.enabled === true
&& modelProvider.system_configuration.quota_configurations.find(
item => item.quota_type === modelProvider.system_configuration.current_quota_type,
)
)
)
return {
modelProvider,
needsConfiguration,
}
}, [modelProviders, providerName])
const [pluginInfo, setPluginInfo] = useState<PluginInfoFromMarketPlace | null>(null)
const [isPluginChecked, setIsPluginChecked] = useState(false)
const [loading, setLoading] = useState(false)
@ -124,6 +133,34 @@ const AgentModelTrigger: FC<AgentModelTriggerProps> = ({
})
}
const handleInstall = async (pluginInfo: PluginInfoFromMarketPlace) => {
setLoading(true)
try {
const { all_installed } = await installPackageFromMarketPlace(pluginInfo.latest_package_identifier)
if (all_installed) {
[
ModelTypeEnum.textGeneration,
ModelTypeEnum.textEmbedding,
ModelTypeEnum.rerank,
ModelTypeEnum.moderation,
ModelTypeEnum.speech2text,
ModelTypeEnum.tts,
].forEach((type: ModelTypeEnum) => {
if (scope?.includes(type))
updateModelList(type)
})
updateModelProviders()
setInstalled(true)
}
}
catch (error) {
console.error('Installation failed:', error)
}
finally {
setLoading(false)
}
}
return (
<div
className={cn(
@ -158,15 +195,18 @@ const AgentModelTrigger: FC<AgentModelTriggerProps> = ({
{!installed && !modelProvider && pluginInfo && (
<InstallButton
loading={loading}
onInstall={async () => {
setLoading(true)
const { all_installed } = await installPackageFromMarketPlace(pluginInfo.latest_package_identifier)
if (all_installed)
setInstalled(true)
onInstall={(e) => {
e.stopPropagation()
handleInstall(pluginInfo)
}}
t={t}
/>
)}
{modelProvider && !disabled && !needsConfiguration && (
<div className="flex pr-1 items-center">
<RiEqualizer2Line className="w-4 h-4 text-text-tertiary group-hover:text-text-secondary" />
</div>
)}
</>
) : (
<>

View File

@ -1,4 +1,5 @@
import Tooltip from '@/app/components/base/tooltip'
import Link from 'next/link'
import { RiErrorWarningFill } from '@remixicon/react'
type StatusIndicatorsProps = {
@ -28,7 +29,16 @@ const StatusIndicators = ({ needsConfiguration, modelProvider, disabled, pluginI
<div className='min-w-[200px] text-text-secondary body-xs-regular'>
{t('workflow.nodes.agent.modelNotInMarketplace.desc')}
</div>
<div className='text-text-accent body-xs-regular'>{t('workflow.nodes.agent.modelNotInMarketplace.manageInPlugins')}</div>
<div className='text-text-accent body-xs-regular cursor-pointer z-[100]'>
<Link
href={'/plugins'}
onClick={(e) => {
e.stopPropagation()
}}
>
{t('workflow.nodes.agent.linkToPlugin')}
</Link>
</div>
</div>
}
asChild={false}

View File

@ -22,19 +22,21 @@ const ModelTrigger: FC<ModelTriggerProps> = ({
return (
<div
className={cn('group flex items-center px-2 h-8 rounded-lg bg-components-input-bg-disabled cursor-pointer', className)}
className={cn('group flex flex-grow items-center p-[3px] pl-1 h-6 gap-1 rounded-lg bg-components-input-bg-disabled cursor-pointer', className)}
>
<ModelIcon
className='shrink-0 mr-1.5'
provider={currentProvider}
modelName={modelName}
/>
<div className='mr-1 text-[13px] font-medium text-text-secondary truncate'>
{modelName}
<div className='flex items-center py-[1px] gap-1 grow'>
<ModelIcon
className="m-0.5 w-4 h-4"
provider={currentProvider}
modelName={modelName}
/>
<div className='system-sm-regular text-components-input-text-filled truncate'>
{modelName}
</div>
</div>
<div className='shrink-0 flex items-center justify-center w-4 h-4'>
<Tooltip popupContent={t('common.modelProvider.deprecated')}>
<AlertTriangle className='w-4 h-4 text-[#F79009]' />
<AlertTriangle className='w-4 h-4 text-text-warning-secondary' />
</Tooltip>
</div>
</div>

View File

@ -14,7 +14,7 @@ const ModelTrigger: FC<ModelTriggerProps> = ({
return (
<div
className={cn(
'flex items-center px-2 h-8 rounded-lg bg-components-input-bg-normal hover:bg-components-input-bg-hover cursor-pointer', open && 'bg-components-input-bg-hover',
'flex items-center p-1 gap-0.5 rounded-lg bg-components-input-bg-normal hover:bg-components-input-bg-hover cursor-pointer', open && 'bg-components-input-bg-hover',
className,
)}
>

View File

@ -104,6 +104,7 @@ const ModelSelector: FC<ModelSelectorProps> = ({
modelList={modelList}
onSelect={handleSelect}
scopeFeatures={scopeFeatures}
onHide={() => setOpen(false)}
/>
</PortalToFollowElemContent>
</div>

View File

@ -34,7 +34,7 @@ const ModelTrigger: FC<ModelTriggerProps> = ({
return (
<div
className={cn(
'group flex items-center px-2 h-8 rounded-lg bg-components-input-bg-normal',
'group flex items-center p-1 gap-0.5 h-8 rounded-lg bg-components-input-bg-normal',
!readonly && 'hover:bg-components-input-bg-hover cursor-pointer',
open && 'bg-components-input-bg-hover',
model.status !== ModelStatusEnum.active && 'bg-components-input-bg-disabled hover:bg-components-input-bg-disabled',

View File

@ -1,10 +1,25 @@
import type { FC } from 'react'
import { useTranslation } from 'react-i18next'
import {
RiFileTextLine,
RiFilmAiLine,
RiImageCircleAiLine,
RiVoiceAiFill,
} from '@remixicon/react'
import type {
DefaultModel,
Model,
ModelItem,
} from '../declarations'
import {
ModelFeatureEnum,
ModelFeatureTextEnum,
ModelTypeEnum,
} from '../declarations'
import {
modelTypeFormat,
sizeFormat,
} from '../utils'
import {
useLanguage,
useUpdateModelList,
@ -12,15 +27,16 @@ import {
} from '../hooks'
import ModelIcon from '../model-icon'
import ModelName from '../model-name'
import ModelBadge from '../model-badge'
import {
ConfigurationMethodEnum,
MODEL_STATUS_TEXT,
ModelStatusEnum,
} from '../declarations'
import { Check } from '@/app/components/base/icons/src/vender/line/general'
import { useModalContext } from '@/context/modal-context'
import { useProviderContext } from '@/context/provider-context'
import Tooltip from '@/app/components/base/tooltip'
import cn from '@/utils/classnames'
type PopupItemProps = {
defaultModel?: DefaultModel
@ -71,34 +87,86 @@ const PopupItem: FC<PopupItemProps> = ({
model.models.map(modelItem => (
<Tooltip
key={modelItem.model}
popupContent={modelItem.status !== ModelStatusEnum.active ? MODEL_STATUS_TEXT[modelItem.status][language] : undefined}
position='right'
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'>
<ModelIcon
className={cn('shrink-0 w-5 h-5')}
provider={model}
modelName={modelItem.model}
/>
<div className='truncate text-text-primary system-md-medium'>{modelItem.label[language] || modelItem.label.en_US}</div>
</div>
{/* {currentProvider?.description && (
<div className='text-text-tertiary system-xs-regular'>{currentProvider?.description?.[language] || currentProvider?.description?.en_US}</div>
)} */}
<div className='flex flex-wrap gap-1'>
{modelItem.model_type && (
<ModelBadge>
{modelTypeFormat(modelItem.model_type)}
</ModelBadge>
)}
{modelItem.model_properties.mode && (
<ModelBadge>
{(modelItem.model_properties.mode as string).toLocaleUpperCase()}
</ModelBadge>
)}
{modelItem.model_properties.context_size && (
<ModelBadge>
{sizeFormat(modelItem.model_properties.context_size as number)}
</ModelBadge>
)}
</div>
{modelItem.model_type === ModelTypeEnum.textGeneration && modelItem.features?.some(feature => [ModelFeatureEnum.vision, ModelFeatureEnum.audio, ModelFeatureEnum.video, ModelFeatureEnum.document].includes(feature)) && (
<div className='pt-2'>
<div className='mb-1 text-text-tertiary system-2xs-medium-uppercase'>{t('common.model.capabilities')}</div>
<div className='flex flex-wrap gap-1'>
{modelItem.features?.includes(ModelFeatureEnum.vision) && (
<ModelBadge>
<RiImageCircleAiLine className='w-3.5 h-3.5 mr-0.5' />
<span>{ModelFeatureTextEnum.vision}</span>
</ModelBadge>
)}
{modelItem.features?.includes(ModelFeatureEnum.audio) && (
<ModelBadge>
<RiVoiceAiFill className='w-3.5 h-3.5 mr-0.5' />
<span>{ModelFeatureTextEnum.audio}</span>
</ModelBadge>
)}
{modelItem.features?.includes(ModelFeatureEnum.video) && (
<ModelBadge>
<RiFilmAiLine className='w-3.5 h-3.5 mr-0.5' />
<span>{ModelFeatureTextEnum.video}</span>
</ModelBadge>
)}
{modelItem.features?.includes(ModelFeatureEnum.document) && (
<ModelBadge>
<RiFileTextLine className='w-3.5 h-3.5 mr-0.5' />
<span>{ModelFeatureTextEnum.document}</span>
</ModelBadge>
)}
</div>
</div>
)}
</div>
}
>
<div
key={modelItem.model}
className={`
group relative flex items-center px-3 py-1.5 h-8 rounded-lg gap-1
${modelItem.status === ModelStatusEnum.active ? 'cursor-pointer hover:bg-state-base-hover' : 'cursor-not-allowed hover:bg-state-base-hover-alt'}
`}
className={cn('group relative flex items-center px-3 py-1.5 h-8 rounded-lg gap-1', modelItem.status === ModelStatusEnum.active ? 'cursor-pointer hover:bg-state-base-hover' : 'cursor-not-allowed hover:bg-state-base-hover-alt')}
onClick={() => handleSelect(model.provider, modelItem)}
>
<div className='flex items-center gap-2'>
<ModelIcon
className={`
shrink-0 w-4 h-4
${modelItem.status !== ModelStatusEnum.active && 'opacity-60'}
`}
className={cn('shrink-0 w-5 h-5')}
provider={model}
modelName={modelItem.model}
/>
<ModelName
className={`
text-text-secondary system-sm-medium
${modelItem.status !== ModelStatusEnum.active && 'opacity-60'}
`}
className={cn('text-text-secondary system-sm-medium', modelItem.status !== ModelStatusEnum.active && 'opacity-60')}
modelItem={modelItem}
showMode
showFeatures
/>
</div>
{

View File

@ -1,6 +1,8 @@
import type { FC } from 'react'
import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import {
RiArrowRightUpLine,
RiSearchLine,
} from '@remixicon/react'
import type {
@ -12,21 +14,26 @@ import { ModelFeatureEnum } from '../declarations'
import { useLanguage } from '../hooks'
import PopupItem from './popup-item'
import { XCircle } from '@/app/components/base/icons/src/vender/solid/general'
import { useModalContext } from '@/context/modal-context'
type PopupProps = {
defaultModel?: DefaultModel
modelList: Model[]
onSelect: (provider: string, model: ModelItem) => void
scopeFeatures?: string[]
onHide: () => void
}
const Popup: FC<PopupProps> = ({
defaultModel,
modelList,
onSelect,
scopeFeatures = [],
onHide,
}) => {
const { t } = useTranslation()
const language = useLanguage()
const [searchText, setSearchText] = useState('')
const { setShowAccountSettingModal } = useModalContext()
const filteredModelList = useMemo(() => {
return modelList.map((model) => {
@ -99,6 +106,13 @@ 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={() => {
onHide()
setShowAccountSettingModal({ payload: 'provider' })
}}>
<span className='system-xs-medium'>{t('common.model.settingsLink')}</span>
<RiArrowRightUpLine className='ml-0.5 w-3 h-3' />
</div>
</div>
)
}

View File

@ -40,7 +40,7 @@ const SearchBox = ({
locale={locale}
/>
<div className='mx-1 w-[1px] h-3.5 bg-divider-regular'></div>
<div className='grow flex items-center p-1 pl-2'>
<div className='relative grow flex items-center p-1 pl-2'>
<div className='flex items-center mr-2 w-full'>
<input
className={cn(
@ -54,9 +54,11 @@ const SearchBox = ({
/>
{
search && (
<ActionButton onClick={() => onSearchChange('')}>
<RiCloseLine className='w-4 h-4' />
</ActionButton>
<div className='absolute right-2 top-1/2 -translate-y-1/2'>
<ActionButton onClick={() => onSearchChange('')}>
<RiCloseLine className='w-4 h-4' />
</ActionButton>
</div>
)
}
</div>

View File

@ -190,6 +190,7 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
currentModel={currentModel}
providerName={value?.provider}
modelId={value?.model}
scope={scope}
/>
: <Trigger
disabled={disabled}
@ -204,7 +205,7 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
)
}
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className={cn('z-[60]', portalToFollowElemContentClassName)}>
<PortalToFollowElemContent className={cn('z-50', portalToFollowElemContentClassName)}>
<div className={cn(popupClassName, 'w-[389px] rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg')}>
<div className={cn('max-h-[420px] p-4 pt-3 overflow-y-auto')}>
<div className='relative'>

View File

@ -392,6 +392,12 @@ export type StrategyParamItem = {
required: boolean
default: any
options: any[]
template: {
enabled: boolean
},
auto_generate: {
type: string
}
}
export type StrategyDetail = {

View File

@ -74,11 +74,13 @@ const List = ({
)
}
const maxWidthClassName = 'max-w-[300px]'
return (
<>
{hasRes && (
<div
className={cn('sticky z-10 flex justify-between h-8 px-4 py-1 text-text-primary system-sm-medium cursor-pointer', stickyClassName)}
className={cn('sticky z-10 flex justify-between h-8 px-4 py-1 text-text-primary system-sm-medium cursor-pointer', stickyClassName, maxWidthClassName)}
onClick={handleHeadClick}
>
<span>{t('plugin.fromMarketplace')}</span>
@ -93,7 +95,7 @@ const List = ({
</Link>
</div>
)}
<div className='p-1' ref={nextToStickyELemRef}>
<div className={cn('p-1', maxWidthClassName)} ref={nextToStickyELemRef}>
{list.map((item, index) => (
<Item
key={index}

View File

@ -68,6 +68,7 @@ const Tabs: FC<TabsProps> = ({
{
activeTab === TabsEnum.Tools && (
<AllTools
className='w-[315px]'
searchText={searchText}
onSelect={onSelect}
tags={tags}

View File

@ -64,7 +64,7 @@ const ToolItem: FC<Props> = ({
})
}}
>
<div className='h-8 leading-8 border-l-2 border-divider-subtle pl-4 truncate text-text-secondary system-sm-medium'>{payload.name}</div>
<div className='h-8 leading-8 border-l-2 border-divider-subtle pl-4 truncate text-text-secondary system-sm-medium'>{payload.label[language]}</div>
</div>
</Tooltip >
)

View File

@ -89,15 +89,15 @@ export const AgentStrategySelector = (props: AgentStrategySelectorProps) => {
const { t } = useTranslation()
return <PortalToFollowElem open={open} onOpenChange={setOpen} placement='bottom'>
<PortalToFollowElemTrigger className='w-full'>
<div className='py-2 pl-3 pr-2 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 && <img
{icon && <div className='flex items-center justify-center w-6 h-6'><img
src={icon}
width={20}
height={20}
className='rounded-md border-[0.5px] border-components-panel-border-subtle bg-background-default-dodge'
alt='icon'
/>}
/></div>}
<p
className={classNames(value ? 'text-components-input-text-filled' : 'text-components-input-text-placeholder', 'text-xs px-1')}
>
@ -126,8 +126,6 @@ export const AgentStrategySelector = (props: AgentStrategySelectorProps) => {
agent_strategy_provider_name: tool!.provider_name,
agent_strategy_label: tool!.tool_label,
agent_output_schema: tool!.output_schema,
agent_configurations: {},
agent_parameters: {},
})
setOpen(false)
}}

View File

@ -1,5 +1,5 @@
import type { CredentialFormSchemaNumberInput } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { type CredentialFormSchema, FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { type CredentialFormSchema, FormTypeEnum, ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import type { ToolVarInputs } from '../../tool/types'
import ListEmpty from '@/app/components/base/list-empty'
import { AgentStrategySelector } from './agent-strategy-selector'
@ -13,14 +13,14 @@ import ToolSelector from '@/app/components/plugins/plugin-detail-panel/tool-sele
import MultipleToolSelector from '@/app/components/plugins/plugin-detail-panel/multiple-tool-selector'
import Field from './field'
import type { ComponentProps } from 'react'
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { useDefaultModel, useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
import Editor from './prompt/editor'
import { useWorkflowStore } from '../../../store'
export type Strategy = {
agent_strategy_provider_name: string
agent_strategy_name: string
agent_strategy_label: string
agent_configurations?: Record<string, any>
agent_parameters?: Record<string, ToolVarInputs>
agent_output_schema: Record<string, any>
}
@ -36,85 +36,26 @@ type CustomSchema<Type, Field = {}> = Omit<CredentialFormSchema, 'type'> & { typ
type ToolSelectorSchema = CustomSchema<'tool-selector'>
type MultipleToolSelectorSchema = CustomSchema<'array[tools]'>
type CustomField = ToolSelectorSchema | MultipleToolSelectorSchema
const devMockForm = [{
name: 'instruction',
label: {
en_US: 'Instruction',
zh_Hans: '指令',
pt_BR: 'Instruction',
ja_JP: 'Instruction',
type StringSchema = CustomSchema<'string', {
template?: {
enabled: boolean
},
placeholder: null,
scope: null,
auto_generate: {
type: 'prompt_instruction',
},
template: {
enabled: true,
},
required: true,
default: null,
min: null,
max: null,
options: [],
type: 'string',
},
{
name: 'query',
label: {
en_US: 'Query',
zh_Hans: '查询',
pt_BR: 'Query',
ja_JP: 'Query',
},
placeholder: null,
scope: null,
auto_generate: null,
template: null,
required: true,
default: null,
min: null,
max: null,
options: [],
type: 'string',
},
{
name: 'max iterations',
label: {
en_US: 'Max Iterations',
zh_Hans: '最大迭代次数',
pt_BR: 'Max Iterations',
ja_JP: 'Max Iterations',
},
placeholder: null,
scope: null,
auto_generate: null,
template: null,
required: true,
default: '1',
min: 1,
max: 10,
type: FormTypeEnum.textNumber,
tooltip: {
en_US: 'The maximum number of iterations to run',
zh_Hans: '运行的最大迭代次数',
pt_BR: 'The maximum number of iterations to run',
ja_JP: 'The maximum number of iterations to run',
},
}].map((item) => {
return {
...item,
variable: item.name,
auto_generate?: {
type: string
}
})
}>
type CustomField = ToolSelectorSchema | MultipleToolSelectorSchema | StringSchema
export const AgentStrategy = (props: AgentStrategyProps) => {
const { strategy, onStrategyChange, formSchema, formValue, onFormValueChange } = props
const { t } = useTranslation()
const language = useLanguage()
const defaultModel = useDefaultModel(ModelTypeEnum.textGeneration)
const workflowStore = useWorkflowStore()
const {
setControlPromptEditorRerenderKey,
} = workflowStore.getState()
const override: ComponentProps<typeof Form<CustomField>>['override'] = [
[FormTypeEnum.textNumber],
(schema, props) => {
@ -188,6 +129,41 @@ export const AgentStrategy = (props: AgentStrategyProps) => {
/>
)
}
case 'string': {
const value = props.value[schema.variable]
const onChange = (value: string) => {
props.onChange({ ...props.value, [schema.variable]: value })
}
const handleGenerated = (value: string) => {
onChange(value)
setControlPromptEditorRerenderKey(Math.random())
}
return <Editor
value={value}
onChange={onChange}
onGenerated={handleGenerated}
title={schema.label[language]}
headerClassName='bg-transparent px-0 text-text-secondary system-sm-semibold-uppercase'
containerClassName='bg-transparent'
gradientBorder={false}
isSupportPromptGenerator={!!schema.auto_generate?.type}
titleTooltip={schema.tooltip?.[language]}
editorContainerClassName='px-0'
isSupportJinja={schema.template?.enabled}
varList={[]}
modelConfig={
defaultModel.data
? {
mode: 'chat',
name: defaultModel.data.model,
provider: defaultModel.data.provider.provider,
completion_params: {},
} : undefined
}
placeholderClassName='px-2 py-1'
inputClassName='px-2 py-1 bg-components-input-bg-normal focus:bg-components-input-bg-active focus:border-components-input-border-active focus:border rounded-lg'
/>
}
}
}
return <div className='space-y-2'>
@ -196,10 +172,7 @@ export const AgentStrategy = (props: AgentStrategyProps) => {
strategy
? <div>
<Form<CustomField>
formSchemas={[
...formSchema,
...devMockForm as any,
]}
formSchemas={formSchema}
value={formValue}
onChange={onFormValueChange}
validating={false}

View File

@ -1,5 +1,5 @@
'use client'
import type { FC } from 'react'
import type { FC, ReactNode } from 'react'
import React, { useCallback, useRef } from 'react'
import {
RiDeleteBinLine,
@ -68,6 +68,12 @@ type Props = {
onEditionTypeChange?: (editionType: EditionType) => void
varList?: Variable[]
handleAddVariable?: (payload: any) => void
containerClassName?: string
gradientBorder?: boolean
titleTooltip?: ReactNode
inputClassName?: string
editorContainerClassName?: string
placeholderClassName?: string
}
const Editor: FC<Props> = ({
@ -96,6 +102,12 @@ const Editor: FC<Props> = ({
handleAddVariable,
onGenerated,
modelConfig,
containerClassName,
gradientBorder = true,
titleTooltip,
inputClassName,
placeholderClassName,
editorContainerClassName,
}) => {
const { t } = useTranslation()
const { eventEmitter } = useEventEmitterContextContext()
@ -129,10 +141,13 @@ const Editor: FC<Props> = ({
return (
<Wrap className={cn(className, wrapClassName)} style={wrapStyle} isInNode isExpand={isExpand}>
<div ref={ref} className={cn(isFocus ? s.gradientBorder : 'bg-gray-100', isExpand && 'h-full', '!rounded-[9px] p-0.5')}>
<div className={cn(isFocus ? 'bg-gray-50' : 'bg-gray-100', isExpand && 'h-full flex flex-col', 'rounded-lg')}>
<div className={cn(headerClassName, 'pt-1 pl-3 pr-2 flex justify-between items-center')}>
<div className='leading-4 text-xs font-semibold text-gray-700 uppercase'>{title}</div>
<div ref={ref} className={cn(isFocus ? (gradientBorder && s.gradientBorder) : 'bg-gray-100', isExpand && 'h-full', '!rounded-[9px] p-0.5', containerClassName)}>
<div className={cn(isFocus ? 'bg-gray-50' : 'bg-gray-100', isExpand && 'h-full flex flex-col', 'rounded-lg', containerClassName)}>
<div className={cn('pt-1 pl-3 pr-2 flex justify-between items-center', headerClassName)}>
<div className='flex gap-2'>
<div className='leading-4 text-xs font-semibold text-gray-700 uppercase'>{title}</div>
{titleTooltip && <Tooltip popupContent={titleTooltip} />}
</div>
<div className='flex items-center'>
<div className='leading-[18px] text-xs font-medium text-gray-500'>{value?.length || 0}</div>
{isSupportPromptGenerator && (
@ -201,12 +216,13 @@ const Editor: FC<Props> = ({
<div className={cn('pb-2', isExpand && 'flex flex-col grow')}>
{!(isSupportJinja && editionType === EditionType.jinja2)
? (
<div className={cn(isExpand ? 'grow' : 'max-h-[536px]', 'relative px-3 min-h-[56px] overflow-y-auto')}>
<div className={cn(isExpand ? 'grow' : 'max-h-[536px]', 'relative px-3 min-h-[56px] overflow-y-auto', editorContainerClassName)}>
<PromptEditor
key={controlPromptEditorRerenderKey}
placeholderClassName={placeholderClassName}
instanceId={instanceId}
compact
className='min-h-[56px]'
className={cn('min-h-[56px]', inputClassName)}
style={isExpand ? { height: editorExpandHeight - 5 } : {}}
value={value}
contextBlock={{
@ -254,7 +270,7 @@ const Editor: FC<Props> = ({
</div>
)
: (
<div className={cn(isExpand ? 'grow' : 'max-h-[536px]', 'relative px-3 min-h-[56px] overflow-y-auto')}>
<div className={cn(isExpand ? 'grow' : 'max-h-[536px]', 'relative px-3 min-h-[56px] overflow-y-auto', editorContainerClassName)}>
<CodeEditor
availableVars={nodesOutputVars || []}
varList={varList}
@ -266,6 +282,7 @@ const Editor: FC<Props> = ({
onChange={onChange}
noWrapper
isExpand={isExpand}
className={inputClassName}
/>
</div>
)}

View File

@ -12,12 +12,12 @@ export type SettingItemProps = PropsWithChildren<{
export const SettingItem = ({ label, children, status, tooltip }: SettingItemProps) => {
const indicator: ComponentProps<typeof Indicator>['color'] = status === 'error' ? 'red' : status === 'warning' ? 'yellow' : undefined
const needTooltip = ['error', 'warning'].includes(status as any)
return <div className='flex items-center h-6 justify-between bg-workflow-block-parma-bg rounded-md px-1 space-x-1 text-xs font-normal relative'>
<div className={classNames('shrink-0 truncate text-xs font-medium text-text-tertiary uppercase', !!children && 'max-w-[100px]')}>
return <div className='flex items-center justify-between bg-workflow-block-parma-bg rounded-md py-1 px-1.5 space-x-1 text-xs font-normal relative'>
<div className={classNames('shrink-0 truncate text-text-tertiary system-xs-medium-uppercase', !!children && 'max-w-[100px]')}>
{label}
</div>
<Tooltip popupContent={tooltip} disabled={!needTooltip}>
<div className='truncate text-right text-xs font-normal text-text-secondary'>
<div className='truncate text-right system-xs-medium text-text-secondary'>
{children}
</div>
</Tooltip>

View File

@ -21,7 +21,7 @@ const AgentNode: FC<NodeProps<AgentNodeType>> = (props) => {
const models = currentStrategy?.parameters
.filter(param => param.type === FormTypeEnum.modelSelector)
.reduce((acc, param) => {
const item = inputs.agent_configurations?.[param.name]
const item = inputs.agent_parameters?.[param.name]?.value
if (!item) {
if (param.required) {
acc.push({ param: param.name })
@ -40,7 +40,7 @@ const AgentNode: FC<NodeProps<AgentNodeType>> = (props) => {
currentStrategy?.parameters.forEach((param) => {
if (param.type === FormTypeEnum.toolSelector) {
const field = param.name
const value = inputs.agent_configurations?.[field]
const value = inputs.agent_parameters?.[field]?.value
if (value) {
tools.push({
providerName: value.provider_name as any,
@ -49,7 +49,7 @@ const AgentNode: FC<NodeProps<AgentNodeType>> = (props) => {
}
if (param.type === FormTypeEnum.multiToolSelector) {
const field = param.name
const value = inputs.agent_configurations?.[field]
const value = inputs.agent_parameters?.[field]?.value
if (value) {
(value as unknown as any[]).forEach((item) => {
tools.push({
@ -60,7 +60,7 @@ const AgentNode: FC<NodeProps<AgentNodeType>> = (props) => {
}
})
return tools
}, [currentStrategy?.parameters, inputs.agent_configurations])
}, [currentStrategy?.parameters, inputs.agent_parameters])
return <div className='mb-1 px-3 py-1 space-y-1'>
{inputs.agent_strategy_name
? <SettingItem

View File

@ -11,7 +11,7 @@ import type { CredentialFormSchema } from '@/app/components/header/account-setti
const i18nPrefix = 'workflow.nodes.agent'
function strategyParamToCredientialForm(param: StrategyParamItem): CredentialFormSchema {
export function strategyParamToCredientialForm(param: StrategyParamItem): CredentialFormSchema {
return {
...param as any,
variable: param.name,
@ -20,7 +20,7 @@ function strategyParamToCredientialForm(param: StrategyParamItem): CredentialFor
}
const AgentPanel: FC<NodePanelProps<AgentNodeType>> = (props) => {
const { inputs, setInputs, currentStrategy } = useConfig(props.id, props.data)
const { inputs, setInputs, currentStrategy, formData, onFormChange } = useConfig(props.id, props.data)
const { t } = useTranslation()
return <div className='my-2'>
<Field title={t('workflow.nodes.agent.strategy.label')} className='px-4' >
@ -28,28 +28,21 @@ const AgentPanel: FC<NodePanelProps<AgentNodeType>> = (props) => {
strategy={inputs.agent_strategy_name ? {
agent_strategy_provider_name: inputs.agent_strategy_provider_name!,
agent_strategy_name: inputs.agent_strategy_name!,
agent_configurations: inputs.agent_configurations,
agent_strategy_label: inputs.agent_strategy_label!,
agent_output_schema: inputs.output_schema,
agent_parameters: inputs.agent_parameters,
} : undefined}
onStrategyChange={(strategy) => {
setInputs({
...inputs,
agent_strategy_provider_name: strategy?.agent_strategy_provider_name,
agent_strategy_name: strategy?.agent_strategy_name,
agent_configurations: strategy?.agent_configurations,
agent_parameters: strategy?.agent_parameters,
agent_strategy_label: strategy?.agent_strategy_label,
output_schema: strategy!.agent_output_schema,
})
}}
formSchema={currentStrategy?.parameters?.map(strategyParamToCredientialForm) || []}
formValue={inputs.agent_configurations || {}}
onFormValueChange={value => setInputs({
...inputs,
agent_configurations: value,
})}
formValue={formData}
onFormValueChange={onFormChange}
/>
</Field>
<div>

View File

@ -5,7 +5,6 @@ export type AgentNodeType = CommonNodeType & {
agent_strategy_provider_name?: string
agent_strategy_name?: string
agent_strategy_label?: string
agent_parameters?: Record<string, ToolVarInputs>
agent_configurations?: Record<string, any>
agent_parameters?: ToolVarInputs
output_schema: Record<string, any>
}

View File

@ -1,10 +1,13 @@
import { useStrategyProviderDetail } from '@/service/use-strategy'
import useNodeCrud from '../_base/hooks/use-node-crud'
import useVarList from '../_base/hooks/use-var-list'
import useOneStepRun from '../_base/hooks/use-one-step-run'
import type { AgentNodeType } from './types'
import {
useNodesReadOnly,
} from '@/app/components/workflow/hooks'
import { useMemo } from 'react'
import { type ToolVarInputs, VarType } from '../tool/types'
const useConfig = (id: string, payload: AgentNodeType) => {
const { nodesReadOnly: readOnly } = useNodesReadOnly()
@ -14,12 +17,59 @@ const useConfig = (id: string, payload: AgentNodeType) => {
inputs,
setInputs,
})
const strategies = useStrategyProviderDetail(
const strategyProvider = useStrategyProviderDetail(
inputs.agent_strategy_provider_name || '',
)
const currentStrategy = strategies.data?.declaration.strategies.find(
// single run
const agentInputKey = `${id}.input_selector`
const {
isShowSingleRun,
showSingleRun,
hideSingleRun,
toVarInputs,
runningStatus,
handleRun,
handleStop,
runInputData,
setRunInputData,
runResult,
} = useOneStepRun<AgentNodeType>({
id,
data: inputs,
defaultRunInputData: {
[agentInputKey]: [''],
},
})
const currentStrategy = strategyProvider.data?.declaration.strategies.find(
str => str.identity.name === inputs.agent_strategy_name,
)
const currentStrategyStatus = useMemo(() => {
if (strategyProvider.isLoading) return 'loading'
if (strategyProvider.isError) return 'plugin-not-found'
if (!currentStrategy) return 'strategy-not-found'
return 'success'
}, [currentStrategy, strategyProvider])
const formData = useMemo(() => {
return Object.fromEntries(
Object.entries(inputs.agent_parameters || {}).map(([key, value]) => {
return [key, value.value]
}),
)
}, [inputs.agent_parameters])
const onFormChange = (value: Record<string, any>) => {
const res: ToolVarInputs = {}
Object.entries(value).forEach(([key, val]) => {
res[key] = {
type: VarType.constant,
value: val,
}
})
setInputs({
...inputs,
agent_parameters: res,
})
}
return {
readOnly,
inputs,
@ -27,6 +77,22 @@ const useConfig = (id: string, payload: AgentNodeType) => {
handleVarListChange,
handleAddVariable,
currentStrategy,
formData,
onFormChange,
currentStrategyStatus,
strategyProvider: strategyProvider.data,
isShowSingleRun,
showSingleRun,
hideSingleRun,
toVarInputs,
runningStatus,
handleRun,
handleStop,
runInputData,
setRunInputData,
runResult,
agentInputKey,
}
}

View File

@ -1,5 +1,5 @@
import type { FC } from 'react'
import React from 'react'
import React, { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import Split from '../_base/components/split'
import type { ToolNodeType } from './types'
@ -15,6 +15,8 @@ import BeforeRunForm from '@/app/components/workflow/nodes/_base/components/befo
import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars'
import ResultPanel from '@/app/components/workflow/run/result-panel'
import { useToolIcon } from '@/app/components/workflow/hooks'
import { useLogs } from '@/app/components/workflow/run/hooks'
import formatToTracingNodeList from '@/app/components/workflow/run/utils/format-log'
const i18nPrefix = 'workflow.nodes.tool'
@ -51,6 +53,12 @@ const Panel: FC<NodePanelProps<ToolNodeType>> = ({
outputSchema,
} = useConfig(id, data)
const toolIcon = useToolIcon(data)
const logsParams = useLogs()
const nodeInfo = useMemo(() => {
if (!runResult)
return null
return formatToTracingNodeList([runResult], t)[0]
}, [runResult, t])
if (isLoading) {
return <div className='flex h-[200px] items-center justify-center'>
@ -161,7 +169,8 @@ const Panel: FC<NodePanelProps<ToolNodeType>> = ({
runningStatus={runningStatus}
onRun={handleRun}
onStop={handleStop}
result={<ResultPanel {...runResult} showSteps={false} />}
{...logsParams}
result={<ResultPanel {...runResult} showSteps={false} {...logsParams} nodeInfo={nodeInfo} />}
/>
)}
</div>

View File

@ -4,7 +4,7 @@ import Button from '@/app/components/base/button'
import type { AgentLogItemWithChildren } from '@/types/workflow'
type AgentLogNavProps = {
agentOrToolLogItemStack: { id: string; label: string }[]
agentOrToolLogItemStack: AgentLogItemWithChildren[]
onShowAgentOrToolLog: (detail?: AgentLogItemWithChildren) => void
}
const AgentLogNav = ({

View File

@ -1,9 +1,10 @@
import { RiAlertFill } from '@remixicon/react'
import AgentLogItem from './agent-log-item'
import AgentLogNav from './agent-log-nav'
import type { AgentLogItemWithChildren } from '@/types/workflow'
type AgentResultPanelProps = {
agentOrToolLogItemStack: { id: string; label: string }[]
agentOrToolLogItemStack: AgentLogItemWithChildren[]
agentOrToolLogListMap: Record<string, AgentLogItemWithChildren[]>
onShowAgentOrToolLog: (detail?: AgentLogItemWithChildren) => void
}
@ -34,6 +35,22 @@ const AgentResultPanel = ({
}
</div>
}
{
top.hasCircle && (
<div className='flex items-center rounded-xl px-3 pr-2 border border-components-panel-border bg-components-panel-bg-blur shadow-md'>
<div
className='absolute inset-0 opacity-[0.4] rounded-xl'
style={{
background: 'linear-gradient(92deg, rgba(247, 144, 9, 0.25) 0%, rgba(255, 255, 255, 0.00) 100%)',
}}
></div>
<RiAlertFill className='mr-1.5 w-4 h-4 text-text-warning-secondary' />
<div className='system-xs-medium text-text-primary'>
There is circular invocation of tools/nodes in the current workflow.
</div>
</div>
)
}
</div>
)
}

View File

@ -33,7 +33,7 @@ export const useLogs = () => {
setIterationResultDurationMap(iterDurationMap)
}, [setShowIteratingDetailTrue, setIterationResultList, setIterationResultDurationMap])
const [agentOrToolLogItemStack, setAgentOrToolLogItemStack] = useState<{ id: string; label: string }[]>([])
const [agentOrToolLogItemStack, setAgentOrToolLogItemStack] = useState<AgentLogItemWithChildren[]>([])
const agentOrToolLogItemStackRef = useRef(agentOrToolLogItemStack)
const [agentOrToolLogListMap, setAgentOrToolLogListMap] = useState<Record<string, AgentLogItemWithChildren[]>>({})
const agentOrToolLogListMapRef = useRef(agentOrToolLogListMap)
@ -43,14 +43,14 @@ export const useLogs = () => {
agentOrToolLogItemStackRef.current = []
return
}
const { id, label, children } = detail
const { id, children } = detail
let currentAgentOrToolLogItemStack = agentOrToolLogItemStackRef.current.slice()
const index = currentAgentOrToolLogItemStack.findIndex(logItem => logItem.id === id)
if (index > -1)
currentAgentOrToolLogItemStack = currentAgentOrToolLogItemStack.slice(0, index + 1)
else
currentAgentOrToolLogItemStack = [...currentAgentOrToolLogItemStack.slice(), { id, label }]
currentAgentOrToolLogItemStack = [...currentAgentOrToolLogItemStack.slice(), detail]
setAgentOrToolLogItemStack(currentAgentOrToolLogItemStack)
agentOrToolLogItemStackRef.current = currentAgentOrToolLogItemStack

View File

@ -81,6 +81,7 @@ const NodePanel: FC<Props> = ({
const isIterationNode = nodeInfo.node_type === BlockEnum.Iteration
const isRetryNode = hasRetryNode(nodeInfo.node_type) && nodeInfo.retryDetail
const isAgentNode = nodeInfo.node_type === BlockEnum.Agent
const isToolNode = nodeInfo.node_type === BlockEnum.Tool
return (
<div className={cn('px-2 py-1', className)}>
@ -144,7 +145,7 @@ const NodePanel: FC<Props> = ({
/>
)}
{
isAgentNode && onShowAgentOrToolLog && (
(isAgentNode || isToolNode) && onShowAgentOrToolLog && (
<AgentLogTrigger
nodeInfo={nodeInfo}
onShowAgentOrToolLog={onShowAgentOrToolLog}

View File

@ -60,6 +60,7 @@ const ResultPanel: FC<ResultPanelProps> = ({
const isIterationNode = nodeInfo?.node_type === BlockEnum.Iteration
const isRetryNode = hasRetryNode(nodeInfo?.node_type) && nodeInfo?.retryDetail
const isAgentNode = nodeInfo?.node_type === BlockEnum.Agent
const isToolNode = nodeInfo?.node_type === BlockEnum.Tool
return (
<div className='bg-components-panel-bg py-2'>
@ -90,7 +91,7 @@ const ResultPanel: FC<ResultPanelProps> = ({
)
}
{
isAgentNode && handleShowAgentOrToolLog && (
(isAgentNode || isToolNode) && handleShowAgentOrToolLog && (
<AgentLogTrigger
nodeInfo={nodeInfo}
onShowAgentOrToolLog={handleShowAgentOrToolLog}

View File

@ -17,7 +17,7 @@ export type SpecialResultPanelProps = {
iterationResultList?: NodeTracing[][]
iterationResultDurationMap?: IterationDurationMap
agentOrToolLogItemStack?: { id: string; label: string }[]
agentOrToolLogItemStack?: AgentLogItemWithChildren[]
agentOrToolLogListMap?: Record<string, AgentLogItemWithChildren[]>
handleShowAgentOrToolLog?: (detail?: AgentLogItemWithChildren) => void
}

View File

@ -1,4 +1,5 @@
import { BlockEnum } from '@/app/components/workflow/types'
import { has } from 'immer/dist/internal'
export const agentNodeData = (() => {
const node = {
@ -89,3 +90,95 @@ export const agentNodeData = (() => {
}],
}
})()
export const oneStepCircle = (() => {
const node = {
node_type: BlockEnum.Agent,
execution_metadata: {
agent_log: [
{ id: '1', label: 'Node 1' },
{ id: '1', parent_id: '1', label: 'Node 1' },
{ id: '1', parent_id: '1', label: 'Node 1' },
{ id: '1', parent_id: '1', label: 'Node 1' },
{ id: '1', parent_id: '1', label: 'Node 1' },
{ id: '1', parent_id: '1', label: 'Node 1' },
],
},
}
return {
in: [node],
expect: [{
...node,
agentLog: [
{
id: '1',
label: 'Node 1',
hasCircle: true,
children: [],
},
],
}],
}
})()
export const multiStepsCircle = (() => {
const node = {
node_type: BlockEnum.Agent,
execution_metadata: {
agent_log: [
// 1 -> [2 -> 4 -> 1, 3]
{ id: '1', label: 'Node 1' },
{ id: '2', parent_id: '1', label: 'Node 2' },
{ id: '3', parent_id: '1', label: 'Node 3' },
{ id: '4', parent_id: '2', label: 'Node 4' },
// Loop
{ id: '1', parent_id: '4', label: 'Node 1' },
{ id: '2', parent_id: '1', label: 'Node 2' },
{ id: '4', parent_id: '2', label: 'Node 4' },
// { id: '1', parent_id: '4', label: 'Node 1' },
// { id: '2', parent_id: '1', label: 'Node 2' },
// { id: '4', parent_id: '2', label: 'Node 4' },
],
},
}
return {
in: [node],
expect: [{
...node,
agentLog: [
{
id: '1',
label: 'Node 1',
children: [
{
id: '2',
parent_id: '1',
label: 'Node 2',
children: [
{
id: '4',
parent_id: '2',
label: 'Node 4',
children: [],
hasCircle: true,
}
],
},
{
id: '3',
parent_id: '1',
label: 'Node 3',
},
],
},
],
}],
}
})()
export const CircleNestCircle = (() => {
})()

View File

@ -1,9 +1,16 @@
import exp from 'constants'
import format from '.'
import { agentNodeData } from './data'
import { agentNodeData, oneStepCircle, multiStepsCircle } from './data'
describe('agent', () => {
test('list should transform to tree', () => {
// console.log(format(agentNodeData.in as any))
expect(format(agentNodeData.in as any)).toEqual(agentNodeData.expect)
})
test('list should remove circle log item', () => {
// format(oneStepCircle.in as any)
expect(format(oneStepCircle.in as any)).toEqual(oneStepCircle.expect)
expect(format(multiStepsCircle.in as any)).toEqual(multiStepsCircle.expect)
})
})

View File

@ -1,8 +1,62 @@
import { BlockEnum } from '@/app/components/workflow/types'
import type { AgentLogItem, AgentLogItemWithChildren, NodeTracing } from '@/types/workflow'
import { cloneDeep } from 'lodash-es'
const supportedAgentLogNodes = [BlockEnum.Agent, BlockEnum.Tool]
const remove = (node: AgentLogItemWithChildren, removeId: string) => {
const { children } = node
if (!children || children.length === 0) {
return
}
children.forEach((child, index) => {
if (child.id === removeId) {
node.hasCircle = true
children.splice(index, 1)
return
}
remove(child, removeId)
})
}
const removeRepeatedSiblings = (list: AgentLogItemWithChildren[]) => {
if (!list || list.length === 0) {
return []
}
const result: AgentLogItemWithChildren[] = []
const addedItemIds: string[] = []
list.forEach((item) => {
if (!addedItemIds.includes(item.id)) {
result.push(item)
addedItemIds.push(item.id)
}
})
return result
}
const removeCircleLogItem = (log: AgentLogItemWithChildren) => {
let newLog = cloneDeep(log)
newLog.children = removeRepeatedSiblings(newLog.children)
let { id, children } = newLog
if (!children || children.length === 0) {
return log
}
// check one step circle
const hasOneStepCircle = !!children.find(c => c.id === id)
if (hasOneStepCircle) {
newLog.hasCircle = true
newLog.children = newLog.children.filter(c => c.id !== id)
children = newLog.children
}
children.forEach((child, index) => {
remove(child, id) // check multi steps circle
children[index] = removeCircleLogItem(child)
})
return newLog
}
const listToTree = (logs: AgentLogItem[]) => {
if (!logs || logs.length === 0)
return []
@ -24,10 +78,15 @@ const listToTree = (logs: AgentLogItem[]) => {
})
return tree
}
const format = (list: NodeTracing[]): NodeTracing[] => {
const result: NodeTracing[] = list.map((item) => {
let treeList: AgentLogItemWithChildren[] = []
let removedCircleTree: AgentLogItemWithChildren[] = []
if (supportedAgentLogNodes.includes(item.node_type) && item.execution_metadata?.agent_log && item.execution_metadata?.agent_log.length > 0)
item.agentLog = listToTree(item.execution_metadata.agent_log)
treeList = listToTree(item.execution_metadata.agent_log)
removedCircleTree = treeList.length > 0 ? treeList.map(t => removeCircleLogItem(t)) : []
item.agentLog = removedCircleTree
return item
})

View File

@ -126,6 +126,8 @@ const translation = {
Custom: 'Custom',
},
addMoreModel: 'Go to settings to add more models',
settingsLink: 'Model Provider Settings',
capabilities: 'MultiModal Capabilities',
},
menus: {
status: 'beta',

View File

@ -716,7 +716,7 @@ const translation = {
},
modelNotInMarketplace: {
title: 'Model not installed',
desc: 'This model is not installed from the marketplace. Please go to Plugins to reinstall.',
desc: 'This model is installed from Local or GitHub repository. Please use after installation.',
manageInPlugins: 'Manage in Plugins',
},
configureModel: 'Configure Model',

View File

@ -126,6 +126,8 @@ const translation = {
Custom: '自定义',
},
addMoreModel: '添加更多模型',
settingsLink: '模型设置',
capabilities: '多模态能力',
},
menus: {
status: 'beta',

View File

@ -716,7 +716,7 @@ const translation = {
},
modelNotInMarketplace: {
title: '模型未安装',
desc: '此模型未从市场安装。请转到插件重新安装。',
desc: '此模型安装自本地或 GitHub 仓库。请安装后使用。',
manageInPlugins: '在插件中管理',
},
model: '模型',

View File

@ -20,6 +20,7 @@ export type AgentLogItem = {
}
export type AgentLogItemWithChildren = AgentLogItem & {
hasCircle?: boolean
children: AgentLogItemWithChildren[]
}