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/canvas.py ADDED
@@ -0,0 +1,318 @@
1
+ """Canvas utilities for parsing, exporting, and loading canvas objects."""
2
+
3
+ import io
4
+ import json
5
+ import base64
6
+ import re
7
+ from pathlib import Path
8
+ from typing import Any, Dict, List
9
+ from datetime import datetime
10
+
11
+ def parse_canvas_object(obj: Any, workspace_root: Path) -> Dict[str, Any]:
12
+ """Parse Python objects into canvas-renderable format.
13
+
14
+ Supports:
15
+ - pd.DataFrame (inline in markdown)
16
+ - matplotlib.figure.Figure (saved to .canvas/ folder)
17
+ - plotly.graph_objects.Figure (saved to .canvas/ folder)
18
+ - PIL.Image (saved to .canvas/ folder)
19
+ - dict (Plotly JSON - saved to .canvas/ folder)
20
+ - str (Markdown with Mermaid support - inline)
21
+ """
22
+ obj_type = type(obj).__name__
23
+ module = type(obj).__module__
24
+
25
+ # Ensure .canvas directory exists
26
+ canvas_dir = workspace_root / ".canvas"
27
+ canvas_dir.mkdir(exist_ok=True)
28
+
29
+ # Pandas DataFrame - keep inline
30
+ if module.startswith('pandas') and obj_type == 'DataFrame':
31
+ return {
32
+ "type": "dataframe",
33
+ "data": obj.to_dict('records'),
34
+ "columns": list(obj.columns),
35
+ "html": obj.to_html(index=False, classes="dataframe-table")
36
+ }
37
+
38
+ # Matplotlib Figure - save to file
39
+ elif module.startswith('matplotlib') and 'Figure' in obj_type:
40
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
41
+ filename = f"matplotlib_{timestamp}.png"
42
+ filepath = canvas_dir / filename
43
+
44
+ obj.savefig(filepath, format='png', bbox_inches='tight', dpi=100)
45
+
46
+ # Also store base64 for in-memory rendering
47
+ buf = io.BytesIO()
48
+ obj.savefig(buf, format='png', bbox_inches='tight', dpi=100)
49
+ buf.seek(0)
50
+ img_base64 = base64.b64encode(buf.read()).decode('utf-8')
51
+ buf.close()
52
+
53
+ return {
54
+ "type": "matplotlib",
55
+ "file": filename, # Relative to .canvas/ directory where canvas.md lives
56
+ "data": img_base64 # Keep for current session rendering
57
+ }
58
+
59
+ # Plotly Figure - save to file
60
+ elif module.startswith('plotly') and 'Figure' in obj_type:
61
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
62
+ filename = f"plotly_{timestamp}.json"
63
+ filepath = canvas_dir / filename
64
+
65
+ plotly_data = json.loads(obj.to_json())
66
+ filepath.write_text(json.dumps(plotly_data, indent=2))
67
+
68
+ return {
69
+ "type": "plotly",
70
+ "file": filename, # Relative to .canvas/ directory where canvas.md lives
71
+ "data": plotly_data # Keep for current session rendering
72
+ }
73
+
74
+ # PIL Image - save to file
75
+ elif module.startswith('PIL') and 'Image' in obj_type:
76
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
77
+ filename = f"image_{timestamp}.png"
78
+ filepath = canvas_dir / filename
79
+
80
+ obj.save(filepath, format='PNG')
81
+
82
+ # Also store base64 for in-memory rendering
83
+ buf = io.BytesIO()
84
+ obj.save(buf, format='PNG')
85
+ buf.seek(0)
86
+ img_base64 = base64.b64encode(buf.read()).decode('utf-8')
87
+ buf.close()
88
+
89
+ return {
90
+ "type": "image",
91
+ "file": filename, # Relative to .canvas/ directory where canvas.md lives
92
+ "data": img_base64 # Keep for current session rendering
93
+ }
94
+
95
+ # Plotly dict format - save to file
96
+ elif isinstance(obj, dict) and ('data' in obj or 'layout' in obj):
97
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
98
+ filename = f"plotly_{timestamp}.json"
99
+ filepath = canvas_dir / filename
100
+
101
+ filepath.write_text(json.dumps(obj, indent=2))
102
+
103
+ return {
104
+ "type": "plotly",
105
+ "file": filename, # Relative to .canvas/ directory where canvas.md lives
106
+ "data": obj # Keep for current session rendering
107
+ }
108
+
109
+ # Markdown string - check for Mermaid diagrams - keep inline
110
+ elif isinstance(obj, str):
111
+ # Check if it's a Mermaid diagram
112
+ if re.search(r'```mermaid', obj, re.IGNORECASE):
113
+ # Extract mermaid code - more flexible pattern
114
+ match = re.search(r'```mermaid\s*\n?(.*?)```', obj, re.DOTALL | re.IGNORECASE)
115
+ if match:
116
+ mermaid_code = match.group(1).strip()
117
+ return {
118
+ "type": "mermaid",
119
+ "data": mermaid_code
120
+ }
121
+
122
+ return {
123
+ "type": "markdown",
124
+ "data": obj
125
+ }
126
+
127
+ # Unknown type - convert to string - keep inline
128
+ else:
129
+ return {
130
+ "type": "markdown",
131
+ "data": f"```\n{str(obj)}\n```"
132
+ }
133
+
134
+
135
+ def export_canvas_to_markdown(canvas_items: List[Dict], workspace_root: Path, output_path: str = None):
136
+ """Export canvas to markdown file with file references."""
137
+ # Ensure .canvas directory exists
138
+ canvas_dir = workspace_root / ".canvas"
139
+ canvas_dir.mkdir(exist_ok=True)
140
+
141
+ if not output_path:
142
+ output_path = canvas_dir / "canvas.md"
143
+
144
+ lines = [
145
+ "# Canvas Export",
146
+ f"\n*Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*\n",
147
+ ]
148
+
149
+ for i, parsed in enumerate(canvas_items):
150
+ item_type = parsed.get("type", "unknown")
151
+
152
+ # Add title if present
153
+ if "title" in parsed:
154
+ lines.append(f"\n## {parsed['title']}\n")
155
+
156
+ if item_type == "markdown":
157
+ lines.append(f"\n{parsed.get('data', '')}\n")
158
+
159
+ elif item_type == "mermaid":
160
+ lines.append(f"\n```mermaid\n{parsed.get('data', '')}\n```\n")
161
+
162
+ elif item_type == "dataframe":
163
+ lines.append(f"\n{parsed.get('html', '')}\n")
164
+
165
+ elif item_type == "matplotlib" or item_type == "image":
166
+ # Reference the file instead of embedding base64
167
+ file_ref = parsed.get("file", "")
168
+ if file_ref:
169
+ lines.append(f"\n![Image]({file_ref})\n")
170
+ else:
171
+ # Fallback to base64 if no file
172
+ img_data = parsed.get("data", "")
173
+ lines.append(f"\n![Chart {i+1}](data:image/png;base64,{img_data})\n")
174
+
175
+ elif item_type == "plotly":
176
+ # Reference the file
177
+ file_ref = parsed.get("file", "")
178
+ if file_ref:
179
+ lines.append(f"\n```plotly\n{file_ref}\n```\n")
180
+ else:
181
+ # Fallback to inline
182
+ lines.append(f"\n```json\n{json.dumps(parsed.get('data'), indent=2)}\n```\n")
183
+
184
+ # Write to file
185
+ output_file = Path(output_path)
186
+ output_file.write_text("\n".join(lines))
187
+ return str(output_file)
188
+
189
+
190
+ def load_canvas_from_markdown(workspace_root: Path, markdown_path: str = None) -> List[Dict]:
191
+ """Load canvas from markdown file and referenced assets."""
192
+ if not markdown_path:
193
+ markdown_path = workspace_root / ".canvas" / "canvas.md"
194
+ else:
195
+ markdown_path = Path(markdown_path)
196
+
197
+ if not markdown_path.exists():
198
+ return []
199
+
200
+ content = markdown_path.read_text()
201
+ canvas_items = []
202
+
203
+ # First, extract all code blocks to process them separately
204
+ code_blocks = []
205
+
206
+ # Find all mermaid blocks
207
+ for match in re.finditer(r'```mermaid\s*\n(.*?)```', content, re.DOTALL | re.IGNORECASE):
208
+ start, end = match.span()
209
+ code_blocks.append({
210
+ 'type': 'mermaid',
211
+ 'start': start,
212
+ 'end': end,
213
+ 'content': match.group(1).strip()
214
+ })
215
+
216
+ # Find all plotly blocks (supports both relative filenames and legacy .canvas/ paths)
217
+ for match in re.finditer(r'```plotly\s*\n([^\n]+)\n```', content, re.DOTALL):
218
+ start, end = match.span()
219
+ code_blocks.append({
220
+ 'type': 'plotly_file',
221
+ 'start': start,
222
+ 'end': end,
223
+ 'content': match.group(1).strip()
224
+ })
225
+
226
+ # Find all image references (supports both relative filenames and legacy .canvas/ paths)
227
+ for match in re.finditer(r'!\[.*?\]\(([^)]+)\)', content):
228
+ start, end = match.span()
229
+ file_ref = match.group(1)
230
+ # Skip data: URLs (base64 embedded images)
231
+ if not file_ref.startswith('data:'):
232
+ code_blocks.append({
233
+ 'type': 'image_file',
234
+ 'start': start,
235
+ 'end': end,
236
+ 'content': file_ref
237
+ })
238
+
239
+ # Find all HTML tables
240
+ for match in re.finditer(r'<table.*?</table>', content, re.DOTALL):
241
+ start, end = match.span()
242
+ code_blocks.append({
243
+ 'type': 'table',
244
+ 'start': start,
245
+ 'end': end,
246
+ 'content': match.group(0)
247
+ })
248
+
249
+ # Sort blocks by position
250
+ code_blocks.sort(key=lambda x: x['start'])
251
+
252
+ # Process content in order
253
+ last_pos = 0
254
+ for block in code_blocks:
255
+ # Add any markdown content before this block
256
+ if block['start'] > last_pos:
257
+ markdown_text = content[last_pos:block['start']].strip()
258
+ # Clean up metadata lines but keep actual content
259
+ lines = markdown_text.split('\n')
260
+ filtered_lines = []
261
+ for line in lines:
262
+ # Skip only the exact metadata lines
263
+ if line.strip() in ['# Canvas Export', ''] or line.strip().startswith('*Generated:'):
264
+ continue
265
+ filtered_lines.append(line)
266
+
267
+ cleaned_text = '\n'.join(filtered_lines).strip()
268
+ if cleaned_text:
269
+ canvas_items.append({
270
+ "type": "markdown",
271
+ "data": cleaned_text
272
+ })
273
+
274
+ # Add the block itself
275
+ if block['type'] == 'mermaid':
276
+ canvas_items.append({
277
+ "type": "mermaid",
278
+ "data": block['content']
279
+ })
280
+ elif block['type'] == 'plotly_file':
281
+ file_ref = block['content']
282
+ file_path = markdown_path.parent / file_ref
283
+ if file_path.exists():
284
+ plotly_data = json.loads(file_path.read_text())
285
+ canvas_items.append({
286
+ "type": "plotly",
287
+ "file": file_ref,
288
+ "data": plotly_data
289
+ })
290
+ elif block['type'] == 'image_file':
291
+ file_ref = block['content']
292
+ file_path = markdown_path.parent / file_ref
293
+ if file_path.exists():
294
+ with open(file_path, 'rb') as f:
295
+ img_base64 = base64.b64encode(f.read()).decode('utf-8')
296
+ canvas_items.append({
297
+ "type": "image",
298
+ "file": file_ref,
299
+ "data": img_base64
300
+ })
301
+ elif block['type'] == 'table':
302
+ canvas_items.append({
303
+ "type": "dataframe",
304
+ "html": block['content']
305
+ })
306
+
307
+ last_pos = block['end']
308
+
309
+ # Add any remaining markdown after the last block
310
+ if last_pos < len(content):
311
+ markdown_text = content[last_pos:].strip()
312
+ if markdown_text:
313
+ canvas_items.append({
314
+ "type": "markdown",
315
+ "data": markdown_text
316
+ })
317
+
318
+ return canvas_items
cowork_dash/cli.py ADDED
@@ -0,0 +1,273 @@
1
+ #!/usr/bin/env python3
2
+ """Command-line interface for Cowork Dash (formerly DeepAgent Dash)."""
3
+
4
+ import sys
5
+ import shutil
6
+ from pathlib import Path
7
+ import argparse
8
+
9
+
10
+ def init_project(name: str, template: str = "default"):
11
+ """Initialize a new Cowork Dash project."""
12
+ project_dir = Path(name).resolve()
13
+
14
+ if project_dir.exists():
15
+ print(f"❌ Error: Directory '{name}' already exists")
16
+ return 1
17
+
18
+ print(f"📦 Creating project: {project_dir}")
19
+
20
+ # Create project structure
21
+ project_dir.mkdir(parents=True)
22
+ workspace_dir = project_dir / "workspace"
23
+ workspace_dir.mkdir()
24
+
25
+ # Copy config template
26
+ import cowork_dash
27
+ package_dir = Path(cowork_dash.__file__).parent
28
+ template_file = package_dir / "config.py"
29
+
30
+ if not template_file.exists():
31
+ print(f"❌ Error: Template not found at {template_file}")
32
+ return 1
33
+
34
+ shutil.copy(template_file, project_dir / "config.py")
35
+
36
+ # Create .env template
37
+ env_template = """# Cowork Dash Environment Variables
38
+
39
+ # API Keys
40
+ ANTHROPIC_API_KEY=your_api_key_here
41
+
42
+ # Optional: Override config.py settings (uses DEEPAGENT_* prefix for compatibility)
43
+ # DEEPAGENT_WORKSPACE_ROOT=./workspace
44
+ # DEEPAGENT_PORT=8050
45
+ # DEEPAGENT_HOST=localhost
46
+ # DEEPAGENT_DEBUG=False
47
+ """
48
+ (project_dir / ".env.example").write_text(env_template)
49
+
50
+ # Create .gitignore
51
+ gitignore = """# Python
52
+ __pycache__/
53
+ *.py[cod]
54
+ *$py.class
55
+ *.so
56
+ .Python
57
+ env/
58
+ venv/
59
+ ENV/
60
+
61
+ # Cowork Dash
62
+ .env
63
+ workspace/
64
+ canvas.md
65
+ .canvas/
66
+ *.log
67
+
68
+ # IDE
69
+ .vscode/
70
+ .idea/
71
+ *.swp
72
+ *.swo
73
+ """
74
+ (project_dir / ".gitignore").write_text(gitignore)
75
+
76
+ # Create README
77
+ readme = f"""# {name}
78
+
79
+ A Cowork Dash project.
80
+
81
+ ## Setup
82
+
83
+ 1. **Configure your API key** (if using DeepAgents):
84
+ ```bash
85
+ cp .env.example .env
86
+ # Edit .env and add your ANTHROPIC_API_KEY
87
+ ```
88
+
89
+ 2. **Edit config.py** to customize your agent and settings
90
+
91
+ 3. **Run the application**:
92
+ ```bash
93
+ cowork-dash run
94
+ ```
95
+
96
+ ## Usage
97
+
98
+ ```bash
99
+ # Run with defaults from config.py
100
+ cowork-dash run
101
+
102
+ # Override settings
103
+ cowork-dash run --port 8080 --debug
104
+
105
+ # Use custom agent
106
+ cowork-dash run --agent my_agent.py:agent
107
+
108
+ # See all options
109
+ cowork-dash run --help
110
+ ```
111
+
112
+ ## Project Structure
113
+
114
+ ```
115
+ {name}/
116
+ ├── config.py # Main configuration (edit this)
117
+ ├── workspace/ # Your agent's workspace
118
+ ├── .env.example # Environment variables template
119
+ └── .gitignore # Git ignore patterns
120
+ ```
121
+
122
+ ## Documentation
123
+
124
+ - [Cowork Dash Documentation](https://github.com/dkedar7/cowork-dash)
125
+ - [CLI Usage Guide](https://github.com/dkedar7/cowork-dash/blob/main/docs/CLI_USAGE.md)
126
+ """
127
+ (project_dir / "README.md").write_text(readme)
128
+
129
+ print(f"✓ Created project structure")
130
+ print(f"✓ Created config.py")
131
+ print(f"✓ Created workspace/")
132
+ print(f"✓ Created .env.example")
133
+ print(f"✓ Created .gitignore")
134
+ print(f"✓ Created README.md")
135
+ print(f"\n{'='*50}")
136
+ print(f"🎉 Project '{name}' created successfully!")
137
+ print(f"{'='*50}\n")
138
+ print(f"Next steps:")
139
+ print(f" 1. cd {name}")
140
+ print(f" 2. cp .env.example .env # If using DeepAgents")
141
+ print(f" 3. Edit .env and add your ANTHROPIC_API_KEY")
142
+ print(f" 4. Edit config.py to customize your agent")
143
+ print(f" 5. cowork-dash run")
144
+ print()
145
+
146
+ return 0
147
+
148
+
149
+ def run_app_cli(args):
150
+ """Run the application with CLI arguments."""
151
+ # Import here to avoid loading Dash when just running init
152
+ from .app import run_app
153
+
154
+ return run_app(
155
+ workspace=args.workspace,
156
+ agent_spec=args.agent,
157
+ port=args.port,
158
+ host=args.host,
159
+ debug=args.debug,
160
+ title=args.title,
161
+ config_file=args.config
162
+ )
163
+
164
+
165
+ def main():
166
+ """Main CLI entry point."""
167
+ parser = argparse.ArgumentParser(
168
+ prog="cowork-dash",
169
+ description="Cowork Dash - AI Agent Web Interface",
170
+ formatter_class=argparse.RawDescriptionHelpFormatter,
171
+ epilog="""
172
+ Examples:
173
+ # Initialize a new project
174
+ cowork-dash init my-agent-project
175
+
176
+ # Run with defaults from config.py
177
+ cowork-dash run
178
+
179
+ # Run with custom settings
180
+ cowork-dash run --workspace ~/projects --port 8080
181
+
182
+ # Run with custom agent
183
+ cowork-dash run --agent my_agent.py:agent
184
+
185
+ # Debug mode
186
+ cowork-dash run --debug
187
+
188
+ For more help: https://github.com/dkedar7/cowork-dash
189
+ """
190
+ )
191
+
192
+ subparsers = parser.add_subparsers(dest="command", help="Commands")
193
+
194
+ # cowork-dash init
195
+ init_parser = subparsers.add_parser(
196
+ "init",
197
+ help="Initialize a new project",
198
+ description="Create a new Cowork Dash project with config template"
199
+ )
200
+ init_parser.add_argument("name", help="Project name/directory")
201
+ init_parser.add_argument(
202
+ "--template",
203
+ default="default",
204
+ help="Template to use (default: default)"
205
+ )
206
+
207
+ # cowork-dash run
208
+ run_parser = subparsers.add_parser(
209
+ "run",
210
+ help="Run the application",
211
+ description="Run Cowork Dash with optional configuration overrides"
212
+ )
213
+ run_parser.add_argument(
214
+ "--workspace",
215
+ type=str,
216
+ help="Workspace directory path (overrides config.py)"
217
+ )
218
+ run_parser.add_argument(
219
+ "--agent",
220
+ type=str,
221
+ metavar="PATH:OBJECT",
222
+ help='Agent specification as "path/to/file.py:object_name"'
223
+ )
224
+ run_parser.add_argument(
225
+ "--port",
226
+ type=int,
227
+ help="Port to run on (overrides config.py)"
228
+ )
229
+ run_parser.add_argument(
230
+ "--host",
231
+ type=str,
232
+ help="Host to bind to (overrides config.py)"
233
+ )
234
+ run_parser.add_argument(
235
+ "--debug",
236
+ action="store_true",
237
+ help="Enable debug mode"
238
+ )
239
+ run_parser.add_argument(
240
+ "--no-debug",
241
+ action="store_true",
242
+ help="Disable debug mode"
243
+ )
244
+ run_parser.add_argument(
245
+ "--title",
246
+ type=str,
247
+ help="Application title (overrides config.py)"
248
+ )
249
+ run_parser.add_argument(
250
+ "--config",
251
+ type=str,
252
+ default="./config.py",
253
+ help="Config file path (default: ./config.py)"
254
+ )
255
+
256
+ # Parse arguments
257
+ args = parser.parse_args()
258
+
259
+ # Handle commands
260
+ if args.command == "init":
261
+ return init_project(args.name, args.template)
262
+
263
+ elif args.command == "run":
264
+ return run_app_cli(args)
265
+
266
+ else:
267
+ # No command provided - show help
268
+ parser.print_help()
269
+ return 1
270
+
271
+
272
+ if __name__ == "__main__":
273
+ sys.exit(main())