fleet-python 0.2.1__tar.gz → 0.2.3__tar.gz
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.
- {fleet_python-0.2.1/fleet_python.egg-info → fleet_python-0.2.3}/PKG-INFO +3 -1
- fleet_python-0.2.3/examples/dsl_example.py +127 -0
- fleet_python-0.2.3/examples/example.py +38 -0
- fleet_python-0.2.3/examples/json_tasks_example.py +82 -0
- fleet_python-0.2.3/examples/nova_act_example.py +29 -0
- fleet_python-0.2.3/examples/openai_example.py +233 -0
- fleet_python-0.2.3/examples/openai_simple_example.py +61 -0
- {fleet_python-0.2.1 → fleet_python-0.2.3}/examples/quickstart.py +5 -5
- {fleet_python-0.2.1 → fleet_python-0.2.3}/fleet/__init__.py +17 -1
- {fleet_python-0.2.1 → fleet_python-0.2.3}/fleet/base.py +1 -1
- {fleet_python-0.2.1 → fleet_python-0.2.3}/fleet/client.py +77 -30
- fleet_python-0.2.3/fleet/env/__init__.py +3 -0
- fleet_python-0.2.3/fleet/env/client.py +15 -0
- {fleet_python-0.2.1/fleet/env → fleet_python-0.2.3/fleet/instance}/__init__.py +6 -3
- {fleet_python-0.2.1/fleet/env → fleet_python-0.2.3/fleet/instance}/client.py +44 -8
- {fleet_python-0.2.1/fleet/env → fleet_python-0.2.3/fleet/instance}/models.py +13 -0
- fleet_python-0.2.3/fleet/playwright.py +291 -0
- {fleet_python-0.2.1 → fleet_python-0.2.3}/fleet/resources/base.py +5 -2
- {fleet_python-0.2.1 → fleet_python-0.2.3}/fleet/resources/browser.py +15 -8
- {fleet_python-0.2.1 → fleet_python-0.2.3}/fleet/resources/sqlite.py +3 -3
- fleet_python-0.2.3/fleet/verifiers/__init__.py +16 -0
- fleet_python-0.2.3/fleet/verifiers/code.py +132 -0
- fleet_python-0.2.3/fleet/verifiers/db.py +706 -0
- fleet_python-0.2.3/fleet/verifiers/sql_differ.py +187 -0
- {fleet_python-0.2.1 → fleet_python-0.2.3/fleet_python.egg-info}/PKG-INFO +3 -1
- {fleet_python-0.2.1 → fleet_python-0.2.3}/fleet_python.egg-info/SOURCES.txt +12 -2
- {fleet_python-0.2.1 → fleet_python-0.2.3}/fleet_python.egg-info/requires.txt +3 -0
- {fleet_python-0.2.1 → fleet_python-0.2.3}/pyproject.toml +4 -1
- fleet_python-0.2.1/examples/example.py +0 -51
- fleet_python-0.2.1/examples/nova_act_example.py +0 -180
- fleet_python-0.2.1/examples/openai_example.py +0 -329
- {fleet_python-0.2.1 → fleet_python-0.2.3}/LICENSE +0 -0
- {fleet_python-0.2.1 → fleet_python-0.2.3}/README.md +0 -0
- {fleet_python-0.2.1 → fleet_python-0.2.3}/fleet/exceptions.py +0 -0
- {fleet_python-0.2.1/fleet/env → fleet_python-0.2.3/fleet/instance}/base.py +0 -0
- {fleet_python-0.2.1 → fleet_python-0.2.3}/fleet/models.py +0 -0
- {fleet_python-0.2.1 → fleet_python-0.2.3}/fleet_python.egg-info/dependency_links.txt +0 -0
- {fleet_python-0.2.1 → fleet_python-0.2.3}/fleet_python.egg-info/top_level.txt +0 -0
- {fleet_python-0.2.1 → fleet_python-0.2.3}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fleet-python
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: Python SDK for Fleet environments
|
|
5
5
|
Author-email: Fleet AI <nic@fleet.so>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -31,6 +31,8 @@ Requires-Dist: black>=22.0.0; extra == "dev"
|
|
|
31
31
|
Requires-Dist: isort>=5.0.0; extra == "dev"
|
|
32
32
|
Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
33
33
|
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
34
|
+
Provides-Extra: playwright
|
|
35
|
+
Requires-Dist: playwright>=1.40.0; extra == "playwright"
|
|
34
36
|
Dynamic: license-file
|
|
35
37
|
|
|
36
38
|
# Fleet SDK
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import fleet as flt
|
|
3
|
+
from fleet.verifiers import DatabaseSnapshot, IgnoreConfig, TASK_SUCCESSFUL_SCORE
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def validate_new_deal_creation(
|
|
7
|
+
before: DatabaseSnapshot,
|
|
8
|
+
after: DatabaseSnapshot,
|
|
9
|
+
transcript: str | None = None,
|
|
10
|
+
) -> int:
|
|
11
|
+
"""Validate that a new deal was created"""
|
|
12
|
+
|
|
13
|
+
# Find the new deal entry
|
|
14
|
+
new_deal = after.table("entries").eq("id", 32302).first()
|
|
15
|
+
if not new_deal:
|
|
16
|
+
raise AssertionError("Expected new deal with id 32302 not found")
|
|
17
|
+
|
|
18
|
+
# Verify it's a deal type
|
|
19
|
+
if new_deal["type"] != "deal":
|
|
20
|
+
raise AssertionError(
|
|
21
|
+
f"Expected entry type to be 'deal', got '{new_deal['type']}'"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
# Verify the deal has a name (should be "testing" based on the diff)
|
|
25
|
+
if not new_deal["name"]:
|
|
26
|
+
raise AssertionError("Expected deal to have a name")
|
|
27
|
+
|
|
28
|
+
# Parse the properties JSON to check basic deal properties
|
|
29
|
+
import json
|
|
30
|
+
|
|
31
|
+
properties = json.loads(new_deal["properties"])
|
|
32
|
+
|
|
33
|
+
# Verify it has basic deal properties
|
|
34
|
+
if "dealstage" not in properties:
|
|
35
|
+
raise AssertionError("Expected deal to have a dealstage property")
|
|
36
|
+
|
|
37
|
+
if "deal_type" not in properties:
|
|
38
|
+
raise AssertionError("Expected deal to have a deal_type property")
|
|
39
|
+
|
|
40
|
+
if "priority" not in properties:
|
|
41
|
+
raise AssertionError("Expected deal to have a priority property")
|
|
42
|
+
|
|
43
|
+
# Configure ignore settings
|
|
44
|
+
ignore_config = IgnoreConfig(
|
|
45
|
+
tables={"pageviews"},
|
|
46
|
+
table_fields={
|
|
47
|
+
"entries": {"createdDate", "lastModifiedDate", "createdAt", "updatedAt"},
|
|
48
|
+
},
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# Build expected changes
|
|
52
|
+
expected_changes = [
|
|
53
|
+
{
|
|
54
|
+
"table": "entries",
|
|
55
|
+
"pk": 32302,
|
|
56
|
+
"field": None,
|
|
57
|
+
"after": "__added__",
|
|
58
|
+
}
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
before.diff(after, ignore_config).expect_only(expected_changes)
|
|
62
|
+
return TASK_SUCCESSFUL_SCORE
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
async def main():
|
|
66
|
+
# Create a new instance
|
|
67
|
+
print("Creating new Hubspot instance...")
|
|
68
|
+
env = await flt.env.make("hubspot:v1.2.7")
|
|
69
|
+
print(f"New Instance: {env.instance_id}")
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
# Reset the instance
|
|
73
|
+
response = await env.reset(seed=42)
|
|
74
|
+
print(f"Reset response: {response}")
|
|
75
|
+
|
|
76
|
+
# Run verifier before insertion (should fail)
|
|
77
|
+
print("\nRunning verifier before insertion...")
|
|
78
|
+
response = await env.verify(validate_new_deal_creation)
|
|
79
|
+
print(f"Success: {response.success}")
|
|
80
|
+
print(f"Result: {response.result}")
|
|
81
|
+
print(f"Error: {response.error}")
|
|
82
|
+
print(f"Message: {response.message}")
|
|
83
|
+
|
|
84
|
+
# Insert the deal entry
|
|
85
|
+
print("\nInserting deal entry...")
|
|
86
|
+
insert_query = """
|
|
87
|
+
INSERT INTO entries (id, name, type, owner_id, createdDate, lastModifiedDate, createdAt, updatedAt, archivedAt, properties)
|
|
88
|
+
VALUES (
|
|
89
|
+
32302,
|
|
90
|
+
'testing',
|
|
91
|
+
'deal',
|
|
92
|
+
1,
|
|
93
|
+
'2025-07-10T01:32:01.089Z',
|
|
94
|
+
'2025-07-10T01:32:01.089Z',
|
|
95
|
+
'2025-07-10 01:32:01',
|
|
96
|
+
'2025-07-10 01:32:01',
|
|
97
|
+
NULL,
|
|
98
|
+
'{"amount":null,"closedate":"2025-08-09","close_date":"2025-08-09","dealstage":"appointmentscheduled","pipeline":"default","description":null,"priority":"medium","deal_stage_probability":null,"deal_type":"newbusiness","hs_createdate":"2025-07-10T01:32:01.089Z","hs_object_source":"INTEGRATION","hs_object_source_id":"14696758","hs_object_source_label":"INTEGRATION","hs_is_closed":"false","hs_is_closed_count":"0","hs_is_closed_lost":"false","hs_is_closed_won":"false","hs_is_deal_split":"false","hs_is_open_count":"1","hs_num_associated_active_deal_registrations":"0","hs_num_associated_deal_registrations":"0","hs_num_associated_deal_splits":"0","hs_num_of_associated_line_items":"0","hs_num_target_accounts":"0","hs_number_of_call_engagements":"0","hs_number_of_inbound_calls":"0","hs_number_of_outbound_calls":"0","hs_number_of_overdue_tasks":"0","num_associated_contacts":"0","num_notes":"0","hs_closed_amount":"0","hs_closed_amount_in_home_currency":"0","hs_closed_deal_close_date":"0","hs_closed_deal_create_date":"0","hs_closed_won_count":"0","hs_v2_date_entered_current_stage":"2025-07-10T01:32:01.089Z","hs_v2_time_in_current_stage":"2025-07-10T01:32:01.089Z","hs_duration":"1752111121089","hs_open_deal_create_date":"1752111121089","days_to_close":"29","hs_days_to_close_raw":"29.936098506944443"}'
|
|
99
|
+
)
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
db = env.db()
|
|
103
|
+
insert_result = await db.exec(insert_query)
|
|
104
|
+
print(f"Insert result: {insert_result}")
|
|
105
|
+
|
|
106
|
+
# Verify the insertion
|
|
107
|
+
print("\nVerifying insertion...")
|
|
108
|
+
query_result = await db.query("SELECT * FROM entries WHERE id = 32302")
|
|
109
|
+
print(f"Query result: {query_result}")
|
|
110
|
+
|
|
111
|
+
# Run verifier after insertion (should succeed)
|
|
112
|
+
print("\nRunning verifier after insertion...")
|
|
113
|
+
response = await env.verify(validate_new_deal_creation)
|
|
114
|
+
print(f"Success: {response.success}")
|
|
115
|
+
print(f"Result: {response.result}")
|
|
116
|
+
print(f"Error: {response.error}")
|
|
117
|
+
print(f"Message: {response.message}")
|
|
118
|
+
|
|
119
|
+
finally:
|
|
120
|
+
# Delete the instance
|
|
121
|
+
print("\nDeleting instance...")
|
|
122
|
+
await env.close()
|
|
123
|
+
print("Instance deleted.")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
if __name__ == "__main__":
|
|
127
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Example demonstrating browser control with Fleet Manager Client."""
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
import fleet as flt
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
async def main():
|
|
9
|
+
environments = await flt.env.list_envs()
|
|
10
|
+
print("Environments:", len(environments))
|
|
11
|
+
|
|
12
|
+
# Create a new instance
|
|
13
|
+
env = await flt.env.make("hubspot:v1.2.7")
|
|
14
|
+
print("New Instance:", env.instance_id)
|
|
15
|
+
|
|
16
|
+
response = await env.reset(seed=42)
|
|
17
|
+
print("Reset response:", response)
|
|
18
|
+
|
|
19
|
+
print(await env.resources())
|
|
20
|
+
|
|
21
|
+
sqlite = env.db()
|
|
22
|
+
print("SQLite:", await sqlite.describe())
|
|
23
|
+
|
|
24
|
+
print("Query:", await sqlite.query("SELECT * FROM users"))
|
|
25
|
+
|
|
26
|
+
sqlite = await env.state("sqlite://current").describe()
|
|
27
|
+
print("SQLite:", sqlite)
|
|
28
|
+
|
|
29
|
+
browser = env.browser()
|
|
30
|
+
print("CDP URL:", await browser.cdp_url())
|
|
31
|
+
print("Devtools URL:", await browser.devtools_url())
|
|
32
|
+
|
|
33
|
+
# Delete the instance
|
|
34
|
+
await env.close()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
if __name__ == "__main__":
|
|
38
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import asyncio
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
from typing import TypedDict, List
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import fleet as flt
|
|
8
|
+
from nova_act import NovaAct, ActResult
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
MAX_STEPS = 30
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Problem(TypedDict):
|
|
15
|
+
id: str
|
|
16
|
+
problem: str
|
|
17
|
+
category: str
|
|
18
|
+
difficulty: str
|
|
19
|
+
verifier_func: str
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def extract_function_name(function_str: str) -> str | None:
|
|
23
|
+
match = re.search(r"(?:async\s+)?def\s+(\w+)\s*\(", function_str)
|
|
24
|
+
if match:
|
|
25
|
+
return match.group(1)
|
|
26
|
+
raise ValueError(f"No function name found in {function_str}")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
async def main():
|
|
30
|
+
parser = argparse.ArgumentParser(
|
|
31
|
+
description="Load and display Jira problems from JSON file"
|
|
32
|
+
)
|
|
33
|
+
parser.add_argument(
|
|
34
|
+
"json_file", type=str, help="Path to the JSON file containing problems"
|
|
35
|
+
)
|
|
36
|
+
args = parser.parse_args()
|
|
37
|
+
|
|
38
|
+
file_path = Path(args.json_file)
|
|
39
|
+
if not file_path.exists():
|
|
40
|
+
raise FileNotFoundError(f"Error: File '{args.json_file}' not found")
|
|
41
|
+
|
|
42
|
+
env = await flt.env.make("fira:v1.2.7")
|
|
43
|
+
print(f"New Instance: {env.urls.app}")
|
|
44
|
+
|
|
45
|
+
successes = 0
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
with open(args.json_file, "r") as f:
|
|
49
|
+
data = json.load(f)
|
|
50
|
+
problems: List[Problem] = data["problems"]
|
|
51
|
+
|
|
52
|
+
print(f"Loaded {len(problems)} problems from '{args.json_file}'")
|
|
53
|
+
|
|
54
|
+
for i, problem in enumerate(problems):
|
|
55
|
+
print(f"Solving problem {i + 1} of {len(problems)}: {problem['id']}")
|
|
56
|
+
await env.reset()
|
|
57
|
+
|
|
58
|
+
def run_nova() -> ActResult:
|
|
59
|
+
with NovaAct(starting_page=env.urls.app, headless=True) as nova:
|
|
60
|
+
return nova.act(problem["problem"], max_steps=MAX_STEPS)
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
await asyncio.to_thread(run_nova)
|
|
64
|
+
except Exception as e:
|
|
65
|
+
print(f"Error: {e}")
|
|
66
|
+
|
|
67
|
+
function_name = extract_function_name(problem["verifier_func"])
|
|
68
|
+
print(f"Verifying {function_name} ({problem['id']})...")
|
|
69
|
+
response = await env.verify_raw(problem["verifier_func"], function_name)
|
|
70
|
+
print(response)
|
|
71
|
+
if response.success:
|
|
72
|
+
successes += 1
|
|
73
|
+
|
|
74
|
+
print(f"Successes: {successes}")
|
|
75
|
+
print(f"Total: {len(problems)}")
|
|
76
|
+
print(f"Success rate: {successes / len(problems)}")
|
|
77
|
+
finally:
|
|
78
|
+
await env.close()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
if __name__ == "__main__":
|
|
82
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import fleet as flt
|
|
3
|
+
from nova_act import NovaAct, ActResult
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
async def main():
|
|
7
|
+
instance = await flt.env.make("hubspot")
|
|
8
|
+
cdp_url = await instance.browser().cdp_url()
|
|
9
|
+
|
|
10
|
+
loop = asyncio.get_event_loop()
|
|
11
|
+
|
|
12
|
+
def run_nova() -> ActResult:
|
|
13
|
+
with NovaAct(
|
|
14
|
+
starting_page=instance.urls.app,
|
|
15
|
+
cdp_endpoint_url=cdp_url,
|
|
16
|
+
preview={"playwright_actuation": True},
|
|
17
|
+
) as nova:
|
|
18
|
+
future = asyncio.run_coroutine_threadsafe(
|
|
19
|
+
instance.browser().devtools_url(), loop
|
|
20
|
+
)
|
|
21
|
+
print("Devtools URL:", future.result())
|
|
22
|
+
return nova.act("Create a deal")
|
|
23
|
+
|
|
24
|
+
await asyncio.to_thread(run_nova)
|
|
25
|
+
await instance.close()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
if __name__ == "__main__":
|
|
29
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from openai import AsyncOpenAI
|
|
3
|
+
import fleet as flt
|
|
4
|
+
import json
|
|
5
|
+
from typing import Callable
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
client = AsyncOpenAI()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def sanitize_message(msg: dict) -> dict:
|
|
12
|
+
"""Return a copy of the message with image_url omitted for computer_call_output messages."""
|
|
13
|
+
if msg.get("type") == "computer_call_output":
|
|
14
|
+
output = msg.get("output", {})
|
|
15
|
+
if isinstance(output, dict):
|
|
16
|
+
sanitized = msg.copy()
|
|
17
|
+
sanitized["output"] = {**output, "image_url": "[omitted]"}
|
|
18
|
+
return sanitized
|
|
19
|
+
return msg
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Agent:
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
browser,
|
|
26
|
+
model="computer-use-preview",
|
|
27
|
+
tools: list[dict] = [],
|
|
28
|
+
acknowledge_safety_check_callback: Callable = lambda: False,
|
|
29
|
+
):
|
|
30
|
+
self.model = model
|
|
31
|
+
self.computer = browser
|
|
32
|
+
self.tools = tools
|
|
33
|
+
self.print_steps = True
|
|
34
|
+
self.debug = False
|
|
35
|
+
self.show_images = False
|
|
36
|
+
self.acknowledge_safety_check_callback = acknowledge_safety_check_callback
|
|
37
|
+
|
|
38
|
+
if browser:
|
|
39
|
+
dimensions = browser.get_dimensions()
|
|
40
|
+
self.tools += [
|
|
41
|
+
{
|
|
42
|
+
"type": "computer-preview",
|
|
43
|
+
"display_width": dimensions[0],
|
|
44
|
+
"display_height": dimensions[1],
|
|
45
|
+
"environment": browser.get_environment(),
|
|
46
|
+
},
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
def debug_print(self, *args):
|
|
50
|
+
if self.debug:
|
|
51
|
+
print(*args)
|
|
52
|
+
|
|
53
|
+
async def handle_item(self, item):
|
|
54
|
+
"""Handle each item; may cause a computer action + screenshot."""
|
|
55
|
+
if self.debug:
|
|
56
|
+
print(f"Handling item of type: {item.get('type')}")
|
|
57
|
+
|
|
58
|
+
if item["type"] == "message":
|
|
59
|
+
if self.print_steps:
|
|
60
|
+
print(item["content"][0]["text"])
|
|
61
|
+
|
|
62
|
+
if item["type"] == "function_call":
|
|
63
|
+
name, args = item["name"], json.loads(item["arguments"])
|
|
64
|
+
if self.print_steps:
|
|
65
|
+
print(f"{name}({args})")
|
|
66
|
+
|
|
67
|
+
if hasattr(self.computer, name): # if function exists on computer, call it
|
|
68
|
+
method = getattr(self.computer, name)
|
|
69
|
+
await method(**args)
|
|
70
|
+
return [
|
|
71
|
+
{
|
|
72
|
+
"type": "function_call_output",
|
|
73
|
+
"call_id": item["call_id"],
|
|
74
|
+
"output": "success", # hard-coded output for demo
|
|
75
|
+
}
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
if item["type"] == "computer_call":
|
|
79
|
+
action = item["action"]
|
|
80
|
+
action_type = action["type"]
|
|
81
|
+
action_args = {k: v for k, v in action.items() if k != "type"}
|
|
82
|
+
if self.print_steps:
|
|
83
|
+
print(f"{action_type}({action_args})")
|
|
84
|
+
|
|
85
|
+
method = getattr(self.computer, action_type)
|
|
86
|
+
await method(**action_args)
|
|
87
|
+
|
|
88
|
+
screenshot_base64 = await self.computer.screenshot()
|
|
89
|
+
|
|
90
|
+
# if user doesn't ack all safety checks exit with error
|
|
91
|
+
pending_checks = item.get("pending_safety_checks", [])
|
|
92
|
+
for check in pending_checks:
|
|
93
|
+
message = check["message"]
|
|
94
|
+
if not self.acknowledge_safety_check_callback(message):
|
|
95
|
+
raise ValueError(
|
|
96
|
+
f"Safety check failed: {message}. Cannot continue with unacknowledged safety checks."
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
call_output = {
|
|
100
|
+
"type": "computer_call_output",
|
|
101
|
+
"call_id": item["call_id"],
|
|
102
|
+
"acknowledged_safety_checks": pending_checks,
|
|
103
|
+
"output": {
|
|
104
|
+
"type": "input_image",
|
|
105
|
+
"image_url": f"data:image/png;base64,{screenshot_base64}",
|
|
106
|
+
},
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
# additional URL safety checks for browser environments
|
|
110
|
+
if self.computer.get_environment() == "browser":
|
|
111
|
+
current_url = self.computer.get_current_url()
|
|
112
|
+
call_output["output"]["current_url"] = current_url
|
|
113
|
+
|
|
114
|
+
return [call_output]
|
|
115
|
+
return []
|
|
116
|
+
|
|
117
|
+
async def run_full_turn(
|
|
118
|
+
self, input_items, print_steps=True, debug=False, show_images=False
|
|
119
|
+
):
|
|
120
|
+
self.print_steps = print_steps
|
|
121
|
+
self.debug = debug
|
|
122
|
+
self.show_images = show_images
|
|
123
|
+
new_items = []
|
|
124
|
+
|
|
125
|
+
# keep looping until we get a final response
|
|
126
|
+
while new_items[-1].get("role") != "assistant" if new_items else True:
|
|
127
|
+
self.debug_print([sanitize_message(msg) for msg in input_items + new_items])
|
|
128
|
+
|
|
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(
|
|
138
|
+
model=self.model,
|
|
139
|
+
input=clean_input,
|
|
140
|
+
tools=self.tools,
|
|
141
|
+
truncation="auto",
|
|
142
|
+
)
|
|
143
|
+
|
|
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"):
|
|
160
|
+
if self.debug:
|
|
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")
|
|
164
|
+
raise ValueError(f"API Error: {error_msg}")
|
|
165
|
+
else:
|
|
166
|
+
raise ValueError("No output from model")
|
|
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
|
|
183
|
+
|
|
184
|
+
return new_items
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
tools = []
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
async def ainput(prompt: str = "") -> str:
|
|
191
|
+
"""Async version of input()"""
|
|
192
|
+
loop = asyncio.get_event_loop()
|
|
193
|
+
return await loop.run_in_executor(None, input, prompt)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
async def main():
|
|
197
|
+
# Create a Fleet environment instance
|
|
198
|
+
instance = await flt.env.make("hubspot")
|
|
199
|
+
|
|
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=[])
|
|
206
|
+
items = [
|
|
207
|
+
{
|
|
208
|
+
"role": "developer",
|
|
209
|
+
"content": "You have access to a clone of Hubspot. You can use the computer to navigate the browser and perform actions.",
|
|
210
|
+
}
|
|
211
|
+
]
|
|
212
|
+
|
|
213
|
+
while True:
|
|
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()
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
if __name__ == "__main__":
|
|
233
|
+
asyncio.run(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())
|
|
@@ -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
|
|
@@ -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,
|
|
@@ -29,6 +30,16 @@ from .env import (
|
|
|
29
30
|
ChromeStartResponse,
|
|
30
31
|
ChromeStatusResponse,
|
|
31
32
|
)
|
|
33
|
+
from .verifiers import *
|
|
34
|
+
from . import env
|
|
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
|
|
32
43
|
|
|
33
44
|
__version__ = "0.1.1"
|
|
34
45
|
__all__ = [
|
|
@@ -39,6 +50,7 @@ __all__ = [
|
|
|
39
50
|
"FleetConfigurationError",
|
|
40
51
|
"Fleet",
|
|
41
52
|
"AsyncFleet",
|
|
53
|
+
"AsyncInstanceClient",
|
|
42
54
|
"InstanceRequest",
|
|
43
55
|
"ResetRequest",
|
|
44
56
|
"ResetResponse",
|
|
@@ -47,3 +59,7 @@ __all__ = [
|
|
|
47
59
|
"ChromeStartResponse",
|
|
48
60
|
"ChromeStatusResponse",
|
|
49
61
|
]
|
|
62
|
+
|
|
63
|
+
# Add playwright wrapper to exports if available
|
|
64
|
+
if _PLAYWRIGHT_AVAILABLE:
|
|
65
|
+
__all__.append("FleetPlaywrightWrapper")
|