wizardflow 0.3.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 (90) hide show
  1. {wizardflow-0.3.0 → wizardflow-0.4.0}/PKG-INFO +18 -7
  2. {wizardflow-0.3.0 → wizardflow-0.4.0}/README.md +17 -6
  3. {wizardflow-0.3.0 → wizardflow-0.4.0}/examples/multibranch.html +28 -11
  4. {wizardflow-0.3.0 → wizardflow-0.4.0}/examples/multibranch.md +21 -11
  5. {wizardflow-0.3.0 → wizardflow-0.4.0}/examples/multibranch.py +23 -2
  6. {wizardflow-0.3.0 → wizardflow-0.4.0}/examples/quickstart.html +20 -6
  7. {wizardflow-0.3.0 → wizardflow-0.4.0}/examples/quickstart.md +12 -6
  8. {wizardflow-0.3.0 → wizardflow-0.4.0}/examples/quickstart.py +17 -2
  9. {wizardflow-0.3.0 → wizardflow-0.4.0}/pyproject.toml +1 -1
  10. {wizardflow-0.3.0 → wizardflow-0.4.0}/src/wizardflow/__init__.py +6 -2
  11. {wizardflow-0.3.0 → wizardflow-0.4.0}/src/wizardflow/_render.py +9 -0
  12. {wizardflow-0.3.0 → wizardflow-0.4.0}/src/wizardflow/_ui/404.html +7 -25
  13. {wizardflow-0.3.0 → wizardflow-0.4.0}/src/wizardflow/_ui/__next.__PAGE__.txt +2 -2
  14. wizardflow-0.4.0/src/wizardflow/_ui/__next._full.txt +23 -0
  15. {wizardflow-0.3.0 → wizardflow-0.4.0}/src/wizardflow/_ui/__next._head.txt +1 -1
  16. wizardflow-0.4.0/src/wizardflow/_ui/__next._index.txt +7 -0
  17. {wizardflow-0.3.0 → wizardflow-0.4.0}/src/wizardflow/_ui/__next._tree.txt +1 -1
  18. wizardflow-0.3.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.3.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.4.0/src/wizardflow/_ui/_not-found/__next._full.txt +18 -0
  23. {wizardflow-0.3.0 → wizardflow-0.4.0}/src/wizardflow/_ui/_not-found/__next._head.txt +1 -1
  24. wizardflow-0.4.0/src/wizardflow/_ui/_not-found/__next._index.txt +7 -0
  25. {wizardflow-0.3.0 → wizardflow-0.4.0}/src/wizardflow/_ui/_not-found/__next._not-found/__PAGE__.txt +1 -1
  26. {wizardflow-0.3.0 → wizardflow-0.4.0}/src/wizardflow/_ui/_not-found/__next._not-found.txt +1 -1
  27. {wizardflow-0.3.0 → wizardflow-0.4.0}/src/wizardflow/_ui/_not-found/__next._tree.txt +1 -1
  28. {wizardflow-0.3.0 → wizardflow-0.4.0}/src/wizardflow/_ui/_not-found.html +7 -25
  29. wizardflow-0.4.0/src/wizardflow/_ui/_not-found.txt +18 -0
  30. wizardflow-0.4.0/src/wizardflow/_ui/index.html +7 -0
  31. wizardflow-0.4.0/src/wizardflow/_ui/index.txt +23 -0
  32. {wizardflow-0.3.0 → wizardflow-0.4.0}/src/wizardflow/_ui/sitemap.xml +1 -1
  33. {wizardflow-0.3.0 → wizardflow-0.4.0}/src/wizardflow/client.py +20 -7
  34. {wizardflow-0.3.0 → wizardflow-0.4.0}/src/wizardflow/html.py +9 -1
  35. {wizardflow-0.3.0 → wizardflow-0.4.0}/src/wizardflow/markdown.py +6 -1
  36. {wizardflow-0.3.0 → wizardflow-0.4.0}/tests/test_html.py +23 -0
  37. {wizardflow-0.3.0 → wizardflow-0.4.0}/tests/test_markdown.py +21 -0
  38. {wizardflow-0.3.0 → wizardflow-0.4.0}/tests/test_reader.py +10 -0
  39. {wizardflow-0.3.0 → wizardflow-0.4.0}/tests/test_trace.py +37 -0
  40. wizardflow-0.3.0/src/wizardflow/_ui/__next._full.txt +0 -22
  41. wizardflow-0.3.0/src/wizardflow/_ui/__next._index.txt +0 -6
  42. wizardflow-0.3.0/src/wizardflow/_ui/_next/static/chunks/142r2alz4_x6t.js +0 -1
  43. wizardflow-0.3.0/src/wizardflow/_ui/_next/static/chunks/2q_qu9w0vej6s.js +0 -115
  44. wizardflow-0.3.0/src/wizardflow/_ui/_not-found/__next._full.txt +0 -17
  45. wizardflow-0.3.0/src/wizardflow/_ui/_not-found/__next._index.txt +0 -6
  46. wizardflow-0.3.0/src/wizardflow/_ui/_not-found.txt +0 -17
  47. wizardflow-0.3.0/src/wizardflow/_ui/index.html +0 -25
  48. wizardflow-0.3.0/src/wizardflow/_ui/index.txt +0 -22
  49. {wizardflow-0.3.0 → wizardflow-0.4.0}/.gitignore +0 -0
  50. {wizardflow-0.3.0 → wizardflow-0.4.0}/CONTRIBUTING.md +0 -0
  51. {wizardflow-0.3.0 → wizardflow-0.4.0}/LICENSE +0 -0
  52. {wizardflow-0.3.0 → wizardflow-0.4.0}/assets/demo.gif +0 -0
  53. {wizardflow-0.3.0 → wizardflow-0.4.0}/examples/data_types.py +0 -0
  54. {wizardflow-0.3.0 → wizardflow-0.4.0}/scripts/build_ui.py +0 -0
  55. {wizardflow-0.3.0 → wizardflow-0.4.0}/src/wizardflow/_ui/_next/static/chunks/05-c3ty_6dwfk.js +0 -0
  56. {wizardflow-0.3.0 → wizardflow-0.4.0}/src/wizardflow/_ui/_next/static/chunks/09s72r58ijxwx.js +0 -0
  57. {wizardflow-0.3.0 → wizardflow-0.4.0}/src/wizardflow/_ui/_next/static/chunks/0cz1d0mv5g_q7.js +0 -0
  58. {wizardflow-0.3.0 → wizardflow-0.4.0}/src/wizardflow/_ui/_next/static/chunks/14mrh2-p_w84d.js +0 -0
  59. {wizardflow-0.3.0 → wizardflow-0.4.0}/src/wizardflow/_ui/_next/static/chunks/18clj6xsyhc_j.css +0 -0
  60. {wizardflow-0.3.0 → wizardflow-0.4.0}/src/wizardflow/_ui/_next/static/chunks/2-j1b74l3lxxu.css +0 -0
  61. {wizardflow-0.3.0 → wizardflow-0.4.0}/src/wizardflow/_ui/_next/static/chunks/27jktro2p5rq9.js +0 -0
  62. {wizardflow-0.3.0 → wizardflow-0.4.0}/src/wizardflow/_ui/_next/static/chunks/2gj5bmeov-r8f.js +0 -0
  63. {wizardflow-0.3.0 → wizardflow-0.4.0}/src/wizardflow/_ui/_next/static/chunks/310vm2bl3xxpt.js +0 -0
  64. {wizardflow-0.3.0 → wizardflow-0.4.0}/src/wizardflow/_ui/_next/static/chunks/3lt2ss7dftc2n.css +0 -0
  65. {wizardflow-0.3.0 → wizardflow-0.4.0}/src/wizardflow/_ui/_next/static/chunks/3n7dm2ojtyzwn.js +0 -0
  66. {wizardflow-0.3.0 → wizardflow-0.4.0}/src/wizardflow/_ui/_next/static/chunks/turbopack-2fblmukzx7kws.js +0 -0
  67. {wizardflow-0.3.0/src/wizardflow/_ui/_next/static/AJjiS0WLQjNzSyuZBS0Ff → wizardflow-0.4.0/src/wizardflow/_ui/_next/static/hZT2Q8C4grDYd_BRHVtw0}/_buildManifest.js +0 -0
  68. {wizardflow-0.3.0/src/wizardflow/_ui/_next/static/AJjiS0WLQjNzSyuZBS0Ff → wizardflow-0.4.0/src/wizardflow/_ui/_next/static/hZT2Q8C4grDYd_BRHVtw0}/_clientMiddlewareManifest.js +0 -0
  69. {wizardflow-0.3.0/src/wizardflow/_ui/_next/static/AJjiS0WLQjNzSyuZBS0Ff → wizardflow-0.4.0/src/wizardflow/_ui/_next/static/hZT2Q8C4grDYd_BRHVtw0}/_ssgManifest.js +0 -0
  70. {wizardflow-0.3.0 → wizardflow-0.4.0}/src/wizardflow/_ui/_next/static/media/4fa387ec64143e14-s.2tuy5pz7dlieh.woff2 +0 -0
  71. {wizardflow-0.3.0 → wizardflow-0.4.0}/src/wizardflow/_ui/_next/static/media/53b9e256198e5412-s.390ncx5urfkfu.woff2 +0 -0
  72. {wizardflow-0.3.0 → wizardflow-0.4.0}/src/wizardflow/_ui/_next/static/media/5ce348bf30bf5439-s.31988l_ccedte.woff2 +0 -0
  73. {wizardflow-0.3.0 → wizardflow-0.4.0}/src/wizardflow/_ui/_next/static/media/6306c77e7c8268e4-s.2dbetqa9o8jxf.woff2 +0 -0
  74. {wizardflow-0.3.0 → wizardflow-0.4.0}/src/wizardflow/_ui/_next/static/media/7178b3e590c64307-s.21jp631_3pja2.woff2 +0 -0
  75. {wizardflow-0.3.0 → wizardflow-0.4.0}/src/wizardflow/_ui/_next/static/media/797e433ab948586e-s.p.0r6juujl39pe6.woff2 +0 -0
  76. {wizardflow-0.3.0 → wizardflow-0.4.0}/src/wizardflow/_ui/_next/static/media/7d817b4c03b0c5f1-s.1uyisp29ctx0d.woff2 +0 -0
  77. {wizardflow-0.3.0 → wizardflow-0.4.0}/src/wizardflow/_ui/_next/static/media/8a480f0b521d4e75-s.1qq4vpdcun5oj.woff2 +0 -0
  78. {wizardflow-0.3.0 → wizardflow-0.4.0}/src/wizardflow/_ui/_next/static/media/bbc41e54d2fcbd21-s.1rgnod-3esatf.woff2 +0 -0
  79. {wizardflow-0.3.0 → wizardflow-0.4.0}/src/wizardflow/_ui/_next/static/media/caa3a2e1cccd8315-s.p.0wgildi0cnwt9.woff2 +0 -0
  80. {wizardflow-0.3.0 → wizardflow-0.4.0}/src/wizardflow/_ui/_next/static/media/fef07dbb0973bf53-s.3p2_lha1f2xer.woff2 +0 -0
  81. {wizardflow-0.3.0 → wizardflow-0.4.0}/src/wizardflow/_ui/_next/static/media/icon.09qublg69ek7b.svg +0 -0
  82. {wizardflow-0.3.0 → wizardflow-0.4.0}/src/wizardflow/_ui/icon.svg +0 -0
  83. {wizardflow-0.3.0 → wizardflow-0.4.0}/src/wizardflow/_ui/opengraph-image +0 -0
  84. {wizardflow-0.3.0 → wizardflow-0.4.0}/src/wizardflow/_ui/robots.txt +0 -0
  85. {wizardflow-0.3.0 → wizardflow-0.4.0}/src/wizardflow/cli.py +0 -0
  86. {wizardflow-0.3.0 → wizardflow-0.4.0}/src/wizardflow/constants.py +0 -0
  87. {wizardflow-0.3.0 → wizardflow-0.4.0}/src/wizardflow/py.typed +0 -0
  88. {wizardflow-0.3.0 → wizardflow-0.4.0}/src/wizardflow/reader.py +0 -0
  89. {wizardflow-0.3.0 → wizardflow-0.4.0}/tests/conftest.py +0 -0
  90. {wizardflow-0.3.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.3.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
@@ -150,7 +154,11 @@ wizardflow.end_message("msg-2") # writes msg-2
150
154
 
151
155
  Pass the **string `id`**, never a handle object — safe to hand to a callback or
152
156
  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.
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.
154
162
 
155
163
  ### Choosing message ids
156
164
 
@@ -169,8 +177,9 @@ Do **not** use a session id, user id, or thread id as the message id. Those
169
177
  identify a *conversation*, and a conversation contains many messages — after
170
178
  the first `end_message(session_id)`, every further `log(session_id, ...)` in
171
179
  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:
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:
174
183
  the viewer only displays and groups by them, so encoding meaning into them
175
184
  buys nothing. Hardcoded ids like `"msg-1"` are fine for demo scripts; `uuid4`
176
185
  is the right default for real applications.
@@ -302,9 +311,11 @@ metadata never fails extraction (the edge is just emitted plain). Call it
302
311
  **message id**, the second is the node. With `label`/`content` it records a
303
312
  payload; bare `log(id, "node")` records a visit with no payloads. The message
304
313
  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.
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.
308
319
  - `reinit(name=None, description=None, meta=None)` — start a **new trace
309
320
  file** (fresh timestamped name), keeping the graph and output configuration.
310
321
  Use it at natural boundaries of a long-lived process — a new user session, a
@@ -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
@@ -103,7 +107,11 @@ wizardflow.end_message("msg-2") # writes msg-2
103
107
 
104
108
  Pass the **string `id`**, never a handle object — safe to hand to a callback or
105
109
  across threads. A message is created on first reference and finalized by
106
- `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.
107
115
 
108
116
  ### Choosing message ids
109
117
 
@@ -122,8 +130,9 @@ Do **not** use a session id, user id, or thread id as the message id. Those
122
130
  identify a *conversation*, and a conversation contains many messages — after
123
131
  the first `end_message(session_id)`, every further `log(session_id, ...)` in
124
132
  that session raises, because that "message" has already ended. If you want the
125
- session visible in the trace, put it in `init(meta=...)` or in the message
126
- title (`end_message(msg_id, title=...)`) — never in the id. Ids are opaque:
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:
127
136
  the viewer only displays and groups by them, so encoding meaning into them
128
137
  buys nothing. Hardcoded ids like `"msg-1"` are fine for demo scripts; `uuid4`
129
138
  is the right default for real applications.
@@ -255,9 +264,11 @@ metadata never fails extraction (the edge is just emitted plain). Call it
255
264
  **message id**, the second is the node. With `label`/`content` it records a
256
265
  payload; bare `log(id, "node")` records a visit with no payloads. The message
257
266
  is created on first reference; this only accumulates in memory.
258
- - `end_message(id, title=None)` — finalize a message and append it to the
259
- trace; returns the current trace path. The **only** call that touches disk.
260
- 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.
261
272
  - `reinit(name=None, description=None, meta=None)` — start a **new trace
262
273
  file** (fresh timestamped name), keeping the graph and output configuration.
263
274
  Use it at natural boundaries of a long-lived process — a new user session, a
@@ -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.`
@@ -43,6 +43,16 @@ wiz = wizardflow.init(
43
43
  ("retriever", "generator"), # branches rejoin
44
44
  ("generator", "final_response"),
45
45
  ],
46
+ # Optional one-line description per node (keyed by id), shown behind an
47
+ # info icon in the viewer when the node is selected. Plumbing nodes
48
+ # (user_input, final_response) are intentionally left out.
49
+ node_descriptions={
50
+ "router": "Classifies the request and picks a branch: planner or retriever.",
51
+ "planner": "Decomposes a tool-using request into concrete tool calls.",
52
+ "retriever": "Fetches the most relevant documents for the query.",
53
+ "tool_node": "Runs the planned tool calls against external APIs.",
54
+ "generator": "Writes the answer from the tool result or retrieved docs.",
55
+ },
46
56
  )
