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.
- {fleet_python-0.2.84/fleet_python.egg-info → fleet_python-0.2.86}/PKG-INFO +2 -1
- {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/__init__.py +1 -1
- {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/_async/__init__.py +1 -1
- {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/_async/base.py +1 -1
- fleet_python-0.2.86/fleet/agent/gemini_cua/Dockerfile +44 -0
- fleet_python-0.2.86/fleet/agent/gemini_cua/requirements.txt +4 -0
- fleet_python-0.2.86/fleet/agent/gemini_cua/start.sh +31 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/agent/orchestrator.py +4 -8
- {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/base.py +1 -1
- fleet_python-0.2.86/fleet/utils/playwright.py +440 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86/fleet_python.egg-info}/PKG-INFO +2 -1
- {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet_python.egg-info/SOURCES.txt +4 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet_python.egg-info/requires.txt +3 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/pyproject.toml +5 -1
- {fleet_python-0.2.84 → fleet_python-0.2.86}/LICENSE +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/README.md +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/examples/diff_example.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/examples/dsl_example.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/examples/example.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/examples/exampleResume.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/examples/example_account.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/examples/example_action_log.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/examples/example_client.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/examples/example_mcp_anthropic.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/examples/example_mcp_openai.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/examples/example_sync.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/examples/example_task.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/examples/example_tasks.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/examples/example_verifier.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/examples/export_tasks.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/examples/fetch_tasks.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/examples/gemini_example.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/examples/import_tasks.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/examples/iterate_verifiers.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/examples/json_tasks_example.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/examples/nova_act_example.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/examples/openai_example.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/examples/openai_simple_example.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/examples/query_builder_example.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/examples/quickstart.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/examples/test_cdp_logging.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/_async/client.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/_async/env/__init__.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/_async/env/client.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/_async/exceptions.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/_async/global_client.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/_async/instance/__init__.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/_async/instance/base.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/_async/instance/client.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/_async/models.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/_async/resources/__init__.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/_async/resources/base.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/_async/resources/browser.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/_async/resources/mcp.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/_async/resources/sqlite.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/_async/tasks.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/_async/verifiers/__init__.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/_async/verifiers/bundler.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/_async/verifiers/verifier.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/agent/__init__.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/agent/gemini_cua/__init__.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/agent/gemini_cua/agent.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/agent/gemini_cua/mcp_server.py +0 -0
- /fleet_python-0.2.84/fleet/utils/playwright.py → /fleet_python-0.2.86/fleet/agent/gemini_cua/playwright_utils.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/agent/types.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/agent/utils.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/cli.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/client.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/config.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/env/__init__.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/env/client.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/eval/__init__.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/eval/uploader.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/exceptions.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/global_client.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/instance/__init__.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/instance/base.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/instance/client.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/instance/models.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/models.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/proxy/__init__.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/proxy/proxy.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/proxy/whitelist.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/resources/__init__.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/resources/base.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/resources/browser.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/resources/mcp.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/resources/sqlite.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/tasks.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/types.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/utils/__init__.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/utils/http_logging.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/utils/logging.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/verifiers/__init__.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/verifiers/bundler.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/verifiers/code.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/verifiers/db.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/verifiers/decorator.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/verifiers/parse.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/verifiers/sql_differ.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet/verifiers/verifier.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet_python.egg-info/dependency_links.txt +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet_python.egg-info/entry_points.txt +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/fleet_python.egg-info/top_level.txt +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/scripts/fix_sync_imports.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/scripts/unasync.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/setup.cfg +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/tests/__init__.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/tests/test_app_method.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/tests/test_expect_only.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/tests/test_instance_dispatch.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/tests/test_sqlite_resource_dual_mode.py +0 -0
- {fleet_python-0.2.84 → fleet_python-0.2.86}/tests/test_sqlite_shared_memory_behavior.py +0 -0
- {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.
|
|
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
|
|
@@ -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,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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
422
|
+
sys.executable, str(agent_script),
|
|
427
423
|
stdout=asyncio.subprocess.PIPE,
|
|
428
424
|
stderr=asyncio.subprocess.PIPE,
|
|
429
425
|
env=env,
|
|
@@ -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.
|
|
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
|
|
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "fleet-python"
|
|
7
7
|
|
|
8
|
-
version = "0.2.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|