Skip to content

Commit

Permalink
[Arch] EventStreamRuntime supports browser (All-Hands-AI#2899)
Browse files Browse the repository at this point in the history
* fix the case when source and tmp are not on the same device

* always build a dev box (with updated source code) for development purpose

* tail the log before removing the container

* move browse function

* support browser!
  • Loading branch information
xingyaoww authored Jul 11, 2024
1 parent ced7499 commit 96b5cb7
Show file tree
Hide file tree
Showing 6 changed files with 65 additions and 12 deletions.
3 changes: 3 additions & 0 deletions opendevin/runtime/browser/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .utils import browse

__all__ = ['browse']
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,31 @@

from opendevin.core.exceptions import BrowserUnavailableException
from opendevin.core.schema import ActionType
from opendevin.events.action import BrowseInteractiveAction, BrowseURLAction
from opendevin.events.observation import BrowserOutputObservation
from opendevin.runtime.browser.browser_env import BrowserEnv


async def browse(action, browser: BrowserEnv | None) -> BrowserOutputObservation:
async def browse(
action: BrowseURLAction | BrowseInteractiveAction, browser: BrowserEnv | None
) -> BrowserOutputObservation:
if browser is None:
raise BrowserUnavailableException()
if action.action == ActionType.BROWSE:

if isinstance(action, BrowseURLAction):
# legacy BrowseURLAction
asked_url = action.url
if not asked_url.startswith('http'):
asked_url = os.path.abspath(os.curdir) + action.url
action_str = f'goto("{asked_url}")'
elif action.action == ActionType.BROWSE_INTERACTIVE:

elif isinstance(action, BrowseInteractiveAction):
# new BrowseInteractiveAction, supports full featured BrowserGym actions
# action in BrowserGym: see https://github.com/ServiceNow/BrowserGym/blob/main/core/src/browsergym/core/action/functions.py
action_str = action.browser_actions
else:
raise ValueError(f'Invalid action type: {action.action}')

try:
# obs provided by BrowserGym: see https://github.com/ServiceNow/BrowserGym/blob/main/core/src/browsergym/core/env.py#L396
obs = browser.step(action_str)
Expand Down
12 changes: 12 additions & 0 deletions opendevin/runtime/client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
from opendevin.core.logger import opendevin_logger as logger
from opendevin.events.action import (
Action,
BrowseInteractiveAction,
BrowseURLAction,
CmdRunAction,
FileReadAction,
FileWriteAction,
Expand All @@ -24,6 +26,8 @@
Observation,
)
from opendevin.events.serialization import event_from_dict, event_to_dict
from opendevin.runtime.browser import browse
from opendevin.runtime.browser.browser_env import BrowserEnv
from opendevin.runtime.plugins import (
ALL_PLUGINS,
JupyterPlugin,
Expand All @@ -47,6 +51,7 @@ def __init__(self, plugins_to_load: list[Plugin], work_dir: str) -> None:
self._init_bash_shell(work_dir)
self.lock = asyncio.Lock()
self.plugins: dict[str, Plugin] = {}
self.browser = BrowserEnv()

for plugin in plugins_to_load:
plugin.initialize()
Expand Down Expand Up @@ -174,8 +179,15 @@ async def write(self, action: FileWriteAction) -> Observation:
return ErrorObservation(f'Malformed paths not permitted: {filepath}')
return FileWriteObservation(content='', path=filepath)

async def browse(self, action: BrowseURLAction) -> Observation:
return await browse(action, self.browser)

async def browse_interactive(self, action: BrowseInteractiveAction) -> Observation:
return await browse(action, self.browser)

def close(self):
self.shell.close()
self.browser.close()


# def test_run_commond():
Expand Down
10 changes: 8 additions & 2 deletions opendevin/runtime/client/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,17 +173,23 @@ def close(self, close_client: bool = True):
for container in containers:
try:
if container.name.startswith(self.container_name_prefix):
# tail the logs before removing the container
logs = container.logs(tail=1000).decode('utf-8')
logger.info(
f'==== Container logs ====\n{logs}\n==== End of container logs ===='
)
container.remove(force=True)
except docker.errors.NotFound:
pass
if close_client:
self.docker_client.close()

async def on_event(self, event: Event) -> None:
print('EventStreamRuntime: on_event triggered')
logger.info(f'EventStreamRuntime: on_event triggered: {event}')
if isinstance(event, Action):
logger.info(event, extra={'msg_type': 'ACTION'})
observation = await self.run_action(event)
print('EventStreamRuntime: observation', observation)
logger.info(observation, extra={'msg_type': 'OBSERVATION'})
# observation._cause = event.id # type: ignore[attr-defined]
source = event.source if event.source else EventSource.AGENT
await self.event_stream.add_event(observation, source)
Expand Down
2 changes: 1 addition & 1 deletion opendevin/runtime/server/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from opendevin.runtime.runtime import Runtime
from opendevin.storage.local import LocalFileStore

from .browse import browse
from ..browser import browse
from .files import read_file, write_file


Expand Down
38 changes: 32 additions & 6 deletions opendevin/runtime/utils/image_agnostic.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
import shutil
import tempfile

import docker
Expand Down Expand Up @@ -48,7 +49,9 @@ def generate_dockerfile_for_eventstream_runtime(
else:
dockerfile_content = (
f'FROM {base_image}\n'
# FIXME: make this more generic / cross-platform
'RUN apt update && apt install -y wget sudo\n'
'RUN apt-get update && apt-get install -y libgl1-mesa-glx\n' # Extra dependency for OpenCV
'RUN mkdir -p /opendevin && mkdir -p /opendevin/logs && chmod 777 /opendevin/logs\n'
'RUN echo "" > /opendevin/bash.bashrc\n'
'RUN if [ ! -d /opendevin/miniforge3 ]; then \\\n'
Expand All @@ -58,16 +61,18 @@ def generate_dockerfile_for_eventstream_runtime(
' chmod -R g+w /opendevin/miniforge3 && \\\n'
' bash -c ". /opendevin/miniforge3/etc/profile.d/conda.sh && conda config --set changeps1 False && conda config --append channels conda-forge"; \\\n'
' fi\n'
'RUN /opendevin/miniforge3/bin/mamba install python=3.11\n'
'RUN /opendevin/miniforge3/bin/mamba install conda-forge::poetry\n'
'RUN /opendevin/miniforge3/bin/mamba install python=3.11 -y\n'
'RUN /opendevin/miniforge3/bin/mamba install conda-forge::poetry -y\n'
)

tarball_path = create_project_source_dist()
filename = os.path.basename(tarball_path)
filename = filename.removesuffix('.tar.gz')

# move the tarball to temp_dir
os.rename(tarball_path, os.path.join(temp_dir, 'project.tar.gz'))
_res = shutil.copy(tarball_path, os.path.join(temp_dir, 'project.tar.gz'))
if _res:
os.remove(tarball_path)
logger.info(
f'Source distribution moved to {os.path.join(temp_dir, "project.tar.gz")}'
)
Expand All @@ -88,6 +93,8 @@ def generate_dockerfile_for_eventstream_runtime(
'RUN cd /opendevin/code && '
'/opendevin/miniforge3/bin/mamba run -n base poetry env use python3.11 && '
'/opendevin/miniforge3/bin/mamba run -n base poetry install\n'
# for browser (update if needed)
'RUN apt-get update && cd /opendevin/code && /opendevin/miniforge3/bin/mamba run -n base poetry run playwright install --with-deps chromium\n'
)
return dockerfile_content

Expand Down Expand Up @@ -162,10 +169,14 @@ def _build_sandbox_image(
raise e


def _get_new_image_name(base_image: str, is_eventstream_runtime: bool) -> str:
def _get_new_image_name(
base_image: str, is_eventstream_runtime: bool, dev_mode: bool = False
) -> str:
prefix = 'od_sandbox'
if is_eventstream_runtime:
prefix = 'od_eventstream_runtime'
if dev_mode:
prefix += '_dev'
if ':' not in base_image:
base_image = base_image + ':latest'

Expand Down Expand Up @@ -202,10 +213,25 @@ def get_od_sandbox_image(
skip_init = False
if image_exists:
if is_eventstream_runtime:
skip_init = True
# An eventstream runtime image is already built for the base image (with poetry and dev dependencies)
# but it might not contain the latest version of the source code and dependencies.
# So we need to build a new (dev) image with the latest source code and dependencies.
# FIXME: In production, we should just build once (since the source code will not change)
base_image = new_image_name
new_image_name = _get_new_image_name(
base_image, is_eventstream_runtime, dev_mode=True
)

# Delete the existing image named `new_image_name` if any
images = docker_client.images.list()
for image in images:
if new_image_name in image.tags:
docker_client.images.remove(image.id, force=True)

# We will reuse the existing image but will update the source code in it.
skip_init = True
logger.info(
f'Reusing existing od_sandbox image [{new_image_name}] but will update the source code.'
f'Reusing existing od_sandbox image [{base_image}] but will update the source code into [{new_image_name}]'
)
else:
return new_image_name
Expand Down

0 comments on commit 96b5cb7

Please sign in to comment.