flow-xray 0.2.0__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.
- flow_xray-0.2.0/.gitignore +17 -0
- flow_xray-0.2.0/LICENSE +21 -0
- flow_xray-0.2.0/PKG-INFO +147 -0
- flow_xray-0.2.0/README.md +121 -0
- flow_xray-0.2.0/flow_xray/__init__.py +38 -0
- flow_xray-0.2.0/flow_xray/__main__.py +5 -0
- flow_xray-0.2.0/flow_xray/cli.py +149 -0
- flow_xray-0.2.0/flow_xray/debugger.py +119 -0
- flow_xray-0.2.0/flow_xray/export_dot.py +50 -0
- flow_xray-0.2.0/flow_xray/export_html.py +219 -0
- flow_xray-0.2.0/flow_xray/ir.py +39 -0
- flow_xray-0.2.0/flow_xray/static/viewer.html +52 -0
- flow_xray-0.2.0/flow_xray/trace.py +354 -0
- flow_xray-0.2.0/flow_xray/value.py +133 -0
- flow_xray-0.2.0/flow_xray/viewer.py +69 -0
- flow_xray-0.2.0/pyproject.toml +48 -0
|
@@ -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
|
flow_xray-0.2.0/LICENSE
ADDED
|
@@ -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.
|
flow_xray-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: flow-xray
|
|
3
|
+
Version: 0.2.0
|
|
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="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" 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="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="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" 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="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.0"
|
|
@@ -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)
|