From b56bc75a256a076c9521a73c28483439f844db77 Mon Sep 17 00:00:00 2001 From: WeiHaoxuan <156066259+Whxuan0701@users.noreply.github.com> Date: Tue, 18 Aug 2026 19:54:14 +0800 Subject: [PATCH] feat(core): add dependency-free graph serialization --- burr/core/application.py | 9 ++++ burr/core/graph.py | 31 +++++++++++++ docs/reference/application.rst | 18 ++++++++ tests/core/test_graph.py | 80 +++++++++++++++++++++++++++++++++- 4 files changed, 136 insertions(+), 2 deletions(-) diff --git a/burr/core/application.py b/burr/core/application.py index 25bce4a10..841bbf2b7 100644 --- a/burr/core/application.py +++ b/burr/core/application.py @@ -606,6 +606,15 @@ class ApplicationGraph(Graph): entrypoint: Action + def to_dict(self) -> dict[str, Any]: + """Return JSON-serializable application graph metadata. + + :return: Graph metadata including the application's entrypoint. + """ + graph_data = super().to_dict() + graph_data.update(type="application_graph", entrypoint=self.entrypoint.name) + return graph_data + @dataclasses.dataclass class ApplicationIdentifiers: diff --git a/burr/core/graph.py b/burr/core/graph.py index fcbc9c5cb..9e521bcd0 100644 --- a/burr/core/graph.py +++ b/burr/core/graph.py @@ -177,6 +177,37 @@ def get_actions_by_tag(self, tag: str) -> List[Action]: ) return self._action_tag_map.get(tag) + def to_dict(self) -> dict[str, Any]: + """Return a dependency-free, JSON-serializable representation of the graph. + + Actions and transitions retain their declaration order. Unordered action + metadata is sorted so repeated exports are deterministic. + + :return: Graph metadata suitable for external tools and user interfaces. + """ + actions = [] + for action in self.actions: + required_inputs, optional_inputs = action.optional_and_required_inputs + actions.append( + { + "name": action.name, + "reads": sorted(action.reads), + "writes": sorted(action.writes), + "inputs": sorted(required_inputs), + "optional_inputs": sorted(optional_inputs), + "tags": sorted(action.tags), + } + ) + transitions = [ + { + "from": transition.from_.name, + "to": transition.to.name, + "condition": transition.condition.name, + } + for transition in self.transitions + ] + return {"type": "graph", "actions": actions, "transitions": transitions} + def visualize( self, output_file_path: Optional[Union[str, pathlib.Path]] = None, diff --git a/docs/reference/application.rst b/docs/reference/application.rst index d1a03d82e..4183c2597 100644 --- a/docs/reference/application.rst +++ b/docs/reference/application.rst @@ -51,6 +51,24 @@ of situations. The ``GraphBuilder`` class is used to build a graph, and the ``Graph`` class can be passed to the application builder. +Graphs can also be exported without Graphviz or tracking dependencies. ``to_dict()`` +returns JSON-serializable action and transition metadata for external tools, API +responses, and custom user interfaces: + +.. code-block:: python + + import json + + graph_data = application.graph.to_dict() + payload = json.dumps(graph_data) + +``ApplicationGraph.to_dict()`` additionally includes the application entrypoint and +uses ``application_graph`` as its type. This compact metadata format intentionally +omits action source code and is distinct from the tracking subsystem's persisted +``ApplicationModel`` format. Action and transition declaration order is preserved, +while set-like metadata such as reads, writes, inputs, and tags is sorted for +deterministic output. + .. autoclass:: burr.core.graph.GraphBuilder :members: diff --git a/tests/core/test_graph.py b/tests/core/test_graph.py index 22cdda7b6..95ffb5892 100644 --- a/tests/core/test_graph.py +++ b/tests/core/test_graph.py @@ -15,11 +15,13 @@ # specific language governing permissions and limitations # under the License. -from typing import Callable +import json +from typing import Callable, Union import pytest from burr.core import Action, Condition, Result, State, default +from burr.core.application import ApplicationGraph from burr.core.graph import GraphBuilder, _validate_actions, _validate_transitions @@ -31,7 +33,7 @@ def __init__( writes: list[str], fn: Callable[..., dict], update_fn: Callable[[dict, State], State], - inputs: list[str], + inputs: Union[list[str], tuple[list[str], list[str]]], tags: list[str] = None, ): super(PassedInAction, self).__init__() @@ -204,3 +206,77 @@ def test_get_actions_by_tag(): assert len(graph.get_actions_by_tag("tag3")) == 1 with pytest.raises(ValueError, match="not found"): graph.get_actions_by_tag("tag4") + + +def test_graph_to_dict_returns_dependency_free_structure(): + export_action = PassedInAction( + reads=["z_read", "a_read"], + writes=["z_write", "a_write"], + fn=lambda state, **inputs: inputs, + update_fn=lambda result, state: state, + inputs=(["z_required", "a_required"], ["z_optional", "a_optional"]), + tags=["z_tag", "a_tag"], + ) + graph = ( + GraphBuilder() + .with_actions(zeta=export_action, alpha=Result("a_read")) + .with_transitions( + ("zeta", "alpha", Condition.expr("a_read < z_read")), + ("alpha", "zeta"), + ) + .build() + ) + + exported = graph.to_dict() + + assert exported == { + "type": "graph", + "actions": [ + { + "name": "zeta", + "reads": ["a_read", "z_read"], + "writes": ["a_write", "z_write"], + "inputs": ["a_required", "z_required"], + "optional_inputs": ["a_optional", "z_optional"], + "tags": ["a_tag", "z_tag"], + }, + { + "name": "alpha", + "reads": ["a_read"], + "writes": [], + "inputs": [], + "optional_inputs": [], + "tags": [], + }, + ], + "transitions": [ + {"from": "zeta", "to": "alpha", "condition": "a_read < z_read"}, + {"from": "alpha", "to": "zeta", "condition": "default"}, + ], + } + assert json.loads(json.dumps(exported)) == exported + + +def test_application_graph_to_dict_includes_entrypoint(): + counter = base_counter_action.with_name("counter") + graph = ApplicationGraph( + actions=[counter], + transitions=[], + entrypoint=counter, + ) + + assert graph.to_dict() == { + "type": "application_graph", + "entrypoint": "counter", + "actions": [ + { + "name": "counter", + "reads": ["count"], + "writes": ["count"], + "inputs": [], + "optional_inputs": [], + "tags": ["tag1", "tag2"], + } + ], + "transitions": [], + }