Răsfoiți Sursa

[Arch, Eval] Allowing injecting additional dependency to OD runtime docker image (#3237)

* allowing injecting additional dependency to OD runtime docker image

* update runtime build

* make `extra_deps` optional str | None
Xingyao Wang 1 an în urmă
părinte
comite
6a12a9f83c

+ 6 - 0
opendevin/core/config.py

@@ -154,6 +154,11 @@ class SandboxConfig(metaclass=Singleton):
         initialize_plugins: Whether to initialize plugins.
         update_source_code: Whether to update the source code in the EventStreamRuntime.
             Used for development of EventStreamRuntime.
+        od_runtime_extra_deps: The extra dependencies to install in the runtime image (typically used for evaluation).
+            This will be rendered into the end of the Dockerfile that builds the runtime image.
+            It can contain any valid shell commands (e.g., pip install numpy).
+            The path to the interpreter is available as $OD_INTERPRETER_PATH,
+            which can be used to install dependencies for the OD-specific Python interpreter.
         browsergym_eval_env: The BrowserGym environment to use for evaluation.
             Default is None for general purpose browsing. Check evaluation/miniwob and evaluation/webarena for examples.
 
@@ -173,6 +178,7 @@ class SandboxConfig(metaclass=Singleton):
     use_host_network: bool = False
     initialize_plugins: bool = True
     update_source_code: bool = False
+    od_runtime_extra_deps: str | None = None
     browsergym_eval_env: str | None = None
 
     def defaults_to_dict(self) -> dict:

+ 6 - 0
opendevin/runtime/client/runtime.py

@@ -74,6 +74,11 @@ class EventStreamRuntime(Runtime):
         self.action_semaphore = asyncio.Semaphore(1)  # Ensure one action at a time
 
     async def ainit(self, env_vars: dict[str, str] | None = None):
+        if self.config.sandbox.od_runtime_extra_deps:
+            logger.info(
+                f'Installing extra user-provided dependencies in the runtime image: {self.config.sandbox.od_runtime_extra_deps}'
+            )
+
         self.container_image = build_runtime_image(
             self.container_image,
             self.docker_client,
@@ -81,6 +86,7 @@ class EventStreamRuntime(Runtime):
             # inside the container. This is useful when you want to test/debug the
             # latest code in the runtime docker container.
             update_source_code=self.config.sandbox.update_source_code,
+            extra_deps=self.config.sandbox.od_runtime_extra_deps,
         )
         self.container = await self._init_container(
             self.sandbox_workspace_dir,

+ 23 - 4
opendevin/runtime/utils/runtime_build.py

@@ -63,7 +63,10 @@ def _put_source_code_to_dir(temp_dir: str) -> str:
 
 
 def _generate_dockerfile(
-    base_image: str, source_code_dirname: str, skip_init: bool = False
+    base_image: str,
+    source_code_dirname: str,
+    skip_init: bool = False,
+    extra_deps: str | None = None,
 ) -> str:
     """Generate the Dockerfile content for the eventstream runtime image based on user-provided base image."""
     env = Environment(
@@ -76,6 +79,7 @@ def _generate_dockerfile(
         base_image=base_image,
         source_code_dirname=source_code_dirname,
         skip_init=skip_init,
+        extra_deps=extra_deps if extra_deps is not None else '',
     )
     return dockerfile_content
 
@@ -84,11 +88,15 @@ def prep_docker_build_folder(
     dir_path: str,
     base_image: str,
     skip_init: bool = False,
+    extra_deps: str | None = None,
 ):
     """Prepares the docker build folder by copying the source code and generating the Dockerfile."""
     source_code_dirname = _put_source_code_to_dir(dir_path)
     dockerfile_content = _generate_dockerfile(
-        base_image, source_code_dirname, skip_init=skip_init
+        base_image,
+        source_code_dirname,
+        skip_init=skip_init,
+        extra_deps=extra_deps,
     )
     logger.info(
         (
@@ -106,6 +114,7 @@ def _build_sandbox_image(
     target_image_name: str,
     docker_client: docker.DockerClient,
     skip_init: bool = False,
+    extra_deps: str | None = None,
 ):
     try:
         with tempfile.TemporaryDirectory() as temp_dir:
@@ -115,7 +124,9 @@ def _build_sandbox_image(
                 )
             else:
                 logger.info(f'Building agnostic sandbox image: {target_image_name}')
-            prep_docker_build_folder(temp_dir, base_image, skip_init=skip_init)
+            prep_docker_build_folder(
+                temp_dir, base_image, skip_init=skip_init, extra_deps=extra_deps
+            )
             api_client = docker_client.api
             build_logs = api_client.build(
                 path=temp_dir,
@@ -185,6 +196,8 @@ def build_runtime_image(
     docker_client: docker.DockerClient,
     update_source_code: bool = False,
     save_to_local_store: bool = False,  # New parameter to control saving to local store
+    extra_deps: str
+    | None = None,  # whether to install extra dependencies inside the image
 ) -> str:
     """Build the runtime image for the OpenDevin runtime.
 
@@ -247,7 +260,13 @@ def build_runtime_image(
     if not skip_init:
         logger.info(f'Building image [{new_image_name}] from scratch')
 
-    _build_sandbox_image(base_image, new_image_name, docker_client, skip_init=skip_init)
+    _build_sandbox_image(
+        base_image,
+        new_image_name,
+        docker_client,
+        skip_init=skip_init,
+        extra_deps=extra_deps,
+    )
 
     # Only for development: allow to save image as archive:
     if not image_exists and save_to_local_store:

+ 2 - 0
opendevin/runtime/utils/runtime_templates/Dockerfile.j2

@@ -59,6 +59,8 @@ RUN cd /opendevin/code && \
     apt-get update && \
     /opendevin/miniforge3/bin/mamba run -n base poetry run pip install playwright && \
     /opendevin/miniforge3/bin/mamba run -n base poetry run playwright install --with-deps chromium && \
+    export OD_INTERPRETER_PATH=$(/opendevin/miniforge3/bin/mamba run -n base poetry run python -c "import sys; print(sys.executable)") && \
+    {{ extra_deps }} {% if extra_deps %} && {% endif %} \
     /opendevin/miniforge3/bin/mamba run -n base poetry cache clear --all . && \
     {% if not skip_init %}chmod -R g+rws /opendevin/poetry && {% endif %} \
     apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* && \

+ 2 - 0
tests/unit/test_runtime_build.py

@@ -161,6 +161,7 @@ def test_build_runtime_image_from_scratch(mock_docker_client, mock_build_sandbox
         f'{RUNTIME_IMAGE_PREFIX}:{OD_VERSION}_image_debian_tag_11',
         mock_docker_client,
         skip_init=False,
+        extra_deps=None,
     )
 
 
@@ -200,4 +201,5 @@ def test_build_runtime_image_exist_with_update_source(
         f'{RUNTIME_IMAGE_PREFIX}_dev:{OD_VERSION}_image_debian_tag_11',
         mock_docker_client,
         skip_init=True,
+        extra_deps=None,
     )