mirror of
http://112.124.100.131/huang.ze/ebiz-dify-ai.git
synced 2025-12-11 20:06:54 +08:00
Feature/newnew workflow loop node (#14863)
Co-authored-by: arkunzz <4873204@qq.com>
This commit is contained in:
@@ -31,6 +31,16 @@ describe('parseDSL', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('should parse loop nodes correctly', () => {
|
||||
const dsl = '(loop, loopNode, plainNode1 -> plainNode2)'
|
||||
const result = parseDSL(dsl)
|
||||
expect(result).toEqual([
|
||||
{ id: 'loopNode', node_id: 'loopNode', title: 'loopNode', node_type: 'loop', execution_metadata: {}, status: 'succeeded' },
|
||||
{ id: 'plainNode1', node_id: 'plainNode1', title: 'plainNode1', execution_metadata: { loop_id: 'loopNode', loop_index: 0 }, status: 'succeeded' },
|
||||
{ id: 'plainNode2', node_id: 'plainNode2', title: 'plainNode2', execution_metadata: { loop_id: 'loopNode', loop_index: 0 }, status: 'succeeded' },
|
||||
])
|
||||
})
|
||||
|
||||
it('should parse parallel nodes correctly', () => {
|
||||
const dsl = '(parallel, parallelNode, nodeA, nodeB -> nodeC)'
|
||||
const result = parseDSL(dsl)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
type IterationInfo = { iterationId: string; iterationIndex: number }
|
||||
type NodePlain = { nodeType: 'plain'; nodeId: string; } & Partial<IterationInfo>
|
||||
type NodeComplex = { nodeType: string; nodeId: string; params: (NodePlain | (NodeComplex & Partial<IterationInfo>) | Node[] | number)[] } & Partial<IterationInfo>
|
||||
type LoopInfo = { loopId: string; loopIndex: number }
|
||||
type NodePlain = { nodeType: 'plain'; nodeId: string; } & (Partial<IterationInfo> & Partial<LoopInfo>)
|
||||
type NodeComplex = { nodeType: string; nodeId: string; params: (NodePlain | (NodeComplex & (Partial<IterationInfo> & Partial<LoopInfo>)) | Node[] | number)[] } & (Partial<IterationInfo> & Partial<LoopInfo>)
|
||||
type Node = NodePlain | NodeComplex
|
||||
|
||||
/**
|
||||
@@ -46,9 +47,10 @@ function parseTopLevelFlow(dsl: string): string[] {
|
||||
* If the node is complex (e.g., has parentheses), it extracts the node type, node ID, and parameters.
|
||||
* @param nodeStr - The node string to parse.
|
||||
* @param parentIterationId - The ID of the parent iteration node (if applicable).
|
||||
* @param parentLoopId - The ID of the parent loop node (if applicable).
|
||||
* @returns A parsed node object.
|
||||
*/
|
||||
function parseNode(nodeStr: string, parentIterationId?: string): Node {
|
||||
function parseNode(nodeStr: string, parentIterationId?: string, parentLoopId?: string): Node {
|
||||
// Check if the node is a complex node
|
||||
if (nodeStr.startsWith('(') && nodeStr.endsWith(')')) {
|
||||
const innerContent = nodeStr.slice(1, -1).trim() // Remove outer parentheses
|
||||
@@ -74,7 +76,7 @@ function parseNode(nodeStr: string, parentIterationId?: string): Node {
|
||||
|
||||
// Extract nodeType, nodeId, and params
|
||||
const [nodeType, nodeId, ...paramsRaw] = parts
|
||||
const params = parseParams(paramsRaw, nodeType === 'iteration' ? nodeId.trim() : parentIterationId)
|
||||
const params = parseParams(paramsRaw, nodeType === 'iteration' ? nodeId.trim() : parentIterationId, nodeType === 'loop' ? nodeId.trim() : parentLoopId)
|
||||
const complexNode = {
|
||||
nodeType: nodeType.trim(),
|
||||
nodeId: nodeId.trim(),
|
||||
@@ -84,6 +86,10 @@ function parseNode(nodeStr: string, parentIterationId?: string): Node {
|
||||
(complexNode as any).iterationId = parentIterationId;
|
||||
(complexNode as any).iterationIndex = 0 // Fixed as 0
|
||||
}
|
||||
if (parentLoopId) {
|
||||
(complexNode as any).loopId = parentLoopId;
|
||||
(complexNode as any).loopIndex = 0 // Fixed as 0
|
||||
}
|
||||
return complexNode
|
||||
}
|
||||
|
||||
@@ -93,6 +99,10 @@ function parseNode(nodeStr: string, parentIterationId?: string): Node {
|
||||
plainNode.iterationId = parentIterationId
|
||||
plainNode.iterationIndex = 0 // Fixed as 0
|
||||
}
|
||||
if (parentLoopId) {
|
||||
plainNode.loopId = parentLoopId
|
||||
plainNode.loopIndex = 0 // Fixed as 0
|
||||
}
|
||||
return plainNode
|
||||
}
|
||||
|
||||
@@ -101,18 +111,19 @@ function parseNode(nodeStr: string, parentIterationId?: string): Node {
|
||||
* Supports nested flows and complex sub-nodes.
|
||||
* Adds iteration-specific metadata recursively.
|
||||
* @param paramParts - The parameters string split by commas.
|
||||
* @param iterationId - The ID of the iteration node, if applicable.
|
||||
* @param parentIterationId - The ID of the parent iteration node (if applicable).
|
||||
* @param parentLoopId - The ID of the parent loop node (if applicable).
|
||||
* @returns An array of parsed parameters (plain nodes, nested nodes, or flows).
|
||||
*/
|
||||
function parseParams(paramParts: string[], iterationId?: string): (Node | Node[] | number)[] {
|
||||
function parseParams(paramParts: string[], parentIteration?: string, parentLoopId?: string): (Node | Node[] | number)[] {
|
||||
return paramParts.map((part) => {
|
||||
if (part.includes('->')) {
|
||||
// Parse as a flow and return an array of nodes
|
||||
return parseTopLevelFlow(part).map(node => parseNode(node, iterationId))
|
||||
return parseTopLevelFlow(part).map(node => parseNode(node, parentIteration || undefined, parentLoopId || undefined))
|
||||
}
|
||||
else if (part.startsWith('(')) {
|
||||
// Parse as a nested complex node
|
||||
return parseNode(part, iterationId)
|
||||
return parseNode(part, parentIteration || undefined, parentLoopId || undefined)
|
||||
}
|
||||
else if (!Number.isNaN(Number(part.trim()))) {
|
||||
// Parse as a numeric parameter
|
||||
@@ -120,7 +131,7 @@ function parseParams(paramParts: string[], iterationId?: string): (Node | Node[]
|
||||
}
|
||||
else {
|
||||
// Parse as a plain node
|
||||
return parseNode(part, iterationId)
|
||||
return parseNode(part, parentIteration || undefined, parentLoopId || undefined)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -153,7 +164,7 @@ function convertPlainNode(node: Node): NodeData[] {
|
||||
* Converts a retry node to node data.
|
||||
*/
|
||||
function convertRetryNode(node: Node): NodeData[] {
|
||||
const { nodeId, iterationId, iterationIndex, params } = node as NodeComplex
|
||||
const { nodeId, iterationId, iterationIndex, loopId, loopIndex, params } = node as NodeComplex
|
||||
const retryCount = params ? Number.parseInt(params[0] as unknown as string, 10) : 0
|
||||
const result: NodeData[] = [
|
||||
{
|
||||
@@ -173,6 +184,9 @@ function convertRetryNode(node: Node): NodeData[] {
|
||||
execution_metadata: iterationId ? {
|
||||
iteration_id: iterationId,
|
||||
iteration_index: iterationIndex || 0,
|
||||
} : loopId ? {
|
||||
loop_id: loopId,
|
||||
loop_index: loopIndex || 0,
|
||||
} : {},
|
||||
status: 'retry',
|
||||
})
|
||||
@@ -216,6 +230,41 @@ function convertIterationNode(node: Node): NodeData[] {
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an loop node to node data.
|
||||
*/
|
||||
function convertLoopNode(node: Node): NodeData[] {
|
||||
const { nodeId, params } = node as NodeComplex
|
||||
const result: NodeData[] = [
|
||||
{
|
||||
id: nodeId,
|
||||
node_id: nodeId,
|
||||
title: nodeId,
|
||||
node_type: 'loop',
|
||||
status: 'succeeded',
|
||||
execution_metadata: {},
|
||||
},
|
||||
]
|
||||
|
||||
params?.forEach((param: any) => {
|
||||
if (Array.isArray(param)) {
|
||||
param.forEach((childNode: Node) => {
|
||||
const childData = convertToNodeData([childNode])
|
||||
childData.forEach((data) => {
|
||||
data.execution_metadata = {
|
||||
...data.execution_metadata,
|
||||
loop_id: nodeId,
|
||||
loop_index: 0,
|
||||
}
|
||||
})
|
||||
result.push(...childData)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a parallel node to node data.
|
||||
*/
|
||||
@@ -290,6 +339,9 @@ function convertToNodeData(nodes: Node[], parentParallelId?: string, parentStart
|
||||
case 'iteration':
|
||||
result.push(...convertIterationNode(node))
|
||||
break
|
||||
case 'loop':
|
||||
result.push(...convertLoopNode(node))
|
||||
break
|
||||
case 'parallel':
|
||||
result.push(...convertParallelNode(node, parentParallelId, parentStartNodeId))
|
||||
break
|
||||
|
||||
@@ -1,9 +1,80 @@
|
||||
import type { NodeTracing } from '@/types/workflow'
|
||||
import formatIterationNode from './iteration'
|
||||
import { addChildrenToIterationNode } from './iteration'
|
||||
import { addChildrenToLoopNode } from './loop'
|
||||
import formatParallelNode from './parallel'
|
||||
import formatRetryNode from './retry'
|
||||
import formatAgentNode from './agent'
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
import { BlockEnum } from '../../../types'
|
||||
|
||||
const formatIterationAndLoopNode = (list: NodeTracing[], t: any) => {
|
||||
const clonedList = cloneDeep(list)
|
||||
|
||||
// Identify all loop and iteration nodes
|
||||
const loopNodeIds = clonedList
|
||||
.filter(item => item.node_type === BlockEnum.Loop)
|
||||
.map(item => item.node_id)
|
||||
|
||||
const iterationNodeIds = clonedList
|
||||
.filter(item => item.node_type === BlockEnum.Iteration)
|
||||
.map(item => item.node_id)
|
||||
|
||||
// Identify all child nodes for both loop and iteration
|
||||
const loopChildrenNodeIds = clonedList
|
||||
.filter(item => item.execution_metadata?.loop_id && loopNodeIds.includes(item.execution_metadata.loop_id))
|
||||
.map(item => item.node_id)
|
||||
|
||||
const iterationChildrenNodeIds = clonedList
|
||||
.filter(item => item.execution_metadata?.iteration_id && iterationNodeIds.includes(item.execution_metadata.iteration_id))
|
||||
.map(item => item.node_id)
|
||||
|
||||
// Filter out child nodes as they will be included in their parent nodes
|
||||
const result = clonedList
|
||||
.filter(item => !loopChildrenNodeIds.includes(item.node_id) && !iterationChildrenNodeIds.includes(item.node_id))
|
||||
.map((item) => {
|
||||
// Process Loop nodes
|
||||
if (item.node_type === BlockEnum.Loop) {
|
||||
const childrenNodes = clonedList.filter(child => child.execution_metadata?.loop_id === item.node_id)
|
||||
const error = childrenNodes.find(child => child.status === 'failed')
|
||||
if (error) {
|
||||
item.status = 'failed'
|
||||
item.error = error.error
|
||||
}
|
||||
const addedChildrenList = addChildrenToLoopNode(item, childrenNodes)
|
||||
|
||||
// Handle parallel nodes in loop node
|
||||
if (addedChildrenList.details && addedChildrenList.details.length > 0) {
|
||||
addedChildrenList.details = addedChildrenList.details.map((row) => {
|
||||
return formatParallelNode(row, t)
|
||||
})
|
||||
}
|
||||
return addedChildrenList
|
||||
}
|
||||
|
||||
// Process Iteration nodes
|
||||
if (item.node_type === BlockEnum.Iteration) {
|
||||
const childrenNodes = clonedList.filter(child => child.execution_metadata?.iteration_id === item.node_id)
|
||||
const error = childrenNodes.find(child => child.status === 'failed')
|
||||
if (error) {
|
||||
item.status = 'failed'
|
||||
item.error = error.error
|
||||
}
|
||||
const addedChildrenList = addChildrenToIterationNode(item, childrenNodes)
|
||||
|
||||
// Handle parallel nodes in iteration node
|
||||
if (addedChildrenList.details && addedChildrenList.details.length > 0) {
|
||||
addedChildrenList.details = addedChildrenList.details.map((row) => {
|
||||
return formatParallelNode(row, t)
|
||||
})
|
||||
}
|
||||
return addedChildrenList
|
||||
}
|
||||
|
||||
return item
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
const formatToTracingNodeList = (list: NodeTracing[], t: any) => {
|
||||
const allItems = cloneDeep([...list]).sort((a, b) => a.index - b.index)
|
||||
@@ -14,8 +85,8 @@ const formatToTracingNodeList = (list: NodeTracing[], t: any) => {
|
||||
const formattedAgentList = formatAgentNode(allItems)
|
||||
const formattedRetryList = formatRetryNode(formattedAgentList) // retry one node
|
||||
// would change the structure of the list. Iteration and parallel can include each other.
|
||||
const formattedIterationList = formatIterationNode(formattedRetryList, t)
|
||||
const formattedParallelList = formatParallelNode(formattedIterationList, t)
|
||||
const formattedLoopAndIterationList = formatIterationAndLoopNode(formattedRetryList, t)
|
||||
const formattedParallelList = formatParallelNode(formattedLoopAndIterationList, t)
|
||||
|
||||
const result = formattedParallelList
|
||||
// console.log(allItems)
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import type { NodeTracing } from '@/types/workflow'
|
||||
import formatParallelNode from '../parallel'
|
||||
function addChildrenToIterationNode(iterationNode: NodeTracing, childrenNodes: NodeTracing[]): NodeTracing {
|
||||
|
||||
export function addChildrenToIterationNode(iterationNode: NodeTracing, childrenNodes: NodeTracing[]): NodeTracing {
|
||||
const details: NodeTracing[][] = []
|
||||
childrenNodes.forEach((item, index) => {
|
||||
if (!item.execution_metadata) return
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import format from '.'
|
||||
import graphToLogStruct from '../graph-to-log-struct'
|
||||
|
||||
describe('loop', () => {
|
||||
const list = graphToLogStruct('start -> (loop, loopNode, plainNode1 -> plainNode2)')
|
||||
const [startNode, loopNode, ...loops] = list
|
||||
const result = format(list as any, () => { })
|
||||
test('result should have no nodes in loop node', () => {
|
||||
expect((result as any).find((item: any) => !!item.execution_metadata?.loop_id)).toBeUndefined()
|
||||
})
|
||||
test('loop should put nodes in details', () => {
|
||||
expect(result as any).toEqual([
|
||||
startNode,
|
||||
{
|
||||
...loopNode,
|
||||
details: [
|
||||
[loops[0], loops[1]],
|
||||
],
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,56 @@
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import type { NodeTracing } from '@/types/workflow'
|
||||
import formatParallelNode from '../parallel'
|
||||
|
||||
export function addChildrenToLoopNode(loopNode: NodeTracing, childrenNodes: NodeTracing[]): NodeTracing {
|
||||
const details: NodeTracing[][] = []
|
||||
childrenNodes.forEach((item) => {
|
||||
if (!item.execution_metadata) return
|
||||
const { parallel_mode_run_id, loop_index = 0 } = item.execution_metadata
|
||||
const runIndex: number = (parallel_mode_run_id || loop_index) as number
|
||||
if (!details[runIndex])
|
||||
details[runIndex] = []
|
||||
|
||||
details[runIndex].push(item)
|
||||
})
|
||||
return {
|
||||
...loopNode,
|
||||
details,
|
||||
}
|
||||
}
|
||||
|
||||
const format = (list: NodeTracing[], t: any): NodeTracing[] => {
|
||||
const loopNodeIds = list
|
||||
.filter(item => item.node_type === BlockEnum.Loop)
|
||||
.map(item => item.node_id)
|
||||
const loopChildrenNodeIds = list
|
||||
.filter(item => item.execution_metadata?.loop_id && loopNodeIds.includes(item.execution_metadata.loop_id))
|
||||
.map(item => item.node_id)
|
||||
// move loop children nodes to loop node's details field
|
||||
const result = list
|
||||
.filter(item => !loopChildrenNodeIds.includes(item.node_id))
|
||||
.map((item) => {
|
||||
if (item.node_type === BlockEnum.Loop) {
|
||||
const childrenNodes = list.filter(child => child.execution_metadata?.loop_id === item.node_id)
|
||||
const error = childrenNodes.find(child => child.status === 'failed')
|
||||
if (error) {
|
||||
item.status = 'failed'
|
||||
item.error = error.error
|
||||
}
|
||||
const addedChildrenList = addChildrenToLoopNode(item, childrenNodes)
|
||||
// handle parallel node in loop node
|
||||
if (addedChildrenList.details && addedChildrenList.details.length > 0) {
|
||||
addedChildrenList.details = addedChildrenList.details.map((row) => {
|
||||
return formatParallelNode(row, t)
|
||||
})
|
||||
}
|
||||
return addedChildrenList
|
||||
}
|
||||
|
||||
return item
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export default format
|
||||
@@ -12,6 +12,7 @@ const format = (list: NodeTracing[]): NodeTracing[] => {
|
||||
}).map((item) => {
|
||||
const { execution_metadata } = item
|
||||
const isInIteration = !!execution_metadata?.iteration_id
|
||||
const isInLoop = !!execution_metadata?.loop_id
|
||||
const nodeId = item.node_id
|
||||
const isRetryBelongNode = retryNodeIds.includes(nodeId)
|
||||
|
||||
@@ -19,11 +20,18 @@ const format = (list: NodeTracing[]): NodeTracing[] => {
|
||||
return {
|
||||
...item,
|
||||
retryDetail: retryNodes.filter((node) => {
|
||||
if (!isInIteration)
|
||||
if (!isInIteration && !isInLoop)
|
||||
return node.node_id === nodeId
|
||||
|
||||
// retry node in iteration
|
||||
return node.node_id === nodeId && node.execution_metadata?.iteration_index === execution_metadata?.iteration_index
|
||||
if (isInIteration)
|
||||
return node.node_id === nodeId && node.execution_metadata?.iteration_index === execution_metadata?.iteration_index
|
||||
|
||||
// retry node in loop
|
||||
if (isInLoop)
|
||||
return node.node_id === nodeId && node.execution_metadata?.loop_index === execution_metadata?.loop_index
|
||||
|
||||
return false
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user