| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408 |
- """
- Tests for Agency Swarm visualization functionality.
- """
- import tempfile
- from pathlib import Path
- from unittest.mock import patch
- import pytest
- from agency_swarm import Agency, Agent
- from agency_swarm.ui import HTMLVisualizationGenerator, LayoutAlgorithms
- @pytest.fixture
- def sample_agency():
- """Create a sample agency for testing visualization."""
- ceo = Agent(name="CEO", instructions="You are the CEO")
- manager = Agent(name="Manager", instructions="You manage projects")
- worker = Agent(name="Worker", instructions="You do the work")
- agency = Agency(ceo, communication_flows=[ceo > manager, manager > worker])
- return agency
- @pytest.fixture
- def sample_agency_data():
- """Sample agency data structure for testing."""
- return {
- "nodes": [
- {
- "id": "CEO",
- "type": "agent",
- "data": {"label": "CEO", "description": "You are the CEO", "isEntryPoint": True},
- "position": {"x": 100, "y": 100},
- },
- {
- "id": "Manager",
- "type": "agent",
- "data": {"label": "Manager", "description": "You manage projects", "isEntryPoint": False},
- "position": {"x": 200, "y": 200},
- },
- {
- "id": "Worker",
- "type": "agent",
- "data": {"label": "Worker", "description": "You do the work", "isEntryPoint": False},
- "position": {"x": 300, "y": 300},
- },
- ],
- "edges": [
- {"id": "CEO-Manager", "source": "CEO", "target": "Manager", "type": "communication"},
- {"id": "Manager-Worker", "source": "Manager", "target": "Worker", "type": "communication"},
- ],
- "metadata": {"agencyName": "Test Agency", "totalAgents": 3, "totalTools": 0},
- }
- class TestLayoutAlgorithms:
- """Test the layout algorithms."""
- def test_hierarchical_layout_basic(self, sample_agency_data):
- """Test basic hierarchical layout functionality."""
- nodes = sample_agency_data["nodes"]
- edges = sample_agency_data["edges"]
- positions = LayoutAlgorithms.hierarchical_layout(nodes, edges, width=800, height=600)
- # Check that all agents got positions
- assert "CEO" in positions
- assert "Manager" in positions
- assert "Worker" in positions
- # Check that positions have x and y coordinates
- for _node_id, pos in positions.items():
- assert "x" in pos
- assert "y" in pos
- assert isinstance(pos["x"], int | float)
- assert isinstance(pos["y"], int | float)
- def test_hierarchical_layout_entry_points_on_top(self, sample_agency_data):
- """Test that entry points are positioned at the top."""
- nodes = sample_agency_data["nodes"]
- edges = sample_agency_data["edges"]
- positions = LayoutAlgorithms.hierarchical_layout(nodes, edges, width=800, height=600)
- ceo_y = positions["CEO"]["y"]
- manager_y = positions["Manager"]["y"]
- worker_y = positions["Worker"]["y"]
- # CEO (entry point) should be at the top
- assert ceo_y < manager_y
- assert manager_y < worker_y
- def test_hierarchical_layout_with_tools(self):
- """Test hierarchical layout with tools included."""
- nodes = [
- {"id": "CEO", "type": "agent", "data": {"label": "CEO", "isEntryPoint": True}},
- {"id": "tool1", "type": "tool", "data": {"label": "Tool1", "parentAgent": "CEO"}},
- ]
- edges = []
- positions = LayoutAlgorithms.hierarchical_layout(nodes, edges, width=800, height=600)
- assert "CEO" in positions
- assert "tool1" in positions
- # Tool should be positioned relative to its parent agent
- assert positions["tool1"]["x"] != positions["CEO"]["x"] or positions["tool1"]["y"] != positions["CEO"]["y"]
- def test_hierarchical_layout_orphaned_tools(self):
- """Test positioning of tools without parent agents."""
- nodes = [
- {"id": "CEO", "type": "agent", "data": {"label": "CEO", "isEntryPoint": True}},
- {
- "id": "orphan_tool",
- "type": "tool",
- "data": {"label": "Orphan Tool"}, # No parentAgent
- },
- ]
- edges = []
- positions = LayoutAlgorithms.hierarchical_layout(nodes, edges, width=800, height=600)
- assert "CEO" in positions
- assert "orphan_tool" in positions
- # Orphaned tool should be positioned at bottom
- assert positions["orphan_tool"]["y"] > positions["CEO"]["y"]
- def test_apply_layout(self, sample_agency_data):
- """Test the apply_layout method."""
- result = LayoutAlgorithms.apply_layout(sample_agency_data)
- # Check that structure is preserved
- assert "nodes" in result
- assert "edges" in result
- assert "metadata" in result
- # Check that positions were updated
- for node in result["nodes"]:
- assert "position" in node
- assert "x" in node["position"]
- assert "y" in node["position"]
- class TestHTMLVisualizationGenerator:
- """Test the HTML visualization generator."""
- def test_init(self):
- """Test HTMLVisualizationGenerator initialization."""
- generator = HTMLVisualizationGenerator()
- assert generator.template_dir.exists()
- assert (generator.template_dir / "visualization.html").exists()
- assert (generator.template_dir / "styles.css").exists()
- assert (generator.template_dir / "visualization.js").exists()
- def test_load_template(self):
- """Test template loading."""
- generator = HTMLVisualizationGenerator()
- # Test loading existing template
- html_content = generator._load_template("visualization.html")
- assert isinstance(html_content, str)
- assert len(html_content) > 0
- assert "html" in html_content.lower()
- def test_load_template_not_found(self):
- """Test error handling for missing template."""
- generator = HTMLVisualizationGenerator()
- with pytest.raises(FileNotFoundError):
- generator._load_template("nonexistent.html")
- @patch("webbrowser.open")
- def test_open_in_browser_success(self, mock_webbrowser):
- """Test opening file in browser successfully."""
- generator = HTMLVisualizationGenerator()
- test_path = Path("/test/path.html")
- generator._open_in_browser(test_path)
- mock_webbrowser.assert_called_once_with(f"file://{test_path}")
- @patch("webbrowser.open", side_effect=Exception("Browser error"))
- @patch("builtins.print")
- def test_open_in_browser_error(self, mock_print, mock_webbrowser):
- """Test error handling when browser fails to open."""
- generator = HTMLVisualizationGenerator()
- test_path = Path("/test/path.html")
- generator._open_in_browser(test_path)
- # Should print error messages
- assert mock_print.call_count >= 2
- def test_generate_interactive_html(self, sample_agency_data):
- """Test interactive HTML generation."""
- generator = HTMLVisualizationGenerator()
- with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False) as tmp:
- output_file = tmp.name
- try:
- with patch.object(generator, "_open_in_browser"):
- result_path = generator.generate_interactive_html(
- agency_data=sample_agency_data, output_file=output_file, open_browser=False
- )
- assert result_path == str(Path(output_file).resolve())
- assert Path(output_file).exists()
- # Check file content
- with open(output_file) as f:
- content = f.read()
- assert "Test Agency" in content
- assert "CEO" in content
- assert "Manager" in content
- assert "Worker" in content
- finally:
- # Cleanup
- if Path(output_file).exists():
- Path(output_file).unlink()
- def test_generate_component_files(self, sample_agency_data):
- """Test generation of separate component files."""
- generator = HTMLVisualizationGenerator()
- with tempfile.TemporaryDirectory() as tmp_dir:
- files = generator.generate_component_files(agency_data=sample_agency_data, output_dir=tmp_dir)
- # Check that all expected files were created
- assert "html" in files
- assert "css" in files
- assert "js" in files
- for file_path in files.values():
- assert Path(file_path).exists()
- assert Path(file_path).stat().st_size > 0
- @patch.object(HTMLVisualizationGenerator, "generate_interactive_html")
- def test_create_visualization_from_agency(self, mock_generate, sample_agency):
- """Test creating visualization directly from agency."""
- mock_generate.return_value = "/path/to/output.html"
- result = HTMLVisualizationGenerator.create_visualization_from_agency(
- agency=sample_agency,
- output_file="test.html",
- include_tools=True,
- open_browser=False,
- )
- assert result == "/path/to/output.html"
- mock_generate.assert_called_once()
- class TestAgencyTUI:
- """Test terminal UI entry points on Agency."""
- def test_tui_delegates_to_visualization(self, sample_agency):
- """Agency.tui should delegate to the visualization entry point."""
- with patch("agency_swarm.agency.visualization.tui") as mock_tui:
- sample_agency.tui(show_reasoning=True, reload=False)
- mock_tui.assert_called_once_with(sample_agency, show_reasoning=True, reload=False)
- def test_terminal_demo_alias_delegates_to_tui(self, sample_agency):
- """Agency.terminal_demo should remain a compatibility alias for tui."""
- with patch.object(sample_agency, "tui") as mock_tui:
- sample_agency.terminal_demo(show_reasoning=True, reload=False)
- mock_tui.assert_called_once_with(show_reasoning=True, reload=False)
- class TestAgencyVisualizationIntegration:
- """Test Agency class visualization methods."""
- def test_visualize(self, sample_agency):
- """Test Agency.visualize method."""
- with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False) as tmp:
- output_file = tmp.name
- try:
- with patch("webbrowser.open"):
- result_path = sample_agency.visualize(output_file=output_file, include_tools=True, open_browser=False)
- assert result_path == str(Path(output_file).resolve())
- assert Path(output_file).exists()
- # Check that file contains expected content
- with open(output_file) as f:
- content = f.read()
- assert "CEO" in content
- assert "Manager" in content
- assert "Worker" in content
- finally:
- if Path(output_file).exists():
- Path(output_file).unlink()
- def test_visualize_import_error(self, sample_agency):
- """Test handling of import errors in visualization."""
- # Patch the import inside the visualize method
- with patch("agency_swarm.agency.Agency.visualize") as mock_method:
- mock_method.side_effect = ImportError("Visualization module not available")
- with pytest.raises(ImportError, match="Visualization module not available"):
- sample_agency.visualize(open_browser=False)
- def test_get_agency_graph_basic(self, sample_agency):
- """Test basic agency graph generation."""
- structure = sample_agency.get_agency_graph()
- assert "nodes" in structure
- assert "edges" in structure
- assert "metadata" not in structure
- # Check nodes
- nodes = structure["nodes"]
- assert len(nodes) >= 3 # At least CEO, Manager, Worker
- agent_nodes = [n for n in nodes if n["type"] == "agent"]
- assert len(agent_nodes) == 3
- # Check edges
- edges = structure["edges"]
- communication_edges = [e for e in edges if e["type"] == "communication"]
- assert len(communication_edges) >= 2 # CEO->Manager, Manager->Worker
- def test_get_agency_graph_with_tools(self, sample_agency):
- """Test agency graph generation with tools included."""
- # Test that the method works with include_tools=True
- # We'll test the structure without actually adding tools to avoid tool type complications
- structure = sample_agency.get_agency_graph(include_tools=True)
- assert "nodes" in structure
- assert "edges" in structure
- assert "metadata" not in structure
- # Check that the structure is valid even when no tools are present
- agent_nodes = [n for n in structure["nodes"] if n["type"] == "agent"]
- assert len(agent_nodes) >= 3
- # Tool nodes and edges should be empty if no tools are added, which is fine
- tool_nodes = [n for n in structure["nodes"] if n["type"] == "tool"]
- tool_edges = [e for e in structure["edges"] if e["type"] == "tool"]
- # These should be lists, even if empty
- assert isinstance(tool_nodes, list)
- assert isinstance(tool_edges, list)
- def test_get_agency_graph_without_tools(self, sample_agency):
- """Test agency graph generation without tools."""
- structure = sample_agency.get_agency_graph(include_tools=False)
- # Should only have agent nodes
- tool_nodes = [n for n in structure["nodes"] if n["type"] == "tool"]
- assert len(tool_nodes) == 0
- # Should only have communication edges
- tool_edges = [e for e in structure["edges"] if e["type"] == "tool"]
- assert len(tool_edges) == 0
- def test_get_agency_graph_hierarchical_layout(self, sample_agency):
- """Test hierarchical layout in get_agency_graph."""
- # Test hierarchical layout
- structure = sample_agency.get_agency_graph()
- # Check that nodes have positions
- for node in structure["nodes"]:
- assert "position" in node
- assert "x" in node["position"]
- assert "y" in node["position"]
- def test_layout_algorithms_manager_vs_leaf_positioning(self):
- """Test that manager agents and leaf agents position tools differently."""
- # Create a more complex structure with manager and leaf agents
- nodes = [
- {"id": "Manager", "type": "agent", "data": {"label": "Manager", "isEntryPoint": True}},
- {"id": "Worker1", "type": "agent", "data": {"label": "Worker1", "isEntryPoint": False}},
- {"id": "Worker2", "type": "agent", "data": {"label": "Worker2", "isEntryPoint": False}},
- {"id": "manager_tool", "type": "tool", "data": {"label": "Manager Tool", "parentAgent": "Manager"}},
- {"id": "worker_tool", "type": "tool", "data": {"label": "Worker Tool", "parentAgent": "Worker1"}},
- ]
- # Manager has multiple outgoing connections
- edges = [
- {"source": "Manager", "target": "Worker1", "type": "communication"},
- {"source": "Manager", "target": "Worker2", "type": "communication"},
- ]
- positions = LayoutAlgorithms.hierarchical_layout(nodes, edges)
- # Both tools should be positioned
- assert "manager_tool" in positions
- assert "worker_tool" in positions
- # Manager tool should be positioned to the right (x > manager x)
- # Worker tool should be positioned below (y > worker y)
- manager_pos = positions["Manager"]
- worker_pos = positions["Worker1"]
- manager_tool_pos = positions["manager_tool"]
- worker_tool_pos = positions["worker_tool"]
- # These assertions test the smart positioning logic
- assert manager_tool_pos["x"] > manager_pos["x"] # Tool to the right of manager
- assert worker_tool_pos["y"] > worker_pos["y"] # Tool below worker
|