Feat: tool setting support variable (#13465)

Co-authored-by: zxhlyh <jasonapring2015@outlook.com>
This commit is contained in:
KVOJJJin
2025-02-12 12:54:10 +08:00
committed by GitHub
parent 824f8d8994
commit 2ea3b64a45
26 changed files with 620 additions and 79 deletions

View File

@ -10,6 +10,8 @@ import ActionButton from '@/app/components/base/action-button'
import Tooltip from '@/app/components/base/tooltip'
import Divider from '@/app/components/base/divider'
import type { ToolValue } from '@/app/components/workflow/block-selector/types'
import type { Node } from 'reactflow'
import type { NodeOutPutVar } from '@/app/components/workflow/types'
import cn from '@/utils/classnames'
type Props = {
@ -21,6 +23,9 @@ type Props = {
supportCollapse?: boolean
scope?: string
onChange: (value: ToolValue[]) => void
nodeOutputVars: NodeOutPutVar[],
availableNodes: Node[],
nodeId?: string
}
const MultipleToolSelector = ({
@ -32,6 +37,9 @@ const MultipleToolSelector = ({
supportCollapse,
scope,
onChange,
nodeOutputVars,
availableNodes,
nodeId,
}: Props) => {
const { t } = useTranslation()
const enabledCount = value.filter(item => item.enabled).length
@ -121,6 +129,9 @@ const MultipleToolSelector = ({
{!collapse && (
<>
<ToolSelector
nodeId={nodeId}
nodeOutputVars={nodeOutputVars}
availableNodes={availableNodes}
scope={scope}
value={undefined}
selectedTools={value}
@ -140,6 +151,9 @@ const MultipleToolSelector = ({
{value.length > 0 && value.map((item, index) => (
<div className='mb-1' key={index}>
<ToolSelector
nodeId={nodeId}
nodeOutputVars={nodeOutputVars}
availableNodes={availableNodes}
scope={scope}
value={item}
selectedTools={value}

View File

@ -21,8 +21,10 @@ import ToolCredentialForm from '@/app/components/plugins/plugin-detail-panel/too
import Toast from '@/app/components/base/toast'
import Textarea from '@/app/components/base/textarea'
import Divider from '@/app/components/base/divider'
import TabSlider from '@/app/components/base/tab-slider-plain'
import ReasoningConfigForm from '@/app/components/plugins/plugin-detail-panel/tool-selector/reasoning-config-form'
import Form from '@/app/components/header/account-setting/model-provider-page/model-modal/Form'
import { addDefaultValue, toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
import { generateFormValue, getPlainValue, getStructureValue, toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
import { useAppContext } from '@/context/app-context'
import {
@ -41,6 +43,8 @@ import type {
Placement,
} from '@floating-ui/react'
import { MARKETPLACE_API_PREFIX } from '@/config'
import type { Node } from 'reactflow'
import type { NodeOutPutVar } from '@/app/components/workflow/types'
import cn from '@/utils/classnames'
type Props = {
@ -54,6 +58,7 @@ type Props = {
provider_name: string
tool_name: string
tool_label: string
settings?: Record<string, any>
parameters?: Record<string, any>
extra?: Record<string, any>
}) => void
@ -65,6 +70,9 @@ type Props = {
onControlledStateChange?: (state: boolean) => void
panelShowState?: boolean
onPanelShowStateChange?: (state: boolean) => void
nodeOutputVars: NodeOutPutVar[],
availableNodes: Node[],
nodeId?: string,
}
const ToolSelector: FC<Props> = ({
value,
@ -81,6 +89,9 @@ const ToolSelector: FC<Props> = ({
onControlledStateChange,
panelShowState,
onPanelShowStateChange,
nodeOutputVars,
availableNodes,
nodeId = '',
}) => {
const { t } = useTranslation()
const [isShow, onShowChange] = useState(false)
@ -107,17 +118,20 @@ const ToolSelector: FC<Props> = ({
const [isShowChooseTool, setIsShowChooseTool] = useState(false)
const handleSelectTool = (tool: ToolDefaultValue) => {
const paramValues = addDefaultValue(tool.params, toolParametersToFormSchemas(tool.paramSchemas.filter(param => param.form !== 'llm') as any))
const settingValues = generateFormValue(tool.params, toolParametersToFormSchemas(tool.paramSchemas.filter(param => param.form !== 'llm') as any))
const paramValues = generateFormValue(tool.params, toolParametersToFormSchemas(tool.paramSchemas.filter(param => param.form === 'llm') as any), true)
const toolValue = {
provider_name: tool.provider_id,
type: tool.provider_type,
tool_name: tool.tool_name,
tool_label: tool.tool_label,
settings: settingValues,
parameters: paramValues,
enabled: tool.is_team_authorization,
extra: {
description: '',
},
schemas: tool.paramSchemas,
}
onSelect(toolValue)
// setIsShowChooseTool(false)
@ -133,14 +147,33 @@ const ToolSelector: FC<Props> = ({
} as any)
}
const currentToolParams = useMemo(() => {
// tool settings & params
const currentToolSettings = useMemo(() => {
if (!currentProvider) return []
return currentProvider.tools.find(tool => tool.name === value?.tool_name)?.parameters.filter(param => param.form !== 'llm') || []
}, [currentProvider, value])
const currentToolParams = useMemo(() => {
if (!currentProvider) return []
return currentProvider.tools.find(tool => tool.name === value?.tool_name)?.parameters.filter(param => param.form === 'llm') || []
}, [currentProvider, value])
const [currType, setCurrType] = useState('settings')
const showTabSlider = currentToolSettings.length > 0 && currentToolParams.length > 0
const userSettingsOnly = currentToolSettings.length > 0 && !currentToolParams.length
const reasoningConfigOnly = currentToolParams.length > 0 && !currentToolSettings.length
const formSchemas = useMemo(() => toolParametersToFormSchemas(currentToolParams), [currentToolParams])
const settingsFormSchemas = useMemo(() => toolParametersToFormSchemas(currentToolSettings), [currentToolSettings])
const paramsFormSchemas = useMemo(() => toolParametersToFormSchemas(currentToolParams), [currentToolParams])
const handleFormChange = (v: Record<string, any>) => {
const handleSettingsFormChange = (v: Record<string, any>) => {
const newValue = getStructureValue(v)
const toolValue = {
...value,
settings: newValue,
}
onSelect(toolValue as any)
}
const handleParamsFormChange = (v: Record<string, any>) => {
const toolValue = {
...value,
parameters: v,
@ -281,12 +314,9 @@ const ToolSelector: FC<Props> = ({
</div>
{/* authorization */}
{currentProvider && currentProvider.type === CollectionType.builtIn && currentProvider.allow_delete && (
<div className='px-4 pt-3 flex flex-col'>
<div className='flex items-center gap-2'>
<div className='shrink-0 text-text-tertiary system-xs-medium-uppercase'>{t('plugin.detailPanel.toolSelector.auth')}</div>
<Divider bgStyle='gradient' className='grow' />
</div>
<div className='py-2'>
<>
<Divider className='my-1 w-full' />
<div className='px-4 py-2'>
{!currentProvider.is_team_authorization && (
<Button
variant='primary'
@ -309,37 +339,87 @@ const ToolSelector: FC<Props> = ({
</Button>
)}
</div>
</div>
</>
)}
{/* tool settings */}
{currentToolParams.length > 0 && currentProvider?.is_team_authorization && (
<div className='px-4 pt-3'>
<div className='flex items-center gap-2'>
<div className='shrink-0 text-text-tertiary system-xs-medium-uppercase'>{t('plugin.detailPanel.toolSelector.settings')}</div>
<Divider bgStyle='gradient' className='grow' />
</div>
<div className='py-2'>
<Form
value={value?.parameters || {}}
onChange={handleFormChange}
formSchemas={formSchemas as any}
isEditMode={true}
showOnVariableMap={{}}
validating={false}
inputClassName='bg-components-input-bg-normal hover:bg-components-input-bg-hover'
fieldMoreInfo={item => item.url
? (<a
href={item.url}
target='_blank' rel='noopener noreferrer'
className='inline-flex items-center text-xs text-text-accent'
>
{t('tools.howToGet')}
<RiArrowRightUpLine className='ml-1 w-3 h-3' />
</a>)
: null}
{(currentToolSettings.length > 0 || currentToolParams.length > 0) && currentProvider?.is_team_authorization && (
<>
<Divider className='my-1 w-full' />
{/* tabs */}
{nodeId && showTabSlider && (
<TabSlider
className='shrink-0 mt-1 px-4'
itemClassName='py-3'
noBorderBottom
smallItem
value={currType}
onChange={(value) => {
setCurrType(value)
}}
options={[
{ value: 'settings', text: t('plugin.detailPanel.toolSelector.settings')! },
{ value: 'params', text: t('plugin.detailPanel.toolSelector.params')! },
]}
/>
</div>
</div>
)}
{nodeId && showTabSlider && currType === 'params' && (
<div className='px-4 py-2'>
<div className='text-text-tertiary system-xs-regular'>{t('plugin.detailPanel.toolSelector.paramsTip1')}</div>
<div className='text-text-tertiary system-xs-regular'>{t('plugin.detailPanel.toolSelector.paramsTip2')}</div>
</div>
)}
{/* user settings only */}
{userSettingsOnly && (
<div className='p-4 pb-1'>
<div className='text-text-primary system-sm-semibold-uppercase'>{t('plugin.detailPanel.toolSelector.settings')}</div>
</div>
)}
{/* reasoning config only */}
{nodeId && reasoningConfigOnly && (
<div className='mb-1 p-4 pb-1'>
<div className='text-text-primary system-sm-semibold-uppercase'>{t('plugin.detailPanel.toolSelector.params')}</div>
<div className='pb-1'>
<div className='text-text-tertiary system-xs-regular'>{t('plugin.detailPanel.toolSelector.paramsTip1')}</div>
<div className='text-text-tertiary system-xs-regular'>{t('plugin.detailPanel.toolSelector.paramsTip2')}</div>
</div>
</div>
)}
{/* user settings form */}
{(currType === 'settings' || userSettingsOnly) && (
<div className='px-4 py-2'>
<Form
value={getPlainValue(value?.settings || {})}
onChange={handleSettingsFormChange}
formSchemas={settingsFormSchemas as any}
isEditMode={true}
showOnVariableMap={{}}
validating={false}
inputClassName='bg-components-input-bg-normal hover:bg-components-input-bg-hover'
fieldMoreInfo={item => item.url
? (<a
href={item.url}
target='_blank' rel='noopener noreferrer'
className='inline-flex items-center text-xs text-text-accent'
>
{t('tools.howToGet')}
<RiArrowRightUpLine className='ml-1 w-3 h-3' />
</a>)
: null}
/>
</div>
)}
{/* reasoning config form */}
{nodeId && (currType === 'params' || reasoningConfigOnly) && (
<ReasoningConfigForm
value={value?.parameters || {}}
onChange={handleParamsFormChange}
schemas={paramsFormSchemas as any}
nodeOutputVars={nodeOutputVars}
availableNodes={availableNodes}
nodeId={nodeId}
/>
)}
</>
)}
</>
)}

View File

@ -0,0 +1,275 @@
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import produce from 'immer'
import {
RiArrowRightUpLine,
} from '@remixicon/react'
import Tooltip from '@/app/components/base/tooltip'
import Switch from '@/app/components/base/switch'
import Input from '@/app/components/workflow/nodes/_base/components/input-support-select-var'
import VarReferencePicker from '@/app/components/workflow/nodes/_base/components/variable/var-reference-picker'
import AppSelector from '@/app/components/plugins/plugin-detail-panel/app-selector'
import ModelParameterModal from '@/app/components/plugins/plugin-detail-panel/model-selector'
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import type { Node } from 'reactflow'
import type {
NodeOutPutVar,
ValueSelector,
Var,
} from '@/app/components/workflow/types'
import type { ToolVarInputs } from '@/app/components/workflow/nodes/tool/types'
import { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types'
import { VarType } from '@/app/components/workflow/types'
import cn from '@/utils/classnames'
type Props = {
value: Record<string, any>
onChange: (val: Record<string, any>) => void
schemas: any[]
nodeOutputVars: NodeOutPutVar[],
availableNodes: Node[],
nodeId: string
}
const ReasoningConfigForm: React.FC<Props> = ({
value,
onChange,
schemas,
nodeOutputVars,
availableNodes,
nodeId,
}) => {
const { t } = useTranslation()
const language = useLanguage()
const handleAutomatic = (key: string, val: any) => {
onChange({
...value,
[key]: {
value: val ? null : value[key]?.value,
auto: val ? 1 : 0,
},
})
}
const [inputsIsFocus, setInputsIsFocus] = useState<Record<string, boolean>>({})
const handleInputFocus = useCallback((variable: string) => {
return (value: boolean) => {
setInputsIsFocus((prev) => {
return {
...prev,
[variable]: value,
}
})
}
}, [])
const handleNotMixedTypeChange = useCallback((variable: string) => {
return (varValue: ValueSelector | string, varKindType: VarKindType) => {
const newValue = produce(value, (draft: ToolVarInputs) => {
const target = draft[variable].value
if (target) {
target.type = varKindType
target.value = varValue
}
else {
draft[variable].value = {
type: varKindType,
value: varValue,
}
}
})
onChange(newValue)
}
}, [value, onChange])
const handleMixedTypeChange = useCallback((variable: string) => {
return (itemValue: string) => {
const newValue = produce(value, (draft: ToolVarInputs) => {
const target = draft[variable].value
if (target) {
target.value = itemValue
}
else {
draft[variable].value = {
type: VarKindType.mixed,
value: itemValue,
}
}
})
onChange(newValue)
}
}, [value, onChange])
const handleFileChange = useCallback((variable: string) => {
return (varValue: ValueSelector | string) => {
const newValue = produce(value, (draft: ToolVarInputs) => {
draft[variable].value = {
type: VarKindType.variable,
value: varValue,
}
})
onChange(newValue)
}
}, [value, onChange])
const handleAppChange = useCallback((variable: string) => {
return (app: {
app_id: string
inputs: Record<string, any>
files?: any[]
}) => {
const newValue = produce(value, (draft: ToolVarInputs) => {
draft[variable].value = app as any
})
onChange(newValue)
}
}, [onChange, value])
const handleModelChange = useCallback((variable: string) => {
return (model: any) => {
const newValue = produce(value, (draft: ToolVarInputs) => {
draft[variable].value = {
...draft[variable].value,
...model,
} as any
})
onChange(newValue)
}
}, [onChange, value])
const renderField = (schema: any) => {
const {
variable,
label,
required,
tooltip,
type,
scope,
url,
} = schema
const auto = value[variable]?.auto
const tooltipContent = (tooltip && (
<Tooltip
popupContent={<div className='w-[200px]'>
{tooltip[language] || tooltip.en_US}
</div>}
triggerClassName='ml-1 w-4 h-4'
asChild={false} />
))
const varInput = value[variable].value
const isNumber = type === FormTypeEnum.textNumber
const isSelect = type === FormTypeEnum.select
const isFile = type === FormTypeEnum.file || type === FormTypeEnum.files
const isAppSelector = type === FormTypeEnum.appSelector
const isModelSelector = type === FormTypeEnum.modelSelector
// const isToolSelector = type === FormTypeEnum.toolSelector
const isString = !isNumber && !isSelect && !isFile && !isAppSelector && !isModelSelector
return (
<div key={variable} className='space-y-1'>
<div className='flex items-center justify-between py-2 system-sm-semibold text-text-secondary'>
<div className='flex items-center space-x-2'>
<span className={cn('text-text-secondary code-sm-semibold')}>{label[language] || label.en_US}</span>
{required && (
<span className='ml-1 text-red-500'>*</span>
)}
{tooltipContent}
</div>
<div className='flex items-center gap-1 px-2 py-1 rounded-[6px] border border-divider-subtle bg-background-default-lighter cursor-pointer hover:bg-state-base-hover' onClick={() => handleAutomatic(variable, !auto)}>
<span className='text-text-secondary system-xs-medium'>{t('plugin.detailPanel.toolSelector.auto')}</span>
<Switch
size='xs'
defaultValue={!!auto}
onChange={val => handleAutomatic(variable, val)}
/>
</div>
</div>
{auto === 0 && (
<>
{isString && (
<Input
className={cn(inputsIsFocus[variable] ? 'shadow-xs bg-gray-50 border-gray-300' : 'bg-gray-100 border-gray-100', 'rounded-lg px-3 py-[6px] border')}
value={varInput?.value as string || ''}
onChange={handleMixedTypeChange(variable)}
nodesOutputVars={nodeOutputVars}
availableNodes={availableNodes}
onFocusChange={handleInputFocus(variable)}
placeholder={t('workflow.nodes.http.insertVarPlaceholder')!}
placeholderClassName='!leading-[21px]'
/>
)}
{/* {isString && (
<VarReferencePicker
zIndex={1001}
readonly={false}
isShowNodeName
nodeId={nodeId}
value={varInput?.value || ''}
onChange={handleNotMixedTypeChange(variable)}
defaultVarKindType={VarKindType.variable}
filterVar={(varPayload: Var) => varPayload.type === VarType.number || varPayload.type === VarType.secret || varPayload.type === VarType.string}
/>
)} */}
{(isNumber || isSelect) && (
<VarReferencePicker
zIndex={1001}
readonly={false}
isShowNodeName
nodeId={nodeId}
value={varInput?.type === VarKindType.constant ? (varInput?.value ?? '') : (varInput?.value ?? [])}
onChange={handleNotMixedTypeChange(variable)}
defaultVarKindType={varInput?.type || (isNumber ? VarKindType.constant : VarKindType.variable)}
isSupportConstantValue
filterVar={isNumber ? (varPayload: Var) => varPayload.type === schema._type : undefined}
availableVars={isSelect ? nodeOutputVars : undefined}
schema={schema}
/>
)}
{isFile && (
<VarReferencePicker
zIndex={1001}
readonly={false}
isShowNodeName
nodeId={nodeId}
value={varInput?.value || []}
onChange={handleFileChange(variable)}
defaultVarKindType={VarKindType.variable}
filterVar={(varPayload: Var) => varPayload.type === VarType.file || varPayload.type === VarType.arrayFile}
/>
)}
{isAppSelector && (
<AppSelector
disabled={false}
scope={scope || 'all'}
value={varInput as any}
onSelect={handleAppChange(variable)}
/>
)}
{isModelSelector && (
<ModelParameterModal
popupClassName='!w-[387px]'
isAdvancedMode
isInWorkflow
value={varInput as any}
setModel={handleModelChange(variable)}
scope={scope}
/>
)}
</>
)}
{url && (
<a
href={url}
target='_blank' rel='noopener noreferrer'
className='inline-flex items-center text-xs text-text-accent'
>
{t('tools.howToGet')}
<RiArrowRightUpLine className='ml-1 w-3 h-3' />
</a>
)}
</div>
)
}
return (
<div className='px-4 py-2 space-y-3'>
{schemas.map(schema => renderField(schema))}
</div>
)
}
export default ReasoningConfigForm