minitap-mobile-use 3.3.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.
Files changed (115) hide show
  1. minitap/mobile_use/__init__.py +0 -0
  2. minitap/mobile_use/agents/contextor/contextor.md +55 -0
  3. minitap/mobile_use/agents/contextor/contextor.py +175 -0
  4. minitap/mobile_use/agents/contextor/types.py +36 -0
  5. minitap/mobile_use/agents/cortex/cortex.md +135 -0
  6. minitap/mobile_use/agents/cortex/cortex.py +152 -0
  7. minitap/mobile_use/agents/cortex/types.py +15 -0
  8. minitap/mobile_use/agents/executor/executor.md +42 -0
  9. minitap/mobile_use/agents/executor/executor.py +87 -0
  10. minitap/mobile_use/agents/executor/tool_node.py +152 -0
  11. minitap/mobile_use/agents/hopper/hopper.md +15 -0
  12. minitap/mobile_use/agents/hopper/hopper.py +44 -0
  13. minitap/mobile_use/agents/orchestrator/human.md +12 -0
  14. minitap/mobile_use/agents/orchestrator/orchestrator.md +21 -0
  15. minitap/mobile_use/agents/orchestrator/orchestrator.py +134 -0
  16. minitap/mobile_use/agents/orchestrator/types.py +11 -0
  17. minitap/mobile_use/agents/outputter/human.md +25 -0
  18. minitap/mobile_use/agents/outputter/outputter.py +85 -0
  19. minitap/mobile_use/agents/outputter/test_outputter.py +167 -0
  20. minitap/mobile_use/agents/planner/human.md +14 -0
  21. minitap/mobile_use/agents/planner/planner.md +126 -0
  22. minitap/mobile_use/agents/planner/planner.py +101 -0
  23. minitap/mobile_use/agents/planner/types.py +51 -0
  24. minitap/mobile_use/agents/planner/utils.py +70 -0
  25. minitap/mobile_use/agents/summarizer/summarizer.py +35 -0
  26. minitap/mobile_use/agents/video_analyzer/__init__.py +5 -0
  27. minitap/mobile_use/agents/video_analyzer/human.md +5 -0
  28. minitap/mobile_use/agents/video_analyzer/video_analyzer.md +37 -0
  29. minitap/mobile_use/agents/video_analyzer/video_analyzer.py +111 -0
  30. minitap/mobile_use/clients/browserstack_client.py +477 -0
  31. minitap/mobile_use/clients/idb_client.py +429 -0
  32. minitap/mobile_use/clients/ios_client.py +332 -0
  33. minitap/mobile_use/clients/ios_client_config.py +141 -0
  34. minitap/mobile_use/clients/ui_automator_client.py +330 -0
  35. minitap/mobile_use/clients/wda_client.py +526 -0
  36. minitap/mobile_use/clients/wda_lifecycle.py +367 -0
  37. minitap/mobile_use/config.py +413 -0
  38. minitap/mobile_use/constants.py +3 -0
  39. minitap/mobile_use/context.py +106 -0
  40. minitap/mobile_use/controllers/__init__.py +0 -0
  41. minitap/mobile_use/controllers/android_controller.py +524 -0
  42. minitap/mobile_use/controllers/controller_factory.py +46 -0
  43. minitap/mobile_use/controllers/device_controller.py +182 -0
  44. minitap/mobile_use/controllers/ios_controller.py +436 -0
  45. minitap/mobile_use/controllers/platform_specific_commands_controller.py +199 -0
  46. minitap/mobile_use/controllers/types.py +106 -0
  47. minitap/mobile_use/controllers/unified_controller.py +193 -0
  48. minitap/mobile_use/graph/graph.py +160 -0
  49. minitap/mobile_use/graph/state.py +115 -0
  50. minitap/mobile_use/main.py +309 -0
  51. minitap/mobile_use/sdk/__init__.py +12 -0
  52. minitap/mobile_use/sdk/agent.py +1294 -0
  53. minitap/mobile_use/sdk/builders/__init__.py +10 -0
  54. minitap/mobile_use/sdk/builders/agent_config_builder.py +307 -0
  55. minitap/mobile_use/sdk/builders/index.py +15 -0
  56. minitap/mobile_use/sdk/builders/task_request_builder.py +236 -0
  57. minitap/mobile_use/sdk/constants.py +1 -0
  58. minitap/mobile_use/sdk/examples/README.md +83 -0
  59. minitap/mobile_use/sdk/examples/__init__.py +1 -0
  60. minitap/mobile_use/sdk/examples/app_lock_messaging.py +54 -0
  61. minitap/mobile_use/sdk/examples/platform_manual_task_example.py +67 -0
  62. minitap/mobile_use/sdk/examples/platform_minimal_example.py +48 -0
  63. minitap/mobile_use/sdk/examples/simple_photo_organizer.py +76 -0
  64. minitap/mobile_use/sdk/examples/smart_notification_assistant.py +225 -0
  65. minitap/mobile_use/sdk/examples/video_transcription_example.py +117 -0
  66. minitap/mobile_use/sdk/services/cloud_mobile.py +656 -0
  67. minitap/mobile_use/sdk/services/platform.py +434 -0
  68. minitap/mobile_use/sdk/types/__init__.py +51 -0
  69. minitap/mobile_use/sdk/types/agent.py +84 -0
  70. minitap/mobile_use/sdk/types/exceptions.py +138 -0
  71. minitap/mobile_use/sdk/types/platform.py +183 -0
  72. minitap/mobile_use/sdk/types/task.py +269 -0
  73. minitap/mobile_use/sdk/utils.py +29 -0
  74. minitap/mobile_use/services/accessibility.py +100 -0
  75. minitap/mobile_use/services/llm.py +247 -0
  76. minitap/mobile_use/services/telemetry.py +421 -0
  77. minitap/mobile_use/tools/index.py +67 -0
  78. minitap/mobile_use/tools/mobile/back.py +52 -0
  79. minitap/mobile_use/tools/mobile/erase_one_char.py +56 -0
  80. minitap/mobile_use/tools/mobile/focus_and_clear_text.py +317 -0
  81. minitap/mobile_use/tools/mobile/focus_and_input_text.py +153 -0
  82. minitap/mobile_use/tools/mobile/launch_app.py +86 -0
  83. minitap/mobile_use/tools/mobile/long_press_on.py +169 -0
  84. minitap/mobile_use/tools/mobile/open_link.py +62 -0
  85. minitap/mobile_use/tools/mobile/press_key.py +83 -0
  86. minitap/mobile_use/tools/mobile/stop_app.py +62 -0
  87. minitap/mobile_use/tools/mobile/swipe.py +156 -0
  88. minitap/mobile_use/tools/mobile/tap.py +154 -0
  89. minitap/mobile_use/tools/mobile/video_recording.py +177 -0
  90. minitap/mobile_use/tools/mobile/wait_for_delay.py +81 -0
  91. minitap/mobile_use/tools/scratchpad.py +147 -0
  92. minitap/mobile_use/tools/test_utils.py +413 -0
  93. minitap/mobile_use/tools/tool_wrapper.py +16 -0
  94. minitap/mobile_use/tools/types.py +35 -0
  95. minitap/mobile_use/tools/utils.py +336 -0
  96. minitap/mobile_use/utils/app_launch_utils.py +173 -0
  97. minitap/mobile_use/utils/cli_helpers.py +37 -0
  98. minitap/mobile_use/utils/cli_selection.py +143 -0
  99. minitap/mobile_use/utils/conversations.py +31 -0
  100. minitap/mobile_use/utils/decorators.py +124 -0
  101. minitap/mobile_use/utils/errors.py +6 -0
  102. minitap/mobile_use/utils/file.py +13 -0
  103. minitap/mobile_use/utils/logger.py +183 -0
  104. minitap/mobile_use/utils/media.py +186 -0
  105. minitap/mobile_use/utils/recorder.py +52 -0
  106. minitap/mobile_use/utils/requests_utils.py +37 -0
  107. minitap/mobile_use/utils/shell_utils.py +20 -0
  108. minitap/mobile_use/utils/test_ui_hierarchy.py +178 -0
  109. minitap/mobile_use/utils/time.py +6 -0
  110. minitap/mobile_use/utils/ui_hierarchy.py +132 -0
  111. minitap/mobile_use/utils/video.py +281 -0
  112. minitap_mobile_use-3.3.0.dist-info/METADATA +329 -0
  113. minitap_mobile_use-3.3.0.dist-info/RECORD +115 -0
  114. minitap_mobile_use-3.3.0.dist-info/WHEEL +4 -0
  115. minitap_mobile_use-3.3.0.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,138 @@
