Skip to content

Commit dd881ee

Browse files
authoredMar 25, 2025··
feat: Add Graphviz-based agent visualization functionality (#147)
This pull request introduces functionality for visualizing agent structures using Graphviz. The changes include adding a new dependency, implementing functions to generate and draw graphs, and adding tests for these functions. New functionality for visualizing agent structures: * Added `graphviz` as a new dependency in `pyproject.toml`. * Implemented functions in `src/agents/visualizations.py` to generate and draw graphs for agents using Graphviz. These functions include `get_main_graph`, `get_all_nodes`, `get_all_edges`, and `draw_graph`. Testing the new visualization functionality: * Added tests in `tests/test_visualizations.py` to verify the correctness of the graph generation and drawing functions. The tests cover `get_main_graph`, `get_all_nodes`, `get_all_edges`, and `draw_graph`. For example, given the following code: ```python from agents import Agent, function_tool from agents.visualizations import draw_graph @function_tool def get_weather(city: str) -> str: return f"The weather in {city} is sunny." spanish_agent = Agent( name="Spanish agent", instructions="You only speak Spanish.", ) english_agent = Agent( name="English agent", instructions="You only speak English", ) triage_agent = Agent( name="Triage agent", instructions="Handoff to the appropriate agent based on the language of the request.", handoffs=[spanish_agent, english_agent], tools=[get_weather], ) draw_graph(triage_agent) ``` Generates the following image: <img width="614" alt="Screenshot 2025-03-13 at 18 36 23" src="https://github.com/user-attachments/assets/d01fe502-6886-4efb-aaf8-c92e4524b0fe" />
2 parents a0c5abc + c16deb2 commit dd881ee

File tree

8 files changed

+381
-1
lines changed

8 files changed

+381
-1
lines changed
 

Diff for: ‎.gitignore

+1-1
Original file line numberDiff line numberDiff line change
@@ -141,4 +141,4 @@ cython_debug/
141141
.ruff_cache/
142142

143143
# PyPI configuration file
144-
.pypirc
144+
.pypirc

Diff for: ‎docs/assets/images/graph.png

92.8 KB
Loading

Diff for: ‎docs/visualization.md

+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# Agent Visualization
2+
3+
Agent visualization allows you to generate a structured graphical representation of agents and their relationships using **Graphviz**. This is useful for understanding how agents, tools, and handoffs interact within an application.
4+
5+
## Installation
6+
7+
Install the optional `viz` dependency group:
8+
9+
```bash
10+
pip install "openai-agents[viz]"
11+
```
12+
13+
## Generating a Graph
14+
15+
You can generate an agent visualization using the `draw_graph` function. This function creates a directed graph where:
16+
17+
- **Agents** are represented as yellow boxes.
18+
- **Tools** are represented as green ellipses.
19+
- **Handoffs** are directed edges from one agent to another.
20+
21+
### Example Usage
22+
23+
```python
24+
from agents import Agent, function_tool
25+
from agents.extensions.visualization import draw_graph
26+
27+
@function_tool
28+
def get_weather(city: str) -> str:
29+
return f"The weather in {city} is sunny."
30+
31+
spanish_agent = Agent(
32+
name="Spanish agent",
33+
instructions="You only speak Spanish.",
34+
)
35+
36+
english_agent = Agent(
37+
name="English agent",
38+
instructions="You only speak English",
39+
)
40+
41+
triage_agent = Agent(
42+
name="Triage agent",
43+
instructions="Handoff to the appropriate agent based on the language of the request.",
44+
handoffs=[spanish_agent, english_agent],
45+
tools=[get_weather],
46+
)
47+
48+
draw_graph(triage_agent)
49+
```
50+
51+
![Agent Graph](./assets/images/graph.png)
52+
53+
This generates a graph that visually represents the structure of the **triage agent** and its connections to sub-agents and tools.
54+
55+
56+
## Understanding the Visualization
57+
58+
The generated graph includes:
59+
60+
- A **start node** (`__start__`) indicating the entry point.
61+
- Agents represented as **rectangles** with yellow fill.
62+
- Tools represented as **ellipses** with green fill.
63+
- Directed edges indicating interactions:
64+
- **Solid arrows** for agent-to-agent handoffs.
65+
- **Dotted arrows** for tool invocations.
66+
- An **end node** (`__end__`) indicating where execution terminates.
67+
68+
## Customizing the Graph
69+
70+
### Showing the Graph
71+
By default, `draw_graph` displays the graph inline. To show the graph in a separate window, write the following:
72+
73+
```python
74+
draw_graph(triage_agent).view()
75+
```
76+
77+
### Saving the Graph
78+
By default, `draw_graph` displays the graph inline. To save it as a file, specify a filename:
79+
80+
```python
81+
draw_graph(triage_agent, filename="agent_graph.png")
82+
```
83+
84+
This will generate `agent_graph.png` in the working directory.
85+
86+

Diff for: ‎mkdocs.yml

+1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ nav:
3636
- multi_agent.md
3737
- models.md
3838
- config.md
39+
- visualization.md
3940
- Voice agents:
4041
- voice/quickstart.md
4142
- voice/pipeline.md

Diff for: ‎pyproject.toml

+3
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ Repository = "https://github.com/openai/openai-agents-python"
3535

3636
[project.optional-dependencies]
3737
voice = ["numpy>=2.2.0, <3; python_version>='3.10'", "websockets>=15.0, <16"]
38+
viz = ["graphviz>=0.17"]
3839

3940
[dependency-groups]
4041
dev = [
@@ -56,7 +57,9 @@ dev = [
5657
"pynput",
5758
"textual",
5859
"websockets",
60+
"graphviz",
5961
]
62+
6063
[tool.uv.workspace]
6164
members = ["agents"]
6265

Diff for: ‎src/agents/extensions/visualization.py

+137
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
from typing import Optional
2+
3+
import graphviz # type: ignore
4+
5+
from agents import Agent
6+
from agents.handoffs import Handoff
7+
from agents.tool import Tool
8+
9+
10+
def get_main_graph(agent: Agent) -> str:
11+
"""
12+
Generates the main graph structure in DOT format for the given agent.
13+
14+
Args:
15+
agent (Agent): The agent for which the graph is to be generated.
16+
17+
Returns:
18+
str: The DOT format string representing the graph.
19+
"""
20+
parts = [
21+
"""
22+
digraph G {
23+
graph [splines=true];
24+
node [fontname="Arial"];
25+
edge [penwidth=1.5];
26+
"""
27+
]
28+
parts.append(get_all_nodes(agent))
29+
parts.append(get_all_edges(agent))
30+
parts.append("}")
31+
return "".join(parts)
32+
33+
34+
def get_all_nodes(agent: Agent, parent: Optional[Agent] = None) -> str:
35+
"""
36+
Recursively generates the nodes for the given agent and its handoffs in DOT format.
37+
38+
Args:
39+
agent (Agent): The agent for which the nodes are to be generated.
40+
41+
Returns:
42+
str: The DOT format string representing the nodes.
43+
"""
44+
parts = []
45+
46+
# Start and end the graph
47+
parts.append(
48+
'"__start__" [label="__start__", shape=ellipse, style=filled, '
49+
"fillcolor=lightblue, width=0.5, height=0.3];"
50+
'"__end__" [label="__end__", shape=ellipse, style=filled, '
51+
"fillcolor=lightblue, width=0.5, height=0.3];"
52+
)
53+
# Ensure parent agent node is colored
54+
if not parent:
55+
parts.append(
56+
f'"{agent.name}" [label="{agent.name}", shape=box, style=filled, '
57+
"fillcolor=lightyellow, width=1.5, height=0.8];"
58+
)
59+
60+
for tool in agent.tools:
61+
parts.append(
62+
f'"{tool.name}" [label="{tool.name}", shape=ellipse, style=filled, '
63+
f"fillcolor=lightgreen, width=0.5, height=0.3];"
64+
)
65+
66+
for handoff in agent.handoffs:
67+
if isinstance(handoff, Handoff):
68+
parts.append(
69+
f'"{handoff.agent_name}" [label="{handoff.agent_name}", '
70+
f"shape=box, style=filled, style=rounded, "
71+
f"fillcolor=lightyellow, width=1.5, height=0.8];"
72+
)
73+
if isinstance(handoff, Agent):
74+
parts.append(
75+
f'"{handoff.name}" [label="{handoff.name}", '
76+
f"shape=box, style=filled, style=rounded, "
77+
f"fillcolor=lightyellow, width=1.5, height=0.8];"
78+
)
79+
parts.append(get_all_nodes(handoff))
80+
81+
return "".join(parts)
82+
83+
84+
def get_all_edges(agent: Agent, parent: Optional[Agent] = None) -> str:
85+
"""
86+
Recursively generates the edges for the given agent and its handoffs in DOT format.
87+
88+
Args:
89+
agent (Agent): The agent for which the edges are to be generated.
90+
parent (Agent, optional): The parent agent. Defaults to None.
91+
92+
Returns:
93+
str: The DOT format string representing the edges.
94+
"""
95+
parts = []
96+
97+
if not parent:
98+
parts.append(f'"__start__" -> "{agent.name}";')
99+
100+
for tool in agent.tools:
101+
parts.append(f"""
102+
"{agent.name}" -> "{tool.name}" [style=dotted, penwidth=1.5];
103+
"{tool.name}" -> "{agent.name}" [style=dotted, penwidth=1.5];""")
104+
105+
for handoff in agent.handoffs:
106+
if isinstance(handoff, Handoff):
107+
parts.append(f"""
108+
"{agent.name}" -> "{handoff.agent_name}";""")
109+
if isinstance(handoff, Agent):
110+
parts.append(f"""
111+
"{agent.name}" -> "{handoff.name}";""")
112+
parts.append(get_all_edges(handoff, agent))
113+
114+
if not agent.handoffs and not isinstance(agent, Tool): # type: ignore
115+
parts.append(f'"{agent.name}" -> "__end__";')
116+
117+
return "".join(parts)
118+
119+
120+
def draw_graph(agent: Agent, filename: Optional[str] = None) -> graphviz.Source:
121+
"""
122+
Draws the graph for the given agent and optionally saves it as a PNG file.
123+
124+
Args:
125+
agent (Agent): The agent for which the graph is to be drawn.
126+
filename (str): The name of the file to save the graph as a PNG.
127+
128+
Returns:
129+
graphviz.Source: The graphviz Source object representing the graph.
130+
"""
131+
dot_code = get_main_graph(agent)
132+
graph = graphviz.Source(dot_code)
133+
134+
if filename:
135+
graph.render(filename, format="png")
136+
137+
return graph

Diff for: ‎tests/test_visualization.py

+136
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
from unittest.mock import Mock
2+
3+
import graphviz # type: ignore
4+
import pytest
5+
6+
from agents import Agent
7+
from agents.extensions.visualization import (
8+
draw_graph,
9+
get_all_edges,
10+
get_all_nodes,
11+
get_main_graph,
12+
)
13+
from agents.handoffs import Handoff
14+
15+
16+
@pytest.fixture
17+
def mock_agent():
18+
tool1 = Mock()
19+
tool1.name = "Tool1"
20+
tool2 = Mock()
21+
tool2.name = "Tool2"
22+
23+
handoff1 = Mock(spec=Handoff)
24+
handoff1.agent_name = "Handoff1"
25+
26+
agent = Mock(spec=Agent)
27+
agent.name = "Agent1"
28+
agent.tools = [tool1, tool2]
29+
agent.handoffs = [handoff1]
30+
31+
return agent
32+
33+
34+
def test_get_main_graph(mock_agent):
35+
result = get_main_graph(mock_agent)
36+
print(result)
37+
assert "digraph G" in result
38+
assert "graph [splines=true];" in result
39+
assert 'node [fontname="Arial"];' in result
40+
assert "edge [penwidth=1.5];" in result
41+
assert (
42+
'"__start__" [label="__start__", shape=ellipse, style=filled, '
43+
"fillcolor=lightblue, width=0.5, height=0.3];" in result
44+
)
45+
assert (
46+
'"__end__" [label="__end__", shape=ellipse, style=filled, '
47+
"fillcolor=lightblue, width=0.5, height=0.3];" in result
48+
)
49+
assert (
50+
'"Agent1" [label="Agent1", shape=box, style=filled, '
51+
"fillcolor=lightyellow, width=1.5, height=0.8];" in result
52+
)
53+
assert (
54+
'"Tool1" [label="Tool1", shape=ellipse, style=filled, '
55+
"fillcolor=lightgreen, width=0.5, height=0.3];" in result
56+
)
57+
assert (
58+
'"Tool2" [label="Tool2", shape=ellipse, style=filled, '
59+
"fillcolor=lightgreen, width=0.5, height=0.3];" in result
60+
)
61+
assert (
62+
'"Handoff1" [label="Handoff1", shape=box, style=filled, style=rounded, '
63+
"fillcolor=lightyellow, width=1.5, height=0.8];" in result
64+
)
65+
66+
67+
def test_get_all_nodes(mock_agent):
68+
result = get_all_nodes(mock_agent)
69+
assert (
70+
'"__start__" [label="__start__", shape=ellipse, style=filled, '
71+
"fillcolor=lightblue, width=0.5, height=0.3];" in result
72+
)
73+
assert (
74+
'"__end__" [label="__end__", shape=ellipse, style=filled, '
75+
"fillcolor=lightblue, width=0.5, height=0.3];" in result
76+
)
77+
assert (
78+
'"Agent1" [label="Agent1", shape=box, style=filled, '
79+
"fillcolor=lightyellow, width=1.5, height=0.8];" in result
80+
)
81+
assert (
82+
'"Tool1" [label="Tool1", shape=ellipse, style=filled, '
83+
"fillcolor=lightgreen, width=0.5, height=0.3];" in result
84+
)
85+
assert (
86+
'"Tool2" [label="Tool2", shape=ellipse, style=filled, '
87+
"fillcolor=lightgreen, width=0.5, height=0.3];" in result
88+
)
89+
assert (
90+
'"Handoff1" [label="Handoff1", shape=box, style=filled, style=rounded, '
91+
"fillcolor=lightyellow, width=1.5, height=0.8];" in result
92+
)
93+
94+
95+
def test_get_all_edges(mock_agent):
96+
result = get_all_edges(mock_agent)
97+
assert '"__start__" -> "Agent1";' in result
98+
assert '"Agent1" -> "__end__";'
99+
assert '"Agent1" -> "Tool1" [style=dotted, penwidth=1.5];' in result
100+
assert '"Tool1" -> "Agent1" [style=dotted, penwidth=1.5];' in result
101+
assert '"Agent1" -> "Tool2" [style=dotted, penwidth=1.5];' in result
102+
assert '"Tool2" -> "Agent1" [style=dotted, penwidth=1.5];' in result
103+
assert '"Agent1" -> "Handoff1";' in result
104+
105+
106+
def test_draw_graph(mock_agent):
107+
graph = draw_graph(mock_agent)
108+
assert isinstance(graph, graphviz.Source)
109+
assert "digraph G" in graph.source
110+
assert "graph [splines=true];" in graph.source
111+
assert 'node [fontname="Arial"];' in graph.source
112+
assert "edge [penwidth=1.5];" in graph.source
113+
assert (
114+
'"__start__" [label="__start__", shape=ellipse, style=filled, '
115+
"fillcolor=lightblue, width=0.5, height=0.3];" in graph.source
116+
)
117+
assert (
118+
'"__end__" [label="__end__", shape=ellipse, style=filled, '
119+
"fillcolor=lightblue, width=0.5, height=0.3];" in graph.source
120+
)
121+
assert (
122+
'"Agent1" [label="Agent1", shape=box, style=filled, '
123+
"fillcolor=lightyellow, width=1.5, height=0.8];" in graph.source
124+
)
125+
assert (
126+
'"Tool1" [label="Tool1", shape=ellipse, style=filled, '
127+
"fillcolor=lightgreen, width=0.5, height=0.3];" in graph.source
128+
)
129+
assert (
130+
'"Tool2" [label="Tool2", shape=ellipse, style=filled, '
131+
"fillcolor=lightgreen, width=0.5, height=0.3];" in graph.source
132+
)
133+
assert (
134+
'"Handoff1" [label="Handoff1", shape=box, style=filled, style=rounded, '
135+
"fillcolor=lightyellow, width=1.5, height=0.8];" in graph.source
136+
)

Diff for: ‎uv.lock

+17
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)
Please sign in to comment.