fleet-python 0.2.1__py3-none-any.whl → 0.2.2__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 fleet-python might be problematic. Click here for more details.
- examples/dsl_example.py +112 -0
- examples/example.py +11 -24
- examples/openai_example.py +197 -78
- examples/quickstart.py +5 -5
- fleet/__init__.py +3 -1
- fleet/base.py +1 -1
- fleet/client.py +60 -28
- fleet/env/__init__.py +2 -21
- fleet/env/client.py +9 -253
- fleet/manager/__init__.py +22 -0
- fleet/manager/client.py +258 -0
- fleet/resources/base.py +5 -2
- fleet/resources/browser.py +20 -10
- fleet/resources/sqlite.py +3 -3
- fleet/verifiers/__init__.py +4 -0
- fleet/verifiers/database_snapshot.py +666 -0
- fleet/verifiers/sql_differ.py +187 -0
- {fleet_python-0.2.1.dist-info → fleet_python-0.2.2.dist-info}/METADATA +1 -1
- fleet_python-0.2.2.dist-info/RECORD +27 -0
- fleet_python-0.2.1.dist-info/RECORD +0 -21
- /fleet/{env → manager}/base.py +0 -0
- /fleet/{env → manager}/models.py +0 -0
- {fleet_python-0.2.1.dist-info → fleet_python-0.2.2.dist-info}/WHEEL +0 -0
- {fleet_python-0.2.1.dist-info → fleet_python-0.2.2.dist-info}/licenses/LICENSE +0 -0
- {fleet_python-0.2.1.dist-info → fleet_python-0.2.2.dist-info}/top_level.txt +0 -0
examples/dsl_example.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
from fleet.verifiers import DatabaseSnapshot, IgnoreConfig
|
|
2
|
+
|
|
3
|
+
async def validate_give_me_more_tasks(
|
|
4
|
+
before: DatabaseSnapshot,
|
|
5
|
+
after: DatabaseSnapshot,
|
|
6
|
+
transcript: str | None = None,
|
|
7
|
+
) -> int:
|
|
8
|
+
"""Validate that bugs are moved to sprint 3 and assigned correctly."""
|
|
9
|
+
|
|
10
|
+
# Get user IDs
|
|
11
|
+
raj_user = after.table("users").eq("name", "Raj Patel").first()
|
|
12
|
+
sarah_kim_user = after.table("users").eq("name", "Sarah Kim").first()
|
|
13
|
+
|
|
14
|
+
if not raj_user:
|
|
15
|
+
raise AssertionError("User 'Raj Patel' not found")
|
|
16
|
+
if not sarah_kim_user:
|
|
17
|
+
raise AssertionError("User 'Sarah Kim' not found")
|
|
18
|
+
|
|
19
|
+
raj_id = raj_user["id"]
|
|
20
|
+
sarah_kim_id = sarah_kim_user["id"]
|
|
21
|
+
|
|
22
|
+
# Verify SCRUM-555 (data pipeline bug) is assigned to Sarah Kim
|
|
23
|
+
after.table("issues").eq("id", "SCRUM-555").assert_eq("owner", sarah_kim_id)
|
|
24
|
+
|
|
25
|
+
# Verify other bugs are assigned to Raj Patel
|
|
26
|
+
other_bugs = [
|
|
27
|
+
"SCRUM-780",
|
|
28
|
+
"SCRUM-781",
|
|
29
|
+
"SCRUM-790",
|
|
30
|
+
"SCRUM-822",
|
|
31
|
+
"SCRUM-882",
|
|
32
|
+
"SCRUM-897",
|
|
33
|
+
"SCRUM-956",
|
|
34
|
+
"SCRUM-1331",
|
|
35
|
+
"SCRUM-1312",
|
|
36
|
+
"SCRUM-1210",
|
|
37
|
+
"SCRUM-1230",
|
|
38
|
+
"SCRUM-1282",
|
|
39
|
+
]
|
|
40
|
+
for bug_id in other_bugs:
|
|
41
|
+
after.table("issues").eq("id", bug_id).assert_eq("owner", raj_id)
|
|
42
|
+
|
|
43
|
+
# Verify all bugs are in sprint_3
|
|
44
|
+
all_bugs = ["SCRUM-555"] + other_bugs
|
|
45
|
+
for bug_id in all_bugs:
|
|
46
|
+
after.table("sprint_issues").eq("issue_id", bug_id).assert_eq(
|
|
47
|
+
"sprint_id", "sprint_3"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# Configure ignore settings
|
|
51
|
+
ignore_config = IgnoreConfig(
|
|
52
|
+
tables={"activities", "pageviews", "sprint_issues"},
|
|
53
|
+
table_fields={
|
|
54
|
+
"issues": {"updated_at", "created_at", "rowid"},
|
|
55
|
+
"users": {"updated_at", "created_at", "rowid"},
|
|
56
|
+
"sprint_issues": {"updated_at", "created_at", "rowid"},
|
|
57
|
+
},
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# Build expected changes
|
|
61
|
+
expected_changes: list[dict] = []
|
|
62
|
+
|
|
63
|
+
# Assignment changes
|
|
64
|
+
expected_changes.append(
|
|
65
|
+
{
|
|
66
|
+
"table": "issues",
|
|
67
|
+
"pk": "SCRUM-555",
|
|
68
|
+
"field": "owner",
|
|
69
|
+
"after": sarah_kim_id,
|
|
70
|
+
}
|
|
71
|
+
)
|
|
72
|
+
for bug_id in other_bugs:
|
|
73
|
+
expected_changes.append(
|
|
74
|
+
{
|
|
75
|
+
"table": "issues",
|
|
76
|
+
"pk": bug_id,
|
|
77
|
+
"field": "owner",
|
|
78
|
+
"after": raj_id,
|
|
79
|
+
}
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# Sprint changes
|
|
83
|
+
for bug_id in all_bugs:
|
|
84
|
+
# Remove from previous sprint if present
|
|
85
|
+
before_assignment = (
|
|
86
|
+
before.table("sprint_issues").eq("issue_id", bug_id).first()
|
|
87
|
+
)
|
|
88
|
+
if before_assignment:
|
|
89
|
+
old_sprint = before_assignment.get("sprint_id")
|
|
90
|
+
expected_changes.append(
|
|
91
|
+
{
|
|
92
|
+
"table": "sprint_issues",
|
|
93
|
+
"pk": (old_sprint, bug_id),
|
|
94
|
+
"field": None,
|
|
95
|
+
"after": "__removed__",
|
|
96
|
+
}
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# Add to sprint_3
|
|
100
|
+
expected_changes.append(
|
|
101
|
+
{
|
|
102
|
+
"table": "sprint_issues",
|
|
103
|
+
"pk": ("sprint_3", bug_id),
|
|
104
|
+
"field": None,
|
|
105
|
+
"after": "__added__",
|
|
106
|
+
}
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# Enforce invariant
|
|
110
|
+
before.diff(after, ignore_config).expect_only(expected_changes)
|
|
111
|
+
|
|
112
|
+
return TASK_SUCCESSFUL_SCORE
|
examples/example.py
CHANGED
|
@@ -6,45 +6,32 @@ import fleet as flt
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
async def main():
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
environments = await fleet.list_envs()
|
|
9
|
+
environments = await flt.env.list_envs()
|
|
12
10
|
print("Environments:", len(environments))
|
|
13
11
|
|
|
14
12
|
# Create a new instance
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
)
|
|
18
|
-
print("New Instance:", instance.instance_id)
|
|
19
|
-
|
|
20
|
-
environment = await fleet.environment(instance.env_key)
|
|
21
|
-
print("Environment Default Version:", environment.default_version)
|
|
13
|
+
env = await flt.env.make("hubspot:v1.2.7")
|
|
14
|
+
print("New Instance:", env.instance_id)
|
|
22
15
|
|
|
23
|
-
response = await
|
|
16
|
+
response = await env.reset(seed=42)
|
|
24
17
|
print("Reset response:", response)
|
|
25
18
|
|
|
26
|
-
print(await
|
|
19
|
+
print(await env.resources())
|
|
27
20
|
|
|
28
|
-
sqlite =
|
|
21
|
+
sqlite = env.db()
|
|
29
22
|
print("SQLite:", await sqlite.describe())
|
|
30
23
|
|
|
31
24
|
print("Query:", await sqlite.query("SELECT * FROM users"))
|
|
32
25
|
|
|
33
|
-
sqlite = await
|
|
26
|
+
sqlite = await env.state("sqlite://current").describe()
|
|
34
27
|
print("SQLite:", sqlite)
|
|
35
28
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
)
|
|
39
|
-
|
|
40
|
-
browser = await instance.env.browser("cdp").describe()
|
|
41
|
-
print("CDP Page URL:", browser.cdp_page_url)
|
|
42
|
-
print("CDP Browser URL:", browser.cdp_browser_url)
|
|
43
|
-
print("CDP Devtools URL:", browser.cdp_devtools_url)
|
|
29
|
+
browser = env.browser()
|
|
30
|
+
print("CDP URL:", await browser.cdp_url())
|
|
31
|
+
print("Devtools URL:", await browser.devtools_url())
|
|
44
32
|
|
|
45
33
|
# Delete the instance
|
|
46
|
-
|
|
47
|
-
print("Instance deleted:", instance.terminated_at)
|
|
34
|
+
await env.close()
|
|
48
35
|
|
|
49
36
|
|
|
50
37
|
if __name__ == "__main__":
|
examples/openai_example.py
CHANGED
|
@@ -1,7 +1,65 @@
|
|
|
1
|
-
import time
|
|
2
1
|
import base64
|
|
3
|
-
from typing import List, Dict, Callable
|
|
4
|
-
from playwright.
|
|
2
|
+
from typing import List, Dict, Callable, Optional
|
|
3
|
+
from playwright.async_api import async_playwright, Browser, Page
|
|
4
|
+
import httpx
|
|
5
|
+
import json
|
|
6
|
+
import io
|
|
7
|
+
from io import BytesIO
|
|
8
|
+
from PIL import Image
|
|
9
|
+
import os
|
|
10
|
+
import asyncio
|
|
11
|
+
import fleet as flt
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def sanitize_message(msg: dict) -> dict:
|
|
15
|
+
"""Return a copy of the message with image_url omitted for computer_call_output messages."""
|
|
16
|
+
if msg.get("type") == "computer_call_output":
|
|
17
|
+
output = msg.get("output", {})
|
|
18
|
+
if isinstance(output, dict):
|
|
19
|
+
sanitized = msg.copy()
|
|
20
|
+
sanitized["output"] = {**output, "image_url": "[omitted]"}
|
|
21
|
+
return sanitized
|
|
22
|
+
return msg
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
async def create_response(**kwargs):
|
|
26
|
+
url = "https://api.openai.com/v1/responses"
|
|
27
|
+
headers = {
|
|
28
|
+
"Authorization": f"Bearer {os.getenv('OPENAI_API_KEY')}",
|
|
29
|
+
"Content-Type": "application/json",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
openai_org = os.getenv("OPENAI_ORG")
|
|
33
|
+
if openai_org:
|
|
34
|
+
headers["Openai-Organization"] = openai_org
|
|
35
|
+
|
|
36
|
+
# Configure timeout: 30 seconds for connect, 60 seconds for read
|
|
37
|
+
timeout = httpx.Timeout(connect=60.0, read=60.0, write=60.0, pool=60.0)
|
|
38
|
+
|
|
39
|
+
async with httpx.AsyncClient(timeout=timeout) as client:
|
|
40
|
+
response = await client.post(url, headers=headers, json=kwargs)
|
|
41
|
+
|
|
42
|
+
if response.status_code != 200:
|
|
43
|
+
print(f"Error: {response.status_code} {response.text}")
|
|
44
|
+
|
|
45
|
+
return response.json()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def pp(obj):
|
|
49
|
+
print(json.dumps(obj, indent=4))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def show_image(base_64_image):
|
|
53
|
+
image_data = base64.b64decode(base_64_image)
|
|
54
|
+
image = Image.open(BytesIO(image_data))
|
|
55
|
+
image.show()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def calculate_image_dimensions(base_64_image):
|
|
59
|
+
image_data = base64.b64decode(base_64_image)
|
|
60
|
+
image = Image.open(io.BytesIO(image_data))
|
|
61
|
+
return image.size
|
|
62
|
+
|
|
5
63
|
|
|
6
64
|
# Optional: key mapping if your model uses "CUA" style keys
|
|
7
65
|
CUA_KEY_TO_PLAYWRIGHT_KEY = {
|
|
@@ -48,136 +106,147 @@ class BasePlaywrightComputer:
|
|
|
48
106
|
return "browser"
|
|
49
107
|
|
|
50
108
|
def get_dimensions(self):
|
|
51
|
-
return (
|
|
109
|
+
return (1920, 1080)
|
|
52
110
|
|
|
53
111
|
def __init__(self):
|
|
54
112
|
self._playwright = None
|
|
55
113
|
self._browser: Browser | None = None
|
|
56
114
|
self._page: Page | None = None
|
|
57
115
|
|
|
58
|
-
def
|
|
116
|
+
async def __aenter__(self):
|
|
59
117
|
# Start Playwright and call the subclass hook for getting browser/page
|
|
60
|
-
self._playwright =
|
|
61
|
-
self._browser, self._page = self._get_browser_and_page()
|
|
118
|
+
self._playwright = await async_playwright().start()
|
|
119
|
+
self._browser, self._page = await self._get_browser_and_page()
|
|
62
120
|
|
|
63
121
|
# Set up network interception to flag URLs matching domains in BLOCKED_DOMAINS
|
|
64
|
-
def handle_route(route, request):
|
|
65
|
-
route.continue_()
|
|
122
|
+
async def handle_route(route, request):
|
|
123
|
+
await route.continue_()
|
|
66
124
|
|
|
67
|
-
self._page.route("**/*", handle_route)
|
|
125
|
+
await self._page.route("**/*", handle_route)
|
|
68
126
|
|
|
69
127
|
return self
|
|
70
128
|
|
|
71
|
-
def
|
|
72
|
-
if self._browser:
|
|
73
|
-
|
|
129
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
130
|
+
# if self._browser:
|
|
131
|
+
# await self._browser.close()
|
|
74
132
|
if self._playwright:
|
|
75
|
-
self._playwright.stop()
|
|
133
|
+
await self._playwright.stop()
|
|
76
134
|
|
|
77
135
|
def get_current_url(self) -> str:
|
|
78
136
|
return self._page.url
|
|
79
137
|
|
|
80
138
|
# --- Common "Computer" actions ---
|
|
81
|
-
def screenshot(self) -> str:
|
|
139
|
+
async def screenshot(self) -> str:
|
|
82
140
|
"""Capture only the viewport (not full_page)."""
|
|
83
|
-
png_bytes = self._page.screenshot(full_page=False)
|
|
141
|
+
png_bytes = await self._page.screenshot(full_page=False)
|
|
84
142
|
return base64.b64encode(png_bytes).decode("utf-8")
|
|
85
143
|
|
|
86
|
-
def click(self, x: int, y: int, button: str = "left") -> None:
|
|
144
|
+
async def click(self, x: int, y: int, button: str = "left") -> None:
|
|
87
145
|
if button == "back":
|
|
88
|
-
self.back()
|
|
146
|
+
await self.back()
|
|
89
147
|
elif button == "forward":
|
|
90
|
-
self.forward()
|
|
148
|
+
await self.forward()
|
|
91
149
|
elif button == "wheel":
|
|
92
|
-
self._page.mouse.wheel(x, y)
|
|
150
|
+
await self._page.mouse.wheel(x, y)
|
|
93
151
|
else:
|
|
94
152
|
button_mapping = {"left": "left", "right": "right"}
|
|
95
153
|
button_type = button_mapping.get(button, "left")
|
|
96
|
-
self._page.mouse.click(x, y, button=button_type)
|
|
154
|
+
await self._page.mouse.click(x, y, button=button_type)
|
|
97
155
|
|
|
98
|
-
def double_click(self, x: int, y: int) -> None:
|
|
99
|
-
self._page.mouse.dblclick(x, y)
|
|
156
|
+
async def double_click(self, x: int, y: int) -> None:
|
|
157
|
+
await self._page.mouse.dblclick(x, y)
|
|
100
158
|
|
|
101
|
-
def scroll(self, x: int, y: int, scroll_x: int, scroll_y: int) -> None:
|
|
102
|
-
self._page.mouse.move(x, y)
|
|
103
|
-
self._page.evaluate(f"window.scrollBy({scroll_x}, {scroll_y})")
|
|
159
|
+
async def scroll(self, x: int, y: int, scroll_x: int, scroll_y: int) -> None:
|
|
160
|
+
await self._page.mouse.move(x, y)
|
|
161
|
+
await self._page.evaluate(f"window.scrollBy({scroll_x}, {scroll_y})")
|
|
104
162
|
|
|
105
|
-
def type(self, text: str) -> None:
|
|
106
|
-
self._page.keyboard.type(text)
|
|
163
|
+
async def type(self, text: str) -> None:
|
|
164
|
+
await self._page.keyboard.type(text)
|
|
107
165
|
|
|
108
|
-
def wait(self, ms: int = 1000) -> None:
|
|
109
|
-
|
|
166
|
+
async def wait(self, ms: int = 1000) -> None:
|
|
167
|
+
await asyncio.sleep(ms / 1000)
|
|
110
168
|
|
|
111
|
-
def move(self, x: int, y: int) -> None:
|
|
112
|
-
self._page.mouse.move(x, y)
|
|
169
|
+
async def move(self, x: int, y: int) -> None:
|
|
170
|
+
await self._page.mouse.move(x, y)
|
|
113
171
|
|
|
114
|
-
def keypress(self, keys: List[str]) -> None:
|
|
172
|
+
async def keypress(self, keys: List[str]) -> None:
|
|
115
173
|
mapped_keys = [CUA_KEY_TO_PLAYWRIGHT_KEY.get(key.lower(), key) for key in keys]
|
|
116
174
|
for key in mapped_keys:
|
|
117
|
-
self._page.keyboard.down(key)
|
|
175
|
+
await self._page.keyboard.down(key)
|
|
118
176
|
for key in reversed(mapped_keys):
|
|
119
|
-
self._page.keyboard.up(key)
|
|
177
|
+
await self._page.keyboard.up(key)
|
|
120
178
|
|
|
121
|
-
def drag(self, path: List[Dict[str, int]]) -> None:
|
|
179
|
+
async def drag(self, path: List[Dict[str, int]]) -> None:
|
|
122
180
|
if not path:
|
|
123
181
|
return
|
|
124
|
-
self._page.mouse.move(path[0]["x"], path[0]["y"])
|
|
125
|
-
self._page.mouse.down()
|
|
182
|
+
await self._page.mouse.move(path[0]["x"], path[0]["y"])
|
|
183
|
+
await self._page.mouse.down()
|
|
126
184
|
for point in path[1:]:
|
|
127
|
-
self._page.mouse.move(point["x"], point["y"])
|
|
128
|
-
self._page.mouse.up()
|
|
185
|
+
await self._page.mouse.move(point["x"], point["y"])
|
|
186
|
+
await self._page.mouse.up()
|
|
129
187
|
|
|
130
188
|
# --- Extra browser-oriented actions ---
|
|
131
|
-
def goto(self, url: str) -> None:
|
|
189
|
+
async def goto(self, url: str) -> None:
|
|
132
190
|
try:
|
|
133
|
-
return self._page.goto(url)
|
|
191
|
+
return await self._page.goto(url)
|
|
134
192
|
except Exception as e:
|
|
135
193
|
print(f"Error navigating to {url}: {e}")
|
|
136
194
|
|
|
137
|
-
def back(self) -> None:
|
|
138
|
-
return self._page.go_back()
|
|
195
|
+
async def back(self) -> None:
|
|
196
|
+
return await self._page.go_back()
|
|
139
197
|
|
|
140
|
-
def forward(self) -> None:
|
|
141
|
-
return self._page.go_forward()
|
|
198
|
+
async def forward(self) -> None:
|
|
199
|
+
return await self._page.go_forward()
|
|
142
200
|
|
|
143
201
|
# --- Subclass hook ---
|
|
144
|
-
def _get_browser_and_page(self) -> tuple[Browser, Page]:
|
|
202
|
+
async def _get_browser_and_page(self) -> tuple[Browser, Page]:
|
|
145
203
|
"""Subclasses must implement, returning (Browser, Page)."""
|
|
146
204
|
raise NotImplementedError
|
|
147
205
|
|
|
148
206
|
|
|
149
|
-
class
|
|
207
|
+
class FleetPlaywrightBrowser(BasePlaywrightComputer):
|
|
150
208
|
"""Launches a local Chromium instance using Playwright."""
|
|
151
209
|
|
|
152
|
-
def __init__(
|
|
210
|
+
def __init__(
|
|
211
|
+
self,
|
|
212
|
+
fleet: flt.AsyncFleet,
|
|
213
|
+
env_key: str,
|
|
214
|
+
version: Optional[str] = None,
|
|
215
|
+
headless: bool = False,
|
|
216
|
+
):
|
|
153
217
|
super().__init__()
|
|
218
|
+
self.fleet = fleet
|
|
219
|
+
self.env_key = env_key
|
|
220
|
+
self.version = version
|
|
154
221
|
self.headless = headless
|
|
155
222
|
|
|
156
|
-
def _get_browser_and_page(self) -> tuple[Browser, Page]:
|
|
223
|
+
async def _get_browser_and_page(self) -> tuple[Browser, Page]:
|
|
157
224
|
width, height = self.get_dimensions()
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
browser = self._playwright.chromium.launch(
|
|
164
|
-
chromium_sandbox=True,
|
|
165
|
-
headless=self.headless,
|
|
166
|
-
args=launch_args,
|
|
167
|
-
env={"DISPLAY": ":0"},
|
|
225
|
+
|
|
226
|
+
# Create an instance of the environment
|
|
227
|
+
print(f"Creating instance of {self.env_key} {self.version}...")
|
|
228
|
+
self.instance = await self.fleet.make(
|
|
229
|
+
flt.InstanceRequest(env_key=self.env_key, version=self.version)
|
|
168
230
|
)
|
|
169
231
|
|
|
170
|
-
|
|
232
|
+
# Start the browser
|
|
233
|
+
print("Starting browser...")
|
|
234
|
+
await self.instance.env.browser("cdp").start()
|
|
235
|
+
print("Getting CDP URL...")
|
|
236
|
+
cdp = await self.instance.env.browser("cdp").describe()
|
|
237
|
+
print("DevTools URL:", cdp.cdp_devtools_url)
|
|
238
|
+
|
|
239
|
+
# Connect to the browser
|
|
240
|
+
browser = await self._playwright.chromium.connect_over_cdp(cdp.cdp_browser_url)
|
|
171
241
|
|
|
172
242
|
# Add event listeners for page creation and closure
|
|
243
|
+
context = browser.contexts[0]
|
|
173
244
|
context.on("page", self._handle_new_page)
|
|
174
245
|
|
|
175
|
-
page = context.
|
|
176
|
-
page.set_viewport_size({"width": width, "height": height})
|
|
246
|
+
page = context.pages[0]
|
|
247
|
+
await page.set_viewport_size({"width": width, "height": height})
|
|
177
248
|
page.on("close", self._handle_page_close)
|
|
178
249
|
|
|
179
|
-
page.goto("https://bing.com")
|
|
180
|
-
|
|
181
250
|
return browser, page
|
|
182
251
|
|
|
183
252
|
def _handle_new_page(self, page: Page):
|
|
@@ -207,7 +276,7 @@ class Agent:
|
|
|
207
276
|
def __init__(
|
|
208
277
|
self,
|
|
209
278
|
model="computer-use-preview",
|
|
210
|
-
computer:
|
|
279
|
+
computer: FleetPlaywrightBrowser = None,
|
|
211
280
|
tools: list[dict] = [],
|
|
212
281
|
acknowledge_safety_check_callback: Callable = lambda: False,
|
|
213
282
|
):
|
|
@@ -234,8 +303,11 @@ class Agent:
|
|
|
234
303
|
if self.debug:
|
|
235
304
|
pp(*args)
|
|
236
305
|
|
|
237
|
-
def handle_item(self, item):
|
|
306
|
+
async def handle_item(self, item):
|
|
238
307
|
"""Handle each item; may cause a computer action + screenshot."""
|
|
308
|
+
if self.debug:
|
|
309
|
+
print(f"Handling item of type: {item.get('type')}")
|
|
310
|
+
|
|
239
311
|
if item["type"] == "message":
|
|
240
312
|
if self.print_steps:
|
|
241
313
|
print(item["content"][0]["text"])
|
|
@@ -247,7 +319,7 @@ class Agent:
|
|
|
247
319
|
|
|
248
320
|
if hasattr(self.computer, name): # if function exists on computer, call it
|
|
249
321
|
method = getattr(self.computer, name)
|
|
250
|
-
method(**args)
|
|
322
|
+
await method(**args)
|
|
251
323
|
return [
|
|
252
324
|
{
|
|
253
325
|
"type": "function_call_output",
|
|
@@ -264,9 +336,9 @@ class Agent:
|
|
|
264
336
|
print(f"{action_type}({action_args})")
|
|
265
337
|
|
|
266
338
|
method = getattr(self.computer, action_type)
|
|
267
|
-
method(**action_args)
|
|
339
|
+
await method(**action_args)
|
|
268
340
|
|
|
269
|
-
screenshot_base64 = self.computer.screenshot()
|
|
341
|
+
screenshot_base64 = await self.computer.screenshot()
|
|
270
342
|
if self.show_images:
|
|
271
343
|
show_image(screenshot_base64)
|
|
272
344
|
|
|
@@ -292,13 +364,12 @@ class Agent:
|
|
|
292
364
|
# additional URL safety checks for browser environments
|
|
293
365
|
if self.computer.get_environment() == "browser":
|
|
294
366
|
current_url = self.computer.get_current_url()
|
|
295
|
-
check_blocklisted_url(current_url)
|
|
296
367
|
call_output["output"]["current_url"] = current_url
|
|
297
368
|
|
|
298
369
|
return [call_output]
|
|
299
370
|
return []
|
|
300
371
|
|
|
301
|
-
def run_full_turn(
|
|
372
|
+
async def run_full_turn(
|
|
302
373
|
self, input_items, print_steps=True, debug=False, show_images=False
|
|
303
374
|
):
|
|
304
375
|
self.print_steps = print_steps
|
|
@@ -310,7 +381,7 @@ class Agent:
|
|
|
310
381
|
while new_items[-1].get("role") != "assistant" if new_items else True:
|
|
311
382
|
self.debug_print([sanitize_message(msg) for msg in input_items + new_items])
|
|
312
383
|
|
|
313
|
-
response = create_response(
|
|
384
|
+
response = await create_response(
|
|
314
385
|
model=self.model,
|
|
315
386
|
input=input_items + new_items,
|
|
316
387
|
tools=self.tools,
|
|
@@ -318,12 +389,60 @@ class Agent:
|
|
|
318
389
|
)
|
|
319
390
|
self.debug_print(response)
|
|
320
391
|
|
|
321
|
-
if "output" not in response
|
|
322
|
-
|
|
323
|
-
|
|
392
|
+
if "output" not in response:
|
|
393
|
+
if self.debug:
|
|
394
|
+
print("Full response:", response)
|
|
395
|
+
if "error" in response:
|
|
396
|
+
error_msg = response["error"].get("message", "Unknown error")
|
|
397
|
+
raise ValueError(f"API Error: {error_msg}")
|
|
398
|
+
else:
|
|
399
|
+
raise ValueError("No output from model")
|
|
324
400
|
else:
|
|
325
|
-
|
|
401
|
+
# Append each item from the model output to conversation history
|
|
402
|
+
# in the exact order we received them, **without filtering** so that
|
|
403
|
+
# required pairs such as reasoning → computer_call are preserved.
|
|
326
404
|
for item in response["output"]:
|
|
327
|
-
|
|
405
|
+
# First, record the original item itself.
|
|
406
|
+
new_items.append(item)
|
|
407
|
+
|
|
408
|
+
# Next, perform any local side-effects (browser actions, etc.).
|
|
409
|
+
handled_items = await self.handle_item(item)
|
|
410
|
+
|
|
411
|
+
# If the handler generated additional items (e.g. computer_call_output)
|
|
412
|
+
# we append those *immediately* so the order remains:
|
|
413
|
+
# reasoning → computer_call → computer_call_output
|
|
414
|
+
if handled_items:
|
|
415
|
+
new_items += handled_items
|
|
328
416
|
|
|
329
417
|
return new_items
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
tools = []
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
async def ainput(prompt: str = "") -> str:
|
|
424
|
+
"""Async version of input()"""
|
|
425
|
+
loop = asyncio.get_event_loop()
|
|
426
|
+
return await loop.run_in_executor(None, input, prompt)
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
async def main():
|
|
430
|
+
fleet = flt.AsyncFleet()
|
|
431
|
+
|
|
432
|
+
async with FleetPlaywrightBrowser(fleet, "hubspot", "v1.2.7") as computer:
|
|
433
|
+
agent = Agent(computer=computer, tools=tools)
|
|
434
|
+
items = [
|
|
435
|
+
{
|
|
436
|
+
"role": "developer",
|
|
437
|
+
"content": "You have access to a clone of Hubspot. You can use the computer to navigate the browser and perform actions.",
|
|
438
|
+
}
|
|
439
|
+
]
|
|
440
|
+
while True:
|
|
441
|
+
user_input = await ainput("> ")
|
|
442
|
+
items.append({"role": "user", "content": user_input})
|
|
443
|
+
output_items = await agent.run_full_turn(items, show_images=False, debug=False)
|
|
444
|
+
items += output_items
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
if __name__ == "__main__":
|
|
448
|
+
asyncio.run(main())
|
examples/quickstart.py
CHANGED
|
@@ -35,7 +35,7 @@ async def main():
|
|
|
35
35
|
# 1. List available environments
|
|
36
36
|
print("\n📋 Available environments:")
|
|
37
37
|
try:
|
|
38
|
-
environments = await fleet.
|
|
38
|
+
environments = await fleet.manager.list_envs()
|
|
39
39
|
for env in environments:
|
|
40
40
|
print(f" - {env.env_key}: {env.name}")
|
|
41
41
|
print(f" Description: {env.description}")
|
|
@@ -48,7 +48,7 @@ async def main():
|
|
|
48
48
|
# 2. Create a new environment instance
|
|
49
49
|
print("\n🚀 Creating new environment...")
|
|
50
50
|
try:
|
|
51
|
-
env = await fleet.
|
|
51
|
+
env = await fleet.manager.make("fira:v1.2.5", region="us-west-1")
|
|
52
52
|
print(f"✅ Environment created with instance ID: {env.instance_id}")
|
|
53
53
|
|
|
54
54
|
# Execute a simple action
|
|
@@ -85,7 +85,7 @@ async def main():
|
|
|
85
85
|
# 3. List running instances
|
|
86
86
|
print("\n🏃 Listing running instances...")
|
|
87
87
|
try:
|
|
88
|
-
instances = await fleet.
|
|
88
|
+
instances = await fleet.manager.list_instances(status="running")
|
|
89
89
|
if instances:
|
|
90
90
|
print(f"Found {len(instances)} running instances:")
|
|
91
91
|
for instance in instances:
|
|
@@ -99,13 +99,13 @@ async def main():
|
|
|
99
99
|
print("\n🔗 Connecting to existing instance...")
|
|
100
100
|
try:
|
|
101
101
|
# Only get running instances
|
|
102
|
-
running_instances = await fleet.
|
|
102
|
+
running_instances = await fleet.manager.list_instances(status="running")
|
|
103
103
|
if running_instances:
|
|
104
104
|
# Find a running instance that's not the one we just created/deleted
|
|
105
105
|
target_instance = running_instances[0]
|
|
106
106
|
print(f"Connecting to running instance: {target_instance.instance_id}")
|
|
107
107
|
|
|
108
|
-
env = await fleet.
|
|
108
|
+
env = await fleet.manager.get(target_instance.instance_id)
|
|
109
109
|
print(f"✅ Connected to instance: {env.instance_id}")
|
|
110
110
|
|
|
111
111
|
# Execute an action on the existing instance
|
fleet/__init__.py
CHANGED
|
@@ -21,7 +21,7 @@ from .exceptions import (
|
|
|
21
21
|
FleetConfigurationError,
|
|
22
22
|
)
|
|
23
23
|
from .client import Fleet, AsyncFleet, InstanceRequest
|
|
24
|
-
from .
|
|
24
|
+
from .manager import (
|
|
25
25
|
ResetRequest,
|
|
26
26
|
ResetResponse,
|
|
27
27
|
CDPDescribeResponse,
|
|
@@ -29,6 +29,8 @@ from .env import (
|
|
|
29
29
|
ChromeStartResponse,
|
|
30
30
|
ChromeStatusResponse,
|
|
31
31
|
)
|
|
32
|
+
from .verifiers import *
|
|
33
|
+
from . import env
|
|
32
34
|
|
|
33
35
|
__version__ = "0.1.1"
|
|
34
36
|
__all__ = [
|
fleet/base.py
CHANGED