1
+ """
2
+ Exceptions for the Mobile-use SDK.
3
+
4
+ This module defines the exception hierarchy used throughout the Mobile-use SDK.
5
+ """
6
+
7
+ from typing import Literal
8
+
9
+
10
+ class MobileUseError(Exception):
11
+ """Base exception class for all Mobile-use SDK exceptions."""
12
+
13
+ def __init__(self, message="An error occurred in the Mobile-use SDK"):
14
+ self.message = message
15
+ super().__init__(self.message)
16
+
17
+
18
+ class DeviceError(MobileUseError):
19
+ """Exception raised for errors related to mobile devices."""
20
+
21
+ def __init__(self, message="A device-related error occurred"):
22
+ super().__init__(message)
23
+
24
+
25
+ class DeviceNotFoundError(DeviceError):
26
+ """Exception raised when no mobile device is found."""
27
+
28
+ def __init__(self, message="No mobile device found"):
29
+ super().__init__(message)
30
+
31
+
32
+ class ServerError(MobileUseError):
33
+ """Exception raised for errors related to Mobile-use servers."""
34
+
35
+ def __init__(self, message="A server-related error occurred"):
36
+ super().__init__(message)
37
+
38
+
39
+ class ServerStartupError(ServerError):
40
+ """Exception raised when Mobile-use servers fail to start."""
41
+
42
+ def __init__(self, server_name=None, message=None):
43
+ if server_name and not message:
44
+ message = f"Failed to start {server_name}"
45
+ elif not message:
46
+ message = "Failed to start Mobile-use servers"
47
+ super().__init__(message)
48
+ self.server_name = server_name
49
+
50
+
51
+ class AgentError(MobileUseError):
52
+ """Exception raised for errors related to the Mobile-use agent."""
53
+
54
+ def __init__(self, message="An agent-related error occurred"):
55
+ super().__init__(message)
56
+
57
+
58
+ class AgentNotInitializedError(AgentError):
59
+ """Exception raised when attempting operations on an uninitialized agent."""
60
+
61
+ def __init__(self, message="Agent is not initialized. Call init() first"):
62
+ super().__init__(message)
63
+
64
+
65
+ class AgentTaskRequestError(AgentError):
66
+ """Exception raised when a requested task is invalid."""
67
+
68
+ def __init__(self, message="An agent task-related error occurred"):
69
+ super().__init__(message)
70
+
71
+
72
+ class AgentProfileNotFoundError(AgentTaskRequestError):
73
+ """Exception raised when an agent profile is not found."""
74
+
75
+ def __init__(self, profile_name: str):
76
+ super().__init__(f"Agent profile {profile_name} not found")
77
+
78
+
79
+ EXECUTABLES = Literal["adb", "xcrun", "idb", "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
+ "idb": "https://fbidb.io/docs/installation/",
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 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
+
134
+ class PlatformServiceError(MobileUseError):
135
+ """Exception raised when a platform service call fails."""
136
+
137
+ def __init__(self, message="A platform service-related error occurred"):
138
+ super().__init__(message)
@@ -0,0 +1,183 @@
1
+ from datetime import datetime
2
+ from typing import Annotated, Any, Literal
3
+
4
+ from pydantic import BaseModel, ConfigDict, Field, PlainSerializer
5
+ from pydantic.v1.utils import to_lower_camel
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
+ enable_video_tools: bool = Field(..., description="Whether video tools are enabled")
45
+ created_at: str = Field(..., description="Creation timestamp (ISO format)")
46
+ updated_at: str = Field(..., description="Last update timestamp (ISO format)")
47
+
48
+
49
+ class TaskResponse(BaseApiModel):
50
+ """Response model for task."""
51
+
52
+ id: str = Field(..., description="Task ID")
53
+ name: str = Field(..., description="Task name")
54
+ description: str | None = Field(None, description="Task description")
55
+ input_prompt: str = Field(..., description="Input prompt")
56
+ output_description: str | None = Field(None, description="Output description")
57
+ options: TaskOptionsResponse = Field(..., description="Task options")
58
+ created_at: str = Field(..., description="Creation timestamp (ISO format)")
59
+ updated_at: str = Field(..., description="Last update timestamp (ISO format)")
60
+
61
+
62
+ class CreateTaskRunRequest(BaseApiModel):
63
+ """Request model for creating a task run."""
64
+
65
+ task_id: str = Field(..., description="ID of the task to run")
66
+ llm_profile_id: str = Field(..., description="LLM profile ID to use")
67
+ virtual_mobile_id: str | None = Field(None, description="Virtual mobile ID to use")
68
+ locked_app_package: str | None = Field(None, description="App package to lock for the task run")
69
+ execution_origin: str | None = Field(None, description="Origin of the task execution")
70
+ enable_video_tools: bool = Field(
71
+ default=False,
72
+ description="Whether video tools are enabled for this task run",
73
+ )
74
+
75
+
76
+ class CreateOrphanTaskRunRequest(BaseApiModel):
77
+ """Request model for creating an orphan task run (without a parent task)."""
78
+
79
+ input_prompt: str = Field(
80
+ ...,
81
+ description="Input prompt for the task run",
82
+ )
83
+ output_description: str | None = Field(
84
+ None,
85
+ description="Optional output description",
86
+ )
87
+ llm_profile_id: str = Field(
88
+ ...,
89
+ description="LLM profile ID to use",
90
+ )
91
+ virtual_mobile_id: str | None = Field(
92
+ None,
93
+ description="Optional virtual mobile ID on which it runs",
94
+ )
95
+ locked_app_package: str | None = Field(
96
+ None,
97
+ description="Optional app package to lock execution to",
98
+ )
99
+ execution_origin: str | None = Field(
100
+ None,
101
+ description="Origin of the task run execution",
102
+ )
103
+ max_steps: int | None = Field(
104
+ None,
105
+ description="Maximum number of steps for the task run",
106
+ )
107
+ enable_video_tools: bool = Field(
108
+ default=False,
109
+ description="Whether video tools are enabled for this task run",
110
+ )
111
+
112
+
113
+ class UpdateTaskRunStatusRequest(BaseApiModel):
114
+ """Request model for updating task run status."""
115
+
116
+ status: TaskRunStatus = Field(..., description="New status of the task run")
117
+ message: str | None = Field(None, description="Message associated with the status")
118
+ output: str | None = Field(None, description="Output of the task run")
119
+
120
+
121
+ class TaskRunResponse(BaseApiModel):
122
+ """Response model for a single task run."""
123
+
124
+ id: str = Field(..., description="Unique identifier for the task run")
125
+ task: TaskResponse | None = Field(
126
+ ..., description="ID of the task this run is for or None if manually created"
127
+ )
128
+ llm_profile: LLMProfileResponse = Field(..., description="LLM profile ID used for this run")
129
+ status: TaskRunStatus = Field(..., description="Current status of the task run")
130
+ input_prompt: str = Field(..., description="Input prompt for this task run")
131
+ output_description: str | None = Field(None, description="Description of expected output")
132
+ created_at: datetime = Field(..., description="When the task run was created")
133
+ started_at: datetime | None = Field(None, description="When the task run started")
134
+ finished_at: datetime | None = Field(None, description="When the task run finished")
135
+ locked_app_package: str | None = Field(
136
+ None, description="The app package to lock for the task run"
137
+ )
138
+
139
+
140
+ SubgoalState = Literal["pending", "started", "completed", "failed"]
141
+
142
+
143
+ class MobileUseSubgoal(BaseModel):
144
+ """Upsert MobileUseSubgoal API model."""
145
+
146
+ name: str = Field(..., description="Name of the subgoal")
147
+ state: SubgoalState = Field(default="pending", description="Current state of the subgoal")
148
+ started_at: IsoDatetime | None = Field(default=None, description="When the subgoal started")
149
+ ended_at: IsoDatetime | None = Field(default=None, description="When the subgoal ended")
150
+
151
+
152
+ class UpsertTaskRunPlanRequest(BaseApiModel):
153
+ """Upsert MobileUseSubgoal API model."""
154
+
155
+ started_at: IsoDatetime = Field(..., description="When the plan started")
156
+ subgoals: list[MobileUseSubgoal] = Field(..., description="Subgoals of the plan")
157
+ ended_at: IsoDatetime | None = Field(
158
+ default=None,
159
+ description="When the plan ended (replanned or completed)",
160
+ )
161
+
162
+
163
+ class TaskRunPlanResponse(UpsertTaskRunPlanRequest):
164
+ """Response model for a task run plan."""
165
+
166
+ id: str = Field(..., description="Unique identifier for the task run plan")
167
+ task_run_id: str = Field(..., description="ID of the task run this plan is for")
168
+
169
+
170
+ class UpsertTaskRunAgentThoughtRequest(BaseApiModel):
171
+ """Upsert MobileUseAgentThought request model."""
172
+
173
+ agent: str = Field(..., description="Agent that produced the thought")
174
+ content: str = Field(..., description="Content of the thought")
175
+ timestamp: IsoDatetime = Field(..., description="Timestamp of the thought (UTC)")
176
+
177
+
178
+ class TrajectoryGifUploadResponse(BaseApiModel):
179
+ """Response containing trajectory GIF signed upload URL."""
180
+
181
+ signed_url: str = Field(..., description="Signed upload URL for the trajectory GIF")
182
+ task_run_id: str = Field(..., description="Task run ID for which the GIF URL was generated")
183
+ file_path: str = Field(..., description="Path where the GIF will be stored")
@@ -0,0 +1,269 @@
1
+ """
2
+ Task-related type definitions for the Mobile-use SDK.
3
+ """
4
+
5
+ import tempfile
6
+ from asyncio import Event
7
+ from collections.abc import Callable, Coroutine
8
+ from datetime import datetime
9
+ from pathlib import Path
10
+ from typing import Any, TypeVar, overload
11
+
12
+ from pydantic import BaseModel, ConfigDict, Field
13
+
14
+ from minitap.mobile_use.config import LLMConfig, get_default_llm_config
15
+ from minitap.mobile_use.constants import RECURSION_LIMIT
16
+ from minitap.mobile_use.context import DeviceContext
17
+ from minitap.mobile_use.sdk.types.platform import TaskRunResponse, TaskRunStatus
18
+ from minitap.mobile_use.sdk.utils import load_llm_config_override
19
+
20
+
21
+ class AgentProfile(BaseModel):
22
+ """
23
+ Represents a mobile-use agent profile.
24
+
25
+ Attributes:
26
+ name: Name of the agent - used to reference the agent when running tasks.
27
+ llm_config: LLM configuration for the agent.
28
+ """
29
+
30
+ name: str
31
+ llm_config: LLMConfig = Field(default_factory=get_default_llm_config)
32
+
33
+ @overload
34
+ def __init__(self, *, name: str, llm_config: LLMConfig): ...
35
+
36
+ @overload
37
+ def __init__(self, *, name: str, from_file: str): ...
38
+
39
+ def __init__(
40
+ self,
41
+ *,
42
+ name: str,
43
+ llm_config: LLMConfig | None = None,
44
+ from_file: str | None = None,
45
+ **kwargs,
46
+ ):
47
+ kwargs["name"] = name
48
+ if from_file:
49
+ kwargs["llm_config"] = load_llm_config_override(Path(from_file))
50
+ elif llm_config:
51
+ kwargs["llm_config"] = llm_config
52
+ else:
53
+ raise ValueError("Either llm_config or from_file must be provided")
54
+ super().__init__(**kwargs)
55
+
56
+ def __str__(self):
57
+ return f"Profile {self.name}:\n{self.llm_config}"
58
+
59
+
60
+ T = TypeVar("T", bound=BaseModel)
61
+ TOutput = TypeVar("TOutput", bound=BaseModel | None)
62
+
63
+
64
+ class TaskRequestBase(BaseModel):
65
+ """
66
+ Defines common parameters of a mobile automation task request.
67
+ """
68
+
69
+ max_steps: int = RECURSION_LIMIT
70
+ record_trace: bool = False
71
+ trace_path: Path = Path("mobile-use-traces")
72
+ llm_output_path: Path | None = None
73
+ thoughts_output_path: Path | None = None
74
+
75
+
76
+ class TaskRequestCommon(TaskRequestBase):
77
+ """
78
+ Defines common parameters for any task request.
79
+ """
80
+
81
+ max_steps: int = RECURSION_LIMIT
82
+ locked_app_package: str | None = None
83
+
84
+
85
+ class TaskRequest[TOutput](TaskRequestCommon):
86
+ """
87
+ Defines the format of a mobile automation task request.
88
+
89
+ Attributes:
90
+ goal: Natural language description of the goal to achieve
91
+ profile: Optional agent profile to use for executing the task
92
+ task_name: Optional name for the task
93
+ output_description: Optional natural language description of expected output format
94
+ output_format: Optional pydantic model for the output format of the task
95
+ max_steps: Maximum number of steps the agent can take (default: 20)
96
+ record_trace: Whether to record a trace (screenshots, actions) of the execution
97
+ (default: False)
98
+ trace_path: Directory path to save trace data if recording is enabled
99
+ llm_output_path: Path to save LLM output data
100
+ thoughts_output_path: Path to save thoughts output data
101
+ """
102
+
103
+ goal: str
104
+ profile: str | None = None
105
+ task_name: str | None = None
106
+ output_description: str | None = None
107
+ output_format: type[TOutput] | None = None
108
+ enable_remote_tracing: bool = False
109
+
110
+
111
+ class ManualTaskConfig(BaseModel):
112
+ """
113
+ Configuration for manually creating a task without fetching from the platform.
114
+
115
+ Attributes:
116
+ goal: Natural language description of the goal to achieve
117
+ output_description: Optional natural language description of expected output format
118
+ task_name: Optional name for the task
119
+ """
120
+
121
+ goal: str
122
+ output_description: str | None = None
123
+ task_name: str | None = None
124
+
125
+
126
+ class PlatformTaskRequest[TOutput](TaskRequestBase):
127
+ """
128
+ Minitap-specific task request for SDK usage via the gateway platform.
129
+
130
+ Attributes:
131
+ task: Either a task name to fetch from the platform, or a
132
+ ManualTaskConfig to create manually
133
+ profile: Optional profile name specified by the user on the platform
134
+ execution_origin: Origin of the task execution (default: "sdk")
135
+ record_trace: Whether to record traces (default: True for platform tasks)
136
+ trace_path: Path to save traces (default: temp directory)
137
+ """
138
+
139
+ task: str | ManualTaskConfig
140
+ profile: str | None = None
141
+ execution_origin: str = "sdk"
142
+ record_trace: bool = True
143
+ trace_path: Path = Path(tempfile.gettempdir()) / "mobile-use-traces"
144
+
145
+
146
+ class CloudDevicePlatformTaskRequest[TOutput](PlatformTaskRequest[TOutput]):
147
+ model_config = ConfigDict(arbitrary_types_allowed=True)
148
+
149
+ task_run_id_available_event: Event = Event()
150
+ task_run_id: str | None = None
151
+ virtual_mobile_id: str | None = None
152
+
153
+
154
+ class TaskResult(BaseModel):
155
+ """
156
+ Result of a mobile automation task.
157
+
158
+ Attributes:
159
+ content: Raw result content (could be text or structured data)
160
+ error: Error message if the task failed
161
+ execution_time_seconds: How long the task took to execute
162
+ steps_taken: Number of steps the agent took to complete the task
163
+ """
164
+
165
+ content: Any = None
166
+ error: str | None = None
167
+ execution_time_seconds: float
168
+ steps_taken: int
169
+
170
+ def get_as_model(self, model_class: type[T]) -> T:
171
+ """
172
+ Parse the content into a Pydantic model instance.
173
+
174
+ Args:
175
+ model_class: The Pydantic model class to parse the data into
176
+
177
+ Returns:
178
+ An instance of the specified model class
179
+
180
+ Raises:
181
+ ValueError: If content is None or not compatible with the model
182
+ """
183
+ if self.content is None:
184
+ raise ValueError("No content available to parse into a model")
185
+ if isinstance(self.content, model_class):
186
+ return self.content
187
+ return model_class.model_validate(self.content)
188
+
189
+
190
+ class Task(BaseModel):
191
+ """
192
+ A mobile automation task to be executed.
193
+
194
+ Attributes:
195
+ id: Unique identifier for the task
196
+ device: Information about the target device
197
+ status: Current status of the task execution
198
+ request: User task request
199
+ created_at: ISO timestamp when the task was created
200
+ ended_at: ISO timestamp when the task ended
201
+ """
202
+
203
+ id: str
204
+ device: DeviceContext
205
+ status: TaskRunStatus
206
+ status_message: str | None = None
207
+ on_status_changed: Callable[[TaskRunStatus, str | None, Any | None], Coroutine] | None = None
208
+ request: TaskRequest
209
+ created_at: datetime
210
+ ended_at: datetime | None = None
211
+ result: TaskResult | None = None
212
+
213
+ async def finalize(
214
+ self,
215
+ content: Any | None = None,
216
+ state: dict | None = None,
217
+ error: str | None = None,
218
+ cancelled: bool = False,
219
+ ):
220
+ new_status: TaskRunStatus = "completed" if error is None else "failed"
221
+ if new_status == "failed" and cancelled:
222
+ new_status = "cancelled"
223
+ message = "Task completed successfully"
224
+ if new_status == "failed":
225
+ message = "Task failed" + (f": {error}" if error else "")
226
+ elif new_status == "cancelled":
227
+ message = "Task cancelled" + (f": {error}" if error else "")
228
+ await self.set_status(status=new_status, message=message, output=content or error)
229
+ self.ended_at = datetime.now()
230
+
231
+ duration = self.ended_at - self.created_at
232
+ steps_taken = -1
233
+ if state is not None:
234
+ metadata = state.get("metadata", None)
235
+ if metadata:
236
+ steps_taken = metadata.get("step_count", -1)
237
+
238
+ self.result = TaskResult(
239
+ content=content,
240
+ error=error,
241
+ execution_time_seconds=duration.total_seconds(),
242
+ steps_taken=steps_taken,
243
+ )
244
+
245
+ def get_name(self) -> str:
246
+ if isinstance(self.request, PlatformTaskRequest):
247
+ if isinstance(self.request.task, str):
248
+ return self.request.task
249
+ else:
250
+ # ManualTaskConfig - use first 50 chars of goal
251
+ return f"Manual: {self.request.task.goal[:50]}"
252
+ return self.request.task_name or self.id
253
+
254
+ async def set_status(
255
+ self,
256
+ status: TaskRunStatus,
257
+ message: str | None = None,
258
+ output: Any | None = None,
259
+ ):
260
+ self.status = status
261
+ self.status_message = message
262
+ if self.on_status_changed:
263
+ await self.on_status_changed(status, message, output)
264
+
265
+
266
+ class PlatformTaskInfo(BaseModel):
267
+ task_request: TaskRequest = Field(..., description="Task request")
268
+ llm_profile: AgentProfile = Field(..., description="LLM profile")
269
+ task_run: TaskRunResponse = Field(..., description="Task run instance on the platform")
@@ -0,0 +1,29 @@
1
+ import os
2
+ from pathlib import Path
3
+
4
+ from pydantic import ValidationError
5
+
6
+ from minitap.mobile_use.config import LLMConfig, deep_merge_llm_config, get_default_llm_config
7
+ from minitap.mobile_use.utils.file import load_jsonc
8
+ from minitap.mobile_use.utils.logger import get_logger
9
+
10
+ logger = get_logger(__name__)
11
+
12
+
13
+ def load_llm_config_override(path: Path) -> LLMConfig:
14
+ default_config = get_default_llm_config()
15
+
16
+ override_config_dict = {}
17
+ if os.path.exists(path):
18
+ logger.info(f"Loading custom LLM config from {path.resolve()}...")
19
+ with open(path) as f:
20
+ override_config_dict = load_jsonc(f)
21
+ else:
22
+ logger.warning("Custom LLM config not found - using the default config")
23
+
24
+ try:
25
+ return deep_merge_llm_config(default_config, override_config_dict)
26
+ except ValidationError as e:
27
+ logger.error(f"Invalid LLM config: {e}")
28
+ logger.info("Falling back to default config")
29
+ return default_config