Compare commits

...

1 Commits

Author SHA1 Message Date
Devin AI
a4b22e92ad fix: save flow plot HTML to current working directory instead of temp dir
Fixes #4991

Previously,  saved generated HTML/CSS/JS files to a
hidden system temp directory (via ), making the output
inaccessible from the user's project folder. The CLI also printed a
misleading message suggesting the file was saved locally.

Changes:
-  now defaults to saving files in the current
  working directory () instead of a temp directory.
- Added an  parameter to both  and
   so users can optionally specify a custom output directory.
- Added 5 new tests covering CWD output, explicit output_dir, and
  absolute path guarantees.

Co-Authored-By: João <joao@crewai.com>
2026-03-20 19:55:22 +00:00
3 changed files with 118 additions and 15 deletions

View File

@@ -3153,12 +3153,19 @@ class Flow(Generic[T], metaclass=FlowMeta):
else:
logger.warning(message)
def plot(self, filename: str = "crewai_flow.html", show: bool = True) -> str:
def plot(
self,
filename: str = "crewai_flow.html",
show: bool = True,
output_dir: str | None = None,
) -> str:
"""Create interactive HTML visualization of Flow structure.
Args:
filename: Output HTML filename (default: "crewai_flow.html").
show: Whether to open in browser (default: True).
output_dir: Directory to save generated files. Defaults to the
current working directory.
Returns:
Absolute path to generated HTML file.
@@ -3171,7 +3178,9 @@ class Flow(Generic[T], metaclass=FlowMeta):
),
)
structure = build_flow_structure(self)
return render_interactive(structure, filename=filename, show=show)
return render_interactive(
structure, filename=filename, show=show, output_dir=output_dir
)
@staticmethod
def _show_tracing_disabled_message() -> None:

View File

@@ -2,7 +2,6 @@
import json
from pathlib import Path
import tempfile
from typing import Any, ClassVar
import webbrowser
@@ -205,20 +204,24 @@ def render_interactive(
dag: FlowStructure,
filename: str = "flow_dag.html",
show: bool = True,
output_dir: str | None = None,
) -> str:
"""Create interactive HTML visualization of Flow structure.
Generates three output files in a temporary directory: HTML template,
CSS stylesheet, and JavaScript. Optionally opens the visualization in
default browser.
Generates three output files: HTML template, CSS stylesheet, and
JavaScript. Files are saved to the specified output directory, or the
current working directory when *output_dir* is ``None``. Optionally
opens the visualization in the default browser.
Args:
dag: FlowStructure to visualize.
filename: Output HTML filename (basename only, no path).
show: Whether to open in browser.
output_dir: Directory to save generated files. Defaults to the
current working directory (``os.getcwd()``).
Returns:
Absolute path to generated HTML file in temporary directory.
Absolute path to generated HTML file.
"""
node_positions = calculate_node_positions(dag)
@@ -403,12 +406,13 @@ def render_interactive(
extensions=[CSSExtension, JSExtension],
)
temp_dir = Path(tempfile.mkdtemp(prefix="crewai_flow_"))
output_path = temp_dir / Path(filename).name
dest_dir = Path(output_dir) if output_dir else Path.cwd()
dest_dir.mkdir(parents=True, exist_ok=True)
output_path = dest_dir / Path(filename).name
css_filename = output_path.stem + "_style.css"
css_output_path = temp_dir / css_filename
css_output_path = dest_dir / css_filename
js_filename = output_path.stem + "_script.js"
js_output_path = temp_dir / js_filename
js_output_path = dest_dir / js_filename
css_file = template_dir / "style.css"
css_content = css_file.read_text(encoding="utf-8")

View File

@@ -333,9 +333,9 @@ def test_visualization_plot_method():
"""Test that flow.plot() method works."""
flow = SimpleFlow()
html_file = flow.plot("test_plot.html", show=False)
assert os.path.exists(html_file)
with tempfile.TemporaryDirectory() as tmp_dir:
html_file = flow.plot("test_plot.html", show=False, output_dir=tmp_dir)
assert os.path.exists(html_file)
def test_router_paths_to_string_conditions():
@@ -667,4 +667,94 @@ def test_no_warning_for_properly_typed_router(caplog):
# No warnings should be logged
warning_messages = [r.message for r in caplog.records if r.levelno >= logging.WARNING]
assert not any("Could not determine return paths" in msg for msg in warning_messages)
assert not any("Found listeners waiting for triggers" in msg for msg in warning_messages)
assert not any("Found listeners waiting for triggers" in msg for msg in warning_messages)
def test_plot_saves_to_current_working_directory():
"""Test that plot() saves the HTML file to the current working directory by default.
Regression test for https://github.com/crewAIInc/crewAI/issues/4991
"""
flow = SimpleFlow()
with tempfile.TemporaryDirectory() as tmp_dir:
original_cwd = os.getcwd()
try:
os.chdir(tmp_dir)
html_file = flow.plot("test_cwd_plot.html", show=False)
# The returned path must live inside the CWD, not a hidden temp dir
assert Path(html_file).parent == Path(tmp_dir)
assert os.path.exists(html_file)
assert html_file == str(Path(tmp_dir) / "test_cwd_plot.html")
finally:
os.chdir(original_cwd)
def test_plot_saves_to_explicit_output_dir():
"""Test that plot() saves files to a user-specified output directory."""
flow = SimpleFlow()
with tempfile.TemporaryDirectory() as output_dir:
html_file = flow.plot(
"custom_output.html", show=False, output_dir=output_dir
)
assert Path(html_file).parent == Path(output_dir)
assert os.path.exists(html_file)
# CSS and JS companion files should also be in the same directory
html_path = Path(html_file)
css_file = html_path.parent / f"{html_path.stem}_style.css"
js_file = html_path.parent / f"{html_path.stem}_script.js"
assert css_file.exists()
assert js_file.exists()
def test_render_interactive_saves_to_cwd_by_default():
"""Test that render_interactive() writes to CWD when output_dir is None.
Regression test for https://github.com/crewAIInc/crewAI/issues/4991
"""
flow = SimpleFlow()
structure = build_flow_structure(flow)
with tempfile.TemporaryDirectory() as tmp_dir:
original_cwd = os.getcwd()
try:
os.chdir(tmp_dir)
html_file = visualize_flow_structure(
structure, "cwd_test.html", show=False
)
assert Path(html_file).parent == Path(tmp_dir)
assert os.path.exists(html_file)
finally:
os.chdir(original_cwd)
def test_render_interactive_saves_to_specified_output_dir():
"""Test that render_interactive() writes to the specified output_dir."""
flow = SimpleFlow()
structure = build_flow_structure(flow)
with tempfile.TemporaryDirectory() as output_dir:
html_file = visualize_flow_structure(
structure, "output_dir_test.html", show=False, output_dir=output_dir
)
assert Path(html_file).parent == Path(output_dir)
assert os.path.exists(html_file)
with open(html_file, "r", encoding="utf-8") as f:
html_content = f.read()
assert "<!DOCTYPE html>" in html_content
def test_plot_returned_path_is_absolute():
"""Test that the path returned by plot() is always absolute."""
flow = SimpleFlow()
with tempfile.TemporaryDirectory() as tmp_dir:
html_file = flow.plot("abs_path_test.html", show=False, output_dir=tmp_dir)
assert os.path.isabs(html_file)