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