sergiopaniego HF Staff commited on
Commit
5ce546d
·
verified ·
1 Parent(s): 0e9aac9

Upload folder using huggingface_hub

Browse files
Files changed (10) hide show
  1. Dockerfile +11 -30
  2. README.md +32 -0
  3. __init__.py +33 -0
  4. harness.py +290 -0
  5. models.py +1 -1
  6. pyproject.toml +6 -11
  7. server/app.py +7 -12
  8. server/browsergym_environment.py +5 -7
  9. server/requirements.txt +4 -7
  10. uv.lock +0 -0
Dockerfile CHANGED
@@ -1,35 +1,14 @@
1
- # Use public Python base image for HuggingFace compatibility
2
- FROM python:3.11-slim
3
 
4
  # Set working directory
5
  WORKDIR /app/env
6
 
7
- # Install system dependencies for Playwright and browsers
8
  RUN apt-get update && apt-get install -y --no-install-recommends \
9
- # Playwright browser dependencies
10
- libnss3 \
11
- libnspr4 \
12
- libatk1.0-0 \
13
- libatk-bridge2.0-0 \
14
- libcups2 \
15
- libdrm2 \
16
- libdbus-1-3 \
17
- libxkbcommon0 \
18
- libatspi2.0-0 \
19
- libxcomposite1 \
20
- libxdamage1 \
21
- libxfixes3 \
22
- libxrandr2 \
23
- libgbm1 \
24
- libpango-1.0-0 \
25
- libcairo2 \
26
- libasound2 \
27
- libxshmfence1 \
28
  fonts-unifont \
29
  fonts-noto-color-emoji \
30
- # Additional dependencies
31
  git \
32
- wget \
33
  curl \
