fleet-python 0.2.20__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/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
- InstanceRecord,
27
+ InstanceResponse,
32
28
  Environment as EnvironmentModel,
33
29
  VerifiersCheckResponse,
34
- VerificationResponse,
35
30
  VerifiersExecuteResponse,
36
- ToolLogEntry,
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 LoggingBrowserResource(BrowserResource):
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._session_id: Optional[str] = None # ADD THIS
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
- """Get browser resource with automatic logging."""
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,23 +103,14 @@ class Environment(EnvironmentBase):
211
103
  def resources(self) -> List[Resource]:
212
104
  return self.instance.resources()
213
105
 
214
- def close(self) -> InstanceRecord:
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:
227
110
  return self.instance.verify(validator)
228
111
 
229
112
  def verify_raw(
230
- self, function_code: str, function_name: str
113
+ self, function_code: str, function_name: str | None = None
231
114
  ) -> ExecuteFunctionResponse:
232
115
  return self.instance.verify_raw(function_code, function_name)
233
116
 
@@ -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,130 +149,18 @@ 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__(
386
155
  self,
387
- api_key: Optional[str] = os.getenv("FLEET_API_KEY"),
156
+ api_key: Optional[str] = None,
388
157
  base_url: Optional[str] = None,
389
158
  httpx_client: Optional[httpx.Client] = None,
390
159
  max_retries: int = DEFAULT_MAX_RETRIES,
391
160
  timeout: float = DEFAULT_TIMEOUT,
392
161
  ):
162
+ if api_key is None:
163
+ api_key = os.getenv("FLEET_API_KEY")
393
164
  self._httpx_client = httpx_client or default_httpx_client(max_retries, timeout)
394
165
  self.client = SyncWrapper(
395
166
  api_key=api_key,
@@ -410,20 +181,17 @@ class Fleet:
410
181
  return EnvironmentModel(**response.json())
411
182
 
412
183
  def make(
413
- self,
414
- env_key: str,
415
- region: Optional[str] = None,
416
- session_id: Optional[str] = None,
417
- ) -> Environment:
184
+ self, env_key: str, region: Optional[str] = None
185
+ ) -> SyncEnv:
418
186
  if ":" in env_key:
419
187
  env_key_part, version = env_key.split(":", 1)
420
- if not version.startswith("v"):
188
+ if not version.startswith("v") and len(version) != 0 and version[0].isdigit():
421
189
  version = f"v{version}"
422
190
  else:
423
191
  env_key_part = env_key
424
192
  version = None
425
193
 
426
- 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")
427
195
  region_base_url = REGION_BASE_URL.get(region)
428
196
  response = self.client.request(
429
197
  "POST",
@@ -431,38 +199,13 @@ class Fleet:
431
199
  json=request.model_dump(),
432
200
  base_url=region_base_url,
433
201
  )
434
- instance = Environment(client=self.client, **response.json())
202
+ instance = SyncEnv(client=self.client, **response.json())
435
203
  instance.instance.load()
436
-
437
- # Start tool logging session automatically
438
- if session_id is None:
439
- session_id = f"env-{instance.instance_id}-{int(time.time())}"
440
-
441
- try:
442
- instance.instance.client.request(
443
- "POST",
444
- "/start-tool-session",
445
- json={
446
- "session_id": session_id,
447
- "metadata": {
448
- "env_key": env_key,
449
- "instance_id": instance.instance_id,
450
- "region": region or "default",
451
- "started_at": datetime.utcnow().isoformat() + "Z",
452
- },
453
- },
454
- )
455
- instance._session_id = session_id
456
- logger.info(f"Started tool logging session: {session_id}")
457
- except Exception as e:
458
- logger.warning(f"Failed to start tool logging session: {e}")
459
- instance._session_id = None
460
-
461
204
  return instance
462
205
 
463
206
  def instances(
464
207
  self, status: Optional[str] = None, region: Optional[str] = None
465
- ) -> List[Environment]:
208
+ ) -> List[SyncEnv]:
466
209
  params = {}
467
210
  if status:
468
211
  params["status"] = status
@@ -471,13 +214,13 @@ class Fleet:
471
214
 
472
215
  response = self.client.request("GET", "/v1/env/instances", params=params)
