FEAT: NEW WORKFLOW ENGINE (#3160)

Co-authored-by: Joel <iamjoel007@gmail.com>
Co-authored-by: Yeuoly <admin@srmxy.cn>
Co-authored-by: JzoNg <jzongcode@gmail.com>
Co-authored-by: StyleZhang <jasonapring2015@outlook.com>
Co-authored-by: jyong <jyong@dify.ai>
Co-authored-by: nite-knite <nkCoding@gmail.com>
Co-authored-by: jyong <718720800@qq.com>
This commit is contained in:
takatost
2024-04-08 18:51:46 +08:00
committed by GitHub
parent 2fb9850af5
commit 7753ba2d37
1161 changed files with 103836 additions and 10327 deletions

View File

@@ -0,0 +1,57 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import TextEditor from '../../_base/components/editor/text-editor'
import MemoryConfig from '../../_base/components/memory-config'
import type { Memory } from '@/app/components/workflow/types'
const i18nPrefix = 'workflow.nodes.questionClassifiers'
type Props = {
instruction: string
onInstructionChange: (instruction: string) => void
hideMemorySetting: boolean
memory?: Memory
onMemoryChange: (memory?: Memory) => void
readonly?: boolean
}
const AdvancedSetting: FC<Props> = ({
instruction,
onInstructionChange,
hideMemorySetting,
memory,
onMemoryChange,
readonly,
}) => {
const { t } = useTranslation()
return (
<>
<TextEditor
title={t(`${i18nPrefix}.instruction`)!}
value={instruction}
onChange={onInstructionChange}
minHeight={160}
placeholder={t(`${i18nPrefix}.instructionPlaceholder`)!}
headerRight={(
<div className='flex items-center h-full'>
<div className='text-xs font-medium text-gray-500'>{instruction?.length || 0}</div>
<div className='mx-3 h-3 w-px bg-gray-200'></div>
</div>
)}
readonly={readonly}
/>
{!hideMemorySetting && (
<MemoryConfig
className='mt-4'
readonly={false}
config={{ data: memory }}
onChange={onMemoryChange}
canSetRoleName={false}
/>
)}
</>
)
}
export default React.memo(AdvancedSetting)

View File

@@ -0,0 +1,63 @@
'use client'
import type { FC } from 'react'
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import type { Topic } from '../types'
import TextEditor from '../../_base/components/editor/text-editor'
import { Trash03 } from '@/app/components/base/icons/src/vender/line/general'
const i18nPrefix = 'workflow.nodes.questionClassifiers'
type Props = {
payload: Topic
onChange: (payload: Topic) => void
onRemove: () => void
index: number
readonly?: boolean
}
const ClassItem: FC<Props> = ({
payload,
onChange,
onRemove,
index,
readonly,
}) => {
const { t } = useTranslation()
const handleNameChange = useCallback((value: string) => {
onChange({ ...payload, name: value })
}, [onChange, payload])
return (
<TextEditor
title={<div>
<div className='w-[200px]'>
<div
className='leading-4 text-xs font-semibold text-gray-700'
>
{`${t(`${i18nPrefix}.class`)} ${index}`}
</div>
</div>
</div>}
value={payload.name}
onChange={handleNameChange}
placeholder={t(`${i18nPrefix}.topicPlaceholder`)!}
headerRight={(
<div className='flex items-center h-full'>
<div className='text-xs font-medium text-gray-500'>{payload.name.length}</div>
<div className='mx-3 h-3 w-px bg-gray-200'></div>
{!readonly && (
<Trash03
className='mr-1 w-3.5 h-3.5 text-gray-500 cursor-pointer'
onClick={onRemove}
/>
)}
</div>
)}
readonly={readonly}
minHeight={64}
/>
)
}
export default React.memo(ClassItem)

View File

@@ -0,0 +1,82 @@
'use client'
import type { FC } from 'react'
import React, { useCallback } from 'react'
import produce from 'immer'
import { useTranslation } from 'react-i18next'
import { useEdgesInteractions } from '../../../hooks'
import AddButton from '../../_base/components/add-button'
import Item from './class-item'
import type { Topic } from '@/app/components/workflow/nodes/question-classifier/types'
const i18nPrefix = 'workflow.nodes.questionClassifiers'
type Props = {
id: string
list: Topic[]
onChange: (list: Topic[]) => void
readonly?: boolean
}
const ClassList: FC<Props> = ({
id,
list,
onChange,
readonly,
}) => {
const { t } = useTranslation()
const { handleEdgeDeleteByDeleteBranch } = useEdgesInteractions()
const handleClassChange = useCallback((index: number) => {
return (value: Topic) => {
const newList = produce(list, (draft) => {
draft[index] = value
})
onChange(newList)
}
}, [list, onChange])
const handleAddClass = useCallback(() => {
const newList = produce(list, (draft) => {
draft.push({ id: `${Date.now()}`, name: '' })
})
onChange(newList)
}, [list, onChange])
const handleRemoveClass = useCallback((index: number) => {
return () => {
handleEdgeDeleteByDeleteBranch(id, list[index].id)
const newList = produce(list, (draft) => {
draft.splice(index, 1)
})
onChange(newList)
}
}, [list, onChange, handleEdgeDeleteByDeleteBranch, id])
// Todo Remove; edit topic name
return (
<div className='space-y-2'>
{
list.map((item, index) => {
return (
<Item
key={index}
payload={item}
onChange={handleClassChange(index)}
onRemove={handleRemoveClass(index)}
index={index + 1}
readonly={readonly}
/>
)
})
}
{!readonly && (
<AddButton
onClick={handleAddClass}
text={t(`${i18nPrefix}.addClass`)}
/>
)}
</div>
)
}
export default React.memo(ClassList)

View File

@@ -0,0 +1,60 @@
import type { NodeDefault } from '../../types'
import { BlockEnum } from '../../types'
import type { QuestionClassifierNodeType } from './types'
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/constants'
const i18nPrefix = 'workflow'
const nodeDefault: NodeDefault<QuestionClassifierNodeType> = {
defaultValue: {
query_variable_selector: [],
model: {
provider: '',
name: '',
mode: 'chat',
completion_params: {
temperature: 0.7,
},
},
classes: [
{
id: '1',
name: '',
},
{
id: '2',
name: '',
},
],
},
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.filter(type => type !== BlockEnum.VariableAssigner)
},
checkValid(payload: QuestionClassifierNodeType, t: any) {
let errorMessages = ''
if (!errorMessages && (!payload.query_variable_selector || payload.query_variable_selector.length === 0))
errorMessages = t(`${i18nPrefix}.errorMsg.fieldRequired`, { field: t(`${i18nPrefix}.nodes.questionClassifiers.inputVars`) })
if (!errorMessages && !payload.model.provider)
errorMessages = t(`${i18nPrefix}.errorMsg.fieldRequired`, { field: t(`${i18nPrefix}.nodes.questionClassifiers.model`) })
if (!errorMessages && (!payload.classes || payload.classes.length === 0))
errorMessages = t(`${i18nPrefix}.errorMsg.fieldRequired`, { field: t(`${i18nPrefix}.nodes.questionClassifiers.class`) })
if (!errorMessages && (payload.classes.some(item => !item.name)))
errorMessages = t(`${i18nPrefix}.errorMsg.fieldRequired`, { field: t(`${i18nPrefix}.nodes.questionClassifiers.topicName`) })
return {
isValid: !errorMessages,
errorMessage: errorMessages,
}
},
}
export default nodeDefault

