wizardflow 0.2.0__tar.gz → 0.4.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.
Files changed (96) hide show
  1. {wizardflow-0.2.0 → wizardflow-0.4.0}/PKG-INFO +135 -20
  2. {wizardflow-0.2.0 → wizardflow-0.4.0}/README.md +134 -19
  3. {wizardflow-0.2.0 → wizardflow-0.4.0}/examples/multibranch.html +28 -11
  4. {wizardflow-0.2.0 → wizardflow-0.4.0}/examples/multibranch.md +21 -11
  5. {wizardflow-0.2.0 → wizardflow-0.4.0}/examples/multibranch.py +23 -2
  6. {wizardflow-0.2.0 → wizardflow-0.4.0}/examples/quickstart.html +20 -6
  7. {wizardflow-0.2.0 → wizardflow-0.4.0}/examples/quickstart.md +12 -6
  8. {wizardflow-0.2.0 → wizardflow-0.4.0}/examples/quickstart.py +17 -2
  9. {wizardflow-0.2.0 → wizardflow-0.4.0}/pyproject.toml +1 -1
  10. {wizardflow-0.2.0 → wizardflow-0.4.0}/src/wizardflow/__init__.py +26 -2
  11. {wizardflow-0.2.0 → wizardflow-0.4.0}/src/wizardflow/_render.py +9 -0
  12. wizardflow-0.4.0/src/wizardflow/_ui/404.html +7 -0
  13. {wizardflow-0.2.0 → wizardflow-0.4.0}/src/wizardflow/_ui/__next.__PAGE__.txt +3 -3
  14. wizardflow-0.4.0/src/wizardflow/_ui/__next._full.txt +23 -0
  15. wizardflow-0.4.0/src/wizardflow/_ui/__next._head.txt +6 -0
  16. wizardflow-0.4.0/src/wizardflow/_ui/__next._index.txt +7 -0
  17. {wizardflow-0.2.0 → wizardflow-0.4.0}/src/wizardflow/_ui/__next._tree.txt +2 -2
  18. wizardflow-0.2.0/src/wizardflow/_ui/_next/static/chunks/0ux_7aev0a2kt.js → wizardflow-0.4.0/src/wizardflow/_ui/_next/static/chunks/07ur0v9hb4ida.js +3 -3
  19. wizardflow-0.2.0/src/wizardflow/_ui/_next/static/chunks/0u1x9l49cgb30.js → wizardflow-0.4.0/src/wizardflow/_ui/_next/static/chunks/1_way7swvumpv.js +1 -1
  20. wizardflow-0.4.0/src/wizardflow/_ui/_next/static/chunks/221oy04d3f6t2.js +7 -0
  21. wizardflow-0.4.0/src/wizardflow/_ui/_next/static/chunks/35wrz4n1txs_o.js +265 -0
  22. wizardflow-0.2.0/src/wizardflow/_ui/_next/static/chunks/0mz_v1wicwnmn.css → wizardflow-0.4.0/src/wizardflow/_ui/_next/static/chunks/3lt2ss7dftc2n.css +1 -1
  23. wizardflow-0.4.0/src/wizardflow/_ui/_next/static/media/icon.09qublg69ek7b.svg +34 -0
  24. wizardflow-0.4.0/src/wizardflow/_ui/_not-found/__next._full.txt +18 -0
  25. wizardflow-0.4.0/src/wizardflow/_ui/_not-found/__next._head.txt +6 -0
  26. wizardflow-0.4.0/src/wizardflow/_ui/_not-found/__next._index.txt +7 -0
  27. {wizardflow-0.2.0 → wizardflow-0.4.0}/src/wizardflow/_ui/_not-found/__next._not-found/__PAGE__.txt +1 -1
  28. {wizardflow-0.2.0 → wizardflow-0.4.0}/src/wizardflow/_ui/_not-found/__next._not-found.txt +1 -1
  29. {wizardflow-0.2.0 → wizardflow-0.4.0}/src/wizardflow/_ui/_not-found/__next._tree.txt +1 -1
  30. wizardflow-0.4.0/src/wizardflow/_ui/_not-found.html +7 -0
  31. wizardflow-0.4.0/src/wizardflow/_ui/_not-found.txt +18 -0
  32. wizardflow-0.4.0/src/wizardflow/_ui/icon.svg +34 -0
  33. wizardflow-0.4.0/src/wizardflow/_ui/index.html +7 -0
  34. wizardflow-0.4.0/src/wizardflow/_ui/index.txt +23 -0
  35. {wizardflow-0.2.0 → wizardflow-0.4.0}/src/wizardflow/_ui/sitemap.xml +1 -1
  36. {wizardflow-0.2.0 → wizardflow-0.4.0}/src/wizardflow/client.py +143 -19
  37. {wizardflow-0.2.0 → wizardflow-0.4.0}/src/wizardflow/html.py +34 -1
  38. {wizardflow-0.2.0 → wizardflow-0.4.0}/src/wizardflow/markdown.py +29 -4
  39. {wizardflow-0.2.0 → wizardflow-0.4.0}/tests/test_html.py +40 -0
  40. {wizardflow-0.2.0 → wizardflow-0.4.0}/tests/test_markdown.py +48 -0
  41. {wizardflow-0.2.0 → wizardflow-0.4.0}/tests/test_reader.py +10 -0
  42. {wizardflow-0.2.0 → wizardflow-0.4.0}/tests/test_trace.py +287 -0
  43. wizardflow-0.2.0/src/wizardflow/_ui/404.html +0 -25
  44. wizardflow-0.2.0/src/wizardflow/_ui/__next._full.txt +0 -22
  45. wizardflow-0.2.0/src/wizardflow/_ui/__next._head.txt +0 -6
  46. wizardflow-0.2.0/src/wizardflow/_ui/__next._index.txt +0 -6
  47. wizardflow-0.2.0/src/wizardflow/_ui/_next/static/chunks/142r2alz4_x6t.js +0 -1
  48. wizardflow-0.2.0/src/wizardflow/_ui/_next/static/chunks/4158u-399exxn.js +0 -115
  49. wizardflow-0.2.0/src/wizardflow/_ui/_next/static/media/icon.3okpzkln1vq00.svg +0 -19
  50. wizardflow-0.2.0/src/wizardflow/_ui/_not-found/__next._full.txt +0 -17
  51. wizardflow-0.2.0/src/wizardflow/_ui/_not-found/__next._head.txt +0 -6
  52. wizardflow-0.2.0/src/wizardflow/_ui/_not-found/__next._index.txt +0 -6
  53. wizardflow-0.2.0/src/wizardflow/_ui/_not-found.html +0 -25
  54. wizardflow-0.2.0/src/wizardflow/_ui/_not-found.txt +0 -17
  55. wizardflow-0.2.0/src/wizardflow/_ui/icon.svg +0 -19
  56. wizardflow-0.2.0/src/wizardflow/_ui/index.html +0 -25
  57. wizardflow-0.2.0/src/wizardflow/_ui/index.txt +0 -22
  58. {wizardflow-0.2.0 → wizardflow-0.4.0}/.gitignore +0 -0
  59. {wizardflow-0.2.0 → wizardflow-0.4.0}/CONTRIBUTING.md +0 -0
  60. {wizardflow-0.2.0 → wizardflow-0.4.0}/LICENSE +0 -0
  61. {wizardflow-0.2.0 → wizardflow-0.4.0}/assets/demo.gif +0 -0
  62. {wizardflow-0.2.0 → wizardflow-0.4.0}/examples/data_types.py +0 -0
  63. {wizardflow-0.2.0 → wizardflow-0.4.0}/scripts/build_ui.py +0 -0
  64. {wizardflow-0.2.0 → wizardflow-0.4.0}/src/wizardflow/_ui/_next/static/chunks/05-c3ty_6dwfk.js +0 -0
  65. {wizardflow-0.2.0 → wizardflow-0.4.0}/src/wizardflow/_ui/_next/static/chunks/09s72r58ijxwx.js +0 -0
  66. {wizardflow-0.2.0 → wizardflow-0.4.0}/src/wizardflow/_ui/_next/static/chunks/0cz1d0mv5g_q7.js +0 -0
  67. {wizardflow-0.2.0 → wizardflow-0.4.0}/src/wizardflow/_ui/_next/static/chunks/14mrh2-p_w84d.js +0 -0
  68. {wizardflow-0.2.0 → wizardflow-0.4.0}/src/wizardflow/_ui/_next/static/chunks/18clj6xsyhc_j.css +0 -0
  69. {wizardflow-0.2.0 → wizardflow-0.4.0}/src/wizardflow/_ui/_next/static/chunks/2-j1b74l3lxxu.css +0 -0
  70. {wizardflow-0.2.0 → wizardflow-0.4.0}/src/wizardflow/_ui/_next/static/chunks/27jktro2p5rq9.js +0 -0
  71. {wizardflow-0.2.0 → wizardflow-0.4.0}/src/wizardflow/_ui/_next/static/chunks/2gj5bmeov-r8f.js +0 -0
  72. {wizardflow-0.2.0 → wizardflow-0.4.0}/src/wizardflow/_ui/_next/static/chunks/310vm2bl3xxpt.js +0 -0
  73. {wizardflow-0.2.0 → wizardflow-0.4.0}/src/wizardflow/_ui/_next/static/chunks/3n7dm2ojtyzwn.js +0 -0
  74. {wizardflow-0.2.0 → wizardflow-0.4.0}/src/wizardflow/_ui/_next/static/chunks/turbopack-2fblmukzx7kws.js +0 -0
  75. {wizardflow-0.2.0/src/wizardflow/_ui/_next/static/F7xCRRyix77IKo6qy-ltF → wizardflow-0.4.0/src/wizardflow/_ui/_next/static/hZT2Q8C4grDYd_BRHVtw0}/_buildManifest.js +0 -0
  76. {wizardflow-0.2.0/src/wizardflow/_ui/_next/static/F7xCRRyix77IKo6qy-ltF → wizardflow-0.4.0/src/wizardflow/_ui/_next/static/hZT2Q8C4grDYd_BRHVtw0}/_clientMiddlewareManifest.js +0 -0
  77. {wizardflow-0.2.0/src/wizardflow/_ui/_next/static/F7xCRRyix77IKo6qy-ltF → wizardflow-0.4.0/src/wizardflow/_ui/_next/static/hZT2Q8C4grDYd_BRHVtw0}/_ssgManifest.js +0 -0
  78. {wizardflow-0.2.0 → wizardflow-0.4.0}/src/wizardflow/_ui/_next/static/media/4fa387ec64143e14-s.2tuy5pz7dlieh.woff2 +0 -0
  79. {wizardflow-0.2.0 → wizardflow-0.4.0}/src/wizardflow/_ui/_next/static/media/53b9e256198e5412-s.390ncx5urfkfu.woff2 +0 -0
  80. {wizardflow-0.2.0 → wizardflow-0.4.0}/src/wizardflow/_ui/_next/static/media/5ce348bf30bf5439-s.31988l_ccedte.woff2 +0 -0
  81. {wizardflow-0.2.0 → wizardflow-0.4.0}/src/wizardflow/_ui/_next/static/media/6306c77e7c8268e4-s.2dbetqa9o8jxf.woff2 +0 -0
  82. {wizardflow-0.2.0 → wizardflow-0.4.0}/src/wizardflow/_ui/_next/static/media/7178b3e590c64307-s.21jp631_3pja2.woff2 +0 -0
  83. {wizardflow-0.2.0 → wizardflow-0.4.0}/src/wizardflow/_ui/_next/static/media/797e433ab948586e-s.p.0r6juujl39pe6.woff2 +0 -0
  84. {wizardflow-0.2.0 → wizardflow-0.4.0}/src/wizardflow/_ui/_next/static/media/7d817b4c03b0c5f1-s.1uyisp29ctx0d.woff2 +0 -0
  85. {wizardflow-0.2.0 → wizardflow-0.4.0}/src/wizardflow/_ui/_next/static/media/8a480f0b521d4e75-s.1qq4vpdcun5oj.woff2 +0 -0
  86. {wizardflow-0.2.0 → wizardflow-0.4.0}/src/wizardflow/_ui/_next/static/media/bbc41e54d2fcbd21-s.1rgnod-3esatf.woff2 +0 -0
  87. {wizardflow-0.2.0 → wizardflow-0.4.0}/src/wizardflow/_ui/_next/static/media/caa3a2e1cccd8315-s.p.0wgildi0cnwt9.woff2 +0 -0
  88. {wizardflow-0.2.0 → wizardflow-0.4.0}/src/wizardflow/_ui/_next/static/media/fef07dbb0973bf53-s.3p2_lha1f2xer.woff2 +0 -0
  89. {wizardflow-0.2.0 → wizardflow-0.4.0}/src/wizardflow/_ui/opengraph-image +0 -0
  90. {wizardflow-0.2.0 → wizardflow-0.4.0}/src/wizardflow/_ui/robots.txt +0 -0
  91. {wizardflow-0.2.0 → wizardflow-0.4.0}/src/wizardflow/cli.py +0 -0
  92. {wizardflow-0.2.0 → wizardflow-0.4.0}/src/wizardflow/constants.py +0 -0
  93. {wizardflow-0.2.0 → wizardflow-0.4.0}/src/wizardflow/py.typed +0 -0
  94. {wizardflow-0.2.0 → wizardflow-0.4.0}/src/wizardflow/reader.py +0 -0
  95. {wizardflow-0.2.0 → wizardflow-0.4.0}/tests/conftest.py +0 -0
  96. {wizardflow-0.2.0 → wizardflow-0.4.0}/tests/test_cli.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wizardflow
