fleet-python 0.2.21__py3-none-any.whl → 0.2.22__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.
- fleet/__init__.py +1 -1
- fleet/_async/client.py +40 -8
- fleet/_async/models.py +321 -0
- fleet/_async/resources/sqlite.py +1 -1
- fleet/_async/verifiers/verifier.py +4 -6
- fleet/client.py +64 -542
- fleet/env/client.py +4 -4
- fleet/instance/client.py +4 -15
- fleet/models.py +234 -189
- fleet/resources/browser.py +8 -7
- fleet/resources/sqlite.py +42 -459
- fleet/tasks.py +1 -1
- fleet/verifiers/verifier.py +10 -13
- {fleet_python-0.2.21.dist-info → fleet_python-0.2.22.dist-info}/METADATA +1 -1
- {fleet_python-0.2.21.dist-info → fleet_python-0.2.22.dist-info}/RECORD +18 -17
- {fleet_python-0.2.21.dist-info → fleet_python-0.2.22.dist-info}/WHEEL +0 -0
- {fleet_python-0.2.21.dist-info → fleet_python-0.2.22.dist-info}/licenses/LICENSE +0 -0
- {fleet_python-0.2.21.dist-info → fleet_python-0.2.22.dist-info}/top_level.txt +0 -0
fleet/client.py
CHANGED
|
@@ -19,29 +19,18 @@ import cloudpickle
|
|
|
19
19
|
import httpx
|
|
20
20
|
import logging
|
|
21
21
|
import os
|
|
22
|
-
from typing import List, Optional
|
|
23
|
-
import json
|
|
24
|
-
import time
|
|
25
|
-
from datetime import datetime
|
|
26
|
-
from typing import Dict, List, Any, Tuple
|
|
22
|
+
from typing import List, Optional, Dict
|
|
27
23
|
|
|
28
24
|
from .base import EnvironmentBase, SyncWrapper
|
|
29
25
|
from .models import (
|
|
30
26
|
InstanceRequest,
|
|
31
|
-
|
|
27
|
+
InstanceResponse,
|
|
32
28
|
Environment as EnvironmentModel,
|
|
33
29
|
VerifiersCheckResponse,
|
|
34
|
-
VerificationResponse,
|
|
35
30
|
VerifiersExecuteResponse,
|
|
36
|
-
|
|
37
|
-
ActionLogEntry,
|
|
38
|
-
EnvironmentSnapshot,
|
|
39
|
-
SnapshotValidation,
|
|
40
|
-
ToolLogResponse,
|
|
41
|
-
ToolSessionStartRequest,
|
|
42
|
-
ToolSessionStartResponse,
|
|
43
|
-
ToolLogQueryRequest,
|
|
31
|
+
TaskListResponse,
|
|
44
32
|
)
|
|
33
|
+
from .tasks import Task
|
|
45
34
|
|
|
46
35
|
from .instance import (
|
|
47
36
|
InstanceClient,
|
|
@@ -52,110 +41,28 @@ from .instance import (
|
|
|
52
41
|
from .config import DEFAULT_MAX_RETRIES, DEFAULT_TIMEOUT, REGION_BASE_URL
|
|
53
42
|
from .instance.base import default_httpx_client
|
|
54
43
|
from .instance.client import ValidatorType
|
|
55
|
-
from .instance.models import (
|
|
56
|
-
Resource as ResourceModel,
|
|
57
|
-
CDPDescribeResponse, # Add this
|
|
58
|
-
)
|
|
59
44
|
from .resources.base import Resource
|
|
60
45
|
from .resources.sqlite import SQLiteResource
|
|
61
46
|
from .resources.browser import BrowserResource
|
|
62
|
-
from .resources.mcp import MCPResource
|
|
63
47
|
|
|
64
48
|
logger = logging.getLogger(__name__)
|
|
65
49
|
|
|
66
50
|
|
|
67
|
-
class
|
|
68
|
-
"""Browser resource wrapper that automatically logs all tool usage."""
|
|
69
|
-
|
|
70
|
-
def __init__(
|
|
71
|
-
self,
|
|
72
|
-
resource: ResourceModel,
|
|
73
|
-
client: "SyncWrapper",
|
|
74
|
-
session_id: Optional[str] = None,
|
|
75
|
-
):
|
|
76
|
-
super().__init__(resource, client)
|
|
77
|
-
self._session_id = session_id
|
|
78
|
-
|
|
79
|
-
def _log_tool_action(
|
|
80
|
-
self,
|
|
81
|
-
action: str,
|
|
82
|
-
parameters: Dict[str, Any],
|
|
83
|
-
result: Any = None,
|
|
84
|
-
error: str = None,
|
|
85
|
-
):
|
|
86
|
-
"""Log a tool action to the tool_log table."""
|
|
87
|
-
if not self._session_id:
|
|
88
|
-
return
|
|
89
|
-
|
|
90
|
-
start_time = time.time()
|
|
91
|
-
try:
|
|
92
|
-
self.client.request(
|
|
93
|
-
"POST",
|
|
94
|
-
"/log-tool",
|
|
95
|
-
json={
|
|
96
|
-
"tool_name": "browser",
|
|
97
|
-
"action": action,
|
|
98
|
-
"parameters": parameters,
|
|
99
|
-
"result": result if result else {},
|
|
100
|
-
"success": error is None,
|
|
101
|
-
"error": error,
|
|
102
|
-
"duration_ms": int((time.time() - start_time) * 1000),
|
|
103
|
-
"session_id": self._session_id,
|
|
104
|
-
"user_agent": "fleet-sdk",
|
|
105
|
-
},
|
|
106
|
-
)
|
|
107
|
-
except Exception as e:
|
|
108
|
-
logger.warning(f"Failed to log tool action: {e}")
|
|
109
|
-
|
|
110
|
-
def start(self, width: int = 1920, height: int = 1080) -> CDPDescribeResponse:
|
|
111
|
-
"""Start browser and log the action."""
|
|
112
|
-
parameters = {
|
|
113
|
-
"width": width,
|
|
114
|
-
"height": height,
|
|
115
|
-
"resolution": f"{width}x{height}",
|
|
116
|
-
}
|
|
117
|
-
try:
|
|
118
|
-
result = super().start(width, height)
|
|
119
|
-
self._log_tool_action("start", parameters, {"success": True})
|
|
120
|
-
return result
|
|
121
|
-
except Exception as e:
|
|
122
|
-
self._log_tool_action("start", parameters, error=str(e))
|
|
123
|
-
raise
|
|
124
|
-
|
|
125
|
-
def screenshot(self) -> Dict[str, Any]:
|
|
126
|
-
"""Take screenshot and log the action."""
|
|
127
|
-
parameters = {}
|
|
128
|
-
try:
|
|
129
|
-
# This assumes there's a screenshot method - you may need to implement it
|
|
130
|
-
result = {
|
|
131
|
-
"url": self.devtools_url(),
|
|
132
|
-
"timestamp": datetime.utcnow().isoformat(),
|
|
133
|
-
}
|
|
134
|
-
self._log_tool_action("screenshot", parameters, result)
|
|
135
|
-
return result
|
|
136
|
-
except Exception as e:
|
|
137
|
-
self._log_tool_action("screenshot", parameters, error=str(e))
|
|
138
|
-
raise
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
class Environment(EnvironmentBase):
|
|
51
|
+
class SyncEnv(EnvironmentBase):
|
|
142
52
|
def __init__(self, client: Optional[SyncWrapper], **kwargs):
|
|
143
53
|
super().__init__(**kwargs)
|
|
144
54
|
self._client = client
|
|
145
|
-
self._instance: Optional[InstanceClient] = None
|
|
146
55
|
self._apps: Dict[str, InstanceClient] = {}
|
|
147
|
-
self.
|
|
56
|
+
self._instance: Optional[InstanceClient] = None
|
|
148
57
|
|
|
149
58
|
@property
|
|
150
59
|
def instance(self) -> InstanceClient:
|
|
151
60
|
if self._instance is None:
|
|
152
61
|
self._instance = InstanceClient(
|
|
153
|
-
self.manager_url,
|
|
154
|
-
self.env_key,
|
|
155
|
-
self._client.httpx_client if self._client else None,
|
|
62
|
+
self.manager_url, self._client.httpx_client if self._client else None
|
|
156
63
|
)
|
|
157
64
|
return self._instance
|
|
158
|
-
|
|
65
|
+
|
|
159
66
|
def app(self, name: str) -> InstanceClient:
|
|
160
67
|
if name not in self._apps:
|
|
161
68
|
# Extract base URL by removing the current app path (e.g., /sentry/api/v1/env)
|
|
@@ -169,16 +76,10 @@ class Environment(EnvironmentBase):
|
|
|
169
76
|
|
|
170
77
|
self._apps[name] = InstanceClient(
|
|
171
78
|
f"{base_url}/{name}/api/v1/env",
|
|
172
|
-
self.env_key,
|
|
173
79
|
self._client.httpx_client if self._client else None,
|
|
174
80
|
)
|
|
175
81
|
return self._apps[name]
|
|
176
82
|
|
|
177
|
-
@property
|
|
178
|
-
def session_id(self) -> Optional[str]: # ADD THIS PROPERTY
|
|
179
|
-
"""Get the current tool logging session ID."""
|
|
180
|
-
return self._session_id
|
|
181
|
-
|
|
182
83
|
@property
|
|
183
84
|
def _load_client(self) -> SyncWrapper:
|
|
184
85
|
if self._client is None:
|
|
@@ -193,17 +94,8 @@ class Environment(EnvironmentBase):
|
|
|
193
94
|
def db(self, name: str = "current") -> SQLiteResource:
|
|
194
95
|
return self.instance.db(name)
|
|
195
96
|
|
|
196
|
-
@property
|
|
197
|
-
def mcp(self) -> MCPResource:
|
|
198
|
-
return self.instance.mcp()
|
|
199
|
-
|
|
200
97
|
def browser(self, name: str = "cdp") -> BrowserResource:
|
|
201
|
-
|
|
202
|
-
base_browser = self.instance.browser(name)
|
|
203
|
-
# Wrap it with logging capabilities
|
|
204
|
-
return LoggingBrowserResource(
|
|
205
|
-
base_browser.resource, base_browser.client, self._session_id
|
|
206
|
-
)
|
|
98
|
+
return self.instance.browser(name)
|
|
207
99
|
|
|
208
100
|
def state(self, uri: str) -> Resource:
|
|
209
101
|
return self.instance.state(uri)
|
|
@@ -211,16 +103,7 @@ class Environment(EnvironmentBase):
|
|
|
211
103
|
def resources(self) -> List[Resource]:
|
|
212
104
|
return self.instance.resources()
|
|
213
105
|
|
|
214
|
-
def close(self) ->
|
|
215
|
-
if hasattr(self, "_session_id") and self._session_id:
|
|
216
|
-
try:
|
|
217
|
-
self.instance.client.request(
|
|
218
|
-
"POST", f"/end-tool-session/{self._session_id}"
|
|
219
|
-
)
|
|
220
|
-
logger.info(f"Ended tool logging session: {self._session_id}")
|
|
221
|
-
except Exception as e:
|
|
222
|
-
logger.warning(f"Failed to end tool logging session: {e}")
|
|
223
|
-
|
|
106
|
+
def close(self) -> InstanceResponse:
|
|
224
107
|
return _delete_instance(self._load_client, self.instance_id)
|
|
225
108
|
|
|
226
109
|
def verify(self, validator: ValidatorType) -> ExecuteFunctionResponse:
|
|
@@ -235,26 +118,26 @@ class Environment(EnvironmentBase):
|
|
|
235
118
|
return _check_bundle_exists(self._load_client, bundle_hash)
|
|
236
119
|
|
|
237
120
|
def execute_verifier_remote(
|
|
238
|
-
self,
|
|
239
|
-
bundle_data: bytes,
|
|
121
|
+
self,
|
|
122
|
+
bundle_data: bytes,
|
|
240
123
|
bundle_sha: str,
|
|
241
124
|
key: str,
|
|
242
125
|
function_name: str,
|
|
243
|
-
args: tuple,
|
|
244
|
-
kwargs: dict,
|
|
126
|
+
args: tuple,
|
|
127
|
+
kwargs: dict,
|
|
245
128
|
timeout: Optional[int] = 30,
|
|
246
129
|
needs_upload: bool = True,
|
|
247
130
|
) -> VerifiersExecuteResponse:
|
|
248
131
|
return _execute_verifier_remote(
|
|
249
|
-
self._load_client,
|
|
250
|
-
bundle_data,
|
|
132
|
+
self._load_client,
|
|
133
|
+
bundle_data,
|
|
251
134
|
bundle_sha,
|
|
252
135
|
key,
|
|
253
136
|
function_name,
|
|
254
|
-
args,
|
|
255
|
-
kwargs,
|
|
137
|
+
args,
|
|
138
|
+
kwargs,
|
|
256
139
|
timeout,
|
|
257
|
-
needs_upload
|
|
140
|
+
needs_upload
|
|
258
141
|
)
|
|
259
142
|
|
|
260
143
|
def __getstate__(self):
|
|
@@ -266,120 +149,6 @@ class Environment(EnvironmentBase):
|
|
|
266
149
|
def __setstate__(self, state):
|
|
267
150
|
self.__dict__.update(state)
|
|
268
151
|
|
|
269
|
-
def get_snapshot(
|
|
270
|
-
self, browser_resource: Optional[BrowserResource] = None
|
|
271
|
-
) -> EnvironmentSnapshot:
|
|
272
|
-
"""
|
|
273
|
-
Get a snapshot of the current environment state including action logs and tool logs.
|
|
274
|
-
|
|
275
|
-
Args:
|
|
276
|
-
browser_resource: Optional browser resource to capture current state from
|
|
277
|
-
|
|
278
|
-
Returns:
|
|
279
|
-
EnvironmentSnapshot containing all logs and state information
|
|
280
|
-
"""
|
|
281
|
-
# Get current timestamp
|
|
282
|
-
timestamp = datetime.utcnow().isoformat() + "Z"
|
|
283
|
-
|
|
284
|
-
# Get session ID from current tool logs or generate new one
|
|
285
|
-
session_id = self._get_current_session_id() or f"snapshot-{int(time.time())}"
|
|
286
|
-
|
|
287
|
-
# Query tool logs
|
|
288
|
-
tool_logs_response = self.instance.client.request(
|
|
289
|
-
"POST",
|
|
290
|
-
"/query-tool-logs",
|
|
291
|
-
json={
|
|
292
|
-
"session_id": session_id,
|
|
293
|
-
"limit": None, # Get all logs for this session
|
|
294
|
-
},
|
|
295
|
-
)
|
|
296
|
-
tool_logs_data = tool_logs_response.json()
|
|
297
|
-
tool_logs = [
|
|
298
|
-
ToolLogEntry(**entry) for entry in tool_logs_data.get("entries", [])
|
|
299
|
-
]
|
|
300
|
-
|
|
301
|
-
# Query action logs
|
|
302
|
-
action_logs_query = """
|
|
303
|
-
SELECT id, timestamp, action_type, payload, sql, args, path
|
|
304
|
-
FROM action_log
|
|
305
|
-
ORDER BY timestamp DESC
|
|
306
|
-
LIMIT 10000
|
|
307
|
-
"""
|
|
308
|
-
action_logs_response = self.instance.client.request(
|
|
309
|
-
"POST",
|
|
310
|
-
"/resources/sqlite/action_log/query",
|
|
311
|
-
json={"query": action_logs_query, "read_only": True},
|
|
312
|
-
)
|
|
313
|
-
action_logs_data = action_logs_response.json()
|
|
314
|
-
|
|
315
|
-
action_logs = []
|
|
316
|
-
if action_logs_data.get("success") and action_logs_data.get("rows"):
|
|
317
|
-
columns = action_logs_data["columns"]
|
|
318
|
-
for row in action_logs_data["rows"]:
|
|
319
|
-
entry_dict = dict(zip(columns, row))
|
|
320
|
-
action_logs.append(ActionLogEntry(**entry_dict))
|
|
321
|
-
|
|
322
|
-
# Get current page URL and viewport if browser is available
|
|
323
|
-
page_url = ""
|
|
324
|
-
viewport_size = (1920, 1080) # Default
|
|
325
|
-
|
|
326
|
-
if browser_resource:
|
|
327
|
-
try:
|
|
328
|
-
# Get current page URL via CDP
|
|
329
|
-
cdp_info = browser_resource.describe()
|
|
330
|
-
# You might need to implement a method to get current URL via CDP
|
|
331
|
-
# For now, we'll look for the last navigation in tool logs
|
|
332
|
-
for log in reversed(tool_logs):
|
|
333
|
-
if log.tool_name == "browser" and log.action == "screenshot":
|
|
334
|
-
if log.result and "url" in log.result:
|
|
335
|
-
page_url = log.result["url"]
|
|
336
|
-
break
|
|
337
|
-
|
|
338
|
-
# Get viewport size from last browser start or from logs
|
|
339
|
-
for log in reversed(tool_logs):
|
|
340
|
-
if log.tool_name == "browser" and log.action == "start":
|
|
341
|
-
if log.parameters and "resolution" in log.parameters:
|
|
342
|
-
res = log.parameters["resolution"].split("x")
|
|
343
|
-
viewport_size = (int(res[0]), int(res[1]))
|
|
344
|
-
break
|
|
345
|
-
except Exception as e:
|
|
346
|
-
logger.warning(f"Could not get browser state: {e}")
|
|
347
|
-
|
|
348
|
-
# Create snapshot
|
|
349
|
-
return EnvironmentSnapshot(
|
|
350
|
-
env_key=self.env_key,
|
|
351
|
-
instance_id=self.instance_id,
|
|
352
|
-
timestamp=timestamp,
|
|
353
|
-
session_id=session_id,
|
|
354
|
-
tool_logs=tool_logs,
|
|
355
|
-
action_logs=action_logs,
|
|
356
|
-
page_url=page_url,
|
|
357
|
-
viewport_size=viewport_size,
|
|
358
|
-
metadata={
|
|
359
|
-
"snapshot_version": "1.0",
|
|
360
|
-
"tool_log_count": len(tool_logs),
|
|
361
|
-
"action_log_count": len(action_logs),
|
|
362
|
-
},
|
|
363
|
-
)
|
|
364
|
-
|
|
365
|
-
def _get_current_session_id(self) -> Optional[str]:
|
|
366
|
-
"""Get the current session ID from the environment."""
|
|
367
|
-
# First try to use the stored session ID
|
|
368
|
-
if hasattr(self, "_session_id") and self._session_id:
|
|
369
|
-
return self._session_id
|
|
370
|
-
|
|
371
|
-
# Otherwise, try to get the most recent session ID from tool logs
|
|
372
|
-
try:
|
|
373
|
-
response = self.instance.client.request(
|
|
374
|
-
"POST", "/query-tool-logs", json={"limit": 1}
|
|
375
|
-
)
|
|
376
|
-
data = response.json()
|
|
377
|
-
if data.get("entries") and len(data["entries"]) > 0:
|
|
378
|
-
return data["entries"][0].get("session_id")
|
|
379
|
-
except Exception:
|
|
380
|
-
pass
|
|
381
|
-
return None
|
|
382
|
-
|
|
383
152
|
|
|
384
153
|
class Fleet:
|
|
385
154
|
def __init__(
|
|
@@ -412,11 +181,8 @@ class Fleet:
|
|
|
412
181
|
return EnvironmentModel(**response.json())
|
|
413
182
|
|
|
414
183
|
def make(
|
|
415
|
-
self,
|
|
416
|
-
|
|
417
|
-
region: Optional[str] = None,
|
|
418
|
-
session_id: Optional[str] = None,
|
|
419
|
-
) -> Environment:
|
|
184
|
+
self, env_key: str, region: Optional[str] = None
|
|
185
|
+
) -> SyncEnv:
|
|
420
186
|
if ":" in env_key:
|
|
421
187
|
env_key_part, version = env_key.split(":", 1)
|
|
422
188
|
if not version.startswith("v") and len(version) != 0 and version[0].isdigit():
|
|
@@ -425,7 +191,7 @@ class Fleet:
|
|
|
425
191
|
env_key_part = env_key
|
|
426
192
|
version = None
|
|
427
193
|
|
|
428
|
-
request = InstanceRequest(env_key=env_key_part, version=version, region=region)
|
|
194
|
+
request = InstanceRequest(env_key=env_key_part, version=version, region=region, created_from="sdk")
|
|
429
195
|
region_base_url = REGION_BASE_URL.get(region)
|
|
430
196
|
response = self.client.request(
|
|
431
197
|
"POST",
|
|
@@ -433,38 +199,13 @@ class Fleet:
|
|
|
433
199
|
json=request.model_dump(),
|
|
434
200
|
base_url=region_base_url,
|
|
435
201
|
)
|
|
436
|
-
instance =
|
|
202
|
+
instance = SyncEnv(client=self.client, **response.json())
|
|
437
203
|
instance.instance.load()
|
|
438
|
-
|
|
439
|
-
# Start tool logging session automatically
|
|
440
|
-
if session_id is None:
|
|
441
|
-
session_id = f"env-{instance.instance_id}-{int(time.time())}"
|
|
442
|
-
|
|
443
|
-
try:
|
|
444
|
-
instance.instance.client.request(
|
|
445
|
-
"POST",
|
|
446
|
-
"/start-tool-session",
|
|
447
|
-
json={
|
|
448
|
-
"session_id": session_id,
|
|
449
|
-
"metadata": {
|
|
450
|
-
"env_key": env_key,
|
|
451
|
-
"instance_id": instance.instance_id,
|
|
452
|
-
"region": region or "default",
|
|
453
|
-
"started_at": datetime.utcnow().isoformat() + "Z",
|
|
454
|
-
},
|
|
455
|
-
},
|
|
456
|
-
)
|
|
457
|
-
instance._session_id = session_id
|
|
458
|
-
logger.info(f"Started tool logging session: {session_id}")
|
|
459
|
-
except Exception as e:
|
|
460
|
-
logger.warning(f"Failed to start tool logging session: {e}")
|
|
461
|
-
instance._session_id = None
|
|
462
|
-
|
|
463
204
|
return instance
|
|
464
205
|
|
|
465
206
|
def instances(
|
|
466
207
|
self, status: Optional[str] = None, region: Optional[str] = None
|
|
467
|
-
) -> List[
|
|
208
|
+
) -> List[SyncEnv]:
|
|
468
209
|
params = {}
|
|
469
210
|
if status:
|
|
470
211
|
params["status"] = status
|
|
@@ -473,13 +214,13 @@ class Fleet:
|
|
|
473
214
|
|
|
474
215
|
response = self.client.request("GET", "/v1/env/instances", params=params)
|
|
475
216
|
return [
|
|
476
|
-
|
|
217
|
+
SyncEnv(client=self.client, **instance_data)
|
|
477
218
|
for instance_data in response.json()
|
|
478
219
|
]
|
|
479
220
|
|
|
480
|
-
def instance(self, instance_id: str) ->
|
|
221
|
+
def instance(self, instance_id: str) -> SyncEnv:
|
|
481
222
|
response = self.client.request("GET", f"/v1/env/instances/{instance_id}")
|
|
482
|
-
instance =
|
|
223
|
+
instance = SyncEnv(client=self.client, **response.json())
|
|
483
224
|
instance.instance.load()
|
|
484
225
|
return instance
|
|
485
226
|
|
|
@@ -489,111 +230,49 @@ class Fleet:
|
|
|
489
230
|
def execute_verifier_remote(
|
|
490
231
|
self, bundle_data: bytes, args: tuple, kwargs: dict, timeout: Optional[int] = 30
|
|
491
232
|
) -> VerifiersExecuteResponse:
|
|
492
|
-
return _execute_verifier_remote(
|
|
233
|
+
return _execute_verifier_remote(
|
|
234
|
+
self.client, bundle_data, args, kwargs, timeout
|
|
235
|
+
)
|
|
493
236
|
|
|
494
|
-
def delete(self, instance_id: str) ->
|
|
237
|
+
def delete(self, instance_id: str) -> InstanceResponse:
|
|
495
238
|
return _delete_instance(self.client, instance_id)
|
|
496
239
|
|
|
497
|
-
def
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
validate: bool = True,
|
|
501
|
-
playback_speed: float = 1.0,
|
|
502
|
-
) -> Tuple[Environment, SnapshotValidation]:
|
|
503
|
-
"""
|
|
504
|
-
Resume an environment from a snapshot by recreating the state.
|
|
505
|
-
|
|
240
|
+
def load_tasks(self, env_key: Optional[str] = None) -> List[Task]:
|
|
241
|
+
"""Load tasks for the authenticated team, optionally filtered by environment.
|
|
242
|
+
|
|
506
243
|
Args:
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
playback_speed: Speed multiplier for replaying actions (1.0 = normal speed)
|
|
510
|
-
|
|
244
|
+
env_key: Optional environment key to filter tasks by
|
|
245
|
+
|
|
511
246
|
Returns:
|
|
512
|
-
|
|
247
|
+
List[Task] containing Task objects
|
|
513
248
|
"""
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
# Update tool logs with environment name before replay
|
|
533
|
-
for log in snapshot.tool_logs:
|
|
534
|
-
log_dict = log.dict()
|
|
535
|
-
log_dict["session_id"] = replay_session_id
|
|
536
|
-
log_dict["metadata"] = {"env_name": snapshot.env_key, "replay": True}
|
|
537
|
-
|
|
538
|
-
# Start browser with same viewport
|
|
539
|
-
browser = new_env.browser()
|
|
540
|
-
browser.start(width=snapshot.viewport_size[0], height=snapshot.viewport_size[1])
|
|
541
|
-
|
|
542
|
-
# Replay tool logs in order
|
|
543
|
-
validation_errors = []
|
|
544
|
-
last_timestamp = None
|
|
545
|
-
|
|
546
|
-
for i, tool_log in enumerate(snapshot.tool_logs):
|
|
547
|
-
try:
|
|
548
|
-
# Calculate wait time between actions
|
|
549
|
-
if last_timestamp and playback_speed > 0:
|
|
550
|
-
current_ts = datetime.fromisoformat(tool_log.timestamp.rstrip("Z"))
|
|
551
|
-
last_ts = datetime.fromisoformat(last_timestamp.rstrip("Z"))
|
|
552
|
-
wait_time = (current_ts - last_ts).total_seconds() / playback_speed
|
|
553
|
-
if wait_time > 0:
|
|
554
|
-
time.sleep(min(wait_time, 5)) # Cap at 5 seconds
|
|
555
|
-
|
|
556
|
-
# Replay the tool action
|
|
557
|
-
_replay_tool_action(
|
|
558
|
-
None,
|
|
559
|
-
tool_log,
|
|
560
|
-
new_env.instance._client,
|
|
561
|
-
replay_session_id,
|
|
562
|
-
)
|
|
563
|
-
|
|
564
|
-
last_timestamp = tool_log.timestamp
|
|
565
|
-
|
|
566
|
-
except Exception as e:
|
|
567
|
-
error_msg = f"Failed to replay action {i}: {tool_log.tool_name}.{tool_log.action} - {e}"
|
|
568
|
-
logger.error(error_msg)
|
|
569
|
-
validation_errors.append(error_msg)
|
|
570
|
-
|
|
571
|
-
# End replay session
|
|
572
|
-
new_env.instance.client.request(
|
|
573
|
-
"POST", f"/end-tool-session/{replay_session_id}"
|
|
574
|
-
)
|
|
575
|
-
|
|
576
|
-
# Validate if requested
|
|
577
|
-
validation = SnapshotValidation(
|
|
578
|
-
success=True,
|
|
579
|
-
page_match=True,
|
|
580
|
-
action_log_match=True,
|
|
581
|
-
discrepancies=validation_errors,
|
|
582
|
-
message="Replay completed",
|
|
583
|
-
)
|
|
584
|
-
|
|
585
|
-
if validate:
|
|
586
|
-
validation = _validate_resumed_state(
|
|
587
|
-
new_env, snapshot, None, validation_errors
|
|
249
|
+
params = {}
|
|
250
|
+
if env_key is not None:
|
|
251
|
+
params["env_key"] = env_key
|
|
252
|
+
|
|
253
|
+
response = self.client.request("GET", "/v1/tasks", params=params)
|
|
254
|
+
task_list_response = TaskListResponse(**response.json())
|
|
255
|
+
|
|
256
|
+
# Transform TaskResponse objects to Task objects
|
|
257
|
+
tasks = []
|
|
258
|
+
for task_response in task_list_response.tasks:
|
|
259
|
+
task = Task(
|
|
260
|
+
key=task_response.key,
|
|
261
|
+
prompt=task_response.prompt,
|
|
262
|
+
env_id=task_response.environment_id, # Map environment_id -> env_id
|
|
263
|
+
created_at=task_response.created_at,
|
|
264
|
+
verifier=None, # Keep blank for now as requested
|
|
265
|
+
metadata={} # Default empty metadata
|
|
588
266
|
)
|
|
589
|
-
|
|
590
|
-
|
|
267
|
+
tasks.append(task)
|
|
268
|
+
|
|
269
|
+
return tasks
|
|
591
270
|
|
|
592
271
|
|
|
593
272
|
# Shared
|
|
594
|
-
def _delete_instance(client: SyncWrapper, instance_id: str) ->
|
|
273
|
+
def _delete_instance(client: SyncWrapper, instance_id: str) -> InstanceResponse:
|
|
595
274
|
response = client.request("DELETE", f"/v1/env/instances/{instance_id}")
|
|
596
|
-
return
|
|
275
|
+
return InstanceResponse(**response.json())
|
|
597
276
|
|
|
598
277
|
|
|
599
278
|
def _check_bundle_exists(
|
|
@@ -629,182 +308,25 @@ def _execute_verifier_remote(
|
|
|
629
308
|
"timeout": timeout,
|
|
630
309
|
"region": "us-west-1", # TODO: make configurable
|
|
631
310
|
}
|
|
632
|
-
|
|
311
|
+
|
|
633
312
|
# Add bundle data only if upload is needed
|
|
634
313
|
if needs_upload:
|
|
635
314
|
bundle_b64 = base64.b64encode(bundle_data).decode("utf-8")
|
|
636
315
|
request_data["bundle"] = bundle_b64
|
|
637
|
-
|
|
316
|
+
|
|
638
317
|
# Debug logging
|
|
639
|
-
logger.debug(
|
|
640
|
-
f"Sending verifier execute request: key={key}, sha256={bundle_sha[:8]}..., function_name={function_name}"
|
|
641
|
-
)
|
|
318
|
+
logger.debug(f"Sending verifier execute request: key={key}, sha256={bundle_sha[:8]}..., function_name={function_name}")
|
|
642
319
|
logger.debug(f"Request has bundle: {needs_upload}")
|
|
643
320
|
logger.debug(f"Using client with base_url: {client.base_url}")
|
|
644
321
|
logger.debug(f"Request data keys: {list(request_data.keys())}")
|
|
645
|
-
logger.debug(
|
|
646
|
-
f"Bundle size: {len(request_data.get('bundle', ''))} chars"
|
|
647
|
-
if "bundle" in request_data
|
|
648
|
-
else "No bundle"
|
|
649
|
-
)
|
|
322
|
+
logger.debug(f"Bundle size: {len(request_data.get('bundle', ''))} chars" if 'bundle' in request_data else "No bundle")
|
|
650
323
|
|
|
651
324
|
# Note: This should be called on the instance URL, not the orchestrator
|
|
652
325
|
# The instance has manager URLs for verifier execution
|
|
653
326
|
response = client.request("POST", "/v1/verifiers/execute", json=request_data)
|
|
654
|
-
|
|
327
|
+
|
|
655
328
|
# Debug the response
|
|
656
329
|
response_json = response.json()
|
|
657
330
|
logger.debug(f"Verifier execute response: {response_json}")
|
|
658
331
|
|
|
659
332
|
return VerifiersExecuteResponse(**response_json)
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
def _replay_tool_action(
|
|
663
|
-
playwright_wrapper,
|
|
664
|
-
tool_log: ToolLogEntry,
|
|
665
|
-
client: "SyncWrapper",
|
|
666
|
-
session_id: str,
|
|
667
|
-
) -> None:
|
|
668
|
-
"""Replay a single tool action."""
|
|
669
|
-
start_time = time.time()
|
|
670
|
-
|
|
671
|
-
try:
|
|
672
|
-
if tool_log.tool_name == "browser":
|
|
673
|
-
# Map browser actions to playwright wrapper methods
|
|
674
|
-
action_map = {
|
|
675
|
-
"screenshot": lambda: playwright_wrapper.screenshot(),
|
|
676
|
-
"left_click": lambda: playwright_wrapper.click(
|
|
677
|
-
tool_log.parameters.get("x"), tool_log.parameters.get("y")
|
|
678
|
-
),
|
|
679
|
-
"type": lambda: playwright_wrapper.type(
|
|
680
|
-
tool_log.parameters.get("text", "")
|
|
681
|
-
),
|
|
682
|
-
"key": lambda: playwright_wrapper.key(
|
|
683
|
-
tool_log.parameters.get("text", "")
|
|
684
|
-
),
|
|
685
|
-
"scroll": lambda: playwright_wrapper.scroll(
|
|
686
|
-
direction=tool_log.parameters.get("scroll_direction", "down"),
|
|
687
|
-
amount=tool_log.parameters.get("scroll_amount", 3),
|
|
688
|
-
),
|
|
689
|
-
"mouse_move": lambda: playwright_wrapper.mouse_move(
|
|
690
|
-
tool_log.parameters.get("x"), tool_log.parameters.get("y")
|
|
691
|
-
),
|
|
692
|
-
"wait": lambda: time.sleep(tool_log.parameters.get("duration", 1)),
|
|
693
|
-
# Add more action mappings as needed
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
if tool_log.action in action_map:
|
|
697
|
-
result = action_map[tool_log.action]()
|
|
698
|
-
else:
|
|
699
|
-
logger.warning(f"Unknown browser action: {tool_log.action}")
|
|
700
|
-
result = None
|
|
701
|
-
|
|
702
|
-
elif tool_log.tool_name == "complete_task":
|
|
703
|
-
# Don't replay task completion
|
|
704
|
-
logger.info("Skipping complete_task during replay")
|
|
705
|
-
return
|
|
706
|
-
|
|
707
|
-
elif tool_log.tool_name == "report_issue":
|
|
708
|
-
# Log but don't replay issue reports
|
|
709
|
-
logger.info(f"Previous issue reported: {tool_log.parameters}")
|
|
710
|
-
return
|
|
711
|
-
|
|
712
|
-
elif tool_log.tool_name == "give_up":
|
|
713
|
-
# Don't replay give up
|
|
714
|
-
logger.warning(f"Original session gave up: {tool_log.parameters}")
|
|
715
|
-
return
|
|
716
|
-
|
|
717
|
-
# Log the replayed action
|
|
718
|
-
duration_ms = int((time.time() - start_time) * 1000)
|
|
719
|
-
|
|
720
|
-
client.request(
|
|
721
|
-
"POST",
|
|
722
|
-
"/log-tool",
|
|
723
|
-
json={
|
|
724
|
-
"tool_name": tool_log.tool_name,
|
|
725
|
-
"action": tool_log.action,
|
|
726
|
-
"parameters": tool_log.parameters,
|
|
727
|
-
"result": result if result else tool_log.result,
|
|
728
|
-
"success": True,
|
|
729
|
-
"duration_ms": duration_ms,
|
|
730
|
-
"session_id": session_id,
|
|
731
|
-
"user_agent": "snapshot_replay",
|
|
732
|
-
},
|
|
733
|
-
)
|
|
734
|
-
|
|
735
|
-
except Exception as e:
|
|
736
|
-
# Log failure
|
|
737
|
-
duration_ms = int((time.time() - start_time) * 1000)
|
|
738
|
-
|
|
739
|
-
client.request(
|
|
740
|
-
"POST",
|
|
741
|
-
"/log-tool",
|
|
742
|
-
json={
|
|
743
|
-
"tool_name": tool_log.tool_name,
|
|
744
|
-
"action": tool_log.action,
|
|
745
|
-
"parameters": tool_log.parameters,
|
|
746
|
-
"success": False,
|
|
747
|
-
"error": str(e),
|
|
748
|
-
"duration_ms": duration_ms,
|
|
749
|
-
"session_id": session_id,
|
|
750
|
-
"user_agent": "snapshot_replay",
|
|
751
|
-
},
|
|
752
|
-
)
|
|
753
|
-
raise
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
def _validate_resumed_state(
|
|
757
|
-
new_env: Environment,
|
|
758
|
-
snapshot: EnvironmentSnapshot,
|
|
759
|
-
playwright_wrapper,
|
|
760
|
-
existing_errors: List[str],
|
|
761
|
-
) -> SnapshotValidation:
|
|
762
|
-
"""Validate that the resumed state matches the snapshot."""
|
|
763
|
-
discrepancies = existing_errors.copy()
|
|
764
|
-
|
|
765
|
-
# Check current page URL
|
|
766
|
-
page_match = True
|
|
767
|
-
try:
|
|
768
|
-
current_screenshot = playwright_wrapper.screenshot()
|
|
769
|
-
current_url = current_screenshot.get("url", "")
|
|
770
|
-
|
|
771
|
-
if current_url != snapshot.page_url:
|
|
772
|
-
page_match = False
|
|
773
|
-
discrepancies.append(
|
|
774
|
-
f"Page URL mismatch: expected '{snapshot.page_url}', got '{current_url}'"
|
|
775
|
-
)
|
|
776
|
-
except Exception as e:
|
|
777
|
-
page_match = False
|
|
778
|
-
discrepancies.append(f"Could not verify page URL: {e}")
|
|
779
|
-
|
|
780
|
-
# Compare action logs
|
|
781
|
-
action_log_match = True
|
|
782
|
-
try:
|
|
783
|
-
# Get new action logs
|
|
784
|
-
new_snapshot = new_env.get_snapshot()
|
|
785
|
-
|
|
786
|
-
# Compare counts
|
|
787
|
-
if len(new_snapshot.action_logs) != len(snapshot.action_logs):
|
|
788
|
-
action_log_match = False
|
|
789
|
-
discrepancies.append(
|
|
790
|
-
f"Action log count mismatch: expected {len(snapshot.action_logs)}, "
|
|
791
|
-
f"got {len(new_snapshot.action_logs)}"
|
|
792
|
-
)
|
|
793
|
-
|
|
794
|
-
# Could do more detailed comparison here if needed
|
|
795
|
-
|
|
796
|
-
except Exception as e:
|
|
797
|
-
action_log_match = False
|
|
798
|
-
discrepancies.append(f"Could not verify action logs: {e}")
|
|
799
|
-
|
|
800
|
-
success = page_match and action_log_match and len(discrepancies) == 0
|
|
801
|
-
|
|
802
|
-
return SnapshotValidation(
|
|
803
|
-
success=success,
|
|
804
|
-
page_match=page_match,
|
|
805
|
-
action_log_match=action_log_match,
|
|
806
|
-
discrepancies=discrepancies,
|
|
807
|
-
message="Validation completed"
|
|
808
|
-
if success
|
|
809
|
-
else "Validation failed with discrepancies",
|
|
810
|
-
)
|