mirror of
https://github.com/langgenius/dify.git
synced 2026-04-13 22:57:26 +08:00
Merge branch 'feat/plugins' into dev/plugin-deploy
This commit is contained in:
commit
a66d92054a
@ -3,7 +3,7 @@ import { RiInstallLine, RiLoader2Line } from '@remixicon/react'
|
||||
|
||||
type InstallButtonProps = {
|
||||
loading: boolean
|
||||
onInstall: () => void
|
||||
onInstall: (e: React.MouseEvent) => void
|
||||
t: any
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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,
|
||||
)}
|
||||
>
|
||||
|
||||
@ -104,6 +104,7 @@ const ModelSelector: FC<ModelSelectorProps> = ({
|
||||
modelList={modelList}
|
||||
onSelect={handleSelect}
|
||||
scopeFeatures={scopeFeatures}
|
||||
onHide={() => setOpen(false)}
|
||||
/>
|
||||
</PortalToFollowElemContent>
|
||||
</div>
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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>
|
||||
{
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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'>
|
||||
|
||||
@ -392,6 +392,12 @@ export type StrategyParamItem = {
|
||||
required: boolean
|
||||
default: any
|
||||
options: any[]
|
||||
template: {
|
||||
enabled: boolean
|
||||
},
|
||||
auto_generate: {
|
||||
type: string
|
||||
}
|
||||
}
|
||||
|
||||
export type StrategyDetail = {
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -68,6 +68,7 @@ const Tabs: FC<TabsProps> = ({
|
||||
{
|
||||
activeTab === TabsEnum.Tools && (
|
||||
<AllTools
|
||||
className='w-[315px]'
|
||||
searchText={searchText}
|
||||
onSelect={onSelect}
|
||||
tags={tags}
|
||||
|
||||
@ -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 >
|
||||
)
|
||||
|
||||
@ -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)
|
||||
}}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
}
|
||||
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 = ({
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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 = (() => {
|
||||
})()
|
||||
|
||||
@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@ -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
|
||||
})
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -126,6 +126,8 @@ const translation = {
|
||||
Custom: '自定义',
|
||||
},
|
||||
addMoreModel: '添加更多模型',
|
||||
settingsLink: '模型设置',
|
||||
capabilities: '多模态能力',
|
||||
},
|
||||
menus: {
|
||||
status: 'beta',
|
||||
|
||||
@ -716,7 +716,7 @@ const translation = {
|
||||
},
|
||||
modelNotInMarketplace: {
|
||||
title: '模型未安装',
|
||||
desc: '此模型未从市场安装。请转到插件重新安装。',
|
||||
desc: '此模型安装自本地或 GitHub 仓库。请安装后使用。',
|
||||
manageInPlugins: '在插件中管理',
|
||||
},
|
||||
model: '模型',
|
||||
|
||||
@ -20,6 +20,7 @@ export type AgentLogItem = {
|
||||
}
|
||||
|
||||
export type AgentLogItemWithChildren = AgentLogItem & {
|
||||
hasCircle?: boolean
|
||||
children: AgentLogItemWithChildren[]
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user