Spaces:
Running
Running
Download src/miniweb/browsergym/task.py from sparklabuofu/MiniWeb-demo: direct link, hf CLI and curl.
- Browser
- Download file 8.78 kB
-
https://huggingface.co/spaces/sparklabuofu/MiniWeb-demo/resolve/main/src/miniweb/browsergym/task.py
- Command line
-
hf download hf://spaces/sparklabuofu/MiniWeb-demo/src/miniweb/browsergym/task.py
-
curl -L -o task.py https://huggingface.co/spaces/sparklabuofu/MiniWeb-demo/resolve/main/src/miniweb/browsergym/task.py
8.78 kB
| """BrowserGym task wrapper for MiniWeb — PoC (browser-gym branch). | |
| Bridges MiniWeb onto BrowserGym's AbstractBrowserTask: | |
| setup(page) -> reset session, navigate to start URL, return goal | |
| validate(page, chat) -> fetch THIS session's trajectory, run verify_task | |
| teardown() -> noop (server is shared) | |
| The grade reuses MiniWeb's existing verifier verbatim; the only new bit is fetching | |
| the trajectory session-scoped THROUGH the browser (page.request carries the | |
| _data_overlay_sid cookie), so many BrowserGym envs can share one MiniWeb server. | |
| """ | |
| from helpers.paths import REPO_ROOT | |
| from helpers.paths import SITES_DIR | |
| import json | |
| import os | |
| import pathlib | |
| from browsergym.core.task import AbstractBrowserTask | |
| from miniweb.evaluation.verifiers import verify_task | |
| from miniweb.evaluation.trajectory import merge_server_log | |
| ROOT = REPO_ROOT | |
| ANN = ROOT / "data" / "annotations" | |
| def _start_url(base, tdir, task): | |
| """Where the agent starts, mapped onto the local server. | |
| Multi-site tasks start on the PORTAL directory (`/`): crossing between sites | |
| is part of the task, so neither site's deep page is a fair start. Single-site | |
| tasks start where the reference recording began — the first OBSERVATION url | |
| (the page the human was actually on), then the first ACTION url. NEVER a | |
| network/XHR event — those are API calls (e.g. /api/prices/live?symbols=SPY) | |
| that render as raw JSON/CSV, not the page. | |
| """ | |
| if len(task.get("sites") or []) > 1: | |
| return base + "/" | |
| tj = tdir / "trajectory.json" | |
| if tj.exists(): | |
| try: | |
| traj = json.loads(tj.read_text()) | |
| for kind in ("observation", "action"): | |
| for e in traj: | |
| u = e.get("url") | |
| if e.get("type") == kind and u and "/sites/" in u: | |
| return base + u[u.find("/sites/"):] | |
| except (json.JSONDecodeError, OSError): | |
| pass | |
| sites = task.get("sites") or [] | |
| sid = (sites[0].get("id") if isinstance(sites[0], dict) else sites[0]) if sites else "" | |
| return f"{base}/sites/{sid}/" | |
| def _assemble(recorded, log, beacons): | |
| """Same assembly as run_agent_verify.build_trajectory, but from session-scoped feeds. | |
| Network events come from the server request log (union with any client-recorded | |
| ones) — queries intact, nothing synthesized from actions.""" | |
| traj = [e for e in recorded if e.get("type") != "network"] | |
| if not any(e.get("type") == "action" for e in traj): | |
| for b in beacons: | |
| traj.append({"type": "action", **b}) | |
| return merge_server_log(traj, log) | |
| def fetch_session_trajectory(page, base): | |
| """Pull THIS browser session's trajectory (the cookie selects the session).""" | |
| def entries(path): | |
| try: | |
| return page.request.get(base + path).json().get("entries", []) or [] | |
| except Exception: | |
| return [] | |
| return _assemble(entries("/_admin/record"), entries("/_admin/log"), | |
| entries("/_admin/beacon")) | |
| def _site_brand(sid): | |
| """The site's brand/display name from site.json (falls back to the id).""" | |
| try: | |
| return json.loads((SITES_DIR / sid / "site.json").read_text()).get("name", sid) | |
| except Exception: | |
| return sid | |
| def _nav_preamble(task): | |
| """Natural, assistant-style orientation (kept free of evaluation cues so the | |
| agent behaves as it would for a real user): describes the browser home page and | |
| how to move between the user's sites, and how to wrap up. No goto tool — the | |
| agent uses the page's own controls (search bar, site shortcuts, tab bar).""" | |
| names = [_site_brand(s.get("id") if isinstance(s, dict) else s) | |
| for s in (task.get("sites") or []) if (s.get("id") if isinstance(s, dict) else s)] | |
| sites_block = (" You may need: " + ", ".join(names) + ".\n\n") if names else " " | |
| return ("You are a helpful assistant operating the user's web browser. You are on " | |
| "the user's home page, which lists their websites — open and move between " | |
| "them using the page's search box, the site shortcuts, and the tab bar." + | |
| sites_block + | |
| "When you have done what the user asked, call finish_task() to wrap up. If " | |
| "they asked a question, reply with report_answer(<answer>) instead. Use " | |
| "send_msg_to_user only for brief progress updates.\n\n" | |
| "The user says: ") | |
| from miniweb.browsergym.actions import FINAL_ANSWER_PREFIX, TASK_DONE_PREFIX | |
| def _agent_texts(chat_messages): | |
| """The messages the AGENT produced this episode. BrowserGym seeds a stock | |
| assistant greeting ("Hi! I am your UI assistant...") BEFORE the goal, so we take | |
| only assistant messages that come AFTER the goal (the first user-role message).""" | |
| msgs = list(chat_messages or []) | |
| start = 0 | |
| for i, m in enumerate(msgs): | |
| if isinstance(m, dict) and m.get("role") == "user": | |
| start = i + 1 | |
| break | |
| out = [] | |
| for m in msgs[start:]: | |
| if isinstance(m, dict): | |
| if m.get("role") in (None, "assistant", "infeasible") and m.get("message"): | |
| out.append(str(m["message"])) | |
| elif isinstance(m, str) and m: | |
| out.append(m) | |
| return out | |
| def _final_answer(chat_messages): | |
| """The agent's explicitly-reported final answer, via report_answer (last marked | |
| message, marker stripped). '' if the agent never reported one.""" | |
| for t in reversed(_agent_texts(chat_messages)): | |
| if FINAL_ANSWER_PREFIX in t: | |
| return t.split(FINAL_ANSWER_PREFIX, 1)[1].strip() | |
| return "" | |
| def _task_finished(chat_messages): | |
| """True once the agent has declared it is done — via finish_task (no answer) or | |
| report_answer (with an answer). Both end the task; grading runs at that point.""" | |
| return any(FINAL_ANSWER_PREFIX in t or TASK_DONE_PREFIX in t | |
| for t in _agent_texts(chat_messages)) | |
| def _collect_answer(chat_messages): | |
| """Fallback for agents that don't report an explicit answer: everything the agent | |
| said to the human, joined (so a multi-part answer split across messages is whole).""" | |
| return "\n".join(_agent_texts(chat_messages)).strip() | |
| class MiniWebTask(AbstractBrowserTask): | |
| def __init__(self, seed: int = 0, task_id: str = None, base_url: str = None): | |
| super().__init__(seed) | |
| self.task_id = task_id | |
| self.base = (base_url or os.environ.get("MINIWEB_URL", "http://localhost:8099")).rstrip("/") | |
| self.tdir = ANN / task_id | |
| self.task = json.loads((self.tdir / "task.json").read_text()) | |
| self.verifier = json.loads((self.tdir / "verifier.json").read_text()) | |
| self.expected = (self.task.get("expected_answer") or "").strip() | |
| def setup(self, page): | |
| # session-scoped reset (safe on a shared server), then land on the start page. | |
| # authenticate_by_form tasks must start LOGGED OUT: ?no_autologin=1 sets the | |
| # session flag the auto-login guard honours (per-session equivalent of the | |
| # native runner's MINIWEB_NO_AUTOLOGIN server env). | |
| reset = f"{self.base}/_reset_data" | |
| if "authenticate_by_form" in (self.verifier.get("macros") or {}): | |
| reset += "?no_autologin=1" | |
| try: | |
| page.goto(reset) | |
| except Exception: | |
| pass | |
| page.goto(_start_url(self.base, self.tdir, self.task)) | |
| goal = _nav_preamble(self.task) + self.task["instruction"] | |
| return goal, {"task_id": self.task_id} | |
| def validate(self, page, chat_messages): | |
| # The agent's answer: its explicit report_answer/finish_task value if given, | |
| # else its messages to the human (BrowserGym's seed greeting is excluded). | |
| answer = _final_answer(chat_messages) or _collect_answer(chat_messages) | |
| traj = fetch_session_trajectory(page, self.base) | |
| report = verify_task(self.verifier, traj, answer, | |
| question=self.task.get("instruction", "")) | |
| passed = report["passed"] | |
| # The agent ends the task by calling finish_task (or report_answer); grading | |
| # runs at that point, pass or fail. A solved task also ends immediately. | |
| # Otherwise the agent keeps going until the step limit (BrowserGym truncates). | |
| finished = _task_finished(chat_messages) | |
| done = passed or finished | |
| reward = 1.0 if passed else 0.0 | |
| return reward, done, "", {"task_id": self.task_id, | |
| "by_macro": report["by_macro"], | |
| "ended_by": "verifier" if passed else | |
| ("agent" if finished else "")} | |
| def teardown(self): | |
| pass | |