agent node

This commit is contained in:
JzoNg
2025-06-11 22:47:31 +08:00
parent 8af635459a
commit e3fcee124a
7 changed files with 232 additions and 185 deletions

View File

@ -5,7 +5,6 @@ import { useTranslation } from 'react-i18next'
import Link from 'next/link'
import {
RiArrowLeftLine,
RiArrowRightUpLine,
} from '@remixicon/react'
import {
PortalToFollowElem,
@ -15,6 +14,7 @@ import {
import ToolTrigger from '@/app/components/plugins/plugin-detail-panel/tool-selector/tool-trigger'
import ToolItem from '@/app/components/plugins/plugin-detail-panel/tool-selector/tool-item'
import ToolPicker from '@/app/components/workflow/block-selector/tool-picker'
import ToolForm from '@/app/components/workflow/nodes/tool/components/tool-form'
import Button from '@/app/components/base/button'
import Indicator from '@/app/components/header/indicator'
import ToolCredentialForm from '@/app/components/plugins/plugin-detail-panel/tool-selector/tool-credentials-form'
@ -23,8 +23,7 @@ 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 { generateFormValue, getPlainValue, getStructureValue, toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
import { generateFormValue, toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
import { useAppContext } from '@/context/app-context'
import {
@ -173,11 +172,9 @@ const ToolSelector: FC<Props> = ({
const paramsFormSchemas = useMemo(() => toolParametersToFormSchemas(currentToolParams), [currentToolParams])
const handleSettingsFormChange = (v: Record<string, any>) => {
const newValue = getStructureValue(v)
const toolValue = {
...value,
settings: newValue,
settings: v,
}
onSelect(toolValue as any)
}
@ -400,24 +397,12 @@ const ToolSelector: FC<Props> = ({
{/* user settings form */}
{(currType === 'settings' || userSettingsOnly) && (
<div className='px-4 py-2'>
<Form
value={getPlainValue(value?.settings || {})}
<ToolForm
readOnly={false}
nodeId={nodeId}
schema={settingsFormSchemas as any}
value={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 h-3 w-3' />
</a>)
: null}
/>
</div>
)}

View File

@ -7,17 +7,22 @@ import {
} 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 MixedInput from '@/app/components/workflow/nodes/_base/components/input-support-select-var'
import Input from '@/app/components/base/input'
import FormInputTypeSwitch from '@/app/components/workflow/nodes/_base/components/form-input-type-switch'
import FormInputBoolean from '@/app/components/workflow/nodes/_base/components/form-input-boolean'
import { SimpleSelect } from '@/app/components/base/select'
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
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 { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
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'
@ -46,14 +51,13 @@ const ReasoningConfigForm: React.FC<Props> = ({
}) => {
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 getVarKindType = (type: FormTypeEnum) => {
if (type === FormTypeEnum.file || type === FormTypeEnum.files)
return VarKindType.variable
if (type === FormTypeEnum.select || type === FormTypeEnum.boolean || type === FormTypeEnum.textNumber)
return VarKindType.constant
if (type === FormTypeEnum.textInput || type === FormTypeEnum.secretInput)
return VarKindType.mixed
}
const [inputsIsFocus, setInputsIsFocus] = useState<Record<string, boolean>>({})
@ -67,52 +71,38 @@ const ReasoningConfigForm: React.FC<Props> = ({
})
}
}, [])
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) => {
const handleAutomatic = (key: string, val: any, type: FormTypeEnum) => {
onChange({
...value,
[key]: {
value: val ? null : { type: getVarKindType(type), value: null },
auto: val ? 1 : 0,
},
})
}
const handleTypeChange = useCallback((variable: string, defaultValue: any) => {
return (newType: VarKindType) => {
const res = produce(value, (draft: ToolVarInputs) => {
draft[variable].value = {
type: VarKindType.variable,
value: varValue,
type: newType,
value: newType === VarKindType.variable ? '' : defaultValue,
}
})
onChange(newValue)
onChange(res)
}
}, [value, onChange])
}, [onChange, value])
const handleValueChange = useCallback((variable: string, varType: FormTypeEnum) => {
return (newValue: any) => {
const res = produce(value, (draft: ToolVarInputs) => {
draft[variable].value = {
type: getVarKindType(varType),
value: newValue,
}
})
onChange(res)
}
}, [onChange, value])
const handleAppChange = useCallback((variable: string) => {
return (app: {
app_id: string
@ -136,6 +126,17 @@ const ReasoningConfigForm: React.FC<Props> = ({
onChange(newValue)
}
}, [onChange, value])
const handleVariableSelectorChange = useCallback((variable: string) => {
return (newValue: ValueSelector | string) => {
const res = produce(value, (draft: ToolVarInputs) => {
draft[variable].value = {
type: VarKindType.variable,
value: newValue,
}
})
onChange(res)
}
}, [onChange, value])
const [isShowSchema, {
setTrue: showSchema,
@ -147,6 +148,7 @@ const ReasoningConfigForm: React.FC<Props> = ({
const renderField = (schema: any, showSchema: (schema: SchemaRoot, rootName: string) => void) => {
const {
default: defaultValue,
variable,
label,
required,
@ -155,6 +157,8 @@ const ReasoningConfigForm: React.FC<Props> = ({
scope,
url,
input_schema,
placeholder,
options,
} = schema
const auto = value[variable]?.auto
const tooltipContent = (tooltip && (
@ -166,28 +170,55 @@ const ReasoningConfigForm: React.FC<Props> = ({
asChild={false} />
))
const varInput = value[variable].value
const isString = type === FormTypeEnum.textInput || type === FormTypeEnum.secretInput
const isNumber = type === FormTypeEnum.textNumber
const isSelect = type === FormTypeEnum.select
const isFile = type === FormTypeEnum.file || type === FormTypeEnum.files
const isObject = type === FormTypeEnum.object
const isArray = type === FormTypeEnum.array
const isShowSchemaTooltip = isObject || isArray
const isShowJSONEditor = isObject || isArray
const isFile = type === FormTypeEnum.file || type === FormTypeEnum.files
const isBoolean = type === FormTypeEnum.boolean
const isSelect = type === FormTypeEnum.select
const isAppSelector = type === FormTypeEnum.appSelector
const isModelSelector = type === FormTypeEnum.modelSelector
// const isToolSelector = type === FormTypeEnum.toolSelector
const isString = !isNumber && !isSelect && !isFile && !isAppSelector && !isModelSelector && !isObject && !isArray
const valueType = (() => {
if (isNumber) return VarType.number
if (isSelect) return VarType.string
if (isFile) return VarType.file
if (isObject) return VarType.object
if (isArray) return VarType.array
return VarType.string
})()
const showTypeSwitch = isNumber || isObject || isArray
const isConstant = varInput?.type === VarKindType.constant || !varInput?.type
const showVariableSelector = isFile || varInput?.type === VarKindType.variable
const targetVarType = () => {
if (isString)
return VarType.string
else if (isNumber)
return VarType.number
else if (type === FormTypeEnum.files)
return VarType.arrayFile
else if (type === FormTypeEnum.file)
return VarType.file
else if (isBoolean)
return VarType.boolean
else if (isObject)
return VarType.object
else if (isArray)
return VarType.arrayObject
else
return VarType.string
}
const getFilterVar = () => {
if (isNumber)
return (varPayload: any) => varPayload.type === VarType.number
else if (isString)
return (varPayload: any) => [VarType.string, VarType.number, VarType.secret].includes(varPayload.type)
else if (isFile)
return (varPayload: any) => [VarType.file, VarType.arrayFile].includes(varPayload.type)
else if (isBoolean)
return (varPayload: any) => varPayload.type === VarType.boolean
else if (isObject)
return (varPayload: any) => varPayload.type === VarType.object
else if (isArray)
return (varPayload: any) => [VarType.array, VarType.arrayString, VarType.arrayNumber, VarType.arrayObject].includes(varPayload.type)
return undefined
}
return (
<div key={variable} className='space-y-1'>
<div key={variable} className='space-y-0.5'>
<div className='system-sm-semibold flex items-center justify-between py-2 text-text-secondary'>
<div className='flex items-center'>
<span className={cn('code-sm-semibold max-w-[140px] truncate text-text-secondary')} title={label[language] || label.en_US}>{label[language] || label.en_US}</span>
@ -196,8 +227,8 @@ const ReasoningConfigForm: React.FC<Props> = ({
)}
{tooltipContent}
<span className='system-xs-regular mx-1 text-text-quaternary'>·</span>
<span className='system-xs-regular text-text-tertiary'>{valueType}</span>
{isShowSchemaTooltip && (
<span className='system-xs-regular text-text-tertiary'>{targetVarType()}</span>
{isShowJSONEditor && (
<Tooltip
popupContent={<div className='system-xs-medium text-text-secondary'>
{t('workflow.nodes.agent.clickToViewParameterSchema')}
@ -213,22 +244,25 @@ const ReasoningConfigForm: React.FC<Props> = ({
)}
</div>
<div className='flex cursor-pointer items-center gap-1 rounded-[6px] border border-divider-subtle bg-background-default-lighter px-2 py-1 hover:bg-state-base-hover' onClick={() => handleAutomatic(variable, !auto)}>
<div className='flex cursor-pointer items-center gap-1 rounded-[6px] border border-divider-subtle bg-background-default-lighter px-2 py-1 hover:bg-state-base-hover' onClick={() => handleAutomatic(variable, !auto, type)}>
<span className='system-xs-medium text-text-secondary'>{t('plugin.detailPanel.toolSelector.auto')}</span>
<Switch
size='xs'
defaultValue={!!auto}
onChange={val => handleAutomatic(variable, val)}
onChange={val => handleAutomatic(variable, val, type)}
/>
</div>
</div>
{auto === 0 && (
<>
<div className={cn('gap-1', !(isShowJSONEditor && isConstant) && 'flex')}>
{showTypeSwitch && (
<FormInputTypeSwitch value={varInput?.type || VarKindType.constant} onChange={handleTypeChange(variable, defaultValue)}/>
)}
{isString && (
<Input
className={cn(inputsIsFocus[variable] ? 'border-gray-300 bg-gray-50 shadow-xs' : 'border-gray-100 bg-gray-100', 'rounded-lg border px-3 py-[6px]')}
<MixedInput
className={cn(inputsIsFocus[variable] ? 'border-gray-300 bg-gray-50 shadow-xs' : 'border-gray-100 bg-gray-100', 'grow rounded-lg border px-3 py-[6px]')}
value={varInput?.value as string || ''}
onChange={handleMixedTypeChange(variable)}
onChange={handleValueChange(variable, type)}
nodesOutputVars={nodeOutputVars}
availableNodes={availableNodes}
onFocusChange={handleInputFocus(variable)}
@ -236,53 +270,50 @@ const ReasoningConfigForm: React.FC<Props> = ({
placeholderClassName='!leading-[21px]'
/>
)}
{/* {isString && (
<VarReferencePicker
zIndex={1001}
readonly={false}
isShowNodeName
nodeId={nodeId}
{isNumber && isConstant && (
<Input
className='h-8 grow'
type='number'
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}
onChange={handleValueChange(variable, type)}
placeholder={placeholder?.[language] || placeholder?.en_US}
/>
)}
{(isFile || isObject || isArray) && (
<VarReferencePicker
zIndex={1001}
readonly={false}
isShowNodeName
nodeId={nodeId}
value={varInput?.value || []}
onChange={handleFileChange(variable)}
defaultVarKindType={VarKindType.variable}
filterVar={(varPayload: Var) => {
if(isFile)
return varPayload.type === VarType.file || varPayload.type === VarType.arrayFile
if(isObject)
return varPayload.type === VarType.object
if(isArray)
return [VarType.array, VarType.arrayNumber, VarType.arrayString, VarType.arrayObject, VarType.arrayFile].includes(varPayload.type)
return true
}}
{isBoolean && (
<FormInputBoolean
value={varInput?.value as boolean}
onChange={handleValueChange(variable, type)}
/>
)}
{isSelect && (
<SimpleSelect
wrapperClassName='h-8 grow'
defaultValue={varInput?.value}
items={options.filter((option: { show_on: any[] }) => {
if (option.show_on.length)
return option.show_on.every(showOnItem => value[showOnItem.variable] === showOnItem.value)
return true
}).map((option: { value: any; label: { [x: string]: any; en_US: any } }) => ({ value: option.value, name: option.label[language] || option.label.en_US }))}
onSelect={item => handleValueChange(variable, type)(item.value as string)}
placeholder={placeholder?.[language] || placeholder?.en_US}
/>
)}
{isShowJSONEditor && isConstant && (
<div className='mt-1 w-full'>
<CodeEditor
title='JSON'
value={varInput?.value as any}
isExpand
isInNode
height={100}
language={CodeLanguage.json}
onChange={handleValueChange(variable, type)}
className='w-full'
placeholder={<div className='whitespace-pre'>{placeholder?.[language] || placeholder?.en_US}</div>}
/>
</div>
)}
{isAppSelector && (
<AppSelector
disabled={false}
@ -301,7 +332,20 @@ const ReasoningConfigForm: React.FC<Props> = ({
scope={scope}
/>
)}
</>
{showVariableSelector && (
<VarReferencePicker
className='h-8 grow'
readonly={false}
isShowNodeName
nodeId={nodeId}
value={varInput?.value || []}
onChange={handleVariableSelectorChange(variable)}
filterVar={getFilterVar()}
schema={schema}
valueTypePlaceHolder={targetVarType()}
/>
)}
</div>
)}
{url && (
<a

View File

@ -64,37 +64,52 @@ export const addDefaultValue = (value: Record<string, any>, formSchemas: { varia
return newValues
}
export const generateFormValue = (value: Record<string, any>, formSchemas: { variable: string; default?: any }[], isReasoning = false) => {
const correctInitialData = (type: string, target: any, defaultValue: any) => {
if (type === 'text-input' || type === 'secret-input')
target.type = 'mixed'
if (type === 'boolean') {
if (typeof defaultValue === 'string')
target.value = defaultValue === 'true' || defaultValue === '1'
if (typeof defaultValue === 'boolean')
target.value = defaultValue
if (typeof defaultValue === 'number')
target.value = defaultValue === 1
}
if (type === 'number-input') {
if (typeof defaultValue === 'string' && defaultValue !== '')
target.value = Number.parseFloat(defaultValue)
}
if (type === 'app-selector' || type === 'model-selector')
target.value = defaultValue
return target
}
export const generateFormValue = (value: Record<string, any>, formSchemas: { variable: string; default?: any; type: string }[], isReasoning = false) => {
const newValues = {} as any
formSchemas.forEach((formSchema) => {
const itemValue = value[formSchema.variable]
if ((formSchema.default !== undefined) && (value === undefined || itemValue === null || itemValue === '' || itemValue === undefined)) {
const value = formSchema.default
newValues[formSchema.variable] = {
...(isReasoning ? { value: null, auto: 1 } : { value: formSchema.default }),
value: {
type: 'constant',
value: formSchema.default,
},
...(isReasoning ? { auto: 1, value: null } : {}),
}
if (!isReasoning)
newValues[formSchema.variable].value = correctInitialData(formSchema.type, newValues[formSchema.variable].value, value)
}
})
return newValues
}
export const getPlainValue = (value: Record<string, any>) => {
const plainValue = { ...value }
Object.keys(plainValue).forEach((key) => {
plainValue[key] = value[key].value
})
return plainValue
}
export const getStructureValue = (value: Record<string, any>) => {
const newValue = { ...value } as any
Object.keys(newValue).forEach((key) => {
newValue[key] = {
value: value[key],
}
})
return newValue
}
export const getConfiguredValue = (value: Record<string, any>, formSchemas: { variable: string; type: string; default?: any }[]) => {
const newValues = { ...value }
formSchemas.forEach((formSchema) => {
@ -105,27 +120,7 @@ export const getConfiguredValue = (value: Record<string, any>, formSchemas: { va
type: 'constant',
value: formSchema.default,
}
if (formSchema.type === 'text-input' || formSchema.type === 'secret-input')
newValues[formSchema.variable].type = 'mixed'
if (formSchema.type === 'boolean') {
if (typeof value === 'string')
newValues[formSchema.variable].value = value === 'true' || value === '1'
if (typeof value === 'boolean')
newValues[formSchema.variable].value = value
if (typeof value === 'number')
newValues[formSchema.variable].value = value === 1
}
if (formSchema.type === 'number-input') {
if (typeof value === 'string' && value !== '')
newValues[formSchema.variable].value = Number.parseFloat(value)
}
if (formSchema.type === 'app-selector' || formSchema.type === 'model-selector')
newValues[formSchema.variable] = value
newValues[formSchema.variable] = correctInitialData(formSchema.type, newValues[formSchema.variable], value)
}
})
return newValues

View File

@ -117,7 +117,7 @@ const FormInputItem: FC<Props> = ({
const getVarKindType = () => {
if (isFile)
return VarKindType.variable
if (isSelect || isAppSelector || isModelSelector || isBoolean)
if (isSelect || isBoolean || isNumber)
return VarKindType.constant
if (isString)
return VarKindType.mixed

View File

@ -7,6 +7,7 @@ import { renderI18nObject } from '@/i18n'
const nodeDefault: NodeDefault<AgentNodeType> = {
defaultValue: {
version: '2',
},
getAvailablePrevNodes(isChatMode) {
return isChatMode
@ -60,15 +61,28 @@ const nodeDefault: NodeDefault<AgentNodeType> = {
const schemas = toolValue.schemas || []
const userSettings = toolValue.settings
const reasoningConfig = toolValue.parameters
const version = payload.version
schemas.forEach((schema: any) => {
if (schema?.required) {
if (schema.form === 'form' && !userSettings[schema.name]?.value) {
if (schema.form === 'form' && !version && !userSettings[schema.name]?.value) {
return {
isValid: false,
errorMessage: t('workflow.errorMsg.toolParameterRequired', { field: renderI18nObject(param.label, language), param: renderI18nObject(schema.label, language) }),
}
}
if (schema.form === 'llm' && reasoningConfig[schema.name].auto === 0 && !userSettings[schema.name]?.value) {
if (schema.form === 'form' && version && !userSettings[schema.name]?.value.value) {
return {
isValid: false,
errorMessage: t('workflow.errorMsg.toolParameterRequired', { field: renderI18nObject(param.label, language), param: renderI18nObject(schema.label, language) }),
}
}
if (schema.form === 'llm' && !version && reasoningConfig[schema.name].auto === 0 && !reasoningConfig[schema.name]?.value) {
return {
isValid: false,
errorMessage: t('workflow.errorMsg.toolParameterRequired', { field: renderI18nObject(param.label, language), param: renderI18nObject(schema.label, language) }),
}
}
if (schema.form === 'llm' && version && reasoningConfig[schema.name].auto === 0 && !reasoningConfig[schema.name]?.value.value) {
return {
isValid: false,
errorMessage: t('workflow.errorMsg.toolParameterRequired', { field: renderI18nObject(param.label, language), param: renderI18nObject(schema.label, language) }),

View File

@ -11,6 +11,7 @@ export type AgentNodeType = CommonNodeType & {
output_schema: Record<string, any>
plugin_unique_identifier?: string
memory?: Memory
version?: string
}
export enum AgentFeature {

View File

@ -27,6 +27,7 @@ import type { QuestionClassifierNodeType } from '../nodes/question-classifier/ty
import type { IfElseNodeType } from '../nodes/if-else/types'
import { branchNameCorrect } from '../nodes/if-else/utils'
import type { IterationNodeType } from '../nodes/iteration/types'
import type { AgentNodeType } from '../nodes/agent/types'
import type { LoopNodeType } from '../nodes/loop/types'
import type { ToolNodeType } from '../nodes/tool/types'
import {
@ -304,6 +305,13 @@ export const initialNodes = (originNodes: Node[], originEdges: Edge[]) => {
}
}
if (node.data.type === BlockEnum.Agent && !(node as Node<AgentNodeType>).data.version) {
// TODO: formatting legacy agent node data
// (node as Node<ToolNodeType>).data.version = '2'
// const toolData = (node as Node<AgentNodeType>).data.agent_parameters?.tool
// const multipleTools = (node as Node<AgentNodeType>).data.agent_parameters?.multiple_tools
}
return node
})
}