droidrun 0.3.1__py3-none-any.whl → 0.3.3__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/cli/main.py CHANGED
@@ -7,13 +7,23 @@ import click
7
7
  import os
8
8
  import logging
9
9
  import warnings
10
+ from contextlib import nullcontext
10
11
  from rich.console import Console
12
+ from adbutils import adb
11
13
  from droidrun.agent.droid import DroidAgent
12
14
  from droidrun.agent.utils.llm_picker import load_llm
13
- from droidrun.adb import DeviceManager
14
15
  from droidrun.tools import AdbTools, IOSTools
16
+ from droidrun.agent.context.personas import DEFAULT, BIG_AGENT
15
17
  from functools import wraps
16
18
  from droidrun.cli.logs import LogHandler
19
+ from droidrun.telemetry import print_telemetry_message
20
+ from droidrun.portal import (
21
+ download_portal_apk,
22
+ enable_portal_accessibility,
23
+ PORTAL_PACKAGE_NAME,
24
+ ping_portal,
25
+ )
26
+ from droidrun.macro.cli import macro_cli
17
27
 
18
28
  # Suppress all warnings
19
29
  warnings.filterwarnings("ignore")
@@ -21,7 +31,6 @@ os.environ["TOKENIZERS_PARALLELISM"] = "false"
21
31
  os.environ["GRPC_ENABLE_FORK_SUPPORT"] = "false"
22
32
 
23
33
  console = Console()
24
- device_manager = DeviceManager()
25
34
 
26
35
 
27
36
  def configure_logging(goal: str, debug: bool):
@@ -30,16 +39,21 @@ def configure_logging(goal: str, debug: bool):
30
39
 
31
40
  handler = LogHandler(goal)
32
41
  handler.setFormatter(
33
- logging.Formatter("%(levelname)s %(message)s", "%H:%M:%S")
42
+ logging.Formatter("%(levelname)s %(name)s %(message)s", "%H:%M:%S")
34
43
  if debug
35
44
  else logging.Formatter("%(message)s", "%H:%M:%S")
36
45
  )
37
46
  logger.addHandler(handler)
38
47
 
39
-
40
48
  logger.setLevel(logging.DEBUG if debug else logging.INFO)
41
49
  logger.propagate = False
42
50
 
51
+ if debug:
52
+ tools_logger = logging.getLogger("droidrun-tools")
53
+ tools_logger.addHandler(handler)
54
+ tools_logger.propagate = False
55
+ tools_logger.setLevel(logging.DEBUG if debug else logging.INFO)
56
+
43
57
  return handler
44
58
 
45
59
 
@@ -64,9 +78,11 @@ async def run_command(
64
78
  reasoning: bool,
65
79
  reflection: bool,
66
80
  tracing: bool,
67
- debug: bool,
81
+ debug: bool,
82
+ use_tcp: bool,
68
83
  save_trajectory: bool = False,
69
84
  ios: bool = False,
85
+ allow_drag: bool = False,
70
86
  **kwargs,
71
87
  ):
72
88
  """Run a command on your Android device using natural language."""
