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,30 @@
import {
useIsChatMode,
useWorkflow,
} from '@/app/components/workflow/hooks'
import { toNodeOutputVars } from '@/app/components/workflow/nodes/_base/components/variable/utils'
import type { ValueSelector, Var } from '@/app/components/workflow/types'
type Params = {
onlyLeafNodeVar?: boolean
filterVar: (payload: Var, selector: ValueSelector) => boolean
}
const useAvailableVarList = (nodeId: string, {
onlyLeafNodeVar,
filterVar,
}: Params = {
onlyLeafNodeVar: false,
filterVar: () => true,
}) => {
const { getTreeLeafNodes, getBeforeNodesInSameBranch } = useWorkflow()
const isChatMode = useIsChatMode()
const availableNodes = onlyLeafNodeVar ? getTreeLeafNodes(nodeId) : getBeforeNodesInSameBranch(nodeId)
const availableVars = toNodeOutputVars(availableNodes, isChatMode, filterVar)
return {
availableVars,
availableNodes,
}
}
export default useAvailableVarList

View File

@@ -0,0 +1,19 @@
import { useNodeDataUpdate } from '@/app/components/workflow/hooks'
import type { CommonNodeType } from '@/app/components/workflow/types'
const useNodeCrud = <T>(id: string, data: CommonNodeType<T>) => {
const { handleNodeDataUpdateWithSyncDraft } = useNodeDataUpdate()
const setInputs = (newInputs: CommonNodeType<T>) => {
handleNodeDataUpdateWithSyncDraft({
id,
data: newInputs,
})
}
return {
inputs: data,
setInputs,
}
}
export default useNodeCrud

View File

