feat: workflow new nodes (#4683)

Co-authored-by: Joel <iamjoel007@gmail.com>
Co-authored-by: Patryk Garstecki <patryk20120@yahoo.pl>
Co-authored-by: Sebastian.W <thiner@gmail.com>
Co-authored-by: 呆萌闷油瓶 <253605712@qq.com>
Co-authored-by: takatost <takatost@users.noreply.github.com>
Co-authored-by: rechardwang <wh_goodjob@163.com>
Co-authored-by: Nite Knite <nkCoding@gmail.com>
Co-authored-by: Chenhe Gu <guchenhe@gmail.com>
Co-authored-by: Joshua <138381132+joshua20231026@users.noreply.github.com>
Co-authored-by: Weaxs <459312872@qq.com>
Co-authored-by: Ikko Eltociear Ashimine <eltociear@gmail.com>
Co-authored-by: leejoo0 <81673835+leejoo0@users.noreply.github.com>
Co-authored-by: JzoNg <jzongcode@gmail.com>
Co-authored-by: sino <sino2322@gmail.com>
Co-authored-by: Vikey Chen <vikeytk@gmail.com>
Co-authored-by: wanghl <Wang-HL@users.noreply.github.com>
Co-authored-by: Haolin Wang-汪皓临 <haolin.wang@atlaslovestravel.com>
Co-authored-by: Zixuan Cheng <61724187+Theysua@users.noreply.github.com>
Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com>
Co-authored-by: Bowen Liang <bowenliang@apache.org>
Co-authored-by: Bowen Liang <liangbowen@gf.com.cn>
Co-authored-by: fanghongtai <42790567+fanghongtai@users.noreply.github.com>
Co-authored-by: wxfanghongtai <wxfanghongtai@gf.com.cn>
Co-authored-by: Matri <qjp@bithuman.io>
Co-authored-by: Benjamin <benjaminx@gmail.com>
This commit is contained in:
zxhlyh
2024-05-27 21:57:08 +08:00
committed by GitHub
parent 444fdb79dc
commit 45deaee762
210 changed files with 9951 additions and 2223 deletions

View File

@@ -0,0 +1,92 @@
'use client'
import type { FC } from 'react'
import {
memo,
useCallback,
} from 'react'
import cn from 'classnames'
import { useTranslation } from 'react-i18next'
import BlockSelector from '../../../../block-selector'
import type { Param, ParamType } from '../../types'
import { useStore } from '@/app/components/workflow/store'
import type { ToolDefaultValue } from '@/app/components/workflow/block-selector/types'
import type { ToolParameter } from '@/app/components/tools/types'
import { CollectionType } from '@/app/components/tools/types'
import type { BlockEnum } from '@/app/components/workflow/types'
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
const i18nPrefix = 'workflow.nodes.parameterExtractor'
type Props = {
onImport: (params: Param[]) => void
}
function toParmExactParams(toolParams: ToolParameter[], lan: string): Param[] {
return toolParams.map((item) => {
return {
name: item.name,
type: item.type as ParamType,
required: item.required,
description: item.llm_description,
options: item.options?.map(option => option.label[lan] || option.label.en_US) || [],
}
})
}
const ImportFromTool: FC<Props> = ({
onImport,
}) => {
const { t } = useTranslation()
const language = useLanguage()
const buildInTools = useStore(s => s.buildInTools)
const customTools = useStore(s => s.customTools)
const workflowTools = useStore(s => s.workflowTools)
const handleSelectTool = useCallback((_type: BlockEnum, toolInfo?: ToolDefaultValue) => {
const { provider_id, provider_type, tool_name } = toolInfo!
const currentTools = (() => {
switch (provider_type) {
case CollectionType.builtIn:
return buildInTools
case CollectionType.custom:
return customTools
case CollectionType.workflow:
return workflowTools
default:
return []
}
})()
const currCollection = currentTools.find(item => item.id === provider_id)
const currTool = currCollection?.tools.find(tool => tool.name === tool_name)
const toExactParams = (currTool?.parameters || []).filter((item: any) => item.form === 'llm')
const formattedParams = toParmExactParams(toExactParams, language)
onImport(formattedParams)
}, [buildInTools, customTools, language, onImport, workflowTools])
const renderTrigger = useCallback((open: boolean) => {
return (
<div>
<div className={cn(
'flex items-center h-6 px-2 cursor-pointer rounded-md hover:bg-gray-100 text-xs font-medium text-gray-500',
open && 'bg-gray-100',
)}>
{t(`${i18nPrefix}.importFromTool`)}
</div>
</div>
)
}, [t])
return (
<BlockSelector
placement='bottom-end'
offset={{
mainAxis: 4,
crossAxis: 52,
}}
trigger={renderTrigger}
onSelect={handleSelectTool}
noBlocks
/>
)
}
export default memo(ImportFromTool)

View File

@@ -0,0 +1,59 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import type { Param } from '../../types'
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
import { Edit03, Trash03 } from '@/app/components/base/icons/src/vender/line/general'
const i18nPrefix = 'workflow.nodes.parameterExtractor'
type Props = {
payload: Param
onEdit: () => void
onDelete: () => void
}
const Item: FC<Props> = ({
payload,
onEdit,
onDelete,
}) => {
const { t } = useTranslation()
return (
<div className='relative px-2.5 py-2 rounded-lg bg-white border-[0.5px] border-gray-200 hover:shadow-xs group'>
<div className='flex justify-between'>
<div className='flex items-center'>
<Variable02 className='w-3.5 h-3.5 text-primary-500' />
<div className='ml-1 text-[13px] font-medium text-gray-900'>{payload.name}</div>
<div className='ml-2 text-xs font-normal text-gray-500 capitalize'>{payload.type}</div>
</div>
{payload.required && (
<div className='uppercase leading-4 text-xs font-normal text-gray-500'>{t(`${i18nPrefix}.addExtractParameterContent.required`)}</div>
)}
</div>
<div className='mt-0.5 leading-[18px] text-xs font-normal text-gray-500'>{payload.description}</div>
<div
className='group-hover:flex absolute top-0 right-1 hidden h-full items-center w-[119px] justify-end space-x-1 rounded-lg'
style={{
background: 'linear-gradient(270deg, #FFF 49.99%, rgba(255, 255, 255, 0.00) 98.1%)',
}}
>
<div
className='p-1 cursor-pointer rounded-md hover:bg-black/5'
onClick={onEdit}
>
<Edit03 className='w-4 h-4 text-gray-500' />
</div>
<div
className='p-1 cursor-pointer rounded-md hover:bg-black/5'
onClick={onDelete}
>
<Trash03 className='w-4 h-4 text-gray-500' />
</div>
</div>
</div>
)
}
export default React.memo(Item)

View File

@@ -0,0 +1,85 @@
'use client'
import type { FC } from 'react'
import React, { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useBoolean } from 'ahooks'
import type { Param } from '../../types'
import ListNoDataPlaceholder from '../../../_base/components/list-no-data-placeholder'
import Item from './item'
import EditParam from './update'
import type { MoreInfo } from '@/app/components/workflow/types'
const i18nPrefix = 'workflow.nodes.parameterExtractor'
type Props = {
readonly: boolean
list: Param[]
onChange: (list: Param[], moreInfo?: MoreInfo) => void
}
const List: FC<Props> = ({
list,
onChange,
}) => {
const { t } = useTranslation()
const [isShowEditModal, {
setTrue: showEditModal,
setFalse: hideEditModal,
}] = useBoolean(false)
const handleItemChange = useCallback((index: number) => {
return (payload: Param, moreInfo?: MoreInfo) => {
const newList = list.map((item, i) => {
if (i === index)
return payload
return item
})
onChange(newList, moreInfo)
hideEditModal()
}
}, [hideEditModal, list, onChange])
const [currEditItemIndex, setCurrEditItemIndex] = useState<number>(-1)
const handleItemEdit = useCallback((index: number) => {
return () => {
setCurrEditItemIndex(index)
showEditModal()
}
}, [showEditModal])
const handleItemDelete = useCallback((index: number) => {
return () => {
const newList = list.filter((_, i) => i !== index)
onChange(newList)
}
}, [list, onChange])
if (list.length === 0) {
return (
<ListNoDataPlaceholder >{t(`${i18nPrefix}.extractParametersNotSet`)}</ListNoDataPlaceholder>
)
}
return (
<div className='space-y-1'>
{list.map((item, index) => (
<Item
key={index}
payload={item}
onDelete={handleItemDelete(index)}
onEdit={handleItemEdit(index)}
/>
))}
{isShowEditModal && (
<EditParam
type='edit'
payload={list[currEditItemIndex]}
onSave={handleItemChange(currEditItemIndex)}
onCancel={hideEditModal}
/>
)}
</div>
)
}
export default React.memo(List)

View File

@@ -0,0 +1,193 @@
'use client'
import type { FC } from 'react'
import React, { useCallback, useState } from 'react'
import { useBoolean } from 'ahooks'
import { useTranslation } from 'react-i18next'
import cn from 'classnames'
import type { Param } from '../../types'
import { ParamType } from '../../types'
import AddButton from '@/app/components/base/button/add-button'
import Modal from '@/app/components/base/modal'
import Button from '@/app/components/base/button'
import Field from '@/app/components/app/configuration/config-var/config-modal/field'
import Select from '@/app/components/base/select'
import Switch from '@/app/components/base/switch'
import Toast from '@/app/components/base/toast'
import ConfigSelect from '@/app/components/app/configuration/config-var/config-select'
import { ChangeType, type MoreInfo } from '@/app/components/workflow/types'
import { checkKeys } from '@/utils/var'
const i18nPrefix = 'workflow.nodes.parameterExtractor'
const errorI18nPrefix = 'workflow.errorMsg'
const inputClassName = 'w-full px-3 text-sm leading-9 text-gray-900 border-0 rounded-lg grow h-9 bg-gray-100 focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200'
const DEFAULT_PARAM: Param = {
name: '',
type: ParamType.string,
description: '',
required: false,
}
type Props = {
type: 'add' | 'edit'
payload?: Param
onSave: (payload: Param, moreInfo?: MoreInfo) => void
onCancel?: () => void
}
const TYPES = [ParamType.string, ParamType.number, ParamType.arrayString, ParamType.arrayNumber, ParamType.arrayObject]
const AddExtractParameter: FC<Props> = ({
type,
payload,
onSave,
onCancel,
}) => {
const { t } = useTranslation()
const isAdd = type === 'add'
const [param, setParam] = useState<Param>(isAdd ? DEFAULT_PARAM : payload as Param)
const [renameInfo, setRenameInfo] = useState<MoreInfo | undefined>(undefined)
const handleParamChange = useCallback((key: string) => {
return (value: any) => {
if (key === 'name') {
const { isValid, errorKey, errorMessageKey } = checkKeys([value], true)
if (!isValid) {
Toast.notify({
type: 'error',
message: t(`appDebug.varKeyError.${errorMessageKey}`, { key: errorKey }),
})
return
}
}
setRenameInfo(key === 'name'
? {
type: ChangeType.changeVarName,
payload: {
beforeKey: param.name,
afterKey: value,
},
}
: undefined)
setParam((prev) => {
return {
...prev,
[key]: value,
}
})
}
}, [param.name, t])
const [isShowModal, {
setTrue: doShowModal,
setFalse: doHideModal,
}] = useBoolean(!isAdd)
const hideModal = useCallback(() => {
doHideModal()
onCancel?.()
}, [onCancel, doHideModal])
const showAddModal = useCallback(() => {
if (isAdd)
setParam(DEFAULT_PARAM)
doShowModal()
}, [isAdd, doShowModal])
const checkValid = useCallback(() => {
let errMessage = ''
if (!param.name)
errMessage = t(`${errorI18nPrefix}.fieldRequired`, { field: t(`${i18nPrefix}.addExtractParameterContent.name`) })
if (!errMessage && param.type === ParamType.select && (!param.options || param.options.length === 0))
errMessage = t(`${errorI18nPrefix}.fieldRequired`, { field: t('appDebug.variableConig.options') })
if (!errMessage && !param.description)
errMessage = t(`${errorI18nPrefix}.fieldRequired`, { field: t(`${i18nPrefix}.addExtractParameterContent.description`) })
if (errMessage) {
Toast.notify({
type: 'error',
message: errMessage,
})
return false
}
return true
}, [param, t])
const handleSave = useCallback(() => {
if (!checkValid())
return
onSave(param, renameInfo)
hideModal()
}, [checkValid, onSave, param, hideModal, renameInfo])
return (
<div>
{isAdd && (
<AddButton className='mx-1' onClick={showAddModal} />
)}
{isShowModal && (
<Modal
title={t(`${i18nPrefix}.addExtractParameter`)}
isShow
onClose={hideModal}
className='!w-[400px] !max-w-[400px] !p-4'
wrapperClassName='!z-[100]'
>
<div>
<div className='space-y-2'>
<Field title={t(`${i18nPrefix}.addExtractParameterContent.name`)}>
<input
type='text'
className={inputClassName}
value={param.name}
onChange={e => handleParamChange('name')(e.target.value)}
placeholder={t(`${i18nPrefix}.addExtractParameterContent.namePlaceholder`)!}
/>
</Field>
<Field title={t(`${i18nPrefix}.addExtractParameterContent.type`)}>
<Select
defaultValue={param.type}
allowSearch={false}
bgClassName='bg-gray-100'
onSelect={v => handleParamChange('type')(v.value)}
optionClassName='capitalize'
items={
TYPES.map(type => ({
value: type,
name: type,
}))
}
/>
</Field>
{param.type === ParamType.select && (
<Field title={t('appDebug.variableConig.options')}>
<ConfigSelect options={param.options || []} onChange={handleParamChange('options')} />
</Field>
)}
<Field title={t(`${i18nPrefix}.addExtractParameterContent.description`)}>
<textarea
className={cn(inputClassName, '!h-[80px]')}
value={param.description}
onChange={e => handleParamChange('description')(e.target.value)}
placeholder={t(`${i18nPrefix}.addExtractParameterContent.descriptionPlaceholder`)!}
/>
</Field>
<Field title={t(`${i18nPrefix}.addExtractParameterContent.required`)}>
<>
<div className='mb-1.5 leading-[18px] text-xs font-normal text-gray-500'>{t(`${i18nPrefix}.addExtractParameterContent.requiredContent`)}</div>
<Switch size='l' defaultValue={param.required} onChange={handleParamChange('required')} />
</>
</Field>
</div>
<div className='mt-4 flex justify-end space-x-2'>
<Button className='flex !h-8 !w-[95px] text-[13px] font-medium text-gray-700' onClick={hideModal} >{t('common.operation.cancel')}</Button>
<Button className='flex !h-8 !w-[95px] text-[13px] font-medium' type='primary' onClick={handleSave} >{isAdd ? t('common.operation.add') : t('common.operation.save')}</Button>
</div>
</div>
</Modal>
)}
</div>
)
}
export default React.memo(AddExtractParameter)

View File

@@ -0,0 +1,70 @@
'use client'
import type { FC } from 'react'
import React, { useCallback } from 'react'
import cn from 'classnames'
import { useTranslation } from 'react-i18next'
import { ReasoningModeType } from '../types'
import Field from '../../_base/components/field'
const i18nPrefix = 'workflow.nodes.parameterExtractor'
type ItemProps = {
isChosen: boolean
text: string
onClick: () => void
}
const Item: FC<ItemProps> = ({
isChosen,
text,
onClick,
}) => {
return (
<div
className={cn(isChosen ? 'border-[1.5px] border-primary-400 bg-white' : 'border border-gray-100 bg-gray-25', 'grow w-0 shrink-0 flex items-center h-8 justify-center rounded-lg cursor-pointer text-[13px] font-normal text-gray-900')}
onClick={() => !isChosen ? onClick() : () => { }}
>
{text}
</div>
)
}
type Props = {
type: ReasoningModeType
onChange: (type: ReasoningModeType) => void
}
const ReasoningModePicker: FC<Props> = ({
type,
onChange,
}) => {
const { t } = useTranslation()
const handleChange = useCallback((type: ReasoningModeType) => {
return () => {
onChange(type)
}
}, [onChange])
return (
<Field
title={t(`${i18nPrefix}.reasoningMode`)}
tooltip={t(`${i18nPrefix}.reasoningModeTip`)!}
>
<div className='flex space-x-1'>
<Item
isChosen={type === ReasoningModeType.functionCall}
text='Function/Tool Calling'
onClick={handleChange(ReasoningModeType.functionCall)}
/>
<Item
isChosen={type === ReasoningModeType.prompt}
text='Prompt'
onClick={handleChange(ReasoningModeType.prompt)}
/>
</div>
</Field>
)
}
export default React.memo(ReasoningModePicker)

View File

@@ -0,0 +1,64 @@
import { BlockEnum } from '../../types'
import type { NodeDefault } from '../../types'
import { type ParameterExtractorNodeType, ReasoningModeType } from './types'
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/constants'
const i18nPrefix = 'workflow'
const nodeDefault: NodeDefault<ParameterExtractorNodeType> = {
defaultValue: {
query: [],
model: {
provider: '',
name: '',
mode: 'chat',
completion_params: {
temperature: 0.7,
},
},
reasoning_mode: ReasoningModeType.prompt,
},
getAvailablePrevNodes(isChatMode: boolean) {
const nodes = isChatMode
? ALL_CHAT_AVAILABLE_BLOCKS
: ALL_COMPLETION_AVAILABLE_BLOCKS.filter(type => type !== BlockEnum.End)
return nodes
},
getAvailableNextNodes(isChatMode: boolean) {
const nodes = isChatMode ? ALL_CHAT_AVAILABLE_BLOCKS : ALL_COMPLETION_AVAILABLE_BLOCKS
return nodes
},
checkValid(payload: ParameterExtractorNodeType, t: any) {
let errorMessages = ''
if (!errorMessages && (!payload.query || payload.query.length === 0))
errorMessages = t(`${i18nPrefix}.errorMsg.fieldRequired`, { field: t(`${i18nPrefix}.nodes.parameterExtractor.inputVar`) })
if (!errorMessages && !payload.model.provider)
errorMessages = t(`${i18nPrefix}.errorMsg.fieldRequired`, { field: t(`${i18nPrefix}.nodes.parameterExtractor.model`) })
if (!errorMessages && (!payload.parameters || payload.parameters.length === 0))
errorMessages = t(`${i18nPrefix}.errorMsg.fieldRequired`, { field: t(`${i18nPrefix}.nodes.parameterExtractor.extractParameters`) })
if (!errorMessages) {
payload.parameters.forEach((param) => {
if (errorMessages)
return
if (!param.name) {
errorMessages = t(`${i18nPrefix}.errorMsg.fieldRequired`, { field: t(`${i18nPrefix}.nodes.parameterExtractor.addExtractParameterContent.namePlaceholder`) })
return
}
if (!param.type) {
errorMessages = t(`${i18nPrefix}.errorMsg.fieldRequired`, { field: t(`${i18nPrefix}.nodes.parameterExtractor.addExtractParameterContent.typePlaceholder`) })
return
}
if (!param.description)
errorMessages = t(`${i18nPrefix}.errorMsg.fieldRequired`, { field: t(`${i18nPrefix}.nodes.parameterExtractor.addExtractParameterContent.descriptionPlaceholder`) })
})
}
return {
isValid: !errorMessages,
errorMessage: errorMessages,
}
},
}
export default nodeDefault

View File

@@ -0,0 +1,31 @@
import type { FC } from 'react'
import React from 'react'
import type { ParameterExtractorNodeType } from './types'
import {
useTextGenerationCurrentProviderAndModelAndModelList,
} from '@/app/components/header/account-setting/model-provider-page/hooks'
import ModelSelector from '@/app/components/header/account-setting/model-provider-page/model-selector'
import type { NodeProps } from '@/app/components/workflow/types'
const Node: FC<NodeProps<ParameterExtractorNodeType>> = ({
data,
}) => {
const { provider, name: modelId } = data.model || {}
const {
textGenerationModelList,
} = useTextGenerationCurrentProviderAndModelAndModelList()
const hasSetModel = provider && modelId
return (
<div className='mb-1 px-3 py-1'>
{hasSetModel && (
<ModelSelector
defaultModel={{ provider, model: modelId }}
modelList={textGenerationModelList}
readonly
/>
)}
</div>
)
}
export default React.memo(Node)

View File

@@ -0,0 +1,227 @@
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import MemoryConfig from '../_base/components/memory-config'
import VarReferencePicker from '../_base/components/variable/var-reference-picker'
import Editor from '../_base/components/prompt/editor'
import ResultPanel from '../../run/result-panel'
import useConfig from './use-config'
import type { ParameterExtractorNodeType } from './types'
import ExtractParameter from './components/extract-parameter/list'
import ImportFromTool from './components/extract-parameter/import-from-tool'
import AddExtractParameter from './components/extract-parameter/update'
import ReasoningModePicker from './components/reasoning-mode-picker'
import Field from '@/app/components/workflow/nodes/_base/components/field'
import Split from '@/app/components/workflow/nodes/_base/components/split'
import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal'
import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars'
import { InputVarType, type NodePanelProps } from '@/app/components/workflow/types'
import TooltipPlus from '@/app/components/base/tooltip-plus'
import { HelpCircle } from '@/app/components/base/icons/src/vender/line/general'
import BeforeRunForm from '@/app/components/workflow/nodes/_base/components/before-run-form'
import { VarType } from '@/app/components/workflow/types'
const i18nPrefix = 'workflow.nodes.parameterExtractor'
const i18nCommonPrefix = 'workflow.common'
const Panel: FC<NodePanelProps<ParameterExtractorNodeType>> = ({
id,
data,
}) => {
const { t } = useTranslation()
const {
readOnly,
inputs,
handleInputVarChange,
filterVar,
isChatModel,
isChatMode,
isCompletionModel,
handleModelChanged,
handleImportFromTool,
handleCompletionParamsChange,
addExtractParameter,
handleExactParamsChange,
handleInstructionChange,
hasSetBlockStatus,
handleMemoryChange,
isSupportFunctionCall,
handleReasoningModeChange,
availableVars,
availableNodesWithParent,
inputVarValues,
varInputs,
isShowSingleRun,
hideSingleRun,
runningStatus,
handleRun,
handleStop,
runResult,
setInputVarValues,
} = useConfig(id, data)
const model = inputs.model
return (
<div className='mt-2'>
<div className='px-4 pb-4 space-y-4'>
<Field
title={t(`${i18nPrefix}.inputVar`)}
>
<>
<VarReferencePicker
readonly={readOnly}
nodeId={id}
isShowNodeName
value={inputs.query || []}
onChange={handleInputVarChange}
filterVar={filterVar}
/>
</>
</Field>
<Field
title={t(`${i18nCommonPrefix}.model`)}
>
<ModelParameterModal
popupClassName='!w-[387px]'
isInWorkflow
isAdvancedMode={true}
mode={model?.mode}
provider={model?.provider}
completionParams={model?.completion_params}
modelId={model?.name}
setModel={handleModelChanged}
onCompletionParamsChange={handleCompletionParamsChange}
hideDebugWithMultipleModel
debugWithMultipleModel={false}
readonly={readOnly}
/>
</Field>
<Field
title={t(`${i18nPrefix}.extractParameters`)}
operations={
!readOnly
? (
<div className='flex items-center space-x-1'>
{!readOnly && (
<ImportFromTool onImport={handleImportFromTool} />
)}
{!readOnly && (<div className='w-px h-3 bg-gray-200'></div>)}
<AddExtractParameter type='add' onSave={addExtractParameter} />
</div>
)
: undefined
}
>
<ExtractParameter
readonly={readOnly}
list={inputs.parameters || []}
onChange={handleExactParamsChange}
/>
</Field>
<Editor
title={
<div className='flex items-center space-x-1'>
<span className='uppercase'>{t(`${i18nPrefix}.instruction`)}</span>
<TooltipPlus popupContent={
<div className='w-[120px]'>
{t(`${i18nPrefix}.instructionTip`)}
</div>}>
<HelpCircle className='w-3.5 h-3.5 ml-0.5 text-gray-400' />
</TooltipPlus>
</div>
}
value={inputs.instruction}
onChange={handleInstructionChange}
readOnly={readOnly}
isChatModel={isChatModel}
isChatApp={isChatMode}
isShowContext={false}
hasSetBlockStatus={hasSetBlockStatus}
nodesOutputVars={availableVars}
availableNodes={availableNodesWithParent}
/>
<Field
title={t(`${i18nPrefix}.advancedSetting`)}
supportFold
>
<>
{/* Memory */}
{isChatMode && (
<div className='mt-4'>
<MemoryConfig
readonly={readOnly}
config={{ data: inputs.memory }}
onChange={handleMemoryChange}
canSetRoleName={isCompletionModel}
/>
</div>
)}
{isSupportFunctionCall && (
<div className='mt-2'>
<ReasoningModePicker
type={inputs.reasoning_mode}
onChange={handleReasoningModeChange}
/>
</div>
)}
</>
</Field>
</div>
{inputs.parameters?.length > 0 && (<>
<Split />
<div className='px-4 pt-4 pb-2'>
<OutputVars>
<>
<VarItem
name='__is_success'
type={VarType.number}
description={t(`${i18nPrefix}.isSuccess`)}
/>
<VarItem
name='__reason'
type={VarType.string}
description={t(`${i18nPrefix}.errorReason`)}
/>
{inputs.parameters.map((param, index) => (
<VarItem
key={index}
name={param.name}
type={param.type}
description={param.description}
/>
))}
</>
</OutputVars>
</div>
</>)}
{isShowSingleRun && (
<BeforeRunForm
nodeName={inputs.title}
onHide={hideSingleRun}
forms={[
{
inputs: [{
label: t(`${i18nPrefix}.inputVar`)!,
variable: 'query',
type: InputVarType.paragraph,
required: true,
}, ...varInputs],
values: inputVarValues,
onChange: setInputVarValues,
},
]}
runningStatus={runningStatus}
onRun={handleRun}
onStop={handleStop}
result={<ResultPanel {...runResult} showSteps={false} />}
/>
)}
</div>
)
}
export default React.memo(Panel)

View File

@@ -0,0 +1,33 @@
import type { CommonNodeType, Memory, ModelConfig, ValueSelector } from '@/app/components/workflow/types'
export enum ParamType {
string = 'string',
number = 'number',
bool = 'bool',
select = 'select',
arrayString = 'array[string]',
arrayNumber = 'array[number]',
arrayObject = 'array[object]',
}
export type Param = {
name: string
type: ParamType
options?: string[]
description: string
required?: boolean
}
export enum ReasoningModeType {
prompt = 'prompt',
functionCall = 'function_call',
}
export type ParameterExtractorNodeType = CommonNodeType & {
model: ModelConfig
query: ValueSelector
reasoning_mode: ReasoningModeType
parameters: Param[]
instruction: string
memory?: Memory
}

View File

@@ -0,0 +1,258 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import produce from 'immer'
import type { Memory, MoreInfo, ValueSelector, Var } from '../../types'
import { ChangeType, VarType } from '../../types'
import { useStore } from '../../store'
import {
useIsChatMode,
useNodesReadOnly,
useWorkflow,
} from '../../hooks'
import useOneStepRun from '../_base/hooks/use-one-step-run'
import type { Param, ParameterExtractorNodeType, ReasoningModeType } from './types'
import { useModelListAndDefaultModelAndCurrentProviderAndModel, useTextGenerationCurrentProviderAndModelAndModelList } from '@/app/components/header/account-setting/model-provider-page/hooks'
import {
ModelFeatureEnum,
ModelTypeEnum,
} from '@/app/components/header/account-setting/model-provider-page/declarations'
import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud'
import { checkHasQueryBlock } from '@/app/components/base/prompt-editor/constants'
import useAvailableVarList from '@/app/components/workflow/nodes/_base/hooks/use-available-var-list'
const useConfig = (id: string, payload: ParameterExtractorNodeType) => {
const { nodesReadOnly: readOnly } = useNodesReadOnly()
const { handleOutVarRenameChange } = useWorkflow()
const isChatMode = useIsChatMode()
const defaultConfig = useStore(s => s.nodesDefaultConfigs)[payload.type]
const [defaultRolePrefix, setDefaultRolePrefix] = useState<{ user: string; assistant: string }>({ user: '', assistant: '' })
const { inputs, setInputs: doSetInputs } = useNodeCrud<ParameterExtractorNodeType>(id, payload)
const inputRef = useRef(inputs)
const setInputs = useCallback((newInputs: ParameterExtractorNodeType) => {
if (newInputs.memory && !newInputs.memory.role_prefix) {
const newPayload = produce(newInputs, (draft) => {
draft.memory!.role_prefix = defaultRolePrefix
})
doSetInputs(newPayload)
inputRef.current = newPayload
return
}
doSetInputs(newInputs)
inputRef.current = newInputs
}, [doSetInputs, defaultRolePrefix])
const filterVar = useCallback((varPayload: Var) => {
return [VarType.string].includes(varPayload.type)
}, [])
const handleInputVarChange = useCallback((newInputVar: ValueSelector | string) => {
const newInputs = produce(inputs, (draft) => {
draft.query = newInputVar as ValueSelector || []
})
setInputs(newInputs)
}, [inputs, setInputs])
const handleExactParamsChange = useCallback((newParams: Param[], moreInfo?: MoreInfo) => {
const newInputs = produce(inputs, (draft) => {
draft.parameters = newParams
})
setInputs(newInputs)
if (moreInfo && moreInfo?.type === ChangeType.changeVarName && moreInfo.payload)
handleOutVarRenameChange(id, [id, moreInfo.payload.beforeKey], [id, moreInfo.payload.afterKey!])
}, [handleOutVarRenameChange, id, inputs, setInputs])
const addExtractParameter = useCallback((payload: Param) => {
const newInputs = produce(inputs, (draft) => {
if (!draft.parameters)
draft.parameters = []
draft.parameters.push(payload)
})
setInputs(newInputs)
}, [inputs, setInputs])
// model
const model = inputs.model || {
provider: '',
name: '',
mode: 'chat',
completion_params: {
temperature: 0.7,
},
}
const modelMode = inputs.model?.mode
const isChatModel = modelMode === 'chat'
const isCompletionModel = !isChatModel
const appendDefaultPromptConfig = useCallback((draft: ParameterExtractorNodeType, defaultConfig: any, _passInIsChatMode?: boolean) => {
const promptTemplates = defaultConfig.prompt_templates
if (!isChatModel) {
setDefaultRolePrefix({
user: promptTemplates.completion_model.conversation_histories_role.user_prefix,
assistant: promptTemplates.completion_model.conversation_histories_role.assistant_prefix,
})
}
}, [isChatModel])
// const [modelChanged, setModelChanged] = useState(false)
const {
currentProvider,
currentModel,
} = useModelListAndDefaultModelAndCurrentProviderAndModel(ModelTypeEnum.textGeneration)
const handleModelChanged = useCallback((model: { provider: string; modelId: string; mode?: string }) => {
const newInputs = produce(inputRef.current, (draft) => {
draft.model.provider = model.provider
draft.model.name = model.modelId
draft.model.mode = model.mode!
const isModeChange = model.mode !== inputRef.current.model?.mode
if (isModeChange && defaultConfig && Object.keys(defaultConfig).length > 0)
appendDefaultPromptConfig(draft, defaultConfig, model.mode === 'chat')
})
setInputs(newInputs)
// setModelChanged(true)
}, [setInputs, defaultConfig, appendDefaultPromptConfig])
useEffect(() => {
if (currentProvider?.provider && currentModel?.model && !model.provider) {
handleModelChanged({
provider: currentProvider?.provider,
modelId: currentModel?.model,
mode: currentModel?.model_properties?.mode as string,
})
}
}, [model?.provider, currentProvider, currentModel, handleModelChanged])
const {
currentModel: currModel,
} = useTextGenerationCurrentProviderAndModelAndModelList(
{
provider: model.provider,
model: model.name,
},
)
const isSupportFunctionCall = currModel?.features?.includes(ModelFeatureEnum.toolCall) || currModel?.features?.includes(ModelFeatureEnum.multiToolCall)
const filterInputVar = useCallback((varPayload: Var) => {
return [VarType.number, VarType.string].includes(varPayload.type)
}, [])
const {
availableVars,
availableNodesWithParent,
} = useAvailableVarList(id, {
onlyLeafNodeVar: false,
filterVar: filterInputVar,
})
const handleCompletionParamsChange = useCallback((newParams: Record<string, any>) => {
const newInputs = produce(inputs, (draft) => {
draft.model.completion_params = newParams
})
setInputs(newInputs)
}, [inputs, setInputs])
const handleInstructionChange = useCallback((newInstruction: string) => {
const newInputs = produce(inputs, (draft) => {
draft.instruction = newInstruction
})
setInputs(newInputs)
}, [inputs, setInputs])
const hasSetBlockStatus = {
history: false,
query: isChatMode ? checkHasQueryBlock(inputs.instruction) : false,
context: false,
}
const handleMemoryChange = useCallback((newMemory?: Memory) => {
const newInputs = produce(inputs, (draft) => {
draft.memory = newMemory
})
setInputs(newInputs)
}, [inputs, setInputs])
const handleReasoningModeChange = useCallback((newReasoningMode: ReasoningModeType) => {
const newInputs = produce(inputs, (draft) => {
draft.reasoning_mode = newReasoningMode
})
setInputs(newInputs)
}, [inputs, setInputs])
const handleImportFromTool = useCallback((params: Param[]) => {
const newInputs = produce(inputs, (draft) => {
draft.parameters = params
})
setInputs(newInputs)
}, [inputs, setInputs])
// single run
const {
isShowSingleRun,
hideSingleRun,
getInputVars,
runningStatus,
handleRun,
handleStop,
runInputData,
setRunInputData,
runResult,
} = useOneStepRun<ParameterExtractorNodeType>({
id,
data: inputs,
defaultRunInputData: {
query: '',
},
})
const varInputs = getInputVars([inputs.instruction])
const inputVarValues = (() => {
const vars: Record<string, any> = {}
Object.keys(runInputData)
.forEach((key) => {
vars[key] = runInputData[key]
})
return vars
})()
const setInputVarValues = useCallback((newPayload: Record<string, any>) => {
setRunInputData(newPayload)
}, [setRunInputData])
return {
readOnly,
handleInputVarChange,
filterVar,
isChatMode,
inputs,
isChatModel,
isCompletionModel,
handleModelChanged,
handleCompletionParamsChange,
handleImportFromTool,
handleExactParamsChange,
addExtractParameter,
handleInstructionChange,
hasSetBlockStatus,
availableVars,
availableNodesWithParent,
isSupportFunctionCall,
handleReasoningModeChange,
handleMemoryChange,
varInputs,
inputVarValues,
isShowSingleRun,
hideSingleRun,
runningStatus,
handleRun,
handleStop,
runResult,
setInputVarValues,
}
}
export default useConfig