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,421 @@
1
+ """Telemetry service for mobile-use SDK using PostHog."""
2
+
3
+ import json
4
+ import platform
5
+ import threading
6
+ from pathlib import Path
7
+
8
+ import uuid_utils
9
+ from posthog import Posthog
10
+
11
+ from minitap.mobile_use.config import settings
12
+ from minitap.mobile_use.utils.logger import get_logger
13
+
14
+ logger = get_logger(__name__)
15
+
16
+
17
+ POSTHOG_API_KEY = "phc_MTwMcqOjMpTdTdrYwQUlsWaKkB7C8MPAw9YyZhRv8B8"
18
+ POSTHOG_HOST = "https://eu.i.posthog.com"
19
+ EVENT_PREFIX = "mobile_use_"
20
+
21
+ TELEMETRY_CONFIG_DIR = Path.home() / ".minitap"
22
+ TELEMETRY_CONFIG_FILE = TELEMETRY_CONFIG_DIR / "telemetry.json"
23
+
24
+
25
+ class TelemetryConfig:
26
+ """Manages telemetry consent and configuration persistence."""
27
+
28
+ def __init__(self):
29
+ self._distinct_id: str | None = None
30
+ self._enabled: bool | None = None
31
+
32
+ @property
33
+ def config_exists(self) -> bool:
34
+ return TELEMETRY_CONFIG_FILE.exists()
35
+
36
+ def load(self) -> dict:
37
+ """Load telemetry config from disk."""
38
+ if not self.config_exists:
39
+ return {}
40
+ try:
41
+ with open(TELEMETRY_CONFIG_FILE) as f:
42
+ return json.load(f)
43
+ except (json.JSONDecodeError, OSError):
44
+ return {}
45
+
46
+ def save(self, enabled: bool, distinct_id: str) -> None:
47
+ """
48
+ Save telemetry config to disk.
49
+
50
+ Only persists if enabled=True. If disabled, we don't persist
51
+ so the user will be asked again next session.
52
+ """
53
+ TELEMETRY_CONFIG_DIR.mkdir(parents=True, exist_ok=True)
54
+ if enabled:
55
+ config = {"enabled": enabled, "distinct_id": distinct_id}
56
+ with open(TELEMETRY_CONFIG_FILE, "w") as f:
57
+ json.dump(config, f, indent=2)
58
+ else:
59
+ # Don't persist denial - user will be asked again next session
60
+ # But still save distinct_id for consistency if they enable later
61
+ config = {"distinct_id": distinct_id}
62
+ with open(TELEMETRY_CONFIG_FILE, "w") as f:
63
+ json.dump(config, f, indent=2)
64
+
65
+ @property
66
+ def distinct_id(self) -> str:
67
+ """Get or generate a persistent anonymous distinct ID."""
68
+ if self._distinct_id is None:
69
+ config = self.load()
70
+ self._distinct_id = config.get("distinct_id") or str(uuid_utils.uuid4())
71
+ return self._distinct_id
72
+
73
+ @property
74
+ def enabled(self) -> bool | None:
75
+ """
76
+ Check if telemetry is enabled.
77
+
78
+ Priority:
79
+ 1. Environment variable MOBILE_USE_TELEMETRY_ENABLED (true/false/1/0)
80
+ 2. Persisted config file
81
+ 3. None if not configured (needs consent prompt)
82
+ """
83
+ if self._enabled is not None:
84
+ return self._enabled
85
+
86
+ telemetry_enabled = settings.MOBILE_USE_TELEMETRY_ENABLED
87
+ if telemetry_enabled is not None:
88
+ self._enabled = telemetry_enabled
89
+ return self._enabled
90
+
91
+ config = self.load()
92
+ if "enabled" in config:
93
+ self._enabled = config["enabled"]
94
+ return self._enabled
95
+
96
+ return None
97
+
98
+ def set_enabled(self, enabled: bool) -> None:
99
+ """Set telemetry enabled state and persist to config."""
100
+ self._enabled = enabled
101
+ self.save(enabled=enabled, distinct_id=self.distinct_id)
102
+
103
+
104
+ class TelemetryService:
105
+ """PostHog telemetry service for mobile-use SDK."""
106
+
107
+ _instance: "TelemetryService | None" = None
108
+ _lock: threading.Lock = threading.Lock()
109
+
110
+ def __init__(self):
111
+ self._config = TelemetryConfig()
112
+ self._initialized = False
113
+ self._client: Posthog | None = None
114
+ self._session_id: str | None = None
115
+ self._session_context: dict = {}
116
+
117
+ @classmethod
118
+ def get_instance(cls) -> "TelemetryService":
119
+ """Get singleton instance of TelemetryService (thread-safe)."""
120
+ if cls._instance is None:
121
+ with cls._lock:
122
+ if cls._instance is None:
123
+ cls._instance = TelemetryService()
124
+ return cls._instance
125
+
126
+ @property
127
+ def is_enabled(self) -> bool:
128
+ """Check if telemetry is enabled."""
129
+ return self._config.enabled is True
130
+
131
+ @property
132
+ def needs_consent(self) -> bool:
133
+ """Check if user consent is needed (not yet configured)."""
134
+ return self._config.enabled is None
135
+
136
+ def initialize(self) -> None:
137
+ """Initialize PostHog client if telemetry is enabled."""
138
+ if self._initialized:
139
+ return
140
+
141
+ if not self.is_enabled:
142
+ logger.debug("Telemetry disabled, skipping PostHog initialization")
143
+ return
144
+
145
+ try:
146
+ self._client = Posthog(
147
+ project_api_key=POSTHOG_API_KEY,
148
+ host=POSTHOG_HOST,
149
+ debug=False,
150
+ )
151
+ self._initialized = True
152
+ logger.debug("PostHog telemetry initialized")
153
+ except Exception as e:
154
+ logger.debug(f"Failed to initialize PostHog: {e}")
155
+ self._initialized = False
156
+
157
+ def set_consent(self, enabled: bool) -> None:
158
+ """Set user consent for telemetry."""
159
+ self._config.set_enabled(enabled)
160
+ if enabled:
161
+ self.initialize()
162
+ else:
163
+ if self._client:
164
+ self._client.disabled = True
165
+
166
+ def start_session(self, context: dict | None = None) -> str:
167
+ """
168
+ Start a new telemetry session for CLI usage.
169
+
170
+ Args:
171
+ context: Initial session context (e.g., goal, platform, device_id)
172
+
173
+ Returns:
174
+ The session ID
175
+ """
176
+ self._session_id = str(uuid_utils.uuid7())
177
+ self._session_context = context or {}
178
+
179
+ if self.is_enabled:
180
+ self.capture(
181
+ "session_started",
182
+ {
183
+ "$session_id": self._session_id,
184
+ **self._session_context,
185
+ },
186
+ )
187
+
188
+ return self._session_id
189
+
190
+ def update_session_context(self, context: dict) -> None:
191
+ """Update the current session context with additional data."""
192
+ self._session_context.update(context)
193
+
194
+ def end_session(self, success: bool = True, error: str | None = None) -> None:
195
+ """
196
+ End the current telemetry session.
197
+
198
+ Args:
199
+ success: Whether the session completed successfully
200
+ error: Error message if session failed
201
+ """
202
+ if not self._session_id:
203
+ return
204
+
205
+ if self.is_enabled:
206
+ self.capture(
207
+ "session_ended",
208
+ {
209
+ "$session_id": self._session_id,
210
+ "success": success,
211
+ "error": error,
212
+ **self._session_context,
213
+ },
214
+ )
215
+
216
+ self._session_id = None
217
+ self._session_context = {}
218
+
219
+ def capture_action(self, action: str, details: dict | None = None) -> None:
220
+ """
221
+ Capture an action within the current session.
222
+
223
+ Args:
224
+ action: The action name (e.g., "screenshot_taken", "tap_performed")
225
+ details: Additional action details
226
+ """
227
+ properties = {
228
+ "action": action,
229
+ **(details or {}),
230
+ }
231
+
232
+ if self._session_id:
233
+ properties["$session_id"] = self._session_id
234
+ properties.update(self._session_context)
235
+
236
+ self.capture("action", properties)
237
+
238
+ def capture(self, event: str, properties: dict | None = None) -> None:
239
+ """Capture a telemetry event."""
240
+ if not self.is_enabled:
241
+ return
242
+
243
+ # Lazy initialization for SDK usage (non-CLI)
244
+ # If user has previously consented via config/env, auto-initialize
245
+ if not self._initialized:
246
+ self.initialize()
247
+ if not self._initialized:
248
+ return
249
+
250
+ try:
251
+ all_properties = {
252
+ "sdk_version": self._get_sdk_version(),
253
+ "python_version": platform.python_version(),
254
+ "os": platform.system(),
255
+ "os_version": platform.release(),
256
+ **(properties or {}),
257
+ }
258
+
259
+ # Include session ID if available
260
+ if self._session_id and "$session_id" not in all_properties:
261
+ all_properties["$session_id"] = self._session_id
262
+
263
+ if self._client:
264
+ prefixed_event = f"{EVENT_PREFIX}{event}"
265
+ self._client.capture(
266
+ distinct_id=self._config.distinct_id,
267
+ event=prefixed_event,
268
+ properties=all_properties,
269
+ )
270
+ except Exception as e:
271
+ logger.debug(f"Failed to capture telemetry event: {e}")
272
+
273
+ def capture_exception(
274
+ self,
275
+ exception: Exception,
276
+ context: dict | None = None,
277
+ ) -> None:
278
+ """
279
+ Capture an exception event to PostHog Error Tracking.
280
+
281
+ Uses PostHog's native capture_exception method for proper
282
+ integration with the Error Tracking dashboard.
283
+
284
+ Args:
285
+ exception: The exception to capture
286
+ context: Additional context properties
287
+ """
288
+ if not self.is_enabled:
289
+ return
290
+
291
+ # Lazy init for SDK usage
292
+ if not self._initialized:
293
+ self.initialize()
294
+ if not self._initialized:
295
+ return
296
+
297
+ try:
298
+ if self._client:
299
+ # Use PostHog's native capture_exception for Error Tracking
300
+ self._client.capture_exception(
301
+ exception,
302
+ distinct_id=self._config.distinct_id,
303
+ properties={
304
+ "sdk_version": self._get_sdk_version(),
305
+ "source": "mobile_use",
306
+ **(context or {}),
307
+ },
308
+ )
309
+ except Exception as e:
310
+ logger.debug(f"Failed to capture exception telemetry: {e}")
311
+
312
+ def capture_task_started(
313
+ self,
314
+ task_id: str,
315
+ platform: str,
316
+ has_locked_app: bool = False,
317
+ ) -> None:
318
+ """Capture task started event."""
319
+ self.capture(
320
+ "task_started",
321
+ {
322
+ "task_id": task_id,
323
+ "device_platform": platform,
324
+ "has_locked_app": has_locked_app,
325
+ },
326
+ )
327
+
328
+ def capture_task_completed(
329
+ self,
330
+ task_id: str,
331
+ success: bool,
332
+ steps_count: int,
333
+ duration_seconds: float,
334
+ cancelled: bool = False,
335
+ ) -> None:
336
+ """Capture task completed event."""
337
+ self.capture(
338
+ "task_completed",
339
+ {
340
+ "task_id": task_id,
341
+ "success": success,
342
+ "cancelled": cancelled,
343
+ "steps_count": steps_count,
344
+ "duration_seconds": duration_seconds,
345
+ },
346
+ )
347
+
348
+ def capture_agent_initialized(self, platform: str, device_id: str | None = None) -> None:
349
+ """Capture agent initialization event."""
350
+ self.capture(
351
+ "agent_initialized",
352
+ {
353
+ "device_platform": platform,
354
+ "has_device_id": device_id is not None,
355
+ },
356
+ )
357
+
358
+ def capture_cortex_decision(
359
+ self,
360
+ task_id: str,
361
+ has_decisions: bool = False,
362
+ has_goals_completion: bool = False,
363
+ completed_subgoals_count: int = 0,
364
+ ) -> None:
365
+ """Capture cortex agent decision event (only non-sensitive flags)."""
366
+ self.capture(
367
+ "cortex_decision",
368
+ {
369
+ "task_id": task_id,
370
+ "has_decisions": has_decisions,
371
+ "has_goals_completion": has_goals_completion,
372
+ "completed_subgoals_count": completed_subgoals_count,
373
+ },
374
+ )
375
+
376
+ def capture_executor_action(
377
+ self,
378
+ task_id: str,
379
+ tool_name: str,
380
+ success: bool,
381
+ error: str | None = None,
382
+ ) -> None:
383
+ """Capture executor tool action event."""
384
+ self.capture(
385
+ "executor_action",
386
+ {
387
+ "task_id": task_id,
388
+ "tool_name": tool_name,
389
+ "success": success,
390
+ "error": error,
391
+ },
392
+ )
393
+
394
+ def flush(self) -> None:
395
+ """Flush pending events to PostHog."""
396
+ if self.is_enabled and self._initialized and self._client:
397
+ try:
398
+ self._client.flush()
399
+ except Exception as e:
400
+ logger.debug(f"Failed to flush telemetry: {e}")
401
+
402
+ def shutdown(self) -> None:
403
+ """Shutdown telemetry service."""
404
+ self.flush()
405
+ if self._client:
406
+ try:
407
+ self._client.shutdown()
408
+ except Exception:
409
+ pass
410
+
411
+ def _get_sdk_version(self) -> str:
412
+ """Get the mobile-use SDK version."""
413
+ try:
414
+ from importlib.metadata import version
415
+
416
+ return version("minitap-mobile-use")
417
+ except Exception:
418
+ return "unknown"
419
+
420
+
421
+ telemetry = TelemetryService.get_instance()
@@ -0,0 +1,67 @@
1
+ from langchain_core.tools import BaseTool
2
+
3
+ from minitap.mobile_use.context import MobileUseContext
4
+ from minitap.mobile_use.tools.mobile.back import back_wrapper
5
+ from minitap.mobile_use.tools.mobile.erase_one_char import erase_one_char_wrapper
6
+ from minitap.mobile_use.tools.mobile.focus_and_clear_text import focus_and_clear_text_wrapper
7
+ from minitap.mobile_use.tools.mobile.focus_and_input_text import focus_and_input_text_wrapper
8
+ from minitap.mobile_use.tools.mobile.launch_app import launch_app_wrapper
9
+ from minitap.mobile_use.tools.mobile.long_press_on import long_press_on_wrapper
10
+ from minitap.mobile_use.tools.mobile.open_link import open_link_wrapper
11
+ from minitap.mobile_use.tools.mobile.press_key import press_key_wrapper
12
+ from minitap.mobile_use.tools.mobile.stop_app import stop_app_wrapper
13
+ from minitap.mobile_use.tools.mobile.swipe import swipe_wrapper
14
+ from minitap.mobile_use.tools.mobile.tap import tap_wrapper
15
+ from minitap.mobile_use.tools.mobile.video_recording import (
16
+ start_video_recording_wrapper,
17
+ stop_video_recording_wrapper,
18
+ )
19
+ from minitap.mobile_use.tools.mobile.wait_for_delay import wait_for_delay_wrapper
20
+ from minitap.mobile_use.tools.scratchpad import (
21
+ list_notes_wrapper,
22
+ read_note_wrapper,
23
+ save_note_wrapper,
24
+ )
25
+ from minitap.mobile_use.tools.tool_wrapper import CompositeToolWrapper, ToolWrapper
26
+
27
+ EXECUTOR_WRAPPERS_TOOLS = [
28
+ back_wrapper,
29
+ open_link_wrapper,
30
+ tap_wrapper,
31
+ long_press_on_wrapper,
32
+ swipe_wrapper,
33
+ focus_and_input_text_wrapper,
34
+ erase_one_char_wrapper,
35
+ launch_app_wrapper,
36
+ stop_app_wrapper,
37
+ focus_and_clear_text_wrapper,
38
+ press_key_wrapper,
39
+ wait_for_delay_wrapper,
40
+ # Scratchpad tools for persistent memory
41
+ save_note_wrapper,
42
+ read_note_wrapper,
43
+ list_notes_wrapper,
44
+ ]
45
+
46
+ VIDEO_RECORDING_WRAPPERS = [
47
+ start_video_recording_wrapper,
48
+ stop_video_recording_wrapper,
49
+ ]
50
+
51
+
52
+ def get_tools_from_wrappers(
53
+ ctx: "MobileUseContext",
54
+ wrappers: list[ToolWrapper],
55
+ ) -> list[BaseTool]:
56
+ tools: list[BaseTool] = []
57
+ for wrapper in wrappers:
58
+ if isinstance(wrapper, CompositeToolWrapper):
59
+ tools.extend(wrapper.composite_tools_fn_getter(ctx))
60
+ continue
61
+
62
+ tools.append(wrapper.tool_fn_getter(ctx))
63
+ return tools
64
+
65
+
66
+ def format_tools_list(ctx: MobileUseContext, wrappers: list[ToolWrapper]) -> str:
67
+ return ", ".join([tool.name for tool in get_tools_from_wrappers(ctx, wrappers)])
@@ -0,0 +1,52 @@
1
+ from typing import Annotated
2
+
3
+ from langchain_core.messages import ToolMessage
4
+ from langchain_core.tools import tool
5
+ from langchain_core.tools.base import InjectedToolCallId
6
+ from langgraph.prebuilt import InjectedState
7
+ from langgraph.types import Command
8
+
9
+ from minitap.mobile_use.constants import EXECUTOR_MESSAGES_KEY
10
+ from minitap.mobile_use.context import MobileUseContext
11
+ from minitap.mobile_use.controllers.unified_controller import UnifiedMobileController
12
+ from minitap.mobile_use.graph.state import State
13
+ from minitap.mobile_use.tools.tool_wrapper import ToolWrapper
14
+
15
+
16
+ def get_back_tool(ctx: MobileUseContext):
17
+ @tool
18
+ async def back(
19
+ agent_thought: str,
20
+ tool_call_id: Annotated[str, InjectedToolCallId],
21
+ state: Annotated[State, InjectedState],
22
+ ) -> Command:
23
+ """Navigates to the previous screen. (Only works on Android for the moment)"""
24
+ controller = UnifiedMobileController(ctx)
25
+ success = await controller.go_back()
26
+ has_failed = not success
27
+ output = "Failed to go back" if has_failed else None
28
+ tool_message = ToolMessage(
29
+ tool_call_id=tool_call_id,
30
+ content=back_wrapper.on_failure_fn() if has_failed else back_wrapper.on_success_fn(),
31
+ additional_kwargs={"error": output} if has_failed else {},
32
+ status="error" if has_failed else "success",
33
+ )
34
+ return Command(
35
+ update=await state.asanitize_update(
36
+ ctx=ctx,
37
+ update={
38
+ "agents_thoughts": [agent_thought],
39
+ EXECUTOR_MESSAGES_KEY: [tool_message],
40
+ },
41
+ agent="executor",
42
+ ),
43
+ )
44
+
45
+ return back
46
+
47
+
48
+ back_wrapper = ToolWrapper(
49
+ tool_fn_getter=get_back_tool,
50
+ on_success_fn=lambda: "Navigated to the previous screen.",
51
+ on_failure_fn=lambda: "Failed to navigate to the previous screen.",
52
+ )
@@ -0,0 +1,56 @@
1
+ from typing import Annotated
2
+
3
+ from langchain_core.messages import ToolMessage
4
+ from langchain_core.tools import tool
5
+ from langchain_core.tools.base import InjectedToolCallId
6
+ from langgraph.prebuilt import InjectedState
7
+ from langgraph.types import Command
8
+
9
+ from minitap.mobile_use.constants import EXECUTOR_MESSAGES_KEY
10
+ from minitap.mobile_use.context import MobileUseContext
11
+ from minitap.mobile_use.controllers.unified_controller import UnifiedMobileController
12
+ from minitap.mobile_use.graph.state import State
13
+ from minitap.mobile_use.tools.tool_wrapper import ToolWrapper
14
+
15
+
16
+ def get_erase_one_char_tool(ctx: MobileUseContext):
17
+ @tool
18
+ async def erase_one_char(
19
+ agent_thought: str,
20
+ tool_call_id: Annotated[str, InjectedToolCallId],
21
+ state: Annotated[State, InjectedState],
22
+ ) -> Command:
23
+ """
24
+ Erase one character from a text area.
25
+ It acts the same as pressing backspace a single time.
26
+ """
27
+ controller = UnifiedMobileController(ctx)
28
+ output = await controller.erase_text(nb_chars=1)
29
+ has_failed = not output
30
+ tool_message = ToolMessage(
31
+ tool_call_id=tool_call_id,
32
+ content=erase_one_char_wrapper.on_failure_fn()
33
+ if has_failed
34
+ else erase_one_char_wrapper.on_success_fn(),
35
+ additional_kwargs={"error": "Failed to erase character"} if has_failed else {},
36
+ status="error" if has_failed else "success",
37
+ )
38
+ return Command(
39
+ update=await state.asanitize_update(
40
+ ctx=ctx,
41
+ update={
42
+ "agents_thoughts": [agent_thought],
43
+ EXECUTOR_MESSAGES_KEY: [tool_message],
44
+ },
45
+ agent="executor",
46
+ ),
47
+ )
48
+
49
+ return erase_one_char
50
+
51
+
52
+ erase_one_char_wrapper = ToolWrapper(
53
+ tool_fn_getter=get_erase_one_char_tool,
54
+ on_success_fn=lambda: "Erased one character successfully.",
55
+ on_failure_fn=lambda: "Failed to erase one character.",
56
+ )