Spaces:
Running
Running
File size: 19,179 Bytes
e6ce96e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 | # Browser Agent Component
This document describes the browser agent tool used by the HARvestGym RL agent β how it works, how to build it, and how it integrates with the environment.
---
## What It Is
The browser agent is a multi-stage tool the RL agent calls at the start of every episode. Given a natural language task and a URL, it:
1. **Checks if a pre-recorded HAR file exists** for this application
2. If HAR exists β loads it directly (no browser launched)
3. If no HAR β **launches a real browser** (Chromium via Playwright), connects an LLM, performs the task, and records all network traffic as a HAR file
4. **Processes the HAR** (from either source) to extract an OpenAPI-like spec
5. **Builds GEMMA embeddings** over the extracted spec so `search_endpoints()` can do semantic search
6. **Returns a summary** β the list of API endpoint names and HTTP methods only
The browser agent is a script that orchestrates multiple processing stages. The RL agent sees only the final summary output β a list of endpoints like `GET /products`, `POST /guest-carts`. No headers, no body schemas, no parameter details. To get full details about any endpoint, the agent calls `search_endpoints()` with a natural language query β this searches the GEMMA embeddings built during the browser agent's processing stage.
---
## Library: browser-use
**Repository:** [browser-use/browser-use](https://github.com/browser-use/browser-use)
**Stars:** 86k+ (April 2026)
**License:** MIT
**Language:** Python 3.11+
`browser-use` connects any LLM to a Playwright-controlled browser. The LLM receives the page state (DOM, screenshot, or both), decides on an action (click, type, navigate, extract), and `browser-use` executes it. It uses a sense-plan-act loop with built-in error handling.
Install:
```bash
pip install browser-use
playwright install chromium
```
---
## How It Works: Full Pipeline
### Stage 1 β Obtain HAR Data
The browser agent first checks whether a pre-recorded HAR file exists for the target application. If it does, the browser is never launched β this saves 30β120 seconds per episode.
```python
import json, os
from urllib.parse import urlparse
HAR_MAP = {
":7770": "hars/shopping.har",
":7780": "hars/shopping_admin.har",
":9999": "hars/forum.har",
":3000": "hars/osm.har",
":8888": "hars/wikipedia.har",
}
def resolve_har_path(url: str) -> str | None:
"""Check if a pre-recorded HAR exists for this app."""
for port_key, path in HAR_MAP.items():
if port_key in url:
if os.path.exists(path):
return path
return None
async def get_har_data(task: str, url: str, llm_model: str) -> dict:
"""
Stage 1: Get HAR data β from file if available, from live browser otherwise.
Returns the parsed HAR JSON.
"""
har_path = resolve_har_path(url)
if har_path:
# HAR exists β load directly, no browser needed
with open(har_path) as f:
return json.load(f)
else:
# No HAR β run live browser session and capture traffic
raw_log = await run_browser_agent_live(task, url, llm_model)
return convert_raw_log_to_har(raw_log)
```
### Stage 2 β Live Browser Session (only if no HAR exists)
When no pre-recorded HAR is available, the browser agent launches a real Chromium browser, connects the LLM, and performs the task while intercepting all network traffic:
```python
from playwright.async_api import async_playwright
from browser_use import Agent
from langchain_openai import ChatOpenAI
async def run_browser_agent_live(task: str, url: str, llm_model: str) -> list[dict]:
"""
Runs browser-use agent on the given task, intercepts all network traffic,
returns raw request/response log.
"""
requests_log = []
async with async_playwright() as p:
browser = await p.chromium.launch(headless=False)
context = await browser.new_context()
page = await context.new_page()
# Attach network interceptor
async def on_request(request):
requests_log.append({
"type": "request",
"url": request.url,
"method": request.method,
"headers": dict(request.headers),
"post_data": request.post_data,
})
async def on_response(response):
try:
body = await response.text()
except Exception:
body = None
requests_log.append({
"type": "response",
"url": response.url,
"status": response.status,
"headers": dict(response.headers),
"body": body,
})
page.on("request", on_request)
page.on("response", on_response)
# Navigate to app first
await page.goto(url)
# Run browser agent
llm = ChatOpenAI(model=llm_model, base_url="https://router.huggingface.co/v1")
agent = Agent(task=task, llm=llm, page=page)
await agent.run()
await browser.close()
return requests_log
```
### Stage 3 β Filter and Extract OpenAPI-like Spec
The HAR data (from either source) contains everything: fonts, analytics, CDN requests, JS bundles, CSS. The browser agent filters this down and extracts a structured OpenAPI-like specification:
```python
import re
from urllib.parse import urlparse
SKIP_EXTENSIONS = {".css", ".png", ".jpg", ".svg", ".ico", ".woff", ".woff2", ".ttf", ".gif"}
SKIP_DOMAINS = {"google-analytics.com", "doubleclick.net", "cloudflare.com", "cdn.", "fonts.googleapis.com"}
SKIP_PATH_PREFIXES = ["/static/", "/media/", "/_next/", "/assets/", "/__webpack"]
def is_application_api_call(url: str, app_base_url: str) -> bool:
parsed = urlparse(url)
app_host = urlparse(app_base_url).netloc
if parsed.netloc != app_host:
return False
path = parsed.path.lower()
for ext in SKIP_EXTENSIONS:
if path.endswith(ext):
return False
for prefix in SKIP_PATH_PREFIXES:
if path.startswith(prefix):
return False
return True
def extract_openapi_spec(har_data: dict, app_base_url: str) -> list[dict]:
"""
Stage 3: Process HAR entries into an OpenAPI-like spec.
Each entry becomes a structured endpoint document with method, path,
query params, request body schema, response body schema, status codes, auth info.
"""
entries = har_data["log"]["entries"]
seen = set()
spec_entries = []
for entry in entries:
req = entry["request"]
resp = entry["response"]
raw_url = req["url"]
method = req["method"]
# Filter non-API traffic
if not is_application_api_call(raw_url, app_base_url):
continue
# Skip HTML page navigations
content_type = _get_response_content_type(resp)
if "text/html" in content_type and method == "GET":
continue
# Normalise path: replace IDs with {id}
parsed = urlparse(raw_url)
path = _normalise_path(parsed.path)
# Deduplicate by (method, normalised_path)
key = f"{method} {path}"
if key in seen:
continue
seen.add(key)
# Extract auth info
has_auth = any(
h["name"].lower() in ("authorization", "x-api-key", "cookie")
for h in req["headers"]
)
# Build endpoint spec document
spec_entry = {
"method": method,
"path": path,
"query_params": parsed.query or None,
"request_headers": {h["name"]: h["value"] for h in req["headers"]
if h["name"].lower() in ("content-type", "authorization", "x-requested-with")},
"request_body": _extract_body(req),
"status_code": resp["status"],
"response_content_type": content_type,
"response_body_sample": _truncate_body(resp),
"auth_observed": has_auth,
}
spec_entries.append(spec_entry)
return spec_entries
```
### Stage 4 β Build GEMMA Embeddings for Search
The extracted spec entries are converted to text documents and embedded using GEMMA embeddings. These embeddings power the `search_endpoints()` tool β when the RL agent queries "how to add item to cart", the semantic search finds the matching endpoint spec.
```python
from sentence_transformers import SentenceTransformer
import numpy as np
def build_endpoint_embeddings(spec_entries: list[dict], app_name: str) -> tuple[np.ndarray, list[str]]:
"""
Stage 4: Convert spec entries to text chunks and build GEMMA embeddings.
These embeddings are stored in memory for the duration of the episode,
enabling search_endpoints() to do semantic search.
"""
model = SentenceTransformer("google/embeddinggemma-300m")
chunks = []
for entry in spec_entries:
chunk = spec_entry_to_text(entry, app_name)
chunks.append(chunk)
# GEMMA encode_document: "title: {endpoint} | text: {rest of chunk}"
embeddings = model.encode_document(chunks, batch_size=32)
# Use similarity metric from google/embeddinggemma-300m model card
return embeddings, chunks
def spec_entry_to_text(entry: dict, app_name: str) -> str:
"""Convert a single spec entry to a searchable text document."""
parts = [
f"app: {app_name}",
f"endpoint: {entry['method']} {entry['path']}",
f"status: {entry['status_code']}",
f"auth: {'required' if entry['auth_observed'] else 'none'}",
]
if entry.get("query_params"):
parts.append(f"query: {entry['query_params']}")
if entry.get("request_body"):
parts.append(f"body: {entry['request_body']}")
if entry.get("response_body_sample"):
parts.append(f"response_sample: {entry['response_body_sample']}")
return " | ".join(parts)
```
### Stage 5 β Return Summary to RL Agent
The browser agent returns **only a summary** β endpoint names and HTTP methods. No headers, no body schemas, no parameter details. The agent must call `search_endpoints()` to get the full details.
```python
def build_browser_agent_output(spec_entries: list[dict], app_name: str) -> dict:
"""
Stage 5: Build the summary output returned to the RL agent.
This is intentionally sparse β just endpoint names and methods.
"""
summary_endpoints = []
for entry in spec_entries:
summary_endpoints.append({
"method": entry["method"],
"path": entry["path"],
})
return {
"app": app_name,
"endpoints": summary_endpoints,
"total_endpoints": len(summary_endpoints),
"note": (
"These endpoints were observed for this application. "
"Use search_endpoints() with a natural language query to get "
"the full schema, parameters, and auth details for any endpoint."
)
}
```
### Full Orchestration
```python
async def browser_agent(task: str, url: str) -> dict:
"""
Complete browser agent pipeline:
1. Get HAR data (from file or live browser)
2. Filter and extract OpenAPI-like spec
3. Build GEMMA embeddings for search_endpoints()
4. Return summary endpoint list to RL agent
"""
app_name = resolve_app_name(url)
llm_model = "browser-use/bu-30b-a3b-preview"
# Stage 1-2: Get HAR data
har_data = await get_har_data(task, url, llm_model)
# Stage 3: Extract OpenAPI-like spec
spec_entries = extract_openapi_spec(har_data, url)
# Stage 4: Build GEMMA embeddings (stored in environment for search_endpoints)
embeddings, chunks = build_endpoint_embeddings(spec_entries, app_name)
store_episode_embeddings(app_name, embeddings, chunks) # makes search_endpoints() work
# Stage 5: Return summary to RL agent
return build_browser_agent_output(spec_entries, app_name)
```
---
## Output Example
What the RL agent sees (summary only β no schemas, no headers, no body details):
```json
{
"app": "shopping",
"endpoints": [
{"method": "POST", "path": "/rest/V1/integration/customer/token"},
{"method": "GET", "path": "/rest/V1/products"},
{"method": "GET", "path": "/rest/V1/products/{id}"},
{"method": "POST", "path": "/rest/V1/guest-carts"},
{"method": "POST", "path": "/rest/V1/guest-carts/{id}/items"},
{"method": "GET", "path": "/rest/V1/guest-carts/{id}/totals"},
{"method": "POST", "path": "/rest/V1/guest-carts/{id}/order"},
{"method": "GET", "path": "/rest/V1/categories"}
],
"total_endpoints": 8,
"note": "These endpoints were observed for this application. Use search_endpoints() with a natural language query to get the full schema, parameters, and auth details for any endpoint."
}
```
To get full details, the agent calls:
```
search_endpoints("add item to guest cart")
β returns full schema: POST /rest/V1/guest-carts/{cartId}/items, body params, auth, response fields
```
---
## How search_endpoints() Uses the Embeddings
The GEMMA embeddings built in Stage 4 are what power `search_endpoints()`. When the RL agent calls `search_endpoints("create guest cart")`:
1. The query is encoded using GEMMA `encode_query`
2. Dot product similarity against all endpoint embeddings
3. Top-3 matching endpoint spec documents are returned with full details
```python
def search_endpoints(query: str, embeddings, texts, model, top_k=3) -> list[str]:
q_emb = model.encode_query(query) # shape: (D,)
# Use similarity metric specified by google/embeddinggemma-300m model card
scores = model.similarity(q_emb, embeddings).squeeze(0) # shape: (N,)
top_idx = np.argsort(scores)[::-1][:top_k]
return [texts[i] for i in top_idx]
```
The endpoint documents returned by search contain the full extracted spec β method, path, query params, request body structure, response samples, auth requirements. This is the detailed view that complements the summary list from `browser_agent`.
---
## LLM Choice for Browser Agent
We use **[`browser-use/bu-30b-a3b-preview`](https://huggingface.co/browser-use/bu-30b-a3b-preview)** β a model purpose-built and fine-tuned specifically for browser-use tasks.
| Property | Value |
|----------|-------|
| **Base model** | Qwen3-VL-30B-A3B-Instruct |
| **Architecture** | Vision-Language MoE (Mixture of Experts) |
| **Total parameters** | 30B |
| **Active parameters** | 3B (MoE β only 3B fire per forward pass) |
| **Context length** | 65,536 tokens |
| **Specialization** | Superior DOM understanding + visual reasoning for web tasks |
This model is designed to be served with vLLM and integrates directly with the `browser-use` library via its `ChatOpenAI`-compatible interface:
```python
from browser_use import Agent, ChatOpenAI
llm = ChatOpenAI(
base_url="http://localhost:8000/v1", # vLLM server
model="browser-use/bu-30b-a3b-preview",
temperature=0.6,
top_p=0.95,
dont_force_structured_output=True, # speeds up inference
)
agent = Agent(task=task, llm=llm)
agent.run_sync()
```
Serve with vLLM:
```bash
vllm serve browser-use/bu-30b-a3b-preview \
--max-model-len 65536 \
--host 0.0.0.0 \
--port 8000
```
Because 3B parameters are active per forward pass (MoE), this model is fast enough for deployment without requiring a full large-model GPU allocation.
---
## Training vs. Inference: What Changes
```
Training Inference
β β
browser_agent β HAR file exists β loads from β HAR file may not exist
Stage 1 β disk, no browser launched β β launches live browser session
β β β records traffic as HAR
β β
browser_agent β Processes HAR β extracts spec β Same processing pipeline
Stages 2-4 β β builds GEMMA embeddings β on the live-captured traffic
β β returns summary β
β β
curl_exec β hits REAL live server β hits REAL live server
calls β (WebArena EC2) β (WebArena EC2)
β β
judge β probes REAL live server β probes REAL live server
verification β to verify task completion β to verify task completion
```
**What changes between training and inference:** only Stage 1 β where the HAR data comes from. During training, pre-recorded HAR files exist for all tasks, so the browser is never launched. At inference, the HAR may not exist for novel tasks, so the browser runs live.
**What never changes:** Stages 3-5 (spec extraction, embedding, summary output) run identically regardless of the HAR source. And `curl_exec` always hits the real live server β no responses are ever mocked.
---
## Integration with the Environment
```
RL Environment (FastAPI server)
β
βββ receives Action: {tool: "browser_agent", input: {task, url}}
β
βββ Stage 1: HAR file exists?
β βββ YES β load HAR from disk (~0ms)
β βββ NO β spawn live browser session (30-120s)
β βββ Playwright + bu-30b-a3b-preview
β βββ intercept all HTTP traffic
β βββ produce HAR data
β
βββ Stage 3: Extract OpenAPI-like spec from HAR
β
βββ Stage 4: Build GEMMA embeddings β stored in env for search_endpoints()
β
βββ Stage 5: Return summary endpoint list as Observation.last_tool_result
β
β (agent now knows WHAT endpoints exist, but not HOW to call them)
β
βΌ
search_endpoints("natural language query")
β β semantic search over GEMMA embeddings
β β returns full endpoint schema with params, auth, response fields
βΌ
curl_exec("curl -X POST ...")
β β executes against real live WebArena server (EC2)
β β indexes full response into episode BM25 store
βΌ
search_episode_data("keyword query")
β β BM25 search over indexed responses from this episode
βΌ
done() β judge evaluates against ground truth
```
---
## Reference Tools
- [browser-use GitHub](https://github.com/browser-use/browser-use) β the core library
- [browser-use docs](https://docs.browser-use.com) β configuration, custom actions, LLM setup
- [Playwright network events](https://playwright.dev/python/docs/network) β request/response interception API
- [har-to-openapi](https://github.com/jonluca/har-to-openapi) β alternative: convert HAR files to OpenAPI spec format
- [jsluice](https://github.com/BishopFox/jsluice) β extract API routes from JavaScript bundles (useful supplement to network interception) - Future Scope
|