@@ -0,0 +1,275 @@
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { unionBy } from 'lodash-es'
import {
useIsChatMode,
useNodeDataUpdate,
useWorkflow,
} from '@/app/components/workflow/hooks'
import { getNodeInfoById, isSystemVar, toNodeOutputVars } from '@/app/components/workflow/nodes/_base/components/variable/utils'
import type { CommonNodeType, InputVar, ValueSelector, Var, Variable } from '@/app/components/workflow/types'
import { BlockEnum, InputVarType, NodeRunningStatus, VarType } from '@/app/components/workflow/types'
import { useStore as useAppStore } from '@/app/components/app/store'
import { singleNodeRun } from '@/service/workflow'
import Toast from '@/app/components/base/toast'
import LLMDefault from '@/app/components/workflow/nodes/llm/default'
import KnowledgeRetrievalDefault from '@/app/components/workflow/nodes/knowledge-retrieval/default'
import IfElseDefault from '@/app/components/workflow/nodes/if-else/default'
import CodeDefault from '@/app/components/workflow/nodes/code/default'
import TemplateTransformDefault from '@/app/components/workflow/nodes/template-transform/default'
import QuestionClassifyDefault from '@/app/components/workflow/nodes/question-classifier/default'
import HTTPDefault from '@/app/components/workflow/nodes/http/default'
import ToolDefault from '@/app/components/workflow/nodes/tool/default'
import VariableAssigner from '@/app/components/workflow/nodes/variable-assigner/default'
import { getInputVars as doGetInputVars } from '@/app/components/base/prompt-editor/constants'
const { checkValid: checkLLMValid } = LLMDefault
const { checkValid: checkKnowledgeRetrievalValid } = KnowledgeRetrievalDefault
const { checkValid: checkIfElseValid } = IfElseDefault
const { checkValid: checkCodeValid } = CodeDefault
const { checkValid: checkTemplateTransformValid } = TemplateTransformDefault
const { checkValid: checkQuestionClassifyValid } = QuestionClassifyDefault
const { checkValid: checkHttpValid } = HTTPDefault
const { checkValid: checkToolValid } = ToolDefault
const { checkValid: checkVariableAssignerValid } = VariableAssigner
const checkValidFns: Record<BlockEnum, Function> = {
[BlockEnum.LLM]: checkLLMValid,
[BlockEnum.KnowledgeRetrieval]: checkKnowledgeRetrievalValid,
[BlockEnum.IfElse]: checkIfElseValid,
[BlockEnum.Code]: checkCodeValid,
[BlockEnum.TemplateTransform]: checkTemplateTransformValid,
[BlockEnum.QuestionClassifier]: checkQuestionClassifyValid,
[BlockEnum.HttpRequest]: checkHttpValid,
[BlockEnum.Tool]: checkToolValid,
[BlockEnum.VariableAssigner]: checkVariableAssignerValid,
} as any
type Params<T> = {
id: string
data: CommonNodeType<T>
defaultRunInputData: Record<string, any>
moreDataForCheckValid?: any
}
const varTypeToInputVarType = (type: VarType, {
isSelect,
isParagraph,
}: {
isSelect: boolean
isParagraph: boolean
}) => {
if (isSelect)
return InputVarType.select
if (isParagraph)
return InputVarType.paragraph
if (type === VarType.number)
return InputVarType.number
if ([VarType.object, VarType.array, VarType.arrayNumber, VarType.arrayString, VarType.arrayObject].includes(type))
return InputVarType.json
if (type === VarType.arrayFile)
return InputVarType.files
return InputVarType.textInput
}
const useOneStepRun = <T>({
id,
data,
defaultRunInputData,
moreDataForCheckValid,
}: Params<T>) => {
const { t } = useTranslation()
const { getBeforeNodesInSameBranch } = useWorkflow() as any
const isChatMode = useIsChatMode()
const availableNodes = getBeforeNodesInSameBranch(id)
const allOutputVars = toNodeOutputVars(getBeforeNodesInSameBranch(id), isChatMode)
const getVar = (valueSelector: ValueSelector): Var | undefined => {
let res: Var | undefined
const isSystem = valueSelector[0] === 'sys'
const targetVar = isSystem ? allOutputVars.find(item => !!item.isStartNode) : allOutputVars.find(v => v.nodeId === valueSelector[0])
if (!targetVar)
return undefined
if (isSystem)
return targetVar.vars.find(item => item.variable.split('.')[1] === valueSelector[1])
let curr: any = targetVar.vars
valueSelector.slice(1).forEach((key, i) => {
const isLast = i === valueSelector.length - 2
curr = curr.find((v: any) => v.variable === key)
if (isLast) {
res = curr
}
else {
if (curr.type === VarType.object)
curr = curr.children
}
})
return res
}
const checkValid = checkValidFns[data.type]
const appId = useAppStore.getState().appDetail?.id
const [runInputData, setRunInputData] = useState<Record<string, any>>(defaultRunInputData || {})
const [runResult, setRunResult] = useState<any>(null)
const { handleNodeDataUpdate }: { handleNodeDataUpdate: (data: any) => void } = useNodeDataUpdate()
const [canShowSingleRun, setCanShowSingleRun] = useState(false)
const isShowSingleRun = data._isSingleRun && canShowSingleRun
useEffect(() => {
if (!checkValid) {
setCanShowSingleRun(true)
return
}
if (data._isSingleRun) {
const { isValid, errorMessage } = checkValid(data, t, moreDataForCheckValid)
setCanShowSingleRun(isValid)
if (!isValid) {
handleNodeDataUpdate({
id,
data: {
...data,
_isSingleRun: false,
},
})
Toast.notify({
type: 'error',
message: errorMessage,
})
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data._isSingleRun])
const hideSingleRun = () => {
handleNodeDataUpdate({
id,
data: {
...data,
_isSingleRun: false,
},
})
}
const runningStatus = data._singleRunningStatus || NodeRunningStatus.NotStart
const isCompleted = runningStatus === NodeRunningStatus.Succeeded || runningStatus === NodeRunningStatus.Failed
const handleRun = async (submitData: Record<string, any>) => {
handleNodeDataUpdate({
id,
data: {
...data,
_singleRunningStatus: NodeRunningStatus.Running,
},
})
let res: any
try {
res = await singleNodeRun(appId!, id, { inputs: submitData }) as any
if (res.error)
throw new Error(res.error)
}
catch (e: any) {
handleNodeDataUpdate({
id,
data: {
...data,
_singleRunningStatus: NodeRunningStatus.Failed,
},
})
return false
}
finally {
setRunResult({
...res,
created_by: res.created_by_account?.name || '',
})
}
handleNodeDataUpdate({
id,
data: {
...data,
_singleRunningStatus: NodeRunningStatus.Succeeded,
},
})
}
const handleStop = () => {
handleNodeDataUpdate({
id,
data: {
...data,
_singleRunningStatus: NodeRunningStatus.NotStart,
},
})
}
const toVarInputs = (variables: Variable[]): InputVar[] => {
if (!variables)
return []
const varInputs = variables.map((item) => {
const originalVar = getVar(item.value_selector)
if (!originalVar) {
return {
label: item.label || item.variable,
variable: item.variable,
type: InputVarType.textInput,
required: true,
}
}
return {
label: item.label || item.variable,
variable: item.variable,
type: varTypeToInputVarType(originalVar.type, {
isSelect: !!originalVar.isSelect,
isParagraph: !!originalVar.isParagraph,
}),
required: item.required !== false,
options: originalVar.options,
}
})
return varInputs
}
const getInputVars = (textList: string[]) => {
const valueSelectors: ValueSelector[] = []
textList.forEach((text) => {
valueSelectors.push(...doGetInputVars(text))
})
const variables = unionBy(valueSelectors, item => item.join('.')).map((item) => {
const varInfo = getNodeInfoById(availableNodes, item[0])?.data
return {
label: {
nodeType: varInfo?.type,
nodeName: varInfo?.title || availableNodes[0]?.data.title, // default start node title
variable: isSystemVar(item) ? item.join('.') : item[item.length - 1],
},
variable: `#${item.join('.')}#`,
value_selector: item,
}
})
const varInputs = toVarInputs(variables)
return varInputs
}
return {
isShowSingleRun,
hideSingleRun,
toVarInputs,
getInputVars,
runningStatus,
isCompleted,
handleRun,
handleStop,
runInputData,
setRunInputData,
runResult,
}
}
export default useOneStepRun

View File

@@ -0,0 +1,96 @@
import { useCallback, useState } from 'react'
import produce from 'immer'
import { useBoolean } from 'ahooks'
import { type OutputVar } from '../../code/types'
import type { ValueSelector } from '@/app/components/workflow/types'
import { VarType } from '@/app/components/workflow/types'
import {
useWorkflow,
} from '@/app/components/workflow/hooks'
type Params<T> = {
id: string
inputs: T
setInputs: (newInputs: T) => void
varKey?: string
outputKeyOrders: string[]
onOutputKeyOrdersChange: (newOutputKeyOrders: string[]) => void
}
function useOutputVarList<T>({
id,
inputs,
setInputs,
varKey = 'outputs',
outputKeyOrders = [],
onOutputKeyOrdersChange,
}: Params<T>) {
const { handleOutVarRenameChange, isVarUsedInNodes, removeUsedVarInNodes } = useWorkflow()
const handleVarsChange = useCallback((newVars: OutputVar, changedIndex?: number, newKey?: string) => {
const newInputs = produce(inputs, (draft: any) => {
draft[varKey] = newVars
})
setInputs(newInputs)
if (changedIndex !== undefined) {
const newOutputKeyOrders = produce(outputKeyOrders, (draft) => {
draft[changedIndex] = newKey!
})
onOutputKeyOrdersChange(newOutputKeyOrders)
}
if (newKey)
handleOutVarRenameChange(id, [id, outputKeyOrders[changedIndex!]], [id, newKey])
}, [inputs, setInputs, handleOutVarRenameChange, id, outputKeyOrders, varKey, onOutputKeyOrdersChange])
const handleAddVariable = useCallback(() => {
const newKey = `var_${Object.keys((inputs as any)[varKey]).length + 1}`
const newInputs = produce(inputs, (draft: any) => {
draft[varKey] = {
...draft[varKey],
[newKey]: {
type: VarType.string,
children: null,
},
}
})
setInputs(newInputs)
onOutputKeyOrdersChange([...outputKeyOrders, newKey])
}, [inputs, setInputs, varKey, outputKeyOrders, onOutputKeyOrdersChange])
const [isShowRemoveVarConfirm, {
setTrue: showRemoveVarConfirm,
setFalse: hideRemoveVarConfirm,
}] = useBoolean(false)
const [removedVar, setRemovedVar] = useState<ValueSelector>([])
const removeVarInNode = useCallback(() => {
removeUsedVarInNodes(removedVar)
hideRemoveVarConfirm()
}, [hideRemoveVarConfirm, removeUsedVarInNodes, removedVar])
const handleRemoveVariable = useCallback((index: number) => {
const key = outputKeyOrders[index]
if (isVarUsedInNodes([id, key])) {
showRemoveVarConfirm()
setRemovedVar([id, key])
return
}
const newInputs = produce(inputs, (draft: any) => {
delete draft[varKey][key]
})
setInputs(newInputs)
onOutputKeyOrdersChange(outputKeyOrders.filter((_, i) => i !== index))
}, [outputKeyOrders, isVarUsedInNodes, id, inputs, setInputs, onOutputKeyOrdersChange, showRemoveVarConfirm, varKey])
return {
handleVarsChange,
handleAddVariable,
handleRemoveVariable,
isShowRemoveVarConfirm,
hideRemoveVarConfirm,
onRemoveVarConfirm: removeVarInNode,
}
}
export default useOutputVarList

View File

@@ -0,0 +1,116 @@
import {
useCallback,
useEffect,
useRef,
useState,
} from 'react'
export type UseResizePanelPrarams = {
direction?: 'horizontal' | 'vertical' | 'both'
triggerDirection?: 'top' | 'right' | 'bottom' | 'left' | 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left'
minWidth?: number
maxWidth?: number
minHeight?: number
maxHeight?: number
onResized?: (width: number, height: number) => void
}
export const useResizePanel = (params?: UseResizePanelPrarams) => {
const {
direction = 'both',
triggerDirection = 'bottom-right',
minWidth = -Infinity,
maxWidth = Infinity,
minHeight = -Infinity,
maxHeight = Infinity,
onResized,
} = params || {}
const triggerRef = useRef<HTMLDivElement>(null)
const containerRef = useRef<HTMLDivElement>(null)
const initXRef = useRef(0)
const initYRef = useRef(0)
const initContainerWidthRef = useRef(0)
const initContainerHeightRef = useRef(0)
const isResizingRef = useRef(false)
const [prevUserSelectStyle, setPrevUserSelectStyle] = useState(getComputedStyle(document.body).userSelect)
const handleStartResize = useCallback((e: MouseEvent) => {
initXRef.current = e.clientX
initYRef.current = e.clientY
initContainerWidthRef.current = containerRef.current?.offsetWidth || minWidth
initContainerHeightRef.current = containerRef.current?.offsetHeight || minHeight
isResizingRef.current = true
setPrevUserSelectStyle(getComputedStyle(document.body).userSelect)
document.body.style.userSelect = 'none'
}, [minWidth, minHeight])
const handleResize = useCallback((e: MouseEvent) => {
if (!isResizingRef.current)
return
if (!containerRef.current)
return
if (direction === 'horizontal' || direction === 'both') {
const offsetX = e.clientX - initXRef.current
let width = 0
if (triggerDirection === 'left' || triggerDirection === 'top-left' || triggerDirection === 'bottom-left')
width = initContainerWidthRef.current - offsetX
else if (triggerDirection === 'right' || triggerDirection === 'top-right' || triggerDirection === 'bottom-right')
width = initContainerWidthRef.current + offsetX
if (width < minWidth)
width = minWidth
if (width > maxWidth)
width = maxWidth
containerRef.current.style.width = `${width}px`
}
if (direction === 'vertical' || direction === 'both') {
const offsetY = e.clientY - initYRef.current
let height = 0
if (triggerDirection === 'top' || triggerDirection === 'top-left' || triggerDirection === 'top-right')
height = initContainerHeightRef.current - offsetY
else if (triggerDirection === 'bottom' || triggerDirection === 'bottom-left' || triggerDirection === 'bottom-right')
height = initContainerHeightRef.current + offsetY
if (height < minHeight)
height = minHeight
if (height > maxHeight)
height = maxHeight
containerRef.current.style.height = `${height}px`
}
}, [
direction,
triggerDirection,
minWidth,
maxWidth,
minHeight,
maxHeight,
])
const handleStopResize = useCallback(() => {
isResizingRef.current = false
document.body.style.userSelect = prevUserSelectStyle
if (onResized && containerRef.current)
onResized(containerRef.current.offsetWidth, containerRef.current.offsetHeight)
}, [prevUserSelectStyle, onResized])
useEffect(() => {
const element = triggerRef.current
element?.addEventListener('mousedown', handleStartResize)
document.addEventListener('mousemove', handleResize)
document.addEventListener('mouseup', handleStopResize)
return () => {
if (element)
element.removeEventListener('mousedown', handleStartResize)
document.removeEventListener('mousemove', handleResize)
}
}, [handleStartResize, handleResize, handleStopResize])
return {
triggerRef,
containerRef,
}
}

View File

@@ -0,0 +1,26 @@
import { useEffect, useState } from 'react'
type Params = {
ref: React.RefObject<HTMLDivElement>
hasFooter?: boolean
}
const useToggleExpend = ({ ref, hasFooter = true }: Params) => {
const [isExpand, setIsExpand] = useState(false)
const [wrapHeight, setWrapHeight] = useState(ref.current?.clientHeight)
const editorExpandHeight = isExpand ? wrapHeight! - (hasFooter ? 56 : 29) : 0
useEffect(() => {
setWrapHeight(ref.current?.clientHeight)
}, [isExpand])
const wrapClassName = isExpand && 'absolute z-10 left-4 right-6 top-[52px] bottom-0 pb-4 bg-white'
return {
wrapClassName,
editorExpandHeight,
isExpand,
setIsExpand,
}
}
export default useToggleExpend

View File

@@ -0,0 +1,37 @@
import { useCallback } from 'react'
import produce from 'immer'
import type { Variable } from '@/app/components/workflow/types'
type Params<T> = {
inputs: T
setInputs: (newInputs: T) => void
varKey?: string
}
function useVarList<T>({
inputs,
setInputs,
varKey = 'variables',
}: Params<T>) {
const handleVarListChange = useCallback((newList: Variable[] | string) => {
const newInputs = produce(inputs, (draft: any) => {
draft[varKey] = newList as Variable[]
})
setInputs(newInputs)
}, [inputs, setInputs, varKey])
const handleAddVariable = useCallback(() => {
const newInputs = produce(inputs, (draft: any) => {
draft[varKey].push({
variable: '',
value_selector: [],
})
})
setInputs(newInputs)
}, [inputs, setInputs, varKey])
return {
handleVarListChange,
handleAddVariable,
}
}
export default useVarList