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,413 @@
1
+ import json
2
+ import os
3
+ from pathlib import Path
4
+ from typing import Annotated, Any, Literal
5
+
6
+ import google.auth
7
+ from dotenv import load_dotenv
8
+ from google.auth.exceptions import DefaultCredentialsError
9
+ from pydantic import BaseModel, Field, SecretStr, ValidationError, model_validator
10
+ from pydantic_settings import BaseSettings
11
+
12
+ from minitap.mobile_use.utils.file import load_jsonc
13
+ from minitap.mobile_use.utils.logger import get_logger
14
+
15
+ ### Environment Variables
16
+
17
+ load_dotenv(verbose=True)
18
+ logger = get_logger(__name__)
19
+
20
+
21
+ class Settings(BaseSettings):
22
+ OPENAI_API_KEY: SecretStr | None = None
23
+ GOOGLE_API_KEY: SecretStr | None = None
24
+ XAI_API_KEY: SecretStr | None = None
25
+ OPEN_ROUTER_API_KEY: SecretStr | None = None
26
+ MINITAP_API_KEY: SecretStr | None = None
27
+
28
+ OPENAI_BASE_URL: str | None = None
29
+ MINITAP_BASE_URL: str = "https://platform.minitap.ai"
30
+
31
+ ADB_HOST: str | None = None
32
+ ADB_PORT: int | None = None
33
+
34
+ MOBILE_USE_TELEMETRY_ENABLED: bool | None = None
35
+
36
+ model_config = {"env_file": ".env", "extra": "ignore"}
37
+
38
+
39
+ settings = Settings()
40
+
41
+
42
+ def prepare_output_files() -> tuple[str | None, str | None]:
43
+ events_output_path = os.getenv("EVENTS_OUTPUT_PATH") or None
44
+ results_output_path = os.getenv("RESULTS_OUTPUT_PATH") or None
45
+
46
+ def validate_and_prepare_file(file_path: str) -> str | None:
47
+ if not file_path:
48
+ return None
49
+
50
+ path_obj = Path(file_path)
51
+
52
+ if path_obj.exists() and path_obj.is_dir():
53
+ logger.error(f"Error: Path '{file_path}' points to an existing directory, not a file.")
54
+ return None
55
+
56
+ if not path_obj.suffix or file_path.endswith(("/", "\\")):
57
+ logger.error(f"Error: Path '{file_path}' appears to be a directory path, not a file.")
58
+ return None
59
+
60
+ try:
61
+ path_obj.parent.mkdir(parents=True, exist_ok=True)
62
+ path_obj.touch(exist_ok=True)
63
+ return file_path
64
+ except OSError as e:
65
+ logger.error(f"Error creating file '{file_path}': {e}")
66
+ return None
67
+
68
+ validated_events_path = (
69
+ validate_and_prepare_file(events_output_path) if events_output_path else None
70
+ )
71
+ validated_results_path = (
72
+ validate_and_prepare_file(results_output_path) if results_output_path else None
73
+ )
74
+
75
+ return validated_events_path, validated_results_path
76
+
77
+
78
+ def record_events(output_path: Path | None, events: list[str] | BaseModel | Any):
79
+ if not output_path:
80
+ return
81
+
82
+ if isinstance(events, str):
83
+ events_content = events
84
+ elif isinstance(events, BaseModel):
85
+ events_content = events.model_dump_json(indent=2)
86
+ else:
87
+ events_content = json.dumps(events, indent=2)
88
+
89
+ with open(output_path, "w", encoding="utf-8") as f:
90
+ f.write(events_content)
91
+
92
+
93
+ ### LLM Configuration
94
+
95
+ LLMProvider = Literal["openai", "google", "openrouter", "xai", "vertexai", "minitap"]
96
+ LLMUtilsNode = Literal["outputter", "hopper", "video_analyzer"]
97
+ LLMUtilsNodeWithFallback = LLMUtilsNode
98
+ AgentNode = Literal[
99
+ "planner",
100
+ "orchestrator",
101
+ "contextor",
102
+ "cortex",
103
+ "executor",
104
+ ]
105
+ AgentNodeWithFallback = AgentNode
106
+
107
+ ROOT_DIR = Path(__file__).parent.parent.parent
108
+ DEFAULT_LLM_CONFIG_FILENAME = "llm-config.defaults.jsonc"
109
+ OVERRIDE_LLM_CONFIG_FILENAME = "llm-config.override.jsonc"
110
+
111
+
112
+ def validate_vertex_ai_credentials():
113
+ try:
114
+ _, project = google.auth.default()
115
+ if not project:
116
+ raise Exception("VertexAI requires a Google Cloud project to be set.")
117
+ except DefaultCredentialsError as e:
118
+ raise Exception(
119
+ f"VertexAI requires valid Google Application Default Credentials (ADC): {e}"
120
+ )
121
+
122
+
123
+ class LLM(BaseModel):
124
+ provider: LLMProvider
125
+ model: str
126
+
127
+ def validate_provider(self, name: str):
128
+ match self.provider:
129
+ case "openai":
130
+ if not settings.OPENAI_API_KEY:
131
+ raise Exception(f"{name} requires OPENAI_API_KEY in .env")
132
+ case "google":
133
+ if not settings.GOOGLE_API_KEY:
134
+ raise Exception(f"{name} requires GOOGLE_API_KEY in .env")
135
+ case "vertexai":
136
+ validate_vertex_ai_credentials()
137
+ case "openrouter":
138
+ if not settings.OPEN_ROUTER_API_KEY:
139
+ raise Exception(f"{name} requires OPEN_ROUTER_API_KEY in .env")
140
+ case "xai":
141
+ if not settings.XAI_API_KEY:
142
+ raise Exception(f"{name} requires XAI_API_KEY in .env")
143
+ case "minitap":
144
+ if not settings.MINITAP_API_KEY:
145
+ raise Exception(f"{name} requires MINITAP_API_KEY in .env")
146
+
147
+ def __str__(self):
148
+ return f"{self.provider}/{self.model}"
149
+
150
+
151
+ class LLMWithFallback(LLM):
152
+ fallback: LLM
153
+
154
+ def __str__(self):
155
+ return f"{self.provider}/{self.model} (fallback: {self.fallback})"
156
+
157
+
158
+ class LLMConfigUtils(BaseModel):
159
+ outputter: LLMWithFallback
160
+ hopper: LLMWithFallback
161
+ video_analyzer: LLMWithFallback | None = None
162
+
163
+
164
+ class LLMConfig(BaseModel):
165
+ planner: LLMWithFallback
166
+ orchestrator: LLMWithFallback
167
+ contextor: LLMWithFallback
168
+ cortex: LLMWithFallback
169
+ executor: LLMWithFallback
170
+ utils: LLMConfigUtils
171
+
172
+ def validate_providers(self):
173
+ self.planner.validate_provider("Planner")
174
+ self.orchestrator.validate_provider("Orchestrator")
175
+ self.contextor.validate_provider("Contextor")
176
+ self.cortex.validate_provider("Cortex")
177
+ self.executor.validate_provider("Executor")
178
+ self.utils.outputter.validate_provider("Outputter")
179
+ self.utils.hopper.validate_provider("Hopper")
180
+ if self.utils.video_analyzer:
181
+ self.utils.video_analyzer.validate_provider("VideoAnalyzer")
182
+
183
+ def __str__(self):
184
+ return f"""
185
+ 📃 Planner: {self.planner}
186
+ 🎯 Orchestrator: {self.orchestrator}
187
+ 🔍 Contextor: {self.contextor}
188
+ 🧠 Cortex: {self.cortex}
189
+ 🛠️ Executor: {self.executor}
190
+ 🧩 Utils:
191
+ 🔽 Hopper: {self.utils.hopper}
192
+ 📝 Outputter: {self.utils.outputter}
193
+ 🎬 Video Analyzer: {self.utils.video_analyzer or "Not configured"}
194
+ """
195
+
196
+ def get_agent(self, item: AgentNode) -> LLMWithFallback:
197
+ return getattr(self, item)
198
+
199
+ def get_utils(self, item: LLMUtilsNode) -> LLMWithFallback:
200
+ value = getattr(self.utils, item)
201
+ if value is None:
202
+ raise ValueError(
203
+ f"Utils '{item}' is not configured. "
204
+ f"Please add it to your LLM config or enable it via AgentConfigBuilder."
205
+ )
206
+ return value
207
+
208
+
209
+ def get_default_llm_config() -> LLMConfig:
210
+ try:
211
+ if not os.path.exists(ROOT_DIR / DEFAULT_LLM_CONFIG_FILENAME):
212
+ raise Exception("Default llm config not found")
213
+ with open(ROOT_DIR / DEFAULT_LLM_CONFIG_FILENAME) as f:
214
+ default_config_dict = load_jsonc(f)
215
+ return LLMConfig.model_validate(default_config_dict["default"])
216
+ except Exception as e:
217
+ logger.error(f"Failed to load default llm config: {e}. Falling back to hardcoded config")
218
+ return LLMConfig(
219
+ planner=LLMWithFallback(
220
+ provider="openai",
221
+ model="gpt-5-nano",
222
+ fallback=LLM(provider="openai", model="gpt-5-mini"),
223
+ ),
224
+ orchestrator=LLMWithFallback(
225
+ provider="openai",
226
+ model="gpt-5-nano",
227
+ fallback=LLM(provider="openai", model="gpt-5-mini"),
228
+ ),
229
+ contextor=LLMWithFallback(
230
+ provider="openai",
231
+ model="gpt-5-nano",
232
+ fallback=LLM(provider="openai", model="gpt-5-mini"),
233
+ ),
234
+ cortex=LLMWithFallback(
235
+ provider="openai",
236
+ model="gpt-5",
237
+ fallback=LLM(provider="openai", model="o4-mini"),
238
+ ),
239
+ executor=LLMWithFallback(
240
+ provider="openai",
241
+ model="gpt-5-nano",
242
+ fallback=LLM(provider="openai", model="gpt-5-mini"),
243
+ ),
244
+ utils=LLMConfigUtils(
245
+ outputter=LLMWithFallback(
246
+ provider="openai",
247
+ model="gpt-5-nano",
248
+ fallback=LLM(provider="openai", model="gpt-5-mini"),
249
+ ),
250
+ hopper=LLMWithFallback(
251
+ provider="openai",
252
+ model="gpt-5-nano",
253
+ fallback=LLM(provider="openai", model="gpt-5-mini"),
254
+ ),
255
+ ),
256
+ )
257
+
258
+
259
+ def get_default_minitap_llm_config(validate: bool = True) -> LLMConfig | None:
260
+ """
261
+ Returns a default LLM config using the Minitap provider.
262
+ Only returns a config if MINITAP_API_KEY is available.
263
+
264
+ Returns:
265
+ LLMConfig with minitap provider if API key is available, None otherwise
266
+ """
267
+ if validate and not settings.MINITAP_API_KEY:
268
+ return None
269
+
270
+ return LLMConfig(
271
+ planner=LLMWithFallback(
272
+ provider="minitap",
273
+ model="meta-llama/llama-4-scout",
274
+ fallback=LLM(provider="minitap", model="meta-llama/llama-4-maverick"),
275
+ ),
276
+ orchestrator=LLMWithFallback(
277
+ provider="minitap",
278
+ model="openai/gpt-oss-120b",
279
+ fallback=LLM(provider="minitap", model="meta-llama/llama-4-maverick"),
280
+ ),
281
+ contextor=LLMWithFallback(
282
+ provider="minitap",
283
+ model="meta-llama/llama-3.1-8b-instruct",
284
+ fallback=LLM(provider="minitap", model="meta-llama/llama-3.3-70b-instruct"),
285
+ ),
286
+ cortex=LLMWithFallback(
287
+ provider="minitap",
288
+ model="google/gemini-2.5-pro",
289
+ fallback=LLM(provider="minitap", model="openai/gpt-5"),
290
+ ),
291
+ executor=LLMWithFallback(
292
+ provider="minitap",
293
+ model="meta-llama/llama-3.3-70b-instruct",
294
+ fallback=LLM(provider="minitap", model="openai/gpt-5-mini"),
295
+ ),
296
+ utils=LLMConfigUtils(
297
+ outputter=LLMWithFallback(
298
+ provider="minitap",
299
+ model="openai/gpt-5-nano",
300
+ fallback=LLM(provider="minitap", model="openai/gpt-5-mini"),
301
+ ),
302
+ hopper=LLMWithFallback(
303
+ provider="minitap",
304
+ model="openai/gpt-5-nano",
305
+ fallback=LLM(provider="minitap", model="openai/gpt-5-mini"),
306
+ ),
307
+ ),
308
+ )
309
+
310
+
311
+ def deep_merge_llm_config(default: LLMConfig, override: dict) -> LLMConfig:
312
+ def _deep_merge_dict(base: dict, extra: dict, path: str = ""):
313
+ for key, value in extra.items():
314
+ current_path = f"{path}.{key}" if path else key
315
+
316
+ if key not in base:
317
+ logger.warning(
318
+ f"Unsupported config key '{current_path}' found in override config. "
319
+ f"Ignoring this key."
320
+ )
321
+ continue
322
+
323
+ if isinstance(value, dict) and isinstance(base[key], dict):
324
+ _deep_merge_dict(base[key], value, current_path)
325
+ else:
326
+ base[key] = value
327
+
328
+ merged_dict = default.model_dump()
329
+ _deep_merge_dict(merged_dict, override)
330
+ return LLMConfig.model_validate(merged_dict)
331
+
332
+
333
+ def parse_llm_config() -> LLMConfig:
334
+ if not os.path.exists(ROOT_DIR / DEFAULT_LLM_CONFIG_FILENAME):
335
+ return get_default_llm_config()
336
+
337
+ override_config_dict = {}
338
+ if os.path.exists(ROOT_DIR / OVERRIDE_LLM_CONFIG_FILENAME):
339
+ logger.info("Loading custom llm config...")
340
+ with open(ROOT_DIR / OVERRIDE_LLM_CONFIG_FILENAME) as f:
341
+ override_config_dict = load_jsonc(f)
342
+ else:
343
+ logger.warning("Custom llm config not found, loading default config")
344
+
345
+ try:
346
+ default_config = get_default_llm_config()
347
+ return deep_merge_llm_config(default_config, override_config_dict)
348
+
349
+ except ValidationError as e:
350
+ logger.error(f"Invalid llm config: {e}. Falling back to default config")
351
+ return get_default_llm_config()
352
+
353
+
354
+ def initialize_llm_config() -> LLMConfig:
355
+ llm_config = parse_llm_config()
356
+ llm_config.validate_providers()
357
+ logger.success("LLM config initialized")
358
+ return llm_config
359
+
360
+
361
+ ### Output config
362
+
363
+
364
+ class OutputConfig(BaseModel):
365
+ structured_output: Annotated[
366
+ type[BaseModel] | dict | None,
367
+ Field(
368
+ default=None,
369
+ description=(
370
+ "Optional structured schema (as a BaseModel or dict) to shape the output. "
371
+ "If provided, it takes precedence over 'output_description'."
372
+ ),
373
+ ),
374
+ ]
375
+ output_description: Annotated[
376
+ str | None,
377
+ Field(
378
+ default=None,
379
+ description=(
380
+ "Optional natural language description of the expected output format. "
381
+ "Used only if 'structured_output' is not provided. "
382
+ "Example: 'Output a JSON with 3 keys: color, price, websiteUrl'."
383
+ ),
384
+ ),
385
+ ]
386
+
387
+ def __str__(self):
388
+ s_builder = ""
389
+ if self.structured_output:
390
+ s_builder += f"Structured Output: {self.structured_output}\n"
391
+ if self.output_description:
392
+ s_builder += f"Output Description: {self.output_description}\n"
393
+ if self.output_description and self.structured_output:
394
+ s_builder += (
395
+ "Both 'structured_output' and 'output_description' are provided. "
396
+ "'structured_output' will take precedence.\n"
397
+ )
398
+ return s_builder
399
+
400
+ @model_validator(mode="after")
401
+ def warn_if_both_outputs_provided(self):
402
+ if self.structured_output and self.output_description:
403
+ import warnings
404
+
405
+ warnings.warn(
406
+ "Both 'structured_output' and 'output_description' are provided. "
407
+ "'structured_output' will take precedence.",
408
+ stacklevel=2,
409
+ )
410
+ return self
411
+
412
+ def needs_structured_format(self):
413
+ return self.structured_output or self.output_description
@@ -0,0 +1,3 @@
1
+ RECURSION_LIMIT = 400
2
+ MAX_MESSAGES_IN_HISTORY = 25
3
+ EXECUTOR_MESSAGES_KEY = "executor_messages"
@@ -0,0 +1,106 @@
1
+ """
2
+ Context variables for global state management.
3
+
4
+ Uses ContextVar to avoid prop drilling and maintain clean function signatures.
5
+ """
6
+
7
+ from collections.abc import Callable, Coroutine
8
+ from enum import Enum
9
+ from pathlib import Path
10
+ from typing import Literal
11
+
12
+ from adbutils import AdbClient
13
+ from openai import BaseModel
14
+ from pydantic import ConfigDict
15
+
16
+ from minitap.mobile_use.agents.planner.types import Subgoal
17
+ from minitap.mobile_use.clients.ios_client import IosClientWrapper
18
+ from minitap.mobile_use.clients.ui_automator_client import UIAutomatorClient
19
+ from minitap.mobile_use.config import AgentNode, LLMConfig
20
+
21
+
22
+ class AppLaunchResult(BaseModel):
23
+ """Result of initial app launch attempt."""
24
+
25
+ locked_app_package: str
26
+ locked_app_initial_launch_success: bool | None
27
+ locked_app_initial_launch_error: str | None
28
+
29
+
30
+ class DevicePlatform(str, Enum):
31
+ """Mobile device platform enumeration."""
32
+
33
+ ANDROID = "android"
34
+ IOS = "ios"
35
+
36
+
37
+ class DeviceContext(BaseModel):
38
+ host_platform: Literal["WINDOWS", "LINUX"]
39
+ mobile_platform: DevicePlatform
40
+ device_id: str
41
+ device_width: int
42
+ device_height: int
43
+
44
+ def to_str(self):
45
+ return (
46
+ f"Host platform: {self.host_platform}\n"
47
+ f"Mobile platform: {self.mobile_platform.value}\n"
48
+ f"Device ID: {self.device_id}\n"
49
+ f"Device width: {self.device_width}\n"
50
+ f"Device height: {self.device_height}\n"
51
+ )
52
+
53
+
54
+ class ExecutionSetup(BaseModel):
55
+ """Execution setup for a task."""
56
+
57
+ traces_path: Path | None = None
58
+ trace_name: str | None = None
59
+ enable_remote_tracing: bool = False
60
+ app_lock_status: AppLaunchResult | None = None
61
+
62
+ def get_locked_app_package(self) -> str | None:
63
+ """
64
+ Get the locked app package name if app locking is enabled.
65
+
66
+ Returns:
67
+ The locked app package name, or None if app locking is not enabled.
68
+ """
69
+ if self.app_lock_status:
70
+ return self.app_lock_status.locked_app_package
71
+ return None
72
+
73
+
74
+ IsReplan = bool
75
+
76
+
77
+ class MobileUseContext(BaseModel):
78
+ model_config = ConfigDict(arbitrary_types_allowed=True)
79
+
80
+ trace_id: str
81
+ device: DeviceContext
82
+ llm_config: LLMConfig
83
+ adb_client: AdbClient | None = None
84
+ ui_adb_client: UIAutomatorClient | None = None
85
+ ios_client: IosClientWrapper | None = None
86
+ execution_setup: ExecutionSetup | None = None
87
+ on_agent_thought: Callable[[AgentNode, str], Coroutine] | None = None
88
+ on_plan_changes: Callable[[list[Subgoal], IsReplan], Coroutine] | None = None
89
+ minitap_api_key: str | None = None
90
+ video_recording_enabled: bool = False
91
+
92
+ def get_adb_client(self) -> AdbClient:
93
+ if self.adb_client is None:
94
+ raise ValueError("No ADB client in context.")
95
+ return self.adb_client # type: ignore
96
+
97
+ def get_ui_adb_client(self) -> UIAutomatorClient:
98
+ if self.ui_adb_client is None:
99
+ raise ValueError("No UIAutomator client in context.")
100
+ return self.ui_adb_client
101
+
102
+ def get_ios_client(self) -> IosClientWrapper:
103
+ """Get the iOS client (IDB for simulators, WDA for physical devices)."""
104
+ if self.ios_client is None:
105
+ raise ValueError("No iOS client in context.")
106
+ return self.ios_client
File without changes