-
Notifications
You must be signed in to change notification settings - Fork 1.1k
feat: Add Graphviz-based agent visualization functionality #147
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
Merged
Merged
Changes from 6 commits
Commits
Show all changes
35 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 9f7d596
feat: add optional dependency for visualization using Graphviz
MartinEBravo a5b7abe
feat: enhance visualization functions with optional type hints and im…
MartinEBravo 623063b
refactor: clean up visualization functions by removing unused nodes a…
MartinEBravo 900a97f
Merge branch 'main' of https://github.com/openai/openai-agents-python…
MartinEBravo b3addcf
Add visualization optional dependency for graphviz
MartinEBravo 9d04671
Rename visualization dependency to viz in pyproject.toml
MartinEBravo 48fad9e
Merge branch 'main' of https://github.com/openai/openai-agents-python…
MartinEBravo 57ecebf
Refactor visualization node label formatting in get_all_nodes function
MartinEBravo 698fd69
Update installation instructions for visualization dependency to use …
MartinEBravo 6be9b2a
Update visualization.md
rm-openai 59aed34
Update pyproject.toml
rm-openai 2f2606e
Add graphviz as a dependency and update import statements
MartinEBravo d8922ff
Add visualization.md to navigation in mkdocs.yml
MartinEBravo b9d32cd
Refactor get_all_edges function to remove unused parent parameter
MartinEBravo 29103ca
Add Jupyter Notebook files to .gitignore
MartinEBravo 5ad53d8
Add start and end nodes to graph visualization and update edge genera…
MartinEBravo 351b607
Refactor visualization functions to improve formatting and streamline…
MartinEBravo 3068e42
Fix type ignore comment for agent check in get_all_edges function
MartinEBravo 70aff1d
linting
MartinEBravo c16deb2
Remove Jupyter Notebook files from .gitignore
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 graphviz | ||
``` | ||
|
||
## 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.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) | ||
``` | ||
|
||
 | ||
|
||
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,115 @@ | ||
import graphviz | ||
|
||
from src.agents.agent 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, 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, fillcolor=lightyellow, width=1.5, height=0.8];""") | ||
parts.append(get_all_nodes(handoff)) | ||
|
||
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,111 @@ | ||
from unittest.mock import Mock | ||
|
||
import graphviz | ||
import pytest | ||
|
||
from src.agents.agent import Agent | ||
from src.agents.visualizations 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 | ||
) |
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.
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.
Fixed!