minitap-mobile-use 2.2.0__py3-none-any.whl → 2.4.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.

Files changed (59) hide show
  1. minitap/mobile_use/agents/contextor/contextor.py +6 -4
  2. minitap/mobile_use/agents/cortex/cortex.md +114 -27
  3. minitap/mobile_use/agents/cortex/cortex.py +8 -5
  4. minitap/mobile_use/agents/executor/executor.md +15 -10
  5. minitap/mobile_use/agents/executor/executor.py +6 -5
  6. minitap/mobile_use/agents/executor/utils.py +2 -1
  7. minitap/mobile_use/agents/hopper/hopper.py +6 -3
  8. minitap/mobile_use/agents/orchestrator/orchestrator.py +26 -11
  9. minitap/mobile_use/agents/outputter/outputter.py +6 -3
  10. minitap/mobile_use/agents/outputter/test_outputter.py +104 -42
  11. minitap/mobile_use/agents/planner/planner.md +20 -22
  12. minitap/mobile_use/agents/planner/planner.py +10 -7
  13. minitap/mobile_use/agents/planner/types.py +4 -2
  14. minitap/mobile_use/agents/planner/utils.py +14 -0
  15. minitap/mobile_use/agents/summarizer/summarizer.py +2 -2
  16. minitap/mobile_use/config.py +6 -1
  17. minitap/mobile_use/context.py +13 -3
  18. minitap/mobile_use/controllers/mobile_command_controller.py +1 -14
  19. minitap/mobile_use/graph/state.py +7 -3
  20. minitap/mobile_use/sdk/agent.py +204 -29
  21. minitap/mobile_use/sdk/examples/README.md +19 -1
  22. minitap/mobile_use/sdk/examples/platform_minimal_example.py +46 -0
  23. minitap/mobile_use/sdk/services/platform.py +244 -0
  24. minitap/mobile_use/sdk/types/__init__.py +14 -14
  25. minitap/mobile_use/sdk/types/exceptions.py +57 -0
  26. minitap/mobile_use/sdk/types/platform.py +125 -0
  27. minitap/mobile_use/sdk/types/task.py +60 -17
  28. minitap/mobile_use/servers/device_hardware_bridge.py +3 -2
  29. minitap/mobile_use/servers/stop_servers.py +11 -12
  30. minitap/mobile_use/servers/utils.py +6 -9
  31. minitap/mobile_use/services/llm.py +89 -5
  32. minitap/mobile_use/tools/index.py +2 -8
  33. minitap/mobile_use/tools/mobile/back.py +3 -3
  34. minitap/mobile_use/tools/mobile/clear_text.py +67 -38
  35. minitap/mobile_use/tools/mobile/erase_one_char.py +5 -4
  36. minitap/mobile_use/tools/mobile/{take_screenshot.py → glimpse_screen.py} +23 -15
  37. minitap/mobile_use/tools/mobile/input_text.py +67 -16
  38. minitap/mobile_use/tools/mobile/launch_app.py +54 -22
  39. minitap/mobile_use/tools/mobile/long_press_on.py +15 -8
  40. minitap/mobile_use/tools/mobile/open_link.py +15 -8
  41. minitap/mobile_use/tools/mobile/press_key.py +15 -8
  42. minitap/mobile_use/tools/mobile/stop_app.py +14 -8
  43. minitap/mobile_use/tools/mobile/swipe.py +11 -5
  44. minitap/mobile_use/tools/mobile/tap.py +103 -21
  45. minitap/mobile_use/tools/mobile/wait_for_animation_to_end.py +3 -3
  46. minitap/mobile_use/tools/test_utils.py +377 -0
  47. minitap/mobile_use/tools/types.py +35 -0
  48. minitap/mobile_use/tools/utils.py +149 -39
  49. minitap/mobile_use/utils/recorder.py +1 -1
  50. minitap/mobile_use/utils/test_ui_hierarchy.py +178 -0
  51. minitap/mobile_use/utils/ui_hierarchy.py +11 -4
  52. {minitap_mobile_use-2.2.0.dist-info → minitap_mobile_use-2.4.0.dist-info}/METADATA +6 -4
  53. minitap_mobile_use-2.4.0.dist-info/RECORD +99 -0
  54. minitap/mobile_use/tools/mobile/copy_text_from.py +0 -73
  55. minitap/mobile_use/tools/mobile/find_packages.py +0 -69
  56. minitap/mobile_use/tools/mobile/paste_text.py +0 -62
  57. minitap_mobile_use-2.2.0.dist-info/RECORD +0 -96
  58. {minitap_mobile_use-2.2.0.dist-info → minitap_mobile_use-2.4.0.dist-info}/WHEEL +0 -0
  59. {minitap_mobile_use-2.2.0.dist-info → minitap_mobile_use-2.4.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,244 @@
1
+ import json
2
+ from datetime import UTC, datetime
3
+ from typing import Any
4
+
5
+ import httpx
6
+ from pydantic import BaseModel, ValidationError
7
+
8
+ from minitap.mobile_use.agents.planner.types import Subgoal, SubgoalStatus
9
+ from minitap.mobile_use.config import LLMConfig, settings
10
+ from minitap.mobile_use.sdk.types.exceptions import PlatformServiceError
11
+ from minitap.mobile_use.sdk.types.platform import (
12
+ CreateTaskRunRequest,
13
+ LLMProfileResponse,
14
+ MobileUseSubgoal,
15
+ SubgoalState,
16
+ TaskResponse,
17
+ TaskRunPlanResponse,
18
+ TaskRunResponse,
19
+ TaskRunStatus,
20
+ UpdateTaskRunStatusRequest,
21
+ UpsertTaskRunAgentThoughtRequest,
22
+ UpsertTaskRunPlanRequest,
23
+ )
24
+ from minitap.mobile_use.sdk.types.task import (
25
+ AgentProfile,
26
+ PlatformTaskInfo,
27
+ PlatformTaskRequest,
28
+ TaskRequest,
29
+ )
30
+ from minitap.mobile_use.utils.logger import get_logger
31
+
32
+ logger = get_logger(__name__)
33
+
34
+ DEFAULT_PROFILE = "default"
35
+
36
+
37
+ class PlatformService:
38
+ def __init__(self, api_key: str | None = None):
39
+ self._base_url = settings.MINITAP_API_BASE_URL
40
+
41
+ if api_key:
42
+ self._api_key = api_key
43
+ elif settings.MINITAP_API_KEY:
44
+ self._api_key = settings.MINITAP_API_KEY.get_secret_value()
45
+ else:
46
+ raise PlatformServiceError(
47
+ message="Please provide an API key or set MINITAP_API_KEY environment variable.",
48
+ )
49
+
50
+ self._timeout = httpx.Timeout(timeout=120)
51
+ self._client = httpx.AsyncClient(
52
+ base_url=f"{self._base_url}/api",
53
+ timeout=self._timeout,
54
+ headers={
55
+ "Authorization": f"Bearer {self._api_key}",
56
+ "Content-Type": "application/json",
57
+ },
58
+ )
59
+
60
+ async def create_task_run(self, request: PlatformTaskRequest) -> PlatformTaskInfo:
61
+ try:
62
+ logger.info(f"Getting task: {request.task}")
63
+ response = await self._client.get(url=f"v1/tasks/{request.task}")
64
+ response.raise_for_status()
65
+ task_data = response.json()
66
+ task = TaskResponse(**task_data)
67
+ profile, agent_profile = await self._get_profile(
68
+ profile_name=request.profile or DEFAULT_PROFILE,
69
+ )
70
+ task_request = TaskRequest(
71
+ # Remote configuration
72
+ max_steps=task.options.max_steps,
73
+ goal=task.input_prompt,
74
+ output_description=task.output_description,
75
+ enable_remote_tracing=task.options.enable_tracing,
76
+ profile=profile.name,
77
+ # Local configuration
78
+ record_trace=request.record_trace,
79
+ trace_path=request.trace_path,
80
+ llm_output_path=request.llm_output_path,
81
+ thoughts_output_path=request.thoughts_output_path,
82
+ )
83
+ task_run = await self._create_task_run(task=task, profile=profile)
84
+ return PlatformTaskInfo(
85
+ task_request=task_request,
86
+ llm_profile=agent_profile,
87
+ task_run=task_run,
88
+ )
89
+ except httpx.HTTPStatusError as e:
90
+ raise PlatformServiceError(message=f"Failed to get task: {e}")
91
+
92
+ async def update_task_run_status(
93
+ self,
94
+ task_run_id: str,
95
+ status: TaskRunStatus,
96
+ message: str | None = None,
97
+ output: Any | None = None,
98
+ ) -> None:
99
+ try:
100
+ logger.info(f"Updating task run status for task run: {task_run_id}")
101
+
102
+ sanitized_output: str | None = None
103
+ if isinstance(output, dict):
104
+ sanitized_output = json.dumps(output)
105
+ elif isinstance(output, list):
106
+ sanitized_output = json.dumps(output)
107
+ elif isinstance(output, BaseModel):
108
+ sanitized_output = output.model_dump_json()
109
+ elif isinstance(output, str):
110
+ sanitized_output = output
111
+ else:
112
+ sanitized_output = str(output)
113
+
114
+ update = UpdateTaskRunStatusRequest(
115
+ status=status,
116
+ message=message,
117
+ output=sanitized_output,
118
+ )
119
+ response = await self._client.patch(
120
+ url=f"v1/task-runs/{task_run_id}/status",
121
+ json=update.model_dump(),
122
+ )
123
+ response.raise_for_status()
124
+ except httpx.HTTPStatusError as e:
125
+ raise PlatformServiceError(message=f"Failed to update task run status: {e}")
126
+
127
+ async def upsert_task_run_plan(
128
+ self,
129
+ task_run_id: str,
130
+ started_at: datetime,
131
+ plan: list[Subgoal],
132
+ ended_at: datetime | None = None,
133
+ plan_id: str | None = None,
134
+ ) -> TaskRunPlanResponse:
135
+ try:
136
+ logger.info(f"Upserting task run plan for task run: {task_run_id}")
137
+ ended, subgoals = self._to_api_subgoals(plan)
138
+ if not ended_at and ended:
139
+ ended_at = datetime.now(UTC)
140
+ update = UpsertTaskRunPlanRequest(
141
+ started_at=started_at,
142
+ subgoals=subgoals,
143
+ ended_at=ended_at,
144
+ )
145
+ if plan_id:
146
+ response = await self._client.put(
147
+ url=f"v1/task-runs/{task_run_id}/plans/{plan_id}",
148
+ json=update.model_dump(),
149
+ )
150
+ else:
151
+ response = await self._client.post(
152
+ url=f"v1/task-runs/{task_run_id}/plans",
153
+ json=update.model_dump(),
154
+ )
155
+ response.raise_for_status()
156
+ return TaskRunPlanResponse(**response.json())
157
+
158
+ except ValidationError as e:
159
+ raise PlatformServiceError(message=f"API response validation error: {e}")
160
+ except httpx.HTTPStatusError as e:
161
+ raise PlatformServiceError(message=f"Failed to upsert task run plan: {e}")
162
+
163
+ async def add_agent_thought(self, task_run_id: str, agent: str, thought: str) -> None:
164
+ try:
165
+ logger.info(f"Adding agent thought for task run: {task_run_id}")
166
+ update = UpsertTaskRunAgentThoughtRequest(
167
+ agent=agent,
168
+ content=thought,
169
+ timestamp=datetime.now(UTC),
170
+ )
171
+ response = await self._client.post(
172
+ url=f"v1/task-runs/{task_run_id}/agent-thoughts",
173
+ json=update.model_dump(),
174
+ )
175
+ response.raise_for_status()
176
+ except httpx.HTTPStatusError as e:
177
+ raise PlatformServiceError(message=f"Failed to add agent thought: {e}")
178
+
179
+ def _to_api_subgoals(self, subgoals: list[Subgoal]) -> tuple[bool, list[MobileUseSubgoal]]:
180
+ """
181
+ Returns a tuple of (plan_ended, subgoal_models)
182
+ """
183
+ subgoal_models: list[MobileUseSubgoal] = []
184
+ plan_ended = True
185
+ for subgoal in subgoals:
186
+ if subgoal.status != SubgoalStatus.SUCCESS:
187
+ plan_ended = False
188
+ subgoal_models.append(self._to_api_subgoal(subgoal))
189
+ return plan_ended, subgoal_models
190
+
191
+ def _to_api_subgoal(self, subgoal: Subgoal) -> MobileUseSubgoal:
192
+ state: SubgoalState = "pending"
193
+ match subgoal.status:
194
+ case SubgoalStatus.SUCCESS:
195
+ state = "completed"
196
+ case SubgoalStatus.FAILURE:
197
+ state = "failed"
198
+ case SubgoalStatus.PENDING:
199
+ state = "started"
200
+ case SubgoalStatus.NOT_STARTED:
201
+ state = "pending"
202
+ return MobileUseSubgoal(
203
+ name=subgoal.description,
204
+ state=state,
205
+ started_at=subgoal.started_at,
206
+ ended_at=subgoal.ended_at,
207
+ )
208
+
209
+ async def _create_task_run(
210
+ self,
211
+ task: TaskResponse,
212
+ profile: LLMProfileResponse,
213
+ ) -> TaskRunResponse:
214
+ try:
215
+ logger.info(f"Creating task run for task: {task.name}")
216
+ task_run = CreateTaskRunRequest(
217
+ task_id=task.id,
218
+ llm_profile_id=profile.id,
219
+ )
220
+ response = await self._client.post(url="v1/task-runs", json=task_run.model_dump())
221
+ response.raise_for_status()
222
+ task_run_data = response.json()
223
+ return TaskRunResponse(**task_run_data)
224
+ except ValidationError as e:
225
+ raise PlatformServiceError(message=f"API response validation error: {e}")
226
+ except httpx.HTTPStatusError as e:
227
+ raise PlatformServiceError(message=f"Failed to create task run: {e}")
228
+
229
+ async def _get_profile(self, profile_name: str) -> tuple[LLMProfileResponse, AgentProfile]:
230
+ try:
231
+ logger.info(f"Getting agent profile: {profile_name}")
232
+ response = await self._client.get(url=f"v1/llm-profiles/{profile_name}")
233
+ response.raise_for_status()
234
+ profile_data = response.json()
235
+ profile = LLMProfileResponse(**profile_data)
236
+ agent_profile = AgentProfile(
237
+ name=profile.name,
238
+ llm_config=LLMConfig(**profile.llms),
239
+ )
240
+ return profile, agent_profile
241
+ except ValidationError as e:
242
+ raise PlatformServiceError(message=f"API response validation error: {e}")
243
+ except httpx.HTTPStatusError as e:
244
+ raise PlatformServiceError(message=f"Failed to get agent profile: {e}")
@@ -1,29 +1,29 @@
1
1
  """Type definitions for the mobile-use SDK."""
2
2
 
3
3
  from minitap.mobile_use.sdk.types.agent import (
4
- ApiBaseUrl,
5
4
  AgentConfig,
5
+ ApiBaseUrl,
6
6
  DevicePlatform,
7
7
  ServerConfig,
8
8
  )
9
- from minitap.mobile_use.sdk.types.task import (
10
- AgentProfile,
11
- TaskRequest,
12
- TaskStatus,
13
- TaskResult,
14
- TaskRequestCommon,
15
- Task,
16
- )
17
9
  from minitap.mobile_use.sdk.types.exceptions import (
18
- AgentProfileNotFoundError,
19
- AgentTaskRequestError,
20
- DeviceNotFoundError,
21
- ServerStartupError,
22
10
  AgentError,
23
11
  AgentNotInitializedError,
12
+ AgentProfileNotFoundError,
13
+ AgentTaskRequestError,
24
14
  DeviceError,
15
+ DeviceNotFoundError,
25
16
  MobileUseError,
26
17
  ServerError,
18
+ ServerStartupError,
19
+ )
20
+ from minitap.mobile_use.sdk.types.task import (
21
+ AgentProfile,
22
+ PlatformTaskRequest,
23
+ Task,
24
+ TaskRequest,
25
+ TaskRequestCommon,
26
+ TaskResult,
27
27
  )
28
28
 
29
29
  __all__ = [
@@ -33,7 +33,7 @@ __all__ = [
33
33
  "AgentProfile",
34
34
  "ServerConfig",
35
35
  "TaskRequest",
36
- "TaskStatus",
36
+ "PlatformTaskRequest",
37
37
  "TaskResult",
38
38
  "TaskRequestCommon",
39
39
  "Task",
@@ -4,6 +4,8 @@ Exceptions for the Mobile-use SDK.
4
4
  This module defines the exception hierarchy used throughout the Mobile-use SDK.
5
5
  """
6
6
 
7
+ from typing import Literal
8
+
7
9
 
8
10
  class MobileUseError(Exception):
9
11
  """Base exception class for all Mobile-use SDK exceptions."""
@@ -72,3 +74,58 @@ class AgentProfileNotFoundError(AgentTaskRequestError):
72
74
 
73
75
  def __init__(self, profile_name: str):
74
76
  super().__init__(f"Agent profile {profile_name} not found")
77
+
78
+
79
+ EXECUTABLES = Literal["adb", "maestro", "xcrun", "cli_tools"]
80
+
81
+
82
+ class ExecutableNotFoundError(MobileUseError):
83
+ """Exception raised when a required executable is not found."""
84
+
85
+ def __init__(self, executable_name: EXECUTABLES):
86
+ install_instructions: dict[EXECUTABLES, str] = {
87
+ "adb": "https://developer.android.com/tools/adb",
88
+ "maestro": "https://docs.maestro.dev/getting-started/installing-maestro",
89
+ "xcrun": "Install with: xcode-select --install",
90
+ }
91
+ if executable_name == "cli_tools":
92
+ message = (
93
+ "ADB or Xcode Command Line Tools not found in PATH. "
94
+ "At least one of them is required to run mobile-use "
95
+ "depending on the device platform you wish to run (Android: adb, iOS: xcrun)."
96
+ "Refer to the following links for installation instructions :"
97
+ f"\n- ADB: {install_instructions['adb']}"
98
+ f"\n- Xcode Command Line Tools: {install_instructions['xcrun']}"
99
+ )
100
+ else:
101
+ message = f"Required executable '{executable_name}' not found in PATH."
102
+ if executable_name in install_instructions:
103
+ message += f"\nTo install it, please visit: {install_instructions[executable_name]}"
104
+ super().__init__(message)
105
+
106
+
107
+ class AgentInvalidApiKeyError(AgentTaskRequestError):
108
+ """Exception raise when the API key could not have been found"""
109
+
110
+ def __init__(self):
111
+ super().__init__(
112
+ "Minitap API key is incorrect. Visit https://platform.minitap.ai/api-keys "
113
+ "to get your API key."
114
+ )
115
+
116
+
117
+ class PlatformServiceUninitializedError(MobileUseError):
118
+ """Exception raised when a platform service call fails."""
119
+
120
+ def __init__(self):
121
+ super().__init__(
122
+ "Platform service is not initialized. "
123
+ "To use Minitap platform service, visit https://platform.minitap.ai.",
124
+ )
125
+
126
+
127
+ class PlatformServiceError(MobileUseError):
128
+ """Exception raised when a platform service call fails."""
129
+
130
+ def __init__(self, message="A platform service-related error occurred"):
131
+ super().__init__(message)
@@ -0,0 +1,125 @@
1
+ from typing import Annotated, Any, Literal
2
+ from pydantic import BaseModel, ConfigDict, Field, PlainSerializer
3
+ from pydantic.v1.utils import to_lower_camel
4
+ from datetime import datetime
5
+
6
+
7
+ TaskRunStatus = Literal["pending", "running", "completed", "failed", "cancelled"]
8
+
9
+ IsoDatetime = Annotated[
10
+ datetime,
11
+ PlainSerializer(
12
+ func=lambda v: v.isoformat() if v else None,
13
+ return_type=str,
14
+ when_used="unless-none",
15
+ ),
16
+ ]
17
+
18
+
19
+ class BaseApiModel(BaseModel):
20
+ model_config = ConfigDict(
21
+ alias_generator=to_lower_camel,
22
+ populate_by_name=True,
23
+ str_strip_whitespace=True,
24
+ )
25
+
26
+
27
+ class LLMProfileResponse(BaseApiModel):
28
+ """Response model for LLM profile."""
29
+
30
+ id: str = Field(..., description="Profile ID")
31
+ name: str = Field(..., description="Profile name")
32
+ description: str | None = Field(None, description="Profile description")
33
+ llms: dict[str, Any] = Field(..., description="LLM configuration")
34
+ created_at: str = Field(..., description="Creation timestamp (ISO format)")
35
+ updated_at: str = Field(..., description="Last update timestamp (ISO format)")
36
+
37
+
38
+ class TaskOptionsResponse(BaseApiModel):
39
+ """Response model for task options."""
40
+
41
+ id: str = Field(..., description="Options ID")
42
+ enable_tracing: bool = Field(..., description="Whether tracing is enabled")
43
+ max_steps: int = Field(..., description="Maximum number of steps")
44
+ created_at: str = Field(..., description="Creation timestamp (ISO format)")
45
+ updated_at: str = Field(..., description="Last update timestamp (ISO format)")
46
+
47
+
48
+ class TaskResponse(BaseApiModel):
49
+ """Response model for task."""
50
+
51
+ id: str = Field(..., description="Task ID")
52
+ name: str = Field(..., description="Task name")
53
+ description: str | None = Field(None, description="Task description")
54
+ input_prompt: str = Field(..., description="Input prompt")
55
+ output_description: str | None = Field(None, description="Output description")
56
+ options: TaskOptionsResponse = Field(..., description="Task options")
57
+ created_at: str = Field(..., description="Creation timestamp (ISO format)")
58
+ updated_at: str = Field(..., description="Last update timestamp (ISO format)")
59
+
60
+
61
+ class CreateTaskRunRequest(BaseApiModel):
62
+ """Request model for creating a task run."""
63
+
64
+ task_id: str = Field(..., description="ID of the task to run")
65
+ llm_profile_id: str = Field(..., description="LLM profile ID to use")
66
+
67
+
68
+ class UpdateTaskRunStatusRequest(BaseApiModel):
69
+ """Request model for updating task run status."""
70
+
71
+ status: TaskRunStatus = Field(..., description="New status of the task run")
72
+ message: str | None = Field(None, description="Message associated with the status")
73
+ output: str | None = Field(None, description="Output of the task run")
74
+
75
+
76
+ class TaskRunResponse(BaseApiModel):
77
+ """Response model for a single task run."""
78
+
79
+ id: str = Field(..., description="Unique identifier for the task run")
80
+ task: TaskResponse = Field(..., description="ID of the task this run is for")
81
+ llm_profile: LLMProfileResponse = Field(..., description="LLM profile ID used for this run")
82
+ status: TaskRunStatus = Field(..., description="Current status of the task run")
83
+ input_prompt: str = Field(..., description="Input prompt for this task run")
84
+ output_description: str | None = Field(None, description="Description of expected output")
85
+ created_at: datetime = Field(..., description="When the task run was created")
86
+ started_at: datetime | None = Field(None, description="When the task run started")
87
+ finished_at: datetime | None = Field(None, description="When the task run finished")
88
+
89
+
90
+ SubgoalState = Literal["pending", "started", "completed", "failed"]
91
+
92
+
93
+ class MobileUseSubgoal(BaseModel):
94
+ """Upsert MobileUseSubgoal API model."""
95
+
96
+ name: str = Field(..., description="Name of the subgoal")
97
+ state: SubgoalState = Field(default="pending", description="Current state of the subgoal")
98
+ started_at: IsoDatetime | None = Field(default=None, description="When the subgoal started")
99
+ ended_at: IsoDatetime | None = Field(default=None, description="When the subgoal ended")
100
+
101
+
102
+ class UpsertTaskRunPlanRequest(BaseApiModel):
103
+ """Upsert MobileUseSubgoal API model."""
104
+
105
+ started_at: IsoDatetime = Field(..., description="When the plan started")
106
+ subgoals: list[MobileUseSubgoal] = Field(..., description="Subgoals of the plan")
107
+ ended_at: IsoDatetime | None = Field(
108
+ default=None,
109
+ description="When the plan ended (replanned or completed)",
110
+ )
111
+
112
+
113
+ class TaskRunPlanResponse(UpsertTaskRunPlanRequest):
114
+ """Response model for a task run plan."""
115
+
116
+ id: str = Field(..., description="Unique identifier for the task run plan")
117
+ task_run_id: str = Field(..., description="ID of the task run this plan is for")
118
+
119
+
120
+ class UpsertTaskRunAgentThoughtRequest(BaseApiModel):
121
+ """Upsert MobileUseAgentThought request model."""
122
+
123
+ agent: str = Field(..., description="Agent that produced the thought")
124
+ content: str = Field(..., description="Content of the thought")
125
+ timestamp: IsoDatetime = Field(..., description="Timestamp of the thought (UTC)")
@@ -2,8 +2,8 @@
2
2
  Task-related type definitions for the Mobile-use SDK.
3
3
  """
4
4
 
5
+ from collections.abc import Callable, Coroutine
5
6
  from datetime import datetime
6
- from enum import Enum
7
7
  from pathlib import Path
8
8
  from typing import Any, TypeVar, overload
9
9
 
@@ -12,6 +12,7 @@ from pydantic import BaseModel, Field
12
12
  from minitap.mobile_use.config import LLMConfig, get_default_llm_config
13
13
  from minitap.mobile_use.constants import RECURSION_LIMIT
14
14
  from minitap.mobile_use.context import DeviceContext
15
+ from minitap.mobile_use.sdk.types.platform import TaskRunResponse, TaskRunStatus
15
16
  from minitap.mobile_use.sdk.utils import load_llm_config_override
16
17
 
17
18
 
@@ -54,21 +55,11 @@ class AgentProfile(BaseModel):
54
55
  return f"Profile {self.name}:\n{self.llm_config}"
55
56
 
56
57
 
57
- class TaskStatus(str, Enum):
58
- """Task execution status enumeration."""
59
-
60
- PENDING = "PENDING"
61
- RUNNING = "RUNNING"
62
- COMPLETED = "COMPLETED"
63
- FAILED = "FAILED"
64
- CANCELLED = "CANCELLED"
65
-
66
-
67
58
  T = TypeVar("T", bound=BaseModel)
68
59
  TOutput = TypeVar("TOutput", bound=BaseModel | None)
69
60
 
70
61
 
71
- class TaskRequestCommon(BaseModel):
62
+ class TaskRequestBase(BaseModel):
72
63
  """
73
64
  Defines common parameters of a mobile automation task request.
74
65
  """
@@ -80,6 +71,14 @@ class TaskRequestCommon(BaseModel):
80
71
  thoughts_output_path: Path | None = None
81
72
 
82
73
 
74
+ class TaskRequestCommon(TaskRequestBase):
75
+ """
76
+ Defines common parameters for any task request.
77
+ """
78
+
79
+ max_steps: int = RECURSION_LIMIT
80
+
81
+
83
82
  class TaskRequest[TOutput](TaskRequestCommon):
84
83
  """
85
84
  Defines the format of a mobile automation task request.
@@ -103,6 +102,23 @@ class TaskRequest[TOutput](TaskRequestCommon):
103
102
  task_name: str | None = None
104
103
  output_description: str | None = None
105
104
  output_format: type[TOutput] | None = None
105
+ enable_remote_tracing: bool = False
106
+
107
+
108
+ class PlatformTaskRequest[TOutput](TaskRequestBase):
109
+ """
110
+ Minitap-specific task request for SDK usage via the gateway platform.
111
+
112
+ Attributes:
113
+ task: Required task name specified by the user on the platform
114
+ profile: Optional profile name specified by the user on the platform
115
+ api_key: Optional API key to authenticate with the platform
116
+ (overrides MINITAP_API_KEY env variable)
117
+ """
118
+
119
+ task: str
120
+ profile: str | None = None
121
+ api_key: str | None = None
106
122
 
107
123
 
108
124
  class TaskResult(BaseModel):
@@ -156,22 +172,30 @@ class Task(BaseModel):
156
172
 
157
173
  id: str
158
174
  device: DeviceContext
159
- status: TaskStatus
175
+ status: TaskRunStatus
176
+ status_message: str | None = None
177
+ on_status_changed: Callable[[TaskRunStatus, str | None, Any | None], Coroutine] | None = None
160
178
  request: TaskRequest
161
179
  created_at: datetime
162
180
  ended_at: datetime | None = None
163
181
  result: TaskResult | None = None
164
182
 
165
- def finalize(
183
+ async def finalize(
166
184
  self,
167
185
  content: Any | None = None,
168
186
  state: dict | None = None,
169
187
  error: str | None = None,
170
188
  cancelled: bool = False,
171
189
  ):
172
- self.status = TaskStatus.COMPLETED if error is None else TaskStatus.FAILED
173
- if self.status == TaskStatus.FAILED and cancelled:
174
- self.status = TaskStatus.CANCELLED
190
+ new_status: TaskRunStatus = "completed" if error is None else "failed"
191
+ if new_status == "failed" and cancelled:
192
+ new_status = "cancelled"
193
+ message = "Task completed successfully"
194
+ if new_status == "failed":
195
+ message = "Task failed" + (f": {error}" if error else "")
196
+ elif new_status == "cancelled":
197
+ message = "Task cancelled" + (f": {error}" if error else "")
198
+ await self.set_status(status=new_status, message=message, output=content or error)
175
199
  self.ended_at = datetime.now()
176
200
 
177
201
  duration = self.ended_at - self.created_at
@@ -189,4 +213,23 @@ class Task(BaseModel):
189
213
  )
190
214
 
191
215
  def get_name(self) -> str:
216
+ if isinstance(self.request, PlatformTaskRequest):
217
+ return self.request.task
192
218
  return self.request.task_name or self.id
219
+
220
+ async def set_status(
221
+ self,
222
+ status: TaskRunStatus,
223
+ message: str | None = None,
224
+ output: Any | None = None,
225
+ ):
226
+ self.status = status
227
+ self.status_message = message
228
+ if self.on_status_changed:
229
+ await self.on_status_changed(status, message, output)
230
+
231
+
232
+ class PlatformTaskInfo(BaseModel):
233
+ task_request: TaskRequest = Field(..., description="Task request")
234
+ llm_profile: AgentProfile = Field(..., description="LLM profile")
235
+ task_run: TaskRunResponse = Field(..., description="Task run instance on the platform")
@@ -6,6 +6,7 @@ import time
6
6
  from enum import Enum
7
7
 
8
8
  import requests
9
+
9
10
  from minitap.mobile_use.context import DevicePlatform
10
11
  from minitap.mobile_use.servers.utils import is_port_in_use
11
12
 
@@ -37,7 +38,7 @@ class DeviceHardwareBridge:
37
38
  try:
38
39
  creation_flags = 0
39
40
  if hasattr(subprocess, "CREATE_NO_WINDOW"):
40
- creation_flags = subprocess.CREATE_NO_WINDOW
41
+ creation_flags = subprocess.CREATE_NO_WINDOW # pyright: ignore[reportAttributeAccessIssue]
41
42
 
42
43
  maestro_platform = "android" if self.platform == DevicePlatform.ANDROID else "ios"
43
44
  cmd = ["maestro", "--device", self.device_id, "--platform", maestro_platform]
@@ -175,7 +176,7 @@ class DeviceHardwareBridge:
175
176
  ]
176
177
 
177
178
  def start(self):
178
- if is_port_in_use(DEVICE_HARDWARE_BRIDGE_PORT):
179
+ if is_port_in_use(port=DEVICE_HARDWARE_BRIDGE_PORT):
179
180
  print("Maestro port already in use - assuming Maestro is running.")
180
181
  self.status = BridgeStatus.RUNNING
181
182
  return True