-
Notifications
You must be signed in to change notification settings - Fork 722
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Add Graphviz-based agent visualization functionality #147
Open
MartinEBravo
wants to merge
15
commits into
openai:main
Choose a base branch
from
MartinEBravo:feat/draw_graph
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+345
−0
Open
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
5865c6f
Add graphviz as a dependency in pyproject.toml
MartinEBravo cecdcd0
Add visualization functions for agents using Graphviz
MartinEBravo 9b972b3
Add unit tests for visualization functions in test_visualizations.py
MartinEBravo 2993d26
Add documentation and example for agent visualization using Graphviz
MartinEBravo 29e9983
Linting
MartinEBravo e984274
Merge branch 'main' of https://github.com/openai/openai-agents-python…
MartinEBravo aff1d60
Merge branch 'main' of https://github.com/openai/openai-agents-python…
MartinEBravo f7c594d
feat: add visualization functions for agent graphs
MartinEBravo 6f2f729
refactor: move graphviz dependency to visualization section
MartinEBravo c745fe1
feat: add documentation for agent visualization using Graphviz
MartinEBravo 53367be
feat: add visualization module for agent graphs using Graphviz
MartinEBravo 39ff00d
rename: test_visualization.py
MartinEBravo 0079bca
style: improve code formatting and readability in visualization funct…
MartinEBravo b7627cb
style: improve string formatting
MartinEBravo f4edc1f
style: improve string formatting in visualization functions
MartinEBravo File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
# Agent Visualization | ||
|
||
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. | ||
|
||
## Installation | ||
|
||
The visualization functionality relies on the **Graphviz** package. To use it, ensure you have Graphviz installed and add it as a dependency in `pyproject.toml`. Alternatively, install it directly via pip: | ||
|
||
```bash | ||
pip install openai-agents[visualization] | ||
``` | ||
|
||
## Generating a Graph | ||
|
||
You can generate an agent visualization using the `draw_graph` function. This function creates a directed graph where: | ||
|
||
- **Agents** are represented as yellow boxes. | ||
- **Tools** are represented as green ellipses. | ||
- **Handoffs** are directed edges from one agent to another. | ||
|
||
### Example Usage | ||
|
||
```python | ||
from agents import Agent, function_tool | ||
from agents.extensions.visualization 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) | ||
``` | ||
|
||
 | ||
