|
|
@@ -3,7 +3,6 @@ import base64
|
|
|
import io
|
|
|
import json
|
|
|
import multiprocessing
|
|
|
-import os
|
|
|
import time
|
|
|
import uuid
|
|
|
|
|
|
@@ -18,41 +17,24 @@ from PIL import Image
|
|
|
from opendevin.core.exceptions import BrowserInitException
|
|
|
from opendevin.core.logger import opendevin_logger as logger
|
|
|
|
|
|
+BROWSER_EVAL_GET_GOAL_ACTION = 'GET_EVAL_GOAL'
|
|
|
+BROWSER_EVAL_GET_REWARDS_ACTION = 'GET_EVAL_REWARDS'
|
|
|
+
|
|
|
|
|
|
class BrowserEnv:
|
|
|
- def __init__(
|
|
|
- self,
|
|
|
- browsergym_eval: str = '',
|
|
|
- browsergym_eval_save_dir: str = '',
|
|
|
- ):
|
|
|
+ def __init__(self, browsergym_eval_env: str | None = None):
|
|
|
self.html_text_converter = self.get_html_text_converter()
|
|
|
self.eval_mode = False
|
|
|
self.eval_dir = ''
|
|
|
- # EVAL only: browsergym_eval and browsergym_eval_save_dir must be provided for evaluation
|
|
|
- self.browsergym_eval = browsergym_eval
|
|
|
- self.browsergym_eval_save_dir = browsergym_eval_save_dir
|
|
|
- if self.browsergym_eval:
|
|
|
- assert (
|
|
|
- self.browsergym_eval_save_dir
|
|
|
- ), 'browsergym_eval_save_dir must be provided for evaluation.'
|
|
|
- self.eval_mode = True
|
|
|
- self.eval_dir = os.path.join(
|
|
|
- self.browsergym_eval_save_dir, self.browsergym_eval.split('/')[1]
|
|
|
- )
|
|
|
- os.makedirs(self.eval_dir, exist_ok=True)
|
|
|
+
|
|
|
+ # EVAL only: browsergym_eval_env must be provided for evaluation
|
|
|
+ self.browsergym_eval_env = browsergym_eval_env
|
|
|
+ self.eval_mode = bool(browsergym_eval_env)
|
|
|
+
|
|
|
# Initialize browser environment process
|
|
|
multiprocessing.set_start_method('spawn', force=True)
|
|
|
self.browser_side, self.agent_side = multiprocessing.Pipe()
|
|
|
|
|
|
- try:
|
|
|
- self.original_cwd = os.getcwd()
|
|
|
- except FileNotFoundError:
|
|
|
- logger.warning(
|
|
|
- 'Current working directory does not exist. Using /tmp as fallback.'
|
|
|
- )
|
|
|
- self.original_cwd = '/tmp'
|
|
|
- os.chdir('/tmp')
|
|
|
-
|
|
|
self.init_browser()
|
|
|
atexit.register(self.close)
|
|
|
|
|
|
@@ -74,17 +56,6 @@ class BrowserEnv:
|
|
|
)
|
|
|
def init_browser(self):
|
|
|
logger.info('Starting browser env...')
|
|
|
-
|
|
|
- # Ensure we're in a valid directory before starting the process
|
|
|
- try:
|
|
|
- os.chdir(self.original_cwd)
|
|
|
- logger.debug(f'Changed back to original directory: {self.original_cwd}')
|
|
|
- except Exception as e:
|
|
|
- logger.error(f'Failed to change to original directory: {e}')
|
|
|
- # If we can't change to the original directory, try to use a known valid directory
|
|
|
- os.chdir('/tmp')
|
|
|
- logger.debug('Changed to /tmp directory as fallback')
|
|
|
-
|
|
|
try:
|
|
|
self.process = multiprocessing.Process(target=self.browser_process)
|
|
|
self.process.start()
|
|
|
@@ -98,8 +69,17 @@ class BrowserEnv:
|
|
|
|
|
|
def browser_process(self):
|
|
|
if self.eval_mode:
|
|
|
- logger.info('Creating browser env for evaluation purpose.')
|
|
|
- env = gym.make(self.browsergym_eval)
|
|
|
+ assert self.browsergym_eval_env is not None
|
|
|
+ logger.info('Initializing browser env for web browsing evaluation.')
|
|
|
+ if 'webarena' in self.browsergym_eval_env:
|
|
|
+ import browsergym.webarena # noqa F401 register webarena tasks as gym environments
|
|
|
+ elif 'miniwob' in self.browsergym_eval_env:
|
|
|
+ import browsergym.miniwob # noqa F401 register miniwob tasks as gym environments
|
|
|
+ else:
|
|
|
+ raise ValueError(
|
|
|
+ f'Unsupported browsergym eval env: {self.browsergym_eval_env}'
|
|
|
+ )
|
|
|
+ env = gym.make(self.browsergym_eval_env)
|
|
|
else:
|
|
|
env = gym.make(
|
|
|
'browsergym/openended',
|
|
|
@@ -108,20 +88,22 @@ class BrowserEnv:
|
|
|
headless=True,
|
|
|
disable_env_checker=True,
|
|
|
)
|
|
|
+
|
|
|
obs, info = env.reset()
|
|
|
- # EVAL only: save the goal into file for evaluation
|
|
|
+
|
|
|
+ # EVAL ONLY: save the goal into file for evaluation
|
|
|
+ self.eval_goal = None
|
|
|
+ self.eval_rewards: list[float] = []
|
|
|
if self.eval_mode:
|
|
|
- rewards = [] # store rewards if in eval mode
|
|
|
- logger.info(obs['goal'])
|
|
|
- with open(
|
|
|
- os.path.join(self.eval_dir, 'goal.txt'), 'w', encoding='utf-8'
|
|
|
- ) as f:
|
|
|
- f.write(obs['goal'])
|
|
|
+ logger.info(f"Browsing goal: {obs['goal']}")
|
|
|
+ self.eval_goal = obs['goal']
|
|
|
+
|
|
|
logger.info('Browser env started.')
|
|
|
while True:
|
|
|
try:
|
|
|
if self.browser_side.poll(timeout=0.01):
|
|
|
unique_request_id, action_data = self.browser_side.recv()
|
|
|
+
|
|
|
# shutdown the browser environment
|
|
|
if unique_request_id == 'SHUTDOWN':
|
|
|
logger.info('SHUTDOWN recv, shutting down browser env...')
|
|
|
@@ -130,17 +112,29 @@ class BrowserEnv:
|
|
|
elif unique_request_id == 'IS_ALIVE':
|
|
|
self.browser_side.send(('ALIVE', None))
|
|
|
continue
|
|
|
+
|
|
|
+ # EVAL ONLY: Get evaluation info
|
|
|
+ if action_data['action'] == BROWSER_EVAL_GET_GOAL_ACTION:
|
|
|
+ self.browser_side.send(
|
|
|
+ (unique_request_id, {'text_content': self.eval_goal})
|
|
|
+ )
|
|
|
+ continue
|
|
|
+ elif action_data['action'] == BROWSER_EVAL_GET_REWARDS_ACTION:
|
|
|
+ self.browser_side.send(
|
|
|
+ (
|
|
|
+ unique_request_id,
|
|
|
+ {'text_content': json.dumps(self.eval_rewards)},
|
|
|
+ )
|
|
|
+ )
|
|
|
+ continue
|
|
|
+
|
|
|
action = action_data['action']
|
|
|
obs, reward, terminated, truncated, info = env.step(action)
|
|
|
- # EVAL only: save the rewards into file for evaluation
|
|
|
+
|
|
|
+ # EVAL ONLY: Save the rewards into file for evaluation
|
|
|
if self.eval_mode:
|
|
|
- rewards.append(reward)
|
|
|
- with open(
|
|
|
- os.path.join(self.eval_dir, 'rewards.json'),
|
|
|
- 'w',
|
|
|
- encoding='utf-8',
|
|
|
- ) as f:
|
|
|
- f.write(json.dumps(rewards))
|
|
|
+ self.eval_rewards.append(reward)
|
|
|
+
|
|
|
# add text content of the page
|
|
|
html_str = flatten_dom_to_str(obs['dom_object'])
|
|
|
obs['text_content'] = self.html_text_converter.handle(html_str)
|
|
|
@@ -158,6 +152,7 @@ class BrowserEnv:
|
|
|
return
|
|
|
|
|
|
def step(self, action_str: str, timeout: float = 30) -> dict:
|
|
|
+ """Execute an action in the browser environment and return the observation."""
|
|
|
unique_request_id = str(uuid.uuid4())
|
|
|
self.agent_side.send((unique_request_id, {'action': action_str}))
|
|
|
start_time = time.time()
|