quash-mcp 0.2.2__tar.gz → 0.2.4__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.
Potentially problematic release.
This version of quash-mcp might be problematic. Click here for more details.
- {quash_mcp-0.2.2 → quash_mcp-0.2.4}/PKG-INFO +1 -1
- {quash_mcp-0.2.2 → quash_mcp-0.2.4}/pyproject.toml +1 -1
- {quash_mcp-0.2.2 → quash_mcp-0.2.4}/quash_mcp/backend_client.py +91 -0
- quash_mcp-0.2.4/quash_mcp/device/state_capture.py +115 -0
- quash_mcp-0.2.4/quash_mcp/tools/execute.py +31 -0
- quash_mcp-0.2.4/quash_mcp/tools/execute_v3.py +306 -0
- {quash_mcp-0.2.2 → quash_mcp-0.2.4}/.gitignore +0 -0
- {quash_mcp-0.2.2 → quash_mcp-0.2.4}/README.md +0 -0
- {quash_mcp-0.2.2 → quash_mcp-0.2.4}/SETUP_CLAUDE_CODE.md +0 -0
- {quash_mcp-0.2.2 → quash_mcp-0.2.4}/quash_mcp/__init__.py +0 -0
- {quash_mcp-0.2.2 → quash_mcp-0.2.4}/quash_mcp/__main__.py +0 -0
- {quash_mcp-0.2.2 → quash_mcp-0.2.4}/quash_mcp/device/__init__.py +0 -0
- {quash_mcp-0.2.2 → quash_mcp-0.2.4}/quash_mcp/device/adb_tools.py +0 -0
- {quash_mcp-0.2.2 → quash_mcp-0.2.4}/quash_mcp/device/portal.py +0 -0
- {quash_mcp-0.2.2 → quash_mcp-0.2.4}/quash_mcp/server.py +0 -0
- {quash_mcp-0.2.2 → quash_mcp-0.2.4}/quash_mcp/state.py +0 -0
- {quash_mcp-0.2.2 → quash_mcp-0.2.4}/quash_mcp/tools/__init__.py +0 -0
- {quash_mcp-0.2.2 → quash_mcp-0.2.4}/quash_mcp/tools/build.py +0 -0
- {quash_mcp-0.2.2 → quash_mcp-0.2.4}/quash_mcp/tools/build_old.py +0 -0
- {quash_mcp-0.2.2 → quash_mcp-0.2.4}/quash_mcp/tools/configure.py +0 -0
- {quash_mcp-0.2.2 → quash_mcp-0.2.4}/quash_mcp/tools/connect.py +0 -0
- /quash_mcp-0.2.2/quash_mcp/tools/execute.py → /quash_mcp-0.2.4/quash_mcp/tools/execute_v2_backup.py +0 -0
- {quash_mcp-0.2.2 → quash_mcp-0.2.4}/quash_mcp/tools/runsuite.py +0 -0
- {quash_mcp-0.2.2 → quash_mcp-0.2.4}/quash_mcp/tools/usage.py +0 -0
- {quash_mcp-0.2.2 → quash_mcp-0.2.4}/test_backend_integration.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: quash-mcp
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: Model Context Protocol server for Quash - AI-powered mobile automation agent
|
|
5
5
|
Project-URL: Homepage, https://quashbugs.com
|
|
6
6
|
Project-URL: Repository, https://github.com/quash/quash-mcp
|
|
@@ -190,6 +190,97 @@ class BackendClient:
|
|
|
190
190
|
logger.error(f"Failed to log execution: {e}")
|
|
191
191
|
return {"logged": False, "error": str(e)}
|
|
192
192
|
|
|
193
|
+
async def execute_step(
|
|
194
|
+
self,
|
|
195
|
+
api_key: str,
|
|
196
|
+
session_id: str,
|
|
197
|
+
step_number: int,
|
|
198
|
+
task: str,
|
|
199
|
+
ui_state: Dict[str, Any],
|
|
200
|
+
chat_history: list,
|
|
201
|
+
config: Dict[str, Any],
|
|
202
|
+
screenshot_bytes: Optional[bytes] = None
|
|
203
|
+
) -> Dict[str, Any]:
|
|
204
|
+
"""
|
|
205
|
+
Execute single agent step (V3 - Step-by-step execution).
|
|
206
|
+
|
|
207
|
+
Sends device state to backend, receives next action to execute.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
api_key: Quash API key
|
|
211
|
+
session_id: Unique session identifier
|
|
212
|
+
step_number: Current step number
|
|
213
|
+
task: Original task description
|
|
214
|
+
ui_state: Device UI state (a11y_tree, phone_state)
|
|
215
|
+
chat_history: Previous conversation messages
|
|
216
|
+
config: Execution configuration
|
|
217
|
+
screenshot_bytes: Optional screenshot (only if vision=True)
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
Dict with action to execute:
|
|
221
|
+
{
|
|
222
|
+
"action": {"type": str, "code": str, "reasoning": str},
|
|
223
|
+
"completed": bool,
|
|
224
|
+
"success": bool (if completed),
|
|
225
|
+
"final_message": str (if completed),
|
|
226
|
+
"assistant_response": str,
|
|
227
|
+
"tokens_used": {"prompt": int, "completion": int, "total": int},
|
|
228
|
+
"cost": float
|
|
229
|
+
}
|
|
230
|
+
"""
|
|
231
|
+
import json
|
|
232
|
+
|
|
233
|
+
try:
|
|
234
|
+
# Prepare form data (multipart)
|
|
235
|
+
data_dict = {
|
|
236
|
+
"api_key": api_key,
|
|
237
|
+
"session_id": session_id,
|
|
238
|
+
"step_number": step_number,
|
|
239
|
+
"task": task,
|
|
240
|
+
"ui_state": ui_state,
|
|
241
|
+
"chat_history": chat_history,
|
|
242
|
+
"config": config
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
# Convert to JSON string
|
|
246
|
+
data_json = json.dumps(data_dict)
|
|
247
|
+
|
|
248
|
+
# Prepare form data (data field as string)
|
|
249
|
+
form_data = {"data": data_json}
|
|
250
|
+
|
|
251
|
+
# Prepare files dict (only screenshot if provided)
|
|
252
|
+
files = {}
|
|
253
|
+
if screenshot_bytes:
|
|
254
|
+
files["screenshot"] = ("screenshot.png", screenshot_bytes, "image/png")
|
|
255
|
+
|
|
256
|
+
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
|
257
|
+
# Send both form data and files (multipart/form-data)
|
|
258
|
+
response = await client.post(
|
|
259
|
+
f"{self.base_url}/api/agent/step",
|
|
260
|
+
data=form_data,
|
|
261
|
+
files=files if files else None
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
if response.status_code == 200:
|
|
265
|
+
return response.json()
|
|
266
|
+
else:
|
|
267
|
+
error_msg = f"Backend error: HTTP {response.status_code}"
|
|
268
|
+
logger.error(error_msg)
|
|
269
|
+
return {
|
|
270
|
+
"status": "error",
|
|
271
|
+
"message": error_msg,
|
|
272
|
+
"error": error_msg
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
except Exception as e:
|
|
276
|
+
error_msg = f"Failed to execute step: {str(e)}"
|
|
277
|
+
logger.error(error_msg)
|
|
278
|
+
return {
|
|
279
|
+
"status": "error",
|
|
280
|
+
"message": error_msg,
|
|
281
|
+
"error": str(e)
|
|
282
|
+
}
|
|
283
|
+
|
|
193
284
|
|
|
194
285
|
# Singleton instance
|
|
195
286
|
_backend_client = None
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Device state capture utilities.
|
|
3
|
+
Captures UI state and screenshots from Android devices.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
import requests
|
|
8
|
+
from typing import Dict, Any, Optional, Tuple
|
|
9
|
+
from adbutils import adb
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger("quash-device")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_current_package(serial: str) -> str:
|
|
15
|
+
"""
|
|
16
|
+
Get the currently focused app package.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
serial: Device serial number
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
Package name of current app
|
|
23
|
+
"""
|
|
24
|
+
try:
|
|
25
|
+
device = adb.device(serial)
|
|
26
|
+
output = device.shell("dumpsys window windows | grep -E 'mCurrentFocus'")
|
|
27
|
+
# Parse output like: mCurrentFocus=Window{abc123 u0 com.android.settings/com.android.settings.MainActivity}
|
|
28
|
+
if "/" in output:
|
|
29
|
+
package = output.split("/")[0].split()[-1]
|
|
30
|
+
return package
|
|
31
|
+
return "unknown"
|
|
32
|
+
except Exception as e:
|
|
33
|
+
logger.warning(f"Failed to get current package: {e}")
|
|
34
|
+
return "unknown"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def get_accessibility_tree(serial: str, tcp_port: int = 8080) -> str:
|
|
38
|
+
"""
|
|
39
|
+
Get accessibility tree from Portal app via TCP.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
serial: Device serial number
|
|
43
|
+
tcp_port: Local TCP port for Portal communication
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
Accessibility tree XML string
|
|
47
|
+
"""
|
|
48
|
+
try:
|
|
49
|
+
device = adb.device(serial)
|
|
50
|
+
local_port = device.forward_port(tcp_port)
|
|
51
|
+
|
|
52
|
+
response = requests.get(
|
|
53
|
+
f"http://localhost:{local_port}/get_a11y_tree",
|
|
54
|
+
timeout=10
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
if response.status_code == 200:
|
|
58
|
+
return response.text
|
|
59
|
+
else:
|
|
60
|
+
logger.warning(f"Failed to get accessibility tree: HTTP {response.status_code}")
|
|
61
|
+
return "<hierarchy></hierarchy>"
|
|
62
|
+
|
|
63
|
+
except Exception as e:
|
|
64
|
+
logger.warning(f"Failed to get accessibility tree: {e}")
|
|
65
|
+
return "<hierarchy></hierarchy>"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def capture_screenshot(serial: str) -> Optional[bytes]:
|
|
69
|
+
"""
|
|
70
|
+
Capture screenshot from device.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
serial: Device serial number
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
Screenshot as PNG bytes, or None if failed
|
|
77
|
+
"""
|
|
78
|
+
try:
|
|
79
|
+
device = adb.device(serial)
|
|
80
|
+
screenshot_bytes = device.shell("screencap -p", stream=True)
|
|
81
|
+
return screenshot_bytes
|
|
82
|
+
except Exception as e:
|
|
83
|
+
logger.error(f"Failed to capture screenshot: {e}")
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def get_device_state(serial: str) -> Tuple[Dict[str, Any], Optional[bytes]]:
|
|
88
|
+
"""
|
|
89
|
+
Get complete device state: UI state and screenshot.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
serial: Device serial number
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
Tuple of (ui_state_dict, screenshot_bytes)
|
|
96
|
+
"""
|
|
97
|
+
# Get current package
|
|
98
|
+
current_package = get_current_package(serial)
|
|
99
|
+
|
|
100
|
+
# Get accessibility tree
|
|
101
|
+
a11y_tree = get_accessibility_tree(serial)
|
|
102
|
+
|
|
103
|
+
# Build UI state
|
|
104
|
+
ui_state = {
|
|
105
|
+
"a11y_tree": a11y_tree,
|
|
106
|
+
"phone_state": {
|
|
107
|
+
"package": current_package,
|
|
108
|
+
"activity": "unknown", # Can be added later
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
# Capture screenshot
|
|
113
|
+
screenshot = capture_screenshot(serial)
|
|
114
|
+
|
|
115
|
+
return ui_state, screenshot
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Execute tool - Run automation tasks via step-by-step backend communication.
|
|
3
|
+
|
|
4
|
+
V3: Hybrid architecture - AI logic on backend (private), device access local (public).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Dict, Any, Callable, Optional
|
|
8
|
+
from .execute_v3 import execute_v3
|
|
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
|
+
Uses step-by-step execution:
|
|
19
|
+
- Captures device state locally
|
|
20
|
+
- Sends to backend for AI decision
|
|
21
|
+
- Executes actions locally
|
|
22
|
+
- Keeps proprietary AI logic private on backend
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
task: Natural language task description
|
|
26
|
+
progress_callback: Optional callback for progress updates
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
Dict with execution result and details
|
|
30
|
+
"""
|
|
31
|
+
return await execute_v3(task=task, progress_callback=progress_callback)
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Execute tool V3 - Step-by-step execution with local device access.
|
|
3
|
+
|
|
4
|
+
AI logic runs on backend (private), device access happens locally (public).
|
|
5
|
+
This hybrid approach keeps proprietary code private while allowing local device control.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import time
|
|
9
|
+
import uuid
|
|
10
|
+
from typing import Dict, Any, Callable, Optional
|
|
11
|
+
from ..state import get_state
|
|
12
|
+
from ..backend_client import get_backend_client
|
|
13
|
+
from ..device.state_capture import get_device_state
|
|
14
|
+
from ..device.adb_tools import AdbTools
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
async def execute_v3(
|
|
18
|
+
task: str,
|
|
19
|
+
progress_callback: Optional[Callable[[str], None]] = None
|
|
20
|
+
) -> Dict[str, Any]:
|
|
21
|
+
"""
|
|
22
|
+
Execute automation task using step-by-step backend communication.
|
|
23
|
+
|
|
24
|
+
Each step:
|
|
25
|
+
1. Capture device state locally (UI + optional screenshot)
|
|
26
|
+
2. Send to backend for AI decision
|
|
27
|
+
3. Execute returned action locally
|
|
28
|
+
4. Repeat until complete
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
task: Natural language task description
|
|
32
|
+
progress_callback: Optional callback for progress updates
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
Dict with execution result and details
|
|
36
|
+
"""
|
|
37
|
+
state = get_state()
|
|
38
|
+
backend = get_backend_client()
|
|
39
|
+
|
|
40
|
+
# Check prerequisites
|
|
41
|
+
if not state.is_device_connected():
|
|
42
|
+
return {
|
|
43
|
+
"status": "error",
|
|
44
|
+
"message": "❌ No device connected. Please run 'connect' first.",
|
|
45
|
+
"prerequisite": "connect"
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if not state.is_configured():
|
|
49
|
+
return {
|
|
50
|
+
"status": "error",
|
|
51
|
+
"message": "❌ Configuration incomplete. Please run 'configure' with your Quash API key.",
|
|
52
|
+
"prerequisite": "configure"
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if not state.portal_ready:
|
|
56
|
+
return {
|
|
57
|
+
"status": "error",
|
|
58
|
+
"message": "⚠️ Portal accessibility service not ready. Please ensure it's enabled on the device.",
|
|
59
|
+
"prerequisite": "connect"
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# Get API key and config
|
|
63
|
+
quash_api_key = state.config["api_key"]
|
|
64
|
+
config = {
|
|
65
|
+
"model": state.config["model"],
|
|
66
|
+
"temperature": state.config["temperature"],
|
|
67
|
+
"vision": state.config["vision"],
|
|
68
|
+
"reasoning": state.config["reasoning"],
|
|
69
|
+
"reflection": state.config["reflection"],
|
|
70
|
+
"debug": state.config["debug"]
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
# Validate API key
|
|
74
|
+
validation_result = await backend.validate_api_key(quash_api_key)
|
|
75
|
+
|
|
76
|
+
if not validation_result.get("valid", False):
|
|
77
|
+
error_msg = validation_result.get("error", "Invalid API key")
|
|
78
|
+
return {
|
|
79
|
+
"status": "error",
|
|
80
|
+
"message": f"❌ API Key validation failed: {error_msg}",
|
|
81
|
+
"prerequisite": "configure"
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
# Check credits
|
|
85
|
+
user_info = validation_result.get("user", {})
|
|
86
|
+
credits = user_info.get("credits", 0)
|
|
87
|
+
|
|
88
|
+
if credits <= 0:
|
|
89
|
+
return {
|
|
90
|
+
"status": "error",
|
|
91
|
+
"message": f"❌ Insufficient credits. Current balance: ${credits:.2f}",
|
|
92
|
+
"user": user_info
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
# Progress logging helper
|
|
96
|
+
def log_progress(message: str):
|
|
97
|
+
if progress_callback:
|
|
98
|
+
progress_callback(message)
|
|
99
|
+
|
|
100
|
+
log_progress(f"✅ API Key validated - Credits: ${credits:.2f}")
|
|
101
|
+
log_progress(f"👤 User: {user_info.get('name', 'Unknown')}")
|
|
102
|
+
log_progress(f"🚀 Starting task: {task}")
|
|
103
|
+
log_progress(f"📱 Device: {state.device_serial}")
|
|
104
|
+
log_progress(f"🧠 Model: {config['model']}")
|
|
105
|
+
|
|
106
|
+
# Initialize execution
|
|
107
|
+
start_time = time.time()
|
|
108
|
+
session_id = f"session_{uuid.uuid4().hex[:12]}"
|
|
109
|
+
step_number = 0
|
|
110
|
+
chat_history = []
|
|
111
|
+
total_tokens = {"prompt": 0, "completion": 0, "total": 0}
|
|
112
|
+
total_cost = 0.0
|
|
113
|
+
|
|
114
|
+
# Initialize local ADB tools for code execution
|
|
115
|
+
adb_tools = AdbTools(serial=state.device_serial, use_tcp=True)
|
|
116
|
+
|
|
117
|
+
# Code executor namespace
|
|
118
|
+
executor_globals = {
|
|
119
|
+
"__builtins__": __builtins__,
|
|
120
|
+
"adb_tools": adb_tools
|
|
121
|
+
}
|
|
122
|
+
executor_locals = {}
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
# ============================================================
|
|
126
|
+
# STEP-BY-STEP EXECUTION LOOP
|
|
127
|
+
# ============================================================
|
|
128
|
+
while step_number < 15: # Max 15 steps
|
|
129
|
+
step_number += 1
|
|
130
|
+
log_progress(f"🧠 Step {step_number}: Thinking...")
|
|
131
|
+
|
|
132
|
+
# 1. Capture device state
|
|
133
|
+
try:
|
|
134
|
+
ui_state_dict, screenshot_bytes = get_device_state(state.device_serial)
|
|
135
|
+
|
|
136
|
+
# Only include screenshot if vision is enabled
|
|
137
|
+
if not config["vision"]:
|
|
138
|
+
screenshot_bytes = None
|
|
139
|
+
|
|
140
|
+
except Exception as e:
|
|
141
|
+
log_progress(f"⚠️ Warning: Failed to capture device state: {e}")
|
|
142
|
+
ui_state_dict = {
|
|
143
|
+
"a11y_tree": "<hierarchy></hierarchy>",
|
|
144
|
+
"phone_state": {"package": "unknown"}
|
|
145
|
+
}
|
|
146
|
+
screenshot_bytes = None
|
|
147
|
+
|
|
148
|
+
# 2. Send to backend for AI decision
|
|
149
|
+
step_result = await backend.execute_step(
|
|
150
|
+
api_key=quash_api_key,
|
|
151
|
+
session_id=session_id,
|
|
152
|
+
step_number=step_number,
|
|
153
|
+
task=task,
|
|
154
|
+
ui_state=ui_state_dict,
|
|
155
|
+
chat_history=chat_history,
|
|
156
|
+
config=config,
|
|
157
|
+
screenshot_bytes=screenshot_bytes
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# Handle backend errors
|
|
161
|
+
if "error" in step_result:
|
|
162
|
+
log_progress(f"💥 Backend error: {step_result['message']}")
|
|
163
|
+
return {
|
|
164
|
+
"status": "error",
|
|
165
|
+
"message": step_result["message"],
|
|
166
|
+
"error": step_result["error"],
|
|
167
|
+
"steps_taken": step_number,
|
|
168
|
+
"tokens": total_tokens,
|
|
169
|
+
"cost": total_cost,
|
|
170
|
+
"duration_seconds": time.time() - start_time
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
# Update usage tracking
|
|
174
|
+
step_tokens = step_result.get("tokens_used", {})
|
|
175
|
+
step_cost = step_result.get("cost", 0.0)
|
|
176
|
+
|
|
177
|
+
total_tokens["prompt"] += step_tokens.get("prompt", 0)
|
|
178
|
+
total_tokens["completion"] += step_tokens.get("completion", 0)
|
|
179
|
+
total_tokens["total"] += step_tokens.get("total", 0)
|
|
180
|
+
total_cost += step_cost
|
|
181
|
+
|
|
182
|
+
# Get action from backend
|
|
183
|
+
action = step_result.get("action", {})
|
|
184
|
+
action_type = action.get("type")
|
|
185
|
+
code = action.get("code")
|
|
186
|
+
reasoning = action.get("reasoning")
|
|
187
|
+
|
|
188
|
+
# Log reasoning
|
|
189
|
+
if reasoning:
|
|
190
|
+
log_progress(f"🤔 Reasoning: {reasoning}")
|
|
191
|
+
|
|
192
|
+
# Update chat history
|
|
193
|
+
assistant_response = step_result.get("assistant_response", "")
|
|
194
|
+
chat_history.append({"role": "assistant", "content": assistant_response})
|
|
195
|
+
|
|
196
|
+
# 3. Check if task is complete
|
|
197
|
+
if step_result.get("completed", False):
|
|
198
|
+
success = step_result.get("success", False)
|
|
199
|
+
final_message = step_result.get("final_message", "Task completed")
|
|
200
|
+
|
|
201
|
+
duration = time.time() - start_time
|
|
202
|
+
|
|
203
|
+
if success:
|
|
204
|
+
log_progress(f"✅ Task completed successfully in {step_number} steps")
|
|
205
|
+
log_progress(f"💰 Usage: {total_tokens['total']} tokens, ${total_cost:.4f}")
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
"status": "success",
|
|
209
|
+
"steps_taken": step_number,
|
|
210
|
+
"final_message": final_message,
|
|
211
|
+
"message": f"✅ Success: {final_message}",
|
|
212
|
+
"tokens": total_tokens,
|
|
213
|
+
"cost": total_cost,
|
|
214
|
+
"duration_seconds": duration
|
|
215
|
+
}
|
|
216
|
+
else:
|
|
217
|
+
log_progress(f"❌ Task failed: {final_message}")
|
|
218
|
+
log_progress(f"💰 Usage: {total_tokens['total']} tokens, ${total_cost:.4f}")
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
"status": "failed",
|
|
222
|
+
"steps_taken": step_number,
|
|
223
|
+
"final_message": final_message,
|
|
224
|
+
"message": f"❌ Failed: {final_message}",
|
|
225
|
+
"tokens": total_tokens,
|
|
226
|
+
"cost": total_cost,
|
|
227
|
+
"duration_seconds": duration
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
# 4. Execute action locally
|
|
231
|
+
if code and action_type == "execute_code":
|
|
232
|
+
log_progress(f"⚡ Executing action...")
|
|
233
|
+
|
|
234
|
+
try:
|
|
235
|
+
# Execute code in sandbox
|
|
236
|
+
exec(code, executor_globals, executor_locals)
|
|
237
|
+
|
|
238
|
+
# Get execution result
|
|
239
|
+
execution_output = executor_locals.get("_result", "Code executed successfully")
|
|
240
|
+
|
|
241
|
+
# Add execution result to chat history
|
|
242
|
+
chat_history.append({
|
|
243
|
+
"role": "user",
|
|
244
|
+
"content": f"Execution Result:\n```\n{execution_output}\n```"
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
except Exception as e:
|
|
248
|
+
error_msg = f"Error during execution: {str(e)}"
|
|
249
|
+
log_progress(f"💥 Action failed: {error_msg}")
|
|
250
|
+
|
|
251
|
+
# Add error to chat history
|
|
252
|
+
chat_history.append({
|
|
253
|
+
"role": "user",
|
|
254
|
+
"content": f"Execution Result:\n```\n{error_msg}\n```"
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
else:
|
|
258
|
+
# No code to execute
|
|
259
|
+
log_progress("⚠️ No action code provided by backend")
|
|
260
|
+
chat_history.append({
|
|
261
|
+
"role": "user",
|
|
262
|
+
"content": "No code was provided. Please provide code to execute."
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
# Max steps reached
|
|
266
|
+
log_progress(f"⚠️ Reached maximum steps ({step_number})")
|
|
267
|
+
log_progress(f"💰 Usage: {total_tokens['total']} tokens, ${total_cost:.4f}")
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
"status": "failed",
|
|
271
|
+
"steps_taken": step_number,
|
|
272
|
+
"final_message": f"Reached maximum step limit of {step_number}",
|
|
273
|
+
"message": "❌ Failed: Maximum steps reached",
|
|
274
|
+
"tokens": total_tokens,
|
|
275
|
+
"cost": total_cost,
|
|
276
|
+
"duration_seconds": time.time() - start_time
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
except KeyboardInterrupt:
|
|
280
|
+
log_progress("⏹️ Task interrupted by user")
|
|
281
|
+
return {
|
|
282
|
+
"status": "interrupted",
|
|
283
|
+
"message": "⏹️ Task execution interrupted",
|
|
284
|
+
"steps_taken": step_number,
|
|
285
|
+
"tokens": total_tokens,
|
|
286
|
+
"cost": total_cost,
|
|
287
|
+
"duration_seconds": time.time() - start_time
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
except Exception as e:
|
|
291
|
+
error_msg = str(e)
|
|
292
|
+
log_progress(f"💥 Error: {error_msg}")
|
|
293
|
+
return {
|
|
294
|
+
"status": "error",
|
|
295
|
+
"message": f"💥 Execution error: {error_msg}",
|
|
296
|
+
"error": error_msg,
|
|
297
|
+
"steps_taken": step_number,
|
|
298
|
+
"tokens": total_tokens,
|
|
299
|
+
"cost": total_cost,
|
|
300
|
+
"duration_seconds": time.time() - start_time
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
finally:
|
|
304
|
+
# Cleanup TCP forwarding
|
|
305
|
+
if adb_tools:
|
|
306
|
+
adb_tools.teardown_tcp_forward()
|
|
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
|
/quash_mcp-0.2.2/quash_mcp/tools/execute.py → /quash_mcp-0.2.4/quash_mcp/tools/execute_v2_backup.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|