View File

@@ -0,0 +1,65 @@
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import type { NodeProps } from 'reactflow'
import InfoPanel from '../_base/components/info-panel'
import { NodeSourceHandle } from '../_base/components/node-handle'
import type { QuestionClassifierNodeType } 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'
const i18nPrefix = 'workflow.nodes.questionClassifiers'
const Node: FC<NodeProps<QuestionClassifierNodeType>> = (props) => {
const { t } = useTranslation()
const { data } = props
const { provider, name: modelId } = data.model
// const tempTopics = data.topics
const topics = data.classes
const {
textGenerationModelList,
} = useTextGenerationCurrentProviderAndModelAndModelList()
const hasSetModel = provider && modelId
if (!hasSetModel && !topics.length)
return null
return (
<div className='mb-1 px-3 py-1'>
{hasSetModel && (
<ModelSelector
defaultModel={{ provider, model: modelId }}
modelList={textGenerationModelList}
readonly
/>
)}
{
!!topics.length && (
<div className='mt-2 space-y-0.5'>
{topics.map((topic, index) => (
<div
key={index}
className='relative'
>
<InfoPanel
title={`${t(`${i18nPrefix}.class`)} ${index + 1}`}
content={topic.name}
/>
<NodeSourceHandle
{...props}
handleId={topic.id}
handleClassName='!top-1/2 !-translate-y-1/2 !-right-[21px]'
/>
</div>
))}
</div>
)
}
</div>
)
}
export default React.memo(Node)

