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.
- minitap/mobile_use/__init__.py +0 -0
- minitap/mobile_use/agents/contextor/contextor.md +55 -0
- minitap/mobile_use/agents/contextor/contextor.py +175 -0
- minitap/mobile_use/agents/contextor/types.py +36 -0
- minitap/mobile_use/agents/cortex/cortex.md +135 -0
- minitap/mobile_use/agents/cortex/cortex.py +152 -0
- minitap/mobile_use/agents/cortex/types.py +15 -0
- minitap/mobile_use/agents/executor/executor.md +42 -0
- minitap/mobile_use/agents/executor/executor.py +87 -0
- minitap/mobile_use/agents/executor/tool_node.py +152 -0
- minitap/mobile_use/agents/hopper/hopper.md +15 -0
- minitap/mobile_use/agents/hopper/hopper.py +44 -0
- minitap/mobile_use/agents/orchestrator/human.md +12 -0
- minitap/mobile_use/agents/orchestrator/orchestrator.md +21 -0
- minitap/mobile_use/agents/orchestrator/orchestrator.py +134 -0
- minitap/mobile_use/agents/orchestrator/types.py +11 -0
- minitap/mobile_use/agents/outputter/human.md +25 -0
- minitap/mobile_use/agents/outputter/outputter.py +85 -0
- minitap/mobile_use/agents/outputter/test_outputter.py +167 -0
- minitap/mobile_use/agents/planner/human.md +14 -0
- minitap/mobile_use/agents/planner/planner.md +126 -0
- minitap/mobile_use/agents/planner/planner.py +101 -0
- minitap/mobile_use/agents/planner/types.py +51 -0
- minitap/mobile_use/agents/planner/utils.py +70 -0
- minitap/mobile_use/agents/summarizer/summarizer.py +35 -0
- minitap/mobile_use/agents/video_analyzer/__init__.py +5 -0
- minitap/mobile_use/agents/video_analyzer/human.md +5 -0
- minitap/mobile_use/agents/video_analyzer/video_analyzer.md +37 -0
- minitap/mobile_use/agents/video_analyzer/video_analyzer.py +111 -0
- minitap/mobile_use/clients/browserstack_client.py +477 -0
- minitap/mobile_use/clients/idb_client.py +429 -0
- minitap/mobile_use/clients/ios_client.py +332 -0
- minitap/mobile_use/clients/ios_client_config.py +141 -0
- minitap/mobile_use/clients/ui_automator_client.py +330 -0
- minitap/mobile_use/clients/wda_client.py +526 -0
- minitap/mobile_use/clients/wda_lifecycle.py +367 -0
- minitap/mobile_use/config.py +413 -0
- minitap/mobile_use/constants.py +3 -0
- minitap/mobile_use/context.py +106 -0
- minitap/mobile_use/controllers/__init__.py +0 -0
- minitap/mobile_use/controllers/android_controller.py +524 -0
- minitap/mobile_use/controllers/controller_factory.py +46 -0
- minitap/mobile_use/controllers/device_controller.py +182 -0
- minitap/mobile_use/controllers/ios_controller.py +436 -0
- minitap/mobile_use/controllers/platform_specific_commands_controller.py +199 -0
- minitap/mobile_use/controllers/types.py +106 -0
- minitap/mobile_use/controllers/unified_controller.py +193 -0
- minitap/mobile_use/graph/graph.py +160 -0
- minitap/mobile_use/graph/state.py +115 -0
- minitap/mobile_use/main.py +309 -0
- minitap/mobile_use/sdk/__init__.py +12 -0
- minitap/mobile_use/sdk/agent.py +1294 -0
- minitap/mobile_use/sdk/builders/__init__.py +10 -0
- minitap/mobile_use/sdk/builders/agent_config_builder.py +307 -0
- minitap/mobile_use/sdk/builders/index.py +15 -0
- minitap/mobile_use/sdk/builders/task_request_builder.py +236 -0
- minitap/mobile_use/sdk/constants.py +1 -0
- minitap/mobile_use/sdk/examples/README.md +83 -0
- minitap/mobile_use/sdk/examples/__init__.py +1 -0
- minitap/mobile_use/sdk/examples/app_lock_messaging.py +54 -0
- minitap/mobile_use/sdk/examples/platform_manual_task_example.py +67 -0
- minitap/mobile_use/sdk/examples/platform_minimal_example.py +48 -0
- minitap/mobile_use/sdk/examples/simple_photo_organizer.py +76 -0
- minitap/mobile_use/sdk/examples/smart_notification_assistant.py +225 -0
- minitap/mobile_use/sdk/examples/video_transcription_example.py +117 -0
- minitap/mobile_use/sdk/services/cloud_mobile.py +656 -0
- minitap/mobile_use/sdk/services/platform.py +434 -0
- minitap/mobile_use/sdk/types/__init__.py +51 -0
- minitap/mobile_use/sdk/types/agent.py +84 -0
- minitap/mobile_use/sdk/types/exceptions.py +138 -0
- minitap/mobile_use/sdk/types/platform.py +183 -0
- minitap/mobile_use/sdk/types/task.py +269 -0
- minitap/mobile_use/sdk/utils.py +29 -0
- minitap/mobile_use/services/accessibility.py +100 -0
- minitap/mobile_use/services/llm.py +247 -0
- minitap/mobile_use/services/telemetry.py +421 -0
- minitap/mobile_use/tools/index.py +67 -0
- minitap/mobile_use/tools/mobile/back.py +52 -0
- minitap/mobile_use/tools/mobile/erase_one_char.py +56 -0
- minitap/mobile_use/tools/mobile/focus_and_clear_text.py +317 -0
- minitap/mobile_use/tools/mobile/focus_and_input_text.py +153 -0
- minitap/mobile_use/tools/mobile/launch_app.py +86 -0
- minitap/mobile_use/tools/mobile/long_press_on.py +169 -0
- minitap/mobile_use/tools/mobile/open_link.py +62 -0
- minitap/mobile_use/tools/mobile/press_key.py +83 -0
- minitap/mobile_use/tools/mobile/stop_app.py +62 -0
- minitap/mobile_use/tools/mobile/swipe.py +156 -0
- minitap/mobile_use/tools/mobile/tap.py +154 -0
- minitap/mobile_use/tools/mobile/video_recording.py +177 -0
- minitap/mobile_use/tools/mobile/wait_for_delay.py +81 -0
- minitap/mobile_use/tools/scratchpad.py +147 -0
- minitap/mobile_use/tools/test_utils.py +413 -0
- minitap/mobile_use/tools/tool_wrapper.py +16 -0
- minitap/mobile_use/tools/types.py +35 -0
- minitap/mobile_use/tools/utils.py +336 -0
- minitap/mobile_use/utils/app_launch_utils.py +173 -0
- minitap/mobile_use/utils/cli_helpers.py +37 -0
- minitap/mobile_use/utils/cli_selection.py +143 -0
- minitap/mobile_use/utils/conversations.py +31 -0
- minitap/mobile_use/utils/decorators.py +124 -0
- minitap/mobile_use/utils/errors.py +6 -0
- minitap/mobile_use/utils/file.py +13 -0
- minitap/mobile_use/utils/logger.py +183 -0
- minitap/mobile_use/utils/media.py +186 -0
- minitap/mobile_use/utils/recorder.py +52 -0
- minitap/mobile_use/utils/requests_utils.py +37 -0
- minitap/mobile_use/utils/shell_utils.py +20 -0
- minitap/mobile_use/utils/test_ui_hierarchy.py +178 -0
- minitap/mobile_use/utils/time.py +6 -0
- minitap/mobile_use/utils/ui_hierarchy.py +132 -0
- minitap/mobile_use/utils/video.py +281 -0
- minitap_mobile_use-3.3.0.dist-info/METADATA +329 -0
- minitap_mobile_use-3.3.0.dist-info/RECORD +115 -0
- minitap_mobile_use-3.3.0.dist-info/WHEEL +4 -0
- minitap_mobile_use-3.3.0.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from datetime import UTC, datetime
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
from pydantic import BaseModel, ValidationError
|
|
8
|
+
|
|
9
|
+
from minitap.mobile_use.agents.planner.types import Subgoal, SubgoalStatus
|
|
10
|
+
from minitap.mobile_use.config import deep_merge_llm_config, get_default_llm_config, settings
|
|
11
|
+
from minitap.mobile_use.sdk.types.exceptions import PlatformServiceError
|
|
12
|
+
from minitap.mobile_use.sdk.types.platform import (
|
|
13
|
+
CreateOrphanTaskRunRequest,
|
|
14
|
+
CreateTaskRunRequest,
|
|
15
|
+
LLMProfileResponse,
|
|
16
|
+
MobileUseSubgoal,
|
|
17
|
+
SubgoalState,
|
|
18
|
+
TaskResponse,
|
|
19
|
+
TaskRunPlanResponse,
|
|
20
|
+
TaskRunResponse,
|
|
21
|
+
TaskRunStatus,
|
|
22
|
+
TrajectoryGifUploadResponse,
|
|
23
|
+
UpdateTaskRunStatusRequest,
|
|
24
|
+
UpsertTaskRunAgentThoughtRequest,
|
|
25
|
+
UpsertTaskRunPlanRequest,
|
|
26
|
+
)
|
|
27
|
+
from minitap.mobile_use.sdk.types.task import (
|
|
28
|
+
AgentProfile,
|
|
29
|
+
CloudDevicePlatformTaskRequest,
|
|
30
|
+
ManualTaskConfig,
|
|
31
|
+
PlatformTaskInfo,
|
|
32
|
+
PlatformTaskRequest,
|
|
33
|
+
TaskRequest,
|
|
34
|
+
)
|
|
35
|
+
from minitap.mobile_use.utils.logger import get_logger
|
|
36
|
+
|
|
37
|
+
logger = get_logger(__name__)
|
|
38
|
+
|
|
39
|
+
DEFAULT_PROFILE = "default"
|
|
40
|
+
MAX_GIF_SIZE_BYTES = 300 * 1024 * 1024
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class PlatformService:
|
|
44
|
+
def __init__(self, api_key: str | None = None):
|
|
45
|
+
self._base_url = settings.MINITAP_BASE_URL
|
|
46
|
+
|
|
47
|
+
if api_key:
|
|
48
|
+
self._api_key = api_key
|
|
49
|
+
elif settings.MINITAP_API_KEY:
|
|
50
|
+
self._api_key = settings.MINITAP_API_KEY.get_secret_value()
|
|
51
|
+
else:
|
|
52
|
+
raise PlatformServiceError(
|
|
53
|
+
message="Please provide an API key or set MINITAP_API_KEY environment variable.",
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
self._timeout = httpx.Timeout(timeout=120)
|
|
57
|
+
self._client = httpx.AsyncClient(
|
|
58
|
+
base_url=f"{self._base_url}/api",
|
|
59
|
+
timeout=self._timeout,
|
|
60
|
+
headers={
|
|
61
|
+
"Authorization": f"Bearer {self._api_key}",
|
|
62
|
+
"Content-Type": "application/json",
|
|
63
|
+
},
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
async def create_task_run(
|
|
67
|
+
self,
|
|
68
|
+
request: PlatformTaskRequest,
|
|
69
|
+
locked_app_package: str | None = None,
|
|
70
|
+
enable_video_tools: bool = False,
|
|
71
|
+
) -> PlatformTaskInfo:
|
|
72
|
+
try:
|
|
73
|
+
virtual_mobile_id = None
|
|
74
|
+
if isinstance(request, CloudDevicePlatformTaskRequest):
|
|
75
|
+
virtual_mobile_id = request.virtual_mobile_id
|
|
76
|
+
|
|
77
|
+
if isinstance(request.task, str):
|
|
78
|
+
# Fetch task from platform
|
|
79
|
+
logger.info(f"Getting task: {request.task}")
|
|
80
|
+
response = await self._client.get(url=f"v1/tasks/{request.task}")
|
|
81
|
+
response.raise_for_status()
|
|
82
|
+
task_data = response.json()
|
|
83
|
+
task = TaskResponse(**task_data)
|
|
84
|
+
|
|
85
|
+
profile, agent_profile = await self.get_profile(
|
|
86
|
+
profile_name=request.profile or DEFAULT_PROFILE,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
task_request = TaskRequest(
|
|
90
|
+
# Remote configuration
|
|
91
|
+
max_steps=task.options.max_steps,
|
|
92
|
+
goal=task.input_prompt,
|
|
93
|
+
output_description=task.output_description,
|
|
94
|
+
enable_remote_tracing=task.options.enable_tracing,
|
|
95
|
+
profile=profile.name,
|
|
96
|
+
# Local configuration
|
|
97
|
+
record_trace=request.record_trace,
|
|
98
|
+
trace_path=request.trace_path,
|
|
99
|
+
llm_output_path=request.llm_output_path,
|
|
100
|
+
thoughts_output_path=request.thoughts_output_path,
|
|
101
|
+
task_name=task.name,
|
|
102
|
+
locked_app_package=locked_app_package,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
if task.options.enable_video_tools and not enable_video_tools:
|
|
106
|
+
raise PlatformServiceError(
|
|
107
|
+
message=(
|
|
108
|
+
"You're trying to run a task requiring video recording tools "
|
|
109
|
+
"on an agent where they are disabled. "
|
|
110
|
+
"Use .with_video_recording_tools() when building the agent."
|
|
111
|
+
)
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
task_run = await self._create_task_run(
|
|
115
|
+
task=task,
|
|
116
|
+
profile=profile,
|
|
117
|
+
virtual_mobile_id=virtual_mobile_id,
|
|
118
|
+
locked_app_package=locked_app_package,
|
|
119
|
+
execution_origin=request.execution_origin,
|
|
120
|
+
enable_video_tools=enable_video_tools,
|
|
121
|
+
)
|
|
122
|
+
else:
|
|
123
|
+
# Create task manually from ManualTaskConfig
|
|
124
|
+
logger.info(f"Creating manual task with goal: {request.task.goal}")
|
|
125
|
+
|
|
126
|
+
profile, agent_profile = await self.get_profile(
|
|
127
|
+
profile_name=request.profile or DEFAULT_PROFILE,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
task_request = TaskRequest(
|
|
131
|
+
# Manual configuration
|
|
132
|
+
max_steps=request.max_steps,
|
|
133
|
+
goal=request.task.goal,
|
|
134
|
+
output_description=request.task.output_description,
|
|
135
|
+
enable_remote_tracing=True,
|
|
136
|
+
profile=profile.name,
|
|
137
|
+
# Local configuration
|
|
138
|
+
record_trace=request.record_trace,
|
|
139
|
+
trace_path=request.trace_path,
|
|
140
|
+
llm_output_path=request.llm_output_path,
|
|
141
|
+
thoughts_output_path=request.thoughts_output_path,
|
|
142
|
+
task_name=request.task.task_name,
|
|
143
|
+
locked_app_package=locked_app_package,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
task_run = await self._create_manual_task_run(
|
|
147
|
+
manual_config=request.task,
|
|
148
|
+
profile=profile,
|
|
149
|
+
virtual_mobile_id=virtual_mobile_id,
|
|
150
|
+
locked_app_package=locked_app_package,
|
|
151
|
+
execution_origin=request.execution_origin,
|
|
152
|
+
max_steps=request.max_steps,
|
|
153
|
+
enable_video_tools=enable_video_tools,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
return PlatformTaskInfo(
|
|
157
|
+
task_request=task_request,
|
|
158
|
+
llm_profile=agent_profile,
|
|
159
|
+
task_run=task_run,
|
|
160
|
+
)
|
|
161
|
+
except httpx.HTTPStatusError as e:
|
|
162
|
+
raise PlatformServiceError(message=f"Failed to get task: {e}")
|
|
163
|
+
|
|
164
|
+
async def update_task_run_status(
|
|
165
|
+
self,
|
|
166
|
+
task_run_id: str,
|
|
167
|
+
status: TaskRunStatus,
|
|
168
|
+
message: str | None = None,
|
|
169
|
+
output: Any | None = None,
|
|
170
|
+
) -> None:
|
|
171
|
+
try:
|
|
172
|
+
logger.info(f"Updating task run status for task run: {task_run_id}")
|
|
173
|
+
|
|
174
|
+
sanitized_output: str | None = None
|
|
175
|
+
if isinstance(output, dict):
|
|
176
|
+
sanitized_output = json.dumps(output)
|
|
177
|
+
elif isinstance(output, list):
|
|
178
|
+
sanitized_output = json.dumps(output)
|
|
179
|
+
elif isinstance(output, BaseModel):
|
|
180
|
+
sanitized_output = output.model_dump_json()
|
|
181
|
+
elif isinstance(output, str):
|
|
182
|
+
sanitized_output = output
|
|
183
|
+
else:
|
|
184
|
+
sanitized_output = str(output)
|
|
185
|
+
|
|
186
|
+
update = UpdateTaskRunStatusRequest(
|
|
187
|
+
status=status,
|
|
188
|
+
message=message,
|
|
189
|
+
output=sanitized_output,
|
|
190
|
+
)
|
|
191
|
+
response = await self._client.patch(
|
|
192
|
+
url=f"v1/task-runs/{task_run_id}/status",
|
|
193
|
+
json=update.model_dump(),
|
|
194
|
+
)
|
|
195
|
+
response.raise_for_status()
|
|
196
|
+
except httpx.HTTPStatusError as e:
|
|
197
|
+
raise PlatformServiceError(message=f"Failed to update task run status: {e}")
|
|
198
|
+
|
|
199
|
+
async def upsert_task_run_plan(
|
|
200
|
+
self,
|
|
201
|
+
task_run_id: str,
|
|
202
|
+
started_at: datetime,
|
|
203
|
+
plan: list[Subgoal],
|
|
204
|
+
ended_at: datetime | None = None,
|
|
205
|
+
plan_id: str | None = None,
|
|
206
|
+
) -> TaskRunPlanResponse:
|
|
207
|
+
try:
|
|
208
|
+
logger.info(f"Upserting task run plan for task run: {task_run_id}")
|
|
209
|
+
ended, subgoals = self._to_api_subgoals(plan)
|
|
210
|
+
if not ended_at and ended:
|
|
211
|
+
ended_at = datetime.now(UTC)
|
|
212
|
+
update = UpsertTaskRunPlanRequest(
|
|
213
|
+
started_at=started_at,
|
|
214
|
+
subgoals=subgoals,
|
|
215
|
+
ended_at=ended_at,
|
|
216
|
+
)
|
|
217
|
+
if plan_id:
|
|
218
|
+
response = await self._client.put(
|
|
219
|
+
url=f"v1/task-runs/{task_run_id}/plans/{plan_id}",
|
|
220
|
+
json=update.model_dump(),
|
|
221
|
+
)
|
|
222
|
+
else:
|
|
223
|
+
response = await self._client.post(
|
|
224
|
+
url=f"v1/task-runs/{task_run_id}/plans",
|
|
225
|
+
json=update.model_dump(),
|
|
226
|
+
)
|
|
227
|
+
response.raise_for_status()
|
|
228
|
+
return TaskRunPlanResponse(**response.json())
|
|
229
|
+
|
|
230
|
+
except ValidationError as e:
|
|
231
|
+
raise PlatformServiceError(message=f"API response validation error: {e}")
|
|
232
|
+
except httpx.HTTPStatusError as e:
|
|
233
|
+
raise PlatformServiceError(message=f"Failed to upsert task run plan: {e}")
|
|
234
|
+
|
|
235
|
+
async def add_agent_thought(self, task_run_id: str, agent: str, thought: str) -> None:
|
|
236
|
+
try:
|
|
237
|
+
logger.info(f"Adding agent thought for task run: {task_run_id}")
|
|
238
|
+
update = UpsertTaskRunAgentThoughtRequest(
|
|
239
|
+
agent=agent,
|
|
240
|
+
content=thought,
|
|
241
|
+
timestamp=datetime.now(UTC),
|
|
242
|
+
)
|
|
243
|
+
response = await self._client.post(
|
|
244
|
+
url=f"v1/task-runs/{task_run_id}/agent-thoughts",
|
|
245
|
+
json=update.model_dump(),
|
|
246
|
+
)
|
|
247
|
+
response.raise_for_status()
|
|
248
|
+
except httpx.HTTPStatusError as e:
|
|
249
|
+
raise PlatformServiceError(message=f"Failed to add agent thought: {e}")
|
|
250
|
+
|
|
251
|
+
async def upload_trace_gif(self, task_run_id: str, gif_path: Path) -> str | None:
|
|
252
|
+
"""
|
|
253
|
+
Upload a trajectory GIF to the platform using streaming to avoid RAM overload.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
task_run_id: ID of the task run to upload the GIF for
|
|
257
|
+
gif_path: Path to the GIF file to upload
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
Task run ID on success, None on failure
|
|
261
|
+
|
|
262
|
+
Raises:
|
|
263
|
+
PlatformServiceError: If there's a critical error during upload
|
|
264
|
+
"""
|
|
265
|
+
try:
|
|
266
|
+
if not gif_path.exists():
|
|
267
|
+
logger.warning(f"GIF file not found at {gif_path}")
|
|
268
|
+
return None
|
|
269
|
+
|
|
270
|
+
file_size = gif_path.stat().st_size
|
|
271
|
+
if file_size > MAX_GIF_SIZE_BYTES:
|
|
272
|
+
logger.warning(
|
|
273
|
+
f"GIF file size ({file_size / (1024 * 1024):.2f}MB) exceeds "
|
|
274
|
+
f"maximum allowed size ({MAX_GIF_SIZE_BYTES / (1024 * 1024):.0f}MB)"
|
|
275
|
+
)
|
|
276
|
+
return None
|
|
277
|
+
|
|
278
|
+
logger.info(
|
|
279
|
+
f"Getting signed upload URL for task run: {task_run_id} "
|
|
280
|
+
f"(file size: {file_size / (1024 * 1024):.2f}MB)"
|
|
281
|
+
)
|
|
282
|
+
response = await self._client.post(
|
|
283
|
+
url=f"storage/trajectory-gif-upload/{task_run_id}",
|
|
284
|
+
)
|
|
285
|
+
response.raise_for_status()
|
|
286
|
+
gif_upload_data = TrajectoryGifUploadResponse(**response.json())
|
|
287
|
+
|
|
288
|
+
logger.info(f"Streaming GIF upload to signed URL for task run: {task_run_id}")
|
|
289
|
+
|
|
290
|
+
async def file_iterator():
|
|
291
|
+
chunk_size = 1024 * 1024 # 1MB chunks
|
|
292
|
+
with open(gif_path, "rb") as gif_file:
|
|
293
|
+
while chunk := gif_file.read(chunk_size):
|
|
294
|
+
yield chunk
|
|
295
|
+
|
|
296
|
+
async with httpx.AsyncClient() as upload_client:
|
|
297
|
+
async with upload_client.stream(
|
|
298
|
+
method="PUT",
|
|
299
|
+
url=gif_upload_data.signed_url,
|
|
300
|
+
content=file_iterator(),
|
|
301
|
+
headers={
|
|
302
|
+
"Content-Type": "image/gif",
|
|
303
|
+
},
|
|
304
|
+
timeout=httpx.Timeout(timeout=60.0),
|
|
305
|
+
) as upload_response:
|
|
306
|
+
await upload_response.aread()
|
|
307
|
+
upload_response.raise_for_status()
|
|
308
|
+
|
|
309
|
+
logger.info(f"Successfully uploaded trajectory GIF for task run: {task_run_id}")
|
|
310
|
+
return task_run_id
|
|
311
|
+
|
|
312
|
+
except Exception as e:
|
|
313
|
+
logger.warning(f"Failed to upload trace GIF: {e}")
|
|
314
|
+
try:
|
|
315
|
+
await self._client.delete(url=f"v1/task-runs/{task_run_id}/gif")
|
|
316
|
+
except Exception as clear_error:
|
|
317
|
+
logger.warning(f"Failed to delete GIF after upload failure: {clear_error}")
|
|
318
|
+
return None
|
|
319
|
+
|
|
320
|
+
def _to_api_subgoals(self, subgoals: list[Subgoal]) -> tuple[bool, list[MobileUseSubgoal]]:
|
|
321
|
+
"""
|
|
322
|
+
Returns a tuple of (plan_ended, subgoal_models)
|
|
323
|
+
"""
|
|
324
|
+
subgoal_models: list[MobileUseSubgoal] = []
|
|
325
|
+
plan_ended = True
|
|
326
|
+
for subgoal in subgoals:
|
|
327
|
+
if subgoal.status != SubgoalStatus.SUCCESS:
|
|
328
|
+
plan_ended = False
|
|
329
|
+
subgoal_models.append(self._to_api_subgoal(subgoal))
|
|
330
|
+
return plan_ended, subgoal_models
|
|
331
|
+
|
|
332
|
+
def _to_api_subgoal(self, subgoal: Subgoal) -> MobileUseSubgoal:
|
|
333
|
+
state: SubgoalState = "pending"
|
|
334
|
+
match subgoal.status:
|
|
335
|
+
case SubgoalStatus.SUCCESS:
|
|
336
|
+
state = "completed"
|
|
337
|
+
case SubgoalStatus.FAILURE:
|
|
338
|
+
state = "failed"
|
|
339
|
+
case SubgoalStatus.PENDING:
|
|
340
|
+
state = "started"
|
|
341
|
+
case SubgoalStatus.NOT_STARTED:
|
|
342
|
+
state = "pending"
|
|
343
|
+
return MobileUseSubgoal(
|
|
344
|
+
name=subgoal.description,
|
|
345
|
+
state=state,
|
|
346
|
+
started_at=subgoal.started_at,
|
|
347
|
+
ended_at=subgoal.ended_at,
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
async def _create_task_run(
|
|
351
|
+
self,
|
|
352
|
+
task: TaskResponse,
|
|
353
|
+
profile: LLMProfileResponse,
|
|
354
|
+
virtual_mobile_id: str | None = None,
|
|
355
|
+
locked_app_package: str | None = None,
|
|
356
|
+
execution_origin: str | None = None,
|
|
357
|
+
enable_video_tools: bool = False,
|
|
358
|
+
) -> TaskRunResponse:
|
|
359
|
+
try:
|
|
360
|
+
logger.info(f"Creating task run for task: {task.name}")
|
|
361
|
+
task_run = CreateTaskRunRequest(
|
|
362
|
+
task_id=task.id,
|
|
363
|
+
llm_profile_id=profile.id,
|
|
364
|
+
virtual_mobile_id=virtual_mobile_id,
|
|
365
|
+
locked_app_package=locked_app_package,
|
|
366
|
+
execution_origin=execution_origin,
|
|
367
|
+
enable_video_tools=enable_video_tools,
|
|
368
|
+
)
|
|
369
|
+
response = await self._client.post(url="v1/task-runs", json=task_run.model_dump())
|
|
370
|
+
response.raise_for_status()
|
|
371
|
+
task_run_data = response.json()
|
|
372
|
+
return TaskRunResponse(**task_run_data)
|
|
373
|
+
except ValidationError as e:
|
|
374
|
+
raise PlatformServiceError(message=f"API response validation error: {e}")
|
|
375
|
+
except httpx.HTTPStatusError as e:
|
|
376
|
+
raise PlatformServiceError(message=f"Failed to create task run: {e}")
|
|
377
|
+
|
|
378
|
+
async def _create_manual_task_run(
|
|
379
|
+
self,
|
|
380
|
+
manual_config: ManualTaskConfig,
|
|
381
|
+
profile: LLMProfileResponse,
|
|
382
|
+
virtual_mobile_id: str | None = None,
|
|
383
|
+
locked_app_package: str | None = None,
|
|
384
|
+
execution_origin: str | None = None,
|
|
385
|
+
max_steps: int | None = None,
|
|
386
|
+
enable_video_tools: bool = False,
|
|
387
|
+
) -> TaskRunResponse:
|
|
388
|
+
"""
|
|
389
|
+
Create an orphan task run from a manual task configuration.
|
|
390
|
+
This creates a task run without a pre-existing task using the /orphan endpoint.
|
|
391
|
+
"""
|
|
392
|
+
try:
|
|
393
|
+
logger.info(f"Creating orphan task run with goal: {manual_config.goal}")
|
|
394
|
+
task_run = CreateOrphanTaskRunRequest(
|
|
395
|
+
input_prompt=manual_config.goal,
|
|
396
|
+
output_description=manual_config.output_description,
|
|
397
|
+
llm_profile_id=profile.id,
|
|
398
|
+
virtual_mobile_id=virtual_mobile_id,
|
|
399
|
+
locked_app_package=locked_app_package,
|
|
400
|
+
execution_origin=execution_origin,
|
|
401
|
+
max_steps=max_steps,
|
|
402
|
+
enable_video_tools=enable_video_tools,
|
|
403
|
+
)
|
|
404
|
+
response = await self._client.post(
|
|
405
|
+
url="v1/task-runs/orphan",
|
|
406
|
+
json=task_run.model_dump(),
|
|
407
|
+
)
|
|
408
|
+
response.raise_for_status()
|
|
409
|
+
task_run_data = response.json()
|
|
410
|
+
return TaskRunResponse(**task_run_data)
|
|
411
|
+
|
|
412
|
+
except ValidationError as e:
|
|
413
|
+
raise PlatformServiceError(message=f"API response validation error: {e}")
|
|
414
|
+
except httpx.HTTPStatusError as e:
|
|
415
|
+
raise PlatformServiceError(message=f"Failed to create orphan task run: {e}")
|
|
416
|
+
|
|
417
|
+
async def get_profile(self, profile_name: str) -> tuple[LLMProfileResponse, AgentProfile]:
|
|
418
|
+
try:
|
|
419
|
+
logger.info(f"Getting agent profile: {profile_name}")
|
|
420
|
+
response = await self._client.get(url=f"v1/llm-profiles/{profile_name}")
|
|
421
|
+
response.raise_for_status()
|
|
422
|
+
profile_data = response.json()
|
|
423
|
+
profile = LLMProfileResponse(**profile_data)
|
|
424
|
+
default_config = get_default_llm_config()
|
|
425
|
+
merged_config = deep_merge_llm_config(default_config, profile.llms)
|
|
426
|
+
agent_profile = AgentProfile(
|
|
427
|
+
name=profile.name,
|
|
428
|
+
llm_config=merged_config,
|
|
429
|
+
)
|
|
430
|
+
return profile, agent_profile
|
|
431
|
+
except ValidationError as e:
|
|
432
|
+
raise PlatformServiceError(message=f"API response validation error: {e}")
|
|
433
|
+
except httpx.HTTPStatusError as e:
|
|
434
|
+
raise PlatformServiceError(message=f"Failed to get agent profile: {e}")
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Type definitions for the mobile-use SDK."""
|
|
2
|
+
|
|
3
|
+
from minitap.mobile_use.sdk.types.agent import (
|
|
4
|
+
AgentConfig,
|
|
5
|
+
ApiBaseUrl,
|
|
6
|
+
DevicePlatform,
|
|
7
|
+
ServerConfig,
|
|
8
|
+
)
|
|
9
|
+
from minitap.mobile_use.sdk.types.exceptions import (
|
|
10
|
+
AgentError,
|
|
11
|
+
AgentNotInitializedError,
|
|
12
|
+
AgentProfileNotFoundError,
|
|
13
|
+
AgentTaskRequestError,
|
|
14
|
+
DeviceError,
|
|
15
|
+
DeviceNotFoundError,
|
|
16
|
+
MobileUseError,
|
|
17
|
+
ServerError,
|
|
18
|
+
ServerStartupError,
|
|
19
|
+
)
|
|
20
|
+
from minitap.mobile_use.sdk.types.task import (
|
|
21
|
+
AgentProfile,
|
|
22
|
+
ManualTaskConfig,
|
|
23
|
+
PlatformTaskRequest,
|
|
24
|
+
Task,
|
|
25
|
+
TaskRequest,
|
|
26
|
+
TaskRequestCommon,
|
|
27
|
+
TaskResult,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"ApiBaseUrl",
|
|
32
|
+
"AgentConfig",
|
|
33
|
+
"DevicePlatform",
|
|
34
|
+
"AgentProfile",
|
|
35
|
+
"ServerConfig",
|
|
36
|
+
"TaskRequest",
|
|
37
|
+
"ManualTaskConfig",
|
|
38
|
+
"PlatformTaskRequest",
|
|
39
|
+
"TaskResult",
|
|
40
|
+
"TaskRequestCommon",
|
|
41
|
+
"Task",
|
|
42
|
+
"AgentProfileNotFoundError",
|
|
43
|
+
"AgentTaskRequestError",
|
|
44
|
+
"DeviceNotFoundError",
|
|
45
|
+
"ServerStartupError",
|
|
46
|
+
"AgentError",
|
|
47
|
+
"AgentNotInitializedError",
|
|
48
|
+
"DeviceError",
|
|
49
|
+
"MobileUseError",
|
|
50
|
+
"ServerError",
|
|
51
|
+
]
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
from typing import Literal
|
|
2
|
+
from urllib.parse import urlparse
|
|
3
|
+
|
|
4
|
+
from langchain_core.callbacks.base import Callbacks
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
|
|
7
|
+
from minitap.mobile_use.clients.ios_client_config import BrowserStackClientConfig, IosClientConfig
|
|
8
|
+
from minitap.mobile_use.context import DevicePlatform
|
|
9
|
+
from minitap.mobile_use.sdk.types.task import AgentProfile, TaskRequestCommon
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ApiBaseUrl(BaseModel):
|
|
13
|
+
"""
|
|
14
|
+
Defines an API base URL.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
scheme: Literal["http", "https"]
|
|
18
|
+
host: str
|
|
19
|
+
port: int | None = None
|
|
20
|
+
|
|
21
|
+
def __eq__(self, other):
|
|
22
|
+
if not isinstance(other, ApiBaseUrl):
|
|
23
|
+
return False
|
|
24
|
+
return self.to_url() == other.to_url()
|
|
25
|
+
|
|
26
|
+
def to_url(self):
|
|
27
|
+
return (
|
|
28
|
+
f"{self.scheme}://{self.host}:{self.port}"
|
|
29
|
+
if self.port is not None
|
|
30
|
+
else f"{self.scheme}://{self.host}"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def from_url(cls, url: str) -> "ApiBaseUrl":
|
|
35
|
+
parsed_url = urlparse(url)
|
|
36
|
+
if parsed_url.scheme not in ["http", "https"]:
|
|
37
|
+
raise ValueError(f"Invalid scheme: {parsed_url.scheme}")
|
|
38
|
+
if parsed_url.hostname is None:
|
|
39
|
+
raise ValueError("Invalid hostname")
|
|
40
|
+
return cls(
|
|
41
|
+
scheme=parsed_url.scheme, # type: ignore
|
|
42
|
+
host=parsed_url.hostname,
|
|
43
|
+
port=parsed_url.port,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class ServerConfig(BaseModel):
|
|
48
|
+
"""
|
|
49
|
+
Configuration for the required servers.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
adb_host: str
|
|
53
|
+
adb_port: int
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class AgentConfig(BaseModel):
|
|
57
|
+
"""
|
|
58
|
+
Mobile-use agent configuration.
|
|
59
|
+
|
|
60
|
+
Attributes:
|
|
61
|
+
agent_profiles: Map an agent profile name to its configuration.
|
|
62
|
+
task_config_defaults: Default task request configuration.
|
|
63
|
+
default_profile: default profile to use for tasks
|
|
64
|
+
device_id: Specific device to target (if None, first available is used).
|
|
65
|
+
device_platform: Platform of the device to target.
|
|
66
|
+
servers: Custom server configurations.
|
|
67
|
+
cloud_mobile_id_or_ref: ID or reference name of cloud mobile (virtual mobile)
|
|
68
|
+
to use for remote execution.
|
|
69
|
+
video_recording_enabled: Whether video recording tools are enabled.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
agent_profiles: dict[str, AgentProfile]
|
|
73
|
+
task_request_defaults: TaskRequestCommon
|
|
74
|
+
default_profile: AgentProfile
|
|
75
|
+
device_id: str | None = None
|
|
76
|
+
device_platform: DevicePlatform | None = None
|
|
77
|
+
servers: ServerConfig
|
|
78
|
+
graph_config_callbacks: Callbacks = None
|
|
79
|
+
cloud_mobile_id_or_ref: str | None = None
|
|
80
|
+
ios_client_config: IosClientConfig | None = None
|
|
81
|
+
browserstack_config: BrowserStackClientConfig | None = None
|
|
82
|
+
video_recording_enabled: bool = False
|
|
83
|
+
|
|
84
|
+
model_config = {"arbitrary_types_allowed": True}
|