473
216
  return [
474
- Environment(client=self.client, **instance_data)
217
+ SyncEnv(client=self.client, **instance_data)
475
218
  for instance_data in response.json()
476
219
  ]
477
220
 
478
- def instance(self, instance_id: str) -> Environment:
221
+ def instance(self, instance_id: str) -> SyncEnv:
479
222
  response = self.client.request("GET", f"/v1/env/instances/{instance_id}")
480
- instance = Environment(client=self.client, **response.json())
223
+ instance = SyncEnv(client=self.client, **response.json())
481
224
  instance.instance.load()
482
225
  return instance
483
226
 
@@ -487,111 +230,49 @@ class Fleet:
487
230
  def execute_verifier_remote(
488
231
  self, bundle_data: bytes, args: tuple, kwargs: dict, timeout: Optional[int] = 30
489
232
  ) -> VerifiersExecuteResponse:
490
- return _execute_verifier_remote(self.client, bundle_data, args, kwargs, timeout)
233
+ return _execute_verifier_remote(
234
+ self.client, bundle_data, args, kwargs, timeout
235
+ )
491
236
 
492
- def delete(self, instance_id: str) -> InstanceRecord:
237
+ def delete(self, instance_id: str) -> InstanceResponse:
493
238
  return _delete_instance(self.client, instance_id)
494
239
 
495
- def resume(
496
- self,
497
- snapshot: EnvironmentSnapshot,
498
- validate: bool = True,
499
- playback_speed: float = 1.0,
500
- ) -> Tuple[Environment, SnapshotValidation]:
501
- """
502
- Resume an environment from a snapshot by recreating the state.
503
-
240
+ def load_tasks(self, env_key: Optional[str] = None) -> List[Task]:
241
+ """Load tasks for the authenticated team, optionally filtered by environment.
242
+
504
243
  Args:
505
- snapshot: EnvironmentSnapshot to resume from
506
- validate: Whether to validate the resumed state matches the snapshot
507
- playback_speed: Speed multiplier for replaying actions (1.0 = normal speed)
508
-
244
+ env_key: Optional environment key to filter tasks by
245
+
509
246
  Returns:
