minitap-mobile-use 0.0.1.dev0__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.

Potentially problematic release.


This version of minitap-mobile-use might be problematic. Click here for more details.

Files changed (95) hide show
  1. minitap/mobile_use/__init__.py +0 -0
  2. minitap/mobile_use/agents/contextor/contextor.py +42 -0
  3. minitap/mobile_use/agents/cortex/cortex.md +93 -0
  4. minitap/mobile_use/agents/cortex/cortex.py +107 -0
  5. minitap/mobile_use/agents/cortex/types.py +11 -0
  6. minitap/mobile_use/agents/executor/executor.md +73 -0
  7. minitap/mobile_use/agents/executor/executor.py +84 -0
  8. minitap/mobile_use/agents/executor/executor_context_cleaner.py +27 -0
  9. minitap/mobile_use/agents/executor/utils.py +11 -0
  10. minitap/mobile_use/agents/hopper/hopper.md +13 -0
  11. minitap/mobile_use/agents/hopper/hopper.py +45 -0
  12. minitap/mobile_use/agents/orchestrator/human.md +13 -0
  13. minitap/mobile_use/agents/orchestrator/orchestrator.md +18 -0
  14. minitap/mobile_use/agents/orchestrator/orchestrator.py +114 -0
  15. minitap/mobile_use/agents/orchestrator/types.py +14 -0
  16. minitap/mobile_use/agents/outputter/human.md +25 -0
  17. minitap/mobile_use/agents/outputter/outputter.py +75 -0
  18. minitap/mobile_use/agents/outputter/test_outputter.py +107 -0
  19. minitap/mobile_use/agents/planner/human.md +12 -0
  20. minitap/mobile_use/agents/planner/planner.md +64 -0
  21. minitap/mobile_use/agents/planner/planner.py +64 -0
  22. minitap/mobile_use/agents/planner/types.py +44 -0
  23. minitap/mobile_use/agents/planner/utils.py +45 -0
  24. minitap/mobile_use/agents/summarizer/summarizer.py +34 -0
  25. minitap/mobile_use/clients/device_hardware_client.py +23 -0
  26. minitap/mobile_use/clients/ios_client.py +44 -0
  27. minitap/mobile_use/clients/screen_api_client.py +53 -0
  28. minitap/mobile_use/config.py +285 -0
  29. minitap/mobile_use/constants.py +2 -0
  30. minitap/mobile_use/context.py +65 -0
  31. minitap/mobile_use/controllers/__init__.py +0 -0
  32. minitap/mobile_use/controllers/mobile_command_controller.py +379 -0
  33. minitap/mobile_use/controllers/platform_specific_commands_controller.py +74 -0
  34. minitap/mobile_use/graph/graph.py +149 -0
  35. minitap/mobile_use/graph/state.py +73 -0
  36. minitap/mobile_use/main.py +122 -0
  37. minitap/mobile_use/sdk/__init__.py +12 -0
  38. minitap/mobile_use/sdk/agent.py +524 -0
  39. minitap/mobile_use/sdk/builders/__init__.py +10 -0
  40. minitap/mobile_use/sdk/builders/agent_config_builder.py +213 -0
  41. minitap/mobile_use/sdk/builders/index.py +15 -0
  42. minitap/mobile_use/sdk/builders/task_request_builder.py +218 -0
  43. minitap/mobile_use/sdk/constants.py +14 -0
  44. minitap/mobile_use/sdk/examples/README.md +45 -0
  45. minitap/mobile_use/sdk/examples/__init__.py +1 -0
  46. minitap/mobile_use/sdk/examples/simple_photo_organizer.py +76 -0
  47. minitap/mobile_use/sdk/examples/smart_notification_assistant.py +177 -0
  48. minitap/mobile_use/sdk/types/__init__.py +49 -0
  49. minitap/mobile_use/sdk/types/agent.py +73 -0
  50. minitap/mobile_use/sdk/types/exceptions.py +74 -0
  51. minitap/mobile_use/sdk/types/task.py +191 -0
  52. minitap/mobile_use/sdk/utils.py +28 -0
  53. minitap/mobile_use/servers/config.py +19 -0
  54. minitap/mobile_use/servers/device_hardware_bridge.py +212 -0
  55. minitap/mobile_use/servers/device_screen_api.py +143 -0
  56. minitap/mobile_use/servers/start_servers.py +151 -0
  57. minitap/mobile_use/servers/stop_servers.py +215 -0
  58. minitap/mobile_use/servers/utils.py +11 -0
  59. minitap/mobile_use/services/accessibility.py +100 -0
  60. minitap/mobile_use/services/llm.py +143 -0
  61. minitap/mobile_use/tools/index.py +54 -0
  62. minitap/mobile_use/tools/mobile/back.py +52 -0
  63. minitap/mobile_use/tools/mobile/copy_text_from.py +77 -0
  64. minitap/mobile_use/tools/mobile/erase_text.py +124 -0
  65. minitap/mobile_use/tools/mobile/input_text.py +74 -0
  66. minitap/mobile_use/tools/mobile/launch_app.py +59 -0
  67. minitap/mobile_use/tools/mobile/list_packages.py +78 -0
  68. minitap/mobile_use/tools/mobile/long_press_on.py +62 -0
  69. minitap/mobile_use/tools/mobile/open_link.py +59 -0
  70. minitap/mobile_use/tools/mobile/paste_text.py +66 -0
  71. minitap/mobile_use/tools/mobile/press_key.py +58 -0
  72. minitap/mobile_use/tools/mobile/run_flow.py +57 -0
  73. minitap/mobile_use/tools/mobile/stop_app.py +58 -0
  74. minitap/mobile_use/tools/mobile/swipe.py +56 -0
  75. minitap/mobile_use/tools/mobile/take_screenshot.py +70 -0
  76. minitap/mobile_use/tools/mobile/tap.py +66 -0
  77. minitap/mobile_use/tools/mobile/wait_for_animation_to_end.py +68 -0
  78. minitap/mobile_use/tools/tool_wrapper.py +33 -0
  79. minitap/mobile_use/utils/cli_helpers.py +40 -0
  80. minitap/mobile_use/utils/cli_selection.py +144 -0
  81. minitap/mobile_use/utils/conversations.py +31 -0
  82. minitap/mobile_use/utils/decorators.py +123 -0
  83. minitap/mobile_use/utils/errors.py +6 -0
  84. minitap/mobile_use/utils/file.py +13 -0
  85. minitap/mobile_use/utils/logger.py +184 -0
  86. minitap/mobile_use/utils/media.py +73 -0
  87. minitap/mobile_use/utils/recorder.py +55 -0
  88. minitap/mobile_use/utils/requests_utils.py +37 -0
  89. minitap/mobile_use/utils/shell_utils.py +20 -0
  90. minitap/mobile_use/utils/time.py +6 -0
  91. minitap/mobile_use/utils/ui_hierarchy.py +30 -0
  92. minitap_mobile_use-0.0.1.dev0.dist-info/METADATA +274 -0
  93. minitap_mobile_use-0.0.1.dev0.dist-info/RECORD +95 -0
  94. minitap_mobile_use-0.0.1.dev0.dist-info/WHEEL +4 -0
  95. minitap_mobile_use-0.0.1.dev0.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,6 @@
