Files
2026-07-13 13:39:38 +08:00

145 lines
4.8 KiB
Python

"""ComputerTool — Anthropic computer_use Toolset backed by browser PageActions."""
from __future__ import annotations
import asyncio
import base64
import logging
from typing import TYPE_CHECKING, Any
from livekit import rtc
from livekit.agents import llm
from .tools import ComputerUse
if TYPE_CHECKING:
from livekit.plugins.browser import PageActions # type: ignore[import-untyped]
logger = logging.getLogger(__name__)
_POST_ACTION_DELAY = 0.3
class ComputerTool(llm.Toolset):
"""Anthropic computer_use tool backed by browser PageActions.
Usage::
from livekit.plugins.browser import PageActions
actions = PageActions(page=page)
tool = ComputerTool(actions=actions, width=1280, height=720)
"""
def __init__(
self,
*,
actions: PageActions,
width: int = 1280,
height: int = 720,
) -> None:
super().__init__(
id="computer",
tools=[ComputerUse(display_width_px=width, display_height_px=height)],
)
self._actions = actions
async def execute(self, action: str, **kwargs: Any) -> list[dict[str, Any]]:
"""Dispatch an Anthropic computer_use action and return screenshot content."""
actions = self._actions
match action:
case "screenshot":
pass
case "left_click":
x, y = _require_coordinate(kwargs)
await actions.left_click(x, y, modifiers=kwargs.get("text"))
case "right_click":
x, y = _require_coordinate(kwargs)
await actions.right_click(x, y)
case "double_click":
x, y = _require_coordinate(kwargs)
await actions.double_click(x, y)
case "triple_click":
x, y = _require_coordinate(kwargs)
await actions.triple_click(x, y)
case "middle_click":
x, y = _require_coordinate(kwargs)
await actions.middle_click(x, y)
case "mouse_move":
x, y = _require_coordinate(kwargs)
await actions.mouse_move(x, y)
case "left_click_drag":
sx, sy = _require_coordinate(kwargs, key="start_coordinate")
ex, ey = _require_coordinate(kwargs)
await actions.left_click_drag(start_x=sx, start_y=sy, end_x=ex, end_y=ey)
case "left_mouse_down":
x, y = _require_coordinate(kwargs)
await actions.left_mouse_down(x, y)
case "left_mouse_up":
x, y = _require_coordinate(kwargs)
await actions.left_mouse_up(x, y)
case "scroll":
x, y = _require_coordinate(kwargs)
await actions.scroll(
x,
y,
direction=kwargs.get("scroll_direction", "down"),
amount=int(kwargs.get("scroll_amount", 3)),
)
case "type":
await actions.type_text(_require(kwargs, "text"))
case "key":
await actions.key(_require(kwargs, "text"))
case "hold_key":
await actions.hold_key(
_require(kwargs, "text"),
duration=float(kwargs.get("duration", 0.5)),
)
case "wait":
await actions.wait()
case _:
raise ValueError(f"Unknown computer_use action: {action!r}")
await asyncio.sleep(_POST_ACTION_DELAY)
frame = actions.last_frame
if frame is None:
return [{"type": "text", "text": "(no frame available yet)"}]
return _screenshot_content(frame)
async def aclose(self) -> None:
await super().aclose()
self._actions.aclose()
def _require(kwargs: dict[str, Any], key: str) -> Any:
"""Extract a required argument, raising ValueError if missing."""
if key not in kwargs:
raise ValueError(f"Missing required argument: {key!r}")
return kwargs[key]
def _require_coordinate(kwargs: dict[str, Any], *, key: str = "coordinate") -> tuple[int, int]:
"""Extract and unpack a coordinate pair from Anthropic's [x, y] format."""
coord = _require(kwargs, key)
return int(coord[0]), int(coord[1])
def _screenshot_content(frame: rtc.VideoFrame) -> list[dict[str, Any]]:
"""Build Anthropic tool_result content blocks with a screenshot."""
from livekit.agents.utils.images import EncodeOptions, encode
png_bytes = encode(frame, EncodeOptions(format="PNG"))
b64 = base64.b64encode(png_bytes).decode("utf-8")
return [
{
"type": "image",
"source": {
"type": "base64",
"media_type": "image/png",
"data": b64,
},
}
]