@@ -78,6 +94,7 @@ async def run_command(
78
94
  with log_handler.render() as live:
79
95
  try:
80
96
  logger.info(f"🚀 Starting: {command}")
97
+ print_telemetry_message()
81
98
 
82
99
  if not kwargs.get("temperature"):
83
100
  kwargs["temperature"] = 0
@@ -87,23 +104,34 @@ async def run_command(
87
104
  # Device setup
88
105
  if device is None and not ios:
89
106
  logger.info("🔍 Finding connected device...")
90
- device_manager = DeviceManager()
91
- devices = await device_manager.list_devices()
107
+
108
+ devices = adb.list()
92
109
  if not devices:
93
110
  raise ValueError("No connected devices found.")
94
111
  device = devices[0].serial
95
112
  logger.info(f"📱 Using device: {device}")
96
113
  elif device is None and ios:
97
- raise ValueError("iOS device not specified. Please specify the device base url (http://device-ip:6643) via --device")
114
+ raise ValueError(
115
+ "iOS device not specified. Please specify the device base url (http://device-ip:6643) via --device"
116
+ )
98
117
  else:
99
118
  logger.info(f"📱 Using device: {device}")
100
119
 
101
- tools = AdbTools(serial=device) if not ios else IOSTools(url=device)
120
+ tools = AdbTools(serial=device, use_tcp=use_tcp) if not ios else IOSTools(url=device)
121
+ # Set excluded tools based on CLI flags
122
+ excluded_tools = [] if allow_drag else ["drag"]
123
+
124
+ # Select personas based on --drag flag
125
+ personas = [BIG_AGENT] if allow_drag else [DEFAULT]
102
126
 
103
127
  # LLM setup
104
128
  log_handler.update_step("Initializing LLM...")
105
129
  llm = load_llm(
106
- provider_name=provider, model=model, base_url=base_url, api_base=api_base, **kwargs
130
+ provider_name=provider,
131
+ model=model,
132
+ base_url=base_url,
133
+ api_base=api_base,
134
+ **kwargs,
107
135
  )
108
136
  logger.info(f"🧠 LLM ready: {provider}/{model}")
109
137
 
@@ -116,10 +144,13 @@ async def run_command(
116
144
  if tracing:
117
145
  logger.info("🔍 Tracing enabled")
118
146
 
147
+
119
148
  droid_agent = DroidAgent(
120
149
  goal=command,
121
150
  llm=llm,
122
151
  tools=tools,
152
+ personas=personas,
153
+ excluded_tools=excluded_tools,
123
154
  max_steps=steps,
124
155
  timeout=1000,
125
156
  vision=vision,
@@ -127,7 +158,7 @@ async def run_command(
127
158
  reflection=reflection,
128
159
  enable_tracing=tracing,
129
160
  debug=debug,
130
- save_trajectories=save_trajectory
161
+ save_trajectories=save_trajectory,
131
162
  )
132
163
 
133
164
  logger.info("▶️ Starting agent execution...")
@@ -202,13 +233,19 @@ class DroidRunCLI(click.Group):
202
233
  default=None,
203
234
  )
204
235
  @click.option(
205
- "--vision", is_flag=True, help="Enable vision capabilites by using screenshots", default=False
236
+ "--vision",
237
+ is_flag=True,
238
+ help="Enable vision capabilites by using screenshots",
239
+ default=False,
206
240
  )
207
241
  @click.option(
208
242
  "--reasoning", is_flag=True, help="Enable planning with reasoning", default=False
209
243
  )
210
244
  @click.option(
211
- "--reflection", is_flag=True, help="Enable reflection step for higher reasoning", default=False
245
+ "--reflection",
246
+ is_flag=True,
247
+ help="Enable reflection step for higher reasoning",
248
+ default=False,
212
249
  )
213
250
  @click.option(
214
251
  "--tracing", is_flag=True, help="Enable Arize Phoenix tracing", default=False
@@ -216,6 +253,9 @@ class DroidRunCLI(click.Group):
216
253
  @click.option(
217
254
  "--debug", is_flag=True, help="Enable verbose debug logging", default=False
218
255
  )
256
+ @click.option(
257
+ "--use-tcp", is_flag=True, help="Use TCP communication for device control", default=False
258
+ )
219
259
  @click.option(
220
260
  "--save-trajectory",
221
261
  is_flag=True,
@@ -236,6 +276,7 @@ def cli(
236
276
  reflection: bool,
237
277
  tracing: bool,
238
278
  debug: bool,
279
+ use_tcp: bool,
239
280
  save_trajectory: bool,
240
281
  ):
241
282
  """DroidRun - Control your Android device through LLM agents."""
@@ -271,13 +312,19 @@ def cli(
271
312
  default=None,
272
313
  )
273
314
  @click.option(
274
- "--vision", is_flag=True, help="Enable vision capabilites by using screenshots", default=False
315
+ "--vision",
316
+ is_flag=True,
317
+ help="Enable vision capabilites by using screenshots",
318
+ default=False,
275
319
  )
276
320
  @click.option(
277
321
  "--reasoning", is_flag=True, help="Enable planning with reasoning", default=False
278
322
  )
279
323
  @click.option(
280
- "--reflection", is_flag=True, help="Enable reflection step for higher reasoning", default=False
324
+ "--reflection",
325
+ is_flag=True,
326
+ help="Enable reflection step for higher reasoning",
327
+ default=False,
281
328
  )
282
329
  @click.option(
283
330
  "--tracing", is_flag=True, help="Enable Arize Phoenix tracing", default=False
@@ -285,6 +332,9 @@ def cli(
285
332
  @click.option(
286
333
  "--debug", is_flag=True, help="Enable verbose debug logging", default=False
287
334
  )
335
+ @click.option(
336
+ "--use-tcp", is_flag=True, help="Use TCP communication for device control", default=False
337
+ )
288
338
  @click.option(
289
339
  "--save-trajectory",
290
340
  is_flag=True,
@@ -292,8 +342,13 @@ def cli(
292
342
  default=False,
293
343
  )
294
344
  @click.option(
295
- "--ios", is_flag=True, help="Run on iOS device", default=False
345
+ "--drag",
346
+ "allow_drag",
347
+ is_flag=True,
348
+ help="Enable drag tool",
349
+ default=False,
296
350
  )
351
+ @click.option("--ios", is_flag=True, help="Run on iOS device", default=False)
297
352
  def run(
298
353
  command: str,
299
354
  device: str | None,
@@ -308,7 +363,9 @@ def run(
308
363
  reflection: bool,
309
364
  tracing: bool,
310
365
  debug: bool,
366
+ use_tcp: bool,
311
367
  save_trajectory: bool,
368
+ allow_drag: bool,
312
369
  ios: bool,
313
370
  ):
314
371
  """Run a command on your Android device using natural language."""
@@ -326,18 +383,19 @@ def run(
326
383
  reflection,
327
384
  tracing,
328
385
  debug,
386
+ use_tcp,
329
387
  temperature=temperature,
330
388
  save_trajectory=save_trajectory,
331
- ios=ios
389
+ allow_drag=allow_drag,
390
+ ios=ios,
332
391
  )
333
392
 
334
393
 
335
394
  @cli.command()
336
- @coro
337
- async def devices():
395
+ def devices():
338
396
  """List connected Android devices."""
339
397
  try:
340
- devices = await device_manager.list_devices()
398
+ devices = adb.list()
341
399
  if not devices:
342
400
  console.print("[yellow]No devices connected.[/]")
343
401
  return
@@ -350,28 +408,25 @@ async def devices():
350
408
 
351
409
 
352
410
  @cli.command()
353
- @click.argument("ip_address")
354
- @click.option("--port", "-p", default=5555, help="ADB port (default: 5555)")
355
- @coro
356
- async def connect(ip_address: str, port: int):
411
+ @click.argument("serial")
412
+ def connect(serial: str):
357
413
  """Connect to a device over TCP/IP."""
358
414
  try:
359
- device = await device_manager.connect(ip_address, port)
360
- if device:
361
- console.print(f"[green]Successfully connected to {ip_address}:{port}[/]")
415
+ device = adb.connect(serial)
416
+ if device.count("already connected"):
417
+ console.print(f"[green]Successfully connected to {serial}[/]")
362
418
  else:
363
- console.print(f"[red]Failed to connect to {ip_address}:{port}[/]")
419
+ console.print(f"[red]Failed to connect to {serial}: {device}[/]")
364
420
  except Exception as e:
365
421
  console.print(f"[red]Error connecting to device: {e}[/]")
366
422
 
367
423
 
368
424
  @cli.command()
369
425
  @click.argument("serial")
370
- @coro
371
- async def disconnect(serial: str):
426
+ def disconnect(serial: str):
372
427
  """Disconnect from a device."""
373
428
  try:
374
- success = await device_manager.disconnect(serial)
429
+ success = adb.disconnect(serial, raise_error=True)
375
430
  if success:
376
431
  console.print(f"[green]Successfully disconnected from {serial}[/]")
377
432
  else:
@@ -381,18 +436,16 @@ async def disconnect(serial: str):
381
436
 
382
437
 
383
438
  @cli.command()
384
- @click.option("--path", required=True, help="Path to the APK file to install")
385
439
  @click.option("--device", "-d", help="Device serial number or IP address", default=None)
386
- @coro
387
- async def setup(path: str, device: str | None):
388
- """Install an APK file and enable it as an accessibility service."""
440
+ @click.option("--path", help="Path to the Droidrun Portal APK to install on the device. If not provided, the latest portal apk version will be downloaded and installed.", default=None)
441
+ @click.option(
442
+ "--debug", is_flag=True, help="Enable verbose debug logging", default=False
443
+ )
444
+ def setup(path: str | None, device: str | None, debug: bool):
445
+ """Install and enable the DroidRun Portal on a device."""
389
446
  try:
390
- if not os.path.exists(path):
391
- console.print(f"[bold red]Error:[/] APK file not found at {path}")
392
- return
393
-
394
447
  if not device:
395
- devices = await device_manager.list_devices()
448
+ devices = adb.list()
396
449
  if not devices:
397
450
  console.print("[yellow]No devices connected.[/]")
398
451
  return
@@ -400,72 +453,108 @@ async def setup(path: str, device: str | None):
400
453
  device = devices[0].serial
401
454
  console.print(f"[blue]Using device:[/] {device}")
402
455
 
403
- device_obj = await device_manager.get_device(device)
456
+ device_obj = adb.device(device)
404
457
  if not device_obj:
405
458
  console.print(
406
459
  f"[bold red]Error:[/] Could not get device object for {device}"
407
460
  )
408
461
  return
409
-
410
- console.print(f"[bold blue]Step 1/2: Installing APK:[/] {path}")
411
- result = await device_obj.install_app(path, False, True)
412
462
 
413
- if "Error" in result:
414
- console.print(f"[bold red]Installation failed:[/] {result}")
415
- return
463
+ if not path:
464
+ console.print("[bold blue]Downloading DroidRun Portal APK...[/]")
465
+ apk_context = download_portal_apk(debug)
416
466
  else:
467
+ console.print(f"[bold blue]Using provided APK:[/] {path}")
468
+ apk_context = nullcontext(path)
469
+
470
+ with apk_context as apk_path:
471
+ if not os.path.exists(apk_path):
472
+ console.print(f"[bold red]Error:[/] APK file not found at {apk_path}")
473
+ return
474
+
475
+ console.print(f"[bold blue]Step 1/2: Installing APK:[/] {apk_path}")
476
+ try:
477
+ device_obj.install(apk_path, uninstall=True, flags=["-g"], silent=not debug)
478
+ except Exception as e:
479
+ console.print(f"[bold red]Installation failed:[/] {e}")
480
+ return
481
+
417
482
  console.print(f"[bold green]Installation successful![/]")
418
483
 
419
- console.print(f"[bold blue]Step 2/2: Enabling accessibility service[/]")
484
+ console.print(f"[bold blue]Step 2/2: Enabling accessibility service[/]")
420
485
 
421
- package = "com.droidrun.portal"
486
+ try:
487
+ enable_portal_accessibility(device_obj)
422
488
 
423
- try:
424
- await device_obj._adb.shell(
425
- device,
426
- "settings put secure enabled_accessibility_services com.droidrun.portal/com.droidrun.portal.DroidrunPortalService",
427
- )
489
+ console.print("[green]Accessibility service enabled successfully![/]")
490
+ console.print(
491
+ "\n[bold green]Setup complete![/] The DroidRun Portal is now installed and ready to use."
492
+ )
428
493
 
429
- await device_obj._adb.shell(
430
- device, "settings put secure accessibility_enabled 1"
431
- )
494
+ except Exception as e:
495
+ console.print(
496
+ f"[yellow]Could not automatically enable accessibility service: {e}[/]"
497
+ )
498
+ console.print(
499
+ "[yellow]Opening accessibility settings for manual configuration...[/]"
500
+ )
501
+
502
+ device_obj.shell(
503
+ "am start -a android.settings.ACCESSIBILITY_SETTINGS"
504
+ )
505
+
506
+ console.print(
507
+ "\n[yellow]Please complete the following steps on your device:[/]"
508
+ )
509
+ console.print(
510
+ f"1. Find [bold]{PORTAL_PACKAGE_NAME}[/] in the accessibility services list"
511
+ )
512
+ console.print("2. Tap on the service name")
513
+ console.print(
514
+ "3. Toggle the switch to [bold]ON[/] to enable the service"
515
+ )
516
+ console.print("4. Accept any permission dialogs that appear")
517
+
518
+ console.print(
519
+ "\n[bold green]APK installation complete![/] Please manually enable the accessibility service using the steps above."
520
+ )
432
521
 
433
- console.print("[green]Accessibility service enabled successfully![/]")
434
- console.print(
435
- "\n[bold green]Setup complete![/] The DroidRun Portal is now installed and ready to use."
436
- )
522
+ except Exception as e:
523
+ console.print(f"[bold red]Error:[/] {e}")
437
524
 
438
- except Exception as e:
439
- console.print(
440
- f"[yellow]Could not automatically enable accessibility service: {e}[/]"
441
- )
442
- console.print(
443
- "[yellow]Opening accessibility settings for manual configuration...[/]"
444
- )
525
+ if debug:
526
+ import traceback
445
527
 
446
- await device_obj._adb.shell(
447
- device, "am start -a android.settings.ACCESSIBILITY_SETTINGS"
448
- )
528
+ traceback.print_exc()
449
529
 
450
- console.print(
451
- "\n[yellow]Please complete the following steps on your device:[/]"
452
- )
453
- console.print(
454
- f"1. Find [bold]{package}[/] in the accessibility services list"
455
- )
456
- console.print("2. Tap on the service name")
457
- console.print("3. Toggle the switch to [bold]ON[/] to enable the service")
458
- console.print("4. Accept any permission dialogs that appear")
459
530
 
460
- console.print(
461
- "\n[bold green]APK installation complete![/] Please manually enable the accessibility service using the steps above."
462
- )
531
+ @cli.command()
532
+ @click.option("--device", "-d", help="Device serial number or IP address", default=None)
533
+ @click.option(
534
+ "--debug", is_flag=True, help="Enable verbose debug logging", default=False
535
+ )
536
+ def ping(device: str | None, debug: bool):
537
+ """Ping a device to check if it is ready and accessible."""
538
+ try:
539
+ device_obj = adb.device(device)
540
+ if not device_obj:
541
+ console.print(f"[bold red]Error:[/] Could not find device {device}")
542
+ return
463
543
 
544
+ ping_portal(device_obj, debug)
545
+ console.print(
546
+ "[bold green]Portal is installed and accessible. You're good to go![/]"
547
+ )
464
548
  except Exception as e:
465
549
  console.print(f"[bold red]Error:[/] {e}")
466
- import traceback
550
+ if debug:
551
+ import traceback
552
+
553
+ traceback.print_exc()
554
+
467
555
 
468
- traceback.print_exc()
556
+ # Add macro commands as a subgroup
557
+ cli.add_command(macro_cli, name="macro")
469
558
 
470
559
 
471
560
  if __name__ == "__main__":
@@ -481,9 +570,11 @@ if __name__ == "__main__":
481
570
  reflection = False
482
571
  tracing = True
483
572
  debug = True
573
+ use_tcp = True
484
574
  base_url = None
485
575
  api_base = None
486
576
  ios = False
577
+ allow_drag = False
487
578
  run_command(
488
579
  command=command,
489
580
  device=device,
@@ -496,8 +587,10 @@ if __name__ == "__main__":
496
587
  reflection=reflection,
497
588
  tracing=tracing,
498
589
  debug=debug,
590
+ use_tcp=use_tcp,
499
591
  base_url=base_url,
500
592
  api_base=api_base,
501
593
  api_key=api_key,
502
- ios=ios
594
+ allow_drag=allow_drag,
595
+ ios=ios,
503
596
  )
@@ -0,0 +1,14 @@
1
+ """
2
+ DroidRun Macro Module - Record and replay UI automation sequences.
3
+
4
+ This module provides functionality to replay macro sequences that were
5
+ recorded during DroidAgent execution.
6
+ """
7
+
8
+ from .replay import MacroPlayer, replay_macro_file, replay_macro_folder
9
+
10
+ __all__ = [
11
+ "MacroPlayer",
12
+ "replay_macro_file",
13
+ "replay_macro_folder"
14
+ ]
@@ -0,0 +1,10 @@
1
+ """
2
+ Entry point for running DroidRun macro CLI as a module.
3
+
4
+ Usage: python -m droidrun.macro <command>
5
+ """
6
+
7
+ from droidrun.macro.cli import macro_cli
8
+
9
+ if __name__ == "__main__":
10
+ macro_cli()