Feat/loop break node (#17268)
This commit is contained in:
@@ -14,6 +14,9 @@ const HelpLink = ({
|
||||
const { t } = useTranslation()
|
||||
const link = useNodeHelpLink(nodeType)
|
||||
|
||||
if (!link)
|
||||
return null
|
||||
|
||||
return (
|
||||
<TooltipPlus
|
||||
popupContent={t('common.userProfile.helpCenter')}
|
||||
|
||||
@@ -164,7 +164,7 @@ const PanelOperatorPopup = ({
|
||||
)
|
||||
}
|
||||
{
|
||||
showHelpLink && (
|
||||
showHelpLink && link && (
|
||||
<>
|
||||
<div className='p-1'>
|
||||
<a
|
||||
|
||||
@@ -58,7 +58,8 @@ const VariableTag = ({
|
||||
{node && (
|
||||
<>
|
||||
<VarBlockIcon
|
||||
type={BlockEnum.Start}
|
||||
type={node.data.type || BlockEnum.Start}
|
||||
className='mr-0.5'
|
||||
/>
|
||||
<div
|
||||
className='max-w-[60px] truncate font-medium text-text-secondary'
|
||||
|
||||
@@ -3,7 +3,7 @@ import { isArray, uniq } from 'lodash-es'
|
||||
import type { CodeNodeType } from '../../../code/types'
|
||||
import type { EndNodeType } from '../../../end/types'
|
||||
import type { AnswerNodeType } from '../../../answer/types'
|
||||
import type { LLMNodeType } from '../../../llm/types'
|
||||
import { type LLMNodeType, type StructuredOutput, Type } from '../../../llm/types'
|
||||
import type { KnowledgeRetrievalNodeType } from '../../../knowledge-retrieval/types'
|
||||
import type { IfElseNodeType } from '../../../if-else/types'
|
||||
import type { TemplateTransformNodeType } from '../../../template-transform/types'
|
||||
@@ -21,6 +21,8 @@ import { BlockEnum, InputVarType, VarType } from '@/app/components/workflow/type
|
||||
import type { StartNodeType } from '@/app/components/workflow/nodes/start/types'
|
||||
import type { ConversationVariable, EnvironmentVariable, Node, NodeOutPutVar, ValueSelector, Var } from '@/app/components/workflow/types'
|
||||
import type { VariableAssignerNodeType } from '@/app/components/workflow/nodes/variable-assigner/types'
|
||||
import type { Field as StructField } from '@/app/components/workflow/nodes/llm/types'
|
||||
|
||||
import {
|
||||
HTTP_REQUEST_OUTPUT_STRUCT,
|
||||
KNOWLEDGE_RETRIEVAL_OUTPUT_STRUCT,
|
||||
@@ -55,19 +57,81 @@ const inputVarTypeToVarType = (type: InputVarType): VarType => {
|
||||
} as any)[type] || VarType.string
|
||||
}
|
||||
|
||||
const structTypeToVarType = (type: Type): VarType => {
|
||||
return ({
|
||||
[Type.string]: VarType.string,
|
||||
[Type.number]: VarType.number,
|
||||
[Type.boolean]: VarType.boolean,
|
||||
[Type.object]: VarType.object,
|
||||
[Type.array]: VarType.array,
|
||||
} as any)[type] || VarType.string
|
||||
}
|
||||
|
||||
export const varTypeToStructType = (type: VarType): Type => {
|
||||
return ({
|
||||
[VarType.string]: Type.string,
|
||||
[VarType.number]: Type.number,
|
||||
[VarType.boolean]: Type.boolean,
|
||||
[VarType.object]: Type.object,
|
||||
[VarType.array]: Type.array,
|
||||
} as any)[type] || Type.string
|
||||
}
|
||||
|
||||
const findExceptVarInStructuredProperties = (properties: Record<string, StructField>, filterVar: (payload: Var, selector: ValueSelector) => boolean): Record<string, StructField> => {
|
||||
const res = produce(properties, (draft) => {
|
||||
Object.keys(properties).forEach((key) => {
|
||||
const item = properties[key]
|
||||
const isObj = item.type === Type.object
|
||||
if (!isObj && !filterVar({
|
||||
variable: key,
|
||||
type: structTypeToVarType(item.type),
|
||||
}, [key])) {
|
||||
delete properties[key]
|
||||
return
|
||||
}
|
||||
if (item.type === Type.object && item.properties)
|
||||
item.properties = findExceptVarInStructuredProperties(item.properties, filterVar)
|
||||
})
|
||||
return draft
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
const findExceptVarInStructuredOutput = (structuredOutput: StructuredOutput, filterVar: (payload: Var, selector: ValueSelector) => boolean): StructuredOutput => {
|
||||
const res = produce(structuredOutput, (draft) => {
|
||||
const properties = draft.schema.properties
|
||||
Object.keys(properties).forEach((key) => {
|
||||
const item = properties[key]
|
||||
const isObj = item.type === Type.object
|
||||
if (!isObj && !filterVar({
|
||||
variable: key,
|
||||
type: structTypeToVarType(item.type),
|
||||
}, [key])) {
|
||||
delete properties[key]
|
||||
return
|
||||
}
|
||||
if (item.type === Type.object && item.properties)
|
||||
item.properties = findExceptVarInStructuredProperties(item.properties, filterVar)
|
||||
})
|
||||
return draft
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
const findExceptVarInObject = (obj: any, filterVar: (payload: Var, selector: ValueSelector) => boolean, value_selector: ValueSelector, isFile?: boolean): Var => {
|
||||
const { children } = obj
|
||||
const isStructuredOutput = !!(children as StructuredOutput)?.schema?.properties
|
||||
|
||||
const res: Var = {
|
||||
variable: obj.variable,
|
||||
type: isFile ? VarType.file : VarType.object,
|
||||
children: children.filter((item: Var) => {
|
||||
children: isStructuredOutput ? findExceptVarInStructuredOutput(children, filterVar) : children.filter((item: Var) => {
|
||||
const { children } = item
|
||||
const currSelector = [...value_selector, item.variable]
|
||||
if (!children)
|
||||
return filterVar(item, currSelector)
|
||||
|
||||
const obj = findExceptVarInObject(item, filterVar, currSelector, false) // File doesn't contains file children
|
||||
return obj.children && obj.children?.length > 0
|
||||
return obj.children && (obj.children as Var[])?.length > 0
|
||||
}),
|
||||
}
|
||||
return res
|
||||
@@ -139,10 +203,17 @@ const formatItem = (
|
||||
}
|
||||
|
||||
case BlockEnum.LLM: {
|
||||
res.vars = LLM_OUTPUT_STRUCT
|
||||
res.vars = [...LLM_OUTPUT_STRUCT]
|
||||
if (data.structured_output_enabled && data.structured_output?.schema?.properties && Object.keys(data.structured_output.schema.properties).length > 0) {
|
||||
res.vars.push({
|
||||
variable: 'structured_output',
|
||||
type: VarType.object,
|
||||
children: data.structured_output,
|
||||
})
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case BlockEnum.KnowledgeRetrieval: {
|
||||
res.vars = KNOWLEDGE_RETRIEVAL_OUTPUT_STRUCT
|
||||
break
|
||||
@@ -286,6 +357,21 @@ const formatItem = (
|
||||
break
|
||||
}
|
||||
|
||||
case BlockEnum.Loop: {
|
||||
const { loop_variables } = data as LoopNodeType
|
||||
res.isLoop = true
|
||||
res.vars = loop_variables?.map((v) => {
|
||||
return {
|
||||
variable: v.label,
|
||||
type: v.var_type,
|
||||
isLoopVariable: true,
|
||||
nodeId: res.nodeId,
|
||||
}
|
||||
}) || []
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case BlockEnum.DocExtractor: {
|
||||
res.vars = [
|
||||
{
|
||||
@@ -405,7 +491,7 @@ const formatItem = (
|
||||
return false
|
||||
|
||||
const obj = findExceptVarInObject(isFile ? { ...v, children } : v, filterVar, selector, isFile)
|
||||
return obj?.children && obj?.children.length > 0
|
||||
return obj?.children && ((obj?.children as Var[]).length > 0 || Object.keys((obj?.children as StructuredOutput)?.schema?.properties || {}).length > 0)
|
||||
}).map((v) => {
|
||||
const isFile = v.type === VarType.file
|
||||
|
||||
@@ -457,7 +543,7 @@ export const toNodeOutputVars = (
|
||||
},
|
||||
}
|
||||
const res = [
|
||||
...nodes.filter(node => SUPPORT_OUTPUT_VARS_NODE.includes(node.data.type)),
|
||||
...nodes.filter(node => SUPPORT_OUTPUT_VARS_NODE.includes(node?.data?.type)),
|
||||
...(environmentVariables.length > 0 ? [ENV_NODE] : []),
|
||||
...((isChatMode && conversationVariables.length > 0) ? [CHAT_VAR_NODE] : []),
|
||||
].map((node) => {
|
||||
@@ -579,8 +665,7 @@ export const getVarType = ({
|
||||
isConstant,
|
||||
environmentVariables = [],
|
||||
conversationVariables = [],
|
||||
}:
|
||||
{
|
||||
}: {
|
||||
valueSelector: ValueSelector
|
||||
parentNode?: Node | null
|
||||
isIterationItem?: boolean
|
||||
@@ -644,7 +729,7 @@ export const getVarType = ({
|
||||
const isEnv = isENV(valueSelector)
|
||||
const isChatVar = isConversationVar(valueSelector)
|
||||
const startNode = availableNodes.find((node: any) => {
|
||||
return node.data.type === BlockEnum.Start
|
||||
return node?.data.type === BlockEnum.Start
|
||||
})
|
||||
|
||||
const targetVarNodeId = isSystem ? startNode?.id : valueSelector[0]
|
||||
@@ -655,10 +740,30 @@ export const getVarType = ({
|
||||
|
||||
let type: VarType = VarType.string
|
||||
let curr: any = targetVar.vars
|
||||
|
||||
if (isSystem || isEnv || isChatVar) {
|
||||
return curr.find((v: any) => v.variable === (valueSelector as ValueSelector).join('.'))?.type
|
||||
}
|
||||
else {
|
||||
const targetVar = curr.find((v: any) => v.variable === valueSelector[1])
|
||||
if (!targetVar)
|
||||
return VarType.string
|
||||
|
||||
const isStructuredOutputVar = !!targetVar.children?.schema?.properties
|
||||
if (isStructuredOutputVar) {
|
||||
let currProperties = targetVar.children.schema;
|
||||
(valueSelector as ValueSelector).slice(2).forEach((key, i) => {
|
||||
const isLast = i === valueSelector.length - 3
|
||||
if (!currProperties)
|
||||
return
|
||||
|
||||
currProperties = currProperties.properties[key]
|
||||
if (isLast)
|
||||
type = structTypeToVarType(currProperties?.type)
|
||||
})
|
||||
return type
|
||||
}
|
||||
|
||||
(valueSelector as ValueSelector).slice(1).forEach((key, i) => {
|
||||
const isLast = i === valueSelector.length - 2
|
||||
if (Array.isArray(curr))
|
||||
@@ -741,6 +846,9 @@ export const toNodeAvailableVars = ({
|
||||
},
|
||||
],
|
||||
}
|
||||
const iterationIndex = beforeNodesOutputVars.findIndex(v => v.nodeId === iterationNode?.id)
|
||||
if (iterationIndex > -1)
|
||||
beforeNodesOutputVars.splice(iterationIndex, 1)
|
||||
beforeNodesOutputVars.unshift(iterationVar)
|
||||
}
|
||||
return beforeNodesOutputVars
|
||||
@@ -1181,17 +1289,27 @@ export const updateNodeVars = (oldNode: Node, oldVarSelector: ValueSelector, new
|
||||
})
|
||||
return newNode
|
||||
}
|
||||
|
||||
const varToValueSelectorList = (v: Var, parentValueSelector: ValueSelector, res: ValueSelector[]) => {
|
||||
if (!v.variable)
|
||||
return
|
||||
|
||||
res.push([...parentValueSelector, v.variable])
|
||||
const isStructuredOutput = !!(v.children as StructuredOutput)?.schema?.properties
|
||||
|
||||
if (v.children && v.children.length > 0) {
|
||||
v.children.forEach((child) => {
|
||||
if ((v.children as Var[])?.length > 0) {
|
||||
(v.children as Var[]).forEach((child) => {
|
||||
varToValueSelectorList(child, [...parentValueSelector, v.variable], res)
|
||||
})
|
||||
}
|
||||
if (isStructuredOutput) {
|
||||
Object.keys((v.children as StructuredOutput)?.schema?.properties || {}).forEach((key) => {
|
||||
varToValueSelectorList({
|
||||
variable: key,
|
||||
type: structTypeToVarType((v.children as StructuredOutput)?.schema?.properties[key].type),
|
||||
}, [...parentValueSelector, v.variable], res)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const varsToValueSelectorList = (vars: Var | Var[], parentValueSelector: ValueSelector, res: ValueSelector[]) => {
|
||||
@@ -1225,7 +1343,16 @@ export const getNodeOutputVars = (node: Node, isChatMode: boolean): ValueSelecto
|
||||
}
|
||||
|
||||
case BlockEnum.LLM: {
|
||||
varsToValueSelectorList(LLM_OUTPUT_STRUCT, [id], res)
|
||||
const vars = [...LLM_OUTPUT_STRUCT]
|
||||
const llmNodeData = data as LLMNodeType
|
||||
if (llmNodeData.structured_output_enabled && llmNodeData.structured_output?.schema?.properties && Object.keys(llmNodeData.structured_output.schema.properties).length > 0) {
|
||||
vars.push({
|
||||
variable: 'structured_output',
|
||||
type: VarType.object,
|
||||
children: llmNodeData.structured_output,
|
||||
})
|
||||
}
|
||||
varsToValueSelectorList(vars, [id], res)
|
||||
break
|
||||
}
|
||||
|
||||
|
||||
@@ -101,7 +101,7 @@ const VarReferencePicker: FC<Props> = ({
|
||||
const isChatMode = useIsChatMode()
|
||||
|
||||
const { getCurrentVariableType } = useWorkflowVariables()
|
||||
const { availableNodes, availableVars } = useAvailableVarList(nodeId, {
|
||||
const { availableVars, availableNodesWithParent: availableNodes } = useAvailableVarList(nodeId, {
|
||||
onlyLeafNodeVar,
|
||||
passedInAvailableNodes,
|
||||
filterVar,
|
||||
|
||||
@@ -16,6 +16,7 @@ import Input from '@/app/components/base/input'
|
||||
import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others'
|
||||
import { checkKeys } from '@/utils/var'
|
||||
import { FILE_STRUCT } from '@/app/components/workflow/constants'
|
||||
import { Loop } from '@/app/components/base/icons/src/vender/workflow'
|
||||
|
||||
type ObjectChildrenProps = {
|
||||
nodeId: string
|
||||
@@ -38,6 +39,7 @@ type ItemProps = {
|
||||
itemWidth?: number
|
||||
isSupportFileVar?: boolean
|
||||
isException?: boolean
|
||||
isLoopVar?: boolean
|
||||
}
|
||||
|
||||
const Item: FC<ItemProps> = ({
|
||||
@@ -50,6 +52,7 @@ const Item: FC<ItemProps> = ({
|
||||
itemWidth,
|
||||
isSupportFileVar,
|
||||
isException,
|
||||
isLoopVar,
|
||||
}) => {
|
||||
const isFile = itemData.type === VarType.file
|
||||
const isObj = ([VarType.object, VarType.file].includes(itemData.type) && itemData.children && itemData.children.length > 0)
|
||||
@@ -112,9 +115,10 @@ const Item: FC<ItemProps> = ({
|
||||
onMouseDown={e => e.preventDefault()}
|
||||
>
|
||||
<div className='flex w-0 grow items-center'>
|
||||
{!isEnv && !isChatVar && <Variable02 className={cn('h-3.5 w-3.5 shrink-0 text-text-accent', isException && 'text-text-warning')} />}
|
||||
{!isEnv && !isChatVar && !isLoopVar && <Variable02 className={cn('h-3.5 w-3.5 shrink-0 text-text-accent', isException && 'text-text-warning')} />}
|
||||
{isEnv && <Env className='h-3.5 w-3.5 shrink-0 text-util-colors-violet-violet-600' />}
|
||||
{isChatVar && <BubbleX className='h-3.5 w-3.5 text-util-colors-teal-teal-700' />}
|
||||
{isChatVar && <BubbleX className='h-3.5 w-3.5 shrink-0 text-util-colors-teal-teal-700' />}
|
||||
{isLoopVar && <Loop className='h-3.5 w-3.5 shrink-0 text-util-colors-cyan-cyan-500' />}
|
||||
{!isEnv && !isChatVar && (
|
||||
<div title={itemData.variable} className='system-sm-medium ml-1 w-0 grow truncate text-text-secondary'>{itemData.variable}</div>
|
||||
)}
|
||||
@@ -317,6 +321,7 @@ const VarReferenceVars: FC<Props> = ({
|
||||
itemWidth={itemWidth}
|
||||
isSupportFileVar={isSupportFileVar}
|
||||
isException={v.isException}
|
||||
isLoopVar={item.isLoop}
|
||||
/>
|
||||
))}
|
||||
</div>))
|
||||
|
||||
@@ -24,11 +24,11 @@ const useAvailableVarList = (nodeId: string, {
|
||||
onlyLeafNodeVar: false,
|
||||
filterVar: () => true,
|
||||
}) => {
|
||||
const { getTreeLeafNodes, getBeforeNodesInSameBranch } = useWorkflow()
|
||||
const { getTreeLeafNodes, getBeforeNodesInSameBranchIncludeParent } = useWorkflow()
|
||||
const { getNodeAvailableVars } = useWorkflowVariables()
|
||||
const isChatMode = useIsChatMode()
|
||||
|
||||
const availableNodes = passedInAvailableNodes || (onlyLeafNodeVar ? getTreeLeafNodes(nodeId) : getBeforeNodesInSameBranch(nodeId))
|
||||
const availableNodes = passedInAvailableNodes || (onlyLeafNodeVar ? getTreeLeafNodes(nodeId) : getBeforeNodesInSameBranchIncludeParent(nodeId))
|
||||
|
||||
const {
|
||||
parentNode: iterationNode,
|
||||
@@ -46,7 +46,7 @@ const useAvailableVarList = (nodeId: string, {
|
||||
return {
|
||||
availableVars,
|
||||
availableNodes,
|
||||
availableNodesWithParent: iterationNode ? [...availableNodes, iterationNode] : availableNodes,
|
||||
availableNodesWithParent: availableNodes,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,9 +26,7 @@ export const useNodeHelpLink = (nodeType: BlockEnum) => {
|
||||
[BlockEnum.VariableAggregator]: 'variable-aggregator',
|
||||
[BlockEnum.Assigner]: 'variable-assigner',
|
||||
[BlockEnum.Iteration]: 'iteration',
|
||||
[BlockEnum.IterationStart]: 'iteration',
|
||||
[BlockEnum.Loop]: 'loop',
|
||||
[BlockEnum.LoopStart]: 'loop',
|
||||
[BlockEnum.ParameterExtractor]: 'parameter-extractor',
|
||||
[BlockEnum.HttpRequest]: 'http-request',
|
||||
[BlockEnum.Tool]: 'tools',
|
||||
@@ -52,9 +50,7 @@ export const useNodeHelpLink = (nodeType: BlockEnum) => {
|
||||
[BlockEnum.VariableAggregator]: 'variable-aggregator',
|
||||
[BlockEnum.Assigner]: 'variable-assigner',
|
||||
[BlockEnum.Iteration]: 'iteration',
|
||||
[BlockEnum.IterationStart]: 'iteration',
|
||||
[BlockEnum.Loop]: 'loop',
|
||||
[BlockEnum.LoopStart]: 'loop',
|
||||
[BlockEnum.ParameterExtractor]: 'parameter-extractor',
|
||||
[BlockEnum.HttpRequest]: 'http-request',
|
||||
[BlockEnum.Tool]: 'tools',
|
||||
@@ -62,7 +58,12 @@ export const useNodeHelpLink = (nodeType: BlockEnum) => {
|
||||
[BlockEnum.ListFilter]: 'list-operator',
|
||||
[BlockEnum.Agent]: 'agent',
|
||||
}
|
||||
}, [language])
|
||||
}, [language]) as Record<string, string>
|
||||
|
||||
return `${prefixLink}${linkMap[nodeType]}`
|
||||
const link = linkMap[nodeType]
|
||||
|
||||
if (!link)
|
||||
return ''
|
||||
|
||||
return `${prefixLink}${link}`
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type {
|
||||
FC,
|
||||
ReactNode,
|
||||
ReactElement,
|
||||
} from 'react'
|
||||
import {
|
||||
cloneElement,
|
||||
@@ -46,7 +46,7 @@ import BlockIcon from '@/app/components/workflow/block-icon'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
|
||||
type BaseNodeProps = {
|
||||
children: ReactNode
|
||||
children: ReactElement
|
||||
} & NodeProps
|
||||
|
||||
const BaseNode: FC<BaseNodeProps> = ({
|
||||
@@ -104,6 +104,30 @@ const BaseNode: FC<BaseNodeProps> = ({
|
||||
}
|
||||
}, [data._runningStatus, showSelectedBorder])
|
||||
|
||||
const LoopIndex = useMemo(() => {
|
||||
let text = ''
|
||||
|
||||
if (data._runningStatus === NodeRunningStatus.Running)
|
||||
text = t('workflow.nodes.loop.currentLoopCount', { count: data._loopIndex })
|
||||
if (data._runningStatus === NodeRunningStatus.Succeeded || data._runningStatus === NodeRunningStatus.Failed)
|
||||
text = t('workflow.nodes.loop.totalLoopCount', { count: data._loopIndex })
|
||||
|
||||
if (text) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'system-xs-medium mr-2 text-text-tertiary',
|
||||
data._runningStatus === NodeRunningStatus.Running && 'text-text-accent',
|
||||
)}
|
||||
>
|
||||
{text}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}, [data._loopIndex, data._runningStatus, t])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
@@ -233,11 +257,7 @@ const BaseNode: FC<BaseNodeProps> = ({
|
||||
)
|
||||
}
|
||||
{
|
||||
data._loopLength && data._loopIndex && data._runningStatus === NodeRunningStatus.Running && (
|
||||
<div className='mr-1.5 text-xs font-medium text-primary-600'>
|
||||
{data._loopIndex > data._loopLength ? data._loopLength : data._loopIndex}/{data._loopLength}
|
||||
</div>
|
||||
)
|
||||
data.type === BlockEnum.Loop && data._loopIndex && LoopIndex
|
||||
}
|
||||
{
|
||||
(data._runningStatus === NodeRunningStatus.Running || data._singleRunningStatus === NodeRunningStatus.Running) && (
|
||||
|
||||
Reference in New Issue
Block a user