|
5 | 5 | from typing import Any, Dict |
6 | 6 |
|
7 | 7 | from llama_index.core.workflow import StopEvent, Workflow |
| 8 | +from llama_index.core.workflow.drawing import StepConfig |
| 9 | +from llama_index.core.workflow.events import ( |
| 10 | + HumanResponseEvent, |
| 11 | + InputRequiredEvent, |
| 12 | +) |
| 13 | +from llama_index.core.workflow.utils import ( |
| 14 | + get_steps_from_class, |
| 15 | + get_steps_from_instance, |
| 16 | +) |
8 | 17 | from uipath._cli._utils._console import ConsoleLogger |
9 | 18 | from uipath._cli._utils._parse_ast import generate_bindings_json # type: ignore |
10 | 19 | from uipath._cli.middlewares import MiddlewareResult |
@@ -91,6 +100,176 @@ def generate_schema_from_workflow(workflow: Workflow) -> Dict[str, Any]: |
91 | 100 | return schema |
92 | 101 |
|
93 | 102 |
|
| 103 | +def draw_all_possible_flows_mermaid( |
| 104 | + workflow: Workflow, |
| 105 | + filename: str = "workflow_all_flows.mermaid", |
| 106 | +) -> str: |
| 107 | + """Draws all possible flows of the workflow as a Mermaid diagram.""" |
| 108 | + # Initialize Mermaid flowchart string |
| 109 | + mermaid_diagram = ["flowchart TD"] |
| 110 | + |
| 111 | + # Add nodes from all steps |
| 112 | + steps = get_steps_from_class(workflow) |
| 113 | + if not steps: |
| 114 | + # If no steps are defined in the class, try to get them from the instance |
| 115 | + steps = get_steps_from_instance(workflow) |
| 116 | + |
| 117 | + # Track all nodes and edges to avoid duplicates |
| 118 | + nodes = set() |
| 119 | + edges = set() |
| 120 | + |
| 121 | + # Track event types to avoid duplicates |
| 122 | + event_types = {} |
| 123 | + |
| 124 | + # Only one kind of `StopEvent` is allowed in a `Workflow`. |
| 125 | + current_stop_event = None |
| 126 | + for _, step_func in steps.items(): |
| 127 | + step_config : StepConfig = getattr(step_func, "__step_config", None) |
| 128 | + if step_config is None: |
| 129 | + continue |
| 130 | + |
| 131 | + for return_type in step_config.return_types: |
| 132 | + if issubclass(return_type, StopEvent): |
| 133 | + current_stop_event = return_type |
| 134 | + break |
| 135 | + |
| 136 | + if current_stop_event: |
| 137 | + break |
| 138 | + |
| 139 | + # First pass: collect all event types (both return types and accepted events) |
| 140 | + for _, step_func in steps.items(): |
| 141 | + step_config = getattr(step_func, "__step_config", None) |
| 142 | + if step_config is None: |
| 143 | + continue |
| 144 | + |
| 145 | + # Collect accepted event types |
| 146 | + for event_type in step_config.accepted_events: |
| 147 | + if event_type == StopEvent and event_type != current_stop_event: |
| 148 | + continue |
| 149 | + |
| 150 | + event_name = event_type.__name__ |
| 151 | + event_types[event_name] = event_type |
| 152 | + |
| 153 | + # Collect return types |
| 154 | + for return_type in step_config.return_types: |
| 155 | + if return_type is type(None): |
| 156 | + continue |
| 157 | + |
| 158 | + return_name = return_type.__name__ |
| 159 | + event_types[return_name] = return_type |
| 160 | + |
| 161 | + # Generate step nodes |
| 162 | + for step_name, step_func in steps.items(): |
| 163 | + step_config = getattr(step_func, "__step_config", None) |
| 164 | + if step_config is None: |
| 165 | + continue |
| 166 | + |
| 167 | + # Add step node (use step_name with cleaned ID) |
| 168 | + step_id = f"step_{clean_id(step_name)}" |
| 169 | + if step_id not in nodes: |
| 170 | + nodes.add(step_id) |
| 171 | + mermaid_diagram.append(f' {step_id}["{step_name}"]:::stepStyle') |
| 172 | + |
| 173 | + # Generate event nodes (only once per event type) |
| 174 | + for event_name, event_type in event_types.items(): |
| 175 | + event_id = f"event_{clean_id(event_name)}" |
| 176 | + if event_id not in nodes: |
| 177 | + nodes.add(event_id) |
| 178 | + style = get_event_style(event_type) |
| 179 | + mermaid_diagram.append(f' {event_id}("{event_name}"):::{style}') |
| 180 | + |
| 181 | + if issubclass(event_type, InputRequiredEvent): |
| 182 | + # Add node for conceptual external step |
| 183 | + if "external_step" not in nodes: |
| 184 | + nodes.add("external_step") |
| 185 | + mermaid_diagram.append( |
| 186 | + ' external_step["external_step"]:::externalStyle' |
| 187 | + ) |
| 188 | + |
| 189 | + # Generate edges |
| 190 | + for step_name, step_func in steps.items(): |
| 191 | + step_config = getattr(step_func, "__step_config", None) |
| 192 | + if step_config is None: |
| 193 | + continue |
| 194 | + |
| 195 | + step_id = f"step_{clean_id(step_name)}" |
| 196 | + |
| 197 | + # Add edges for return types |
| 198 | + for return_type in step_config.return_types: |
| 199 | + if return_type is not type(None): |
| 200 | + return_name = return_type.__name__ |
| 201 | + return_id = f"event_{clean_id(return_name)}" |
| 202 | + edge = f"{step_id} --> {return_id}" |
| 203 | + if edge not in edges: |
| 204 | + edges.add(edge) |
| 205 | + mermaid_diagram.append(f" {edge}") |
| 206 | + |
| 207 | + if issubclass(return_type, InputRequiredEvent): |
| 208 | + return_name = return_type.__name__ |
| 209 | + return_id = f"event_{clean_id(return_name)}" |
| 210 | + edge = f"{return_id} --> external_step" |
| 211 | + if edge not in edges: |
| 212 | + edges.add(edge) |
| 213 | + mermaid_diagram.append(f" {edge}") |
| 214 | + |
| 215 | + # Add edges for accepted events |
| 216 | + for event_type in step_config.accepted_events: |
| 217 | + event_name = event_type.__name__ |
| 218 | + event_id = f"event_{clean_id(event_name)}" |
| 219 | + |
| 220 | + if step_name == "_done" and issubclass(event_type, StopEvent): |
| 221 | + stop_event_name = current_stop_event.__name__ |
| 222 | + stop_event_id = f"event_{clean_id(stop_event_name)}" |
| 223 | + edge = f"{stop_event_id} --> {step_id}" |
| 224 | + if edge not in edges: |
| 225 | + edges.add(edge) |
| 226 | + mermaid_diagram.append(f" {edge}") |
| 227 | + else: |
| 228 | + edge = f"{event_id} --> {step_id}" |
| 229 | + if edge not in edges: |
| 230 | + edges.add(edge) |
| 231 | + mermaid_diagram.append(f" {edge}") |
| 232 | + |
| 233 | + if issubclass(event_type, HumanResponseEvent): |
| 234 | + edge = f"external_step --> {event_id}" |
| 235 | + if edge not in edges: |
| 236 | + edges.add(edge) |
| 237 | + mermaid_diagram.append(f" {edge}") |
| 238 | + |
| 239 | + # Add style definitions |
| 240 | + mermaid_diagram.append(" classDef stepStyle fill:#ADD8E6,stroke:#333") |
| 241 | + mermaid_diagram.append(" classDef externalStyle fill:#BEDAE4,stroke:#333") |
| 242 | + mermaid_diagram.append(" classDef defaultEventStyle fill:#FFA07A,stroke:#333") |
| 243 | + mermaid_diagram.append(" classDef stopEventStyle fill:#98FB98,stroke:#333") |
| 244 | + mermaid_diagram.append(" classDef inputRequiredStyle fill:#FFD700,stroke:#333") |
| 245 | + |
| 246 | + # Join all lines |
| 247 | + mermaid_string = "\n".join(mermaid_diagram) |
| 248 | + |
| 249 | + # Write to file if filename is provided |
| 250 | + if filename: |
| 251 | + with open(filename, "w") as f: |
| 252 | + f.write(mermaid_string) |
| 253 | + |
| 254 | + return mermaid_string |
| 255 | + |
| 256 | + |
| 257 | +def clean_id(name: str) -> str: |
| 258 | + """Convert a name to a valid Mermaid ID.""" |
| 259 | + # Replace invalid characters with underscores |
| 260 | + return name.replace(" ", "_").replace("-", "_").replace(".", "_") |
| 261 | + |
| 262 | + |
| 263 | +def get_event_style(event_type) -> str: |
| 264 | + """Return the appropriate Mermaid style class for an event type.""" |
| 265 | + if issubclass(event_type, StopEvent): |
| 266 | + return "stopEventStyle" |
| 267 | + elif issubclass(event_type, InputRequiredEvent): |
| 268 | + return "inputRequiredStyle" |
| 269 | + else: |
| 270 | + return "defaultEventStyle" |
| 271 | + |
| 272 | + |
94 | 273 | async def llamaindex_init_middleware_async(entrypoint: str) -> MiddlewareResult: |
95 | 274 | """Middleware to check for llama_index.json and create uipath.json with schemas""" |
96 | 275 | config = LlamaIndexConfig() |
@@ -131,6 +310,10 @@ async def llamaindex_init_middleware_async(entrypoint: str) -> MiddlewareResult: |
131 | 310 | } |
132 | 311 | entrypoints.append(new_entrypoint) |
133 | 312 |
|
| 313 | + draw_all_possible_flows_mermaid( |
| 314 | + loaded_workflow, filename=f"{workflow.name}.mermaid" |
| 315 | + ) |
| 316 | + |
134 | 317 | except Exception as e: |
135 | 318 | console.error(f"Error during workflow load: {e}") |
136 | 319 | return MiddlewareResult( |
|
0 commit comments