mirror of
https://github.com/langgenius/dify.git
synced 2026-11-13 06:41:07 +08:00
176 lines
6.7 KiB
Python
176 lines
6.7 KiB
Python
"""Deterministically reconstruct the span hierarchy of one workflow run.
|
|
|
|
Parents come from graph predecessors, workflow tools, and iteration/loop containers;
|
|
each iteration or loop round gets a synthetic wrapper. Ambiguous repeated node
|
|
IDs within an invocation and cyclic predecessor data are dropped rather than guessed.
|
|
"""
|
|
|
|
import re
|
|
from collections.abc import Mapping, Sequence
|
|
from dataclasses import dataclass
|
|
from datetime import datetime, timedelta
|
|
from typing import Any, Literal
|
|
|
|
from graphon.entities import WorkflowNodeExecution
|
|
from graphon.enums import WorkflowNodeExecutionStatus
|
|
|
|
_WRAPPER_INDEX_PATTERN = re.compile(r"^[A-Za-z0-9_.:-]+$")
|
|
_WRAPPER_FIELDS: tuple[tuple[Literal["iteration", "loop"], str, str], ...] = (
|
|
("iteration", "iteration_id", "iteration_index"),
|
|
("loop", "loop_id", "loop_index"),
|
|
)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class WrapperKey:
|
|
kind: Literal["iteration", "loop"]
|
|
container_execution_id: str
|
|
index: str
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class WrapperSpec:
|
|
id: str
|
|
key: WrapperKey
|
|
parent_execution_id: str
|
|
child_execution_ids: frozenset[str]
|
|
start_time: datetime
|
|
end_time: datetime
|
|
has_error: bool
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class WorkflowHierarchy:
|
|
parent_by_execution_id: Mapping[str, str]
|
|
wrapper_by_child_execution_id: Mapping[str, WrapperSpec]
|
|
wrappers: tuple[WrapperSpec, ...]
|
|
|
|
|
|
def execution_metadata(execution: WorkflowNodeExecution) -> dict[str, Any]:
|
|
return {str(key): value for key, value in (execution.metadata or {}).items()}
|
|
|
|
|
|
def finished_at(execution: WorkflowNodeExecution) -> datetime:
|
|
return execution.finished_at or execution.created_at + timedelta(seconds=execution.elapsed_time or 0)
|
|
|
|
|
|
type _NodeKey = tuple[str, str | None, str | None, str]
|
|
|
|
|
|
def _node_key(item: WorkflowNodeExecution, node_id: str, tool_parents: Mapping[str, str]) -> _NodeKey:
|
|
invocation = (item.process_data or {}).get("workflow_tool_invocation_id")
|
|
return item.workflow_id, tool_parents.get(item.id), invocation if isinstance(invocation, str) else None, node_id
|
|
|
|
|
|
def _unique_execution_by_node_id(
|
|
executions: Sequence[WorkflowNodeExecution], tool_parents: Mapping[str, str]
|
|
) -> dict[_NodeKey, str]:
|
|
result: dict[_NodeKey, str] = {}
|
|
ambiguous: set[_NodeKey] = set()
|
|
for item in executions:
|
|
key = _node_key(item, item.node_id, tool_parents)
|
|
if key in ambiguous:
|
|
continue
|
|
previous = result.get(key)
|
|
if previous is None:
|
|
result[key] = item.id
|
|
elif previous != item.id:
|
|
result.pop(key, None)
|
|
ambiguous.add(key)
|
|
return result
|
|
|
|
|
|
def _normalize_index(value: Any) -> str | None:
|
|
if isinstance(value, bool):
|
|
return None
|
|
if isinstance(value, int):
|
|
return str(value) if value >= 0 else None
|
|
if isinstance(value, str) and _WRAPPER_INDEX_PATTERN.fullmatch(value):
|
|
return value
|
|
return None
|
|
|
|
|
|
def _remove_cycles(parent_by_execution_id: dict[str, str]) -> None:
|
|
cyclic: set[str] = set()
|
|
for start in sorted(parent_by_execution_id):
|
|
path: list[str] = []
|
|
position: dict[str, int] = {}
|
|
current = start
|
|
while current in parent_by_execution_id:
|
|
if current in position:
|
|
cyclic.update(path[position[current] :])
|
|
break
|
|
position[current] = len(path)
|
|
path.append(current)
|
|
current = parent_by_execution_id[current]
|
|
for execution in cyclic:
|
|
parent_by_execution_id.pop(execution, None)
|
|
|
|
|
|
def build_workflow_hierarchy(
|
|
executions: Sequence[WorkflowNodeExecution], tool_parents: Mapping[str, str]
|
|
) -> WorkflowHierarchy:
|
|
execution_by_node_id = _unique_execution_by_node_id(executions, tool_parents)
|
|
parent_by_execution_id: dict[str, str] = {}
|
|
|
|
for item in executions:
|
|
parent_execution_id = (
|
|
execution_by_node_id.get(_node_key(item, item.predecessor_node_id, tool_parents))
|
|
if item.predecessor_node_id is not None
|
|
else None
|
|
)
|
|
if parent_execution_id is None:
|
|
metadata = execution_metadata(item)
|
|
for structured_key in ("iteration_id", "loop_id"):
|
|
container_node_id = metadata.get(structured_key)
|
|
if isinstance(container_node_id, str):
|
|
parent_execution_id = execution_by_node_id.get(_node_key(item, container_node_id, tool_parents))
|
|
if parent_execution_id is not None:
|
|
break
|
|
if parent_execution_id is None:
|
|
parent_execution_id = tool_parents.get(item.id)
|
|
if parent_execution_id and parent_execution_id != item.id:
|
|
parent_by_execution_id[item.id] = parent_execution_id
|
|
|
|
_remove_cycles(parent_by_execution_id)
|
|
|
|
grouped: dict[WrapperKey, list[WorkflowNodeExecution]] = {}
|
|
for item in executions:
|
|
metadata = execution_metadata(item)
|
|
for kind, container_key, index_key in _WRAPPER_FIELDS:
|
|
container_node_id = metadata.get(container_key)
|
|
index = _normalize_index(metadata.get(index_key))
|
|
if not isinstance(container_node_id, str) or index is None:
|
|
continue
|
|
container_execution_id = execution_by_node_id.get(_node_key(item, container_node_id, tool_parents))
|
|
if container_execution_id is None or container_execution_id == item.id:
|
|
continue
|
|
wrapper_key = WrapperKey(kind=kind, container_execution_id=container_execution_id, index=index)
|
|
grouped.setdefault(wrapper_key, []).append(item)
|
|
break
|
|
|
|
wrappers: list[WrapperSpec] = []
|
|
wrapper_by_child_execution_id: dict[str, WrapperSpec] = {}
|
|
for wrapper_key in sorted(grouped, key=lambda key: (key.kind, key.container_execution_id, key.index)):
|
|
children = grouped[wrapper_key]
|
|
child_ids = frozenset(item.id for item in children)
|
|
wrapper = WrapperSpec(
|
|
id=f"{wrapper_key.kind}:{wrapper_key.container_execution_id}:{wrapper_key.index}",
|
|
key=wrapper_key,
|
|
parent_execution_id=wrapper_key.container_execution_id,
|
|
child_execution_ids=child_ids,
|
|
start_time=min(item.created_at for item in children),
|
|
end_time=max(finished_at(item) for item in children),
|
|
has_error=any(item.status == WorkflowNodeExecutionStatus.FAILED for item in children),
|
|
)
|
|
wrappers.append(wrapper)
|
|
for child_id in child_ids:
|
|
wrapper_by_child_execution_id[child_id] = wrapper
|
|
parent_by_execution_id[child_id] = wrapper.id
|
|
|
|
return WorkflowHierarchy(
|
|
parent_by_execution_id=parent_by_execution_id,
|
|
wrapper_by_child_execution_id=wrapper_by_child_execution_id,
|
|
wrappers=tuple(wrappers),
|
|
)
|