lvwerra HF Staff commited on
Commit
528426e
·
1 Parent(s): 51b6e17
.claude/settings.local.json CHANGED
@@ -1,7 +1,11 @@
1
  {
2
  "permissions": {
3
  "allow": [
4
- "Bash(open index.html)"
 
 
 
 
5
  ],
6
  "deny": [],
7
  "ask": []
 
1
  {
2
  "permissions": {
3
  "allow": [
4
+ "Bash(open index.html)",
5
+ "Bash(npm test:*)",
6
+ "Bash(npx playwright test:*)",
7
+ "Bash(source env312/bin/activate:*)",
8
+ "Bash(python -m pytest:*)"
9
  ],
10
  "deny": [],
11
  "ask": []
backend/code.py CHANGED
@@ -25,6 +25,55 @@ TOOLS = [
25
  "required": ["code"]
26
  }
27
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  }
29
  ]
30
 
@@ -81,12 +130,126 @@ def format_thinking_cell(content: str):
81
  }
82
 
83
 
84
- def stream_code_execution(client, model: str, messages: List[Dict], sbx: Sandbox):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  """
86
  Stream code execution results
87
 
88
  Yields:
89
  dict: Updates with type 'thinking', 'code', or 'done'
 
 
 
 
 
 
 
90
  """
91
  turns = 0
92
  done = False
@@ -140,9 +303,53 @@ def stream_code_execution(client, model: str, messages: List[Dict], sbx: Sandbox
140
  try:
141
  args = json.loads(tool_call.function.arguments)
142
  code = args["code"]
143
- except:
144
- yield {"type": "error", "content": "Failed to parse code arguments"}
145
- return
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
146
 
147
  # Send code cell (before execution)
148
  yield {"type": "code_start", "code": code}
@@ -204,6 +411,127 @@ def stream_code_execution(client, model: str, messages: List[Dict], sbx: Sandbox
204
  }]
205
  })
206
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
  messages.append({
208
  "role": "tool",
209
  "tool_call_id": tool_call.id,
 
25
  "required": ["code"]
26
  }
27
  }
28
+ },
29
+ {
30
+ "type": "function",
31
+ "function": {
32
+ "name": "upload_files",
33
+ "description": "Upload files from the local workspace to the code execution environment for analysis. Files will be available at /home/user/<filename>. Use this to load data files, scripts, or any files you need to analyze.",
34
+ "parameters": {
35
+ "type": "object",
36
+ "properties": {
37
+ "paths": {
38
+ "type": "array",
39
+ "items": {"type": "string"},
40
+ "description": "List of file paths relative to the workspace root (e.g., ['data/sales.csv', 'config.json'])"
41
+ }
42
+ },
43
+ "required": ["paths"]
44
+ }
45
+ }
46
+ },
47
+ {
48
+ "type": "function",
49
+ "function": {
50
+ "name": "download_files",
51
+ "description": "Download files from the code execution environment to the local workspace. Use this to save generated files, processed data, or any output files you want to keep.",
52
+ "parameters": {
53
+ "type": "object",
54
+ "properties": {
55
+ "files": {
56
+ "type": "array",
57
+ "items": {
58
+ "type": "object",
59
+ "properties": {
60
+ "sandbox_path": {
61
+ "type": "string",
62
+ "description": "Path in the sandbox (e.g., '/home/user/output.csv')"
63
+ },
64
+ "local_path": {
65
+ "type": "string",
66
+ "description": "Destination path relative to workspace (e.g., 'results/output.csv')"
67
+ }
68
+ },
69
+ "required": ["sandbox_path", "local_path"]
70
+ },
71
+ "description": "List of files to download with their sandbox and local paths"
72
+ }
73
+ },
74
+ "required": ["files"]
75
+ }
76
+ }
77
  }
78
  ]
79
 
 
130
  }
131
 
132
 
133
+ def upload_files_to_sandbox(sbx: Sandbox, paths: List[str], files_root: str) -> str:
134
+ """
135
+ Upload multiple files to the sandbox.
136
+
137
+ Args:
138
+ sbx: E2B sandbox instance
139
+ paths: List of relative file paths
140
+ files_root: Root directory to resolve relative paths
141
+
142
+ Returns:
143
+ String describing what was uploaded or errors encountered
144
+ """
145
+ results = []
146
+
147
+ for rel_path in paths:
148
+ # Normalize the path (remove ./ prefix if present)
149
+ rel_path = rel_path.lstrip('./')
150
+ local_path = os.path.join(files_root, rel_path)
151
+
152
+ # Security check: ensure path doesn't escape files_root
153
+ real_local = os.path.realpath(local_path)
154
+ real_root = os.path.realpath(files_root)
155
+ if not real_local.startswith(real_root):
156
+ results.append(f"Error: {rel_path} - path outside workspace")
157
+ continue
158
+
159
+ if not os.path.exists(local_path):
160
+ results.append(f"Error: {rel_path} - file not found")
161
+ continue
162
+
163
+ if not os.path.isfile(local_path):
164
+ results.append(f"Error: {rel_path} - not a file")
165
+ continue
166
+
167
+ try:
168
+ # Get just the filename for the sandbox path
169
+ filename = os.path.basename(rel_path)
170
+ sandbox_path = f"/home/user/{filename}"
171
+
172
+ with open(local_path, "rb") as f:
173
+ sbx.files.write(sandbox_path, f)
174
+
175
+ results.append(f"Uploaded: {rel_path} -> {sandbox_path}")
176
+ except Exception as e:
177
+ results.append(f"Error uploading {rel_path}: {str(e)}")
178
+
179
+ return "\n".join(results)
180
+
181
+
182
+ def download_files_from_sandbox(sbx: Sandbox, files: List[Dict], files_root: str) -> str:
183
+ """
184
+ Download multiple files from the sandbox to the local workspace.
185
+
186
+ Args:
187
+ sbx: E2B sandbox instance
188
+ files: List of dicts with 'sandbox_path' and 'local_path' keys
189
+ files_root: Root directory to resolve relative paths
190
+
191
+ Returns:
192
+ String describing what was downloaded or errors encountered
193
+ """
194
+ results = []
195
+
196
+ for file_spec in files:
197
+ sandbox_path = file_spec.get('sandbox_path', '')
198
+ local_rel_path = file_spec.get('local_path', '')
199
+
200
+ if not sandbox_path or not local_rel_path:
201
+ results.append(f"Error: Missing sandbox_path or local_path")
202
+ continue
203
+
204
+ # Normalize the local path (remove ./ prefix if present)
205
+ local_rel_path = local_rel_path.lstrip('./')
206
+ local_path = os.path.join(files_root, local_rel_path)
207
+
208
+ # Security check: ensure path doesn't escape files_root
209
+ real_local = os.path.realpath(os.path.dirname(local_path))
210
+ real_root = os.path.realpath(files_root)
211
+ # Need to handle case where parent dir doesn't exist yet
212
+ test_path = local_path
213
+ while not os.path.exists(os.path.dirname(test_path)):
214
+ test_path = os.path.dirname(test_path)
215
+ real_local = os.path.realpath(test_path)
216
+ if not real_local.startswith(real_root):
217
+ results.append(f"Error: {local_rel_path} - path outside workspace")
218
+ continue
219
+
220
+ try:
221
+ # Read file content from sandbox
222
+ content = sbx.files.read(sandbox_path)
223
+
224
+ # Create parent directories if needed
225
+ os.makedirs(os.path.dirname(local_path), exist_ok=True)
226
+
227
+ # Write to local file
228
+ # Content from e2b can be bytes or string
229
+ mode = 'wb' if isinstance(content, bytes) else 'w'
230
+ with open(local_path, mode) as f:
231
+ f.write(content)
232
+
233
+ results.append(f"Downloaded: {sandbox_path} -> {local_rel_path}")
234
+ except Exception as e:
235
+ results.append(f"Error downloading {sandbox_path}: {str(e)}")
236
+
237
+ return "\n".join(results)
238
+
239
+
240
+ def stream_code_execution(client, model: str, messages: List[Dict], sbx: Sandbox, files_root: str = None):
241
  """
242
  Stream code execution results
243
 
244
  Yields:
245
  dict: Updates with type 'thinking', 'code', or 'done'
246
+
247
+ Args:
248
+ client: OpenAI-compatible client
249
+ model: Model name to use
250
+ messages: Conversation messages
251
+ sbx: E2B sandbox instance
252
+ files_root: Root directory for file uploads (optional)
253
  """
254
  turns = 0
255
  done = False
 
303
  try:
304
  args = json.loads(tool_call.function.arguments)
305
  code = args["code"]
306
+ except json.JSONDecodeError as e:
307
+ error_msg = f"JSON parse error: {e}. Raw arguments: {tool_call.function.arguments[:500]}"
308
+ print(f"[code.py] {error_msg}")
309
+ # Treat as tool error so LLM can recover
310
+ output = f"Error parsing code arguments: {e}"
311
+ messages.append({
312
+ "role": "assistant",
313
+ "content": content,
314
+ "tool_calls": [{
315
+ "id": tool_call.id,
316
+ "type": "function",
317
+ "function": {
318
+ "name": tool_call.function.name,
319
+ "arguments": tool_call.function.arguments,
320
+ }
321
+ }]
322
+ })
323
+ messages.append({
324
+ "role": "tool",
325
+ "tool_call_id": tool_call.id,
326
+ "content": output
327
+ })
328
+ yield {"type": "error", "content": f"Failed to parse code arguments: {e}"}
329
+ continue
330
+ except KeyError as e:
331
+ error_msg = f"Missing required key {e} in arguments: {tool_call.function.arguments[:500]}"
332
+ print(f"[code.py] {error_msg}")
333
+ output = f"Error: Missing required 'code' parameter"
334
+ messages.append({
335
+ "role": "assistant",
336
+ "content": content,
337
+ "tool_calls": [{
338
+ "id": tool_call.id,
339
+ "type": "function",
340
+ "function": {
341
+ "name": tool_call.function.name,
342
+ "arguments": tool_call.function.arguments,
343
+ }
344
+ }]
345
+ })
346
+ messages.append({
347
+ "role": "tool",
348
+ "tool_call_id": tool_call.id,
349
+ "content": output
350
+ })
351
+ yield {"type": "error", "content": output}
352
+ continue
353
 
354
  # Send code cell (before execution)
355
  yield {"type": "code_start", "code": code}
 
411
  }]
412
  })
413
 