View File

@@ -0,0 +1,129 @@
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import VarReferencePicker from '../_base/components/variable/var-reference-picker'
import useConfig from './use-config'
import ClassList from './components/class-list'
import AdvancedSetting from './components/advanced-setting'
import type { QuestionClassifierNodeType } from './types'
import Field from '@/app/components/workflow/nodes/_base/components/field'
import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal'
import { InputVarType, type NodePanelProps } from '@/app/components/workflow/types'
import BeforeRunForm from '@/app/components/workflow/nodes/_base/components/before-run-form'
import ResultPanel from '@/app/components/workflow/run/result-panel'
const i18nPrefix = 'workflow.nodes.questionClassifiers'
const Panel: FC<NodePanelProps<QuestionClassifierNodeType>> = ({
id,
data,
}) => {
const { t } = useTranslation()
const {
readOnly,
inputs,
handleModelChanged,
isChatMode,
handleCompletionParamsChange,
handleQueryVarChange,
handleTopicsChange,
handleInstructionChange,
handleMemoryChange,
isShowSingleRun,
hideSingleRun,
runningStatus,
handleRun,
handleStop,
query,
setQuery,
runResult,
filterVar,
} = useConfig(id, data)
const model = inputs.model
return (
<div>
<div className='mt-2 px-4 space-y-4'>
<Field
title={t(`${i18nPrefix}.inputVars`)}
>
<VarReferencePicker
readonly={readOnly}
isShowNodeName
nodeId={id}
value={inputs.query_variable_selector}
onChange={handleQueryVarChange}
filterVar={filterVar}
/>
</Field>
<Field
title={t(`${i18nPrefix}.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}.class`)}
>
<ClassList
id={id}
list={inputs.classes}
onChange={handleTopicsChange}
readonly={readOnly}
/>
</Field>
<Field
title={t(`${i18nPrefix}.advancedSetting`)}
supportFold
>
<AdvancedSetting
hideMemorySetting={!isChatMode}
instruction={inputs.instruction}
onInstructionChange={handleInstructionChange}
memory={inputs.memory}
onMemoryChange={handleMemoryChange}
readonly={readOnly}
/>
</Field>
</div>
{isShowSingleRun && (
<BeforeRunForm
nodeName={inputs.title}
onHide={hideSingleRun}
forms={[
{
inputs: [{
label: t(`${i18nPrefix}.inputVars`)!,
variable: 'query',
type: InputVarType.paragraph,
required: true,
}],
values: { query },
onChange: keyValue => setQuery((keyValue as any).query),
},
]}
runningStatus={runningStatus}
onRun={handleRun}
onStop={handleStop}
result={<ResultPanel {...runResult} showSteps={false} />}
/>
)}
</div>
)
}
export default React.memo(Panel)

View File

@@ -0,0 +1,14 @@
import type { CommonNodeType, Memory, ModelConfig, ValueSelector } from '@/app/components/workflow/types'
export type Topic = {
id: string
name: string
}
export type QuestionClassifierNodeType = CommonNodeType & {
query_variable_selector: ValueSelector
model: ModelConfig
classes: Topic[]
instruction: string
memory?: Memory
}

View File

