meshagent-computers 0.6.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of meshagent-computers might be problematic. Click here for more details.
- meshagent/computers/__init__.py +21 -0
- meshagent/computers/agent.py +229 -0
- meshagent/computers/base_playwright.py +173 -0
- meshagent/computers/browserbase.py +196 -0
- meshagent/computers/computer.py +36 -0
- meshagent/computers/docker.py +179 -0
- meshagent/computers/local_playwright.py +25 -0
- meshagent/computers/operator.py +79 -0
- meshagent/computers/scrapybara.py +212 -0
- meshagent/computers/utils.py +78 -0
- meshagent/computers/version.py +1 -0
- meshagent_computers-0.6.7.dist-info/METADATA +69 -0
- meshagent_computers-0.6.7.dist-info/RECORD +16 -0
- meshagent_computers-0.6.7.dist-info/WHEEL +5 -0
- meshagent_computers-0.6.7.dist-info/licenses/LICENSE +201 -0
- meshagent_computers-0.6.7.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from .computer import Computer
|
|
2
|
+
from .browserbase import BrowserbaseBrowser
|
|
3
|
+
from .local_playwright import LocalPlaywrightComputer
|
|
4
|
+
from .docker import DockerComputer
|
|
5
|
+
from .scrapybara import ScrapybaraBrowser, ScrapybaraUbuntu
|
|
6
|
+
from .operator import Operator
|
|
7
|
+
from .agent import ComputerAgent
|
|
8
|
+
from .version import __version__
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
Computer,
|
|
13
|
+
BrowserbaseBrowser,
|
|
14
|
+
LocalPlaywrightComputer,
|
|
15
|
+
DockerComputer,
|
|
16
|
+
ScrapybaraBrowser,
|
|
17
|
+
ScrapybaraUbuntu,
|
|
18
|
+
Operator,
|
|
19
|
+
ComputerAgent,
|
|
20
|
+
__version__,
|
|
21
|
+
]
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
from meshagent.agents import LLMAdapter
|
|
2
|
+
from meshagent.tools import Tool, Toolkit, ToolContext
|
|
3
|
+
from meshagent.computers import Computer, Operator, BrowserbaseBrowser
|
|
4
|
+
from meshagent.agents.chat import ChatBot, ChatThreadContext
|
|
5
|
+
from meshagent.api import RemoteParticipant
|
|
6
|
+
from meshagent.openai.tools.responses_adapter import OpenAIResponsesTool
|
|
7
|
+
|
|
8
|
+
from typing import Optional, Type, Callable
|
|
9
|
+
import base64
|
|
10
|
+
import logging
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger("computer")
|
|
13
|
+
logger.setLevel(logging.WARN)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ComputerToolkit(Toolkit):
|
|
17
|
+
def __init__(
|
|
18
|
+
self, name: str, computer: Computer, operator: Operator, tools: list[Tool]
|
|
19
|
+
):
|
|
20
|
+
super().__init__(name=name, tools=tools)
|
|
21
|
+
self.computer = computer
|
|
22
|
+
self.operator = operator
|
|
23
|
+
self.started = False
|
|
24
|
+
self._starting = None
|
|
25
|
+
|
|
26
|
+
async def ensure_started(self):
|
|
27
|
+
self.started = False
|
|
28
|
+
|
|
29
|
+
if not self.started:
|
|
30
|
+
self.started = True
|
|
31
|
+
await self.computer.__aenter__()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def make_computer_toolkit(
|
|
35
|
+
*,
|
|
36
|
+
operator_cls: Type[Operator],
|
|
37
|
+
computer_cls: Type[Computer],
|
|
38
|
+
render_screen: Callable[[bytes], None],
|
|
39
|
+
):
|
|
40
|
+
operator = operator_cls()
|
|
41
|
+
computer = computer_cls()
|
|
42
|
+
|
|
43
|
+
class ComputerTool(OpenAIResponsesTool):
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
*,
|
|
47
|
+
operator: Operator,
|
|
48
|
+
computer: Computer,
|
|
49
|
+
title="computer_call",
|
|
50
|
+
description="handle computer calls from computer use preview",
|
|
51
|
+
rules=[],
|
|
52
|
+
thumbnail_url=None,
|
|
53
|
+
):
|
|
54
|
+
super().__init__(
|
|
55
|
+
name="computer_call",
|
|
56
|
+
# TODO: give a correct schema
|
|
57
|
+
title=title,
|
|
58
|
+
description=description,
|
|
59
|
+
rules=rules,
|
|
60
|
+
thumbnail_url=thumbnail_url,
|
|
61
|
+
)
|
|
62
|
+
self.computer = computer
|
|
63
|
+
|
|
64
|
+
def get_open_ai_tool_definitions(self) -> list[dict]:
|
|
65
|
+
return [
|
|
66
|
+
{
|
|
67
|
+
"type": "computer_use_preview",
|
|
68
|
+
"display_width": self.computer.dimensions[0],
|
|
69
|
+
"display_height": self.computer.dimensions[1],
|
|
70
|
+
"environment": self.computer.environment,
|
|
71
|
+
}
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
def get_open_ai_output_handlers(self):
|
|
75
|
+
return {"computer_call": self.handle_computer_call}
|
|
76
|
+
|
|
77
|
+
async def handle_computer_call(self, context: ToolContext, **arguments):
|
|
78
|
+
outputs = await operator.play(computer=self.computer, item=arguments)
|
|
79
|
+
for output in outputs:
|
|
80
|
+
if output["type"] == "computer_call_output":
|
|
81
|
+
if output["output"] is not None:
|
|
82
|
+
if output["output"]["type"] == "input_image":
|
|
83
|
+
b64: str = output["output"]["image_url"]
|
|
84
|
+
image_data_b64 = b64.split(",", 1)
|
|
85
|
+
|
|
86
|
+
image_bytes = base64.b64decode(image_data_b64[1])
|
|
87
|
+
render_screen(image_bytes)
|
|
88
|
+
|
|
89
|
+
nonlocal computer_toolkit
|
|
90
|
+
if len(computer_toolkit.tools) == 1:
|
|
91
|
+
# HACK: after looking at the page, add the other tools,
|
|
92
|
+
# if we add these first then the computer-use-preview mode fails if it calls them before using the computer
|
|
93
|
+
computer_toolkit.tools.extend(
|
|
94
|
+
[
|
|
95
|
+
ScreenshotTool(computer=computer),
|
|
96
|
+
GotoURL(computer=computer),
|
|
97
|
+
]
|
|
98
|
+
)
|
|
99
|
+
return outputs[0]
|
|
100
|
+
|
|
101
|
+
class ScreenshotTool(Tool):
|
|
102
|
+
def __init__(self, computer: Computer):
|
|
103
|
+
self.computer = computer
|
|
104
|
+
|
|
105
|
+
super().__init__(
|
|
106
|
+
name="screenshot",
|
|
107
|
+
# TODO: give a correct schema
|
|
108
|
+
input_schema={
|
|
109
|
+
"additionalProperties": False,
|
|
110
|
+
"type": "object",
|
|
111
|
+
"required": ["full_page", "save_path"],
|
|
112
|
+
"properties": {
|
|
113
|
+
"full_page": {"type": "boolean"},
|
|
114
|
+
"save_path": {
|
|
115
|
+
"type": "string",
|
|
116
|
+
"description": "a file path to save the screenshot to (should end with .png)",
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
description="take a screenshot of the current page",
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
async def execute(self, context: ToolContext, save_path: str, full_page: bool):
|
|
124
|
+
screenshot_bytes = await self.computer.screenshot_bytes(full_page=full_page)
|
|
125
|
+
handle = await context.room.storage.open(path=save_path, overwrite=True)
|
|
126
|
+
await context.room.storage.write(handle=handle, data=screenshot_bytes)
|
|
127
|
+
await context.room.storage.close(handle=handle)
|
|
128
|
+
|
|
129
|
+
return f"saved screenshot to {save_path}"
|
|
130
|
+
|
|
131
|
+
class GotoURL(Tool):
|
|
132
|
+
def __init__(self, computer: Computer):
|
|
133
|
+
self.computer = computer
|
|
134
|
+
|
|
135
|
+
super().__init__(
|
|
136
|
+
name="goto",
|
|
137
|
+
description="goes to a specific URL. Make sure it starts with http:// or https://",
|
|
138
|
+
# TODO: give a correct schema
|
|
139
|
+
input_schema={
|
|
140
|
+
"additionalProperties": False,
|
|
141
|
+
"type": "object",
|
|
142
|
+
"required": ["url"],
|
|
143
|
+
"properties": {
|
|
144
|
+
"url": {
|
|
145
|
+
"type": "string",
|
|
146
|
+
"description": "Fully qualified URL to navigate to.",
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
async def execute(self, context: ToolContext, url: str):
|
|
153
|
+
if not url.startswith("https://") and not url.startswith("http://"):
|
|
154
|
+
url = "https://" + url
|
|
155
|
+
|
|
156
|
+
await self.computer.goto(url)
|
|
157
|
+
|
|
158
|
+
render_screen(await self.computer.screenshot_bytes(full_page=False))
|
|
159
|
+
|
|
160
|
+
computer_tool = ComputerTool(computer=computer, operator=operator)
|
|
161
|
+
|
|
162
|
+
computer_toolkit = ComputerToolkit(
|
|
163
|
+
name="meshagent.openai.computer",
|
|
164
|
+
computer=computer,
|
|
165
|
+
operator=operator,
|
|
166
|
+
tools=[computer_tool],
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
return computer_toolkit
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class ComputerAgent(ChatBot):
|
|
173
|
+
def __init__(
|
|
174
|
+
self,
|
|
175
|
+
*,
|
|
176
|
+
name,
|
|
177
|
+
title=None,
|
|
178
|
+
description=None,
|
|
179
|
+
requires=None,
|
|
180
|
+
labels=None,
|
|
181
|
+
computer_cls: Type[Computer] = BrowserbaseBrowser,
|
|
182
|
+
operator_cls: Type[Operator] = Operator,
|
|
183
|
+
rules: Optional[list[str]] = None,
|
|
184
|
+
llm_adapter: Optional[LLMAdapter] = None,
|
|
185
|
+
toolkits: list[Toolkit] = None,
|
|
186
|
+
):
|
|
187
|
+
if rules is None:
|
|
188
|
+
rules = [
|
|
189
|
+
"if asked to go to a URL, you MUST use the goto function to go to the url if it is available",
|
|
190
|
+
"after going directly to a URL, the screen will change so you should take a look at it to know what to do next",
|
|
191
|
+
]
|
|
192
|
+
super().__init__(
|
|
193
|
+
name=name,
|
|
194
|
+
title=title,
|
|
195
|
+
description=description,
|
|
196
|
+
requires=requires,
|
|
197
|
+
labels=labels,
|
|
198
|
+
llm_adapter=llm_adapter,
|
|
199
|
+
toolkits=toolkits,
|
|
200
|
+
rules=rules,
|
|
201
|
+
)
|
|
202
|
+
self.computer_cls = computer_cls
|
|
203
|
+
self.operator_cls = operator_cls
|
|
204
|
+
|
|
205
|
+
async def get_thread_toolkits(
|
|
206
|
+
self, *, thread_context: ChatThreadContext, participant: RemoteParticipant
|
|
207
|
+
):
|
|
208
|
+
toolkits = await super().get_thread_toolkits(
|
|
209
|
+
thread_context=thread_context, participant=participant
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
def render_screen(image_bytes: bytes):
|
|
213
|
+
for participant in thread_context.participants:
|
|
214
|
+
self.room.messaging.send_message_nowait(
|
|
215
|
+
to=participant,
|
|
216
|
+
type="computer_screen",
|
|
217
|
+
message={},
|
|
218
|
+
attachment=image_bytes,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
computer_toolkit = make_computer_toolkit(
|
|
222
|
+
operator_cls=self.operator_cls,
|
|
223
|
+
computer_cls=self.computer_cls,
|
|
224
|
+
render_screen=render_screen,
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
await computer_toolkit.ensure_started()
|
|
228
|
+
|
|
229
|
+
return [computer_toolkit, *toolkits]
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import base64
|
|
3
|
+
from typing import List, Dict, Literal
|
|
4
|
+
from playwright.async_api import async_playwright, Browser, Page, Route, Request
|
|
5
|
+
from meshagent.computers.utils import check_blocklisted_url
|
|
6
|
+
|
|
7
|
+
# Optional: key mapping if your model uses "CUA" style keys
|
|
8
|
+
CUA_KEY_TO_PLAYWRIGHT_KEY = {
|
|
9
|
+
"/": "Divide",
|
|
10
|
+
"\\": "Backslash",
|
|
11
|
+
"alt": "Alt",
|
|
12
|
+
"arrowdown": "ArrowDown",
|
|
13
|
+
"arrowleft": "ArrowLeft",
|
|
14
|
+
"arrowright": "ArrowRight",
|
|
15
|
+
"arrowup": "ArrowUp",
|
|
16
|
+
"backspace": "Backspace",
|
|
17
|
+
"capslock": "CapsLock",
|
|
18
|
+
"cmd": "Meta",
|
|
19
|
+
"ctrl": "Control",
|
|
20
|
+
"delete": "Delete",
|
|
21
|
+
"end": "End",
|
|
22
|
+
"enter": "Enter",
|
|
23
|
+
"esc": "Escape",
|
|
24
|
+
"home": "Home",
|
|
25
|
+
"insert": "Insert",
|
|
26
|
+
"option": "Alt",
|
|
27
|
+
"pagedown": "PageDown",
|
|
28
|
+
"pageup": "PageUp",
|
|
29
|
+
"shift": "Shift",
|
|
30
|
+
"space": " ",
|
|
31
|
+
"super": "Meta",
|
|
32
|
+
"tab": "Tab",
|
|
33
|
+
"win": "Meta",
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class BasePlaywrightComputer:
|
|
38
|
+
"""
|
|
39
|
+
Abstract base for Playwright-based computers:
|
|
40
|
+
|
|
41
|
+
- Subclasses override `_get_browser_and_page()` to do local or remote connection,
|
|
42
|
+
returning (Browser, Page).
|
|
43
|
+
- This base class handles context creation (`__enter__`/`__exit__`),
|
|
44
|
+
plus standard "Computer" actions like click, scroll, etc.
|
|
45
|
+
- We also have extra browser actions: `goto(url)` and `back()`.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
environment: Literal["browser"] = "browser"
|
|
49
|
+
dimensions = (1024, 768)
|
|
50
|
+
|
|
51
|
+
def __init__(self):
|
|
52
|
+
self._playwright = None
|
|
53
|
+
self._browser: Browser | None = None
|
|
54
|
+
self._page: Page | None = None
|
|
55
|
+
|
|
56
|
+
async def __aenter__(self):
|
|
57
|
+
# Start Playwright and call the subclass hook for getting browser/page
|
|
58
|
+
self._context = async_playwright()
|
|
59
|
+
self._playwright = await self._context.__aenter__()
|
|
60
|
+
self._browser, self._page = await self._get_browser_and_page()
|
|
61
|
+
|
|
62
|
+
# Set up network interception to flag URLs matching domains in BLOCKED_DOMAINS
|
|
63
|
+
async def handle_route(route: Route, request: Request):
|
|
64
|
+
url = request.url
|
|
65
|
+
if check_blocklisted_url(url):
|
|
66
|
+
print(f"Flagging blocked domain: {url}")
|
|
67
|
+
await route.abort()
|
|
68
|
+
else:
|
|
69
|
+
await route.continue_()
|
|
70
|
+
|
|
71
|
+
await self._page.route("**/*", handle_route)
|
|
72
|
+
|
|
73
|
+
return self
|
|
74
|
+
|
|
75
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
76
|
+
if self._browser:
|
|
77
|
+
await self._browser.close()
|
|
78
|
+
if self._playwright:
|
|
79
|
+
await self._context.__aexit__(exc_type, exc_val, exc_tb)
|
|
80
|
+
|
|
81
|
+
async def ensure_page(self):
|
|
82
|
+
# After a timeout, we might loose our browser
|
|
83
|
+
if self._page is None or not self._browser.is_connected:
|
|
84
|
+
self._browser, self._page = await self._get_browser_and_page()
|
|
85
|
+
|
|
86
|
+
# --- Common "Computer" actions ---
|
|
87
|
+
|
|
88
|
+
async def screenshot_bytes(self, full_page: bool = False) -> bytes:
|
|
89
|
+
await self.ensure_page()
|
|
90
|
+
png_bytes = await self._page.screenshot(full_page=full_page)
|
|
91
|
+
return png_bytes
|
|
92
|
+
|
|
93
|
+
async def screenshot(self, full_page: bool = False) -> str:
|
|
94
|
+
await self.ensure_page()
|
|
95
|
+
png_bytes = await self.screenshot_bytes(full_page=full_page)
|
|
96
|
+
return base64.b64encode(png_bytes).decode("utf-8")
|
|
97
|
+
|
|
98
|
+
async def click(self, x: int, y: int, button: str = "left") -> None:
|
|
99
|
+
await self.ensure_page()
|
|
100
|
+
match button:
|
|
101
|
+
case "back":
|
|
102
|
+
await self.back()
|
|
103
|
+
case "forward":
|
|
104
|
+
await self.forward()
|
|
105
|
+
case "wheel":
|
|
106
|
+
await self._page.mouse.wheel(x, y)
|
|
107
|
+
case _:
|
|
108
|
+
button_mapping = {"left": "left", "right": "right"}
|
|
109
|
+
button_type = button_mapping.get(button, "left")
|
|
110
|
+
await self._page.mouse.click(x, y, button=button_type)
|
|
111
|
+
|
|
112
|
+
async def double_click(self, x: int, y: int) -> None:
|
|
113
|
+
await self.ensure_page()
|
|
114
|
+
await self._page.mouse.dblclick(x, y)
|
|
115
|
+
|
|
116
|
+
async def scroll(self, x: int, y: int, scroll_x: int, scroll_y: int) -> None:
|
|
117
|
+
await self.ensure_page()
|
|
118
|
+
await self._page.mouse.move(x, y)
|
|
119
|
+
await self._page.evaluate(f"window.scrollBy({scroll_x}, {scroll_y})")
|
|
120
|
+
|
|
121
|
+
async def type(self, text: str) -> None:
|
|
122
|
+
await self.ensure_page()
|
|
123
|
+
await self._page.keyboard.type(text)
|
|
124
|
+
|
|
125
|
+
async def wait(self, ms: int = 1000) -> None:
|
|
126
|
+
await self.ensure_page()
|
|
127
|
+
time.sleep(ms / 1000)
|
|
128
|
+
|
|
129
|
+
async def move(self, x: int, y: int) -> None:
|
|
130
|
+
await self.ensure_page()
|
|
131
|
+
await self._page.mouse.move(x, y)
|
|
132
|
+
|
|
133
|
+
async def keypress(self, keys: List[str]) -> None:
|
|
134
|
+
await self.ensure_page()
|
|
135
|
+
for key in keys:
|
|
136
|
+
mapped_key = CUA_KEY_TO_PLAYWRIGHT_KEY.get(key.lower(), key)
|
|
137
|
+
await self._page.keyboard.press(mapped_key)
|
|
138
|
+
|
|
139
|
+
async def drag(self, path: List[Dict[str, int]]) -> None:
|
|
140
|
+
await self.ensure_page()
|
|
141
|
+
if not path:
|
|
142
|
+
return
|
|
143
|
+
|
|
144
|
+
await self._page.mouse.move(path[0]["x"], path[0]["y"])
|
|
145
|
+
await self._page.mouse.down()
|
|
146
|
+
for point in path[1:]:
|
|
147
|
+
await self._page.mouse.move(point["x"], point["y"])
|
|
148
|
+
await self._page.mouse.up()
|
|
149
|
+
|
|
150
|
+
async def get_current_url(self) -> str:
|
|
151
|
+
await self.ensure_page()
|
|
152
|
+
return self._page.url
|
|
153
|
+
|
|
154
|
+
# --- Extra browser-oriented actions ---
|
|
155
|
+
async def goto(self, url: str) -> None:
|
|
156
|
+
await self.ensure_page()
|
|
157
|
+
try:
|
|
158
|
+
return await self._page.goto(url)
|
|
159
|
+
except Exception as e:
|
|
160
|
+
print(f"Error navigating to {url}: {e}")
|
|
161
|
+
|
|
162
|
+
async def back(self) -> None:
|
|
163
|
+
await self.ensure_page()
|
|
164
|
+
return await self._page.go_back()
|
|
165
|
+
|
|
166
|
+
async def forward(self) -> None:
|
|
167
|
+
await self.ensure_page()
|
|
168
|
+
return await self._page.go_forward()
|
|
169
|
+
|
|
170
|
+
# --- Subclass hook ---
|
|
171
|
+
async def _get_browser_and_page(self) -> tuple[Browser, Page]:
|
|
172
|
+
"""Subclasses must implement, returning (Browser, Page)."""
|
|
173
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import Tuple
|
|
3
|
+
from playwright.async_api import Browser, Page, Error as PlaywrightError
|
|
4
|
+
from .base_playwright import BasePlaywrightComputer
|
|
5
|
+
from browserbase import AsyncBrowserbase
|
|
6
|
+
from dotenv import load_dotenv
|
|
7
|
+
|
|
8
|
+
load_dotenv()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class BrowserbaseBrowser(BasePlaywrightComputer):
|
|
12
|
+
"""
|
|
13
|
+
Browserbase is a headless browser platform that offers a remote browser API. You can use it to control thousands of browsers from anywhere.
|
|
14
|
+
You can find more information about Browserbase at https://www.browserbase.com/computer-use or view our OpenAI CUA Quickstart at https://docs.browserbase.com/integrations/openai-cua/introduction.
|
|
15
|
+
|
|
16
|
+
IMPORTANT: This Browserbase computer requires the use of the `goto` tool defined in playwright_with_custom_functions.py.
|
|
17
|
+
Make sure to include this tool in your configuration when using the Browserbase computer.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
width: int = 1024,
|
|
23
|
+
height: int = 768,
|
|
24
|
+
region: str = "us-west-2",
|
|
25
|
+
proxy: bool = False,
|
|
26
|
+
virtual_mouse: bool = True,
|
|
27
|
+
ad_blocker: bool = False,
|
|
28
|
+
):
|
|
29
|
+
"""
|
|
30
|
+
Initialize the Browserbase instance. Additional configuration options for features such as persistent cookies, ad blockers, file downloads and more can be found in the Browserbase API documentation: https://docs.browserbase.com/reference/api/create-a-session
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
width (int): The width of the browser viewport. Default is 1024.
|
|
34
|
+
height (int): The height of the browser viewport. Default is 768.
|
|
35
|
+
region (str): The region for the Browserbase session. Default is "us-west-2". Pick a region close to you for better performance. https://docs.browserbase.com/guides/multi-region
|
|
36
|
+
proxy (bool): Whether to use a proxy for the session. Default is False. Turn on proxies if you're browsing is frequently interrupted. https://docs.browserbase.com/features/proxies
|
|
37
|
+
virtual_mouse (bool): Whether to enable the virtual mouse cursor. Default is True.
|
|
38
|
+
ad_blocker (bool): Whether to enable the built-in ad blocker. Default is False.
|
|
39
|
+
"""
|
|
40
|
+
super().__init__()
|
|
41
|
+
self.bb = AsyncBrowserbase(api_key=os.getenv("BROWSERBASE_API_KEY"))
|
|
42
|
+
self.project_id = os.getenv("BROWSERBASE_PROJECT_ID")
|
|
43
|
+
self.session = None
|
|
44
|
+
self.dimensions = (width, height)
|
|
45
|
+
self.region = region
|
|
46
|
+
self.proxy = proxy
|
|
47
|
+
self.virtual_mouse = virtual_mouse
|
|
48
|
+
self.ad_blocker = ad_blocker
|
|
49
|
+
|
|
50
|
+
async def _get_browser_and_page(self) -> Tuple[Browser, Page]:
|
|
51
|
+
"""
|
|
52
|
+
Create a Browserbase session and connect to it.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Tuple[Browser, Page]: A tuple containing the connected browser and page objects.
|
|
56
|
+
"""
|
|
57
|
+
# Create a session on Browserbase with specified parameters
|
|
58
|
+
width, height = self.dimensions
|
|
59
|
+
session_params = {
|
|
60
|
+
"project_id": self.project_id,
|
|
61
|
+
"browser_settings": {
|
|
62
|
+
"viewport": {"width": width, "height": height},
|
|
63
|
+
"blockAds": self.ad_blocker,
|
|
64
|
+
},
|
|
65
|
+
"region": self.region,
|
|
66
|
+
"proxies": self.proxy,
|
|
67
|
+
}
|
|
68
|
+
self.session = await self.bb.sessions.create(**session_params)
|
|
69
|
+
|
|
70
|
+
# Print the live session URL
|
|
71
|
+
print(
|
|
72
|
+
f"Watch and control this browser live at https://www.browserbase.com/sessions/{self.session.id}"
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
# Connect to the remote session
|
|
76
|
+
browser = await self._playwright.chromium.connect_over_cdp(
|
|
77
|
+
self.session.connect_url, timeout=60000
|
|
78
|
+
)
|
|
79
|
+
context = browser.contexts[0]
|
|
80
|
+
|
|
81
|
+
# Add event listeners for page creation and closure
|
|
82
|
+
context.on("page", self._handle_new_page)
|
|
83
|
+
|
|
84
|
+
# Only add the init script if virtual_mouse is True
|
|
85
|
+
if self.virtual_mouse:
|
|
86
|
+
await context.add_init_script("""
|
|
87
|
+
// Only run in the top frame
|
|
88
|
+
if (window.self === window.top) {
|
|
89
|
+
function initCursor() {
|
|
90
|
+
const CURSOR_ID = '__cursor__';
|
|
91
|
+
|
|
92
|
+
// Check if cursor element already exists
|
|
93
|
+
if (document.getElementById(CURSOR_ID)) return;
|
|
94
|
+
|
|
95
|
+
const cursor = document.createElement('div');
|
|
96
|
+
cursor.id = CURSOR_ID;
|
|
97
|
+
Object.assign(cursor.style, {
|
|
98
|
+
position: 'fixed',
|
|
99
|
+
top: '0px',
|
|
100
|
+
left: '0px',
|
|
101
|
+
width: '20px',
|
|
102
|
+
height: '20px',
|
|
103
|
+
backgroundImage: 'url("data:image/svg+xml;utf8,<svg xmlns=\\'http://www.w3.org/2000/svg\\' viewBox=\\'0 0 24 24\\' fill=\\'black\\' stroke=\\'white\\' stroke-width=\\'1\\' stroke-linejoin=\\'round\\' stroke-linecap=\\'round\\'><polygon points=\\'2,2 2,22 8,16 14,22 17,19 11,13 20,13\\'/></svg>")',
|
|
104
|
+
backgroundSize: 'cover',
|
|
105
|
+
pointerEvents: 'none',
|
|
106
|
+
zIndex: '99999',
|
|
107
|
+
transform: 'translate(-2px, -2px)',
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
document.body.appendChild(cursor);
|
|
111
|
+
|
|
112
|
+
document.addEventListener("mousemove", (e) => {
|
|
113
|
+
cursor.style.top = e.clientY + "px";
|
|
114
|
+
cursor.style.left = e.clientX + "px";
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Use requestAnimationFrame for early execution
|
|
119
|
+
requestAnimationFrame(function checkBody() {
|
|
120
|
+
if (document.body) {
|
|
121
|
+
initCursor();
|
|
122
|
+
} else {
|
|
123
|
+
requestAnimationFrame(checkBody);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
""")
|
|
128
|
+
|
|
129
|
+
page = context.pages[0]
|
|
130
|
+
page.on("close", self._handle_page_close)
|
|
131
|
+
|
|
132
|
+
await page.goto("https://google.com")
|
|
133
|
+
|
|
134
|
+
return browser, page
|
|
135
|
+
|
|
136
|
+
async def _handle_new_page(self, page: Page):
|
|
137
|
+
"""Handle the creation of a new page."""
|
|
138
|
+
print("New page created")
|
|
139
|
+
self._page = page
|
|
140
|
+
page.on("close", self._handle_page_close)
|
|
141
|
+
|
|
142
|
+
async def _handle_page_close(self, page: Page):
|
|
143
|
+
"""Handle the closure of a page."""
|
|
144
|
+
print("Page closed")
|
|
145
|
+
if self._page == page:
|
|
146
|
+
if self._browser.contexts[0].pages:
|
|
147
|
+
self._page = self._browser.contexts[0].pages[-1]
|
|
148
|
+
else:
|
|
149
|
+
print("Warning: All pages have been closed.")
|
|
150
|
+
self._page = None
|
|
151
|
+
|
|
152
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
153
|
+
"""
|
|
154
|
+
Clean up resources when exiting the context manager.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
exc_type: The type of the exception that caused the context to be exited.
|
|
158
|
+
exc_val: The exception instance that caused the context to be exited.
|
|
159
|
+
exc_tb: A traceback object encapsulating the call stack at the point where the exception occurred.
|
|
160
|
+
"""
|
|
161
|
+
if self._page:
|
|
162
|
+
await self._page.close()
|
|
163
|
+
if self._browser:
|
|
164
|
+
await self._browser.close()
|
|
165
|
+
if self._playwright:
|
|
166
|
+
await self._playwright.stop()
|
|
167
|
+
|
|
168
|
+
if self.session:
|
|
169
|
+
print(
|
|
170
|
+
f"Session completed. View replay at https://browserbase.com/sessions/{self.session.id}"
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
async def screenshot(self) -> str:
|
|
174
|
+
await self.ensure_page()
|
|
175
|
+
|
|
176
|
+
"""
|
|
177
|
+
Capture a screenshot of the current viewport using CDP.
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
str: A base64 encoded string of the screenshot.
|
|
181
|
+
"""
|
|
182
|
+
try:
|
|
183
|
+
# Get CDP session from the page
|
|
184
|
+
cdp_session = await self._page.context.new_cdp_session(self._page)
|
|
185
|
+
|
|
186
|
+
# Capture screenshot using CDP
|
|
187
|
+
result = await cdp_session.send(
|
|
188
|
+
"Page.captureScreenshot", {"format": "png", "fromSurface": True}
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
return result["data"]
|
|
192
|
+
except PlaywrightError as error:
|
|
193
|
+
print(
|
|
194
|
+
f"CDP screenshot failed, falling back to standard screenshot: {error}"
|
|
195
|
+
)
|
|
196
|
+
return await super().screenshot()
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from typing import Protocol, List, Literal, Dict
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Computer(Protocol):
|
|
5
|
+
"""Defines the 'shape' (methods/properties) our loop expects."""
|
|
6
|
+
|
|
7
|
+
@property
|
|
8
|
+
def environment(self) -> Literal["windows", "mac", "linux", "browser"]: ...
|
|
9
|
+
@property
|
|
10
|
+
def dimensions(self) -> tuple[int, int]: ...
|
|
11
|
+
|
|
12
|
+
async def screenshot(self) -> str: ...
|
|
13
|
+
|
|
14
|
+
async def click(self, x: int, y: int, button: str = "left") -> None: ...
|
|
15
|
+
|
|
16
|
+
async def double_click(self, x: int, y: int) -> None: ...
|
|
17
|
+
|
|
18
|
+
async def scroll(self, x: int, y: int, scroll_x: int, scroll_y: int) -> None: ...
|
|
19
|
+
|
|
20
|
+
async def type(self, text: str) -> None: ...
|
|
21
|
+
|
|
22
|
+
async def wait(self, ms: int = 1000) -> None: ...
|
|
23
|
+
|
|
24
|
+
async def move(self, x: int, y: int) -> None: ...
|
|
25
|
+
|
|
26
|
+
async def keypress(self, keys: List[str]) -> None: ...
|
|
27
|
+
|
|
28
|
+
async def drag(self, path: List[Dict[str, int]]) -> None: ...
|
|
29
|
+
|
|
30
|
+
async def get_current_url() -> str: ...
|
|
31
|
+
|
|
32
|
+
async def __aenter__(self) -> "Computer":
|
|
33
|
+
return self
|
|
34
|
+
|
|
35
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb) -> "Computer":
|
|
36
|
+
return self
|