agentbuild 0.0.1__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.
- agentbuild/__init__.py +10 -0
- agentbuild/jupyter.py +162 -0
- agentbuild/py.typed +0 -0
- agentbuild-0.0.1.dist-info/METADATA +80 -0
- agentbuild-0.0.1.dist-info/RECORD +6 -0
- agentbuild-0.0.1.dist-info/WHEEL +4 -0
agentbuild/__init__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Tools to make agent research easier."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
__version__ = version("agentbuild")
|
|
7
|
+
except PackageNotFoundError: # pragma: no cover - package not installed
|
|
8
|
+
__version__ = "0.0.0"
|
|
9
|
+
|
|
10
|
+
__all__ = ["__version__"]
|
agentbuild/jupyter.py
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""IPython magics for working with Jupyter notebooks.
|
|
2
|
+
|
|
3
|
+
Load them in a notebook with ``%load_ext agentbuild.jupyter``. Notebooks exported from
|
|
4
|
+
Databricks work too: their cells are identified by the Databricks cell ID (``nuid``) and can
|
|
5
|
+
also be referred to by their title.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from collections.abc import Callable
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from IPython.core.error import UsageError
|
|
16
|
+
from IPython.core.magic import Magics, line_magic, magics_class
|
|
17
|
+
from IPython.core.magic_arguments import argument, magic_arguments, parse_argstring
|
|
18
|
+
|
|
19
|
+
Cell = dict[str, Any]
|
|
20
|
+
|
|
21
|
+
DATABRICKS_CELL_METADATA = "application/vnd.databricks.v1+cell"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def read_notebook_cells(path: str | Path) -> list[Cell]:
|
|
25
|
+
"""Return the cells of a notebook file."""
|
|
26
|
+
with open(Path(path).expanduser(), encoding="utf-8") as f:
|
|
27
|
+
notebook = json.load(f)
|
|
28
|
+
if notebook.get("nbformat") != 4:
|
|
29
|
+
raise ValueError(f"{path} is not an nbformat 4 notebook")
|
|
30
|
+
return notebook["cells"]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def cell_source(cell: Cell) -> str:
|
|
34
|
+
source = cell.get("source", "")
|
|
35
|
+
return "".join(source) if isinstance(source, list) else source
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _databricks_metadata(cell: Cell) -> dict[str, Any]:
|
|
39
|
+
return cell.get("metadata", {}).get(DATABRICKS_CELL_METADATA) or {}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def cell_ids(cell: Cell) -> list[str]:
|
|
43
|
+
"""Return the cell's Jupyter ID and Databricks ID (``nuid``), whichever it has."""
|
|
44
|
+
ids = [cell.get("id"), _databricks_metadata(cell).get("nuid")]
|
|
45
|
+
return [i for i in ids if i]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def cell_title(cell: Cell) -> str:
|
|
49
|
+
"""Return the cell's Databricks title, or "" if it has none."""
|
|
50
|
+
return _databricks_metadata(cell).get("title") or ""
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def find_cell(cells: list[Cell], ref: str) -> Cell:
|
|
54
|
+
"""Return the one cell that ``ref`` names.
|
|
55
|
+
|
|
56
|
+
``ref`` is tried, in order, as an exact cell ID, an exact Databricks title, a title
|
|
57
|
+
ignoring case, and the start of a cell ID.
|
|
58
|
+
"""
|
|
59
|
+
matchers: list[Callable[[Cell], bool]] = [
|
|
60
|
+
lambda cell: ref in cell_ids(cell),
|
|
61
|
+
lambda cell: ref == cell_title(cell),
|
|
62
|
+
lambda cell: ref.casefold() == cell_title(cell).casefold(),
|
|
63
|
+
lambda cell: any(i.startswith(ref) for i in cell_ids(cell)),
|
|
64
|
+
]
|
|
65
|
+
for matches in matchers:
|
|
66
|
+
positions = [i for i, cell in enumerate(cells) if matches(cell)]
|
|
67
|
+
if len(positions) == 1:
|
|
68
|
+
return cells[positions[0]]
|
|
69
|
+
if positions:
|
|
70
|
+
listed = ", ".join(f"#{i}" for i in positions)
|
|
71
|
+
raise ValueError(f"{ref!r} matches more than one cell: {listed}")
|
|
72
|
+
hint = ""
|
|
73
|
+
if not any(map(cell_ids, cells)):
|
|
74
|
+
hint = " (the notebook has no cell IDs; open and save it in a recent Jupyter to add them)"
|
|
75
|
+
raise ValueError(f"no cell matches {ref!r}{hint}")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def join_cells(cells: list[Cell], refs: list[str]) -> str:
|
|
79
|
+
"""Return the code of the cells that ``refs`` name, joined in the order given."""
|
|
80
|
+
chosen = [find_cell(cells, ref) for ref in refs]
|
|
81
|
+
for ref, cell in zip(refs, chosen, strict=True):
|
|
82
|
+
cell_type = cell.get("cell_type")
|
|
83
|
+
if cell_type != "code":
|
|
84
|
+
raise ValueError(f"{ref!r} is a {cell_type} cell, not a code cell")
|
|
85
|
+
return "\n\n".join(cell_source(cell).strip("\n") for cell in chosen)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def short_ids(ids: list[str | None]) -> list[str]:
|
|
89
|
+
"""Shorten IDs to the shortest length, at least 8, at which they stay unique."""
|
|
90
|
+
real = [i for i in ids if i]
|
|
91
|
+
length = 8
|
|
92
|
+
while len({i[:length] for i in real}) < len(set(real)):
|
|
93
|
+
length += 1
|
|
94
|
+
return [i[:length] if i else "-" for i in ids]
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def format_cell_table(cells: list[Cell]) -> str:
|
|
98
|
+
"""Return a table of each cell's position, ID, type, title and first line."""
|
|
99
|
+
ids = short_ids([next(iter(cell_ids(cell)), None) for cell in cells])
|
|
100
|
+
titles = [cell_title(cell) for cell in cells]
|
|
101
|
+
has_titles = any(titles)
|
|
102
|
+
header = ["#", "id", "type", *(["title"] if has_titles else []), "first line"]
|
|
103
|
+
rows = [header]
|
|
104
|
+
for i, (cell, short_id, title) in enumerate(zip(cells, ids, titles, strict=True)):
|
|
105
|
+
first_line = next(iter(cell_source(cell).strip().splitlines()), "")
|
|
106
|
+
title_column = [title[:40]] if has_titles else []
|
|
107
|
+
rows.append([str(i), short_id, cell.get("cell_type", "?"), *title_column, first_line[:60]])
|
|
108
|
+
widths = [max(len(row[k]) for row in rows) for k in range(len(header))]
|
|
109
|
+
return "\n".join(
|
|
110
|
+
" ".join(value.ljust(width) for value, width in zip(row, widths, strict=True)).rstrip()
|
|
111
|
+
for row in rows
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _unquote(value: str) -> str:
|
|
116
|
+
# IPython splits magic arguments without removing quotes, so that Windows paths keep
|
|
117
|
+
# their backslashes. Remove one pair of matching quotes here instead.
|
|
118
|
+
if len(value) >= 2 and value[0] == value[-1] and value[0] in "\"'":
|
|
119
|
+
return value[1:-1]
|
|
120
|
+
return value
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@magics_class
|
|
124
|
+
class NotebookMagics(Magics):
|
|
125
|
+
@magic_arguments()
|
|
126
|
+
@argument("path", help="Notebook file, absolute or relative to the working directory.")
|
|
127
|
+
@argument(
|
|
128
|
+
"-c",
|
|
129
|
+
"--cells",
|
|
130
|
+
nargs="+",
|
|
131
|
+
metavar="CELL",
|
|
132
|
+
help=(
|
|
133
|
+
"Cells to copy, in the order to paste them. Each is a cell ID, the start of one, "
|
|
134
|
+
"or a Databricks cell title (quote titles that contain spaces)."
|
|
135
|
+
),
|
|
136
|
+
)
|
|
137
|
+
@line_magic
|
|
138
|
+
def load_cells(self, line: str) -> None:
|
|
139
|
+
"""Copy the code of cells from another notebook into the current cell.
|
|
140
|
+
|
|
141
|
+
Like %load, this replaces the current cell with a commented-out copy of
|
|
142
|
+
the magic line followed by the copied code. Without --cells, it lists
|
|
143
|
+
the notebook's cells, with their IDs and titles, instead.
|
|
144
|
+
"""
|
|
145
|
+
args = parse_argstring(self.load_cells, line)
|
|
146
|
+
try:
|
|
147
|
+
cells = read_notebook_cells(_unquote(args.path))
|
|
148
|
+
except (OSError, ValueError) as e:
|
|
149
|
+
raise UsageError(str(e)) from e
|
|
150
|
+
if not args.cells:
|
|
151
|
+
print(format_cell_table(cells))
|
|
152
|
+
return
|
|
153
|
+
try:
|
|
154
|
+
code = join_cells(cells, [_unquote(ref) for ref in args.cells])
|
|
155
|
+
except ValueError as e:
|
|
156
|
+
hint = f"Run `%load_cells {args.path}` to list the notebook's cells."
|
|
157
|
+
raise UsageError(f"{e}\n{hint}") from e
|
|
158
|
+
self.shell.set_next_input(f"# %load_cells {line}\n{code}", replace=True)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def load_ipython_extension(ipython: Any) -> None:
|
|
162
|
+
ipython.register_magics(NotebookMagics)
|
agentbuild/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: agentbuild
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Tools to make agent research easier.
|
|
5
|
+
Author: trducng
|
|
6
|
+
Classifier: Operating System :: OS Independent
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Requires-Python: >=3.11
|
|
9
|
+
Requires-Dist: ipython>=8
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# agentbuild
|
|
13
|
+
|
|
14
|
+
Tools to make agent research easier.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install agentbuild
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Jupyter: `%load_cells`
|
|
23
|
+
|
|
24
|
+
Copy the code of cells from another notebook into the current cell, the way `%load` does for
|
|
25
|
+
files.
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
%load_ext agentbuild.jupyter
|
|
29
|
+
|
|
30
|
+
%load_cells other.ipynb # list the notebook's cells, IDs and titles
|
|
31
|
+
%load_cells other.ipynb --cells 7f3a9c21 b2e4d0aa # copy those cells into this cell
|
|
32
|
+
%load_cells "my export.ipynb" -c "Import Libraries" 7f3a
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Each cell after `--cells` (or `-c`) can be:
|
|
36
|
+
|
|
37
|
+
- a cell ID: the Jupyter cell ID, or for notebooks exported from Databricks, the Databricks cell ID
|
|
38
|
+
(`nuid`);
|
|
39
|
+
- the start of a cell ID, such as the 8 characters the listing shows;
|
|
40
|
+
- a Databricks cell title, matched exactly or ignoring case. Quote titles that contain spaces.
|
|
41
|
+
|
|
42
|
+
If a value matches more than one cell, the magic stops with an error instead of guessing.
|
|
43
|
+
|
|
44
|
+
Running the magic with `--cells` replaces the current cell with a commented-out copy of the magic
|
|
45
|
+
line, followed by the code of the chosen cells in the order given. Run the cell again to execute
|
|
46
|
+
that code. Relative paths are resolved from the kernel's working directory, which is normally the
|
|
47
|
+
notebook's folder.
|
|
48
|
+
|
|
49
|
+
To load the extension in every session, add this to
|
|
50
|
+
`~/.ipython/profile_default/ipython_config.py`:
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
c.InteractiveShellApp.extensions = ["agentbuild.jupyter"]
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Development
|
|
57
|
+
|
|
58
|
+
Requires [uv](https://docs.astral.sh/uv/) and `make`.
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
make install # install the package + dev tools, set up git hooks
|
|
62
|
+
make help # list all commands
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Common commands:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
make test # run tests
|
|
69
|
+
make test-matrix # run tests on Python 3.11-3.15 (override with PYTHONS="3.11 3.12")
|
|
70
|
+
make cov # run tests with a coverage report
|
|
71
|
+
make lint # lint + format check
|
|
72
|
+
make format # auto-fix lint issues and format
|
|
73
|
+
make check # run the same checks as CI
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Release
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
make publish # build and upload to PyPI (needs a PyPI token)
|
|
80
|
+
```
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
agentbuild/__init__.py,sha256=CCOwo0Tlu7ovAovriTsnfhivqej1Zg4y0N5au2V4ZS0,277
|
|
2
|
+
agentbuild/jupyter.py,sha256=i0xGtcBI5KmmWcGrshaUme7kylWY6Co_qyyUkdoP0cA,6362
|
|
3
|
+
agentbuild/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
agentbuild-0.0.1.dist-info/METADATA,sha256=CtPNcwbySLdNJV8aXBUOrCyx66fHQiZm92Mx5NOJ9Kk,2342
|
|
5
|
+
agentbuild-0.0.1.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
6
|
+
agentbuild-0.0.1.dist-info/RECORD,,
|