quash-mcp 0.2.12__py3-none-any.whl → 0.2.13__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/backend_client.py +14 -95
- quash_mcp/device/adb_tools.py +42 -0
- quash_mcp/device/state_capture.py +5 -0
- quash_mcp/models.py +42 -0
- quash_mcp/tools/execute_v3.py +91 -142
- {quash_mcp-0.2.12.dist-info → quash_mcp-0.2.13.dist-info}/METADATA +1 -1
- {quash_mcp-0.2.12.dist-info → quash_mcp-0.2.13.dist-info}/RECORD +9 -8
- {quash_mcp-0.2.12.dist-info → quash_mcp-0.2.13.dist-info}/WHEEL +0 -0
- {quash_mcp-0.2.12.dist-info → quash_mcp-0.2.13.dist-info}/entry_points.txt +0 -0
quash_mcp/backend_client.py
CHANGED
|
@@ -8,6 +8,10 @@ import httpx
|
|
|
8
8
|
from typing import Dict, Any, Optional
|
|
9
9
|
import logging
|
|
10
10
|
|
|
11
|
+
from .models import SessionDTO
|
|
12
|
+
|
|
13
|
+
from .models import SessionDTO
|
|
14
|
+
|
|
11
15
|
logger = logging.getLogger(__name__)
|
|
12
16
|
|
|
13
17
|
|
|
@@ -192,61 +196,18 @@ class BackendClient:
|
|
|
192
196
|
|
|
193
197
|
async def execute_step(
|
|
194
198
|
self,
|
|
195
|
-
|
|
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],
|
|
199
|
+
session: "SessionDTO",
|
|
202
200
|
screenshot_bytes: Optional[bytes] = None
|
|
203
201
|
) -> Dict[str, Any]:
|
|
204
202
|
"""
|
|
205
|
-
Execute single agent step (V3 -
|
|
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
|
-
}
|
|
203
|
+
Execute single agent step (V3 - DTO-based execution).
|
|
230
204
|
"""
|
|
231
205
|
import json
|
|
232
206
|
|
|
233
207
|
try:
|
|
234
208
|
# Prepare form data (multipart)
|
|
235
|
-
|
|
236
|
-
|
|
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}
|
|
209
|
+
session_json = session.model_dump_json()
|
|
210
|
+
form_data = {"session_data": session_json}
|
|
250
211
|
|
|
251
212
|
# Prepare files dict (only screenshot if provided)
|
|
252
213
|
files = {}
|
|
@@ -254,7 +215,6 @@ class BackendClient:
|
|
|
254
215
|
files["screenshot"] = ("screenshot.png", screenshot_bytes, "image/png")
|
|
255
216
|
|
|
256
217
|
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
|
257
|
-
# Send both form data and files (multipart/form-data)
|
|
258
218
|
response = await client.post(
|
|
259
219
|
f"{self.base_url}/api/agent/step",
|
|
260
220
|
data=form_data,
|
|
@@ -283,60 +243,18 @@ class BackendClient:
|
|
|
283
243
|
|
|
284
244
|
async def finalize_session(
|
|
285
245
|
self,
|
|
286
|
-
|
|
287
|
-
session_id: str,
|
|
288
|
-
task: str,
|
|
289
|
-
device_serial: str,
|
|
290
|
-
status: str,
|
|
291
|
-
final_message: Optional[str] = None,
|
|
292
|
-
error: Optional[str] = None,
|
|
293
|
-
duration_seconds: float = 0.0,
|
|
294
|
-
config: Optional[Dict[str, Any]] = None
|
|
246
|
+
session: "SessionDTO",
|
|
295
247
|
) -> Dict[str, Any]:
|
|
296
248
|
"""
|
|
297
249
|
Finalize a session and aggregate execution record.
|
|
298
|
-
|
|
299
|
-
Called when task ends for ANY reason: normal completion, max steps, error, interrupt.
|
|
300
|
-
|
|
301
|
-
Args:
|
|
302
|
-
api_key: Quash API key
|
|
303
|
-
session_id: Session identifier to finalize
|
|
304
|
-
task: Original task description
|
|
305
|
-
device_serial: Device serial number
|
|
306
|
-
status: "success", "failed", "max_steps", "error", "interrupted"
|
|
307
|
-
final_message: Final message from agent
|
|
308
|
-
error: Error message if failed
|
|
309
|
-
duration_seconds: Total execution time
|
|
310
|
-
config: Execution configuration
|
|
311
|
-
|
|
312
|
-
Returns:
|
|
313
|
-
Dict with finalization result:
|
|
314
|
-
{
|
|
315
|
-
"finalized": bool,
|
|
316
|
-
"execution_id": str,
|
|
317
|
-
"total_steps": int,
|
|
318
|
-
"total_tokens": {"prompt": int, "completion": int, "total": int},
|
|
319
|
-
"total_cost": float,
|
|
320
|
-
"error": str (if failed)
|
|
321
|
-
}
|
|
322
250
|
"""
|
|
323
|
-
logger.info(f"🏁 Finalizing session {session_id}
|
|
251
|
+
logger.info(f"🏁 Finalizing session {session.session_id}")
|
|
324
252
|
|
|
325
253
|
try:
|
|
326
254
|
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
|
327
255
|
response = await client.post(
|
|
328
256
|
f"{self.base_url}/api/agent/finalize",
|
|
329
|
-
|
|
330
|
-
"api_key": api_key,
|
|
331
|
-
"session_id": session_id,
|
|
332
|
-
"task": task,
|
|
333
|
-
"device_serial": device_serial,
|
|
334
|
-
"status": status,
|
|
335
|
-
"final_message": final_message,
|
|
336
|
-
"error": error,
|
|
337
|
-
"duration_seconds": duration_seconds,
|
|
338
|
-
"config": config or {}
|
|
339
|
-
}
|
|
257
|
+
data={"session_data": session.model_dump_json(exclude={'ui_state', 'chat_history'})}
|
|
340
258
|
)
|
|
341
259
|
|
|
342
260
|
if response.status_code == 200:
|
|
@@ -345,8 +263,9 @@ class BackendClient:
|
|
|
345
263
|
logger.info(f"✅ Session finalized: {result.get('total_steps')} steps, ${result.get('total_cost', 0):.4f}")
|
|
346
264
|
return result
|
|
347
265
|
else:
|
|
348
|
-
|
|
349
|
-
|
|
266
|
+
error_details = response.json() if response.headers.get("content-type") == "application/json" else response.text
|
|
267
|
+
logger.warning(f"Failed to finalize session: HTTP {response.status_code} - Details: {error_details}")
|
|
268
|
+
return {"finalized": False, "error": f"HTTP {response.status_code} - Details: {error_details}"}
|
|
350
269
|
|
|
351
270
|
except Exception as e:
|
|
352
271
|
logger.error(f"Failed to finalize session: {e}")
|
quash_mcp/device/adb_tools.py
CHANGED
|
@@ -102,6 +102,48 @@ class AdbTools:
|
|
|
102
102
|
logger.error(f"Failed to remove TCP port forwarding: {e}")
|
|
103
103
|
return False
|
|
104
104
|
|
|
105
|
+
def get_accessibility_tree(self) -> str:
|
|
106
|
+
"""
|
|
107
|
+
Get the current accessibility tree (UI hierarchy) as an XML string.
|
|
108
|
+
"""
|
|
109
|
+
try:
|
|
110
|
+
# Use uiautomator dump to get the UI hierarchy
|
|
111
|
+
result = self.device.shell("uiautomator dump --compressed")
|
|
112
|
+
# The dump command writes to /sdcard/window_dump.xml
|
|
113
|
+
# We need to read it back
|
|
114
|
+
xml_content = self.device.pull("/sdcard/window_dump.xml").decode("utf-8")
|
|
115
|
+
# Clean up the dumped file from the device
|
|
116
|
+
self.device.shell("rm /sdcard/window_dump.xml")
|
|
117
|
+
return xml_content
|
|
118
|
+
except Exception as e:
|
|
119
|
+
logger.error(f"Failed to get accessibility tree: {e}", exc_info=True)
|
|
120
|
+
return f"<error>Failed to get accessibility tree: {e}</error>"
|
|
121
|
+
|
|
122
|
+
def get_phone_state(self) -> dict[str, any]:
|
|
123
|
+
"""
|
|
124
|
+
Get basic phone state information, like current app package.
|
|
125
|
+
"""
|
|
126
|
+
try:
|
|
127
|
+
current_app = self.device.current_app()
|
|
128
|
+
return {
|
|
129
|
+
"package": current_app.package,
|
|
130
|
+
"activity": current_app.activity,
|
|
131
|
+
"pid": current_app.pid,
|
|
132
|
+
}
|
|
133
|
+
except Exception as e:
|
|
134
|
+
logger.error(f"Failed to get phone state: {e}", exc_info=True)
|
|
135
|
+
return {"package": "unknown", "error": str(e)}
|
|
136
|
+
|
|
137
|
+
def get_screenshot(self) -> bytes:
|
|
138
|
+
"""
|
|
139
|
+
Get a screenshot of the device as PNG bytes.
|
|
140
|
+
"""
|
|
141
|
+
try:
|
|
142
|
+
return self.device.screenshot()
|
|
143
|
+
except Exception as e:
|
|
144
|
+
logger.error(f"Failed to get screenshot: {e}", exc_info=True)
|
|
145
|
+
return b""
|
|
146
|
+
|
|
105
147
|
def __del__(self):
|
|
106
148
|
"""Cleanup when the object is destroyed."""
|
|
107
149
|
if hasattr(self, "tcp_forwarded") and self.tcp_forwarded:
|
|
@@ -103,6 +103,11 @@ def get_device_state(serial: str) -> Tuple[Dict[str, Any], Optional[bytes]]:
|
|
|
103
103
|
# Get current package
|
|
104
104
|
current_package = get_current_package(serial)
|
|
105
105
|
|
|
106
|
+
logger.debug("Capturing device state...")
|
|
107
|
+
|
|
108
|
+
# Get current package
|
|
109
|
+
current_package = get_current_package(serial)
|
|
110
|
+
|
|
106
111
|
# Get accessibility tree
|
|
107
112
|
a11y_tree = get_accessibility_tree(serial)
|
|
108
113
|
|
quash_mcp/models.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from pydantic import BaseModel, Field
|
|
2
|
+
from typing import Optional, List, Dict, Any
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
class TokensInfo(BaseModel):
|
|
6
|
+
prompt: int
|
|
7
|
+
completion: int
|
|
8
|
+
total: int
|
|
9
|
+
|
|
10
|
+
class ConfigInfo(BaseModel):
|
|
11
|
+
model: str
|
|
12
|
+
temperature: float
|
|
13
|
+
vision: bool = False
|
|
14
|
+
reasoning: bool = False
|
|
15
|
+
reflection: bool = False
|
|
16
|
+
debug: bool = False
|
|
17
|
+
|
|
18
|
+
class UIStateInfo(BaseModel):
|
|
19
|
+
a11y_tree: str
|
|
20
|
+
phone_state: Dict[str, Any]
|
|
21
|
+
|
|
22
|
+
class ChatHistoryMessage(BaseModel):
|
|
23
|
+
role: str
|
|
24
|
+
content: str
|
|
25
|
+
|
|
26
|
+
class AgentStepDTO(BaseModel):
|
|
27
|
+
step_number: int
|
|
28
|
+
reasoning: Optional[str] = None
|
|
29
|
+
code: Optional[str] = None
|
|
30
|
+
tokens_used: TokensInfo
|
|
31
|
+
cost: float
|
|
32
|
+
timestamp: datetime = Field(default_factory=datetime.utcnow)
|
|
33
|
+
|
|
34
|
+
class SessionDTO(BaseModel):
|
|
35
|
+
session_id: str
|
|
36
|
+
api_key: str
|
|
37
|
+
task: str
|
|
38
|
+
device_serial: str
|
|
39
|
+
config: ConfigInfo
|
|
40
|
+
chat_history: List[ChatHistoryMessage] = []
|
|
41
|
+
steps: List[AgentStepDTO] = []
|
|
42
|
+
ui_state: Optional[UIStateInfo] = None
|
quash_mcp/tools/execute_v3.py
CHANGED
|
@@ -149,7 +149,7 @@ def wait_for_action_effect(
|
|
|
149
149
|
"""
|
|
150
150
|
# Check if action should change UI
|
|
151
151
|
code_lower = executed_code.lower()
|
|
152
|
-
if "get_state" in code_lower
|
|
152
|
+
if "get_state" in code_lower:
|
|
153
153
|
# Action doesn't change UI - no need to wait
|
|
154
154
|
time.sleep(0.1)
|
|
155
155
|
return get_state_func(device_serial)[0], None, False
|
|
@@ -177,6 +177,8 @@ def wait_for_action_effect(
|
|
|
177
177
|
# MAIN EXECUTION FUNCTION
|
|
178
178
|
# ============================================================
|
|
179
179
|
|
|
180
|
+
from ..models import SessionDTO, UIStateInfo, ChatHistoryMessage, ConfigInfo, AgentStepDTO
|
|
181
|
+
|
|
180
182
|
async def execute_v3(
|
|
181
183
|
task: str,
|
|
182
184
|
max_steps: int = 15,
|
|
@@ -184,25 +186,6 @@ async def execute_v3(
|
|
|
184
186
|
) -> Dict[str, Any]:
|
|
185
187
|
"""
|
|
186
188
|
Execute automation task using step-by-step backend communication.
|
|
187
|
-
|
|
188
|
-
Each step:
|
|
189
|
-
1. Capture device state (State A)
|
|
190
|
-
2. Send to backend for AI decision
|
|
191
|
-
3. Execute returned action locally
|
|
192
|
-
4. POLL until state changes (State B ≠ State A) or timeout
|
|
193
|
-
5. Send State B to backend in next iteration
|
|
194
|
-
6. Repeat until complete
|
|
195
|
-
|
|
196
|
-
This ensures the backend always sees the UPDATED state after each action,
|
|
197
|
-
preventing the agent from making decisions based on stale state.
|
|
198
|
-
|
|
199
|
-
Args:
|
|
200
|
-
task: Natural language task description
|
|
201
|
-
max_steps: Maximum number of steps to execute (default: 15)
|
|
202
|
-
progress_callback: Optional callback for progress updates
|
|
203
|
-
|
|
204
|
-
Returns:
|
|
205
|
-
Dict with execution result and details
|
|
206
189
|
"""
|
|
207
190
|
state = get_state()
|
|
208
191
|
backend = get_backend_client()
|
|
@@ -272,15 +255,18 @@ async def execute_v3(
|
|
|
272
255
|
log_progress(f"🚀 Starting task: {task}")
|
|
273
256
|
log_progress(f"📱 Device: {state.device_serial}")
|
|
274
257
|
log_progress(f"🧠 Model: {config['model']}")
|
|
258
|
+
|
|
275
259
|
log_progress(f"🔢 Max steps: {max_steps}")
|
|
276
260
|
|
|
277
|
-
# Initialize
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
261
|
+
# Initialize Session DTO
|
|
262
|
+
|
|
263
|
+
session = SessionDTO(
|
|
264
|
+
session_id=f"session_{uuid.uuid4().hex[:12]}",
|
|
265
|
+
api_key=quash_api_key,
|
|
266
|
+
task=task,
|
|
267
|
+
device_serial=state.device_serial,
|
|
268
|
+
config=ConfigInfo(**config)
|
|
269
|
+
)
|
|
284
270
|
|
|
285
271
|
# Initialize local ADB tools for code execution
|
|
286
272
|
adb_tools = AdbTools(serial=state.device_serial, use_tcp=True)
|
|
@@ -337,43 +323,48 @@ async def execute_v3(
|
|
|
337
323
|
|
|
338
324
|
executor_locals = {}
|
|
339
325
|
|
|
326
|
+
start_time = time.time()
|
|
327
|
+
|
|
340
328
|
try:
|
|
341
329
|
# ============================================================
|
|
342
330
|
# STEP-BY-STEP EXECUTION LOOP
|
|
343
331
|
# ============================================================
|
|
344
|
-
while step_number < max_steps: # Use user-provided max_steps
|
|
345
|
-
step_number += 1
|
|
346
|
-
log_progress(f"🧠 Step {step_number}/{max_steps}: Analyzing...")
|
|
347
332
|
|
|
348
|
-
|
|
333
|
+
while len(session.steps) < max_steps:
|
|
334
|
+
|
|
335
|
+
log_progress(f"🧠 Step {len(session.steps) + 1}/{max_steps}: Analyzing...")
|
|
336
|
+
|
|
337
|
+
# 1. Capture device state and update session DTO
|
|
349
338
|
try:
|
|
350
339
|
ui_state_dict, screenshot_bytes = get_device_state(state.device_serial)
|
|
351
340
|
|
|
352
|
-
|
|
341
|
+
session.ui_state = UIStateInfo(**ui_state_dict)
|
|
342
|
+
# Update local tools with new state
|
|
343
|
+
if mahoraga_tools and "a11y_tree" in ui_state_dict and isinstance(ui_state_dict["a11y_tree"], str):
|
|
344
|
+
try:
|
|
345
|
+
import json
|
|
346
|
+
a11y_tree_obj = json.loads(ui_state_dict["a11y_tree"])
|
|
347
|
+
mahoraga_tools.update_state(a11y_tree_obj)
|
|
348
|
+
except (json.JSONDecodeError, TypeError):
|
|
349
|
+
pass # Ignore if not a valid JSON string
|
|
350
|
+
|
|
353
351
|
if not config["vision"]:
|
|
354
352
|
screenshot_bytes = None
|
|
355
353
|
|
|
356
|
-
# Log current state
|
|
357
354
|
current_package = ui_state_dict.get("phone_state", {}).get("package", "unknown")
|
|
358
355
|
log_progress(f"📱 Current app: {current_package}")
|
|
359
356
|
|
|
360
357
|
except Exception as e:
|
|
361
358
|
log_progress(f"⚠️ Warning: Failed to capture device state: {e}")
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
359
|
+
session.ui_state = UIStateInfo(
|
|
360
|
+
a11y_tree="<error>Failed to capture UI</error>",
|
|
361
|
+
phone_state={"package": "unknown"}
|
|
362
|
+
)
|
|
366
363
|
screenshot_bytes = None
|
|
367
364
|
|
|
368
|
-
# 2. Send to backend for AI decision
|
|
365
|
+
# 2. Send session DTO to backend for AI decision
|
|
369
366
|
step_result = await backend.execute_step(
|
|
370
|
-
|
|
371
|
-
session_id=session_id,
|
|
372
|
-
step_number=step_number,
|
|
373
|
-
task=task,
|
|
374
|
-
ui_state=ui_state_dict,
|
|
375
|
-
chat_history=chat_history,
|
|
376
|
-
config=config,
|
|
367
|
+
session=session,
|
|
377
368
|
screenshot_bytes=screenshot_bytes
|
|
378
369
|
)
|
|
379
370
|
|
|
@@ -384,20 +375,20 @@ async def execute_v3(
|
|
|
384
375
|
"status": "error",
|
|
385
376
|
"message": step_result["message"],
|
|
386
377
|
"error": step_result["error"],
|
|
387
|
-
"steps_taken":
|
|
388
|
-
"tokens":
|
|
389
|
-
"cost":
|
|
378
|
+
"steps_taken": len(session.steps),
|
|
379
|
+
"tokens": None,
|
|
380
|
+
"cost": None,
|
|
390
381
|
"duration_seconds": time.time() - start_time
|
|
391
382
|
}
|
|
392
383
|
|
|
393
|
-
# Update
|
|
394
|
-
|
|
395
|
-
|
|
384
|
+
# Update Session DTO with new step and chat history
|
|
385
|
+
new_step_data = step_result.get("new_step")
|
|
386
|
+
if new_step_data:
|
|
387
|
+
new_step = AgentStepDTO(**new_step_data)
|
|
388
|
+
session.steps.append(new_step)
|
|
389
|
+
assistant_response = step_result.get("assistant_response", "")
|
|
390
|
+
session.chat_history.append(ChatHistoryMessage(role="assistant", content=assistant_response))
|
|
396
391
|
|
|
397
|
-
total_tokens["prompt"] += step_tokens.get("prompt", 0)
|
|
398
|
-
total_tokens["completion"] += step_tokens.get("completion", 0)
|
|
399
|
-
total_tokens["total"] += step_tokens.get("total", 0)
|
|
400
|
-
total_cost += step_cost
|
|
401
392
|
|
|
402
393
|
# Get action from backend
|
|
403
394
|
action = step_result.get("action", {})
|
|
@@ -405,15 +396,13 @@ async def execute_v3(
|
|
|
405
396
|
code = action.get("code")
|
|
406
397
|
reasoning = action.get("reasoning")
|
|
407
398
|
|
|
399
|
+
|
|
408
400
|
# Log reasoning
|
|
409
401
|
if reasoning:
|
|
410
402
|
log_progress(f"🤔 Reasoning: {reasoning}")
|
|
411
403
|
|
|
412
|
-
# Update chat history
|
|
413
|
-
assistant_response = step_result.get("assistant_response", "")
|
|
414
|
-
chat_history.append({"role": "assistant", "content": assistant_response})
|
|
415
404
|
|
|
416
|
-
# 3. Check if task is complete
|
|
405
|
+
# 3. Check if task is complete BEFORE executing action
|
|
417
406
|
if step_result.get("completed", False):
|
|
418
407
|
success = step_result.get("success", False)
|
|
419
408
|
final_message = step_result.get("final_message", "Task completed")
|
|
@@ -421,62 +410,64 @@ async def execute_v3(
|
|
|
421
410
|
duration = time.time() - start_time
|
|
422
411
|
|
|
423
412
|
if success:
|
|
424
|
-
log_progress(f"✅ Task completed successfully
|
|
425
|
-
|
|
413
|
+
log_progress(f"✅ Task completed successfully!")
|
|
414
|
+
else:
|
|
415
|
+
log_progress(f"❌ Task marked as failed")
|
|
416
|
+
|
|
417
|
+
# Finalize session on backend
|
|
418
|
+
finalize_result = await backend.finalize_session(session=session)
|
|
419
|
+
|
|
420
|
+
if success:
|
|
421
|
+
log_progress(f"✅ Task completed successfully in {len(session.steps)} steps")
|
|
422
|
+
log_progress(f"💰 Usage: {finalize_result.get('total_tokens', {}).get('total')} tokens, ${finalize_result.get('total_cost', 0):.4f}")
|
|
426
423
|
|
|
427
424
|
return {
|
|
428
425
|
"status": "success",
|
|
429
|
-
"steps_taken":
|
|
426
|
+
"steps_taken": len(session.steps),
|
|
430
427
|
"final_message": final_message,
|
|
431
428
|
"message": f"✅ Success: {final_message}",
|
|
432
|
-
"tokens": total_tokens,
|
|
433
|
-
"cost": total_cost,
|
|
429
|
+
"tokens": finalize_result.get("total_tokens"),
|
|
430
|
+
"cost": finalize_result.get("total_cost"),
|
|
434
431
|
"duration_seconds": duration
|
|
435
432
|
}
|
|
436
433
|
else:
|
|
437
434
|
log_progress(f"❌ Task failed: {final_message}")
|
|
438
|
-
log_progress(f"💰 Usage: {total_tokens
|
|
435
|
+
log_progress(f"💰 Usage: {finalize_result.get('total_tokens', {}).get('total')} tokens, ${finalize_result.get('total_cost', 0):.4f}")
|
|
439
436
|
|
|
440
437
|
return {
|
|
441
438
|
"status": "failed",
|
|
442
|
-
"steps_taken":
|
|
439
|
+
"steps_taken": len(session.steps),
|
|
443
440
|
"final_message": final_message,
|
|
444
441
|
"message": f"❌ Failed: {final_message}",
|
|
445
|
-
"tokens": total_tokens,
|
|
446
|
-
"cost": total_cost,
|
|
442
|
+
"tokens": finalize_result.get("total_tokens"),
|
|
443
|
+
"cost": finalize_result.get("total_cost"),
|
|
447
444
|
"duration_seconds": duration
|
|
448
445
|
}
|
|
449
446
|
|
|
450
|
-
|
|
447
|
+
|
|
448
|
+
# 4. Execute action locally (only if task is not complete)
|
|
451
449
|
if code and action_type == "execute_code":
|
|
452
450
|
log_progress(f"⚡ Executing action...")
|
|
453
451
|
|
|
454
|
-
#
|
|
455
|
-
|
|
452
|
+
log_progress(f"```python\n{code}\n```") # Log the code
|
|
453
|
+
|
|
454
|
+
old_ui_state = session.ui_state.model_dump().copy()
|
|
456
455
|
|
|
457
456
|
try:
|
|
458
457
|
import io
|
|
459
458
|
import contextlib
|
|
460
459
|
|
|
461
|
-
# Capture stdout and stderr to get tool function outputs
|
|
462
460
|
stdout = io.StringIO()
|
|
463
461
|
stderr = io.StringIO()
|
|
464
462
|
|
|
465
463
|
with contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr):
|
|
466
|
-
# Execute code in sandbox
|
|
467
464
|
exec(code, executor_globals, executor_locals)
|
|
468
465
|
|
|
469
|
-
# Get captured output
|
|
470
466
|
execution_output = stdout.getvalue()
|
|
471
467
|
error_output = stderr.getvalue()
|
|
472
468
|
|
|
473
|
-
# ============================================================
|
|
474
|
-
# CRITICAL: Wait for state change (polling-based event detection)
|
|
475
|
-
# ============================================================
|
|
476
469
|
log_progress(f"⏳ Waiting for UI state to update...")
|
|
477
|
-
|
|
478
470
|
try:
|
|
479
|
-
# Poll until state changes or timeout
|
|
480
471
|
new_ui_state_dict, _, state_changed = wait_for_action_effect(
|
|
481
472
|
get_device_state,
|
|
482
473
|
state.device_serial,
|
|
@@ -486,7 +477,6 @@ async def execute_v3(
|
|
|
486
477
|
poll_interval=0.5
|
|
487
478
|
)
|
|
488
479
|
|
|
489
|
-
# Log what happened
|
|
490
480
|
if state_changed:
|
|
491
481
|
old_pkg = old_ui_state.get("phone_state", {}).get("package", "")
|
|
492
482
|
new_pkg = new_ui_state_dict.get("phone_state", {}).get("package", "")
|
|
@@ -495,6 +485,7 @@ async def execute_v3(
|
|
|
495
485
|
log_progress(f"✅ State changed: App switched ({old_pkg} → {new_pkg})")
|
|
496
486
|
else:
|
|
497
487
|
log_progress(f"✅ State changed: UI updated")
|
|
488
|
+
|
|
498
489
|
else:
|
|
499
490
|
log_progress(f"⚠️ WARNING: State did NOT change after action (timeout)")
|
|
500
491
|
log_progress(f" This might mean the action had no effect or took too long")
|
|
@@ -502,10 +493,8 @@ async def execute_v3(
|
|
|
502
493
|
except Exception as e:
|
|
503
494
|
log_progress(f"⚠️ Error during state change detection: {e}")
|
|
504
495
|
state_changed = False
|
|
505
|
-
# Fallback: Just wait a bit
|
|
506
496
|
time.sleep(1.5)
|
|
507
497
|
|
|
508
|
-
# Build feedback message
|
|
509
498
|
feedback_parts = []
|
|
510
499
|
|
|
511
500
|
if execution_output:
|
|
@@ -523,56 +512,36 @@ async def execute_v3(
|
|
|
523
512
|
|
|
524
513
|
log_progress(f"✅ {feedback[:200]}")
|
|
525
514
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
"role": "user",
|
|
529
|
-
"content": f"Execution Result:\n```\n{feedback}\n```"
|
|
530
|
-
})
|
|
515
|
+
session.chat_history.append(ChatHistoryMessage(role="user", content=f"Execution Result:\n```\n{feedback}\n```"))
|
|
516
|
+
|
|
531
517
|
|
|
532
518
|
except Exception as e:
|
|
533
519
|
error_msg = f"Error during execution: {str(e)}"
|
|
534
520
|
log_progress(f"💥 Action failed: {error_msg}")
|
|
535
521
|
|
|
536
|
-
|
|
537
|
-
chat_history.append({
|
|
538
|
-
"role": "user",
|
|
539
|
-
"content": f"Execution Error:\n```\n{error_msg}\n```"
|
|
540
|
-
})
|
|
522
|
+
session.chat_history.append(ChatHistoryMessage(role="user", content=f"Execution Error:\n```\n{error_msg}\n```"))
|
|
541
523
|
|
|
542
|
-
|
|
543
|
-
# No code to execute
|
|
524
|
+
elif not code:
|
|
544
525
|
log_progress("⚠️ No action code provided by backend")
|
|
545
|
-
chat_history.append(
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
})
|
|
526
|
+
session.chat_history.append(ChatHistoryMessage(role="user", content="No code was provided. Please provide code to execute."))
|
|
527
|
+
|
|
528
|
+
|
|
549
529
|
|
|
550
530
|
# Max steps reached
|
|
551
531
|
log_progress(f"⚠️ Reached maximum steps ({max_steps})")
|
|
552
|
-
log_progress(f"💰 Usage: {total_tokens['total']} tokens, ${total_cost:.4f}")
|
|
553
532
|
|
|
554
533
|
duration = time.time() - start_time
|
|
555
534
|
|
|
556
|
-
# Finalize session on backend
|
|
557
|
-
await backend.finalize_session(
|
|
558
|
-
api_key=quash_api_key,
|
|
559
|
-
session_id=session_id,
|
|
560
|
-
task=task,
|
|
561
|
-
device_serial=state.device_serial,
|
|
562
|
-
status="max_steps",
|
|
563
|
-
final_message=f"Reached maximum step limit of {max_steps}",
|
|
564
|
-
error=None,
|
|
565
|
-
duration_seconds=duration,
|
|
566
|
-
config=config
|
|
567
|
-
)
|
|
535
|
+
# Finalize session on backend
|
|
536
|
+
finalize_result = await backend.finalize_session(session=session)
|
|
568
537
|
|
|
569
538
|
return {
|
|
570
539
|
"status": "failed",
|
|
571
|
-
"steps_taken":
|
|
540
|
+
"steps_taken": len(session.steps),
|
|
572
541
|
"final_message": f"Reached maximum step limit of {max_steps}",
|
|
573
542
|
"message": "❌ Failed: Maximum steps reached",
|
|
574
|
-
"tokens": total_tokens,
|
|
575
|
-
"cost": total_cost,
|
|
543
|
+
"tokens": finalize_result.get("total_tokens"),
|
|
544
|
+
"cost": finalize_result.get("total_cost"),
|
|
576
545
|
"duration_seconds": duration
|
|
577
546
|
}
|
|
578
547
|
|
|
@@ -581,24 +550,14 @@ async def execute_v3(
|
|
|
581
550
|
duration = time.time() - start_time
|
|
582
551
|
|
|
583
552
|
# Finalize session on backend
|
|
584
|
-
await backend.finalize_session(
|
|
585
|
-
api_key=quash_api_key,
|
|
586
|
-
session_id=session_id,
|
|
587
|
-
task=task,
|
|
588
|
-
device_serial=state.device_serial,
|
|
589
|
-
status="interrupted",
|
|
590
|
-
final_message="Task interrupted by user",
|
|
591
|
-
error=None,
|
|
592
|
-
duration_seconds=duration,
|
|
593
|
-
config=config
|
|
594
|
-
)
|
|
553
|
+
finalize_result = await backend.finalize_session(session=session)
|
|
595
554
|
|
|
596
555
|
return {
|
|
597
556
|
"status": "interrupted",
|
|
598
557
|
"message": "ℹ️ Task execution interrupted",
|
|
599
|
-
"steps_taken":
|
|
600
|
-
"tokens": total_tokens,
|
|
601
|
-
"cost": total_cost,
|
|
558
|
+
"steps_taken": len(session.steps),
|
|
559
|
+
"tokens": finalize_result.get("total_tokens"),
|
|
560
|
+
"cost": finalize_result.get("total_cost"),
|
|
602
561
|
"duration_seconds": duration
|
|
603
562
|
}
|
|
604
563
|
|
|
@@ -608,25 +567,15 @@ async def execute_v3(
|
|
|
608
567
|
duration = time.time() - start_time
|
|
609
568
|
|
|
610
569
|
# Finalize session on backend
|
|
611
|
-
await backend.finalize_session(
|
|
612
|
-
api_key=quash_api_key,
|
|
613
|
-
session_id=session_id,
|
|
614
|
-
task=task,
|
|
615
|
-
device_serial=state.device_serial,
|
|
616
|
-
status="error",
|
|
617
|
-
final_message=None,
|
|
618
|
-
error=error_msg,
|
|
619
|
-
duration_seconds=duration,
|
|
620
|
-
config=config
|
|
621
|
-
)
|
|
570
|
+
finalize_result = await backend.finalize_session(session=session)
|
|
622
571
|
|
|
623
572
|
return {
|
|
624
573
|
"status": "error",
|
|
625
574
|
"message": f"💥 Execution error: {error_msg}",
|
|
626
575
|
"error": error_msg,
|
|
627
|
-
"steps_taken":
|
|
628
|
-
"tokens": total_tokens,
|
|
629
|
-
"cost": total_cost,
|
|
576
|
+
"steps_taken": len(session.steps),
|
|
577
|
+
"tokens": finalize_result.get("total_tokens"),
|
|
578
|
+
"cost": finalize_result.get("total_cost"),
|
|
630
579
|
"duration_seconds": duration
|
|
631
580
|
}
|
|
632
581
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: quash-mcp
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.13
|
|
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
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
quash_mcp/__init__.py,sha256=LImiWCRgjAbb5DZXBq2DktUEAbftvnO61Vil4Ayun9A,39
|
|
2
2
|
quash_mcp/__main__.py,sha256=WCg5OlnXhr6i0XJHAUGpbhliMy3qE2SJkFzVD4wO-lw,239
|
|
3
|
-
quash_mcp/backend_client.py,sha256=
|
|
3
|
+
quash_mcp/backend_client.py,sha256=S7IF95GEUtPZJPK9Dy0Gbbv7EV6SexOuAX0sw-u5i6I,9926
|
|
4
|
+
quash_mcp/models.py,sha256=0S7uCZZRRtQuSwIwKTDKZnX2HRMYOBDoDcl7-b8Tzpk,1001
|
|
4
5
|
quash_mcp/server.py,sha256=scUGnplxjsvyYLK2q6hrjl-5Chkdnat9pODDtLzsQFY,15519
|
|
5
6
|
quash_mcp/state.py,sha256=Tnt795GnZcas-h62Y6KYyIZVopeoWPM0TbRwOeVFYj4,4394
|
|
6
7
|
quash_mcp/device/__init__.py,sha256=6e8CtHolt-vJKPxZUU_Vsd6-QGqos9VrFykaLTT90rk,772
|
|
7
|
-
quash_mcp/device/adb_tools.py,sha256=
|
|
8
|
+
quash_mcp/device/adb_tools.py,sha256=SsYnzGjG3XsfbiAHiC7PpgLdC149kRH-YkoXQZvxvWc,5439
|
|
8
9
|
quash_mcp/device/portal.py,sha256=sDLJOruUwwNNxIDriiXB4vT0BZYILidgzVgdhHCEkDY,5241
|
|
9
|
-
quash_mcp/device/state_capture.py,sha256=
|
|
10
|
+
quash_mcp/device/state_capture.py,sha256=WUNewRlgi_A6L8usWsga-9iqMTroCuSaZ0OdY8EheU0,3473
|
|
10
11
|
quash_mcp/tools/__init__.py,sha256=r4fMAjHDjHUbimRwYW7VYUDkQHs12UVsG_IBmWpeX9s,249
|
|
11
12
|
quash_mcp/tools/build.py,sha256=M6tGXWrQNkdtCYYrK14gUaoufQvyoor_hNN0lBPSVHY,30321
|
|
12
13
|
quash_mcp/tools/build_old.py,sha256=6M9gaqZ_dX4B7UFTxSMD8T1BX0zEwQUL7RJ8ItNfB54,6016
|
|
@@ -14,10 +15,10 @@ quash_mcp/tools/configure.py,sha256=cv4RTolu6qae-XzyACSJUDrALfd0gYC-XE5s66_zfNk,
|
|
|
14
15
|
quash_mcp/tools/connect.py,sha256=Kc7RGRUgtd2sR_bv6U4CB4kWSaLfsDc5kBo9u4FEjzs,4799
|
|
15
16
|
quash_mcp/tools/execute.py,sha256=kR3VzIl31Lek-js4Hgxs-S_ls4YwKnbqkt79KFbvFuM,909
|
|
16
17
|
quash_mcp/tools/execute_v2_backup.py,sha256=waWnaD0dEVcOJgRBbqZo3HnxME1s6YUOn8aRbm4R3X4,6081
|
|
17
|
-
quash_mcp/tools/execute_v3.py,sha256=
|
|
18
|
+
quash_mcp/tools/execute_v3.py,sha256=_tm0pcgSeGchooUFpiv1sTHY7w0psrCy2lQ3IDGhwKo,21899
|
|
18
19
|
quash_mcp/tools/runsuite.py,sha256=gohLk9FpN8v7F0a69fspqOqUexTcslpYf3qU-iIZZ3s,7220
|
|
19
20
|
quash_mcp/tools/usage.py,sha256=g76A6FO36fThoyRFG7q92QmS3Kh1pIKOrhYOzUdIubA,1155
|
|
20
|
-
quash_mcp-0.2.
|
|
21
|
-
quash_mcp-0.2.
|
|
22
|
-
quash_mcp-0.2.
|
|
23
|
-
quash_mcp-0.2.
|
|
21
|
+
quash_mcp-0.2.13.dist-info/METADATA,sha256=EdlucJ9MZlAjuTOgQzA-GVGgTcR8Eqo0NfhG4Tfv6EQ,8424
|
|
22
|
+
quash_mcp-0.2.13.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
23
|
+
quash_mcp-0.2.13.dist-info/entry_points.txt,sha256=9sbDxrx0ApGDVRS-IE3mQgSao3DwKnnV_k-_ipFn9QI,52
|
|
24
|
+
quash_mcp-0.2.13.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|