mic3333 commited on
Commit
eebd9e7
·
verified ·
1 Parent(s): 4fbe2a3

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +32 -491
app.py CHANGED
@@ -1,477 +1,6 @@
1
- #!/usr/bin/env python3
2
- """
3
- LibreChat Custom Code Interpreter - HuggingFace Space
4
- A secure code execution server using Gradio + MCP with SSE support
5
- """
6
-
7
- import json
8
- import subprocess
9
- import tempfile
10
- import os
11
- import sys
12
- import uuid
13
- import time
14
- import base64
15
- import io
16
- import re
17
- from pathlib import Path
18
- from typing import Dict, Any, Optional, List
19
- import gradio as gr
20
- import matplotlib
21
- matplotlib.use('Agg') # Use non-interactive backend
22
- import matplotlib.pyplot as plt
23
- import numpy as np
24
- import pandas as pd
25
-
26
- class SecureCodeExecutor:
27
- def __init__(self):
28
- self.sessions = {}
29
- self.max_execution_time = 30
30
- self.max_output_length = 10000
31
- self.allowed_languages = ["python", "javascript", "bash"]
32
-
33
- # Security: List of blocked commands/imports
34
- self.blocked_imports = [
35
- 'subprocess', 'os', 'sys', 'shutil', 'glob', 'pickle',
36
- 'marshal', 'imp', 'importlib', '__import__'
37
- ]
38
- self.blocked_bash_commands = [
39
- 'rm', 'sudo', 'chmod', 'chown', 'dd', 'mkfs', 'fdisk',
40
- 'curl', 'wget', 'ssh', 'scp', 'nc', 'netcat'
41
- ]
42
-
43
- def create_session(self) -> str:
44
- """Create a new execution session"""
45
- session_id = str(uuid.uuid4())[:8] # Shorter ID for HF
46
- self.sessions[session_id] = {
47
- 'created_at': time.time(),
48
- 'variables': {},
49
- 'history': [],
50
- 'files': {}
51
- }
52
- return session_id
53
-
54
- def cleanup_old_sessions(self):
55
- """Remove sessions older than 1 hour"""
56
- current_time = time.time()
57
- old_sessions = [
58
- sid for sid, session in self.sessions.items()
59
- if current_time - session['created_at'] > 3600
60
- ]
61
- for sid in old_sessions:
62
- del self.sessions[sid]
63
-
64
- def is_code_safe(self, code: str, language: str) -> tuple[bool, str]:
65
- """Check if code is safe to execute using whole-word matching."""
66
- if language == "python":
67
- # Use regex to find whole-word matches for blocked imports
68
- for blocked in self.blocked_imports:
69
- # \b matches a word boundary. This prevents 'imp' from matching 'import'
70
- pattern = r'\b' + re.escape(blocked) + r'\b'
71
- if re.search(pattern, code):
72
- return False, f"Blocked import/function: {blocked}"
73
-
74
- # Check for dangerous patterns that are not whole words
75
- dangerous_patterns = ['exec(', 'eval(', 'open(', 'file(', '__']
76
- for pattern in dangerous_patterns:
77
- if pattern in code:
78
- return False, f"Dangerous pattern detected: {pattern}"
79
-
80
- elif language == "bash":
81
- # Use regex to find whole-word matches for blocked commands
82
- for blocked in self.blocked_bash_commands:
83
- pattern = r'\b' + re.escape(blocked) + r'\b'
84
- if re.search(pattern, code.lower()):
85
- return False, f"Blocked command: {blocked}"
86
-
87
- return True, ""
88
-
89
- def execute_python_code(self, code: str, session_id: Optional[str] = None) -> Dict[str, Any]:
90
- """Execute Python code with visualization support"""
91
- # Security check
92
- is_safe, reason = self.is_code_safe(code, "python")
93
- if not is_safe:
94
- return {
95
- "success": False,
96
- "stdout": "",
97
- "stderr": f"Security violation: {reason}",
98
- "execution_time": time.time()
99
- }
100
-
101
- # Prepare execution environment
102
- setup_code = '''
103
- import matplotlib
104
- matplotlib.use('Agg')
105
- import matplotlib.pyplot as plt
106
- import numpy as np
107
- import pandas as pd
108
- import json
109
- import math
110
- import random
111
- import base64
112
- import io
113
- from datetime import datetime, timedelta
114
-
115
- # Custom print function to capture output
116
- _output_buffer = []
117
- _original_print = print
118
- def print(*args, **kwargs):
119
- _output_buffer.append(' '.join(str(arg) for arg in args))
120
-
121
- # Function to save plots as base64
122
- def save_current_plot():
123
- if plt.get_fignums(): # Check if there are any figures
124
- buffer = io.BytesIO()
125
- plt.savefig(buffer, format='png', bbox_inches='tight', dpi=100)
126
- buffer.seek(0)
127
- plot_data = buffer.getvalue()
128
- buffer.close()
129
- return base64.b64encode(plot_data).decode()
130
- return None
131
- '''
132
-
133
- # Combine setup and user code
134
- full_code = setup_code + "\n" + code
135
-
136
- # Finalization code to capture plot and print the buffer to real stdout
137
- finalization_code = '''
138
- # Capture the plot if one exists and print it directly to stdout for the parent process
139
- _plot_data = save_current_plot()
140
- if _plot_data:
141
- _original_print('PLOT_DATA:' + _plot_data)
142
-
143
- # Print everything from the custom print buffer to stdout
144
- if _output_buffer:
145
- _original_print('\\n'.join(_output_buffer))
146
- '''
147
- full_code += "\n" + finalization_code
148
-
149
- try:
150
- # Create temporary file
151
- with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
152
- f.write(full_code)
153
- temp_file = f.name
154
-
155
- # Execute with timeout
156
- result = subprocess.run(
157
- [sys.executable, temp_file],
158
- capture_output=True,
159
- text=True,
160
- timeout=self.max_execution_time,
161
- cwd=tempfile.gettempdir()
162
- )
163
-
164
- # Process output
165
- stdout = result.stdout
166
- stderr = result.stderr
167
- plot_data = None
168
-
169
- # Extract plot data if present
170
- if 'PLOT_DATA:' in stdout:
171
- lines = stdout.split('\n')
172
- clean_lines = []
173
- for line in lines:
174
- if line.startswith('PLOT_DATA:'):
175
- plot_data = line.replace('PLOT_DATA:', '')
176
- else:
177
- clean_lines.append(line)
178
- stdout = '\n'.join(clean_lines)
179
-
180
- # Limit output length
181
- if len(stdout) > self.max_output_length:
182
- stdout = stdout[:self.max_output_length] + "\n... (output truncated)"
183
-
184
- execution_result = {
185
- "success": result.returncode == 0,
186
- "stdout": stdout.strip(),
187
- "stderr": stderr.strip() if stderr else "",
188
- "execution_time": time.time(),
189
- "return_code": result.returncode
190
- }
191
-
192
- if plot_data:
193
- execution_result["plot"] = plot_data
194
-
195
- return execution_result
196
-
197
- except subprocess.TimeoutExpired:
198
- return {
199
- "success": False,
200
- "stdout": "",
201
- "stderr": "Execution timed out (30s limit)",
202
- "execution_time": time.time()
203
- }
204
- except Exception as e:
205
- return {
206
- "success": False,
207
- "stdout": "",
208
- "stderr": str(e),
209
- "execution_time": time.time()
210
- }
211
- finally:
212
- if 'temp_file' in locals():
213
- try:
214
- os.unlink(temp_file)
215
- except:
216
- pass
217
-
218
- def execute_javascript_code(self, code: str, session_id: Optional[str] = None) -> Dict[str, Any]:
219
- """Execute JavaScript code using Node.js"""
220
- # Security check
221
- is_safe, reason = self.is_code_safe(code, "javascript")
222
- if not is_safe:
223
- return {
224
- "success": False,
225
- "stdout": "",
226
- "stderr": f"Security violation: {reason}",
227
- "execution_time": time.time()
228
- }
229
-
230
- try:
231
- with tempfile.NamedTemporaryFile(mode='w', suffix='.js', delete=False) as f:
232
- f.write(code)
233
- temp_file = f.name
234
-
235
- result = subprocess.run(
236
- ['node', temp_file],
237
- capture_output=True,
238
- text=True,
239
- timeout=self.max_execution_time
240
- )
241
-
242
- stdout = result.stdout
243
- if len(stdout) > self.max_output_length:
244
- stdout = stdout[:self.max_output_length] + "\n... (output truncated)"
245
-
246
- return {
247
- "success": result.returncode == 0,
248
- "stdout": stdout.strip(),
249
- "stderr": result.stderr.strip() if result.stderr else "",
250
- "execution_time": time.time(),
251
- "return_code": result.returncode
252
- }
253
-
254
- except subprocess.TimeoutExpired:
255
- return {
256
- "success": False,
257
- "stdout": "",
258
- "stderr": "Execution timed out (30s limit)",
259
- "execution_time": time.time()
260
- }
261
- except Exception as e:
262
- return {
263
- "success": False,
264
- "stdout": "",
265
- "stderr": str(e),
266
- "execution_time": time.time()
267
- }
268
- finally:
269
- if 'temp_file' in locals():
270
- try:
271
- os.unlink(temp_file)
272
- except:
273
- pass
274
-
275
- def execute_bash_command(self, command: str, session_id: Optional[str] = None) -> Dict[str, Any]:
276
- """Execute bash commands with security restrictions"""
277
- # Security check
278
- is_safe, reason = self.is_code_safe(command, "bash")
279
- if not is_safe:
280
- return {
281
- "success": False,
282
- "stdout": "",
283
- "stderr": f"Security violation: {reason}",
284
- "execution_time": time.time()
285
- }
286
-
287
- try:
288
- result = subprocess.run(
289
- command,
290
- shell=True,
291
- capture_output=True,
292
- text=True,
293
- timeout=self.max_execution_time,
294
- cwd=tempfile.gettempdir()
295
- )
296
-
297
- stdout = result.stdout
298
- if len(stdout) > self.max_output_length:
299
- stdout = stdout[:self.max_output_length] + "\n... (output truncated)"
300
-
301
- return {
302
- "success": result.returncode == 0,
303
- "stdout": stdout.strip(),
304
- "stderr": result.stderr.strip() if result.stderr else "",
305
- "execution_time": time.time(),
306
- "return_code": result.returncode
307
- }
308
-
309
- except subprocess.TimeoutExpired:
310
- return {
311
- "success": False,
312
- "stdout": "",
313
- "stderr": "Command timed out (30s limit)",
314
- "execution_time": time.time()
315
- }
316
- except Exception as e:
317
- return {
318
- "success": False,
319
- "stdout": "",
320
- "stderr": str(e),
321
- "execution_time": time.time()
322
- }
323
-
324
- def execute_code(self, code: str, language: str = "python", session_id: Optional[str] = None) -> str:
325
- """Main execution function - returns JSON for MCP compatibility"""
326
- # Cleanup old sessions periodically
327
- if len(self.sessions) > 10:
328
- self.cleanup_old_sessions()
329
-
330
- if language not in self.allowed_languages:
331
- return json.dumps({
332
- "success": False,
333
- "error": f"Language '{language}' not supported. Allowed: {', '.join(self.allowed_languages)}"
334
- })
335
-
336
- # Create session if needed
337
- if session_id and session_id not in self.sessions:
338
- session_id = self.create_session()
339
- elif not session_id:
340
- session_id = self.create_session()
341
-
342
- # Execute based on language
343
- if language == "python":
344
- result = self.execute_python_code(code, session_id)
345
- elif language == "javascript":
346
- result = self.execute_javascript_code(code, session_id)
347
- elif language == "bash":
348
- result = self.execute_bash_command(code, session_id)
349
- else:
350
- result = {
351
- "success": False,
352
- "error": f"Execution handler for {language} not implemented"
353
- }
354
-
355
- # Store in session history
356
- if session_id in self.sessions:
357
- self.sessions[session_id]['history'].append({
358
- 'code': code,
359
- 'language': language,
360
- 'result': result,
361
- 'timestamp': time.time()
362
- })
363
-
364
- result['session_id'] = session_id
365
- return json.dumps(result, indent=2)
366
-
367
- # Global executor instance
368
- executor = SecureCodeExecutor()
369
-
370
- # MCP Functions
371
- def execute_python_code(code: str, session_id: str = None) -> str:
372
- """
373
- Execute Python code safely with visualization support.
374
-
375
- Args:
376
- code (str): Python code to execute
377
- session_id (str, optional): Session ID for persistent context
378
-
379
- Returns:
380
- str: JSON string with execution results
381
- """
382
- return executor.execute_code(code, "python", session_id)
383
-
384
- def execute_javascript_code(code: str, session_id: str = None) -> str:
385
- """
386
- Execute JavaScript code using Node.js.
387
-
388
- Args:
389
- code (str): JavaScript code to execute
390
- session_id (str, optional): Session ID for persistent context
391
-
392
- Returns:
393
- str: JSON string with execution results
394
- """
395
- return executor.execute_code(code, "javascript", session_id)
396
-
397
- def execute_bash_command(command: str, session_id: str = None) -> str:
398
- """
399
- Execute bash commands with security restrictions.
400
-
401
- Args:
402
- command (str): Bash command to execute
403
- session_id (str, optional): Session ID for persistent context
404
-
405
- Returns:
406
- str: JSON string with execution results
407
- """
408
- return executor.execute_code(command, "bash", session_id)
409
-
410
- def create_execution_session() -> str:
411
- """
412
- Create a new execution session for maintaining state.
413
-
414
- Returns:
415
- str: JSON string containing new session ID
416
- """
417
- session_id = executor.create_session()
418
- return json.dumps({"session_id": session_id, "created_at": time.time()})
419
-
420
- def list_execution_sessions() -> str:
421
- """
422
- List all active execution sessions.
423
-
424
- Returns:
425
- str: JSON string containing session information
426
- """
427
- return json.dumps({
428
- "sessions": list(executor.sessions.keys()),
429
- "count": len(executor.sessions),
430
- "timestamp": time.time()
431
- })
432
-
433
- def get_execution_history(session_id: str) -> str:
434
- """
435
- Get execution history for a specific session.
436
-
437
- Args:
438
- session_id (str): Session ID to get history for
439
-
440
- Returns:
441
- str: JSON string containing execution history
442
- """
443
- if session_id not in executor.sessions:
444
- return json.dumps({"error": "Session not found"})
445
-
446
- return json.dumps({
447
- "session_id": session_id,
448
- "history": executor.sessions[session_id]['history'],
449
- "created_at": executor.sessions[session_id]['created_at']
450
- })
451
-
452
- def get_system_info() -> str:
453
- """
454
- Get system information and available packages.
455
-
456
- Returns:
457
- str: JSON string containing system information
458
- """
459
- return json.dumps({
460
- "python_version": sys.version,
461
- "available_packages": [
462
- "numpy", "pandas", "matplotlib", "json", "math",
463
- "random", "datetime", "base64", "io"
464
- ],
465
- "execution_limits": {
466
- "max_time": executor.max_execution_time,
467
- "max_output": executor.max_output_length
468
- },
469
- "supported_languages": executor.allowed_languages
470
- })
471
-
472
- # Gradio Interface
473
  def gradio_execute_code(code: str, language: str, session_id: str = ""):
