droidrun 0.3.10.dev3__py3-none-any.whl → 0.3.10.dev5__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.
- droidrun/agent/codeact/__init__.py +1 -4
- droidrun/agent/codeact/codeact_agent.py +95 -86
- droidrun/agent/codeact/events.py +1 -2
- droidrun/agent/context/__init__.py +5 -9
- droidrun/agent/context/episodic_memory.py +1 -3
- droidrun/agent/context/task_manager.py +8 -2
- droidrun/agent/droid/droid_agent.py +102 -141
- droidrun/agent/droid/events.py +45 -14
- droidrun/agent/executor/__init__.py +6 -4
- droidrun/agent/executor/events.py +29 -9
- droidrun/agent/executor/executor_agent.py +86 -28
- droidrun/agent/executor/prompts.py +8 -2
- droidrun/agent/manager/__init__.py +6 -7
- droidrun/agent/manager/events.py +16 -4
- droidrun/agent/manager/manager_agent.py +130 -69
- droidrun/agent/manager/prompts.py +1 -159
- droidrun/agent/utils/chat_utils.py +64 -2
- droidrun/agent/utils/device_state_formatter.py +54 -26
- droidrun/agent/utils/executer.py +66 -80
- droidrun/agent/utils/inference.py +11 -10
- droidrun/agent/utils/tools.py +58 -6
- droidrun/agent/utils/trajectory.py +18 -12
- droidrun/cli/logs.py +118 -56
- droidrun/cli/main.py +154 -136
- droidrun/config_manager/__init__.py +9 -7
- droidrun/config_manager/app_card_loader.py +148 -0
- droidrun/config_manager/config_manager.py +200 -102
- droidrun/config_manager/path_resolver.py +104 -0
- droidrun/config_manager/prompt_loader.py +75 -0
- droidrun/macro/__init__.py +1 -1
- droidrun/macro/cli.py +23 -18
- droidrun/telemetry/__init__.py +2 -2
- droidrun/telemetry/events.py +3 -3
- droidrun/telemetry/tracker.py +1 -1
- droidrun/tools/adb.py +1 -1
- droidrun/tools/ios.py +3 -2
- {droidrun-0.3.10.dev3.dist-info → droidrun-0.3.10.dev5.dist-info}/METADATA +10 -4
- droidrun-0.3.10.dev5.dist-info/RECORD +61 -0
- droidrun/agent/codeact/prompts.py +0 -26
- droidrun/agent/context/agent_persona.py +0 -16
- droidrun/agent/context/context_injection_manager.py +0 -66
- droidrun/agent/context/personas/__init__.py +0 -11
- droidrun/agent/context/personas/app_starter.py +0 -44
- droidrun/agent/context/personas/big_agent.py +0 -96
- droidrun/agent/context/personas/default.py +0 -95
- droidrun/agent/context/personas/ui_expert.py +0 -108
- droidrun/agent/planner/__init__.py +0 -13
- droidrun/agent/planner/events.py +0 -21
- droidrun/agent/planner/planner_agent.py +0 -311
- droidrun/agent/planner/prompts.py +0 -124
- droidrun-0.3.10.dev3.dist-info/RECORD +0 -70
- {droidrun-0.3.10.dev3.dist-info → droidrun-0.3.10.dev5.dist-info}/WHEEL +0 -0
- {droidrun-0.3.10.dev3.dist-info → droidrun-0.3.10.dev5.dist-info}/entry_points.txt +0 -0
- {droidrun-0.3.10.dev3.dist-info → droidrun-0.3.10.dev5.dist-info}/licenses/LICENSE +0 -0
droidrun/cli/main.py
CHANGED
@@ -8,15 +8,25 @@ import os
|
|
8
8
|
import warnings
|
9
9
|
from contextlib import nullcontext
|
10
10
|
from functools import wraps
|
11
|
+
from pathlib import Path
|
11
12
|
|
12
13
|
import click
|
13
14
|
from adbutils import adb
|
14
15
|
from rich.console import Console
|
15
16
|
|
16
|
-
from droidrun.agent.context.personas import BIG_AGENT, DEFAULT
|
17
17
|
from droidrun.agent.droid import DroidAgent
|
18
18
|
from droidrun.agent.utils.llm_picker import load_llm
|
19
19
|
from droidrun.cli.logs import LogHandler
|
20
|
+
from droidrun.config_manager.config_manager import (
|
21
|
+
AgentConfig,
|
22
|
+
CodeActConfig,
|
23
|
+
ManagerConfig,
|
24
|
+
ExecutorConfig,
|
25
|
+
DeviceConfig,
|
26
|
+
LoggingConfig,
|
27
|
+
ToolsConfig,
|
28
|
+
TracingConfig,
|
29
|
+
)
|
20
30
|
from droidrun.macro.cli import macro_cli
|
21
31
|
from droidrun.portal import (
|
22
32
|
PORTAL_PACKAGE_NAME,
|
@@ -28,8 +38,8 @@ from droidrun.portal import (
|
|
28
38
|
)
|
29
39
|
from droidrun.telemetry import print_telemetry_message
|
30
40
|
from droidrun.tools import AdbTools, IOSTools
|
31
|
-
from droidrun.config_manager import
|
32
|
-
|
41
|
+
from droidrun.config_manager import ConfigManager
|
42
|
+
|
33
43
|
|
34
44
|
# Suppress all warnings
|
35
45
|
warnings.filterwarnings("ignore")
|
@@ -39,11 +49,11 @@ os.environ["GRPC_ENABLE_FORK_SUPPORT"] = "false"
|
|
39
49
|
console = Console()
|
40
50
|
|
41
51
|
|
42
|
-
def configure_logging(goal: str, debug: bool):
|
52
|
+
def configure_logging(goal: str, debug: bool, rich_text: bool = True):
|
43
53
|
logger = logging.getLogger("droidrun")
|
44
54
|
logger.handlers = []
|
45
55
|
|
46
|
-
handler = LogHandler(goal)
|
56
|
+
handler = LogHandler(goal, rich_text=rich_text)
|
47
57
|
handler.setFormatter(
|
48
58
|
logging.Formatter("%(levelname)s %(name)s %(message)s", "%H:%M:%S")
|
49
59
|
if debug
|
@@ -74,20 +84,21 @@ def coro(f):
|
|
74
84
|
@coro
|
75
85
|
async def run_command(
|
76
86
|
command: str,
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
87
|
+
config_path: str | None = None,
|
88
|
+
device: str | None = None,
|
89
|
+
provider: str | None = None,
|
90
|
+
model: str | None = None,
|
91
|
+
steps: int | None = None,
|
92
|
+
base_url: str | None = None,
|
93
|
+
api_base: str | None = None,
|
94
|
+
vision: bool | None = None,
|
95
|
+
manager_vision: bool | None = None,
|
96
|
+
executor_vision: bool | None = None,
|
97
|
+
codeact_vision: bool | None = None,
|
98
|
+
reasoning: bool | None = None,
|
99
|
+
tracing: bool | None = None,
|
100
|
+
debug: bool | None = None,
|
101
|
+
use_tcp: bool | None = None,
|
91
102
|
save_trajectory: str | None = None,
|
92
103
|
ios: bool = False,
|
93
104
|
allow_drag: bool | None = None,
|
@@ -95,9 +106,11 @@ async def run_command(
|
|
95
106
|
**kwargs,
|
96
107
|
):
|
97
108
|
"""Run a command on your Android device using natural language."""
|
109
|
+
# Load custom config if provided
|
110
|
+
config = ConfigManager(config_path)
|
98
111
|
# Initialize logging first (use config default if debug not specified)
|
99
112
|
debug_mode = debug if debug is not None else config.logging.debug
|
100
|
-
log_handler = configure_logging(command, debug_mode)
|
113
|
+
log_handler = configure_logging(command, debug_mode, config.logging.rich_text)
|
101
114
|
logger = logging.getLogger("droidrun")
|
102
115
|
|
103
116
|
log_handler.update_step("Initializing...")
|
@@ -108,115 +121,105 @@ async def run_command(
|
|
108
121
|
print_telemetry_message()
|
109
122
|
|
110
123
|
# ================================================================
|
111
|
-
# STEP 1:
|
124
|
+
# STEP 1: Build config objects with CLI overrides
|
112
125
|
# ================================================================
|
113
|
-
|
114
|
-
|
115
|
-
reasoning_mode = config.agent.reasoning
|
116
|
-
vision_config = config.agent.vision
|
117
|
-
device_serial = config.device.serial
|
118
|
-
use_tcp_mode = config.device.use_tcp
|
119
|
-
debug_mode = config.logging.debug
|
120
|
-
save_traj = config.logging.save_trajectory
|
121
|
-
tracing_enabled = config.tracing.enabled
|
122
|
-
allow_drag_tool = config.tools.allow_drag
|
123
|
-
|
124
|
-
# ================================================================
|
125
|
-
# STEP 2: Apply CLI overrides if explicitly specified
|
126
|
-
# ================================================================
|
127
|
-
|
128
|
-
if steps is not None:
|
129
|
-
max_steps = steps
|
130
|
-
logger.debug(f"CLI override: max_steps={max_steps}")
|
131
|
-
|
132
|
-
if reasoning is not None:
|
133
|
-
reasoning_mode = reasoning
|
134
|
-
logger.debug(f"CLI override: reasoning={reasoning_mode}")
|
135
|
-
|
136
|
-
if debug is not None:
|
137
|
-
debug_mode = debug
|
138
|
-
logger.debug(f"CLI override: debug={debug_mode}")
|
139
|
-
|
140
|
-
if tracing is not None:
|
141
|
-
tracing_enabled = tracing
|
142
|
-
logger.debug(f"CLI override: tracing={tracing_enabled}")
|
143
|
-
|
144
|
-
if save_trajectory is not None:
|
145
|
-
save_traj = save_trajectory
|
146
|
-
logger.debug(f"CLI override: save_trajectory={save_traj}")
|
147
|
-
|
148
|
-
if use_tcp is not None:
|
149
|
-
use_tcp_mode = use_tcp
|
150
|
-
logger.debug(f"CLI override: use_tcp={use_tcp_mode}")
|
151
|
-
|
152
|
-
if device is not None:
|
153
|
-
device_serial = device
|
154
|
-
logger.debug(f"CLI override: device={device_serial}")
|
155
|
-
|
156
|
-
if allow_drag is not None:
|
157
|
-
allow_drag_tool = allow_drag
|
158
|
-
logger.debug(f"CLI override: allow_drag={allow_drag_tool}")
|
159
|
-
|
160
|
-
# Override vision settings
|
126
|
+
|
127
|
+
# Build agent-specific configs with vision overrides
|
161
128
|
if vision is not None:
|
162
|
-
#
|
163
|
-
|
129
|
+
# --vision flag overrides all agents
|
130
|
+
manager_vision_val = vision
|
131
|
+
executor_vision_val = vision
|
132
|
+
codeact_vision_val = vision
|
164
133
|
logger.debug(f"CLI override: vision={vision} (all agents)")
|
165
134
|
else:
|
166
|
-
#
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
135
|
+
# Use individual overrides or config defaults
|
136
|
+
manager_vision_val = manager_vision if manager_vision is not None else config.agent.manager.vision
|
137
|
+
executor_vision_val = executor_vision if executor_vision is not None else config.agent.executor.vision
|
138
|
+
codeact_vision_val = codeact_vision if codeact_vision is not None else config.agent.codeact.vision
|
139
|
+
|
140
|
+
manager_cfg = ManagerConfig(
|
141
|
+
vision=manager_vision_val,
|
142
|
+
system_prompt=config.agent.manager.system_prompt
|
143
|
+
)
|
144
|
+
|
145
|
+
executor_cfg = ExecutorConfig(
|
146
|
+
vision=executor_vision_val,
|
147
|
+
system_prompt=config.agent.executor.system_prompt
|
148
|
+
)
|
149
|
+
|
150
|
+
codeact_cfg = CodeActConfig(
|
151
|
+
vision=codeact_vision_val,
|
152
|
+
system_prompt=config.agent.codeact.system_prompt,
|
153
|
+
user_prompt=config.agent.codeact.user_prompt
|
154
|
+
)
|
155
|
+
|
156
|
+
agent_cfg = AgentConfig(
|
157
|
+
max_steps=steps if steps is not None else config.agent.max_steps,
|
158
|
+
reasoning=reasoning if reasoning is not None else config.agent.reasoning,
|
159
|
+
after_sleep_action=config.agent.after_sleep_action,
|
160
|
+
wait_for_stable_ui=config.agent.wait_for_stable_ui,
|
161
|
+
prompts_dir=config.agent.prompts_dir,
|
162
|
+
manager=manager_cfg,
|
163
|
+
executor=executor_cfg,
|
164
|
+
codeact=codeact_cfg,
|
165
|
+
app_cards=config.agent.app_cards,
|
166
|
+
)
|
167
|
+
|
168
|
+
device_cfg = DeviceConfig(
|
169
|
+
serial=device if device is not None else config.device.serial,
|
170
|
+
use_tcp=use_tcp if use_tcp is not None else config.device.use_tcp,
|
171
|
+
)
|
172
|
+
|
173
|
+
tools_cfg = ToolsConfig(
|
174
|
+
allow_drag=allow_drag if allow_drag is not None else config.tools.allow_drag,
|
175
|
+
)
|
176
|
+
|
177
|
+
logging_cfg = LoggingConfig(
|
178
|
+
debug=debug if debug is not None else config.logging.debug,
|
179
|
+
save_trajectory=save_trajectory if save_trajectory is not None else config.logging.save_trajectory,
|
180
|
+
rich_text=config.logging.rich_text,
|
181
|
+
)
|
182
|
+
|
183
|
+
tracing_cfg = TracingConfig(
|
184
|
+
enabled=tracing if tracing is not None else config.tracing.enabled,
|
185
|
+
)
|
186
|
+
|
184
187
|
# ================================================================
|
185
188
|
# STEP 3: Load LLMs
|
186
189
|
# ================================================================
|
187
|
-
|
190
|
+
|
188
191
|
log_handler.update_step("Loading LLMs...")
|
189
|
-
|
192
|
+
|
190
193
|
# Check if user wants custom LLM for all agents
|
191
194
|
if provider is not None or model is not None:
|
192
195
|
# User specified custom provider/model - use for all agents
|
193
196
|
logger.info("🔧 Using custom LLM for all agents")
|
194
|
-
|
197
|
+
|
195
198
|
# Use provided values or fall back to first profile's defaults
|
196
199
|
if provider is None:
|
197
200
|
provider = list(config.llm_profiles.values())[0].provider
|
198
201
|
if model is None:
|
199
202
|
model = list(config.llm_profiles.values())[0].model
|
200
|
-
|
203
|
+
|
201
204
|
# Build kwargs
|
202
205
|
llm_kwargs = {}
|
203
206
|
if temperature is not None:
|
204
207
|
llm_kwargs['temperature'] = temperature
|
205
208
|
else:
|
206
|
-
llm_kwargs['temperature'] = kwargs.get('temperature',
|
209
|
+
llm_kwargs['temperature'] = kwargs.get('temperature', 0.3)
|
207
210
|
if base_url is not None:
|
208
211
|
llm_kwargs['base_url'] = base_url
|
209
212
|
if api_base is not None:
|
210
213
|
llm_kwargs['api_base'] = api_base
|
211
214
|
llm_kwargs.update(kwargs)
|
212
|
-
|
215
|
+
|
213
216
|
# Load single LLM for all agents
|
214
217
|
custom_llm = load_llm(
|
215
218
|
provider_name=provider,
|
216
219
|
model=model,
|
217
220
|
**llm_kwargs
|
218
221
|
)
|
219
|
-
|
222
|
+
|
220
223
|
# Use same LLM for all agents
|
221
224
|
llms = {
|
222
225
|
'manager': custom_llm,
|
@@ -229,87 +232,82 @@ async def run_command(
|
|
229
232
|
else:
|
230
233
|
# No custom provider/model - use profiles from config
|
231
234
|
logger.info("📋 Loading LLMs from config profiles...")
|
232
|
-
|
235
|
+
|
233
236
|
profile_names = ['manager', 'executor', 'codeact', 'text_manipulator', 'app_opener']
|
234
|
-
|
237
|
+
|
235
238
|
# Apply temperature override to all profiles if specified
|
236
239
|
overrides = {}
|
237
240
|
if temperature is not None:
|
238
241
|
overrides = {name: {'temperature': temperature} for name in profile_names}
|
239
|
-
|
242
|
+
|
240
243
|
llms = config.load_all_llms(profile_names=profile_names, **overrides)
|
241
244
|
logger.info(f"🧠 Loaded {len(llms)} agent-specific LLMs from profiles")
|
242
|
-
|
245
|
+
|
243
246
|
# ================================================================
|
244
247
|
# STEP 4: Setup device and tools
|
245
248
|
# ================================================================
|
246
|
-
|
249
|
+
|
247
250
|
log_handler.update_step("Setting up tools...")
|
248
|
-
|
251
|
+
|
252
|
+
device_serial = device_cfg.serial
|
249
253
|
if device_serial is None and not ios:
|
250
254
|
logger.info("🔍 Finding connected device...")
|
251
255
|
devices = adb.list()
|
252
256
|
if not devices:
|
253
257
|
raise ValueError("No connected devices found.")
|
254
258
|
device_serial = devices[0].serial
|
259
|
+
device_cfg = DeviceConfig(serial=device_serial, use_tcp=device_cfg.use_tcp)
|
255
260
|
logger.info(f"📱 Using device: {device_serial}")
|
256
261
|
elif device_serial is None and ios:
|
257
|
-
raise ValueError(
|
258
|
-
"iOS device not specified. Please specify device base url via --device"
|
259
|
-
)
|
262
|
+
raise ValueError("iOS device not specified. Please specify device base url via --device")
|
260
263
|
else:
|
261
264
|
logger.info(f"📱 Using device: {device_serial}")
|
262
|
-
|
265
|
+
|
263
266
|
tools = (
|
264
267
|
AdbTools(
|
265
268
|
serial=device_serial,
|
266
|
-
use_tcp=
|
269
|
+
use_tcp=device_cfg.use_tcp,
|
267
270
|
app_opener_llm=llms.get('app_opener'),
|
268
271
|
text_manipulator_llm=llms.get('text_manipulator')
|
269
272
|
)
|
270
273
|
if not ios
|
271
274
|
else IOSTools(url=device_serial)
|
272
275
|
)
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
# Select personas based on drag flag
|
278
|
-
personas = [BIG_AGENT] if allow_drag_tool else [DEFAULT]
|
279
|
-
|
276
|
+
|
277
|
+
excluded_tools = [] if tools_cfg.allow_drag else ["drag"]
|
278
|
+
|
280
279
|
# ================================================================
|
281
280
|
# STEP 5: Initialize DroidAgent with all settings
|
282
281
|
# ================================================================
|
283
|
-
|
282
|
+
|
284
283
|
log_handler.update_step("Initializing DroidAgent...")
|
285
|
-
|
286
|
-
mode = "planning with reasoning" if
|
284
|
+
|
285
|
+
mode = "planning with reasoning" if agent_cfg.reasoning else "direct execution"
|
287
286
|
logger.info(f"🤖 Agent mode: {mode}")
|
288
|
-
logger.info(f"👁️ Vision settings: Manager={
|
289
|
-
f"Executor={
|
290
|
-
|
291
|
-
if
|
287
|
+
logger.info(f"👁️ Vision settings: Manager={agent_cfg.manager.vision}, "
|
288
|
+
f"Executor={agent_cfg.executor.vision}, CodeAct={agent_cfg.codeact.vision}")
|
289
|
+
|
290
|
+
if tracing_cfg.enabled:
|
292
291
|
logger.info("🔍 Tracing enabled")
|
293
|
-
|
292
|
+
|
294
293
|
droid_agent = DroidAgent(
|
295
294
|
goal=command,
|
296
295
|
llms=llms,
|
297
|
-
vision=vision_config,
|
298
296
|
tools=tools,
|
299
|
-
|
297
|
+
config=config,
|
298
|
+
agent_config=agent_cfg,
|
299
|
+
device_config=device_cfg,
|
300
|
+
tools_config=tools_cfg,
|
301
|
+
logging_config=logging_cfg,
|
302
|
+
tracing_config=tracing_cfg,
|
300
303
|
excluded_tools=excluded_tools,
|
301
|
-
max_steps=max_steps,
|
302
304
|
timeout=1000,
|
303
|
-
reasoning=reasoning_mode,
|
304
|
-
enable_tracing=tracing_enabled,
|
305
|
-
debug=debug_mode,
|
306
|
-
save_trajectories=save_traj,
|
307
305
|
)
|
308
|
-
|
306
|
+
|
309
307
|
# ================================================================
|
310
308
|
# STEP 6: Run agent
|
311
309
|
# ================================================================
|
312
|
-
|
310
|
+
|
313
311
|
logger.info("▶️ Starting agent execution...")
|
314
312
|
logger.info("Press Ctrl+C to stop")
|
315
313
|
log_handler.update_step("Running agent...")
|
@@ -332,24 +330,23 @@ async def run_command(
|
|
332
330
|
log_handler.is_success = False
|
333
331
|
log_handler.current_step = f"Error: {e}"
|
334
332
|
logger.error(f"💥 Error: {e}")
|
335
|
-
if
|
333
|
+
if logging_cfg.debug:
|
336
334
|
import traceback
|
337
|
-
|
338
335
|
logger.debug(traceback.format_exc())
|
339
336
|
|
340
337
|
except Exception as e:
|
341
338
|
log_handler.current_step = f"Error: {e}"
|
342
339
|
logger.error(f"💥 Setup error: {e}")
|
340
|
+
debug_mode = debug if debug is not None else config.logging.debug
|
343
341
|
if debug_mode:
|
344
342
|
import traceback
|
345
|
-
|
346
343
|
logger.debug(traceback.format_exc())
|
347
344
|
|
348
345
|
|
349
346
|
class DroidRunCLI(click.Group):
|
350
347
|
def parse_args(self, ctx, args):
|
351
348
|
# If the first arg is not an option and not a known command, treat as 'run'
|
352
|
-
if args and """not args[0].startswith("-")""" and args[0] not in self.commands:
|
349
|
+
if args and """not args[0].startswith("-")""" and args[0] not in self.commands: # TODO: the string always evaluates to True
|
353
350
|
args.insert(0, "run")
|
354
351
|
|
355
352
|
return super().parse_args(ctx, args)
|
@@ -431,6 +428,7 @@ def cli(
|
|
431
428
|
|
432
429
|
@cli.command()
|
433
430
|
@click.argument("command", type=str)
|
431
|
+
@click.option("--config", "-c", help="Path to custom config file", default=None)
|
434
432
|
@click.option("--device", "-d", help="Device serial number or IP address", default=None)
|
435
433
|
@click.option(
|
436
434
|
"--provider",
|
@@ -512,6 +510,7 @@ def cli(
|
|
512
510
|
@click.option("--ios", type=bool, default=None, help="Run on iOS device")
|
513
511
|
def run(
|
514
512
|
command: str,
|
513
|
+
config: str | None,
|
515
514
|
device: str | None,
|
516
515
|
provider: str | None,
|
517
516
|
model: str | None,
|
@@ -532,9 +531,11 @@ def run(
|
|
532
531
|
ios: bool | None,
|
533
532
|
):
|
534
533
|
"""Run a command on your Android device using natural language."""
|
534
|
+
|
535
535
|
# Call our standalone function
|
536
536
|
return run_command(
|
537
537
|
command,
|
538
|
+
config,
|
538
539
|
device,
|
539
540
|
provider,
|
540
541
|
model,
|
@@ -612,6 +613,23 @@ def disconnect(serial: str):
|
|
612
613
|
)
|
613
614
|
def setup(path: str | None, device: str | None, debug: bool):
|
614
615
|
"""Install and enable the DroidRun Portal on a device."""
|
616
|
+
from droidrun.config_manager.path_resolver import PathResolver
|
617
|
+
|
618
|
+
# Ensure config.yaml exists (check working dir, then project dir)
|
619
|
+
try:
|
620
|
+
config_path = PathResolver.resolve("config.yaml")
|
621
|
+
console.print(f"[blue]Using existing config: {config_path}[/]")
|
622
|
+
except FileNotFoundError:
|
623
|
+
# Config not found, try to create from example
|
624
|
+
try:
|
625
|
+
example_path = PathResolver.resolve("config_example.yaml")
|
626
|
+
config_path = PathResolver.resolve("config.yaml", create_if_missing=True)
|
627
|
+
|
628
|
+
import shutil
|
629
|
+
shutil.copy2(example_path, config_path)
|
630
|
+
console.print(f"[blue]Created config.yaml from example at: {config_path}[/]")
|
631
|
+
except FileNotFoundError:
|
632
|
+
console.print("[yellow]Warning: config_example.yaml not found, config.yaml not created[/]")
|
615
633
|
try:
|
616
634
|
if not device:
|
617
635
|
devices = adb.list()
|
@@ -757,5 +775,5 @@ if __name__ == "__main__":
|
|
757
775
|
save_trajectory = "none"
|
758
776
|
allow_drag = False
|
759
777
|
run_command(
|
760
|
-
command
|
778
|
+
command
|
761
779
|
)
|
@@ -1,25 +1,27 @@
|
|
1
|
+
from droidrun.config_manager.app_card_loader import AppCardLoader
|
1
2
|
from droidrun.config_manager.config_manager import (
|
3
|
+
AgentConfig,
|
4
|
+
AppCardConfig,
|
2
5
|
ConfigManager,
|
3
|
-
|
6
|
+
DeviceConfig,
|
4
7
|
DroidRunConfig,
|
5
8
|
LLMProfile,
|
6
|
-
AgentConfig,
|
7
|
-
DeviceConfig,
|
8
|
-
TelemetryConfig,
|
9
|
-
TracingConfig,
|
10
9
|
LoggingConfig,
|
10
|
+
TelemetryConfig,
|
11
11
|
ToolsConfig,
|
12
|
+
TracingConfig,
|
12
13
|
)
|
13
14
|
|
14
15
|
__all__ = [
|
15
16
|
"ConfigManager",
|
16
|
-
"config",
|
17
17
|
"DroidRunConfig",
|
18
18
|
"LLMProfile",
|
19
19
|
"AgentConfig",
|
20
|
+
"AppCardConfig",
|
20
21
|
"DeviceConfig",
|
21
22
|
"TelemetryConfig",
|
22
23
|
"TracingConfig",
|
23
24
|
"LoggingConfig",
|
24
25
|
"ToolsConfig",
|
25
|
-
|
26
|
+
"AppCardLoader",
|
27
|
+
]
|
@@ -0,0 +1,148 @@
|
|
1
|
+
"""
|
2
|
+
App card loading utility for package-specific prompts.
|
3
|
+
|
4
|
+
Supports flexible file path resolution and caches loaded content.
|
5
|
+
"""
|
6
|
+
|
7
|
+
import json
|
8
|
+
from pathlib import Path
|
9
|
+
from typing import Dict, Optional
|
10
|
+
|
11
|
+
from droidrun.config_manager.path_resolver import PathResolver
|
12
|
+
|
13
|
+
|
14
|
+
class AppCardLoader:
|
15
|
+
"""Load app cards based on package names with content caching."""
|
16
|
+
|
17
|
+
_mapping_cache: Optional[Dict[str, str]] = None
|
18
|
+
_cache_dir: Optional[str] = None
|
19
|
+
_content_cache: Dict[str, str] = {}
|
20
|
+
|
21
|
+
@staticmethod
|
22
|
+
def load_app_card(
|
23
|
+
package_name: str, app_cards_dir: str = "config/app_cards"
|
24
|
+
) -> str:
|
25
|
+
"""
|
26
|
+
Load app card for a package name.
|
27
|
+
|
28
|
+
Path resolution:
|
29
|
+
- Checks working directory first (for user overrides)
|
30
|
+
- Falls back to project directory (for default cards)
|
31
|
+
- Supports absolute paths (used as-is)
|
32
|
+
|
33
|
+
File loading from app_cards.json:
|
34
|
+
1. Relative to app_cards_dir (most common):
|
35
|
+
{"com.google.gm": "gmail.md"}
|
36
|
+
→ {app_cards_dir}/gmail.md
|
37
|
+
|
38
|
+
2. Relative path (checks working dir, then project dir):
|
39
|
+
{"com.google.gm": "config/custom_cards/gmail.md"}
|
40
|
+
|
41
|
+
3. Absolute path:
|
42
|
+
{"com.google.gm": "/usr/share/droidrun/cards/gmail.md"}
|
43
|
+
|
44
|
+
Args:
|
45
|
+
package_name: Android package name (e.g., "com.google.android.gm")
|
46
|
+
app_cards_dir: Directory path (relative or absolute)
|
47
|
+
|
48
|
+
Returns:
|
49
|
+
App card content or empty string if not found
|
50
|
+
"""
|
51
|
+
if not package_name:
|
52
|
+
return ""
|
53
|
+
|
54
|
+
# Check content cache first (key: package_name:app_cards_dir)
|
55
|
+
cache_key = f"{package_name}:{app_cards_dir}"
|
56
|
+
if cache_key in AppCardLoader._content_cache:
|
57
|
+
return AppCardLoader._content_cache[cache_key]
|
58
|
+
|
59
|
+
# Load mapping (with cache)
|
60
|
+
mapping = AppCardLoader._load_mapping(app_cards_dir)
|
61
|
+
|
62
|
+
# Get file path from mapping
|
63
|
+
if package_name not in mapping:
|
64
|
+
# Cache the empty result to avoid repeated lookups
|
65
|
+
AppCardLoader._content_cache[cache_key] = ""
|
66
|
+
return ""
|
67
|
+
|
68
|
+
file_path_str = mapping[package_name]
|
69
|
+
file_path = Path(file_path_str)
|
70
|
+
|
71
|
+
# Determine resolution strategy
|
72
|
+
if file_path.is_absolute():
|
73
|
+
# Absolute path: use as-is
|
74
|
+
app_card_path = file_path
|
75
|
+
elif file_path_str.startswith(("config/", "prompts/", "docs/")):
|
76
|
+
# Project-relative path: resolve with unified resolver
|
77
|
+
app_card_path = PathResolver.resolve(file_path_str)
|
78
|
+
else:
|
79
|
+
# App_cards-relative: resolve dir first, then append filename
|
80
|
+
cards_dir_resolved = PathResolver.resolve(app_cards_dir)
|
81
|
+
app_card_path = cards_dir_resolved / file_path_str
|
82
|
+
|
83
|
+
# Read file
|
84
|
+
try:
|
85
|
+
if not app_card_path.exists():
|
86
|
+
# Cache the empty result
|
87
|
+
AppCardLoader._content_cache[cache_key] = ""
|
88
|
+
return ""
|
89
|
+
|
90
|
+
content = app_card_path.read_text(encoding="utf-8")
|
91
|
+
# Cache the content
|
92
|
+
AppCardLoader._content_cache[cache_key] = content
|
93
|
+
return content
|
94
|
+
except Exception:
|
95
|
+
# Cache the empty result on error
|
96
|
+
AppCardLoader._content_cache[cache_key] = ""
|
97
|
+
return ""
|
98
|
+
|
99
|
+
@staticmethod
|
100
|
+
def _load_mapping(app_cards_dir: str) -> Dict[str, str]:
|
101
|
+
"""Load and cache the app_cards.json mapping."""
|
102
|
+
# Cache invalidation: if dir changed, reload
|
103
|
+
if (
|
104
|
+
AppCardLoader._mapping_cache is not None
|
105
|
+
and AppCardLoader._cache_dir == app_cards_dir
|
106
|
+
):
|
107
|
+
return AppCardLoader._mapping_cache
|
108
|
+
|
109
|
+
# Resolve app cards directory
|
110
|
+
cards_dir_resolved = PathResolver.resolve(app_cards_dir)
|
111
|
+
mapping_path = cards_dir_resolved / "app_cards.json"
|
112
|
+
|
113
|
+
try:
|
114
|
+
if not mapping_path.exists():
|
115
|
+
AppCardLoader._mapping_cache = {}
|
116
|
+
AppCardLoader._cache_dir = app_cards_dir
|
117
|
+
return {}
|
118
|
+
|
119
|
+
with open(mapping_path, "r", encoding="utf-8") as f:
|
120
|
+
mapping = json.load(f)
|
121
|
+
|
122
|
+
AppCardLoader._mapping_cache = mapping
|
123
|
+
AppCardLoader._cache_dir = app_cards_dir
|
124
|
+
return mapping
|
125
|
+
except Exception:
|
126
|
+
AppCardLoader._mapping_cache = {}
|
127
|
+
AppCardLoader._cache_dir = app_cards_dir
|
128
|
+
return {}
|
129
|
+
|
130
|
+
@staticmethod
|
131
|
+
def clear_cache() -> None:
|
132
|
+
"""Clear all caches (useful for testing or runtime reloading)."""
|
133
|
+
AppCardLoader._mapping_cache = None
|
134
|
+
AppCardLoader._cache_dir = None
|
135
|
+
AppCardLoader._content_cache.clear()
|
136
|
+
|
137
|
+
@staticmethod
|
138
|
+
def get_cache_stats() -> Dict[str, int]:
|
139
|
+
"""
|
140
|
+
Get cache statistics.
|
141
|
+
|
142
|
+
Returns:
|
143
|
+
Dict with cache stats (useful for debugging)
|
144
|
+
"""
|
145
|
+
return {
|
146
|
+
"mapping_cached": 1 if AppCardLoader._mapping_cache is not None else 0,
|
147
|
+
"content_entries": len(AppCardLoader._content_cache),
|
148
|
+
}
|