| 1 | import asyncio
|
| 2 | from typing import Any
|
| 3 |
|
| 4 | from pydantic import BaseModel
|
| 5 | from swerex.deployment.modal import ModalDeployment
|
| 6 | from swerex.runtime.abstract import Command as RexCommand
|
| 7 |
|
| 8 |
|
| 9 | class SwerexModalEnvironmentConfig(BaseModel):
|
| 10 | image: str
|
| 11 | """Image to use for the deployment. Can be:
|
| 12 | - Dockerhub image name (e.g. `python:3.11-slim`)
|
| 13 | - ECR image name (e.g. `123456789012.dkr.ecr.us-east-1.amazonaws.com/my-image:tag`)
|
| 14 | - Path to a Dockerfile
|
| 15 | """
|
| 16 | cwd: str = "/"
|
| 17 | """Working directory in which to execute commands."""
|
| 18 | timeout: int = 30
|
| 19 | """Timeout for executing commands in the container."""
|
| 20 | env: dict[str, str] = {}
|
| 21 | """Environment variables to set when executing commands."""
|
| 22 | startup_timeout: float = 60.0
|
| 23 | """The time to wait for the runtime to start."""
|
| 24 | runtime_timeout: float = 3600.0
|
| 25 | """The runtime timeout (how long the Modal sandbox can stay alive)."""
|
| 26 | deployment_timeout: float = 3600.0
|
| 27 | """The deployment timeout."""
|
| 28 | install_pipx: bool = True
|
| 29 | """Whether to install pipx in the container (required for swe-rex runtime)."""
|
| 30 | modal_sandbox_kwargs: dict[str, Any] = {}
|
| 31 | """Additional arguments to pass to `modal.Sandbox.create`."""
|
| 32 |
|
| 33 |
|
| 34 | class SwerexModalEnvironment:
|
| 35 | def __init__(self, **kwargs):
|
| 36 | """This class executes bash commands in a Modal sandbox using SWE-ReX for remote execution.
|
| 37 |
|
| 38 | Modal (https://modal.com) provides serverless cloud compute that can be used to run
|
| 39 | sandboxed environments. This environment class uses SWE-ReX's ModalDeployment to
|
| 40 | create and manage Modal sandboxes for command execution.
|
| 41 |
|
| 42 | This is useful for:
|
| 43 | - Training coding agents at scale with remote execution
|
| 44 | - Running evaluations in isolated cloud environments
|
| 45 | - Parallel execution across many instances
|
| 46 |
|
| 47 | See `SwerexModalEnvironmentConfig` for keyword arguments.
|
| 48 | """
|
| 49 | self.config = SwerexModalEnvironmentConfig(**kwargs)
|
| 50 | self.deployment = ModalDeployment(
|
| 51 | image=self.config.image,
|
| 52 | startup_timeout=self.config.startup_timeout,
|
| 53 | runtime_timeout=self.config.runtime_timeout,
|
| 54 | deployment_timeout=self.config.deployment_timeout,
|
| 55 | install_pipx=self.config.install_pipx,
|
| 56 | modal_sandbox_kwargs=self.config.modal_sandbox_kwargs,
|
| 57 | )
|
| 58 | asyncio.run(self.deployment.start())
|
| 59 |
|
| 60 | def execute(self, command: str, cwd: str = "", *, timeout: int | None = None) -> dict[str, Any]:
|
| 61 | """Execute a command in the environment and return the raw output."""
|
| 62 | output = asyncio.run(
|
| 63 | self.deployment.runtime.execute(
|
| 64 | RexCommand(
|
| 65 | command=command,
|
| 66 | shell=True,
|
| 67 | check=False,
|
| 68 | cwd=cwd or self.config.cwd,
|
| 69 | timeout=timeout or self.config.timeout,
|
| 70 | merge_output_streams=True,
|
| 71 | env=self.config.env if self.config.env else None,
|
| 72 | )
|
| 73 | )
|
| 74 | )
|
| 75 | return {
|
| 76 | "output": output.stdout,
|
| 77 | "returncode": output.exit_code,
|
| 78 | }
|
| 79 |
|
| 80 | def get_template_vars(self) -> dict[str, Any]:
|
| 81 | return self.config.model_dump()
|
| 82 |
|
| 83 | def stop(self):
|
| 84 | async def _stop():
|
| 85 | await asyncio.wait_for(self.deployment.stop(), timeout=10)
|
| 86 |
|
| 87 | try:
|
| 88 | asyncio.run(_stop())
|
| 89 | except Exception:
|
| 90 | pass
|
| 91 |
|