414
+ messages.append({
415
+ "role": "tool",
416
+ "tool_call_id": tool_call.id,
417
+ "content": output
418
+ })
419
+
420
+ elif tool_call.function.name == "upload_files":
421
+ # Parse arguments
422
+ try:
423
+ args = json.loads(tool_call.function.arguments)
424
+ paths = args["paths"]
425
+ except (json.JSONDecodeError, KeyError) as e:
426
+ error_msg = f"Failed to parse upload_files arguments: {e}. Raw: {tool_call.function.arguments[:500]}"
427
+ print(f"[code.py] {error_msg}")
428
+ output = f"Error parsing upload_files arguments: {e}"
429
+ messages.append({
430
+ "role": "assistant",
431
+ "content": content,
432
+ "tool_calls": [{
433
+ "id": tool_call.id,
434
+ "type": "function",
435
+ "function": {
436
+ "name": tool_call.function.name,
437
+ "arguments": tool_call.function.arguments,
438
+ }
439
+ }]
440
+ })
441
+ messages.append({
442
+ "role": "tool",
443
+ "tool_call_id": tool_call.id,
444
+ "content": output
445
+ })
446
+ yield {"type": "error", "content": output}
447
+ continue
448
+
449
+ # Check if files_root is available
450
+ if not files_root:
451
+ output = "Error: File upload not available - no workspace configured"
452
+ else:
453
+ # Upload files
454
+ output = upload_files_to_sandbox(sbx, paths, files_root)
455
+
456
+ # Send upload notification to UI
457
+ yield {"type": "upload", "paths": paths, "output": output}
458
+
459
+ # Add to message history
460
+ messages.append({
461
+ "role": "assistant",
462
+ "content": content,
463
+ "tool_calls": [{
464
+ "id": tool_call.id,
465
+ "type": "function",
466
+ "function": {
467
+ "name": tool_call.function.name,
468
+ "arguments": tool_call.function.arguments,
469
+ }
470
+ }]
471
+ })
472
+
473
+ messages.append({
474
+ "role": "tool",
475
+ "tool_call_id": tool_call.id,
476
+ "content": output
477
+ })
478
+
479
+ elif tool_call.function.name == "download_files":
480
+ # Parse arguments
481
+ try:
482
+ args = json.loads(tool_call.function.arguments)
483
+ files = args["files"]
484
+ except (json.JSONDecodeError, KeyError) as e:
485
+ error_msg = f"Failed to parse download_files arguments: {e}. Raw: {tool_call.function.arguments[:500]}"
486
+ print(f"[code.py] {error_msg}")
487
+ output = f"Error parsing download_files arguments: {e}"
488
+ messages.append({
489
+ "role": "assistant",
490
+ "content": content,
491
+ "tool_calls": [{
492
+ "id": tool_call.id,
493
+ "type": "function",
494
+ "function": {
495
+ "name": tool_call.function.name,
496
+ "arguments": tool_call.function.arguments,
497
+ }
498
+ }]
499
+ })
500
+ messages.append({
501
+ "role": "tool",
502
+ "tool_call_id": tool_call.id,
503
+ "content": output
504
+ })
505
+ yield {"type": "error", "content": output}
506
+ continue
507
+
508
+ # Check if files_root is available
509
+ if not files_root:
510
+ output = "Error: File download not available - no workspace configured"
511
+ else:
512
+ # Download files
513
+ output = download_files_from_sandbox(sbx, files, files_root)
514
+
515
+ # Extract paths for UI display
516
+ paths = [f"{f.get('sandbox_path', '')} -> {f.get('local_path', '')}" for f in files]
517
+
518
+ # Send download notification to UI
519
+ yield {"type": "download", "paths": paths, "output": output}
520
+
521
+ # Add to message history
522
+ messages.append({
523
+ "role": "assistant",
524
+ "content": content,
525
+ "tool_calls": [{
526
+ "id": tool_call.id,
527
+ "type": "function",
528
+ "function": {
529
+ "name": tool_call.function.name,
530
+ "arguments": tool_call.function.arguments,
531
+ }
532
+ }]
533
+ })
534
+
535
  messages.append({
536
  "role": "tool",
537
  "tool_call_id": tool_call.id,
backend/main.py CHANGED
@@ -88,6 +88,11 @@ Examples:
88
  You: Summarize the research results without using tools
89
 
90
  Be concise and helpful. Don't duplicate effort - either answer directly OR launch a notebook, not both. Answer questions about results directly without launching new notebooks.
 
 
 
 
 
91
  """,
92
  "agent": """You are an autonomous agent assistant specialized in breaking down and executing multi-step tasks.
93
 
@@ -103,24 +108,37 @@ Focus on being proactive, organized, and thorough in completing multi-step workf
103
 
104
  Your role is to:
105
  - Write and execute Python code to solve problems
106
- - Analyze data and create visualizations
107
  - Debug code and explain errors
108
  - Break down complex tasks into executable steps
109
 
110
  You have access to a Jupyter kernel with these packages:
111
  pandas, matplotlib, numpy, scipy, scikit-learn, seaborn, plotly, requests, beautifulsoup4, and more.
112
 
113
- Use the execute_code tool to run Python code. The execution environment is stateful - variables and imports persist between calls.
 
 
 
 
 
 
 
 
 
 
 
114
 
115
  When solving problems:
116
  1. Break down the task into logical steps
117
  2. Execute code incrementally
118
  3. Check outputs before proceeding
119
- 4. Create clear visualizations when helpful
120
 
121
- IMPORTANT: When you generate plots/figures, they are automatically named as figure_1, figure_2, etc. The execution output will show which figures were created (e.g., "[Generated figures: figure_1, figure_2]").
122
 
123
- When you have completed the task, always provide a summary using the <result> tag. To include figures in your result, use self-closing figure tags like <figure_1>, <figure_2>, <figure_3> etc. (they will be replaced with actual images):
 
 
124
 
125
  Example:
126
  <result>
@@ -133,7 +151,7 @@ The plot shows both functions overlaid on the same axes.
133
 
134
  IMPORTANT: Use self-closing tags like <figure_1> (NOT </figure_1> or <figure_1></figure_1>). Each tag will be replaced with the actual image.
135
 
136
- The result will be sent back to the command center with embedded images.
137
 
138
  Focus on being precise, practical, and thorough in your coding assistance.
139
  """,
@@ -203,6 +221,12 @@ class Message(BaseModel):
203
  content: str
204
 
205
 
 
 
 
 
 
 
206
  class ChatRequest(BaseModel):
207
  messages: List[Message]
208
  notebook_type: str = "command"
@@ -216,6 +240,7 @@ class ChatRequest(BaseModel):
216
  research_parallel_workers: Optional[int] = None # Number of parallel workers for research
217
  research_max_websites: Optional[int] = None # Max websites to analyze per research session
218
  notebook_id: Optional[str] = None # Unique notebook/tab ID for session management
 
219
 
220
 
221
  class TitleRequest(BaseModel):
@@ -241,7 +266,8 @@ async def stream_code_notebook(
241
  model: str,
242
  e2b_key: str,
243
  session_id: str,
244
- tab_id: str = "default"
 
245
  ):
246
  """Handle code notebook with execution capabilities"""
247
 
@@ -264,8 +290,8 @@ async def stream_code_notebook(
264
  # Create OpenAI client with user's endpoint
265
  client = OpenAI(base_url=endpoint, api_key=token)
266
 
267
- # Add system prompt for code notebook
268
- system_prompt = SYSTEM_PROMPTS["code"]
269
  full_messages = [
270
  {"role": "system", "content": system_prompt}
271
  ] + messages
@@ -274,7 +300,7 @@ async def stream_code_notebook(
274
  record_api_call(tab_id, full_messages)
275
 
276
  # Stream code execution
277
- for update in stream_code_execution(client, model, full_messages, sbx):
278
  # Forward updates to frontend
279
  yield f"data: {json.dumps(update)}\n\n"
280
 
@@ -306,7 +332,7 @@ async def stream_code_notebook(
306
  yield f"data: {json.dumps({'type': 'info', 'content': 'New sandbox created. Retrying execution...'})}\n\n"
307
 
308
  # Retry code execution with new sandbox
309
- for update in stream_code_execution(client, model, full_messages, sbx):
310
  yield f"data: {json.dumps(update)}\n\n"
311
 
312
  except Exception as retry_error:
@@ -347,8 +373,8 @@ async def stream_research_notebook(
347
  # Create OpenAI client
348
  client = OpenAI(base_url=endpoint, api_key=token)
349
 
350
- # Get system prompt for research
351
- system_prompt = SYSTEM_PROMPTS.get("research", "")
352
 
353
  # Store for debugging (simplified version for research)
354
  full_messages = [{"role": "system", "content": system_prompt}] + messages
@@ -399,8 +425,8 @@ async def stream_command_center_notebook(
399
  if tab_id not in CONVERSATION_HISTORY:
400
  CONVERSATION_HISTORY[tab_id] = []
401
 
402
- # Add system prompt for command center
403
- system_prompt = SYSTEM_PROMPTS["command"]
404
 
405
  # Build full messages: system + stored history + new messages
406
  print(f"DEBUG: tab_id={tab_id}, incoming messages={messages}")
@@ -458,8 +484,8 @@ async def stream_chat_response(
458
  print(f"Messages: {len(messages)} messages")
459
  print(f"Token provided: {bool(token)}")
460
 
461
- # Prepare messages with appropriate system prompt based on notebook type
462
- system_prompt = SYSTEM_PROMPTS.get(notebook_type, SYSTEM_PROMPTS["command"])
463
  full_messages = [
464
  {"role": "system", "content": system_prompt}
465
  ] + messages
@@ -623,6 +649,9 @@ async def chat_stream(request: ChatRequest):
623
  # Get tab_id for debugging
624
  tab_id = request.notebook_id or "0"
625
 
 
 
 
626
  # Route to code execution handler for code notebooks
627
  if request.notebook_type == "code":
628
  # Use notebook_id as session key, fallback to "default" if not provided
@@ -636,7 +665,8 @@ async def chat_stream(request: ChatRequest):
636
  request.model or "gpt-4",
637
  request.e2b_key or "",
638
  session_id,
639
- tab_id
 
640
  ),
641
  media_type="text/event-stream",
642
  headers={
@@ -809,6 +839,15 @@ async def health():
809
  # These can be overridden via command-line arguments or set_*_file functions
810
  SETTINGS_FILE = os.path.join(PROJECT_ROOT, "settings.json")
811
  WORKSPACE_FILE = os.path.join(PROJECT_ROOT, "workspace.json")
 
 
 
 
 
 
 
 
 
812
 
813
  def set_settings_file(path: str):
814
  """Set the settings file path (used for testing)"""
@@ -822,10 +861,11 @@ def set_workspace_file(path: str):
822
 
823
  def set_data_dir(directory: str):
824
  """Set the data directory containing settings.json and workspace.json"""
825
- global SETTINGS_FILE, WORKSPACE_FILE
826
  os.makedirs(directory, exist_ok=True)
827
  SETTINGS_FILE = os.path.join(directory, "settings.json")
828
  WORKSPACE_FILE = os.path.join(directory, "workspace.json")
 
829
 
830
 
831
  @app.get("/api/settings")
@@ -923,6 +963,129 @@ async def clear_workspace():
923
  raise HTTPException(status_code=500, detail=f"Failed to clear workspace: {str(e)}")
924
 
925
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
926
  # ============================================
927
  # Static File Serving (Frontend)
928
  # ============================================
 
88
  You: Summarize the research results without using tools
89
 
90
  Be concise and helpful. Don't duplicate effort - either answer directly OR launch a notebook, not both. Answer questions about results directly without launching new notebooks.
91
+
92
+ IMPORTANT guidelines when delegating to notebooks:
93
+ - Do NOT ask notebooks to generate plots, visualizations, or artifacts unless the user explicitly requests them. Just ask for analysis/answers.
94
+ - Do NOT ask notebooks to save or create files unless the user explicitly requests it.
95
+ - NEVER overwrite existing files without explicit user permission.
96
  """,
97
  "agent": """You are an autonomous agent assistant specialized in breaking down and executing multi-step tasks.
98
 
 
108
 
109
  Your role is to:
110
  - Write and execute Python code to solve problems
111
+ - Analyze data and answer questions
112
  - Debug code and explain errors
113
  - Break down complex tasks into executable steps
114
 
115
  You have access to a Jupyter kernel with these packages:
116
  pandas, matplotlib, numpy, scipy, scikit-learn, seaborn, plotly, requests, beautifulsoup4, and more.
117
 
118
+ You have three tools available:
119
+ - execute_code: Run Python code. The execution environment is stateful - variables and imports persist between calls.
120
+ - upload_files: Upload files from the workspace to the execution environment for analysis. Files will be available at /home/user/<filename>. Use this when you need to analyze data files, scripts, or other files from the project.
121
+ - download_files: Download files from the execution environment to the workspace. ONLY use this when the user explicitly asks to save/download files, or when saving files is clearly part of the task (e.g., "generate a dataset and save it"). Do NOT automatically save intermediate files, plots, or outputs unless requested.
122
+
123
+ ## IMPORTANT Guidelines
124
+
125
+ **Only create artifacts when explicitly requested:**
126
+ - Do NOT generate plots, charts, or visualizations unless the user explicitly asks for them
127
+ - Do NOT save files unless the user explicitly asks to save/export/download
128
+ - NEVER overwrite existing files without explicit user permission - always ask first or use a new filename
129
+ - Focus on answering questions and providing analysis, not producing visual outputs by default
130
 
131
  When solving problems:
132
  1. Break down the task into logical steps
133
  2. Execute code incrementally
134
  3. Check outputs before proceeding
135
+ 4. Only create visualizations if explicitly requested
136
 
137
+ IMPORTANT: When you DO generate plots/figures (only when requested), they are automatically named as figure_1, figure_2, etc. The execution output will show which figures were created (e.g., "[Generated figures: figure_1, figure_2]").
138
 
139
+ ## CRITICAL: You MUST provide a <result> tag
140
+
141
+ When you have completed the task, you MUST ALWAYS provide a summary using the <result> tag. This is REQUIRED - without it, your work will not be visible in the command center. To include figures in your result (when you've created them), use self-closing figure tags like <figure_1>, <figure_2>, <figure_3> etc.:
142
 
143
  Example:
144
  <result>
 
151
 
152
  IMPORTANT: Use self-closing tags like <figure_1> (NOT </figure_1> or <figure_1></figure_1>). Each tag will be replaced with the actual image.
153
 
154
+ The result will be sent back to the command center with embedded images. DO NOT forget the <result> tag - it is mandatory for every completed task.
155
 
156
  Focus on being precise, practical, and thorough in your coding assistance.
157
  """,
 
221
  content: str
222
 
223
 
224
+ class FrontendContext(BaseModel):
225
+ """Dynamic context from the frontend that can affect system prompts"""
226
+ theme: Optional[Dict] = None # Current theme colors {name, accent, bg, etc.}
227
+ open_notebooks: Optional[List[str]] = None # List of open notebook types/names
228
+
229
+
230
  class ChatRequest(BaseModel):
231
  messages: List[Message]
232
  notebook_type: str = "command"
 
240
  research_parallel_workers: Optional[int] = None # Number of parallel workers for research
241
  research_max_websites: Optional[int] = None # Max websites to analyze per research session
242
  notebook_id: Optional[str] = None # Unique notebook/tab ID for session management
243
+ frontend_context: Optional[FrontendContext] = None # Dynamic context from frontend
244
 
245
 
246
  class TitleRequest(BaseModel):
 
266
  model: str,
267
  e2b_key: str,
268
  session_id: str,
269
+ tab_id: str = "default",
270
+ frontend_context: Optional[Dict] = None
271
  ):
272
  """Handle code notebook with execution capabilities"""
273
 
 
290
  # Create OpenAI client with user's endpoint
291
  client = OpenAI(base_url=endpoint, api_key=token)
292
 
293
+ # Add system prompt for code notebook (with file tree and styling context)
294
+ system_prompt = get_system_prompt("code", frontend_context)
295
  full_messages = [
296
  {"role": "system", "content": system_prompt}
297
  ] + messages
 
300
  record_api_call(tab_id, full_messages)
301
 
302
  # Stream code execution
303
+ for update in stream_code_execution(client, model, full_messages, sbx, files_root=FILES_ROOT):
304
  # Forward updates to frontend
305
  yield f"data: {json.dumps(update)}\n\n"
306
 
 
332
  yield f"data: {json.dumps({'type': 'info', 'content': 'New sandbox created. Retrying execution...'})}\n\n"
333
 
334
  # Retry code execution with new sandbox
335
+ for update in stream_code_execution(client, model, full_messages, sbx, files_root=FILES_ROOT):
336
  yield f"data: {json.dumps(update)}\n\n"
337
 
338
  except Exception as retry_error:
 
373
  # Create OpenAI client
374
  client = OpenAI(base_url=endpoint, api_key=token)
375
 
376
+ # Get system prompt for research (with file tree)
377
+ system_prompt = get_system_prompt("research")
378
 
379
  # Store for debugging (simplified version for research)
380
  full_messages = [{"role": "system", "content": system_prompt}] + messages
 
425
  if tab_id not in CONVERSATION_HISTORY:
426
  CONVERSATION_HISTORY[tab_id] = []
427
 
428
+ # Add system prompt for command center (with file tree)
429
+ system_prompt = get_system_prompt("command")
430
 
431
  # Build full messages: system + stored history + new messages
432
  print(f"DEBUG: tab_id={tab_id}, incoming messages={messages}")
 
484
  print(f"Messages: {len(messages)} messages")
485
  print(f"Token provided: {bool(token)}")
486
 
487
+ # Prepare messages with appropriate system prompt based on notebook type (with file tree)
488
+ system_prompt = get_system_prompt(notebook_type)
489
  full_messages = [
490
  {"role": "system", "content": system_prompt}
491
  ] + messages
 
649
  # Get tab_id for debugging
650
  tab_id = request.notebook_id or "0"
651
 
652
+ # Convert frontend_context to dict if provided
653
+ frontend_context = request.frontend_context.model_dump() if request.frontend_context else None
654
+
655
  # Route to code execution handler for code notebooks
656
  if request.notebook_type == "code":
657
  # Use notebook_id as session key, fallback to "default" if not provided
 
665
  request.model or "gpt-4",
666
  request.e2b_key or "",
667
  session_id,
668
+ tab_id,
669
+ frontend_context
670
  ),
671
  media_type="text/event-stream",