47
57
 
48
58
  # msg-1 — takes the planner / tool branch.
@@ -55,7 +65,14 @@ wizardflow.log("msg-1", "tool_node") # ran the tool, logged no payload
55
65
  wizardflow.log("msg-1", "generator", "llm_input", "Answer using the tool result...")
56
66
  wizardflow.log("msg-1", "generator", "llm_output", "It's 19C and partly cloudy in Berlin.")
57
67
  wizardflow.log("msg-1", "final_response", "Output", "It's 19C and partly cloudy in Berlin.")
58
- wizardflow.end_message("msg-1", title="Weather in Berlin")
68
+ # Optional meta: flat facts about the message as a whole (short scalars only),
69
+ # shown on the message's chip in the viewer. Here it records which branch ran
70
+ # and how the run turned out.
71
+ wizardflow.end_message(
72
+ "msg-1",
73
+ title="Weather in Berlin",
74
+ meta={"branch": "planner", "outcome": "answered", "latency_ms": 10210},
75
+ )
59
76
 
60
77
  # msg-2 — takes the retriever branch (skips planner/tool entirely).
61
78
  wizardflow.log("msg-2", "user_input", "Input", "Summarize the attached research paper.")
@@ -71,6 +88,10 @@ wizardflow.log("msg-2", "generator", "llm_output",
71
88
  "The paper proposes a sparse attention variant with near-linear cost.")
