chore: import upstream snapshot with attribution
Validate YAML Workflows / Validate YAML Configuration Files (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 12:37:51 +08:00
commit d0e4308def
614 changed files with 74458 additions and 0 deletions
+218
View File
@@ -0,0 +1,218 @@
"""Cycle detection and management for workflow graphs."""
from typing import Dict, List, Set, Optional, Any
from dataclasses import dataclass, field
from entity.configs import Node
@dataclass
class CycleInfo:
"""Information about a detected cycle in the workflow graph."""
cycle_id: str
nodes: Set[str] # Node IDs in the cycle
entry_nodes: Set[str] # Nodes that can enter the cycle (kept for compatibility)
exit_edges: List[Dict[str, Any]] # Edges that can exit the cycle
iteration_count: int = 0
max_iterations: Optional[int] = None # Safety limit
is_active: bool = False
execution_state: Dict[str, Any] = field(default_factory=dict)
# New fields for refactored cycle execution
initial_node: Optional[str] = None # The unique initial node when first entering the cycle
configured_entry_node: Optional[str] = None # User-configured entry node (if any)
max_iterations_default: int = 100 # Default maximum iterations if max_iterations is None
def add_node(self, node_id: str) -> None:
"""Add a node to the cycle."""
self.nodes.add(node_id)
def add_entry_node(self, node_id: str) -> None:
"""Add an entry node to the cycle."""
self.entry_nodes.add(node_id)
def add_exit_edge(self, edge_config: Dict[str, Any]) -> None:
"""Add an exit edge configuration."""
self.exit_edges.append(edge_config)
def increment_iteration(self) -> None:
"""Increment the iteration counter."""
self.iteration_count += 1
def is_within_iteration_limit(self) -> bool:
"""Check if the cycle should continue executing."""
max_iter = self.max_iterations if self.max_iterations is not None else self.max_iterations_default
return self.iteration_count < max_iter
def get_max_iterations(self) -> int:
"""Get the effective maximum iterations."""
return self.max_iterations if self.max_iterations is not None else self.max_iterations_default
def reset_iteration_count(self) -> None:
"""Reset the iteration counter."""
self.iteration_count = 0
def is_node_in_cycle(self, node_id: str) -> bool:
"""Check if a node is part of this cycle."""
return node_id in self.nodes
def is_entry_node(self, node_id: str) -> bool:
"""Check if a node is an entry node for this cycle."""
return node_id in self.entry_nodes
class CycleDetector:
"""Detects cycles in workflow graphs using Tarjan's algorithm."""
def __init__(self):
self.index_counter = 0
self.index: Dict[str, int] = {}
self.low_link: Dict[str, int] = {}
self.stack: List[str] = []
self.on_stack: Set[str] = set()
self.cycles: List[Set[str]] = []
def detect_cycles(self, nodes: Dict[str, Node]) -> List[Set[str]]:
"""Detect all cycles in the graph using Tarjan's strongly connected components' algorithm."""
self.cycles.clear()
self.index_counter = 0
self.index.clear()
self.low_link.clear()
self.stack.clear()
self.on_stack.clear()
for node_id in nodes:
if node_id not in self.index:
self._strong_connect(node_id, nodes)
# return [cycle for cycle in self.cycles if len(cycle) > 1 or self._has_self_loop(next(iter(cycle)), nodes)]
return self.cycles
def _has_self_loop(self, node_id: str, nodes: Dict[str, Node]) -> bool:
"""Check if a node has a self-loop."""
node = nodes.get(node_id)
if not node:
return False
return any(edge_link.target.id == node_id for edge_link in node.iter_outgoing_edges())
def _strong_connect(self, node_id: str, nodes: Dict[str, Node]) -> None:
"""Recursive part of Tarjan's algorithm."""
self.index[node_id] = self.index_counter
self.low_link[node_id] = self.index_counter
self.index_counter += 1
self.stack.append(node_id)
self.on_stack.add(node_id)
node = nodes.get(node_id)
if not node:
return
# Consider successors of node
for edge_link in node.iter_outgoing_edges():
successor_id = edge_link.target.id
if successor_id not in self.index:
self._strong_connect(successor_id, nodes)
self.low_link[node_id] = min(self.low_link[node_id], self.low_link[successor_id])
elif successor_id in self.on_stack:
self.low_link[node_id] = min(self.low_link[node_id], self.index[successor_id])
# If node is a root node, pop the stack and generate an SCC
if self.low_link[node_id] == self.index[node_id]:
cycle = set()
while True:
w = self.stack.pop()
self.on_stack.remove(w)
cycle.add(w)
if w == node_id:
break
if len(cycle) > 1 or self._has_self_loop(node_id, nodes):
self.cycles.append(cycle)
class CycleManager:
"""Manages execution of cycles in the workflow graph."""
def __init__(self):
self.cycles: Dict[str, CycleInfo] = {}
self.node_to_cycle: Dict[str, str] = {} # Maps node ID to cycle ID
self.active_cycles: Set[str] = set()
def initialize_cycles(self, cycles: List[Set[str]], nodes: Dict[str, Node]) -> None:
"""Initialize cycle information from detected cycles."""
self.cycles.clear()
self.node_to_cycle.clear()
for i, cycle_nodes in enumerate(cycles):
cycle_id = f"cycle_{i}_{cycle_nodes}"
cycle_info = CycleInfo(
cycle_id=cycle_id,
nodes=set(cycle_nodes),
entry_nodes=set(),
exit_edges=[]
)
# Find entry nodes and exit edges
self._analyze_cycle_structure(cycle_info, nodes)
self.cycles[cycle_id] = cycle_info
# Map nodes to their cycle
for node_id in cycle_nodes:
self.node_to_cycle[node_id] = cycle_id
def _analyze_cycle_structure(self, cycle: CycleInfo, nodes: Dict[str, Node]) -> None:
"""Analyze cycle structure to find entry nodes and exit edges."""
cycle_nodes = cycle.nodes
# Find entry nodes (nodes with predecessors outside the cycle)
def judge_entry_predecessor(_predecessor: Node, _predecessor_id: str) -> bool:
if _predecessor_id in cycle_nodes:
return False
for _edge_link in _predecessor.iter_outgoing_edges():
if _edge_link.target.id == node_id:
if _edge_link.trigger:
return True
else:
return False
return False
for node_id in cycle_nodes:
node = nodes.get(node_id)
if not node:
continue
for predecessor in node.predecessors:
if judge_entry_predecessor(predecessor, predecessor.id):
cycle.add_entry_node(node_id)
break
# Find exit edges (edges from cycle nodes to nodes outside the cycle)
for node_id in cycle_nodes:
node = nodes.get(node_id)
if not node:
continue
for edge_link in node.iter_outgoing_edges():
if edge_link.target.id not in cycle_nodes and edge_link.trigger:
exit_edge = {
"from": node_id,
"to": edge_link.target.id,
"condition": edge_link.condition,
"trigger": edge_link.trigger,
"config": edge_link.config
}
cycle.add_exit_edge(exit_edge)
def activate_cycle(self, cycle_id: str) -> None:
"""Activate a cycle for execution."""
if cycle_id in self.cycles:
self.cycles[cycle_id].is_active = True
self.active_cycles.add(cycle_id)
def deactivate_cycle(self, cycle_id: str) -> None:
"""Deactivate a cycle."""
if cycle_id in self.cycles:
self.cycles[cycle_id].is_active = False
self.cycles[cycle_id].iteration_count = 0
self.active_cycles.discard(cycle_id)