Skip to content

Commit fb05a89

Browse files
authored
Merge pull request #184 from meta-pytorch/add-browsergym-example
[ENHANCEMENT] BrowserGym Env
2 parents ed5d302 + 0a6400f commit fb05a89

File tree

5 files changed

+106
-40
lines changed

5 files changed

+106
-40
lines changed

examples/browsergym_example.py

Lines changed: 17 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
11
"""BrowserGym MiniWoB example with Qwen deciding the next action.
22
3-
This is an inference example for the BrowserGym environment. It uses the OpenAI
4-
client and a vision language model to decide the next action. We use Hugging Face
5-
Inference Providers API to access the model, but you can use any other provider that
3+
This is an inference example for the BrowserGym environment. It uses the OpenAI
4+
client and a vision language model to decide the next action. We use Hugging Face
5+
Inference Providers API to access the model, but you can use any other provider that
66
is compatible with the OpenAI API.
77
88
Prerequisites:
9-
- Clone the MiniWoB++ tasks repository.
10-
- Serve the HTML bundle with `python -m http.server 8888` inside the
11-
`miniwob-plusplus/miniwob/html` directory.
12-
- Export the MiniWoB URL (must include the `/miniwob/` suffix):
13-
`export MINIWOB_URL=http://host.docker.internal:8888/miniwob/`
9+
- (Optional) Export the MiniWoB URL if you are hosting the tasks yourself
10+
(must include the `/miniwob/` suffix); the BrowserGym Docker image now
11+
serves the MiniWoB bundle internally on port 8888.
1412
- Export your Hugging Face token for the router:
1513
`export HF_TOKEN=your_token_here`
1614
@@ -99,10 +97,12 @@ def extract_clickable_elements(observation) -> List[Dict[str, str]]:
9997

10098
bbox = props.get("bbox") or []
10199
bbox_str = ", ".join(bbox) if bbox else "?"
102-
clickables.append({
103-
"bid": str(bid),
104-
"bbox": bbox_str,
105-
})
100+
clickables.append(
101+
{
102+
"bid": str(bid),
103+
"bbox": bbox_str,
104+
}
105+
)
106106

107107
# Keep a stable ordering for readability
108108
clickables.sort(key=lambda item: item["bid"])
@@ -170,18 +170,16 @@ def parse_model_action(response_text: str) -> str:
170170

171171

172172
def main() -> None:
173-
174173
client = OpenAI(base_url=API_BASE_URL, api_key=API_KEY)
175174

176-
env = BrowserGymEnv.from_hub(
177-
"browsergym-env:latest",
175+
env = BrowserGymEnv.from_docker_image(
176+
image="browsergym-env:latest",
178177
env_vars={
179178
"BROWSERGYM_BENCHMARK": "miniwob",
180179
"BROWSERGYM_TASK_NAME": "click-test",
181180
},
182-
ports={8000: 8000},
183181
)
184-
182+
185183
history: List[str] = []
186184

187185
try:
@@ -227,9 +225,7 @@ def main() -> None:
227225
response_text = completion.choices[0].message.content or ""
228226
# pylint: disable=broad-except
229227
except Exception as exc: # noqa: BLE001
230-
failure_msg = (
231-
f"Model request failed ({exc}). Using fallback action."
232-
)
228+
failure_msg = f"Model request failed ({exc}). Using fallback action."
233229
print(failure_msg)
234230
response_text = FALLBACK_ACTION
235231

@@ -242,8 +238,7 @@ def main() -> None:
242238
reward = result.reward or 0.0
243239
error_flag = " ERROR" if observation.last_action_error else ""
244240
history_line = (
245-
f"Step {step}: {action_str} -> reward {reward:+.2f}"
246-
f"{error_flag}"
241+
f"Step {step}: {action_str} -> reward {reward:+.2f}{error_flag}"
247242
)
248243
history.append(history_line)
249244
print(

src/envs/browsergym_env/server/Dockerfile

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,21 +37,29 @@ RUN pip install --no-cache-dir -r /tmp/browsergym_requirements.txt && \
3737
# Install Playwright browsers (Chromium by default)
3838
RUN playwright install chromium
3939

40+
# Install MiniWoB++ tasks
41+
RUN pip install browsergym-miniwob
42+
RUN git clone --depth 1 https://github.com/Farama-Foundation/miniwob-plusplus.git /app/miniwob-plusplus
43+
4044
# Copy OpenEnv core and browsergym_env code
4145
WORKDIR /app
4246
COPY src/core/ /app/src/core/
4347
COPY src/envs/browsergym_env/ /app/src/envs/browsergym_env/
4448
COPY src/envs/browsergym_env/README.md /app/README.md
49+
RUN chmod +x /app/src/envs/browsergym_env/server/start.sh
4550

4651
# Set environment variables
4752
ENV PYTHONPATH=/app/src
4853
ENV PYTHONUNBUFFERED=1
4954
ENV BROWSERGYM_BENCHMARK=miniwob
50-
ENV BROWSERGYM_TASK_NAME=""
55+
ENV BROWSERGYM_TASK_NAME="click-test"
5156
ENV BROWSERGYM_HEADLESS=true
5257
ENV BROWSERGYM_VIEWPORT_WIDTH=1280
5358
ENV BROWSERGYM_VIEWPORT_HEIGHT=720
5459
ENV BROWSERGYM_TIMEOUT=10000
60+
ENV MINIWOB_HTML_DIR=/app/miniwob-plusplus/miniwob/html
61+
ENV MINIWOB_HTTP_PORT=8888
62+
ENV MINIWOB_URL=http://127.0.0.1:8888/miniwob/
5563

5664
# For WebArena tasks, these should be set by the user when running the container:
5765
# ENV SHOPPING=
@@ -63,8 +71,9 @@ ENV BROWSERGYM_TIMEOUT=10000
6371
# ENV HOMEPAGE=
6472

6573
EXPOSE 8000
74+
EXPOSE 8888
6675

6776
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
6877
CMD curl -f http://localhost:8000/health || exit 1
6978

70-
CMD ["uvicorn", "envs.browsergym_env.server.app:app", "--host", "0.0.0.0", "--port", "8000"]
79+
CMD ["/app/src/envs/browsergym_env/server/start.sh"]

src/envs/browsergym_env/server/app.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
viewport_width = int(os.environ.get("BROWSERGYM_VIEWPORT_WIDTH", "1280"))
1717
viewport_height = int(os.environ.get("BROWSERGYM_VIEWPORT_HEIGHT", "720"))
1818
timeout = float(os.environ.get("BROWSERGYM_TIMEOUT", "10000"))
19+
port = int(os.environ.get("BROWSERGYM_PORT", "8000"))
1920

2021
# Create the environment instance
2122
env = BrowserGymEnvironment(
@@ -38,4 +39,4 @@
3839
if __name__ == "__main__":
3940
import uvicorn
4041

41-
uvicorn.run(app, host="0.0.0.0", port=8000)
42+
uvicorn.run(app, host="0.0.0.0", port=port)

src/envs/browsergym_env/server/browsergym_environment.py

Lines changed: 47 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"""
1010