672
  headers={
 
839
  # These can be overridden via command-line arguments or set_*_file functions
840
  SETTINGS_FILE = os.path.join(PROJECT_ROOT, "settings.json")
841
  WORKSPACE_FILE = os.path.join(PROJECT_ROOT, "workspace.json")
842
+ FILES_ROOT = PROJECT_ROOT # Root directory for file tree
843
+
844
+ # Directories/patterns to exclude from file tree
845
+ FILES_EXCLUDE = {
846
+ 'node_modules', '__pycache__', '.git', '.pytest_cache',
847
+ 'env', 'venv', 'env312', '.venv', 'dist', 'build',
848
+ '.egg-info', '.tox', '.coverage', 'htmlcov',
849
+ 'test-results', 'playwright-report'
850
+ }
851
 
852
  def set_settings_file(path: str):
853
  """Set the settings file path (used for testing)"""
 
861
 
862
  def set_data_dir(directory: str):
863
  """Set the data directory containing settings.json and workspace.json"""
864
+ global SETTINGS_FILE, WORKSPACE_FILE, FILES_ROOT
865
  os.makedirs(directory, exist_ok=True)
866
  SETTINGS_FILE = os.path.join(directory, "settings.json")
867
  WORKSPACE_FILE = os.path.join(directory, "workspace.json")
868
+ FILES_ROOT = directory
869
 
870
 
871
  @app.get("/api/settings")
 
963
  raise HTTPException(status_code=500, detail=f"Failed to clear workspace: {str(e)}")
964
 
965
 
966
+ # ============================================
967
+ # File Tree API
968
+ # ============================================
969
+
970
+ def build_file_tree(root_path: str, show_hidden: bool = False) -> list:
971
+ """Build a file tree structure from a directory"""
972
+ tree = []
973
+
974
+ try:
975
+ entries = sorted(os.listdir(root_path))
976
+ except PermissionError:
977
+ return tree
978
+
979
+ for entry in entries:
980
+ # Skip hidden files unless show_hidden is True
981
+ if entry.startswith('.') and not show_hidden:
982
+ continue
983
+
984
+ # Skip excluded directories
985
+ if entry in FILES_EXCLUDE:
986
+ continue
987
+
988
+ full_path = os.path.join(root_path, entry)
989
+ rel_path = os.path.relpath(full_path, FILES_ROOT)
990
+
991
+ if os.path.isdir(full_path):
992
+ children = build_file_tree(full_path, show_hidden)
993
+ tree.append({
994
+ "name": entry,
995
+ "type": "folder",
996
+ "path": rel_path,
997
+ "children": children
998
+ })
999
+ else:
1000
+ tree.append({
1001
+ "name": entry,
1002
+ "type": "file",
1003
+ "path": rel_path
1004
+ })
1005
+
1006
+ return tree
1007
+
1008
+
1009
+ def format_file_tree_text(tree: list, prefix: str = "", is_last: bool = True) -> str:
1010
+ """Format file tree as text for system prompts"""
1011
+ lines = []
1012
+
1013
+ for i, item in enumerate(tree):
1014
+ is_last_item = (i == len(tree) - 1)
1015
+ connector = "└── " if is_last_item else "├── "
1016
+ lines.append(f"{prefix}{connector}{item['name']}{'/' if item['type'] == 'folder' else ''}")
1017
+
1018
+ if item['type'] == 'folder' and item.get('children'):
1019
+ extension = " " if is_last_item else "│ "
1020
+ child_text = format_file_tree_text(item['children'], prefix + extension, is_last_item)
1021
+ if child_text:
1022
+ lines.append(child_text)
1023
+
1024
+ return "\n".join(lines)
1025
+
1026
+
1027
+ def get_file_tree_for_prompt() -> str:
1028
+ """Get formatted file tree text for inclusion in system prompts"""
1029
+ tree = build_file_tree(FILES_ROOT, show_hidden=False)
1030
+ tree_text = format_file_tree_text(tree)
1031
+ return f"Working Directory: {FILES_ROOT}\n{tree_text}"
1032
+
1033
+
1034
+ def get_styling_context(theme: Optional[Dict] = None) -> str:
1035
+ """Generate styling guidance for code notebooks based on current theme"""
1036
+ # App style description
1037
+ style_desc = """## Visual Style Guidelines
1038
+ The application has a minimalist, technical aesthetic with clean lines and muted colors. When generating plots or visualizations:
1039
+ - Use white/light backgrounds to match the notebook style
1040
+ - Prefer clean, simple chart styles without excessive decoration
1041
+ - Use the theme accent color as the primary color for data series
1042
+ - Use neutral grays (#666, #999, #ccc) for secondary elements, gridlines, and text
1043
+ - Use 300 DPI for all figures unless the user specifies otherwise (e.g., plt.figure(figsize=..., dpi=300) or plt.savefig(..., dpi=300))"""
1044
+
1045
+ if theme:
1046
+ accent = theme.get('accent', '#1b5e20')
1047
+ bg = theme.get('bg', '#e8f5e9')
1048
+ name = theme.get('name', 'forest')
1049
+ style_desc += f"""
1050
+
1051
+ Current theme: {name}
1052
+ - Primary/accent color: {accent} (use for main data series, highlights)
1053
+ - Light background: {bg} (use for fills, light accents)
1054
+ - Keep chart backgrounds white (#ffffff) for readability"""
1055
+
1056
+ return style_desc
1057
+
1058
+
1059
+ def get_system_prompt(notebook_type: str, frontend_context: Optional[Dict] = None) -> str:
1060
+ """Get system prompt for a notebook type with dynamic context appended"""
1061
+ base_prompt = SYSTEM_PROMPTS.get(notebook_type, SYSTEM_PROMPTS["command"])
1062
+ file_tree = get_file_tree_for_prompt()
1063
+
1064
+ # Build the full prompt with context sections
1065
+ sections = [base_prompt, f"## Project Files\n{file_tree}"]
1066
+
1067
+ # Add styling context for code notebooks
1068
+ if notebook_type == "code" and frontend_context:
1069
+ theme = frontend_context.get('theme') if frontend_context else None
1070
+ styling = get_styling_context(theme)
1071
+ sections.append(styling)
1072
+
1073
+ return "\n\n".join(sections)
1074
+
1075
+
1076
+ @app.get("/api/files")
1077
+ async def get_file_tree(show_hidden: bool = False):
1078
+ """Get file tree structure for the working directory"""
1079
+ try:
1080
+ tree = build_file_tree(FILES_ROOT, show_hidden)
1081
+ return {
1082
+ "root": FILES_ROOT,
1083
+ "tree": tree
1084
+ }
1085
+ except Exception as e:
1086
+ raise HTTPException(status_code=500, detail=f"Failed to read file tree: {str(e)}")
1087
+
1088
+
1089
  # ============================================
1090
  # Static File Serving (Frontend)
1091
  # ============================================
dev/dr-tulu-integradion.md ADDED
@@ -0,0 +1,585 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # DR-TULU Integration Specification
2
+
3
+ ## Overview
4
+
5
+ Refactor the existing deep research backend (`research_notebook.py`) to use DR-TULU-8B as the driving model. The key architectural change is shifting from **orchestrator-driven** (our code decides when to search) to **model-driven** (DR-TULU decides when to call tools).
6
+
7
+ ## Constraints
8
+
9
+ 1. **Model served via hosted vLLM endpoint** (OpenAI-compatible API)
10
+ 2. **Backend API must remain the same** - `stream_research()` yields the same event types
11
+ 3. Keep existing tool implementations (`search_web`, `extract_content`) where possible
12
+
13
+ ---
14
+
15
+ ## Current Architecture (research_notebook.py)
16
+
17
+ ```python
18
+ def stream_research(client, model, question, serper_key, max_iterations=5, ...):
19
+ """
20
+ Current flow:
21
+ 1. Our code calls generate_queries() to get search queries
22
+ 2. Our code executes searches in parallel
23
+ 3. Our code calls analyze_content() on each result
24
+ 4. Our code calls assess_completeness() to decide if done
25
+ 5. Our code calls generate_final_report()
26
+
27
+ Yields events: status, queries, source, query_stats, assessment, result_preview, result, done, error
28
+ """
29
+ ```
30
+
31
+ **Key yield event types to preserve:**
32
+ ```python
33
+ {"type": "status", "message": str}
34
+ {"type": "queries", "queries": List[str], "iteration": int}
35
+ {"type": "source", "query_index": int, "query_text": str, "title": str, "url": str, "analysis": str, "finding_count": int, "is_relevant": bool, "is_error": bool, "error_message": str}
36
+ {"type": "query_stats", "query_index": int, "relevant_count": int, "irrelevant_count": int, "error_count": int}
37
+ {"type": "assessment", "sufficient": bool, "missing_aspects": List[str], "findings_count": int, "reasoning": str}
38
+ {"type": "result_preview", "content": str, "figures": dict}
39
+ {"type": "result", "content": str, "figures": dict}
40
+ {"type": "done"}
41
+ {"type": "error", "content": str}
42
+ ```
43
+
44
+ ---
45
+
46
+ ## DR-TULU Architecture
47
+
48
+ ### System Prompt
49
+
50
+ Use this system prompt (from `unified_tool_calling_cli.yaml`):
51
+
52
+ ```
53
+ You are a research assistant who answers questions through iterative reasoning and research.
54
+ Your name is DR Tulu. You are a model trained by people from the University of Washington, Allen Institute for AI, Meta, MIT, and CMU for a paper called "DR Tulu: Reinforcement Learning with Evolving Rubrics for Deep Research".
55
+
56
+ ## Process
57
+ - Use <think></think> tags to show your reasoning at any point.
58
+ - Use <call_tool name="...">query</call_tool> when you need information (see tools below).
59
+ - You can alternate between thinking and searching multiple times.
60
+ - Only provide <answer></answer> tags when you have enough information for a complete response.
61
+ - Support every non-trivial claim with retrieved evidence. Wrap the exact claim span in <cite id="ID1,ID2">...</cite>, where id are snippet IDs from searched results.
62
+
63
+ ## Calling Tools (<call_tool name="...">query</call_tool>)
64
+
65
+ 1. google_search
66
+ - Purpose: general web search.
67
+ - Input via: <call_tool name="google_search">your query</call_tool>
68
+ - Output: web search snippets.
69
+
70
+ 2. browse_webpage
71
+ - Purpose: open a specific URL and extract readable page text.
72
+ - Input via: <call_tool name="browse_webpage">https://example.com/article</call_tool>
73
+ - Output: webpage content.
74
+
75
+ ## Tool Output
76
+ - After you issue a tool call, we will execute it and return results wrapped in <tool_output> tags.
77
+ - For web search: <tool_output><snippet id=UNIQUE_ID>content</snippet>...</tool_output>
78
+ - For web browsing: <tool_output><webpage id=UNIQUE_ID>content</webpage></tool_output>
79
+
80
+ ## Answer and Citation Format
81
+ - Once you collect all necessary information, generate the final answer with <answer></answer> tags.
82
+ - In your answer, wrap supported text in <cite id="SNIPPET_ID">...</cite> using exact IDs from returned snippets.
83
+ ```
84
+
85
+ ### Tool Call Format (Model Output)
86
+
87
+ DR-TULU emits tool calls in this XML format:
88
+
89
+ ```xml
90
+ <call_tool name="google_search">renewable energy trends 2024</call_tool>
91
+ <call_tool name="browse_webpage">https://example.com/article</call_tool>
92
+ ```
93
+
94
+ ### Tool Response Format (What We Inject)
95
+
96
+ After executing tools, append results in this format:
97
+
98
+ ```xml
99
+ <tool_output>
100
+ <snippet id="S_abc123" url="https://example.com/page1" title="Page Title">
101
+ Content from the search result or webpage...
102
+ </snippet>
103
+ <snippet id="S_def456" url="https://example.com/page2" title="Another Title">
104
+ More content...
105
+ </snippet>
106
+ </tool_output>
107
+ ```
108
+
109
+ For browse_webpage:
110
+ ```xml
111
+ <tool_output>
112
+ <webpage id="W_xyz789" url="https://example.com/article" title="Article Title">
113
+ Full extracted content from the page...
114
+ </webpage>
115
+ </tool_output>
116
+ ```
117
+
118
+ ### Other Tags
119
+
120
+ - `<think>...</think>` - Model's reasoning (can yield as status/progress)
121
+ - `<answer>...</answer>` - Final output (yield as result)
122
+ - `<cite id="S_abc123">claim text</cite>` - Citations in the answer
123
+
124
+ ---
125
+
126
+ ## New Implementation
127
+
128
+ ### Core Loop
129
+
130
+ ```python
131
+ import re
132
+ import uuid
133
+ from typing import AsyncGenerator, Dict, Any, List, Optional
134
+
135
+ def generate_snippet_id() -> str:
136
+ """Generate unique snippet ID"""
137
+ return f"S_{uuid.uuid4().hex[:8]}"
138
+
139
+ def generate_webpage_id() -> str:
140
+ """Generate unique webpage ID"""
141
+ return f"W_{uuid.uuid4().hex[:8]}"
142
+
143
+ def parse_tool_calls(text: str) -> List[Dict[str, Any]]:
144
+ """
145
+ Parse <call_tool name="...">query</call_tool> from model output.
146
+ Returns list of {"name": str, "query": str, "params": dict}
147
+ """
148
+ pattern = r'<call_tool\s+name="([^"]+)"([^>]*)>([^<]+)</call_tool>'
149
+ matches = re.findall(pattern, text)
150
+
151
+ tool_calls = []
152
+ for name, params_str, query in matches:
153
+ # Parse optional params like limit="8" year="2021-2025"
154
+ params = {}
155
+ param_pattern = r'(\w+)="([^"]+)"'
156
+ for param_name, param_value in re.findall(param_pattern, params_str):
157
+ params[param_name] = param_value
158
+
159
+ tool_calls.append({
160
+ "name": name.strip(),
161
+ "query": query.strip(),
162
+ "params": params
163
+ })
164
+
165
+ return tool_calls
166
+
167
+ def parse_think_blocks(text: str) -> List[str]:
168
+ """Extract <think>...</think> content"""
169
+ pattern = r'<think>(.*?)</think>'
170
+ return re.findall(pattern, text, re.DOTALL)
171
+
172
+ def parse_answer(text: str) -> Optional[str]:
173
+ """Extract <answer>...</answer> content"""
174
+ pattern = r'<answer>(.*?)</answer>'
175
+ match = re.search(pattern, text, re.DOTALL)
176
+ return match.group(1).strip() if match else None
177
+
178
+ def format_search_results(results: List[Dict], query: str) -> str:
179
+ """
180
+ Format search results as DR-TULU tool output.
181
+
182
+ Input results format (from existing search_web):
183
+ [{"title": str, "url": str, "snippet": str}, ...]
184
+
185
+ Output: <tool_output><snippet id="...">...</snippet></tool_output>
186
+ """
187
+ if not results:
188
+ return "<tool_output>No results found.</tool_output>"
189
+
190
+ snippets = []
191
+ for r in results:
192
+ snippet_id = generate_snippet_id()
193
+ snippets.append(
194
+ f'<snippet id="{snippet_id}" url="{r["url"]}" title="{r["title"]}">\n'
195
+ f'{r["snippet"]}\n'
196
+ f'</snippet>'
197
+ )
198
+
199
+ return f"<tool_output>\n" + "\n".join(snippets) + "\n</tool_output>"
200
+
201
+ def format_webpage_content(url: str, title: str, content: str) -> str:
202
+ """
203
+ Format extracted webpage as DR-TULU tool output.
204
+ """
205
+ if not content:
206
+ return f"<tool_output>Could not extract content from {url}</tool_output>"
207
+
208
+ webpage_id = generate_webpage_id()
209
+ # Truncate very long content
210
+ if len(content) > 8000:
211
+ content = content[:8000] + "\n[Content truncated...]"
212
+
213
+ return (
214
+ f"<tool_output>\n"
215
+ f'<webpage id="{webpage_id}" url="{url}" title="{title}">\n'
216
+ f'{content}\n'
217
+ f'</webpage>\n'
218
+ f"</tool_output>"
219
+ )
220
+
221
+ async def execute_tool(
222
+ tool_name: str,
223
+ query: str,
224
+ params: dict,
225
+ serper_key: str
226
+ ) -> tuple[str, List[Dict]]:
227
+ """
228
+ Execute a tool and return (formatted_output, raw_results).
229
+
230
+ Uses existing search_web() and extract_content() functions.
231
+ """
232
+ if tool_name == "google_search":
233
+ # Use existing search_web function
234
+ results = search_web(query, serper_key, num_results=params.get("limit", 10))
235
+ formatted = format_search_results(results, query)
236
+ return formatted, results
237
+
238
+ elif tool_name == "browse_webpage":
239
+ # query is the URL for browse_webpage
240
+ url = query
241
+ content = extract_content(url)
242
+ title = url # Could extract from content if needed
243
+ formatted = format_webpage_content(url, title, content or "")
244
+ return formatted, [{"url": url, "content": content}]
245
+
246
+ else:
247
+ return f"<tool_output>Unknown tool: {tool_name}</tool_output>", []
248
+
249
+
250
+ def stream_research(
251
+ client,
252
+ model: str,
253
+ question: str,
254
+ serper_key: str,
255
+ max_tool_calls: int = 20,
256
+ system_prompt: str = "",
257
+ **kwargs # Accept other params for backwards compat
258
+ ):
259
+ """
260
+ Stream deep research results using DR-TULU.
261
+
262
+ The model drives the research loop - it decides when to search,
263
+ what to search for, and when it has enough information to answer.
264
+
265
+ Yields same event types as before for API compatibility.
266
+ """
267
+
268
+ # Build system prompt
269
+ dr_tulu_system = get_dr_tulu_system_prompt() # See above
270
+ if system_prompt:
271
+ dr_tulu_system += f"\n\n{system_prompt}"
272
+
273
+ messages = [
274
+ {"role": "system", "content": dr_tulu_system},
275
+ {"role": "user", "content": question}
276
+ ]
277
+
278
+ yield {"type": "status", "message": f"Starting research: {question}"}
279
+
280
+ tool_call_count = 0
281
+ findings = [] # Track sources for compatibility
282
+ all_queries = [] # Track queries for compatibility
283
+
284
+ while tool_call_count < max_tool_calls:
285
+ # Call DR-TULU
286
+ yield {"type": "status", "message": "Thinking..."}
287
+
288
+ try:
289
+ response = client.chat.completions.create(
290
+ model=model,
291
+ messages=messages,
292
+ max_tokens=4096,
293
+ temperature=0.7,
294
+ )
295
+ assistant_message = response.choices[0].message.content
296
+ except Exception as e:
297
+ yield {"type": "error", "content": f"Model error: {str(e)}"}
298
+ yield {"type": "done"}
299
+ return
300
+
301
+ # Parse thinking blocks and yield as status
302
+ think_blocks = parse_think_blocks(assistant_message)
303
+ for thought in think_blocks:
304
+ yield {"type": "status", "message": f"Reasoning: {thought[:200]}..."}
305
+
306
+ # Check for final answer
307
+ answer = parse_answer(assistant_message)
308
+ if answer:
309
+ yield {"type": "status", "message": "Research complete! Generating report..."}
310
+ yield {"type": "result_preview", "content": answer, "figures": {}}
311
+ yield {"type": "result", "content": answer, "figures": {}}
312
+ yield {"type": "done"}
313
+ return
314
+
315
+ # Parse and execute tool calls
316
+ tool_calls = parse_tool_calls(assistant_message)
317
+
318
+ if not tool_calls:
319
+ # No tool calls and no answer - model might be stuck
320
+ # Append message and continue to prompt for more
321
+ messages.append({"role": "assistant", "content": assistant_message})
322
+ messages.append({"role": "user", "content": "Please continue your research or provide your answer."})
323
+ continue
324
+
325
+ # Track queries for compatibility
326
+ new_queries = [tc["query"] for tc in tool_calls if tc["name"] == "google_search"]
327
+ if new_queries:
328
+ all_queries.extend(new_queries)
329
+ yield {
330
+ "type": "queries",
331
+ "queries": new_queries,
332
+ "iteration": len(all_queries) // 5 + 1 # Rough iteration count
333
+ }
334
+
335
+ # Execute tools and collect results
336
+ tool_outputs = []
337
+
338
+ for i, tc in enumerate(tool_calls):
339
+ tool_call_count += 1
340
+
341
+ yield {
342
+ "type": "status",
343
+ "message": f"Searching: {tc['query'][:50]}..." if tc["name"] != "browse_webpage" else f"Browsing: {tc['query'][:50]}..."
344
+ }
345
+
346
+ formatted_output, raw_results = execute_tool(
347
+ tc["name"],
348
+ tc["query"],
349
+ tc["params"],
350
+ serper_key
351
+ )
352
+ tool_outputs.append(formatted_output)
353
+
354
+ # Yield source events for compatibility
355
+ if tc["name"] == "google_search":
356
+ for j, result in enumerate(raw_results):
357
+ findings.append({
358
+ "source": result.get("url", ""),
359
+ "title": result.get("title", ""),
360
+ "analysis": result.get("snippet", "")
361
+ })
362
+ yield {
363
+ "type": "source",
364
+ "query_index": i,
365
+ "query_text": tc["query"],
366
+ "title": result.get("title", ""),
367
+ "url": result.get("url", ""),
368
+ "analysis": result.get("snippet", ""),
369
+ "finding_count": len(findings),
370
+ "is_relevant": True, # DR-TULU decides relevance
371
+ "is_error": False,
372
+ "error_message": ""
373
+ }
374
+
375
+ elif tc["name"] == "browse_webpage":
376
+ for result in raw_results:
377
+ content = result.get("content", "")
378
+ is_error = not content
379
+ findings.append({
380
+ "source": tc["query"],
381
+ "title": tc["query"],
382
+ "analysis": content[:500] if content else "Failed to extract"
383
+ })
384
+ yield {
385
+ "type": "source",
386
+ "query_index": i,
387
+ "query_text": tc["query"],
388
+ "title": tc["query"],
389
+ "url": tc["query"],
390
+ "analysis": content[:500] if content else "Failed to extract content",
391
+ "finding_count": len(findings),
392
+ "is_relevant": not is_error,
393
+ "is_error": is_error,
394
+ "error_message": "Content extraction failed" if is_error else ""
395
+ }
396
+
397
+ if tool_call_count >= max_tool_calls:
398
+ break
399
+
400
+ # Append assistant message and tool results to conversation
401
+ messages.append({"role": "assistant", "content": assistant_message})
402
+
403
+ # Combine all tool outputs into one user message
404
+ combined_output = "\n\n".join(tool_outputs)
405
+ messages.append({"role": "user", "content": combined_output})
406
+
407
+ # Max tool calls reached - ask for final answer
408
+ messages.append({
409
+ "role": "user",
410
+ "content": "You have reached the maximum number of tool calls. Please provide your final answer now based on the information gathered."
411
+ })
412
+
413
+ try:
414
+ response = client.chat.completions.create(
415
+ model=model,
416
+ messages=messages,
417
+ max_tokens=4096,
418
+ temperature=0.7,
419
+ )
420
+ final_message = response.choices[0].message.content
421
+ answer = parse_answer(final_message) or final_message
422
+
423
+ yield {"type": "result_preview", "content": answer, "figures": {}}
424
+ yield {"type": "result", "content": answer, "figures": {}}
425
+ except Exception as e:
426
+ yield {"type": "error", "content": f"Failed to generate final answer: {str(e)}"}
427
+
428
+ yield {"type": "done"}
429
+
430
+
431
+ def get_dr_tulu_system_prompt() -> str:
432
+ """Return the DR-TULU system prompt"""
433
+ return '''You are a research assistant who answers questions through iterative reasoning and research.
434
+
435
+ ## Process
436
+ - Use <think></think> tags to show your reasoning at any point.
437
+ - Use <call_tool name="...">query</call_tool> when you need information (see tools below).
438
+ - You can alternate between thinking and searching multiple times.
439
+ - Only provide <answer></answer> tags when you have enough information for a complete response.
440
+ - Support every non-trivial claim with retrieved evidence. Wrap the exact claim span in <cite id="ID1,ID2">...</cite>, where id are snippet IDs from searched results.
441
+
442
+ ## Calling Tools (<call_tool name="...">query</call_tool>)
443
+
444
+ 1. google_search
445
+ - Purpose: general web search.
446
+ - Input via: <call_tool name="google_search">your query</call_tool>
447
+ - Output: web search snippets.
448
+
449
+ 2. browse_webpage
450
+ - Purpose: open a specific URL and extract readable page text.
451
+ - Input via: <call_tool name="browse_webpage">https://example.com/article</call_tool>
452
+ - Output: webpage content.
453
+
454
+ ## Tool Output
455
+ - After you issue a tool call, we will execute it and return results wrapped in <tool_output> tags.
456
+ - For web search: <tool_output><snippet id=UNIQUE_ID url="..." title="...">content</snippet>...</tool_output>
457
+ - For web browsing: <tool_output><webpage id=UNIQUE_ID url="..." title="...">content</webpage></tool_output>
458
+
459
+ ## Answer and Citation Format
460
+ - Once you collect all necessary information, generate the final answer with <answer></answer> tags.
461
+ - In your answer, wrap supported text in <cite id="SNIPPET_ID">...</cite> using exact IDs from returned snippets.
462
+ - Write comprehensive, well-structured answers with clear sections when appropriate.
463
+ '''
464
+ ```
465
+
466
+ ---
467
+
468
+ ## Migration Checklist
469
+
470
+ ### Keep From Original
471
+ - [x] `search_web()` function - works as-is
472
+ - [x] `extract_content()` function - works as-is
473
+ - [x] Event yield interface - same types
474
+ - [x] Function signature of `stream_research()` - mostly same params
475
+
476
+ ### Remove/Replace
477
+ - [ ] `generate_queries()` - DR-TULU generates its own
478
+ - [ ] `analyze_content()` - DR-TULU does this internally
479
+ - [ ] `assess_completeness()` - DR-TULU decides when done
480
+ - [ ] `generate_final_report()` - DR-TULU generates answer directly
481
+ - [ ] Iteration loop logic - replaced by model-driven loop
482
+ - [ ] ThreadPoolExecutor parallelism - can keep for tool execution, but loop is sequential
483
+
484
+ ### New Components
485
+ - [ ] `parse_tool_calls()` - parse DR-TULU XML output
486
+ - [ ] `parse_think_blocks()` - extract reasoning
487
+ - [ ] `parse_answer()` - extract final answer
488
+ - [ ] `format_search_results()` - format for DR-TULU
489
+ - [ ] `format_webpage_content()` - format for DR-TULU
490
+ - [ ] `execute_tool()` - unified tool execution
491
+ - [ ] `get_dr_tulu_system_prompt()` - system prompt
492
+
493
+ ---
494
+
495
+ ## Testing
496
+
497
+ ### Basic Test
498
+ ```python
499
+ from openai import OpenAI
500
+
501
+ client = OpenAI(
502
+ base_url="https://your-vllm-endpoint/v1",
503
+ api_key="your-key"
504
+ )
505
+
506
+ for event in stream_research(
507
+ client=client,
508
+ model="rl-research/DR-Tulu-8B",
509
+ question="What are the latest developments in quantum computing?",
510
+ serper_key="your-serper-key",
511
+ max_tool_calls=10
512
+ ):
513
+ print(event["type"], event.get("message", event.get("content", ""))[:100])
514
+ ```
515
+
516
+ ### Verify Event Compatibility
517
+ Ensure the UI receives the same event types it expects:
518
+ - `status` with `message`
519
+ - `queries` with `queries` list
520
+ - `source` with url, title, analysis, etc.
521
+ - `result` with `content`
522
+ - `done`
523
+
524
+ ---
525
+
526
+ ## Optional Enhancements
527
+
528
+ ### 1. Async Version
529
+ Convert to async for better performance:
530
+ ```python
531
+ async def stream_research_async(...):
532
+ async for event in ...:
533
+ yield event
534
+ ```
535
+
536
+ ### 2. Parallel Tool Execution
537
+ When DR-TULU emits multiple tool calls, execute them in parallel:
538
+ ```python
539
+ import asyncio
540
+
541
+ async def execute_tools_parallel(tool_calls, serper_key):
542
+ tasks = [execute_tool(tc["name"], tc["query"], tc["params"], serper_key) for tc in tool_calls]
543
+ return await asyncio.gather(*tasks)
544
+ ```
545
+
546
+ ### 3. Streaming Model Output
547
+ If vLLM supports streaming, yield thinking in real-time:
548
+ ```python
549
+ for chunk in client.chat.completions.create(..., stream=True):
550
+ # Parse partial output for <think> tags
551
+ # Yield status updates as thinking happens
552
+ ```
553
+
554
+ ### 4. Add snippet_search Tool
555
+ If you want scientific paper search (Semantic Scholar):
556
+ ```python
557
+ def search_papers(query: str, s2_api_key: str, limit: int = 5, year: str = None) -> List[Dict]:
558
+ # Implement Semantic Scholar API call
559
+ pass
560
+ ```
561
+
562
+ ---
563
+
564
+ ## Model Configuration
565
+
566
+ ### vLLM Serving
567
+ ```bash
568
+ vllm serve rl-research/DR-Tulu-8B \
569
+ --dtype auto \
570
+ --port 8000 \
571
+ --max-model-len 40960
572
+ ```
573
+
574
+ ### Client Configuration
575
+ ```python
576
+ client = OpenAI(
577
+ base_url="https://your-endpoint/v1",
578
+ api_key="your-api-key" # or "EMPTY" for local vLLM
579
+ )
580
+ ```
581
+
582
+ ### Recommended Parameters
583
+ - `max_tokens`: 4096 (for reasoning + tool calls)
584
+ - `temperature`: 0.7 (as used in DR-TULU training)
585
+ - `max_tool_calls`: 10-20 (DR-TULU averages ~4.3 per query)
features.md CHANGED
@@ -1,10 +1,11 @@
1
  Features
2
- - [ ] maybe rename the whole thing to "taskbook"
3
  - [ ] Add working files system tree of working directory
4
  - should be in sytem prompt
5
  - should have a visual widget (e.g. next to settings/debug) or always open on the left
 
6
  - drag and drop elements into chat which copies the path
7
  - [ ] include the color scheme in the system prompt of the models (e.g. when they make plots)
 
8
  - [ ] add AI2 DeepResearch model as the research model
9
  - [ ] rename the chat notebook to "base"
10
  - [ ] add general code/websearch to the base task
 
1
  Features
 
2
  - [ ] Add working files system tree of working directory
3
  - should be in sytem prompt
4
  - should have a visual widget (e.g. next to settings/debug) or always open on the left
5
+ - widget could look like a the deep research one a bit
6
  - drag and drop elements into chat which copies the path
7
  - [ ] include the color scheme in the system prompt of the models (e.g. when they make plots)
8
+ - [ ] maybe rename the whole thing to "taskbook"
9
  - [ ] add AI2 DeepResearch model as the research model
10
  - [ ] rename the chat notebook to "base"
11
  - [ ] add general code/websearch to the base task
frontend/index.html CHANGED
@@ -6,7 +6,7 @@
6
  <title>Productive</title>
7
  <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;700&display=swap" rel="stylesheet">
8
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css">
9
- <link rel="stylesheet" href="style.css?v=4">
10
  </head>
11
  <body>
12
  <div class="app-container">
@@ -26,6 +26,7 @@
26
  </div>
27
  </div>
28
  <div class="tab-bar-spacer"></div>
 
29
  <button class="debug-btn" id="debugBtn">DEBUG</button>
30
  <button class="settings-btn" id="settingsBtn">SETTINGS</button>
31
  </div>
@@ -300,9 +301,27 @@
300
  </div>
301
  </div>
302
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
303
  <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script>
304
  <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-python.min.js"></script>
305
- <script src="research-ui.js?v=3"></script>
306
- <script src="script.js?v=3"></script>
307
  </body>
308
  </html>
 
6
  <title>Productive</title>
7
  <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;700&display=swap" rel="stylesheet">
8
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css">
9
+ <link rel="stylesheet" href="style.css?v=15">
10
  </head>
11
  <body>
12
  <div class="app-container">
 
26
  </div>
27
  </div>
28
  <div class="tab-bar-spacer"></div>
29
+ <button class="files-btn" id="filesBtn">FILES</button>
30
  <button class="debug-btn" id="debugBtn">DEBUG</button>
31
  <button class="settings-btn" id="settingsBtn">SETTINGS</button>
32
  </div>
 
301
  </div>
302
  </div>
303
 
304
+ <!-- Files Panel -->
305
+ <div class="files-panel" id="filesPanel">
306
+ <div class="files-panel-header">
307
+ <h3>FILES</h3>
308
+ <div class="files-panel-controls">
309
+ <label class="files-show-hidden">
310
+ <input type="checkbox" id="showHiddenFiles">
311
+ <span>Hidden</span>
312
+ </label>
313
+ <button class="files-refresh-btn" id="filesRefresh" title="Refresh">↻</button>
314
+ </div>
315
+ <button class="files-panel-close" id="filesPanelClose">×</button>
316
+ </div>
317
+ <div class="files-panel-body" id="fileTree">
318
+ <div class="files-loading">Loading...</div>
319
+ </div>
320
+ </div>
321
+
322
  <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script>
323
  <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-python.min.js"></script>
324
+ <script src="research-ui.js?v=15"></script>
325
+ <script src="script.js?v=15"></script>
326
  </body>
327
  </html>
frontend/script.js CHANGED
@@ -550,7 +550,8 @@ async function streamChatResponse(messages, chatContainer, notebookType, tabId)
550
  research_sub_agent_model: currentSettings.researchSubAgentModel || null,
551
  research_parallel_workers: currentSettings.researchParallelWorkers || null,
552
  research_max_websites: currentSettings.researchMaxWebsites || null,
553
- notebook_id: tabId.toString() // Send unique tab ID for sandbox sessions
 
554
  })
