import time import base64 from typing import List, Dict, Literal from playwright.sync_api import sync_playwright, Browser, Page from utils import check_blocklisted_url # Optional: key mapping if your model uses "CUA" style keys CUA_KEY_TO_PLAYWRIGHT_KEY = { "/": "Divide", "\\": "Backslash", "alt": "Alt", "arrowdown": "ArrowDown", "arrowleft": "ArrowLeft", "arrowright": "ArrowRight", "arrowup": "ArrowUp", "backspace": "Backspace", "capslock": "CapsLock", "cmd": "Meta", "ctrl": "Control", "delete": "Delete", "end": "End", "enter": "Enter", "esc": "Escape", "home": "Home", "insert": "Insert", "option": "Alt", "pagedown": "PageDown", "pageup": "PageUp", "shift": "Shift", "space": " ", "super": "Meta", "tab": "Tab", "win": "Meta", } class BasePlaywrightComputer: """ Abstract base for Playwright-based computers: - Subclasses override `_get_browser_and_page()` to do local or remote connection, returning (Browser, Page). - This base class handles context creation (`__enter__`/`__exit__`), plus standard "Computer" actions like click, scroll, etc. - We also have extra browser actions: `goto(url)` and `back()`. """ environment: Literal["browser"] = "browser" dimensions = (954, 1048) def __init__(self): self._playwright = None self._browser: Browser | None = None self._page: Page | None = None def __enter__(self): # Start Playwright and call the subclass hook for getting browser/page self._playwright = sync_playwright().start() self._browser, self._page = self._get_browser_and_page() # Set up network interception to flag URLs matching domains in BLOCKED_DOMAINS def handle_route(route, request): url = request.url if check_blocklisted_url(url): print(f"Flagging blocked domain: {url}") route.abort() else: route.continue_() self._page.route("**/*", handle_route) return self def __exit__(self, exc_type, exc_val, exc_tb): if self._browser: self._browser.close() if self._playwright: self._playwright.stop() # --- Common "Computer" actions --- def screenshot(self) -> str: """Capture only the viewport (not full_page).""" png_bytes = self._page.screenshot(full_page=False) return base64.b64encode(png_bytes).decode("utf-8") def click(self, x: int, y: int, button: str = "left") -> None: match button: case "back": self.back() case "forward": self.forward() case "wheel": self._page.mouse.wheel(x, y) case _: button_mapping = {"left": "left", "right": "right"} button_type = button_mapping.get(button, "left") self._page.mouse.click(x, y, button=button_type) def double_click(self, x: int, y: int) -> None: self._page.mouse.dblclick(x, y) def scroll(self, x: int, y: int, scroll_x: int, scroll_y: int) -> None: self._page.mouse.move(x, y) self._page.evaluate(f"window.scrollBy({scroll_x}, {scroll_y})") def type(self, text: str) -> None: self._page.keyboard.type(text) def wait(self, ms: int = 1000) -> None: time.sleep(ms / 1000) def move(self, x: int, y: int) -> None: self._page.mouse.move(x, y) def keypress(self, keys: List[str]) -> None: for key in keys: mapped_key = CUA_KEY_TO_PLAYWRIGHT_KEY.get(key.lower(), key) self._page.keyboard.press(mapped_key) def drag(self, path: List[Dict[str, int]]) -> None: if not path: return self._page.mouse.move(path[0]["x"], path[0]["y"]) self._page.mouse.down() for point in path[1:]: self._page.mouse.move(point["x"], point["y"]) self._page.mouse.up() def get_current_url(self) -> str: return self._page.url # --- Extra browser-oriented actions --- def goto(self, url: str) -> None: try: return self._page.goto(url) except Exception as e: print(f"Error navigating to {url}: {e}") def back(self) -> None: return self._page.go_back() def forward(self) -> None: return self._page.go_forward() # --- Subclass hook --- def _get_browser_and_page(self) -> tuple[Browser, Page]: """Subclasses must implement, returning (Browser, Page).""" raise NotImplementedError class LocalPlaywrightComputer(BasePlaywrightComputer): """Launches a local Chromium instance using Playwright.""" def __init__(self, headless: bool = False): super().__init__() self.headless = headless def _get_browser_and_page(self) -> tuple[Browser, Page]: width, height = self.dimensions launch_args = [f"--window-size={width},{height}", "--disable-extensions", "--disable-file-system"] browser = self._playwright.chromium.launch( chromium_sandbox=True, headless=self.headless, args=launch_args, env={} ) page = browser.new_page() page.set_viewport_size({"width": width, "height": height}) page.goto("https://duckduckgo.com") return browser, page