474
- """Gradio interface for code execution"""
475
  if not session_id:
476
  session_id = None
477
 
@@ -479,21 +8,35 @@ def gradio_execute_code(code: str, language: str, session_id: str = ""):
479
  result = json.loads(result_json)
480
 
481
  output = ""
 
 
482
  if result.get("success"):
483
  if result.get("stdout"):
484
  output += f"Output:\n{result['stdout']}\n\n"
485
  if result.get("stderr"):
486
  output += f"Warnings:\n{result['stderr']}\n\n"
 
 
487
  if result.get("plot"):
488
- output += f"Plot generated (base64): {result['plot'][:100]}...\n\n"
 
 
 
 
 
 
 
 
 
 
489
  else:
490
- output += f"Error:\n{result.get('stderr', result.get('error', 'Unknown error'))}\n\n"
491
 
492
- output += f"Session ID: {result.get('session_id', 'N/A')}"
493
 
494
- return output
495
-
496
- # Create Gradio interface
497
  with gr.Blocks(title="LibreChat Code Interpreter") as demo:
498
  gr.Markdown("# LibreChat Code Interpreter")
499
  gr.Markdown("Execute Python, JavaScript, and Bash code safely through MCP integration.")
@@ -518,12 +61,18 @@ with gr.Blocks(title="LibreChat Code Interpreter") as demo:
518
 
