-
Notifications
You must be signed in to change notification settings - Fork 445
Add custom task system for BrowserGym environment #201
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,24 +6,59 @@ | |
| - WebArena: Realistic evaluation with 812 complex tasks | ||
| - VisualWebArena: Visual web navigation tasks | ||
| - WorkArena: Enterprise task automation | ||
| - Custom: User-defined tasks with custom HTML and reward logic | ||
| """ | ||
|
|
||
| import importlib | ||
| import logging | ||
| from typing import Any, Dict, Optional | ||
| import os | ||
| import sys | ||
| from typing import Any, Dict, Optional, TYPE_CHECKING | ||
| from uuid import uuid4 | ||
|
|
||
| import gymnasium as gym | ||
|
|
||
| from openenv.core.env_server.interfaces import Environment | ||
| from browsergym_env.models import ( | ||
| from envs.browsergym_env.models import ( | ||
| BrowserGymAction, | ||
| BrowserGymObservation, | ||
| BrowserGymState, | ||
| ) | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
| # Add the server directory to sys.path to allow custom module imports | ||
| _SERVER_DIR = os.path.dirname(os.path.abspath(__file__)) | ||
| if _SERVER_DIR not in sys.path: | ||
| sys.path.insert(0, _SERVER_DIR) # noqa: E402 | ||
|
|
||
| # Import custom models for custom benchmark | ||
| if TYPE_CHECKING: | ||
| from custom.custom_models import ( | ||
| CustomGymAction, | ||
| CustomGymObservation, | ||
| CustomGymState, | ||
| ) | ||
|
|
||
| try: | ||
| from custom.custom_models import ( | ||
| CustomGymAction as _CustomGymAction, | ||
| CustomGymObservation as _CustomGymObservation, | ||
| CustomGymState as _CustomGymState, | ||
| ) | ||
|
|
||
| CUSTOM_AVAILABLE = True | ||
| CustomGymAction = _CustomGymAction | ||
| CustomGymObservation = _CustomGymObservation | ||
| CustomGymState = _CustomGymState | ||
| _CUSTOM_IMPORT_ERROR = None | ||
| except ImportError as e: | ||
| CUSTOM_AVAILABLE = False | ||
| CustomGymAction = None # type: ignore | ||
| CustomGymObservation = None # type: ignore | ||
| CustomGymState = None # type: ignore | ||
| _CUSTOM_IMPORT_ERROR = str(e) | ||
|
|
||
|
|
||
| def _get_axtree_txt(obs: Dict[str, Any]) -> str: | ||
| """Extract accessibility tree text from BrowserGym observation. | ||
|
|
@@ -124,59 +159,103 @@ def __init__( | |
| self.timeout = timeout | ||
| self.gym_kwargs = dict(gym_kwargs) | ||
|
|
||
| # Build environment ID | ||
| if task_name: | ||
| self.env_id = f"browsergym/{benchmark}.{task_name}" | ||
| # Check if this is a custom benchmark | ||
| self.is_custom = benchmark == "custom" | ||
|
|
||
| if self.is_custom: | ||
| # Custom benchmark handling | ||
| if not CUSTOM_AVAILABLE: | ||
| raise ValueError( | ||
| f"Custom benchmark requested but custom module import failed: {_CUSTOM_IMPORT_ERROR}" | ||
| ) | ||
|
|
||
| if not task_name: | ||
| raise ValueError("task_name is required for custom benchmark") | ||
|
|
||
| # Import and instantiate the custom task | ||
| try: | ||
| from custom.custom_tasks import get_custom_task | ||
|
|
||
| self.custom_env = get_custom_task( | ||
| task_name, | ||
| headless=headless, | ||
| viewport_width=viewport_width, | ||
| viewport_height=viewport_height, | ||
| timeout=timeout, | ||
| ) | ||
| except ImportError as e: | ||
| raise ValueError( | ||
| f"Failed to import custom task '{task_name}': {e}\n" | ||
| f"Make sure the task is registered in custom/custom_tasks.py" | ||
| ) from e | ||
|
|
||
| self.gym_env = None | ||
| self.env_id = f"custom/{task_name}" | ||
|
|
||
| # Use CustomGymState for custom benchmarks | ||
| self._state = CustomGymState( | ||
| episode_id=str(uuid4()), | ||
| step_count=0, | ||
| benchmark="custom", | ||
| task_name=task_name, | ||
| ) | ||
| else: | ||
| self.env_id = f"browsergym/{benchmark}" | ||
|
|
||
| # force import the benchmark module | ||
| benchmark_modules = { | ||
| "miniwob": "browsergym.miniwob", | ||
| "webarena": "browsergym.webarena", | ||
| "visualwebarena": "browsergym.visualwebarena", | ||
| "workarena": "browsergym.workarena", | ||
| } | ||
| module_path = benchmark_modules.get(benchmark) | ||
| try: | ||
| if module_path: | ||
| importlib.import_module(module_path) | ||
| # Original BrowserGym benchmark handling | ||
| # Build environment ID | ||
| if task_name: | ||
| self.env_id = f"browsergym/{benchmark}.{task_name}" | ||
| else: | ||
| importlib.import_module("browsergym") | ||
| except ModuleNotFoundError as import_error: | ||
| message = ( | ||
| "Failed to import BrowserGym benchmark " | ||
| f"'{benchmark}': {import_error}\n" | ||
| "Install the matching browsergym package " | ||
| f"(e.g., browsergym-{benchmark})." | ||
| ) | ||
| raise ValueError(message) from import_error | ||
|
|
||
| # Create the BrowserGym environment | ||
| try: | ||
| self.gym_env = gym.make( | ||
| self.env_id, | ||
| headless=headless, | ||
| viewport={"width": viewport_width, "height": viewport_height}, | ||
| timeout=timeout, | ||
| **self.gym_kwargs, | ||
| ) | ||
| except Exception as e: # noqa: BLE001 - gym.make | ||
| message = ( | ||
| "Failed to create BrowserGym environment " | ||
| f"'{self.env_id}': {e}\n" | ||
| "Make sure the benchmark package is installed " | ||
| f"(e.g., pip install browsergym-{benchmark})." | ||
| self.env_id = f"browsergym/{benchmark}" | ||
|
|
||
| # force import the benchmark module | ||
| benchmark_modules = { | ||
| "miniwob": "browsergym.miniwob", | ||
| "webarena": "browsergym.webarena", | ||
| "visualwebarena": "browsergym.visualwebarena", | ||
| "workarena": "browsergym.workarena", | ||
| } | ||
| module_path = benchmark_modules.get(benchmark) | ||
| try: | ||
| if module_path: | ||
| importlib.import_module(module_path) | ||
| else: | ||
| importlib.import_module("browsergym") | ||
| except ModuleNotFoundError as import_error: | ||
| message = ( | ||
| "Failed to import BrowserGym benchmark " | ||
| f"'{benchmark}': {import_error}\n" | ||
| "Install the matching browsergym package " | ||
| f"(e.g., browsergym-{benchmark})." | ||
| ) | ||
| raise ValueError(message) from import_error | ||
|
|
||
| # Create the BrowserGym environment | ||
| try: | ||
| self.gym_env = gym.make( | ||
| self.env_id, | ||
| headless=headless, | ||
| viewport={"width": viewport_width, "height": viewport_height}, | ||
| timeout=timeout, | ||
| **self.gym_kwargs, | ||
| ) | ||
| except Exception as e: # noqa: BLE001 - gym.make | ||
| message = ( | ||
| "Failed to create BrowserGym environment " | ||
| f"'{self.env_id}': {e}\n" | ||
| "Make sure the benchmark package is installed " | ||
| f"(e.g., pip install browsergym-{benchmark})." | ||
| ) | ||
| raise ValueError(message) from e | ||
|
|
||
| # State tracking | ||
| self._state = BrowserGymState( | ||
| episode_id=str(uuid4()), | ||
| step_count=0, | ||
| benchmark=benchmark, | ||
| task_name=task_name or "", | ||
| ) | ||
| raise ValueError(message) from e | ||
|
|
||
| # State tracking | ||
| self._state = BrowserGymState( | ||
| episode_id=str(uuid4()), | ||
| step_count=0, | ||
| benchmark=benchmark, | ||
| task_name=task_name or "", | ||
| ) | ||
| self.custom_env = None | ||
|
|
||
| self._last_obs: Optional[Dict[str, Any]] = None | ||
| self._last_info: Optional[Dict[str, Any]] = None | ||
|
|
@@ -195,6 +274,13 @@ def reset( | |
| Returns: | ||
| Initial observation for the task | ||
| """ | ||
| if self.is_custom: | ||
| # Handle custom environment reset | ||
| obs = self.custom_env.reset(seed=seed) | ||
| self._state = self.custom_env.state | ||
|
Comment on lines
+279
to
+280
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. logic: Directly overwriting Prompt To Fix With AIThis is a comment left during a code review.
Path: src/envs/browsergym_env/server/browsergym_environment.py
Line: 212:213
Comment:
**logic:** Directly overwriting `self._state` with `self.custom_env.state` bypasses the OpenEnv state management. This could break state tracking and episode management.
How can I resolve this? If you propose a fix, please make it concise. |
||
| # Convert CustomGymObservation to BrowserGymObservation | ||
| return self._convert_custom_observation(obs) | ||
|
|
||
| # Generate new episode ID | ||
| self._state = BrowserGymState( | ||
| episode_id=str(uuid4()), | ||
|
|
@@ -240,6 +326,13 @@ def step(self, action: BrowserGymAction) -> BrowserGymObservation: | |
| Returns: | ||
| Observation after executing the action | ||
| """ | ||
| if self.is_custom: | ||
| # Handle custom environment step | ||
| custom_action = CustomGymAction(action_str=action.action_str) | ||
| obs = self.custom_env.step(custom_action) | ||
| self._state = self.custom_env.state | ||
| return self._convert_custom_observation(obs) | ||
|
|
||
| self._state.step_count += 1 | ||
|
|
||
| # Execute action in gym environment | ||
|
|
@@ -364,12 +457,36 @@ def _create_observation( | |
| metadata=browsergym_metadata, | ||
| ) | ||
|
|
||
| def _convert_custom_observation(self, custom_obs: "CustomGymObservation") -> BrowserGymObservation: | ||
| """Convert CustomGymObservation to BrowserGymObservation. | ||
|
|
||
| Args: | ||
| custom_obs: Observation from custom environment | ||
|
|
||
| Returns: | ||
| BrowserGymObservation compatible with OpenEnv interface | ||
| """ | ||
| return BrowserGymObservation( | ||
| text=custom_obs.text, | ||
| url=custom_obs.url, | ||
| goal=custom_obs.goal, | ||
| axtree_txt=custom_obs.text, # Reuse text for compatibility | ||
| pruned_html="", | ||
| error=custom_obs.error, | ||
| last_action_error=custom_obs.last_action_error, | ||
| done=custom_obs.done, | ||
| reward=custom_obs.reward, | ||
| metadata=custom_obs.metadata, | ||
| ) | ||
|
|
||
| @property | ||
| def state(self) -> BrowserGymState: | ||
| """Get the current environment state.""" | ||
| return self._state | ||
|
|
||
| def close(self) -> None: | ||
| """Clean up environment resources.""" | ||
| if hasattr(self, "gym_env"): | ||
| if self.is_custom and self.custom_env: | ||
| self.custom_env.close() | ||
| elif hasattr(self, "gym_env") and self.gym_env: | ||
| self.gym_env.close() | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| """Custom BrowserGym tasks module. | ||
|
|
||
| This module provides custom task functionality for BrowserGym environments. | ||
| Custom tasks are registered in custom_tasks.py and can be used by setting | ||
| benchmark="custom" in BrowserGymEnvironment. | ||
| """ | ||
|
|
||
| # The custom tasks are registered in custom_tasks.py | ||
| # No need to import anything here - imports happen when needed |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
style: The try/except import pattern with global variable assignment creates type checking complications. The
# type: ignorecomments suggest this approach is fighting the type system. Is there a specific reason for this pattern instead of using conditional imports or factory functions?Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Prompt To Fix With AI