File size: 32,695 Bytes
744e5e2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
"""
Modal Sandbox wrapper to provide E2B-compatible interface for the Jupyter Agent.
Simplified implementation using Modal's native API.
"""

import modal
import datetime
from typing import Optional, Dict, List
import json
import logging
import time

logger = logging.getLogger(__name__)


class ModalResult:
    """Mock E2B result structure for displaying outputs like plots"""
    
    def __init__(self, text: str = "", html: str = "", png: str = "", svg: str = "", 
                 jpeg: str = "", pdf: str = "", latex: str = "", json: str = "",
                 javascript: str = "", is_main_result: bool = True):
        self.text = text
        self.html = html
        self.png = png
        self.svg = svg
        self.jpeg = jpeg
        self.pdf = pdf
        self.latex = latex
        self.json = json
        self.javascript = javascript
        self.is_main_result = is_main_result

class ModalExecution:
    """Mock E2B execution result to maintain compatibility with existing code"""
    
    def __init__(self, stdout: str = "", stderr: str = "", error: Optional[Dict] = None, results: List[ModalResult] = None):
        self.logs = ModalLogs(stdout, stderr)
        self.error = ModalError(error) if error else None
        self.results = results or []
        self.execution_count = 1

class ModalLogs:
    """Mock E2B logs structure"""
    
    def __init__(self, stdout: str = "", stderr: str = ""):
        self.stdout = [stdout] if stdout else []
        self.stderr = [stderr] if stderr else []

class ModalError:
    """Mock E2B error structure"""
    
    def __init__(self, error_data: Dict):
        self.name = error_data.get('name', 'Error')
        self.value = error_data.get('value', 'Unknown error')
        self.traceback = error_data.get('traceback', f"{self.name}: {self.value}")

class ModalFiles:
    """Simplified Modal files interface using native Modal Sandbox API"""
    
    def __init__(self, modal_sandbox):
        self.modal_sandbox = modal_sandbox  # ModalSandbox wrapper
        self.max_file_size = 100 * 1024 * 1024  # 100MB limit
    
    @property
    def _sandbox(self):
        """Get the actual Modal sandbox instance"""
        return self.modal_sandbox._sandbox
    
    def write(self, path: str, content):
        """Write file to Modal sandbox using native Modal API"""
        try:
            # Handle file-like objects
            if hasattr(content, 'read'):
                file_content = content.read()
                # Reset file pointer if possible
                if hasattr(content, 'seek'):
                    content.seek(0)
            else:
                file_content = content
            
            # Check file size for bytes content
            content_size = len(file_content) if isinstance(file_content, (bytes, str)) else 0
            if content_size > self.max_file_size:
                raise ValueError(f"File size ({content_size} bytes) exceeds maximum allowed size ({self.max_file_size} bytes)")
            
            # Use Modal's native file API
            if isinstance(file_content, bytes):
                # Write binary content
                with self._sandbox.open(path, "wb") as f:
                    f.write(file_content)
            else:
                # Write text content
                with self._sandbox.open(path, "w") as f:
                    f.write(str(file_content))
                
            logger.debug(f"Successfully wrote file {path} ({content_size} bytes) using Modal native API")
            
        except Exception as e:
            logger.error(f"Failed to write file {path}: {str(e)}")
            raise RuntimeError(f"Could not write file {path}: {str(e)}")
    
    def read(self, path: str, mode: str = "r"):
        """Read file from Modal sandbox using native API"""
        try:
            with self._sandbox.open(path, mode) as f:
                return f.read()
        except Exception as e:
            logger.error(f"Failed to read file {path}: {str(e)}")
            raise
    
    def exists(self, path: str) -> bool:
        """Check if file exists in Modal sandbox"""
        try:
            # Try to open the file to check existence
            with self._sandbox.open(path, "r"):
                pass
            return True
        except Exception:
            return False
    
    def list_files(self, directory: str = ".") -> List[str]:
        """List files in directory using Modal's native ls method"""
        try:
            return self._sandbox.ls(directory)
        except Exception as e:
            logger.error(f"Failed to list files in {directory}: {str(e)}")
            return []
    
    def verify_file_upload(self, path: str, expected_size: Optional[int] = None) -> bool:
        """Verify that a file was uploaded correctly"""
        try:
            if not self.exists(path):
                logger.error(f"File {path} does not exist after upload")
                return False
            
            # Check file size if expected size is provided
            if expected_size is not None:
                # Use Modal's exec to get file size
                result = self._sandbox.exec("wc", "-c", path)
                result.wait()
                
                if result.returncode == 0:
                    output = result.stdout.read().strip()
                    actual_size = int(output.split()[0])
                    if actual_size != expected_size:
                        logger.error(f"File {path} size mismatch: expected {expected_size}, got {actual_size}")
                        return False
                    else:
                        logger.debug(f"File {path} size verified: {actual_size} bytes")
                else:
                    logger.warning(f"Could not verify file size for {path}")
            
            logger.debug(f"File {path} upload verification successful")
            return True
            
        except Exception as e:
            logger.error(f"Failed to verify file upload {path}: {str(e)}")
            return False

