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.

@@ -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