@@ -0,0 +1,163 @@
import { useCallback, useEffect, useRef } from 'react'
import produce from 'immer'
import { BlockEnum, VarType } from '../../types'
import type { Memory, ValueSelector, Var } from '../../types'
import {
useIsChatMode, useNodesReadOnly,
useWorkflow,
} from '../../hooks'
import { useStore } from '../../store'
import type { QuestionClassifierNodeType } from './types'
import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud'
import useOneStepRun from '@/app/components/workflow/nodes/_base/hooks/use-one-step-run'
import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
const useConfig = (id: string, payload: QuestionClassifierNodeType) => {
const { nodesReadOnly: readOnly } = useNodesReadOnly()
const isChatMode = useIsChatMode()
const defaultConfig = useStore(s => s.nodesDefaultConfigs)[payload.type]
const { getBeforeNodesInSameBranch } = useWorkflow()
const startNode = getBeforeNodesInSameBranch(id).find(node => node.data.type === BlockEnum.Start)
const startNodeId = startNode?.id
const { inputs, setInputs } = useNodeCrud<QuestionClassifierNodeType>(id, payload)
const inputRef = useRef(inputs)
useEffect(() => {
inputRef.current = inputs
}, [inputs])
// model
const {
currentProvider,
currentModel,
} = useModelListAndDefaultModelAndCurrentProviderAndModel(ModelTypeEnum.textGeneration)
const model = inputs.model
const modelMode = inputs.model?.mode
const isChatModel = modelMode === 'chat'
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!
})
setInputs(newInputs)
}, [setInputs])
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 handleCompletionParamsChange = useCallback((newParams: Record<string, any>) => {
const newInputs = produce(inputs, (draft) => {
draft.model.completion_params = newParams
})
setInputs(newInputs)
}, [inputs, setInputs])
const handleQueryVarChange = useCallback((newVar: ValueSelector | string) => {
const newInputs = produce(inputs, (draft) => {
draft.query_variable_selector = newVar as ValueSelector
})
setInputs(newInputs)
// console.log(newInputs.query_variable_selector)
}, [inputs, setInputs])
useEffect(() => {
const isReady = defaultConfig && Object.keys(defaultConfig).length > 0
if (isReady) {
let query_variable_selector: ValueSelector = []
if (isChatMode && inputs.query_variable_selector.length === 0 && startNodeId)
query_variable_selector = [startNodeId, 'sys.query']
setInputs({
...inputs,
...defaultConfig,
query_variable_selector: inputs.query_variable_selector.length > 0 ? inputs.query_variable_selector : query_variable_selector,
})
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [defaultConfig])
const handleClassesChange = useCallback((newClasses: any) => {
const newInputs = produce(inputs, (draft) => {
draft.classes = newClasses
draft._targetBranches = newClasses
})
setInputs(newInputs)
}, [inputs, setInputs])
const handleInstructionChange = useCallback((instruction: string) => {
const newInputs = produce(inputs, (draft) => {
draft.instruction = instruction
})
setInputs(newInputs)
}, [inputs, setInputs])
const handleMemoryChange = useCallback((memory?: Memory) => {
const newInputs = produce(inputs, (draft) => {
draft.memory = memory
})
setInputs(newInputs)
}, [inputs, setInputs])
// single run
const {
isShowSingleRun,
hideSingleRun,
runningStatus,
handleRun,
handleStop,
runInputData,
setRunInputData,
runResult,
} = useOneStepRun<QuestionClassifierNodeType>({
id,
data: inputs,
defaultRunInputData: {
query: '',
},
})
const query = runInputData.query
const setQuery = useCallback((newQuery: string) => {
setRunInputData({
...runInputData,
query: newQuery,
})
}, [runInputData, setRunInputData])
const filterVar = useCallback((varPayload: Var) => {
return varPayload.type === VarType.string
}, [])
return {
readOnly,
inputs,
handleModelChanged,
isChatMode,
isChatModel,
handleCompletionParamsChange,
handleQueryVarChange,
filterVar,
handleTopicsChange: handleClassesChange,
handleInstructionChange,
handleMemoryChange,
isShowSingleRun,
hideSingleRun,
runningStatus,
handleRun,
handleStop,
query,
setQuery,
runResult,
}
}
export default useConfig

View File

@@ -0,0 +1,5 @@
import type { QuestionClassifierNodeType } from './types'
export const checkNodeValid = (payload: QuestionClassifierNodeType) => {
return true
}