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,43 @@
import { BlockEnum } from '../../types'
import type { NodeDefault } from '../../types'
import { CodeLanguage, type CodeNodeType } from './types'
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/constants'
const i18nPrefix = 'workflow.errorMsg'
const nodeDefault: NodeDefault<CodeNodeType> = {
defaultValue: {
code: '',
code_language: CodeLanguage.python3,
variables: [],
outputs: {},
},
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: CodeNodeType, t: any) {
let errorMessages = ''
const { code, variables } = payload
if (!errorMessages && variables.filter(v => !v.variable).length > 0)
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t(`${i18nPrefix}.fields.variable`) })
if (!errorMessages && variables.filter(v => !v.value_selector.length).length > 0)
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t(`${i18nPrefix}.fields.variableValue`) })
if (!errorMessages && !code)
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t(`${i18nPrefix}.fields.code`) })
return {
isValid: !errorMessages,
errorMessage: errorMessages,
}
},
}
export default nodeDefault

View File

@@ -0,0 +1,13 @@
import type { FC } from 'react'
import React from 'react'
import type { CodeNodeType } from './types'
import type { NodeProps } from '@/app/components/workflow/types'
const Node: FC<NodeProps<CodeNodeType>> = () => {
return (
// No summary content
<div></div>
)
}
export default React.memo(Node)

View File

@@ -0,0 +1,142 @@
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import RemoveEffectVarConfirm from '../_base/components/remove-effect-var-confirm'
import useConfig from './use-config'
import type { CodeNodeType } from './types'
import { CodeLanguage } from './types'
import VarList from '@/app/components/workflow/nodes/_base/components/variable/var-list'
import OutputVarList from '@/app/components/workflow/nodes/_base/components/variable/output-var-list'
import AddButton from '@/app/components/base/button/add-button'
import Field from '@/app/components/workflow/nodes/_base/components/field'
import Split from '@/app/components/workflow/nodes/_base/components/split'
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
import TypeSelector from '@/app/components/workflow/nodes/_base/components/selector'
import 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.code'
const codeLanguages = [
{
label: 'Python3',
value: CodeLanguage.python3,
},
{
label: 'JavaScript',
value: CodeLanguage.javascript,
},
]
const Panel: FC<NodePanelProps<CodeNodeType>> = ({
id,
data,
}) => {
const { t } = useTranslation()
const {
readOnly,
inputs,
outputKeyOrders,
handleVarListChange,
handleAddVariable,
handleRemoveVariable,
handleCodeChange,
handleCodeLanguageChange,
handleVarsChange,
handleAddOutputVariable,
filterVar,
isShowRemoveVarConfirm,
hideRemoveVarConfirm,
onRemoveVarConfirm,
// single run
isShowSingleRun,
hideSingleRun,
runningStatus,
handleRun,
handleStop,
runResult,
varInputs,
inputVarValues,
setInputVarValues,
} = useConfig(id, data)
return (
<div className='mt-2'>
<div className='px-4 pb-4 space-y-4'>
<Field
title={t(`${i18nPrefix}.inputVars`)}
operations={
!readOnly ? <AddButton onClick={handleAddVariable} /> : undefined
}
>
<VarList
readonly={readOnly}
nodeId={id}
list={inputs.variables}
onChange={handleVarListChange}
filterVar={filterVar}
/>
</Field>
<Split />
<CodeEditor
readOnly={readOnly}
title={
<TypeSelector
options={codeLanguages}
value={inputs.code_language}
onChange={handleCodeLanguageChange}
/>
}
language={inputs.code_language}
value={inputs.code}
onChange={handleCodeChange}
/>
</div>
<Split />
<div className='px-4 pt-4 pb-2'>
<Field
title={t(`${i18nPrefix}.outputVars`)}
operations={
<AddButton onClick={handleAddOutputVariable} />
}
>
<OutputVarList
readonly={readOnly}
outputs={inputs.outputs}
outputKeyOrders={outputKeyOrders}
onChange={handleVarsChange}
onRemove={handleRemoveVariable}
/>
</Field>
</div>
{
isShowSingleRun && (
<BeforeRunForm
nodeName={inputs.title}
onHide={hideSingleRun}
forms={[
{
inputs: varInputs,
values: inputVarValues,
onChange: setInputVarValues,
},
]}
runningStatus={runningStatus}
onRun={handleRun}
onStop={handleStop}
result={<ResultPanel {...runResult} showSteps={false} />}
/>
)
}
<RemoveEffectVarConfirm
isShow={isShowRemoveVarConfirm}
onCancel={hideRemoveVarConfirm}
onConfirm={onRemoveVarConfirm}
/>
</div >
)
}
export default React.memo(Panel)

View File

@@ -0,0 +1,19 @@
import type { CommonNodeType, VarType, Variable } from '@/app/components/workflow/types'
export enum CodeLanguage {
python3 = 'python3',
javascript = 'javascript',
json = 'json',
}
export type OutputVar = Record<string, {
type: VarType
children: null // support nest in the future,
}>
export type CodeNodeType = CommonNodeType & {
variables: Variable[]
code_language: CodeLanguage
code: string
outputs: OutputVar
}

