fleet-python 0.2.17__py3-none-any.whl → 0.2.19__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 CHANGED
@@ -37,9 +37,6 @@ from .verifiers import (
37
37
  TASK_SUCCESSFUL_SCORE,
38
38
  )
39
39
 
40
- # Import Playwright wrapper
41
- from .playwright import FleetPlaywrightWrapper
42
-
43
40
  # Import async verifiers (default verifier is async for modern usage)
44
41
  from ._async.verifiers import (
45
42
  verifier,
@@ -75,8 +72,6 @@ __all__ = [
75
72
  "FleetAPIError",
76
73
  "FleetTimeoutError",
77
74
  "FleetConfigurationError",
78
- # Playwright wrapper
79
- "FleetPlaywrightWrapper",
80
75
  # Verifiers (async is default)
81
76
  "verifier",
82
77
  "verifier_sync",
fleet/client.py CHANGED
@@ -537,12 +537,6 @@ class Fleet:
537
537
  browser = new_env.browser()
538
538
  browser.start(width=snapshot.viewport_size[0], height=snapshot.viewport_size[1])
539
539
 
540
- from fleet.playwright import FleetPlaywrightWrapper
541
-
542
- playwright_wrapper = FleetPlaywrightWrapper(
543
- cdp_url=browser.cdp_url(), instance_client=new_env.instance
544
- )
545
-
546
540
  # Replay tool logs in order
547
541
  validation_errors = []
548
542
  last_timestamp = None
@@ -559,7 +553,7 @@ class Fleet:
559
553
 
560
554
  # Replay the tool action
561
555
  _replay_tool_action(
562
- playwright_wrapper,
556
+ None,
563
557
  tool_log,
564
558
  new_env.instance._client,
565
559
  replay_session_id,
@@ -588,7 +582,7 @@ class Fleet:
588
582
 
589
583
  if validate:
590
584
  validation = _validate_resumed_state(
591
- new_env, snapshot, playwright_wrapper, validation_errors
585
+ new_env, snapshot, None, validation_errors
592
586
  )
593
587
 
594
588
  return new_env, validation
@@ -664,7 +658,7 @@ def _execute_verifier_remote(
664
658
 
665
659
 
666
660
  def _replay_tool_action(
667
- playwright_wrapper: "FleetPlaywrightWrapper",
661
+ playwright_wrapper,
668
662
  tool_log: ToolLogEntry,
669
663
  client: "SyncWrapper",
670
664
  session_id: str,
@@ -760,7 +754,7 @@ def _replay_tool_action(
760
754
  def _validate_resumed_state(
761
755
  new_env: Environment,
762
756
  snapshot: EnvironmentSnapshot,
763
- playwright_wrapper: "FleetPlaywrightWrapper",
757
+ playwright_wrapper,
764
758
  existing_errors: List[str],
765
759
  ) -> SnapshotValidation:
766
760
  """Validate that the resumed state matches the snapshot."""
fleet/resources/mcp.py CHANGED
@@ -1,5 +1,4 @@
1
1
  from typing import Dict
2
- import aiohttp
3
2
  import json
4
3
 
5
4
 
@@ -24,6 +23,7 @@ class MCPResource:
24
23
  }
25
24
 
26
25
  async def list_tools(self):
26
+ import aiohttp
27
27
  """
28
28
  Make an async request to list available tools from the MCP endpoint.
29
29
 
fleet/resources/sqlite.py CHANGED
@@ -18,7 +18,7 @@ from ..verifiers.db import IgnoreConfig, _get_row_identifier, _format_row_for_er
18
18
 
19
19
 
20
20
  class SyncDatabaseSnapshot:
21
- """Async database snapshot that fetches data through API and stores locally for diffing."""
21
+ """Lazy database snapshot that fetches data on-demand through API."""
22
22
 
23
23
  def __init__(self, resource: "SQLiteResource", name: str | None = None):
24
24
  self.resource = resource
@@ -26,11 +26,12 @@ class SyncDatabaseSnapshot:
26
26
  self.created_at = datetime.utcnow()
27
27
  self._data: dict[str, list[dict[str, Any]]] = {}
28
28
  self._schemas: dict[str, list[str]] = {}
29
- self._fetched = False
29
+ self._table_names: list[str] | None = None
30
+ self._fetched_tables: set[str] = set()
30
31
 
31
- def _ensure_fetched(self):
32
- """Fetch all data from remote database if not already fetched."""
33
- if self._fetched:
32
+ def _ensure_tables_list(self):
33
+ """Fetch just the list of table names if not already fetched."""
34
+ if self._table_names is not None:
34
35
  return
35
36
 
36
37
  # Get all tables
@@ -39,34 +40,37 @@ class SyncDatabaseSnapshot:
39
40
  )
40
41
 
41
42
  if not tables_response.rows:
42
- self._fetched = True
43
+ self._table_names = []
43
44
  return
44
45
 
45
- table_names = [row[0] for row in tables_response.rows]
46
-
47
- # Fetch data from each table
48
- for table in table_names:
49
- # Get table schema
50
- schema_response = self.resource.query(f"PRAGMA table_info({table})")
51
- if schema_response.rows:
52
- self._schemas[table] = [row[1] for row in schema_response.rows] # Column names
53
-
54
- # Get all data
55
- data_response = self.resource.query(f"SELECT * FROM {table}")
56
- if data_response.rows and data_response.columns:
57
- self._data[table] = [
58
- dict(zip(data_response.columns, row))
59
- for row in data_response.rows
60
- ]
61
- else:
62
- self._data[table] = []
63
-
64
- self._fetched = True
46
+ self._table_names = [row[0] for row in tables_response.rows]
47
+
48
+ def _ensure_table_data(self, table: str):
49
+ """Fetch data for a specific table on demand."""
50
+ if table in self._fetched_tables:
51
+ return
52
+
53
+ # Get table schema
54
+ schema_response = self.resource.query(f"PRAGMA table_info({table})")
55
+ if schema_response.rows:
56
+ self._schemas[table] = [row[1] for row in schema_response.rows] # Column names
57
+
58
+ # Get all data for this table
59
+ data_response = self.resource.query(f"SELECT * FROM {table}")
60
+ if data_response.rows and data_response.columns:
61
+ self._data[table] = [
62
+ dict(zip(data_response.columns, row))
63
+ for row in data_response.rows
64
+ ]
65
+ else:
66
+ self._data[table] = []
67
+
68
+ self._fetched_tables.add(table)
65
69
 
66
70
  def tables(self) -> list[str]:
67
71
  """Get list of all tables in the snapshot."""
68
- self._ensure_fetched()
69
- return list(self._data.keys())
72
+ self._ensure_tables_list()
73
+ return list(self._table_names) if self._table_names else []
70
74
 
71
75
  def table(self, table_name: str) -> "SyncSnapshotQueryBuilder":
72
76
  """Create a query builder for snapshot data."""
@@ -78,13 +82,12 @@ class SyncDatabaseSnapshot:
78
82
  ignore_config: IgnoreConfig | None = None,
79
83
  ) -> "SyncSnapshotDiff":
80
84
  """Compare this snapshot with another."""
81
- self._ensure_fetched()
82
- other._ensure_fetched()
85
+ # No need to fetch all data upfront - diff will fetch on demand
83
86
  return SyncSnapshotDiff(self, other, ignore_config)
84
87
 
85
88
 
86
89
  class SyncSnapshotQueryBuilder:
87
- """Query builder that works on local snapshot data."""
90
+ """Query builder that works on snapshot data - can use targeted queries when possible."""
88
91
 
89
92
  def __init__(self, snapshot: SyncDatabaseSnapshot, table: str):
90
93
  self._snapshot = snapshot
@@ -94,10 +97,63 @@ class SyncSnapshotQueryBuilder:
94
97
  self._limit: int | None = None
95
98
  self._order_by: str | None = None
96
99
  self._order_desc: bool = False
100
+ self._use_targeted_query = True # Try to use targeted queries when possible
101
+
102
+ def _can_use_targeted_query(self) -> bool:
103
+ """Check if we can use a targeted query instead of loading all data."""
104
+ # We can use targeted query if:
105
+ # 1. We have simple equality conditions
106
+ # 2. No complex operations like joins
107
+ # 3. The query is selective (has conditions)
108
+ if not self._conditions:
109
+ return False
110
+ for col, op, val in self._conditions:
111
+ if op not in ["=", "IS", "IS NOT"]:
112
+ return False
113
+ return True
114
+
115
+ def _execute_targeted_query(self) -> list[dict[str, Any]]:
116
+ """Execute a targeted query directly instead of loading all data."""
117
+ # Build WHERE clause
118
+ where_parts = []
119
+ for col, op, val in self._conditions:
120
+ if op == "=" and val is None:
121
+ where_parts.append(f"{col} IS NULL")
122
+ elif op == "IS":
123
+ where_parts.append(f"{col} IS NULL")
124
+ elif op == "IS NOT":
125
+ where_parts.append(f"{col} IS NOT NULL")
126
+ elif op == "=":
127
+ if isinstance(val, str):
128
+ escaped_val = val.replace("'", "''")
129
+ where_parts.append(f"{col} = '{escaped_val}'")
130
+ else:
131
+ where_parts.append(f"{col} = '{val}'")
132
+
133
+ where_clause = " AND ".join(where_parts)
134
+
135
+ # Build full query
136
+ cols = ", ".join(self._select_cols)
137
+ query = f"SELECT {cols} FROM {self._table} WHERE {where_clause}"
138
+
139
+ if self._order_by:
140
+ query += f" ORDER BY {self._order_by}"
141
+ if self._limit is not None:
142
+ query += f" LIMIT {self._limit}"
143
+
144
+ # Execute query
145
+ response = self._snapshot.resource.query(query)
146
+ if response.rows and response.columns:
147
+ return [dict(zip(response.columns, row)) for row in response.rows]
148
+ return []
97
149
 
98
150
  def _get_data(self) -> list[dict[str, Any]]:
99
- """Get table data from snapshot."""
100
- self._snapshot._ensure_fetched()
151
+ """Get table data - use targeted query if possible, otherwise load all data."""
152
+ if self._use_targeted_query and self._can_use_targeted_query():
153
+ return self._execute_targeted_query()
154
+
155
+ # Fall back to loading all data
156
+ self._snapshot._ensure_table_data(self._table)
101
157
  return self._snapshot._data.get(self._table, [])
102
158
 
103
159
  def eq(self, column: str, value: Any) -> "SyncSnapshotQueryBuilder":
@@ -121,6 +177,11 @@ class SyncSnapshotQueryBuilder:
121
177
  return rows[0] if rows else None
122
178
 
123
179
  def all(self) -> list[dict[str, Any]]:
180
+ # If we can use targeted query, _get_data already applies filters
181
+ if self._use_targeted_query and self._can_use_targeted_query():
182
+ return self._get_data()
183
+
184
+ # Otherwise, get all data and apply filters manually
124
185
  data = self._get_data()
125
186
 
126
187
  # Apply filters
@@ -188,6 +249,7 @@ class SyncSnapshotDiff:
188
249
  self.after = after
189
250
  self.ignore_config = ignore_config or IgnoreConfig()
190
251
  self._cached: dict[str, Any] | None = None
252
+ self._targeted_mode = False # Flag to use targeted queries
191
253
 
192
254
  def _get_primary_key_columns(self, table: str) -> list[str]:
193
255
  """Get primary key columns for a table."""
@@ -228,6 +290,10 @@ class SyncSnapshotDiff:
228
290
  # Get primary key columns
229
291
  pk_columns = self._get_primary_key_columns(tbl)
230
292
 
293
+ # Ensure data is fetched for this table
294
+ self.before._ensure_table_data(tbl)
295
+ self.after._ensure_table_data(tbl)
296
+
231
297
  # Get data from both snapshots
232
298
  before_data = self.before._data.get(tbl, [])
233
299
  after_data = self.after._data.get(tbl, [])
@@ -303,10 +369,245 @@ class SyncSnapshotDiff:
303
369
  self._cached = diff
304
370
  return diff
305
371
 
306
- def expect_only(self, allowed_changes: list[dict[str, Any]]):
307
- """Ensure only specified changes occurred."""
372
+ def _can_use_targeted_queries(self, allowed_changes: list[dict[str, Any]]) -> bool:
373
+ """Check if we can use targeted queries for optimization."""
374
+ # We can use targeted queries if all allowed changes specify table and pk
375
+ for change in allowed_changes:
376
+ if "table" not in change or "pk" not in change:
377
+ return False
378
+ return True
379
+
380
+ def _expect_only_targeted(self, allowed_changes: list[dict[str, Any]]):
381
+ """Optimized version that only queries specific rows mentioned in allowed_changes."""
382
+ # Group allowed changes by table
383
+ changes_by_table: dict[str, list[dict[str, Any]]] = {}
384
+ for change in allowed_changes:
385
+ table = change["table"]
386
+ if table not in changes_by_table:
387
+ changes_by_table[table] = []
388
+ changes_by_table[table].append(change)
389
+
390
+ # For each table with allowed changes, query only those specific rows
391
+ for table, table_changes in changes_by_table.items():
392
+ if self.ignore_config.should_ignore_table(table):
393
+ continue
394
+
395
+ # Get primary key columns
396
+ pk_columns = self._get_primary_key_columns(table)
397
+
398
+ # Extract unique PKs to check
399
+ pks_to_check = {change["pk"] for change in table_changes}
400
+
401
+ # Query only these specific rows from both snapshots
402
+ for pk in pks_to_check:
403
+ # Build WHERE clause for this PK
404
+ where_sql = self._build_pk_where_clause(pk_columns, pk)
405
+
406
+ # Query before snapshot
407
+ before_query = f"SELECT * FROM {table} WHERE {where_sql}"
408
+ before_response = self.before.resource.query(before_query)
409
+ before_row = dict(zip(before_response.columns, before_response.rows[0])) if before_response.rows else None
410
+
411
+ # Query after snapshot
412
+ after_response = self.after.resource.query(before_query)
413
+ after_row = dict(zip(after_response.columns, after_response.rows[0])) if after_response.rows else None
414
+
415
+ # Check changes for this row
416
+ if before_row and after_row:
417
+ # Modified row - check fields
418
+ for field in set(before_row.keys()) | set(after_row.keys()):
419
+ if self.ignore_config.should_ignore_field(table, field):
420
+ continue
421
+ before_val = before_row.get(field)
422
+ after_val = after_row.get(field)
423
+ if not _values_equivalent(before_val, after_val):
424
+ # Check if this change is allowed
425
+ if not self._is_field_change_allowed(table_changes, pk, field, after_val):
426
+ raise AssertionError(
427
+ f"Unexpected change in table '{table}', "
428
+ f"row {pk}, field '{field}': "
429
+ f"{repr(before_val)} -> {repr(after_val)}"
430
+ )
431
+ elif not before_row and after_row:
432
+ # Added row
433
+ if not self._is_row_change_allowed(table_changes, pk, "__added__"):
434
+ raise AssertionError(f"Unexpected row added in table '{table}': {pk}")
435
+ elif before_row and not after_row:
436
+ # Removed row
437
+ if not self._is_row_change_allowed(table_changes, pk, "__removed__"):
438
+ raise AssertionError(f"Unexpected row removed from table '{table}': {pk}")
439
+
440
+ # Now check tables not mentioned in allowed_changes to ensure no changes
441
+ all_tables = set(self.before.tables()) | set(self.after.tables())
442
+ for table in all_tables:
443
+ if table in changes_by_table or self.ignore_config.should_ignore_table(table):
444
+ continue
445
+
446
+ # For tables with no allowed changes, just check row counts
447
+ before_count_response = self.before.resource.query(f"SELECT COUNT(*) FROM {table}")
448
+ before_count = before_count_response.rows[0][0] if before_count_response.rows else 0
449
+
450
+ after_count_response = self.after.resource.query(f"SELECT COUNT(*) FROM {table}")
451
+ after_count = after_count_response.rows[0][0] if after_count_response.rows else 0
452
+
453
+ if before_count != after_count:
454
+ raise AssertionError(
455
+ f"Unexpected change in table '{table}': "
456
+ f"row count changed from {before_count} to {after_count}"
457
+ )
458
+
459
+ return self
460
+
461
+ def _build_pk_where_clause(self, pk_columns: list[str], pk_value: Any) -> str:
462
+ """Build WHERE clause for primary key lookup."""
463
+ # Escape single quotes in values to prevent SQL injection
464
+ def escape_value(val: Any) -> str:
465
+ if val is None:
466
+ return "NULL"
467
+ elif isinstance(val, str):
468
+ escaped = str(val).replace("'", "''")
469
+ return f"'{escaped}'"
470
+ else:
471
+ return f"'{val}'"
472
+
473
+ if len(pk_columns) == 1:
474
+ return f"{pk_columns[0]} = {escape_value(pk_value)}"
475
+ else:
476
+ # Composite key
477
+ if isinstance(pk_value, tuple):
478
+ conditions = [f"{col} = {escape_value(val)}" for col, val in zip(pk_columns, pk_value)]
479
+ return " AND ".join(conditions)
480
+ else:
481
+ # Shouldn't happen if data is consistent
482
+ return f"{pk_columns[0]} = {escape_value(pk_value)}"
483
+
484
+ def _is_field_change_allowed(self, table_changes: list[dict[str, Any]], pk: Any, field: str, after_val: Any) -> bool:
485
+ """Check if a specific field change is allowed."""
486
+ for change in table_changes:
487
+ if (str(change.get("pk")) == str(pk) and
488
+ change.get("field") == field and
489
+ _values_equivalent(change.get("after"), after_val)):
490
+ return True
491
+ return False
492
+
493
+ def _is_row_change_allowed(self, table_changes: list[dict[str, Any]], pk: Any, change_type: str) -> bool:
494
+ """Check if a row addition/deletion is allowed."""
495
+ for change in table_changes:
496
+ if str(change.get("pk")) == str(pk) and change.get("after") == change_type:
497
+ return True
498
+ return False
499
+
500
+ def _expect_no_changes(self):
501
+ """Efficiently verify that no changes occurred between snapshots using row counts."""
502
+ try:
503
+ # Get all tables from both snapshots
504
+ before_tables = set(self.before.tables())
505
+ after_tables = set(self.after.tables())
506
+
507
+ # Check for added/removed tables (excluding ignored ones)
508
+ added_tables = after_tables - before_tables
509
+ removed_tables = before_tables - after_tables
510
+
511
+ for table in added_tables:
512
+ if not self.ignore_config.should_ignore_table(table):
513
+ raise AssertionError(f"Unexpected table added: {table}")
514
+
515
+ for table in removed_tables:
516
+ if not self.ignore_config.should_ignore_table(table):
517
+ raise AssertionError(f"Unexpected table removed: {table}")
518
+
519
+ # For each table, compare row counts
520
+ all_tables = before_tables | after_tables
521
+ for table in all_tables:
522
+ if self.ignore_config.should_ignore_table(table):
523
+ continue
524
+
525
+ # Get row counts from both snapshots
526
+ before_count = 0
527
+ after_count = 0
528
+
529
+ if table in before_tables:
530
+ before_count_response = self.before.resource.query(f"SELECT COUNT(*) FROM {table}")
531
+ before_count = before_count_response.rows[0][0] if before_count_response.rows else 0
532
+
533
+ if table in after_tables:
534
+ after_count_response = self.after.resource.query(f"SELECT COUNT(*) FROM {table}")
535
+ after_count = after_count_response.rows[0][0] if after_count_response.rows else 0
536
+
537
+ if before_count != after_count:
538
+ raise AssertionError(
539
+ f"Unexpected change in table '{table}': "
540
+ f"row count changed from {before_count} to {after_count}"
541
+ )
542
+
543
+ # If counts match but there could be modifications, we need to check further
544
+ # For now, we'll do a more detailed check only if counts are small
545
+ # to avoid performance issues
546
+ if before_count > 0 and before_count <= 1000: # Threshold for detailed check
547
+ # Do a quick hash check of the data
548
+ self._verify_table_unchanged(table)
549
+ elif before_count > 1000:
550
+ # For large tables, we could sample or use checksums
551
+ # For now, we'll trust that count matching means no changes
552
+ # This is a reasonable assumption for expect_only([])
553
+ pass
554
+
555
+ return self
556
+
557
+ except AssertionError:
558
+ # Re-raise assertion errors (these are expected failures)
559
+ raise
560
+ except Exception as e:
561
+ # If the optimized check fails for other reasons, fall back to full diff
562
+ print(f"Warning: Optimized no-changes check failed: {e}")
563
+ print("Falling back to full diff...")
564
+ return self._expect_only_fallback([])
565
+
566
+ def _verify_table_unchanged(self, table: str):
567
+ """Verify that a table's data hasn't changed (for small tables)."""
568
+ # Get primary key columns
569
+ pk_columns = self._get_primary_key_columns(table)
570
+
571
+ # Get sorted data from both snapshots
572
+ order_by = ", ".join(pk_columns) if pk_columns else "rowid"
573
+
574
+ before_response = self.before.resource.query(f"SELECT * FROM {table} ORDER BY {order_by}")
575
+ after_response = self.after.resource.query(f"SELECT * FROM {table} ORDER BY {order_by}")
576
+
577
+ # Quick check: if column counts differ, there's a schema change
578
+ if before_response.columns != after_response.columns:
579
+ raise AssertionError(f"Schema changed in table '{table}'")
580
+
581
+ # Compare row by row
582
+ if len(before_response.rows) != len(after_response.rows):
583
+ raise AssertionError(
584
+ f"Row count mismatch in table '{table}': "
585
+ f"{len(before_response.rows)} vs {len(after_response.rows)}"
586
+ )
587
+
588
+ for i, (before_row, after_row) in enumerate(zip(before_response.rows, after_response.rows)):
589
+ before_dict = dict(zip(before_response.columns, before_row))
590
+ after_dict = dict(zip(after_response.columns, after_row))
591
+
592
+ # Compare fields, ignoring those in ignore config
593
+ for field in before_response.columns:
594
+ if self.ignore_config.should_ignore_field(table, field):
595
+ continue
596
+
597
+ if not _values_equivalent(before_dict.get(field), after_dict.get(field)):
598
+ pk_val = before_dict.get(pk_columns[0]) if pk_columns else i
599
+ raise AssertionError(
600
+ f"Unexpected change in table '{table}', row {pk_val}, "
601
+ f"field '{field}': {repr(before_dict.get(field))} -> {repr(after_dict.get(field))}"
602
+ )
603
+
604
+ def _expect_only_fallback(self, allowed_changes: list[dict[str, Any]]):
605
+ """Fallback to full diff collection when optimized methods fail."""
308
606
  diff = self._collect()
607
+ return self._validate_diff_against_allowed_changes(diff, allowed_changes)
309
608
 
609
+ def _validate_diff_against_allowed_changes(self, diff: dict[str, Any], allowed_changes: list[dict[str, Any]]):
610
+ """Validate a collected diff against allowed changes."""
310
611
  def _is_change_allowed(
311
612
  table: str, row_id: Any, field: str | None, after_value: Any
312
613
  ) -> bool:
@@ -419,6 +720,20 @@ class SyncSnapshotDiff:
419
720
  raise AssertionError("\n".join(error_lines))
420
721
 
421
722
  return self
723
+
724
+ def expect_only(self, allowed_changes: list[dict[str, Any]]):
725
+ """Ensure only specified changes occurred."""
726
+ # Special case: empty allowed_changes means no changes should have occurred
727
+ if not allowed_changes:
728
+ return self._expect_no_changes()
729
+
730
+ # For expect_only, we can optimize by only checking the specific rows mentioned
731
+ if self._can_use_targeted_queries(allowed_changes):
732
+ return self._expect_only_targeted(allowed_changes)
733
+
734
+ # Fall back to full diff for complex cases
735
+ diff = self._collect()
736
+ return self._validate_diff_against_allowed_changes(diff, allowed_changes)
422
737
 
423
738
 
424
739
  class SyncQueryBuilder:
@@ -669,9 +984,8 @@ class SQLiteResource(Resource):
669
984
 
670
985
  def snapshot(self, name: str | None = None) -> SyncDatabaseSnapshot:
671
986
  """Create a snapshot of the current database state."""
672
- snapshot = SyncDatabaseSnapshot(self, name)
673
- snapshot._ensure_fetched()
674
- return snapshot
987
+ # No longer fetch all data upfront - let it be lazy
988
+ return SyncDatabaseSnapshot(self, name)
675
989
 
676
990
  def diff(
677
991
  self,
fleet/verifiers/code.py CHANGED
@@ -1 +1,2 @@
1
- TASK_SUCCESSFUL_SCORE = 1
1
+ TASK_SUCCESSFUL_SCORE = 1
2
+ TASK_FAILED_SCORE = 0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fleet-python
3
- Version: 0.2.17
3
+ Version: 0.2.19
4
4
  Summary: Python SDK for Fleet environments
5
5
  Author-email: Fleet AI <nic@fleet.so>
6
6
  License: Apache-2.0
@@ -15,20 +15,18 @@ examples/openai_example.py,sha256=I2vk_SJN9BkSRQCYRJfbtGJ-HJ2xzQj-lOjwqmLos5M,82
15
15
  examples/openai_simple_example.py,sha256=I42ytIwv0INgDO39pp1MOQSqsJz2YYH8GeNNBaUtq3A,1748
16
16
  examples/query_builder_example.py,sha256=Q3lUBETHpu1aS2FXAO79ADYqCxOjMMMZNgCcFVapiII,3918
17
17
  examples/quickstart.py,sha256=1VT39IRRhemsJgxi0O0gprdpcw7HB4pYO97GAYagIcg,3788
18
- fleet/__init__.py,sha256=V2y7yMsloc56E4QdluakrVRiITQLmasxmAOBJ1oJ3cU,2365
18
+ fleet/__init__.py,sha256=9EsG068VCTqCCyBA2jw8IMLw4_oFkWLhtY394CtjEgM,2234
19
19
  fleet/base.py,sha256=0yYuMN0lBkrfTTZBt5NQp5112xWgziuWEk4GuHJB1wE,9189
20
- fleet/client.py,sha256=O08gVtngI-Fgo6kyQ-PdoaV8L63b2oQY20Ot2ZzBmSk,28540
20
+ fleet/client.py,sha256=HrHkxQMk9_2GsyG7_edauKets3xySglCUuXW3WtXmTE,28263
21
21
  fleet/config.py,sha256=zd19st83NJdW9DdOq7Irpc0x-iUnMad0JOtAr_nD5DM,273
22
22
  fleet/exceptions.py,sha256=fUmPwWhnT8SR97lYsRq0kLHQHKtSh2eJS0VQ2caSzEI,5055
23
23
  fleet/models.py,sha256=YMHFAgBF4OqoAOv-arHze3bPDDZ5DAzb3CXhVLF6pPw,9019
24
- fleet/playwright.py,sha256=BmRvez5DUa0ttAQB084hPAyt9_8WxdzCGBGF-GZbTuQ,8593
25
24
  fleet/tasks.py,sha256=w-0vVGfEuCWRHMEJ73SN141J7Lz2pD_0v-nNG4TyJTU,1645
26
25
  fleet/types.py,sha256=eXeI8BFmiU5hln0PVvJbUZs7BSjl6wSqAtN9zaJT6yY,652
27
26
  fleet/_async/__init__.py,sha256=AJWCnuo7XKja4yBb8fK2wX7ntciLXQrpzdRHwjTRP6M,62
28
27
  fleet/_async/base.py,sha256=s0rYOtXsMJeitOvpa-Oh8ciLV226p_TIPp3fplzWvV4,9209
29
28
  fleet/_async/client.py,sha256=yyNawgGro2sfFh0ok7DtbjnyxaBGrHg9RtTFEISvwdE,10581
30
29
  fleet/_async/exceptions.py,sha256=fUmPwWhnT8SR97lYsRq0kLHQHKtSh2eJS0VQ2caSzEI,5055
31
- fleet/_async/playwright.py,sha256=NXKcwJezl6jEV2CKBgeaB7zLshFWypgSA5GYXevDRX8,8908
32
30
  fleet/_async/tasks.py,sha256=w-0vVGfEuCWRHMEJ73SN141J7Lz2pD_0v-nNG4TyJTU,1645
33
31
  fleet/_async/env/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
34
32
  fleet/_async/env/client.py,sha256=PQGKcvnVTr3o2KNsgQM7ZBqaCjimv-wEn-XiYGm44Ko,753
@@ -51,19 +49,19 @@ fleet/instance/models.py,sha256=ZTiue0YOuhuwX8jYfJAoCzGfqjLqqXRLqK1LVFhq6rQ,4183
51
49
  fleet/resources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
52
50
  fleet/resources/base.py,sha256=203gD54NP1IvjuSqFo-f7FvrkhtjChggtzrxJK7xf2E,667
53
51
  fleet/resources/browser.py,sha256=OEqfNYPAptANza0T8K7UEn6nI7JnzlCICFMW8r2r83Q,1367
54
- fleet/resources/mcp.py,sha256=55LBwhNDqSeFVCD0O3OP9m2DdJE-KEYRQiCnVyze8DM,1864
55
- fleet/resources/sqlite.py,sha256=U0KneMzFKFrcuXcGrl4gkFPNoWST7_gWINphcA_UPJA,26251
52
+ fleet/resources/mcp.py,sha256=lDMyY4pMC3Khwp2Wycc6Ds6KeLAUOgHkDnmztuAnXm8,1872
53
+ fleet/resources/sqlite.py,sha256=aYR6OOrnUHwcUcbkXPLk4biNZI4VXGtPpN3F9n6t3-4,42132
56
54
  fleet/verifiers/__init__.py,sha256=-dm2x0wC8UbfGcMCbPtjb0-LospGUhD-S3Pm4oha6BY,445
57
55
  fleet/verifiers/bundler.py,sha256=A4yR3wBOcVZYFAv87CD58QlJn6L4QXeilrasnVm8n74,26185
58
- fleet/verifiers/code.py,sha256=NJ4OLZnpqLkI1lXY7-5m2GuZklLxMzHUCnRMVyN2_OI,25
56
+ fleet/verifiers/code.py,sha256=EOi6ES8Zdzlm9iybRFaJmz9t2W4Ulo2wrCdbEBqxzbc,47
59
57
  fleet/verifiers/db.py,sha256=tssmvJjDHuBIy8qlL_P5-UdmEFUw2DZcqLsWZ8ot3Xw,27766
60
58
  fleet/verifiers/decorator.py,sha256=Q-KHhicnIYFwX7FX_VZguzNfu8ZslqNUeWxcS2CwNVY,3386
61
59
  fleet/verifiers/sql_differ.py,sha256=dmiGCFXVMEMbAX519OjhVqgA8ZvhnvdmC1BVpL7QCF0,6490
62
60
  fleet/verifiers/verifier.py,sha256=C5L24M7VBHWKaWlryC1EdW1g7keZBGzVSjgXC_SlpXA,12205
63
- fleet_python-0.2.17.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
61
+ fleet_python-0.2.19.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
64
62
  scripts/fix_sync_imports.py,sha256=BIQfnaOoQ7bwR1c-pDqH9ifN47W1bwL7OafWVXZNkuA,4368
65
63
  scripts/unasync.py,sha256=--Fmaae47o-dZ1HYgX1c3Nvi-rMjcFymTRlJcWWnmpw,725
66
- fleet_python-0.2.17.dist-info/METADATA,sha256=j4d0xitYxfrKzk_5a97Phx5hC4ZNKXL8bQHlAe5izFA,3297
67
- fleet_python-0.2.17.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
68
- fleet_python-0.2.17.dist-info/top_level.txt,sha256=_3DSmTohvSDf3AIP_BYfGzhwO1ECFwuzg83X-wHCx3Y,23
69
- fleet_python-0.2.17.dist-info/RECORD,,
64
+ fleet_python-0.2.19.dist-info/METADATA,sha256=pzkr1yf1dDYvvaEvnwuit1M8goW9Xp3ah50dPyxmHew,3297
65
+ fleet_python-0.2.19.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
66
+ fleet_python-0.2.19.dist-info/top_level.txt,sha256=_3DSmTohvSDf3AIP_BYfGzhwO1ECFwuzg83X-wHCx3Y,23
67
+ fleet_python-0.2.19.dist-info/RECORD,,
@@ -1,291 +0,0 @@
1
- import base64
2
- from typing import List, Dict, Any
3
- from playwright.async_api import async_playwright, Browser, Page
4
- from .client import AsyncEnv
5
-
6
-
7
- # Key mapping for computer use actions
8
- CUA_KEY_TO_PLAYWRIGHT_KEY = {
9
- "/": "Divide",
10
- "\\": "Backslash",
11
- "alt": "Alt",
12
- "arrowdown": "ArrowDown",
13
- "arrowleft": "ArrowLeft",
14
- "arrowright": "ArrowRight",
15
- "arrowup": "ArrowUp",
16
- "backspace": "Backspace",
17
- "capslock": "CapsLock",
18
- "cmd": "Meta",
19
- "ctrl": "Control",
20
- "delete": "Delete",
21
- "end": "End",
22
- "enter": "Enter",
23
- "esc": "Escape",
24
- "home": "Home",
25
- "insert": "Insert",
26
- "option": "Alt",
27
- "pagedown": "PageDown",
28
- "pageup": "PageUp",
29
- "shift": "Shift",
30
- "space": " ",
31
- "super": "Meta",
32
- "tab": "Tab",
33
- "win": "Meta",
34
- }
35
-
36
-
37
- class AsyncFleetPlaywrightWrapper:
38
- """
39
- A wrapper that adds Playwright browser automation to Fleet environment instances.
40
-
41
- This class handles:
42
- - Browser connection via CDP
43
- - Computer actions (click, scroll, type, etc.)
44
- - Screenshot capture
45
- - Integration with OpenAI computer use API
46
-
47
- Usage:
48
- instance = await fleet.env.make(env_key="hubspot", version="v1.2.7")
49
- browser = AsyncFleetPlaywrightWrapper(instance)
50
- await browser.start()
51
-
52
- # Use browser methods
53
- screenshot = await browser.screenshot()
54
- tools = [browser.openai_cua_tool]
55
-
56
- # Clean up when done
57
- await browser.close()
58
- """
59
-
60
- def get_environment(self):
61
- return "browser"
62
-
63
- def get_dimensions(self):
64
- return (1920, 1080)
65
-
66
- def __init__(
67
- self,
68
- env: AsyncEnv,
69
- display_width: int = 1920,
70
- display_height: int = 1080,
71
- ):
72
- """
73
- Initialize the Fleet Playwright wrapper.
74
-
75
- Args:
76
- env: Fleet environment instance
77
- display_width: Browser viewport width
78
- display_height: Browser viewport height
79
- """
80
- self.env = env
81
- self.display_width = display_width
82
- self.display_height = display_height
83
-
84
- self._playwright = None
85
- self._browser: Browser | None = None
86
- self._page: Page | None = None
87
- self._started = False
88
-
89
- async def start(self):
90
- """Start the browser and establish connection."""
91
- if self._started:
92
- return
93
-
94
- # Start Playwright
95
- self._playwright = await async_playwright().start()
96
-
97
- # Start browser on the Fleet instance
98
- print("Starting browser...")
99
- await self.env.browser().start()
100
- cdp = await self.env.browser().describe()
101
-
102
- # Connect to browser
103
- self._browser = await self._playwright.chromium.connect_over_cdp(
104
- cdp.cdp_browser_url
105
- )
106
- self._page = self._browser.contexts[0].pages[0]
107
- await self._page.set_viewport_size(
108
- {"width": self.display_width, "height": self.display_height}
109
- )
110
-
111
- self._started = True
112
- print(f"Track agent: {cdp.cdp_devtools_url}")
113
-
114
- async def close(self):
115
- """Close the browser connection."""
116
- if self._playwright:
117
- await self._playwright.stop()
118
- self._playwright = None
119
- self._browser = None
120
- self._page = None
121
- self._started = False
122
-
123
- def _ensure_started(self):
124
- """Ensure browser is started before operations."""
125
- if not self._started:
126
- raise RuntimeError("Browser not started. Call await browser.start() first.")
127
-
128
- @property
129
- def openai_cua_tool(self) -> Dict[str, Any]:
130
- """
131
- Tool definition for OpenAI computer use API.
132
-
133
- Returns:
134
- Tool definition dict for use with OpenAI responses API
135
- """
136
- return {
137
- "type": "computer_use_preview",
138
- "display_width": self.display_width,
139
- "display_height": self.display_height,
140
- "environment": "browser",
141
- }
142
-
143
- async def screenshot(self) -> str:
144
- """
145
- Take a screenshot and return base64 encoded string.
146
-
147
- Returns:
148
- Base64 encoded PNG screenshot
149
- """
150
- self._ensure_started()
151
-
152
- png_bytes = await self._page.screenshot(full_page=False)
153
- return base64.b64encode(png_bytes).decode("utf-8")
154
-
155
- def get_current_url(self) -> str:
156
- """Get the current page URL."""
157
- self._ensure_started()
158
- return self._page.url
159
-
160
- async def execute_computer_action(self, action: Dict[str, Any]) -> Dict[str, Any]:
161
- """
162
- Execute a computer action and return the result for OpenAI API.
163
-
164
- Args:
165
- action: Computer action dict from OpenAI response
166
-
167
- Returns:
168
- Result dict for computer_call_output
169
- """
170
- self._ensure_started()
171
-
172
- action_type = action["type"]
173
- action_args = {k: v for k, v in action.items() if k != "type"}
174
-
175
- print(f"Executing: {action_type}({action_args})")
176
-
177
- # Execute the action
178
- if hasattr(self, f"_{action_type}"):
179
- method = getattr(self, f"_{action_type}")
180
- await method(**action_args)
181
- else:
182
- raise ValueError(f"Unsupported action type: {action_type}")
183
-
184
- # Take screenshot after action
185
- screenshot_base64 = await self.screenshot()
186
-
187
- return {
188
- "type": "input_image",
189
- "image_url": f"data:image/png;base64,{screenshot_base64}",
190
- "current_url": self.get_current_url(),
191
- }
192
-
193
- # Computer action implementations
194
- async def _click(self, x: int, y: int, button: str = "left") -> None:
195
- """Click at coordinates."""
196
- self._ensure_started()
197
- await self._page.mouse.click(x, y, button=button)
198
-
199
- async def _double_click(self, x: int, y: int) -> None:
200
- """Double-click at coordinates."""
201
- self._ensure_started()
202
- await self._page.mouse.dblclick(x, y)
203
-
204
- async def _scroll(self, x: int, y: int, scroll_x: int, scroll_y: int) -> None:
205
- """Scroll from coordinates."""
206
- self._ensure_started()
207
- await self._page.mouse.move(x, y)
208
- await self._page.evaluate(f"window.scrollBy({scroll_x}, {scroll_y})")
209
-
210
- async def _type(self, text: str) -> None:
211
- """Type text."""
212
- self._ensure_started()
213
- await self._page.keyboard.type(text)
214
-
215
- async def _keypress(self, keys: List[str]) -> None:
216
- """Press key combination."""
217
- self._ensure_started()
218
- mapped_keys = [CUA_KEY_TO_PLAYWRIGHT_KEY.get(key.lower(), key) for key in keys]
219
- for key in mapped_keys:
220
- await self._page.keyboard.down(key)
221
- for key in reversed(mapped_keys):
222
- await self._page.keyboard.up(key)
223
-
224
- async def _move(self, x: int, y: int) -> None:
225
- """Move mouse to coordinates."""
226
- self._ensure_started()
227
- await self._page.mouse.move(x, y)
228
-
229
- async def _drag(self, path: List[Dict[str, int]]) -> None:
230
- """Drag mouse along path."""
231
- self._ensure_started()
232
- if not path:
233
- return
234
- await self._page.mouse.move(path[0]["x"], path[0]["y"])
235
- await self._page.mouse.down()
236
- for point in path[1:]:
237
- await self._page.mouse.move(point["x"], point["y"])
238
- await self._page.mouse.up()
239
-
240
- async def _wait(self, ms: int = 1000) -> None:
241
- """Wait for specified milliseconds."""
242
- import asyncio
243
-
244
- await asyncio.sleep(ms / 1000)
245
-
246
- # Browser-specific actions
247
- async def _goto(self, url: str) -> None:
248
- """Navigate to URL."""
249
- self._ensure_started()
250
- try:
251
- await self._page.goto(url)
252
- except Exception as e:
253
- print(f"Error navigating to {url}: {e}")
254
-
255
- async def _back(self) -> None:
256
- """Go back in browser history."""
257
- self._ensure_started()
258
- await self._page.go_back()
259
-
260
- async def _forward(self) -> None:
261
- """Go forward in browser history."""
262
- self._ensure_started()
263
- await self._page.go_forward()
264
-
265
- async def _refresh(self) -> None:
266
- """Refresh the page."""
267
- self._ensure_started()
268
- await self._page.reload()
269
-
270
- # ------------------------------------------------------------------
271
- # Public aliases (no leading underscore) expected by the Agent &
272
- # OpenAI computer-use API. They forward directly to the underscored
273
- # implementations above so the external interface matches the older
274
- # BasePlaywrightComputer class.
275
- # ------------------------------------------------------------------
276
-
277
- # Mouse / keyboard actions
278
- click = _click
279
- double_click = _double_click
280
- scroll = _scroll
281
- type = _type # noqa: A003 – shadowing built-in for API compatibility
282
- keypress = _keypress
283
- move = _move
284
- drag = _drag
285
- wait = _wait
286
-
287
- # Browser navigation actions
288
- goto = _goto
289
- back = _back
290
- forward = _forward
291
- refresh = _refresh
fleet/playwright.py DELETED
@@ -1,290 +0,0 @@
1
- import base64
2
- from typing import List, Dict, Any
3
- from playwright.sync_api import sync_playwright, Browser, Page
4
- from .client import Environment
5
-
6
-
7
- # Key mapping for computer use actions
8
- CUA_KEY_TO_PLAYWRIGHT_KEY = {
9
- "/": "Divide",
10
- "\\": "Backslash",
11
- "alt": "Alt",
12
- "arrowdown": "ArrowDown",
13
- "arrowleft": "ArrowLeft",
14
- "arrowright": "ArrowRight",
15
- "arrowup": "ArrowUp",
16
- "backspace": "Backspace",
17
- "capslock": "CapsLock",
18
- "cmd": "Meta",
19
- "ctrl": "Control",
20
- "delete": "Delete",
21
- "end": "End",
22
- "enter": "Enter",
23
- "esc": "Escape",
24
- "home": "Home",
25
- "insert": "Insert",
26
- "option": "Alt",
27
- "pagedown": "PageDown",
28
- "pageup": "PageUp",
29
- "shift": "Shift",
30
- "space": " ",
31
- "super": "Meta",
32
- "tab": "Tab",
33
- "win": "Meta",
34
- }
35
-
36
-
37
- class FleetPlaywrightWrapper:
38
- """
39
- A wrapper that adds Playwright browser automation to Fleet environment instances.
40
-
41
- This class handles:
42
- - Browser connection via CDP
43
- - Computer actions (click, scroll, type, etc.)
44
- - Screenshot capture
45
- - Integration with OpenAI computer use API
46
-
47
- Usage:
48
- instance = fleet.env.make(env_key="hubspot", version="v1.2.7")
49
- browser = FleetPlaywrightWrapper(instance)
50
- browser.start()
51
-
52
- # Use browser methods
53
- screenshot = browser.screenshot()
54
- tools = [browser.openai_cua_tool]
55
-
56
- # Clean up when done
57
- browser.close()
58
- """
59
-
60
- def get_environment(self):
61
- return "browser"
62
-
63
- def get_dimensions(self):
64
- return (1920, 1080)
65
-
66
- def __init__(
67
- self,
68
- env: Environment,
69
- display_width: int = 1920,
70
- display_height: int = 1080,
71
- ):
72
- """
73
- Initialize the Fleet Playwright wrapper.
74
-
75
- Args:
76
- env: Fleet environment instance
77
- display_width: Browser viewport width
78
- display_height: Browser viewport height
79
- """
80
- self.env = env
81
- self.display_width = display_width
82
- self.display_height = display_height
83
-
84
- self._playwright = None
85
- self._browser: Browser | None = None
86
- self._page: Page | None = None
87
- self._started = False
88
-
89
- def start(self):
90
- """Start the browser and establish connection."""
91
- if self._started:
92
- return
93
-
94
- # Start Playwright
95
- self._playwright = sync_playwright().start()
96
-
97
- # Start browser on the Fleet instance
98
- print("Starting browser...")
99
- self.env.browser().start()
100
- cdp = self.env.browser().describe()
101
-
102
- # Connect to browser
103
- self._browser = self._playwright.chromium.connect_over_cdp(
104
- cdp.cdp_browser_url
105
- )
106
- self._page = self._browser.contexts[0].pages[0]
107
- self._page.set_viewport_size(
108
- {"width": self.display_width, "height": self.display_height}
109
- )
110
-
111
- self._started = True
112
- print(f"Track agent: {cdp.cdp_devtools_url}")
113
-
114
- def close(self):
115
- """Close the browser connection."""
116
- if self._playwright:
117
- self._playwright.stop()
118
- self._playwright = None
119
- self._browser = None
120
- self._page = None
121
- self._started = False
122
-
123
- def _ensure_started(self):
124
- """Ensure browser is started before operations."""
125
- if not self._started:
126
- raise RuntimeError("Browser not started. Call browser.start() first.")
127
-
128
- @property
129
- def openai_cua_tool(self) -> Dict[str, Any]:
130
- """
131
- Tool definition for OpenAI computer use API.
132
-
133
- Returns:
134
- Tool definition dict for use with OpenAI responses API
135
- """
136
- return {
137
- "type": "computer_use_preview",
138
- "display_width": self.display_width,
139
- "display_height": self.display_height,
140
- "environment": "browser",
141
- }
142
-
143
- def screenshot(self) -> str:
144
- """
145
- Take a screenshot and return base64 encoded string.
146
-
147
- Returns:
148
- Base64 encoded PNG screenshot
149
- """
150
- self._ensure_started()
151
-
152
- png_bytes = self._page.screenshot(full_page=False)
153
- return base64.b64encode(png_bytes).decode("utf-8")
154
-
155
- def get_current_url(self) -> str:
156
- """Get the current page URL."""
157
- self._ensure_started()
158
- return self._page.url
159
-
160
- def execute_computer_action(self, action: Dict[str, Any]) -> Dict[str, Any]:
161
- """
162
- Execute a computer action and return the result for OpenAI API.
163
-
164
- Args:
165
- action: Computer action dict from OpenAI response
166
-
167
- Returns:
168
- Result dict for computer_call_output
169
- """
170
- self._ensure_started()
171
-
172
- action_type = action["type"]
173
- action_args = {k: v for k, v in action.items() if k != "type"}
174
-
175
- print(f"Executing: {action_type}({action_args})")
176
-
177
- # Execute the action
178
- if hasattr(self, f"_{action_type}"):
179
- method = getattr(self, f"_{action_type}")
180
- method(**action_args)
181
- else:
182
- raise ValueError(f"Unsupported action type: {action_type}")
183
-
184
- # Take screenshot after action
185
- screenshot_base64 = self.screenshot()
186
-
187
- return {
188
- "type": "input_image",
189
- "image_url": f"data:image/png;base64,{screenshot_base64}",
190
- "current_url": self.get_current_url(),
191
- }
192
-
193
- # Computer action implementations
194
- def _click(self, x: int, y: int, button: str = "left") -> None:
195
- """Click at coordinates."""
196
- self._ensure_started()
197
- self._page.mouse.click(x, y, button=button)
198
-
199
- def _double_click(self, x: int, y: int) -> None:
200
- """Double-click at coordinates."""
201
- self._ensure_started()
202
- self._page.mouse.dblclick(x, y)
203
-
204
- def _scroll(self, x: int, y: int, scroll_x: int, scroll_y: int) -> None:
205
- """Scroll from coordinates."""
206
- self._ensure_started()
207
- self._page.mouse.move(x, y)
208
- self._page.evaluate(f"window.scrollBy({scroll_x}, {scroll_y})")
209
-
210
- def _type(self, text: str) -> None:
211
- """Type text."""
212
- self._ensure_started()
213
- self._page.keyboard.type(text)
214
-
215
- def _keypress(self, keys: List[str]) -> None:
216
- """Press key combination."""
217
- self._ensure_started()
218
- mapped_keys = [CUA_KEY_TO_PLAYWRIGHT_KEY.get(key.lower(), key) for key in keys]
219
- for key in mapped_keys:
220
- self._page.keyboard.down(key)
221
- for key in reversed(mapped_keys):
222
- self._page.keyboard.up(key)
223
-
224
- def _move(self, x: int, y: int) -> None:
225
- """Move mouse to coordinates."""
226
- self._ensure_started()
227
- self._page.mouse.move(x, y)
228
-
229
- def _drag(self, path: List[Dict[str, int]]) -> None:
230
- """Drag mouse along path."""
231
- self._ensure_started()
232
- if not path:
233
- return
234
- self._page.mouse.move(path[0]["x"], path[0]["y"])
235
- self._page.mouse.down()
236
- for point in path[1:]:
237
- self._page.mouse.move(point["x"], point["y"])
238
- self._page.mouse.up()
239
-
240
- def _wait(self, ms: int = 1000) -> None:
241
- """Wait for specified milliseconds."""
242
-
243
- time.sleep(ms / 1000)
244
-
245
- # Browser-specific actions
246
- def _goto(self, url: str) -> None:
247
- """Navigate to URL."""
248
- self._ensure_started()
249
- try:
250
- self._page.goto(url)
251
- except Exception as e:
252
- print(f"Error navigating to {url}: {e}")
253
-
254
- def _back(self) -> None:
255
- """Go back in browser history."""
256
- self._ensure_started()
257
- self._page.go_back()
258
-
259
- def _forward(self) -> None:
260
- """Go forward in browser history."""
261
- self._ensure_started()
262
- self._page.go_forward()
263
-
264
- def _refresh(self) -> None:
265
- """Refresh the page."""
266
- self._ensure_started()
267
- self._page.reload()
268
-
269
- # ------------------------------------------------------------------
270
- # Public aliases (no leading underscore) expected by the Agent &
271
- # OpenAI computer-use API. They forward directly to the underscored
272
- # implementations above so the external interface matches the older
273
- # BasePlaywrightComputer class.
274
- # ------------------------------------------------------------------
275
-
276
- # Mouse / keyboard actions
277
- click = _click
278
- double_click = _double_click
279
- scroll = _scroll
280
- type = _type # noqa: A003 – shadowing built-in for API compatibility
281
- keypress = _keypress
282
- move = _move
283
- drag = _drag
284
- wait = _wait
285
-
286
- # Browser navigation actions
287
- goto = _goto
288
- back = _back
289
- forward = _forward
290
- refresh = _refresh