cowork-dash 0.1.2__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.
cowork_dash/tools.py ADDED
@@ -0,0 +1,699 @@
1
+ from typing import Any, Dict, List, Optional
2
+ import sys
3
+ import io
4
+ import traceback
5
+ import subprocess
6
+ from contextlib import redirect_stdout, redirect_stderr
7
+
8
+ from .config import WORKSPACE_ROOT
9
+ from .canvas import parse_canvas_object
10
+
11
+
12
+ # =============================================================================
13
+ # JUPYTER-LIKE CODE EXECUTION TOOLS
14
+ # =============================================================================
15
+
16
+ class NotebookState:
17
+ """
18
+ Maintains persistent state for Jupyter-like code execution.
19
+
20
+ This class manages:
21
+ - An ordered list of code cells (the "script")
22
+ - A persistent namespace for variable state across cells
23
+ - Execution history and outputs
24
+ - Canvas items generated during cell execution
25
+ """
26
+
27
+ def __init__(self):
28
+ self._cells: List[Dict[str, Any]] = []
29
+ self._namespace: Dict[str, Any] = {}
30
+ self._execution_count: int = 0
31
+ self._ipython_shell = None
32
+ self._canvas_items: List[Dict[str, Any]] = [] # Collected canvas items
33
+ self._initialize_namespace()
34
+
35
+ def _initialize_namespace(self):
36
+ """Initialize the namespace with common imports and utilities."""
37
+ # Pre-populate with commonly used modules
38
+ init_code = """
39
+ import sys
40
+ import os
41
+ import json
42
+ from pathlib import Path
43
+
44
+ # Data science essentials (imported if available)
45
+ try:
46
+ import pandas as pd
47
+ except ImportError:
48
+ pass
49
+
50
+ try:
51
+ import numpy as np
52
+ except ImportError:
53
+ pass
54
+
55
+ try:
56
+ import matplotlib
57
+ matplotlib.use('Agg') # Non-interactive backend
58
+ import matplotlib.pyplot as plt
59
+ except ImportError:
60
+ pass
61
+
62
+ try:
63
+ import plotly.express as px
64
+ import plotly.graph_objects as go
65
+ except ImportError:
66
+ pass
67
+ """
68
+ # Execute initialization silently
69
+ try:
70
+ exec(init_code, self._namespace)
71
+ except Exception:
72
+ pass # Ignore import errors for optional packages
73
+
74
+ # Inject add_to_canvas function that captures items
75
+ def _add_to_canvas_wrapper(content: Any) -> Dict[str, Any]:
76
+ """Add content to the canvas for visualization.
77
+
78
+ Supports: DataFrames, matplotlib figures, plotly figures,
79
+ PIL images, and markdown strings.
80
+ """
81
+ try:
82
+ parsed = parse_canvas_object(content, workspace_root=WORKSPACE_ROOT)
83
+ self._canvas_items.append(parsed)
84
+ return parsed
85
+ except Exception as e:
86
+ error_result = {
87
+ "type": "error",
88
+ "data": f"Failed to add to canvas: {str(e)}",
89
+ "error": str(e)
90
+ }
91
+ self._canvas_items.append(error_result)
92
+ return error_result
93
+
94
+ self._namespace["add_to_canvas"] = _add_to_canvas_wrapper
95
+
96
+ def _get_ipython(self):
97
+ """Get or create an IPython InteractiveShell for enhanced execution."""
98
+ if self._ipython_shell is None:
99
+ try:
100
+ from IPython.core.interactiveshell import InteractiveShell
101
+ self._ipython_shell = InteractiveShell.instance()
102
+ # Share the namespace
103
+ self._ipython_shell.user_ns = self._namespace
104
+ except ImportError:
105
+ # IPython not available, will use exec() fallback
106
+ pass
107
+ return self._ipython_shell
108
+
109
+ @property
110
+ def cells(self) -> List[Dict[str, Any]]:
111
+ """Return a copy of all cells."""
112
+ return [cell.copy() for cell in self._cells]
113
+
114
+ @property
115
+ def namespace(self) -> Dict[str, Any]:
116
+ """Return the current namespace (variable state)."""
117
+ return self._namespace
118
+
119
+ def get_cell(self, cell_index: int) -> Optional[Dict[str, Any]]:
120
+ """Get a cell by index."""
121
+ if 0 <= cell_index < len(self._cells):
122
+ return self._cells[cell_index].copy()
123
+ return None
124
+
125
+ def add_cell(self, code: str, cell_type: str = "code") -> Dict[str, Any]:
126
+ """Add a new cell to the end of the script."""
127
+ cell = {
128
+ "index": len(self._cells),
129
+ "type": cell_type,
130
+ "source": code,
131
+ "execution_count": None,
132
+ "outputs": [],
133
+ "status": "pending"
134
+ }
135
+ self._cells.append(cell)
136
+ return cell.copy()
137
+
138
+ def insert_cell(self, index: int, code: str, cell_type: str = "code") -> Dict[str, Any]:
139
+ """Insert a cell at a specific index."""
140
+ if index < 0:
141
+ index = 0
142
+ if index > len(self._cells):
143
+ index = len(self._cells)
144
+
145
+ cell = {
146
+ "index": index,
147
+ "type": cell_type,
148
+ "source": code,
149
+ "execution_count": None,
150
+ "outputs": [],
151
+ "status": "pending"
152
+ }
153
+ self._cells.insert(index, cell)
154
+
155
+ # Update indices for subsequent cells
156
+ for i in range(index + 1, len(self._cells)):
157
+ self._cells[i]["index"] = i
158
+
159
+ return cell.copy()
160
+
161
+ def modify_cell(self, cell_index: int, new_code: str) -> Dict[str, Any]:
162
+ """Modify the code in an existing cell."""
163
+ if not (0 <= cell_index < len(self._cells)):
164
+ return {
165
+ "error": f"Cell index {cell_index} out of range. Valid range: 0-{len(self._cells) - 1}"
166
+ }
167
+
168
+ self._cells[cell_index]["source"] = new_code
169
+ self._cells[cell_index]["status"] = "modified"
170
+ self._cells[cell_index]["outputs"] = [] # Clear previous outputs
171
+
172
+ return self._cells[cell_index].copy()
173
+
174
+ def delete_cell(self, cell_index: int) -> Dict[str, Any]:
175
+ """Delete a cell by index."""
176
+ if not (0 <= cell_index < len(self._cells)):
177
+ return {
178
+ "error": f"Cell index {cell_index} out of range. Valid range: 0-{len(self._cells) - 1}"
179
+ }
180
+
181
+ deleted_cell = self._cells.pop(cell_index)
182
+
183
+ # Update indices for subsequent cells
184
+ for i in range(cell_index, len(self._cells)):
185
+ self._cells[i]["index"] = i
186
+
187
+ return {"deleted": deleted_cell, "remaining_cells": len(self._cells)}
188
+
189
+ def execute_cell(self, cell_index: int) -> Dict[str, Any]:
190
+ """Execute a single cell and capture its output."""
191
+ if not (0 <= cell_index < len(self._cells)):
192
+ return {
193
+ "error": f"Cell index {cell_index} out of range. Valid range: 0-{len(self._cells) - 1}"
194
+ }
195
+
196
+ cell = self._cells[cell_index]
197
+
198
+ if cell["type"] != "code":
199
+ return {
200
+ "index": cell_index,
201
+ "type": cell["type"],
202
+ "source": cell["source"],
203
+ "output": "(markdown cell - not executed)",
204
+ "status": "skipped"
205
+ }
206
+
207
+ self._execution_count += 1
208
+ cell["execution_count"] = self._execution_count
209
+
210
+ # Track canvas items added during this cell's execution
211
+ canvas_count_before = len(self._canvas_items)
212
+
213
+ # Capture stdout and stderr
214
+ stdout_capture = io.StringIO()
215
+ stderr_capture = io.StringIO()
216
+
217
+ result = {
218
+ "index": cell_index,
219
+ "execution_count": self._execution_count,
220
+ "source": cell["source"],
221
+ "stdout": "",
222
+ "stderr": "",
223
+ "result": None,
224
+ "error": None,
225
+ "status": "success",
226
+ "canvas_items": [] # Canvas items added during execution
227
+ }
228
+
229
+ try:
230
+ # Try IPython first for better execution handling
231
+ ipython = self._get_ipython()
232
+
233
+ if ipython is not None:
234
+ # Use IPython's run_cell for magic commands support
235
+ with redirect_stdout(stdout_capture), redirect_stderr(stderr_capture):
236
+ exec_result = ipython.run_cell(cell["source"], store_history=True)
237
+
238
+ result["stdout"] = stdout_capture.getvalue()
239
+ result["stderr"] = stderr_capture.getvalue()
240
+
241
+ if exec_result.success:
242
+ if exec_result.result is not None:
243
+ result["result"] = repr(exec_result.result)
244
+ else:
245
+ if exec_result.error_in_exec:
246
+ result["error"] = str(exec_result.error_in_exec)
247
+ result["status"] = "error"
248
+ elif exec_result.error_before_exec:
249
+ result["error"] = str(exec_result.error_before_exec)
250
+ result["status"] = "error"
251
+ else:
252
+ # Fallback to exec() if IPython is not available
253
+ with redirect_stdout(stdout_capture), redirect_stderr(stderr_capture):
254
+ # Compile to check for expression vs statement
255
+ code = cell["source"].strip()
256
+
257
+ # Try to evaluate as expression first (to get return value)
258
+ try:
259
+ # Check if it's a simple expression
260
+ compiled = compile(code, "<cell>", "eval")
261
+ exec_result = eval(compiled, self._namespace)
262
+ if exec_result is not None:
263
+ result["result"] = repr(exec_result)
264
+ except SyntaxError:
265
+ # It's a statement, execute it
266
+ exec(code, self._namespace)
267
+
268
+ result["stdout"] = stdout_capture.getvalue()
269
+ result["stderr"] = stderr_capture.getvalue()
270
+
271
+ except Exception:
272
+ result["error"] = traceback.format_exc()
273
+ result["status"] = "error"
274
+ result["stdout"] = stdout_capture.getvalue()
275
+ result["stderr"] = stderr_capture.getvalue()
276
+
277
+ # Capture any canvas items added during this cell's execution
278
+ canvas_items_added = self._canvas_items[canvas_count_before:]
279
+ result["canvas_items"] = canvas_items_added
280
+
281
+ # Store outputs in cell
282
+ cell["outputs"] = [result]
283
+ cell["status"] = result["status"]
284
+
285
+ return result
286
+
287
+ def execute_all(self) -> List[Dict[str, Any]]:
288
+ """Execute all cells in order."""
289
+ results = []
290
+ for i in range(len(self._cells)):
291
+ results.append(self.execute_cell(i))
292
+ return results
293
+
294
+ def get_script(self) -> str:
295
+ """Get all code cells concatenated as a single script."""
296
+ code_cells = [cell["source"] for cell in self._cells if cell["type"] == "code"]
297
+ return "\n\n".join(code_cells)
298
+
299
+ def get_variables(self) -> Dict[str, str]:
300
+ """Get a summary of user-defined variables in the namespace."""
301
+ # Filter out modules, builtins, and private variables
302
+ user_vars = {}
303
+ for name, value in self._namespace.items():
304
+ if name.startswith("_"):
305
+ continue
306
+ if isinstance(value, type(sys)): # Skip modules
307
+ continue
308
+ if callable(value) and hasattr(value, "__module__"):
309
+ # Skip imported functions
310
+ if value.__module__ != "__main__" and value.__module__ not in [None, "builtins"]:
311
+ continue
312
+ try:
313
+ # Get a short repr
314
+ value_repr = repr(value)
315
+ if len(value_repr) > 100:
316
+ value_repr = value_repr[:97] + "..."
317
+ user_vars[name] = f"{type(value).__name__}: {value_repr}"
318
+ except Exception:
319
+ user_vars[name] = f"{type(value).__name__}: <unable to repr>"
320
+ return user_vars
321
+
322
+ def get_canvas_items(self) -> List[Dict[str, Any]]:
323
+ """Get all canvas items collected during execution."""
324
+ return self._canvas_items.copy()
325
+
326
+ def clear_canvas_items(self) -> Dict[str, Any]:
327
+ """Clear collected canvas items."""
328
+ count = len(self._canvas_items)
329
+ self._canvas_items = []
330
+ return {"cleared": count}
331
+
332
+ def reset(self):
333
+ """Reset the notebook state (clear all cells and namespace)."""
334
+ self._cells = []
335
+ self._namespace = {}
336
+ self._execution_count = 0
337
+ self._canvas_items = []
338
+ self._initialize_namespace()
339
+ return {"status": "reset", "message": "Notebook state cleared"}
340
+
341
+
342
+ # Global notebook state instance
343
+ _notebook_state = NotebookState()
344
+
345
+
346
+ def create_cell(code: str, cell_type: str = "code") -> Dict[str, Any]:
347
+ """
348
+ Create a new code or markdown cell and add it to the end of the script.
349
+
350
+ This simulates creating a new cell in a Jupyter notebook. The cell is added
351
+ but not executed - use execute_cell() to run it.
352
+
353
+ Args:
354
+ code: The Python code or markdown content for the cell
355
+ cell_type: Either "code" or "markdown" (default: "code")
356
+
357
+ Returns:
358
+ Dictionary with cell information including:
359
+ - index: The cell's position in the notebook
360
+ - type: The cell type
361
+ - source: The cell's code/content
362
+ - status: "pending" (not yet executed)
363
+
364
+ Examples:
365
+ # Create a code cell
366
+ create_cell("x = 42\\nprint(f'x = {x}')")
367
+
368
+ # Create a markdown cell
369
+ create_cell("## Analysis Results", cell_type="markdown")
370
+ """
371
+ return _notebook_state.add_cell(code, cell_type)
372
+
373
+
374
+ def insert_cell(index: int, code: str, cell_type: str = "code") -> Dict[str, Any]:
375
+ """
376
+ Insert a new cell at a specific position in the script.
377
+
378
+ This is useful when you need to add code between existing cells,
379
+ such as adding a missing import or intermediate calculation.
380
+
381
+ Args:
382
+ index: Position to insert the cell (0-based). Cells after this
383
+ position will be shifted down.
384
+ code: The Python code or markdown content
385
+ cell_type: Either "code" or "markdown" (default: "code")
386
+
387
+ Returns:
388
+ Dictionary with cell information including index and status
389
+
390
+ Examples:
391
+ # Insert an import at the beginning
392
+ insert_cell(0, "import pandas as pd")
393
+
394
+ # Insert a cell between cells 2 and 3
395
+ insert_cell(3, "intermediate_result = process(data)")
396
+ """
397
+ return _notebook_state.insert_cell(index, code, cell_type)
398
+
399
+
400
+ def modify_cell(cell_index: int, new_code: str) -> Dict[str, Any]:
401
+ """
402
+ Modify the code in an existing cell.
403
+
404
+ Use this to fix errors, update logic, or refine code in a cell.
405
+ The cell's outputs are cleared and status set to "modified".
406
+ You'll need to re-execute the cell to see the new results.
407
+
408
+ Args:
409
+ cell_index: The index of the cell to modify (0-based)
410
+ new_code: The new code to replace the existing code
411
+
412
+ Returns:
413
+ Dictionary with updated cell information, or error if index invalid
414
+
415
+ Examples:
416
+ # Fix a typo in cell 2
417
+ modify_cell(2, "result = data.groupby('category').mean()")
418
+
419
+ # Update a calculation
420
+ modify_cell(0, "threshold = 0.95 # Updated from 0.9")
421
+ """
422
+ return _notebook_state.modify_cell(cell_index, new_code)
423
+
424
+
425
+ def delete_cell(cell_index: int) -> Dict[str, Any]:
426
+ """
427
+ Delete a cell from the script.
428
+
429
+ Removes the cell at the specified index. Subsequent cells will have
430
+ their indices updated. Note: This does NOT undo any side effects
431
+ from executing the deleted cell (variables remain in namespace).
432
+
433
+ Args:
434
+ cell_index: The index of the cell to delete (0-based)
435
+
436
+ Returns:
437
+ Dictionary with deleted cell info and remaining cell count
438
+
439
+ Examples:
440
+ # Remove cell 3
441
+ delete_cell(3)
442
+ """
443
+ return _notebook_state.delete_cell(cell_index)
444
+
445
+
446
+ def execute_cell(cell_index: int) -> Dict[str, Any]:
447
+ """
448
+ Execute a single cell and return its output.
449
+
450
+ Runs the code in the specified cell within the persistent namespace.
451
+ Variables created or modified will be available to subsequent cells.
452
+ Captures stdout, stderr, and the cell's return value (if any).
453
+
454
+ Args:
455
+ cell_index: The index of the cell to execute (0-based)
456
+
457
+ Returns:
458
+ Dictionary containing:
459
+ - index: Cell index
460
+ - execution_count: Global execution counter
461
+ - source: The executed code
462
+ - stdout: Captured print() output
463
+ - stderr: Captured error output
464
+ - result: Return value of the last expression (if any)
465
+ - error: Error traceback (if execution failed)
466
+ - status: "success" or "error"
467
+
468
+ Examples:
469
+ # Execute the first cell
470
+ execute_cell(0)
471
+
472
+ # Execute and check for errors
473
+ result = execute_cell(2)
474
+ if result["status"] == "error":
475
+ print(result["error"])
476
+ """
477
+ return _notebook_state.execute_cell(cell_index)
478
+
479
+
480
+ def execute_all_cells() -> List[Dict[str, Any]]:
481
+ """
482
+ Execute all cells in the script in order.
483
+
484
+ Runs each cell sequentially from the beginning. Useful for
485
+ re-running the entire notebook after modifications.
486
+
487
+ Returns:
488
+ List of execution results for each cell
489
+
490
+ Examples:
491
+ # Run entire notebook
492
+ results = execute_all_cells()
493
+ errors = [r for r in results if r.get("status") == "error"]
494
+ """
495
+ return _notebook_state.execute_all()
496
+
497
+
498
+ def get_script() -> Dict[str, Any]:
499
+ """
500
+ Get the complete script and current state.
501
+
502
+ Returns all cells, the concatenated code, and current variable state.
503
+ Useful for reviewing the notebook or exporting the code.
504
+
505
+ Returns:
506
+ Dictionary containing:
507
+ - cells: List of all cells with their content and outputs
508
+ - script: All code cells concatenated as a single script
509
+ - variables: Summary of user-defined variables
510
+ - cell_count: Total number of cells
511
+
512
+ Examples:
513
+ # Review current state
514
+ state = get_script()
515
+ print(f"Notebook has {state['cell_count']} cells")
516
+ print(state['script'])
517
+ """
518
+ return {
519
+ "cells": _notebook_state.cells,
520
+ "script": _notebook_state.get_script(),
521
+ "variables": _notebook_state.get_variables(),
522
+ "cell_count": len(_notebook_state.cells)
523
+ }
524
+
525
+
526
+ def get_variables() -> Dict[str, str]:
527
+ """
528
+ Get a summary of all user-defined variables in the namespace.
529
+
530
+ Returns variable names with their types and values (truncated if long).
531
+ Useful for understanding what data is available for use in new cells.
532
+
533
+ Returns:
534
+ Dictionary mapping variable names to "type: value" strings
535
+
536
+ Examples:
537
+ # Check available variables
538
+ vars = get_variables()
539
+ for name, info in vars.items():
540
+ print(f"{name}: {info}")
541
+ """
542
+ return _notebook_state.get_variables()
543
+
544
+
545
+ def reset_notebook() -> Dict[str, Any]:
546
+ """
547
+ Reset the notebook state completely.
548
+
549
+ Clears all cells and resets the namespace to its initial state.
550
+ Use with caution - this cannot be undone.
551
+
552
+ Returns:
553
+ Dictionary confirming the reset
554
+
555
+ Examples:
556
+ # Start fresh
557
+ reset_notebook()
558
+ """
559
+ return _notebook_state.reset()
560
+
561
+
562
+ def get_notebook_canvas_items() -> List[Dict[str, Any]]:
563
+ """
564
+ Get all canvas items generated during notebook cell execution.
565
+
566
+ When code in cells calls add_to_canvas(), the items are collected here.
567
+ Use this to retrieve visualizations generated by executed code.
568
+
569
+ Returns:
570
+ List of canvas item dictionaries with type and data
571
+
572
+ Examples:
573
+ # After executing cells that created charts
574
+ items = get_notebook_canvas_items()
575
+ for item in items:
576
+ print(f"Type: {item['type']}")
577
+ """
578
+ return _notebook_state.get_canvas_items()
579
+
580
+
581
+ def clear_notebook_canvas_items() -> Dict[str, Any]:
582
+ """
583
+ Clear all canvas items collected from notebook execution.
584
+
585
+ Returns:
586
+ Dictionary with count of cleared items
587
+ """
588
+ return _notebook_state.clear_canvas_items()
589
+
590
+
591
+ # =============================================================================
592
+ # CANVAS TOOLS
593
+ # =============================================================================
594
+
595
+ def add_to_canvas(content: Any) -> Dict[str, Any]:
596
+ """Add an item to the canvas for visualization. Canvas is like a note-taking tool where
597
+ you can store charts, dataframes, images, and markdown text for the user to see.
598
+
599
+ Args:
600
+ content: Can be a pandas DataFrame, matplotlib Figure, plotly Figure,
601
+ PIL Image, dictionary (for Plotly JSON), or string (for Markdown)
602
+ workspace_root: Path to the workspace root directory
603
+
604
+ Returns:
605
+ Dictionary with the parsed canvas object
606
+
607
+ Examples:
608
+ # Add a DataFrame
609
+ import pandas as pd
610
+ df = pd.DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]})
611
+ add_to_canvas(df)
612
+
613
+ # Add a Matplotlib chart
614
+ import matplotlib.pyplot as plt
615
+ fig, ax = plt.subplots()
616
+ ax.plot([1, 2, 3], [1, 4, 9])
617
+ add_to_canvas(fig)
618
+
619
+ # Add Markdown text
620
+ add_to_canvas("## Key Findings\\n- Point 1\\n- Point 2")
621
+ """
622
+ try:
623
+ # Parse the content into canvas format
624
+ parsed = parse_canvas_object(content, workspace_root=WORKSPACE_ROOT)
625
+ # Return the parsed object (deepagents will handle the JSON serialization)
626
+ return parsed
627
+ except Exception as e:
628
+ return {
629
+ "type": "error",
630
+ "data": f"Failed to add to canvas: {str(e)}",
631
+ "error": str(e)
632
+ }
633
+
634
+
635
+ # =============================================================================
636
+ # BASH TOOL
637
+ # =============================================================================
638
+
639
+ def bash(command: str, timeout: int = 60) -> Dict[str, Any]:
640
+ """Execute a bash command and return the output.
641
+
642
+ Runs the command in the workspace directory. Use this for file operations,
643
+ git commands, installing packages, or any shell operations.
644
+
645
+ Args:
646
+ command: The bash command to execute
647
+ timeout: Maximum time in seconds to wait for the command (default: 60)
648
+
649
+ Returns:
650
+ Dictionary containing:
651
+ - stdout: Standard output from the command
652
+ - stderr: Standard error output
653
+ - return_code: Exit code (0 typically means success)
654
+ - status: "success" or "error"
655
+
656
+ Examples:
657
+ # List files
658
+ bash("ls -la")
659
+
660
+ # Check git status
661
+ bash("git status")
662
+
663
+ # Install a package
664
+ bash("pip install pandas")
665
+
666
+ # Run a script
667
+ bash("python script.py")
668
+ """
669
+ try:
670
+ result = subprocess.run(
671
+ command,
672
+ shell=True,
673
+ cwd=str(WORKSPACE_ROOT),
674
+ capture_output=True,
675
+ text=True,
676
+ timeout=timeout
677
+ )
678
+
679
+ return {
680
+ "stdout": result.stdout,
681
+ "stderr": result.stderr,
682
+ "return_code": result.returncode,
683
+ "status": "success" if result.returncode == 0 else "error"
684
+ }
685
+
686
+ except subprocess.TimeoutExpired:
687
+ return {
688
+ "stdout": "",
689
+ "stderr": f"Command timed out after {timeout} seconds",
690
+ "return_code": -1,
691
+ "status": "error"
692
+ }
693
+ except Exception as e:
694
+ return {
695
+ "stdout": "",
696
+ "stderr": str(e),
697
+ "return_code": -1,
698
+ "status": "error"
699
+ }