View File

@@ -0,0 +1,169 @@
import { useCallback, useEffect, useState } from 'react'
import produce from 'immer'
import useVarList from '../_base/hooks/use-var-list'
import useOutputVarList from '../_base/hooks/use-output-var-list'
import { BlockEnum, VarType } from '../../types'
import type { Var } from '../../types'
import { useStore } from '../../store'
import type { CodeNodeType, OutputVar } from './types'
import { CodeLanguage } 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 { fetchNodeDefault } from '@/service/workflow'
import { useStore as useAppStore } from '@/app/components/app/store'
import {
useNodesReadOnly,
} from '@/app/components/workflow/hooks'
const useConfig = (id: string, payload: CodeNodeType) => {
const { nodesReadOnly: readOnly } = useNodesReadOnly()
const appId = useAppStore.getState().appDetail?.id
const [allLanguageDefault, setAllLanguageDefault] = useState<Record<CodeLanguage, CodeNodeType> | null>(null)
useEffect(() => {
if (appId) {
(async () => {
const { config: javaScriptConfig } = await fetchNodeDefault(appId, BlockEnum.Code, { code_language: CodeLanguage.javascript }) as any
const { config: pythonConfig } = await fetchNodeDefault(appId, BlockEnum.Code, { code_language: CodeLanguage.python3 }) as any
setAllLanguageDefault({
[CodeLanguage.javascript]: javaScriptConfig as CodeNodeType,
[CodeLanguage.python3]: pythonConfig as CodeNodeType,
} as any)
})()
}
}, [appId])
const defaultConfig = useStore(s => s.nodesDefaultConfigs)[payload.type]
const { inputs, setInputs } = useNodeCrud<CodeNodeType>(id, payload)
const { handleVarListChange, handleAddVariable } = useVarList<CodeNodeType>({
inputs,
setInputs,
})
const [outputKeyOrders, setOutputKeyOrders] = useState<string[]>([])
const syncOutputKeyOrders = useCallback((outputs: OutputVar) => {
setOutputKeyOrders(Object.keys(outputs))
}, [])
useEffect(() => {
if (inputs.code) {
if (inputs.outputs && Object.keys(inputs.outputs).length > 0)
syncOutputKeyOrders(inputs.outputs)
return
}
const isReady = defaultConfig && Object.keys(defaultConfig).length > 0
if (isReady) {
setInputs({
...inputs,
...defaultConfig,
})
syncOutputKeyOrders(defaultConfig.outputs)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [defaultConfig])
const handleCodeChange = useCallback((code: string) => {
const newInputs = produce(inputs, (draft) => {
draft.code = code
})
setInputs(newInputs)
}, [inputs, setInputs])
const handleCodeLanguageChange = useCallback((codeLanguage: CodeLanguage) => {
const currDefaultConfig = allLanguageDefault?.[codeLanguage]
const newInputs = produce(inputs, (draft) => {
draft.code_language = codeLanguage
if (!currDefaultConfig)
return
draft.code = currDefaultConfig.code
draft.variables = currDefaultConfig.variables
draft.outputs = currDefaultConfig.outputs
})
setInputs(newInputs)
}, [allLanguageDefault, inputs, setInputs])
const {
handleVarsChange,
handleAddVariable: handleAddOutputVariable,
handleRemoveVariable,
isShowRemoveVarConfirm,
hideRemoveVarConfirm,
onRemoveVarConfirm,
} = useOutputVarList<CodeNodeType>({
id,
inputs,
setInputs,
outputKeyOrders,
onOutputKeyOrdersChange: setOutputKeyOrders,
})
const filterVar = useCallback((varPayload: Var) => {
return [VarType.string, VarType.number, VarType.object, VarType.array, VarType.arrayNumber, VarType.arrayString, VarType.arrayObject].includes(varPayload.type)
}, [])
// single run
const {
isShowSingleRun,
hideSingleRun,
toVarInputs,
runningStatus,
isCompleted,
handleRun,
handleStop,
runInputData,
setRunInputData,
runResult,
} = useOneStepRun<CodeNodeType>({
id,
data: inputs,
defaultRunInputData: {},
})
const varInputs = toVarInputs(inputs.variables)
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,
inputs,
outputKeyOrders,
handleVarListChange,
handleAddVariable,
handleRemoveVariable,
handleCodeChange,
handleCodeLanguageChange,
handleVarsChange,
filterVar,
handleAddOutputVariable,
isShowRemoveVarConfirm,
hideRemoveVarConfirm,
onRemoveVarConfirm,
// single run
isShowSingleRun,
hideSingleRun,
runningStatus,
isCompleted,
handleRun,
handleStop,
varInputs,
inputVarValues,
setInputVarValues,
runResult,
}
}
export default useConfig

View File

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