72
89
  wizardflow.log("msg-2", "final_response", "Output",
73
90
  "The paper proposes a sparse attention variant with near-linear cost.")
74
- wizardflow.end_message("msg-2", title="Summarize research paper")
91
+ wizardflow.end_message(
92
+ "msg-2",
93
+ title="Summarize research paper",
94
+ meta={"branch": "retriever", "outcome": "answered", "docs_used": 2, "latency_ms": 7130},
95
+ )
75
96
 
76
97
  print(f"wrote {wiz.current_path}")
@@ -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,33 +62,42 @@ 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 the next node.</li>
69
+ <li><span class="label">planner</span> — Decomposes the request into concrete tool calls.</li>
70
+ <li><span class="label">tool_node</span> — Runs the planned tool calls against external APIs.</li>
71
+ </ul>
72
+ </section>
60
73
  <section class="message">
61
74
  <h2>msg-1</h2>
62
75
  <section class="step">
63
- <h3>user_input <span class="time">· 21:39:58</span></h3>
76
+ <h3>user_input <span class="time">· 13:13:58</span></h3>
64
77
  <p class="scalar"><span class="label">Input</span> <code>What&#x27;s the weather in Berlin?</code></p>
65
78
  </section>
66
79
  <section class="step">
67
- <h3>router <span class="time">· 21:39:58</span></h3>
80
+ <h3>router <span class="time">· 13:13:58</span></h3>
68
81
  <p class="scalar"><span class="label">llm_input</span> <code>Route this request...</code></p>