|
||
This generates a graph that visually represents the structure of the **triage agent** and its connections to sub-agents and tools. | ||
|
||
|
||
## Understanding the Visualization | ||
|
||
The generated graph includes: | ||
|
||
- A **start node** (`__start__`) indicating the entry point. | ||
- Agents represented as **rectangles** with yellow fill. | ||
- Tools represented as **ellipses** with green fill. | ||
- Directed edges indicating interactions: | ||
- **Solid arrows** for agent-to-agent handoffs. | ||
- **Dotted arrows** for tool invocations. | ||
- An **end node** (`__end__`) indicating where execution terminates. | ||
|
||
## Customizing the Graph | ||
|
||
### Showing the Graph | ||
By default, `draw_graph` displays the graph inline. To show the graph in a separate window, write the following: | ||
|
||
```python | ||
draw_graph(triage_agent).view() | ||
``` | ||
|
||
### Saving the Graph | ||
By default, `draw_graph` displays the graph inline. To save it as a file, specify a filename: | ||
|
||
```python | ||
draw_graph(triage_agent, filename="agent_graph.png") | ||
``` | ||
|
||
This will generate `agent_graph.png` in the working directory. | ||
|
||
## Testing the Visualization | ||
|
||
The visualization functionality includes test coverage to ensure correctness. Tests are located in `tests/test_visualizations.py` and verify: | ||
|
||
- Node and edge correctness in `get_main_graph()`. | ||
- Proper agent and tool representation in `get_all_nodes()`. | ||
- Accurate relationship mapping in `get_all_edges()`. | ||
- Graph rendering functionality in `draw_graph()`. | ||
|
||
Run tests using: | ||
|
||
```bash | ||
pytest tests/test_visualizations.py | ||
``` | ||
|
||
## Conclusion | ||
|
||
Agent visualization provides a powerful way to **understand, debug, and communicate** how agents interact within an application. By leveraging **Graphviz**, you can generate intuitive visual representations of complex agent structures effortlessly. | ||
|
||
For further details on agent functionality, see the [Agents documentation](agents.md). | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
import graphviz | ||
|
||
from agents import Agent | ||
|
||
|
||
def get_main_graph(agent: Agent) -> str: | ||
""" | ||
Generates the main graph structure in DOT format for the given agent. | ||
|
||
Args: | ||
agent (Agent): The agent for which the graph is to be generated. | ||
|
||
Returns: | ||
str: The DOT format string representing the graph. | ||
""" | ||
parts = [ | ||
""" | ||
digraph G { | ||
graph [splines=true]; | ||
node [fontname="Arial"]; | ||
edge [penwidth=1.5]; | ||
"__start__" [shape=ellipse, style=filled, fillcolor=lightblue]; | ||
"__end__" [shape=ellipse, style=filled, fillcolor=lightblue]; | ||
""" | ||
] | ||
parts.append(get_all_nodes(agent)) | ||
parts.append(get_all_edges(agent)) | ||
parts.append("}") | ||
return "".join(parts) | ||
|
||
|
||
def get_all_nodes(agent: Agent, parent: Agent = None) -> str: | ||
""" | ||
Recursively generates the nodes for the given agent and its handoffs in DOT format. | ||
|
||
Args: | ||
agent (Agent): The agent for which the nodes are to be generated. | ||
|
||
Returns: | ||
str: The DOT format string representing the nodes. | ||
""" | ||
parts = [] | ||
# Ensure parent agent node is colored | ||
if not parent: | ||
parts.append( | ||
f'"{agent.name}" [label="{agent.name}", shape=box, style=filled, ' | ||
"fillcolor=lightyellow, width=1.5, height=0.8];" | ||
) | ||
|
||
# Smaller tools (ellipse, green) | ||
for tool in agent.tools: | ||
parts.append( | ||
f'"{tool.name}" [label="{tool.name}", shape=ellipse, style=filled, ' | ||
f"fillcolor=lightgreen, width=0.5, height=0.3];" | ||
) | ||
|
||
# Bigger handoffs (rounded box, yellow) | ||
for handoff in agent.handoffs: | ||
parts.append( | ||
f'"{handoff.name}" [label="{handoff.name}", shape=box, style=filled, style=rounded, ' | ||
f"fillcolor=lightyellow, width=1.5, height=0.8];" | ||
) | ||
parts.append(get_all_nodes(handoff)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think this is right - the handoffs list is |
||
|
||
return "".join(parts) | ||
|
||
|
||
def get_all_edges(agent: Agent, parent: Agent = None) -> str: | ||
""" | ||
Recursively generates the edges for the given agent and its handoffs in DOT format. | ||
|
||
Args: | ||
agent (Agent): The agent for which the edges are to be generated. | ||
parent (Agent, optional): The parent agent. Defaults to None. | ||
|
||
Returns: | ||
str: The DOT format string representing the edges. | ||
""" | ||
parts = [] | ||
|
||
if not parent: | ||
parts.append(f""" | ||
"__start__" -> "{agent.name}";""") | ||
|
||
for tool in agent.tools: | ||
parts.append(f""" | ||
"{agent.name}" -> "{tool.name}" [style=dotted, penwidth=1.5]; | ||
"{tool.name}" -> "{agent.name}" [style=dotted, penwidth=1.5];""") | ||
|
||
if not agent.handoffs: | ||
parts.append(f""" | ||
"{agent.name}" -> "__end__";""") | ||
|
||
for handoff in agent.handoffs: | ||
parts.append(f""" | ||
"{agent.name}" -> "{handoff.name}";""") | ||
parts.append(get_all_edges(handoff, agent)) | ||
|
||
return "".join(parts) | ||
|
||
|
||
def draw_graph(agent: Agent, filename: str = None) -> graphviz.Source: | ||
""" | ||
Draws the graph for the given agent and optionally saves it as a PNG file. | ||
|
||
Args: | ||
agent (Agent): The agent for which the graph is to be drawn. | ||
filename (str): The name of the file to save the graph as a PNG. | ||
|
||
Returns: | ||
graphviz.Source: The graphviz Source object representing the graph. | ||
""" | ||
dot_code = get_main_graph(agent) | ||
graph = graphviz.Source(dot_code) | ||
|
||
if filename: | ||
graph.render(filename, format="png") | ||
|
||
return graph |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
from unittest.mock import Mock | ||
|
||
import graphviz | ||
import pytest | ||
|
||
from agents import Agent | ||
from agents.extensions.visualization import ( | ||
draw_graph, | ||
get_all_edges, | ||
get_all_nodes, | ||
get_main_graph, | ||
) | ||
|
||
|
||
@pytest.fixture | ||
def mock_agent(): | ||
tool1 = Mock() | ||
tool1.name = "Tool1" | ||
tool2 = Mock() | ||
tool2.name = "Tool2" | ||
|
||
handoff1 = Mock() | ||
handoff1.name = "Handoff1" | ||
handoff1.tools = [] | ||
handoff1.handoffs = [] | ||
|
||
agent = Mock(spec=Agent) | ||
agent.name = "Agent1" | ||
agent.tools = [tool1, tool2] | ||
agent.handoffs = [handoff1] | ||
|
||
return agent | ||
|
||
|
||
def test_get_main_graph(mock_agent): | ||
result = get_main_graph(mock_agent) | ||
assert "digraph G" in result | ||
assert "graph [splines=true];" in result | ||
assert 'node [fontname="Arial"];' in result | ||
assert "edge [penwidth=1.5];" in result | ||
assert '"__start__" [shape=ellipse, style=filled, fillcolor=lightblue];' in result | ||
assert '"__end__" [shape=ellipse, style=filled, fillcolor=lightblue];' in result | ||
assert ( | ||
'"Agent1" [label="Agent1", shape=box, style=filled, ' | ||
"fillcolor=lightyellow, width=1.5, height=0.8];" in result | ||
) | ||
assert ( | ||
'"Tool1" [label="Tool1", shape=ellipse, style=filled, ' | ||
"fillcolor=lightgreen, width=0.5, height=0.3];" in result | ||
) | ||
assert ( | ||
'"Tool2" [label="Tool2", shape=ellipse, style=filled, ' | ||
"fillcolor=lightgreen, width=0.5, height=0.3];" in result | ||
) | ||
assert ( | ||
'"Handoff1" [label="Handoff1", shape=box, style=filled, style=rounded, ' | ||
"fillcolor=lightyellow, width=1.5, height=0.8];" in result | ||
) | ||
|
||
|
||
def test_get_all_nodes(mock_agent): | ||
result = get_all_nodes(mock_agent) | ||
assert ( | ||
'"Agent1" [label="Agent1", shape=box, style=filled, ' | ||
"fillcolor=lightyellow, width=1.5, height=0.8];" in result | ||
) | ||
assert ( | ||
'"Tool1" [label="Tool1", shape=ellipse, style=filled, ' | ||
"fillcolor=lightgreen, width=0.5, height=0.3];" in result | ||
) | ||
assert ( | ||
'"Tool2" [label="Tool2", shape=ellipse, style=filled, ' | ||
"fillcolor=lightgreen, width=0.5, height=0.3];" in result | ||
) | ||
assert ( | ||
'"Handoff1" [label="Handoff1", shape=box, style=filled, style=rounded, ' | ||
"fillcolor=lightyellow, width=1.5, height=0.8];" in result | ||
) | ||
|
||
|
||
def test_get_all_edges(mock_agent): | ||
result = get_all_edges(mock_agent) | ||
assert '"__start__" -> "Agent1";' in result | ||
assert '"Agent1" -> "Tool1" [style=dotted, penwidth=1.5];' in result | ||
assert '"Tool1" -> "Agent1" [style=dotted, penwidth=1.5];' in result | ||
assert '"Agent1" -> "Tool2" [style=dotted, penwidth=1.5];' in result | ||
assert '"Tool2" -> "Agent1" [style=dotted, penwidth=1.5];' in result | ||
assert '"Agent1" -> "Handoff1";' in result | ||
assert '"Handoff1" -> "__end__";' in result | ||
|
||
|
||
def test_draw_graph(mock_agent): | ||
graph = draw_graph(mock_agent) | ||
assert isinstance(graph, graphviz.Source) | ||
assert "digraph G" in graph.source | ||
assert "graph [splines=true];" in graph.source | ||
assert 'node [fontname="Arial"];' in graph.source | ||
assert "edge [penwidth=1.5];" in graph.source | ||
assert '"__start__" [shape=ellipse, style=filled, fillcolor=lightblue];' in graph.source | ||
assert '"__end__" [shape=ellipse, style=filled, fillcolor=lightblue];' in graph.source | ||
assert ( | ||
'"Agent1" [label="Agent1", shape=box, style=filled, ' | ||
"fillcolor=lightyellow, width=1.5, height=0.8];" in graph.source | ||
) | ||
assert ( | ||
'"Tool1" [label="Tool1", shape=ellipse, style=filled, ' | ||
"fillcolor=lightgreen, width=0.5, height=0.3];" in graph.source | ||
) | ||
assert ( | ||
'"Tool2" [label="Tool2", shape=ellipse, style=filled, ' | ||
"fillcolor=lightgreen, width=0.5, height=0.3];" in graph.source | ||
) | ||
assert ( | ||
'"Handoff1" [label="Handoff1", shape=box, style=filled, style=rounded, ' | ||
"fillcolor=lightyellow, width=1.5, height=0.8];" in graph.source | ||
) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you want
handoff.agent_name
nothandoff.name
, if it's aHandoff
instance