Spaces:
Running
Running
File size: 27,103 Bytes
f2c780a e6ce96e f2c780a e6ce96e f2c780a e6ce96e 9eebce3 e6ce96e 9eebce3 e6ce96e 9eebce3 e6ce96e 9eebce3 e6ce96e 9eebce3 e6ce96e 9eebce3 e6ce96e 9eebce3 e6ce96e 9eebce3 e6ce96e 9eebce3 e6ce96e 9eebce3 e6ce96e 9eebce3 e6ce96e 9eebce3 e6ce96e 9eebce3 e6ce96e 9eebce3 e6ce96e 9eebce3 e6ce96e 9eebce3 e6ce96e 9eebce3 e6ce96e 9eebce3 e6ce96e 9eebce3 e6ce96e 9eebce3 e6ce96e 9eebce3 e6ce96e 9eebce3 e6ce96e 9eebce3 e6ce96e 9eebce3 e6ce96e 9eebce3 e6ce96e 9eebce3 e6ce96e 9eebce3 e6ce96e 9eebce3 e6ce96e 9eebce3 e6ce96e 9eebce3 e6ce96e 9eebce3 e6ce96e 9eebce3 e6ce96e 9eebce3 e6ce96e 9eebce3 e6ce96e 9eebce3 e6ce96e 9eebce3 e6ce96e 9eebce3 e6ce96e 9eebce3 e6ce96e 9eebce3 e6ce96e 9eebce3 e6ce96e 9eebce3 e6ce96e 9eebce3 e6ce96e 9eebce3 e6ce96e 9eebce3 e6ce96e 9eebce3 e6ce96e 9eebce3 e6ce96e 9eebce3 e6ce96e 9eebce3 e6ce96e 9eebce3 e6ce96e 9eebce3 e6ce96e 9eebce3 e6ce96e 9eebce3 e6ce96e 9eebce3 e6ce96e 9eebce3 e6ce96e 9eebce3 e6ce96e 9eebce3 e6ce96e 9eebce3 e6ce96e 9eebce3 e6ce96e 9eebce3 e6ce96e 9eebce3 e6ce96e 9eebce3 e6ce96e 9eebce3 e6ce96e 9eebce3 e6ce96e 9eebce3 e6ce96e 9eebce3 e6ce96e 9eebce3 e6ce96e 9eebce3 e6ce96e 9eebce3 e6ce96e 9eebce3 e6ce96e 9eebce3 e6ce96e 9eebce3 e6ce96e 9eebce3 e6ce96e 9eebce3 e6ce96e 9eebce3 e6ce96e 9eebce3 e6ce96e 9eebce3 e6ce96e 9eebce3 e6ce96e 9eebce3 e6ce96e f2c780a 9eebce3 | 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 | ---
title: HARvestGym
emoji: πΈοΈ
colorFrom: blue
colorTo: purple
sdk: docker
pinned: false
tags:
- openenv
- reinforcement-learning
- api-agent
- web-tasks
base_path: /web
---
# HARvestGym
### Can a small model learn to reverse-engineer any web application's API β and complete real tasks through those APIs, without ever opening a browser?
Web applications are full of APIs. Every click in a browser triggers an HTTP call with a precise schema, a specific authentication header, an exact sequence of prerequisites. **HARvestGym trains a small model to do all of that directly** β given a task and a URL, it discovers the relevant endpoints, figures out what each one needs, chains the calls in the right order, and completes the task without any browser.
The model starts with nothing: no schema, no documentation, no endpoint list. It uses tools to explore β issuing requests, inspecting responses, building up its own understanding of how the application works. This is what a developer does when they reverse-engineer an API. The model learns to do the same.
---
## How It Works
```
Task + App URL
β
βΌ
Policy Model (RL Agent)
small model β no prior knowledge of the app
Step 1 βββΊ browser_agent(task, url) β filtered API endpoint list
Step 2+ βββΊ search_endpoints(query) β full schema for a specific endpoint
βββΊ curl_exec(command) β execute HTTP call, get response
βββΊ search_episode_data(query) β search prior response bodies
βββΊ done(result) β declare task complete
β
βΌ
Live WebArena Apps (EC2) βββ real HTTP responses (always live, never mocked)
β
βΌ
Deterministic Judge (compares against ground truth API catalog)
β
βΌ
Reward Signal βββΊ GRPO βββΊ updated policy
```
The agent calls `browser_agent` once at the start β this runs a real browser to complete the same task while recording all network traffic, then returns the filtered list of API endpoints observed. The agent now has a map of what endpoints exist. What it does *not* know:
- which of those endpoints are actually needed for this specific task
- in what order they must be called (you cannot add to a cart before the cart exists)
- where each required parameter value comes from
- how to re-authenticate if a session expires mid-episode
The model must learn to discover all of this on its own.
---
## Architecture
```
TRAINING LOOP
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β
β Task + App URL β
β β β
β βΌ β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Policy Model (RL Agent) β β
β β small model β no prior knowledge of the app β β
β β β β
β β Observation: task + history + session_state + last_result β β
β β β β
β β Step 1 βββΊ browser_agent(task, url) β β
β β Step 2+ βββΊ search_endpoints(query) β β
β β βββΊ curl_exec(command) β β
β β βββΊ search_episode_data(query) β β
β β βββΊ done(result) β β
β ββββββββββ¬ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β β
β ββββββββ΄βββββββββββββββββββββββββββββββ β
β β β β
β βΌ βΌ β
β βββββββββββββββββββββββ βββββββββββββββββββββββββββββββββββββββ β
β β Browser Agent β β Environment β β
β β (step 1 only) β β β β
β β β β β’ Executes curl_exec via subprocessβ β
β β Training: β β β’ Auto-injects session cookies β β
β β Load pre-recorded β β β’ Smart-truncates response bodies β β
β β cached HAR from β β β’ Indexes full responses into β β
β β disk or launch β β per-episode BM25 + GEMMA store β β
β β on real browser β β β’ Manages session_state: cookies, β β
β β β β CSRF tokens, auth headers β β
β β Inference: β ββββββββββββββββ¬βββββββββββββββββββββββ β
β β Launch real browserβ β β
β β via Playwright + β β HTTP calls (always live) β
β β bu-30b-a3b-preview β βΌ β
β β β βββββββββββββββββββββββββββββββββββββββ β
β β Both paths produce: β β WebArena EC2 (live apps) β β
β β β’ Filtered HAR β β β β
β β β’ OpenAPI-like specβ β :7770 Shopping (Magento 2) β β
β β β’ GEMMA embeddings β β :7780 Shopping Admin β β
β β for search_ β β :9999 Forum (Postmill) β β
β β endpoints() β β :8888 Wikipedia (Kiwix) β β
β βββββββββββββββββββββββ β :3000 Map (OpenStreetMap) β β
β ββββββββββββββββ¬βββββββββββββββββββββββ β
β β β
β β episode trajectory β
β βΌ β
β βββββββββββββββββββββββββββββββββββββββ β
β β Deterministic Judge β β
β β β β
β β Per-template programmatic grader: β β
β β β’ Inspects episode trajectory β β
β β β’ Optionally probes live app state β β
β β β’ Verifies parameter sourcing β β
β β (TASK_SPEC / PREV_CALL / β β
β β AUTH_FLOW / STATIC / DERIVED) β β
β β β’ Scores [0.0 β 1.0] β β
β ββββββββββββββββ¬βββββββββββββββββββββββ β
β β β
β βΌ β
β βββββββββββββββββββββββββββββββββββββββ β
β β Reward Signal β β
β β β β
β β Per-step: β β
β β +0.2 valid API call (2xx) β β
β β +0.1 new path explored β β
β β +0.25 correct param sourcing β β
β β β0.15 repeated identical call β β
β β β0.3 browser_agent called again β β
β β β β
β β Episode end: β β
β β +2.0β+5.0 task complete (easyβhardβ β
β β β1.5 task failed β β
β ββββββββββββββββ¬βββββββββββββββββββββββ β
β β β
β βΌ β
β βββββββββββββββββββββββββββββββββββββββ β
β β GRPO (via HF TRL) β β
β β β β
β β 8 parallel rollouts per prompt β β
β β Computes advantages without β β
β β a value function β β
β β Updates policy weights β β
β βββββββββββββββββββββββββββββββββββββββ β
β β β
β ββββΊ updated Policy Model β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
```
---
## Target Applications
All running on a single AWS EC2 instance β real production software, no simulation.
| App | Port | Software |
| -------------- | ---- | ------------------------------------------------- |
| Shopping | 7770 | Magento 2 β open-source e-commerce platform |
| Shopping Admin | 7780 | Magento 2 Admin β backend panel for the same store|
| Forum | 9999 | Postmill β open-source Reddit-like forum |
| Wikipedia | 8888 | Kiwix β read-only offline mirror of Wikipedia |
| Map | 3000 | OpenStreetMap β collaborative mapping platform |
Source: [WebArena environment_docker](https://github.com/web-arena-x/webarena/tree/main/environment_docker)
---
## Tasks
HARvestGym trains on **7 task templates** across three complexity tiers. Each template is a parameterized scenario: one reward function, one ground truth catalog entry, one grader β but potentially hundreds of distinct episode variations produced by substituting different values for the template slots (`{product_name}`, `{category_name}`, etc.).
### Complexity Tiers
| Tier | Characteristic | API calls required |
| ------ | --------------------------------------------- | ------------------ |
| Easy | Single call, no auth | 1 |
| Medium | Auth + 1β2 dependent calls | 2β3 |
| Hard | Multi-step chain with ID threading, full auth | 4β8+ |
The model only graduates to harder templates once it reliably solves easier ones.
### Task Templates
| # | Tier | App | Template | Key Challenge |
| --- | ------ | -------------- | ------------------------------------------------------ | ------------------------------------------------------- |
| 1 | Easy | Shopping | List products in category `{category_name}` | Single GET with query params |
| 2 | Easy | Wikipedia | Retrieve article summary for `{title}` | Single GET, path parameter resolution |
| 3 | Medium | Shopping | Add `{product_name}` to a guest cart | 2 calls: create cart β add item; ID threading |
| 4 | Medium | Forum | Retrieve all posts in `{forum_category}` (authed) | Login β extract session β GET |
| 5 | Hard | Forum | Create a post titled `{title}` in `{category}` | Login β extract CSRF `form_key` β POST with full schema |
| 6 | Hard | Shopping | Guest checkout for `{product_name}` | 5+ chained calls; cart β item β shipping β payment |
| 7 | Hard | Shopping Admin | Create a new product with SKU `{sku}`, price `{price}` | Admin bearer token β full Magento product schema |
**Template parameters** are populated from a static parameter pool built by querying the live applications before training (see `parameter_pools.json`, refreshed via `scripts/build_parameter_pools.py`). Each episode samples randomly from its pool β the model never sees the pool directly, it must discover the correct values through its own API calls.
Each task has a deterministic programmatic grader (score in `[0.0, 1.0]`):
- **Easy graders**: check HTTP response body for expected values
- **Medium graders**: probe application state after episode (e.g., fetch the cart, verify item is present)
- **Hard graders**: verify multi-step state change in the application (e.g., post exists, checkout created)
---
## Spaces
### Observation Space
What the model sees at each step:
```python
class Observation(BaseModel):
task: str # Natural language task
app_base_url: str # Root URL of the target application
last_tool_result: Any # Result of last tool call
history: list[dict] # Full episode trajectory: [{action, tool_result}, ...]
session_state: dict # Auto-managed: cookies, tokens, CSRF values
step_count: int
max_steps: int # 20
```
`session_state` is maintained by the environment β the model decides *when* to authenticate and *which* session values to use; the environment handles *extraction* from `Set-Cookie` headers and response bodies.
**Response truncation** rules applied in order:
1. Non-JSON body (HTML, CSS): truncated to 3,000 characters
2. JSON primitive (string, number): never truncated β these are tokens, IDs
3. Error response (4xx/5xx): never truncated β the model needs every word to self-correct
4. Small JSON (no large arrays): returned as-is
5. Large JSON array (β₯ 3 items): first 2 items shown + `_list_truncated` annotation + hint to call `search_episode_data()`
Every `curl_exec` call indexes the *full* response into a per-episode hybrid index (BM25 + GEMMA embeddings) *before* truncation β so all items are always retrievable even when only 2 were shown.
### Action Space
The model outputs a single tool call per step.
| Tool | Input | Output |
| ---------------------------- | --------------------------------- | ------------------------------------------------------------------------------- |
| `browser_agent(task, url)` | Task string + app base URL | Summary list of API endpoint names + methods (e.g. `GET /products`) |
| `search_endpoints(query)` | Natural language query | Top-3 endpoint schemas (method, path, auth, params with sources, response fields)|
| `curl_exec(command)` | Full curl command string | `{status_code, headers, body}` β body smart-truncated; full body indexed |
| `search_episode_data(query)` | Keyword or natural language query | Top-5 JSON objects from this episode's request/response history |
| `done(result?)` | Optional result string | Ends episode, triggers judge evaluation |
`browser_agent` is called **exactly once per episode at step 1**. Calling it again applies a β0.3 penalty. During training, it loads a cached HAR file; at inference, it launches a live browser session.
Full technical specifications for all tools: [`TOOLS.md`](./TOOLS.md)
### Reward Space
**Per-step:**
| Signal | Value | Trigger |
| ---------------------------- | ------ | -------------------------------------------------------------------- |
| Valid API call (2xx) | +0.2 | `curl_exec` returns 2xx status |
| New path called this episode | +0.1 | Normalized path not called before β discourages looping |
| Correct parameter sourcing | +0.25 | Judge: value came from the correct source type |
| Session value correctly used | +0.1 | Auth token/cookie present and correct in curl call |
| Repeated identical call | β0.15 | Exact duplicate curl command issued twice |
| browser_agent called again | β0.3 | `browser_agent` called after step 1 |
| Malformed curl command | β0.1 | curl cannot be parsed or executed |
| 4xx response (recoverable) | β0.05 | Call failed but episode continues |
**Episode end:**
| Outcome | Reward |
| ----------------------------------------------------------- | ------------------------------------------ |
| Task completed correctly | +2.0 to +5.0 (scales with difficulty tier) |
| Partial completion (right endpoints, wrong param threading) | +0.5 to +1.5 |
| Authentication correctly obtained (even if task fails) | +0.3 |
| Timeout / task failed entirely | β1.5 |
Target signal separation: successful episodes `+3` to `+7`, failed episodes `β2` to `β1`. Required for GRPO.
> **Reward design note:** Pure step-level rewards can teach a model to "look busy" β accumulating exploration rewards while never completing the task. The terminal outcome reward is designed to dominate the sum of all per-step rewards. The curriculum is the primary defense: Easy tasks have a trivially short optimal path (2 steps), so there's no room to accumulate fake exploration reward before the model learns that the terminal reward is what matters.
---
## Key Design Decisions
### Browser Agent as a Discovery Tool
The RL agent has access to a **browser agent tool** powered by [`bu-30b-a3b-preview`](https://huggingface.co/browser-use/bu-30b-a3b-preview) β a 30B MoE vision-language model (3B active parameters) served via the [browser-use](https://github.com/browser-use/browser-use) library on Playwright. When called, it completes the task in a real browser while intercepting all network traffic, then returns the filtered API call list.
**Training vs. inference:** The browser agent output is pre-computed and cached per task during training β the RL model receives it instantly, no live browser session runs. At inference, the browser agent runs live to handle novel tasks.
Full details: [`BROWSER_AGENT.md`](BROWSER_AGENT.md)
### Ground Truth from the Codebase, Not the Browser
The browser agent shows *what* API calls happen. It does not explain *why* β where each parameter comes from or what field constraints exist. That comes from a one-time static analysis of each WebArena application's Docker image source, producing a **ground truth API catalog**:
```
endpoint: POST /rest/V1/guest-carts/{cartId}/items
path_params:
cartId: obtained from: POST /rest/V1/guest-carts β response body
body:
cartItem.sku: the product's SKU, from: GET /rest/V1/products β items[].sku
cartItem.qty: quantity, from: task specification
cartItem.quote_id: same as cartId
```
The judge uses this to verify not just *what* the model called, but *where each parameter value came from*. Source types: `TASK_SPEC`, `PREV_CALL`, `AUTH_FLOW`, `STATIC`, `DERIVED`. This is how partial credit works β the model gets reward for correctly threading a `cart_id` even if the final call had a wrong field elsewhere.
Full extraction process: [`GROUND_TRUTH_EXTRACTION.md`](GROUND_TRUTH_EXTRACTION.md)
### HTML and Form-Based Applications
Not every endpoint returns JSON. The Forum (Postmill) relies on HTML form submissions with CSRF tokens; Wikipedia (Kiwix) serves static HTML pages. The agent handles both:
- **CSRF tokens**: The model GETs the form page, reads the `value` attribute of `input[name="_csrf_token"]` from the returned HTML, and places it in the subsequent POST. If the token is beyond the 3,000-character truncation point, it calls `search_episode_data("_csrf_token")` β the full HTML is indexed before truncation.
- **HTML-only responses**: Wikipedia responses are returned in the standard `{status_code, headers, body}` envelope. Search results appear in `<a href>` tags; article content in `<p>` tags.
---
## Example Episode
```
TASK: Add "Radiant Tee" to a guest cart at http://ec2-16-59-2-56.../
STEP 1: browser_agent("Add Radiant Tee to a guest cart", "http://ec2-...:7770/")
β {"endpoints": ["POST /rest/V1/guest-carts", "GET /rest/V1/products",
"POST /rest/V1/guest-carts/{id}/items", ...]}
STEP 2: search_endpoints("create guest cart")
β ["endpoint: POST /rest/V1/guest-carts | auth: none | returns: string (cartId)"]
STEP 3: curl_exec("curl -X POST 'http://ec2-.../rest/V1/guest-carts' -H 'Content-Type: application/json'")
β {status_code: 200, body: "cart-abc123"}
STEP 4: search_endpoints("find product by name get sku")
β ["endpoint: GET /rest/V1/products | query: searchCriteria filters | returns: .items[].sku"]
STEP 5: curl_exec("curl 'http://ec2-.../rest/V1/products?searchCriteria[filter_groups][0][filters][0][field]=name&...[value]=Radiant+Tee'")
β {status_code: 200, body: {"items":[{"sku":"MH01","name":"Radiant Tee","price":22.0}]}}
STEP 6: search_endpoints("add item to guest cart cartId")
β ["endpoint: POST /rest/V1/guest-carts/{cartId}/items | path: cartId from POST /rest/V1/guest-carts | body: cartItem.sku, cartItem.qty, cartItem.quote_id"]
STEP 7: curl_exec("curl -X POST 'http://ec2-.../rest/V1/guest-carts/cart-abc123/items' -H 'Content-Type: application/json' -d '{\"cartItem\":{\"sku\":\"MH01\",\"qty\":1,\"quote_id\":\"cart-abc123\"}}'")
β {status_code: 200, body: {"item_id": 5, "sku": "MH01", "qty": 1}}
STEP 8: done("Radiant Tee added to cart")
```
Values from prior responses (`cart-abc123`, `MH01`) are threaded directly from the growing episode history. If step 5 had returned 200 products truncated to 2, the model would call `search_episode_data("Radiant Tee sku")` to retrieve `MH01` from the episode index.
---
## Setup
### Prerequisites
- Docker installed and running
- Python 3.11+ with [`uv`](https://github.com/astral-sh/uv)
- A Hugging Face token with read access
### Local Development
```bash
# Clone and enter the project
git clone <your-hf-space-url>
cd HARvestGym
# Install dependencies
uv sync
# Validate the OpenEnv spec
openenv validate
# Build and run the Docker image
docker build -t harvgym .
docker run -p 8000:8000 harvgym
# Run the inference script
HF_TOKEN=hf_xxx uv run inference.py
```
### Environment Variables
| Variable | Default | Required | Purpose |
| -------------- | ------------------------------------ | -------- | ----------------------------------------- |
| `HF_TOKEN` | β | **Yes** | HuggingFace auth token |
| `API_BASE_URL` | `https://router.huggingface.co/v1` | No | LLM API endpoint |
| `MODEL_NAME` | `google/gemma-4-31B-it` | No | Model for inference |
| `HARVGYM_TASK` | `har_classify_easy` | No | Override which task to run |
### API Endpoints
```bash
# Reset episode
curl -X POST http://localhost:8000/reset
# Execute a step
curl -X POST http://localhost:8000/step \
-H "Content-Type: application/json" \
-d '{"tool": "browser_agent", "args": {"task": "...", "url": "..."}}'
# Get current state
curl http://localhost:8000/state
```
---
## Baseline Performance
Scores generated by running `uv run inference.py` with `google/gemma-4-31B-it` via the HuggingFace Router.
| Task | Difficulty | Score | Steps | Result | Notes |
| ---- | ---------- | ----- | ----- | ------ | ----- |
| `easy_list_pants` | Easy | **0.74** | 6 | PASS | List products in 'Pants' category |
| `medium_cart_camera_backpack` | Medium | **0.56** | 20 | PASS | Add Camera Backpack to guest cart |
| `medium_cart_flannel_jacket` | Medium | **0.60** | 20 | PASS | Add Flannel Jacket to guest cart |
| `hard_checkout_ripstop_pants` | Hard | **0.22** | 20 | FAIL | Full guest checkout (hit step limit) |
| **Overall** | β | **0.53** | β | **3/4 passed** | |
> **To regenerate:** `HF_TOKEN=hf_xxx uv run inference.py`
|