Feat/loop break node (#17268)

This commit is contained in:
zxhlyh
2025-04-01 16:52:07 +08:00
committed by GitHub
parent 627a9e2ce1
commit 713902dc47
64 changed files with 1397 additions and 139 deletions

View File

@@ -14,6 +14,9 @@ const HelpLink = ({
const { t } = useTranslation()
const link = useNodeHelpLink(nodeType)
if (!link)
return null
return (
<TooltipPlus
popupContent={t('common.userProfile.helpCenter')}

View File

@@ -164,7 +164,7 @@ const PanelOperatorPopup = ({
)
}
{
showHelpLink && (
showHelpLink && link && (
<>
<div className='p-1'>
<a

View File

@@ -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'

View File

@@ -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
}

View File

@@ -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,

View File

@@ -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>))