flow-xray 0.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,17 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ .pytest_cache/
8
+
9
+ # secrets
10
+ .env
11
+
12
+ # generated artifacts (re-create with flow-xray run / python graph_debug.py)
13
+ *.png
14
+ !assets/*.png
15
+ agent_trace.html
16
+ forward.html
17
+ backward.html
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 kirillostapenko
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,147 @@
1
+ Metadata-Version: 2.4
2
+ Name: flow-xray
3
+ Version: 0.2.1
4
+ Summary: Execution graph debugger — see what your agent / pipeline actually does.
5
+ Project-URL: Homepage, https://github.com/kroq86/flow-xray
6
+ Project-URL: Repository, https://github.com/kroq86/flow-xray
7
+ Project-URL: Issues, https://github.com/kroq86/flow-xray/issues
8
+ Author-email: KirillOstapenko <djkroq@gmail.com>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: DAG,LLM,agent,debugger,execution-graph,observability,tracing
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Topic :: Software Development :: Debuggers
22
+ Requires-Python: >=3.10
23
+ Provides-Extra: dev
24
+ Requires-Dist: pytest>=7; extra == 'dev'
25
+ Description-Content-Type: text/markdown
26
+
27
+ <p align="center">
28
+ <img src="https://raw.githubusercontent.com/kroq86/flow-xray/main/assets/banner.png" alt="flow-xray" width="100%">
29
+ </p>
30
+
31
+ <p align="center">
32
+ <b>See what your agent actually does.</b><br>
33
+ One decorator, one HTML file — a visual execution graph instead of logs.
34
+ </p>
35
+
36
+ <p align="center">
37
+ <a href="https://pypi.org/project/flow-xray/"><img src="https://img.shields.io/pypi/v/flow-xray?color=blue&cacheSeconds=60" alt="PyPI"></a>
38
+ <a href="https://github.com/kroq86/flow-xray/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-green" alt="License"></a>
39
+ <img src="https://img.shields.io/badge/python-3.10+-blue" alt="Python">
40
+ </p>
41
+
42
+ ---
43
+
44
+ ```python
45
+ from flow_xray import trace
46
+
47
+ @trace
48
+ def call_llm(prompt):
49
+ return openai.chat(prompt)
50
+
51
+ @trace
52
+ def agent(query):
53
+ plan = call_llm(f"plan: {query}")
54
+ return call_llm(f"answer based on: {plan}")
55
+
56
+ result = trace.run(agent, "weather in Tokyo?")
57
+ result.to_html("trace.html")
58
+ ```
59
+
60
+ <p align="center">
61
+ <img src="https://raw.githubusercontent.com/kroq86/flow-xray/main/assets/demo.png" alt="flow-xray trace viewer" width="100%">
62
+ </p>
63
+
64
+ Open `trace.html` — you get a **DAG** of every traced step with inputs, outputs, latency, tokens, cost, and errors. Click a node to inspect. No server, no account, no log viewer — one local file.
65
+
66
+ ## Install
67
+
68
+ ```bash
69
+ pip install flow-xray
70
+ ```
71
+
72
+ ## Usage
73
+
74
+ ### Decorator + `trace.run`
75
+
76
+ ```python
77
+ from flow_xray import trace
78
+
79
+ @trace
80
+ def step_a(x):
81
+ return x + 1
82
+
83
+ @trace
84
+ def pipeline(x):
85
+ return step_a(x) * 2
86
+
87
+ result = trace.run(pipeline, 5)
88
+ result.to_html("pipeline.html")
89
+ ```
90
+
91
+ ### CLI
92
+
93
+ ```bash
94
+ flow-xray run my_agent.py --html trace.html
95
+ ```
96
+
97
+ The script must use `@trace` on the functions you want captured. The CLI provides the session; just call your functions normally.
98
+
99
+ ### Token / cost tracking
100
+
101
+ Token usage and estimated cost are auto-extracted from OpenAI response objects, or you can set them manually:
102
+
103
+ ```python
104
+ @trace
105
+ def call_llm(prompt):
106
+ resp = openai.chat.completions.create(...)
107
+ trace.meta(model=resp.model,
108
+ prompt_tokens=resp.usage.prompt_tokens,
109
+ completion_tokens=resp.usage.completion_tokens)
110
+ return resp.choices[0].message.content
111
+ ```
112
+
113
+ ### What you see
114
+
115
+ - **Nodes** = function calls (name + latency + tokens)
116
+ - **Edges** = caller → callee
117
+ - **Green** = OK, **Red** = error, **Yellow** = slow (>1s)
118
+ - **Header** = total nodes, latency, tokens, estimated cost
119
+ - **Click a node** → side panel shows inputs, output, error, timing, model, tokens, cost
120
+
121
+ ## Why this exists
122
+
123
+ Langfuse, Helicone, LangSmith — they give you **timelines and logs**.
124
+
125
+ But when your agent pipeline branches, retries, or chains 6 tools — you don't need another table. You need a **graph**.
126
+
127
+ flow-xray is **not** an agent framework. It's the layer **below** them — like Chrome DevTools is to browsers.
128
+
129
+ ## How it works
130
+
131
+ `@trace` wraps functions. When called inside a `trace.run()` session (or `flow-xray run` CLI), it records:
132
+ - function name
133
+ - bound arguments
134
+ - return value or exception
135
+ - wall-clock latency
136
+ - token usage and estimated cost (auto or manual)
137
+ - parent/child relationships (call stack → DAG)
138
+
139
+ `result.to_html()` embeds the trace as JSON in a self-contained HTML page that renders via WASM Graphviz (CDN, works offline after first load).
140
+
141
+ ## Also included
142
+
143
+ Scalar autodiff core (micrograd-style `Value` graph with DOT/JSON export and stepping debugger) lives under `flow-xray dot` CLI and `from flow_xray import Value`. See `examples/` and `plan.md`.
144
+
145
+ ## License
146
+
147
+ MIT
@@ -0,0 +1,121 @@
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/kroq86/flow-xray/main/assets/banner.png" alt="flow-xray" width="100%">
3
+ </p>
4
+
5
+ <p align="center">
6
+ <b>See what your agent actually does.</b><br>
7
+ One decorator, one HTML file — a visual execution graph instead of logs.
8
+ </p>
9
+
10
+ <p align="center">
11
+ <a href="https://pypi.org/project/flow-xray/"><img src="https://img.shields.io/pypi/v/flow-xray?color=blue&cacheSeconds=60" alt="PyPI"></a>
12
+ <a href="https://github.com/kroq86/flow-xray/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-green" alt="License"></a>
13
+ <img src="https://img.shields.io/badge/python-3.10+-blue" alt="Python">
14
+ </p>
15
+
16
+ ---
17
+
18
+ ```python
19
+ from flow_xray import trace
20
+
21
+ @trace
22
+ def call_llm(prompt):
23
+ return openai.chat(prompt)
24
+
25
+ @trace
26
+ def agent(query):
27
+ plan = call_llm(f"plan: {query}")
28
+ return call_llm(f"answer based on: {plan}")
29
+
30
+ result = trace.run(agent, "weather in Tokyo?")
31
+ result.to_html("trace.html")
32
+ ```
33
+
34
+ <p align="center">
35
+ <img src="https://raw.githubusercontent.com/kroq86/flow-xray/main/assets/demo.png" alt="flow-xray trace viewer" width="100%">
36
+ </p>
37
+
38
+ Open `trace.html` — you get a **DAG** of every traced step with inputs, outputs, latency, tokens, cost, and errors. Click a node to inspect. No server, no account, no log viewer — one local file.
39
+
40
+ ## Install
41
+
42
+ ```bash
43
+ pip install flow-xray
44
+ ```
45
+
46
+ ## Usage
47
+
48
+ ### Decorator + `trace.run`
49
+
50
+ ```python
51
+ from flow_xray import trace
52
+
53
+ @trace
54
+ def step_a(x):
55
+ return x + 1
56
+
57
+ @trace
58
+ def pipeline(x):
59
+ return step_a(x) * 2
60
+
61
+ result = trace.run(pipeline, 5)
62
+ result.to_html("pipeline.html")
63
+ ```
64
+
65
+ ### CLI
66
+
67
+ ```bash
68
+ flow-xray run my_agent.py --html trace.html
69
+ ```
70
+
71
+ The script must use `@trace` on the functions you want captured. The CLI provides the session; just call your functions normally.
72
+
73
+ ### Token / cost tracking
74
+
75
+ Token usage and estimated cost are auto-extracted from OpenAI response objects, or you can set them manually:
76
+
77
+ ```python
78
+ @trace
79
+ def call_llm(prompt):
80
+ resp = openai.chat.completions.create(...)
81
+ trace.meta(model=resp.model,
82
+ prompt_tokens=resp.usage.prompt_tokens,
83
+ completion_tokens=resp.usage.completion_tokens)
84
+ return resp.choices[0].message.content
85
+ ```
86
+
87
+ ### What you see
88
+
89
+ - **Nodes** = function calls (name + latency + tokens)
90
+ - **Edges** = caller → callee
91
+ - **Green** = OK, **Red** = error, **Yellow** = slow (>1s)
92
+ - **Header** = total nodes, latency, tokens, estimated cost
93
+ - **Click a node** → side panel shows inputs, output, error, timing, model, tokens, cost
94
+
95
+ ## Why this exists
96
+
97
+ Langfuse, Helicone, LangSmith — they give you **timelines and logs**.
98
+
99
+ But when your agent pipeline branches, retries, or chains 6 tools — you don't need another table. You need a **graph**.
100
+
101
+ flow-xray is **not** an agent framework. It's the layer **below** them — like Chrome DevTools is to browsers.
102
+
103
+ ## How it works
104
+
105
+ `@trace` wraps functions. When called inside a `trace.run()` session (or `flow-xray run` CLI), it records:
106
+ - function name
107
+ - bound arguments
108
+ - return value or exception
109
+ - wall-clock latency
110
+ - token usage and estimated cost (auto or manual)
111
+ - parent/child relationships (call stack → DAG)
112
+
113
+ `result.to_html()` embeds the trace as JSON in a self-contained HTML page that renders via WASM Graphviz (CDN, works offline after first load).
114
+
115
+ ## Also included
116
+
117
+ Scalar autodiff core (micrograd-style `Value` graph with DOT/JSON export and stepping debugger) lives under `flow-xray dot` CLI and `from flow_xray import Value`. See `examples/` and `plan.md`.
118
+
119
+ ## License
120
+
121
+ MIT
@@ -0,0 +1,38 @@
1
+ """
2
+ flow-xray — execution graph debugger.
3
+
4
+ Primary API: ``trace`` (decorator + runner for capturing execution DAGs).
5
+
6
+ Also includes scalar Value autodiff core + inspection exports.
7
+ """
8
+
9
+ from flow_xray.trace import TraceNode, TraceResult, trace
10
+
11
+ from flow_xray.debugger import GraphDebugger, TwoStepDebugger, iter_backward_steps
12
+ from flow_xray.export_dot import value_graph_to_dot, write_dot
13
+ from flow_xray.export_html import standalone_viewer_html, write_standalone_viewer_html
14
+ from flow_xray.ir import graph_to_ir, ir_to_json
15
+ from flow_xray.value import Value, backward_from, topological_order, zero_grad
16
+
17
+ __all__ = [
18
+ # execution tracing (primary)
19
+ "trace",
20
+ "TraceNode",
21
+ "TraceResult",
22
+ # scalar autodiff
23
+ "Value",
24
+ "backward_from",
25
+ "topological_order",
26
+ "zero_grad",
27
+ "value_graph_to_dot",
28
+ "write_dot",
29
+ "standalone_viewer_html",
30
+ "write_standalone_viewer_html",
31
+ "TwoStepDebugger",
32
+ "GraphDebugger",
33
+ "iter_backward_steps",
34
+ "graph_to_ir",
35
+ "ir_to_json",
36
+ ]
37
+
38
+ __version__ = "0.2.1"
@@ -0,0 +1,5 @@
1
+ """``python -m flow_xray run|dot …``"""
2
+
3
+ from flow_xray.cli import main
4
+
5
+ raise SystemExit(main())
@@ -0,0 +1,149 @@
1
+ """
2
+ CLI entry points.
3
+
4
+ ``flow-xray run`` — execute a script with @trace, export HTML execution graph.
5
+ ``flow-xray dot`` — scalar-Value DOT/HTML export (demo / from script).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ import sys
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+
16
+ # ---------------------------------------------------------------------------
17
+ # gtype run (execution tracing)
18
+ # ---------------------------------------------------------------------------
19
+
20
+ def cmd_run(args: argparse.Namespace) -> int:
21
+ from flow_xray.trace import TraceResult, _TraceSession, _count_all
22
+
23
+ path = Path(args.script)
24
+ if not path.is_file():
25
+ print(f"error: not a file: {path}", file=sys.stderr)
26
+ return 2
27
+ # Use a non-__main__ name so scripts' `if __name__ == "__main__"` blocks
28
+ # are skipped; @trace-decorated functions are captured by the CLI session.
29
+ ns: dict[str, Any] = {"__name__": "__flow_xray_run__", "__file__": str(path)}
30
+ session = _TraceSession()
31
+ with session:
32
+ exec(compile(path.read_text(encoding="utf-8"), str(path), "exec"), ns, ns)
33
+ result = TraceResult(session.roots)
34
+ html_path = args.html or path.stem + "_trace.html"
35
+ result.to_html(html_path, title=args.title or f"Trace: {path.name}")
36
+ total, errs = _count_all(result.roots)
37
+ print(f"Wrote {html_path} ({total} nodes, {errs} errors)", file=sys.stderr)
38
+ return 0
39
+
40
+
41
+ # ---------------------------------------------------------------------------
42
+ # gtype dot (scalar Value graph — legacy)
43
+ # ---------------------------------------------------------------------------
44
+
45
+ def _demo_root() -> Any:
46
+ from flow_xray.value import Value
47
+ a = Value(2.0, label="a")
48
+ b = Value(-3.0, label="b")
49
+ c = Value(10.0, label="c")
50
+ L = ((a * b) + c).relu().tanh()
51
+ L.label = "L"
52
+ return L
53
+
54
+
55
+ def cmd_demo(args: argparse.Namespace) -> int:
56
+ from flow_xray.export_dot import value_graph_to_dot, write_dot
57
+ from flow_xray.export_html import write_standalone_viewer_html
58
+ root = _demo_root()
59
+ forward = value_graph_to_dot(root, show_grad=False)
60
+ root.backward()
61
+ backward = value_graph_to_dot(root, show_grad=True)
62
+ if args.output_dir:
63
+ out = Path(args.output_dir)
64
+ out.mkdir(parents=True, exist_ok=True)
65
+ write_dot(str(out / "forward.dot"), forward)
66
+ write_dot(str(out / "backward.dot"), backward)
67
+ print(f"Wrote {out / 'forward.dot'} and {out / 'backward.dot'}", file=sys.stderr)
68
+ return 0
69
+ if args.html:
70
+ dot = backward if args.which == "backward" else forward
71
+ title = f"flow-xray demo ({args.which})"
72
+ write_standalone_viewer_html(args.html, dot, title=title)
73
+ print(f"Wrote {args.html} — open in browser (no dot/PNG).", file=sys.stderr)
74
+ return 0
75
+ if args.which == "backward":
76
+ sys.stdout.write(backward)
77
+ else:
78
+ sys.stdout.write(forward)
79
+ return 0
80
+
81
+
82
+ def cmd_export(args: argparse.Namespace) -> int:
83
+ from flow_xray.export_dot import value_graph_to_dot, write_dot
84
+ from flow_xray.export_html import write_standalone_viewer_html
85
+ from flow_xray.value import Value
86
+ path = Path(args.script)
87
+ ns: dict[str, Any] = {"__name__": "__main__", "__file__": str(path)}
88
+ exec(compile(path.read_text(encoding="utf-8"), str(path), "exec"), ns, ns)
89
+ root = ns.get("root")
90
+ if root is None:
91
+ print("error: script must define variable `root` (a Value)", file=sys.stderr)
92
+ return 2
93
+ if not isinstance(root, Value):
94
+ print("error: `root` must be a flow_xray.Value", file=sys.stderr)
95
+ return 2
96
+ forward = value_graph_to_dot(root, show_grad=False)
97
+ if args.backward:
98
+ root.backward()
99
+ dot = value_graph_to_dot(root, show_grad=True)
100
+ else:
101
+ dot = forward
102
+ if args.html:
103
+ title = "flow-xray graph" + (" (backward)" if args.backward else " (forward)")
104
+ write_standalone_viewer_html(args.html, dot, title=title)
105
+ print(f"Wrote {args.html} — open in browser.", file=sys.stderr)
106
+ return 0
107
+ out_path = args.output
108
+ if out_path:
109
+ write_dot(out_path, dot)
110
+ print(out_path, file=sys.stderr)
111
+ else:
112
+ sys.stdout.write(dot)
113
+ return 0
114
+
115
+
116
+ def main(argv: list[str] | None = None) -> int:
117
+ argv = argv if argv is not None else sys.argv[1:]
118
+ p = argparse.ArgumentParser(prog="flow-xray", description="flow-xray — execution graph debugger")
119
+ sub = p.add_subparsers(dest="command", required=True)
120
+
121
+ # -- gtype run -------------------------------------------------------
122
+ r = sub.add_parser("run", help="run a script with @trace, export HTML execution graph")
123
+ r.add_argument("script", help="path to Python file using @trace")
124
+ r.add_argument("--html", metavar="FILE.html", help="output path (default: <script>_trace.html)")
125
+ r.add_argument("--title", metavar="TEXT", help="title shown in the viewer")
126
+ r.set_defaults(func=cmd_run)
127
+
128
+ # -- gtype dot demo --------------------------------------------------
129
+ d = sub.add_parser("dot", help="scalar-Value DOT subcommands")
130
+ dot_sub = d.add_subparsers(dest="dot_cmd", required=True)
131
+ dd = dot_sub.add_parser("demo", help="built-in (a*b+c).relu().tanh() graph")
132
+ dd.add_argument("which", nargs="?", default="forward", choices=("forward", "backward"))
133
+ dd.add_argument("--output-dir", metavar="DIR")
134
+ dd.add_argument("--html", metavar="FILE.html")
135
+ dd.set_defaults(func=cmd_demo)
136
+
137
+ de = dot_sub.add_parser("export", help="run .py that defines `root` (Value)")
138
+ de.add_argument("script")
139
+ de.add_argument("-o", "--output", metavar="FILE.dot")
140
+ de.add_argument("--backward", action="store_true")
141
+ de.add_argument("--html", metavar="FILE.html")
142
+ de.set_defaults(func=cmd_export)
143
+
144
+ args = p.parse_args(argv)
145
+ return int(args.func(args))
146
+
147
+
148
+ if __name__ == "__main__":
149
+ raise SystemExit(main())
@@ -0,0 +1,119 @@
1
+ """
2
+ Stepping / snapshot debugger — delivery hooks on top of core + export.
3
+
4
+ Stepping model
5
+ --------------
6
+
7
+ 1. **Two-phase (global)** — ``TwoStepDebugger``:
8
+ - Phase A: export DOT with ``data`` only (``show_grad=False``).
9
+ - Phase B: after ``backward()`` (or ``backward_from``), export DOT with ``grad`` on each node.
10
+
11
+ 2. **Per-node backward** — ``iter_backward_steps`` / ``GraphDebugger.backward_trace``:
12
+ - Start from ``zero_grad``, set ``root.grad = 1``, walk reverse topological order,
13
+ calling ``_backward()`` on **one** node at a time. After each call, partial gradients
14
+ are filled only along edges that have already been processed; later steps add more.
15
+ - This is for *inspection* of the backward schedule, not a separate math semantics.
16
+
17
+ 3. **Named snapshots** — ``GraphDebugger.named_snapshots`` returns strings (DOT) keyed by
18
+ ``forward``, ``after_backward``, and ``backward_step_{k}`` (k = 0..n-1 in reverse topo).
19
+
20
+ **Rewind:** not supported; re-build the expression or keep a copy if you need replay.
21
+
22
+ Double backward is not supported as a product guarantee; use ``zero_grad`` + single ``backward``.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ from collections.abc import Iterator
28
+ from typing import Any
29
+
30
+ from flow_xray.export_dot import value_graph_to_dot
31
+ from flow_xray.ir import graph_to_ir
32
+ from flow_xray.value import Value, topological_order, zero_grad
33
+
34
+
35
+ class TwoStepDebugger:
36
+ """
37
+ Two global phases: forward DOT (data only), backward DOT (data + grad).
38
+
39
+ Usage::
40
+
41
+ dbg = TwoStepDebugger(y)
42
+ dot1 = dbg.step_forward_dot()
43
+ y.backward()
44
+ dot2 = dbg.step_backward_dot()
45
+ """
46
+
47
+ __slots__ = ("_root",)
48
+
49
+ def __init__(self, root: Value) -> None:
50
+ self._root = root
51
+
52
+ def step_forward_dot(self) -> str:
53
+ return value_graph_to_dot(self._root, show_grad=False)
54
+
55
+ def step_backward_dot(self) -> str:
56
+ return value_graph_to_dot(self._root, show_grad=True)
57
+
58
+
59
+ def iter_backward_steps(root: Value, *, seed: float = 1.0) -> Iterator[Value]:
60
+ """
61
+ Yield each ``Value`` in reverse topological order as its ``_backward`` is applied.
62
+
63
+ Mutates ``root`` subgraph gradients. Starts from a clean slate (zeros then seed).
64
+ """
65
+ zero_grad(root)
66
+ root.grad = float(seed)
67
+ for v in reversed(topological_order(root)):
68
+ v._backward()
69
+ yield v
70
+
71
+
72
+ class GraphDebugger:
73
+ """
74
+ Captures DOT (+ optional IR) for forward, each backward micro-step, and final state.
75
+ """
76
+
77
+ __slots__ = ("root",)
78
+
79
+ def __init__(self, root: Value) -> None:
80
+ self.root = root
81
+
82
+ def dot_forward(self) -> str:
83
+ return value_graph_to_dot(self.root, show_grad=False)
84
+
85
+ def dot_backward(self) -> str:
86
+ return value_graph_to_dot(self.root, show_grad=True)
87
+
88
+ def ir_forward(self) -> dict[str, Any]:
89
+ return graph_to_ir(self.root, include_grad=False)
90
+
91
+ def ir_backward(self) -> dict[str, Any]:
92
+ return graph_to_ir(self.root, include_grad=True)
93
+
94
+ def named_snapshots(
95
+ self,
96
+ *,
97
+ seed: float = 1.0,
98
+ include_step_dots: bool = True,
99
+ ) -> dict[str, str]:
100
+ """
101
+ Return DOT strings. Keys: ``forward``, optional ``backward_step_0``..``n-1``,
102
+ ``after_backward``.
103
+
104
+ Runs backward on ``self.root`` (mutating grads). Caller should not rely on pre-backward
105
+ grad state after this returns.
106
+ """
107
+ out: dict[str, str] = {"forward": self.dot_forward()}
108
+ zero_grad(self.root)
109
+ self.root.grad = float(seed)
110
+ rev = list(reversed(topological_order(self.root)))
111
+ if include_step_dots:
112
+ for k, v in enumerate(rev):
113
+ v._backward()
114
+ out[f"backward_step_{k}"] = value_graph_to_dot(self.root, show_grad=True)
115
+ else:
116
+ for v in rev:
117
+ v._backward()
118
+ out["after_backward"] = value_graph_to_dot(self.root, show_grad=True)
119
+ return out
@@ -0,0 +1,50 @@
1
+ """DOT export adapter — inspection layer; does not mutate the graph."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from flow_xray.value import Value, topological_order
6
+
7
+
8
+ def _dot_escape(s: str) -> str:
9
+ return s.replace("\\", "\\\\").replace('"', '\\"')
10
+
11
+
12
+ def value_graph_to_dot(root: Value, *, show_grad: bool = False) -> str:
13
+ """
14
+ Emit Graphviz DOT for the DAG rooted at ``root``.
15
+
16
+ Node DOT ids use ``Value.node_id`` (stable for object lifetime).
17
+ """
18
+ nodes = topological_order(root)
19
+ lines = [
20
+ "digraph G {",
21
+ ' rankdir="BT";',
22
+ " node [shape=box, fontname=Helvetica];",
23
+ " edge [fontname=Helvetica, fontsize=10];",
24
+ ]
25
+ for v in nodes:
26
+ nid = v.node_id
27
+ title = _dot_escape(v.display_name)
28
+ parts = [title]
29
+ if v._op:
30
+ parts.append(f"op: {_dot_escape(v._op)}")
31
+ parts.append(f"data: {v.data:g}")
32
+ if show_grad:
33
+ parts.append(f"grad: {v.grad:g}")
34
+ label = "\\n".join(parts)
35
+ lines.append(f' {nid} [label="{label}"];')
36
+
37
+ for v in nodes:
38
+ child_id = v.node_id
39
+ for p in v._prev:
40
+ pid = p.node_id
41
+ op_lbl = _dot_escape(v._op or "")
42
+ lines.append(f' {pid} -> {child_id} [label="{op_lbl}"];')
43
+
44
+ lines.append("}")
45
+ return "\n".join(lines)
46
+
47
+
48
+ def write_dot(path: str, dot: str) -> None:
49
+ with open(path, "w", encoding="utf-8") as f:
50
+ f.write(dot)