Skip to content

Commit 831c68b

Browse files
authored
Merge pull request #13 from UiPath/fix/generate_mermaid
feat: generate mermaid diagram
2 parents 7156579 + cc7433e commit 831c68b

2 files changed

Lines changed: 184 additions & 1 deletion

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "uipath-llamaindex"
3-
version = "0.0.9"
3+
version = "0.0.10"
44
description = "UiPath LlamaIndex SDK"
55
readme = { file = "README.md", content-type = "text/markdown" }
66
requires-python = ">=3.10"

src/uipath_llamaindex/_cli/cli_init.py

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,15 @@
55
from typing import Any, Dict
66

77
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+
)
817
from uipath._cli._utils._console import ConsoleLogger
918
from uipath._cli._utils._parse_ast import generate_bindings_json # type: ignore
1019
from uipath._cli.middlewares import MiddlewareResult
@@ -91,6 +100,176 @@ def generate_schema_from_workflow(workflow: Workflow) -> Dict[str, Any]:
91100
return schema
92101

93102

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+
94273
async def llamaindex_init_middleware_async(entrypoint: str) -> MiddlewareResult:
95274
"""Middleware to check for llama_index.json and create uipath.json with schemas"""
96275
config = LlamaIndexConfig()
@@ -131,6 +310,10 @@ async def llamaindex_init_middleware_async(entrypoint: str) -> MiddlewareResult:
131310
}
132311
entrypoints.append(new_entrypoint)
133312

313+
draw_all_possible_flows_mermaid(
314+
loaded_workflow, filename=f"{workflow.name}.mermaid"
315+
)
316+
134317
except Exception as e:
135318
console.error(f"Error during workflow load: {e}")
136319
return MiddlewareResult(

0 commit comments

Comments
 (0)