Repository navigation
fix(cli): keep graph edges attached to node names with a colon - #7546
kavishkartha05 wants to merge 6 commits into
Conversation
|
Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA). View this failed invocation of the CLA check for more information. For the most up to date status, view the checks section at the bottom of the pull request. |
There was a problem hiding this comment.
Code Review
This pull request introduces a custom _Digraph subclass that overrides the private _quote_edge method of graphviz.Digraph to prevent colon-named nodes from being incorrectly split into ports, along with comprehensive unit tests to verify this behavior. Feedback suggests avoiding the subclassing of private methods by instead appending formatted edge strings directly to the public dot.body attribute, which provides a more robust and maintainable solution.
| class _Digraph(graphviz.Digraph): # type: ignore[misc] # graphviz is untyped | ||
| """A Digraph whose edge endpoints are quoted as plain node ids. | ||
|
|
||
| Digraph.edge() splits an endpoint on ":" before quoting it, treating the | ||
| rest as a port, so an edge to a node named "srv:tool" is written as | ||
| `a -> srv:tool` and attaches to a phantom node "srv". Quoting the whole | ||
| endpoint keeps the real name as the DOT id, which adk-web reads back from | ||
| the SVG title as the node name. No port syntax is used in these graphs. | ||
|
|
||
| This overrides the private `_quote_edge` hook. Verified on graphviz 0.20.2 | ||
| and 0.21; a unit test fails if the hook is renamed or stops working. | ||
| """ | ||
|
|
||
| _quote_edge = staticmethod(quoting.quote) |
There was a problem hiding this comment.
Overriding the private _quote_edge method of graphviz.Digraph introduces a dependency on the internal implementation details of the graphviz library. While you have mitigated this risk with a unit test (test_digraph_edge_hook_quotes_colon_endpoints), private methods (those prefixed with an underscore) are not part of the public API and can be renamed, removed, or have their signatures changed in future minor or patch releases of graphviz without warning.\n\nAn alternative, more robust approach that avoids subclassing and overriding private methods is to bypass dot.edge() and append the formatted edge strings directly to dot.body (which is a public, documented list of lines for the graph body).\n\nFor example, you could define a helper function:\n\npython\ndef _add_edge(\n dot: graphviz.Digraph,\n from_node: str,\n to_node: str,\n label: str | None = None,\n style: str | None = None,\n color: str | None = None,\n) -> None:\n from_quoted = quoting.quote(from_node)\n to_quoted = quoting.quote(to_node)\n attrs = []\n if label:\n attrs.append(f\"label={quoting.quote(label)}\")\n if style:\n attrs.append(f\"style={quoting.quote(style)}\")\n if color:\n attrs.append(f\"color={quoting.quote(color)}\")\n \n attr_str = f\" [{', '.join(attrs)}]\" if attrs else \"\"\n dot.body.append(f'\\t{from_quoted} -> {to_quoted}{attr_str}')\n\n\nAnd then use standard graphviz.Digraph and replace dot.edge(...) calls with _add_edge(dot, ...). This completely eliminates the need for subclassing, the type: ignore[misc] comment, and the fragile private method override.
There was a problem hiding this comment.
Thanks. I considered the dot.body route. It's public API, but it needs quoting.attr_list and an additional manual trailing newline and replaces all three edge() call sites, so it changes output paths for graphs without colon names. The subclass keeps those byte-identical (checked on 12 scenarios), and test_digraph_edge_hook_quotes_colon_endpoints fails with an explicit message if graphviz renames or changes the hook (verified on 0.20.2 and 0.21). Happy to switch to the dot.body approach if maintainers prefer avoiding the private hook.
graphviz.Digraph.edge() splits an endpoint on ":" before quoting it, so an edge to a node or tool named "srv:tool" was written as `a -> srv:tool`. DOT reads that as node "srv" with port "tool", so the edge attached to a phantom node while the real node was drawn disconnected. Quote edge endpoints through a small Digraph subclass so the real name stays the DOT id. adk-web reads the SVG title as the node name, so renaming ids would break click handling. Output is unchanged for graphs without ":" in any name, including __START__ and __END__. Add the first tests for plot_workflow_graph, using format="dot" so no graphviz binary is needed. Fixes google#7543
2a23d96 to
cb82fb7
Compare
Add tests for a workflow node named "b:x" in each edge position handled by plot_workflow_graph: as an edge target (a -> b:x), as an edge source (b:x -> c) and as a terminal node linked to END (b:x -> __END__). Each test asserts on the DOT source that every edge endpoint is a declared node, that no phantom "b" endpoint appears, and that the node is declared under its real name.
get_agent_graph draws every edge through Digraph.edge(), which splits an endpoint on ":" before quoting it. An edge to a tool named "srv:tool" was written as `root -> srv:tool`, which DOT reads as node "srv" with port "tool", so the edge attached to a phantom node and the real tool node was drawn disconnected. This is the same bug fixed for plot_workflow_graph in this PR. Build the top-level graph and the cluster subgraphs for Sequential, Loop and Parallel agents from the _Digraph subclass added earlier, so edge endpoints are quoted whole and the real name stays the DOT id. Tool edges inside a cluster are drawn on the cluster's own Digraph, so it needs the subclass too. Output is unchanged for graphs without a colon in any name. Add tests for a colon-named tool on a plain agent and inside each cluster type.
Please ensure you have read the contribution guide before creating a pull request.
Link to Issue or Description of Change
1. Link to an existing issue (if applicable):
2. Or, if no issue exists, describe the change:
An issue exists (#7543), so the description below summarizes it.
Problem:
plot_workflow_graph(used by/dev/apps/{app}/build_graph_image) andget_agent_graph(cli/agent_graph.py) declare a node namedsrv:toolas"srv:tool", butgraphviz.Digraph.edge()splits an endpoint on:beforequoting it, so the edge is written as
a -> srv:tool. DOT reads that as nodesrvwith porttool: the edge attaches to a phantom nodesrvand the realnode is drawn disconnected. Function tool names can legitimately contain
:.Agent and workflow-node names must be Python identifiers, so tool names are the
realistic way to trigger this.
Solution:
Quote edge endpoints through a small
Digraphsubclass(
_quote_edge = staticmethod(quoting.quote)), so the real name stays the DOTid and is quoted in edge statements. It is used in both places that draw edges
from user-controlled names:
plot_workflow_graph(
cli/utils/graph_visualization.py) andget_agent_graph(
cli/agent_graph.py), including the cluster subgraphs for Sequential, Loopand Parallel agents, since tool edges inside a cluster are drawn on the
cluster's own
Digraph. Output is byte-identical for graphs with no:in anyname, including
__START__and__END__(checked on 22agent_graphoutputsand 12
plot_workflow_graphoutputs againstupstream/main).Alternatives considered:
__node_0__): rejected. adk-web reads each node's SVG<title>(the DOT id)as the node name for click and expand handling
(
src/app/utils/svg-interaction.utils.ts,agent-structure-graph-dialog.tsin google/adk-web), so renaming ids wouldbreak selecting those nodes.
edge(): rejected. The library splitson
:before quoting, so"srv:tool"becomes"\"srv":"tool\"".dot.bodywith the publicquotinghelpers:not simpler. It needs
quoting.attr_list, a manual trailing newline, andreplaces three
edge()call sites.Caveat: the subclass overrides the private
Digraph._quote_edgehook. Iverified it on graphviz 0.20.2 (the lower bound of
graphviz>=0.20.2,<1) and0.21. A unit test fails with an explicit message if the hook is renamed or stops
working. No port syntax is used anywhere in these graphs.
Testing Plan
Unit Tests:
New tests in
tests/unittests/cli/utils/test_graph_visualization.py(colonnames as edge target, source and terminal node, the
__START__/__END__test,and a guard test that fails if graphviz's
_quote_edgehook goes away) andtests/unittests/cli/test_agent_graph.py(4 cases: a colon-named tool on aplain agent, and inside Sequential, Loop and Parallel clusters). All fail on
upstream/mainand pass here. A partial fix that skips the clusterconstructors fails the cluster cases.
pyteston the three graph test files: 41 passed on graphviz 0.20.2 and on0.21 (both bounds of
graphviz>=0.20.2,<1)pytest tests/unittests/cli -n auto: 1489 passed, 5 skipped, 1 xfailedmypy .as in CI (baseline vs PR): 535 errors on both, 0 newpre-commit run --files <changed files>: all hooks pass (realaddlicenserun, not skipped)
The new tests use
format="dot", so no graphviz binary is needed.Manual End-to-End (E2E) Tests:
I ran
adk webon an agent with a function tool namedsrv:tooland capturedthe response of
GET /dev/apps/colon_agent/build_graph_image:I also opened the agent structure graph in the
adk webUI on both builds:colon_agentpoints to a phantomsrvnode, andthe real
srv:toolnode is drawn disconnected.colon_agenttosrv:tool.<title>is stillsrv:tool(checked in DevTools), which iswhat adk-web reads as the node name. Tool nodes aren't clickable in this
view, so I couldn't exercise click/drill-down directly; it is unaffected by
design, since only edge endpoints are quoted differently.
agent_graph.pychange is covered by the unit tests above and astandalone repro of its DOT output; I did not screenshot that view
separately.
Before (
upstream/main):After (this PR):
Checklist
Additional context
Screenshots are in the manual E2E section above.
🤖 Developed with Claude Code