class ModalSandboxInfo:
    """Mock E2B sandbox info for countdown timer"""
    
    def __init__(self, timeout_seconds: int = 300):
        self.started_at = datetime.datetime.now(datetime.timezone.utc)
        self.end_at = self.started_at + datetime.timedelta(seconds=timeout_seconds)

class ModalSandbox:
    """Modal sandbox wrapper that provides E2B-compatible interface"""
    
    def __init__(self, gpu_config: str = "cpu", cpu_cores: float = 2.0, memory_mb: int = 8192, 
                 timeout: int = 300, environment_vars: Dict[str, str] = None):
        """
        Initialize Modal sandbox with hardware configuration
        
        Args:
            gpu_config: GPU configuration (e.g., "cpu", "T4", "A100-40GB", "H100")
            cpu_cores: Number of CPU cores
            memory_mb: Memory in MB
            timeout: Timeout in seconds
            environment_vars: Environment variables to set
        """
        self.gpu_config = gpu_config
        self.cpu_cores = cpu_cores
        self.memory_mb = memory_mb
        self.timeout = timeout
        self.environment_vars = environment_vars or {}
        self.files = ModalFiles(self)
        self._sandbox = None
        self._app = None
        self._sandbox_info = ModalSandboxInfo(timeout)
        self._persistent_session = None  # For maintaining state across executions
        
        # Define package lists for different hardware configurations
        CPU_PACKAGES = [
            "jupyter-server", "ipykernel", "ipython", "orjson", "pandas", 
            "matplotlib", "pillow", "numpy", "scipy", "scikit-learn", 
            "seaborn", "plotly", "requests", "beautifulsoup4", "opencv-python", 
            "nltk", "textblob", "librosa>=0.10.0", "soundfile", "sympy", "xarray"
        ]
        
        GPU_PACKAGES = [
            "jupyter-server", "ipykernel", "ipython", "orjson", "pandas", 
            "matplotlib", "pillow", "numpy", "scipy", "scikit-learn", 
            "seaborn", "plotly", "requests", "beautifulsoup4", "opencv-python", 
            "nltk", "textblob", "librosa>=0.10.0", "soundfile", "sympy", "xarray",
            # GPU-specific ML/AI packages
            "torch", "transformers", "datasets", "bitsandbytes", "hf_transfer", 
            "peft", "trl", "accelerate", "xformers", "wandb", "deepspeed", 
            "pyyaml", "packaging", "rouge_score", "bert_score", "jiwer", 
            "tqdm", "pyarrow", "sentencepiece", "protobuf", "huggingface_hub"
        ]
        
        # Store package lists for system prompt
        self.available_packages = GPU_PACKAGES if gpu_config != "cpu" else CPU_PACKAGES
        
        # Create appropriate image based on hardware configuration
        if gpu_config == "cpu" or gpu_config == "CPU-only":
            self.base_image = self._create_cpu_image(CPU_PACKAGES)
        else:
            self.base_image = self._create_gpu_image(GPU_PACKAGES)
        
        self._setup_modal()
        logger.info(f"Initialized Modal sandbox with {gpu_config} GPU, {cpu_cores} CPU cores, {memory_mb}MB RAM")
    
    def _create_cpu_image(self, packages):
        """Create CPU-optimized image with basic data science packages"""
        packages_string = " ".join(packages)
        return (modal.Image.debian_slim()
                .apt_install("git", "build-essential")
                .run_commands("pip install --upgrade pip")
                .run_commands("pip install uv")
                .run_commands("uv pip install 'numba>=0.58.0' --system")  # Ensure compatible numba version
                .run_commands(f"uv pip install {packages_string} --system"))
    
    def _create_gpu_image(self, packages):
        """Create GPU-optimized image with ML/AI packages including PyTorch and Transformers"""
        # CUDA Configuration for SGLang
        CUDA_VERSION = "12.8.1"
        CUDA_FLAVOR = "devel"
        CUDA_OS = "ubuntu24.04"
        CUDA_TAG = f"{CUDA_VERSION}-{CUDA_FLAVOR}-{CUDA_OS}"
        
        # Base packages that don't require special handling
        base_packages = [pkg for pkg in packages if pkg not in [
            "torch", "transformers", "bitsandbytes", "accelerate", "xformers", 
            "peft", "trl", "unsloth", "deepspeed"
        ]]
        base_packages_string = " ".join(base_packages)
        
        return (modal.Image.from_registry(f"nvidia/cuda:{CUDA_TAG}", add_python="3.12")
                .env({"DEBIAN_FRONTEND": "noninteractive", "TZ": "UTC"})
                .run_commands("ln -fs /usr/share/zoneinfo/UTC /etc/localtime")
                .apt_install("git", "build-essential")
                .run_commands("pip install --upgrade pip")
                .run_commands("pip install uv")
                .run_commands("uv pip install 'numba>=0.58.0' --system")  # Ensure compatible numba version
                .run_commands("uv pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu124 --system")
                .run_commands(f"uv pip install {base_packages_string} --system")
                .env({"HF_HUB_ENABLE_HF_TRANSFER": "1"}))
    
    def _setup_modal(self):
        """Setup Modal app and sandbox configuration"""
        try:
            # Initialize Modal app using lookup to create if missing
            self._app = modal.App.lookup("jupyter-agent", create_if_missing=True)
            
            # Configure hardware based on user selection
            sandbox_kwargs = {
                "image": self.base_image,
                "timeout": self.timeout,
                "cpu": self.cpu_cores,
                "memory": self.memory_mb,
                "app": self._app
            }
            
            # Add GPU configuration if not CPU-only
            if self.gpu_config != "cpu" and self.gpu_config != "CPU-only":
                if self.gpu_config == "T4":
                    sandbox_kwargs["gpu"] = modal.gpu.T4()
                elif self.gpu_config == "L4":
                    sandbox_kwargs["gpu"] = modal.gpu.L4()
                elif self.gpu_config == "A100-40GB":
                    sandbox_kwargs["gpu"] = modal.gpu.A100(size="40GB")
                elif self.gpu_config == "A100-80GB":
                    sandbox_kwargs["gpu"] = modal.gpu.A100(size="80GB")
                elif self.gpu_config == "H100":
                    sandbox_kwargs["gpu"] = modal.gpu.H100()
                else:
                    print(f"Warning: Unknown GPU config {self.gpu_config}, falling back to CPU")
            
            # Add environment variables
            if self.environment_vars:
                sandbox_kwargs["secrets"] = [
                    modal.Secret.from_dict(self.environment_vars)
                ]
            
            # Create sandbox
            self._sandbox = modal.Sandbox.create(**sandbox_kwargs)
            
        except Exception as e:
            print(f"Error setting up Modal sandbox: {e}")
            raise
    
    def _initialize_persistent_session(self):
        """Initialize a persistent Python session for stateful execution using file-based communication"""
        if self._persistent_session is not None:
            return  # Session already exists
        
        try:
            logger.debug("Initializing persistent Python session with file-based communication")
            
            # Create a persistent Python script that monitors for command files
            session_script = '''
import sys
import json
import traceback
import base64
import io
import time
import os
import matplotlib
matplotlib.use('Agg')  # Set backend before importing pyplot
import matplotlib.pyplot as plt

# Global namespace to maintain state - includes built-ins for better compatibility
_global_namespace = {
    '__builtins__': __builtins__,
    '__name__': '__main__',
    '__doc__': None,
    '__package__': None
}

# Store original show function and setup plot capture
_original_show = plt.show
_captured_figures = []

def _capture_show(*args, **kwargs):
    """Custom show function that captures figures as base64"""
    global _captured_figures
    try:
        for fig_num in plt.get_fignums():
            fig = plt.figure(fig_num)
            buf = io.BytesIO()
            fig.savefig(buf, format='png', bbox_inches='tight', dpi=100)
            buf.seek(0)
            img_base64 = base64.b64encode(buf.getvalue()).decode('utf-8')
            _captured_figures.append(img_base64)
            buf.close()
            plt.close(fig)
    except Exception as e:
        print(f"Error capturing plot: {e}", file=sys.stderr)

# Replace plt.show with our capture function
plt.show = _capture_show

# Signal that session is ready
with open("/tmp/session_ready", "w") as f:
    f.write("READY")

print("Persistent Python session started", flush=True)

# Process commands by monitoring for command files
while True:
    try:
        if os.path.exists("/tmp/execute_command"):
            # Read and execute command
            with open("/tmp/execute_command", "r") as f:
                content = f.read().strip()
                if not content:
                    continue  # Skip empty files
                try:
                    command = json.loads(content)
                except json.JSONDecodeError:
                    print(f"Invalid JSON in command file: {content[:100]}...", file=sys.stderr)
                    continue  # Skip malformed JSON
            
            # Remove command file
            os.remove("/tmp/execute_command")
            
            if command.get("action") == "execute":
                code = command.get("code", "")
                _captured_figures = []  # Reset for this execution
                
                try:
                    # Check if code contains shell commands (lines starting with !)
                    lines = code.strip().split('\\n')
                    shell_commands = []
                    python_code_lines = []
                    
                    for line in lines:
                        stripped_line = line.strip()
                        if stripped_line.startswith('!'):
                            # This is a shell command
                            shell_cmd = stripped_line[1:].strip()  # Remove the !
                            shell_commands.append(shell_cmd)
                        else:
                            # This is Python code
                            python_code_lines.append(line)
                    
                    stdout_parts = []
                    stderr_parts = []
                    
                    # Execute shell commands first
                    for shell_cmd in shell_commands:
                        try:
                            import subprocess
                            result = subprocess.run(
                                shell_cmd,
                                shell=True,
                                capture_output=True,
                                text=True,
                                timeout=60  # 60 second timeout for shell commands
                            )
                            
                            if result.stdout:
                                stdout_parts.append(f"$ {shell_cmd}")
                                stdout_parts.append(result.stdout.rstrip())
                            
                            if result.stderr:
                                stderr_parts.append(f"$ {shell_cmd}")
                                stderr_parts.append(result.stderr.rstrip())
                            
                            # If command failed, add error info
                            if result.returncode != 0:
                                stderr_parts.append(f"Command exited with code {result.returncode}")
                                
                        except subprocess.TimeoutExpired:
                            stderr_parts.append(f"$ {shell_cmd}")
                            stderr_parts.append("Command timed out after 60 seconds")
                        except Exception as e:
                            stderr_parts.append(f"$ {shell_cmd}")
                            stderr_parts.append(f"Error executing shell command: {str(e)}")
                    
                    # Execute Python code if present
                    python_stdout = ""
                    if python_code_lines and any(line.strip() for line in python_code_lines):
                        python_code = '\\n'.join(python_code_lines)
                        
                        # Capture stdout during Python execution
                        import io
                        from contextlib import redirect_stdout
                        
                        stdout_buffer = io.StringIO()
                        
                        with redirect_stdout(stdout_buffer):
                            # Execute code in the persistent namespace
                            exec(python_code, _global_namespace)
                        
                        python_stdout = stdout_buffer.getvalue()
                    
                    # Combine all stdout
                    all_stdout_parts = stdout_parts.copy()
                    if python_stdout:
                        all_stdout_parts.append(python_stdout.rstrip())
                    
                    stdout_output = '\\n'.join(all_stdout_parts) if all_stdout_parts else ""
                    stderr_output = '\\n'.join(stderr_parts) if stderr_parts else ""
                    
                    # Send results back
                    result = {
                        "status": "success",
                        "stdout": stdout_output,
                        "stderr": stderr_output,
                        "plots": _captured_figures.copy()
                    }
                    
                    with open("/tmp/execute_result", "w") as f:
                        f.write(json.dumps(result))
                    
                except Exception as e:
                    error_result = {
                        "status": "error",
                        "error": {
                            "name": type(e).__name__,
                            "value": str(e),
                            "traceback": traceback.format_exc()
                        }
                    }
                    
                    with open("/tmp/execute_result", "w") as f:
                        f.write(json.dumps(error_result))
            
            elif command.get("action") == "terminate":
                break
        
        else:
            # Sleep briefly to avoid busy waiting
            time.sleep(0.1)
            
    except Exception as e:
        print(f"Session error: {e}", file=sys.stderr)
        # Write error to result file
        error_result = {
            "status": "error",
            "error": {
                "name": type(e).__name__,
                "value": str(e),
                "traceback": traceback.format_exc()
            }
        }
        with open("/tmp/execute_result", "w") as f:
            f.write(json.dumps(error_result))
'''
            
            # Start the persistent Python session (no stdin needed)
            self._persistent_session = self._sandbox.exec(
                "python3", "-c", session_script,
                timeout=None  # No timeout for persistent session
            )
            
            # Wait for the session to be ready by checking for the ready file
            max_wait = 10  # Wait up to 10 seconds
            for _ in range(max_wait * 10):  # Check every 0.1 seconds
                try:
                    with self._sandbox.open("/tmp/session_ready", "r") as f:
                        if f.read().strip() == "READY":
                            logger.info("Persistent Python session initialized successfully")
                            return
                except Exception:
                    pass
                time.sleep(0.1)
            
            raise RuntimeError("Failed to initialize persistent session: timeout waiting for ready signal")
            
        except Exception as e:
            logger.error(f"Failed to initialize persistent session: {e}")
            self._persistent_session = None
            raise
    
    def run_code(self, code: str, on_stdout=None) -> ModalExecution:
        """
        Execute Python code or shell commands in persistent Modal sandbox session using file-based communication
        
        Args:
            code: Python code to execute (lines starting with '!' are treated as shell commands)
            on_stdout: Callback for stdout (for compatibility, not fully implemented)
            
        Returns:
            ModalExecution object compatible with E2B execution results
        """
        try:
            if not self._sandbox:
                raise RuntimeError("Sandbox not initialized")
            
            # Initialize persistent session if not already done
            if self._persistent_session is None:
                self._initialize_persistent_session()
            
            logger.debug(f"Executing code in persistent session ({len(code)} chars)")
            
            # Clean up any existing command/result files
            try:
                self._sandbox.exec("rm", "-f", "/tmp/execute_command", "/tmp/execute_result").wait()
            except Exception:
                pass  # Ignore cleanup errors
            
            # Send execution command via file
            command = {
                "action": "execute",
                "code": code
            }
            
            with self._sandbox.open("/tmp/execute_command", "w") as f:
                f.write(json.dumps(command))
            
            # Small delay to ensure file is fully written
            time.sleep(0.01)
            
            # Wait for result file to appear
            max_wait = 60  # Wait up to 60 seconds for code execution
            result = None
            
            for _ in range(max_wait * 10):  # Check every 0.1 seconds
                try:
                    with self._sandbox.open("/tmp/execute_result", "r") as f:
                        result_json = f.read().strip()
                        if result_json:  # Make sure file has content
                            try:
                                result = json.loads(result_json)
                                break
                            except json.JSONDecodeError as e:
                                logger.debug(f"Invalid JSON in result file: {e}")
                                continue  # Try again
                except Exception:
                    pass
                time.sleep(0.1)
            
            if result is None:
                raise RuntimeError("Timeout waiting for code execution result")
            
            # Clean up result file
            try:
                self._sandbox.exec("rm", "-f", "/tmp/execute_result").wait()
            except Exception:
                pass
            
            if result["status"] == "success":
                # Create results for plots only - don't duplicate stdout as execute_result
                results = []
                
                # Add plots
                for i, base64_img in enumerate(result.get("plots", [])):
                    results.append(ModalResult(
                        png=base64_img,
                        is_main_result=(i == 0)  # First plot is main result
                    ))
                
                # Get stdout and stderr output for logs
                stdout_output = result.get("stdout", "")
                stderr_output = result.get("stderr", "")
                
                # Return execution with stdout/stderr in logs, plots in results
                # Don't add stdout to results to avoid duplication
                return ModalExecution(stdout=stdout_output, stderr=stderr_output, error=None, results=results)
            
            elif result["status"] == "error":
                # Execution had an error
                error_info = result["error"]
                error_data = {
                    "name": error_info["name"],
                    "value": error_info["value"], 
                    "traceback": error_info["traceback"]
                }
                return ModalExecution(stdout="", stderr="", error=error_data, results=[])
            
            else:
                raise RuntimeError(f"Unknown status from persistent session: {result['status']}")
            
        except Exception as e:
            # Handle session errors and other exceptions
            logger.error(f"Error executing code in persistent session: {str(e)}")
            
            # Reset persistent session on error
            if self._persistent_session:
                try:
                    self._persistent_session.terminate()
                except Exception:
                    pass
                self._persistent_session = None
            
            error_data = {
                "name": type(e).__name__,
                "value": str(e),
                "traceback": f"Traceback: {type(e).__name__}: {str(e)}"
            }
            return ModalExecution(error=error_data)
    
    def run_shell(self, command: str, timeout: int =60) -> ModalExecution:
        """
        Execute raw shell commands directly in the sandbox without Python wrapper
        
        Args:
            command: Shell command to execute
            timeout: Timeout in seconds (default 60)
            
        Returns:
            ModalExecution object with shell output
        """
        try:
            if not self._sandbox:
                raise RuntimeError("Sandbox not initialized")
                
            logger.debug(f"Executing raw shell command: {command}")
            
            # Use Modal's exec to run shell command directly
            # Split command into parts for exec (simple approach for common commands)
            if ' ' in command:
                # For complex commands, use sh -c
                result = self._sandbox.exec("sh", "-c", command, timeout=timeout)
            else:
                # For simple commands, run directly
                result = self._sandbox.exec(command, timeout=timeout)
            
            # Wait for completion
            result.wait()
            
            # Get output
            stdout_output = ""
            stderr_output = ""
            
            try:
                stdout_output = result.stdout.read() if result.stdout else ""
            except Exception:
                pass
                
            try:
                stderr_output = result.stderr.read() if result.stderr else ""
            except Exception:
                pass
            
            # Check for errors based on return code
            error_data = None
            if result.returncode != 0:
                error_data = {
                    "name": "ShellCommandError",
                    "value": f"Command '{command}' exited with code {result.returncode}",
                    "traceback": f"Command: {command}\nExit Code: {result.returncode}\nSTDERR: {stderr_output}"
                }
            
            logger.debug(f"Shell command completed with exit code: {result.returncode}")
            
            return ModalExecution(
                stdout=stdout_output,
                stderr=stderr_output,
                error=error_data,
                results=[]
            )
            
        except Exception as e:
            logger.error(f"Error executing shell command '{command}': {str(e)}")
            
            # Return error execution
            error_data = {
                "name": type(e).__name__,
                "value": str(e),
                "traceback": f"Shell command failed: {command}\nError: {str(e)}"
            }
            
            return ModalExecution(
                stdout="",
                stderr="",
                error=error_data,
                results=[]
            )

    def get_info(self) -> ModalSandboxInfo:
        """Get sandbox info for countdown timer"""
        return self._sandbox_info
    
    def kill(self):
        """Terminate the sandbox and persistent session"""
        try:
            # Terminate persistent session first
            if self._persistent_session:
                try:
                    # Send terminate command via file
                    terminate_command = {"action": "terminate"}
                    with self._sandbox.open("/tmp/execute_command", "w") as f:
                        f.write(json.dumps(terminate_command))
                except Exception:
                    pass  # Ignore errors during graceful shutdown
                
                try:
                    self._persistent_session.terminate()
                except Exception:
                    pass  # Ignore errors during forced termination
                
                self._persistent_session = None
                logger.info("Persistent session terminated")
            
            # Terminate sandbox
            if self._sandbox:
                self._sandbox.terminate()
                self._sandbox = None
                logger.info("Modal sandbox terminated")
                
        except Exception as e:
            logger.error(f"Error terminating Modal sandbox: {e}")
    
    def __del__(self):
        """Cleanup on deletion"""
        self.kill()


def create_modal_sandbox(gpu_config: str = "cpu", gpu_count: int = 1, cpu_cores: float = 2.0, 
                        memory_gb: float = 8.0, timeout: int = 300, 
                        environment_vars: Dict[str, str] = None) -> ModalSandbox:
    """
    Factory function to create Modal sandbox with specified configuration
    
    Args:
        gpu_config: GPU type ("cpu", "T4", "L4", "A100-40GB", "A100-80GB", "H100")
        gpu_count: Number of GPUs (for future implementation)
        cpu_cores: Number of CPU cores
        memory_gb: Memory in GB
        timeout: Timeout in seconds
        environment_vars: Environment variables
        
    Returns:
        ModalSandbox instance
    """
    memory_mb = int(memory_gb * 1024)
    
    # For multi-GPU support (future implementation)
    if gpu_count > 1:
        print(f"Warning: Multi-GPU ({gpu_count}) not yet implemented, using single GPU")
    
    return ModalSandbox(
        gpu_config=gpu_config,
        cpu_cores=cpu_cores,
        memory_mb=memory_mb,
        timeout=timeout,
        environment_vars=environment_vars
    )