fleet-python 0.2.84__tar.gz → 0.2.86__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. {fleet_python-0.2.84/fleet_python.egg-info → fleet_python-0.2.86}/PKG-INFO +2 -1
  2. {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/__init__.py +1 -1
  3. {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/_async/__init__.py +1 -1
  4. {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/_async/base.py +1 -1
  5. fleet_python-0.2.86/fleet/agent/gemini_cua/Dockerfile +44 -0
  6. fleet_python-0.2.86/fleet/agent/gemini_cua/requirements.txt +4 -0
  7. fleet_python-0.2.86/fleet/agent/gemini_cua/start.sh +31 -0
  8. {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/agent/orchestrator.py +4 -8
  9. {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/base.py +1 -1
  10. fleet_python-0.2.86/fleet/utils/playwright.py +440 -0
  11. {fleet_python-0.2.84 → fleet_python-0.2.86/fleet_python.egg-info}/PKG-INFO +2 -1
  12. {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet_python.egg-info/SOURCES.txt +4 -0
  13. {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet_python.egg-info/requires.txt +3 -0
  14. {fleet_python-0.2.84 → fleet_python-0.2.86}/pyproject.toml +5 -1
  15. {fleet_python-0.2.84 → fleet_python-0.2.86}/LICENSE +0 -0
  16. {fleet_python-0.2.84 → fleet_python-0.2.86}/README.md +0 -0
  17. {fleet_python-0.2.84 → fleet_python-0.2.86}/examples/diff_example.py +0 -0
  18. {fleet_python-0.2.84 → fleet_python-0.2.86}/examples/dsl_example.py +0 -0
  19. {fleet_python-0.2.84 → fleet_python-0.2.86}/examples/example.py +0 -0
  20. {fleet_python-0.2.84 → fleet_python-0.2.86}/examples/exampleResume.py +0 -0
  21. {fleet_python-0.2.84 → fleet_python-0.2.86}/examples/example_account.py +0 -0
  22. {fleet_python-0.2.84 → fleet_python-0.2.86}/examples/example_action_log.py +0 -0
  23. {fleet_python-0.2.84 → fleet_python-0.2.86}/examples/example_client.py +0 -0
  24. {fleet_python-0.2.84 → fleet_python-0.2.86}/examples/example_mcp_anthropic.py +0 -0
  25. {fleet_python-0.2.84 → fleet_python-0.2.86}/examples/example_mcp_openai.py +0 -0
  26. {fleet_python-0.2.84 → fleet_python-0.2.86}/examples/example_sync.py +0 -0
  27. {fleet_python-0.2.84 → fleet_python-0.2.86}/examples/example_task.py +0 -0
  28. {fleet_python-0.2.84 → fleet_python-0.2.86}/examples/example_tasks.py +0 -0
  29. {fleet_python-0.2.84 → fleet_python-0.2.86}/examples/example_verifier.py +0 -0
  30. {fleet_python-0.2.84 → fleet_python-0.2.86}/examples/export_tasks.py +0 -0
  31. {fleet_python-0.2.84 → fleet_python-0.2.86}/examples/fetch_tasks.py +0 -0
  32. {fleet_python-0.2.84 → fleet_python-0.2.86}/examples/gemini_example.py +0 -0
  33. {fleet_python-0.2.84 → fleet_python-0.2.86}/examples/import_tasks.py +0 -0
  34. {fleet_python-0.2.84 → fleet_python-0.2.86}/examples/iterate_verifiers.py +0 -0
  35. {fleet_python-0.2.84 → fleet_python-0.2.86}/examples/json_tasks_example.py +0 -0
  36. {fleet_python-0.2.84 → fleet_python-0.2.86}/examples/nova_act_example.py +0 -0
  37. {fleet_python-0.2.84 → fleet_python-0.2.86}/examples/openai_example.py +0 -0
  38. {fleet_python-0.2.84 → fleet_python-0.2.86}/examples/openai_simple_example.py +0 -0
  39. {fleet_python-0.2.84 → fleet_python-0.2.86}/examples/query_builder_example.py +0 -0
  40. {fleet_python-0.2.84 → fleet_python-0.2.86}/examples/quickstart.py +0 -0
  41. {fleet_python-0.2.84 → fleet_python-0.2.86}/examples/test_cdp_logging.py +0 -0
  42. {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/_async/client.py +0 -0
  43. {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/_async/env/__init__.py +0 -0
  44. {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/_async/env/client.py +0 -0
  45. {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/_async/exceptions.py +0 -0
  46. {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/_async/global_client.py +0 -0
  47. {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/_async/instance/__init__.py +0 -0
  48. {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/_async/instance/base.py +0 -0
  49. {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/_async/instance/client.py +0 -0
  50. {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/_async/models.py +0 -0
  51. {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/_async/resources/__init__.py +0 -0
  52. {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/_async/resources/base.py +0 -0
  53. {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/_async/resources/browser.py +0 -0
  54. {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/_async/resources/mcp.py +0 -0
  55. {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/_async/resources/sqlite.py +0 -0
  56. {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/_async/tasks.py +0 -0
  57. {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/_async/verifiers/__init__.py +0 -0
  58. {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/_async/verifiers/bundler.py +0 -0
  59. {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/_async/verifiers/verifier.py +0 -0
  60. {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/agent/__init__.py +0 -0
  61. {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/agent/gemini_cua/__init__.py +0 -0
  62. {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/agent/gemini_cua/agent.py +0 -0
  63. {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/agent/gemini_cua/mcp_server.py +0 -0
  64. /fleet_python-0.2.84/fleet/utils/playwright.py → /fleet_python-0.2.86/fleet/agent/gemini_cua/playwright_utils.py +0 -0
  65. {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/agent/types.py +0 -0
  66. {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/agent/utils.py +0 -0
  67. {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/cli.py +0 -0
  68. {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/client.py +0 -0
  69. {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/config.py +0 -0
  70. {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/env/__init__.py +0 -0
  71. {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/env/client.py +0 -0
  72. {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/eval/__init__.py +0 -0
  73. {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/eval/uploader.py +0 -0
  74. {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/exceptions.py +0 -0
  75. {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/global_client.py +0 -0
  76. {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/instance/__init__.py +0 -0
  77. {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/instance/base.py +0 -0
  78. {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/instance/client.py +0 -0
  79. {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/instance/models.py +0 -0
  80. {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/models.py +0 -0
  81. {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/proxy/__init__.py +0 -0
  82. {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/proxy/proxy.py +0 -0
  83. {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/proxy/whitelist.py +0 -0
  84. {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/resources/__init__.py +0 -0
  85. {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/resources/base.py +0 -0
  86. {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/resources/browser.py +0 -0
  87. {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/resources/mcp.py +0 -0
  88. {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/resources/sqlite.py +0 -0
  89. {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/tasks.py +0 -0
  90. {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/types.py +0 -0
  91. {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/utils/__init__.py +0 -0
  92. {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/utils/http_logging.py +0 -0
  93. {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/utils/logging.py +0 -0
  94. {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/verifiers/__init__.py +0 -0
  95. {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/verifiers/bundler.py +0 -0
  96. {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/verifiers/code.py +0 -0
  97. {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/verifiers/db.py +0 -0
  98. {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/verifiers/decorator.py +0 -0
  99. {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/verifiers/parse.py +0 -0
  100. {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/verifiers/sql_differ.py +0 -0
  101. {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/verifiers/verifier.py +0 -0
  102. {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet_python.egg-info/dependency_links.txt +0 -0
  103. {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet_python.egg-info/entry_points.txt +0 -0
  104. {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet_python.egg-info/top_level.txt +0 -0
  105. {fleet_python-0.2.84 → fleet_python-0.2.86}/scripts/fix_sync_imports.py +0 -0
  106. {fleet_python-0.2.84 → fleet_python-0.2.86}/scripts/unasync.py +0 -0
  107. {fleet_python-0.2.84 → fleet_python-0.2.86}/setup.cfg +0 -0
  108. {fleet_python-0.2.84 → fleet_python-0.2.86}/tests/__init__.py +0 -0
  109. {fleet_python-0.2.84 → fleet_python-0.2.86}/tests/test_app_method.py +0 -0
  110. {fleet_python-0.2.84 → fleet_python-0.2.86}/tests/test_expect_only.py +0 -0
  111. {fleet_python-0.2.84 → fleet_python-0.2.86}/tests/test_instance_dispatch.py +0 -0
  112. {fleet_python-0.2.84 → fleet_python-0.2.86}/tests/test_sqlite_resource_dual_mode.py +0 -0
  113. {fleet_python-0.2.84 → fleet_python-0.2.86}/tests/test_sqlite_shared_memory_behavior.py +0 -0
  114. {fleet_python-0.2.84 → fleet_python-0.2.86}/tests/test_verifier_from_string.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fleet-python
3
- Version: 0.2.84
3
+ Version: 0.2.86
4
4
  Summary: Python SDK for Fleet environments
5
5
  Author-email: Fleet AI <nic@fleet.so>
6
6
  License: Apache-2.0
@@ -45,6 +45,7 @@ Requires-Dist: playwright>=1.40.0; extra == "playwright"
45
45
  Provides-Extra: eval
46
46
  Requires-Dist: aiohttp>=3.9.0; extra == "eval"
47
47
  Requires-Dist: google-genai>=1.0.0; extra == "eval"
48
+ Requires-Dist: mcp>=0.1.0; python_version >= "3.10" and extra == "eval"
48
49
  Dynamic: license-file
49
50
 
50
51
  # Fleet SDK
@@ -73,7 +73,7 @@ from . import env
73
73
  from . import global_client as _global_client
74
74
  from ._async import global_client as _async_global_client
75
75
 
76
- __version__ = "0.2.84"
76
+ __version__ = "0.2.86"
77
77
 
78
78
  __all__ = [
79
79
  # Core classes
@@ -44,7 +44,7 @@ from ..types import VerifierFunction
44
44
  from .. import env
45
45
  from . import global_client as _async_global_client
46
46
 
47
- __version__ = "0.2.84"
47
+ __version__ = "0.2.86"
48
48
 
49
49
  __all__ = [
50
50
  # Core classes
@@ -26,7 +26,7 @@ from .exceptions import (
26
26
  try:
27
27
  from .. import __version__
28
28
  except ImportError:
29
- __version__ = "0.2.84"
29
+ __version__ = "0.2.86"
30
30
 
31
31
  logger = logging.getLogger(__name__)
32
32
 
@@ -0,0 +1,44 @@
1
+ # MCP Server - Browser control in Docker with optional VNC
2
+ FROM python:3.11-slim
3
+
4
+ # Install dependencies for Chromium and VNC
5
+ RUN apt-get update && apt-get install -y --no-install-recommends \
6
+ # Chromium dependencies
7
+ wget fonts-liberation libasound2 libatk-bridge2.0-0 libatk1.0-0 \
8
+ libatspi2.0-0 libcups2 libdbus-1-3 libdrm2 libgbm1 libgtk-3-0 \
9
+ libnspr4 libnss3 libxcomposite1 libxdamage1 libxfixes3 libxkbcommon0 \
10
+ libxrandr2 xdg-utils \
11
+ # VNC and display for headful mode
12
+ xvfb x11vnc fluxbox \
13
+ # noVNC for web-based viewing
14
+ novnc websockify \
15
+ # Utilities
16
+ procps net-tools \
17
+ && rm -rf /var/lib/apt/lists/*
18
+
19
+ WORKDIR /app
20
+
21
+ # Install Python deps
22
+ COPY requirements.txt .
23
+ RUN pip install --no-cache-dir -r requirements.txt && playwright install chromium
24
+
25
+ # Copy server files (all from same directory)
26
+ COPY playwright_utils.py .
27
+ COPY mcp_server.py .
28
+ COPY start.sh .
29
+ RUN chmod +x start.sh
30
+
31
+ # Environment
32
+ ENV PORT=8765 \
33
+ SCREEN_WIDTH=1366 \
34
+ SCREEN_HEIGHT=768 \
35
+ HEADLESS=true \
36
+ VNC_PORT=5900 \
37
+ NOVNC_PORT=6080 \
38
+ DISPLAY=:99
39
+
40
+ # Expose ports: MCP server, VNC, noVNC
41
+ EXPOSE 8765 5900 6080
42
+
43
+ # Start script handles display setup
44
+ CMD ["./start.sh"]
@@ -0,0 +1,4 @@
1
+ playwright>=1.40.0
2
+ mcp[cli]>=1.2.0
3
+ uvicorn>=0.30.0
4
+ starlette>=0.38.0
@@ -0,0 +1,31 @@
1
+ #!/bin/bash
2
+ set -e
3
+
4
+ # Start virtual display if not headless
5
+ if [ "$HEADLESS" != "true" ]; then
6
+ echo "Starting Xvfb virtual display..."
7
+ Xvfb :99 -screen 0 ${SCREEN_WIDTH}x${SCREEN_HEIGHT}x24 &
8
+ sleep 1
9
+
10
+ echo "Starting fluxbox window manager..."
11
+ fluxbox &
12
+ sleep 1
13
+
14
+ echo "Starting VNC server on port $VNC_PORT..."
15
+ x11vnc -display :99 -forever -shared -rfbport $VNC_PORT -nopw &
16
+ sleep 1
17
+
18
+ echo "Starting noVNC on port $NOVNC_PORT..."
19
+ websockify --web=/usr/share/novnc/ $NOVNC_PORT localhost:$VNC_PORT &
20
+ sleep 1
21
+
22
+ echo ""
23
+ echo "=========================================="
24
+ echo " Browser visible at: http://localhost:$NOVNC_PORT/vnc.html"
25
+ echo "=========================================="
26
+ echo ""
27
+ fi
28
+
29
+ # Start the MCP server
30
+ exec python mcp_server.py
31
+
@@ -21,6 +21,7 @@ import asyncio
21
21
  import json
22
22
  import logging
23
23
  import os
24
+ import sys
24
25
  import time
25
26
  from datetime import datetime
26
27
  from pathlib import Path
@@ -162,17 +163,12 @@ class AgentOrchestrator:
162
163
 
163
164
  image_name = f"fleet-cua-{agent_path.name}"
164
165
 
165
- # Use fleet SDK root as build context (so Dockerfile can access fleet/utils)
166
- # agent_path is like: .../fleet-sdk/fleet/agent/gemini_cua
167
- # We want: .../fleet-sdk
168
- fleet_root = agent_path.parent.parent.parent
169
-
166
+ # Build context is the agent directory (all files are self-contained)
170
167
  with Live(Spinner("dots", text=f"Building Docker image {image_name}..."), console=console, transient=True):
171
168
  proc = await asyncio.create_subprocess_exec(
172
169
  "docker", "build",
173
170
  "-t", image_name,
174
- "-f", str(dockerfile),
175
- str(fleet_root), # Build context is repo root
171
+ str(agent_path), # Build context is agent directory
176
172
  stdout=asyncio.subprocess.PIPE,
177
173
  stderr=asyncio.subprocess.PIPE,
178
174
  )
@@ -423,7 +419,7 @@ class AgentOrchestrator:
423
419
  env.update(self.config.api_keys)
424
420
 
425
421
  proc = await asyncio.create_subprocess_exec(
426
- "python", str(agent_script),
422
+ sys.executable, str(agent_script),
427
423
  stdout=asyncio.subprocess.PIPE,
428
424
  stderr=asyncio.subprocess.PIPE,
429
425
  env=env,
@@ -27,7 +27,7 @@ from .exceptions import (
27
27
  try:
28
28
  from . import __version__
29
29
  except ImportError:
30
- __version__ = "0.2.84"
30
+ __version__ = "0.2.86"
31
31
 
32
32
  logger = logging.getLogger(__name__)
33
33
 
@@ -0,0 +1,440 @@
1
+ """Playwright browser control utilities.
2
+
3
+ Provides PlaywrightComputer class for browser automation with:
4
+ - Mouse actions (click, move, drag, scroll)
5
+ - Keyboard actions (type, key combinations)
6
+ - Screenshot capture
7
+ - Normalized coordinate support (0-1000 range)
8
+
9
+ Key mapping follows the action spec convention for cross-platform compatibility.
10
+ """
11
+
12
+ import asyncio
13
+ import logging
14
+ from typing import List, Optional, Tuple
15
+
16
+ from playwright.async_api import async_playwright, Page, Browser, BrowserContext
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ # =============================================================================
22
+ # Key Mapping - Action spec keys to Playwright keys
23
+ # =============================================================================
24
+
25
+ PLAYWRIGHT_KEY_MAP = {
26
+ # Common keys
27
+ "enter": "Enter", "return": "Enter", "tab": "Tab",
28
+ "escape": "Escape", "esc": "Escape", "space": " ",
29
+ "backspace": "Backspace", "delete": "Delete", "insert": "Insert",
30
+
31
+ # Modifiers
32
+ "alt": "Alt", "alt_left": "Alt", "alt_right": "Alt",
33
+ "control": "Control", "control_left": "Control", "control_right": "Control",
34
+ "ctrl": "Control", "ctrl_left": "Control", "ctrl_right": "Control",
35
+ "shift": "Shift", "shift_left": "Shift", "shift_right": "Shift",
36
+ "caps_lock": "CapsLock", "capslock": "CapsLock",
37
+ "meta": "Meta", "meta_left": "Meta", "meta_right": "Meta",
38
+ "command": "Meta", "cmd": "Meta", "super": "Meta", "win": "Meta", "windows": "Meta",
39
+ "num_lock": "NumLock", "numlock": "NumLock",
40
+ "scroll_lock": "ScrollLock", "scrolllock": "ScrollLock",
41
+
42
+ # Navigation
43
+ "arrow_down": "ArrowDown", "arrow_up": "ArrowUp",
44
+ "arrow_left": "ArrowLeft", "arrow_right": "ArrowRight",
45
+ "down": "ArrowDown", "up": "ArrowUp", "left": "ArrowLeft", "right": "ArrowRight",
46
+ "end": "End", "home": "Home",
47
+ "page_down": "PageDown", "pagedown": "PageDown",
48
+ "page_up": "PageUp", "pageup": "PageUp",
49
+
50
+ # Function keys
51
+ **{f"f{i}": f"F{i}" for i in range(1, 21)},
52
+
53
+ # Symbols
54
+ "backquote": "`", "grave": "`", "tilde": "`",
55
+ "backslash": "\\", "bracket_left": "[", "bracketleft": "[",
56
+ "bracket_right": "]", "bracketright": "]",
57
+ "comma": ",", "double_quote": '"', "doublequote": '"',
58
+ "equal": "=", "equals": "=", "minus": "-", "dash": "-",
59
+ "period": ".", "dot": ".", "quote": "'", "apostrophe": "'",
60
+ "semicolon": ";", "slash": "/", "forward_slash": "/", "forwardslash": "/",
61
+
62
+ # Numpad
63
+ **{f"numpad_{i}": f"Numpad{i}" for i in range(10)},
64
+ **{f"numpad{i}": f"Numpad{i}" for i in range(10)},
65
+ "numpad_add": "NumpadAdd", "numpadadd": "NumpadAdd",
66
+ "numpad_subtract": "NumpadSubtract", "numpadsubtract": "NumpadSubtract",
67
+ "numpad_multiply": "NumpadMultiply", "numpadmultiply": "NumpadMultiply",
68
+ "numpad_divide": "NumpadDivide", "numpaddivide": "NumpadDivide",
69
+ "numpad_decimal": "NumpadDecimal", "numpaddecimal": "NumpadDecimal",
70
+ "numpad_enter": "NumpadEnter", "numpadenter": "NumpadEnter",
71
+
72
+ # Media
73
+ "audio_volume_mute": "AudioVolumeMute",
74
+ "audio_volume_down": "AudioVolumeDown",
75
+ "audio_volume_up": "AudioVolumeUp",
76
+ "media_track_next": "MediaTrackNext",
77
+ "media_track_previous": "MediaTrackPrevious",
78
+ "media_stop": "MediaStop",
79
+ "media_play_pause": "MediaPlayPause",
80
+
81
+ # Other
82
+ "print_screen": "PrintScreen", "printscreen": "PrintScreen",
83
+ "pause": "Pause", "context_menu": "ContextMenu", "contextmenu": "ContextMenu",
84
+ "help": "Help",
85
+ }
86
+
87
+ MODIFIER_KEYS = {
88
+ "Alt", "Control", "Shift", "Meta",
89
+ "alt", "alt_left", "alt_right",
90
+ "control", "control_left", "control_right", "ctrl", "ctrl_left", "ctrl_right",
91
+ "shift", "shift_left", "shift_right",
92
+ "meta", "meta_left", "meta_right", "command", "cmd", "super", "win", "windows",
93
+ }
94
+
95
+ # Key specification for tool docstrings
96
+ KEY_SPEC = (
97
+ "Key specification: * Common: enter, tab, escape, space, backspace, delete "
98
+ "* Modifiers: alt_left, control_left, control_right, shift_left, caps_lock, meta "
99
+ "* Navigation: arrow_down, arrow_right, end, home, page_down "
100
+ "* Function: f1 to f12 "
101
+ "* Alphanumeric: key_a to key_z, digit_0 to digit_9 "
102
+ "* Symbols: backquote, backslash, bracket_left, bracket_right, comma, double_quote, "
103
+ "equal, minus, period, quote, semicolon, slash "
104
+ "* Numpad: numpad_0 to numpad_9, numpad_add, numpad_divide, numpad_enter, numpad_multiply"
105
+ )
106
+
107
+
108
+ def map_key(key: str) -> str:
109
+ """Map action spec key name to Playwright key name.
110
+
111
+ Args:
112
+ key: Key name in action spec format (e.g., "key_a", "control_left")
113
+
114
+ Returns:
115
+ Playwright key name (e.g., "a", "Control")
116
+ """
117
+ k = key.lower().strip()
118
+ if k in PLAYWRIGHT_KEY_MAP:
119
+ return PLAYWRIGHT_KEY_MAP[k]
120
+ if k.startswith("key_") and len(k) == 5:
121
+ return k[4].lower()
122
+ if k.startswith("digit_") and len(k) == 7:
123
+ return k[6]
124
+ if len(key) == 1:
125
+ return key
126
+ return key
127
+
128
+
129
+ def is_modifier(key: str) -> bool:
130
+ """Check if a key is a modifier key.
131
+
132
+ Args:
133
+ key: Key name to check
134
+
135
+ Returns:
136
+ True if the key is a modifier (Alt, Control, Shift, Meta)
137
+ """
138
+ return key.lower().strip() in MODIFIER_KEYS or map_key(key) in {"Alt", "Control", "Shift", "Meta"}
139
+
140
+
141
+ # =============================================================================
142
+ # PlaywrightComputer - Browser control
143
+ # =============================================================================
144
+
145
+ class PlaywrightComputer:
146
+ """Browser control via Playwright.
147
+
148
+ Provides a high-level interface for browser automation:
149
+ - Mouse actions with optional visual highlighting
150
+ - Keyboard input with proper modifier handling
151
+ - Screenshot capture
152
+ - Automatic page load waiting
153
+
154
+ Args:
155
+ screen_size: Tuple of (width, height) for viewport
156
+ initial_url: URL to navigate to on start
157
+ headless: Run browser without visible window
158
+ highlight_mouse: Show visual indicator for mouse actions (useful for debugging)
159
+
160
+ Example:
161
+ computer = PlaywrightComputer(
162
+ screen_size=(1366, 768),
163
+ initial_url="https://example.com",
164
+ headless=False,
165
+ highlight_mouse=True,
166
+ )
167
+ await computer.start()
168
+ await computer.mouse_click(683, 384) # Click center
169
+ screenshot = await computer.screenshot()
170
+ await computer.stop()
171
+ """
172
+
173
+ def __init__(
174
+ self,
175
+ screen_size: Tuple[int, int],
176
+ initial_url: str,
177
+ headless: bool = True,
178
+ highlight_mouse: bool = False,
179
+ ):
180
+ self._screen_size = screen_size
181
+ self._initial_url = initial_url
182
+ self._headless = headless
183
+ self._highlight_mouse = highlight_mouse
184
+ self._playwright = None
185
+ self._browser: Optional[Browser] = None
186
+ self._context: Optional[BrowserContext] = None
187
+ self._page: Optional[Page] = None
188
+
189
+ @property
190
+ def width(self) -> int:
191
+ """Viewport width in pixels."""
192
+ return self._screen_size[0]
193
+
194
+ @property
195
+ def height(self) -> int:
196
+ """Viewport height in pixels."""
197
+ return self._screen_size[1]
198
+
199
+ @property
200
+ def current_url(self) -> str:
201
+ """Current page URL."""
202
+ return self._page.url if self._page else ""
203
+
204
+ async def _handle_new_page(self, new_page: Page):
205
+ """Handle new tab by redirecting to current page."""
206
+ new_url = new_page.url
207
+ await new_page.close()
208
+ await self._page.goto(new_url)
209
+
210
+ async def start(self):
211
+ """Start the browser and navigate to initial URL."""
212
+ logger.info(f"Starting browser (headless={self._headless})...")
213
+ self._playwright = await async_playwright().start()
214
+ self._browser = await self._playwright.chromium.launch(
215
+ headless=self._headless,
216
+ args=[
217
+ "--no-sandbox",
218
+ "--disable-extensions",
219
+ "--disable-file-system",
220
+ "--disable-plugins",
221
+ "--disable-dev-shm-usage",
222
+ "--disable-background-networking",
223
+ "--disable-default-apps",
224
+ "--disable-sync",
225
+ ],
226
+ )
227
+ self._context = await self._browser.new_context(
228
+ viewport={"width": self._screen_size[0], "height": self._screen_size[1]}
229
+ )
230
+ self._page = await self._context.new_page()
231
+ self._context.on("page", self._handle_new_page)
232
+ await self._page.goto(self._initial_url)
233
+ await self._page.wait_for_load_state()
234
+ logger.info(f"Browser ready: {self._initial_url}")
235
+
236
+ async def stop(self):
237
+ """Stop the browser and clean up resources."""
238
+ if self._context:
239
+ await self._context.close()
240
+ if self._browser:
241
+ try:
242
+ await self._browser.close()
243
+ except Exception:
244
+ pass
245
+ if self._playwright:
246
+ await self._playwright.stop()
247
+ logger.info("Browser stopped")
248
+
249
+ async def screenshot(self) -> bytes:
250
+ """Take a screenshot of the current viewport.
251
+
252
+ Returns:
253
+ PNG image data as bytes
254
+ """
255
+ await self._page.wait_for_load_state()
256
+ await asyncio.sleep(0.5)
257
+ return await self._page.screenshot(type="png", full_page=False)
258
+
259
+ async def _highlight(self, x: int, y: int):
260
+ """Show visual highlight at mouse position (for debugging)."""
261
+ if not self._highlight_mouse:
262
+ return
263
+ await self._page.evaluate(f"""
264
+ () => {{
265
+ const div = document.createElement('div');
266
+ div.style.cssText = 'position:fixed;width:20px;height:20px;border-radius:50%;border:4px solid red;pointer-events:none;z-index:9999;left:{x-10}px;top:{y-10}px;';
267
+ document.body.appendChild(div);
268
+ setTimeout(() => div.remove(), 2000);
269
+ }}
270
+ """)
271
+ await asyncio.sleep(1)
272
+
273
+ # -------------------------------------------------------------------------
274
+ # Mouse actions
275
+ # -------------------------------------------------------------------------
276
+
277
+ async def mouse_click(self, x: int, y: int, button: str = "left", repeats: int = 1) -> None:
278
+ """Click at position.
279
+
280
+ Args:
281
+ x: X coordinate in pixels
282
+ y: Y coordinate in pixels
283
+ button: Mouse button ('left', 'middle', 'right')
284
+ repeats: Number of clicks (2 for double-click)
285
+ """
286
+ await self._highlight(x, y)
287
+ for _ in range(repeats):
288
+ await self._page.mouse.click(x, y, button=button)
289
+ await self._page.wait_for_load_state()
290
+
291
+ async def mouse_move(self, x: int, y: int) -> None:
292
+ """Move mouse to position.
293
+
294
+ Args:
295
+ x: X coordinate in pixels
296
+ y: Y coordinate in pixels
297
+ """
298
+ await self._highlight(x, y)
299
+ await self._page.mouse.move(x, y)
300
+ await self._page.wait_for_load_state()
301
+
302
+ async def mouse_down(self, button: str = "left") -> None:
303
+ """Press mouse button down.
304
+
305
+ Args:
306
+ button: Mouse button ('left', 'middle', 'right')
307
+ """
308
+ await self._page.mouse.down(button=button)
309
+
310
+ async def mouse_up(self, button: str = "left") -> None:
311
+ """Release mouse button.
312
+
313
+ Args:
314
+ button: Mouse button ('left', 'middle', 'right')
315
+ """
316
+ await self._page.mouse.up(button=button)
317
+ await self._page.wait_for_load_state()
318
+
319
+ async def mouse_scroll(self, dx: int, dy: int) -> None:
320
+ """Scroll the mouse wheel.
321
+
322
+ Args:
323
+ dx: Horizontal scroll amount in pixels
324
+ dy: Vertical scroll amount in pixels
325
+ """
326
+ await self._page.mouse.wheel(dx, dy)
327
+ await self._page.wait_for_load_state()
328
+
329
+ async def mouse_drag(
330
+ self,
331
+ x_start: int,
332
+ y_start: int,
333
+ x_end: int,
334
+ y_end: int,
335
+ button: str = "left",
336
+ ) -> None:
337
+ """Drag from one position to another.
338
+
339
+ Args:
340
+ x_start: Starting X coordinate
341
+ y_start: Starting Y coordinate
342
+ x_end: Ending X coordinate
343
+ y_end: Ending Y coordinate
344
+ button: Mouse button to hold during drag
345
+ """
346
+ await self._highlight(x_start, y_start)
347
+ await self._page.mouse.move(x_start, y_start)
348
+ await self._page.mouse.down(button=button)
349
+ await self._highlight(x_end, y_end)
350
+ await self._page.mouse.move(x_end, y_end)
351
+ await self._page.mouse.up(button=button)
352
+ await self._page.wait_for_load_state()
353
+
354
+ # -------------------------------------------------------------------------
355
+ # Keyboard actions
356
+ # -------------------------------------------------------------------------
357
+
358
+ async def type_text(self, text: str, press_enter: bool = False) -> None:
359
+ """Type text using the keyboard.
360
+
361
+ Args:
362
+ text: Text to type
363
+ press_enter: Whether to press Enter after typing
364
+ """
365
+ await self._page.keyboard.type(text)
366
+ await self._page.wait_for_load_state()
367
+ if press_enter:
368
+ await self._page.keyboard.press("Enter")
369
+ await self._page.wait_for_load_state()
370
+
371
+ async def key_combination(self, keys: List[str]) -> None:
372
+ """Press a key combination (e.g., Ctrl+C).
373
+
374
+ Handles modifiers properly - holds them down while pressing other keys.
375
+
376
+ Args:
377
+ keys: List of keys to press together
378
+ """
379
+ if not keys:
380
+ return
381
+
382
+ modifiers = [map_key(k) for k in keys if is_modifier(k)]
383
+ regular = [map_key(k) for k in keys if not is_modifier(k)]
384
+
385
+ # Press modifiers down
386
+ for mod in modifiers:
387
+ await self._page.keyboard.down(mod)
388
+
389
+ # Press regular keys
390
+ for key in regular:
391
+ await self._page.keyboard.press(key)
392
+
393
+ # If only modifiers, brief pause
394
+ if not regular and modifiers:
395
+ await asyncio.sleep(0.05)
396
+
397
+ # Release modifiers
398
+ for mod in reversed(modifiers):
399
+ await self._page.keyboard.up(mod)
400
+
401
+ await self._page.wait_for_load_state()
402
+
403
+ async def key_down(self, key: str) -> None:
404
+ """Press a key down (without releasing).
405
+
406
+ Args:
407
+ key: Key to press down
408
+ """
409
+ await self._page.keyboard.down(map_key(key))
410
+
411
+ async def key_up(self, key: str) -> None:
412
+ """Release a key.
413
+
414
+ Args:
415
+ key: Key to release
416
+ """
417
+ await self._page.keyboard.up(map_key(key))
418
+ await self._page.wait_for_load_state()
419
+
420
+ # -------------------------------------------------------------------------
421
+ # Utilities
422
+ # -------------------------------------------------------------------------
423
+
424
+ async def wait(self, seconds: int) -> None:
425
+ """Wait for a number of seconds.
426
+
427
+ Args:
428
+ seconds: Number of seconds to wait
429
+ """
430
+ await asyncio.sleep(seconds)
431
+
432
+ async def goto(self, url: str) -> None:
433
+ """Navigate to a URL.
434
+
435
+ Args:
436
+ url: URL to navigate to
437
+ """
438
+ await self._page.goto(url)
439
+ await self._page.wait_for_load_state()
440
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fleet-python
3
- Version: 0.2.84
3
+ Version: 0.2.86
4
4
  Summary: Python SDK for Fleet environments
5
5
  Author-email: Fleet AI <nic@fleet.so>
6
6
  License: Apache-2.0
@@ -45,6 +45,7 @@ Requires-Dist: playwright>=1.40.0; extra == "playwright"
45
45
  Provides-Extra: eval
46
46
  Requires-Dist: aiohttp>=3.9.0; extra == "eval"
47
47
  Requires-Dist: google-genai>=1.0.0; extra == "eval"
48
+ Requires-Dist: mcp>=0.1.0; python_version >= "3.10" and extra == "eval"
48
49
  Dynamic: license-file
49
50
 
50
51
  # Fleet SDK
@@ -60,9 +60,13 @@ fleet/agent/__init__.py
60
60
  fleet/agent/orchestrator.py
61
61
  fleet/agent/types.py
62
62
  fleet/agent/utils.py
63
+ fleet/agent/gemini_cua/Dockerfile
63
64
  fleet/agent/gemini_cua/__init__.py
64
65
  fleet/agent/gemini_cua/agent.py
65
66
  fleet/agent/gemini_cua/mcp_server.py
67
+ fleet/agent/gemini_cua/playwright_utils.py
68
+ fleet/agent/gemini_cua/requirements.txt
69
+ fleet/agent/gemini_cua/start.sh
66
70
  fleet/env/__init__.py
67
71
  fleet/env/client.py
68
72
  fleet/eval/__init__.py
@@ -26,5 +26,8 @@ rich>=10.0.0
26
26
  aiohttp>=3.9.0
27
27
  google-genai>=1.0.0
28
28
 
29
+ [eval:python_version >= "3.10"]
30
+ mcp>=0.1.0
31
+
29
32
  [playwright]
30
33
  playwright>=1.40.0
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
  [project]
6
6
  name = "fleet-python"
7
7
 
8
- version = "0.2.84"
8
+ version = "0.2.86"
9
9
  description = "Python SDK for Fleet environments"
10
10
  authors = [
11
11
  {name = "Fleet AI", email = "nic@fleet.so"},
@@ -59,6 +59,7 @@ playwright = [
59
59
  eval = [
60
60
  "aiohttp>=3.9.0",
61
61
  "google-genai>=1.0.0",
62
+ "mcp>=0.1.0; python_version >= '3.10'"
62
63
  ]
63
64
 
64
65
  [project.urls]
@@ -70,6 +71,9 @@ Issues = "https://github.com/fleet-ai/fleet-sdk/issues"
70
71
  [tool.setuptools.packages.find]
71
72
  where = ["."]
72
73
 
74
+ [tool.setuptools.package-data]
75
+ "fleet.agent.gemini_cua" = ["Dockerfile", "requirements.txt", "start.sh", "playwright_utils.py"]
76
+
73
77
  [tool.black]
74
78
  line-length = 88
75
79
  target-version = ['py39']
File without changes
File without changes
File without changes