Chromium Browser 1.0.0

open_page.py

4.6 KB · raw

"""Open a page in a real browser and read what it became."""

from __future__ import annotations

from aworg.tools.base import ToolContext, ToolError, ToolResult

from ._cdp import BrowserError, browser
from ._page import describe, extract, summary


NAME = "open_page"

DESCRIPTION = (
    "Open a URL in a real browser and read the page after its JavaScript has "
    "run. Returns the page's text, its links and controls, and anything the "
    "console or the network complained about. The page stays open for "
    "read_page, page_do and screenshot."
)

INPUT_SCHEMA = {
    "type": "object",
    "properties": {
        "url": {
            "type": "string",
            "description": "The full URL, including http:// or https://.",
        },
        "wait": {
            "type": "number",
            "description": (
                "Extra seconds to wait after the page loads, for content that "
                "arrives late. Defaults to 0.6, maximum 15."
            ),
        },
        "links": {
            "type": "boolean",
            "description": (
                "Include the list of links and controls. Defaults to true; "
                "turn it off when you only want the words."
            ),
        },
        "screen": {
            "type": "string",
            "description": (
                "Screen to open it on: 'phone' (390x844, touch), 'tablet' "
                "(820x1180, touch) or 'desktop' (1280x900, the default)."
            ),
        },
        "width": {
            "type": "integer",
            "description": (
                "An exact viewport width in pixels, instead of 'screen'."
            ),
        },
        "height": {
            "type": "integer",
            "description": "An exact viewport height in pixels.",
        },
        "dark": {
            "type": "boolean",
            "description": (
                "Open it as a visitor whose system is set to dark mode, so a "
                "page with a prefers-color-scheme rule shows its other face."
            ),
        },
    },
    "required": ["url"],
}

#: The two that are worth a name. Not a device list: this is a viewport and a
#: touch flag, not an iPhone, and pretending otherwise would have AWORG
#: maintaining a table of somebody else's hardware.
SCREENS = {
    "phone": (390, 844, True),
    "mobile": (390, 844, True),
    "tablet": (820, 1180, True),
    "desktop": (1280, 900, False),
}

MAX_WAIT = 15


async def run(
    context: ToolContext,
    url: str,
    wait: float = 0.6,
    links: bool = True,
    screen: str | None = None,
    width: int | None = None,
    height: int | None = None,
    dark: bool | None = None,
) -> ToolResult:
    url = (url or "").strip()
    if not url:
        raise ToolError("open_page needs a URL.")
    if "://" not in url:
        # A bare host is what a person types, and refusing it teaches the
        # model a rule rather than getting it the page.
        url = "http://" + url

    named = SCREENS.get((screen or "").strip().lower()) if screen else None
    if screen and named is None:
        raise ToolError(
            f"{screen!r} is not a screen this knows. It knows "
            + ", ".join(sorted(SCREENS))
            + " -- or give width and height."
        )

    try:
        page_browser = await browser(processes=context.processes)
        # Set before navigating, so the page lays out at the size it is
        # being asked about rather than reflowing into it afterwards.
        if named or width or height or dark is not None:
            await page_browser.emulate(
                width=width or (named[0] if named else None),
                height=height or (named[1] if named else None),
                mobile=bool(named[2]) if named else False,
                dark=dark,
            )
        if context.activity is not None:
            context.activity.progress = f"loading {url}"
        await page_browser.navigate(url, settle=max(0.0, min(float(wait), MAX_WAIT)))
        page = await extract(page_browser)
    except BrowserError as exc:
        raise ToolError(str(exc)) from exc

    how = ""
    if named or width or height:
        size = f"{width or (named[0] if named else 1280)}" \
               f"x{height or (named[1] if named else 900)}"
        how = f"[{screen or 'custom'} viewport, {size}]"
    if dark is not None:
        how = (how + " " if how else "") + f"[prefers-color-scheme: " \
              f"{'dark' if dark else 'light'}]"

    return ToolResult(
        text=(f"{how}\n" if how else "")
        + describe(page_browser, page, want_links=links),
        payload=page,
        summary=summary(page_browser, page),
    )