3
- Version: 0.2.0
3
+ Version: 0.4.0
4
4
  Summary: Python SDK for recording agent flows into the WizardFlow / AgentTrace file format.
5
5
  Project-URL: Homepage, https://getwizardflow.com
6
6
  Project-URL: Documentation, https://getwizardflow.com
@@ -57,6 +57,10 @@ runtime dependencies**, no daemon, no setup.
57
57
 
58
58
  ![WizardFlow replaying an agent run](https://raw.githubusercontent.com/lkleonk/wizardflow/main/sdk/python/assets/demo.gif)
59
59
 
60
+ ▶ **[Watch this exact run live](https://getwizardflow.com/?example=university-consultant)** —
61
+ the flow from the GIF, replaying in your browser right now. No install, and
62
+ nothing is uploaded.
63
+
60
64
  The file it produces is JSON Lines with a small, documented schema: line 1 is a
61
65
  `header` record carrying the `graph { nodes, edges }`, then one `message`
62
66
  record per line (`steps[] → payloads[] { label, value }`). Every line is plain
@@ -98,6 +102,8 @@ No runtime dependencies. Developing the SDK itself? See
98
102
  ## Quickstart
99
103
 
100
104
  ```python
105
+ import uuid
106
+
101
107
  import wizardflow
102
108
 
103
109
  wizardflow.init(
@@ -110,10 +116,11 @@ wizardflow.init(
110
116
 
111
117
  # Every log names its message in the first argument:
112
118
  # log(message_id, node, payload_label, payload_value)
113
- wizardflow.log("msg-1", "router", "llm_input", prompt) # same node, two payloads ->
114
- wizardflow.log("msg-1", "router", "llm_output", output) # folded into one step
115
- wizardflow.log("msg-1", "tool_node") # visited, no payloads
116
- wizardflow.end_message("msg-1") # -> writes the trace
119
+ msg_id = str(uuid.uuid4()) # one fresh id per message
120
+ wizardflow.log(msg_id, "router", "llm_input", prompt) # same node, two payloads ->
121
+ wizardflow.log(msg_id, "router", "llm_output", output) # folded into one step
122
+ wizardflow.log(msg_id, "tool_node") # visited, no payloads
123
+ wizardflow.end_message(msg_id) # -> writes the trace
117
124
  ```
118
125
 
119
126
  There is **no `save()`** and no autosave: `log()` only accumulates in memory,
@@ -147,7 +154,88 @@ wizardflow.end_message("msg-2") # writes msg-2
147
154
 
148
155
  Pass the **string `id`**, never a handle object — safe to hand to a callback or
149
156
  across threads. A message is created on first reference and finalized by
150
- `end_message`; `end_message(id, title="...")` optionally gives it a human title.
157
+ `end_message`; `end_message(id, title="...")` optionally gives it a human title,
158
+ and `end_message(id, meta={...})` attaches flat metadata about the message as a
159
+ whole (outcome, latency, user id, …) — the viewer shows it on the message's
160
+ timeline chip. Keep meta values short scalars; large structured data belongs in
161
+ `log()` payloads.
162
+
163
+ ### Choosing message ids
164
+
165
+ A message is **one unit of work** — one user turn, one run through the graph.
166
+ Its id only has to be unique within the run, and it **cannot be reused** once
167
+ `end_message` has been called on it. The simplest safe choice is a fresh UUID
168
+ per message:
169
+
170
+ ```python
171
+ import uuid
172
+
173
+ msg_id = str(uuid.uuid4())
174
+ ```
175
+
176
+ Do **not** use a session id, user id, or thread id as the message id. Those
177
+ identify a *conversation*, and a conversation contains many messages — after
178
+ the first `end_message(session_id)`, every further `log(session_id, ...)` in
179
+ that session raises, because that "message" has already ended. If you want the
180
+ session visible in the trace, put it in `init(meta=...)` or in the message's
181
+ meta (`end_message(msg_id, meta={"session": session_id})`) — never in the id.
182
+ Ids are opaque:
183
+ the viewer only displays and groups by them, so encoding meaning into them
184
+ buys nothing. Hardcoded ids like `"msg-1"` are fine for demo scripts; `uuid4`
185
+ is the right default for real applications.
186
+
187
+ ## One flow, or several at once
188
+
189
+ `init()` installs the client it returns as the module default. With a single
190
+ flow — the common case — **ignore the return value** and call
191
+ `wizardflow.log(...)` / `wizardflow.end_message(...)` directly: they work from
192
+ any module, with no handle to thread through your code.
193
+
194
+ Running several agent flows side by side (say, a doctor flow and a patient
195
+ flow) means several traces, and each needs its own instance. Keep the returned
196
+ client per flow and log through it — and name it **`tracer`**, not `client`,
197
+ since `client` in an agent codebase is almost always the LLM client:
198
+
199
+ ```python
200
+ doctor_tracer = wizardflow.init(file_prefix="doctor", nodes=[...], edges=[...])
201
+ patient_tracer = wizardflow.init(file_prefix="patient", nodes=[...], edges=[...])
202
+
203
+ doctor_tracer.log(doc_msg, "diagnose", "llm_input", prompt)
204
+ patient_tracer.log(pat_msg, "intake", "llm_input", prompt)
205
+ ```
206
+
207
+ With more than one tracer, don't mix in the bare module-level calls:
208
+ `wizardflow.log(...)` always targets the most recently initialized tracer.
209
+
210
+ ## Node descriptions, labels & colors
211
+
212
+ Nodes can carry a short human description of what they do. In the viewer it
213
+ stays out of the way: when a node is selected, a small info icon appears next
214
+ to its name in the inspector — click it to read the description. Nodes without
215
+ one show no icon, and `wizardflow md` / `wizardflow html` list the described
216
+ nodes under the graph.
217
+
218
+ ```python
219
+ wizardflow.init(
220
+ nodes=["router", "retriever", "generator"],
221
+ edges=[("router", "retriever"), ("retriever", "generator")],
222
+ node_descriptions={
223
+ "router": "Chooses the next step.",
224
+ "retriever": "Fetches relevant documents.",
225
+ "generator": "Writes the final answer.",
226
+ },
227
+ )
228
+ ```
229
+
230
+ Two sibling kwargs work the same way: `node_labels` maps ids to the display
231
+ name the viewer shows instead of the raw id (especially useful with
232
+ `init_from_langgraph`, where the extracted ids are your function names), and
233
+ `node_colors` sets accent colors. All three are validated against the declared
234
+ nodes: an unknown id raises a `WizardFlowError` at `init()` time so typos fail
235
+ fast; with `silent=True` it is logged as a warning on the `wizardflow` logger
236
+ and skipped instead. All three also work on `init_from_langgraph`, where they
237
+ attach to the extracted node ids — `log()` always targets the **id**, never
238
+ the label.
151
239
 
152
240
  ## LangGraph: automatic topology
153
241
 
@@ -180,8 +268,8 @@ wizardflow.init_from_langgraph(
180
268
  ```
181
269
 
182
270
  By default, a color key that does not match an extracted node id raises a
183
- `WizardFlowError` so typos fail fast. With `silent=True`, unknown color keys are
184
- ignored.
271
+ `WizardFlowError` so typos fail fast. With `silent=True`, unknown color keys
272
+ are skipped and logged as a warning instead.
185
273
 
186
274
  It extracts node ids (keeping `__start__` / `__end__`) and directed edges, and
187
275
  marks runtime branches with `"conditional": true` (deterministic and parallel
@@ -202,30 +290,39 @@ A non-LangGraph object raises `LangGraphExtractionError`; missing conditional
202
290
  metadata never fails extraction (the edge is just emitted plain). Call it
203
291
  **after `compile()`**, when the topology actually exists.
204
292
 
205
- ## API (v0.2)
293
+ ## API
206
294
 
207
- > **Breaking change in 0.2:** traces are now JSON Lines (`.jsonl`), written by
208
- > appending — the old single-document `.json` format is gone from the SDK
209
- > (writer *and* CLI readers). The web UI at getwizardflow.com still opens old
210
- > `.json` traces.
211
-
212
- - `init(output_dir=, file_prefix="wizardflow", name=, description=, nodes=, edges=, meta=, silent=False, max_bytes=16_000_000, max_messages=2_000) -> Client`
295
+ - `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`
213
296
  - `description` lands in `meta.description` (matches the schema field).
214
297
  - `nodes=` enables fast-fail: `log()` to an undeclared node raises
215
298
  `UnknownNodeError` immediately (unless silenced).
299
+ - `node_labels` / `node_colors` / `node_descriptions` map declared node ids
300
+ to a display name / CSS color / short description; an unknown id raises
301
+ unless silenced (then it's logged as a warning and skipped).
216
302
  - `output_dir` is optional; omitted, traces are written in cwd.
217
303
  - `file_prefix` is optional; omitted, filenames start with `wizardflow`.
218
304
  - `max_bytes` / `max_messages` cap each part file before rotation (see below).
219
- - `init_from_langgraph(app, output_dir=, file_prefix="wizardflow", name=, description=, meta=, node_colors=, silent=False, max_bytes=..., max_messages=...) -> Client`
305
+ - `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`
220
306
  - same as `init`, but `nodes`/`edges` come from `app.get_graph()`.
221
- - `node_colors` maps extracted node ids to CSS colors such as `"#A78BFA"`.
307
+ - `node_labels` renames extracted ids for display (they're your function
308
+ names); `node_colors` maps them to CSS colors such as `"#A78BFA"`;
309
+ `node_descriptions` to short descriptions.
222
310
  - `log(id, node, label=None, content=None)` — the first positional is the
223
311
  **message id**, the second is the node. With `label`/`content` it records a
224
312
  payload; bare `log(id, "node")` records a visit with no payloads. The message
225
313
  is created on first reference; this only accumulates in memory.
226
- - `end_message(id, title=None)` — finalize a message and append it to the
227
- trace; returns the current trace path. The **only** call that touches disk.
228
- Optional `title` sets the message's human title. Idempotent.
314
+ - `end_message(id, title=None, meta=None)` — finalize a message and append it
315
+ to the trace; returns the current trace path. The **only** call that touches
316
+ disk. Optional `title` sets the message's human title; optional `meta` (a
317
+ flat dict of short scalars) attaches message-level metadata shown on the
318
+ message's chip in the viewer. Idempotent.
319
+ - `reinit(name=None, description=None, meta=None)` — start a **new trace
320
+ file** (fresh timestamped name), keeping the graph and output configuration.
321
+ Use it at natural boundaries of a long-lived process — a new user session, a
322
+ new day. The old file is left as-is (**no seal**: it's a finished run, not a
323
+ rotated part). Open messages carry over and are written wherever they end;
324
+ completed message ids become reusable. `name`/`description`/`meta` replace
325
+ the current values when given. Returns the new trace path.
229
326
  - `Client.current_path` — the trace file currently being written.
230
327
  - `to_dict()` / `to_json()` — inspect the active part (completed messages),
231
328
  assembled into one `AgentTraceFile` object.
@@ -251,6 +348,10 @@ unparseable final line (a crash mid-append leaves a torn tail; everything
251
348
  before it is intact). Only **completed** messages are written; an in-progress
252
349
  message lives in memory until it ends.
253
350
 
351
+ > Early SDK versions (0.1) wrote a trace as one single-document `.json` file
352
+ > instead. Newer versions write JSONL only, but every reader — the CLI
353
+ > subcommands and the web UI at getwizardflow.com — still opens both formats.
354
+
254
355
  ### How saving works
255
356
 
256
357
  `end_message` **appends one line** to the active part — O(1) no matter how
@@ -312,6 +413,20 @@ Consecutive `log()` calls to the **same** node within a message fold into a
312
413
  single step with multiple payloads (e.g. a router step carrying both
313
414
  `llm_input` and `llm_output`). A `log()` to a different node starts a new step.
314
415
 
416
+ ## What to log (conventions)
417
+
418
+ Payload labels are free-form strings — the schema reserves none of them. These
419
+ conventions just keep traces consistent and readable:
420
+
421
+ - **LLM-backed nodes** — log the exact prompt as `llm_input` and the raw
422
+ completion as `llm_output`. When a run goes wrong, the trace then shows
423
+ precisely what the model saw and what it said — most of what you need to
424
+ debug an agent — and step folding merges the pair into one step.
425
+ - **Everything else** — log the node's real domain data under honest labels: a
426
+ retriever logs its `query` and `results`, a tool node `tool_input` and
427
+ `tool_output`, a deterministic router its `decision`. Don't force
428
+ `llm_input`/`llm_output` onto nodes that never call a model.
429
+
315
430
  ## Examples
316
431
 
317
432
  Fuller runnable examples live in
@@ -10,6 +10,10 @@ runtime dependencies**, no daemon, no setup.
10
10
 
11
11
  ![WizardFlow replaying an agent run](https://raw.githubusercontent.com/lkleonk/wizardflow/main/sdk/python/assets/demo.gif)
12
12
 
13
+ ▶ **[Watch this exact run live](https://getwizardflow.com/?example=university-consultant)** —
14
+ the flow from the GIF, replaying in your browser right now. No install, and
15
+ nothing is uploaded.
16
+
13
17
  The file it produces is JSON Lines with a small, documented schema: line 1 is a
14
18
  `header` record carrying the `graph { nodes, edges }`, then one `message`
15
19
  record per line (`steps[] → payloads[] { label, value }`). Every line is plain
@@ -51,6 +55,8 @@ No runtime dependencies. Developing the SDK itself? See
51
55
  ## Quickstart
52
56
 
53
57
  ```python
58
+ import uuid
59
+
54
60
  import wizardflow
55
61
 
56
62
  wizardflow.init(
@@ -63,10 +69,11 @@ wizardflow.init(
63
69
 
64
70
  # Every log names its message in the first argument:
65
71
  # log(message_id, node, payload_label, payload_value)
66
- wizardflow.log("msg-1", "router", "llm_input", prompt) # same node, two payloads ->
67
- wizardflow.log("msg-1", "router", "llm_output", output) # folded into one step
68
- wizardflow.log("msg-1", "tool_node") # visited, no payloads
69
- wizardflow.end_message("msg-1") # -> writes the trace
72
+ msg_id = str(uuid.uuid4()) # one fresh id per message
73
+ wizardflow.log(msg_id, "router", "llm_input", prompt) # same node, two payloads ->
74
+ wizardflow.log(msg_id, "router", "llm_output", output) # folded into one step
75
+ wizardflow.log(msg_id, "tool_node") # visited, no payloads
76
+ wizardflow.end_message(msg_id) # -> writes the trace
70
77
  ```
71
78
 
72
79
  There is **no `save()`** and no autosave: `log()` only accumulates in memory,
@@ -100,7 +107,88 @@ wizardflow.end_message("msg-2") # writes msg-2
100
107
 
101
108
  Pass the **string `id`**, never a handle object — safe to hand to a callback or
102
109
  across threads. A message is created on first reference and finalized by
103
- `end_message`; `end_message(id, title="...")` optionally gives it a human title.
110
+ `end_message`; `end_message(id, title="...")` optionally gives it a human title,
111
+ and `end_message(id, meta={...})` attaches flat metadata about the message as a
112
+ whole (outcome, latency, user id, …) — the viewer shows it on the message's
113
+ timeline chip. Keep meta values short scalars; large structured data belongs in
114
+ `log()` payloads.
115
+
116
+ ### Choosing message ids
117
+
118
+ A message is **one unit of work** — one user turn, one run through the graph.
119
+ Its id only has to be unique within the run, and it **cannot be reused** once
120
+ `end_message` has been called on it. The simplest safe choice is a fresh UUID
121
+ per message:
122
+
123
+ ```python
124
+ import uuid
125
+
126
+ msg_id = str(uuid.uuid4())
127
+ ```
128
+
129
+ Do **not** use a session id, user id, or thread id as the message id. Those
130
+ identify a *conversation*, and a conversation contains many messages — after
131
+ the first `end_message(session_id)`, every further `log(session_id, ...)` in
132
+ that session raises, because that "message" has already ended. If you want the
133
+ session visible in the trace, put it in `init(meta=...)` or in the message's
134
+ meta (`end_message(msg_id, meta={"session": session_id})`) — never in the id.
135
+ Ids are opaque:
136
+ the viewer only displays and groups by them, so encoding meaning into them
137
+ buys nothing. Hardcoded ids like `"msg-1"` are fine for demo scripts; `uuid4`
138
+ is the right default for real applications.
139
+
140
+ ## One flow, or several at once
141
+
142
+ `init()` installs the client it returns as the module default. With a single
143
+ flow — the common case — **ignore the return value** and call
144
+ `wizardflow.log(...)` / `wizardflow.end_message(...)` directly: they work from
145
+ any module, with no handle to thread through your code.
146
+
147
+ Running several agent flows side by side (say, a doctor flow and a patient
148
+ flow) means several traces, and each needs its own instance. Keep the returned
149
+ client per flow and log through it — and name it **`tracer`**, not `client`,
150
+ since `client` in an agent codebase is almost always the LLM client:
151
+
152
+ ```python
153
+ doctor_tracer = wizardflow.init(file_prefix="doctor", nodes=[...], edges=[...])
154
+ patient_tracer = wizardflow.init(file_prefix="patient", nodes=[...], edges=[...])
155
+
156
+ doctor_tracer.log(doc_msg, "diagnose", "llm_input", prompt)
157
+ patient_tracer.log(pat_msg, "intake", "llm_input", prompt)
158
+ ```
159
+
160
+ With more than one tracer, don't mix in the bare module-level calls:
161
+ `wizardflow.log(...)` always targets the most recently initialized tracer.
162
+
163
+ ## Node descriptions, labels & colors
164
+
165
+ Nodes can carry a short human description of what they do. In the viewer it
166
+ stays out of the way: when a node is selected, a small info icon appears next
167
+ to its name in the inspector — click it to read the description. Nodes without
168
+ one show no icon, and `wizardflow md` / `wizardflow html` list the described
169
+ nodes under the graph.
170
+
171
+ ```python
172
+ wizardflow.init(
173
+ nodes=["router", "retriever", "generator"],
174
+ edges=[("router", "retriever"), ("retriever", "generator")],
175
+ node_descriptions={
176
+ "router": "Chooses the next step.",
177
+ "retriever": "Fetches relevant documents.",
178
+ "generator": "Writes the final answer.",
179
+ },
180
+ )
181
+ ```
182
+
183
+ Two sibling kwargs work the same way: `node_labels` maps ids to the display
184
+ name the viewer shows instead of the raw id (especially useful with
185
+ `init_from_langgraph`, where the extracted ids are your function names), and
186
+ `node_colors` sets accent colors. All three are validated against the declared
187
+ nodes: an unknown id raises a `WizardFlowError` at `init()` time so typos fail
188
+ fast; with `silent=True` it is logged as a warning on the `wizardflow` logger
189
+ and skipped instead. All three also work on `init_from_langgraph`, where they
190
+ attach to the extracted node ids — `log()` always targets the **id**, never
191
+ the label.
104
192
 
105
193
  ## LangGraph: automatic topology
106
194
 
@@ -133,8 +221,8 @@ wizardflow.init_from_langgraph(
133
221
  ```
134
222
 
135
223
  By default, a color key that does not match an extracted node id raises a
136
- `WizardFlowError` so typos fail fast. With `silent=True`, unknown color keys are
137
- ignored.
224
+ `WizardFlowError` so typos fail fast. With `silent=True`, unknown color keys
225
+ are skipped and logged as a warning instead.
138
226
 
139
227
  It extracts node ids (keeping `__start__` / `__end__`) and directed edges, and
140
228
  marks runtime branches with `"conditional": true` (deterministic and parallel
@@ -155,30 +243,39 @@ A non-LangGraph object raises `LangGraphExtractionError`; missing conditional
155
243
  metadata never fails extraction (the edge is just emitted plain). Call it
156
244
  **after `compile()`**, when the topology actually exists.
157
245
 
158
- ## API (v0.2)
246
+ ## API
159
247
 
160
- > **Breaking change in 0.2:** traces are now JSON Lines (`.jsonl`), written by
161
- > appending — the old single-document `.json` format is gone from the SDK
162
- > (writer *and* CLI readers). The web UI at getwizardflow.com still opens old
163
- > `.json` traces.
164
-
165
- - `init(output_dir=, file_prefix="wizardflow", name=, description=, nodes=, edges=, meta=, silent=False, max_bytes=16_000_000, max_messages=2_000) -> Client`
248
+ - `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`
166
249
  - `description` lands in `meta.description` (matches the schema field).
167
250
  - `nodes=` enables fast-fail: `log()` to an undeclared node raises
168
251
  `UnknownNodeError` immediately (unless silenced).
252
+ - `node_labels` / `node_colors` / `node_descriptions` map declared node ids
253
+ to a display name / CSS color / short description; an unknown id raises
254
+ unless silenced (then it's logged as a warning and skipped).
169
255
  - `output_dir` is optional; omitted, traces are written in cwd.
170
256
  - `file_prefix` is optional; omitted, filenames start with `wizardflow`.
171
257
  - `max_bytes` / `max_messages` cap each part file before rotation (see below).
172
- - `init_from_langgraph(app, output_dir=, file_prefix="wizardflow", name=, description=, meta=, node_colors=, silent=False, max_bytes=..., max_messages=...) -> Client`
258
+ - `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`
173
259
  - same as `init`, but `nodes`/`edges` come from `app.get_graph()`.
174
- - `node_colors` maps extracted node ids to CSS colors such as `"#A78BFA"`.
260
+ - `node_labels` renames extracted ids for display (they're your function
261
+ names); `node_colors` maps them to CSS colors such as `"#A78BFA"`;
262
+ `node_descriptions` to short descriptions.
175
263
  - `log(id, node, label=None, content=None)` — the first positional is the
176
264
  **message id**, the second is the node. With `label`/`content` it records a
177
265
  payload; bare `log(id, "node")` records a visit with no payloads. The message
178
266
  is created on first reference; this only accumulates in memory.
179
- - `end_message(id, title=None)` — finalize a message and append it to the
180
- trace; returns the current trace path. The **only** call that touches disk.
181
- Optional `title` sets the message's human title. Idempotent.
267
+ - `end_message(id, title=None, meta=None)` — finalize a message and append it
268
+ to the trace; returns the current trace path. The **only** call that touches
269
+ disk. Optional `title` sets the message's human title; optional `meta` (a
270
+ flat dict of short scalars) attaches message-level metadata shown on the
271
+ message's chip in the viewer. Idempotent.
272
+ - `reinit(name=None, description=None, meta=None)` — start a **new trace
273
+ file** (fresh timestamped name), keeping the graph and output configuration.
274
+ Use it at natural boundaries of a long-lived process — a new user session, a
275
+ new day. The old file is left as-is (**no seal**: it's a finished run, not a
276
+ rotated part). Open messages carry over and are written wherever they end;
277
+ completed message ids become reusable. `name`/`description`/`meta` replace
278
+ the current values when given. Returns the new trace path.
182
279
  - `Client.current_path` — the trace file currently being written.
183
280
  - `to_dict()` / `to_json()` — inspect the active part (completed messages),
184
281
  assembled into one `AgentTraceFile` object.
@@ -204,6 +301,10 @@ unparseable final line (a crash mid-append leaves a torn tail; everything
204
301
  before it is intact). Only **completed** messages are written; an in-progress
205
302
  message lives in memory until it ends.
206
303
 
304
+ > Early SDK versions (0.1) wrote a trace as one single-document `.json` file
305
+ > instead. Newer versions write JSONL only, but every reader — the CLI
306
+ > subcommands and the web UI at getwizardflow.com — still opens both formats.
307
+
207
308
  ### How saving works
208
309
 
209
310
  `end_message` **appends one line** to the active part — O(1) no matter how
@@ -265,6 +366,20 @@ Consecutive `log()` calls to the **same** node within a message fold into a
265
366
  single step with multiple payloads (e.g. a router step carrying both
266
367
  `llm_input` and `llm_output`). A `log()` to a different node starts a new step.
267
368
 
369
+ ## What to log (conventions)
370
+
371
+ Payload labels are free-form strings — the schema reserves none of them. These
372
+ conventions just keep traces consistent and readable:
373
+
374
+ - **LLM-backed nodes** — log the exact prompt as `llm_input` and the raw
375
+ completion as `llm_output`. When a run goes wrong, the trace then shows
376
+ precisely what the model saw and what it said — most of what you need to
377
+ debug an agent — and step folding merges the pair into one step.
378
+ - **Everything else** — log the node's real domain data under honest labels: a
379
+ retriever logs its `query` and `results`, a tool node `tool_input` and
380
+ `tool_output`, a deterministic router its `decision`. Don't force
381
+ `llm_input`/`llm_output` onto nodes that never call a model.
382
+
268
383
  ## Examples
269
384
 
270
385
  Fuller runnable examples live in
@@ -18,6 +18,10 @@ blockquote {
18
18
  color: #8a8a8a; font-style: italic;
19
19
  }
20
20
  table.meta { border-collapse: collapse; font-size: .85rem; margin-bottom: 2rem; }
21
+ section.nodes { margin: -1rem 0 2rem; }
22
+ section.nodes h2 { font-size: 1.05rem; margin: 0 0 .2rem; }
23
+ section.nodes ul { margin: 0; padding-left: 1.2rem; font-size: .9rem; }
24
+ section.nodes li { margin: .15rem 0; }
21
25
  table.meta th, table.meta td {
22
26
  border: 1px solid #8884; padding: .25rem .6rem; text-align: left;
23
27
  }
@@ -27,6 +31,7 @@ section.message {
27
31
  padding: .25rem 1rem 1rem; margin-bottom: 1.5rem;
28
32
  }
29
33
  section.message > h2 { font-size: 1.15rem; margin: .8rem 0 .4rem; }
34
+ section.message > p.msg-meta { font-size: .85rem; color: #8a8a8a; margin: -.2rem 0 .6rem; }
30
35
  section.step { padding: .9rem 0; }
31
36
  section.step:first-of-type { padding-top: .2rem; }
32
37
  /* Divider between consecutive nodes in a message. */
@@ -57,48 +62,60 @@ pre code { background: none; padding: 0; }
57
62
  <table class="meta">
58
63
  <tr><th>version</th><td>0.2</td></tr>
59
64
  </table>
65
+ <section class="nodes">
66
+ <h2>Nodes</h2>
67
+ <ul>
68
+ <li><span class="label">router</span> — Classifies the request and picks a branch: planner or retriever.</li>
69
+ <li><span class="label">planner</span> — Decomposes a tool-using request into concrete tool calls.</li>
70
+ <li><span class="label">retriever</span> — Fetches the most relevant documents for the query.</li>
71
+ <li><span class="label">tool_node</span> — Runs the planned tool calls against external APIs.</li>
72
+ <li><span class="label">generator</span> — Writes the answer from the tool result or retrieved docs.</li>
73
+ </ul>
74
+ </section>
60
75
  <section class="message">
61
76
  <h2>Weather in Berlin</h2>
77
+ <p class="msg-meta"><span class="label">branch</span>: planner · <span class="label">outcome</span>: answered · <span class="label">latency_ms</span>: 10210</p>
62
78
  <section class="step">
63
- <h3>user_input <span class="time">· 21:39:58</span></h3>
79
+ <h3>user_input <span class="time">· 13:14:05</span></h3>
64
80
  <p class="scalar"><span class="label">Input</span> <code>What&#x27;s the weather in Berlin?</code></p>
65
81
  </section>
66
82
  <section class="step">
67
- <h3>router <span class="time">· 21:39:58</span></h3>
83
+ <h3>router <span class="time">· 13:14:05</span></h3>
68
84
  <p class="scalar"><span class="label">llm_input</span> <code>Pick a route for the request...</code></p>
69
85
  <p class="scalar"><span class="label">llm_output</span> <code>{&quot;route&quot;: &quot;planner&quot;, &quot;confidence&quot;: 0.92}</code></p>
70
86
  </section>
71
87
  <section class="step">
72
- <h3>planner <span class="time">· 21:39:58</span></h3>
88
+ <h3>planner <span class="time">· 13:14:05</span></h3>
73
89
  <p class="scalar"><span class="label">llm_input</span> <code>Decompose into tool calls...</code></p>
74
90
  <p class="scalar"><span class="label">llm_output</span> <code>{&quot;plan&quot;: [&quot;weather_api(city=&#x27;Berlin&#x27;)&quot;]}</code></p>
75
91
  </section>
76
92
  <section class="step">
77
- <h3>tool_node <span class="time">· 21:39:58</span></h3>
93
+ <h3>tool_node <span class="time">· 13:14:05</span></h3>
78
94
  </section>
79
95
  <section class="step">
80
- <h3>generator <span class="time">· 21:39:58</span></h3>
96
+ <h3>generator <span class="time">· 13:14:05</span></h3>
81
97
  <p class="scalar"><span class="label">llm_input</span> <code>Answer using the tool result...</code></p>
82
98
  <p class="scalar"><span class="label">llm_output</span> <code>It&#x27;s 19C and partly cloudy in Berlin.</code></p>
83
99
  </section>
84
100
  <section class="step">
85
- <h3>final_response <span class="time">· 21:39:58</span></h3>
101
+ <h3>final_response <span class="time">· 13:14:05</span></h3>
86
102
  <p class="scalar"><span class="label">Output</span> <code>It&#x27;s 19C and partly cloudy in Berlin.</code></p>
87
103
  </section>
88
104
  </section>
89
105
  <section class="message">
90
106
  <h2>Summarize research paper</h2>
107
+ <p class="msg-meta"><span class="label">branch</span>: retriever · <span class="label">outcome</span>: answered · <span class="label">docs_used</span>: 2 · <span class="label">latency_ms</span>: 7130</p>
91
108
  <section class="step">
92
- <h3>user_input <span class="time">· 21:39:58</span></h3>
109
+ <h3>user_input <span class="time">· 13:14:05</span></h3>
93
110
  <p class="scalar"><span class="label">Input</span> <code>Summarize the attached research paper.</code></p>
94
111
  </section>
95
112
  <section class="step">
96
- <h3>router <span class="time">· 21:39:58</span></h3>
113
+ <h3>router <span class="time">· 13:14:05</span></h3>
97
114
  <p class="scalar"><span class="label">llm_input</span> <code>Pick a route for the request...</code></p>
98
115
  <p class="scalar"><span class="label">llm_output</span> <code>{&quot;route&quot;: &quot;retriever&quot;, &quot;confidence&quot;: 0.88}</code></p>
99
116
  </section>
100
117
  <section class="step">
101
- <h3>retriever <span class="time">· 21:39:58</span></h3>
118
+ <h3>retriever <span class="time">· 13:14:05</span></h3>
102
119
  <div class="payload"><span class="label">Input</span><pre><code>{
103
120
  &quot;topK&quot;: 4,
104
121
  &quot;namespace&quot;: &quot;papers&quot;
@@ -115,12 +132,12 @@ pre code { background: none; padding: 0; }
115
132
  ]</code></pre></div>
116
133
  </section>
117
134
  <section class="step">
118
- <h3>generator <span class="time">· 21:39:58</span></h3>
135
+ <h3>generator <span class="time">· 13:14:05</span></h3>
119
136
  <p class="scalar"><span class="label">llm_input</span> <code>Summarize the retrieved documents...</code></p>
120
137
  <p class="scalar"><span class="label">llm_output</span> <code>The paper proposes a sparse attention variant with near-linear cost.</code></p>
121
138
  </section>
122
139
  <section class="step">
123
- <h3>final_response <span class="time">· 21:39:58</span></h3>
140
+ <h3>final_response <span class="time">· 13:14:05</span></h3>
124
141
  <p class="scalar"><span class="label">Output</span> <code>The paper proposes a sparse attention variant with near-linear cost.</code></p>
125
142
  </section>
126
143
  </section>
@@ -26,49 +26,59 @@ flowchart TD
26
26
  n5 --> n6
27
27
  ```
28
28
 
29
+ - **router** — Classifies the request and picks a branch: planner or retriever.
30
+ - **planner** — Decomposes a tool-using request into concrete tool calls.
31
+ - **retriever** — Fetches the most relevant documents for the query.
32
+ - **tool_node** — Runs the planned tool calls against external APIs.
33
+ - **generator** — Writes the answer from the tool result or retrieved docs.
34
+
29
35
  ## Weather in Berlin
30
36
 
31
- ### user_input · 21:39:58
37
+ **branch**: planner · **outcome**: answered · **latency_ms**: 10210
38
+
39
+ ### user_input · 13:14:05
32
40
 
33
41
  **Input**: `What's the weather in Berlin?`
34
42
 
35
- ### router · 21:39:58
43
+ ### router · 13:14:05
36
44
 
37
45
  **llm_input**: `Pick a route for the request...`
38
46
 
39
47
  **llm_output**: `{"route": "planner", "confidence": 0.92}`
40
48
 
41
- ### planner · 21:39:58
49
+ ### planner · 13:14:05
42
50
 
43
51
  **llm_input**: `Decompose into tool calls...`
44
52
 
45
53
  **llm_output**: `{"plan": ["weather_api(city='Berlin')"]}`
46
54
 
47
- ### tool_node · 21:39:58
55
+ ### tool_node · 13:14:05
48
56
 
49
- ### generator · 21:39:58
57
+ ### generator · 13:14:05
50
58
 
51
59
  **llm_input**: `Answer using the tool result...`
52
60
 
53
61
  **llm_output**: `It's 19C and partly cloudy in Berlin.`
54
62
 
55
- ### final_response · 21:39:58
63
+ ### final_response · 13:14:05
56
64
 
57
65
  **Output**: `It's 19C and partly cloudy in Berlin.`
58
66
 
59
67
  ## Summarize research paper
60
68
 
61
- ### user_input · 21:39:58
69
+ **branch**: retriever · **outcome**: answered · **docs_used**: 2 · **latency_ms**: 7130
70
+
71
+ ### user_input · 13:14:05
62
72
 
63
73
  **Input**: `Summarize the attached research paper.`
64
74
 
65
- ### router · 21:39:58
75
+ ### router · 13:14:05
66
76
 
67
77
  **llm_input**: `Pick a route for the request...`
68
78
 
69
79
  **llm_output**: `{"route": "retriever", "confidence": 0.88}`
70
80
 
71
- ### retriever · 21:39:58
81
+ ### retriever · 13:14:05
72
82
 
73
83
  **Input**
74
84
 
@@ -94,12 +104,12 @@ flowchart TD
94
104
  ]
95
105
  ```
96
106
 
97
- ### generator · 21:39:58
107
+ ### generator · 13:14:05
98
108
 
99
109
  **llm_input**: `Summarize the retrieved documents...`
100
110
 
101
111
  **llm_output**: `The paper proposes a sparse attention variant with near-linear cost.`
102
112
 
103
- ### final_response · 21:39:58
113
+ ### final_response · 13:14:05
104
114
 
105
115
  **Output**: `The paper proposes a sparse attention variant with near-linear cost.`