fleet-python 0.2.2__py3-none-any.whl → 0.2.3__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 +107 -92
- examples/json_tasks_example.py +82 -0
- examples/nova_act_example.py +18 -169
- examples/openai_example.py +83 -298
- examples/openai_simple_example.py +61 -0
- examples/quickstart.py +5 -5
- fleet/__init__.py +15 -1
- fleet/client.py +18 -3
- fleet/{manager → instance}/__init__.py +4 -1
- fleet/{manager → instance}/client.py +42 -5
- fleet/{manager → instance}/models.py +13 -0
- fleet/playwright.py +291 -0
- fleet/resources/base.py +1 -1
- fleet/resources/browser.py +6 -9
- fleet/resources/sqlite.py +3 -3
- fleet/verifiers/__init__.py +15 -3
- fleet/verifiers/code.py +132 -0
- fleet/verifiers/{database_snapshot.py → db.py} +62 -22
- fleet/verifiers/sql_differ.py +1 -1
- {fleet_python-0.2.2.dist-info → fleet_python-0.2.3.dist-info}/METADATA +3 -1
- fleet_python-0.2.3.dist-info/RECORD +31 -0
- fleet_python-0.2.2.dist-info/RECORD +0 -27
- /fleet/{manager → instance}/base.py +0 -0
- {fleet_python-0.2.2.dist-info → fleet_python-0.2.3.dist-info}/WHEEL +0 -0
- {fleet_python-0.2.2.dist-info → fleet_python-0.2.3.dist-info}/licenses/LICENSE +0 -0
- {fleet_python-0.2.2.dist-info → fleet_python-0.2.3.dist-info}/top_level.txt +0 -0
examples/openai_example.py
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
|
-
import base64
|
|
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
1
|
import asyncio
|
|
2
|
+
from openai import AsyncOpenAI
|
|
11
3
|
import fleet as flt
|
|
4
|
+
import json
|
|
5
|
+
from typing import Callable
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
client = AsyncOpenAI()
|
|
12
9
|
|
|
13
10
|
|
|
14
11
|
def sanitize_message(msg: dict) -> dict:
|
|
@@ -22,292 +19,42 @@ def sanitize_message(msg: dict) -> dict:
|
|
|
22
19
|
return msg
|
|
23
20
|
|
|
24
21
|
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
# Optional: key mapping if your model uses "CUA" style keys
|
|
65
|
-
CUA_KEY_TO_PLAYWRIGHT_KEY = {
|
|
66
|
-
"/": "Divide",
|
|
67
|
-
"\\": "Backslash",
|
|
68
|
-
"alt": "Alt",
|
|
69
|
-
"arrowdown": "ArrowDown",
|
|
70
|
-
"arrowleft": "ArrowLeft",
|
|
71
|
-
"arrowright": "ArrowRight",
|
|
72
|
-
"arrowup": "ArrowUp",
|
|
73
|
-
"backspace": "Backspace",
|
|
74
|
-
"capslock": "CapsLock",
|
|
75
|
-
"cmd": "Meta",
|
|
76
|
-
"ctrl": "Control",
|
|
77
|
-
"delete": "Delete",
|
|
78
|
-
"end": "End",
|
|
79
|
-
"enter": "Enter",
|
|
80
|
-
"esc": "Escape",
|
|
81
|
-
"home": "Home",
|
|
82
|
-
"insert": "Insert",
|
|
83
|
-
"option": "Alt",
|
|
84
|
-
"pagedown": "PageDown",
|
|
85
|
-
"pageup": "PageUp",
|
|
86
|
-
"shift": "Shift",
|
|
87
|
-
"space": " ",
|
|
88
|
-
"super": "Meta",
|
|
89
|
-
"tab": "Tab",
|
|
90
|
-
"win": "Meta",
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
class BasePlaywrightComputer:
|
|
95
|
-
"""
|
|
96
|
-
Abstract base for Playwright-based computers:
|
|
97
|
-
|
|
98
|
-
- Subclasses override `_get_browser_and_page()` to do local or remote connection,
|
|
99
|
-
returning (Browser, Page).
|
|
100
|
-
- This base class handles context creation (`__enter__`/`__exit__`),
|
|
101
|
-
plus standard "Computer" actions like click, scroll, etc.
|
|
102
|
-
- We also have extra browser actions: `goto(url)` and `back()`.
|
|
103
|
-
"""
|
|
104
|
-
|
|
105
|
-
def get_environment(self):
|
|
106
|
-
return "browser"
|
|
107
|
-
|
|
108
|
-
def get_dimensions(self):
|
|
109
|
-
return (1920, 1080)
|
|
110
|
-
|
|
111
|
-
def __init__(self):
|
|
112
|
-
self._playwright = None
|
|
113
|
-
self._browser: Browser | None = None
|
|
114
|
-
self._page: Page | None = None
|
|
115
|
-
|
|
116
|
-
async def __aenter__(self):
|
|
117
|
-
# Start Playwright and call the subclass hook for getting browser/page
|
|
118
|
-
self._playwright = await async_playwright().start()
|
|
119
|
-
self._browser, self._page = await self._get_browser_and_page()
|
|
120
|
-
|
|
121
|
-
# Set up network interception to flag URLs matching domains in BLOCKED_DOMAINS
|
|
122
|
-
async def handle_route(route, request):
|
|
123
|
-
await route.continue_()
|
|
124
|
-
|
|
125
|
-
await self._page.route("**/*", handle_route)
|
|
126
|
-
|
|
127
|
-
return self
|
|
128
|
-
|
|
129
|
-
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
130
|
-
# if self._browser:
|
|
131
|
-
# await self._browser.close()
|
|
132
|
-
if self._playwright:
|
|
133
|
-
await self._playwright.stop()
|
|
134
|
-
|
|
135
|
-
def get_current_url(self) -> str:
|
|
136
|
-
return self._page.url
|
|
137
|
-
|
|
138
|
-
# --- Common "Computer" actions ---
|
|
139
|
-
async def screenshot(self) -> str:
|
|
140
|
-
"""Capture only the viewport (not full_page)."""
|
|
141
|
-
png_bytes = await self._page.screenshot(full_page=False)
|
|
142
|
-
return base64.b64encode(png_bytes).decode("utf-8")
|
|
143
|
-
|
|
144
|
-
async def click(self, x: int, y: int, button: str = "left") -> None:
|
|
145
|
-
if button == "back":
|
|
146
|
-
await self.back()
|
|
147
|
-
elif button == "forward":
|
|
148
|
-
await self.forward()
|
|
149
|
-
elif button == "wheel":
|
|
150
|
-
await self._page.mouse.wheel(x, y)
|
|
151
|
-
else:
|
|
152
|
-
button_mapping = {"left": "left", "right": "right"}
|
|
153
|
-
button_type = button_mapping.get(button, "left")
|
|
154
|
-
await self._page.mouse.click(x, y, button=button_type)
|
|
155
|
-
|
|
156
|
-
async def double_click(self, x: int, y: int) -> None:
|
|
157
|
-
await self._page.mouse.dblclick(x, y)
|
|
158
|
-
|
|
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})")
|
|
162
|
-
|
|
163
|
-
async def type(self, text: str) -> None:
|
|
164
|
-
await self._page.keyboard.type(text)
|
|
165
|
-
|
|
166
|
-
async def wait(self, ms: int = 1000) -> None:
|
|
167
|
-
await asyncio.sleep(ms / 1000)
|
|
168
|
-
|
|
169
|
-
async def move(self, x: int, y: int) -> None:
|
|
170
|
-
await self._page.mouse.move(x, y)
|
|
171
|
-
|
|
172
|
-
async def keypress(self, keys: List[str]) -> None:
|
|
173
|
-
mapped_keys = [CUA_KEY_TO_PLAYWRIGHT_KEY.get(key.lower(), key) for key in keys]
|
|
174
|
-
for key in mapped_keys:
|
|
175
|
-
await self._page.keyboard.down(key)
|
|
176
|
-
for key in reversed(mapped_keys):
|
|
177
|
-
await self._page.keyboard.up(key)
|
|
178
|
-
|
|
179
|
-
async def drag(self, path: List[Dict[str, int]]) -> None:
|
|
180
|
-
if not path:
|
|
181
|
-
return
|
|
182
|
-
await self._page.mouse.move(path[0]["x"], path[0]["y"])
|
|
183
|
-
await self._page.mouse.down()
|
|
184
|
-
for point in path[1:]:
|
|
185
|
-
await self._page.mouse.move(point["x"], point["y"])
|
|
186
|
-
await self._page.mouse.up()
|
|
187
|
-
|
|
188
|
-
# --- Extra browser-oriented actions ---
|
|
189
|
-
async def goto(self, url: str) -> None:
|
|
190
|
-
try:
|
|
191
|
-
return await self._page.goto(url)
|
|
192
|
-
except Exception as e:
|
|
193
|
-
print(f"Error navigating to {url}: {e}")
|
|
194
|
-
|
|
195
|
-
async def back(self) -> None:
|
|
196
|
-
return await self._page.go_back()
|
|
197
|
-
|
|
198
|
-
async def forward(self) -> None:
|
|
199
|
-
return await self._page.go_forward()
|
|
200
|
-
|
|
201
|
-
# --- Subclass hook ---
|
|
202
|
-
async def _get_browser_and_page(self) -> tuple[Browser, Page]:
|
|
203
|
-
"""Subclasses must implement, returning (Browser, Page)."""
|
|
204
|
-
raise NotImplementedError
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
class FleetPlaywrightBrowser(BasePlaywrightComputer):
|
|
208
|
-
"""Launches a local Chromium instance using Playwright."""
|
|
209
|
-
|
|
210
|
-
def __init__(
|
|
211
|
-
self,
|
|
212
|
-
fleet: flt.AsyncFleet,
|
|
213
|
-
env_key: str,
|
|
214
|
-
version: Optional[str] = None,
|
|
215
|
-
headless: bool = False,
|
|
216
|
-
):
|
|
217
|
-
super().__init__()
|
|
218
|
-
self.fleet = fleet
|
|
219
|
-
self.env_key = env_key
|
|
220
|
-
self.version = version
|
|
221
|
-
self.headless = headless
|
|
222
|
-
|
|
223
|
-
async def _get_browser_and_page(self) -> tuple[Browser, Page]:
|
|
224
|
-
width, height = self.get_dimensions()
|
|
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)
|
|
230
|
-
)
|
|
231
|
-
|
|
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)
|
|
241
|
-
|
|
242
|
-
# Add event listeners for page creation and closure
|
|
243
|
-
context = browser.contexts[0]
|
|
244
|
-
context.on("page", self._handle_new_page)
|
|
245
|
-
|
|
246
|
-
page = context.pages[0]
|
|
247
|
-
await page.set_viewport_size({"width": width, "height": height})
|
|
248
|
-
page.on("close", self._handle_page_close)
|
|
249
|
-
|
|
250
|
-
return browser, page
|
|
251
|
-
|
|
252
|
-
def _handle_new_page(self, page: Page):
|
|
253
|
-
"""Handle the creation of a new page."""
|
|
254
|
-
print("New page created")
|
|
255
|
-
self._page = page
|
|
256
|
-
page.on("close", self._handle_page_close)
|
|
257
|
-
|
|
258
|
-
def _handle_page_close(self, page: Page):
|
|
259
|
-
"""Handle the closure of a page."""
|
|
260
|
-
print("Page closed")
|
|
261
|
-
if self._page == page:
|
|
262
|
-
if self._browser.contexts[0].pages:
|
|
263
|
-
self._page = self._browser.contexts[0].pages[-1]
|
|
264
|
-
else:
|
|
265
|
-
print("Warning: All pages have been closed.")
|
|
266
|
-
self._page = None
|
|
267
|
-
|
|
268
|
-
|
|
269
22
|
class Agent:
|
|
270
|
-
"""
|
|
271
|
-
A sample agent class that can be used to interact with a computer.
|
|
272
|
-
|
|
273
|
-
(See simple_cua_loop.py for a simple example without an agent.)
|
|
274
|
-
"""
|
|
275
|
-
|
|
276
23
|
def __init__(
|
|
277
24
|
self,
|
|
25
|
+
browser,
|
|
278
26
|
model="computer-use-preview",
|
|
279
|
-
computer: FleetPlaywrightBrowser = None,
|
|
280
27
|
tools: list[dict] = [],
|
|
281
28
|
acknowledge_safety_check_callback: Callable = lambda: False,
|
|
282
29
|
):
|
|
283
30
|
self.model = model
|
|
284
|
-
self.computer =
|
|
31
|
+
self.computer = browser
|
|
285
32
|
self.tools = tools
|
|
286
33
|
self.print_steps = True
|
|
287
34
|
self.debug = False
|
|
288
35
|
self.show_images = False
|
|
289
36
|
self.acknowledge_safety_check_callback = acknowledge_safety_check_callback
|
|
290
37
|
|
|
291
|
-
if
|
|
292
|
-
dimensions =
|
|
38
|
+
if browser:
|
|
39
|
+
dimensions = browser.get_dimensions()
|
|
293
40
|
self.tools += [
|
|
294
41
|
{
|
|
295
42
|
"type": "computer-preview",
|
|
296
43
|
"display_width": dimensions[0],
|
|
297
44
|
"display_height": dimensions[1],
|
|
298
|
-
"environment":
|
|
45
|
+
"environment": browser.get_environment(),
|
|
299
46
|
},
|
|
300
47
|
]
|
|
301
48
|
|
|
302
49
|
def debug_print(self, *args):
|
|
303
50
|
if self.debug:
|
|
304
|
-
|
|
51
|
+
print(*args)
|
|
305
52
|
|
|
306
53
|
async def handle_item(self, item):
|
|
307
54
|
"""Handle each item; may cause a computer action + screenshot."""
|
|
308
55
|
if self.debug:
|
|
309
56
|
print(f"Handling item of type: {item.get('type')}")
|
|
310
|
-
|
|
57
|
+
|
|
311
58
|
if item["type"] == "message":
|
|
312
59
|
if self.print_steps:
|
|
313
60
|
print(item["content"][0]["text"])
|
|
@@ -339,8 +86,6 @@ class Agent:
|
|
|
339
86
|
await method(**action_args)
|
|
340
87
|
|
|
341
88
|
screenshot_base64 = await self.computer.screenshot()
|
|
342
|
-
if self.show_images:
|
|
343
|
-
show_image(screenshot_base64)
|
|
344
89
|
|
|
345
90
|
# if user doesn't ack all safety checks exit with error
|
|
346
91
|
pending_checks = item.get("pending_safety_checks", [])
|
|
@@ -381,38 +126,60 @@ class Agent:
|
|
|
381
126
|
while new_items[-1].get("role") != "assistant" if new_items else True:
|
|
382
127
|
self.debug_print([sanitize_message(msg) for msg in input_items + new_items])
|
|
383
128
|
|
|
384
|
-
|
|
129
|
+
# The Responses API rejects unknown keys (e.g. `status`, `encrypted_content`).
|
|
130
|
+
# Strip them from every item before sending.
|
|
131
|
+
def _clean_item(msg: dict) -> dict:
|
|
132
|
+
unwanted_keys = {"status", "encrypted_content"}
|
|
133
|
+
return {k: v for k, v in msg.items() if k not in unwanted_keys}
|
|
134
|
+
|
|
135
|
+
clean_input = [_clean_item(m) for m in (input_items + new_items)]
|
|
136
|
+
|
|
137
|
+
response = await client.responses.create(
|
|
385
138
|
model=self.model,
|
|
386
|
-
input=
|
|
139
|
+
input=clean_input,
|
|
387
140
|
tools=self.tools,
|
|
388
141
|
truncation="auto",
|
|
389
142
|
)
|
|
390
|
-
self.debug_print(response)
|
|
391
143
|
|
|
392
|
-
|
|
144
|
+
# The OpenAI SDK returns a Pydantic model object, not a plain dict.
|
|
145
|
+
# Convert it to a standard Python dict so the rest of the code can
|
|
146
|
+
# remain unchanged from the previous implementation.
|
|
147
|
+
response_dict = (
|
|
148
|
+
response.model_dump() # pydantic v2
|
|
149
|
+
if hasattr(response, "model_dump")
|
|
150
|
+
else (
|
|
151
|
+
response.to_dict_recursive()
|
|
152
|
+
if hasattr(response, "to_dict_recursive")
|
|
153
|
+
else dict(response)
|
|
154
|
+
)
|
|
155
|
+
)
|
|
156
|
+
self.debug_print(response_dict)
|
|
157
|
+
|
|
158
|
+
# Guard against missing/empty output in the response
|
|
159
|
+
if not response_dict.get("output"):
|
|
393
160
|
if self.debug:
|
|
394
|
-
print("Full response:",
|
|
395
|
-
if "error"
|
|
396
|
-
error_msg =
|
|
161
|
+
print("Full response:", response_dict)
|
|
162
|
+
if response_dict.get("error") is not None:
|
|
163
|
+
error_msg = response_dict["error"].get("message", "Unknown error")
|
|
397
164
|
raise ValueError(f"API Error: {error_msg}")
|
|
398
165
|
else:
|
|
399
166
|
raise ValueError("No output from model")
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
167
|
+
|
|
168
|
+
# Append each item from the model output to conversation history
|
|
169
|
+
# in the exact order we received them, **without filtering** so that
|
|
170
|
+
# required pairs such as reasoning → computer_call are preserved.
|
|
171
|
+
for item in response_dict["output"]:
|
|
172
|
+
# First, record the original item itself.
|
|
173
|
+
new_items.append(item)
|
|
174
|
+
|
|
175
|
+
# Next, perform any local side-effects (browser actions, etc.).
|
|
176
|
+
handled_items = await self.handle_item(item)
|
|
177
|
+
|
|
178
|
+
# If the handler generated additional items (e.g. computer_call_output)
|
|
179
|
+
# we append those *immediately* so the order remains:
|
|
180
|
+
# reasoning → computer_call → computer_call_output
|
|
181
|
+
if handled_items:
|
|
182
|
+
new_items += handled_items
|
|
416
183
|
|
|
417
184
|
return new_items
|
|
418
185
|
|
|
@@ -427,21 +194,39 @@ async def ainput(prompt: str = "") -> str:
|
|
|
427
194
|
|
|
428
195
|
|
|
429
196
|
async def main():
|
|
430
|
-
|
|
197
|
+
# Create a Fleet environment instance
|
|
198
|
+
instance = await flt.env.make("hubspot")
|
|
431
199
|
|
|
432
|
-
|
|
433
|
-
|
|
200
|
+
# Create the Playwright wrapper
|
|
201
|
+
browser = flt.FleetPlaywrightWrapper(instance)
|
|
202
|
+
await browser.start()
|
|
203
|
+
|
|
204
|
+
try:
|
|
205
|
+
agent = Agent(browser, model="computer-use-preview", tools=[])
|
|
434
206
|
items = [
|
|
435
207
|
{
|
|
436
208
|
"role": "developer",
|
|
437
209
|
"content": "You have access to a clone of Hubspot. You can use the computer to navigate the browser and perform actions.",
|
|
438
210
|
}
|
|
439
211
|
]
|
|
212
|
+
|
|
440
213
|
while True:
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
214
|
+
try:
|
|
215
|
+
user_input = await ainput("> ")
|
|
216
|
+
items.append({"role": "user", "content": user_input})
|
|
217
|
+
output_items = await agent.run_full_turn(
|
|
218
|
+
items, show_images=False, debug=False
|
|
219
|
+
)
|
|
220
|
+
items += output_items
|
|
221
|
+
except (EOFError, KeyboardInterrupt):
|
|
222
|
+
print("\nShutting down...")
|
|
223
|
+
break
|
|
224
|
+
except Exception as e:
|
|
225
|
+
print(f"Error during interaction: {e}")
|
|
226
|
+
# Continue the loop for other errors
|
|
227
|
+
finally:
|
|
228
|
+
await browser.close()
|
|
229
|
+
await instance.close()
|
|
445
230
|
|
|
446
231
|
|
|
447
232
|
if __name__ == "__main__":
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from openai import AsyncOpenAI
|
|
3
|
+
import fleet as flt
|
|
4
|
+
|
|
5
|
+
client = AsyncOpenAI()
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
async def main():
|
|
9
|
+
instance = await flt.env.make("hubspot")
|
|
10
|
+
|
|
11
|
+
browser = flt.FleetPlaywrightWrapper(instance)
|
|
12
|
+
await browser.start()
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
width, height = browser.get_dimensions()
|
|
16
|
+
tools = [
|
|
17
|
+
{
|
|
18
|
+
"type": "computer-preview",
|
|
19
|
+
"display_width": width,
|
|
20
|
+
"display_height": height,
|
|
21
|
+
"environment": browser.get_environment(),
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
response = await client.responses.create(
|
|
26
|
+
model="computer-use-preview",
|
|
27
|
+
input=[
|
|
28
|
+
{
|
|
29
|
+
"role": "developer",
|
|
30
|
+
"content": "Create a HubSpot deal",
|
|
31
|
+
}
|
|
32
|
+
],
|
|
33
|
+
tools=tools,
|
|
34
|
+
truncation="auto",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
if len(response.output) != 0:
|
|
38
|
+
if response.output[0].type == "message":
|
|
39
|
+
print(response.output[0].content[0].text)
|
|
40
|
+
|
|
41
|
+
if response.output[0].type == "computer_call":
|
|
42
|
+
action = response.output[0].action
|
|
43
|
+
if action.type == "screenshot":
|
|
44
|
+
screenshot_base64 = await browser.screenshot()
|
|
45
|
+
result = {
|
|
46
|
+
"type": "input_image",
|
|
47
|
+
"image_url": f"data:image/png;base64,{screenshot_base64}",
|
|
48
|
+
"current_url": browser.get_current_url(),
|
|
49
|
+
}
|
|
50
|
+
else:
|
|
51
|
+
result = await browser.execute_computer_action(action)
|
|
52
|
+
|
|
53
|
+
print("Computer action result:")
|
|
54
|
+
print(result)
|
|
55
|
+
finally:
|
|
56
|
+
await browser.close()
|
|
57
|
+
await instance.close()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
if __name__ == "__main__":
|
|
61
|
+
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.instance.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.instance.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.instance.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.instance.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.instance.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,8 @@ from .exceptions import (
|
|
|
21
21
|
FleetConfigurationError,
|
|
22
22
|
)
|
|
23
23
|
from .client import Fleet, AsyncFleet, InstanceRequest
|
|
24
|
-
from .
|
|
24
|
+
from .instance import (
|
|
25
|
+
AsyncInstanceClient,
|
|
25
26
|
ResetRequest,
|
|
26
27
|
ResetResponse,
|
|
27
28
|
CDPDescribeResponse,
|
|
@@ -32,6 +33,14 @@ from .manager import (
|
|
|
32
33
|
from .verifiers import *
|
|
33
34
|
from . import env
|
|
34
35
|
|
|
36
|
+
# Optional playwright integration
|
|
37
|
+
try:
|
|
38
|
+
from .playwright import FleetPlaywrightWrapper
|
|
39
|
+
_PLAYWRIGHT_AVAILABLE = True
|
|
40
|
+
except ImportError:
|
|
41
|
+
FleetPlaywrightWrapper = None
|
|
42
|
+
_PLAYWRIGHT_AVAILABLE = False
|
|
43
|
+
|
|
35
44
|
__version__ = "0.1.1"
|
|
36
45
|
__all__ = [
|
|
37
46
|
"env",
|
|
@@ -41,6 +50,7 @@ __all__ = [
|
|
|
41
50
|
"FleetConfigurationError",
|
|
42
51
|
"Fleet",
|
|
43
52
|
"AsyncFleet",
|
|
53
|
+
"AsyncInstanceClient",
|
|
44
54
|
"InstanceRequest",
|
|
45
55
|
"ResetRequest",
|
|
46
56
|
"ResetResponse",
|
|
@@ -49,3 +59,7 @@ __all__ = [
|
|
|
49
59
|
"ChromeStartResponse",
|
|
50
60
|
"ChromeStatusResponse",
|
|
51
61
|
]
|
|
62
|
+
|
|
63
|
+
# Add playwright wrapper to exports if available
|
|
64
|
+
if _PLAYWRIGHT_AVAILABLE:
|
|
65
|
+
__all__.append("FleetPlaywrightWrapper")
|
fleet/client.py
CHANGED
|
@@ -23,7 +23,14 @@ from typing import Optional, List
|
|
|
23
23
|
from .base import EnvironmentBase, AsyncWrapper, SyncWrapper
|
|
24
24
|
from .models import InstanceRequest, InstanceRecord, Environment as EnvironmentModel
|
|
25
25
|
|
|
26
|
-
from .
|
|
26
|
+
from .instance import (
|
|
27
|
+
InstanceClient,
|
|
28
|
+
AsyncInstanceClient,
|
|
29
|
+
ResetRequest,
|
|
30
|
+
ResetResponse,
|
|
31
|
+
ValidatorType,
|
|
32
|
+
ExecuteFunctionResponse,
|
|
33
|
+
)
|
|
27
34
|
from .resources.base import Resource
|
|
28
35
|
from .resources.sqlite import AsyncSQLiteResource
|
|
29
36
|
from .resources.browser import AsyncBrowserResource
|
|
@@ -47,7 +54,7 @@ class Environment(EnvironmentBase):
|
|
|
47
54
|
class AsyncEnvironment(EnvironmentBase):
|
|
48
55
|
def __init__(self, httpx_client: Optional[httpx.AsyncClient] = None, **kwargs):
|
|
49
56
|
super().__init__(**kwargs)
|
|
50
|
-
self._httpx_client = httpx_client or httpx.AsyncClient()
|
|
57
|
+
self._httpx_client = httpx_client or httpx.AsyncClient(timeout=60.0)
|
|
51
58
|
self._instance: Optional[AsyncInstanceClient] = None
|
|
52
59
|
|
|
53
60
|
@property
|
|
@@ -76,6 +83,14 @@ class AsyncEnvironment(EnvironmentBase):
|
|
|
76
83
|
async def close(self) -> InstanceRecord:
|
|
77
84
|
return await AsyncFleet().delete(self.instance_id)
|
|
78
85
|
|
|
86
|
+
async def verify(self, validator: ValidatorType) -> ExecuteFunctionResponse:
|
|
87
|
+
return await self.instance.verify(validator)
|
|
88
|
+
|
|
89
|
+
async def verify_raw(
|
|
90
|
+
self, function_code: str, function_name: str
|
|
91
|
+
) -> ExecuteFunctionResponse:
|
|
92
|
+
return await self.instance.verify_raw(function_code, function_name)
|
|
93
|
+
|
|
79
94
|
|
|
80
95
|
class Fleet:
|
|
81
96
|
def __init__(
|
|
@@ -176,7 +191,7 @@ class AsyncFleet:
|
|
|
176
191
|
async def instance(self, instance_id: str) -> AsyncEnvironment:
|
|
177
192
|
response = await self.client.request("GET", f"/v1/env/instances/{instance_id}")
|
|
178
193
|
instance = AsyncEnvironment(**response.json())
|
|
179
|
-
await instance.
|
|
194
|
+
await instance.instance.load()
|
|
180
195
|
return instance
|
|
181
196
|
|
|
182
197
|
async def delete(self, instance_id: str) -> InstanceRecord:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Fleet SDK Environment Module."""
|
|
2
2
|
|
|
3
|
-
from .client import InstanceClient, AsyncInstanceClient
|
|
3
|
+
from .client import InstanceClient, AsyncInstanceClient, ValidatorType
|
|
4
4
|
from .models import (
|
|
5
5
|
ResetRequest,
|
|
6
6
|
ResetResponse,
|
|
@@ -8,9 +8,11 @@ from .models import (
|
|
|
8
8
|
ChromeStartRequest,
|
|
9
9
|
ChromeStartResponse,
|
|
10
10
|
ChromeStatusResponse,
|
|
11
|
+
ExecuteFunctionResponse,
|
|
11
12
|
)
|
|
12
13
|
|
|
13
14
|
__all__ = [
|
|
15
|
+
"ValidatorType",
|
|
14
16
|
"InstanceClient",
|
|
15
17
|
"AsyncInstanceClient",
|
|
16
18
|
"ResetRequest",
|
|
@@ -19,4 +21,5 @@ __all__ = [
|
|
|
19
21
|
"ChromeStartRequest",
|
|
20
22
|
"ChromeStartResponse",
|
|
21
23
|
"ChromeStatusResponse",
|
|
24
|
+
"ExecuteFunctionResponse"
|
|
22
25
|
]
|