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