34
  && rm -rf /var/lib/apt/lists/*
35
 
@@ -40,15 +19,17 @@ COPY . .
40
  # Make start script executable
41
  RUN chmod +x /app/env/server/start.sh
42
 
43
- # Install Python dependencies using pip install -e . (from pyproject.toml)
44
- RUN pip install --no-cache-dir -e .
45
 
46
- # Install Playwright browsers (Chromium by default)
47
- # Use python -m since playwright command might not be in PATH
48
  RUN python -m playwright install chromium
49
 
50
- # Install MiniWoB++ tasks
51
- RUN git clone --depth 1 https://github.com/Farama-Foundation/miniwob-plusplus.git /app/miniwob-plusplus
 
 
 
52
 
53
  # Set environment variables
54
  ENV PYTHONUNBUFFERED=1
 
1
+ # Use the matching Playwright Python image for browsergym-core 0.14.3.
2
+ FROM mcr.microsoft.com/playwright/python:v1.44.0-jammy
3
 
4
  # Set working directory
5
  WORKDIR /app/env
6
 
7
+ # Install only the extra packages this env still needs on top of the Playwright image.
8
  RUN apt-get update && apt-get install -y --no-install-recommends \
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  fonts-unifont \
10
  fonts-noto-color-emoji \
 
11
  git \
 
12
  curl \
13
  && rm -rf /var/lib/apt/lists/*
14
 
 
19
  # Make start script executable
20
  RUN chmod +x /app/env/server/start.sh
21
 
22
+ RUN pip install --no-cache-dir uv
23
+ RUN uv pip install --system --no-cache -e .
24
 
25
+ # Ensure the installed Playwright package has a matching Chromium runtime.
 
26
  RUN python -m playwright install chromium
27
 
28
+ # Install MiniWoB++ from a pinned snapshot to avoid flaky Hub-time git clones.
29
+ ARG MINIWOB_COMMIT=7fd85d71a4b60325c6585396ec4f48377d049838
30
+ RUN mkdir -p /app/miniwob-plusplus && \
31
+ curl -L "https://github.com/Farama-Foundation/miniwob-plusplus/archive/${MINIWOB_COMMIT}.tar.gz" \
32
+ | tar -xz --strip-components=1 -C /app/miniwob-plusplus
33
 
34
  # Set environment variables
35
  ENV PYTHONUNBUFFERED=1
README.md CHANGED
@@ -73,6 +73,38 @@ for episode in range(1000):
73
  env.close()
74
  ```
75
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  ### Available Tasks by Benchmark
77
 
78
  #### MiniWoB++ Tasks (Training - 100+ tasks)
 
73
  env.close()
74
  ```
75
 
76
+ ## Harness Sessions for TRL
77
+
78
+ If you want BrowserGym to participate in a tool-driven harness instead of a
79
+ hand-written `env.reset()` / `env.step()` loop, use the BrowserGym session
80
+ factory:
81
+
82
+ ```python
83
+ from browsergym_env import BrowserGymEnv
84
+ from browsergym_env.harness import BrowserGymSessionFactory
85
+ from openenv.core.harness import (
86
+ HarnessRunLimits,
87
+ MCPHarnessAdapter,
88
+ build_harness_rollout_func,
89
+ )
90
+
91
+ session_factory = BrowserGymSessionFactory(
92
+ client_factory=lambda: BrowserGymEnv(base_url="https://openenv-browsergym-env.hf.space"),
93
+ )
94
+
95
+ rollout_func = build_harness_rollout_func(
96
+ session_factory=session_factory,
97
+ harness_adapter=MCPHarnessAdapter(),
98
+ model_step_builder=..., # trainer-owned model sampling
99
+ limits=HarnessRunLimits(max_turns=10),
100
+ )
101
+ ```
102
+
103
+ BrowserGym exposes `click`, `fill`, `send_keys`, `scroll`, and `noop` as MCP-style
104
+ tools while still translating them back into the underlying `BrowserGymAction`
105
+ strings. See [examples/browsergym_harness.py](https://github.com/meta-pytorch/OpenEnv/blob/main/examples/browsergym_harness.py)
106
+ for a full TRL-oriented example.
107
+
108
  ### Available Tasks by Benchmark
109
 
110
  #### MiniWoB++ Tasks (Training - 100+ tasks)
__init__.py CHANGED
@@ -61,12 +61,45 @@ Evaluation Example (WebArena - requires backend):
61
  ```
62
  """
63
 
 
 
 
 
 
64
  from .client import BrowserGymEnv
65
  from .models import BrowserGymAction, BrowserGymObservation, BrowserGymState
66
 
 
 
 
67
  __all__ = [
68
  "BrowserGymEnv",
69
  "BrowserGymAction",
70
  "BrowserGymObservation",
71
  "BrowserGymState",
 
 
72
  ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  ```
62
  """
63
 
64
+ from __future__ import annotations
65
+
66
+ from importlib import import_module
67
+ from typing import TYPE_CHECKING
68
+
69
  from .client import BrowserGymEnv
70
  from .models import BrowserGymAction, BrowserGymObservation, BrowserGymState
71
 
72
+ if TYPE_CHECKING:
73
+ from .harness import BrowserGymSessionFactory, build_browsergym_action_tool_call
74
+
75
  __all__ = [
76
  "BrowserGymEnv",
77
  "BrowserGymAction",
78
  "BrowserGymObservation",
79
  "BrowserGymState",
80
+ "BrowserGymSessionFactory",
81
+ "build_browsergym_action_tool_call",
82
  ]
83
+
84
+ _LAZY_ATTRS = {
85
+ "BrowserGymSessionFactory": (".harness", "BrowserGymSessionFactory"),
86
+ "build_browsergym_action_tool_call": (
87
+ ".harness",
88
+ "build_browsergym_action_tool_call",
89
+ ),
90
+ }
91
+
92
+
93
+ def __getattr__(name: str):
94
+ if name not in _LAZY_ATTRS:
95
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
96
+
97
+ module_path, attr_name = _LAZY_ATTRS[name]
98
+ module = import_module(module_path, __name__)
99
+ value = getattr(module, attr_name)
100
+ globals()[name] = value
101
+ return value
102
+
103
+
104
+ def __dir__() -> list[str]:
105
+ return sorted(set(globals().keys()) | set(__all__))
harness.py ADDED
@@ -0,0 +1,290 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Harness-oriented BrowserGym session adapters."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import ast
6
+ import json
7
+ from typing import Any
8
+
9
+ from openenv.core.env_server.mcp_types import Tool
10
+ from openenv.core.harness import (
11
+ ResourceSessionFactory,
12
+ StepEnvSessionAdapter,
13
+ ToolResult,
14
+ VerifyResult,
15
+ )
16
+ from openenv.core.llm_client import ToolCall
17
+
18
+ from .models import BrowserGymAction
19
+
20
+ _BROWSERGYM_TOOLS = [
21
+ Tool(
22
+ name="click",
23
+ description="Click an element by BrowserGym bid.",
24
+ input_schema={
25
+ "type": "object",
26
+ "properties": {
27
+ "bid": {"type": "string"},
28
+ },
29
+ "required": ["bid"],
30
+ },
31
+ ),
32
+ Tool(
33
+ name="fill",
34
+ description="Fill an input field by bid.",
35
+ input_schema={
36
+ "type": "object",
37
+ "properties": {
38
+ "bid": {"type": "string"},
39
+ "text": {"type": "string"},
40
+ },
41
+ "required": ["bid", "text"],
42
+ },
43
+ ),
44
+ Tool(
45
+ name="send_keys",
46
+ description="Send keyboard input to the page.",
47
+ input_schema={
48
+ "type": "object",
49
+ "properties": {
50
+ "text": {"type": "string"},
51
+ },
52
+ "required": ["text"],
53
+ },
54
+ ),
55
+ Tool(
56
+ name="scroll",
57
+ description="Scroll the page up or down.",
58
+ input_schema={
59
+ "type": "object",
60
+ "properties": {
61
+ "direction": {"type": "string", "enum": ["up", "down"]},
62
+ },
63
+ "required": ["direction"],
64
+ },
65
+ ),
66
+ Tool(
67
+ name="noop",
68
+ description="Take no action on the current page.",
69
+ input_schema={"type": "object", "properties": {}},
70
+ ),
71
+ ]
72
+
73
+
74
+ def _quote(value: str) -> str:
75
+ return json.dumps(value, ensure_ascii=False)
76
+
77
+
78
+ def build_browsergym_action_str(tool_name: str, arguments: dict[str, Any]) -> str:
79
+ """Convert a BrowserGym tool call into the action string the env expects."""
80
+
81
+ if tool_name == "click":
82
+ return f"click({_quote(str(arguments['bid']))})"
83
+ if tool_name == "fill":
84
+ return (
85
+ f"fill({_quote(str(arguments['bid']))}, {_quote(str(arguments['text']))})"
86
+ )
87
+ if tool_name == "send_keys":
88
+ return f"send_keys({_quote(str(arguments['text']))})"
89
+ if tool_name == "scroll":
90
+ return f"scroll({_quote(str(arguments['direction']))})"
91
+ if tool_name == "noop":
92
+ return "noop()"
93
+
94
+ raise KeyError(f"Unsupported BrowserGym tool: {tool_name}")
95
+
96
+
97
+ def _format_browsergym_prompt(observation: Any, task: Any) -> str:
98
+ goal = getattr(observation, "goal", "") or (task or "")
99
+ page_text = getattr(observation, "axtree_txt", "") or getattr(
100
+ observation, "text", ""
101
+ )
102
+ error = getattr(observation, "error", "")
103
+
104
+ parts = []
105
+ if goal:
106
+ parts.append(f"Goal: {goal}")
107
+ if error:
108
+ parts.append(f"Previous action error: {error}")
109
+ if page_text:
110
+ parts.append(f"Page structure:\n{page_text}")
111
+ parts.append("Choose the next browser action.")
112
+ return "\n\n".join(parts)
113
+
114
+
115
+ def _build_browsergym_tool_result(
116
+ task: Any,
117
+ ):
118
+ def builder(
119
+ tool_name: str,
120
+ arguments: dict[str, Any],
121
+ result: Any,
122
+ state: Any,
123
+ ) -> ToolResult:
124
+ observation = result.observation
125
+ data = {
126
+ "tool_name": tool_name,
127
+ "arguments": dict(arguments),
128
+ "goal": getattr(observation, "goal", "") or (task or ""),
129
+ "observation_text": getattr(observation, "axtree_txt", "")
130
+ or getattr(observation, "text", ""),
131
+ "url": getattr(observation, "url", ""),
132
+ "error": getattr(observation, "error", ""),
133
+ "last_action_error": getattr(observation, "last_action_error", False),
134
+ "reward": result.reward,
135
+ "done": result.done,
136
+ }
137
+ metadata = {
138
+ "reward": result.reward,
139
+ "state": state.model_dump() if hasattr(state, "model_dump") else state,
140
+ }
141
+ return ToolResult(data=data, done=bool(result.done), metadata=metadata)
142
+
143
+ return builder
144
+
145
+
146
+ def _build_browsergym_verify(
147
+ transcript: list[dict[str, Any]],
148
+ final_state: Any | None,
149
+ last_result: Any | None,
150
+ state: Any,
151
+ ) -> VerifyResult:
152
+ reward = None if last_result is None else last_result.reward
153
+ done = False if last_result is None else bool(last_result.done)
154
+ metrics = {
155
+ "step_count": getattr(state, "step_count", 0),
156
+ "cum_reward": getattr(state, "cum_reward", reward or 0.0),
157
+ "benchmark": getattr(state, "benchmark", ""),
158
+ "task_name": getattr(state, "task_name", ""),
159
+ }
160
+ artifacts = {
161
+ "final_state": state.model_dump() if hasattr(state, "model_dump") else state,
162
+ "final_rollout": final_state,
163
+ "transcript_length": len(transcript),
164
+ }
165
+ return VerifyResult(
166
+ env_reward=reward,
167
+ done=done,
168
+ metrics=metrics,
169
+ artifacts=artifacts,
170
+ )
171
+
172
+
173
+ class BrowserGymSessionFactory(ResourceSessionFactory):
174
+ """Create BrowserGym-backed resource sessions from client factories."""
175
+
176
+ def __init__(self, client_factory, *, default_task: str | None = None):
177
+ self._client_factory = client_factory
178
+ self._default_task = default_task
179
+
180
+ def create(
181
+ self,
182
+ task: Any,
183
+ seed: int | None = None,
184
+ episode_id: str | None = None,
185
+ ) -> StepEnvSessionAdapter:
186
+ session_task = task if task is not None else self._default_task
187
+ client = self._client_factory()
188
+
189
+ reset_kwargs = {}
190
+ if session_task is not None:
191
+ reset_kwargs["task_name"] = session_task
192
+
193
+ return StepEnvSessionAdapter(
194
+ client=client,
195
+ task=session_task,
196
+ seed=seed,
197
+ episode_id=episode_id,
198
+ tool_specs=list(_BROWSERGYM_TOOLS),
199
+ action_builder=lambda name, arguments: BrowserGymAction(
200
+ action_str=build_browsergym_action_str(name, arguments)
201
+ ),
202
+ initial_messages_builder=lambda result, current_task: [
203
+ {
204
+ "role": "user",
205
+ "content": _format_browsergym_prompt(
206
+ result.observation,
207
+ current_task,
208
+ ),
209
+ }
210
+ ],
211
+ tool_result_builder=_build_browsergym_tool_result(session_task),
212
+ verify_builder=_build_browsergym_verify,
213
+ reset_kwargs=reset_kwargs,
214
+ )
215
+
216
+
217
+ def _parse_action_call(action_text: str) -> tuple[str, list[Any]]:
218
+ try:
219
+ expression = ast.parse(action_text.strip(), mode="eval").body
220
+ except SyntaxError as exc:
221
+ raise ValueError(f"Unsupported BrowserGym action: {action_text}") from exc
222
+
223
+ if not isinstance(expression, ast.Call) or not isinstance(
224
+ expression.func, ast.Name
225
+ ):
226
+ raise ValueError(f"Unsupported BrowserGym action: {action_text}")
227
+ if expression.keywords:
228
+ raise ValueError("BrowserGym action arguments must be positional")
229
+
230
+ args: list[Any] = []
231
+ for arg in expression.args:
232
+ try:
233
+ args.append(ast.literal_eval(arg))
234
+ except (SyntaxError, ValueError) as exc:
235
+ raise ValueError("BrowserGym action arguments must be literals") from exc
236
+
237
+ return expression.func.id, args
238
+
239
+
240
+ def _expect_str(value: Any, argument_name: str) -> str:
241
+ if not isinstance(value, str):
242
+ raise ValueError(f"BrowserGym {argument_name} argument must be a string")
243
+ return value
244
+
245
+
246
+ def build_browsergym_action_tool_call(action_text: str) -> ToolCall:
247
+ """Parse a text BrowserGym action into a structured tool call."""
248
+
249
+ name, args = _parse_action_call(action_text)
250
+ if name == "click" and len(args) == 1:
251
+ return ToolCall(
252
+ id="browsergym-click",
253
+ name="click",
254
+ args={"bid": _expect_str(args[0], "bid")},
255
+ )
256
+ if name == "fill" and len(args) == 2:
257
+ return ToolCall(
258
+ id="browsergym-fill",
259
+ name="fill",
260
+ args={
261
+ "bid": _expect_str(args[0], "bid"),
262
+ "text": _expect_str(args[1], "text"),
263
+ },
264
+ )
265
+ if name == "send_keys" and len(args) == 1:
266
+ return ToolCall(
267
+ id="browsergym-send_keys",
268
+ name="send_keys",
269
+ args={"text": _expect_str(args[0], "text")},
270
+ )
271
+ if name == "scroll" and len(args) == 1:
272
+ direction = _expect_str(args[0], "direction")
273
+ if direction not in {"up", "down"}:
274
+ raise ValueError("BrowserGym scroll direction must be 'up' or 'down'")
275
+ return ToolCall(
276
+ id="browsergym-scroll",
277
+ name="scroll",
278
+ args={"direction": direction},
279
+ )
280
+ if name == "noop" and not args:
281
+ return ToolCall(id="browsergym-noop", name="noop", args={})
282
+
283
+ raise ValueError(f"Unsupported BrowserGym action: {action_text}")
284
+
285
+
286
+ __all__ = [
287
+ "BrowserGymSessionFactory",
288
+ "build_browsergym_action_str",
289
+ "build_browsergym_action_tool_call",
290
+ ]
models.py CHANGED
@@ -21,7 +21,7 @@ class BrowserGymAction(Action):
21
  - "click('Submit button')"
22
  - "fill('username', 'john@example.com')"
23
  - "goto('https://example.com')"
24
- - "scroll(down)"
25
  - "send_keys('Enter')"
26
  """
27
 
 
21
  - "click('Submit button')"
22
  - "fill('username', 'john@example.com')"
23
  - "goto('https://example.com')"
24
+ - "scroll('down')"
25
  - "send_keys('Enter')"
26
  """
27
 
pyproject.toml CHANGED
@@ -8,23 +8,18 @@ version = "0.1.0"
8
  description = "BrowserGym Environment for OpenEnv - Web automation using Playwright"
9
  requires-python = ">=3.10"
10
  dependencies = [
11
- "openenv-core[core]>=0.2.1",
12
- "fastapi>=0.104.0",
13
- "uvicorn[standard]>=0.24.0",
14
- "pydantic>=2.0.0",
15
- "requests>=2.25.0",
16
- "browsergym-core>=0.2.0",
17
- "browsergym-miniwob>=0.2.0",
18
- "browsergym-webarena>=0.2.0",
19
  "gymnasium>=0.29.0",
20
- "playwright>=1.40.0",
21
- "greenlet>=3.1.0", # Required for Python 3.13 compatibility
22
  "Pillow>=10.0.0",
23
  ]
24
 
25
  [project.optional-dependencies]
26
  dev = [
27
- "pytest>=8.0.0",
28
  "pytest-cov>=4.0.0",
29
  "ipykernel>=6.29.5",
30
  ]
 
8
  description = "BrowserGym Environment for OpenEnv - Web automation using Playwright"
9
  requires-python = ">=3.10"
10
  dependencies = [
11
+ "openenv-core[core]>=0.2.3",
12
+ "browsergym-core==0.14.3",
13
+ "browsergym-miniwob==0.14.3",
 
 
 
 
 
14
  "gymnasium>=0.29.0",
15
+ "playwright==1.44.0",
16
+ "greenlet==3.0.3",
17
  "Pillow>=10.0.0",
18
  ]
19
 
20
  [project.optional-dependencies]
21
  dev = [
22
+ "pytest>=9.0.3",
23
  "pytest-cov>=4.0.0",
24
  "ipykernel>=6.29.5",
25
  ]
server/app.py CHANGED
@@ -1,6 +1,7 @@
1
  """FastAPI server for the BrowserGym environment."""
2
 
3
  import os
 
4
 
5
  from browsergym_env.models import BrowserGymAction, BrowserGymObservation
6
  from browsergym_env.server.browsergym_environment import BrowserGymEnvironment
@@ -15,28 +16,22 @@ viewport_height = int(os.environ.get("BROWSERGYM_VIEWPORT_HEIGHT", "720"))
15
  timeout = float(os.environ.get("BROWSERGYM_TIMEOUT", "10000"))
16
  port = int(os.environ.get("BROWSERGYM_PORT", "8000"))
17
 
 
18
 
19
- # Factory function to create BrowserGymEnvironment instances
20
- def create_browsergym_environment():
21
- """Factory function that creates BrowserGymEnvironment with config."""
22
- return BrowserGymEnvironment(
23
  benchmark=benchmark,
24
  task_name=task_name,
25
  headless=headless,
26
  viewport_width=viewport_width,
27
  viewport_height=viewport_height,
28
  timeout=timeout,
29
- )
30
-
31
-
32
- # Create the FastAPI app
33
- # Pass the factory function instead of an instance for WebSocket session support
34
- app = create_app(
35
- create_browsergym_environment,
36
  BrowserGymAction,
37
  BrowserGymObservation,
38
  env_name="browsergym_env",
39
- max_concurrent_envs=64
40
  )
41
 
42
 
 
1
  """FastAPI server for the BrowserGym environment."""
2
 
3
  import os
4
+ from functools import partial
5
 
6
  from browsergym_env.models import BrowserGymAction, BrowserGymObservation
7
  from browsergym_env.server.browsergym_environment import BrowserGymEnvironment
 
16
  timeout = float(os.environ.get("BROWSERGYM_TIMEOUT", "10000"))
17
  port = int(os.environ.get("BROWSERGYM_PORT", "8000"))
18
 
19
+ max_concurrent = int(os.environ.get("MAX_CONCURRENT_ENVS", "8"))
20
 
21
+ app = create_app(
22
+ partial(
23
+ BrowserGymEnvironment,
 
24
  benchmark=benchmark,
25
  task_name=task_name,
26
  headless=headless,
27
  viewport_width=viewport_width,
28
  viewport_height=viewport_height,
29
  timeout=timeout,
30
+ ),
 
 
 
 
 
 
31
  BrowserGymAction,
32
  BrowserGymObservation,
33
  env_name="browsergym_env",
34
+ max_concurrent_envs=max_concurrent,
35
  )
36
 
37
 
server/browsergym_environment.py CHANGED
@@ -95,8 +95,9 @@ class BrowserGymEnvironment(Environment):
95
  provide unified access to multiple web navigation benchmarks.
96
  """
97
 
98
- SUPPORTS_CONCURRENT_SESSIONS: bool = True
99
-
 
100
  def __init__(
101
  self,
102
  benchmark: str = "miniwob",
@@ -386,8 +387,5 @@ class BrowserGymEnvironment(Environment):
386
  if hasattr(self, "gym_env"):
387
  try:
388
  self.gym_env.close()
389
- except Exception:
390
- # Playwright's sync API uses greenlets that can fail when close()
391
- # is called from a different thread (e.g. async server cleanup).
392
- # Swallow the error to avoid crashing the server.
393
- pass
 
95
  provide unified access to multiple web navigation benchmarks.
96
  """
97
 
98
+ SUPPORTS_CONCURRENT_SESSIONS = True
99
+ REQUIRES_SINGLE_THREAD_EXECUTOR = True
100
+
101
  def __init__(
102
  self,
103
  benchmark: str = "miniwob",
 
387
  if hasattr(self, "gym_env"):
388
  try:
389
  self.gym_env.close()
390
+ except Exception as exc: # noqa: BLE001 - browsergym/playwright cleanup
391
+ logger.warning("BrowserGym cleanup failed: %s", exc)
 
 
 
server/requirements.txt CHANGED
@@ -1,10 +1,7 @@
1
- browsergym>=0.10.0
2
- browsergym-core>=0.10.0
3
- browsergym-miniwob>=0.10.0
4
- browsergym-webarena>=0.10.0
5
  gymnasium>=0.29.0
6
- playwright>=1.40.0
 
7
  Pillow>=10.0.0
8
  beautifulsoup4>=4.12.0
9
- fastapi>=0.104.0
10
- uvicorn[standard]>=0.24.0
 
1
+ browsergym-core==0.14.3
2
+ browsergym-miniwob==0.14.3
 
 
3
  gymnasium>=0.29.0
4
+ playwright==1.44.0
5
+ greenlet==3.0.3
6
  Pillow>=10.0.0
7
  beautifulsoup4>=4.12.0
 
 
uv.lock ADDED
The diff for this file is too large to render. See raw diff