nc1709 1.15.4__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.
- nc1709/__init__.py +13 -0
- nc1709/agent/__init__.py +36 -0
- nc1709/agent/core.py +505 -0
- nc1709/agent/mcp_bridge.py +245 -0
- nc1709/agent/permissions.py +298 -0
- nc1709/agent/tools/__init__.py +21 -0
- nc1709/agent/tools/base.py +440 -0
- nc1709/agent/tools/bash_tool.py +367 -0
- nc1709/agent/tools/file_tools.py +454 -0
- nc1709/agent/tools/notebook_tools.py +516 -0
- nc1709/agent/tools/search_tools.py +322 -0
- nc1709/agent/tools/task_tool.py +284 -0
- nc1709/agent/tools/web_tools.py +555 -0
- nc1709/agents/__init__.py +17 -0
- nc1709/agents/auto_fix.py +506 -0
- nc1709/agents/test_generator.py +507 -0
- nc1709/checkpoints.py +372 -0
- nc1709/cli.py +3380 -0
- nc1709/cli_ui.py +1080 -0
- nc1709/cognitive/__init__.py +149 -0
- nc1709/cognitive/anticipation.py +594 -0
- nc1709/cognitive/context_engine.py +1046 -0
- nc1709/cognitive/council.py +824 -0
- nc1709/cognitive/learning.py +761 -0
- nc1709/cognitive/router.py +583 -0
- nc1709/cognitive/system.py +519 -0
- nc1709/config.py +155 -0
- nc1709/custom_commands.py +300 -0
- nc1709/executor.py +333 -0
- nc1709/file_controller.py +354 -0
- nc1709/git_integration.py +308 -0
- nc1709/github_integration.py +477 -0
- nc1709/image_input.py +446 -0
- nc1709/linting.py +519 -0
- nc1709/llm_adapter.py +667 -0
- nc1709/logger.py +192 -0
- nc1709/mcp/__init__.py +18 -0
- nc1709/mcp/client.py +370 -0
- nc1709/mcp/manager.py +407 -0
- nc1709/mcp/protocol.py +210 -0
- nc1709/mcp/server.py +473 -0
- nc1709/memory/__init__.py +20 -0
- nc1709/memory/embeddings.py +325 -0
- nc1709/memory/indexer.py +474 -0
- nc1709/memory/sessions.py +432 -0
- nc1709/memory/vector_store.py +451 -0
- nc1709/models/__init__.py +86 -0
- nc1709/models/detector.py +377 -0
- nc1709/models/formats.py +315 -0
- nc1709/models/manager.py +438 -0
- nc1709/models/registry.py +497 -0
- nc1709/performance/__init__.py +343 -0
- nc1709/performance/cache.py +705 -0
- nc1709/performance/pipeline.py +611 -0
- nc1709/performance/tiering.py +543 -0
- nc1709/plan_mode.py +362 -0
- nc1709/plugins/__init__.py +17 -0
- nc1709/plugins/agents/__init__.py +18 -0
- nc1709/plugins/agents/django_agent.py +912 -0
- nc1709/plugins/agents/docker_agent.py +623 -0
- nc1709/plugins/agents/fastapi_agent.py +887 -0
- nc1709/plugins/agents/git_agent.py +731 -0
- nc1709/plugins/agents/nextjs_agent.py +867 -0
- nc1709/plugins/base.py +359 -0
- nc1709/plugins/manager.py +411 -0
- nc1709/plugins/registry.py +337 -0
- nc1709/progress.py +443 -0
- nc1709/prompts/__init__.py +22 -0
- nc1709/prompts/agent_system.py +180 -0
- nc1709/prompts/task_prompts.py +340 -0
- nc1709/prompts/unified_prompt.py +133 -0
- nc1709/reasoning_engine.py +541 -0
- nc1709/remote_client.py +266 -0
- nc1709/shell_completions.py +349 -0
- nc1709/slash_commands.py +649 -0
- nc1709/task_classifier.py +408 -0
- nc1709/version_check.py +177 -0
- nc1709/web/__init__.py +8 -0
- nc1709/web/server.py +950 -0
- nc1709/web/templates/index.html +1127 -0
- nc1709-1.15.4.dist-info/METADATA +858 -0
- nc1709-1.15.4.dist-info/RECORD +86 -0
- nc1709-1.15.4.dist-info/WHEEL +5 -0
- nc1709-1.15.4.dist-info/entry_points.txt +2 -0
- nc1709-1.15.4.dist-info/licenses/LICENSE +9 -0
- nc1709-1.15.4.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Jupyter Notebook Tools
|
|
3
|
+
|
|
4
|
+
Tools for working with Jupyter notebooks (.ipynb files):
|
|
5
|
+
- NotebookRead: Read notebook contents with cells and outputs
|
|
6
|
+
- NotebookEdit: Edit, insert, or delete notebook cells
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Optional, List, Dict, Any
|
|
12
|
+
|
|
13
|
+
from .base import Tool, ToolResult, ToolParameter, ToolPermission
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class NotebookReadTool(Tool):
|
|
17
|
+
"""Read contents of a Jupyter notebook"""
|
|
18
|
+
|
|
19
|
+
name = "NotebookRead"
|
|
20
|
+
description = (
|
|
21
|
+
"Read the contents of a Jupyter notebook (.ipynb file). "
|
|
22
|
+
"Returns all cells with their types (code/markdown), source, and outputs."
|
|
23
|
+
)
|
|
24
|
+
category = "notebook"
|
|
25
|
+
permission = ToolPermission.AUTO # Safe to auto-execute
|
|
26
|
+
|
|
27
|
+
parameters = [
|
|
28
|
+
ToolParameter(
|
|
29
|
+
name="notebook_path",
|
|
30
|
+
description="The absolute path to the Jupyter notebook file",
|
|
31
|
+
type="string",
|
|
32
|
+
required=True,
|
|
33
|
+
),
|
|
34
|
+
ToolParameter(
|
|
35
|
+
name="cell_numbers",
|
|
36
|
+
description="Optional list of specific cell numbers to read (0-indexed). If not provided, reads all cells.",
|
|
37
|
+
type="array",
|
|
38
|
+
required=False,
|
|
39
|
+
default=None,
|
|
40
|
+
),
|
|
41
|
+
ToolParameter(
|
|
42
|
+
name="include_outputs",
|
|
43
|
+
description="Whether to include cell outputs in the result (default: true)",
|
|
44
|
+
type="boolean",
|
|
45
|
+
required=False,
|
|
46
|
+
default=True,
|
|
47
|
+
),
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
def execute(
|
|
51
|
+
self,
|
|
52
|
+
notebook_path: str,
|
|
53
|
+
cell_numbers: List[int] = None,
|
|
54
|
+
include_outputs: bool = True,
|
|
55
|
+
) -> ToolResult:
|
|
56
|
+
"""Read notebook contents"""
|
|
57
|
+
path = Path(notebook_path).expanduser()
|
|
58
|
+
|
|
59
|
+
# Check if file exists
|
|
60
|
+
if not path.exists():
|
|
61
|
+
return ToolResult(
|
|
62
|
+
success=False,
|
|
63
|
+
output="",
|
|
64
|
+
error=f"Notebook not found: {notebook_path}",
|
|
65
|
+
target=notebook_path,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# Check file extension
|
|
69
|
+
if path.suffix.lower() != ".ipynb":
|
|
70
|
+
return ToolResult(
|
|
71
|
+
success=False,
|
|
72
|
+
output="",
|
|
73
|
+
error=f"Not a Jupyter notebook file (expected .ipynb): {notebook_path}",
|
|
74
|
+
target=notebook_path,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
# Read and parse notebook
|
|
79
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
80
|
+
notebook = json.load(f)
|
|
81
|
+
|
|
82
|
+
cells = notebook.get("cells", [])
|
|
83
|
+
total_cells = len(cells)
|
|
84
|
+
|
|
85
|
+
# Filter to specific cells if requested
|
|
86
|
+
if cell_numbers:
|
|
87
|
+
selected_indices = [i for i in cell_numbers if 0 <= i < total_cells]
|
|
88
|
+
if not selected_indices:
|
|
89
|
+
return ToolResult(
|
|
90
|
+
success=False,
|
|
91
|
+
output="",
|
|
92
|
+
error=f"No valid cell numbers in range 0-{total_cells-1}",
|
|
93
|
+
target=notebook_path,
|
|
94
|
+
)
|
|
95
|
+
else:
|
|
96
|
+
selected_indices = list(range(total_cells))
|
|
97
|
+
|
|
98
|
+
# Format output
|
|
99
|
+
output_parts = [
|
|
100
|
+
f"Notebook: {notebook_path}",
|
|
101
|
+
f"Total cells: {total_cells}",
|
|
102
|
+
f"Kernel: {notebook.get('metadata', {}).get('kernelspec', {}).get('display_name', 'Unknown')}",
|
|
103
|
+
"=" * 60,
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
for idx in selected_indices:
|
|
107
|
+
cell = cells[idx]
|
|
108
|
+
cell_type = cell.get("cell_type", "unknown")
|
|
109
|
+
source = "".join(cell.get("source", []))
|
|
110
|
+
cell_id = cell.get("id", f"cell_{idx}")
|
|
111
|
+
|
|
112
|
+
output_parts.append(f"\n[Cell {idx}] ({cell_type}) id={cell_id}")
|
|
113
|
+
output_parts.append("-" * 40)
|
|
114
|
+
|
|
115
|
+
# Show source with line numbers
|
|
116
|
+
for i, line in enumerate(source.split("\n"), 1):
|
|
117
|
+
output_parts.append(f" {i:3}│ {line}")
|
|
118
|
+
|
|
119
|
+
# Include outputs for code cells
|
|
120
|
+
if include_outputs and cell_type == "code":
|
|
121
|
+
outputs = cell.get("outputs", [])
|
|
122
|
+
if outputs:
|
|
123
|
+
output_parts.append("\n Output:")
|
|
124
|
+
for out in outputs[:5]: # Limit outputs shown
|
|
125
|
+
out_type = out.get("output_type", "")
|
|
126
|
+
if out_type == "stream":
|
|
127
|
+
text = "".join(out.get("text", []))[:500]
|
|
128
|
+
output_parts.append(f" [stream] {text}")
|
|
129
|
+
elif out_type == "execute_result":
|
|
130
|
+
data = out.get("data", {})
|
|
131
|
+
if "text/plain" in data:
|
|
132
|
+
text = "".join(data["text/plain"])[:500]
|
|
133
|
+
output_parts.append(f" [result] {text}")
|
|
134
|
+
elif out_type == "error":
|
|
135
|
+
ename = out.get("ename", "Error")
|
|
136
|
+
evalue = out.get("evalue", "")[:200]
|
|
137
|
+
output_parts.append(f" [error] {ename}: {evalue}")
|
|
138
|
+
elif out_type == "display_data":
|
|
139
|
+
data_types = list(out.get("data", {}).keys())
|
|
140
|
+
output_parts.append(f" [display] types: {', '.join(data_types)}")
|
|
141
|
+
|
|
142
|
+
return ToolResult(
|
|
143
|
+
success=True,
|
|
144
|
+
output="\n".join(output_parts),
|
|
145
|
+
target=notebook_path,
|
|
146
|
+
data={
|
|
147
|
+
"total_cells": total_cells,
|
|
148
|
+
"cells_shown": len(selected_indices),
|
|
149
|
+
"kernel": notebook.get("metadata", {}).get("kernelspec", {}).get("name"),
|
|
150
|
+
},
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
except json.JSONDecodeError as e:
|
|
154
|
+
return ToolResult(
|
|
155
|
+
success=False,
|
|
156
|
+
output="",
|
|
157
|
+
error=f"Invalid notebook JSON: {e}",
|
|
158
|
+
target=notebook_path,
|
|
159
|
+
)
|
|
160
|
+
except Exception as e:
|
|
161
|
+
return ToolResult(
|
|
162
|
+
success=False,
|
|
163
|
+
output="",
|
|
164
|
+
error=f"Error reading notebook: {e}",
|
|
165
|
+
target=notebook_path,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class NotebookEditTool(Tool):
|
|
170
|
+
"""Edit a cell in a Jupyter notebook"""
|
|
171
|
+
|
|
172
|
+
name = "NotebookEdit"
|
|
173
|
+
description = (
|
|
174
|
+
"Edit, insert, or delete a cell in a Jupyter notebook. "
|
|
175
|
+
"Use edit_mode='replace' to update a cell, 'insert' to add a new cell, "
|
|
176
|
+
"or 'delete' to remove a cell."
|
|
177
|
+
)
|
|
178
|
+
category = "notebook"
|
|
179
|
+
permission = ToolPermission.ASK # Ask before modifying notebooks
|
|
180
|
+
|
|
181
|
+
parameters = [
|
|
182
|
+
ToolParameter(
|
|
183
|
+
name="notebook_path",
|
|
184
|
+
description="The absolute path to the Jupyter notebook file",
|
|
185
|
+
type="string",
|
|
186
|
+
required=True,
|
|
187
|
+
),
|
|
188
|
+
ToolParameter(
|
|
189
|
+
name="cell_number",
|
|
190
|
+
description="The cell number to edit (0-indexed). For insert, new cell is added after this position.",
|
|
191
|
+
type="integer",
|
|
192
|
+
required=True,
|
|
193
|
+
),
|
|
194
|
+
ToolParameter(
|
|
195
|
+
name="new_source",
|
|
196
|
+
description="The new source code/content for the cell",
|
|
197
|
+
type="string",
|
|
198
|
+
required=True,
|
|
199
|
+
),
|
|
200
|
+
ToolParameter(
|
|
201
|
+
name="cell_type",
|
|
202
|
+
description="Cell type: 'code' or 'markdown'. Required for insert, optional for replace.",
|
|
203
|
+
type="string",
|
|
204
|
+
required=False,
|
|
205
|
+
default=None,
|
|
206
|
+
),
|
|
207
|
+
ToolParameter(
|
|
208
|
+
name="edit_mode",
|
|
209
|
+
description="Edit mode: 'replace' (update cell), 'insert' (add new cell), 'delete' (remove cell)",
|
|
210
|
+
type="string",
|
|
211
|
+
required=False,
|
|
212
|
+
default="replace",
|
|
213
|
+
),
|
|
214
|
+
]
|
|
215
|
+
|
|
216
|
+
def execute(
|
|
217
|
+
self,
|
|
218
|
+
notebook_path: str,
|
|
219
|
+
cell_number: int,
|
|
220
|
+
new_source: str,
|
|
221
|
+
cell_type: str = None,
|
|
222
|
+
edit_mode: str = "replace",
|
|
223
|
+
) -> ToolResult:
|
|
224
|
+
"""Edit notebook cell"""
|
|
225
|
+
path = Path(notebook_path).expanduser()
|
|
226
|
+
|
|
227
|
+
# Validate edit_mode
|
|
228
|
+
if edit_mode not in ["replace", "insert", "delete"]:
|
|
229
|
+
return ToolResult(
|
|
230
|
+
success=False,
|
|
231
|
+
output="",
|
|
232
|
+
error=f"Invalid edit_mode: {edit_mode}. Use 'replace', 'insert', or 'delete'.",
|
|
233
|
+
target=notebook_path,
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
# Check if file exists
|
|
237
|
+
if not path.exists():
|
|
238
|
+
return ToolResult(
|
|
239
|
+
success=False,
|
|
240
|
+
output="",
|
|
241
|
+
error=f"Notebook not found: {notebook_path}",
|
|
242
|
+
target=notebook_path,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
try:
|
|
246
|
+
# Read notebook
|
|
247
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
248
|
+
notebook = json.load(f)
|
|
249
|
+
|
|
250
|
+
cells = notebook.get("cells", [])
|
|
251
|
+
total_cells = len(cells)
|
|
252
|
+
|
|
253
|
+
# Validate cell number
|
|
254
|
+
if edit_mode == "insert":
|
|
255
|
+
# For insert, -1 means at the beginning, cell_number means after that cell
|
|
256
|
+
if cell_number < -1 or cell_number >= total_cells:
|
|
257
|
+
return ToolResult(
|
|
258
|
+
success=False,
|
|
259
|
+
output="",
|
|
260
|
+
error=f"Invalid cell number for insert: {cell_number}. Use -1 to {total_cells-1}.",
|
|
261
|
+
target=notebook_path,
|
|
262
|
+
)
|
|
263
|
+
else:
|
|
264
|
+
if cell_number < 0 or cell_number >= total_cells:
|
|
265
|
+
return ToolResult(
|
|
266
|
+
success=False,
|
|
267
|
+
output="",
|
|
268
|
+
error=f"Cell number {cell_number} out of range. Notebook has {total_cells} cells (0-{total_cells-1}).",
|
|
269
|
+
target=notebook_path,
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
# Perform the edit
|
|
273
|
+
if edit_mode == "delete":
|
|
274
|
+
deleted_cell = cells.pop(cell_number)
|
|
275
|
+
action_msg = f"Deleted cell {cell_number} ({deleted_cell.get('cell_type', 'unknown')})"
|
|
276
|
+
|
|
277
|
+
elif edit_mode == "insert":
|
|
278
|
+
# Determine cell type
|
|
279
|
+
if not cell_type:
|
|
280
|
+
cell_type = "code" # Default to code cell
|
|
281
|
+
if cell_type not in ["code", "markdown"]:
|
|
282
|
+
return ToolResult(
|
|
283
|
+
success=False,
|
|
284
|
+
output="",
|
|
285
|
+
error=f"Invalid cell_type: {cell_type}. Use 'code' or 'markdown'.",
|
|
286
|
+
target=notebook_path,
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
# Create new cell
|
|
290
|
+
new_cell = self._create_cell(cell_type, new_source)
|
|
291
|
+
insert_pos = cell_number + 1 # Insert after the specified cell
|
|
292
|
+
cells.insert(insert_pos, new_cell)
|
|
293
|
+
action_msg = f"Inserted new {cell_type} cell at position {insert_pos}"
|
|
294
|
+
|
|
295
|
+
else: # replace
|
|
296
|
+
old_cell = cells[cell_number]
|
|
297
|
+
old_source = "".join(old_cell.get("source", []))
|
|
298
|
+
|
|
299
|
+
# Update cell type if specified
|
|
300
|
+
if cell_type and cell_type in ["code", "markdown"]:
|
|
301
|
+
old_cell["cell_type"] = cell_type
|
|
302
|
+
|
|
303
|
+
# Update source
|
|
304
|
+
old_cell["source"] = new_source.split("\n")
|
|
305
|
+
# Add newlines except for last line
|
|
306
|
+
old_cell["source"] = [
|
|
307
|
+
line + "\n" if i < len(old_cell["source"]) - 1 else line
|
|
308
|
+
for i, line in enumerate(old_cell["source"])
|
|
309
|
+
]
|
|
310
|
+
|
|
311
|
+
# Clear outputs for code cells
|
|
312
|
+
if old_cell.get("cell_type") == "code":
|
|
313
|
+
old_cell["outputs"] = []
|
|
314
|
+
old_cell["execution_count"] = None
|
|
315
|
+
|
|
316
|
+
action_msg = f"Replaced cell {cell_number} ({old_cell.get('cell_type', 'unknown')})"
|
|
317
|
+
|
|
318
|
+
# Write back
|
|
319
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
320
|
+
json.dump(notebook, f, indent=1)
|
|
321
|
+
|
|
322
|
+
return ToolResult(
|
|
323
|
+
success=True,
|
|
324
|
+
output=f"{action_msg}\nNotebook saved: {notebook_path}",
|
|
325
|
+
target=f"{notebook_path}:cell_{cell_number}",
|
|
326
|
+
data={
|
|
327
|
+
"action": edit_mode,
|
|
328
|
+
"cell_number": cell_number,
|
|
329
|
+
"total_cells": len(cells),
|
|
330
|
+
},
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
except json.JSONDecodeError as e:
|
|
334
|
+
return ToolResult(
|
|
335
|
+
success=False,
|
|
336
|
+
output="",
|
|
337
|
+
error=f"Invalid notebook JSON: {e}",
|
|
338
|
+
target=notebook_path,
|
|
339
|
+
)
|
|
340
|
+
except Exception as e:
|
|
341
|
+
return ToolResult(
|
|
342
|
+
success=False,
|
|
343
|
+
output="",
|
|
344
|
+
error=f"Error editing notebook: {e}",
|
|
345
|
+
target=notebook_path,
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
def _create_cell(self, cell_type: str, source: str) -> Dict[str, Any]:
|
|
349
|
+
"""Create a new notebook cell"""
|
|
350
|
+
import uuid
|
|
351
|
+
|
|
352
|
+
source_lines = source.split("\n")
|
|
353
|
+
# Add newlines except for last line
|
|
354
|
+
source_lines = [
|
|
355
|
+
line + "\n" if i < len(source_lines) - 1 else line
|
|
356
|
+
for i, line in enumerate(source_lines)
|
|
357
|
+
]
|
|
358
|
+
|
|
359
|
+
cell = {
|
|
360
|
+
"cell_type": cell_type,
|
|
361
|
+
"id": str(uuid.uuid4())[:8],
|
|
362
|
+
"metadata": {},
|
|
363
|
+
"source": source_lines,
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if cell_type == "code":
|
|
367
|
+
cell["execution_count"] = None
|
|
368
|
+
cell["outputs"] = []
|
|
369
|
+
|
|
370
|
+
return cell
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
class NotebookRunTool(Tool):
|
|
374
|
+
"""Execute a notebook cell (requires jupyter kernel)"""
|
|
375
|
+
|
|
376
|
+
name = "NotebookRun"
|
|
377
|
+
description = (
|
|
378
|
+
"Execute a specific cell in a Jupyter notebook. "
|
|
379
|
+
"Requires nbconvert and a kernel to be available. "
|
|
380
|
+
"Returns the output of the execution."
|
|
381
|
+
)
|
|
382
|
+
category = "notebook"
|
|
383
|
+
permission = ToolPermission.ASK # Ask before executing code
|
|
384
|
+
|
|
385
|
+
parameters = [
|
|
386
|
+
ToolParameter(
|
|
387
|
+
name="notebook_path",
|
|
388
|
+
description="The absolute path to the Jupyter notebook file",
|
|
389
|
+
type="string",
|
|
390
|
+
required=True,
|
|
391
|
+
),
|
|
392
|
+
ToolParameter(
|
|
393
|
+
name="cell_number",
|
|
394
|
+
description="The cell number to execute (0-indexed)",
|
|
395
|
+
type="integer",
|
|
396
|
+
required=True,
|
|
397
|
+
),
|
|
398
|
+
ToolParameter(
|
|
399
|
+
name="timeout",
|
|
400
|
+
description="Execution timeout in seconds (default: 60)",
|
|
401
|
+
type="integer",
|
|
402
|
+
required=False,
|
|
403
|
+
default=60,
|
|
404
|
+
),
|
|
405
|
+
]
|
|
406
|
+
|
|
407
|
+
def execute(
|
|
408
|
+
self,
|
|
409
|
+
notebook_path: str,
|
|
410
|
+
cell_number: int,
|
|
411
|
+
timeout: int = 60,
|
|
412
|
+
) -> ToolResult:
|
|
413
|
+
"""Execute notebook cell"""
|
|
414
|
+
try:
|
|
415
|
+
import nbformat
|
|
416
|
+
from nbconvert.preprocessors import ExecutePreprocessor
|
|
417
|
+
except ImportError:
|
|
418
|
+
return ToolResult(
|
|
419
|
+
success=False,
|
|
420
|
+
output="",
|
|
421
|
+
error="nbconvert not installed. Install with: pip install nbconvert",
|
|
422
|
+
target=notebook_path,
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
path = Path(notebook_path).expanduser()
|
|
426
|
+
|
|
427
|
+
if not path.exists():
|
|
428
|
+
return ToolResult(
|
|
429
|
+
success=False,
|
|
430
|
+
output="",
|
|
431
|
+
error=f"Notebook not found: {notebook_path}",
|
|
432
|
+
target=notebook_path,
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
try:
|
|
436
|
+
# Read notebook
|
|
437
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
438
|
+
nb = nbformat.read(f, as_version=4)
|
|
439
|
+
|
|
440
|
+
cells = nb.get("cells", [])
|
|
441
|
+
if cell_number < 0 or cell_number >= len(cells):
|
|
442
|
+
return ToolResult(
|
|
443
|
+
success=False,
|
|
444
|
+
output="",
|
|
445
|
+
error=f"Cell number {cell_number} out of range",
|
|
446
|
+
target=notebook_path,
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
cell = cells[cell_number]
|
|
450
|
+
if cell.get("cell_type") != "code":
|
|
451
|
+
return ToolResult(
|
|
452
|
+
success=False,
|
|
453
|
+
output="",
|
|
454
|
+
error=f"Cell {cell_number} is not a code cell",
|
|
455
|
+
target=notebook_path,
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
# Create a mini-notebook with just this cell
|
|
459
|
+
mini_nb = nbformat.v4.new_notebook()
|
|
460
|
+
mini_nb.cells = [cell]
|
|
461
|
+
|
|
462
|
+
# Execute
|
|
463
|
+
ep = ExecutePreprocessor(timeout=timeout, kernel_name='python3')
|
|
464
|
+
ep.preprocess(mini_nb, {'metadata': {'path': str(path.parent)}})
|
|
465
|
+
|
|
466
|
+
# Get outputs
|
|
467
|
+
executed_cell = mini_nb.cells[0]
|
|
468
|
+
outputs = executed_cell.get("outputs", [])
|
|
469
|
+
|
|
470
|
+
# Format outputs
|
|
471
|
+
output_parts = [f"Executed cell {cell_number}:"]
|
|
472
|
+
for out in outputs:
|
|
473
|
+
out_type = out.get("output_type", "")
|
|
474
|
+
if out_type == "stream":
|
|
475
|
+
text = "".join(out.get("text", []))
|
|
476
|
+
output_parts.append(f"[stream] {text}")
|
|
477
|
+
elif out_type == "execute_result":
|
|
478
|
+
data = out.get("data", {})
|
|
479
|
+
if "text/plain" in data:
|
|
480
|
+
text = "".join(data["text/plain"])
|
|
481
|
+
output_parts.append(f"[result] {text}")
|
|
482
|
+
elif out_type == "error":
|
|
483
|
+
ename = out.get("ename", "Error")
|
|
484
|
+
evalue = out.get("evalue", "")
|
|
485
|
+
output_parts.append(f"[error] {ename}: {evalue}")
|
|
486
|
+
|
|
487
|
+
# Update the original notebook's cell
|
|
488
|
+
cells[cell_number] = executed_cell
|
|
489
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
490
|
+
nbformat.write(nb, f)
|
|
491
|
+
|
|
492
|
+
return ToolResult(
|
|
493
|
+
success=True,
|
|
494
|
+
output="\n".join(output_parts),
|
|
495
|
+
target=f"{notebook_path}:cell_{cell_number}",
|
|
496
|
+
data={
|
|
497
|
+
"cell_number": cell_number,
|
|
498
|
+
"execution_count": executed_cell.get("execution_count"),
|
|
499
|
+
},
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
except Exception as e:
|
|
503
|
+
return ToolResult(
|
|
504
|
+
success=False,
|
|
505
|
+
output="",
|
|
506
|
+
error=f"Error executing cell: {e}",
|
|
507
|
+
target=notebook_path,
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
# Register tools
|
|
512
|
+
def register_notebook_tools(registry):
|
|
513
|
+
"""Register all notebook tools with a registry"""
|
|
514
|
+
registry.register_class(NotebookReadTool)
|
|
515
|
+
registry.register_class(NotebookEditTool)
|
|
516
|
+
registry.register_class(NotebookRunTool)
|