519
  with gr.Column():
520
  output_display = gr.Textbox(
521
- lines=15,
522
  label="Execution Result",
523
  interactive=False
524
  )
 
 
 
 
 
 
525
 
526
- # Examples
527
  gr.Markdown("## Examples")
528
 
529
  example_python = gr.Code("""
@@ -574,17 +123,9 @@ console.log('\\nResult:');
574
  console.log(JSON.stringify(result, null, 2));
575
  """, language="javascript", label="JavaScript Example")
576
 
 
577
  execute_btn.click(
578
  fn=gradio_execute_code,
579
  inputs=[code_input, language_dropdown, session_input],
580
- outputs=[output_display]
581
- )
582
-
583
- if __name__ == "__main__":
584
- # Launch with MCP server enabled
585
- demo.launch(
586
- mcp_server=True,
587
- share=False,
588
- server_name="0.0.0.0",
589
- server_port=7860
590
  )
 
1
+ # Replace the gradio_execute_code function with this improved version
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  def gradio_execute_code(code: str, language: str, session_id: str = ""):
3
+ """Gradio interface for code execution with proper image handling"""
4
  if not session_id:
5
  session_id = None
6
 
 
8
  result = json.loads(result_json)
9
 
10
  output = ""
11
+ plot_image = None
12
+
13
  if result.get("success"):
