dyui 0.1.0__py3-none-any.whl

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.
dyui/__init__.py ADDED
@@ -0,0 +1,61 @@
1
+ """DyUI -- a dynamic-UI layer for any LangGraph agent.
2
+
3
+ Emit rich, live UI "cards" from inside any LangGraph node or tool, with any LLM
4
+ (or none). The companion ``@dyui/react`` package renders those cards on the
5
+ frontend via a pluggable registry that also supports raw custom HTML.
6
+
7
+ Quick start (agent side)::
8
+
9
+ from dyui import emit, ui_tool
10
+
11
+ @ui_tool("stat", title="Weather", icon="cloud", accent="cyan")
12
+ def get_weather(city: str) -> dict:
13
+ return {"label": city, "value": 21, "unit": "C"}
14
+
15
+ # or imperatively, anywhere inside a node:
16
+ emit("table", {"columns": ["a", "b"], "rows": [[1, 2]]}, title="Results")
17
+
18
+ Quick start (server side)::
19
+
20
+ from dyui.server import create_dyui_app
21
+ app = create_dyui_app(my_compiled_graph) # uvicorn dyui_app:app
22
+ """
23
+
24
+ from .__version__ import __version__
25
+ from .emit import Card, emit, ui_tool, update
26
+ from .events import DYUI_KEY, Status, UIEvent, is_dyui_payload, parse_envelope
27
+ from .stream import astream_ui, collect, collect_events
28
+ from .templates import HtmlTemplates, render_template
29
+
30
+ __all__ = [
31
+ "__version__",
32
+ "emit",
33
+ "update",
34
+ "ui_tool",
35
+ "Card",
36
+ "UIEvent",
37
+ "Status",
38
+ "DYUI_KEY",
39
+ "is_dyui_payload",
40
+ "parse_envelope",
41
+ "astream_ui",
42
+ "collect",
43
+ "collect_events",
44
+ "HtmlTemplates",
45
+ "render_template",
46
+ # lazily provided (need FastAPI):
47
+ "create_dyui_app",
48
+ "add_dyui_routes",
49
+ "mount_ui",
50
+ "render_ui_page",
51
+ ]
52
+
53
+
54
+ def __getattr__(name: str):
55
+ # Lazily expose the FastAPI helpers so importing ``dyui`` never hard-requires
56
+ # FastAPI (it's an optional ``[server]`` extra).
57
+ if name in {"create_dyui_app", "add_dyui_routes", "mount_ui", "render_ui_page"}:
58
+ from . import server
59
+
60
+ return getattr(server, name)
61
+ raise AttributeError(f"module 'dyui' has no attribute {name!r}")
dyui/__version__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
dyui/cli.py ADDED
@@ -0,0 +1,347 @@
1
+ """The ``dyui`` command-line interface.
2
+
3
+ Commands
4
+ --------
5
+ * ``dyui claude init <file.py>`` / ``dyui gemini init <file.py>`` /
6
+ ``dyui codex init <file.py>`` -- hand your LangGraph agent file to a coding
7
+ agent (Claude Code / Gemini CLI / Codex CLI) together with a full brief on how
8
+ to use DyUI, so it designs + builds custom cards for your specific agent,
9
+ confirms the plan with you, and wires them into the served UI.
10
+ * ``dyui serve [target]`` -- run the agent's served dynamic-UI in one command
11
+ (uvicorn under the hood). ``target`` is a ``file.py`` or ``module:attr``;
12
+ auto-discovered when omitted.
13
+ * ``dyui export <name>`` -- scaffold the whole dynamic UI as an editable React
14
+ project (``@dyui/react``) in ``./<name>``.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import argparse
20
+ import importlib.util
21
+ import os
22
+ import shutil
23
+ import subprocess
24
+ import sys
25
+ from pathlib import Path
26
+ from typing import Any, Optional
27
+
28
+ from .__version__ import __version__
29
+
30
+ # --------------------------------------------------------------------------- #
31
+ # The brief handed to coding agents on `dyui <agent> init`.
32
+ # --------------------------------------------------------------------------- #
33
+ AGENT_BRIEF = r'''# DyUI integration brief (for a coding agent)
34
+
35
+ You are helping a developer add a **dynamic UI** to their existing LangGraph
36
+ agent using the **DyUI** Python package (already `pip install`ed). Your job:
37
+ read their agent code, design a set of UI **cards** tailored to what the agent
38
+ does, get the developer's confirmation, then implement and wire it up.
39
+
40
+ ## How DyUI works (the essentials)
41
+
42
+ DyUI streams "cards" from inside a LangGraph graph to a frontend over LangGraph's
43
+ native `custom` stream channel. You emit cards from any node or tool:
44
+
45
+ ```python
46
+ from dyui import emit, ui_tool, Card, HtmlTemplates
47
+
48
+ # 1) Imperative emit (call anywhere inside a node/tool):
49
+ emit("table", {"columns": ["city","temp"], "rows": [["Berlin",21]]},
50
+ title="Weather", icon="cloud", accent="cyan")
51
+
52
+ # 2) Decorator: auto pending -> done/error around a tool:
53
+ @ui_tool("stat", title="Sum", icon="calculator", accent="emerald")
54
+ def add(a: float, b: float) -> dict:
55
+ return {"label": f"{a}+{b}", "value": a + b} # dict becomes the card props
56
+
57
+ # 3) Context manager: long work, one card updated in place:
58
+ with Card("progress", title="Indexing") as card:
59
+ for i in range(1, 4):
60
+ card.progress({"value": i, "max": 3})
61
+ card.done({"value": 3, "max": 3, "label": "Done"})
62
+ ```
63
+
64
+ `emit(component, props, *, id=None, status="done", surface="default", title=None,
65
+ icon=None, accent=None, ttl_ms=None)`. Re-emit with the same `id` to update a card
66
+ (lifecycle: `pending` -> `active` -> `done`/`error`).
67
+
68
+ ## Built-in card components (no frontend code needed)
69
+
70
+ | component | props |
71
+ |------------|-------|
72
+ | `text` | `{text}` |
73
+ | `markdown` | `{text}` (supports #/##/### , **bold**, `code`, - lists) |
74
+ | `table` | `{columns: string[], rows: any[][]}` |
75
+ | `stat` | `{label, value, unit?, delta?}` |
76
+ | `progress` | `{value, max?, label?}` |
77
+ | `list` | `{items: (string | {title, subtitle?, badge?})[]}` |
78
+ | `keyvalue` | `{data: {k: v}}` |
79
+ | `json` | any |
80
+ | `alert` | `{text, level?: info|success|warning|error, title?}` |
81
+ | `image` | `{src, alt?, caption?}` |
82
+ | `html` | `{html}` (raw, sanitized) -- for fully custom markup |
83
+
84
+ `icon` hints (served UI): `search, calculator, cloud, check, list, sparkles, plus`.
85
+ `accent` is any CSS color or one of `cyan, violet, emerald, amber, rose`.
86
+
87
+ ## Custom cards via HTML templates (preferred for bespoke visuals)
88
+
89
+ Put `.html` files in a `./cards/` folder; `{{ key }}` is HTML-escaped, `{{{ key }}}`
90
+ is raw. Then:
91
+
92
+ ```python
93
+ cards = HtmlTemplates("cards")
94
+ cards.emit("invoice", {"number": 42, "total": "$1,200"}, title="Invoice", accent="emerald")
95
+ ```
96
+
97
+ Reserved kwargs (`id,status,surface,title,icon,accent,ttl_ms`) are card config —
98
+ put template values with those names inside the dict.
99
+
100
+ ## Serving (already handled by the package)
101
+
102
+ ```python
103
+ from dyui.server import create_dyui_app
104
+ app = create_dyui_app(graph, input_adapter=lambda b: {"messages": [("user", b["input"])]})
105
+ ```
106
+ The developer runs `dyui serve` (or `uvicorn module:app`) and opens the browser —
107
+ the package ships an animated chat UI that renders all the cards above.
108
+
109
+ ## YOUR TASK (do these in order)
110
+
111
+ 1. Read the developer's agent file (shown below / at the given path). Identify the
112
+ nodes, tools, and the data each produces.
113
+ 2. **Propose** a concrete set of cards: for each meaningful tool/node output, pick
114
+ a built-in component or design an HTML template, with titles/icons/accents.
115
+ Present this plan as a short table.
116
+ 3. **ASK the developer to confirm** (or adjust) the plan before writing code. Do
117
+ not skip this confirmation step.
118
+ 4. On approval, implement:
119
+ - Add `emit(...)` / `@ui_tool(...)` calls (or `HtmlTemplates`) at the right
120
+ points so cards stream as the agent runs.
121
+ - Create any `./cards/*.html` templates you designed.
122
+ - Ensure the file exposes `app = create_dyui_app(graph, ...)` with an
123
+ `input_adapter` matching the agent's expected input.
124
+ 5. Tell the developer to run `dyui serve <file>` and open the browser.
125
+
126
+ Keep changes minimal and faithful to the existing agent logic — you are adding a
127
+ UI layer, not rewriting the agent.
128
+ '''
129
+
130
+
131
+ def _c(text: str, code: str = "1") -> str:
132
+ if not sys.stdout.isatty():
133
+ return text
134
+ return f"\033[{code}m{text}\033[0m"
135
+
136
+
137
+ # --------------------------------------------------------------------------- #
138
+ # `dyui <agent> init <file>`
139
+ # --------------------------------------------------------------------------- #
140
+ AGENT_LAUNCHERS = {
141
+ # agent -> function(prompt_path) -> argv list
142
+ "claude": lambda p: ["claude", f"Read {p} and follow it exactly."],
143
+ "gemini": lambda p: ["gemini", "-i", f"Read {p} and follow it exactly."],
144
+ "codex": lambda p: ["codex", f"Read {p} and follow it exactly."],
145
+ }
146
+ AGENT_INSTALL_HINT = {
147
+ "claude": "npm install -g @anthropic-ai/claude-code (or see claude.com/claude-code)",
148
+ "gemini": "npm install -g @google/gemini-cli",
149
+ "codex": "npm install -g @openai/codex",
150
+ }
151
+
152
+
153
+ def cmd_init(agent: str, file: str) -> int:
154
+ src = Path(file)
155
+ if not src.exists():
156
+ print(_c(f"error: file not found: {file}", "31"))
157
+ return 1
158
+
159
+ code = src.read_text(encoding="utf-8")
160
+ prompt = (
161
+ AGENT_BRIEF
162
+ + f"\n\n## The developer's LangGraph agent: `{src.name}`\n\n"
163
+ + f"```python\n{code}\n```\n"
164
+ )
165
+ out_dir = Path(".dyui")
166
+ out_dir.mkdir(exist_ok=True)
167
+ prompt_path = (out_dir / "INIT_PROMPT.md").resolve()
168
+ prompt_path.write_text(prompt, encoding="utf-8")
169
+
170
+ print(_c(f"\n DyUI :: {agent} init", "1"))
171
+ print(f" Brief + your agent written to {_c(str(prompt_path), '36')}")
172
+
173
+ launcher = AGENT_LAUNCHERS[agent]
174
+ argv = launcher(str(prompt_path))
175
+ exe = shutil.which(argv[0])
176
+
177
+ manual = f'{argv[0]} "Read {prompt_path} and follow it."'
178
+ if exe is None:
179
+ print(_c(f"\n '{argv[0]}' CLI not found.", "33"))
180
+ print(f" Install it: {AGENT_INSTALL_HINT[agent]}")
181
+ print(f" Then run: {manual}\n")
182
+ return 2
183
+
184
+ # Let users (or tests) skip auto-launch and just get the command to run.
185
+ if os.environ.get("DYUI_NO_LAUNCH"):
186
+ print(f" Run this to start the session:\n {manual}\n")
187
+ return 0
188
+
189
+ argv[0] = exe
190
+ print(f" Launching {_c(argv[0], '36')} - it will design cards and confirm with you.\n")
191
+ try:
192
+ # On Windows the resolved CLI is a .CMD shim and must go through the shell.
193
+ if os.name == "nt":
194
+ return subprocess.run(subprocess.list2cmdline(argv), shell=True).returncode
195
+ return subprocess.run(argv).returncode
196
+ except (FileNotFoundError, OSError) as exc:
197
+ print(_c(f"\n could not launch {agent}: {exc}", "33"))
198
+ print(f" Run it manually: {manual}\n")
199
+ return 2
200
+ except KeyboardInterrupt:
201
+ return 130
202
+
203
+
204
+ # --------------------------------------------------------------------------- #
205
+ # `dyui serve`
206
+ # --------------------------------------------------------------------------- #
207
+ def _load_module_from_path(path: Path) -> Any:
208
+ # Make sibling imports (e.g. `from calculator import ...`) resolve.
209
+ sys.path.insert(0, str(path.resolve().parent))
210
+ spec = importlib.util.spec_from_file_location(path.stem, path)
211
+ if spec is None or spec.loader is None:
212
+ raise ImportError(f"cannot import {path}")
213
+ module = importlib.util.module_from_spec(spec)
214
+ # Register before exec so typing.get_type_hints (used by LangGraph on
215
+ # TypedDict state schemas) can resolve the module's own annotations.
216
+ sys.modules[spec.name] = module
217
+ spec.loader.exec_module(module)
218
+ return module
219
+
220
+
221
+ def _discover_target() -> Optional[str]:
222
+ for name in ("app.py", "agent.py", "main.py", "graph.py"):
223
+ if Path(name).exists():
224
+ return name
225
+ return None
226
+
227
+
228
+ def _resolve_app(target: Optional[str], *, serve_ui: bool, title: str) -> Any:
229
+ """Return a FastAPI app from a target ``file.py`` or ``module:attr``."""
230
+ from .server import create_dyui_app
231
+
232
+ if target is None:
233
+ target = _discover_target()
234
+ if target is None:
235
+ raise SystemExit(
236
+ "error: no target given and none of app.py/agent.py/main.py found.\n"
237
+ " usage: dyui serve <file.py | module:attr>"
238
+ )
239
+ print(_c(f" discovered {target}", "36"))
240
+
241
+ attr = None
242
+ if ":" in target and not target.endswith(".py"):
243
+ mod_name, attr = target.split(":", 1)
244
+ module = __import__(mod_name, fromlist=["*"])
245
+ else:
246
+ path = Path(target)
247
+ if not path.exists():
248
+ raise SystemExit(f"error: file not found: {target}")
249
+ module = _load_module_from_path(path)
250
+
251
+ # Prefer an explicit attr, else `app`, else wrap a `graph`.
252
+ obj = getattr(module, attr) if attr else getattr(module, "app", None)
253
+ if obj is not None:
254
+ return obj
255
+ graph = getattr(module, "graph", None)
256
+ if graph is not None:
257
+ print(_c(" no `app` found — wrapping `graph` with create_dyui_app()", "36"))
258
+ return create_dyui_app(graph, serve_ui=serve_ui, ui_title=title)
259
+ raise SystemExit(
260
+ f"error: {target} exposes neither `app` nor `graph`.\n"
261
+ " Define `app = create_dyui_app(graph)` or expose a compiled `graph`."
262
+ )
263
+
264
+
265
+ def cmd_serve(target: Optional[str], host: str, port: int, no_ui: bool, title: str) -> int:
266
+ try:
267
+ import uvicorn
268
+ except ImportError:
269
+ print(_c("error: uvicorn not installed. Run: pip install 'dyui[server]'", "31"))
270
+ return 1
271
+
272
+ app = _resolve_app(target, serve_ui=not no_ui, title=title)
273
+ print(_c(f"\n DyUI serving on http://{host}:{port}", "1")
274
+ + (f" (UI at /)" if not no_ui else " (UI disabled)"))
275
+ print(f" stream endpoint: POST http://{host}:{port}/dyui/stream\n")
276
+ uvicorn.run(app, host=host, port=port)
277
+ return 0
278
+
279
+
280
+ # --------------------------------------------------------------------------- #
281
+ # `dyui export <name>`
282
+ # --------------------------------------------------------------------------- #
283
+ def cmd_export(name: str, stream_url: str, force: bool, local: Optional[str]) -> int:
284
+ from .scaffold import write_react_project
285
+
286
+ dest = Path(name)
287
+ if dest.exists() and any(dest.iterdir()) and not force:
288
+ print(_c(f"error: ./{name} exists and is not empty (use --force to overwrite)", "31"))
289
+ return 1
290
+ files = write_react_project(
291
+ dest, project_name=name, stream_url=stream_url, local_react=local
292
+ )
293
+ print(_c(f"\n Exported React project to ./{name}/", "1"))
294
+ for f in files:
295
+ print(f" {f}")
296
+ print(
297
+ "\n Next:\n"
298
+ f" cd {name}\n"
299
+ " npm install\n"
300
+ " npm run dev\n\n"
301
+ " Edit src/cards/ to add custom card components; they render the same\n"
302
+ " events your `dyui serve` UI shows.\n"
303
+ )
304
+ return 0
305
+
306
+
307
+ # --------------------------------------------------------------------------- #
308
+ # arg parsing
309
+ # --------------------------------------------------------------------------- #
310
+ def build_parser() -> argparse.ArgumentParser:
311
+ p = argparse.ArgumentParser(prog="dyui", description="Dynamic UI for LangGraph agents.")
312
+ p.add_argument("--version", action="version", version=f"dyui {__version__}")
313
+ sub = p.add_subparsers(dest="cmd", required=True)
314
+
315
+ for agent in ("claude", "gemini", "codex"):
316
+ ap = sub.add_parser(agent, help=f"use {agent} to design + build cards for your agent")
317
+ ap.add_argument("action", choices=["init"], help="init: analyse a file and build cards")
318
+ ap.add_argument("file", help="your LangGraph agent .py file")
319
+
320
+ sp = sub.add_parser("serve", help="run the agent's served dynamic UI")
321
+ sp.add_argument("target", nargs="?", help="file.py or module:attr (auto-discovered if omitted)")
322
+ sp.add_argument("--host", default="127.0.0.1")
323
+ sp.add_argument("--port", type=int, default=8000)
324
+ sp.add_argument("--no-ui", action="store_true", help="serve only the stream endpoint")
325
+ sp.add_argument("--title", default="DyUI")
326
+
327
+ ep = sub.add_parser("export", help="export the dynamic UI as a React project")
328
+ ep.add_argument("name", help="project directory name")
329
+ ep.add_argument("--stream-url", default="http://localhost:8000/dyui/stream")
330
+ ep.add_argument("--local", help="use a local @dyui/react build path instead of the npm package")
331
+ ep.add_argument("--force", action="store_true")
332
+ return p
333
+
334
+
335
+ def main(argv: Optional[list[str]] = None) -> int:
336
+ args = build_parser().parse_args(argv)
337
+ if args.cmd in ("claude", "gemini", "codex"):
338
+ return cmd_init(args.cmd, args.file)
339
+ if args.cmd == "serve":
340
+ return cmd_serve(args.target, args.host, args.port, args.no_ui, args.title)
341
+ if args.cmd == "export":
342
+ return cmd_export(args.name, args.stream_url, args.force, args.local)
343
+ return 1
344
+
345
+
346
+ if __name__ == "__main__":
347
+ raise SystemExit(main())