wizardflow 0.1.0__tar.gz → 0.3.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.
- {wizardflow-0.1.0 → wizardflow-0.3.0}/.gitignore +5 -3
- {wizardflow-0.1.0 → wizardflow-0.3.0}/CONTRIBUTING.md +7 -6
- wizardflow-0.3.0/PKG-INFO +530 -0
- wizardflow-0.3.0/README.md +483 -0
- {wizardflow-0.1.0 → wizardflow-0.3.0}/examples/multibranch.html +16 -16
- {wizardflow-0.1.0 → wizardflow-0.3.0}/examples/multibranch.md +15 -15
- {wizardflow-0.1.0 → wizardflow-0.3.0}/examples/quickstart.html +10 -10
- {wizardflow-0.1.0 → wizardflow-0.3.0}/examples/quickstart.md +9 -9
- {wizardflow-0.1.0 → wizardflow-0.3.0}/examples/quickstart.py +1 -1
- {wizardflow-0.1.0 → wizardflow-0.3.0}/pyproject.toml +1 -1
- {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/__init__.py +24 -0
- wizardflow-0.3.0/src/wizardflow/_ui/404.html +25 -0
- {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_ui/__next.__PAGE__.txt +3 -3
- {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_ui/__next._full.txt +4 -4
- wizardflow-0.3.0/src/wizardflow/_ui/__next._head.txt +6 -0
- {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_ui/__next._index.txt +1 -1
- {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_ui/__next._tree.txt +2 -2
- wizardflow-0.3.0/src/wizardflow/_ui/_next/static/chunks/2q_qu9w0vej6s.js +115 -0
- wizardflow-0.1.0/src/wizardflow/_ui/_next/static/chunks/0mz_v1wicwnmn.css → wizardflow-0.3.0/src/wizardflow/_ui/_next/static/chunks/3lt2ss7dftc2n.css +1 -1
- wizardflow-0.3.0/src/wizardflow/_ui/_next/static/media/icon.09qublg69ek7b.svg +34 -0
- {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_ui/_not-found/__next._full.txt +2 -2
- wizardflow-0.3.0/src/wizardflow/_ui/_not-found/__next._head.txt +6 -0
- {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_ui/_not-found/__next._index.txt +1 -1
- {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_ui/_not-found/__next._not-found/__PAGE__.txt +1 -1
- {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_ui/_not-found/__next._not-found.txt +1 -1
- {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_ui/_not-found/__next._tree.txt +1 -1
- wizardflow-0.3.0/src/wizardflow/_ui/_not-found.html +25 -0
- {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_ui/_not-found.txt +2 -2
- wizardflow-0.3.0/src/wizardflow/_ui/icon.svg +34 -0
- wizardflow-0.3.0/src/wizardflow/_ui/index.html +25 -0
- {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_ui/index.txt +4 -4
- {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_ui/sitemap.xml +1 -1
- {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/cli.py +70 -16
- {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/client.py +231 -78
- wizardflow-0.3.0/src/wizardflow/constants.py +55 -0
- {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/html.py +25 -0
- {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/markdown.py +23 -3
- wizardflow-0.3.0/src/wizardflow/reader.py +134 -0
- {wizardflow-0.1.0 → wizardflow-0.3.0}/tests/test_cli.py +69 -15
- {wizardflow-0.1.0 → wizardflow-0.3.0}/tests/test_html.py +30 -4
- {wizardflow-0.1.0 → wizardflow-0.3.0}/tests/test_markdown.py +40 -4
- wizardflow-0.3.0/tests/test_reader.py +154 -0
- {wizardflow-0.1.0 → wizardflow-0.3.0}/tests/test_trace.py +331 -32
- wizardflow-0.1.0/PKG-INFO +0 -340
- wizardflow-0.1.0/README.md +0 -293
- wizardflow-0.1.0/src/wizardflow/_ui/404.html +0 -25
- wizardflow-0.1.0/src/wizardflow/_ui/__next._head.txt +0 -6
- wizardflow-0.1.0/src/wizardflow/_ui/_next/static/chunks/0vq8h3vx9uqg5.js +0 -114
- wizardflow-0.1.0/src/wizardflow/_ui/_next/static/media/icon.3okpzkln1vq00.svg +0 -19
- wizardflow-0.1.0/src/wizardflow/_ui/_not-found/__next._head.txt +0 -6
- wizardflow-0.1.0/src/wizardflow/_ui/_not-found.html +0 -25
- wizardflow-0.1.0/src/wizardflow/_ui/icon.svg +0 -19
- wizardflow-0.1.0/src/wizardflow/_ui/index.html +0 -25
- wizardflow-0.1.0/src/wizardflow/constants.py +0 -44
- {wizardflow-0.1.0 → wizardflow-0.3.0}/LICENSE +0 -0
- {wizardflow-0.1.0 → wizardflow-0.3.0}/assets/demo.gif +0 -0
- {wizardflow-0.1.0 → wizardflow-0.3.0}/examples/data_types.py +0 -0
- {wizardflow-0.1.0 → wizardflow-0.3.0}/examples/multibranch.py +0 -0
- {wizardflow-0.1.0 → wizardflow-0.3.0}/scripts/build_ui.py +0 -0
- {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_render.py +0 -0
- {wizardflow-0.1.0/src/wizardflow/_ui/_next/static/oQsmH12Xmzb6Zw4-R3ghe → wizardflow-0.3.0/src/wizardflow/_ui/_next/static/AJjiS0WLQjNzSyuZBS0Ff}/_buildManifest.js +0 -0
- {wizardflow-0.1.0/src/wizardflow/_ui/_next/static/oQsmH12Xmzb6Zw4-R3ghe → wizardflow-0.3.0/src/wizardflow/_ui/_next/static/AJjiS0WLQjNzSyuZBS0Ff}/_clientMiddlewareManifest.js +0 -0
- {wizardflow-0.1.0/src/wizardflow/_ui/_next/static/oQsmH12Xmzb6Zw4-R3ghe → wizardflow-0.3.0/src/wizardflow/_ui/_next/static/AJjiS0WLQjNzSyuZBS0Ff}/_ssgManifest.js +0 -0
- {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_ui/_next/static/chunks/05-c3ty_6dwfk.js +0 -0
- {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_ui/_next/static/chunks/09s72r58ijxwx.js +0 -0
- {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_ui/_next/static/chunks/0cz1d0mv5g_q7.js +0 -0
- {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_ui/_next/static/chunks/0u1x9l49cgb30.js +0 -0
- {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_ui/_next/static/chunks/0ux_7aev0a2kt.js +0 -0
- {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_ui/_next/static/chunks/142r2alz4_x6t.js +0 -0
- {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_ui/_next/static/chunks/14mrh2-p_w84d.js +0 -0
- {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_ui/_next/static/chunks/18clj6xsyhc_j.css +0 -0
- {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_ui/_next/static/chunks/2-j1b74l3lxxu.css +0 -0
- {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_ui/_next/static/chunks/27jktro2p5rq9.js +0 -0
- {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_ui/_next/static/chunks/2gj5bmeov-r8f.js +0 -0
- {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_ui/_next/static/chunks/310vm2bl3xxpt.js +0 -0
- {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_ui/_next/static/chunks/3n7dm2ojtyzwn.js +0 -0
- {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_ui/_next/static/chunks/turbopack-2fblmukzx7kws.js +0 -0
- {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_ui/_next/static/media/4fa387ec64143e14-s.2tuy5pz7dlieh.woff2 +0 -0
- {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_ui/_next/static/media/53b9e256198e5412-s.390ncx5urfkfu.woff2 +0 -0
- {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_ui/_next/static/media/5ce348bf30bf5439-s.31988l_ccedte.woff2 +0 -0
- {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_ui/_next/static/media/6306c77e7c8268e4-s.2dbetqa9o8jxf.woff2 +0 -0
- {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_ui/_next/static/media/7178b3e590c64307-s.21jp631_3pja2.woff2 +0 -0
- {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_ui/_next/static/media/797e433ab948586e-s.p.0r6juujl39pe6.woff2 +0 -0
- {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_ui/_next/static/media/7d817b4c03b0c5f1-s.1uyisp29ctx0d.woff2 +0 -0
- {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_ui/_next/static/media/8a480f0b521d4e75-s.1qq4vpdcun5oj.woff2 +0 -0
- {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_ui/_next/static/media/bbc41e54d2fcbd21-s.1rgnod-3esatf.woff2 +0 -0
- {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_ui/_next/static/media/caa3a2e1cccd8315-s.p.0wgildi0cnwt9.woff2 +0 -0
- {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_ui/_next/static/media/fef07dbb0973bf53-s.3p2_lha1f2xer.woff2 +0 -0
- {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_ui/opengraph-image +0 -0
- {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/_ui/robots.txt +0 -0
- {wizardflow-0.1.0 → wizardflow-0.3.0}/src/wizardflow/py.typed +0 -0
- {wizardflow-0.1.0 → wizardflow-0.3.0}/tests/conftest.py +0 -0
|
@@ -10,7 +10,9 @@ venv/
|
|
|
10
10
|
# uv resolves this in CI (uv run / uv build); a zero-dependency library doesn't
|
|
11
11
|
# ship a lockfile, and tracking it would only sweep it into the sdist.
|
|
12
12
|
uv.lock
|
|
13
|
-
# Generated traces from the example scripts (e.g. examples/
|
|
14
|
-
# regenerated on each run, not tracked. Scoped to examples/ so it never
|
|
15
|
-
# the embedded UI bundle (src/wizardflow/_ui/), which must ship in the
|
|
13
|
+
# Generated traces from the example scripts (e.g. examples/quickstart__*.jsonl)
|
|
14
|
+
# — regenerated on each run, not tracked. Scoped to examples/ so it never
|
|
15
|
+
# touches the embedded UI bundle (src/wizardflow/_ui/), which must ship in the
|
|
16
|
+
# package. (*.json covers traces from pre-JSONL SDK versions.)
|
|
16
17
|
/examples/*.json
|
|
18
|
+
/examples/*.jsonl
|
|
@@ -23,15 +23,16 @@ pytest
|
|
|
23
23
|
```
|
|
24
24
|
|
|
25
25
|
The tests pin the emitted schema shape and the recording semantics (step
|
|
26
|
-
folding, completed-only persistence,
|
|
27
|
-
fast-fail, …).
|
|
26
|
+
folding, completed-only persistence, append-only durability, `id=` targeting,
|
|
27
|
+
unknown-node fast-fail, …).
|
|
28
28
|
|
|
29
29
|
## Schema contract
|
|
30
30
|
|
|
31
|
-
The
|
|
32
|
-
monorepo frontend — that TypeScript type
|
|
33
|
-
the
|
|
34
|
-
|
|
31
|
+
The JSONL this SDK serializes to is defined by `src/types/agenttrace.ts` in the
|
|
32
|
+
monorepo frontend — that TypeScript type (including the `header` / `message` /
|
|
33
|
+
`seal` record types) is the schema of record. Change it and the serializer
|
|
34
|
+
(`src/wizardflow/client.py`) plus the reader (`src/wizardflow/reader.py`) and
|
|
35
|
+
their tests must change in lockstep; they must not drift.
|
|
35
36
|
|
|
36
37
|
## Refreshing the bundled UI
|
|
37
38
|
|
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: wizardflow
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Python SDK for recording agent flows into the WizardFlow / AgentTrace file format.
|
|
5
|
+
Project-URL: Homepage, https://getwizardflow.com
|
|
6
|
+
Project-URL: Documentation, https://getwizardflow.com
|
|
7
|
+
Author: Leon Koch
|
|
8
|
+
License: MIT License
|
|
9
|
+
|
|
10
|
+
Copyright (c) 2026 Leon Koch
|
|
11
|
+
|
|
12
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
13
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
14
|
+
in the Software without restriction, including without limitation the rights
|
|
15
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
16
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
17
|
+
furnished to do so, subject to the following conditions:
|
|
18
|
+
|
|
19
|
+
The above copyright notice and this permission notice shall be included in all
|
|
20
|
+
copies or substantial portions of the Software.
|
|
21
|
+
|
|
22
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
23
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
24
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
25
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
26
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
27
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
28
|
+
SOFTWARE.
|
|
29
|
+
License-File: LICENSE
|
|
30
|
+
Keywords: agents,langgraph,llm,observability,tracing
|
|
31
|
+
Classifier: Development Status :: 3 - Alpha
|
|
32
|
+
Classifier: Intended Audience :: Developers
|
|
33
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
34
|
+
Classifier: Operating System :: OS Independent
|
|
35
|
+
Classifier: Programming Language :: Python :: 3
|
|
36
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
41
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
42
|
+
Classifier: Typing :: Typed
|
|
43
|
+
Requires-Python: >=3.9
|
|
44
|
+
Provides-Extra: dev
|
|
45
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
46
|
+
Description-Content-Type: text/markdown
|
|
47
|
+
|
|
48
|
+
# WizardFlow Python SDK
|
|
49
|
+
|
|
50
|
+
**A lightweight tracer for Python agents.** Drop three calls into your code —
|
|
51
|
+
`init`, `log`, `end_message` — and turn a messy multi-agent run into a portable
|
|
52
|
+
trace you can replay as an interactive graph or export to Markdown/HTML. Because
|
|
53
|
+
the trace is just a JSONL file, anyone can replay it: hand it to a teammate or PM
|
|
54
|
+
and they drop it into **[getwizardflow.com](https://getwizardflow.com)** in the
|
|
55
|
+
browser — no Python, no install. Lightweight by design: pure Python, **zero
|
|
56
|
+
runtime dependencies**, no daemon, no setup.
|
|
57
|
+
|
|
58
|
+

|
|
59
|
+
|
|
60
|
+
The file it produces is JSON Lines with a small, documented schema: line 1 is a
|
|
61
|
+
`header` record carrying the `graph { nodes, edges }`, then one `message`
|
|
62
|
+
record per line (`steps[] → payloads[] { label, value }`). Every line is plain
|
|
63
|
+
JSON — `head -1 run.jsonl | jq .graph` just works.
|
|
64
|
+
|
|
65
|
+
## Why WizardFlow?
|
|
66
|
+
|
|
67
|
+
Most agent tracing means an observability platform (LangSmith, Langfuse, …): a
|
|
68
|
+
server or SaaS account, an ingestion pipeline, auto-instrumentation, dashboards
|
|
69
|
+
behind a login. WizardFlow takes the opposite bet:
|
|
70
|
+
|
|
71
|
+
- **The trace is a file.** No server, no account, no daemon — your run becomes
|
|
72
|
+
a `.jsonl` you can commit, diff, grep, or attach to a bug report.
|
|
73
|
+
- **Anyone can replay it.** Hand the file to a teammate or PM and they drop it
|
|
74
|
+
into [getwizardflow.com](https://getwizardflow.com) — no Python, no install,
|
|
75
|
+
and nothing is uploaded (the viewer is fully client-side).
|
|
76
|
+
- **Three calls, zero dependencies.** The whole API is `init`, `log`,
|
|
77
|
+
`end_message` — pure Python that pulls nothing into your environment, and no
|
|
78
|
+
framework required (LangGraph support just calls `app.get_graph()` on
|
|
79
|
+
whatever you pass — langgraph itself is never imported).
|
|
80
|
+
- **Explicit by design.** You place every `log()` call yourself, so a trace
|
|
81
|
+
contains exactly what you logged — nothing else. No auto-instrumentation
|
|
82
|
+
capturing things behind your back, no surprise PII in the payloads, no
|
|
83
|
+
guessing why a span exists.
|
|
84
|
+
|
|
85
|
+
If you need fleet-wide monitoring, token-cost dashboards, or eval pipelines,
|
|
86
|
+
use an observability platform. WizardFlow is for **understanding one run** —
|
|
87
|
+
and being able to hand that run to anybody.
|
|
88
|
+
|
|
89
|
+
## Install
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
pip install wizardflow
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
No runtime dependencies. Developing the SDK itself? See
|
|
96
|
+
[CONTRIBUTING.md](https://github.com/lkleonk/wizardflow/blob/main/sdk/python/CONTRIBUTING.md).
|
|
97
|
+
|
|
98
|
+
## Quickstart
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
import uuid
|
|
102
|
+
|
|
103
|
+
import wizardflow
|
|
104
|
+
|
|
105
|
+
wizardflow.init(
|
|
106
|
+
output_dir="traces", # where trace files are written
|
|
107
|
+
file_prefix="run", # optional; defaults to "wizardflow"
|
|
108
|
+
description="A small router-based agent run.",
|
|
109
|
+
nodes=["user_input", "router", "planner", "tool_node", "final_response"],
|
|
110
|
+
edges=[("user_input", "router"), ("router", "planner")],
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# Every log names its message in the first argument:
|
|
114
|
+
# log(message_id, node, payload_label, payload_value)
|
|
115
|
+
msg_id = str(uuid.uuid4()) # one fresh id per message
|
|
116
|
+
wizardflow.log(msg_id, "router", "llm_input", prompt) # same node, two payloads ->
|
|
117
|
+
wizardflow.log(msg_id, "router", "llm_output", output) # folded into one step
|
|
118
|
+
wizardflow.log(msg_id, "tool_node") # visited, no payloads
|
|
119
|
+
wizardflow.end_message(msg_id) # -> writes the trace
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
There is **no `save()`** and no autosave: `log()` only accumulates in memory,
|
|
123
|
+
and `end_message(id)` is the one call that writes the trace. Writes happen at
|
|
124
|
+
message boundaries — one **append** per finished message, however many `log()`
|
|
125
|
+
calls it contains, so a write costs the same no matter how large the trace has
|
|
126
|
+
grown. `init()` returns a client and also stashes it as the module default, so
|
|
127
|
+
the bare `wizardflow.log(...)` form above works.
|
|
128
|
+
|
|
129
|
+
**Concurrency-safe.** Multi-agent setups end messages from many threads/tasks at
|
|
130
|
+
once; an internal lock serializes the appends so the shared part file is never
|
|
131
|
+
corrupted and no message is lost or duplicated. Every ended message is durable
|
|
132
|
+
on disk the moment `end_message` returns — a crash can at worst tear the line
|
|
133
|
+
being appended, and readers drop a torn final line and load everything before
|
|
134
|
+
it.
|
|
135
|
+
|
|
136
|
+
## Targeting a message
|
|
137
|
+
|
|
138
|
+
The first argument to `log` is the message id, so overlapping messages never
|
|
139
|
+
collide — interleave them freely (as concurrent agents do) and each step routes
|
|
140
|
+
to the right message:
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
# log(message_id, node, payload_label, payload_value)
|
|
144
|
+
wizardflow.log("msg-1", "classifier", "input", text_a)
|
|
145
|
+
wizardflow.log("msg-2", "classifier", "input", text_b) # a different message
|
|
146
|
+
wizardflow.log("msg-1", "generator", "output", answer_a)
|
|
147
|
+
wizardflow.end_message("msg-1") # writes msg-1
|
|
148
|
+
wizardflow.end_message("msg-2") # writes msg-2
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Pass the **string `id`**, never a handle object — safe to hand to a callback or
|
|
152
|
+
across threads. A message is created on first reference and finalized by
|
|
153
|
+
`end_message`; `end_message(id, title="...")` optionally gives it a human title.
|
|
154
|
+
|
|
155
|
+
### Choosing message ids
|
|
156
|
+
|
|
157
|
+
A message is **one unit of work** — one user turn, one run through the graph.
|
|
158
|
+
Its id only has to be unique within the run, and it **cannot be reused** once
|
|
159
|
+
`end_message` has been called on it. The simplest safe choice is a fresh UUID
|
|
160
|
+
per message:
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
import uuid
|
|
164
|
+
|
|
165
|
+
msg_id = str(uuid.uuid4())
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Do **not** use a session id, user id, or thread id as the message id. Those
|
|
169
|
+
identify a *conversation*, and a conversation contains many messages — after
|
|
170
|
+
the first `end_message(session_id)`, every further `log(session_id, ...)` in
|
|
171
|
+
that session raises, because that "message" has already ended. If you want the
|
|
172
|
+
session visible in the trace, put it in `init(meta=...)` or in the message
|
|
173
|
+
title (`end_message(msg_id, title=...)`) — never in the id. Ids are opaque:
|
|
174
|
+
the viewer only displays and groups by them, so encoding meaning into them
|
|
175
|
+
buys nothing. Hardcoded ids like `"msg-1"` are fine for demo scripts; `uuid4`
|
|
176
|
+
is the right default for real applications.
|
|
177
|
+
|
|
178
|
+
## One flow, or several at once
|
|
179
|
+
|
|
180
|
+
`init()` installs the client it returns as the module default. With a single
|
|
181
|
+
flow — the common case — **ignore the return value** and call
|
|
182
|
+
`wizardflow.log(...)` / `wizardflow.end_message(...)` directly: they work from
|
|
183
|
+
any module, with no handle to thread through your code.
|
|
184
|
+
|
|
185
|
+
Running several agent flows side by side (say, a doctor flow and a patient
|
|
186
|
+
flow) means several traces, and each needs its own instance. Keep the returned
|
|
187
|
+
client per flow and log through it — and name it **`tracer`**, not `client`,
|
|
188
|
+
since `client` in an agent codebase is almost always the LLM client:
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
doctor_tracer = wizardflow.init(file_prefix="doctor", nodes=[...], edges=[...])
|
|
192
|
+
patient_tracer = wizardflow.init(file_prefix="patient", nodes=[...], edges=[...])
|
|
193
|
+
|
|
194
|
+
doctor_tracer.log(doc_msg, "diagnose", "llm_input", prompt)
|
|
195
|
+
patient_tracer.log(pat_msg, "intake", "llm_input", prompt)
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
With more than one tracer, don't mix in the bare module-level calls:
|
|
199
|
+
`wizardflow.log(...)` always targets the most recently initialized tracer.
|
|
200
|
+
|
|
201
|
+
## Node descriptions, labels & colors
|
|
202
|
+
|
|
203
|
+
Nodes can carry a short human description of what they do. In the viewer it
|
|
204
|
+
stays out of the way: when a node is selected, a small info icon appears next
|
|
205
|
+
to its name in the inspector — click it to read the description. Nodes without
|
|
206
|
+
one show no icon, and `wizardflow md` / `wizardflow html` list the described
|
|
207
|
+
nodes under the graph.
|
|
208
|
+
|
|
209
|
+
```python
|
|
210
|
+
wizardflow.init(
|
|
211
|
+
nodes=["router", "retriever", "generator"],
|
|
212
|
+
edges=[("router", "retriever"), ("retriever", "generator")],
|
|
213
|
+
node_descriptions={
|
|
214
|
+
"router": "Chooses the next step.",
|
|
215
|
+
"retriever": "Fetches relevant documents.",
|
|
216
|
+
"generator": "Writes the final answer.",
|
|
217
|
+
},
|
|
218
|
+
)
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Two sibling kwargs work the same way: `node_labels` maps ids to the display
|
|
222
|
+
name the viewer shows instead of the raw id (especially useful with
|
|
223
|
+
`init_from_langgraph`, where the extracted ids are your function names), and
|
|
224
|
+
`node_colors` sets accent colors. All three are validated against the declared
|
|
225
|
+
nodes: an unknown id raises a `WizardFlowError` at `init()` time so typos fail
|
|
226
|
+
fast; with `silent=True` it is logged as a warning on the `wizardflow` logger
|
|
227
|
+
and skipped instead. All three also work on `init_from_langgraph`, where they
|
|
228
|
+
attach to the extracted node ids — `log()` always targets the **id**, never
|
|
229
|
+
the label.
|
|
230
|
+
|
|
231
|
+
## LangGraph: automatic topology
|
|
232
|
+
|
|
233
|
+
Instead of listing `nodes`/`edges` by hand, read them straight from a compiled
|
|
234
|
+
LangGraph app:
|
|
235
|
+
|
|
236
|
+
```python
|
|
237
|
+
app = workflow.compile(checkpointer=memory)
|
|
238
|
+
|
|
239
|
+
wizardflow.init_from_langgraph(app, output_dir="traces", file_prefix="trace")
|
|
240
|
+
|
|
241
|
+
wizardflow.log("msg-1", "planner", "Input", state) # runtime logging unchanged
|
|
242
|
+
wizardflow.end_message("msg-1")
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
You can keep the extracted LangGraph topology and still choose node accent
|
|
246
|
+
colors for important nodes:
|
|
247
|
+
|
|
248
|
+
```python
|
|
249
|
+
wizardflow.init_from_langgraph(
|
|
250
|
+
app,
|
|
251
|
+
output_dir="traces",
|
|
252
|
+
file_prefix="trace",
|
|
253
|
+
node_colors={
|
|
254
|
+
"router": "#A78BFA",
|
|
255
|
+
"retriever": "#22D3EE",
|
|
256
|
+
"generator": "#60A5FA",
|
|
257
|
+
},
|
|
258
|
+
)
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
By default, a color key that does not match an extracted node id raises a
|
|
262
|
+
`WizardFlowError` so typos fail fast. With `silent=True`, unknown color keys
|
|
263
|
+
are skipped and logged as a warning instead.
|
|
264
|
+
|
|
265
|
+
It extracts node ids (keeping `__start__` / `__end__`) and directed edges, and
|
|
266
|
+
marks runtime branches with `"conditional": true` (deterministic and parallel
|
|
267
|
+
fan-out edges stay plain):
|
|
268
|
+
|
|
269
|
+
```json
|
|
270
|
+
{
|
|
271
|
+
"edges": [
|
|
272
|
+
{ "source": "__start__", "target": "router" },
|
|
273
|
+
{ "source": "router", "target": "planner", "conditional": true },
|
|
274
|
+
{ "source": "planner", "target": "final_response" }
|
|
275
|
+
]
|
|
276
|
+
}
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
LangGraph is **not** a dependency — extraction is duck-typed on `app.get_graph()`.
|
|
280
|
+
A non-LangGraph object raises `LangGraphExtractionError`; missing conditional
|
|
281
|
+
metadata never fails extraction (the edge is just emitted plain). Call it
|
|
282
|
+
**after `compile()`**, when the topology actually exists.
|
|
283
|
+
|
|
284
|
+
## API
|
|
285
|
+
|
|
286
|
+
- `init(output_dir=, file_prefix="wizardflow", name=, description=, nodes=, edges=, node_labels=, node_colors=, node_descriptions=, meta=, silent=False, max_bytes=16_000_000, max_messages=2_000) -> Client`
|
|
287
|
+
- `description` lands in `meta.description` (matches the schema field).
|
|
288
|
+
- `nodes=` enables fast-fail: `log()` to an undeclared node raises
|
|
289
|
+
`UnknownNodeError` immediately (unless silenced).
|
|
290
|
+
- `node_labels` / `node_colors` / `node_descriptions` map declared node ids
|
|
291
|
+
to a display name / CSS color / short description; an unknown id raises
|
|
292
|
+
unless silenced (then it's logged as a warning and skipped).
|
|
293
|
+
- `output_dir` is optional; omitted, traces are written in cwd.
|
|
294
|
+
- `file_prefix` is optional; omitted, filenames start with `wizardflow`.
|
|
295
|
+
- `max_bytes` / `max_messages` cap each part file before rotation (see below).
|
|
296
|
+
- `init_from_langgraph(app, output_dir=, file_prefix="wizardflow", name=, description=, meta=, node_labels=, node_colors=, node_descriptions=, silent=False, max_bytes=..., max_messages=...) -> Client`
|
|
297
|
+
- same as `init`, but `nodes`/`edges` come from `app.get_graph()`.
|
|
298
|
+
- `node_labels` renames extracted ids for display (they're your function
|
|
299
|
+
names); `node_colors` maps them to CSS colors such as `"#A78BFA"`;
|
|
300
|
+
`node_descriptions` to short descriptions.
|
|
301
|
+
- `log(id, node, label=None, content=None)` — the first positional is the
|
|
302
|
+
**message id**, the second is the node. With `label`/`content` it records a
|
|
303
|
+
payload; bare `log(id, "node")` records a visit with no payloads. The message
|
|
304
|
+
is created on first reference; this only accumulates in memory.
|
|
305
|
+
- `end_message(id, title=None)` — finalize a message and append it to the
|
|
306
|
+
trace; returns the current trace path. The **only** call that touches disk.
|
|
307
|
+
Optional `title` sets the message's human title. Idempotent.
|
|
308
|
+
- `reinit(name=None, description=None, meta=None)` — start a **new trace
|
|
309
|
+
file** (fresh timestamped name), keeping the graph and output configuration.
|
|
310
|
+
Use it at natural boundaries of a long-lived process — a new user session, a
|
|
311
|
+
new day. The old file is left as-is (**no seal**: it's a finished run, not a
|
|
312
|
+
rotated part). Open messages carry over and are written wherever they end;
|
|
313
|
+
completed message ids become reusable. `name`/`description`/`meta` replace
|
|
314
|
+
the current values when given. Returns the new trace path.
|
|
315
|
+
- `Client.current_path` — the trace file currently being written.
|
|
316
|
+
- `to_dict()` / `to_json()` — inspect the active part (completed messages),
|
|
317
|
+
assembled into one `AgentTraceFile` object.
|
|
318
|
+
|
|
319
|
+
### The file format
|
|
320
|
+
|
|
321
|
+
A trace part is JSON Lines: one JSON object per line, each with a `type`:
|
|
322
|
+
|
|
323
|
+
```jsonl
|
|
324
|
+
{"type":"header","version":"0.2","name":"run","meta":{...},"graph":{"nodes":[...],"edges":[...]}}
|
|
325
|
+
{"type":"message","id":"msg-1","label":"First question","steps":[...]}
|
|
326
|
+
{"type":"message","id":"msg-2","steps":[...]}
|
|
327
|
+
{"type":"seal","nextPart":"run__...__part2.jsonl"}
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
- **`header`** (always line 1) — everything about the run except the messages.
|
|
331
|
+
- **`message`** — one completed message, appended by `end_message`.
|
|
332
|
+
- **`seal`** — only on a part that rotated away; its presence means "this part
|
|
333
|
+
is complete, continue at `nextPart`". The active part has no seal.
|
|
334
|
+
|
|
335
|
+
Readers skip records with an unknown `type` (forward compat) and drop an
|
|
336
|
+
unparseable final line (a crash mid-append leaves a torn tail; everything
|
|
337
|
+
before it is intact). Only **completed** messages are written; an in-progress
|
|
338
|
+
message lives in memory until it ends.
|
|
339
|
+
|
|
340
|
+
> Early SDK versions (0.1) wrote a trace as one single-document `.json` file
|
|
341
|
+
> instead. Newer versions write JSONL only, but every reader — the CLI
|
|
342
|
+
> subcommands and the web UI at getwizardflow.com — still opens both formats.
|
|
343
|
+
|
|
344
|
+
### How saving works
|
|
345
|
+
|
|
346
|
+
`end_message` **appends one line** to the active part — O(1) no matter how
|
|
347
|
+
large the part already is, and the moment it returns, that message is durable
|
|
348
|
+
on disk. Nothing is ever rewritten.
|
|
349
|
+
|
|
350
|
+
### Rotation (no single huge file)
|
|
351
|
+
|
|
352
|
+
There's no natural "end" to a chatbot trace, so the SDK caps part size instead.
|
|
353
|
+
The cap exists for the *reader*: a part is what you drop into the viewer, and an
|
|
354
|
+
oversized file makes the browser tab sluggish. Each run writes a timestamped
|
|
355
|
+
entry file whose name carries the run-start time:
|
|
356
|
+
|
|
357
|
+
```
|
|
358
|
+
wizardflow__2026-06-08T16-29-09-123Z.jsonl
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
The name's `wizardflow` is the `file_prefix`; the timestamp is captured when
|
|
362
|
+
`init()` creates the client. If the next message would push the active part past
|
|
363
|
+
`max_bytes` (16 MB by default) — or past `max_messages` (2,000 by default; many
|
|
364
|
+
tiny messages strain the viewer before many bytes do) — the part is sealed and
|
|
365
|
+
the message starts a fresh one:
|
|
366
|
+
|
|
367
|
+
```
|
|
368
|
+
wizardflow__2026-06-08T16-29-09-123Z.jsonl
|
|
369
|
+
wizardflow__2026-06-08T16-29-09-123Z__part2.jsonl
|
|
370
|
+
wizardflow__2026-06-08T16-29-09-123Z__part3.jsonl
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
Rotation only ever happens at a **message boundary**, never mid-message (a lone
|
|
374
|
+
message larger than the cap gets its own oversized part). `max_bytes` is
|
|
375
|
+
clamped to a hard ceiling (64 MB) — past that, parsed-object inflation makes
|
|
376
|
+
the viewer slow on ordinary hardware. Each part is **self-contained** (full
|
|
377
|
+
graph in its header + its slice of messages), chained backward via the header's
|
|
378
|
+
`meta.prevPart` and forward via the seal record's `nextPart`.
|
|
379
|
+
|
|
380
|
+
A single-part trace stays clean (no `part` metadata, no seal). There's no
|
|
381
|
+
`partCount` — the total is genuinely unknown while a continuous run is still
|
|
382
|
+
logging; follow the seal records to walk to the end. Since names are
|
|
383
|
+
timestamped, read the real file back from the client's `current_path` (also the
|
|
384
|
+
path `end_message` returns).
|
|
385
|
+
|
|
386
|
+
### Logging
|
|
387
|
+
|
|
388
|
+
Rotation emits an `INFO` notice on the `wizardflow` logger. Following library
|
|
389
|
+
convention, the SDK attaches a `NullHandler` and configures nothing — you see
|
|
390
|
+
nothing unless you opt in:
|
|
391
|
+
|
|
392
|
+
```python
|
|
393
|
+
import logging
|
|
394
|
+
logging.getLogger("wizardflow").setLevel(logging.INFO)
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
This is separate from `silent=` (which governs raise-vs-swallow for *errors*).
|
|
398
|
+
|
|
399
|
+
### Step folding
|
|
400
|
+
|
|
401
|
+
Consecutive `log()` calls to the **same** node within a message fold into a
|
|
402
|
+
single step with multiple payloads (e.g. a router step carrying both
|
|
403
|
+
`llm_input` and `llm_output`). A `log()` to a different node starts a new step.
|
|
404
|
+
|
|
405
|
+
## What to log (conventions)
|
|
406
|
+
|
|
407
|
+
Payload labels are free-form strings — the schema reserves none of them. These
|
|
408
|
+
conventions just keep traces consistent and readable:
|
|
409
|
+
|
|
410
|
+
- **LLM-backed nodes** — log the exact prompt as `llm_input` and the raw
|
|
411
|
+
completion as `llm_output`. When a run goes wrong, the trace then shows
|
|
412
|
+
precisely what the model saw and what it said — most of what you need to
|
|
413
|
+
debug an agent — and step folding merges the pair into one step.
|
|
414
|
+
- **Everything else** — log the node's real domain data under honest labels: a
|
|
415
|
+
retriever logs its `query` and `results`, a tool node `tool_input` and
|
|
416
|
+
`tool_output`, a deterministic router its `decision`. Don't force
|
|
417
|
+
`llm_input`/`llm_output` onto nodes that never call a model.
|
|
418
|
+
|
|
419
|
+
## Examples
|
|
420
|
+
|
|
421
|
+
Fuller runnable examples live in
|
|
422
|
+
[`examples/`](https://github.com/lkleonk/wizardflow/tree/main/sdk/python/examples):
|
|
423
|
+
|
|
424
|
+
- **`quickstart.py`** — a small linear agent; two messages, the second finalized
|
|
425
|
+
with an `end_message(..., title=...)` title.
|
|
426
|
+
- **`multibranch.py`** — `router` fans out into a planner/tool path and a
|
|
427
|
+
retriever path that rejoin at `generator`. Two messages take different
|
|
428
|
+
branches, so each logs only the nodes it actually visited.
|
|
429
|
+
|
|
430
|
+
## CLI
|
|
431
|
+
|
|
432
|
+
The `wizardflow` command has four subcommands: `ui`, `md`, `html`, and `json`.
|
|
433
|
+
Every one takes the trace file as a positional argument **or** via `--path`
|
|
434
|
+
(pass one, not both):
|
|
435
|
+
|
|
436
|
+
```bash
|
|
437
|
+
wizardflow ui run.jsonl
|
|
438
|
+
wizardflow md run.jsonl
|
|
439
|
+
wizardflow html run.jsonl
|
|
440
|
+
wizardflow json run.jsonl
|
|
441
|
+
# --path is equivalent everywhere:
|
|
442
|
+
wizardflow ui --path run.jsonl
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
The SDK only ever **writes** JSONL, but these commands **read** either framing —
|
|
446
|
+
a `.jsonl` part or a single-document `.json` (what `wizardflow json` emits) — so
|
|
447
|
+
they stay at parity with the web viewer, which also accepts both.
|
|
448
|
+
|
|
449
|
+
### `wizardflow ui` — local viewer
|
|
450
|
+
|
|
451
|
+
```bash
|
|
452
|
+
wizardflow ui run.jsonl [--host 127.0.0.1] [--port 0] [--no-open]
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
Binds a stdlib HTTP server, serves the static WizardFlow UI bundled in the SDK
|
|
456
|
+
package, and opens the selected trace in your browser (the JSONL is assembled
|
|
457
|
+
server-side and served to the UI as one JSON document, re-read on refresh — so
|
|
458
|
+
you can watch a still-running trace grow).
|
|
459
|
+
|
|
460
|
+
| flag | default | meaning |
|
|
461
|
+
| --- | --- | --- |
|
|
462
|
+
| `--host` | `127.0.0.1` | interface to bind |
|
|
463
|
+
| `--port` | `0` | port to bind; `0` asks the OS for a free port |
|
|
464
|
+
| `--no-open` | off | print the local URL instead of launching a browser |
|
|
465
|
+
|
|
466
|
+
### `wizardflow md` — export to Markdown
|
|
467
|
+
|
|
468
|
+
```bash
|
|
469
|
+
wizardflow md run.jsonl # -> stdout
|
|
470
|
+
wizardflow md run.jsonl -o run.md # -> file
|
|
471
|
+
wizardflow md run.jsonl --no-mermaid # omit the graph diagram
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
Renders the full trace as Markdown: a metadata table, a Mermaid `flowchart` of
|
|
475
|
+
the graph, then each message's steps and payloads (scalars inline; multi-line
|
|
476
|
+
strings and dict/list values in fenced code blocks; conditional edges drawn
|
|
477
|
+
dashed).
|
|
478
|
+
|
|
479
|
+
| flag | default | meaning |
|
|
480
|
+
| --- | --- | --- |
|
|
481
|
+
| `-o`, `--output` | — | write to this file instead of stdout |
|
|
482
|
+
| `--mermaid` | on | include the Mermaid graph diagram |
|
|
483
|
+
| `--no-mermaid` | — | omit the Mermaid graph diagram |
|
|
484
|
+
|
|
485
|
+
### `wizardflow html` — export to HTML
|
|
486
|
+
|
|
487
|
+
```bash
|
|
488
|
+
wizardflow html run.jsonl # -> stdout
|
|
489
|
+
wizardflow html run.jsonl -o run.html # -> file
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
Emits a single self-contained document — inline CSS, **no JavaScript, no
|
|
493
|
+
external assets** — that opens offline in any browser and follows your OS
|
|
494
|
+
light/dark mode. Messages-only by design: no graph/Mermaid (use `md` for that).
|
|
495
|
+
|
|
496
|
+
| flag | default | meaning |
|
|
497
|
+
| --- | --- | --- |
|
|
498
|
+
| `-o`, `--output` | — | write to this file instead of stdout |
|
|
499
|
+
|
|
500
|
+
### `wizardflow json` — assemble to one pretty-printed JSON document
|
|
501
|
+
|
|
502
|
+
```bash
|
|
503
|
+
wizardflow json run.jsonl # -> stdout
|
|
504
|
+
wizardflow json run.jsonl -o run.json # -> file
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
The inverse of how the SDK writes. JSONL is built for appending, so the raw
|
|
508
|
+
file is one long line per record and not much fun to read. This assembles the
|
|
509
|
+
part — header plus all its message records, with the seal's `nextPart` folded
|
|
510
|
+
into `meta` — into the indented, single-document `AgentTraceFile` shape, for
|
|
511
|
+
eyeballing, diffing in review, or handing someone a canonical JSON. (The web UI
|
|
512
|
+
reads that single-document form too.)
|
|
513
|
+
|
|
514
|
+
| flag | default | meaning |
|
|
515
|
+
| --- | --- | --- |
|
|
516
|
+
| `-o`, `--output` | — | write to this file instead of stdout |
|
|
517
|
+
|
|
518
|
+
Already have `jq`? You don't need this for a quick look — `jq . run.jsonl`
|
|
519
|
+
pretty-prints every record, and `jq 'select(.type=="message")' run.jsonl` just
|
|
520
|
+
the messages. `wizardflow json` differs in that it *assembles* the part into one
|
|
521
|
+
document (and walks no external tool).
|
|
522
|
+
|
|
523
|
+
Rendered samples (`*.md`, `*.html`) live in the [repo's
|
|
524
|
+
`examples/`](https://github.com/lkleonk/wizardflow/tree/main/sdk/python/examples).
|
|
525
|
+
|
|
526
|
+
## Status / not yet
|
|
527
|
+
|
|
528
|
+
- **Timestamps** are wall-clock at log time — fine for slow/live runs, wrong if
|
|
529
|
+
steps fire faster than ms resolution or you import after the fact. (Open
|
|
530
|
+
design item: explicit per-step timestamps.)
|