1111
import importlib
12+
import os
1213
from typing import Any, Dict, Optional
1314
from uuid import uuid4
1415

@@ -22,6 +23,17 @@
2223
)
2324

2425

26+
_MINIWOB_LOAD_HELP = (
27+
"MiniWoB tasks require the MiniWoB HTML bundle to be served over HTTP. "
28+
"The official BrowserGym Docker image handles this automatically by "
29+
"serving the bundle on port 8888. For custom or non-Docker deployments, "
30+
"clone the MiniWoB++ repository, start a static server inside "
31+
"`miniwob-plusplus/miniwob/html` (e.g. `python -m http.server 8888`), and "
32+
"set the MINIWOB_URL environment variable to the served base URL such as "
33+
"`http://localhost:8888/miniwob/`."
34+
)
35+
36+
2537
class BrowserGymEnvironment(Environment):
2638
"""BrowserGym environment wrapper for OpenEnv.
2739
@@ -59,7 +71,7 @@ def __init__(
5971
self.viewport_width = viewport_width
6072
self.viewport_height = viewport_height
6173
self.timeout = timeout
62-
self.gym_kwargs = gym_kwargs
74+
self.gym_kwargs = dict(gym_kwargs)
6375

6476
# Build environment ID
6577
if task_name:
@@ -69,10 +81,10 @@ def __init__(
6981

7082
# force import the benchmark module
7183
benchmark_modules = {
72-
"miniwob": "browsergym.envs.miniwob",
73-
"webarena": "browsergym.envs.webarena",
74-
"visualwebarena": "browsergym.envs.visualwebarena",
75-
"workarena": "browsergym.envs.workarena",
84+
"miniwob": "browsergym.miniwob",
85+
"webarena": "browsergym.webarena",
86+
"visualwebarena": "browsergym.visualwebarena",
87+
"workarena": "browsergym.workarena",
7688
}
7789
module_path = benchmark_modules.get(benchmark)
7890
try:
@@ -81,10 +93,13 @@ def __init__(
8193
else:
8294
importlib.import_module("browsergym")
8395
except ModuleNotFoundError as import_error:
84-
raise ValueError(
85-
f"Failed to import BrowserGym benchmark '{benchmark}': {import_error}\n"
86-
f"Make sure the package browsergym-{benchmark} is installed."
87-
) from import_error
96+
message = (
97+
"Failed to import BrowserGym benchmark "
98+
f"'{benchmark}': {import_error}\n"
99+
"Install the matching browsergym package "
100+
f"(e.g., browsergym-{benchmark})."
101+
)
102+
raise ValueError(message) from import_error
88103

89104
# Create the BrowserGym environment
90105
try:
@@ -93,13 +108,16 @@ def __init__(
93108
headless=headless,
94109
viewport={"width": viewport_width, "height": viewport_height},
95110
timeout=timeout,
96-
**gym_kwargs,
111+
**self.gym_kwargs,
97112
)
98-
except Exception as e:
99-
raise ValueError(
100-
f"Failed to create BrowserGym environment '{self.env_id}': {e}\n"
101-
f"Make sure the benchmark is installed (e.g., pip install browsergym-{benchmark})"
113+
except Exception as e: # noqa: BLE001 - gym.make
114+
message = (
115+
"Failed to create BrowserGym environment "
116+
f"'{self.env_id}': {e}\n"
117+
"Make sure the benchmark package is installed "
118+
f"(e.g., pip install browsergym-{benchmark})."
102119
)
120+
raise ValueError(message) from e
103121

104122
# State tracking
105123
self._state = BrowserGymState(
@@ -140,7 +158,21 @@ def reset(
140158
reset_options["seed"] = seed
141159

142160
# Reset the gym environment
143-
obs, info = self.gym_env.reset(**reset_options)
161+
try:
162+
obs, info = self.gym_env.reset(**reset_options)
163+
except AttributeError as err:
164+
if "context" in str(err) and hasattr(self.gym_env, "close"):
165+
# BrowserGym can leave partially initialized state after a
166+
# failed reset. Close the hanging resources and try once more.
167+
self.gym_env.close()
168+
obs, info = self.gym_env.reset(**reset_options)
169+
else:
170+
raise
171+
except Exception as err: # noqa: BLE001 - browsergym
172+
message = str(err)
173+
if self.benchmark == "miniwob" and "core is not defined" in message:
174+
raise ValueError(_MINIWOB_LOAD_HELP) from err
175+
raise
144176

145177
self._last_obs = obs
146178
self._last_info = info
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
MINIWOB_HTML_DIR=${MINIWOB_HTML_DIR:-/app/miniwob-plusplus/miniwob/html}
5+
MINIWOB_HTTP_PORT=${MINIWOB_HTTP_PORT:-8888}
6+
BROWSERGYM_PORT=${BROWSERGYM_PORT:-8000}
7+
8+
if [ ! -d "${MINIWOB_HTML_DIR}" ]; then
9+
echo "MiniWoB HTML directory not found at ${MINIWOB_HTML_DIR}" >&2
10+
exit 1
11+
fi
12+
13+
python -m http.server "${MINIWOB_HTTP_PORT}" --bind 0.0.0.0 --directory "${MINIWOB_HTML_DIR}" &
14+
HTTP_SERVER_PID=$!
15+
16+
sleep 1
17+
if ! kill -0 "${HTTP_SERVER_PID}" 2>/dev/null; then
18+
echo "Failed to start MiniWoB static server on port ${MINIWOB_HTTP_PORT}" >&2
19+
exit 1
20+
fi
21+
22+
cleanup() {
23+
kill "${HTTP_SERVER_PID}" 2>/dev/null || true
24+
}
25+
26+
trap cleanup EXIT INT TERM
27+
28+
exec uvicorn envs.browsergym_env.server.app:app --host 0.0.0.0 --port "${BROWSERGYM_PORT}"
29+

0 commit comments

Comments
 (0)