1
+ class ControllerErrors(Exception):
2
+ def __init__(self, message: str):
3
+ self.message = message
4
+
5
+ def __str__(self):
6
+ return self.message
@@ -0,0 +1,13 @@
1
+ import json
2
+ import re
3
+ from typing import IO
4
+
5
+
6
+ def strip_json_comments(text: str) -> str:
7
+ text = re.sub(r"//.*?$", "", text, flags=re.MULTILINE)
8
+ text = re.sub(r"/\*.*?\*/", "", text, flags=re.DOTALL)
9
+ return text
10
+
11
+
12
+ def load_jsonc(file: IO) -> dict:
13
+ return json.loads(strip_json_comments(file.read()))
@@ -0,0 +1,184 @@
1
+ import logging
2
+ import sys
3
+ from enum import Enum
4
+ from pathlib import Path
5
+ from typing import Optional, Union
6
+
7
+ from colorama import Fore, Style, init
8
+
9
+ init(autoreset=True)
10
+
11
+
12
+ class LogLevel(Enum):
13
+ DEBUG = ("DEBUG", Fore.MAGENTA, "🔍")
14
+ INFO = ("INFO", Fore.WHITE, "ℹ")
15
+ SUCCESS = ("SUCCESS", Fore.GREEN, "✓")
16
+ WARNING = ("WARNING", Fore.YELLOW, "⚠")
17
+ ERROR = ("ERROR", Fore.RED, "❌")
18
+ CRITICAL = ("CRITICAL", Fore.RED + Style.BRIGHT, "💥")
19
+
20
+
21
+ class MobileUseLogger:
22
+ def __init__(
23
+ self,
24
+ name: str,
25
+ log_file: Optional[Union[str, Path]] = None,
26
+ console_level: str = "INFO",
27
+ file_level: str = "DEBUG",
28
+ enable_file_logging: bool = True,
29
+ ):
30
+ """
31
+ Initialize the MobileUse logger.
32
+
33
+ Args:
34
+ name: Logger name (usually __name__)
35
+ log_file: Path to log file (defaults to logs/{name}.log)
36
+ console_level: Minimum level for console output
37
+ file_level: Minimum level for file output
38
+ enable_file_logging: Whether to enable file logging
39
+ """
40
+ self.name = name
41
+ self.logger = logging.getLogger(name)
42
+ self.logger.setLevel(logging.DEBUG)
43
+
44
+ self.logger.handlers.clear()
45
+
46
+ self._setup_console_handler(console_level)
47
+
48
+ if enable_file_logging:
49
+ self._setup_file_handler(log_file, file_level)
50
+
51
+ def _setup_console_handler(self, level: str):
52
+ console_handler = logging.StreamHandler(sys.stdout)
53
+ console_handler.setLevel(getattr(logging, level.upper()))
54
+
55
+ console_formatter = ColoredFormatter()
56
+ console_handler.setFormatter(console_formatter)
57
+
58
+ self.logger.addHandler(console_handler)
59
+
60
+ def _setup_file_handler(self, log_file: Optional[Union[str, Path]], level: str):
61
+ if log_file is None:
62
+ log_file = Path("logs") / f"{self.name.replace('.', '_')}.log"
63
+
64
+ log_file = Path(log_file)
65
+ log_file.parent.mkdir(parents=True, exist_ok=True)
66
+
67
+ file_handler = logging.FileHandler(log_file, encoding="utf-8")
68
+ file_handler.setLevel(getattr(logging, level.upper()))
69
+
70
+ file_formatter = logging.Formatter(
71
+ fmt="%(asctime)s | %(name)s | %(levelname)s | %(message)s", datefmt="%Y-%m-%d %H:%M:%S"
72
+ )
73
+ file_handler.setFormatter(file_formatter)
74
+
75
+ self.logger.addHandler(file_handler)
76
+
77
+ def debug(self, message: str, **kwargs):
78
+ self.logger.debug(message, extra={"log_level": LogLevel.DEBUG}, **kwargs)
79
+
80
+ def info(self, message: str, **kwargs):
81
+ self.logger.info(message, extra={"log_level": LogLevel.INFO}, **kwargs)
82
+
83
+ def success(self, message: str, **kwargs):
84
+ self.logger.info(message, extra={"log_level": LogLevel.SUCCESS}, **kwargs)
85
+
86
+ def warning(self, message: str, **kwargs):
87
+ self.logger.warning(message, extra={"log_level": LogLevel.WARNING}, **kwargs)
88
+
89
+ def error(self, message: str, **kwargs):
90
+ self.logger.error(message, extra={"log_level": LogLevel.ERROR}, **kwargs)
91
+
92
+ def critical(self, message: str, **kwargs):
93
+ self.logger.critical(message, extra={"log_level": LogLevel.CRITICAL}, **kwargs)
94
+
95
+ def header(self, message: str, **_kwargs):
96
+ separator = "=" * 60
97
+ colored_separator = f"{Fore.CYAN}{separator}{Style.RESET_ALL}"
98
+ colored_message = f"{Fore.CYAN}{message}{Style.RESET_ALL}"
99
+
100
+ print(colored_separator)
101
+ print(colored_message)
102
+ print(colored_separator)
103
+ self.logger.info(f"\n{separator}\n{message}\n{separator}")
104
+
105
+
106
+ class ColoredFormatter(logging.Formatter):
107
+ def format(self, record):
108
+ log_level = getattr(record, "log_level", LogLevel.INFO)
109
+ _level_name, color, symbol = log_level.value
110
+
111
+ colored_message = f"{color}{symbol} {record.getMessage()}{Style.RESET_ALL}"
112
+
113
+ return colored_message
114
+
115
+
116
+ _loggers = {}
117
+
118
+
119
+ def get_logger(
120
+ name: str,
121
+ log_file: Optional[Union[str, Path]] = None,
122
+ console_level: str = "INFO",
123
+ file_level: str = "DEBUG",
124
+ enable_file_logging: bool = False,
125
+ ) -> MobileUseLogger:
126
+ """
127
+ Get or create a logger instance.
128
+
129
+ Args:
130
+ name: Logger name (usually __name__)
131
+ log_file: Path to log file (defaults to logs/{name}.log)
132
+ console_level: Minimum level for console output
133
+ file_level: Minimum level for file output
134
+ enable_file_logging: Whether to enable file logging
135
+
136
+ Returns:
137
+ MobileUseLogger instance
138
+ """
139
+ if name not in _loggers:
140
+ _loggers[name] = MobileUseLogger(
141
+ name=name,
142
+ log_file=log_file,
143
+ console_level=console_level,
144
+ file_level=file_level,
145
+ enable_file_logging=enable_file_logging,
146
+ )
147
+
148
+ return _loggers[name]
149
+
150
+
151
+ def log_debug(message: str, logger_name: str = "mobile-use"):
152
+ get_logger(logger_name).debug(message)
153
+
154
+
155
+ def log_info(message: str, logger_name: str = "mobile-use"):
156
+ get_logger(logger_name).info(message)
157
+
158
+
159
+ def log_success(message: str, logger_name: str = "mobile-use"):
160
+ get_logger(logger_name).success(message)
161
+
162
+
163
+ def log_warning(message: str, logger_name: str = "mobile-use"):
164
+ get_logger(logger_name).warning(message)
165
+
166
+
167
+ def log_error(message: str, logger_name: str = "mobile-use"):
168
+ get_logger(logger_name).error(message)
169
+
170
+
171
+ def log_critical(message: str, logger_name: str = "mobile-use"):
172
+ get_logger(logger_name).critical(message)
173
+
174
+
175
+ def log_header(message: str, logger_name: str = "mobile-use"):
176
+ get_logger(logger_name).header(message)
177
+
178
+
179
+ def get_server_logger() -> MobileUseLogger:
180
+ return get_logger(
181
+ name="mobile-use.servers",
182
+ console_level="INFO",
183
+ file_level="DEBUG",
184
+ )
@@ -0,0 +1,73 @@
1
+ import base64
2
+ import json
3
+ from io import BytesIO
4
+ from pathlib import Path
5
+
6
+ from PIL import Image
7
+
8
+
9
+ def compress_base64_jpeg(base64_str: str, quality: int = 50) -> str:
10
+ if base64_str.startswith("data:image"):
11
+ base64_str = base64_str.split(",")[1]
12
+
13
+ image_data = base64.b64decode(base64_str)
14
+ image = Image.open(BytesIO(image_data))
15
+
16
+ compressed_io = BytesIO()
17
+ image.save(compressed_io, format="JPEG", quality=quality, optimize=True)
18
+
19
+ compressed_base64 = base64.b64encode(compressed_io.getvalue()).decode("utf-8")
20
+ return compressed_base64
21
+
22
+
23
+ def create_gif_from_trace_folder(trace_folder_path: Path):
24
+ images = []
25
+ image_files = []
26
+
27
+ for file in trace_folder_path.iterdir():
28
+ if file.suffix == ".jpeg":
29
+ image_files.append(file)
30
+
31
+ image_files.sort(key=lambda f: int(f.stem))
32
+
33
+ print("Found " + str(len(image_files)) + " images to compile")
34
+
35
+ for file in image_files:
36
+ with open(file, "rb") as f:
37
+ image = Image.open(f).convert("RGB")
38
+ images.append(image)
39
+
40
+ if len(images) == 0:
41
+ return
42
+
43
+ gif_path = trace_folder_path / "trace.gif"
44
+ images[0].save(gif_path, save_all=True, append_images=images[1:], loop=0)
45
+ print("GIF created at " + str(gif_path))
46
+
47
+
48
+ def remove_images_from_trace_folder(trace_folder_path: Path):
49
+ for file in trace_folder_path.iterdir():
50
+ if file.suffix == ".jpeg":
51
+ file.unlink()
52
+
53
+
54
+ def create_steps_json_from_trace_folder(trace_folder_path: Path):
55
+ steps = []
56
+ for file in trace_folder_path.iterdir():
57
+ if file.suffix == ".json":
58
+ with open(file, "r", encoding="utf-8", errors="ignore") as f:
59
+ json_content = f.read()
60
+ steps.append({"timestamp": int(file.stem), "data": json_content})
61
+
62
+ steps.sort(key=lambda f: f["timestamp"])
63
+
64
+ print("Found " + str(len(steps)) + " steps to compile")
65
+
66
+ with open(trace_folder_path / "steps.json", "w", encoding="utf-8", errors="ignore") as f:
67
+ f.write(json.dumps(steps))
68
+
69
+
70
+ def remove_steps_json_from_trace_folder(trace_folder_path: Path):
71
+ for file in trace_folder_path.iterdir():
72
+ if file.suffix == ".json" and file.name != "steps.json":
73
+ file.unlink()
@@ -0,0 +1,55 @@
1
+ import base64
2
+ import time
3
+ from pathlib import Path
4
+
5
+ from langchain_core.messages import BaseMessage
6
+ from minitap.mobile_use.config import record_events
7
+ from minitap.mobile_use.context import MobileUseContext
8
+ from minitap.mobile_use.controllers.mobile_command_controller import take_screenshot
9
+ from minitap.mobile_use.utils.logger import get_logger
10
+ from minitap.mobile_use.utils.media import compress_base64_jpeg
11
+
12
+ logger = get_logger(__name__)
13
+
14
+
15
+ def record_interaction(ctx: MobileUseContext, response: BaseMessage):
16
+ if not ctx.execution_setup:
17
+ raise ValueError("No execution setup found")
18
+
19
+ logger.info("Recording interaction")
20
+ screenshot_base64 = take_screenshot(ctx)
21
+ logger.info("Screenshot taken")
22
+ try:
23
+ compressed_screenshot_base64 = compress_base64_jpeg(screenshot_base64, 20)
24
+ except Exception as e:
25
+ logger.error(f"Error compressing screenshot: {e}")
26
+ return "Could not record this interaction"
27
+ timestamp = time.time()
28
+ folder = ctx.execution_setup.traces_path.joinpath(ctx.execution_setup.trace_id).resolve()
29
+ folder.mkdir(parents=True, exist_ok=True)
30
+ try:
31
+ with open(
32
+ folder.joinpath(f"{int(timestamp)}.jpeg").resolve(),
33
+ "wb",
34
+ ) as f:
35
+ f.write(base64.b64decode(compressed_screenshot_base64))
36
+
37
+ with open(
38
+ folder.joinpath(f"{int(timestamp)}.json").resolve(),
39
+ "w",
40
+ encoding="utf-8",
41
+ ) as f:
42
+ f.write(response.model_dump_json())
43
+ except Exception as e:
44
+ logger.error(f"Error recording interaction: {e}")
45
+ return "Screenshot recorded successfully"
46
+
47
+
48
+ def log_agent_thoughts(agents_thoughts: list[str], output_path: Path | None):
49
+ if len(agents_thoughts) > 0:
50
+ last_agents_thoughts = agents_thoughts[-1]
51
+ previous_last_agents_thoughts = agents_thoughts[-2] if len(agents_thoughts) > 1 else None
52
+ if previous_last_agents_thoughts != last_agents_thoughts:
53
+ logger.info(f"💭 {last_agents_thoughts}")
54
+ if output_path:
55
+ record_events(output_path=output_path, events=agents_thoughts)
@@ -0,0 +1,37 @@
1
+ import requests
2
+ from minitap.mobile_use.utils.logger import get_logger
3
+
4
+ logger = get_logger(__name__)
5
+
6
+
7
+ def curl_from_request(req: requests.PreparedRequest) -> str:
8
+ """Converts a requests.PreparedRequest object to a valid cURL command string."""
9
+ command = ["curl", f"-X {req.method}"]
10
+
11
+ for key, value in req.headers.items():
12
+ command.append(f'-H "{key}: {value}"')
13
+
14
+ if req.body:
15
+ body = req.body
16
+ if isinstance(body, bytes):
17
+ body = body.decode("utf-8")
18
+ # Escape single quotes in the body for shell safety
19
+ body = body.replace("'", "'\\''")
20
+ command.append(f"-d '{body}'")
21
+
22
+ command.append(f"'{req.url}'")
23
+
24
+ return " ".join(command)
25
+
26
+
27
+ def logging_hook(response, *args, **kwargs):
28
+ """Hook to log the request as a cURL command."""
29
+ curl_command = curl_from_request(response.request)
30
+ logger.debug(f"\n--- cURL Command ---\n{curl_command}\n--------------------")
31
+
32
+
33
+ def get_session_with_curl_logging() -> requests.Session:
34
+ """Returns a requests.Session with cURL logging enabled."""
35
+ session = requests.Session()
36
+ session.hooks["response"] = [logging_hook]
37
+ return session
@@ -0,0 +1,20 @@
1
+ import subprocess
2
+
3
+
4
+ def run_shell_command_on_host(command: str) -> str:
5
+ """Helper to run a shell command on the host and return the output."""
6
+ try:
7
+ result = subprocess.run(
8
+ command,
9
+ shell=True,
10
+ check=True,
11
+ capture_output=True,
12
+ text=True,
13
+ )
14
+ return result.stdout.strip()
15
+ except subprocess.CalledProcessError as e:
16
+ # Log the error and stderr for better debugging
17
+ error_message = (
18
+ f"Command '{command}' failed with exit code {e.returncode}.\nStderr: {e.stderr.strip()}"
19
+ )
20
+ raise RuntimeError(error_message) from e
@@ -0,0 +1,6 @@
1
+ from datetime import datetime
2
+
3
+
4
+ def convert_timestamp_to_str(ts: float) -> str:
5
+ dt = datetime.fromtimestamp(ts)
6
+ return dt.strftime("%Y-%m-%dT%H-%M-%S")
@@ -0,0 +1,30 @@
1
+ from typing import Optional
2
+
3
+
4
+ def find_element_by_resource_id(ui_hierarchy: list[dict], resource_id: str) -> Optional[dict]:
5
+ """
6
+ Find a UI element by its resource-id in the UI hierarchy.
7
+
8
+ Args:
9
+ ui_hierarchy: List of UI element dictionaries
10
+ resource_id: The resource-id to search for
11
+ (e.g., "com.google.android.settings.intelligence:id/open_search_view_edit_text")
12
+
13
+ Returns:
14
+ The complete UI element dictionary if found, None otherwise
15
+ """
16
+
17
+ def search_recursive(elements: list[dict]) -> Optional[dict]:
18
+ for element in elements:
19
+ if isinstance(element, dict):
20
+ if element.get("resourceId") == resource_id:
21
+ return element
22
+
23
+ children = element.get("children", [])
24
+ if children:
25
+ result = search_recursive(children)
26
+ if result:
27
+ return result
28
+ return None
29
+
30
+ return search_recursive(ui_hierarchy)