69
82
  <p class="scalar"><span class="label">llm_output</span> <code>{&quot;route&quot;: &quot;planner&quot;}</code></p>
70
83
  </section>
71
84
  <section class="step">
72
- <h3>tool_node <span class="time">· 21:39:58</span></h3>
85
+ <h3>tool_node <span class="time">· 13:13:58</span></h3>
73
86
  </section>
74
87
  <section class="step">
75
- <h3>final_response <span class="time">· 21:39:58</span></h3>
88
+ <h3>final_response <span class="time">· 13:13:58</span></h3>
76
89
  <p class="scalar"><span class="label">Output</span> <code>It&#x27;s 19C and partly cloudy in Berlin.</code></p>
77
90
  </section>
78
91
  </section>
79
92
  <section class="message">
80
93
  <h2>Summarize the paper</h2>
94
+ <p class="msg-meta"><span class="label">outcome</span>: answered · <span class="label">latency_ms</span>: 320</p>
81
95
  <section class="step">
82
- <h3>user_input <span class="time">· 21:39:58</span></h3>
96
+ <h3>user_input <span class="time">· 13:13:58</span></h3>
83
97
  <p class="scalar"><span class="label">Input</span> <code>Summarize the paper.</code></p>
84
98
  </section>
85
99
  <section class="step">
