Skip to content

Commit 89a54ac

Browse files
authored
Merge pull request #42 from openai/contrib-refactor
Add support and guidelines for contributing computers
2 parents eb2d58b + 9823abc commit 89a54ac

File tree

18 files changed

+237
-77
lines changed

18 files changed

+237
-77
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
__pycache__/
22
.env
3-
.venv/
3+
.venv/
4+
env/

README.md

Lines changed: 105 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ python simple_cua_loop.py
7474

7575
## Computer Environments
7676

77-
CUA can work with any `Computer` environment that can handle the [CUA actions](https://platform.openai.com/docs/api-reference/responses/object#responses/object-output):
77+
CUA can work with any `Computer` environment that can handle the [CUA actions](https://platform.openai.com/docs/api-reference/responses/object#responses/object-output) (plus a few extra):
7878

7979
| Action | Example |
8080
| ---------------------------------- | ------------------------------- |
@@ -109,6 +109,15 @@ For example, to run the sample app with the `Docker` computer environment, you c
109109
python cli.py --show --computer docker
110110
```
111111

112+
#### Contributed Computers
113+
114+
| Computer | Option | Type | Description | Requirements |
115+
| -------- | ------ | ---- | ----------- | ------------ |
116+
| `tbd` | tbd | tbd | tbd | tbd |
117+
118+
> [!NOTE]
119+
> If you've implemented a new computer, please add it to the "Contributed Computers" section of the README.md file. Clearly indicate any auth / signup requirements. See the [Contributing](#contributing) section for more details.
120+
112121
### Docker Setup
113122

114123
If you want to run the sample app with the `Docker` computer environment, you need to build and run a local Docker container.
@@ -152,3 +161,98 @@ However, if you pass in any `tools` that are also defined in your `Computer` met
152161
This repository provides example implementations with basic safety measures in place.
153162
154163
We recommend reviewing the best practices outlined in our [guide](https://platform.openai.com/docs/guides/tools-computer-use#risks-and-safety), and making sure you understand the risks involved with using this tool.
164+
165+
# Contributing
166+
167+
## Computers
168+
169+
To contribute a new computer, you'll need to implement it, test it, and submit a PR. Please follow the steps below:
170+
171+
### 1. Implement your computer
172+
173+
You will create or modify the following files (and only these files):
174+
175+
| File | Updates |
176+
| ------------------------------------------- | ------------------ |
177+
| `computers/contrib/[your_computer_name].py` | Add computer file. |
178+
| `computers/contrib/__init__.py` | Add to imports. |
179+
| `computers/config.py` | Add to config. |
180+
| `README.md` | Add to README. |
181+
182+
Create a new file in `computers/contrib/[your_computer_name].py` and define your computer class. Make sure to implement the methods defined in the `Computer` class – use the existing implementations as a reference.
183+
184+
```python
185+
class YourComputerName:
186+
def __init__(self):
187+
pass
188+
189+
def screenshot(self):
190+
# TODO: implement
191+
pass
192+
193+
def click(self, x, y):
194+
# TODO: implement
195+
pass
196+
197+
# ... add other methods as needed
198+
```
199+
200+
> [!NOTE]
201+
> For playwright-based computers, make sure to subclass `BasePlaywrightComputer` in `computers/shared/base_playwright.py` – see `computers/default/browserbase.py` for an example.
202+
203+
Import your new computer in the `computers/contrib/__init__.py`:
204+
205+
```python
206+
# ... existing computer imports
207+
from .your_computer_name import YourComputerName
208+
```
209+
210+
And add your new computer to the `computers_config` dictionary in `computers/config.py`:
211+
212+
```python
213+
# ... existing computers_config
214+
"your_computer_name": YourComputerName,
215+
```
216+
217+
Feel free to add your new computer to the "Contributed Computers" section of the README.md file. Clearly indicate any auth / signup requirements.
218+
219+
### 2. Test your computer
220+
221+
Test your new computer (with the CLI). Make sure:
222+
223+
- Basic search / navigation works.
224+
- Any setup / teardown is handled correctly.
225+
- Test e2e with a few different tasks.
226+
227+
Potential gotchas (See `default` computers for reference):
228+
229+
- Scrolling, dragging, and control/command keys.
230+
- Resource allocation and teardown.
231+
- Auth / signup requirements.
232+
233+
### 3. Submit a PR
234+
235+
Your PR should clearly define the following:
236+
237+
- Title: `[contrib] Add computer: <your_computer_name>`
238+
- Description:
239+
240+
```
241+
# Add computer: <your_computer_name>
242+
243+
#### Affiliations
244+
245+
What organization / company / institution are you affiliated with?
246+
247+
#### Computer Description
248+
249+
- Computer type (e.g. browser, linux)
250+
251+
#### Testing Plan
252+
253+
- Signup steps.
254+
- Auth steps.
255+
- Sample queries.
256+
```
257+
258+
Thank you for your contribution! Please follow all of the above guidelines. Failure to do so may result in your PR being rejected.

agent/agent.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,13 @@ def __init__(
3333
self.acknowledge_safety_check_callback = acknowledge_safety_check_callback
3434

3535
if computer:
36+
dimensions = computer.get_dimensions()
3637
self.tools += [
3738
{
3839
"type": "computer-preview",
39-
"display_width": computer.dimensions[0],
40-
"display_height": computer.dimensions[1],
41-
"environment": computer.environment,
40+
"display_width": dimensions[0],
41+
"display_height": dimensions[1],
42+
"environment": computer.get_environment(),
4243
},
4344
]
4445

@@ -102,7 +103,7 @@ def handle_item(self, item):
102103
}
103104

104105
# additional URL safety checks for browser environments
105-
if self.computer.environment == "browser":
106+
if self.computer.get_environment() == "browser":
106107
current_url = self.computer.get_current_url()
107108
check_blocklisted_url(current_url)
108109
call_output["output"]["current_url"] = current_url

cli.py

Lines changed: 7 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
11
import argparse
22
from agent.agent import Agent
3-
from computers import (
4-
BrowserbaseBrowser,
5-
ScrapybaraBrowser,
6-
ScrapybaraUbuntu,
7-
LocalPlaywrightComputer,
8-
DockerComputer,
9-
)
3+
from computers.config import *
4+
from computers.default import *
5+
from computers import computers_config
6+
107

118
def acknowledge_safety_check_callback(message: str) -> bool:
129
response = input(
@@ -21,13 +18,7 @@ def main():
2118
)
2219
parser.add_argument(
2320
"--computer",
24-
choices=[
25-
"local-playwright",
26-
"docker",
27-
"browserbase",
28-
"scrapybara-browser",
29-
"scrapybara-ubuntu",
30-
],
21+
choices=computers_config.keys(),
3122
help="Choose the computer environment to use.",
3223
default="local-playwright",
3324
)
@@ -54,16 +45,7 @@ def main():
5445
default="https://bing.com",
5546
)
5647
args = parser.parse_args()
57-
58-
computer_mapping = {
59-
"local-playwright": LocalPlaywrightComputer,
60-
"docker": DockerComputer,
61-
"browserbase": BrowserbaseBrowser,
62-
"scrapybara-browser": ScrapybaraBrowser,
63-
"scrapybara-ubuntu": ScrapybaraUbuntu,
64-
}
65-
66-
ComputerClass = computer_mapping[args.computer]
48+
ComputerClass = computers_config[args.computer]
6749

6850
with ComputerClass() as computer:
6951
agent = Agent(
@@ -72,7 +54,6 @@ def main():
7254
)
7355
items = []
7456

75-
7657
if args.computer in ["browserbase", "local-playwright"]:
7758
if not args.start_url.startswith("http"):
7859
args.start_url = "https://" + args.start_url
@@ -81,7 +62,7 @@ def main():
8162
while True:
8263
try:
8364
user_input = args.input or input("> ")
84-
if user_input == 'exit':
65+
if user_input == "exit":
8566
break
8667
except EOFError as e:
8768
print(f"An error occurred: {e}")

computers/__init__.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
1+
from . import default
2+
from . import contrib
13
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
4+
from .config import computers_config
5+
6+
__all__ = [
7+
"default",
8+
"contrib",
9+
"Computer",
10+
"computers_config",
11+
]

computers/computer.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,9 @@
44
class Computer(Protocol):
55
"""Defines the 'shape' (methods/properties) our loop expects."""
66

7-
@property
8-
def environment(self) -> Literal["windows", "mac", "linux", "browser"]: ...
9-
@property
10-
def dimensions(self) -> tuple[int, int]: ...
7+
def get_environment(self) -> Literal["windows", "mac", "linux", "browser"]: ...
8+
9+
def get_dimensions(self) -> tuple[int, int]: ...
1110

1211
def screenshot(self) -> str: ...
1312

computers/contrib/__init__.py

Whitespace-only changes.

computers/default/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from .browserbase import BrowserbaseBrowser
2+
from .local_playwright import LocalPlaywrightBrowser
3+
from .docker import DockerComputer
4+
from .scrapybara import ScrapybaraBrowser, ScrapybaraUbuntu

computers/browserbase.py renamed to computers/default/browserbase.py

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import os
22
from typing import Tuple, Dict, List, Union, Optional
33
from playwright.sync_api import Browser, Page, BrowserContext, Error as PlaywrightError
4-
from .base_playwright import BasePlaywrightComputer
4+
from ..shared.base_playwright import BasePlaywrightComputer
55
from browserbase import Browserbase
66
from dotenv import load_dotenv
77
import base64
@@ -18,6 +18,9 @@ class BrowserbaseBrowser(BasePlaywrightComputer):
1818
Make sure to include this tool in your configuration when using the Browserbase computer.
1919
"""
2020

21+
def get_dimensions(self):
22+
return self.dimensions
23+
2124
def __init__(
2225
self,
2326
width: int = 1024,
@@ -75,8 +78,7 @@ def _get_browser_and_page(self) -> Tuple[Browser, Page]:
7578

7679
# Connect to the remote session
7780
browser = self._playwright.chromium.connect_over_cdp(
78-
self.session.connect_url,
79-
timeout=60000
81+
self.session.connect_url, timeout=60000
8082
)
8183
context = browser.contexts[0]
8284

@@ -85,7 +87,8 @@ def _get_browser_and_page(self) -> Tuple[Browser, Page]:
8587

8688
# Only add the init script if virtual_mouse is True
8789
if self.virtual_mouse:
88-
context.add_init_script("""
90+
context.add_init_script(
91+
"""
8992
// Only run in the top frame
9093
if (window.self === window.top) {
9194
function initCursor() {
@@ -126,7 +129,8 @@ def _get_browser_and_page(self) -> Tuple[Browser, Page]:
126129
}
127130
});
128131
}
129-
""")
132+
"""
133+
)
130134

131135
page = context.pages[0]
132136
page.on("close", self._handle_page_close)
@@ -184,12 +188,13 @@ def screenshot(self) -> str:
184188
cdp_session = self._page.context.new_cdp_session(self._page)
185189

186190
# Capture screenshot using CDP
187-
result = cdp_session.send("Page.captureScreenshot", {
188-
"format": "png",
189-
"fromSurface": True
190-
})
191+
result = cdp_session.send(
192+
"Page.captureScreenshot", {"format": "png", "fromSurface": True}
193+
)
191194

192-
return result['data']
195+
return result["data"]
193196
except PlaywrightError as error:
194-
print(f"CDP screenshot failed, falling back to standard screenshot: {error}")
197+
print(
198+
f"CDP screenshot failed, falling back to standard screenshot: {error}"
199+
)
195200
return super().screenshot()

computers/docker.py renamed to computers/default/docker.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@
44

55

66
class DockerComputer:
7-
environment = "linux"
8-
dimensions = (1280, 720) # Default fallback; will be updated in __enter__.
7+
def get_environment(self):
8+
return "linux"
9+
10+
def get_dimensions(self):
11+
return (1280, 720) # Default fallback; will be updated in __enter__.
912

1013
def __init__(
1114
self,
@@ -162,5 +165,10 @@ def drag(self, path: list[dict[str, int]]) -> None:
162165
f"DISPLAY={self.display} xdotool mousemove {start_x} {start_y} mousedown 1"
163166
)
164167
for point in path[1:]:
165-
self._exec(f"DISPLAY={self.display} xdotool mousemove {point['x']} {point['y']}")
168+
self._exec(
169+
f"DISPLAY={self.display} xdotool mousemove {point['x']} {point['y']}"
170+
)
166171
self._exec(f"DISPLAY={self.display} xdotool mouseup 1")
172+
173+
def get_current_url(self):
174+
return None

0 commit comments

Comments
 (0)