ipygraph 1.14.2__tar.gz → 1.15.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.
- ipygraph-1.15.0/.claude/launch.json +20 -0
- {ipygraph-1.14.2 → ipygraph-1.15.0}/.gitignore +79 -0
- ipygraph-1.15.0/.jupyter/collaboration_sessions.json +52 -0
- ipygraph-1.15.0/CHANGELOG.md +13 -0
- {ipygraph-1.14.2 → ipygraph-1.15.0}/PKG-INFO +25 -2
- {ipygraph-1.14.2 → ipygraph-1.15.0}/README.md +21 -0
- ipygraph-1.15.0/junit.xml +7 -0
- {ipygraph-1.14.2 → ipygraph-1.15.0}/myextension/_version.py +1 -1
- {ipygraph-1.14.2 → ipygraph-1.15.0}/myextension/agent.py +159 -38
- ipygraph-1.15.0/myextension/agent_providers.py +609 -0
- ipygraph-1.15.0/myextension/dag_runner.py +255 -0
- {ipygraph-1.14.2 → ipygraph-1.15.0}/myextension/handlers.py +3 -0
- ipygraph-1.15.0/myextension/labextension/build_log.json +739 -0
- {ipygraph-1.14.2 → ipygraph-1.15.0}/myextension/labextension/package.json +22 -23
- ipygraph-1.15.0/myextension/labextension/static/lib_index_js.c266c0a863afa8c3.js +12421 -0
- ipygraph-1.15.0/myextension/labextension/static/lib_index_js.c266c0a863afa8c3.js.map +1 -0
- ipygraph-1.15.0/myextension/labextension/static/remoteEntry.182ed622251c7e11.js +979 -0
- ipygraph-1.15.0/myextension/labextension/static/remoteEntry.182ed622251c7e11.js.map +1 -0
- ipygraph-1.15.0/myextension/labextension/static/style_index_js.e7f0aa441eb8fd67.js +1088 -0
- ipygraph-1.15.0/myextension/labextension/static/style_index_js.e7f0aa441eb8fd67.js.map +1 -0
- {ipygraph-1.14.2 → ipygraph-1.15.0}/package.json +22 -23
- {ipygraph-1.14.2 → ipygraph-1.15.0}/pyproject.toml +2 -0
- {ipygraph-1.14.2 → ipygraph-1.15.0}/src/agentApi.ts +99 -2
- {ipygraph-1.14.2 → ipygraph-1.15.0}/src/agentPanel.ts +885 -249
- {ipygraph-1.14.2 → ipygraph-1.15.0}/src/agentTools.ts +101 -31
- {ipygraph-1.14.2 → ipygraph-1.15.0}/src/branchSelector.ts +12 -0
- ipygraph-1.15.0/src/dagModel.ts +394 -0
- ipygraph-1.15.0/src/dagNodePanel.ts +458 -0
- ipygraph-1.15.0/src/dagView.ts +1107 -0
- ipygraph-1.15.0/src/experimentView.ts +549 -0
- {ipygraph-1.14.2 → ipygraph-1.15.0}/src/graphView.ts +45 -13
- {ipygraph-1.14.2 → ipygraph-1.15.0}/src/icon.ts +23 -0
- {ipygraph-1.14.2 → ipygraph-1.15.0}/src/index.ts +161 -14
- {ipygraph-1.14.2 → ipygraph-1.15.0}/src/parallel.ts +2 -2
- {ipygraph-1.14.2 → ipygraph-1.15.0}/src/widget.ts +7 -0
- ipygraph-1.15.0/style/base.css +670 -0
- {ipygraph-1.14.2 → ipygraph-1.15.0}/yarn.lock +913 -1445
- ipygraph-1.14.2/CHANGELOG.md +0 -5
- ipygraph-1.14.2/myextension/agent_providers.py +0 -326
- ipygraph-1.14.2/myextension/labextension/static/232.b7f55d8d983399ee.js +0 -59
- ipygraph-1.14.2/myextension/labextension/static/694.255a445d5c77b9d5.js +0 -495
- ipygraph-1.14.2/myextension/labextension/static/remoteEntry.7c24b058bfa39f37.js +0 -7
- ipygraph-1.14.2/myextension/labextension/static/third-party-licenses.json +0 -52
- ipygraph-1.14.2/style/base.css +0 -58
- {ipygraph-1.14.2 → ipygraph-1.15.0}/.copier-answers.yml +0 -0
- {ipygraph-1.14.2 → ipygraph-1.15.0}/.prettierignore +0 -0
- {ipygraph-1.14.2 → ipygraph-1.15.0}/.yarnrc.yml +0 -0
- {ipygraph-1.14.2 → ipygraph-1.15.0}/LICENSE +0 -0
- {ipygraph-1.14.2 → ipygraph-1.15.0}/NOTICE +0 -0
- {ipygraph-1.14.2 → ipygraph-1.15.0}/RELEASE.md +0 -0
- {ipygraph-1.14.2 → ipygraph-1.15.0}/babel.config.js +0 -0
- {ipygraph-1.14.2 → ipygraph-1.15.0}/install.json +0 -0
- {ipygraph-1.14.2 → ipygraph-1.15.0}/jest.config.js +0 -0
- {ipygraph-1.14.2 → ipygraph-1.15.0}/jupyter-config/server-config/myextension.json +0 -0
- {ipygraph-1.14.2 → ipygraph-1.15.0}/myextension/__init__.py +0 -0
- {ipygraph-1.14.2 → ipygraph-1.15.0}/myextension/app.py +0 -0
- {ipygraph-1.14.2 → ipygraph-1.15.0}/myextension/contents_manager.py +0 -0
- {ipygraph-1.14.2 → ipygraph-1.15.0}/myextension/kaggle.py +0 -0
- {ipygraph-1.14.2 → ipygraph-1.15.0}/myextension/kernel_manager.py +0 -0
- {ipygraph-1.14.2 → ipygraph-1.15.0}/myextension/labextension/static/style.js +0 -0
- {ipygraph-1.14.2 → ipygraph-1.15.0}/setup.py +0 -0
- {ipygraph-1.14.2 → ipygraph-1.15.0}/src/__tests__/myextension.spec.ts +0 -0
- {ipygraph-1.14.2 → ipygraph-1.15.0}/src/agentActions.ts +0 -0
- {ipygraph-1.14.2 → ipygraph-1.15.0}/src/api.ts +0 -0
- {ipygraph-1.14.2 → ipygraph-1.15.0}/src/contract.ts +0 -0
- {ipygraph-1.14.2 → ipygraph-1.15.0}/src/datasetPicker.ts +0 -0
- {ipygraph-1.14.2 → ipygraph-1.15.0}/src/diagramView.ts +0 -0
- {ipygraph-1.14.2 → ipygraph-1.15.0}/src/document.ts +0 -0
- {ipygraph-1.14.2 → ipygraph-1.15.0}/src/dropUpload.ts +0 -0
- {ipygraph-1.14.2 → ipygraph-1.15.0}/src/mdRender.ts +0 -0
- {ipygraph-1.14.2 → ipygraph-1.15.0}/src/model.ts +0 -0
- {ipygraph-1.14.2 → ipygraph-1.15.0}/src/secretsPanel.ts +0 -0
- {ipygraph-1.14.2 → ipygraph-1.15.0}/style/index.css +0 -0
- {ipygraph-1.14.2 → ipygraph-1.15.0}/style/index.js +0 -0
- {ipygraph-1.14.2 → ipygraph-1.15.0}/test_backend.py +0 -0
- {ipygraph-1.14.2 → ipygraph-1.15.0}/tsconfig.json +0 -0
- {ipygraph-1.14.2 → ipygraph-1.15.0}/tsconfig.test.json +0 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "0.0.1",
|
|
3
|
+
"configurations": [
|
|
4
|
+
{
|
|
5
|
+
"name": "jupyterlab",
|
|
6
|
+
"runtimeExecutable": "python3",
|
|
7
|
+
"runtimeArgs": [
|
|
8
|
+
"-m",
|
|
9
|
+
"jupyter",
|
|
10
|
+
"lab",
|
|
11
|
+
"--no-browser",
|
|
12
|
+
"--ServerApp.token=",
|
|
13
|
+
"--ServerApp.password=",
|
|
14
|
+
"--port=8901",
|
|
15
|
+
"--notebook-dir=/Users/muruga/Projects/ipycopilot/jupyterlab-ipygb"
|
|
16
|
+
],
|
|
17
|
+
"port": 8901
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
}
|
|
@@ -131,3 +131,82 @@ dmypy.json
|
|
|
131
131
|
Untitled*.ipynb
|
|
132
132
|
untitled*.ipygb
|
|
133
133
|
*.ipygb
|
|
134
|
+
|
|
135
|
+
# --- mac-mini-sync: generated, secrets, heavy binaries (kept local, see SYNC-NOTES.md) ---
|
|
136
|
+
.DS_Store
|
|
137
|
+
# dependencies & build output
|
|
138
|
+
node_modules/
|
|
139
|
+
.venv*/
|
|
140
|
+
venv/
|
|
141
|
+
__pycache__/
|
|
142
|
+
*.pyc
|
|
143
|
+
*.egg-info/
|
|
144
|
+
dist/
|
|
145
|
+
build/
|
|
146
|
+
.build/
|
|
147
|
+
.next/
|
|
148
|
+
.gradle/
|
|
149
|
+
.kotlin/
|
|
150
|
+
DerivedData/
|
|
151
|
+
Pods/
|
|
152
|
+
xcuserdata/
|
|
153
|
+
target/
|
|
154
|
+
.cache/
|
|
155
|
+
.ipynb_checkpoints/
|
|
156
|
+
.pytest_cache/
|
|
157
|
+
.mypy_cache/
|
|
158
|
+
.ruff_cache/
|
|
159
|
+
.turbo/
|
|
160
|
+
.parcel-cache/
|
|
161
|
+
coverage/
|
|
162
|
+
# local state & secrets
|
|
163
|
+
.env
|
|
164
|
+
.env.*
|
|
165
|
+
!.env.example
|
|
166
|
+
!.env.sample
|
|
167
|
+
*.pem
|
|
168
|
+
*.key
|
|
169
|
+
*.p12
|
|
170
|
+
*.jks
|
|
171
|
+
*.keystore
|
|
172
|
+
credentials*.json
|
|
173
|
+
service-account*
|
|
174
|
+
token.json
|
|
175
|
+
local.properties
|
|
176
|
+
.jupyter_ystore.db*
|
|
177
|
+
.claude/settings.local.json
|
|
178
|
+
.claude/worktrees/
|
|
179
|
+
# heavy media & archives & model weights
|
|
180
|
+
*.mp4
|
|
181
|
+
*.mov
|
|
182
|
+
*.mkv
|
|
183
|
+
*.webm
|
|
184
|
+
*.avi
|
|
185
|
+
*.m4v
|
|
186
|
+
*.wav
|
|
187
|
+
*.mp3
|
|
188
|
+
*.m4a
|
|
189
|
+
*.aac
|
|
190
|
+
*.flac
|
|
191
|
+
*.ogg
|
|
192
|
+
*.aiff
|
|
193
|
+
*.zip
|
|
194
|
+
*.tar
|
|
195
|
+
*.gz
|
|
196
|
+
*.tgz
|
|
197
|
+
*.7z
|
|
198
|
+
*.rar
|
|
199
|
+
*.pt
|
|
200
|
+
*.pth
|
|
201
|
+
*.ckpt
|
|
202
|
+
*.safetensors
|
|
203
|
+
*.onnx
|
|
204
|
+
*.h5
|
|
205
|
+
*.gguf
|
|
206
|
+
*.npz
|
|
207
|
+
# project-specific
|
|
208
|
+
/demo/instagram/reel-ai-era.mp4
|
|
209
|
+
/demo/instagram/reel-day6-merge.mp4
|
|
210
|
+
/demo/instagram/reel-notebook-chaos.mp4
|
|
211
|
+
/demo/ipygraph-demo.mp4
|
|
212
|
+
/demo/ipygraph-story.mp4
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"0cc729d6-c09c-4594-8930-44612bc72185": {
|
|
3
|
+
"version": "3.0.0.a0",
|
|
4
|
+
"created_at": "2026-08-23T15:08:06.652307+00:00",
|
|
5
|
+
"document_version": "2.0.0"
|
|
6
|
+
},
|
|
7
|
+
"dd3b6181-cbfd-4039-93eb-6db2b0246ebd": {
|
|
8
|
+
"version": "3.0.0.a0",
|
|
9
|
+
"created_at": "2026-08-23T15:23:43.147711+00:00",
|
|
10
|
+
"document_version": "2.0.0"
|
|
11
|
+
},
|
|
12
|
+
"f30795eb-41c1-4e74-8d3d-9a803ae9a738": {
|
|
13
|
+
"version": "3.0.0.a0",
|
|
14
|
+
"created_at": "2026-09-18T03:40:58.005128+00:00",
|
|
15
|
+
"document_version": "1.0.0"
|
|
16
|
+
},
|
|
17
|
+
"e3034965-30b6-49aa-98ef-9a85416a7f2e": {
|
|
18
|
+
"version": "3.0.0.a0",
|
|
19
|
+
"created_at": "2026-09-18T03:43:06.459902+00:00",
|
|
20
|
+
"document_version": "2.0.0"
|
|
21
|
+
},
|
|
22
|
+
"1c8cf52b-76a7-4375-add1-1cdafd83fe22": {
|
|
23
|
+
"version": "3.0.0.a0",
|
|
24
|
+
"created_at": "2026-09-18T04:28:13.453173+00:00",
|
|
25
|
+
"document_version": "2.0.0"
|
|
26
|
+
},
|
|
27
|
+
"9d8f9246-4d22-431b-8367-16433fcf4ef7": {
|
|
28
|
+
"version": "3.0.0.a0",
|
|
29
|
+
"created_at": "2026-09-18T09:49:32.246067+00:00",
|
|
30
|
+
"document_version": "2.0.0"
|
|
31
|
+
},
|
|
32
|
+
"37564caf-d4cd-4ad6-91ef-8126414c7141": {
|
|
33
|
+
"version": "3.0.0.a0",
|
|
34
|
+
"created_at": "2026-11-08T07:23:25.129911+00:00",
|
|
35
|
+
"document_version": "2.0.0"
|
|
36
|
+
},
|
|
37
|
+
"cb5a2cbf-7a0a-4537-b52c-5524a382d3dc": {
|
|
38
|
+
"version": "3.0.0.a0",
|
|
39
|
+
"created_at": "2026-11-08T07:24:30.394928+00:00",
|
|
40
|
+
"document_version": "2.0.0"
|
|
41
|
+
},
|
|
42
|
+
"fe954943-09f8-404f-906e-e38f4000fa1e": {
|
|
43
|
+
"version": "3.0.0.a0",
|
|
44
|
+
"created_at": "2026-11-08T07:50:42.939301+00:00",
|
|
45
|
+
"document_version": "1.0.0"
|
|
46
|
+
},
|
|
47
|
+
"08924082-5c22-47a5-9f0c-11c82a5e7fac": {
|
|
48
|
+
"version": "3.0.0.a0",
|
|
49
|
+
"created_at": "2026-11-08T07:50:16.946795+00:00",
|
|
50
|
+
"document_version": "1.0.0"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
<!-- <START NEW CHANGELOG ENTRY> -->
|
|
4
|
+
|
|
5
|
+
## 1.15.0
|
|
6
|
+
|
|
7
|
+
- **`.ipydag` — DAG of notebooks**: a second document type wiring whole `.ipynb` files into a pipeline. Data flows through saved artifact files (`IPYDAG_INPUTS` / `IPYDAG_OUTPUTS` injected per run); ◆ decision (if/else) and ⬡ case (switch) nodes route the flow from values in upstream JSON artifacts. Visual editor with inspector, per-node and full-DAG runs (server-side via nbclient), and a DAG-node header inside member notebooks for editing inputs/outputs in place.
|
|
8
|
+
- **🧪 Experiments view**: a live leaderboard + journal of the whole experiment campaign, derived from the graph — metrics parsed from branch annotations and run outputs, Δ vs the baseline branch, winner highlighted, one-click markdown report.
|
|
9
|
+
- **Streaming agent**: new `/ipygb/agent/stream` endpoint streams model tokens into the panel as they generate (Anthropic, OpenAI, Gemini, claude CLI/SDK); Anthropic prompt caching and a bigger token budget cut turn latency; light sub-agent roles auto-route to fast models; Stop now cancels mid-request.
|
|
10
|
+
- **Agentic UX**: pinned live mission-plan checklist, per-step activity timelines with per-action status/duration and expandable full observations, live sub-agent task rows (branch workers, design debate, Kaggle, validators).
|
|
11
|
+
- Modernized assistant panel UI; verified against JupyterLab 4.6.6.
|
|
12
|
+
|
|
13
|
+
<!-- <END NEW CHANGELOG ENTRY> -->
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: ipygraph
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.15.0
|
|
4
4
|
Summary: Turn your Jupyter notebook into a branching graph of experiments — a JupyterLab extension with a plan-first AI copilot, branch isolation, and fork-join parallel execution.
|
|
5
5
|
Project-URL: Homepage, https://github.com/jupyterlab/jupyterlab-ipygb
|
|
6
6
|
Project-URL: Bug Tracker, https://github.com/jupyterlab/jupyterlab-ipygb/issues
|
|
@@ -222,6 +222,8 @@ Requires-Python: >=3.10
|
|
|
222
222
|
Requires-Dist: dill
|
|
223
223
|
Requires-Dist: jupyter-server<3,>=2.0.0
|
|
224
224
|
Requires-Dist: jupyterlab<5,>=4.0.0
|
|
225
|
+
Requires-Dist: nbclient>=0.7
|
|
226
|
+
Requires-Dist: nbformat>=5.7
|
|
225
227
|
Description-Content-Type: text/markdown
|
|
226
228
|
|
|
227
229
|
<p align="center">
|
|
@@ -275,6 +277,27 @@ ipyGraph adds a new document type: **`.ipygb`** (a "Graphbook"). It's a **supers
|
|
|
275
277
|
|
|
276
278
|
Because it's a superset, the kernel, outputs, and rendering are ordinary Jupyter — nothing proprietary about how your code runs. And you can always **export any root→tip path back to a plain `.ipynb`** to share or hand off.
|
|
277
279
|
|
|
280
|
+
## The `.ipydag` file format
|
|
281
|
+
|
|
282
|
+
Where `.ipygb` branches **cells inside one notebook**, **`.ipydag`** wires **whole notebooks** into a DAG. Each node is an ordinary `.ipynb`; data moves between them through **saved files** ("artifacts") — an upstream notebook writes a file, the downstream notebook reloads it, and the files that support the flow stay on disk next to the `.ipydag`.
|
|
283
|
+
|
|
284
|
+
Three node types:
|
|
285
|
+
|
|
286
|
+
- **📓 Notebook** — runs a `.ipynb`. It declares the artifact files it *writes* (outputs) and the upstream artifacts it *reads* (inputs). When the DAG runs, an injected parameters cell (tagged `ipydag-parameters`) defines `IPYDAG_INPUTS` / `IPYDAG_OUTPUTS` dicts mapping those names to file paths — so notebooks stay ordinary, runnable Jupyter:
|
|
287
|
+
|
|
288
|
+
```python
|
|
289
|
+
import json, pandas as pd
|
|
290
|
+
df = pd.read_parquet(IPYDAG_INPUTS["raw"])
|
|
291
|
+
...
|
|
292
|
+
df.to_parquet(IPYDAG_OUTPUTS["clean"])
|
|
293
|
+
json.dump({"use_nn": True}, open(IPYDAG_OUTPUTS["flags"], "w"))
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
- **◆ Decision (if/else)** — reads a **boolean** key out of a JSON artifact written upstream and routes the flow along its `true` or `false` edge.
|
|
297
|
+
- **⬡ Case (switch)** — reads a **string** the same way and takes the edge whose label matches (or the `default` edge).
|
|
298
|
+
|
|
299
|
+
The file itself is plain JSON (`nodes` + `edges` + layout). A run walks the DAG in topological order, executes each notebook server-side (saving its outputs in place — the executed notebooks are the run's record), evaluates gates, and **skips everything reachable only through untaken branches**. Create one from the Launcher ("Notebook DAG") or try the demo at `demo/dag/pipeline.ipydag`.
|
|
300
|
+
|
|
278
301
|
---
|
|
279
302
|
|
|
280
303
|
## See it
|
|
@@ -49,6 +49,27 @@ ipyGraph adds a new document type: **`.ipygb`** (a "Graphbook"). It's a **supers
|
|
|
49
49
|
|
|
50
50
|
Because it's a superset, the kernel, outputs, and rendering are ordinary Jupyter — nothing proprietary about how your code runs. And you can always **export any root→tip path back to a plain `.ipynb`** to share or hand off.
|
|
51
51
|
|
|
52
|
+
## The `.ipydag` file format
|
|
53
|
+
|
|
54
|
+
Where `.ipygb` branches **cells inside one notebook**, **`.ipydag`** wires **whole notebooks** into a DAG. Each node is an ordinary `.ipynb`; data moves between them through **saved files** ("artifacts") — an upstream notebook writes a file, the downstream notebook reloads it, and the files that support the flow stay on disk next to the `.ipydag`.
|
|
55
|
+
|
|
56
|
+
Three node types:
|
|
57
|
+
|
|
58
|
+
- **📓 Notebook** — runs a `.ipynb`. It declares the artifact files it *writes* (outputs) and the upstream artifacts it *reads* (inputs). When the DAG runs, an injected parameters cell (tagged `ipydag-parameters`) defines `IPYDAG_INPUTS` / `IPYDAG_OUTPUTS` dicts mapping those names to file paths — so notebooks stay ordinary, runnable Jupyter:
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
import json, pandas as pd
|
|
62
|
+
df = pd.read_parquet(IPYDAG_INPUTS["raw"])
|
|
63
|
+
...
|
|
64
|
+
df.to_parquet(IPYDAG_OUTPUTS["clean"])
|
|
65
|
+
json.dump({"use_nn": True}, open(IPYDAG_OUTPUTS["flags"], "w"))
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
- **◆ Decision (if/else)** — reads a **boolean** key out of a JSON artifact written upstream and routes the flow along its `true` or `false` edge.
|
|
69
|
+
- **⬡ Case (switch)** — reads a **string** the same way and takes the edge whose label matches (or the `default` edge).
|
|
70
|
+
|
|
71
|
+
The file itself is plain JSON (`nodes` + `edges` + layout). A run walks the DAG in topological order, executes each notebook server-side (saving its outputs in place — the executed notebooks are the run's record), evaluates gates, and **skips everything reachable only through untaken branches**. Create one from the Launcher ("Notebook DAG") or try the demo at `demo/dag/pipeline.ipydag`.
|
|
72
|
+
|
|
52
73
|
---
|
|
53
74
|
|
|
54
75
|
## See it
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<testsuites name="jest tests" tests="1" failures="0" errors="0" time="1.16">
|
|
3
|
+
<testsuite name="myextension" errors="0" failures="0" skipped="0" timestamp="2026-11-08T07:40:34" time="0.722" tests="1">
|
|
4
|
+
<testcase classname="myextension should be tested" name="myextension should be tested" time="0.001">
|
|
5
|
+
</testcase>
|
|
6
|
+
</testsuite>
|
|
7
|
+
</testsuites>
|
|
@@ -81,10 +81,18 @@ ALWAYS respond with a single JSON object, no prose outside it:
|
|
|
81
81
|
|
|
82
82
|
{
|
|
83
83
|
"reply": "<natural-language explanation + your assumptions>",
|
|
84
|
+
"plan": [ {"step": "<= 8 words>", "status": "pending"|"doing"|"done"} ],
|
|
84
85
|
"actions": [ <zero or more action objects, empty if you are asking> ],
|
|
85
86
|
"questions": [ {"question": "<short question>", "options": ["<opt>", ...]} ]
|
|
86
87
|
}
|
|
87
88
|
|
|
89
|
+
"plan" is your LIVE MISSION CHECKLIST, shown pinned in the panel. Include it \
|
|
90
|
+
in EVERY turn of a multi-step mission: 3-8 steps covering the WHOLE mission \
|
|
91
|
+
(not just this turn), each with its current status — keep step wording \
|
|
92
|
+
STABLE across turns and only flip statuses as work progresses, so the user \
|
|
93
|
+
watches the same list fill in. Put the step you are doing RIGHT NOW as \
|
|
94
|
+
"doing". Omit "plan" only for trivial single-action asks.
|
|
95
|
+
|
|
88
96
|
"questions" is optional; include it only when you genuinely need input. When \
|
|
89
97
|
you ask questions, keep "actions" empty and wait for the user's answer.
|
|
90
98
|
|
|
@@ -344,8 +352,11 @@ sklearn/GBM branches; run heavy neural-net training SEQUENTIALLY with \
|
|
|
344
352
|
run_cells (parallel torch trainings starve each other and a kernel gets \
|
|
345
353
|
killed mid-backward). Then in your reflection compare their metrics \
|
|
346
354
|
against the current best. Keep the \
|
|
347
|
-
winner as the new BEST; annotate every branch with set_comment
|
|
348
|
-
|
|
355
|
+
winner as the new BEST; annotate every branch with set_comment so the graph \
|
|
356
|
+
tells the story. FORMAT these annotations machine-readably — start with the \
|
|
357
|
+
metric, then the takeaway: "accuracy=0.913 — +embeddings beat the baseline \
|
|
358
|
+
by 2pts" — because the 🧪 Experiments view parses them into the live \
|
|
359
|
+
leaderboard (branch · metric · Δ vs baseline · winner) the user watches.
|
|
349
360
|
5. ITERATE. Repeat BRANCH→EVALUATE from the new best, going DEEPER — e.g. \
|
|
350
361
|
linear → MLP → +embeddings → +self-attention → multi-head → deeper/residual \
|
|
351
362
|
— until the metric plateaus for two rounds or you reach a genuinely strong \
|
|
@@ -533,30 +544,62 @@ _FORMAT_RETRY_MSG = (
|
|
|
533
544
|
)
|
|
534
545
|
|
|
535
546
|
|
|
547
|
+
def _prepare_turn(body: dict):
|
|
548
|
+
"""Shared request parsing for the blocking and streaming handlers."""
|
|
549
|
+
provider = body.get("provider", "claude-cli")
|
|
550
|
+
role = body.get("role", "master")
|
|
551
|
+
# Light roles (dataset curation, Kaggle picking) run on the provider's
|
|
552
|
+
# fast model unless the caller pinned one — several-fold latency cut.
|
|
553
|
+
model = providers.pick_model(provider, body.get("model", ""), role)
|
|
554
|
+
messages = body.get("messages", [])
|
|
555
|
+
if not messages:
|
|
556
|
+
raise tornado.web.HTTPError(400, reason="No messages")
|
|
557
|
+
|
|
558
|
+
# Prepend the graph context to the latest user turn.
|
|
559
|
+
convo = [dict(m) for m in messages]
|
|
560
|
+
context = _graph_summary(body.get("graph", {}))
|
|
561
|
+
for m in reversed(convo):
|
|
562
|
+
if m.get("role") == "user":
|
|
563
|
+
m["content"] = f"Current graph:\n{context}\n\nRequest: {m['content']}"
|
|
564
|
+
break
|
|
565
|
+
return (
|
|
566
|
+
provider,
|
|
567
|
+
model,
|
|
568
|
+
role,
|
|
569
|
+
convo,
|
|
570
|
+
body.get("key", ""),
|
|
571
|
+
body.get("base_url", ""),
|
|
572
|
+
)
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
def _final_payload(parsed: dict, raw: str) -> dict:
|
|
576
|
+
"""Normalize a parsed model reply into the response the frontend expects."""
|
|
577
|
+
actions = parsed.get("actions", [])
|
|
578
|
+
if not isinstance(actions, list):
|
|
579
|
+
actions = []
|
|
580
|
+
questions = parsed.get("questions", [])
|
|
581
|
+
if not isinstance(questions, list):
|
|
582
|
+
questions = []
|
|
583
|
+
plan = parsed.get("plan", [])
|
|
584
|
+
if not isinstance(plan, list):
|
|
585
|
+
plan = []
|
|
586
|
+
return {
|
|
587
|
+
"reply": parsed.get("reply", ""),
|
|
588
|
+
"plan": plan,
|
|
589
|
+
"actions": actions,
|
|
590
|
+
"questions": questions,
|
|
591
|
+
"done": bool(parsed.get("done", False)),
|
|
592
|
+
"raw": raw,
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
|
|
536
596
|
class AgentHandler(APIHandler):
|
|
537
597
|
"""POST /ipygb/agent — run one assistant turn."""
|
|
538
598
|
|
|
539
599
|
@tornado.web.authenticated
|
|
540
600
|
async def post(self):
|
|
541
601
|
body = self.get_json_body() or {}
|
|
542
|
-
provider = body
|
|
543
|
-
model = body.get("model", "")
|
|
544
|
-
messages = body.get("messages", [])
|
|
545
|
-
graph = body.get("graph", {})
|
|
546
|
-
key = body.get("key", "")
|
|
547
|
-
base_url = body.get("base_url", "")
|
|
548
|
-
role = body.get("role", "master")
|
|
549
|
-
|
|
550
|
-
if not messages:
|
|
551
|
-
raise tornado.web.HTTPError(400, reason="No messages")
|
|
552
|
-
|
|
553
|
-
# Prepend the graph context to the latest user turn.
|
|
554
|
-
convo = [dict(m) for m in messages]
|
|
555
|
-
context = _graph_summary(graph)
|
|
556
|
-
for m in reversed(convo):
|
|
557
|
-
if m.get("role") == "user":
|
|
558
|
-
m["content"] = f"Current graph:\n{context}\n\nRequest: {m['content']}"
|
|
559
|
-
break
|
|
602
|
+
provider, model, role, convo, key, base_url = _prepare_turn(body)
|
|
560
603
|
|
|
561
604
|
try:
|
|
562
605
|
raw = await tornado.ioloop.IOLoop.current().run_in_executor(
|
|
@@ -594,24 +637,98 @@ class AgentHandler(APIHandler):
|
|
|
594
637
|
parsed, raw = parsed2, raw2
|
|
595
638
|
except Exception:
|
|
596
639
|
self.log.exception("ipygb agent: format retry failed")
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
640
|
+
self.finish(json.dumps(_final_payload(parsed, raw)))
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
class AgentStreamHandler(APIHandler):
|
|
644
|
+
"""
|
|
645
|
+
POST /ipygb/agent/stream — one assistant turn, streamed.
|
|
646
|
+
|
|
647
|
+
The response is a server-sent-event style chunked body; each event is a
|
|
648
|
+
line `data: {...}` followed by a blank line:
|
|
649
|
+
{"delta": "<text>"} model tokens as they are generated
|
|
650
|
+
{"note": "<text>"} progress notes (e.g. a format retry)
|
|
651
|
+
{"result": {...}} the final parsed turn (same shape as /agent)
|
|
652
|
+
{"error": "<text>"} fatal failure (no result follows)
|
|
653
|
+
"""
|
|
654
|
+
|
|
655
|
+
@tornado.web.authenticated
|
|
656
|
+
async def post(self):
|
|
657
|
+
import asyncio
|
|
658
|
+
|
|
659
|
+
body = self.get_json_body() or {}
|
|
660
|
+
provider, model, role, convo, key, base_url = _prepare_turn(body)
|
|
661
|
+
|
|
662
|
+
self.set_header("Content-Type", "text/event-stream")
|
|
663
|
+
self.set_header("Cache-Control", "no-cache")
|
|
664
|
+
self.set_header("X-Accel-Buffering", "no")
|
|
665
|
+
|
|
666
|
+
loop = tornado.ioloop.IOLoop.current()
|
|
667
|
+
q: asyncio.Queue = asyncio.Queue()
|
|
668
|
+
|
|
669
|
+
def produce():
|
|
670
|
+
try:
|
|
671
|
+
for chunk in providers.stream_generate(
|
|
672
|
+
provider, model, get_prompt(role), convo, key, base_url
|
|
673
|
+
):
|
|
674
|
+
loop.add_callback(q.put_nowait, ("delta", chunk))
|
|
675
|
+
loop.add_callback(q.put_nowait, ("end", None))
|
|
676
|
+
except Exception as e: # surfaced as an SSE error event
|
|
677
|
+
loop.add_callback(q.put_nowait, ("error", str(e)))
|
|
678
|
+
|
|
679
|
+
loop.run_in_executor(None, produce)
|
|
680
|
+
|
|
681
|
+
async def emit(obj) -> bool:
|
|
682
|
+
try:
|
|
683
|
+
self.write(f"data: {json.dumps(obj)}\n\n")
|
|
684
|
+
await self.flush()
|
|
685
|
+
return True
|
|
686
|
+
except Exception:
|
|
687
|
+
return False # client went away — drain quietly
|
|
688
|
+
|
|
689
|
+
chunks = []
|
|
690
|
+
failed = None
|
|
691
|
+
while True:
|
|
692
|
+
kind, payload = await q.get()
|
|
693
|
+
if kind == "delta":
|
|
694
|
+
chunks.append(payload)
|
|
695
|
+
await emit({"delta": payload})
|
|
696
|
+
elif kind == "error":
|
|
697
|
+
failed = payload
|
|
698
|
+
break
|
|
699
|
+
else:
|
|
700
|
+
break
|
|
701
|
+
|
|
702
|
+
raw = "".join(chunks)
|
|
703
|
+
if failed is not None and not raw:
|
|
704
|
+
self.log.error("ipygb agent stream failed: %s", failed)
|
|
705
|
+
await emit({"error": failed})
|
|
706
|
+
self.finish()
|
|
707
|
+
return
|
|
708
|
+
|
|
709
|
+
parsed, parsed_ok = _extract_json(raw)
|
|
710
|
+
if not parsed_ok:
|
|
711
|
+
# Same one-shot format correction as the blocking endpoint.
|
|
712
|
+
await emit({"note": "Reply was not valid JSON — retrying once…"})
|
|
713
|
+
retry_convo = convo + [
|
|
714
|
+
{"role": "assistant", "content": raw[-4000:]},
|
|
715
|
+
{"role": "user", "content": _FORMAT_RETRY_MSG},
|
|
716
|
+
]
|
|
717
|
+
try:
|
|
718
|
+
raw2 = await loop.run_in_executor(
|
|
719
|
+
None,
|
|
720
|
+
lambda: providers.generate(
|
|
721
|
+
provider, model, get_prompt(role), retry_convo,
|
|
722
|
+
key, base_url
|
|
723
|
+
),
|
|
724
|
+
)
|
|
725
|
+
parsed2, ok2 = _extract_json(raw2)
|
|
726
|
+
if ok2:
|
|
727
|
+
parsed, raw = parsed2, raw2
|
|
728
|
+
except Exception:
|
|
729
|
+
self.log.exception("ipygb agent: stream format retry failed")
|
|
730
|
+
await emit({"result": _final_payload(parsed, raw)})
|
|
731
|
+
self.finish()
|
|
615
732
|
|
|
616
733
|
|
|
617
734
|
class AgentProvidersHandler(APIHandler):
|
|
@@ -692,6 +809,10 @@ def setup_agent_handlers(web_app):
|
|
|
692
809
|
host_pattern,
|
|
693
810
|
[
|
|
694
811
|
(url_path_join(base_url, "ipygb", "agent"), AgentHandler),
|
|
812
|
+
(
|
|
813
|
+
url_path_join(base_url, "ipygb", "agent", "stream"),
|
|
814
|
+
AgentStreamHandler,
|
|
815
|
+
),
|
|
695
816
|
(
|
|
696
817
|
url_path_join(base_url, "ipygb", "agent", "providers"),
|
|
697
818
|
AgentProvidersHandler,
|