86
- <h3>router <span class="time">· 21:39:58</span></h3>
100
+ <h3>router <span class="time">· 13:13:58</span></h3>
87
101
  <p class="scalar"><span class="label">llm_output</span> <code>{&quot;route&quot;: &quot;planner&quot;}</code></p>
88
102
  </section>
89
103
  </section>
@@ -21,30 +21,36 @@ flowchart TD
21
21
  n3 --> n4
22
22
  ```
23
23
 
24
+ - **router** — Classifies the request and picks the next node.
25
+ - **planner** — Decomposes the request into concrete tool calls.
26
+ - **tool_node** — Runs the planned tool calls against external APIs.
27
+
24
28
  ## msg-1
25
29
 
26
- ### user_input · 21:39:58
30
+ ### user_input · 13:13:58
27
31
 
28
32
  **Input**: `What's the weather in Berlin?`
29
33
 
30
- ### router · 21:39:58
34
+ ### router · 13:13:58
31
35
 
32
36
  **llm_input**: `Route this request...`
33
37
 
34
38
  **llm_output**: `{"route": "planner"}`
35
39
 
36
- ### tool_node · 21:39:58
40
+ ### tool_node · 13:13:58
37
41
 
38
- ### final_response · 21:39:58
42
+ ### final_response · 13:13:58
39
43
 
