minitap-mobile-use 2.3.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.
- minitap/mobile_use/agents/contextor/contextor.py +2 -2
- minitap/mobile_use/agents/cortex/cortex.md +49 -8
- minitap/mobile_use/agents/cortex/cortex.py +8 -4
- minitap/mobile_use/agents/executor/executor.md +14 -11
- minitap/mobile_use/agents/executor/executor.py +6 -5
- minitap/mobile_use/agents/hopper/hopper.py +6 -3
- minitap/mobile_use/agents/orchestrator/orchestrator.py +26 -11
- minitap/mobile_use/agents/outputter/outputter.py +6 -3
- minitap/mobile_use/agents/planner/planner.md +20 -22
- minitap/mobile_use/agents/planner/planner.py +10 -7
- minitap/mobile_use/agents/planner/types.py +4 -2
- minitap/mobile_use/agents/planner/utils.py +14 -0
- minitap/mobile_use/agents/summarizer/summarizer.py +2 -2
- minitap/mobile_use/config.py +6 -1
- minitap/mobile_use/context.py +13 -3
- minitap/mobile_use/controllers/mobile_command_controller.py +1 -14
- minitap/mobile_use/graph/state.py +7 -3
- minitap/mobile_use/sdk/agent.py +188 -23
- minitap/mobile_use/sdk/examples/README.md +19 -1
- minitap/mobile_use/sdk/examples/platform_minimal_example.py +46 -0
- minitap/mobile_use/sdk/services/platform.py +244 -0
- minitap/mobile_use/sdk/types/__init__.py +14 -14
- minitap/mobile_use/sdk/types/exceptions.py +27 -0
- minitap/mobile_use/sdk/types/platform.py +125 -0
- minitap/mobile_use/sdk/types/task.py +60 -17
- minitap/mobile_use/servers/device_hardware_bridge.py +1 -1
- minitap/mobile_use/servers/stop_servers.py +11 -12
- minitap/mobile_use/services/llm.py +89 -5
- minitap/mobile_use/tools/index.py +0 -6
- minitap/mobile_use/tools/mobile/back.py +3 -3
- minitap/mobile_use/tools/mobile/clear_text.py +24 -43
- minitap/mobile_use/tools/mobile/erase_one_char.py +5 -4
- minitap/mobile_use/tools/mobile/glimpse_screen.py +11 -7
- minitap/mobile_use/tools/mobile/input_text.py +21 -51
- minitap/mobile_use/tools/mobile/launch_app.py +54 -22
- minitap/mobile_use/tools/mobile/long_press_on.py +15 -8
- minitap/mobile_use/tools/mobile/open_link.py +15 -8
- minitap/mobile_use/tools/mobile/press_key.py +15 -8
- minitap/mobile_use/tools/mobile/stop_app.py +14 -8
- minitap/mobile_use/tools/mobile/swipe.py +11 -5
- minitap/mobile_use/tools/mobile/tap.py +103 -21
- minitap/mobile_use/tools/mobile/wait_for_animation_to_end.py +3 -3
- minitap/mobile_use/tools/test_utils.py +104 -78
- minitap/mobile_use/tools/types.py +35 -0
- minitap/mobile_use/tools/utils.py +51 -48
- minitap/mobile_use/utils/recorder.py +1 -1
- minitap/mobile_use/utils/ui_hierarchy.py +9 -2
- {minitap_mobile_use-2.3.0.dist-info → minitap_mobile_use-2.4.0.dist-info}/METADATA +3 -1
- {minitap_mobile_use-2.3.0.dist-info → minitap_mobile_use-2.4.0.dist-info}/RECORD +51 -50
- minitap/mobile_use/tools/mobile/copy_text_from.py +0 -75
- minitap/mobile_use/tools/mobile/find_packages.py +0 -69
- minitap/mobile_use/tools/mobile/paste_text.py +0 -88
- {minitap_mobile_use-2.3.0.dist-info → minitap_mobile_use-2.4.0.dist-info}/WHEEL +0 -0
- {minitap_mobile_use-2.3.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
|
-
"
|
|
36
|
+
"PlatformTaskRequest",
|
|
37
37
|
"TaskResult",
|
|
38
38
|
"TaskRequestCommon",
|
|
39
39
|
"Task",
|
|
@@ -102,3 +102,30 @@ class ExecutableNotFoundError(MobileUseError):
|
|
|
102
102
|
if executable_name in install_instructions:
|
|
103
103
|
message += f"\nTo install it, please visit: {install_instructions[executable_name]}"
|
|
104
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
|
|
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:
|
|
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
|
-
|
|
173
|
-
if
|
|
174
|
-
|
|
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")
|
|
@@ -38,7 +38,7 @@ class DeviceHardwareBridge:
|
|
|
38
38
|
try:
|
|
39
39
|
creation_flags = 0
|
|
40
40
|
if hasattr(subprocess, "CREATE_NO_WINDOW"):
|
|
41
|
-
creation_flags = subprocess.CREATE_NO_WINDOW
|
|
41
|
+
creation_flags = subprocess.CREATE_NO_WINDOW # pyright: ignore[reportAttributeAccessIssue]
|
|
42
42
|
|
|
43
43
|
maestro_platform = "android" if self.platform == DevicePlatform.ANDROID else "ios"
|
|
44
44
|
cmd = ["maestro", "--device", self.device_id, "--platform", maestro_platform]
|
|
@@ -70,18 +70,17 @@ def stop_process_gracefully(process: psutil.Process, timeout: int = 5) -> bool:
|
|
|
70
70
|
return False
|
|
71
71
|
|
|
72
72
|
|
|
73
|
-
def
|
|
73
|
+
def check_service_running(port: int, service_name: str) -> bool:
|
|
74
74
|
try:
|
|
75
75
|
if port == server_settings.DEVICE_SCREEN_API_PORT:
|
|
76
|
-
|
|
76
|
+
requests.get(f"http://localhost:{port}/health", timeout=2)
|
|
77
77
|
elif port == DEVICE_HARDWARE_BRIDGE_PORT:
|
|
78
|
-
|
|
78
|
+
requests.get(f"http://localhost:{port}/api/banner-message", timeout=2)
|
|
79
79
|
else:
|
|
80
80
|
return False
|
|
81
81
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
return True
|
|
82
|
+
logger.debug(f"{service_name} is still responding on port {port}")
|
|
83
|
+
return True
|
|
85
84
|
except requests.exceptions.RequestException:
|
|
86
85
|
pass
|
|
87
86
|
|
|
@@ -92,7 +91,7 @@ def stop_device_screen_api() -> bool:
|
|
|
92
91
|
logger.info("Stopping Device Screen API...")
|
|
93
92
|
api_port = server_settings.DEVICE_SCREEN_API_PORT
|
|
94
93
|
|
|
95
|
-
if not
|
|
94
|
+
if not check_service_running(api_port, "Device Screen API"):
|
|
96
95
|
logger.success("Device Screen API is not running")
|
|
97
96
|
return True
|
|
98
97
|
|
|
@@ -109,7 +108,7 @@ def stop_device_screen_api() -> bool:
|
|
|
109
108
|
logger.warning("No Device Screen API processes found, but service is still responding")
|
|
110
109
|
# Still try to verify if service actually stops
|
|
111
110
|
time.sleep(1)
|
|
112
|
-
if not
|
|
111
|
+
if not check_service_running(api_port, "Device Screen API"):
|
|
113
112
|
logger.success("Device Screen API stopped successfully (was orphaned)")
|
|
114
113
|
return True
|
|
115
114
|
return False
|
|
@@ -120,7 +119,7 @@ def stop_device_screen_api() -> bool:
|
|
|
120
119
|
|
|
121
120
|
# Verify service is stopped
|
|
122
121
|
time.sleep(1)
|
|
123
|
-
if
|
|
122
|
+
if check_service_running(api_port, "Device Screen API"):
|
|
124
123
|
logger.error("Device Screen API is still running after stop attempt")
|
|
125
124
|
return False
|
|
126
125
|
|
|
@@ -131,7 +130,7 @@ def stop_device_screen_api() -> bool:
|
|
|
131
130
|
def stop_device_hardware_bridge() -> bool:
|
|
132
131
|
logger.info("Stopping Device Hardware Bridge...")
|
|
133
132
|
|
|
134
|
-
if not
|
|
133
|
+
if not check_service_running(DEVICE_HARDWARE_BRIDGE_PORT, "Maestro Studio"):
|
|
135
134
|
logger.success("Device Hardware Bridge is not running")
|
|
136
135
|
return True
|
|
137
136
|
|
|
@@ -145,7 +144,7 @@ def stop_device_hardware_bridge() -> bool:
|
|
|
145
144
|
logger.warning("No Device Hardware Bridge processes found, but service is still responding")
|
|
146
145
|
# Still try to verify if service actually stops
|
|
147
146
|
time.sleep(1)
|
|
148
|
-
if not
|
|
147
|
+
if not check_service_running(DEVICE_HARDWARE_BRIDGE_PORT, "Maestro Studio"):
|
|
149
148
|
logger.success("Device Hardware Bridge stopped successfully (was orphaned)")
|
|
150
149
|
return True
|
|
151
150
|
return False
|
|
@@ -154,7 +153,7 @@ def stop_device_hardware_bridge() -> bool:
|
|
|
154
153
|
stop_process_gracefully(proc)
|
|
155
154
|
|
|
156
155
|
time.sleep(1)
|
|
157
|
-
if
|
|
156
|
+
if check_service_running(DEVICE_HARDWARE_BRIDGE_PORT, "Maestro Studio"):
|
|
158
157
|
logger.error("Device Hardware Bridge is still running after stop attempt")
|
|
159
158
|
return False
|
|
160
159
|
|