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,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}