Skip to content

Commit 96b5cb7

Browse files
authored
[Arch] EventStreamRuntime supports browser (#2899)
* 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!
1 parent ced7499 commit 96b5cb7

File tree

6 files changed

+65
-12
lines changed

6 files changed

+65
-12
lines changed

opendevin/runtime/browser/__init__.py

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .utils import browse
2+
3+
__all__ = ['browse']

opendevin/runtime/server/browse.py opendevin/runtime/browser/utils.py

+9-3
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,31 @@
22

33
from opendevin.core.exceptions import BrowserUnavailableException
44
from opendevin.core.schema import ActionType
5+
from opendevin.events.action import BrowseInteractiveAction, BrowseURLAction
56
from opendevin.events.observation import BrowserOutputObservation
67
from opendevin.runtime.browser.browser_env import BrowserEnv
78

89

9-
async def browse(action, browser: BrowserEnv | None) -> BrowserOutputObservation:
10+
async def browse(
11+
action: BrowseURLAction | BrowseInteractiveAction, browser: BrowserEnv | None
12+
) -> BrowserOutputObservation:
1013
if browser is None:
1114
raise BrowserUnavailableException()
12-
if action.action == ActionType.BROWSE:
15+
16+
if isinstance(action, BrowseURLAction):
1317
# legacy BrowseURLAction
1418
asked_url = action.url
1519
if not asked_url.startswith('http'):
1620
asked_url = os.path.abspath(os.curdir) + action.url
1721
action_str = f'goto("{asked_url}")'
18-
elif action.action == ActionType.BROWSE_INTERACTIVE:
22+
23+
elif isinstance(action, BrowseInteractiveAction):
1924
# new BrowseInteractiveAction, supports full featured BrowserGym actions
2025
# action in BrowserGym: see https://github.com/ServiceNow/BrowserGym/blob/main/core/src/browsergym/core/action/functions.py
2126
action_str = action.browser_actions
2227
else:
2328
raise ValueError(f'Invalid action type: {action.action}')
29+
2430
try:
2531
# obs provided by BrowserGym: see https://github.com/ServiceNow/BrowserGym/blob/main/core/src/browsergym/core/env.py#L396
2632
obs = browser.step(action_str)

opendevin/runtime/client/client.py

+12
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
from opendevin.core.logger import opendevin_logger as logger
1212
from opendevin.events.action import (
1313
Action,
14+
BrowseInteractiveAction,
15+
BrowseURLAction,
1416
CmdRunAction,
1517
FileReadAction,
1618
FileWriteAction,
@@ -24,6 +26,8 @@
2426
Observation,
2527
)
2628
from opendevin.events.serialization import event_from_dict, event_to_dict
29+
from opendevin.runtime.browser import browse
30+
from opendevin.runtime.browser.browser_env import BrowserEnv
2731
from opendevin.runtime.plugins import (
2832
ALL_PLUGINS,
2933
JupyterPlugin,
@@ -47,6 +51,7 @@ def __init__(self, plugins_to_load: list[Plugin], work_dir: str) -> None:
4751
self._init_bash_shell(work_dir)
4852
self.lock = asyncio.Lock()
4953
self.plugins: dict[str, Plugin] = {}
54+
self.browser = BrowserEnv()
5055

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

182+
async def browse(self, action: BrowseURLAction) -> Observation:
183+
return await browse(action, self.browser)
184+
185+
async def browse_interactive(self, action: BrowseInteractiveAction) -> Observation:
186+
return await browse(action, self.browser)
187+
177188
def close(self):
178189
self.shell.close()
190+
self.browser.close()
179191

180192

181193
# def test_run_commond():

opendevin/runtime/client/runtime.py

+8-2
Original file line numberDiff line numberDiff line change
@@ -173,17 +173,23 @@ def close(self, close_client: bool = True):
173173
for container in containers:
174174
try:
175175
if container.name.startswith(self.container_name_prefix):
176+
# tail the logs before removing the container
177+
logs = container.logs(tail=1000).decode('utf-8')
178+
logger.info(
179+
f'==== Container logs ====\n{logs}\n==== End of container logs ===='
180+
)
176181
container.remove(force=True)
177182
except docker.errors.NotFound:
178183
pass
179184
if close_client:
180185
self.docker_client.close()
181186

182187
async def on_event(self, event: Event) -> None:
183-
print('EventStreamRuntime: on_event triggered')
188+
logger.info(f'EventStreamRuntime: on_event triggered: {event}')
184189
if isinstance(event, Action):
190+
logger.info(event, extra={'msg_type': 'ACTION'})
185191
observation = await self.run_action(event)
186-
print('EventStreamRuntime: observation', observation)
192+
logger.info(observation, extra={'msg_type': 'OBSERVATION'})
187193
# observation._cause = event.id # type: ignore[attr-defined]
188194
source = event.source if event.source else EventSource.AGENT
189195
await self.event_stream.add_event(observation, source)

opendevin/runtime/server/runtime.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from opendevin.runtime.runtime import Runtime
2121
from opendevin.storage.local import LocalFileStore
2222

23-
from .browse import browse
23+
from ..browser import browse
2424
from .files import read_file, write_file
2525

2626

opendevin/runtime/utils/image_agnostic.py

+32-6
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import os
2+
import shutil
23
import tempfile
34

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

6568
tarball_path = create_project_source_dist()
6669
filename = os.path.basename(tarball_path)
6770
filename = filename.removesuffix('.tar.gz')
6871

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

@@ -162,10 +169,14 @@ def _build_sandbox_image(
162169
raise e
163170

164171

165-
def _get_new_image_name(base_image: str, is_eventstream_runtime: bool) -> str:
172+
def _get_new_image_name(
173+
base_image: str, is_eventstream_runtime: bool, dev_mode: bool = False
174+
) -> str:
166175
prefix = 'od_sandbox'
167176
if is_eventstream_runtime:
168177
prefix = 'od_eventstream_runtime'
178+
if dev_mode:
179+
prefix += '_dev'
169180
if ':' not in base_image:
170181
base_image = base_image + ':latest'
171182

@@ -202,10 +213,25 @@ def get_od_sandbox_image(
202213
skip_init = False
203214
if image_exists:
204215
if is_eventstream_runtime:
205-
skip_init = True
216+
# An eventstream runtime image is already built for the base image (with poetry and dev dependencies)
217+
# but it might not contain the latest version of the source code and dependencies.
218+
# So we need to build a new (dev) image with the latest source code and dependencies.
219+
# FIXME: In production, we should just build once (since the source code will not change)
206220
base_image = new_image_name
221+
new_image_name = _get_new_image_name(
222+
base_image, is_eventstream_runtime, dev_mode=True
223+
)
224+
225+
# Delete the existing image named `new_image_name` if any
226+
images = docker_client.images.list()
227+
for image in images:
228+
if new_image_name in image.tags:
229+
docker_client.images.remove(image.id, force=True)
230+
231+
# We will reuse the existing image but will update the source code in it.
232+
skip_init = True
207233
logger.info(
208-
f'Reusing existing od_sandbox image [{new_image_name}] but will update the source code.'
234+
f'Reusing existing od_sandbox image [{base_image}] but will update the source code into [{new_image_name}]'
209235
)
210236
else:
211237
return new_image_name

0 commit comments

Comments
 (0)