40
44
  **Output**: `It's 19C and partly cloudy in Berlin.`
41
45
 
42
46
  ## Summarize the paper
43
47
 
44
- ### user_input · 21:39:58
48
+ **outcome**: answered · **latency_ms**: 320
49
+
50
+ ### user_input · 13:13:58
45
51
 
46
52
  **Input**: `Summarize the paper.`
47
53
 
48
- ### router · 21:39:58
54
+ ### router · 13:13:58
49
55
 
50
56
  **llm_output**: `{"route": "planner"}`
@@ -26,6 +26,15 @@ wiz = wizardflow.init(
26
26
  ("planner", "tool_node"),
27
27
  ("tool_node", "final_response"),
28
28
  ],
29
+ # Optional: a one-line description per node, keyed by node id. It shows
30
+ # behind an info icon in the viewer when the node is selected. Plumbing
31
+ # nodes (user_input, final_response) are left out on purpose — not every
32
+ # node needs one.
33
+ node_descriptions={
34
+ "router": "Classifies the request and picks the next node.",
35
+ "planner": "Decomposes the request into concrete tool calls.",
36
+ "tool_node": "Runs the planned tool calls against external APIs.",
37
+ },
29
38
  )
30
39
 
31
40
  # Message 1 — each log names its message ("msg-1") in the first argument.
@@ -36,10 +45,16 @@ wizardflow.log("msg-1", "tool_node") # visited, no payloads
36
45
  wizardflow.log("msg-1", "final_response", "Output", "It's 19C and partly cloudy in Berlin.")
37
46
  wizardflow.end_message("msg-1") # <- writes the file; msg-1 is now persisted
38
47
 
39
- # Message 2 — same shape; an optional title gives the message a human title.
48
+ # Message 2 — same shape; an optional title gives the message a human title,
49
+ # and optional meta attaches flat facts about the message as a whole (shown on
50
+ # the message's chip in the viewer). Keep meta values short scalars.
40
51
  wizardflow.log("msg-2", "user_input", "Input", "Summarize the paper.")
41
52
  wizardflow.log("msg-2", "router", "llm_output", '{"route": "planner"}')
42
- wizardflow.end_message("msg-2", title="Summarize the paper")
53
+ wizardflow.end_message(
54
+ "msg-2",
55
+ title="Summarize the paper",
56
+ meta={"outcome": "answered", "latency_ms": 320},
57
+ )
43
58
  # -> the part file now contains msg-1 and msg-2
44
59
 
45
60
  print(f"wrote {wiz.current_path}")
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "wizardflow"
7
- version = "0.3.0"
7
+ version = "0.4.0"
8
8
  description = "Python SDK for recording agent flows into the WizardFlow / AgentTrace file format."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -155,8 +155,12 @@ def log(
155
155
  get_default().log(id, node, label, content)
156
156
 
157
157
 
158
- def end_message(id: str, title: Optional[str] = None) -> str:
159
- return get_default().end_message(id, title=title)
158
+ def end_message(
159
+ id: str,
160
+ title: Optional[str] = None,
161
+ meta: Optional[Dict[str, Any]] = None,
162
+ ) -> str:
163
+ return get_default().end_message(id, title=title, meta=meta)
160
164
 
161
165
 
162
166
  def to_dict() -> Dict[str, Any]:
@@ -18,6 +18,15 @@ def short_time(timestamp: Any) -> str:
18
18
  return clock.split(".", 1)[0] if "." in clock else clock
19
19
 
20
20
 
21
+ def meta_text(value: Any) -> str:
22
+ """Render a meta value for display. Meta values are short scalars by
23
+ contract; an out-of-contract dict/list still renders readably as compact
24
+ JSON instead of its Python repr."""
25
+ if isinstance(value, (dict, list)):
26
+ return json.dumps(value, ensure_ascii=False)
27
+ return str(value)
28
+
29
+
21
30
  def classify_value(value: Any) -> Tuple[str, str]:
22
31
  """Classify a payload value for rendering, returning ``(kind, text)``.
23
32