graph-to-log-struct.ts 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357
  1. type IterationInfo = { iterationId: string; iterationIndex: number }
  2. type LoopInfo = { loopId: string; loopIndex: number }
  3. type NodePlain = { nodeType: 'plain'; nodeId: string; } & (Partial<IterationInfo> & Partial<LoopInfo>)
  4. type NodeComplex = { nodeType: string; nodeId: string; params: (NodePlain | (NodeComplex & (Partial<IterationInfo> & Partial<LoopInfo>)) | Node[] | number)[] } & (Partial<IterationInfo> & Partial<LoopInfo>)
  5. type Node = NodePlain | NodeComplex
  6. /**
  7. * Parses a DSL string into an array of node objects.
  8. * @param dsl - The input DSL string.
  9. * @returns An array of parsed nodes.
  10. */
  11. function parseDSL(dsl: string): NodeData[] {
  12. return convertToNodeData(parseTopLevelFlow(dsl).map(nodeStr => parseNode(nodeStr)))
  13. }
  14. /**
  15. * Splits a top-level flow string by "->", respecting nested structures.
  16. * @param dsl - The DSL string to split.
  17. * @returns An array of top-level segments.
  18. */
  19. function parseTopLevelFlow(dsl: string): string[] {
  20. const segments: string[] = []
  21. let buffer = ''
  22. let nested = 0
  23. for (let i = 0; i < dsl.length; i++) {
  24. const char = dsl[i]
  25. if (char === '(') nested++
  26. if (char === ')') nested--
  27. if (char === '-' && dsl[i + 1] === '>' && nested === 0) {
  28. segments.push(buffer.trim())
  29. buffer = ''
  30. i++ // Skip the ">" character
  31. }
  32. else {
  33. buffer += char
  34. }
  35. }
  36. if (buffer.trim())
  37. segments.push(buffer.trim())
  38. return segments
  39. }
  40. /**
  41. * Parses a single node string.
  42. * If the node is complex (e.g., has parentheses), it extracts the node type, node ID, and parameters.
  43. * @param nodeStr - The node string to parse.
  44. * @param parentIterationId - The ID of the parent iteration node (if applicable).
  45. * @param parentLoopId - The ID of the parent loop node (if applicable).
  46. * @returns A parsed node object.
  47. */
  48. function parseNode(nodeStr: string, parentIterationId?: string, parentLoopId?: string): Node {
  49. // Check if the node is a complex node
  50. if (nodeStr.startsWith('(') && nodeStr.endsWith(')')) {
  51. const innerContent = nodeStr.slice(1, -1).trim() // Remove outer parentheses
  52. let nested = 0
  53. let buffer = ''
  54. const parts: string[] = []
  55. // Split the inner content by commas, respecting nested parentheses
  56. for (let i = 0; i < innerContent.length; i++) {
  57. const char = innerContent[i]
  58. if (char === '(') nested++
  59. if (char === ')') nested--
  60. if (char === ',' && nested === 0) {
  61. parts.push(buffer.trim())
  62. buffer = ''
  63. }
  64. else {
  65. buffer += char
  66. }
  67. }
  68. parts.push(buffer.trim())
  69. // Extract nodeType, nodeId, and params
  70. const [nodeType, nodeId, ...paramsRaw] = parts
  71. const params = parseParams(paramsRaw, nodeType === 'iteration' ? nodeId.trim() : parentIterationId, nodeType === 'loop' ? nodeId.trim() : parentLoopId)
  72. const complexNode = {
  73. nodeType: nodeType.trim(),
  74. nodeId: nodeId.trim(),
  75. params,
  76. }
  77. if (parentIterationId) {
  78. (complexNode as any).iterationId = parentIterationId;
  79. (complexNode as any).iterationIndex = 0 // Fixed as 0
  80. }
  81. if (parentLoopId) {
  82. (complexNode as any).loopId = parentLoopId;
  83. (complexNode as any).loopIndex = 0 // Fixed as 0
  84. }
  85. return complexNode
  86. }
  87. // If it's not a complex node, treat it as a plain node
  88. const plainNode: NodePlain = { nodeType: 'plain', nodeId: nodeStr.trim() }
  89. if (parentIterationId) {
  90. plainNode.iterationId = parentIterationId
  91. plainNode.iterationIndex = 0 // Fixed as 0
  92. }
  93. if (parentLoopId) {
  94. plainNode.loopId = parentLoopId
  95. plainNode.loopIndex = 0 // Fixed as 0
  96. }
  97. return plainNode
  98. }
  99. /**
  100. * Parses parameters of a complex node.
  101. * Supports nested flows and complex sub-nodes.
  102. * Adds iteration-specific metadata recursively.
  103. * @param paramParts - The parameters string split by commas.
  104. * @param parentIterationId - The ID of the parent iteration node (if applicable).
  105. * @param parentLoopId - The ID of the parent loop node (if applicable).
  106. * @returns An array of parsed parameters (plain nodes, nested nodes, or flows).
  107. */
  108. function parseParams(paramParts: string[], parentIteration?: string, parentLoopId?: string): (Node | Node[] | number)[] {
  109. return paramParts.map((part) => {
  110. if (part.includes('->')) {
  111. // Parse as a flow and return an array of nodes
  112. return parseTopLevelFlow(part).map(node => parseNode(node, parentIteration || undefined, parentLoopId || undefined))
  113. }
  114. else if (part.startsWith('(')) {
  115. // Parse as a nested complex node
  116. return parseNode(part, parentIteration || undefined, parentLoopId || undefined)
  117. }
  118. else if (!Number.isNaN(Number(part.trim()))) {
  119. // Parse as a numeric parameter
  120. return Number(part.trim())
  121. }
  122. else {
  123. // Parse as a plain node
  124. return parseNode(part, parentIteration || undefined, parentLoopId || undefined)
  125. }
  126. })
  127. }
  128. type NodeData = {
  129. id: string;
  130. node_id: string;
  131. title: string;
  132. node_type?: string;
  133. execution_metadata: Record<string, any>;
  134. status: string;
  135. }
  136. /**
  137. * Converts a plain node to node data.
  138. */
  139. function convertPlainNode(node: Node): NodeData[] {
  140. return [
  141. {
  142. id: node.nodeId,
  143. node_id: node.nodeId,
  144. title: node.nodeId,
  145. execution_metadata: {},
  146. status: 'succeeded',
  147. },
  148. ]
  149. }
  150. /**
  151. * Converts a retry node to node data.
  152. */
  153. function convertRetryNode(node: Node): NodeData[] {
  154. const { nodeId, iterationId, iterationIndex, loopId, loopIndex, params } = node as NodeComplex
  155. const retryCount = params ? Number.parseInt(params[0] as unknown as string, 10) : 0
  156. const result: NodeData[] = [
  157. {
  158. id: nodeId,
  159. node_id: nodeId,
  160. title: nodeId,
  161. execution_metadata: {},
  162. status: 'succeeded',
  163. },
  164. ]
  165. for (let i = 0; i < retryCount; i++) {
  166. result.push({
  167. id: nodeId,
  168. node_id: nodeId,
  169. title: nodeId,
  170. execution_metadata: iterationId ? {
  171. iteration_id: iterationId,
  172. iteration_index: iterationIndex || 0,
  173. } : loopId ? {
  174. loop_id: loopId,
  175. loop_index: loopIndex || 0,
  176. } : {},
  177. status: 'retry',
  178. })
  179. }
  180. return result
  181. }
  182. /**
  183. * Converts an iteration node to node data.
  184. */
  185. function convertIterationNode(node: Node): NodeData[] {
  186. const { nodeId, params } = node as NodeComplex
  187. const result: NodeData[] = [
  188. {
  189. id: nodeId,
  190. node_id: nodeId,
  191. title: nodeId,
  192. node_type: 'iteration',
  193. status: 'succeeded',
  194. execution_metadata: {},
  195. },
  196. ]
  197. params?.forEach((param: any) => {
  198. if (Array.isArray(param)) {
  199. param.forEach((childNode: Node) => {
  200. const childData = convertToNodeData([childNode])
  201. childData.forEach((data) => {
  202. data.execution_metadata = {
  203. ...data.execution_metadata,
  204. iteration_id: nodeId,
  205. iteration_index: 0,
  206. }
  207. })
  208. result.push(...childData)
  209. })
  210. }
  211. })
  212. return result
  213. }
  214. /**
  215. * Converts an loop node to node data.
  216. */
  217. function convertLoopNode(node: Node): NodeData[] {
  218. const { nodeId, params } = node as NodeComplex
  219. const result: NodeData[] = [
  220. {
  221. id: nodeId,
  222. node_id: nodeId,
  223. title: nodeId,
  224. node_type: 'loop',
  225. status: 'succeeded',
  226. execution_metadata: {},
  227. },
  228. ]
  229. params?.forEach((param: any) => {
  230. if (Array.isArray(param)) {
  231. param.forEach((childNode: Node) => {
  232. const childData = convertToNodeData([childNode])
  233. childData.forEach((data) => {
  234. data.execution_metadata = {
  235. ...data.execution_metadata,
  236. loop_id: nodeId,
  237. loop_index: 0,
  238. }
  239. })
  240. result.push(...childData)
  241. })
  242. }
  243. })
  244. return result
  245. }
  246. /**
  247. * Converts a parallel node to node data.
  248. */
  249. function convertParallelNode(node: Node, parentParallelId?: string, parentStartNodeId?: string): NodeData[] {
  250. const { nodeId, params } = node as NodeComplex
  251. const result: NodeData[] = [
  252. {
  253. id: nodeId,
  254. node_id: nodeId,
  255. title: nodeId,
  256. execution_metadata: {
  257. parallel_id: nodeId,
  258. },
  259. status: 'succeeded',
  260. },
  261. ]
  262. params?.forEach((param) => {
  263. if (Array.isArray(param)) {
  264. const startNodeId = param[0]?.nodeId
  265. param.forEach((childNode: Node) => {
  266. const childData = convertToNodeData([childNode])
  267. childData.forEach((data) => {
  268. data.execution_metadata = {
  269. ...data.execution_metadata,
  270. parallel_id: nodeId,
  271. parallel_start_node_id: startNodeId,
  272. ...(parentParallelId && {
  273. parent_parallel_id: parentParallelId,
  274. parent_parallel_start_node_id: parentStartNodeId,
  275. }),
  276. }
  277. })
  278. result.push(...childData)
  279. })
  280. }
  281. else if (param && typeof param === 'object') {
  282. const startNodeId = param.nodeId
  283. const childData = convertToNodeData([param])
  284. childData.forEach((data) => {
  285. data.execution_metadata = {
  286. ...data.execution_metadata,
  287. parallel_id: nodeId,
  288. parallel_start_node_id: startNodeId,
  289. ...(parentParallelId && {
  290. parent_parallel_id: parentParallelId,
  291. parent_parallel_start_node_id: parentStartNodeId,
  292. }),
  293. }
  294. })
  295. result.push(...childData)
  296. }
  297. })
  298. return result
  299. }
  300. /**
  301. * Main function to convert nodes to node data.
  302. */
  303. function convertToNodeData(nodes: Node[], parentParallelId?: string, parentStartNodeId?: string): NodeData[] {
  304. const result: NodeData[] = []
  305. nodes.forEach((node) => {
  306. switch (node.nodeType) {
  307. case 'plain':
  308. result.push(...convertPlainNode(node))
  309. break
  310. case 'retry':
  311. result.push(...convertRetryNode(node))
  312. break
  313. case 'iteration':
  314. result.push(...convertIterationNode(node))
  315. break
  316. case 'loop':
  317. result.push(...convertLoopNode(node))
  318. break
  319. case 'parallel':
  320. result.push(...convertParallelNode(node, parentParallelId, parentStartNodeId))
  321. break
  322. default:
  323. throw new Error(`Unknown nodeType: ${node.nodeType}`)
  324. }
  325. })
  326. return result
  327. }
  328. export default parseDSL