"""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),
)