Files
2026-07-13 13:12:00 +08:00

323 lines
12 KiB
Python

"""Schema-only ``browser_*`` builtin tool classes.
These classes are the **tool surface only** — ``name()``,
``description()`` and ``get_schema()``. They exist so the five
embedded-browser tools are *advertised* to the LLM; they deliberately
do NOT implement ``invoke()``.
Execution lives in the runner dispatch layer
(``omnigent/runner/tool_dispatch.py`` — the ``_BROWSER_TOOLS`` branch),
because the browser protocol needs the runner's ``server_client`` to
POST a blocking action request to the server, and ``ToolContext`` carries
no ``server_client``. Any call that reaches ``Tool.invoke`` here means
the tool was misrouted to the server-side path — the base class raises
``NotImplementedError`` loudly in that case.
Descriptions for the five browser tools that drive the Omnigent
desktop app's embedded browser; they fail cleanly when no desktop
renderer is subscribed (the action times out with a clear error).
"""
from __future__ import annotations
from typing import Any
from omnigent.tools.base import Tool
# The five browser tools. Kept as a module constant so the registration
# factory in ``builtins/__init__.py`` and any test can reference the
# canonical name set without re-listing it.
BROWSER_TOOL_NAMES: frozenset[str] = frozenset(
{
"browser_navigate",
"browser_snapshot",
"browser_click",
"browser_type",
"browser_screenshot",
}
)
class BrowserNavigateTool(Tool):
"""Open or navigate the embedded browser pane to a URL (schema only)."""
@classmethod
def name(cls) -> str:
""":returns: ``"browser_navigate"``."""
return "browser_navigate"
@classmethod
def description(cls) -> str:
""":returns: Human-readable description of the tool."""
return (
"Open or navigate the Omnigent desktop app's embedded "
"browser pane to a URL. Auto-opens the pane if it isn't "
"open yet. Requires the Omnigent desktop window to be "
"running — fails cleanly otherwise. After a load settles, "
"call browser_snapshot to inspect what's on the page."
)
def get_schema(self) -> dict[str, Any]:
"""
Return the OpenAI-format tool schema.
:returns: Dict with ``"type": "function"`` and a
``"function"`` sub-dict.
"""
return {
"type": "function",
"function": {
"name": BrowserNavigateTool.name(),
"description": BrowserNavigateTool.description(),
"parameters": {
"type": "object",
"properties": {
"url": {
"type": "string",
"description": "The URL to open or navigate to.",
},
},
"required": ["url"],
"additionalProperties": False,
},
},
}
class BrowserSnapshotTool(Tool):
"""Capture an accessibility-tree snapshot of the page (schema only)."""
@classmethod
def name(cls) -> str:
""":returns: ``"browser_snapshot"``."""
return "browser_snapshot"
@classmethod
def description(cls) -> str:
""":returns: Human-readable description of the tool."""
return (
"Capture an accessibility-tree snapshot of the embedded "
"browser. Returns a snapshot_id plus the current URL, "
"document.title, and a YAML-shaped tree of every "
"interactive / text-bearing / landmark element on the "
"page, each tagged with [ref=N]. Hand the refs (along with "
"the snapshot_id) to browser_click / browser_type so the "
"renderer can detect when the snapshot has been superseded "
"by a newer one or invalidated by navigation. Refs are "
"dramatically more stable than CSS selectors against "
"generated class names and Shadow DOM."
)
def get_schema(self) -> dict[str, Any]:
"""
Return the OpenAI-format tool schema.
:returns: Dict with ``"type": "function"`` and a
``"function"`` sub-dict.
"""
return {
"type": "function",
"function": {
"name": BrowserSnapshotTool.name(),
"description": BrowserSnapshotTool.description(),
"parameters": {
"type": "object",
"properties": {},
"additionalProperties": False,
},
},
}
class BrowserClickTool(Tool):
"""Click an element by ref or CSS selector (schema only)."""
@classmethod
def name(cls) -> str:
""":returns: ``"browser_click"``."""
return "browser_click"
@classmethod
def description(cls) -> str:
""":returns: Human-readable description of the tool."""
return (
"Click an element in the embedded browser. Prefer the "
"`ref` form (integer id from a recent browser_snapshot "
"result) — refs are stable against generated class names "
"and Shadow DOM. Pass `snapshot_id` alongside `ref` so the "
"renderer can reject stale-snapshot refs with a precise "
"error instead of a generic stale-ref message. CSS "
"`selector` is accepted as a fallback when you already know "
"a stable selector. Exactly one of `ref` or `selector` "
"must be provided."
)
def get_schema(self) -> dict[str, Any]:
"""
Return the OpenAI-format tool schema.
:returns: Dict with ``"type": "function"`` and a
``"function"`` sub-dict.
"""
return {
"type": "function",
"function": {
"name": BrowserClickTool.name(),
"description": BrowserClickTool.description(),
"parameters": {
"type": "object",
"properties": {
"ref": {
"type": "integer",
"description": (
"Non-negative integer id of the element "
"from a recent browser_snapshot. Preferred "
"over selector."
),
},
"snapshot_id": {
"type": "string",
"description": (
"The snapshot_id the ref came from, so a "
"stale ref is rejected with a precise error."
),
},
"selector": {
"type": "string",
"description": (
"CSS selector fallback when you already "
"know a stable selector. Provide either "
"ref or selector, not both."
),
},
},
"additionalProperties": False,
},
},
}
class BrowserTypeTool(Tool):
"""Type text into an input by ref or CSS selector (schema only)."""
@classmethod
def name(cls) -> str:
""":returns: ``"browser_type"``."""
return "browser_type"
@classmethod
def description(cls) -> str:
""":returns: Human-readable description of the tool."""
return (
"Focus an input element and type text into it. Identify the "
"input with `ref` (preferred — integer id from "
"browser_snapshot, pair with `snapshot_id` for precise "
"stale-ref errors) or `selector` (CSS, fallback). "
"Dispatches `input` + `change` events using the native "
"value setter so React/Vue/etc. controlled inputs see the "
"value."
)
def get_schema(self) -> dict[str, Any]:
"""
Return the OpenAI-format tool schema.
:returns: Dict with ``"type": "function"`` and a
``"function"`` sub-dict.
"""
return {
"type": "function",
"function": {
"name": BrowserTypeTool.name(),
"description": BrowserTypeTool.description(),
"parameters": {
"type": "object",
"properties": {
"ref": {
"type": "integer",
"description": (
"Non-negative integer id of the input "
"element from a recent browser_snapshot. "
"Preferred over selector."
),
},
"snapshot_id": {
"type": "string",
"description": (
"The snapshot_id the ref came from, so a "
"stale ref is rejected with a precise error."
),
},
"selector": {
"type": "string",
"description": (
"CSS selector fallback when you already "
"know a stable selector. Provide either "
"ref or selector, not both."
),
},
"text": {
"type": "string",
"description": "The text to type into the input.",
},
},
"required": ["text"],
"additionalProperties": False,
},
},
}
class BrowserScreenshotTool(Tool):
"""Capture a PNG screenshot of the browser pane (schema only)."""
@classmethod
def name(cls) -> str:
""":returns: ``"browser_screenshot"``."""
return "browser_screenshot"
@classmethod
def description(cls) -> str:
""":returns: Human-readable description of the tool."""
return (
"Capture a PNG screenshot of the embedded browser pane. "
"Returns image content the agent surface renders inline. "
"INTENDED FOR VISUAL INSPECTION ONLY — prefer "
"browser_snapshot for picking elements to act on, since "
"screenshots can't carry ref ids and you can't click a "
"pixel location. Use this when you need to verify what "
"something looks like, not to plan an interaction."
)
def get_schema(self) -> dict[str, Any]:
"""
Return the OpenAI-format tool schema.
:returns: Dict with ``"type": "function"`` and a
``"function"`` sub-dict.
"""
return {
"type": "function",
"function": {
"name": BrowserScreenshotTool.name(),
"description": BrowserScreenshotTool.description(),
"parameters": {
"type": "object",
"properties": {},
"additionalProperties": False,
},
},
}
# Ordered tuple of the schema-only browser tool classes, in the same
# order as ``BROWSER_TOOL_NAMES`` reads. The registration factory in
# ``builtins/__init__.py`` iterates this to construct instances.
BROWSER_TOOL_CLASSES: tuple[type[Tool], ...] = (
BrowserNavigateTool,
BrowserSnapshotTool,
BrowserClickTool,
BrowserTypeTool,
BrowserScreenshotTool,
)