utils.ts 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815
  1. import {
  2. Position,
  3. getConnectedEdges,
  4. getIncomers,
  5. getOutgoers,
  6. } from 'reactflow'
  7. import dagre from '@dagrejs/dagre'
  8. import { v4 as uuid4 } from 'uuid'
  9. import {
  10. cloneDeep,
  11. groupBy,
  12. isEqual,
  13. uniqBy,
  14. } from 'lodash-es'
  15. import type {
  16. Edge,
  17. InputVar,
  18. Node,
  19. ToolWithProvider,
  20. ValueSelector,
  21. } from './types'
  22. import {
  23. BlockEnum,
  24. ErrorHandleMode,
  25. NodeRunningStatus,
  26. } from './types'
  27. import {
  28. CUSTOM_NODE,
  29. DEFAULT_RETRY_INTERVAL,
  30. DEFAULT_RETRY_MAX,
  31. ITERATION_CHILDREN_Z_INDEX,
  32. ITERATION_NODE_Z_INDEX,
  33. NODE_WIDTH_X_OFFSET,
  34. START_INITIAL_POSITION,
  35. } from './constants'
  36. import { CUSTOM_ITERATION_START_NODE } from './nodes/iteration-start/constants'
  37. import type { QuestionClassifierNodeType } from './nodes/question-classifier/types'
  38. import type { IfElseNodeType } from './nodes/if-else/types'
  39. import { branchNameCorrect } from './nodes/if-else/utils'
  40. import type { ToolNodeType } from './nodes/tool/types'
  41. import type { IterationNodeType } from './nodes/iteration/types'
  42. import { CollectionType } from '@/app/components/tools/types'
  43. import { toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
  44. const WHITE = 'WHITE'
  45. const GRAY = 'GRAY'
  46. const BLACK = 'BLACK'
  47. const isCyclicUtil = (nodeId: string, color: Record<string, string>, adjList: Record<string, string[]>, stack: string[]) => {
  48. color[nodeId] = GRAY
  49. stack.push(nodeId)
  50. for (let i = 0; i < adjList[nodeId].length; ++i) {
  51. const childId = adjList[nodeId][i]
  52. if (color[childId] === GRAY) {
  53. stack.push(childId)
  54. return true
  55. }
  56. if (color[childId] === WHITE && isCyclicUtil(childId, color, adjList, stack))
  57. return true
  58. }
  59. color[nodeId] = BLACK
  60. if (stack.length > 0 && stack[stack.length - 1] === nodeId)
  61. stack.pop()
  62. return false
  63. }
  64. const getCycleEdges = (nodes: Node[], edges: Edge[]) => {
  65. const adjList: Record<string, string[]> = {}
  66. const color: Record<string, string> = {}
  67. const stack: string[] = []
  68. for (const node of nodes) {
  69. color[node.id] = WHITE
  70. adjList[node.id] = []
  71. }
  72. for (const edge of edges)
  73. adjList[edge.source]?.push(edge.target)
  74. for (let i = 0; i < nodes.length; i++) {
  75. if (color[nodes[i].id] === WHITE)
  76. isCyclicUtil(nodes[i].id, color, adjList, stack)
  77. }
  78. const cycleEdges = []
  79. if (stack.length > 0) {
  80. const cycleNodes = new Set(stack)
  81. for (const edge of edges) {
  82. if (cycleNodes.has(edge.source) && cycleNodes.has(edge.target))
  83. cycleEdges.push(edge)
  84. }
  85. }
  86. return cycleEdges
  87. }
  88. export function getIterationStartNode(iterationId: string): Node {
  89. return generateNewNode({
  90. id: `${iterationId}start`,
  91. type: CUSTOM_ITERATION_START_NODE,
  92. data: {
  93. title: '',
  94. desc: '',
  95. type: BlockEnum.IterationStart,
  96. isInIteration: true,
  97. },
  98. position: {
  99. x: 24,
  100. y: 68,
  101. },
  102. zIndex: ITERATION_CHILDREN_Z_INDEX,
  103. parentId: iterationId,
  104. selectable: false,
  105. draggable: false,
  106. }).newNode
  107. }
  108. export function generateNewNode({ data, position, id, zIndex, type, ...rest }: Omit<Node, 'id'> & { id?: string }): {
  109. newNode: Node
  110. newIterationStartNode?: Node
  111. } {
  112. const newNode = {
  113. id: id || `${Date.now()}`,
  114. type: type || CUSTOM_NODE,
  115. data,
  116. position,
  117. targetPosition: Position.Left,
  118. sourcePosition: Position.Right,
  119. zIndex: data.type === BlockEnum.Iteration ? ITERATION_NODE_Z_INDEX : zIndex,
  120. ...rest,
  121. } as Node
  122. if (data.type === BlockEnum.Iteration) {
  123. const newIterationStartNode = getIterationStartNode(newNode.id);
  124. (newNode.data as IterationNodeType).start_node_id = newIterationStartNode.id;
  125. (newNode.data as IterationNodeType)._children = [newIterationStartNode.id]
  126. return {
  127. newNode,
  128. newIterationStartNode,
  129. }
  130. }
  131. return {
  132. newNode,
  133. }
  134. }
  135. export const preprocessNodesAndEdges = (nodes: Node[], edges: Edge[]) => {
  136. const hasIterationNode = nodes.some(node => node.data.type === BlockEnum.Iteration)
  137. if (!hasIterationNode) {
  138. return {
  139. nodes,
  140. edges,
  141. }
  142. }
  143. const nodesMap = nodes.reduce((prev, next) => {
  144. prev[next.id] = next
  145. return prev
  146. }, {} as Record<string, Node>)
  147. const iterationNodesWithStartNode = []
  148. const iterationNodesWithoutStartNode = []
  149. for (let i = 0; i < nodes.length; i++) {
  150. const currentNode = nodes[i] as Node<IterationNodeType>
  151. if (currentNode.data.type === BlockEnum.Iteration) {
  152. if (currentNode.data.start_node_id) {
  153. if (nodesMap[currentNode.data.start_node_id]?.type !== CUSTOM_ITERATION_START_NODE)
  154. iterationNodesWithStartNode.push(currentNode)
  155. }
  156. else {
  157. iterationNodesWithoutStartNode.push(currentNode)
  158. }
  159. }
  160. }
  161. const newIterationStartNodesMap = {} as Record<string, Node>
  162. const newIterationStartNodes = [...iterationNodesWithStartNode, ...iterationNodesWithoutStartNode].map((iterationNode, index) => {
  163. const newNode = getIterationStartNode(iterationNode.id)
  164. newNode.id = newNode.id + index
  165. newIterationStartNodesMap[iterationNode.id] = newNode
  166. return newNode
  167. })
  168. const newEdges = iterationNodesWithStartNode.map((iterationNode) => {
  169. const newNode = newIterationStartNodesMap[iterationNode.id]
  170. const startNode = nodesMap[iterationNode.data.start_node_id]
  171. const source = newNode.id
  172. const sourceHandle = 'source'
  173. const target = startNode.id
  174. const targetHandle = 'target'
  175. return {
  176. id: `${source}-${sourceHandle}-${target}-${targetHandle}`,
  177. type: 'custom',
  178. source,
  179. sourceHandle,
  180. target,
  181. targetHandle,
  182. data: {
  183. sourceType: newNode.data.type,
  184. targetType: startNode.data.type,
  185. isInIteration: true,
  186. iteration_id: startNode.parentId,
  187. _connectedNodeIsSelected: true,
  188. },
  189. zIndex: ITERATION_CHILDREN_Z_INDEX,
  190. }
  191. })
  192. nodes.forEach((node) => {
  193. if (node.data.type === BlockEnum.Iteration && newIterationStartNodesMap[node.id])
  194. (node.data as IterationNodeType).start_node_id = newIterationStartNodesMap[node.id].id
  195. })
  196. return {
  197. nodes: [...nodes, ...newIterationStartNodes],
  198. edges: [...edges, ...newEdges],
  199. }
  200. }
  201. export const initialNodes = (originNodes: Node[], originEdges: Edge[]) => {
  202. const { nodes, edges } = preprocessNodesAndEdges(cloneDeep(originNodes), cloneDeep(originEdges))
  203. const firstNode = nodes[0]
  204. if (!firstNode?.position) {
  205. nodes.forEach((node, index) => {
  206. node.position = {
  207. x: START_INITIAL_POSITION.x + index * NODE_WIDTH_X_OFFSET,
  208. y: START_INITIAL_POSITION.y,
  209. }
  210. })
  211. }
  212. const iterationNodeMap = nodes.reduce((acc, node) => {
  213. if (node.parentId) {
  214. if (acc[node.parentId])
  215. acc[node.parentId].push(node.id)
  216. else
  217. acc[node.parentId] = [node.id]
  218. }
  219. return acc
  220. }, {} as Record<string, string[]>)
  221. return nodes.map((node) => {
  222. if (!node.type)
  223. node.type = CUSTOM_NODE
  224. const connectedEdges = getConnectedEdges([node], edges)
  225. node.data._connectedSourceHandleIds = connectedEdges.filter(edge => edge.source === node.id).map(edge => edge.sourceHandle || 'source')
  226. node.data._connectedTargetHandleIds = connectedEdges.filter(edge => edge.target === node.id).map(edge => edge.targetHandle || 'target')
  227. if (node.data.type === BlockEnum.IfElse) {
  228. const nodeData = node.data as IfElseNodeType
  229. if (!nodeData.cases && nodeData.logical_operator && nodeData.conditions) {
  230. (node.data as IfElseNodeType).cases = [
  231. {
  232. case_id: 'true',
  233. logical_operator: nodeData.logical_operator,
  234. conditions: nodeData.conditions,
  235. },
  236. ]
  237. }
  238. node.data._targetBranches = branchNameCorrect([
  239. ...(node.data as IfElseNodeType).cases.map(item => ({ id: item.case_id, name: '' })),
  240. { id: 'false', name: '' },
  241. ])
  242. }
  243. if (node.data.type === BlockEnum.QuestionClassifier) {
  244. node.data._targetBranches = (node.data as QuestionClassifierNodeType).classes.map((topic) => {
  245. return topic
  246. })
  247. }
  248. if (node.data.type === BlockEnum.Iteration) {
  249. const iterationNodeData = node.data as IterationNodeType
  250. iterationNodeData._children = iterationNodeMap[node.id] || []
  251. iterationNodeData.is_parallel = iterationNodeData.is_parallel || false
  252. iterationNodeData.parallel_nums = iterationNodeData.parallel_nums || 10
  253. iterationNodeData.error_handle_mode = iterationNodeData.error_handle_mode || ErrorHandleMode.Terminated
  254. }
  255. if (node.data.type === BlockEnum.HttpRequest && !node.data.retry_config) {
  256. node.data.retry_config = {
  257. retry_enabled: true,
  258. max_retries: DEFAULT_RETRY_MAX,
  259. retry_interval: DEFAULT_RETRY_INTERVAL,
  260. }
  261. }
  262. return node
  263. })
  264. }
  265. export const initialEdges = (originEdges: Edge[], originNodes: Node[]) => {
  266. const { nodes, edges } = preprocessNodesAndEdges(cloneDeep(originNodes), cloneDeep(originEdges))
  267. let selectedNode: Node | null = null
  268. const nodesMap = nodes.reduce((acc, node) => {
  269. acc[node.id] = node
  270. if (node.data?.selected)
  271. selectedNode = node
  272. return acc
  273. }, {} as Record<string, Node>)
  274. const cycleEdges = getCycleEdges(nodes, edges)
  275. return edges.filter((edge) => {
  276. return !cycleEdges.find(cycEdge => cycEdge.source === edge.source && cycEdge.target === edge.target)
  277. }).map((edge) => {
  278. edge.type = 'custom'
  279. if (!edge.sourceHandle)
  280. edge.sourceHandle = 'source'
  281. if (!edge.targetHandle)
  282. edge.targetHandle = 'target'
  283. if (!edge.data?.sourceType && edge.source && nodesMap[edge.source]) {
  284. edge.data = {
  285. ...edge.data,
  286. sourceType: nodesMap[edge.source].data.type!,
  287. } as any
  288. }
  289. if (!edge.data?.targetType && edge.target && nodesMap[edge.target]) {
  290. edge.data = {
  291. ...edge.data,
  292. targetType: nodesMap[edge.target].data.type!,
  293. } as any
  294. }
  295. if (selectedNode) {
  296. edge.data = {
  297. ...edge.data,
  298. _connectedNodeIsSelected: edge.source === selectedNode.id || edge.target === selectedNode.id,
  299. } as any
  300. }
  301. return edge
  302. })
  303. }
  304. export const getLayoutByDagre = (originNodes: Node[], originEdges: Edge[]) => {
  305. const dagreGraph = new dagre.graphlib.Graph()
  306. dagreGraph.setDefaultEdgeLabel(() => ({}))
  307. const nodes = cloneDeep(originNodes).filter(node => !node.parentId && node.type === CUSTOM_NODE)
  308. const edges = cloneDeep(originEdges).filter(edge => !edge.data?.isInIteration)
  309. dagreGraph.setGraph({
  310. rankdir: 'LR',
  311. align: 'UL',
  312. nodesep: 40,
  313. ranksep: 60,
  314. ranker: 'tight-tree',
  315. marginx: 30,
  316. marginy: 200,
  317. })
  318. nodes.forEach((node) => {
  319. dagreGraph.setNode(node.id, {
  320. width: node.width!,
  321. height: node.height!,
  322. })
  323. })
  324. edges.forEach((edge) => {
  325. dagreGraph.setEdge(edge.source, edge.target)
  326. })
  327. dagre.layout(dagreGraph)
  328. return dagreGraph
  329. }
  330. export const canRunBySingle = (nodeType: BlockEnum) => {
  331. return nodeType === BlockEnum.LLM
  332. || nodeType === BlockEnum.KnowledgeRetrieval
  333. || nodeType === BlockEnum.Code
  334. || nodeType === BlockEnum.TemplateTransform
  335. || nodeType === BlockEnum.QuestionClassifier
  336. || nodeType === BlockEnum.HttpRequest
  337. || nodeType === BlockEnum.Tool
  338. || nodeType === BlockEnum.ParameterExtractor
  339. || nodeType === BlockEnum.Iteration
  340. || nodeType === BlockEnum.DocExtractor
  341. }
  342. type ConnectedSourceOrTargetNodesChange = {
  343. type: string
  344. edge: Edge
  345. }[]
  346. export const getNodesConnectedSourceOrTargetHandleIdsMap = (changes: ConnectedSourceOrTargetNodesChange, nodes: Node[]) => {
  347. const nodesConnectedSourceOrTargetHandleIdsMap = {} as Record<string, any>
  348. changes.forEach((change) => {
  349. const {
  350. edge,
  351. type,
  352. } = change
  353. const sourceNode = nodes.find(node => node.id === edge.source)!
  354. if (sourceNode) {
  355. nodesConnectedSourceOrTargetHandleIdsMap[sourceNode.id] = nodesConnectedSourceOrTargetHandleIdsMap[sourceNode.id] || {
  356. _connectedSourceHandleIds: [...(sourceNode?.data._connectedSourceHandleIds || [])],
  357. _connectedTargetHandleIds: [...(sourceNode?.data._connectedTargetHandleIds || [])],
  358. }
  359. }
  360. const targetNode = nodes.find(node => node.id === edge.target)!
  361. if (targetNode) {
  362. nodesConnectedSourceOrTargetHandleIdsMap[targetNode.id] = nodesConnectedSourceOrTargetHandleIdsMap[targetNode.id] || {
  363. _connectedSourceHandleIds: [...(targetNode?.data._connectedSourceHandleIds || [])],
  364. _connectedTargetHandleIds: [...(targetNode?.data._connectedTargetHandleIds || [])],
  365. }
  366. }
  367. if (sourceNode) {
  368. if (type === 'remove') {
  369. const index = nodesConnectedSourceOrTargetHandleIdsMap[sourceNode.id]._connectedSourceHandleIds.findIndex((handleId: string) => handleId === edge.sourceHandle)
  370. nodesConnectedSourceOrTargetHandleIdsMap[sourceNode.id]._connectedSourceHandleIds.splice(index, 1)
  371. }
  372. if (type === 'add')
  373. nodesConnectedSourceOrTargetHandleIdsMap[sourceNode.id]._connectedSourceHandleIds.push(edge.sourceHandle || 'source')
  374. }
  375. if (targetNode) {
  376. if (type === 'remove') {
  377. const index = nodesConnectedSourceOrTargetHandleIdsMap[targetNode.id]._connectedTargetHandleIds.findIndex((handleId: string) => handleId === edge.targetHandle)
  378. nodesConnectedSourceOrTargetHandleIdsMap[targetNode.id]._connectedTargetHandleIds.splice(index, 1)
  379. }
  380. if (type === 'add')
  381. nodesConnectedSourceOrTargetHandleIdsMap[targetNode.id]._connectedTargetHandleIds.push(edge.targetHandle || 'target')
  382. }
  383. })
  384. return nodesConnectedSourceOrTargetHandleIdsMap
  385. }
  386. export const genNewNodeTitleFromOld = (oldTitle: string) => {
  387. const regex = /^(.+?)\s*\((\d+)\)\s*$/
  388. const match = oldTitle.match(regex)
  389. if (match) {
  390. const title = match[1]
  391. const num = parseInt(match[2], 10)
  392. return `${title} (${num + 1})`
  393. }
  394. else {
  395. return `${oldTitle} (1)`
  396. }
  397. }
  398. export const getValidTreeNodes = (nodes: Node[], edges: Edge[]) => {
  399. const startNode = nodes.find(node => node.data.type === BlockEnum.Start)
  400. if (!startNode) {
  401. return {
  402. validNodes: [],
  403. maxDepth: 0,
  404. }
  405. }
  406. const list: Node[] = [startNode]
  407. let maxDepth = 1
  408. const traverse = (root: Node, depth: number) => {
  409. if (depth > maxDepth)
  410. maxDepth = depth
  411. const outgoers = getOutgoers(root, nodes, edges)
  412. if (outgoers.length) {
  413. outgoers.forEach((outgoer) => {
  414. list.push(outgoer)
  415. if (outgoer.data.type === BlockEnum.Iteration)
  416. list.push(...nodes.filter(node => node.parentId === outgoer.id))
  417. traverse(outgoer, depth + 1)
  418. })
  419. }
  420. else {
  421. list.push(root)
  422. if (root.data.type === BlockEnum.Iteration)
  423. list.push(...nodes.filter(node => node.parentId === root.id))
  424. }
  425. }
  426. traverse(startNode, maxDepth)
  427. return {
  428. validNodes: uniqBy(list, 'id'),
  429. maxDepth,
  430. }
  431. }
  432. export const getToolCheckParams = (
  433. toolData: ToolNodeType,
  434. buildInTools: ToolWithProvider[],
  435. customTools: ToolWithProvider[],
  436. workflowTools: ToolWithProvider[],
  437. language: string,
  438. ) => {
  439. const { provider_id, provider_type, tool_name } = toolData
  440. const isBuiltIn = provider_type === CollectionType.builtIn
  441. const currentTools = provider_type === CollectionType.builtIn ? buildInTools : provider_type === CollectionType.custom ? customTools : workflowTools
  442. const currCollection = currentTools.find(item => item.id === provider_id)
  443. const currTool = currCollection?.tools.find(tool => tool.name === tool_name)
  444. const formSchemas = currTool ? toolParametersToFormSchemas(currTool.parameters) : []
  445. const toolInputVarSchema = formSchemas.filter((item: any) => item.form === 'llm')
  446. const toolSettingSchema = formSchemas.filter((item: any) => item.form !== 'llm')
  447. return {
  448. toolInputsSchema: (() => {
  449. const formInputs: InputVar[] = []
  450. toolInputVarSchema.forEach((item: any) => {
  451. formInputs.push({
  452. label: item.label[language] || item.label.en_US,
  453. variable: item.variable,
  454. type: item.type,
  455. required: item.required,
  456. })
  457. })
  458. return formInputs
  459. })(),
  460. notAuthed: isBuiltIn && !!currCollection?.allow_delete && !currCollection?.is_team_authorization,
  461. toolSettingSchema,
  462. language,
  463. }
  464. }
  465. export const changeNodesAndEdgesId = (nodes: Node[], edges: Edge[]) => {
  466. const idMap = nodes.reduce((acc, node) => {
  467. acc[node.id] = uuid4()
  468. return acc
  469. }, {} as Record<string, string>)
  470. const newNodes = nodes.map((node) => {
  471. return {
  472. ...node,
  473. id: idMap[node.id],
  474. }
  475. })
  476. const newEdges = edges.map((edge) => {
  477. return {
  478. ...edge,
  479. source: idMap[edge.source],
  480. target: idMap[edge.target],
  481. }
  482. })
  483. return [newNodes, newEdges] as [Node[], Edge[]]
  484. }
  485. export const isMac = () => {
  486. return navigator.userAgent.toUpperCase().includes('MAC')
  487. }
  488. const specialKeysNameMap: Record<string, string | undefined> = {
  489. ctrl: '⌘',
  490. alt: '⌥',
  491. shift: '⇧',
  492. }
  493. export const getKeyboardKeyNameBySystem = (key: string) => {
  494. if (isMac())
  495. return specialKeysNameMap[key] || key
  496. return key
  497. }
  498. const specialKeysCodeMap: Record<string, string | undefined> = {
  499. ctrl: 'meta',
  500. }
  501. export const getKeyboardKeyCodeBySystem = (key: string) => {
  502. if (isMac())
  503. return specialKeysCodeMap[key] || key
  504. return key
  505. }
  506. export const getTopLeftNodePosition = (nodes: Node[]) => {
  507. let minX = Infinity
  508. let minY = Infinity
  509. nodes.forEach((node) => {
  510. if (node.position.x < minX)
  511. minX = node.position.x
  512. if (node.position.y < minY)
  513. minY = node.position.y
  514. })
  515. return {
  516. x: minX,
  517. y: minY,
  518. }
  519. }
  520. export const isEventTargetInputArea = (target: HTMLElement) => {
  521. if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA')
  522. return true
  523. if (target.contentEditable === 'true')
  524. return true
  525. }
  526. export const variableTransformer = (v: ValueSelector | string) => {
  527. if (typeof v === 'string')
  528. return v.replace(/^{{#|#}}$/g, '').split('.')
  529. return `{{#${v.join('.')}#}}`
  530. }
  531. type ParallelInfoItem = {
  532. parallelNodeId: string
  533. depth: number
  534. isBranch?: boolean
  535. }
  536. type NodeParallelInfo = {
  537. parallelNodeId: string
  538. edgeHandleId: string
  539. depth: number
  540. }
  541. type NodeHandle = {
  542. node: Node
  543. handle: string
  544. }
  545. type NodeStreamInfo = {
  546. upstreamNodes: Set<string>
  547. downstreamEdges: Set<string>
  548. }
  549. export const getParallelInfo = (nodes: Node[], edges: Edge[], parentNodeId?: string) => {
  550. let startNode
  551. if (parentNodeId) {
  552. const parentNode = nodes.find(node => node.id === parentNodeId)
  553. if (!parentNode)
  554. throw new Error('Parent node not found')
  555. startNode = nodes.find(node => node.id === (parentNode.data as IterationNodeType).start_node_id)
  556. }
  557. else {
  558. startNode = nodes.find(node => node.data.type === BlockEnum.Start)
  559. }
  560. if (!startNode)
  561. throw new Error('Start node not found')
  562. const parallelList = [] as ParallelInfoItem[]
  563. const nextNodeHandles = [{ node: startNode, handle: 'source' }]
  564. let hasAbnormalEdges = false
  565. const traverse = (firstNodeHandle: NodeHandle) => {
  566. const nodeEdgesSet = {} as Record<string, Set<string>>
  567. const totalEdgesSet = new Set<string>()
  568. const nextHandles = [firstNodeHandle]
  569. const streamInfo = {} as Record<string, NodeStreamInfo>
  570. const parallelListItem = {
  571. parallelNodeId: '',
  572. depth: 0,
  573. } as ParallelInfoItem
  574. const nodeParallelInfoMap = {} as Record<string, NodeParallelInfo>
  575. nodeParallelInfoMap[firstNodeHandle.node.id] = {
  576. parallelNodeId: '',
  577. edgeHandleId: '',
  578. depth: 0,
  579. }
  580. while (nextHandles.length) {
  581. const currentNodeHandle = nextHandles.shift()!
  582. const { node: currentNode, handle: currentHandle = 'source' } = currentNodeHandle
  583. const currentNodeHandleKey = currentNode.id
  584. const connectedEdges = edges.filter(edge => edge.source === currentNode.id && edge.sourceHandle === currentHandle)
  585. const connectedEdgesLength = connectedEdges.length
  586. const outgoers = nodes.filter(node => connectedEdges.some(edge => edge.target === node.id))
  587. const incomers = getIncomers(currentNode, nodes, edges)
  588. if (!streamInfo[currentNodeHandleKey]) {
  589. streamInfo[currentNodeHandleKey] = {
  590. upstreamNodes: new Set<string>(),
  591. downstreamEdges: new Set<string>(),
  592. }
  593. }
  594. if (nodeEdgesSet[currentNodeHandleKey]?.size > 0 && incomers.length > 1) {
  595. const newSet = new Set<string>()
  596. for (const item of totalEdgesSet) {
  597. if (!streamInfo[currentNodeHandleKey].downstreamEdges.has(item))
  598. newSet.add(item)
  599. }
  600. if (isEqual(nodeEdgesSet[currentNodeHandleKey], newSet)) {
  601. parallelListItem.depth = nodeParallelInfoMap[currentNode.id].depth
  602. nextNodeHandles.push({ node: currentNode, handle: currentHandle })
  603. break
  604. }
  605. }
  606. if (nodeParallelInfoMap[currentNode.id].depth > parallelListItem.depth)
  607. parallelListItem.depth = nodeParallelInfoMap[currentNode.id].depth
  608. outgoers.forEach((outgoer) => {
  609. const outgoerConnectedEdges = getConnectedEdges([outgoer], edges).filter(edge => edge.source === outgoer.id)
  610. const sourceEdgesGroup = groupBy(outgoerConnectedEdges, 'sourceHandle')
  611. const incomers = getIncomers(outgoer, nodes, edges)
  612. if (outgoers.length > 1 && incomers.length > 1)
  613. hasAbnormalEdges = true
  614. Object.keys(sourceEdgesGroup).forEach((sourceHandle) => {
  615. nextHandles.push({ node: outgoer, handle: sourceHandle })
  616. })
  617. if (!outgoerConnectedEdges.length)
  618. nextHandles.push({ node: outgoer, handle: 'source' })
  619. const outgoerKey = outgoer.id
  620. if (!nodeEdgesSet[outgoerKey])
  621. nodeEdgesSet[outgoerKey] = new Set<string>()
  622. if (nodeEdgesSet[currentNodeHandleKey]) {
  623. for (const item of nodeEdgesSet[currentNodeHandleKey])
  624. nodeEdgesSet[outgoerKey].add(item)
  625. }
  626. if (!streamInfo[outgoerKey]) {
  627. streamInfo[outgoerKey] = {
  628. upstreamNodes: new Set<string>(),
  629. downstreamEdges: new Set<string>(),
  630. }
  631. }
  632. if (!nodeParallelInfoMap[outgoer.id]) {
  633. nodeParallelInfoMap[outgoer.id] = {
  634. ...nodeParallelInfoMap[currentNode.id],
  635. }
  636. }
  637. if (connectedEdgesLength > 1) {
  638. const edge = connectedEdges.find(edge => edge.target === outgoer.id)!
  639. nodeEdgesSet[outgoerKey].add(edge.id)
  640. totalEdgesSet.add(edge.id)
  641. streamInfo[currentNodeHandleKey].downstreamEdges.add(edge.id)
  642. streamInfo[outgoerKey].upstreamNodes.add(currentNodeHandleKey)
  643. for (const item of streamInfo[currentNodeHandleKey].upstreamNodes)
  644. streamInfo[item].downstreamEdges.add(edge.id)
  645. if (!parallelListItem.parallelNodeId)
  646. parallelListItem.parallelNodeId = currentNode.id
  647. const prevDepth = nodeParallelInfoMap[currentNode.id].depth + 1
  648. const currentDepth = nodeParallelInfoMap[outgoer.id].depth
  649. nodeParallelInfoMap[outgoer.id].depth = Math.max(prevDepth, currentDepth)
  650. }
  651. else {
  652. for (const item of streamInfo[currentNodeHandleKey].upstreamNodes)
  653. streamInfo[outgoerKey].upstreamNodes.add(item)
  654. nodeParallelInfoMap[outgoer.id].depth = nodeParallelInfoMap[currentNode.id].depth
  655. }
  656. })
  657. }
  658. parallelList.push(parallelListItem)
  659. }
  660. while (nextNodeHandles.length) {
  661. const nodeHandle = nextNodeHandles.shift()!
  662. traverse(nodeHandle)
  663. }
  664. return {
  665. parallelList,
  666. hasAbnormalEdges,
  667. }
  668. }
  669. export const hasErrorHandleNode = (nodeType?: BlockEnum) => {
  670. return nodeType === BlockEnum.LLM || nodeType === BlockEnum.Tool || nodeType === BlockEnum.HttpRequest || nodeType === BlockEnum.Code
  671. }
  672. export const getEdgeColor = (nodeRunningStatus?: NodeRunningStatus, isFailBranch?: boolean) => {
  673. if (nodeRunningStatus === NodeRunningStatus.Succeeded)
  674. return 'var(--color-workflow-link-line-success-handle)'
  675. if (nodeRunningStatus === NodeRunningStatus.Failed)
  676. return 'var(--color-workflow-link-line-error-handle)'
  677. if (nodeRunningStatus === NodeRunningStatus.Exception)
  678. return 'var(--color-workflow-link-line-failure-handle)'
  679. if (nodeRunningStatus === NodeRunningStatus.Running) {
  680. if (isFailBranch)
  681. return 'var(--color-workflow-link-line-failure-handle)'
  682. return 'var(--color-workflow-link-line-handle)'
  683. }
  684. return 'var(--color-workflow-link-line-normal)'
  685. }
  686. export const isExceptionVariable = (variable: string, nodeType?: BlockEnum) => {
  687. if ((variable === 'error_message' || variable === 'error_type') && hasErrorHandleNode(nodeType))
  688. return true
  689. return false
  690. }
  691. export const hasRetryNode = (nodeType?: BlockEnum) => {
  692. return nodeType === BlockEnum.LLM || nodeType === BlockEnum.Tool || nodeType === BlockEnum.HttpRequest || nodeType === BlockEnum.Code
  693. }