555
  });
556
 
@@ -592,8 +593,20 @@ async function streamChatResponse(messages, chatContainer, notebookType, tabId)
592
  chatContainer.scrollTop = chatContainer.scrollHeight;
593
 
594
  } else if (data.type === 'code_start') {
595
- // Code cell starting execution
596
- createCodeCell(chatContainer, data.code, '⏳ Executing...', false);
 
 
 
 
 
 
 
 
 
 
 
 
597
  currentMessageEl = null;
598
  chatContainer.scrollTop = chatContainer.scrollHeight;
599
 
@@ -788,17 +801,23 @@ function appendToMessage(messageEl, content) {
788
  }
789
  }
790
 
791
- function createCodeCell(chatContainer, code, output, isError) {
 
 
 
 
792
  const codeCell = document.createElement('div');
793
  codeCell.className = 'code-cell';
794
 
795
  let outputHtml = '';
796
- if (output) {
797
  outputHtml = `<div class="code-cell-output${isError ? ' error' : ''}">${escapeHtml(output)}</div>`;
798
  }
799
 
 
 
800
  codeCell.innerHTML = `
801
- <div class="code-cell-label">CODE</div>
802
  <div class="code-cell-code"><pre><code class="language-python">${escapeHtml(code)}</code></pre></div>
803
  ${outputHtml}
804
  `;
@@ -813,12 +832,61 @@ function createCodeCell(chatContainer, code, output, isError) {
813
  }
814
  }
815
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
816
  function updateLastCodeCell(chatContainer, output, isError, images) {
817
  const codeCells = chatContainer.querySelectorAll('.code-cell');
818
  if (codeCells.length === 0) return;
819
 
820
  const lastCell = codeCells[codeCells.length - 1];
821
 
 
 
 
 
 
 
822
  // Remove existing output if any
823
  const existingOutput = lastCell.querySelector('.code-cell-output');
824
  if (existingOutput) {
@@ -1955,6 +2023,43 @@ function getSettings() {
1955
  return settings;
1956
  }
1957
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1958
  // Sandbox management for code notebooks
1959
  async function startSandbox(tabId) {
1960
  const currentSettings = getSettings();
@@ -2085,9 +2190,11 @@ if (debugBtn) {
2085
  debugBtn.addEventListener('click', () => {
2086
  const isOpening = !debugPanel.classList.contains('active');
2087
 
2088
- // Close settings panel if open
2089
  settingsPanel.classList.remove('active');
2090
  settingsBtn.classList.remove('active');
 
 
2091
 
2092
  // Toggle debug panel
2093
  debugPanel.classList.toggle('active');
@@ -2178,13 +2285,16 @@ const appContainer = document.querySelector('.app-container');
2178
  // Open settings panel when SETTINGS button is clicked
2179
  if (settingsBtn) {
2180
  settingsBtn.addEventListener('click', () => {
 
 
 
 
 
 
2181
  openSettings(); // Populate form fields with current values
2182
  settingsPanel.classList.add('active');
2183
  settingsBtn.classList.add('active');
2184
  appContainer.classList.add('panel-open');
2185
- // Close debug panel if open
2186
- debugPanel.classList.remove('active');
2187
- debugBtn.classList.remove('active');
2188
  });
2189
  }
2190
 
@@ -2197,3 +2307,210 @@ if (settingsPanelClose) {
2197
  });
2198
  }
2199
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
550
  research_sub_agent_model: currentSettings.researchSubAgentModel || null,
551
  research_parallel_workers: currentSettings.researchParallelWorkers || null,
552
  research_max_websites: currentSettings.researchMaxWebsites || null,
553
+ notebook_id: tabId.toString(), // Send unique tab ID for sandbox sessions
554
+ frontend_context: getFrontendContext() // Dynamic context for system prompts
555
  })
556
  });
557
 
 
593
  chatContainer.scrollTop = chatContainer.scrollHeight;
594
 
595
  } else if (data.type === 'code_start') {
596
+ // Code cell starting execution - show with spinner
597
+ createCodeCell(chatContainer, data.code, null, false, true);
598
+ currentMessageEl = null;
599
+ chatContainer.scrollTop = chatContainer.scrollHeight;
600
+
601
+ } else if (data.type === 'upload') {
602
+ // File upload notification
603
+ createUploadMessage(chatContainer, data.paths, data.output);
604
+ currentMessageEl = null;
605
+ chatContainer.scrollTop = chatContainer.scrollHeight;
606
+
607
+ } else if (data.type === 'download') {
608
+ // File download notification
609
+ createDownloadMessage(chatContainer, data.paths, data.output);
610
  currentMessageEl = null;
611
  chatContainer.scrollTop = chatContainer.scrollHeight;
612
 
 
801
  }
802
  }
803
 
804
+ function createSpinnerHtml() {
805
+ return `<div class="tool-spinner"><span></span><span></span><span></span></div>`;
806
+ }
807
+
808
+ function createCodeCell(chatContainer, code, output, isError, isExecuting = false) {
809
  const codeCell = document.createElement('div');
810
  codeCell.className = 'code-cell';
811
 
812
  let outputHtml = '';
813
+ if (output && !isExecuting) {
814
  outputHtml = `<div class="code-cell-output${isError ? ' error' : ''}">${escapeHtml(output)}</div>`;
815
  }
816
 
817
+ const spinnerHtml = isExecuting ? createSpinnerHtml() : '';
818
+
819
  codeCell.innerHTML = `
820
+ <div class="code-cell-label"><span>CODE</span>${spinnerHtml}</div>
821
  <div class="code-cell-code"><pre><code class="language-python">${escapeHtml(code)}</code></pre></div>
822
  ${outputHtml}
823
  `;
 
832
  }
833
  }
834
 
