quash-mcp 0.2.0__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 quash-mcp might be problematic. Click here for more details.
- quash_mcp/__init__.py +1 -0
- quash_mcp/__main__.py +10 -0
- quash_mcp/backend_client.py +203 -0
- quash_mcp/server.py +399 -0
- quash_mcp/state.py +137 -0
- quash_mcp/tools/__init__.py +9 -0
- quash_mcp/tools/build.py +739 -0
- quash_mcp/tools/build_old.py +185 -0
- quash_mcp/tools/configure.py +140 -0
- quash_mcp/tools/connect.py +153 -0
- quash_mcp/tools/execute.py +177 -0
- quash_mcp/tools/runsuite.py +209 -0
- quash_mcp/tools/usage.py +31 -0
- quash_mcp-0.2.0.dist-info/METADATA +271 -0
- quash_mcp-0.2.0.dist-info/RECORD +17 -0
- quash_mcp-0.2.0.dist-info/WHEEL +4 -0
- quash_mcp-0.2.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Build tool - Setup dependencies for Quash MCP.
|
|
3
|
+
Checks and installs required dependencies on the user's machine.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import sys
|
|
7
|
+
import subprocess
|
|
8
|
+
import shutil
|
|
9
|
+
import platform
|
|
10
|
+
from typing import Dict, Any, Tuple
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def check_python_version() -> Tuple[bool, str]:
|
|
14
|
+
"""Check if Python version is >= 3.11."""
|
|
15
|
+
version = sys.version_info
|
|
16
|
+
if version.major >= 3 and version.minor >= 11:
|
|
17
|
+
return True, f"✓ Python {version.major}.{version.minor}.{version.micro}"
|
|
18
|
+
return False, f"✗ Python {version.major}.{version.minor}.{version.micro} (requires >= 3.11)"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def check_adb() -> Tuple[bool, str]:
|
|
22
|
+
"""Check if ADB is installed."""
|
|
23
|
+
if shutil.which("adb"):
|
|
24
|
+
try:
|
|
25
|
+
result = subprocess.run(
|
|
26
|
+
["adb", "version"],
|
|
27
|
+
capture_output=True,
|
|
28
|
+
text=True,
|
|
29
|
+
timeout=5
|
|
30
|
+
)
|
|
31
|
+
version_line = result.stdout.split('\n')[0]
|
|
32
|
+
return True, f"✓ ADB installed ({version_line})"
|
|
33
|
+
except Exception:
|
|
34
|
+
return True, "✓ ADB installed"
|
|
35
|
+
return False, "✗ ADB not found"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def install_adb() -> Tuple[bool, str]:
|
|
39
|
+
"""Attempt to install ADB based on OS."""
|
|
40
|
+
system = platform.system()
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
if system == "Darwin": # macOS
|
|
44
|
+
# Check if Homebrew is available
|
|
45
|
+
if shutil.which("brew"):
|
|
46
|
+
subprocess.run(
|
|
47
|
+
["brew", "install", "android-platform-tools"],
|
|
48
|
+
check=True,
|
|
49
|
+
capture_output=True
|
|
50
|
+
)
|
|
51
|
+
return True, "✓ ADB installed via Homebrew"
|
|
52
|
+
else:
|
|
53
|
+
return False, "✗ Homebrew not found. Install from: https://brew.sh/"
|
|
54
|
+
|
|
55
|
+
elif system == "Linux":
|
|
56
|
+
# Try apt-get
|
|
57
|
+
if shutil.which("apt-get"):
|
|
58
|
+
subprocess.run(
|
|
59
|
+
["sudo", "apt-get", "install", "-y", "adb"],
|
|
60
|
+
check=True,
|
|
61
|
+
capture_output=True
|
|
62
|
+
)
|
|
63
|
+
return True, "✓ ADB installed via apt-get"
|
|
64
|
+
# Try dnf
|
|
65
|
+
elif shutil.which("dnf"):
|
|
66
|
+
subprocess.run(
|
|
67
|
+
["sudo", "dnf", "install", "-y", "android-tools"],
|
|
68
|
+
check=True,
|
|
69
|
+
capture_output=True
|
|
70
|
+
)
|
|
71
|
+
return True, "✓ ADB installed via dnf"
|
|
72
|
+
else:
|
|
73
|
+
return False, "✗ Package manager not found. Install ADB manually."
|
|
74
|
+
|
|
75
|
+
elif system == "Windows":
|
|
76
|
+
return False, "✗ Please install ADB manually from: https://developer.android.com/tools/releases/platform-tools"
|
|
77
|
+
|
|
78
|
+
else:
|
|
79
|
+
return False, f"✗ Unsupported OS: {system}"
|
|
80
|
+
|
|
81
|
+
except subprocess.CalledProcessError as e:
|
|
82
|
+
return False, f"✗ Installation failed: {str(e)}"
|
|
83
|
+
except Exception as e:
|
|
84
|
+
return False, f"✗ Error: {str(e)}"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def check_mahoraga() -> Tuple[bool, str]:
|
|
88
|
+
"""Check if Quash package is available."""
|
|
89
|
+
try:
|
|
90
|
+
import mahoraga
|
|
91
|
+
return True, "✓ Quash package ready"
|
|
92
|
+
except ImportError:
|
|
93
|
+
return False, "✗ Quash package not installed"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def install_mahoraga() -> Tuple[bool, str]:
|
|
97
|
+
"""Install Quash package."""
|
|
98
|
+
try:
|
|
99
|
+
# Get the path to mahoraga directory
|
|
100
|
+
import os
|
|
101
|
+
mahoraga_path = os.path.join(
|
|
102
|
+
os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))),
|
|
103
|
+
"mahoraga"
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
if os.path.exists(mahoraga_path):
|
|
107
|
+
subprocess.run(
|
|
108
|
+
[sys.executable, "-m", "pip", "install", "-e", mahoraga_path],
|
|
109
|
+
check=True,
|
|
110
|
+
capture_output=True
|
|
111
|
+
)
|
|
112
|
+
return True, "✓ Quash installed successfully"
|
|
113
|
+
else:
|
|
114
|
+
return False, f"✗ Quash directory not found at: {mahoraga_path}"
|
|
115
|
+
except subprocess.CalledProcessError as e:
|
|
116
|
+
return False, f"✗ Installation failed: {e.stderr.decode() if e.stderr else str(e)}"
|
|
117
|
+
except Exception as e:
|
|
118
|
+
return False, f"✗ Error: {str(e)}"
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
async def build() -> Dict[str, Any]:
|
|
122
|
+
"""
|
|
123
|
+
Setup and verify all dependencies required for Quash.
|
|
124
|
+
Auto-installs missing dependencies where possible.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Dict with status and details of all dependencies
|
|
128
|
+
"""
|
|
129
|
+
details = {}
|
|
130
|
+
all_ok = True
|
|
131
|
+
|
|
132
|
+
# Check Python version
|
|
133
|
+
python_ok, python_msg = check_python_version()
|
|
134
|
+
details["python"] = python_msg
|
|
135
|
+
if not python_ok:
|
|
136
|
+
all_ok = False
|
|
137
|
+
|
|
138
|
+
# Check and install ADB
|
|
139
|
+
adb_ok, adb_msg = check_adb()
|
|
140
|
+
if not adb_ok:
|
|
141
|
+
# Try to auto-install
|
|
142
|
+
install_ok, install_msg = install_adb()
|
|
143
|
+
details["adb"] = install_msg
|
|
144
|
+
if not install_ok:
|
|
145
|
+
all_ok = False
|
|
146
|
+
else:
|
|
147
|
+
details["adb"] = adb_msg
|
|
148
|
+
|
|
149
|
+
# Check and install Quash
|
|
150
|
+
mahoraga_ok, mahoraga_msg = check_mahoraga()
|
|
151
|
+
if not mahoraga_ok:
|
|
152
|
+
# Try to auto-install
|
|
153
|
+
install_ok, install_msg = install_mahoraga()
|
|
154
|
+
details["mahoraga"] = install_msg
|
|
155
|
+
if not install_ok:
|
|
156
|
+
all_ok = False
|
|
157
|
+
else:
|
|
158
|
+
details["mahoraga"] = mahoraga_msg
|
|
159
|
+
|
|
160
|
+
# Check portal APK (just verify it exists in mahoraga)
|
|
161
|
+
try:
|
|
162
|
+
from mahoraga.portal import use_portal_apk
|
|
163
|
+
details["portal_apk"] = "✓ Portal APK available"
|
|
164
|
+
except Exception as e:
|
|
165
|
+
details["portal_apk"] = f"✗ Portal APK not found: {str(e)}"
|
|
166
|
+
all_ok = False
|
|
167
|
+
|
|
168
|
+
# Determine overall status
|
|
169
|
+
if all_ok:
|
|
170
|
+
status = "success"
|
|
171
|
+
message = "✅ All dependencies ready! You can now use Quash."
|
|
172
|
+
else:
|
|
173
|
+
failed_items = [k for k, v in details.items() if v.startswith("✗")]
|
|
174
|
+
if len(failed_items) == len(details):
|
|
175
|
+
status = "failed"
|
|
176
|
+
message = f"❌ Setup failed. Missing: {', '.join(failed_items)}"
|
|
177
|
+
else:
|
|
178
|
+
status = "partial"
|
|
179
|
+
message = f"⚠️ Partially ready. Issues with: {', '.join(failed_items)}"
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
"status": status,
|
|
183
|
+
"details": details,
|
|
184
|
+
"message": message
|
|
185
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configure tool - Manage agent configuration parameters.
|
|
3
|
+
Allows users to set and update Quash agent execution parameters.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Dict, Any, Optional
|
|
7
|
+
from ..state import get_state
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# Valid configuration parameters and their types
|
|
11
|
+
VALID_PARAMS = {
|
|
12
|
+
"quash_api_key": str,
|
|
13
|
+
"model": str,
|
|
14
|
+
"temperature": float,
|
|
15
|
+
"max_steps": int,
|
|
16
|
+
"vision": bool,
|
|
17
|
+
"reasoning": bool,
|
|
18
|
+
"reflection": bool,
|
|
19
|
+
"debug": bool,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def validate_config(config: Dict[str, Any]) -> tuple[bool, Optional[str]]:
|
|
24
|
+
"""
|
|
25
|
+
Validate configuration parameters.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
(is_valid, error_message)
|
|
29
|
+
"""
|
|
30
|
+
for key, value in config.items():
|
|
31
|
+
if key not in VALID_PARAMS:
|
|
32
|
+
return False, f"Invalid parameter: '{key}'. Valid parameters are: {', '.join(VALID_PARAMS.keys())}"
|
|
33
|
+
|
|
34
|
+
expected_type = VALID_PARAMS[key]
|
|
35
|
+
if not isinstance(value, expected_type):
|
|
36
|
+
return False, f"Parameter '{key}' must be of type {expected_type.__name__}, got {type(value).__name__}"
|
|
37
|
+
|
|
38
|
+
# Validate specific constraints
|
|
39
|
+
if "temperature" in config:
|
|
40
|
+
temp = config["temperature"]
|
|
41
|
+
if not 0 <= temp <= 2:
|
|
42
|
+
return False, "temperature must be between 0 and 2"
|
|
43
|
+
|
|
44
|
+
if "max_steps" in config:
|
|
45
|
+
steps = config["max_steps"]
|
|
46
|
+
if steps < 1:
|
|
47
|
+
return False, "max_steps must be at least 1"
|
|
48
|
+
|
|
49
|
+
if "model" in config:
|
|
50
|
+
model = config["model"]
|
|
51
|
+
# Basic model name validation
|
|
52
|
+
if not model or not isinstance(model, str) or len(model) < 3:
|
|
53
|
+
return False, "Invalid model name"
|
|
54
|
+
|
|
55
|
+
return True, None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
async def configure(
|
|
59
|
+
quash_api_key: Optional[str] = None,
|
|
60
|
+
model: Optional[str] = None,
|
|
61
|
+
temperature: Optional[float] = None,
|
|
62
|
+
max_steps: Optional[int] = None,
|
|
63
|
+
vision: Optional[bool] = None,
|
|
64
|
+
reasoning: Optional[bool] = None,
|
|
65
|
+
reflection: Optional[bool] = None,
|
|
66
|
+
debug: Optional[bool] = None,
|
|
67
|
+
) -> Dict[str, Any]:
|
|
68
|
+
"""
|
|
69
|
+
Configure agent execution parameters.
|
|
70
|
+
Only updates parameters that are provided (not None).
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
quash_api_key: Quash API key for authentication and access
|
|
74
|
+
model: LLM model name (e.g., "openai/gpt-4o")
|
|
75
|
+
temperature: Temperature for LLM (0-2)
|
|
76
|
+
max_steps: Maximum number of execution steps
|
|
77
|
+
vision: Enable vision capabilities (screenshots)
|
|
78
|
+
reasoning: Enable planning with reasoning
|
|
79
|
+
reflection: Enable reflection for self-improvement
|
|
80
|
+
debug: Enable verbose debug logging
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Dict with configuration status and current settings
|
|
84
|
+
"""
|
|
85
|
+
state = get_state()
|
|
86
|
+
|
|
87
|
+
# Collect provided parameters
|
|
88
|
+
updates = {}
|
|
89
|
+
if quash_api_key is not None:
|
|
90
|
+
updates["api_key"] = quash_api_key
|
|
91
|
+
if model is not None:
|
|
92
|
+
updates["model"] = model
|
|
93
|
+
if temperature is not None:
|
|
94
|
+
updates["temperature"] = temperature
|
|
95
|
+
if max_steps is not None:
|
|
96
|
+
updates["max_steps"] = max_steps
|
|
97
|
+
if vision is not None:
|
|
98
|
+
updates["vision"] = vision
|
|
99
|
+
if reasoning is not None:
|
|
100
|
+
updates["reasoning"] = reasoning
|
|
101
|
+
if reflection is not None:
|
|
102
|
+
updates["reflection"] = reflection
|
|
103
|
+
if debug is not None:
|
|
104
|
+
updates["debug"] = debug
|
|
105
|
+
|
|
106
|
+
# If no updates provided, just return current config
|
|
107
|
+
if not updates:
|
|
108
|
+
return {
|
|
109
|
+
"status": "no_changes",
|
|
110
|
+
"current_config": state.get_config_summary(),
|
|
111
|
+
"message": "ℹ️ No parameters provided. Current configuration unchanged."
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
# Validate updates (map api_key back to quash_api_key for validation)
|
|
115
|
+
validation_updates = updates.copy()
|
|
116
|
+
if "api_key" in validation_updates:
|
|
117
|
+
validation_updates["quash_api_key"] = validation_updates.pop("api_key")
|
|
118
|
+
|
|
119
|
+
is_valid, error_msg = validate_config(validation_updates)
|
|
120
|
+
if not is_valid:
|
|
121
|
+
return {
|
|
122
|
+
"status": "error",
|
|
123
|
+
"message": f"❌ Configuration error: {error_msg}",
|
|
124
|
+
"current_config": state.get_config_summary()
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
# Apply updates
|
|
128
|
+
state.update_config(**updates)
|
|
129
|
+
|
|
130
|
+
# Prepare response
|
|
131
|
+
updated_keys = list(updates.keys())
|
|
132
|
+
if "api_key" in updated_keys:
|
|
133
|
+
updated_keys[updated_keys.index("api_key")] = "quash_api_key"
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
"status": "configured",
|
|
137
|
+
"updated_parameters": updated_keys,
|
|
138
|
+
"current_config": state.get_config_summary(),
|
|
139
|
+
"message": f"✅ Configuration updated: {', '.join(updated_keys)}"
|
|
140
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Connect tool - Manage Android device connectivity.
|
|
3
|
+
Connects to Android devices/emulators and verifies accessibility service.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import subprocess
|
|
7
|
+
from typing import Dict, Any, Optional
|
|
8
|
+
from ..state import get_state
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def list_devices() -> list:
|
|
12
|
+
"""List all connected Android devices."""
|
|
13
|
+
try:
|
|
14
|
+
from adbutils import adb
|
|
15
|
+
devices = adb.list()
|
|
16
|
+
return [{"serial": d.serial, "state": d.state} for d in devices]
|
|
17
|
+
except Exception as e:
|
|
18
|
+
return []
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_device_info(serial: str) -> Optional[Dict[str, str]]:
|
|
22
|
+
"""Get detailed information about a device."""
|
|
23
|
+
try:
|
|
24
|
+
from adbutils import adb
|
|
25
|
+
device = adb.device(serial)
|
|
26
|
+
|
|
27
|
+
# Get device properties
|
|
28
|
+
model = device.prop.get("ro.product.model", "Unknown")
|
|
29
|
+
android_version = device.prop.get("ro.build.version.release", "Unknown")
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
"serial": serial,
|
|
33
|
+
"model": model,
|
|
34
|
+
"android_version": android_version
|
|
35
|
+
}
|
|
36
|
+
except Exception as e:
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def check_portal_service(serial: str) -> bool:
|
|
41
|
+
"""Check if Quash Portal accessibility service is enabled."""
|
|
42
|
+
try:
|
|
43
|
+
from adbutils import adb
|
|
44
|
+
from mahoraga.portal import ping_portal
|
|
45
|
+
|
|
46
|
+
device = adb.device(serial)
|
|
47
|
+
ping_portal(device, debug=False)
|
|
48
|
+
return True
|
|
49
|
+
except Exception:
|
|
50
|
+
return False
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def setup_portal(serial: str) -> tuple[bool, str]:
|
|
54
|
+
"""Setup Quash Portal on the device."""
|
|
55
|
+
try:
|
|
56
|
+
from adbutils import adb
|
|
57
|
+
from mahoraga.portal import use_portal_apk, enable_portal_accessibility
|
|
58
|
+
|
|
59
|
+
device = adb.device(serial)
|
|
60
|
+
|
|
61
|
+
# Install APK
|
|
62
|
+
with use_portal_apk(None, debug=False) as apk_path:
|
|
63
|
+
device.install(apk_path, uninstall=True, flags=["-g"], silent=True)
|
|
64
|
+
|
|
65
|
+
# Enable accessibility service
|
|
66
|
+
enable_portal_accessibility(device)
|
|
67
|
+
|
|
68
|
+
return True, "Portal installed and enabled successfully"
|
|
69
|
+
except Exception as e:
|
|
70
|
+
return False, f"Failed to setup portal: {str(e)}"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
async def connect(device_serial: Optional[str] = None) -> Dict[str, Any]:
|
|
74
|
+
"""
|
|
75
|
+
Connect to an Android device or emulator.
|
|
76
|
+
If device_serial is not provided, auto-selects if only one device is connected.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
device_serial: Optional device serial number
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
Dict with connection status and device information
|
|
83
|
+
"""
|
|
84
|
+
state = get_state()
|
|
85
|
+
|
|
86
|
+
# List available devices
|
|
87
|
+
devices = list_devices()
|
|
88
|
+
|
|
89
|
+
if not devices:
|
|
90
|
+
return {
|
|
91
|
+
"status": "failed",
|
|
92
|
+
"message": "❌ No Android devices found. Please connect a device or start an emulator.",
|
|
93
|
+
"instructions": [
|
|
94
|
+
"To start an emulator: Open Android Studio > AVD Manager > Start",
|
|
95
|
+
"To connect a physical device: Enable USB debugging and connect via USB",
|
|
96
|
+
"To connect over WiFi: Run 'adb tcpip 5555' then 'adb connect <device-ip>:5555'"
|
|
97
|
+
]
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
# Select device
|
|
101
|
+
selected_serial = device_serial
|
|
102
|
+
if not selected_serial:
|
|
103
|
+
if len(devices) == 1:
|
|
104
|
+
selected_serial = devices[0]["serial"]
|
|
105
|
+
else:
|
|
106
|
+
return {
|
|
107
|
+
"status": "failed",
|
|
108
|
+
"message": f"❌ Multiple devices found ({len(devices)}). Please specify which one to use.",
|
|
109
|
+
"available_devices": devices
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
# Verify device exists
|
|
113
|
+
if not any(d["serial"] == selected_serial for d in devices):
|
|
114
|
+
return {
|
|
115
|
+
"status": "failed",
|
|
116
|
+
"message": f"❌ Device '{selected_serial}' not found.",
|
|
117
|
+
"available_devices": devices
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
# Get device info
|
|
121
|
+
device_info = get_device_info(selected_serial)
|
|
122
|
+
if not device_info:
|
|
123
|
+
return {
|
|
124
|
+
"status": "failed",
|
|
125
|
+
"message": f"❌ Failed to get information for device '{selected_serial}'."
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
# Check portal accessibility service
|
|
129
|
+
portal_ready = check_portal_service(selected_serial)
|
|
130
|
+
|
|
131
|
+
if not portal_ready:
|
|
132
|
+
# Attempt to setup portal
|
|
133
|
+
setup_success, setup_msg = setup_portal(selected_serial)
|
|
134
|
+
if setup_success:
|
|
135
|
+
portal_ready = True
|
|
136
|
+
portal_message = "✓ Portal setup completed"
|
|
137
|
+
else:
|
|
138
|
+
portal_message = f"⚠️ Portal not ready: {setup_msg}"
|
|
139
|
+
else:
|
|
140
|
+
portal_message = "✓ Portal already enabled"
|
|
141
|
+
|
|
142
|
+
# Update state
|
|
143
|
+
state.device_serial = selected_serial
|
|
144
|
+
state.device_info = device_info
|
|
145
|
+
state.portal_ready = portal_ready
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
"status": "connected" if portal_ready else "partial",
|
|
149
|
+
"device": device_info,
|
|
150
|
+
"portal_ready": portal_ready,
|
|
151
|
+
"portal_message": portal_message,
|
|
152
|
+
"message": f"✅ Connected to {device_info['model']} ({selected_serial})"
|
|
153
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Execute tool - Run automation tasks via backend API.
|
|
3
|
+
All AI/agent logic runs on the backend to protect business logic.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Dict, Any, Callable, Optional
|
|
7
|
+
from ..state import get_state
|
|
8
|
+
from ..backend_client import get_backend_client
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
async def execute(
|
|
12
|
+
task: str,
|
|
13
|
+
progress_callback: Optional[Callable[[str], None]] = None
|
|
14
|
+
) -> Dict[str, Any]:
|
|
15
|
+
"""
|
|
16
|
+
Execute an automation task on the connected Android device.
|
|
17
|
+
|
|
18
|
+
All AI execution happens on the backend - this keeps proprietary logic private.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
task: Natural language task description
|
|
22
|
+
progress_callback: Optional callback for progress updates (not used in V2)
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
Dict with execution result and details
|
|
26
|
+
"""
|
|
27
|
+
state = get_state()
|
|
28
|
+
backend = get_backend_client()
|
|
29
|
+
|
|
30
|
+
# Check prerequisites
|
|
31
|
+
if not state.is_device_connected():
|
|
32
|
+
return {
|
|
33
|
+
"status": "error",
|
|
34
|
+
"message": "❌ No device connected. Please run 'connect' first.",
|
|
35
|
+
"prerequisite": "connect"
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if not state.is_configured():
|
|
39
|
+
return {
|
|
40
|
+
"status": "error",
|
|
41
|
+
"message": "❌ Configuration incomplete. Please run 'configure' with your Quash API key.",
|
|
42
|
+
"prerequisite": "configure"
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if not state.portal_ready:
|
|
46
|
+
return {
|
|
47
|
+
"status": "error",
|
|
48
|
+
"message": "⚠️ Portal accessibility service not ready. Please ensure it's enabled on the device.",
|
|
49
|
+
"prerequisite": "connect"
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
# Get API key and config from state
|
|
53
|
+
quash_api_key = state.config["api_key"]
|
|
54
|
+
|
|
55
|
+
# Validate API key with backend
|
|
56
|
+
validation_result = await backend.validate_api_key(quash_api_key)
|
|
57
|
+
|
|
58
|
+
if not validation_result.get("valid", False):
|
|
59
|
+
error_msg = validation_result.get("error", "Invalid API key")
|
|
60
|
+
return {
|
|
61
|
+
"status": "error",
|
|
62
|
+
"message": f"❌ API Key validation failed: {error_msg}",
|
|
63
|
+
"prerequisite": "configure"
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
# Check user credits
|
|
67
|
+
user_info = validation_result.get("user", {})
|
|
68
|
+
credits = user_info.get("credits", 0)
|
|
69
|
+
|
|
70
|
+
if credits <= 0:
|
|
71
|
+
return {
|
|
72
|
+
"status": "error",
|
|
73
|
+
"message": f"❌ Insufficient credits. Current balance: ${credits:.2f}. Please add credits at https://quashbugs.com",
|
|
74
|
+
"user": user_info
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
# Progress callback (for backward compatibility)
|
|
78
|
+
def log_progress(message: str):
|
|
79
|
+
"""Send progress updates."""
|
|
80
|
+
if progress_callback:
|
|
81
|
+
progress_callback(message)
|
|
82
|
+
|
|
83
|
+
log_progress(f"✅ API Key validated - Credits: ${credits:.2f}")
|
|
84
|
+
log_progress(f"👤 User: {user_info.get('name', 'Unknown')}")
|
|
85
|
+
log_progress(f"🚀 Starting task: {task}")
|
|
86
|
+
log_progress(f"📱 Device: {state.device_serial}")
|
|
87
|
+
log_progress(f"🧠 Model: {state.config['model']}")
|
|
88
|
+
log_progress("⚙️ Executing on backend...")
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
# ============================================================
|
|
92
|
+
# EXECUTE ON BACKEND - ALL AI LOGIC IS PRIVATE
|
|
93
|
+
# The backend handles: LLM setup, agent initialization,
|
|
94
|
+
# execution, pricing, usage tracking, credit deduction
|
|
95
|
+
# ============================================================
|
|
96
|
+
|
|
97
|
+
result = await backend.execute_task(
|
|
98
|
+
api_key=quash_api_key,
|
|
99
|
+
task=task,
|
|
100
|
+
device_serial=state.device_serial,
|
|
101
|
+
config={
|
|
102
|
+
"model": state.config["model"],
|
|
103
|
+
"temperature": state.config["temperature"],
|
|
104
|
+
"vision": state.config["vision"],
|
|
105
|
+
"reasoning": state.config["reasoning"],
|
|
106
|
+
"reflection": state.config["reflection"],
|
|
107
|
+
"debug": state.config["debug"]
|
|
108
|
+
}
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# Process result
|
|
112
|
+
status = result.get("status")
|
|
113
|
+
message = result.get("message", "")
|
|
114
|
+
steps_taken = result.get("steps_taken", 0)
|
|
115
|
+
final_message = result.get("final_message", "")
|
|
116
|
+
tokens = result.get("tokens", {})
|
|
117
|
+
cost = result.get("cost", 0.0)
|
|
118
|
+
duration = result.get("duration_seconds", 0.0)
|
|
119
|
+
error = result.get("error")
|
|
120
|
+
|
|
121
|
+
# Log usage info
|
|
122
|
+
if tokens and cost:
|
|
123
|
+
total_tokens = tokens.get("total", 0)
|
|
124
|
+
log_progress(f"💰 Usage: {total_tokens} tokens, ${cost:.4f}")
|
|
125
|
+
|
|
126
|
+
# Return formatted result
|
|
127
|
+
if status == "success":
|
|
128
|
+
log_progress(f"✅ Task completed successfully in {steps_taken} steps")
|
|
129
|
+
return {
|
|
130
|
+
"status": "success",
|
|
131
|
+
"steps_taken": steps_taken,
|
|
132
|
+
"final_message": final_message,
|
|
133
|
+
"message": message,
|
|
134
|
+
"tokens": tokens,
|
|
135
|
+
"cost": cost,
|
|
136
|
+
"duration_seconds": duration
|
|
137
|
+
}
|
|
138
|
+
elif status == "failed":
|
|
139
|
+
log_progress(f"❌ Task failed: {final_message}")
|
|
140
|
+
return {
|
|
141
|
+
"status": "failed",
|
|
142
|
+
"steps_taken": steps_taken,
|
|
143
|
+
"final_message": final_message,
|
|
144
|
+
"message": message,
|
|
145
|
+
"tokens": tokens,
|
|
146
|
+
"cost": cost,
|
|
147
|
+
"duration_seconds": duration
|
|
148
|
+
}
|
|
149
|
+
elif status == "interrupted":
|
|
150
|
+
log_progress("⏹️ Task interrupted")
|
|
151
|
+
return {
|
|
152
|
+
"status": "interrupted",
|
|
153
|
+
"message": message
|
|
154
|
+
}
|
|
155
|
+
else: # error
|
|
156
|
+
log_progress(f"💥 Error: {error or message}")
|
|
157
|
+
return {
|
|
158
|
+
"status": "error",
|
|
159
|
+
"message": message,
|
|
160
|
+
"error": error or message
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
except KeyboardInterrupt:
|
|
164
|
+
log_progress("⏹️ Task interrupted by user")
|
|
165
|
+
return {
|
|
166
|
+
"status": "interrupted",
|
|
167
|
+
"message": "⏹️ Task execution interrupted"
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
except Exception as e:
|
|
171
|
+
error_msg = str(e)
|
|
172
|
+
log_progress(f"💥 Error: {error_msg}")
|
|
173
|
+
return {
|
|
174
|
+
"status": "error",
|
|
175
|
+
"message": f"💥 Execution error: {error_msg}",
|
|
176
|
+
"error": error_msg
|
|
177
|
+
}
|