14
  if result.get("stdout"):
15
  output += f"Output:\n{result['stdout']}\n\n"
16
  if result.get("stderr"):
17
  output += f"Warnings:\n{result['stderr']}\n\n"
18
+
19
+ # Handle plot data properly
20
  if result.get("plot"):
21
+ import base64
22
+ from PIL import Image
23
+ import io
24
+
25
+ try:
26
+ # Decode base64 to image
27
+ plot_data = base64.b64decode(result["plot"])
28
+ plot_image = Image.open(io.BytesIO(plot_data))
29
+ output += f"📊 Plot generated successfully!\n\n"
30
+ except Exception as e:
31
+ output += f"⚠️ Plot generated but couldn't display: {str(e)}\n\n"
32
  else:
33
+ output += f"Error:\n{result.get('stderr', result.get('error', 'Unknown error'))}\n\n"
34
 
35
+ output += f"🔑 Session ID: {result.get('session_id', 'N/A')}"
36
 
37
+ return output, plot_image
38
+
39
+ # Replace the Gradio interface section with this improved version
40
  with gr.Blocks(title="LibreChat Code Interpreter") as demo:
41
  gr.Markdown("# LibreChat Code Interpreter")
42
  gr.Markdown("Execute Python, JavaScript, and Bash code safely through MCP integration.")
 
61
 
62
  with gr.Column():
63
  output_display = gr.Textbox(
64
+ lines=10,
65
  label="Execution Result",
66
  interactive=False
67
  )
68
+ # Add image output for plots
69
+ plot_display = gr.Image(
70
+ label="Generated Plot",
71
+ visible=True,
72
+ type="pil"
73
+ )
74
 
75
+ # Examples section remains the same...
76
  gr.Markdown("## Examples")
77
 
78
  example_python = gr.Code("""
 
123
  console.log(JSON.stringify(result, null, 2));
124
  """, language="javascript", label="JavaScript Example")
125
 
126
+ # Updated click handler to return both outputs
127
  execute_btn.click(
128
  fn=gradio_execute_code,
129
  inputs=[code_input, language_dropdown, session_input],
130
+ outputs=[output_display, plot_display] # Now returns both text and image
 
 
 
 
 
 
 
 
 
131
  )