835
+ function createFileTransferCell(chatContainer, type, paths, output, isExecuting = false) {
836
+ const cell = document.createElement('div');
837
+ cell.className = 'action-widget';
838
+
839
+ const label = type === 'upload' ? 'UPLOAD' : 'DOWNLOAD';
840
+ const hasError = output && output.includes('Error:');
841
+
842
+ // Indicator: spinner while executing, checkmark when done
843
+ const indicatorHtml = isExecuting
844
+ ? `<div class="orbit-indicator"><span></span><span></span><span></span></div>`
845
+ : `<div class="done-indicator"></div>`;
846
+
847
+ // Format paths as list items
848
+ const pathsList = paths.map(p => `<li>${escapeHtml(p)}</li>`).join('');
849
+
850
+ let outputHtml = '';
851
+ if (output && !isExecuting) {
852
+ const outputClass = hasError ? 'transfer-output error' : 'transfer-output';
853
+ outputHtml = `<div class="${outputClass}">${escapeHtml(output)}</div>`;
854
+ }
855
+
856
+ cell.innerHTML = `
857
+ <div class="action-widget-header">
858
+ <span class="action-widget-type">${label}</span>
859
+ <div class="action-widget-bar-right">${indicatorHtml}</div>
860
+ </div>
861
+ <div class="action-widget-body">
862
+ <ul class="transfer-paths">${pathsList}</ul>
863
+ ${outputHtml}
864
+ </div>
865
+ `;
866
+ chatContainer.appendChild(cell);
867
+ return cell;
868
+ }
869
+
870
+ function createUploadMessage(chatContainer, paths, output) {
871
+ createFileTransferCell(chatContainer, 'upload', paths, output, false);
872
+ }
873
+
874
+ function createDownloadMessage(chatContainer, paths, output) {
875
+ createFileTransferCell(chatContainer, 'download', paths, output, false);
876
+ }
877
+
878
  function updateLastCodeCell(chatContainer, output, isError, images) {
879
  const codeCells = chatContainer.querySelectorAll('.code-cell');
880
  if (codeCells.length === 0) return;
881
 
882
  const lastCell = codeCells[codeCells.length - 1];
883
 
884
+ // Remove spinner if present
885
+ const spinner = lastCell.querySelector('.tool-spinner');
886
+ if (spinner) {
887
+ spinner.remove();
888
+ }
889
+
890
  // Remove existing output if any
891
  const existingOutput = lastCell.querySelector('.code-cell-output');
892
  if (existingOutput) {
 
2023
  return settings;
2024
  }
2025
 
2026
+ // Build frontend context for API requests
2027
+ function getFrontendContext() {
2028
+ const currentThemeName = settings.themeColor || 'forest';
2029
+ const theme = themeColors[currentThemeName];
2030
+
2031
+ return {
2032
+ theme: theme ? {
2033
+ name: currentThemeName,
2034
+ accent: theme.accent,
2035
+ bg: theme.bg,
2036
+ border: theme.border
2037
+ } : null,
2038
+ open_notebooks: getOpenNotebookTypes()
2039
+ };
2040
+ }
2041
+
2042
+ // Get list of open notebook types
2043
+ function getOpenNotebookTypes() {
2044
+ const tabs = document.querySelectorAll('.tab[data-tab-id]');
2045
+ const types = [];
2046
+ tabs.forEach(tab => {
2047
+ const tabId = tab.dataset.tabId;
2048
+ if (tabId === '0') {
2049
+ types.push('command');
2050
+ } else {
2051
+ const content = document.querySelector(`[data-content-id="${tabId}"]`);
2052
+ if (content) {
2053
+ const chatContainer = content.querySelector('.chat-container');
2054
+ if (chatContainer && chatContainer.dataset.notebookType) {
2055
+ types.push(chatContainer.dataset.notebookType);
2056
+ }
2057
+ }
2058
+ }
2059
+ });
2060
+ return types;
2061
+ }
2062
+
2063
  // Sandbox management for code notebooks
2064
  async function startSandbox(tabId) {
2065
  const currentSettings = getSettings();
 
2190
  debugBtn.addEventListener('click', () => {
2191
  const isOpening = !debugPanel.classList.contains('active');
2192
 
2193
+ // Close other panels if open
2194
  settingsPanel.classList.remove('active');
2195
  settingsBtn.classList.remove('active');
2196
+ if (filesPanel) filesPanel.classList.remove('active');
2197
+ if (filesBtn) filesBtn.classList.remove('active');
2198
 
2199
  // Toggle debug panel
2200
  debugPanel.classList.toggle('active');
 
2285
  // Open settings panel when SETTINGS button is clicked
2286
  if (settingsBtn) {
2287
  settingsBtn.addEventListener('click', () => {
2288
+ // Close other panels if open
2289
+ debugPanel.classList.remove('active');
2290
+ debugBtn.classList.remove('active');
2291
+ if (filesPanel) filesPanel.classList.remove('active');
2292
+ if (filesBtn) filesBtn.classList.remove('active');
2293
+
2294
  openSettings(); // Populate form fields with current values
2295
  settingsPanel.classList.add('active');
2296
  settingsBtn.classList.add('active');
2297
  appContainer.classList.add('panel-open');
 
 
 
2298
  });
2299
  }
2300
 
 
2307
  });
2308
  }
2309
 
2310
+
2311
+ // ============= FILES PANEL =============
2312
+
2313
+ const filesPanel = document.getElementById('filesPanel');
2314
+ const filesPanelClose = document.getElementById('filesPanelClose');
2315
+ const filesBtn = document.getElementById('filesBtn');
2316
+ const fileTree = document.getElementById('fileTree');
2317
+ const showHiddenFiles = document.getElementById('showHiddenFiles');
2318
+ const filesRefresh = document.getElementById('filesRefresh');
2319
+
2320
+ // Track expanded folder paths to preserve state on refresh
2321
+ let expandedPaths = new Set();
2322
+ let filesRoot = '';
2323
+
2324
+ // Load file tree from API
2325
+ async function loadFileTree() {
2326
+ const showHidden = showHiddenFiles?.checked || false;
2327
+ try {
2328
+ const response = await fetch(`/api/files?show_hidden=${showHidden}`);
2329
+ if (response.ok) {
2330
+ const data = await response.json();
2331
+ filesRoot = data.root;
2332
+ renderFileTree(data.tree, fileTree, data.root);
2333
+ } else {
2334
+ fileTree.innerHTML = '<div class="files-loading">Failed to load files</div>';
2335
+ }
2336
+ } catch (e) {
2337
+ console.error('Failed to load file tree:', e);
2338
+ fileTree.innerHTML = '<div class="files-loading">Failed to load files</div>';
2339
+ }
2340
+ }
2341
+
2342
+ // Render file tree recursively
2343
+ function renderFileTree(tree, container, rootPath) {
2344
+ container.innerHTML = '';
2345
+ const rootWrapper = document.createElement('div');
2346
+ rootWrapper.className = 'file-tree-root';
2347
+
2348
+ // Add header with folder name
2349
+ const header = document.createElement('div');
2350
+ header.className = 'file-tree-header';
2351
+ const folderName = rootPath.split('/').pop() || rootPath;
2352
+ header.textContent = './' + folderName;
2353
+ rootWrapper.appendChild(header);
2354
+
2355
+ // Container with vertical line
2356
+ const treeContainer = document.createElement('div');
2357
+ treeContainer.className = 'file-tree-container';
2358
+ renderTreeItems(tree, treeContainer);
2359
+ rootWrapper.appendChild(treeContainer);
2360
+
2361
+ container.appendChild(rootWrapper);
2362
+ }
2363
+
2364
+ function renderTreeItems(tree, container) {
2365
+ const len = tree.length;
2366
+ for (let i = 0; i < len; i++) {
2367
+ const item = tree[i];
2368
+ const isLast = (i === len - 1);
2369
+
2370
+ const itemEl = document.createElement('div');
2371
+ itemEl.className = `file-tree-item ${item.type}`;
2372
+ if (isLast) itemEl.classList.add('last');
2373
+ itemEl.dataset.path = item.path;
2374
+
2375
+ // Check if this folder was previously expanded
2376
+ const wasExpanded = expandedPaths.has(item.path);
2377
+
2378
+ // Create the clickable line element
2379
+ const lineEl = document.createElement('div');
2380
+ lineEl.className = 'file-tree-line';
2381
+ lineEl.draggable = true;
2382
+
2383
+ // Only folders get an icon (arrow), files get empty icon
2384
+ const icon = item.type === 'folder' ? (wasExpanded ? '▼' : '▶') : '';
2385
+ lineEl.innerHTML = `
2386
+ <span class="file-tree-icon">${icon}</span>
2387
+ <span class="file-tree-name">${item.name}</span>
2388
+ `;
2389
+ itemEl.appendChild(lineEl);
2390
+
2391
+ container.appendChild(itemEl);
2392
+
2393
+ // Handle folder expansion
2394
+ if (item.type === 'folder' && item.children && item.children.length > 0) {
2395
+ const childrenContainer = document.createElement('div');
2396
+ childrenContainer.className = 'file-tree-children';
2397
+ if (wasExpanded) {
2398
+ childrenContainer.classList.add('expanded');
2399
+ itemEl.classList.add('expanded');
2400
+ }
2401
+ renderTreeItems(item.children, childrenContainer);
2402
+ itemEl.appendChild(childrenContainer);
2403
+
2404
+ // Use click delay to distinguish single vs double click
2405
+ let clickTimer = null;
2406
+ lineEl.addEventListener('click', (e) => {
2407
+ e.stopPropagation();
2408
+ if (clickTimer) {
2409
+ // Double click detected - clear timer and expand/collapse
2410
+ clearTimeout(clickTimer);
2411
+ clickTimer = null;
2412
+ const isExpanded = itemEl.classList.toggle('expanded');
2413
+ childrenContainer.classList.toggle('expanded');
2414
+ const iconEl = lineEl.querySelector('.file-tree-icon');
2415
+ if (iconEl) iconEl.textContent = isExpanded ? '▼' : '▶';
2416
+ if (isExpanded) {
2417
+ expandedPaths.add(item.path);
2418
+ } else {
2419
+ expandedPaths.delete(item.path);
2420
+ }
2421
+ } else {
2422
+ // Single click - wait to see if it's a double click
2423
+ clickTimer = setTimeout(() => {
2424
+ clickTimer = null;
2425
+ insertPathIntoInput('./' + item.path);
2426
+ showClickFeedback(lineEl);
2427
+ }, 250);
2428
+ }
2429
+ });
2430
+ } else if (item.type === 'file') {
2431
+ // Single click on file inserts path
2432
+ lineEl.addEventListener('click', (e) => {
2433
+ e.stopPropagation();
2434
+ insertPathIntoInput('./' + item.path);
2435
+ showClickFeedback(lineEl);
2436
+ });
2437
+ }
2438
+
2439
+ // Drag start handler for future drag-and-drop
2440
+ lineEl.addEventListener('dragstart', (e) => {
2441
+ e.dataTransfer.setData('text/plain', './' + item.path);
2442
+ e.dataTransfer.setData('application/x-file-path', './' + item.path);
2443
+ e.dataTransfer.effectAllowed = 'copy';
2444
+ });
2445
+ }
2446
+ }
2447
+
2448
+ // Helper to insert path into active input
2449
+ function insertPathIntoInput(path) {
2450
+ const inputId = activeTabId === 0 ? 'input-command' : `input-${activeTabId}`;
2451
+ const inputEl = document.getElementById(inputId);
2452
+ if (inputEl) {
2453
+ const start = inputEl.selectionStart;
2454
+ const end = inputEl.selectionEnd;
2455
+ const text = inputEl.value;
2456
+ // Wrap path in backticks and add trailing space
2457
+ const formattedPath = '`' + path + '` ';
2458
+ inputEl.value = text.substring(0, start) + formattedPath + text.substring(end);
2459
+ inputEl.focus();
2460
+ inputEl.selectionStart = inputEl.selectionEnd = start + formattedPath.length;
2461
+ }
2462
+ }
2463
+
2464
+ // Helper to show click feedback
2465
+ function showClickFeedback(el) {
2466
+ const originalColor = el.style.color;
2467
+ el.style.color = 'var(--theme-accent)';
2468
+ setTimeout(() => {
2469
+ el.style.color = originalColor;
2470
+ }, 300);
2471
+ }
2472
+
2473
+ // Open files panel when FILES button is clicked
2474
+ if (filesBtn) {
2475
+ filesBtn.addEventListener('click', () => {
2476
+ const isOpening = !filesPanel.classList.contains('active');
2477
+
2478
+ // Close other panels first
2479
+ settingsPanel.classList.remove('active');
2480
+ settingsBtn.classList.remove('active');
2481
+ debugPanel.classList.remove('active');
2482
+ debugBtn.classList.remove('active');
2483
+
2484
+ // Toggle files panel
2485
+ filesPanel.classList.toggle('active');
2486
+ filesBtn.classList.toggle('active');
2487
+
2488
+ // Load file tree when opening
2489
+ if (isOpening) {
2490
+ loadFileTree();
2491
+ }
2492
+ });
2493
+ }
2494
+
2495
+ // Close files panel
2496
+ if (filesPanelClose) {
2497
+ filesPanelClose.addEventListener('click', () => {
2498
+ filesPanel.classList.remove('active');
2499
+ filesBtn.classList.remove('active');
2500
+ });
2501
+ }
2502
+
2503
+ // Refresh button
2504
+ if (filesRefresh) {
2505
+ filesRefresh.addEventListener('click', () => {
2506
+ loadFileTree();
2507
+ });
2508
+ }
2509
+
2510
+ // Show hidden files toggle
2511
+ if (showHiddenFiles) {
2512
+ showHiddenFiles.addEventListener('change', () => {
2513
+ loadFileTree();
2514
+ });
2515
+ }
2516
+
frontend/style.css CHANGED
@@ -134,11 +134,13 @@ body {
134
  100% { transform: rotate(360deg); }
135
  }
136
 
137
- .settings-btn {
 
 
138
  background: #f5f5f5;
139
  color: #666;
140
  border: none;
141
- border-right: 1px solid #ccc;
142
  padding: 8px 16px;
143
  font-size: 12px;
144
  font-weight: 500;
@@ -148,12 +150,16 @@ body {
148
  font-family: inherit;
149
  }
150
 
151
- .settings-btn:hover {
 
 
152
  background: #eee;
153
  color: #1a1a1a;
154
  }
155
 
156
- .settings-btn.active {
 
 
157
  background: var(--theme-accent);
158
  color: white;
159
  }
@@ -711,6 +717,42 @@ body {
711
  border-radius: 3px;
712
  }
713
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
714
  .input-area {
715
  padding: 15px 20px;
716
  background: white;
@@ -1173,6 +1215,33 @@ body {
1173
  opacity: 0.85;
1174
  }
1175
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1176
  /* Result Preview in CODE notebook */
1177
  .result-preview {
1178
  margin: 16px 0;
@@ -2068,29 +2137,7 @@ body {
2068
  overflow-x: auto;
2069
  }
2070
 
2071
- .debug-btn {
2072
- background: #f5f5f5;
2073
- color: #666;
2074
- border: none;
2075
- border-right: 1px solid #ccc;
2076
- padding: 8px 16px;
2077
- font-size: 12px;
2078
- font-weight: 500;
2079
- letter-spacing: 1px;
2080
- cursor: pointer;
2081
- transition: all 0.2s;
2082
- font-family: inherit;
2083
- }
2084
-
2085
- .debug-btn:hover {
2086
- background: #eee;
2087
- color: #1a1a1a;
2088
- }
2089
-
2090
- .debug-btn.active {
2091
- background: var(--theme-accent);
2092
- color: white;
2093
- }
2094
 
2095
  /* Settings Panel (side panel like debug) */
2096
  .settings-panel {
@@ -2158,3 +2205,246 @@ body {
2158
  padding: 20px;
2159
  overflow-y: auto;
2160
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
  100% { transform: rotate(360deg); }
135
  }
136
 
137
+ .settings-btn,
138
+ .files-btn,
139
+ .debug-btn {
140
  background: #f5f5f5;
141
  color: #666;
142
  border: none;
143
+ border-left: 1px solid #ccc;
144
  padding: 8px 16px;
145
  font-size: 12px;
146
  font-weight: 500;
 
150
  font-family: inherit;
151
  }
152
 
153
+ .settings-btn:hover,
154
+ .files-btn:hover,
155
+ .debug-btn:hover {
156
  background: #eee;
157
  color: #1a1a1a;
158
  }
159
 
160
+ .settings-btn.active,
161
+ .files-btn.active,
162
+ .debug-btn.active {
163
  background: var(--theme-accent);
164
  color: white;
165
  }
 
717
  border-radius: 3px;
718
  }
719
 
720
+ /* Tool cell label with spinner */
721
+ .code-cell-label {
722
+ display: flex;
723
+ align-items: center;
724
+ gap: 8px;
725
+ }
726
+
727
+ .code-cell-label .tool-spinner {
728
+ width: 12px;
729
+ height: 12px;
730
+ position: relative;
731
+ animation: orbit-rotate 1.2s linear infinite;
732
+ }
733
+
734
+ .code-cell-label .tool-spinner span {
735
+ position: absolute;
736
+ width: 3px;
737
+ height: 3px;
738
+ border-radius: 50%;
739
+ background: white;
740
+ top: 50%;
741
+ left: 50%;
742
+ }
743
+
744
+ .code-cell-label .tool-spinner span:nth-child(1) {
745
+ transform: translate(-50%, -50%) translateY(-4.5px);
746
+ }
747
+
748
+ .code-cell-label .tool-spinner span:nth-child(2) {
749
+ transform: translate(-50%, -50%) rotate(120deg) translateY(-4.5px);
750
+ }
751
+
752
+ .code-cell-label .tool-spinner span:nth-child(3) {
753
+ transform: translate(-50%, -50%) rotate(240deg) translateY(-4.5px);
754
+ }
755
+
756
  .input-area {
757
  padding: 15px 20px;
758
  background: white;
 
1215
  opacity: 0.85;
1216
  }
1217
 
1218
+ /* File transfer (upload/download) widget styles */
1219
+ .transfer-paths {
1220
+ margin: 0;
1221
+ padding-left: 20px;
1222
+ font-family: 'JetBrains Mono', monospace;
1223
+ font-size: 12px;
1224
+ }
1225
+
1226
+ .transfer-paths li {
1227
+ margin: 4px 0;
1228
+ color: #333;
1229
+ }
1230
+
1231
+ .transfer-output {
1232
+ margin-top: 8px;
1233
+ padding-top: 8px;
1234
+ border-top: 1px solid #e0e0e0;
1235
+ font-family: 'JetBrains Mono', monospace;
1236
+ font-size: 11px;
1237
+ color: #666;
1238
+ white-space: pre-wrap;
1239
+ }
1240
+
1241
+ .transfer-output.error {
1242
+ color: #c62828;
1243
+ }
1244
+
1245
  /* Result Preview in CODE notebook */
1246
  .result-preview {
1247
  margin: 16px 0;
 
2137
  overflow-x: auto;
2138
  }
2139
 
2140
+ /* Debug button uses same styling as settings/files buttons */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2141
 
2142
  /* Settings Panel (side panel like debug) */
2143
  .settings-panel {
 
2205
  padding: 20px;
2206
  overflow-y: auto;
2207
  }
2208
+
2209
+ /* Files Panel (right side panel, like settings/debug) */
2210
+ .files-panel {
2211
+ position: fixed;
2212
+ top: 37px;
2213
+ right: -320px;
2214
+ width: 320px;
2215
+ height: calc(100vh - 37px);
2216
+ background: white;
2217
+ border-left: 2px solid var(--theme-accent);
2218
+ z-index: 1000;
2219
+ display: flex;
2220
+ flex-direction: column;
2221
+ transition: right 0.3s ease;
2222
+ }
2223
+
2224
+ .files-panel.active {
2225
+ right: 0;
2226
+ }
2227
+
2228
+ .files-panel-header {
2229
+ padding: 8px 12px;
2230
+ border-bottom: 1px solid #e0e0e0;
2231
+ display: flex;
2232
+ justify-content: space-between;
2233
+ align-items: center;
2234
+ background: var(--theme-accent);
2235
+ gap: 8px;
2236
+ }
2237
+
2238
+ .files-panel-header h3 {
2239
+ margin: 0;
2240
+ font-size: 12px;
2241
+ font-weight: 600;
2242
+ color: white;
2243
+ text-transform: uppercase;
2244
+ letter-spacing: 0.5px;
2245
+ }
2246
+
2247
+ .files-panel-controls {
2248
+ display: flex;
2249
+ align-items: center;
2250
+ gap: 8px;
2251
+ margin-left: auto;
2252
+ }
2253
+
2254
+ .files-show-hidden {
2255
+ display: flex;
2256
+ align-items: center;
2257
+ gap: 6px;
2258
+ font-size: 10px;
2259
+ color: rgba(255, 255, 255, 0.8);
2260
+ cursor: pointer;
2261
+ }
2262
+
2263
+ .files-show-hidden input[type="checkbox"] {
2264
+ appearance: none;
2265
+ -webkit-appearance: none;
2266
+ width: 14px;
2267
+ height: 14px;
2268
+ border: 1px solid rgba(255, 255, 255, 0.5);
2269
+ border-radius: 2px;
2270
+ background: transparent;
2271
+ cursor: pointer;
2272
+ position: relative;
2273
+ }
2274
+
2275
+ .files-show-hidden input[type="checkbox"]:checked {
2276
+ background: white;
2277
+ border-color: white;
2278
+ }
2279
+
2280
+ .files-show-hidden input[type="checkbox"]:checked::after {
2281
+ content: '✓';
2282
+ position: absolute;
2283
+ top: -1px;
2284
+ left: 2px;
2285
+ font-size: 11px;
2286
+ color: var(--theme-accent);
2287
+ font-weight: bold;
2288
+ }
2289
+
2290
+ .files-refresh-btn {
2291
+ background: none;
2292
+ border: none;
2293
+ color: white;
2294
+ font-size: 16px;
2295
+ cursor: pointer;
2296
+ padding: 2px 6px;
2297
+ border-radius: 4px;
2298
+ transition: background 0.2s;
2299
+ }
2300
+
2301
+ .files-refresh-btn:hover {
2302
+ background: rgba(255, 255, 255, 0.2);
2303
+ }
2304
+
2305
+ .files-panel-close {
2306
+ background: none;
2307
+ border: none;
2308
+ font-size: 20px;
2309
+ color: white;
2310
+ cursor: pointer;
2311
+ padding: 0;
2312
+ width: 24px;
2313
+ height: 24px;
2314
+ display: flex;
2315
+ align-items: center;
2316
+ justify-content: center;
2317
+ border-radius: 4px;
2318
+ transition: background 0.2s;
2319
+ }
2320
+
2321
+ .files-panel-close:hover {
2322
+ background: rgba(255, 255, 255, 0.2);
2323
+ }
2324
+
2325
+ .files-panel-body {
2326
+ flex: 1;
2327
+ padding: 8px 0;
2328
+ overflow-y: auto;
2329
+ font-size: 12px;
2330
+ }
2331
+
2332
+ .files-loading {
2333
+ padding: 16px;
2334
+ color: #666;
2335
+ text-align: center;
2336
+ }
2337
+
2338
+ /* File Tree Styles - matching research tree pattern */
2339
+ .file-tree-root {
2340
+ padding: 8px 12px;
2341
+ }
2342
+
2343
+ .file-tree-header {
2344
+ font-size: 12px;
2345
+ color: #666;
2346
+ padding: 0 0 4px 0;
2347
+ margin-bottom: 4px;
2348
+ }
2349
+
2350
+ /* Container for tree items - has vertical line */
2351
+ .file-tree-container {
2352
+ position: relative;
2353
+ padding-left: 20px;
2354
+ }
2355
+
2356
+ .file-tree-container::before {
2357
+ content: '';
2358
+ position: absolute;
2359
+ left: 0;
2360
+ top: 0;
2361
+ bottom: 0;
2362
+ width: 1px;
2363
+ background: #ccc;
2364
+ }
2365
+
2366
+ /* Individual tree item */
2367
+ .file-tree-item {
2368
+ position: relative;
2369
+ margin-bottom: 1px;
2370
+ }
2371
+
2372
+ /* Horizontal branch line */
2373
+ .file-tree-item::before {
2374
+ content: '';
2375
+ position: absolute;
2376
+ left: -20px;
2377
+ top: 10px;
2378
+ width: 12px;
2379
+ height: 1px;
2380
+ background: #ccc;
2381
+ }
2382
+
2383
+ /* Last item covers the vertical line below it */
2384
+ .file-tree-item.last::after {
2385
+ content: '';
2386
+ position: absolute;
2387
+ left: -20px;
2388
+ top: 11px;
2389
+ bottom: 0;
2390
+ width: 1px;
2391
+ background: white;
2392
+ }
2393
+
2394
+ .file-tree-line {
2395
+ display: flex;
2396
+ align-items: center;
2397
+ gap: 5px;
2398
+ padding: 3px 0;
2399
+ cursor: pointer;
2400
+ user-select: none;
2401
+ white-space: nowrap;
2402
+ font-size: 12px;
2403
+ color: #333;
2404
+ }
2405
+
2406
+ .file-tree-line:hover {
2407
+ color: var(--theme-accent);
2408
+ }
2409
+
2410
+ .file-tree-line:hover .file-tree-name {
2411
+ text-decoration: underline;
2412
+ }
2413
+
2414
+ .file-tree-icon {
2415
+ font-size: 10px;
2416
+ min-width: 12px;
2417
+ text-align: center;
2418
+ color: var(--theme-accent);
2419
+ flex-shrink: 0;
2420
+ }
2421
+
2422
+ .file-tree-item.file .file-tree-icon {
2423
+ color: transparent;
2424
+ }
2425
+
2426
+ .file-tree-name {
2427
+ overflow: hidden;
2428
+ text-overflow: ellipsis;
2429
+ }
2430
+
2431
+ /* Children container - same pattern as parent */
2432
+ .file-tree-children {
2433
+ display: none;
2434
+ position: relative;
2435
+ padding-left: 20px;
2436
+ }
2437
+
2438
+ .file-tree-children::before {
2439
+ content: '';
2440
+ position: absolute;
2441
+ left: 0;
2442
+ top: 0;
2443
+ bottom: 0;
2444
+ width: 1px;
2445
+ background: #ccc;
2446
+ }
2447
+
2448
+ .file-tree-children.expanded {
2449
+ display: block;
2450
+ }
tests/backend/test_api.py CHANGED
@@ -152,6 +152,102 @@ class TestChatEndpoints:
152
  # Empty messages should return 400
153
  assert response.status_code == 400
154
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
155
 
156
  class TestDebugEndpoints:
157
  """Test debug message history endpoints"""
@@ -198,6 +294,40 @@ class TestTitleGeneration:
198
  assert response.status_code == 500
199
 
200
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
  class TestStaticFiles:
202
  """Test static file serving"""
203
 
 
152
  # Empty messages should return 400
153
  assert response.status_code == 400
154
 
155
+ def test_chat_stream_accepts_frontend_context(self, client):
156
+ """Test that chat accepts frontend_context parameter"""
157
+ response = client.post("/api/chat/stream", json={
158
+ "messages": [{"role": "user", "content": "Hello"}],
159
+ "notebook_type": "code",
160
+ "endpoint": "https://api.openai.com/v1",
161
+ "token": "test",
162
+ "model": "gpt-4",
163
+ "notebook_id": "1",
164
+ "frontend_context": {
165
+ "theme": {
166
+ "name": "forest",
167
+ "accent": "#1b5e20",
168
+ "bg": "#e8f5e9",
169
+ "border": "#1b5e20"
170
+ },
171
+ "open_notebooks": ["command", "code"]
172
+ }
173
+ })
174
+ # Request should be valid (actual streaming would fail due to invalid endpoint, but request is accepted)
175
+ # The request gets accepted and starts streaming, so we won't get a 4xx error
176
+ # Since we're using an invalid API key, it will eventually error but should accept the request format
177
+ assert response.status_code == 200 # SSE stream starts
178
+
179
+
180
+ class TestFrontendContextModel:
181
+ """Test FrontendContext Pydantic model"""
182
+
183
+ def test_frontend_context_model_creation(self):
184
+ """Test that FrontendContext model can be created"""
185
+ import main
186
+ ctx = main.FrontendContext(
187
+ theme={"name": "forest", "accent": "#1b5e20", "bg": "#e8f5e9"},
188
+ open_notebooks=["command", "code"]
189
+ )
190
+ assert ctx.theme["name"] == "forest"
191
+ assert ctx.open_notebooks == ["command", "code"]
192
+
193
+ def test_frontend_context_optional_fields(self):
194
+ """Test that FrontendContext fields are optional"""
195
+ import main
196
+ ctx = main.FrontendContext()
197
+ assert ctx.theme is None
198
+ assert ctx.open_notebooks is None
199
+
200
+ def test_chat_request_with_frontend_context(self):
201
+ """Test ChatRequest with frontend_context"""
202
+ import main
203
+ ctx = main.FrontendContext(
204
+ theme={"name": "sapphire", "accent": "#0d47a1"}
205
+ )
206
+ req = main.ChatRequest(
207
+ messages=[],
208
+ endpoint="https://api.openai.com/v1",
209
+ frontend_context=ctx
210
+ )
211
+ assert req.frontend_context is not None
212
+ assert req.frontend_context.theme["name"] == "sapphire"
213
+
214
+
215
+ class TestStylingContext:
216
+ """Test styling context generation"""
217
+
218
+ def test_get_styling_context_without_theme(self):
219
+ """Test get_styling_context without theme"""
220
+ import main
221
+ result = main.get_styling_context(None)
222
+ assert "Visual Style Guidelines" in result
223
+ assert "minimalist" in result
224
+
225
+ def test_get_styling_context_with_theme(self):
226
+ """Test get_styling_context with theme"""
227
+ import main
228
+ theme = {"name": "forest", "accent": "#1b5e20", "bg": "#e8f5e9"}
229
+ result = main.get_styling_context(theme)
230
+ assert "forest" in result
231
+ assert "#1b5e20" in result
232
+ assert "#e8f5e9" in result
233
+
234
+ def test_get_system_prompt_code_with_context(self):
235
+ """Test get_system_prompt for code notebook includes styling"""
236
+ import main
237
+ context = {"theme": {"name": "ocean", "accent": "#00796b", "bg": "#e0f2f1"}}
238
+ result = main.get_system_prompt("code", context)
239
+ assert "Visual Style Guidelines" in result
240
+ assert "ocean" in result
241
+ assert "#00796b" in result
242
+
243
+ def test_get_system_prompt_chat_no_styling(self):
244
+ """Test get_system_prompt for chat notebook doesn't include styling"""
245
+ import main
246
+ context = {"theme": {"name": "forest", "accent": "#1b5e20"}}
247
+ result = main.get_system_prompt("chat", context)
248
+ # Chat notebooks should not have styling guidelines
249
+ assert "Visual Style Guidelines" not in result
250
+
251
 
252
  class TestDebugEndpoints:
253
  """Test debug message history endpoints"""
 
294
  assert response.status_code == 500
295
 
296
 
297
+ class TestFilesEndpoint:
298
+ """Test file tree endpoint"""
299
+
300
+ def test_get_files_returns_tree(self, client):
301
+ """Test that /api/files returns a file tree"""
302
+ response = client.get("/api/files")
303
+ assert response.status_code == 200
304
+ data = response.json()
305
+ assert "root" in data
306
+ assert "tree" in data
307
+ assert isinstance(data["tree"], list)
308
+
309
+ def test_get_files_with_show_hidden(self, client):
310
+ """Test that show_hidden parameter works"""
311
+ response = client.get("/api/files?show_hidden=true")
312
+ assert response.status_code == 200
313
+ data = response.json()
314
+ assert "tree" in data
315
+
316
+ def test_file_tree_structure(self, client):
317
+ """Test that file tree items have correct structure"""
318
+ response = client.get("/api/files")
319
+ data = response.json()
320
+ # Should have at least some items (frontend, backend folders)
321
+ assert len(data["tree"]) > 0
322
+ for item in data["tree"]:
323
+ assert "name" in item
324
+ assert "type" in item
325
+ assert item["type"] in ["file", "folder"]
326
+ assert "path" in item
327
+ if item["type"] == "folder":
328
+ assert "children" in item
329
+
330
+
331
  class TestStaticFiles:
332
  """Test static file serving"""
333
 
tests/e2e/app.spec.js CHANGED
@@ -129,6 +129,36 @@ test.describe('Debug Panel', () => {
129
  });
130
  });
131
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
132
  test.describe('Input Fields', () => {
133
  test('should have input field in command center', async ({ page }) => {
134
  await page.goto('/');
 
129
  });
130
  });
131
 
132
+ test.describe('Files Panel', () => {
133
+ test('should open and close files panel', async ({ page }) => {
134
+ await page.goto('/');
135
+
136
+ // Click files button
137
+ await page.locator('#filesBtn').click();
138
+
139
+ // Panel should be visible
140
+ await expect(page.locator('#filesPanel')).toHaveClass(/active/);
141
+ await expect(page.locator('#filesBtn')).toHaveClass(/active/);
142
+
143
+ // Should load file tree
144
+ await page.waitForTimeout(500);
145
+ const treeItems = page.locator('.file-tree-item');
146
+ await expect(treeItems.first()).toBeVisible();
147
+
148
+ // Close panel
149
+ await page.locator('#filesPanelClose').click();
150
+ await expect(page.locator('#filesPanel')).not.toHaveClass(/active/);
151
+ });
152
+
153
+ test('should have show hidden toggle and refresh button', async ({ page }) => {
154
+ await page.goto('/');
155
+
156
+ await page.locator('#filesBtn').click();
157
+ await expect(page.locator('#showHiddenFiles')).toBeVisible();
158
+ await expect(page.locator('#filesRefresh')).toBeVisible();
159
+ });
160
+ });
161
+
162
  test.describe('Input Fields', () => {
163
  test('should have input field in command center', async ({ page }) => {
164
  await page.goto('/');
tests/playwright.config.js CHANGED
@@ -29,7 +29,7 @@ export default defineConfig({
29
  },
30
  ],
31
  webServer: {
32
- command: `mkdir -p "${testDataDir}" && cd "${projectRoot}/backend" && python main.py --port 8766 --no-browser --data-dir "${testDataDir}"`,
33
  url: 'http://localhost:8766',
34
  reuseExistingServer: !process.env.CI,
35
  timeout: 30000,
 
29
  },
30
  ],
31
  webServer: {
32
+ command: `mkdir -p "${testDataDir}" && cd "${projectRoot}" && source env312/bin/activate && cd backend && python main.py --port 8766 --no-browser --data-dir "${testDataDir}"`,
33
  url: 'http://localhost:8766',
34
  reuseExistingServer: !process.env.CI,
35
  timeout: 30000,
workspace/test/data.json ADDED
@@ -0,0 +1,802 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "x": 0.0,
4
+ "y": -0.164171512786321
5
+ },
6
+ {
7
+ "x": 0.06314759102693052,
8
+ "y": 0.163486253247035
9
+ },
10
+ {
11
+ "x": 0.12629518205386103,
12
+ "y": 0.06261328015881538
13
+ },
14
+ {
15
+ "x": 0.18944277308079155,
16
+ "y": 0.1287950820033433
17
+ },
18
+ {
19
+ "x": 0.25259036410772207,
20
+ "y": 0.4162089174928977
21
+ },
22
+ {
23
+ "x": 0.3157379551346526,
24
+ "y": 0.10724584107532617
25
+ },
26
+ {
27
+ "x": 0.3788855461615831,
28
+ "y": 0.4987717988412488
29
+ },
30
+ {
31
+ "x": 0.4420331371885136,
32
+ "y": 0.8581795430773769
33
+ },
34
+ {
35
+ "x": 0.5051807282154441,
36
+ "y": 0.36924957416756565
37
+ },
38
+ {
39
+ "x": 0.5683283192423747,
40
+ "y": 0.5772833266593322
41
+ },
42
+ {
43
+ "x": 0.6314759102693052,
44
+ "y": 0.27454277951949413
45
+ },
46
+ {
47
+ "x": 0.6946235012962356,
48
+ "y": 0.7687628470013369
49
+ },
50
+ {
51
+ "x": 0.7577710923231662,
52
+ "y": 0.20400266081078122
53
+ },
54
+ {
55
+ "x": 0.8209186833500968,
56
+ "y": 0.6464892997888316
57
+ },
58
+ {
59
+ "x": 0.8840662743770272,
60
+ "y": 0.883198389772504
61
+ },
62
+ {
63
+ "x": 0.9472138654039577,
64
+ "y": 0.33362855877086944
65
+ },
66
+ {
67
+ "x": 1.0103614564308883,
68
+ "y": 1.3986711499478823
69
+ },
70
+ {
71
+ "x": 1.0735090474578188,
72
+ "y": 0.6106210353416557
73
+ },
74
+ {
75
+ "x": 1.1366566384847494,
76
+ "y": 1.191478286764215
77
+ },
78
+ {
79
+ "x": 1.1998042295116798,
80
+ "y": 1.2612451639413258
81
+ },
82
+ {
83
+ "x": 1.2629518205386103,
84
+ "y": 1.2800466075403019
85
+ },
86
+ {
87
+ "x": 1.326099411565541,
88
+ "y": 1.3440291604812473
89
+ },
90
+ {
91
+ "x": 1.3892470025924712,
92
+ "y": 1.1624599256020136
93
+ },
94
+ {
95
+ "x": 1.4523945936194018,
96
+ "y": 1.0545608609636905
97
+ },
98
+ {
99
+ "x": 1.5155421846463324,
100
+ "y": 1.2356356840077487
101
+ },
102
+ {
103
+ "x": 1.578689775673263,
104
+ "y": 0.9086702625892683
105
+ },
106
+ {
107
+ "x": 1.6418373667001935,
108
+ "y": 0.9439604908602219
109
+ },
110
+ {
111
+ "x": 1.7049849577271239,
112
+ "y": 0.8630401725643511
113
+ },
114
+ {
115
+ "x": 1.7681325487540545,
116
+ "y": 1.121959888544887
117
+ },
118
+ {
119
+ "x": 1.831280139780985,
120
+ "y": 1.1861278593856923
121
+ },
122
+ {
123
+ "x": 1.8944277308079154,
124
+ "y": 0.9237535337281273
125
+ },
126
+ {
127
+ "x": 1.957575321834846,
128
+ "y": 0.6374338821271788
129
+ },
130
+ {
131
+ "x": 2.0207229128617765,
132
+ "y": 0.7915592047370582
133
+ },
134
+ {
135
+ "x": 2.083870503888707,
136
+ "y": 1.0790349015495115
137
+ },
138
+ {
139
+ "x": 2.1470180949156377,
140
+ "y": 0.8412001427609885
141
+ },
142
+ {
143
+ "x": 2.210165685942568,
144
+ "y": 0.7471330119640848
145
+ },
146
+ {
147
+ "x": 2.273313276969499,
148
+ "y": 0.7916058803441647
149
+ },
150
+ {
151
+ "x": 2.336460867996429,
152
+ "y": 1.1478520916781954
153
+ },
154
+ {
155
+ "x": 2.3996084590233595,
156
+ "y": 1.102927250024849
157
+ },
158
+ {
159
+ "x": 2.4627560500502903,
160
+ "y": 1.0222803505714153
161
+ },
162
+ {
163
+ "x": 2.5259036410772207,
164
+ "y": 0.24594572888301336
165
+ },
166
+ {
167
+ "x": 2.589051232104151,
168
+ "y": 0.3044731356625734
169
+ },
170
+ {
171
+ "x": 2.652198823131082,
172
+ "y": 0.980832694746469
173
+ },
174
+ {
175
+ "x": 2.715346414158012,
176
+ "y": 0.3098313928561607
177
+ },
178
+ {
179
+ "x": 2.7784940051849425,
180
+ "y": 0.5088460679801076
181
+ },
182
+ {
183
+ "x": 2.8416415962118733,
184
+ "y": 0.33875060761239734
185
+ },
186
+ {
187
+ "x": 2.9047891872388036,
188
+ "y": 0.031108350101302168
189
+ },
190
+ {
191
+ "x": 2.9679367782657344,
192
+ "y": 0.048145116342521194
193
+ },
194
+ {
195
+ "x": 3.031084369292665,
196
+ "y": 0.4878372210124654
197
+ },
198
+ {
199
+ "x": 3.094231960319595,
200
+ "y": -0.1434572705091495
201
+ },
202
+ {
203
+ "x": 3.157379551346526,
204
+ "y": -0.2506176171524982
205
+ },
206
+ {
207
+ "x": 3.2205271423734563,
208
+ "y": -0.3137172220787668
209
+ },
210
+ {
211
+ "x": 3.283674733400387,
212
+ "y": -0.03622420388515449
213
+ },
214
+ {
215
+ "x": 3.3468223244273174,
216
+ "y": -0.6134287755190555
217
+ },
218
+ {
219
+ "x": 3.4099699154542478,
220
+ "y": -0.5437046121555442
221
+ },
222
+ {
223
+ "x": 3.4731175064811786,
224
+ "y": -0.5672415784533531
225
+ },
226
+ {
227
+ "x": 3.536265097508109,
228
+ "y": -0.6591927925628567
229
+ },
230
+ {
231
+ "x": 3.5994126885350393,
232
+ "y": -0.07212837355303453
233
+ },
234
+ {
235
+ "x": 3.66256027956197,
236
+ "y": -1.1270338805013864
237
+ },
238
+ {
239
+ "x": 3.7257078705889004,
240
+ "y": -0.39267447979562786
241
+ },
242
+ {
243
+ "x": 3.7888554616158308,
244
+ "y": -1.100799104320724
245
+ },
246
+ {
247
+ "x": 3.8520030526427615,
248
+ "y": -1.1534773728612362
249
+ },
250
+ {
251
+ "x": 3.915150643669692,
252
+ "y": -1.0075038531610818
253
+ },
254
+ {
255
+ "x": 3.9782982346966227,
256
+ "y": -1.2143815081773557
257
+ },
258
+ {
259
+ "x": 4.041445825723553,
260
+ "y": -0.7516687923490095
261
+ },
262
+ {
263
+ "x": 4.104593416750483,
264
+ "y": -0.67087026419052
265
+ },
266
+ {
267
+ "x": 4.167741007777414,
268
+ "y": -0.4759748204278203
269
+ },
270
+ {
271
+ "x": 4.230888598804345,
272
+ "y": -0.5949711890796414
273
+ },
274
+ {
275
+ "x": 4.294036189831275,
276
+ "y": -0.9959877106987852
277
+ },
278
+ {
279
+ "x": 4.357183780858206,
280
+ "y": -0.6836905591037317
281
+ },
282
+ {
283
+ "x": 4.420331371885136,
284
+ "y": -0.9883010946906967
285
+ },
286
+ {
287
+ "x": 4.483478962912066,
288
+ "y": -1.0835565110252792
289
+ },
290
+ {
291
+ "x": 4.546626553938998,
292
+ "y": -0.7897297525496717
293
+ },
294
+ {
295
+ "x": 4.609774144965928,
296
+ "y": -1.4249288518380685
297
+ },
298
+ {
299
+ "x": 4.672921735992858,
300
+ "y": -1.093316588219836
301
+ },
302
+ {
303
+ "x": 4.736069327019789,
304
+ "y": -1.2279258571060911
305
+ },
306
+ {
307
+ "x": 4.799216918046719,
308
+ "y": -1.1244341966895806
309
+ },
310
+ {
311
+ "x": 4.862364509073649,
312
+ "y": -1.0131952038478669
313
+ },
314
+ {
315
+ "x": 4.925512100100581,
316
+ "y": -0.7973338101080346
317
+ },
318
+ {
319
+ "x": 4.988659691127511,
320
+ "y": -1.4699281322164164
321
+ },
322
+ {
323
+ "x": 5.051807282154441,
324
+ "y": -1.0445495623407317
325
+ },
326
+ {
327
+ "x": 5.114954873181372,
328
+ "y": -1.0688809476224235
329
+ },
330
+ {
331
+ "x": 5.178102464208302,
332
+ "y": -0.7811048981060283
333
+ },
334
+ {
335
+ "x": 5.241250055235233,
336
+ "y": -0.8622544816438432
337
+ },
338
+ {
339
+ "x": 5.304397646262164,
340
+ "y": -0.8200809952408314
341
+ },
342
+ {
343
+ "x": 5.367545237289094,
344
+ "y": -0.5632508023113583
345
+ },
346
+ {
347
+ "x": 5.430692828316024,
348
+ "y": -0.8531962033267921
349
+ },
350
+ {
351
+ "x": 5.493840419342955,
352
+ "y": -0.5707860276698525
353
+ },
354
+ {
355
+ "x": 5.556988010369885,
356
+ "y": -0.24510318810662557
357
+ },
358
+ {
359
+ "x": 5.620135601396816,
360
+ "y": -0.5845323088129928
361
+ },
362
+ {
363
+ "x": 5.683283192423747,
364
+ "y": -0.8852990629058648
365
+ },
366
+ {
367
+ "x": 5.746430783450677,
368
+ "y": -0.09046021871108612
369
+ },
370
+ {
371
+ "x": 5.809578374477607,
372
+ "y": -0.2620257018787725
373
+ },
374
+ {
375
+ "x": 5.872725965504538,
376
+ "y": -0.4648789856873344
377
+ },
378
+ {
379
+ "x": 5.935873556531469,
380
+ "y": -0.2873710734188944
381
+ },
382
+ {
383
+ "x": 5.999021147558399,
384
+ "y": -0.040189428299803626
385
+ },
386
+ {
387
+ "x": 6.06216873858533,
388
+ "y": -0.2828671334244661
389
+ },
390
+ {
391
+ "x": 6.12531632961226,
392
+ "y": -0.13276545533424783
393
+ },
394
+ {
395
+ "x": 6.18846392063919,
396
+ "y": 0.12576175072770018
397
+ },
398
+ {
399
+ "x": 6.2516115116661215,
400
+ "y": -0.20227606757172212
401
+ },
402
+ {
403
+ "x": 6.314759102693052,
404
+ "y": 0.18443496978191243
405
+ },
406
+ {
407
+ "x": 6.377906693719982,
408
+ "y": 0.6654671164991045
409
+ },
410
+ {
411
+ "x": 6.4410542847469126,
412
+ "y": 0.005923853694681153
413
+ },
414
+ {
415
+ "x": 6.504201875773843,
416
+ "y": 0.17336545296245054
417
+ },
418
+ {
419
+ "x": 6.567349466800774,
420
+ "y": 0.013180109983592536
421
+ },
422
+ {
423
+ "x": 6.6304970578277045,
424
+ "y": 0.44832314627570274
425
+ },
426
+ {
427
+ "x": 6.693644648854635,
428
+ "y": 0.1859579298190778
429
+ },
430
+ {
431
+ "x": 6.756792239881565,
432
+ "y": 0.33142152760319854
433
+ },
434
+ {
435
+ "x": 6.8199398309084955,
436
+ "y": 0.4489501812855101
437
+ },
438
+ {
439
+ "x": 6.883087421935426,
440
+ "y": 0.6286649015324393
441
+ },
442
+ {
443
+ "x": 6.946235012962357,
444
+ "y": 1.1017509992163785
445
+ },
446
+ {
447
+ "x": 7.0093826039892875,
448
+ "y": 0.046005745188661074
449
+ },
450
+ {
451
+ "x": 7.072530195016218,
452
+ "y": 0.8630844450949116
453
+ },
454
+ {
455
+ "x": 7.135677786043148,
456
+ "y": 0.5866169106138275
457
+ },
458
+ {
459
+ "x": 7.1988253770700785,
460
+ "y": 0.8471167331747427
461
+ },
462
+ {
463
+ "x": 7.26197296809701,
464
+ "y": 0.3755080122113592
465
+ },
466
+ {
467
+ "x": 7.32512055912394,
468
+ "y": 1.4001383615856322
469
+ },
470
+ {
471
+ "x": 7.3882681501508705,
472
+ "y": 1.056872792099301
473
+ },
474
+ {
475
+ "x": 7.451415741177801,
476
+ "y": 0.927622796811582
477
+ },
478
+ {
479
+ "x": 7.514563332204731,
480
+ "y": 1.0317644736936173
481
+ },
482
+ {
483
+ "x": 7.5777109232316615,
484
+ "y": 1.2900239127063728
485
+ },
486
+ {
487
+ "x": 7.640858514258593,
488
+ "y": 0.4826112615934133
489
+ },
490
+ {
491
+ "x": 7.704006105285523,
492
+ "y": 1.3282957305777943
493
+ },
494
+ {
495
+ "x": 7.7671536963124534,
496
+ "y": 0.9503048197434517
497
+ },
498
+ {
499
+ "x": 7.830301287339384,
500
+ "y": 0.8337681217692257
501
+ },
502
+ {
503
+ "x": 7.893448878366314,
504
+ "y": 1.2693367746926236
505
+ },
506
+ {
507
+ "x": 7.956596469393245,
508
+ "y": 1.1893868376430397
509
+ },
510
+ {
511
+ "x": 8.019744060420175,
512
+ "y": 1.0221508314347647
513
+ },
514
+ {
515
+ "x": 8.082891651447106,
516
+ "y": 1.0508367571681816
517
+ },
518
+ {
519
+ "x": 8.146039242474037,
520
+ "y": 0.5927617697173999
521
+ },
522
+ {
523
+ "x": 8.209186833500967,
524
+ "y": 1.3099213762130097
525
+ },
526
+ {
527
+ "x": 8.272334424527898,
528
+ "y": 1.5395993246195032
529
+ },
530
+ {
531
+ "x": 8.335482015554827,
532
+ "y": 0.9139099685410099
533
+ },
534
+ {
535
+ "x": 8.398629606581759,
536
+ "y": 0.8605135181886638
537
+ },
538
+ {
539
+ "x": 8.46177719760869,
540
+ "y": 1.0023221551863521
541
+ },
542
+ {
543
+ "x": 8.52492478863562,
544
+ "y": 0.778626113997441
545
+ },
546
+ {
547
+ "x": 8.58807237966255,
548
+ "y": 0.7040585197240989
549
+ },
550
+ {
551
+ "x": 8.65121997068948,
552
+ "y": 0.8356536401766204
553
+ },
554
+ {
555
+ "x": 8.714367561716411,
556
+ "y": 0.551214633626738
557
+ },
558
+ {
559
+ "x": 8.777515152743343,
560
+ "y": 0.12502069242348735
561
+ },
562
+ {
563
+ "x": 8.840662743770272,
564
+ "y": 0.1946906386376765
565
+ },
566
+ {
567
+ "x": 8.903810334797203,
568
+ "y": 0.4898616845022501
569
+ },
570
+ {
571
+ "x": 8.966957925824133,
572
+ "y": 0.0891643765998798
573
+ },
574
+ {
575
+ "x": 9.030105516851064,
576
+ "y": 0.3559871777856316
577
+ },
578
+ {
579
+ "x": 9.093253107877995,
580
+ "y": 0.4509391287898228
581
+ },
582
+ {
583
+ "x": 9.156400698904925,
584
+ "y": -0.17054735202703009
585
+ },
586
+ {
587
+ "x": 9.219548289931856,
588
+ "y": -0.03455046652518412
589
+ },
590
+ {
591
+ "x": 9.282695880958785,
592
+ "y": -0.24024028916603768
593
+ },
594
+ {
595
+ "x": 9.345843471985717,
596
+ "y": 0.49602887100611226
597
+ },
598
+ {
599
+ "x": 9.408991063012646,
600
+ "y": -0.21652837137778178
601
+ },
602
+ {
603
+ "x": 9.472138654039577,
604
+ "y": -0.4173513379049771
605
+ },
606
+ {
607
+ "x": 9.535286245066509,
608
+ "y": -0.01660495139973743
609
+ },
610
+ {
611
+ "x": 9.598433836093438,
612
+ "y": 0.10512700333750288
613
+ },
614
+ {
615
+ "x": 9.66158142712037,
616
+ "y": -0.7939214616363623
617
+ },
618
+ {
619
+ "x": 9.724729018147299,
620
+ "y": -0.23761946954476132
621
+ },
622
+ {
623
+ "x": 9.78787660917423,
624
+ "y": -0.3714976580077742
625
+ },
626
+ {
627
+ "x": 9.851024200201161,
628
+ "y": -0.003531267598519905
629
+ },
630
+ {
631
+ "x": 9.91417179122809,
632
+ "y": -0.7961663694079557
633
+ },
634
+ {
635
+ "x": 9.977319382255022,
636
+ "y": -0.31629538059521983
637
+ },
638
+ {
639
+ "x": 10.040466973281951,
640
+ "y": -0.42770312455028653
641
+ },
642
+ {
643
+ "x": 10.103614564308883,
644
+ "y": -0.8182108550353944
645
+ },
646
+ {
647
+ "x": 10.166762155335814,
648
+ "y": -0.2691246828378265
649
+ },
650
+ {
651
+ "x": 10.229909746362743,
652
+ "y": -0.9194522138147393
653
+ },
654
+ {
655
+ "x": 10.293057337389675,
656
+ "y": -0.7019982784275451
657
+ },
658
+ {
659
+ "x": 10.356204928416604,
660
+ "y": -0.7447445602245387
661
+ },
662
+ {
663
+ "x": 10.419352519443535,
664
+ "y": -0.9736255218039706
665
+ },
666
+ {
667
+ "x": 10.482500110470466,
668
+ "y": -1.0004812863703996
669
+ },
670
+ {
671
+ "x": 10.545647701497396,
672
+ "y": -0.9965174674911578
673
+ },
674
+ {
675
+ "x": 10.608795292524327,
676
+ "y": -0.8664202856415287
677
+ },
678
+ {
679
+ "x": 10.671942883551257,
680
+ "y": -1.1120285277235913
681
+ },
682
+ {
683
+ "x": 10.735090474578188,
684
+ "y": -0.9947249637973103
685
+ },
686
+ {
687
+ "x": 10.79823806560512,
688
+ "y": -0.8874804425495924
689
+ },
690
+ {
691
+ "x": 10.861385656632049,
692
+ "y": -1.0741905493668167
693
+ },
694
+ {
695
+ "x": 10.92453324765898,
696
+ "y": -0.6584825545192039
697
+ },
698
+ {
699
+ "x": 10.98768083868591,
700
+ "y": -0.665907715020841
701
+ },
702
+ {
703
+ "x": 11.05082842971284,
704
+ "y": -0.6659335744731814
705
+ },
706
+ {
707
+ "x": 11.11397602073977,
708
+ "y": -1.0222906279196728
709
+ },
710
+ {
711
+ "x": 11.177123611766701,
712
+ "y": -1.203755211455369
713
+ },
714
+ {
715
+ "x": 11.240271202793632,
716
+ "y": -0.6462563794879772
717
+ },
718
+ {
719
+ "x": 11.303418793820562,
720
+ "y": -0.9571887636228328
721
+ },
722
+ {
723
+ "x": 11.366566384847493,
724
+ "y": -0.7045729933655224
725
+ },
726
+ {
727
+ "x": 11.429713975874423,
728
+ "y": -0.8634863157789737
729
+ },
730
+ {
731
+ "x": 11.492861566901354,
732
+ "y": -0.6082344417839276
733
+ },
734
+ {
735
+ "x": 11.556009157928285,
736
+ "y": -0.4975954484935668
737
+ },
738
+ {
739
+ "x": 11.619156748955215,
740
+ "y": -0.7194991919306026
741
+ },
742
+ {
743
+ "x": 11.682304339982146,
744
+ "y": -0.9492131430891632
745
+ },
746
+ {
747
+ "x": 11.745451931009075,
748
+ "y": -0.8775978335046639
749
+ },
750
+ {
751
+ "x": 11.808599522036006,
752
+ "y": -1.3596565296630223
753
+ },
754
+ {
755
+ "x": 11.871747113062938,
756
+ "y": -0.33550469301555175
757
+ },
758
+ {
759
+ "x": 11.934894704089867,
760
+ "y": -0.46799163746781575
761
+ },
762
+ {
763
+ "x": 11.998042295116798,
764
+ "y": -0.8311283805255221
765
+ },
766
+ {
767
+ "x": 12.061189886143728,
768
+ "y": -0.5108402774253894
769
+ },
770
+ {
771
+ "x": 12.12433747717066,
772
+ "y": -0.9338327430742724
773
+ },
774
+ {
775
+ "x": 12.18748506819759,
776
+ "y": -0.7326288644118045
777
+ },
778
+ {
779
+ "x": 12.25063265922452,
780
+ "y": -0.12990942292220112
781
+ },
782
+ {
783
+ "x": 12.313780250251451,
784
+ "y": 0.18453948514732188
785
+ },
786
+ {
787
+ "x": 12.37692784127838,
788
+ "y": 0.13031087879597583
789
+ },
790
+ {
791
+ "x": 12.440075432305312,
792
+ "y": 0.024949724612247842
793
+ },
794
+ {
795
+ "x": 12.503223023332243,
796
+ "y": -0.586521279399101
797
+ },
798
+ {
799
+ "x": 12.566370614359172,
800
+ "y": 0.38312468760165397
801
+ }
802
+ ]
workspace/test/synthetic_data.csv ADDED
@@ -0,0 +1,151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Customer_ID,Age,Gender,Income,Region,Product_Category,Purchase_Amount,Purchase_Frequency,Payment_Method
2
+ 1,40,Male,65009,North,Books,500,4.0,Credit Card
3
+ 2,33,Female,56428,West,Electronics,500,4.8,Credit Card
4
+ 3,42,Female,49399,North,Electronics,500,4.0,Bank Transfer
5
+ 4,53,Female,84145,North,Electronics,500,4.0,Credit Card
6
+ 5,32,Male,53861,North,Electronics,500,4.8,Bank Transfer
7
+ 6,32,Female,33712,North,Clothing,500,4.8,Cash on Delivery
8
+ 7,53,Female,116815,North,Electronics,500,4.0,Cash on Delivery
9
+ 8,44,Male,75476,East,Electronics,500,4.0,Bank Transfer
10
+ 9,29,Male,20000,South,Electronics,500,4.4,Cash on Delivery
11
+ 10,41,Female,74631,West,Home & Kitchen,500,4.0,Credit Card
12
+ 11,29,Female,24006,West,Clothing,493,4.7,Bank Transfer
13
+ 12,29,Female,59241,South,Clothing,500,4.8,Credit Card
14
+ 13,37,Female,78671,North,Home & Kitchen,500,4.0,Credit Card
15
+ 14,18,Female,20000,South,Books,444,3.7,PayPal
16
+ 15,18,Male,46267,East,Electronics,500,4.8,Bank Transfer
17
+ 16,28,Male,50255,North,Books,500,4.8,Cash on Delivery
18
+ 17,22,Male,49441,East,Home & Kitchen,500,4.8,Bank Transfer
19
+ 18,38,Female,94935,South,Electronics,500,4.0,PayPal
20
+ 19,24,Male,31092,East,Sports,500,4.8,Bank Transfer
21
+ 20,18,Female,20000,South,Sports,416,3.7,Bank Transfer
22
+ 21,52,Female,60209,North,Electronics,500,4.0,Credit Card
23
+ 22,32,Female,31683,West,Clothing,500,4.8,Credit Card
24
+ 23,35,Male,50957,East,Sports,500,4.8,Credit Card
25
+ 24,18,Female,33823,North,Books,500,4.6,Credit Card
26
+ 25,28,Male,47533,West,Electronics,500,4.8,Credit Card
27
+ 26,36,Female,70543,South,Clothing,500,4.0,Credit Card
28
+ 27,21,Male,31760,West,Clothing,500,4.6,Credit Card
29
+ 28,39,Male,87570,East,Home & Kitchen,500,4.0,Credit Card
30
+ 29,27,Female,35206,South,Home & Kitchen,500,4.8,Cash on Delivery
31
+ 30,31,Male,100903,North,Clothing,500,4.8,Credit Card
32
+ 31,27,Male,53013,North,Home & Kitchen,500,4.8,Credit Card
33
+ 32,57,Female,68356,South,Books,500,4.0,PayPal
34
+ 33,34,Female,29582,East,Electronics,500,4.8,Credit Card
35
+ 34,22,Female,42649,North,Clothing,500,4.8,Cash on Delivery
36
+ 35,44,Female,61530,North,Books,500,4.0,Bank Transfer
37
+ 36,20,Female,44280,West,Sports,500,4.8,Credit Card
38
+ 37,37,Male,64964,West,Home & Kitchen,500,4.0,Credit Card
39
+ 38,18,Male,25543,South,Home & Kitchen,500,4.1,Bank Transfer
40
+ 39,19,Male,20000,North,Electronics,500,3.8,Cash on Delivery
41
+ 40,37,Female,25203,North,Clothing,500,4.0,Credit Card
42
+ 41,43,Female,55569,North,Home & Kitchen,500,4.0,Bank Transfer
43
+ 42,37,Male,72627,West,Clothing,500,4.0,Credit Card
44
+ 43,33,Male,53781,North,Clothing,500,4.8,Cash on Delivery
45
+ 44,31,Female,21585,East,Clothing,436,4.6,Credit Card
46
+ 45,18,Male,30463,North,Electronics,500,4.3,Cash on Delivery
47
+ 46,26,Female,46706,East,Clothing,500,4.8,PayPal
48
+ 47,29,Female,25822,East,Clothing,492,4.8,PayPal
49
+ 48,47,Female,73574,West,Clothing,500,4.0,Bank Transfer
50
+ 49,39,Male,59664,North,Electronics,500,4.0,Credit Card
51
+ 50,18,Female,20000,North,Sports,413,3.7,Credit Card
52
+ 51,38,Female,64155,North,Home & Kitchen,500,4.0,Credit Card
53
+ 52,30,Female,56215,West,Books,500,4.8,Credit Card
54
+ 53,26,Female,60661,West,Books,500,4.8,Bank Transfer
55
+ 54,42,Female,84076,East,Home & Kitchen,500,4.0,Credit Card
56
+ 55,47,Female,42946,East,Electronics,500,4.0,Credit Card
57
+ 56,46,Male,50243,East,Home & Kitchen,500,4.0,Cash on Delivery
58
+ 57,24,Female,46300,North,Home & Kitchen,500,4.8,Bank Transfer
59
+ 58,31,Female,56775,West,Books,500,4.8,Cash on Delivery
60
+ 59,38,Male,67300,South,Clothing,500,4.0,Cash on Delivery
61
+ 60,46,Female,120000,South,Electronics,500,4.0,Credit Card
62
+ 61,29,Female,54917,South,Clothing,500,4.8,Credit Card
63
+ 62,32,Female,70711,North,Clothing,500,4.8,Bank Transfer
64
+ 63,21,Male,50580,North,Home & Kitchen,500,4.8,PayPal
65
+ 64,20,Male,43027,East,Home & Kitchen,500,4.8,Credit Card
66
+ 65,44,Female,59694,North,Clothing,500,4.0,Bank Transfer
67
+ 66,51,Female,91679,East,Sports,500,4.0,Credit Card
68
+ 67,34,Male,35543,North,Home & Kitchen,500,4.8,Credit Card
69
+ 68,47,Female,65763,South,Clothing,500,4.0,Credit Card
70
+ 69,39,Female,48792,North,Electronics,500,4.0,Cash on Delivery
71
+ 70,27,Female,42137,South,Electronics,500,4.8,Bank Transfer
72
+ 71,39,Female,104793,East,Clothing,500,4.0,Credit Card
73
+ 72,53,Male,42154,North,Electronics,500,4.0,Bank Transfer
74
+ 73,34,Male,64725,East,Electronics,500,4.8,Credit Card
75
+ 74,53,Male,47245,South,Clothing,500,4.0,Credit Card
76
+ 75,18,Female,20000,East,Electronics,500,3.7,Bank Transfer
77
+ 76,44,Male,87779,West,Sports,500,4.0,Cash on Delivery
78
+ 77,36,Male,55285,East,Electronics,500,4.0,Bank Transfer
79
+ 78,31,Male,24945,North,Home & Kitchen,500,4.8,Bank Transfer
80
+ 79,36,Female,39693,North,Clothing,500,4.0,Credit Card
81
+ 80,18,Female,40591,North,Sports,500,4.8,Credit Card
82
+ 81,32,Male,33392,East,Electronics,500,4.8,Credit Card
83
+ 82,39,Male,62829,North,Clothing,500,4.0,Credit Card
84
+ 83,52,Female,78911,South,Sports,500,4.0,Credit Card
85
+ 84,28,Male,28967,East,Sports,500,4.8,Credit Card
86
+ 85,25,Male,80378,South,Sports,500,4.8,Bank Transfer
87
+ 86,28,Male,54678,East,Clothing,500,4.8,PayPal
88
+ 87,45,Male,26997,North,Electronics,500,4.0,Credit Card
89
+ 88,38,Male,60729,North,Books,500,4.0,Credit Card
90
+ 89,28,Female,28764,East,Sports,500,4.8,PayPal
91
+ 90,41,Male,78548,South,Home & Kitchen,500,4.0,Credit Card
92
+ 91,36,Female,38149,East,Home & Kitchen,500,4.0,Bank Transfer
93
+ 92,46,Male,66705,South,Clothing,500,4.0,Credit Card
94
+ 93,26,Female,49099,North,Books,500,4.8,Credit Card
95
+ 94,31,Female,63815,West,Electronics,500,4.8,Credit Card
96
+ 95,30,Male,20994,East,Clothing,475,4.6,Bank Transfer
97
+ 96,18,Female,20309,East,Clothing,412,3.7,Credit Card
98
+ 97,38,Male,47501,North,Home & Kitchen,500,4.0,Bank Transfer
99
+ 98,38,Female,43933,East,Clothing,500,4.0,Bank Transfer
100
+ 99,35,Male,87809,South,Electronics,500,4.8,Credit Card
101
+ 100,32,Male,56099,North,Home & Kitchen,500,4.8,Bank Transfer
102
+ 101,18,Male,20000,West,Clothing,372,3.7,Bank Transfer
103
+ 102,29,Female,61857,North,Home & Kitchen,500,4.8,Bank Transfer
104
+ 103,30,Female,87443,West,Electronics,500,4.8,Bank Transfer
105
+ 104,25,Female,58149,South,Home & Kitchen,500,4.8,Bank Transfer
106
+ 105,33,Male,20000,North,Home & Kitchen,406,4.7,Bank Transfer
107
+ 106,39,Female,48815,South,Electronics,500,4.0,Bank Transfer
108
+ 107,57,Male,110838,West,Clothing,500,4.0,PayPal
109
+ 108,37,Female,41346,East,Electronics,500,4.0,PayPal
110
+ 109,38,Male,65876,West,Electronics,500,4.0,Bank Transfer
111
+ 110,34,Male,66492,East,Sports,500,4.8,Bank Transfer
112
+ 111,18,Male,20000,East,Clothing,441,3.7,Credit Card
113
+ 112,34,Male,49809,West,Sports,500,4.8,Bank Transfer
114
+ 113,35,Female,20000,North,Clothing,373,4.8,Bank Transfer
115
+ 114,64,Male,75512,South,Books,500,4.0,Credit Card
116
+ 115,32,Female,42948,North,Books,500,4.8,Bank Transfer
117
+ 116,38,Male,32044,West,Home & Kitchen,500,4.0,Credit Card
118
+ 117,34,Female,83648,West,Home & Kitchen,500,4.8,Credit Card
119
+ 118,20,Male,20000,North,Clothing,423,3.8,Bank Transfer
120
+ 119,48,Male,63199,East,Clothing,500,4.0,Credit Card
121
+ 120,44,Female,68614,West,Sports,500,4.0,Bank Transfer
122
+ 121,44,Male,94825,North,Sports,500,4.0,Bank Transfer
123
+ 122,24,Male,20000,East,Sports,472,4.1,Credit Card
124
+ 123,51,Female,99763,West,Electronics,500,4.0,Credit Card
125
+ 124,18,Female,27204,North,Books,500,4.2,Credit Card
126
+ 125,42,Male,43369,East,Sports,500,4.0,PayPal
127
+ 126,61,Male,100742,North,Electronics,500,4.0,Cash on Delivery
128
+ 127,23,Female,38481,West,Electronics,500,4.8,PayPal
129
+ 128,28,Male,29995,West,Books,500,4.8,Credit Card
130
+ 129,36,Male,55396,West,Home & Kitchen,500,4.0,Credit Card
131
+ 130,28,Male,34293,South,Sports,500,4.8,PayPal
132
+ 131,18,Female,29270,West,Electronics,500,4.3,Bank Transfer
133
+ 132,35,Female,65742,West,Books,500,4.8,Credit Card
134
+ 133,22,Male,64720,South,Clothing,500,4.8,Bank Transfer
135
+ 134,40,Male,35243,West,Electronics,500,4.0,Credit Card
136
+ 135,23,Male,77160,North,Electronics,500,4.8,Cash on Delivery
137
+ 136,53,Female,40458,East,Sports,500,4.0,Credit Card
138
+ 137,25,Male,34464,North,Books,500,4.8,Credit Card
139
+ 138,31,Male,58266,North,Home & Kitchen,500,4.8,Cash on Delivery
140
+ 139,44,Female,71619,North,Clothing,500,4.0,Credit Card
141
+ 140,20,Male,20000,East,Sports,418,3.8,Cash on Delivery
142
+ 141,37,Female,51337,South,Clothing,500,4.0,Credit Card
143
+ 142,50,Male,65139,East,Sports,500,4.0,Credit Card
144
+ 143,18,Male,20000,North,Clothing,357,3.7,Credit Card
145
+ 144,37,Male,72492,South,Clothing,500,4.0,Bank Transfer
146
+ 145,38,Female,64140,South,Home & Kitchen,500,4.0,Credit Card
147
+ 146,44,Female,52141,East,Clothing,500,4.0,Credit Card
148
+ 147,20,Female,47991,South,Books,500,4.8,Credit Card
149
+ 148,19,Male,34645,East,Home & Kitchen,500,4.7,PayPal
150
+ 149,41,Female,77757,West,Clothing,500,4.0,Credit Card
151
+ 150,38,Male,69592,East,Sports,500,4.0,Bank Transfer
workspace/test/synthetic_data_analysis.png ADDED