fleet-python 0.2.32__tar.gz → 0.2.34__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.32 → fleet_python-0.2.34}/PKG-INFO +1 -1
- fleet_python-0.2.34/examples/exampleResume.py +191 -0
- fleet_python-0.2.34/examples/test_cdp_logging.py +80 -0
- {fleet_python-0.2.32 → fleet_python-0.2.34}/fleet/client.py +37 -10
- {fleet_python-0.2.32 → fleet_python-0.2.34}/fleet/env/client.py +1 -1
- {fleet_python-0.2.32 → fleet_python-0.2.34}/fleet/global_client.py +1 -1
- {fleet_python-0.2.32 → fleet_python-0.2.34}/fleet/instance/client.py +3 -1
- fleet_python-0.2.34/fleet/resources/mcp.py +55 -0
- {fleet_python-0.2.32 → fleet_python-0.2.34}/fleet/resources/sqlite.py +9 -3
- fleet_python-0.2.34/fleet/tasks.py +155 -0
- {fleet_python-0.2.32 → fleet_python-0.2.34}/fleet/verifiers/verifier.py +7 -0
- {fleet_python-0.2.32 → fleet_python-0.2.34}/fleet_python.egg-info/PKG-INFO +1 -1
- {fleet_python-0.2.32 → fleet_python-0.2.34}/fleet_python.egg-info/SOURCES.txt +2 -0
- {fleet_python-0.2.32 → fleet_python-0.2.34}/pyproject.toml +1 -1
- fleet_python-0.2.32/fleet/resources/mcp.py +0 -54
- fleet_python-0.2.32/fleet/tasks.py +0 -263
- {fleet_python-0.2.32 → fleet_python-0.2.34}/LICENSE +0 -0
- {fleet_python-0.2.32 → fleet_python-0.2.34}/README.md +0 -0
- {fleet_python-0.2.32 → fleet_python-0.2.34}/examples/diff_example.py +0 -0
- {fleet_python-0.2.32 → fleet_python-0.2.34}/examples/dsl_example.py +0 -0
- {fleet_python-0.2.32 → fleet_python-0.2.34}/examples/example.py +0 -0
- {fleet_python-0.2.32 → fleet_python-0.2.34}/examples/example_account.py +0 -0
- {fleet_python-0.2.32 → fleet_python-0.2.34}/examples/example_action_log.py +0 -0
- {fleet_python-0.2.32 → fleet_python-0.2.34}/examples/example_client.py +0 -0
- {fleet_python-0.2.32 → fleet_python-0.2.34}/examples/example_mcp_anthropic.py +0 -0
- {fleet_python-0.2.32 → fleet_python-0.2.34}/examples/example_mcp_openai.py +0 -0
- {fleet_python-0.2.32 → fleet_python-0.2.34}/examples/example_sync.py +0 -0
- {fleet_python-0.2.32 → fleet_python-0.2.34}/examples/example_task.py +0 -0
- {fleet_python-0.2.32 → fleet_python-0.2.34}/examples/example_tasks.py +0 -0
- {fleet_python-0.2.32 → fleet_python-0.2.34}/examples/example_verifier.py +0 -0
- {fleet_python-0.2.32 → fleet_python-0.2.34}/examples/gemini_example.py +0 -0
- {fleet_python-0.2.32 → fleet_python-0.2.34}/examples/json_tasks_example.py +0 -0
- {fleet_python-0.2.32 → fleet_python-0.2.34}/examples/nova_act_example.py +0 -0
- {fleet_python-0.2.32 → fleet_python-0.2.34}/examples/openai_example.py +0 -0
- {fleet_python-0.2.32 → fleet_python-0.2.34}/examples/openai_simple_example.py +0 -0
- {fleet_python-0.2.32 → fleet_python-0.2.34}/examples/query_builder_example.py +0 -0
- {fleet_python-0.2.32 → fleet_python-0.2.34}/examples/quickstart.py +0 -0
- {fleet_python-0.2.32 → fleet_python-0.2.34}/fleet/__init__.py +0 -0
- {fleet_python-0.2.32 → fleet_python-0.2.34}/fleet/_async/__init__.py +0 -0
- {fleet_python-0.2.32 → fleet_python-0.2.34}/fleet/_async/base.py +0 -0
- {fleet_python-0.2.32 → fleet_python-0.2.34}/fleet/_async/client.py +0 -0
- {fleet_python-0.2.32 → fleet_python-0.2.34}/fleet/_async/env/__init__.py +0 -0
- {fleet_python-0.2.32 → fleet_python-0.2.34}/fleet/_async/env/client.py +0 -0
- {fleet_python-0.2.32 → fleet_python-0.2.34}/fleet/_async/exceptions.py +0 -0
- {fleet_python-0.2.32 → fleet_python-0.2.34}/fleet/_async/global_client.py +0 -0
- {fleet_python-0.2.32 → fleet_python-0.2.34}/fleet/_async/instance/__init__.py +0 -0
- {fleet_python-0.2.32 → fleet_python-0.2.34}/fleet/_async/instance/base.py +0 -0
- {fleet_python-0.2.32 → fleet_python-0.2.34}/fleet/_async/instance/client.py +0 -0
- {fleet_python-0.2.32 → fleet_python-0.2.34}/fleet/_async/models.py +0 -0
- {fleet_python-0.2.32 → fleet_python-0.2.34}/fleet/_async/resources/__init__.py +0 -0
- {fleet_python-0.2.32 → fleet_python-0.2.34}/fleet/_async/resources/base.py +0 -0
- {fleet_python-0.2.32 → fleet_python-0.2.34}/fleet/_async/resources/browser.py +0 -0
- {fleet_python-0.2.32 → fleet_python-0.2.34}/fleet/_async/resources/mcp.py +0 -0
- {fleet_python-0.2.32 → fleet_python-0.2.34}/fleet/_async/resources/sqlite.py +0 -0
- {fleet_python-0.2.32 → fleet_python-0.2.34}/fleet/_async/tasks.py +0 -0
- {fleet_python-0.2.32 → fleet_python-0.2.34}/fleet/_async/verifiers/__init__.py +0 -0
- {fleet_python-0.2.32 → fleet_python-0.2.34}/fleet/_async/verifiers/bundler.py +0 -0
- {fleet_python-0.2.32 → fleet_python-0.2.34}/fleet/_async/verifiers/verifier.py +0 -0
- {fleet_python-0.2.32 → fleet_python-0.2.34}/fleet/base.py +0 -0
- {fleet_python-0.2.32 → fleet_python-0.2.34}/fleet/config.py +0 -0
- {fleet_python-0.2.32 → fleet_python-0.2.34}/fleet/env/__init__.py +0 -0
- {fleet_python-0.2.32 → fleet_python-0.2.34}/fleet/exceptions.py +0 -0
- {fleet_python-0.2.32 → fleet_python-0.2.34}/fleet/instance/__init__.py +0 -0
- {fleet_python-0.2.32 → fleet_python-0.2.34}/fleet/instance/base.py +0 -0
- {fleet_python-0.2.32 → fleet_python-0.2.34}/fleet/instance/models.py +0 -0
- {fleet_python-0.2.32 → fleet_python-0.2.34}/fleet/models.py +0 -0
- {fleet_python-0.2.32 → fleet_python-0.2.34}/fleet/resources/__init__.py +0 -0
- {fleet_python-0.2.32 → fleet_python-0.2.34}/fleet/resources/base.py +0 -0
- {fleet_python-0.2.32 → fleet_python-0.2.34}/fleet/resources/browser.py +0 -0
- {fleet_python-0.2.32 → fleet_python-0.2.34}/fleet/types.py +0 -0
- {fleet_python-0.2.32 → fleet_python-0.2.34}/fleet/verifiers/__init__.py +0 -0
- {fleet_python-0.2.32 → fleet_python-0.2.34}/fleet/verifiers/bundler.py +0 -0
- {fleet_python-0.2.32 → fleet_python-0.2.34}/fleet/verifiers/code.py +0 -0
- {fleet_python-0.2.32 → fleet_python-0.2.34}/fleet/verifiers/db.py +0 -0
- {fleet_python-0.2.32 → fleet_python-0.2.34}/fleet/verifiers/decorator.py +0 -0
- {fleet_python-0.2.32 → fleet_python-0.2.34}/fleet/verifiers/parse.py +0 -0
- {fleet_python-0.2.32 → fleet_python-0.2.34}/fleet/verifiers/sql_differ.py +0 -0
- {fleet_python-0.2.32 → fleet_python-0.2.34}/fleet_python.egg-info/dependency_links.txt +0 -0
- {fleet_python-0.2.32 → fleet_python-0.2.34}/fleet_python.egg-info/requires.txt +0 -0
- {fleet_python-0.2.32 → fleet_python-0.2.34}/fleet_python.egg-info/top_level.txt +0 -0
- {fleet_python-0.2.32 → fleet_python-0.2.34}/scripts/fix_sync_imports.py +0 -0
- {fleet_python-0.2.32 → fleet_python-0.2.34}/scripts/unasync.py +0 -0
- {fleet_python-0.2.32 → fleet_python-0.2.34}/setup.cfg +0 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Interactive Browser Snapshot/Resume Example
|
|
4
|
+
|
|
5
|
+
This script:
|
|
6
|
+
1. Creates an environment and browser
|
|
7
|
+
2. Lets you interact with it manually
|
|
8
|
+
3. Takes a snapshot when you type 'close'
|
|
9
|
+
4. Resumes from the snapshot with a new environment
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import sys
|
|
15
|
+
from datetime import datetime
|
|
16
|
+
|
|
17
|
+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
|
18
|
+
|
|
19
|
+
from fleet import Fleet
|
|
20
|
+
|
|
21
|
+
def main():
|
|
22
|
+
# Initialize Fleet client
|
|
23
|
+
api_key = os.getenv("FLEET_API_KEY")
|
|
24
|
+
if not api_key:
|
|
25
|
+
print("Error: FLEET_API_KEY environment variable not set")
|
|
26
|
+
sys.exit(1)
|
|
27
|
+
|
|
28
|
+
fleet = Fleet(api_key=api_key)
|
|
29
|
+
|
|
30
|
+
# Initialize environment variables
|
|
31
|
+
env = None
|
|
32
|
+
new_env = None
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
# Step 1: Create environment and browser
|
|
36
|
+
print("🚀 Creating new environment...")
|
|
37
|
+
env_key = "fira:v1.3.2"
|
|
38
|
+
|
|
39
|
+
env = fleet.make(env_key)
|
|
40
|
+
print(f"✅ Environment created: {env.instance_id}")
|
|
41
|
+
print(f"📝 Session ID: {env.session_id}")
|
|
42
|
+
|
|
43
|
+
# Start browser
|
|
44
|
+
print("\n🌐 Starting browser...")
|
|
45
|
+
browser = env.browser()
|
|
46
|
+
browser.start(width=1920, height=1080)
|
|
47
|
+
|
|
48
|
+
# Get browser URLs with session ID for automatic logging
|
|
49
|
+
cdp_page_url = browser.cdp_page_url()
|
|
50
|
+
cdp_browser_url = browser.cdp_browser_url()
|
|
51
|
+
devtools_url = browser.devtools_url()
|
|
52
|
+
|
|
53
|
+
print(f"✅ Browser started!")
|
|
54
|
+
print(f"🔗 CDP Page URL: {cdp_page_url}")
|
|
55
|
+
print(f"🔗 CDP Browser URL: {cdp_browser_url}")
|
|
56
|
+
print(f"🔗 DevTools URL: {devtools_url}")
|
|
57
|
+
|
|
58
|
+
# Show connection info for debugging
|
|
59
|
+
conn_info = browser.get_connection_info()
|
|
60
|
+
print(f"📊 Logging enabled: {conn_info['logging_enabled']}")
|
|
61
|
+
if conn_info['logging_enabled']:
|
|
62
|
+
print(f"📝 All CDP actions will be automatically logged to session: {env.session_id}")
|
|
63
|
+
|
|
64
|
+
# Step 2: Wait for user interaction
|
|
65
|
+
print("\n" + "="*60)
|
|
66
|
+
print("🎮 INTERACTIVE MODE")
|
|
67
|
+
print("="*60)
|
|
68
|
+
print("You can now interact with the browser manually.")
|
|
69
|
+
print("Open the DevTools URL in Chrome to see and control the browser.")
|
|
70
|
+
print("\nType 'close' and press Enter when you're done to save a snapshot.")
|
|
71
|
+
print("="*60 + "\n")
|
|
72
|
+
|
|
73
|
+
while True:
|
|
74
|
+
user_input = input(">>> ").strip().lower()
|
|
75
|
+
|
|
76
|
+
if user_input == "close":
|
|
77
|
+
break
|
|
78
|
+
elif user_input == "status":
|
|
79
|
+
print(f"Environment: {env.instance_id}")
|
|
80
|
+
print(f"Session: {env.session_id}")
|
|
81
|
+
else:
|
|
82
|
+
print("Type 'close' to save snapshot and exit, or 'status' to see current info")
|
|
83
|
+
|
|
84
|
+
# Step 3: Take snapshot
|
|
85
|
+
print("\n📸 Taking snapshot...")
|
|
86
|
+
snapshot = env.get_snapshot(browser)
|
|
87
|
+
|
|
88
|
+
# Save snapshot to file
|
|
89
|
+
snapshot_file = f"snapshot_{env.session_id}_{int(datetime.now().timestamp())}.json"
|
|
90
|
+
with open(snapshot_file, "w") as f:
|
|
91
|
+
json.dump(snapshot.model_dump(), f, indent=2)
|
|
92
|
+
|
|
93
|
+
print(f"✅ Snapshot saved to: {snapshot_file}")
|
|
94
|
+
print(f" - Tool logs: {len(snapshot.tool_logs)} entries")
|
|
95
|
+
print(f" - Action logs: {len(snapshot.action_logs)} entries")
|
|
96
|
+
print(f" - Page URL: {snapshot.page_url}")
|
|
97
|
+
print(f" - Viewport: {snapshot.viewport_size}")
|
|
98
|
+
|
|
99
|
+
# Close original environment
|
|
100
|
+
print("\n🏁 Closing original environment...")
|
|
101
|
+
env.close()
|
|
102
|
+
env = None # Mark as closed
|
|
103
|
+
|
|
104
|
+
# Step 4: Resume from snapshot
|
|
105
|
+
print("\n" + "="*60)
|
|
106
|
+
print("🔄 RESUMING FROM SNAPSHOT")
|
|
107
|
+
print("="*60)
|
|
108
|
+
|
|
109
|
+
input("\nPress Enter to resume from snapshot...")
|
|
110
|
+
|
|
111
|
+
print("\n🚀 Creating new environment from snapshot...")
|
|
112
|
+
new_env, validation = fleet.resume(
|
|
113
|
+
snapshot,
|
|
114
|
+
validate=True,
|
|
115
|
+
playback_speed=2.0 # Replay at 2x speed
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
print(f"✅ Environment resumed: {new_env.instance_id}")
|
|
119
|
+
print(f"📝 New session ID: {new_env.session_id}")
|
|
120
|
+
|
|
121
|
+
# Show validation results
|
|
122
|
+
print(f"\n📊 Validation Results:")
|
|
123
|
+
print(f" - Success: {validation.success}")
|
|
124
|
+
print(f" - Page match: {validation.page_match}")
|
|
125
|
+
print(f" - Action log match: {validation.action_log_match}")
|
|
126
|
+
|
|
127
|
+
if validation.discrepancies:
|
|
128
|
+
print(f" - Discrepancies ({len(validation.discrepancies)}):")
|
|
129
|
+
for disc in validation.discrepancies[:5]: # Show first 5
|
|
130
|
+
print(f" • {disc}")
|
|
131
|
+
if len(validation.discrepancies) > 5:
|
|
132
|
+
print(f" • ... and {len(validation.discrepancies) - 5} more")
|
|
133
|
+
|
|
134
|
+
# Get new browser reference
|
|
135
|
+
new_browser = new_env.browser()
|
|
136
|
+
print(f"\n🌐 New browser ready!")
|
|
137
|
+
print(f"🔗 CDP Page URL: {new_browser.cdp_page_url()}")
|
|
138
|
+
print(f"🔗 DevTools URL: {new_browser.devtools_url()}")
|
|
139
|
+
print(f"📝 Resume session logging to: {new_env.session_id}")
|
|
140
|
+
|
|
141
|
+
# Step 5: Keep new environment open for inspection
|
|
142
|
+
print("\n" + "="*60)
|
|
143
|
+
print("🎮 RESUMED ENVIRONMENT READY")
|
|
144
|
+
print("="*60)
|
|
145
|
+
print("The resumed environment is now ready. You can inspect it to verify")
|
|
146
|
+
print("it matches your original state.")
|
|
147
|
+
print("\nType 'done' when finished to close the resumed environment.")
|
|
148
|
+
print("="*60 + "\n")
|
|
149
|
+
|
|
150
|
+
while True:
|
|
151
|
+
user_input = input(">>> ").strip().lower()
|
|
152
|
+
|
|
153
|
+
if user_input == "done":
|
|
154
|
+
break
|
|
155
|
+
elif user_input == "status":
|
|
156
|
+
print(f"Environment: {new_env.instance_id}")
|
|
157
|
+
print(f"Session: {new_env.session_id}")
|
|
158
|
+
else:
|
|
159
|
+
print("Type 'done' to close the environment, or 'status' to see current info")
|
|
160
|
+
|
|
161
|
+
# Clean up
|
|
162
|
+
print("\n🏁 Closing resumed environment...")
|
|
163
|
+
new_env.close()
|
|
164
|
+
new_env = None # Mark as closed
|
|
165
|
+
print("✅ All done!")
|
|
166
|
+
|
|
167
|
+
except KeyboardInterrupt:
|
|
168
|
+
print("\n\n⚠️ Interrupted by user")
|
|
169
|
+
except Exception as e:
|
|
170
|
+
print(f"\n❌ Error: {e}")
|
|
171
|
+
finally:
|
|
172
|
+
# Always clean up environments
|
|
173
|
+
if env is not None:
|
|
174
|
+
try:
|
|
175
|
+
print("\n🧹 Cleaning up original environment...")
|
|
176
|
+
env.close()
|
|
177
|
+
print("✅ Original environment closed")
|
|
178
|
+
except Exception as e:
|
|
179
|
+
print(f"⚠️ Failed to close original environment: {e}")
|
|
180
|
+
|
|
181
|
+
if new_env is not None:
|
|
182
|
+
try:
|
|
183
|
+
print("\n🧹 Cleaning up resumed environment...")
|
|
184
|
+
new_env.close()
|
|
185
|
+
print("✅ Resumed environment closed")
|
|
186
|
+
except Exception as e:
|
|
187
|
+
print(f"⚠️ Failed to close resumed environment: {e}")
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
if __name__ == "__main__":
|
|
191
|
+
main()
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Test CDP Logging Example
|
|
4
|
+
|
|
5
|
+
This script demonstrates the new automatic CDP logging functionality.
|
|
6
|
+
All CDP commands and events are now logged automatically at the proxy level.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
|
|
13
|
+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
|
14
|
+
|
|
15
|
+
from fleet import Fleet
|
|
16
|
+
|
|
17
|
+
def main():
|
|
18
|
+
# Initialize Fleet client
|
|
19
|
+
api_key = os.getenv("FLEET_API_KEY")
|
|
20
|
+
if not api_key:
|
|
21
|
+
print("Error: FLEET_API_KEY environment variable not set")
|
|
22
|
+
sys.exit(1)
|
|
23
|
+
|
|
24
|
+
fleet = Fleet(api_key=api_key)
|
|
25
|
+
env = None
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
# Create environment
|
|
29
|
+
print("🚀 Creating new environment...")
|
|
30
|
+
env_key = "fira:v1.3.2" # Replace with your actual environment key
|
|
31
|
+
|
|
32
|
+
env = fleet.make(env_key)
|
|
33
|
+
print(f"✅ Environment created: {env.instance_id}")
|
|
34
|
+
print(f"📝 Session ID: {env.session_id}")
|
|
35
|
+
|
|
36
|
+
# Start browser
|
|
37
|
+
print("\n🌐 Starting browser with automatic CDP logging...")
|
|
38
|
+
browser = env.browser()
|
|
39
|
+
browser.start(width=1920, height=1080)
|
|
40
|
+
|
|
41
|
+
# Show connection information
|
|
42
|
+
conn_info = browser.get_connection_info()
|
|
43
|
+
print("\n📊 Connection Information:")
|
|
44
|
+
print(f" Session ID: {conn_info['session_id']}")
|
|
45
|
+
print(f" CDP Page URL: {conn_info['cdp_page_url']}")
|
|
46
|
+
print(f" CDP Browser URL: {conn_info['cdp_browser_url']}")
|
|
47
|
+
print(f" Logging Enabled: {conn_info['logging_enabled']}")
|
|
48
|
+
|
|
49
|
+
if conn_info['logging_enabled']:
|
|
50
|
+
print(f"\n✅ All CDP actions will be automatically logged!")
|
|
51
|
+
print(f" - Connect any CDP client to: {conn_info['cdp_page_url']}")
|
|
52
|
+
print(f" - All commands and events will be logged to session: {env.session_id}")
|
|
53
|
+
print(f" - Use DevTools at: {conn_info['devtools_url']}")
|
|
54
|
+
else:
|
|
55
|
+
print(f"\n⚠️ Logging not enabled - no session ID available")
|
|
56
|
+
|
|
57
|
+
print("\n" + "="*60)
|
|
58
|
+
print("🎯 LOGGING TEST COMPLETE")
|
|
59
|
+
print("="*60)
|
|
60
|
+
print("The browser is ready with automatic CDP logging enabled.")
|
|
61
|
+
print("Connect any CDP client or Playwright to the URLs above.")
|
|
62
|
+
print("All actions will be automatically captured in the tool_log table.")
|
|
63
|
+
print("\nPress Enter to close the environment...")
|
|
64
|
+
print("="*60 + "\n")
|
|
65
|
+
|
|
66
|
+
input()
|
|
67
|
+
|
|
68
|
+
except Exception as e:
|
|
69
|
+
print(f"\n❌ Error: {e}")
|
|
70
|
+
finally:
|
|
71
|
+
if env is not None:
|
|
72
|
+
try:
|
|
73
|
+
print("\n🧹 Cleaning up environment...")
|
|
74
|
+
env.close()
|
|
75
|
+
print("✅ Environment closed")
|
|
76
|
+
except Exception as e:
|
|
77
|
+
print(f"⚠️ Failed to close environment: {e}")
|
|
78
|
+
|
|
79
|
+
if __name__ == "__main__":
|
|
80
|
+
main()
|
|
@@ -50,7 +50,7 @@ from .instance.client import ValidatorType
|
|
|
50
50
|
from .resources.base import Resource
|
|
51
51
|
from .resources.sqlite import SQLiteResource
|
|
52
52
|
from .resources.browser import BrowserResource
|
|
53
|
-
from
|
|
53
|
+
from ..resources.mcp import MCPResource
|
|
54
54
|
|
|
55
55
|
logger = logging.getLogger(__name__)
|
|
56
56
|
|
|
@@ -254,7 +254,9 @@ class Fleet:
|
|
|
254
254
|
def execute_verifier_remote(
|
|
255
255
|
self, bundle_data: bytes, args: tuple, kwargs: dict, timeout: Optional[int] = 30
|
|
256
256
|
) -> VerifiersExecuteResponse:
|
|
257
|
-
return _execute_verifier_remote(
|
|
257
|
+
return _execute_verifier_remote(
|
|
258
|
+
self.client, bundle_data, args, kwargs, timeout
|
|
259
|
+
)
|
|
258
260
|
|
|
259
261
|
def delete(self, instance_id: str) -> InstanceResponse:
|
|
260
262
|
return _delete_instance(self.client, instance_id)
|
|
@@ -265,7 +267,9 @@ class Fleet:
|
|
|
265
267
|
|
|
266
268
|
return self.load_task_array_from_string(tasks_data)
|
|
267
269
|
|
|
268
|
-
def load_task_array_from_string(
|
|
270
|
+
def load_task_array_from_string(
|
|
271
|
+
self, serialized_tasks: List[Dict]
|
|
272
|
+
) -> List[Task]:
|
|
269
273
|
tasks = []
|
|
270
274
|
|
|
271
275
|
json_tasks = json.loads(serialized_tasks)
|
|
@@ -303,11 +307,20 @@ class Fleet:
|
|
|
303
307
|
)
|
|
304
308
|
return task
|
|
305
309
|
|
|
306
|
-
def load_tasks(
|
|
307
|
-
|
|
310
|
+
def load_tasks(
|
|
311
|
+
self,
|
|
312
|
+
env_key: Optional[str] = None,
|
|
313
|
+
keys: Optional[List[str]] = None,
|
|
314
|
+
version: Optional[str] = None,
|
|
315
|
+
team_id: Optional[str] = None
|
|
316
|
+
) -> List[Task]:
|
|
317
|
+
"""Load tasks for the authenticated team, with optional filtering.
|
|
308
318
|
|
|
309
319
|
Args:
|
|
310
320
|
env_key: Optional environment key to filter tasks by
|
|
321
|
+
keys: Optional list of task keys to filter by
|
|
322
|
+
version: Optional version to filter tasks by (client-side filter)
|
|
323
|
+
team_id: Optional team_id to filter by (admin only)
|
|
311
324
|
|
|
312
325
|
Returns:
|
|
313
326
|
List[Task] containing Task objects
|
|
@@ -315,6 +328,10 @@ class Fleet:
|
|
|
315
328
|
params = {}
|
|
316
329
|
if env_key is not None:
|
|
317
330
|
params["env_key"] = env_key
|
|
331
|
+
if keys is not None:
|
|
332
|
+
params["task_keys"] = keys
|
|
333
|
+
if team_id is not None:
|
|
334
|
+
params["team_id"] = team_id
|
|
318
335
|
|
|
319
336
|
response = self.client.request("GET", "/v1/tasks", params=params)
|
|
320
337
|
task_list_response = TaskListResponse(**response.json())
|
|
@@ -375,6 +392,10 @@ class Fleet:
|
|
|
375
392
|
)
|
|
376
393
|
tasks.append(task)
|
|
377
394
|
|
|
395
|
+
# Apply client-side filtering for version if specified
|
|
396
|
+
if version is not None:
|
|
397
|
+
tasks = [task for task in tasks if task.version == version]
|
|
398
|
+
|
|
378
399
|
return tasks
|
|
379
400
|
|
|
380
401
|
def export_tasks(
|
|
@@ -464,7 +485,7 @@ class Fleet:
|
|
|
464
485
|
def _create_verifier_from_data(
|
|
465
486
|
self, verifier_id: str, verifier_key: str, verifier_code: str, verifier_sha: str
|
|
466
487
|
) -> "SyncVerifierFunction":
|
|
467
|
-
"""Create
|
|
488
|
+
"""Create an AsyncVerifierFunction from verifier data.
|
|
468
489
|
|
|
469
490
|
Args:
|
|
470
491
|
verifier_id: The verifier ID
|
|
@@ -473,10 +494,16 @@ class Fleet:
|
|
|
473
494
|
verifier_sha: The verifier SHA256
|
|
474
495
|
|
|
475
496
|
Returns:
|
|
476
|
-
|
|
497
|
+
AsyncVerifierFunction created from the verifier code
|
|
477
498
|
"""
|
|
478
|
-
from
|
|
499
|
+
from ..tasks import verifier_from_string
|
|
479
500
|
from .verifiers.verifier import SyncVerifierFunction
|
|
501
|
+
|
|
502
|
+
# Convert async verifier code to sync
|
|
503
|
+
if 'async def' in verifier_code:
|
|
504
|
+
verifier_code = verifier_code.replace('async def', 'def')
|
|
505
|
+
if 'await ' in verifier_code:
|
|
506
|
+
verifier_code = verifier_code.replace('await ', '')
|
|
480
507
|
|
|
481
508
|
# Use verifier_from_string to create the verifier
|
|
482
509
|
verifier_func = verifier_from_string(
|
|
@@ -486,10 +513,10 @@ class Fleet:
|
|
|
486
513
|
sha256=verifier_sha,
|
|
487
514
|
)
|
|
488
515
|
|
|
489
|
-
# Ensure we return
|
|
516
|
+
# Ensure we return an AsyncVerifierFunction
|
|
490
517
|
if not isinstance(verifier_func, SyncVerifierFunction):
|
|
491
518
|
raise TypeError(
|
|
492
|
-
f"Expected
|
|
519
|
+
f"Expected AsyncVerifierFunction but got {type(verifier_func).__name__}"
|
|
493
520
|
)
|
|
494
521
|
|
|
495
522
|
# Store the original verifier code for reference
|
|
@@ -63,7 +63,9 @@ class InstanceClient:
|
|
|
63
63
|
def load(self) -> None:
|
|
64
64
|
self._load_resources()
|
|
65
65
|
|
|
66
|
-
def reset(
|
|
66
|
+
def reset(
|
|
67
|
+
self, reset_request: Optional[ResetRequest] = None
|
|
68
|
+
) -> ResetResponse:
|
|
67
69
|
response = self.client.request(
|
|
68
70
|
"POST", "/reset", json=reset_request.model_dump() if reset_request else None
|
|
69
71
|
)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from typing import Dict
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class SyncMCPResource:
|
|
5
|
+
def __init__(self, url: str, env_key: str):
|
|
6
|
+
self.url = url
|
|
7
|
+
self._env_key = env_key
|
|
8
|
+
|
|
9
|
+
def openai(self) -> Dict[str, str]:
|
|
10
|
+
return {
|
|
11
|
+
"type": "mcp",
|
|
12
|
+
"server_label": self._env_key,
|
|
13
|
+
"server_url": self.url,
|
|
14
|
+
"require_approval": "never",
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
def anthropic(self) -> Dict[str, str]:
|
|
18
|
+
return {
|
|
19
|
+
"type": "url",
|
|
20
|
+
"url": self.url,
|
|
21
|
+
"name": self._env_key,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
def list_tools(self):
|
|
25
|
+
import aiohttp
|
|
26
|
+
|
|
27
|
+
"""
|
|
28
|
+
Make an async request to list available tools from the MCP endpoint.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
List of available tools with name, description, and input_schema
|
|
32
|
+
"""
|
|
33
|
+
with aiohttp.ClientSession() as session:
|
|
34
|
+
payload = {"jsonrpc": "2.0", "method": "tools/list", "params": {}, "id": 2}
|
|
35
|
+
|
|
36
|
+
with session.post(self.url, json=payload) as response:
|
|
37
|
+
data = response.json()
|
|
38
|
+
|
|
39
|
+
# Extract tools from the response
|
|
40
|
+
if "result" in data and "tools" in data["result"]:
|
|
41
|
+
tools = data["result"]["tools"]
|
|
42
|
+
|
|
43
|
+
available_tools = [
|
|
44
|
+
{
|
|
45
|
+
"name": tool.get("name"),
|
|
46
|
+
"description": tool.get("description"),
|
|
47
|
+
"input_schema": tool.get("inputSchema"),
|
|
48
|
+
}
|
|
49
|
+
for tool in tools
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
return available_tools
|
|
53
|
+
else:
|
|
54
|
+
# Handle error or empty response
|
|
55
|
+
return []
|
|
@@ -651,7 +651,9 @@ class SQLiteResource(Resource):
|
|
|
651
651
|
)
|
|
652
652
|
return DescribeResponse(**response.json())
|
|
653
653
|
|
|
654
|
-
def query(
|
|
654
|
+
def query(
|
|
655
|
+
self, query: str, args: Optional[List[Any]] = None
|
|
656
|
+
) -> QueryResponse:
|
|
655
657
|
return self._query(query, args, read_only=True)
|
|
656
658
|
|
|
657
659
|
def exec(self, query: str, args: Optional[List[Any]] = None) -> QueryResponse:
|
|
@@ -693,8 +695,12 @@ class SQLiteResource(Resource):
|
|
|
693
695
|
AsyncSnapshotDiff: Object containing the differences between the two databases
|
|
694
696
|
"""
|
|
695
697
|
# Create snapshots of both databases
|
|
696
|
-
before_snapshot = self.snapshot(
|
|
697
|
-
|
|
698
|
+
before_snapshot = self.snapshot(
|
|
699
|
+
name=f"before_{datetime.utcnow().isoformat()}"
|
|
700
|
+
)
|
|
701
|
+
after_snapshot = other.snapshot(
|
|
702
|
+
name=f"after_{datetime.utcnow().isoformat()}"
|
|
703
|
+
)
|
|
698
704
|
|
|
699
705
|
# Return the diff between the snapshots
|
|
700
706
|
return before_snapshot.diff(after_snapshot, ignore_config)
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""Fleet SDK Task Model."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from typing import Any, Dict, Optional, List
|
|
8
|
+
from uuid import UUID
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, Field, validator
|
|
11
|
+
|
|
12
|
+
# Import the shared VerifierFunction type that works for both async and sync
|
|
13
|
+
from fleet.types import VerifierFunction
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Task(BaseModel):
|
|
17
|
+
"""A task model representing a single task in the Fleet system."""
|
|
18
|
+
|
|
19
|
+
key: str = Field(..., description="Unique task key identifier")
|
|
20
|
+
prompt: str = Field(..., description="Task prompt or instruction")
|
|
21
|
+
env_id: str = Field(..., description="Environment identifier")
|
|
22
|
+
env_variables: Optional[Dict[str, Any]] = Field(
|
|
23
|
+
default_factory=dict, description="Environment variables"
|
|
24
|
+
)
|
|
25
|
+
created_at: Optional[datetime] = Field(None, description="Task creation timestamp")
|
|
26
|
+
version: Optional[str] = Field(None, description="Task version")
|
|
27
|
+
verifier_func: Optional[str] = Field(None, description="Verifier function code")
|
|
28
|
+
verifier: Optional[Any] = Field(
|
|
29
|
+
None, description="Verifier function with decorator (async or sync)"
|
|
30
|
+
)
|
|
31
|
+
verifier_id: Optional[str] = Field(None, description="Verifier identifier")
|
|
32
|
+
verifier_sha: Optional[str] = Field(None, description="Verifier SHA256 hash")
|
|
33
|
+
metadata: Optional[Dict[str, Any]] = Field(
|
|
34
|
+
default_factory=dict, description="Additional task metadata"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
@validator("key")
|
|
38
|
+
def validate_key_format(cls, v):
|
|
39
|
+
"""Validate key follows kebab-case format."""
|
|
40
|
+
if not re.match(r"^[a-z0-9]+(-[a-z0-9]+)*$", v):
|
|
41
|
+
raise ValueError(
|
|
42
|
+
f"Invalid task key format: {v}. Must follow kebab-case format."
|
|
43
|
+
)
|
|
44
|
+
return v
|
|
45
|
+
|
|
46
|
+
@validator("created_at", pre=True, always=True)
|
|
47
|
+
def set_created_at(cls, v):
|
|
48
|
+
"""Set created_at to current time if not provided."""
|
|
49
|
+
return v or datetime.now()
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def env_key(self) -> str:
|
|
53
|
+
"""Get the environment key combining env_id and version."""
|
|
54
|
+
if self.version:
|
|
55
|
+
return f"{self.env_id}:{self.version}"
|
|
56
|
+
return self.env_id
|
|
57
|
+
|
|
58
|
+
class Config:
|
|
59
|
+
"""Pydantic model configuration."""
|
|
60
|
+
|
|
61
|
+
json_encoders = {
|
|
62
|
+
datetime: lambda v: v.isoformat(),
|
|
63
|
+
}
|
|
64
|
+
# Allow arbitrary types for the verifier field
|
|
65
|
+
arbitrary_types_allowed = True
|
|
66
|
+
|
|
67
|
+
def verify(self, env, *args, **kwargs) -> float:
|
|
68
|
+
"""Verify the task using the verifier function (sync version).
|
|
69
|
+
|
|
70
|
+
For sync environments, calls the sync verifier directly.
|
|
71
|
+
For async verifiers, automatically runs them with asyncio.run().
|
|
72
|
+
"""
|
|
73
|
+
if self.verifier:
|
|
74
|
+
import inspect
|
|
75
|
+
|
|
76
|
+
result = self.verifier.remote(env, *args, **kwargs)
|
|
77
|
+
|
|
78
|
+
# If the result is a coroutine, we need to run it
|
|
79
|
+
if inspect.iscoroutine(result):
|
|
80
|
+
# Check if we're already in an event loop
|
|
81
|
+
try:
|
|
82
|
+
loop = asyncio.get_running_loop()
|
|
83
|
+
# We're in an async context, can't use asyncio.run()
|
|
84
|
+
raise RuntimeError(
|
|
85
|
+
"Cannot run async verifier in sync mode while event loop is running. "
|
|
86
|
+
"Use await task.verify_async() instead."
|
|
87
|
+
)
|
|
88
|
+
except RuntimeError:
|
|
89
|
+
# No event loop running, safe to use asyncio.run()
|
|
90
|
+
return asyncio.run(result)
|
|
91
|
+
else:
|
|
92
|
+
return result
|
|
93
|
+
else:
|
|
94
|
+
raise ValueError("No verifier function found for this task")
|
|
95
|
+
|
|
96
|
+
def verify_async(self, *args, **kwargs) -> float:
|
|
97
|
+
"""Verify the task using the verifier function (async version).
|
|
98
|
+
|
|
99
|
+
For async environments, awaits the async verifier.
|
|
100
|
+
Works with both sync and async verifiers in async contexts.
|
|
101
|
+
"""
|
|
102
|
+
if self.verifier:
|
|
103
|
+
result = self.verifier.remote(*args, **kwargs)
|
|
104
|
+
# If it's a coroutine, await it
|
|
105
|
+
import inspect
|
|
106
|
+
|
|
107
|
+
if inspect.iscoroutine(result):
|
|
108
|
+
return result
|
|
109
|
+
else:
|
|
110
|
+
return result
|
|
111
|
+
else:
|
|
112
|
+
raise ValueError("No verifier function found for this task")
|
|
113
|
+
|
|
114
|
+
def make_env(self, region: Optional[str] = None):
|
|
115
|
+
"""Create an environment instance for this task's environment.
|
|
116
|
+
|
|
117
|
+
Uses the task's env_id (and version if present) to create the env.
|
|
118
|
+
"""
|
|
119
|
+
if not self.env_id:
|
|
120
|
+
raise ValueError("Task has no env_id defined")
|
|
121
|
+
# Deferred import to avoid circular dependencies
|
|
122
|
+
from .client import Fleet
|
|
123
|
+
|
|
124
|
+
return Fleet().make(env_key=self.env_key, region=region)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def load_tasks(
|
|
128
|
+
env_key: Optional[str] = None,
|
|
129
|
+
keys: Optional[List[str]] = None,
|
|
130
|
+
version: Optional[str] = None,
|
|
131
|
+
team_id: Optional[str] = None
|
|
132
|
+
) -> List[Task]:
|
|
133
|
+
"""Convenience function to load tasks with optional filtering.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
env_key: Optional environment key to filter tasks by
|
|
137
|
+
keys: Optional list of task keys to filter by
|
|
138
|
+
version: Optional version to filter tasks by
|
|
139
|
+
team_id: Optional team_id to filter by (admin only)
|
|
140
|
+
|
|
141
|
+
Examples:
|
|
142
|
+
tasks = await fleet.load_tasks(env_key="fira")
|
|
143
|
+
tasks = await fleet.load_tasks(keys=["task1", "task2"])
|
|
144
|
+
tasks = await fleet.load_tasks(env_key="fira", version="v1.0")
|
|
145
|
+
"""
|
|
146
|
+
# Use the global client by default so users can pre-configure it once
|
|
147
|
+
from .global_client import get_client
|
|
148
|
+
|
|
149
|
+
client = get_client()
|
|
150
|
+
return client.load_tasks(
|
|
151
|
+
env_key=env_key,
|
|
152
|
+
keys=keys,
|
|
153
|
+
version=version,
|
|
154
|
+
team_id=team_id
|
|
155
|
+
)
|
|
@@ -162,6 +162,13 @@ class SyncVerifierFunction:
|
|
|
162
162
|
|
|
163
163
|
def remote(self, env: SyncEnv, *args, **kwargs) -> float:
|
|
164
164
|
"""Remote execution of the verifier function with SHA-based bundle caching."""
|
|
165
|
+
# Async verifiers are now supported by the backend
|
|
166
|
+
# if self._is_async:
|
|
167
|
+
# raise NotImplementedError(
|
|
168
|
+
# f"Async verifier '{self.key}' cannot be executed remotely. "
|
|
169
|
+
# "The remote execution environment only supports synchronous functions. "
|
|
170
|
+
# "Please provide a synchronous version of your verifier."
|
|
171
|
+
# )
|
|
165
172
|
|
|
166
173
|
args_array = list(args)
|
|
167
174
|
args_array.append({"env": env.instance_id})
|