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,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
|
+
)
|