minitap-mobile-use 2.7.2__py3-none-any.whl → 2.8.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 minitap-mobile-use might be problematic. Click here for more details.

@@ -43,7 +43,7 @@ async def run_automation(
43
43
  config.with_graph_config_callbacks(graph_config_callbacks)
44
44
 
45
45
  agent = Agent(config=config.build())
46
- agent.init(
46
+ await agent.init(
47
47
  retry_count=int(os.getenv("MOBILE_USE_HEALTH_RETRIES", 5)),
48
48
  retry_wait_seconds=int(os.getenv("MOBILE_USE_HEALTH_DELAY", 2)),
49
49
  )
@@ -63,7 +63,7 @@ async def run_automation(
63
63
 
64
64
  await agent.run_task(request=task.build())
65
65
 
66
- agent.clean()
66
+ await agent.clean()
67
67
 
68
68
 
69
69
  @app.command()
@@ -13,6 +13,7 @@ from typing import Any, TypeVar, overload
13
13
  from adbutils import AdbClient
14
14
  from dotenv import load_dotenv
15
15
  from langchain_core.messages import AIMessage
16
+ from PIL import Image
16
17
  from pydantic import BaseModel
17
18
 
18
19
  from minitap.mobile_use.agents.outputter.outputter import outputter
@@ -37,12 +38,14 @@ from minitap.mobile_use.graph.state import State
37
38
  from minitap.mobile_use.sdk.builders.agent_config_builder import get_default_agent_config
38
39
  from minitap.mobile_use.sdk.builders.task_request_builder import TaskRequestBuilder
39
40
  from minitap.mobile_use.sdk.constants import DEFAULT_HW_BRIDGE_BASE_URL, DEFAULT_SCREEN_API_BASE_URL
41
+ from minitap.mobile_use.sdk.services.cloud_mobile import CloudMobileService
40
42
  from minitap.mobile_use.sdk.services.platform import PlatformService
41
43
  from minitap.mobile_use.sdk.types.agent import AgentConfig
42
44
  from minitap.mobile_use.sdk.types.exceptions import (
43
45
  AgentNotInitializedError,
44
46
  AgentProfileNotFoundError,
45
47
  AgentTaskRequestError,
48
+ CloudMobileServiceUninitializedError,
46
49
  DeviceNotFoundError,
47
50
  ExecutableNotFoundError,
48
51
  PlatformServiceUninitializedError,
@@ -92,6 +95,7 @@ class Agent:
92
95
  _adb_client: AdbClient | None
93
96
  _current_task: asyncio.Task | None = None
94
97
  _task_lock: asyncio.Lock
98
+ _cloud_mobile_id: str | None = None
95
99
 
96
100
  def __init__(self, *, config: AgentConfig | None = None):
97
101
  self._config = config or get_default_agent_config()
@@ -105,19 +109,38 @@ class Agent:
105
109
  self._config.servers.screen_api_base_url == DEFAULT_SCREEN_API_BASE_URL
106
110
  )
107
111
  self._task_lock = asyncio.Lock()
112
+
108
113
  # Initialize platform service if API key is available in environment
109
- # Note: Can also be initialized later with API key from request
114
+ # Note: Can also be initialized later with API key at agent .init()
110
115
  if settings.MINITAP_API_KEY:
111
116
  self._platform_service = PlatformService()
117
+ self._cloud_mobile_service = CloudMobileService()
112
118
  else:
113
119
  self._platform_service = None
120
+ self._cloud_mobile_service = None
114
121
 
115
- def init(
122
+ async def init(
116
123
  self,
124
+ api_key: str | None = None,
117
125
  server_restart_attempts: int = 3,
118
126
  retry_count: int = 5,
119
127
  retry_wait_seconds: int = 5,
120
128
  ):
129
+ if api_key:
130
+ self._platform_service = PlatformService(api_key=api_key)
131
+ self._cloud_mobile_service = CloudMobileService(api_key=api_key)
132
+
133
+ # Skip initialization for cloud devices - no local setup required
134
+ if self._config.cloud_mobile_id_or_ref:
135
+ if not self._cloud_mobile_service:
136
+ raise CloudMobileServiceUninitializedError()
137
+ self._cloud_mobile_id = await self._cloud_mobile_service.resolve_cloud_mobile_id(
138
+ cloud_mobile_id_or_ref=self._config.cloud_mobile_id_or_ref,
139
+ )
140
+ logger.info("Cloud device configured - skipping local initialization")
141
+ self._initialized = True
142
+ return True
143
+
121
144
  if not which("adb") and not which("xcrun"):
122
145
  raise ExecutableNotFoundError("cli_tools")
123
146
  if self._is_default_hw_bridge and not which("maestro"):
@@ -233,25 +256,30 @@ class Agent:
233
256
  name: str | None = None,
234
257
  request: TaskRequest[TOutput] | PlatformTaskRequest[TOutput] | None = None,
235
258
  ) -> str | dict | TOutput | None:
259
+ # Check if cloud mobile is configured
260
+ if self._config.cloud_mobile_id_or_ref:
261
+ if request is None or not isinstance(request, PlatformTaskRequest):
262
+ raise AgentTaskRequestError(
263
+ "When using a cloud mobile, only PlatformTaskRequest is supported. "
264
+ "Use AgentConfigBuilder.for_cloud_mobile() only with PlatformTaskRequest."
265
+ )
266
+ # Use cloud mobile execution path
267
+ return await self._run_cloud_mobile_task(request=request)
268
+
269
+ # Normal local execution path
236
270
  if request is not None:
237
271
  task_info = None
238
- platform_service = None
239
272
  if isinstance(request, PlatformTaskRequest):
240
- # Initialize platform service with API key from request if provided
241
- if request.api_key:
242
- platform_service = PlatformService(api_key=request.api_key)
243
- elif self._platform_service:
244
- platform_service = self._platform_service
245
- else:
273
+ if not self._platform_service:
246
274
  raise PlatformServiceUninitializedError()
247
- task_info = await platform_service.create_task_run(request=request)
275
+ task_info = await self._platform_service.create_task_run(request=request)
248
276
  if isinstance(request, CloudDevicePlatformTaskRequest):
249
277
  request.task_run_id = task_info.task_run.id
250
278
  request.task_run_id_available_event.set()
251
279
  self._config.agent_profiles[task_info.llm_profile.name] = task_info.llm_profile
252
280
  request = task_info.task_request
253
281
  return await self._run_task(
254
- request=request, task_info=task_info, platform_service=platform_service
282
+ request=request, task_info=task_info, platform_service=self._platform_service
255
283
  )
256
284
  if goal is None:
257
285
  raise AgentTaskRequestError("Goal is required")
@@ -267,6 +295,96 @@ class Agent:
267
295
  task_request.with_name(name=name)
268
296
  return await self._run_task(task_request.build())
269
297
 
298
+ async def _run_cloud_mobile_task(
299
+ self,
300
+ request: PlatformTaskRequest[TOutput],
301
+ ) -> str | dict | TOutput | None:
302
+ """
303
+ Execute a task on a cloud mobile.
304
+
305
+ This method triggers the task execution on the Platform and polls
306
+ for completion without running any agentic logic locally.
307
+ """
308
+ if not self._cloud_mobile_id:
309
+ raise AgentTaskRequestError("Cloud mobile ID is not configured")
310
+
311
+ if not self._cloud_mobile_service:
312
+ raise CloudMobileServiceUninitializedError()
313
+
314
+ # Start cloud mobile if not already started
315
+ logger.info(f"Starting cloud mobile '{self._cloud_mobile_id}'...")
316
+ await self._cloud_mobile_service.start_and_wait_for_ready(
317
+ cloud_mobile_id=self._cloud_mobile_id,
318
+ )
319
+ logger.info(
320
+ f"Starting cloud mobile task execution '{self._cloud_mobile_id}'",
321
+ )
322
+
323
+ def log_callback(message: str):
324
+ """Callback for logging timeline updates."""
325
+ logger.info(message)
326
+
327
+ def status_callback(
328
+ status: TaskRunStatus,
329
+ status_message: str | None,
330
+ ):
331
+ """Callback for status updates."""
332
+ logger.info(f"Task status update: [{status}] {status_message}")
333
+
334
+ async def _execute_cloud(cloud_mobile_service: CloudMobileService, cloud_mobile_id: str):
335
+ try:
336
+ # Execute task on cloud mobile and wait for completion
337
+ final_status, error, output = await cloud_mobile_service.run_task_on_cloud_mobile(
338
+ cloud_mobile_id=cloud_mobile_id,
339
+ request=request,
340
+ on_status_update=status_callback,
341
+ on_log=log_callback,
342
+ )
343
+ if final_status == "completed":
344
+ logger.success("Cloud mobile task completed successfully")
345
+ return output
346
+ if final_status == "failed":
347
+ logger.error(f"Cloud mobile task failed: {error}")
348
+ raise AgentTaskRequestError(
349
+ f"Task execution failed on cloud mobile: {error}",
350
+ )
351
+ if final_status == "cancelled":
352
+ logger.warning("Cloud mobile task was cancelled")
353
+ raise AgentTaskRequestError("Task execution was cancelled")
354
+ logger.error(f"Unknown cloud mobile task status: {final_status}")
355
+ raise AgentTaskRequestError(f"Unknown task status: {final_status}")
356
+ except asyncio.CancelledError:
357
+ # Propagate cancellation to parent coroutine.
358
+ logger.info("Task cancelled during execution, re-raising CancelledError")
359
+ raise
360
+ except AgentTaskRequestError:
361
+ # Re-raise known exceptions
362
+ raise
363
+ except Exception as e:
364
+ logger.error(f"Unexpected error during cloud mobile task execution: {e}")
365
+ raise AgentTaskRequestError(f"Unexpected error: {e}") from e
366
+
367
+ async with self._task_lock:
368
+ if self._current_task and not self._current_task.done():
369
+ logger.warning(
370
+ "Another cloud task is running; cancelling it before starting new one.",
371
+ )
372
+ self.stop_current_task()
373
+ try:
374
+ await self._current_task
375
+ except asyncio.CancelledError:
376
+ pass
377
+ try:
378
+ self._current_task = asyncio.create_task(
379
+ _execute_cloud(
380
+ cloud_mobile_service=self._cloud_mobile_service,
381
+ cloud_mobile_id=self._cloud_mobile_id,
382
+ ),
383
+ )
384
+ return await self._current_task
385
+ finally:
386
+ self._current_task = None
387
+
270
388
  async def _run_task(
271
389
  self,
272
390
  request: TaskRequest[TOutput],
@@ -456,6 +574,9 @@ class Agent:
456
574
  Uses the configured Screen API base URL instead of hardcoding localhost.
457
575
  """
458
576
  try:
577
+ # In cloud mode, local streaming health is irrelevant.
578
+ if self._config.cloud_mobile_id_or_ref:
579
+ return True
459
580
  response = self._screen_api_client.get_with_retry("/streaming-status", timeout=2)
460
581
  if response.status_code == 200:
461
582
  data = response.json()
@@ -465,7 +586,86 @@ class Agent:
465
586
  except Exception:
466
587
  return False
467
588
 
468
- def clean(self, force: bool = False):
589
+ async def get_screenshot(self) -> Image.Image:
590
+ """
591
+ Capture a screenshot from the mobile device.
592
+
593
+ For cloud mobiles, this method calls the mobile-manager endpoint.
594
+ For local mobiles, it uses ADB (Android) or xcrun (iOS) directly.
595
+
596
+ Returns:
597
+ Screenshot as PIL Image
598
+
599
+ Raises:
600
+ AgentNotInitializedError: If the agent is not initialized
601
+ PlatformServiceUninitializedError: If cloud mobile service is not available
602
+ Exception: If screenshot capture fails
603
+ """
604
+ # Check if cloud mobile is configured
605
+ if self._cloud_mobile_id:
606
+ if not self._cloud_mobile_service:
607
+ raise CloudMobileServiceUninitializedError()
608
+ screenshot = await self._cloud_mobile_service.get_screenshot(
609
+ cloud_mobile_id=self._cloud_mobile_id,
610
+ )
611
+ return screenshot
612
+
613
+ # Local device - use ADB or xcrun directly
614
+ if not self._initialized:
615
+ raise AgentNotInitializedError()
616
+
617
+ if self._device_context.mobile_platform == DevicePlatform.ANDROID:
618
+ # Use ADB to capture screenshot
619
+ logger.info("Capturing screenshot from local Android device")
620
+ if not self._adb_client:
621
+ raise Exception("ADB client not initialized")
622
+
623
+ device = self._adb_client.device(serial=self._device_context.device_id)
624
+ screenshot = await asyncio.to_thread(device.screenshot)
625
+ logger.info("Screenshot captured from local Android device")
626
+ return screenshot
627
+
628
+ elif self._device_context.mobile_platform == DevicePlatform.IOS:
629
+ # Use xcrun to capture screenshot
630
+ import functools
631
+ import subprocess
632
+ from io import BytesIO
633
+
634
+ logger.info("Capturing screenshot from local iOS device")
635
+ try:
636
+ # xcrun simctl io <device> screenshot --type=png -
637
+ result = await asyncio.to_thread(
638
+ functools.partial(
639
+ subprocess.run,
640
+ [
641
+ "xcrun",
642
+ "simctl",
643
+ "io",
644
+ self._device_context.device_id,
645
+ "screenshot",
646
+ "--type=png",
647
+ "-",
648
+ ],
649
+ capture_output=True,
650
+ check=True,
651
+ )
652
+ )
653
+ # Convert bytes to PIL Image
654
+ screenshot = Image.open(BytesIO(result.stdout))
655
+ logger.info("Screenshot captured from local iOS device")
656
+ return screenshot
657
+ except subprocess.CalledProcessError as e:
658
+ logger.error(f"Failed to capture screenshot: {e}")
659
+ raise Exception(f"Failed to capture screenshot from iOS device: {e}")
660
+
661
+ else:
662
+ raise Exception(f"Unsupported platform: {self._device_context.mobile_platform}")
663
+
664
+ async def clean(self, force: bool = False):
665
+ if self._cloud_mobile_id:
666
+ self._initialized = False
667
+ logger.info("✅ Cloud-mode agent stopped.")
668
+ return
469
669
  if not self._initialized and not force:
470
670
  return
471
671
  screen_api_ok, hw_bridge_ok = stop_servers(
@@ -45,6 +45,7 @@ class AgentConfigBuilder:
45
45
  self._device_platform: DevicePlatform | None = None
46
46
  self._servers: ServerConfig = get_default_servers()
47
47
  self._graph_config_callbacks: Callbacks = None
48
+ self._cloud_mobile_id_or_ref: str | None = None
48
49
 
49
50
  def add_profile(self, profile: AgentProfile, validate: bool = True) -> "AgentConfigBuilder":
50
51
  """
@@ -95,10 +96,35 @@ class AgentConfigBuilder:
95
96
  platform: The device platform (ANDROID or IOS)
96
97
  device_id: The unique identifier for the device
97
98
  """
99
+ if self._cloud_mobile_id_or_ref is not None:
100
+ raise ValueError(
101
+ "Device ID cannot be set when a cloud mobile is already configured.\n"
102
+ "> for_device() and for_cloud_mobile() are mutually exclusive"
103
+ )
98
104
  self._device_id = device_id
99
105
  self._device_platform = platform
100
106
  return self
101
107
 
108
+ def for_cloud_mobile(self, cloud_mobile_id_or_ref: str) -> "AgentConfigBuilder":
109
+ """
110
+ Configure the mobile-use agent to use a cloud mobile.
111
+
112
+ When using a cloud mobile, tasks are executed remotely via the Platform API,
113
+ and only PlatformTaskRequest can be used.
114
+
115
+ Args:
116
+ cloud_mobile_id_or_ref: The unique identifier or reference name for the cloud mobile.
117
+ Can be either a UUID (e.g., '550e8400-e29b-41d4-a716-446655440000')
118
+ or a reference name (e.g., 'my-test-device')
119
+ """
120
+ if self._device_id is not None:
121
+ raise ValueError(
122
+ "Cloud mobile device ID cannot be set when a device is already configured.\n"
123
+ "> for_device() and for_cloud_mobile() are mutually exclusive"
124
+ )
125
+ self._cloud_mobile_id_or_ref = cloud_mobile_id_or_ref
126
+ return self
127
+
102
128
  def with_default_task_config(self, config: TaskRequestCommon) -> "AgentConfigBuilder":
103
129
  """
104
130
  Set the default task configuration.
@@ -217,6 +243,7 @@ class AgentConfigBuilder:
217
243
  device_platform=self._device_platform,
218
244
  servers=self._servers,
219
245
  graph_config_callbacks=self._graph_config_callbacks,
246
+ cloud_mobile_id_or_ref=self._cloud_mobile_id_or_ref,
220
247
  )
221
248
 
222
249
 
@@ -15,7 +15,7 @@ These examples demonstrate two different ways to use the SDK, each applying an a
15
15
  This script shows the simplest way to run minitap :
16
16
 
17
17
  - Visit https://platform.minitap.ai to create a task and get your API key.
18
- - Initialize the agent with your API key: Agent(minitap_api_key=...).
18
+ - Initialize the agent with your API key: .init(api_key=...).
19
19
  - Ask the agent to run one of the tasks you’ve set up in the Minitap platform
20
20
  (e.g., "like-instagram-post").
21
21
  - The task’s goal and settings live in the Minitap platform, you don’t need
@@ -34,7 +34,7 @@ async def main() -> None:
34
34
  Set MINITAP_API_KEY and MINITAP_BASE_URL environment variables.
35
35
  """
36
36
  agent = Agent()
37
- agent.init()
37
+ await agent.init()
38
38
 
39
39
  # Example 1: Simple manual task
40
40
  result = await agent.run_task(
@@ -58,7 +58,7 @@ async def main() -> None:
58
58
  )
59
59
  print("Result 2:", result)
60
60
 
61
- agent.clean()
61
+ await agent.clean()
62
62
 
63
63
 
64
64
  if __name__ == "__main__":
@@ -31,16 +31,15 @@ async def main() -> None:
31
31
  Set MINITAP_API_KEY and MINITAP_BASE_URL environment variables.
32
32
  """
33
33
  agent = Agent()
34
- agent.init()
34
+ await agent.init(api_key="<api-key>") # or set MINITAP_API_KEY env variable
35
35
  result = await agent.run_task(
36
36
  request=PlatformTaskRequest(
37
37
  task="your-task-name",
38
38
  profile="your-profile-name",
39
- api_key="<api-key>", # or set MINITAP_API_KEY env variable
40
39
  )
41
40
  )
42
41
  print(result)
43
- agent.clean()
42
+ await agent.clean()
44
43
 
45
44
 
46
45
  if __name__ == "__main__":
@@ -33,7 +33,7 @@ async def main() -> None:
33
33
 
34
34
  try:
35
35
  # Initialize agent (finds a device, starts required servers)
36
- agent.init()
36
+ await agent.init()
37
37
 
38
38
  # Calculate yesterday's date for the example
39
39
  yesterday = date.today() - timedelta(days=1)
@@ -69,7 +69,7 @@ async def main() -> None:
69
69
  print(f"Error: {e}")
70
70
  finally:
71
71
  # Always clean up resources
72
- agent.clean()
72
+ await agent.clean()
73
73
 
74
74
 
75
75
  if __name__ == "__main__":
@@ -167,7 +167,7 @@ async def main():
167
167
 
168
168
  try:
169
169
  # Initialize agent (finds a device, starts required servers)
170
- agent.init()
170
+ await agent.init()
171
171
 
172
172
  print("Checking for notifications...")
173
173
 
@@ -217,7 +217,7 @@ async def main():
217
217
 
218
218
  finally:
219
219
  # Clean up
220
- agent.clean()
220
+ await agent.clean()
221
221
  print(f"\nTraces saved to: {traces_dir}")
222
222
 
223
223
 
@@ -0,0 +1,582 @@
1
+ """Service for managing cloud device (virtual mobile) task execution."""
2
+
3
+ import asyncio
4
+ from datetime import UTC, datetime
5
+ from io import BytesIO
6
+ import json
7
+ from typing import Any, Literal
8
+ from collections.abc import Callable
9
+
10
+ import httpx
11
+ from PIL import Image
12
+ from pydantic import BaseModel, Field
13
+
14
+ from minitap.mobile_use.config import settings
15
+ from minitap.mobile_use.sdk.types.exceptions import PlatformServiceError
16
+ from minitap.mobile_use.sdk.types.platform import TaskRunStatus
17
+ from minitap.mobile_use.sdk.types.task import PlatformTaskRequest
18
+ from minitap.mobile_use.utils.logger import get_logger
19
+
20
+ logger = get_logger(__name__)
21
+
22
+
23
+ class RunTaskRequest(BaseModel):
24
+ """Request to run a task on a virtual mobile."""
25
+
26
+ task_request: dict[str, Any] = Field(..., alias="taskRequest", description="Task request")
27
+
28
+
29
+ class RunTaskResponse(BaseModel):
30
+ """Response from running a task on a virtual mobile."""
31
+
32
+ task_run_id: str = Field(..., alias="taskRunId", description="ID of the task run")
33
+
34
+
35
+ class SubgoalTimelineItemResponse(BaseModel):
36
+ """A subgoal timeline item."""
37
+
38
+ name: str = Field(..., alias="name", description="Name of the subgoal")
39
+ state: str = Field(..., alias="state", description="State of the subgoal")
40
+ started_at: datetime | None = Field(
41
+ None,
42
+ alias="startedAt",
43
+ description="Start time of the subgoal",
44
+ )
45
+ ended_at: datetime | None = Field(
46
+ None,
47
+ alias="endedAt",
48
+ description="End time of the subgoal",
49
+ )
50
+
51
+
52
+ class AgentThoughtTimelineItemResponse(BaseModel):
53
+ """An agent thought timeline item."""
54
+
55
+ agent: str = Field(..., alias="agent", description="Agent who thought")
56
+ content: str = Field(..., alias="content", description="Content of the thought")
57
+ timestamp: datetime = Field(..., alias="timestamp", description="Timestamp of the thought")
58
+
59
+
60
+ class TaskRunInfo(BaseModel):
61
+ """Information about a task run."""
62
+
63
+ id: str
64
+ status: TaskRunStatus
65
+ status_message: str | None = None
66
+ output: str | dict[str, Any] | None = None
67
+
68
+
69
+ class TimelineItem(BaseModel):
70
+ """A timeline item (subgoal or agent thought)."""
71
+
72
+ timestamp: datetime
73
+ content: str
74
+
75
+
76
+ VMState = Literal["Stopped", "Starting", "Ready", "Error", "Stopping", "Unknown"]
77
+
78
+
79
+ class VirtualMobileInfo(BaseModel):
80
+ """Information about a virtual mobile."""
81
+
82
+ id: str
83
+ reference_name: str | None = None
84
+ state: VMState
85
+ message: str | None = None
86
+
87
+
88
+ class CloudMobileService:
89
+ """
90
+ Service for executing tasks on cloud mobiles.
91
+
92
+ This service handles:
93
+ - Starting and waiting for cloud mobiles to be ready
94
+ - Triggering task execution via the Platform
95
+ - Polling Platform API for task status and logs
96
+ - Handling task cancellation
97
+ - Timeout management for stalled tasks
98
+ """
99
+
100
+ def __init__(self, api_key: str | None = None, http_timeout_seconds: int = 120):
101
+ self._platform_base_url = settings.MINITAP_BASE_URL
102
+
103
+ if api_key:
104
+ self._api_key = api_key
105
+ elif settings.MINITAP_API_KEY:
106
+ self._api_key = settings.MINITAP_API_KEY.get_secret_value()
107
+ else:
108
+ raise PlatformServiceError(
109
+ message="Please provide an API key or set MINITAP_API_KEY environment variable.",
110
+ )
111
+
112
+ self._timeout = httpx.Timeout(timeout=http_timeout_seconds)
113
+ self._client = httpx.AsyncClient(
114
+ base_url=f"{self._platform_base_url}/api",
115
+ timeout=self._timeout,
116
+ headers={
117
+ "Authorization": f"Bearer {self._api_key}",
118
+ "Content-Type": "application/json",
119
+ },
120
+ )
121
+
122
+ async def start_and_wait_for_ready(
123
+ self,
124
+ cloud_mobile_id: str,
125
+ poll_interval_seconds: float = 5.0,
126
+ timeout_seconds: float = 300.0,
127
+ ) -> VirtualMobileInfo:
128
+ """
129
+ Start a cloud mobile by keeping it alive and wait for it to become ready.
130
+
131
+ Args:
132
+ cloud_mobile_id: ID of the cloud mobile to start
133
+ poll_interval_seconds: Seconds between status polls (default: 5.0)
134
+ timeout_seconds: Maximum time to wait for ready state (default: 300.0)
135
+
136
+ Returns:
137
+ VirtualMobileInfo with the final state
138
+
139
+ Raises:
140
+ PlatformServiceError: If the cloud mobile fails to start or times out
141
+ """
142
+ logger.info(f"Starting cloud mobile '{cloud_mobile_id}'")
143
+
144
+ # Step 1: Trigger keep-alive to start the VM
145
+ await self._keep_alive(cloud_mobile_id)
146
+
147
+ # Step 2: Poll until ready or timeout
148
+ start_time = datetime.now(UTC)
149
+
150
+ while True:
151
+ # Check timeout
152
+ elapsed = (datetime.now(UTC) - start_time).total_seconds()
153
+ if elapsed > timeout_seconds:
154
+ raise PlatformServiceError(
155
+ message=f"Timeout waiting for cloud mobile to be ready after {timeout_seconds}s"
156
+ )
157
+
158
+ # Get current status
159
+ vm_info = await self._get_virtual_mobile_status(cloud_mobile_id)
160
+
161
+ logger.info(
162
+ f"Cloud mobile '{cloud_mobile_id}' status: {vm_info.state} - {vm_info.message}"
163
+ )
164
+
165
+ # Check if ready
166
+ if vm_info.state == "Ready":
167
+ logger.success(f"Cloud mobile '{cloud_mobile_id}' is ready")
168
+ return vm_info
169
+
170
+ # Check for error state
171
+ if vm_info.state == "Error":
172
+ raise PlatformServiceError(
173
+ message=f"Cloud mobile entered error state: {vm_info.message}"
174
+ )
175
+
176
+ # Wait before next poll
177
+ await asyncio.sleep(poll_interval_seconds)
178
+
179
+ async def _keep_alive(self, cloud_mobile_id: str) -> None:
180
+ """Keep a cloud mobile alive to prevent idle shutdown."""
181
+ try:
182
+ response = await self._client.post(f"daas/virtual-mobiles/{cloud_mobile_id}/keep-alive")
183
+ response.raise_for_status()
184
+ logger.info(f"Keep-alive sent to cloud mobile '{cloud_mobile_id}'")
185
+ except httpx.HTTPStatusError as e:
186
+ raise PlatformServiceError(
187
+ message="Failed to keep cloud mobile alive: "
188
+ f"{e.response.status_code} - {e.response.text}"
189
+ )
190
+
191
+ async def _get_virtual_mobile_status(self, cloud_mobile_id: str) -> VirtualMobileInfo:
192
+ """Get the current status of a cloud mobile."""
193
+ try:
194
+ response = await self._client.get(f"daas/virtual-mobiles/{cloud_mobile_id}")
195
+ response.raise_for_status()
196
+ data = response.json()
197
+
198
+ return VirtualMobileInfo(
199
+ id=data["id"],
200
+ reference_name=data.get("referenceName"),
201
+ state=data["state"].get("current", "Unknown"),
202
+ message=data["state"].get("message", "Unknown"),
203
+ )
204
+ except httpx.HTTPStatusError as e:
205
+ raise PlatformServiceError(
206
+ message="Failed to get cloud mobile status: "
207
+ f"{e.response.status_code} - {e.response.text}"
208
+ )
209
+
210
+ async def resolve_cloud_mobile_id(self, cloud_mobile_id_or_ref: str) -> str:
211
+ """
212
+ Resolve a cloud mobile identifier (ID or reference name) to a cloud mobile UUID.
213
+
214
+ Uses the GetVirtualMobile endpoint which supports both UUID and reference name lookup.
215
+
216
+ Args:
217
+ cloud_mobile_id_or_ref: Either a cloud mobile UUID or reference name
218
+
219
+ Returns:
220
+ The cloud mobile UUID
221
+
222
+ Raises:
223
+ PlatformServiceError: If the cloud mobile is not found or resolution fails
224
+ """
225
+ try:
226
+ response = await self._client.get(f"daas/virtual-mobiles/{cloud_mobile_id_or_ref}")
227
+ response.raise_for_status()
228
+ data = response.json()
229
+
230
+ resolved_id = data["id"]
231
+ logger.info(f"Resolved '{cloud_mobile_id_or_ref}' to UUID '{resolved_id}'")
232
+ return resolved_id
233
+ except httpx.HTTPStatusError as e:
234
+ raise PlatformServiceError(
235
+ message=f"Failed to resolve cloud mobile identifier '{cloud_mobile_id_or_ref}': "
236
+ f"{e.response.status_code} - {e.response.text}"
237
+ )
238
+
239
+ async def run_task_on_cloud_mobile(
240
+ self,
241
+ cloud_mobile_id: str,
242
+ request: PlatformTaskRequest,
243
+ on_status_update: Callable[[TaskRunStatus, str | None], None] | None = None,
244
+ on_log: Callable[[str], None] | None = None,
245
+ poll_interval_seconds: float = 2.0,
246
+ stall_timeout_seconds: float = 300.0,
247
+ ) -> tuple[TaskRunStatus, str | None, Any | None]:
248
+ """
249
+ Run a task on a cloud mobile and wait for completion.
250
+
251
+ Args:
252
+ cloud_mobile_id: ID of the cloud mobile to run the task on
253
+ request: Platform task request to execute
254
+ on_status_update: Optional callback for status updates
255
+ on_log: Optional callback for log messages
256
+ poll_interval_seconds: Seconds between status polls (default: 2.0)
257
+ stall_timeout_seconds: Timeout if no new timeline activity (default: 300.0)
258
+
259
+ Returns:
260
+ Tuple of (final_status, error_message, output)
261
+
262
+ Raises:
263
+ PlatformServiceError: If the task execution fails
264
+ """
265
+ task_run_id: str | None = None
266
+ try:
267
+ # Step 1: Trigger the task run
268
+ logger.info(f"Starting task on cloud mobile '{cloud_mobile_id}'")
269
+
270
+ task_run_id = await self._trigger_task_run(
271
+ cloud_mobile_id=cloud_mobile_id,
272
+ request=request,
273
+ )
274
+ logger.info(f"Task run started: {task_run_id}")
275
+
276
+ # Step 2: Poll for completion
277
+ final_status, error, output = await self._poll_task_until_completion(
278
+ cloud_mobile_id=cloud_mobile_id,
279
+ task_run_id=task_run_id,
280
+ on_status_update=on_status_update,
281
+ on_log=on_log,
282
+ poll_interval_seconds=poll_interval_seconds,
283
+ stall_timeout_seconds=stall_timeout_seconds,
284
+ )
285
+
286
+ return final_status, error, output
287
+
288
+ except asyncio.CancelledError:
289
+ # Task was cancelled locally - propagate to the Platform
290
+ logger.info("Task cancelled locally, propagating to the Platform")
291
+ if task_run_id is not None:
292
+ try:
293
+ await self.cancel_task_runs(cloud_mobile_id)
294
+ except Exception as e:
295
+ logger.warning(f"Failed to propagate cancellation: {e}")
296
+ raise
297
+ except Exception as e:
298
+ logger.error(f"Failed to run task on cloud device: {str(e)}")
299
+ raise PlatformServiceError(message=f"Failed to run task on cloud device: {e}")
300
+
301
+ async def _trigger_task_run(self, cloud_mobile_id: str, request: PlatformTaskRequest) -> str:
302
+ """Trigger a task run on the Platform and return the task run ID."""
303
+ try:
304
+ # Build the task request payload
305
+ payload = RunTaskRequest(
306
+ taskRequest={
307
+ "profile": request.profile,
308
+ "task": (
309
+ request.task if isinstance(request.task, str) else request.task.model_dump()
310
+ ),
311
+ }
312
+ )
313
+
314
+ response = await self._client.post(
315
+ f"daas/virtual-mobiles/{cloud_mobile_id}/run-task",
316
+ json=payload.model_dump(by_alias=True),
317
+ )
318
+ response.raise_for_status()
319
+
320
+ result = RunTaskResponse.model_validate(response.json())
321
+ return result.task_run_id
322
+
323
+ except httpx.HTTPStatusError as e:
324
+ raise PlatformServiceError(message=f"Failed to trigger task run on the Platform: {e}")
325
+
326
+ async def _poll_task_until_completion(
327
+ self,
328
+ cloud_mobile_id: str,
329
+ task_run_id: str,
330
+ on_status_update: Callable[[TaskRunStatus, str | None], None] | None,
331
+ on_log: Callable[[str], None] | None,
332
+ poll_interval_seconds: float,
333
+ stall_timeout_seconds: float,
334
+ ) -> tuple[TaskRunStatus, str | None, Any | None]:
335
+ """
336
+ Poll task run status until it completes, fails, or times out.
337
+
338
+ Returns:
339
+ Tuple of (final_status, error_message, output)
340
+ """
341
+ last_poll_time: datetime | None = None
342
+ last_activity_time = datetime.now(UTC)
343
+ current_status: TaskRunStatus = "pending"
344
+ previous_status: TaskRunStatus | None = None
345
+
346
+ while True:
347
+ # Check for stall timeout
348
+ now = datetime.now(UTC)
349
+ time_since_last_activity = (now - last_activity_time).total_seconds()
350
+ if time_since_last_activity > stall_timeout_seconds:
351
+ error_msg = (
352
+ f"Task stalled: No activity for {stall_timeout_seconds} seconds. "
353
+ "The task is considered failed."
354
+ )
355
+ logger.error(f"{error_msg} (task_run_id: {task_run_id})")
356
+ await self.cancel_task_runs(cloud_mobile_id)
357
+ return "cancelled", error_msg, None
358
+
359
+ # Fetch current task run status
360
+ task_info = await self._get_task_run_status(task_run_id)
361
+ current_status = task_info.status
362
+
363
+ # Notify status update
364
+ if previous_status != current_status and on_status_update:
365
+ previous_status = current_status
366
+ last_activity_time = now
367
+ try:
368
+ on_status_update(
369
+ task_info.status,
370
+ task_info.status_message,
371
+ )
372
+ except Exception as e:
373
+ logger.warning(f"Status update callback failed: {e}")
374
+
375
+ # Check for subgoal updates
376
+ subgoal_updates = await self._get_subgoal_updates(
377
+ task_run_id=task_run_id,
378
+ after_timestamp=last_poll_time,
379
+ )
380
+
381
+ # Check for new agent thoughts
382
+ new_thoughts = await self._get_new_agent_thoughts(
383
+ task_run_id=task_run_id,
384
+ after_timestamp=last_poll_time,
385
+ )
386
+
387
+ updates = sorted(
388
+ subgoal_updates + new_thoughts,
389
+ key=lambda item: item.timestamp,
390
+ )
391
+ if updates:
392
+ last_activity_time = now
393
+ for update in updates:
394
+ if on_log:
395
+ try:
396
+ on_log(f"[{update.timestamp}] {update.content}")
397
+ except Exception as e:
398
+ logger.warning(f"Log callback failed: {e}")
399
+
400
+ # Check if task is in terminal state
401
+ if current_status in ["completed", "failed", "cancelled"]:
402
+ logger.info(f"Task '{task_run_id}' reached terminal state: {current_status}")
403
+ error = (
404
+ task_info.status_message if current_status in ["failed", "cancelled"] else None
405
+ )
406
+ return current_status, error, task_info.output
407
+
408
+ # Wait before next poll
409
+ last_poll_time = now
410
+ await asyncio.sleep(poll_interval_seconds)
411
+
412
+ async def _get_task_run_status(self, task_run_id: str) -> TaskRunInfo:
413
+ """Get the current status of a task run."""
414
+ try:
415
+ response = await self._client.get(f"v1/task-runs/{task_run_id}")
416
+ response.raise_for_status()
417
+ data = response.json()
418
+
419
+ output: str | dict[str, Any] | None = None
420
+ raw_output = data.get("output")
421
+ try:
422
+ if raw_output is not None:
423
+ output = json.loads(raw_output)
424
+ except json.JSONDecodeError:
425
+ output = raw_output
426
+
427
+ return TaskRunInfo(
428
+ id=data["id"],
429
+ status=data["status"],
430
+ status_message=data.get("statusMessage"),
431
+ output=output,
432
+ )
433
+ except httpx.HTTPStatusError as e:
434
+ raise PlatformServiceError(message=f"Failed to get task run status: {e}")
435
+
436
+ async def _get_subgoal_updates(
437
+ self,
438
+ task_run_id: str,
439
+ after_timestamp: datetime | None,
440
+ ) -> list[TimelineItem]:
441
+ """Get new subgoals from the timeline after a specific timestamp."""
442
+ try:
443
+ started_subgoals = await self._get_filtered_subgoals(
444
+ task_run_id=task_run_id,
445
+ sort_by="started_at",
446
+ sort_order="asc",
447
+ after_timestamp=after_timestamp,
448
+ )
449
+ ended_subgoals = await self._get_filtered_subgoals(
450
+ task_run_id=task_run_id,
451
+ sort_by="ended_at",
452
+ sort_order="asc",
453
+ after_timestamp=after_timestamp,
454
+ )
455
+ items: list[TimelineItem] = []
456
+ for subgoal in started_subgoals:
457
+ if subgoal.started_at is None:
458
+ continue
459
+ items.append(
460
+ TimelineItem(
461
+ timestamp=subgoal.started_at,
462
+ content=f"[START][{subgoal.name}] {subgoal.state}",
463
+ )
464
+ )
465
+ for subgoal in ended_subgoals:
466
+ if subgoal.ended_at is None:
467
+ continue
468
+ items.append(
469
+ TimelineItem(
470
+ timestamp=subgoal.ended_at,
471
+ content=f"[END][{subgoal.name}] {subgoal.state}",
472
+ )
473
+ )
474
+ return items
475
+ except httpx.HTTPStatusError as e:
476
+ logger.warning(f"Failed to get subgoals timeline: {e}")
477
+ return []
478
+
479
+ async def _get_filtered_subgoals(
480
+ self,
481
+ task_run_id: str,
482
+ sort_by: str,
483
+ sort_order: str,
484
+ after_timestamp: datetime | None,
485
+ ) -> list[SubgoalTimelineItemResponse]:
486
+ params: dict[str, Any] = {
487
+ "page": 1,
488
+ "pageSize": 50,
489
+ "sortBy": sort_by,
490
+ "sortOrder": sort_order,
491
+ }
492
+ if after_timestamp:
493
+ params["after"] = after_timestamp.isoformat()
494
+
495
+ response = await self._client.get(
496
+ f"v1/task-runs/{task_run_id}/subgoals/timeline",
497
+ params=params,
498
+ )
499
+ response.raise_for_status()
500
+ data = response.json()
501
+ subgoals = [
502
+ SubgoalTimelineItemResponse.model_validate(item) for item in data.get("subgoals", [])
503
+ ]
504
+ return subgoals
505
+
506
+ async def _get_new_agent_thoughts(
507
+ self, task_run_id: str, after_timestamp: datetime | None
508
+ ) -> list[TimelineItem]:
509
+ """Get new agent thoughts from the timeline after a specific timestamp."""
510
+ try:
511
+ params: dict[str, Any] = {"page": 1, "pageSize": 50}
512
+ if after_timestamp:
513
+ params["after"] = after_timestamp.isoformat()
514
+
515
+ response = await self._client.get(
516
+ f"v1/task-runs/{task_run_id}/agent-thoughts/timeline",
517
+ params=params,
518
+ )
519
+ response.raise_for_status()
520
+ data = response.json()
521
+ agent_thoughts = [
522
+ AgentThoughtTimelineItemResponse.model_validate(item)
523
+ for item in data.get("agentThoughts", [])
524
+ ]
525
+
526
+ items: list[TimelineItem] = []
527
+ for thought in agent_thoughts:
528
+ items.append(
529
+ TimelineItem(
530
+ timestamp=thought.timestamp,
531
+ content=f"[{thought.agent}] {thought.content}",
532
+ )
533
+ )
534
+ return items
535
+ except httpx.HTTPStatusError as e:
536
+ logger.warning(f"Failed to get agent thoughts timeline: {e}")
537
+ return []
538
+
539
+ async def cancel_task_runs(self, cloud_mobile_id: str) -> None:
540
+ """Cancel all task runs on the Platform for a specific cloud mobile."""
541
+ try:
542
+ # Only one task can run on a cloud mobile at a time.
543
+ # Therefore, cancelling all tasks running on it implies cancelling the task run.
544
+ response = await self._client.post(
545
+ f"v1/task-runs/virtual-mobile/{cloud_mobile_id}/cancel"
546
+ )
547
+ response.raise_for_status()
548
+ logger.info(f"Task runs cancelled on cloud mobile '{cloud_mobile_id}'")
549
+ except httpx.HTTPStatusError as e:
550
+ raise PlatformServiceError(message=f"Failed to cancel task run: {e}")
551
+
552
+ async def get_screenshot(self, cloud_mobile_id: str) -> Image.Image:
553
+ """
554
+ Get a screenshot from a cloud mobile.
555
+
556
+ Args:
557
+ cloud_mobile_id: ID of the cloud mobile to capture screenshot from
558
+
559
+ Returns:
560
+ Screenshot as PIL Image
561
+
562
+ Raises:
563
+ PlatformServiceError: If the screenshot capture fails
564
+ """
565
+ try:
566
+ logger.info(f"Capturing screenshot from cloud mobile '{cloud_mobile_id}'")
567
+ response = await self._client.get(f"daas/virtual-mobiles/{cloud_mobile_id}/screenshot")
568
+ response.raise_for_status()
569
+
570
+ # Convert bytes to PIL Image
571
+ image = Image.open(BytesIO(response.content))
572
+
573
+ size_bytes = len(response.content)
574
+ logger.info(
575
+ f"Screenshot captured from cloud mobile '{cloud_mobile_id}' ({size_bytes} bytes)"
576
+ )
577
+ return image
578
+ except httpx.HTTPStatusError as e:
579
+ raise PlatformServiceError(
580
+ message=f"Failed to get screenshot from cloud mobile: "
581
+ f"{e.response.status_code} - {e.response.text}"
582
+ )
@@ -65,6 +65,8 @@ class AgentConfig(BaseModel):
65
65
  device_id: Specific device to target (if None, first available is used).
66
66
  device_platform: Platform of the device to target.
67
67
  servers: Custom server configurations.
68
+ cloud_mobile_id_or_ref: ID or reference name of cloud mobile (virtual mobile)
69
+ to use for remote execution.
68
70
  """
69
71
 
70
72
  agent_profiles: dict[str, AgentProfile]
@@ -74,5 +76,6 @@ class AgentConfig(BaseModel):
74
76
  device_platform: DevicePlatform | None = None
75
77
  servers: ServerConfig
76
78
  graph_config_callbacks: Callbacks = None
79
+ cloud_mobile_id_or_ref: str | None = None
77
80
 
78
81
  model_config = {"arbitrary_types_allowed": True}
@@ -124,6 +124,13 @@ class PlatformServiceUninitializedError(MobileUseError):
124
124
  )
125
125
 
126
126
 
127
+ class CloudMobileServiceUninitializedError(MobileUseError):
128
+ """Exception raised when a cloud mobile service call fails."""
129
+
130
+ def __init__(self):
131
+ super().__init__("Cloud mobile service is not initialized!")
132
+
133
+
127
134
  class PlatformServiceError(MobileUseError):
128
135
  """Exception raised when a platform service call fails."""
129
136
 
@@ -127,13 +127,10 @@ class PlatformTaskRequest[TOutput](TaskRequestBase):
127
127
  task: Either a task name to fetch from the platform, or a
128
128
  ManualTaskConfig to create manually
129
129
  profile: Optional profile name specified by the user on the platform
130
- api_key: Optional API key to authenticate with the platform
131
- (overrides MINITAP_API_KEY env variable)
132
130
  """
133
131
 
134
132
  task: str | ManualTaskConfig
135
133
  profile: str | None = None
136
- api_key: str | None = None
137
134
 
138
135
 
139
136
  class CloudDevicePlatformTaskRequest[TOutput](PlatformTaskRequest[TOutput]):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: minitap-mobile-use
3
- Version: 2.7.2
3
+ Version: 2.8.0
4
4
  Summary: AI-powered multi-agent system that automates real Android and iOS devices through low-level control using LangGraph.
5
5
  Author: Pierre-Louis Favreau, Jean-Pierre Lo, Nicolas Dehandschoewercker
6
6
  License: MIT License
@@ -36,26 +36,27 @@ minitap/mobile_use/controllers/platform_specific_commands_controller.py,sha256=8
36
36
  minitap/mobile_use/controllers/types.py,sha256=c4dd6b266dd8f157ca1e6a211369ba8e7f65d3fda72b0742bda1128eefd99473,2935
37
37
  minitap/mobile_use/graph/graph.py,sha256=94dbac498a77eab3fbb413182ec4c3272c353837f71268344e150a2c00a9747e,6012
38
38
  minitap/mobile_use/graph/state.py,sha256=1903f49772be7f9725e69718194a7be953203f882ec5e26211a5db6fdc569478,3596
39
- minitap/mobile_use/main.py,sha256=1405a13eab2c3b86b148bb9678e829447522753a10572935fc1e3ed3bbce6878,3927
39
+ minitap/mobile_use/main.py,sha256=1f0568a4e889837591ace797d2a0186ca4dc8e82ae7f9b64588fa8ece5701ee6,3939
40
40
  minitap/mobile_use/sdk/__init__.py,sha256=4e5555c0597242b9523827194a2500b9c6d7e5c04b1ccd2056c9b1f4d42a31cd,318
41
- minitap/mobile_use/sdk/agent.py,sha256=a1380a32b14784ce3cdcd7deaca2ffd643491bc23372297a24798a53bc7a1a37,30579
41
+ minitap/mobile_use/sdk/agent.py,sha256=057d0a347fe87ec5e5a7edfedd9637df41be304e15181a9d60a781940f806450,39228
42
42
  minitap/mobile_use/sdk/builders/__init__.py,sha256=d6c96d39b80900a114698ef205ab5061a541f33bfa99c456d9345e5adb8ff6ff,424
43
- minitap/mobile_use/sdk/builders/agent_config_builder.py,sha256=4b8ed9eb84d9093f18166e4c4f99e3da6941cf374c9c112cb3c37ea0fadcfe02,7917
43
+ minitap/mobile_use/sdk/builders/agent_config_builder.py,sha256=d3f75c7d0431bde7b64915fcb68b30aa2c3e0d714cec09997310d4410f23490d,9206
44
44
  minitap/mobile_use/sdk/builders/index.py,sha256=64336ac3b3dea4673a48e95b8c5ac4196ecd5d2196380377d102593d0a1dc138,442
45
45
  minitap/mobile_use/sdk/builders/task_request_builder.py,sha256=9e6cf7afb68af986d6a81487179bb79d28f63047a068725d92996dbcbe753376,6857
46
46
  minitap/mobile_use/sdk/constants.py,sha256=436ba0700c6cf37ac0c9e3995a5f5a0d54ca87af72686eb9667a2c6a96e30f68,292
47
- minitap/mobile_use/sdk/examples/README.md,sha256=c92e57d6efc5bce9f3c8668b20d9ce80396aefb7a685636ee84de8c7b9c1403e,3247
47
+ minitap/mobile_use/sdk/examples/README.md,sha256=9cdc39b7f2b715c61430e162a3136732efc25d767203072dc5d17f1c5e566783,3239
48
48
  minitap/mobile_use/sdk/examples/__init__.py,sha256=c23868a2ca7e9b76e80d6835fe93c10e13ea8f2287dd6e785511b8ac30354e9b,46
49
- minitap/mobile_use/sdk/examples/platform_manual_task_example.py,sha256=00f54d58fa0abe9a1df20b3593633c239947c5acdf716fe9be1f58f9f56d8caa,1937
50
- minitap/mobile_use/sdk/examples/platform_minimal_example.py,sha256=bdb86142f4bb5d95d54838fc6487bb06e5e21db5106385344138be1653071aea,1389
51
- minitap/mobile_use/sdk/examples/simple_photo_organizer.py,sha256=8ad1cebb5281e3663264560bd15b090add41d2821b1db77e4cbc829860c98df8,2606
52
- minitap/mobile_use/sdk/examples/smart_notification_assistant.py,sha256=ecab26c6fd2af59bb22dbc67f0eeb4a4a73e4394639b34fb2d44f04f218e2b61,8104
49
+ minitap/mobile_use/sdk/examples/platform_manual_task_example.py,sha256=e3ce79c6454aea1641b69ddc1df7552c599fccd88d4c006c55b3c6284bb24282,1949
50
+ minitap/mobile_use/sdk/examples/platform_minimal_example.py,sha256=a42f744e37f9ed3fff51183b7eeedf140d6aabaf29b35bf80a7ab267cc2b5c0e,1387
51
+ minitap/mobile_use/sdk/examples/simple_photo_organizer.py,sha256=4b271309905d3b99cd60060a5e4a12594983838641ef456dbba4b3cbc842edcd,2618
52
+ minitap/mobile_use/sdk/examples/smart_notification_assistant.py,sha256=c5605bf4d4e79ff7f41eee58910a45d317fb217b5aa5dc498ed00c5f0f2f14be,8116
53
+ minitap/mobile_use/sdk/services/cloud_mobile.py,sha256=5ec8e2e37a100c5783c0b2d33b91f33a5abcf299b75516debc05e1bdb6b51b13,21802
53
54
  minitap/mobile_use/sdk/services/platform.py,sha256=516b17f5286f8cb7ef7d5f0d2b0af23b90b17588faa9f638ea5cdb4f3935e64e,12545
54
55
  minitap/mobile_use/sdk/types/__init__.py,sha256=433aff6b35f84a985633204edbbdaca9f2f61fb2b822630f9723c481b9bb5c10,1078
55
- minitap/mobile_use/sdk/types/agent.py,sha256=390d5c642b3480f4a2203ddd28ec115c785f2576bec81e82e4db3c129399c020,2260
56
- minitap/mobile_use/sdk/types/exceptions.py,sha256=684c0049c5af417edf7e46e515be14fd57a0614c81b06ed52f379bc9d0bbebf3,4499
56
+ minitap/mobile_use/sdk/types/agent.py,sha256=d286cf2c4eb0c98c82928e295750c16adf1072d5ace3353ddb512b5bd7593877,2453
57
+ minitap/mobile_use/sdk/types/exceptions.py,sha256=81e12f2bfe3937d730dc47a0938d3911239a162ddbd1a1179473a155946cb836,4722
57
58
  minitap/mobile_use/sdk/types/platform.py,sha256=6d1eefa6fb73aea1c574eeb24b05ee89d0119e6e01cdcd6022923df8f7d511e7,5295
58
- minitap/mobile_use/sdk/types/task.py,sha256=3d44a07bca698b2bbfc18e240c0b4be74e6fb4422ab7ed0a9a9b411e903021b8,8539
59
+ minitap/mobile_use/sdk/types/task.py,sha256=c7f0b3c59ee980eb63e9836268701069e805107b9057ec1a098daa236b30d0ba,8382
59
60
  minitap/mobile_use/sdk/utils.py,sha256=647f1f4a463c3029c3b0eb3c33f7dd778d5f5fd9d293224f5474595a60e1de6f,967
60
61
  minitap/mobile_use/servers/config.py,sha256=8a4a6bce23e2093d047a91e135e2f88627f76ac12177d071f25a3ca739b3afeb,575
61
62
  minitap/mobile_use/servers/device_hardware_bridge.py,sha256=39c20834812d9929163affaedd0e285ab0b349948b3236156c04a3e0bf094272,7456
@@ -96,7 +97,7 @@ minitap/mobile_use/utils/shell_utils.py,sha256=b35ae7f863379adb86c9ba0f9b3b9d495
96
97
  minitap/mobile_use/utils/test_ui_hierarchy.py,sha256=96c1549c05b4f7254a22d57dbd40aea860756f1e0b9d8cc24319383643448422,5911
97
98
  minitap/mobile_use/utils/time.py,sha256=41bfaabb3751de11443ccb4a3f1f53d5ebacc7744c72e32695fdcc3d23f17d49,160
98
99
  minitap/mobile_use/utils/ui_hierarchy.py,sha256=f3370518035d9daf02c08042a9e28ad564f4fc81a2b268103b9a7f8bc5c61d11,3797
99
- minitap_mobile_use-2.7.2.dist-info/WHEEL,sha256=ab6157bc637547491fb4567cd7ddf26b04d63382916ca16c29a5c8e94c9c9ef7,79
100
- minitap_mobile_use-2.7.2.dist-info/entry_points.txt,sha256=663a29cfd551a4eaa0f27335f0bd7e4a732a4e39c76b68ef5c8dc444d4a285fa,60
101
- minitap_mobile_use-2.7.2.dist-info/METADATA,sha256=5d363ea1b75c0ae699cf85b94fa2ccb86b97af30dc3f382638e4c3f907bda627,11995
102
- minitap_mobile_use-2.7.2.dist-info/RECORD,,
100
+ minitap_mobile_use-2.8.0.dist-info/WHEEL,sha256=ab6157bc637547491fb4567cd7ddf26b04d63382916ca16c29a5c8e94c9c9ef7,79
101
+ minitap_mobile_use-2.8.0.dist-info/entry_points.txt,sha256=663a29cfd551a4eaa0f27335f0bd7e4a732a4e39c76b68ef5c8dc444d4a285fa,60
102
+ minitap_mobile_use-2.8.0.dist-info/METADATA,sha256=d1131fea7bfc7ac26bd51ee3266dae6191ffefffde7e8193043059dd6dd40ee3,11995
103
+ minitap_mobile_use-2.8.0.dist-info/RECORD,,