510
- Tuple of (new Environment instance, validation results)
247
+ List[Task] containing Task objects
511
248
  """
512
- # Create new environment instance
513
- new_env = self.make(snapshot.env_key)
514
-
515
- # Start a new tool session for tracking
516
- replay_session_id = f"replay-{snapshot.session_id}-{int(time.time())}"
517
- new_env.instance.client.request(
518
- "POST",
519
- "/start-tool-session",
520
- json={
521
- "session_id": replay_session_id,
522
- "metadata": {
523
- "type": "snapshot_replay",
524
- "original_session_id": snapshot.session_id,
525
- "snapshot_timestamp": snapshot.timestamp,
526
- },
527
- },
528
- )
529
-
530
- # Update tool logs with environment name before replay
531
- for log in snapshot.tool_logs:
532
- log_dict = log.dict()
533
- log_dict["session_id"] = replay_session_id
534
- log_dict["metadata"] = {"env_name": snapshot.env_key, "replay": True}
535
-
536
- # Start browser with same viewport
537
- browser = new_env.browser()
538
- browser.start(width=snapshot.viewport_size[0], height=snapshot.viewport_size[1])
539
-
540
- # Replay tool logs in order
541
- validation_errors = []
542
- last_timestamp = None
543
-
544
- for i, tool_log in enumerate(snapshot.tool_logs):
545
- try:
546
- # Calculate wait time between actions
547
- if last_timestamp and playback_speed > 0:
548
- current_ts = datetime.fromisoformat(tool_log.timestamp.rstrip("Z"))
549
- last_ts = datetime.fromisoformat(last_timestamp.rstrip("Z"))
550
- wait_time = (current_ts - last_ts).total_seconds() / playback_speed
551
- if wait_time > 0:
552
- time.sleep(min(wait_time, 5)) # Cap at 5 seconds
553
-
554
- # Replay the tool action
555
- _replay_tool_action(
556
- None,
557
- tool_log,
558
- new_env.instance._client,
559
- replay_session_id,
560
- )
561
-
562
- last_timestamp = tool_log.timestamp
563
-
564
- except Exception as e:
565
- error_msg = f"Failed to replay action {i}: {tool_log.tool_name}.{tool_log.action} - {e}"
566
- logger.error(error_msg)
567
- validation_errors.append(error_msg)
568
-
569
- # End replay session
570
- new_env.instance.client.request(
571
- "POST", f"/end-tool-session/{replay_session_id}"
572
- )
573
-
574
- # Validate if requested
575
- validation = SnapshotValidation(
576
- success=True,
577
- page_match=True,
578
- action_log_match=True,
579
- discrepancies=validation_errors,
580
- message="Replay completed",
581
- )
582
-
583
- if validate:
584
- validation = _validate_resumed_state(
585
- 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
586
266
  )
587
-
588
- return new_env, validation
267
+ tasks.append(task)
268
+
269
+ return tasks
589
270
 
590
271
 
591
272
  # Shared
592
- def _delete_instance(client: SyncWrapper, instance_id: str) -> InstanceRecord:
273
+ def _delete_instance(client: SyncWrapper, instance_id: str) -> InstanceResponse:
593
274
  response = client.request("DELETE", f"/v1/env/instances/{instance_id}")
594
- return InstanceRecord(**response.json())
275
+ return InstanceResponse(**response.json())
595
276
 
596
277
 
597
278
  def _check_bundle_exists(
@@ -627,182 +308,25 @@ def _execute_verifier_remote(
627
308
  "timeout": timeout,
628
309
  "region": "us-west-1", # TODO: make configurable
629
310
  }
630
-
311
+
631
312
  # Add bundle data only if upload is needed
632
313
  if needs_upload:
633
314
  bundle_b64 = base64.b64encode(bundle_data).decode("utf-8")
634
315
  request_data["bundle"] = bundle_b64
635
-
316
+
636
317
  # Debug logging
637
- logger.debug(
638
- f"Sending verifier execute request: key={key}, sha256={bundle_sha[:8]}..., function_name={function_name}"
639
- )
318
+ logger.debug(f"Sending verifier execute request: key={key}, sha256={bundle_sha[:8]}..., function_name={function_name}")
640
319
  logger.debug(f"Request has bundle: {needs_upload}")
641
320
  logger.debug(f"Using client with base_url: {client.base_url}")
642
321
  logger.debug(f"Request data keys: {list(request_data.keys())}")
643
- logger.debug(
644
- f"Bundle size: {len(request_data.get('bundle', ''))} chars"
645
- if "bundle" in request_data
646
- else "No bundle"
647
- )
322
+ logger.debug(f"Bundle size: {len(request_data.get('bundle', ''))} chars" if 'bundle' in request_data else "No bundle")
648
323
 
649
324
  # Note: This should be called on the instance URL, not the orchestrator
650
325
  # The instance has manager URLs for verifier execution
651
326
  response = client.request("POST", "/v1/verifiers/execute", json=request_data)
652
-
327
+
653
328
  # Debug the response
654
329
  response_json = response.json()
655
330
  logger.debug(f"Verifier execute response: {response_json}")
656
331
 
657
332
  return VerifiersExecuteResponse(**response_json)
658
-
659
-
660
- def _replay_tool_action(
661
- playwright_wrapper,
662
- tool_log: ToolLogEntry,
663
- client: "SyncWrapper",
664
- session_id: str,
665
- ) -> None:
666
- """Replay a single tool action."""
667
- start_time = time.time()
668
-
669
- try:
670
- if tool_log.tool_name == "browser":
671
- # Map browser actions to playwright wrapper methods
672
- action_map = {
673
- "screenshot": lambda: playwright_wrapper.screenshot(),
674
- "left_click": lambda: playwright_wrapper.click(
675
- tool_log.parameters.get("x"), tool_log.parameters.get("y")
676
- ),
677
- "type": lambda: playwright_wrapper.type(
678
- tool_log.parameters.get("text", "")
679
- ),
680
- "key": lambda: playwright_wrapper.key(
681
- tool_log.parameters.get("text", "")
682
- ),
683
- "scroll": lambda: playwright_wrapper.scroll(
684
- direction=tool_log.parameters.get("scroll_direction", "down"),
685
- amount=tool_log.parameters.get("scroll_amount", 3),
686
- ),
687
- "mouse_move": lambda: playwright_wrapper.mouse_move(
688
- tool_log.parameters.get("x"), tool_log.parameters.get("y")
689
- ),
690
- "wait": lambda: time.sleep(tool_log.parameters.get("duration", 1)),
691
- # Add more action mappings as needed
692
- }
693
-
694
- if tool_log.action in action_map:
695
- result = action_map[tool_log.action]()
696
- else:
697
- logger.warning(f"Unknown browser action: {tool_log.action}")
698
- result = None
699
-
700
- elif tool_log.tool_name == "complete_task":
701
- # Don't replay task completion
702
- logger.info("Skipping complete_task during replay")
703
- return
704
-
705
- elif tool_log.tool_name == "report_issue":
706
- # Log but don't replay issue reports
707
- logger.info(f"Previous issue reported: {tool_log.parameters}")
708
- return
709
-
710
- elif tool_log.tool_name == "give_up":
711
- # Don't replay give up
712
- logger.warning(f"Original session gave up: {tool_log.parameters}")
713
- return
714
-
715
- # Log the replayed action
716
- duration_ms = int((time.time() - start_time) * 1000)
717
-
718
- client.request(
719
- "POST",
720
- "/log-tool",
721
- json={
722
- "tool_name": tool_log.tool_name,
723
- "action": tool_log.action,
724
- "parameters": tool_log.parameters,
725
- "result": result if result else tool_log.result,
726
- "success": True,
727
- "duration_ms": duration_ms,
728
- "session_id": session_id,
729
- "user_agent": "snapshot_replay",
730
- },
731
- )
732
-
733
- except Exception as e:
734
- # Log failure
735
- duration_ms = int((time.time() - start_time) * 1000)
736
-
737
- client.request(
738
- "POST",
739
- "/log-tool",
740
- json={
741
- "tool_name": tool_log.tool_name,
742
- "action": tool_log.action,
743
- "parameters": tool_log.parameters,
744
- "success": False,
745
- "error": str(e),
746
- "duration_ms": duration_ms,
747
- "session_id": session_id,
748
- "user_agent": "snapshot_replay",
749
- },
750
- )
751
- raise
752
-
753
-
754
- def _validate_resumed_state(
755
- new_env: Environment,
756
- snapshot: EnvironmentSnapshot,
757
- playwright_wrapper,
758
- existing_errors: List[str],
759
- ) -> SnapshotValidation:
760
- """Validate that the resumed state matches the snapshot."""
761
- discrepancies = existing_errors.copy()
762
-
763
- # Check current page URL
764
- page_match = True
765
- try:
766
- current_screenshot = playwright_wrapper.screenshot()
767
- current_url = current_screenshot.get("url", "")
768
-
769
- if current_url != snapshot.page_url:
770
- page_match = False
771
- discrepancies.append(
772
- f"Page URL mismatch: expected '{snapshot.page_url}', got '{current_url}'"
773
- )
774
- except Exception as e:
775
- page_match = False
776
- discrepancies.append(f"Could not verify page URL: {e}")
777
-
778
- # Compare action logs
779
- action_log_match = True
780
- try:
781
- # Get new action logs
782
- new_snapshot = new_env.get_snapshot()
783
-
784
- # Compare counts
785
- if len(new_snapshot.action_logs) != len(snapshot.action_logs):
786
- action_log_match = False
787
- discrepancies.append(
788
- f"Action log count mismatch: expected {len(snapshot.action_logs)}, "
789
- f"got {len(new_snapshot.action_logs)}"
790
- )
791
-
792
- # Could do more detailed comparison here if needed
793
-
794
- except Exception as e:
795
- action_log_match = False
796
- discrepancies.append(f"Could not verify action logs: {e}")
797
-
798
- success = page_match and action_log_match and len(discrepancies) == 0
799
-
800
- return SnapshotValidation(
801
- success=success,
802
- page_match=page_match,
803
- action_log_match=action_log_match,
804
- discrepancies=discrepancies,
805
- message="Validation completed"
806
- if success
807
- else "Validation failed with discrepancies",
808
- )