Ver código fonte

Persistent docker session (#1998)

Co-authored-by: Robert Brennan <accounts@rbren.io>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
Co-authored-by: Graham Neubig <neubig@gmail.com>
மனோஜ்குமார் பழனிச்சாமி 1 ano atrás
pai
commit
d4ccd48af8

+ 3 - 0
.github/workflows/dummy-agent-test.yml

@@ -10,6 +10,9 @@ on:
     - main
   pull_request:
 
+env:
+  PERSIST_SANDBOX : "false"
+
 jobs:
   test:
     runs-on: ubuntu-latest

+ 3 - 0
.github/workflows/run-integration-tests.yml

@@ -15,6 +15,9 @@ on:
       - 'evaluation/**'
   pull_request:
 
+env:
+  PERSIST_SANDBOX : "false"
+
 jobs:
   integration-tests-on-linux:
     name: Integration Tests on Linux

+ 3 - 0
.github/workflows/run-unit-tests.yml

@@ -15,6 +15,9 @@ on:
       - 'evaluation/**'
   pull_request:
 
+env:
+  PERSIST_SANDBOX : "false"
+
 jobs:
   test-on-macos:
     name: Test on macOS

+ 4 - 2
README.md

@@ -54,10 +54,11 @@ To start the app, run these commands, replacing `$(pwd)/workspace` with the dire
 ```bash
 # The directory you want OpenDevin to work with. MUST be an absolute path!
 export WORKSPACE_BASE=$(pwd)/workspace;
+export SSH_PASSWORD="set some long password here";
 ```
 
-> [!WARNING]  
-> OpenDevin runs bash commands within a Docker sandbox, so it should not affect your machine. 
+> [!WARNING]
+> OpenDevin runs bash commands within a Docker sandbox, so it should not affect your machine.
 > But your workspace directory will be attached to that sandbox, and files in the directory may be modified or deleted.
 
 ```bash
@@ -65,6 +66,7 @@ docker run \
     -it \
     --pull=always \
     -e SANDBOX_USER_ID=$(id -u) \
+    -e SSH_PASSWORD=$SSH_PASSWORD \
     -e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
     -v $WORKSPACE_BASE:/opt/workspace_base \
     -v /var/run/docker.sock:/var/run/docker.sock \

+ 1 - 1
agenthub/codeact_agent/codeact_agent.py

@@ -196,7 +196,7 @@ class CodeActAgent(Agent):
             {'role': 'system', 'content': self.system_message},
             {
                 'role': 'user',
-                'content': f"Here is an example of how you can interact with the environment for task solving:\n{EXAMPLES}\n\nNOW, LET'S START!",
+                'content': f"Here is an example of how you can interact with the environment for task solving:\n{EXAMPLES}\n\nNOW, LET'S START!\n",
             },
         ]
 

+ 2 - 0
docs/modules/usage/intro.mdx

@@ -66,6 +66,7 @@ To start the app, run these commands, replacing `$(pwd)/workspace` with the dire
 ```
 # The directory you want OpenDevin to work with. It MUST be an absolute path!
 export WORKSPACE_BASE=$(pwd)/workspace
+export SSH_PASSWORD="set some long password here";
 ```
 
 :::warning
@@ -78,6 +79,7 @@ docker run \
     --pull=always \
     -e LLM_API_KEY \
     -e SANDBOX_USER_ID=$(id -u) \
+    -e SSH_PASSWORD=$SSH_PASSWORD \
     -e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
     -v $WORKSPACE_BASE:/opt/workspace_base \
     -v /var/run/docker.sock:/var/run/docker.sock \

+ 3 - 0
opendevin/core/config.py

@@ -179,6 +179,9 @@ class AppConfig(metaclass=Singleton):
     disable_color: bool = False
     sandbox_user_id: int = os.getuid() if hasattr(os, 'getuid') else 1000
     sandbox_timeout: int = 120
+    persist_sandbox: bool = True
+    ssh_port: int = 63710
+    ssh_password: str | None = None
     github_token: str | None = None
     jwt_secret: str = uuid.uuid4().hex
     debug: bool = False

+ 1 - 0
opendevin/core/schema/config.py

@@ -2,6 +2,7 @@ from enum import Enum
 
 
 class ConfigType(str, Enum):
+    # For frontend
     LLM_CUSTOM_LLM_PROVIDER = 'LLM_CUSTOM_LLM_PROVIDER'
     LLM_MAX_INPUT_TOKENS = 'LLM_MAX_INPUT_TOKENS'
     LLM_MAX_OUTPUT_TOKENS = 'LLM_MAX_OUTPUT_TOKENS'

+ 44 - 28
opendevin/runtime/docker/ssh_box.py

@@ -216,38 +216,48 @@ class DockerSSHBox(Sandbox):
             )
             raise ex
 
-        self.instance_id = (
-            sid + str(uuid.uuid4()) if sid is not None else str(uuid.uuid4())
-        )
+        if config.persist_sandbox:
+            self.instance_id = 'persisted'
+        else:
+            self.instance_id = (sid or '') + str(uuid.uuid4())
 
         self.timeout = timeout
-        self.container_image = (
-            config.sandbox_container_image
-            if container_image is None
-            else container_image
-        )
+        self.container_image = container_image or config.sandbox_container_image
         self.container_name = self.container_name_prefix + self.instance_id
 
         # set up random user password
-        self._ssh_password = str(uuid.uuid4())
-        self._ssh_port = find_available_tcp_port()
-
-        # always restart the container, cuz the initial be regarded as a new session
-        n_tries = 5
-        while n_tries > 0:
-            try:
-                self.restart_docker_container()
-                break
-            except Exception as e:
-                logger.exception(
-                    'Failed to start Docker container, retrying...', exc_info=False
-                )
-                n_tries -= 1
-                if n_tries == 0:
-                    raise e
-                time.sleep(5)
-        self.setup_user()
-
+        if config.persist_sandbox:
+            if not config.ssh_password:
+                raise Exception('Password must be set for persistent sandbox')
+            self._ssh_password = config.ssh_password
+            self._ssh_port = config.ssh_port
+        else:
+            self._ssh_password = str(uuid.uuid4())
+            self._ssh_port = find_available_tcp_port()
+        try:
+            docker.DockerClient().containers.get(self.container_name)
+            is_initial_session = False
+        except docker.errors.NotFound:
+            is_initial_session = True
+            logger.info('Creating new Docker container')
+        if not config.persist_sandbox or is_initial_session:
+            n_tries = 5
+            while n_tries > 0:
+                try:
+                    self.restart_docker_container()
+                    break
+                except Exception as e:
+                    logger.exception(
+                        'Failed to start Docker container, retrying...', exc_info=False
+                    )
+                    n_tries -= 1
+                    if n_tries == 0:
+                        raise e
+                    time.sleep(5)
+            self.setup_user()
+        else:
+            self.container = self.docker_client.containers.get(self.container_name)
+            logger.info('Using existing Docker container')
         try:
             self.start_ssh_session()
         except pxssh.ExceptionPxssh as e:
@@ -391,6 +401,9 @@ class DockerSSHBox(Sandbox):
         # cd to workspace
         self.ssh.sendline(f'cd {self.sandbox_workspace_dir}')
         self.ssh.prompt()
+        # load bashrc
+        self.ssh.sendline('source ~/.bashrc')
+        self.ssh.prompt()
 
     def get_exec_cmd(self, cmd: str) -> list[str]:
         if self.run_as_devin:
@@ -704,7 +717,10 @@ class DockerSSHBox(Sandbox):
         containers = self.docker_client.containers.list(all=True)
         for container in containers:
             try:
-                if container.name.startswith(self.container_name):
+                if (
+                    container.name.startswith(self.container_name)
+                    and not config.persist_sandbox
+                ):
                     # only remove the container we created
                     # otherwise all other containers with the same prefix will be removed
                     # which will mess up with parallel evaluation

+ 1 - 1
opendevin/runtime/plugins/jupyter/execute_server

@@ -134,7 +134,7 @@ class JupyterKernel:
         )
         self.heartbeat_callback.start()
 
-    async def execute(self, code, timeout=60):
+    async def execute(self, code, timeout=120):
         if not self.ws:
             await self._connect()