feat: model-selector in Agent node (case: installed models)

This commit is contained in:
Yi 2024-12-27 13:57:54 +08:00
parent 67019d128b
commit ef3e904839
15 changed files with 285 additions and 67 deletions

View File

@ -36,6 +36,10 @@ const iconClassName = `
w-5 h-5 mr-2 w-5 h-5 mr-2
` `
const scrolledClassName = `
border-b shadow-xs bg-white/[.98]
`
type IAccountSettingProps = { type IAccountSettingProps = {
onCancel: () => void onCancel: () => void
activeTab?: string activeTab?: string

View File

@ -5,32 +5,37 @@ import type {
} from '../declarations' } from '../declarations'
import { useLanguage } from '../hooks' import { useLanguage } from '../hooks'
import { CubeOutline } from '@/app/components/base/icons/src/vender/line/shapes' import { CubeOutline } from '@/app/components/base/icons/src/vender/line/shapes'
import { OpenaiViolet } from '@/app/components/base/icons/src/public/llm' import { OpenaiBlue, OpenaiViolet } from '@/app/components/base/icons/src/public/llm'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
type ModelIconProps = { type ModelIconProps = {
provider?: Model | ModelProvider provider?: Model | ModelProvider
modelName?: string modelName?: string
className?: string className?: string
isDeprecated?: boolean
} }
const ModelIcon: FC<ModelIconProps> = ({ const ModelIcon: FC<ModelIconProps> = ({
provider, provider,
className, className,
modelName, modelName,
isDeprecated = false,
}) => { }) => {
const language = useLanguage() const language = useLanguage()
if (provider?.provider.includes('openai') && modelName?.includes('gpt-4o'))
if (provider?.provider.includes('openai') && (modelName?.startsWith('gpt-4') || modelName?.includes('4o'))) return <OpenaiBlue className={cn('w-5 h-5', className)}/>
return <OpenaiViolet className={cn('w-4 h-4', className)}/> if (provider?.provider.includes('openai') && modelName?.startsWith('gpt-4'))
return <OpenaiViolet className={cn('w-5 h-5', className)}/>
if (provider?.icon_small) { if (provider?.icon_small) {
return ( return (
// eslint-disable-next-line @next/next/no-img-element
<div className={isDeprecated ? 'opacity-50' : ''}>
<img <img
alt='model-icon' alt='model-icon'
src={`${provider.icon_small[language] || provider.icon_small.en_US}`} src={`${provider.icon_small[language] || provider.icon_small.en_US}`}
className={cn('w-4 h-4', className)} className={cn('w-4 h-4', className)}
/> />
</div>
) )
} }

View File

@ -35,6 +35,7 @@ type FormProps<
validatedSuccess?: boolean validatedSuccess?: boolean
showOnVariableMap: Record<string, string[]> showOnVariableMap: Record<string, string[]>
isEditMode: boolean isEditMode: boolean
isAgentStrategy?: boolean
readonly?: boolean readonly?: boolean
inputClassName?: string inputClassName?: string
isShowDefaultValue?: boolean isShowDefaultValue?: boolean
@ -60,6 +61,7 @@ function Form<
validatedSuccess, validatedSuccess,
showOnVariableMap, showOnVariableMap,
isEditMode, isEditMode,
isAgentStrategy = false,
readonly, readonly,
inputClassName, inputClassName,
isShowDefaultValue = false, isShowDefaultValue = false,
@ -278,6 +280,7 @@ function Form<
popupClassName='!w-[387px]' popupClassName='!w-[387px]'
isAdvancedMode isAdvancedMode
isInWorkflow isInWorkflow
isAgentStrategy={isAgentStrategy}
value={value[variable]} value={value[variable]}
setModel={model => handleModelChanged(variable, model)} setModel={model => handleModelChanged(variable, model)}
readonly={readonly} readonly={readonly}

View File

@ -37,23 +37,24 @@ const ModelName: FC<ModelNameProps> = ({
if (!modelItem) if (!modelItem)
return null return null
return ( return (
<div className={cn('flex items-center truncate text-components-input-text-filled system-sm-regular', className)}> <div className={cn('flex items-center overflow-hidden text-ellipsis truncate text-components-input-text-filled system-sm-regular', className)}>
<div <div
className='truncate' className='truncate'
title={modelItem.label[language] || modelItem.label.en_US} title={modelItem.label[language] || modelItem.label.en_US}
> >
{modelItem.label[language] || modelItem.label.en_US} {modelItem.label[language] || modelItem.label.en_US}
</div> </div>
<div className='flex items-center gap-0.5'>
{ {
showModelType && modelItem.model_type && ( showModelType && modelItem.model_type && (
<ModelBadge className={cn('ml-1', modelTypeClassName)}> <ModelBadge className={modelTypeClassName}>
{modelTypeFormat(modelItem.model_type)} {modelTypeFormat(modelItem.model_type)}
</ModelBadge> </ModelBadge>
) )
} }
{ {
modelItem.model_properties.mode && showMode && ( modelItem.model_properties.mode && showMode && (
<ModelBadge className={cn('ml-1', modeClassName)}> <ModelBadge className={modeClassName}>
{(modelItem.model_properties.mode as string).toLocaleUpperCase()} {(modelItem.model_properties.mode as string).toLocaleUpperCase()}
</ModelBadge> </ModelBadge>
) )
@ -63,17 +64,18 @@ const ModelName: FC<ModelNameProps> = ({
<FeatureIcon <FeatureIcon
key={feature} key={feature}
feature={feature} feature={feature}
className={cn('ml-1', featuresClassName)} className={featuresClassName}
/> />
)) ))
} }
{ {
showContextSize && modelItem.model_properties.context_size && ( showContextSize && modelItem.model_properties.context_size && (
<ModelBadge className='ml-1'> <ModelBadge>
{sizeFormat(modelItem.model_properties.context_size as number)} {sizeFormat(modelItem.model_properties.context_size as number)}
</ModelBadge> </ModelBadge>
) )
} }
</div>
{children} {children}
</div> </div>
) )

View File

@ -0,0 +1,180 @@
import type { FC } from 'react'
import { useTranslation } from 'react-i18next'
import type {
CustomConfigurationModelFixedFields,
ModelItem,
ModelProvider,
} from '../declarations'
import {
ConfigurationMethodEnum,
CustomConfigurationStatusEnum,
} from '../declarations'
import { UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST } from '../provider-added-card'
import { ModelStatusEnum } from '../declarations'
import {
useUpdateModelList,
useUpdateModelProviders,
} from '../hooks'
import ModelIcon from '../model-icon'
import ModelName from '../model-name'
import Button from '@/app/components/base/button'
import cn from '@/utils/classnames'
import { useProviderContext } from '@/context/provider-context'
import { useModalContextSelector } from '@/context/modal-context'
import { useEventEmitterContextContext } from '@/context/event-emitter'
import Tooltip from '@/app/components/base/tooltip'
import { RiEqualizer2Line, RiErrorWarningFill } from '@remixicon/react'
export type AgentModelTriggerProps = {
open?: boolean
disabled?: boolean
currentProvider?: ModelProvider
currentModel?: ModelItem
providerName?: string
modelId?: string
hasDeprecated?: boolean
}
const AgentModelTrigger: FC<AgentModelTriggerProps> = ({
disabled,
currentProvider,
currentModel,
providerName,
modelId,
hasDeprecated,
}) => {
const { t } = useTranslation()
const { modelProviders } = useProviderContext()
const setShowModelModal = useModalContextSelector(state => state.setShowModelModal)
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 handleOpenModal = (
provider: ModelProvider,
configurationMethod: ConfigurationMethodEnum,
CustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields,
) => {
setShowModelModal({
payload: {
currentProvider: provider,
currentConfigurationMethod: configurationMethod,
currentCustomConfigurationModelFixedFields: CustomConfigurationModelFixedFields,
},
onSaveCallback: () => {
updateModelProviders()
provider.supported_model_types.forEach((type) => {
updateModelList(type)
})
if (configurationMethod === ConfigurationMethodEnum.customizableModel
&& provider.custom_configuration.status === CustomConfigurationStatusEnum.active) {
eventEmitter?.emit({
type: UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST,
payload: provider.provider,
} as any)
if (CustomConfigurationModelFixedFields?.__model_type)
updateModelList(CustomConfigurationModelFixedFields.__model_type)
}
},
})
}
return (
<div
className={cn(
'relative group flex items-center p-1 gap-[2px] flex-grow rounded-lg bg-components-input-bg-normal cursor-pointer hover:bg-state-base-hover-alt',
)}
>
{modelId ? (
<>
{currentProvider && (
<ModelIcon
className="m-0.5"
provider={currentProvider}
modelName={currentModel?.model}
isDeprecated={hasDeprecated}
/>
)}
{!currentProvider && (
<ModelIcon
className="m-0.5"
provider={modelProvider}
modelName={modelId}
isDeprecated={hasDeprecated}
/>
)}
{currentModel && (
<ModelName
className="flex px-1 py-[3px] items-center gap-1 grow"
modelItem={currentModel}
showMode
showFeatures
/>
)}
{!currentModel && (
<div className="flex py-[3px] px-1 items-center gap-1 grow opacity-50 truncate">
<div className="text-components-input-text-filled text-ellipsis overflow-hidden system-sm-regular">
{modelId}
</div>
</div>
)}
{needsConfiguration && (
<Button
size="small"
className="z-[100]"
onClick={(e) => {
e.stopPropagation()
handleOpenModal(modelProvider, ConfigurationMethodEnum.predefinedModel, undefined)
}}
>
<div className="flex px-[3px] justify-center items-center gap-1">
{t('workflow.nodes.agent.notAuthorized')}
</div>
<div className="flex w-[14px] h-[14px] justify-center items-center">
<div className="w-2 h-2 shrink-0 rounded-[3px] border border-components-badge-status-light-warning-border-inner
bg-components-badge-status-light-warning-bg shadow-components-badge-status-light-warning-halo" />
</div>
</Button>
)}
{!needsConfiguration && disabled && (
<Tooltip
popupContent={t('workflow.nodes.agent.modelSelectorTooltips.deprecated')}
asChild={false}
>
<RiErrorWarningFill className='w-4 h-4 text-text-destructive' />
</Tooltip>
)
}
</>
) : (
<>
<div className="flex p-1 pl-2 items-center gap-1 grow">
<span className="overflow-hidden text-ellipsis whitespace-nowrap system-sm-regular text-components-input-text-placeholder">
{t('workflow.nodes.agent.configureModel')}
</span>
</div>
<div className="flex pr-1 items-center">
<RiEqualizer2Line className="w-4 h-4 text-text-tertiary group-hover:text-text-secondary" />
</div>
</>
)}
{currentProvider && currentModel && currentModel.status === ModelStatusEnum.active && (
<div className="flex pr-1 items-center">
<RiEqualizer2Line className="w-4 h-4 text-text-tertiary group-hover:text-text-secondary" />
</div>
)}
</div>
)
}
export default AgentModelTrigger

View File

@ -77,14 +77,15 @@ const PopupItem: FC<PopupItemProps> = ({
<div <div
key={modelItem.model} key={modelItem.model}
className={` className={`
group relative flex items-center px-3 py-1.5 h-8 rounded-lg 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'} ${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)} onClick={() => handleSelect(model.provider, modelItem)}
> >
<div className='flex items-center gap-2'>
<ModelIcon <ModelIcon
className={` className={`
shrink-0 mr-2 w-4 h-4 shrink-0 w-4 h-4
${modelItem.status !== ModelStatusEnum.active && 'opacity-60'} ${modelItem.status !== ModelStatusEnum.active && 'opacity-60'}
`} `}
provider={model} provider={model}
@ -92,13 +93,14 @@ const PopupItem: FC<PopupItemProps> = ({
/> />
<ModelName <ModelName
className={` className={`
grow text-sm font-normal text-text-primary text-text-secondary system-sm-medium
${modelItem.status !== ModelStatusEnum.active && 'opacity-60'} ${modelItem.status !== ModelStatusEnum.active && 'opacity-60'}
`} `}
modelItem={modelItem} modelItem={modelItem}
showMode showMode
showFeatures showFeatures
/> />
</div>
{ {
defaultModel?.model === modelItem.model && defaultModel.provider === currentProvider.provider && ( defaultModel?.model === modelItem.model && defaultModel.provider === currentProvider.provider && (
<Check className='shrink-0 w-4 h-4 text-text-accent' /> <Check className='shrink-0 w-4 h-4 text-text-accent' />

View File

@ -44,7 +44,7 @@ const Card = ({
const locale = localeFromProps ? getLanguage(localeFromProps) : defaultLocale const locale = localeFromProps ? getLanguage(localeFromProps) : defaultLocale
const { categoriesMap } = useCategories() const { categoriesMap } = useCategories()
const { category, type, name, org, label, brief, icon, verified } = payload const { category, type, name, org, label, brief, icon, verified } = payload
const isBundle = !['plugin', 'model', 'tool', 'extension', 'agent-strategy'].includes(type) const isBundle = !['plugin', 'model', 'tool', 'extension', 'agent_strategy'].includes(type)
const cornerMark = isBundle ? categoriesMap.bundle?.label : categoriesMap[category]?.label const cornerMark = isBundle ? categoriesMap.bundle?.label : categoriesMap[category]?.label
const getLocalizedText = (obj: Record<string, string> | undefined) => const getLocalizedText = (obj: Record<string, string> | undefined) =>
obj?.[locale] || obj?.['en-US'] || obj?.en_US || '' obj?.[locale] || obj?.['en-US'] || obj?.en_US || ''

View File

@ -44,7 +44,7 @@ export const useCategories = (translateFromOut?: TFunction) => {
const categories = categoryKeys.map((category) => { const categories = categoryKeys.map((category) => {
if (category === 'agent') { if (category === 'agent') {
return { return {
name: 'agent-strategy', name: 'agent_strategy',
label: t(`plugin.category.${category}s`), label: t(`plugin.category.${category}s`),
} }
} }

View File

@ -102,7 +102,7 @@ export const getMarketplaceListCondition = (pluginType: string) => {
return 'category=tool' return 'category=tool'
if (pluginType === PluginType.agent) if (pluginType === PluginType.agent)
return 'category=agent-strategy' return 'category=agent_strategy'
if (pluginType === PluginType.model) if (pluginType === PluginType.model)
return 'category=model' return 'category=model'

View File

@ -13,6 +13,7 @@ import ModelSelector from '@/app/components/header/account-setting/model-provide
import { import {
useModelList, useModelList,
} from '@/app/components/header/account-setting/model-provider-page/hooks' } from '@/app/components/header/account-setting/model-provider-page/hooks'
import AgentModelTrigger from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal/agent-model-trigger'
import Trigger from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal/trigger' import Trigger from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal/trigger'
import type { TriggerProps } from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal/trigger' import type { TriggerProps } from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal/trigger'
import { import {
@ -34,6 +35,7 @@ export type ModelParameterModalProps = {
renderTrigger?: (v: TriggerProps) => ReactNode renderTrigger?: (v: TriggerProps) => ReactNode
readonly?: boolean readonly?: boolean
isInWorkflow?: boolean isInWorkflow?: boolean
isAgentStrategy?: boolean
scope?: string scope?: string
} }
@ -46,6 +48,7 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
renderTrigger, renderTrigger,
readonly, readonly,
isInWorkflow, isInWorkflow,
isAgentStrategy,
scope = ModelTypeEnum.textGeneration, scope = ModelTypeEnum.textGeneration,
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
@ -168,8 +171,16 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
providerName: value?.provider, providerName: value?.provider,
modelId: value?.model, modelId: value?.model,
}) })
: ( : (isAgentStrategy
<Trigger ? <AgentModelTrigger
disabled={disabled}
hasDeprecated={hasDeprecated}
currentProvider={currentProvider}
currentModel={currentModel}
providerName={value?.provider}
modelId={value?.model}
/>
: <Trigger
disabled={disabled} disabled={disabled}
isInWorkflow={isInWorkflow} isInWorkflow={isInWorkflow}
modelDisabled={modelDisabled} modelDisabled={modelDisabled}

View File

@ -6,7 +6,7 @@ export enum PluginType {
tool = 'tool', tool = 'tool',
model = 'model', model = 'model',
extension = 'extension', extension = 'extension',
agent = 'agent-strategy', agent = 'agent_strategy',
} }
export enum PluginSource { export enum PluginSource {
@ -109,7 +109,7 @@ export type PluginDetail = {
} }
export type Plugin = { export type Plugin = {
type: 'plugin' | 'bundle' | 'model' | 'extension' | 'tool' type: 'plugin' | 'bundle' | 'model' | 'extension' | 'tool' | 'agent_strategy'
org: string org: string
author?: string author?: string
name: string name: string

View File

@ -180,6 +180,7 @@ export const AgentStrategy = (props: AgentStrategyProps) => {
validating={false} validating={false}
showOnVariableMap={{}} showOnVariableMap={{}}
isEditMode={true} isEditMode={true}
isAgentStrategy={true}
fieldLabelClassName='uppercase' fieldLabelClassName='uppercase'
customRenderField={renderField} customRenderField={renderField}
/> />

View File

@ -714,6 +714,8 @@ const translation = {
install: 'Install', install: 'Install',
installing: 'Installing', installing: 'Installing',
}, },
configureModel: 'Configure Model',
notAuthorized: 'Not Authorized',
model: 'model', model: 'model',
toolbox: 'toolbox', toolbox: 'toolbox',
strategyNotSet: 'Agentic strategy Not Set', strategyNotSet: 'Agentic strategy Not Set',
@ -723,6 +725,9 @@ const translation = {
toolNotInstallTooltip: '{{tool}} is not installed', toolNotInstallTooltip: '{{tool}} is not installed',
toolNotAuthorizedTooltip: '{{tool}} Not Authorized', toolNotAuthorizedTooltip: '{{tool}} Not Authorized',
strategyNotInstallTooltip: '{{strategy}} is not installed', strategyNotInstallTooltip: '{{strategy}} is not installed',
modelSelectorTooltips: {
deprecated: 'This model is deprecated',
},
}, },
}, },
tracing: { tracing: {

View File

@ -717,12 +717,17 @@ const translation = {
model: '模型', model: '模型',
toolbox: '工具箱', toolbox: '工具箱',
strategyNotSet: '代理策略未设置', strategyNotSet: '代理策略未设置',
configureModel: '配置模型',
notAuthorized: '未授权',
tools: '工具', tools: '工具',
maxIterations: '最大迭代次数', maxIterations: '最大迭代次数',
modelNotInstallTooltip: '此模型未安装', modelNotInstallTooltip: '此模型未安装',
toolNotInstallTooltip: '{{tool}} 未安装', toolNotInstallTooltip: '{{tool}} 未安装',
toolNotAuthorizedTooltip: '{{tool}} 未授权', toolNotAuthorizedTooltip: '{{tool}} 未授权',
strategyNotInstallTooltip: '{{strategy}} 未安装', strategyNotInstallTooltip: '{{strategy}} 未安装',
modelSelectorTooltips: {
deprecated: '此模型已弃用',
},
}, },
}, },
tracing: { tracing: {

View File

@ -7,7 +7,7 @@ import {
useQuery, useQuery,
} from '@tanstack/react-query' } from '@tanstack/react-query'
const NAME_SPACE = 'agent-strategy' const NAME_SPACE = 'agent_strategy'
const useStrategyListKey = [NAME_SPACE, 'strategyList'] const useStrategyListKey = [NAME_SPACE, 'strategyList']
export const useStrategyProviders = () => { export const useStrategyProviders = () => {