Merge branch 'feat/plugins' of https://github.com/langgenius/dify into feat/plugins
commit
055fb22b9b
@ -0,0 +1,28 @@
|
|||||||
|
import graphToLogStruct, { parseNodeString } from './graph-to-log-struct'
|
||||||
|
|
||||||
|
describe('graphToLogStruct', () => {
|
||||||
|
test('parseNodeString', () => {
|
||||||
|
expect(parseNodeString('(node1, param1, (node2, param2, (node3, param1)), param4)')).toEqual({
|
||||||
|
node: 'node1',
|
||||||
|
params: [
|
||||||
|
'param1',
|
||||||
|
{
|
||||||
|
node: 'node2',
|
||||||
|
params: [
|
||||||
|
'param2',
|
||||||
|
{
|
||||||
|
node: 'node3',
|
||||||
|
params: [
|
||||||
|
'param1',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'param4',
|
||||||
|
],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
test('retry nodes', () => {
|
||||||
|
console.log(graphToLogStruct('start -> (retry, 1, 3)'))
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -0,0 +1,122 @@
|
|||||||
|
const STEP_SPLIT = '->'
|
||||||
|
|
||||||
|
const toNodeData = (step: string, info: Record<string, any> = {}): any => {
|
||||||
|
const [nodeId, title] = step.split('@')
|
||||||
|
const data = {
|
||||||
|
id: nodeId,
|
||||||
|
node_id: nodeId,
|
||||||
|
title: title || nodeId,
|
||||||
|
execution_metadata: {},
|
||||||
|
status: 'succeeded',
|
||||||
|
}
|
||||||
|
// const executionMetadata = data.execution_metadata
|
||||||
|
const { isRetry } = info
|
||||||
|
if (isRetry)
|
||||||
|
data.status = 'retry'
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
const toRetryNodeData = ({
|
||||||
|
nodeId,
|
||||||
|
repeatTimes,
|
||||||
|
}: {
|
||||||
|
nodeId: string,
|
||||||
|
repeatTimes: number,
|
||||||
|
}): any => {
|
||||||
|
const res = [toNodeData(nodeId)]
|
||||||
|
for (let i = 0; i < repeatTimes; i++)
|
||||||
|
res.push(toNodeData(nodeId, { isRetry: true }))
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
type NodeStructure = {
|
||||||
|
node: string;
|
||||||
|
params: Array<string | NodeStructure>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseNodeString(input: string): NodeStructure {
|
||||||
|
input = input.trim()
|
||||||
|
if (input.startsWith('(') && input.endsWith(')'))
|
||||||
|
input = input.slice(1, -1)
|
||||||
|
|
||||||
|
const parts: Array<string | NodeStructure> = []
|
||||||
|
let current = ''
|
||||||
|
let depth = 0
|
||||||
|
|
||||||
|
for (let i = 0; i < input.length; i++) {
|
||||||
|
const char = input[i]
|
||||||
|
|
||||||
|
if (char === '(')
|
||||||
|
depth++
|
||||||
|
else if (char === ')')
|
||||||
|
depth--
|
||||||
|
|
||||||
|
if (char === ',' && depth === 0) {
|
||||||
|
parts.push(current.trim())
|
||||||
|
current = ''
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
current += char
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current)
|
||||||
|
parts.push(current.trim())
|
||||||
|
|
||||||
|
const result: NodeStructure = {
|
||||||
|
node: '',
|
||||||
|
params: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < parts.length; i++) {
|
||||||
|
const part = parts[i]
|
||||||
|
|
||||||
|
if (typeof part === 'string' && part.startsWith('('))
|
||||||
|
result.params.push(parseNodeString(part))
|
||||||
|
else if (i === 0)
|
||||||
|
result.node = part as string
|
||||||
|
else
|
||||||
|
result.params.push(part as string)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const toNodes = (input: string): any[] => {
|
||||||
|
const list = input.split(STEP_SPLIT)
|
||||||
|
.map(step => step.trim())
|
||||||
|
|
||||||
|
const res: any[] = []
|
||||||
|
list.forEach((step) => {
|
||||||
|
const isPlainStep = !step.includes('(')
|
||||||
|
if (isPlainStep) {
|
||||||
|
res.push(toNodeData(step))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const { node, params } = parseNodeString(step)
|
||||||
|
switch (node) {
|
||||||
|
case 'retry':
|
||||||
|
res.push(...toRetryNodeData({
|
||||||
|
nodeId: params[0] as string,
|
||||||
|
repeatTimes: Number.parseInt(params[1] as string),
|
||||||
|
}))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* : 1 -> 2 -> 3
|
||||||
|
* iteration: (iteration, 1, [2, 3]) -> 4. (1, [2, 3]) means 1 is parent, [2, 3] is children
|
||||||
|
* parallel: 1 -> (parallel, [1,2,3], [4, (parallel: (6,7))]).
|
||||||
|
* retry: (retry, 1, 3). 1 is parent, 3 is retry times
|
||||||
|
*/
|
||||||
|
const graphToLogStruct = (input: string): any[] => {
|
||||||
|
const list = toNodes(input)
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
|
export default graphToLogStruct
|
||||||
@ -1,105 +0,0 @@
|
|||||||
import type { NodeTracing } from '@/types/workflow'
|
|
||||||
import { BlockEnum } from '../../../types'
|
|
||||||
|
|
||||||
type IterationNodeId = string
|
|
||||||
type RunIndex = string
|
|
||||||
type IterationGroupMap = Map<IterationNodeId, Map<RunIndex, NodeTracing[]>>
|
|
||||||
|
|
||||||
const processIterationNode = (item: NodeTracing) => {
|
|
||||||
return {
|
|
||||||
...item,
|
|
||||||
details: [], // to add the sub nodes in the iteration
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateParallelModeGroup = (nodeGroupMap: IterationGroupMap, runIndex: string, item: NodeTracing, iterationNode: NodeTracing) => {
|
|
||||||
if (!nodeGroupMap.has(iterationNode.node_id))
|
|
||||||
nodeGroupMap.set(iterationNode.node_id, new Map())
|
|
||||||
|
|
||||||
const groupMap = nodeGroupMap.get(iterationNode.node_id)!
|
|
||||||
|
|
||||||
if (!groupMap.has(runIndex))
|
|
||||||
groupMap.set(runIndex, [item])
|
|
||||||
|
|
||||||
else
|
|
||||||
groupMap.get(runIndex)!.push(item)
|
|
||||||
|
|
||||||
if (item.status === 'failed') {
|
|
||||||
iterationNode.status = 'failed'
|
|
||||||
iterationNode.error = item.error
|
|
||||||
}
|
|
||||||
|
|
||||||
iterationNode.details = Array.from(groupMap.values())
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateSequentialModeGroup = (runIndex: number, item: NodeTracing, iterationNode: NodeTracing) => {
|
|
||||||
const { details } = iterationNode
|
|
||||||
if (details) {
|
|
||||||
if (!details[runIndex])
|
|
||||||
details[runIndex] = [item]
|
|
||||||
else
|
|
||||||
details[runIndex].push(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.status === 'failed') {
|
|
||||||
iterationNode.status = 'failed'
|
|
||||||
iterationNode.error = item.error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const addRetryDetail = (result: NodeTracing[], item: NodeTracing) => {
|
|
||||||
const retryNode = result.find(node => node.node_id === item.node_id)
|
|
||||||
|
|
||||||
if (retryNode) {
|
|
||||||
if (retryNode?.retryDetail)
|
|
||||||
retryNode.retryDetail.push(item)
|
|
||||||
else
|
|
||||||
retryNode.retryDetail = [item]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const processNonIterationNode = (result: NodeTracing[], nodeGroupMap: IterationGroupMap, item: NodeTracing) => {
|
|
||||||
const { execution_metadata } = item
|
|
||||||
if (!execution_metadata?.iteration_id) {
|
|
||||||
if (item.status === 'retry') {
|
|
||||||
addRetryDetail(result, item)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
result.push(item)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const parentIterationNode = result.find(node => node.node_id === execution_metadata.iteration_id)
|
|
||||||
const isInIteration = !!parentIterationNode && Array.isArray(parentIterationNode.details)
|
|
||||||
if (!isInIteration)
|
|
||||||
return
|
|
||||||
|
|
||||||
// the parallel in the iteration in mode.
|
|
||||||
const { parallel_mode_run_id, iteration_index = 0 } = execution_metadata
|
|
||||||
const isInParallel = !!parallel_mode_run_id
|
|
||||||
|
|
||||||
if (isInParallel)
|
|
||||||
updateParallelModeGroup(nodeGroupMap, parallel_mode_run_id, item, parentIterationNode)
|
|
||||||
else
|
|
||||||
updateSequentialModeGroup(iteration_index, item, parentIterationNode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// list => tree. Put the iteration node's children into the details field.
|
|
||||||
const formatToTracingNodeList = (list: NodeTracing[]) => {
|
|
||||||
const allItems = [...list].reverse()
|
|
||||||
const result: NodeTracing[] = []
|
|
||||||
const iterationGroupMap = new Map<string, Map<string, NodeTracing[]>>()
|
|
||||||
|
|
||||||
allItems.forEach((item) => {
|
|
||||||
item.node_type === BlockEnum.Iteration
|
|
||||||
? result.push(processIterationNode(item))
|
|
||||||
: processNonIterationNode(result, iterationGroupMap, item)
|
|
||||||
})
|
|
||||||
|
|
||||||
// console.log(allItems)
|
|
||||||
// console.log(result)
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
export default formatToTracingNodeList
|
|
||||||
@ -1,133 +0,0 @@
|
|||||||
export const simpleRetryData = (() => {
|
|
||||||
const startNode = {
|
|
||||||
id: 'f7938b2b-77cd-43f0-814c-2f0ade7cbc60',
|
|
||||||
index: 1,
|
|
||||||
predecessor_node_id: null,
|
|
||||||
node_id: '1735112903395',
|
|
||||||
node_type: 'start',
|
|
||||||
title: 'Start',
|
|
||||||
inputs: {
|
|
||||||
'sys.files': [],
|
|
||||||
'sys.user_id': '6d8ad01f-edf9-43a6-b863-a034b1828ac7',
|
|
||||||
'sys.app_id': '6180ead7-2190-4a61-975c-ec3bf29653da',
|
|
||||||
'sys.workflow_id': 'eef6da45-244b-4c79-958e-f3573f7c12bb',
|
|
||||||
'sys.workflow_run_id': 'fc8970ef-1406-484e-afde-8567dc22f34c',
|
|
||||||
},
|
|
||||||
process_data: null,
|
|
||||||
outputs: {
|
|
||||||
'sys.files': [],
|
|
||||||
'sys.user_id': '6d8ad01f-edf9-43a6-b863-a034b1828ac7',
|
|
||||||
'sys.app_id': '6180ead7-2190-4a61-975c-ec3bf29653da',
|
|
||||||
'sys.workflow_id': 'eef6da45-244b-4c79-958e-f3573f7c12bb',
|
|
||||||
'sys.workflow_run_id': 'fc8970ef-1406-484e-afde-8567dc22f34c',
|
|
||||||
},
|
|
||||||
status: 'succeeded',
|
|
||||||
error: null,
|
|
||||||
elapsed_time: 0.008715,
|
|
||||||
execution_metadata: null,
|
|
||||||
extras: {},
|
|
||||||
created_at: 1735112940,
|
|
||||||
created_by_role: 'account',
|
|
||||||
created_by_account: {
|
|
||||||
id: '6d8ad01f-edf9-43a6-b863-a034b1828ac7',
|
|
||||||
name: '九彩拼盘',
|
|
||||||
email: 'iamjoel007@gmail.com',
|
|
||||||
},
|
|
||||||
created_by_end_user: null,
|
|
||||||
finished_at: 1735112940,
|
|
||||||
}
|
|
||||||
|
|
||||||
const httpNode = {
|
|
||||||
id: '50220407-3420-4ad4-89da-c6959710d1aa',
|
|
||||||
index: 2,
|
|
||||||
predecessor_node_id: '1735112903395',
|
|
||||||
node_id: '1735112908006',
|
|
||||||
node_type: 'http-request',
|
|
||||||
title: 'HTTP Request',
|
|
||||||
inputs: null,
|
|
||||||
process_data: {
|
|
||||||
request: 'GET / HTTP/1.1\r\nHost: 404\r\n\r\n',
|
|
||||||
},
|
|
||||||
outputs: null,
|
|
||||||
status: 'failed',
|
|
||||||
error: 'timed out',
|
|
||||||
elapsed_time: 30.247757,
|
|
||||||
execution_metadata: null,
|
|
||||||
extras: {},
|
|
||||||
created_at: 1735112940,
|
|
||||||
created_by_role: 'account',
|
|
||||||
created_by_account: {
|
|
||||||
id: '6d8ad01f-edf9-43a6-b863-a034b1828ac7',
|
|
||||||
name: '九彩拼盘',
|
|
||||||
email: 'iamjoel007@gmail.com',
|
|
||||||
},
|
|
||||||
created_by_end_user: null,
|
|
||||||
finished_at: 1735112970,
|
|
||||||
}
|
|
||||||
|
|
||||||
const retry1 = {
|
|
||||||
id: 'ed352b36-27fb-49c6-9e8f-cc755bfc25fc',
|
|
||||||
index: 3,
|
|
||||||
predecessor_node_id: '1735112903395',
|
|
||||||
node_id: '1735112908006',
|
|
||||||
node_type: 'http-request',
|
|
||||||
title: 'HTTP Request',
|
|
||||||
inputs: null,
|
|
||||||
process_data: null,
|
|
||||||
outputs: null,
|
|
||||||
status: 'retry',
|
|
||||||
error: 'timed out',
|
|
||||||
elapsed_time: 10.011833,
|
|
||||||
execution_metadata: {
|
|
||||||
iteration_id: null,
|
|
||||||
parallel_mode_run_id: null,
|
|
||||||
},
|
|
||||||
extras: {},
|
|
||||||
created_at: 1735112940,
|
|
||||||
created_by_role: 'account',
|
|
||||||
created_by_account: {
|
|
||||||
id: '6d8ad01f-edf9-43a6-b863-a034b1828ac7',
|
|
||||||
name: '九彩拼盘',
|
|
||||||
email: 'iamjoel007@gmail.com',
|
|
||||||
},
|
|
||||||
created_by_end_user: null,
|
|
||||||
finished_at: 1735112950,
|
|
||||||
}
|
|
||||||
|
|
||||||
const retry2 = {
|
|
||||||
id: '74dfb3d3-dacf-44f2-8784-e36bfa2d6c4e',
|
|
||||||
index: 4,
|
|
||||||
predecessor_node_id: '1735112903395',
|
|
||||||
node_id: '1735112908006',
|
|
||||||
node_type: 'http-request',
|
|
||||||
title: 'HTTP Request',
|
|
||||||
inputs: null,
|
|
||||||
process_data: null,
|
|
||||||
outputs: null,
|
|
||||||
status: 'retry',
|
|
||||||
error: 'timed out',
|
|
||||||
elapsed_time: 10.010368,
|
|
||||||
execution_metadata: {
|
|
||||||
iteration_id: null,
|
|
||||||
parallel_mode_run_id: null,
|
|
||||||
},
|
|
||||||
extras: {},
|
|
||||||
created_at: 1735112950,
|
|
||||||
created_by_role: 'account',
|
|
||||||
created_by_account: {
|
|
||||||
id: '6d8ad01f-edf9-43a6-b863-a034b1828ac7',
|
|
||||||
name: '九彩拼盘',
|
|
||||||
email: 'iamjoel007@gmail.com',
|
|
||||||
},
|
|
||||||
created_by_end_user: null,
|
|
||||||
finished_at: 1735112960,
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
in: [startNode, httpNode, retry1, retry2],
|
|
||||||
expect: [startNode, {
|
|
||||||
...httpNode,
|
|
||||||
retryDetail: [retry1, retry2],
|
|
||||||
}],
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
@ -1,11 +1,21 @@
|
|||||||
import format from '.'
|
import format from '.'
|
||||||
import { simpleRetryData } from './data'
|
import graphToLogStruct from '../graph-to-log-struct'
|
||||||
|
|
||||||
describe('retry', () => {
|
describe('retry', () => {
|
||||||
|
// retry nodeId:1 3 times.
|
||||||
|
const steps = graphToLogStruct('start -> (retry, 1, 3)')
|
||||||
|
const [startNode, retryNode, ...retryDetail] = steps
|
||||||
|
const result = format(steps)
|
||||||
test('should have no retry status nodes', () => {
|
test('should have no retry status nodes', () => {
|
||||||
expect(format(simpleRetryData.in as any).find(item => (item as any).status === 'retry')).toBeUndefined()
|
expect(result.find(item => (item as any).status === 'retry')).toBeUndefined()
|
||||||
})
|
})
|
||||||
test('should put retry nodes in retryDetail', () => {
|
test('should put retry nodes in retryDetail', () => {
|
||||||
expect(format(simpleRetryData.in as any)).toEqual(simpleRetryData.expect)
|
expect(result).toEqual([
|
||||||
|
startNode,
|
||||||
|
{
|
||||||
|
...retryNode,
|
||||||
|
retryDetail,
|
||||||
|
},
|
||||||
|
])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,14 +0,0 @@
|
|||||||
const STEP_SPLIT = '->'
|
|
||||||
|
|
||||||
/*
|
|
||||||
* : 1 -> 2 -> 3
|
|
||||||
* iteration: (iteration, 1, [2, 3]) -> 4. (1, [2, 3]) means 1 is parent, [2, 3] is children
|
|
||||||
* parallel: 1 -> (parallel, [1,2,3], [4, (parallel: (6,7))]).
|
|
||||||
* retry: (retry, 1, [2,3]). 1 is parent, [2, 3] is retry nodes
|
|
||||||
*/
|
|
||||||
const simpleGraphToLogStruct = (input: string): any[] => {
|
|
||||||
const list = input.split(STEP_SPLIT)
|
|
||||||
return list
|
|
||||||
}
|
|
||||||
|
|
||||||
export default simpleGraphToLogStruct
|
|
||||||
Loading…
Reference in New Issue