droidrun 0.3.0__py3-none-any.whl → 0.3.2__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,21 @@ 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
11
12
  from droidrun.agent.droid import DroidAgent
12
13
  from droidrun.agent.utils.llm_picker import load_llm
13
14
  from droidrun.adb import DeviceManager
14
- from droidrun.tools import AdbTools, IOSTools, Tools
15
+ from droidrun.tools import AdbTools, IOSTools
15
16
  from functools import wraps
16
17
  from droidrun.cli.logs import LogHandler
18
+ from droidrun.telemetry import print_telemetry_message
19
+ from droidrun.portal import (
20
+ download_portal_apk,
21
+ enable_portal_accessibility,
22
+ PORTAL_PACKAGE_NAME,
23
+ ping_portal,
24
+ )
17
25
 
18
26
  # Suppress all warnings
19
27
  warnings.filterwarnings("ignore")
@@ -36,7 +44,6 @@ def configure_logging(goal: str, debug: bool):
36
44
  )
37
45
  logger.addHandler(handler)
38
46
 
39
-
40
47
  logger.setLevel(logging.DEBUG if debug else logging.INFO)
41
48
  logger.propagate = False
42
49
 
@@ -59,6 +66,8 @@ async def run_command(
59
66
  model: str,
60
67
  steps: int,
61
68
  base_url: str,
69
+ api_base: str,
70
+ vision: bool,
62
71
  reasoning: bool,
63
72
  reflection: bool,
64
73
  tracing: bool,
@@ -76,6 +85,7 @@ async def run_command(
76
85
  with log_handler.render() as live:
77
86
  try:
78
87
  logger.info(f"🚀 Starting: {command}")
88
+ print_telemetry_message()
79
89
 
80
90
  if not kwargs.get("temperature"):
81
91
  kwargs["temperature"] = 0
@@ -92,7 +102,9 @@ async def run_command(
92
102
  device = devices[0].serial
93
103
  logger.info(f"📱 Using device: {device}")
94
104
  elif device is None and ios:
95
- raise ValueError("iOS device not specified. Please specify the device base url (http://device-ip:6643) via --device")
105
+ raise ValueError(
106
+ "iOS device not specified. Please specify the device base url (http://device-ip:6643) via --device"
107
+ )
96
108
  else:
97
109
  logger.info(f"📱 Using device: {device}")
98
110
 
@@ -101,7 +113,11 @@ async def run_command(
101
113
  # LLM setup
102
114
  log_handler.update_step("Initializing LLM...")
103
115
  llm = load_llm(
104
- provider_name=provider, model=model, base_url=base_url, **kwargs
116
+ provider_name=provider,
117
+ model=model,
118
+ base_url=base_url,
119
+ api_base=api_base,
120
+ **kwargs,
105
121
  )
106
122
  logger.info(f"🧠 LLM ready: {provider}/{model}")
107
123
 
@@ -120,11 +136,12 @@ async def run_command(
120
136
  tools=tools,
121
137
  max_steps=steps,
122
138
  timeout=1000,
139
+ vision=vision,
123
140
  reasoning=reasoning,
124
141
  reflection=reflection,
125
142
  enable_tracing=tracing,
126
143
  debug=debug,
127
- save_trajectories=save_trajectory
144
+ save_trajectories=save_trajectory,
128
145
  )
129
146
 
130
147
  logger.info("▶️ Starting agent execution...")
@@ -176,14 +193,14 @@ class DroidRunCLI(click.Group):
176
193
  @click.option(
177
194
  "--provider",
178
195
  "-p",
179
- help="LLM provider (OpenAI, Ollama, Anthropic, Gemini, DeepSeek)",
180
- default="Gemini",
196
+ help="LLM provider (OpenAI, Ollama, Anthropic, GoogleGenAI, DeepSeek)",
197
+ default="GoogleGenAI",
181
198
  )
182
199
  @click.option(
183
200
  "--model",
184
201
  "-m",
185
202
  help="LLM model name",
186
- default="models/gemini-2.5-pro",
203
+ default="models/gemini-2.5-flash",
187
204
  )
188
205
  @click.option("--temperature", type=float, help="Temperature for LLM", default=0.2)
189
206
  @click.option("--steps", type=int, help="Maximum number of steps", default=15)
@@ -194,10 +211,24 @@ class DroidRunCLI(click.Group):
194
211
  default=None,
195
212
  )
196
213
  @click.option(
197
- "--reasoning", is_flag=True, help="Enable/disable planning with reasoning", default=False
214
+ "--api_base",
215
+ help="Base URL for API (e.g., OpenAI, OpenAI-Like)",
216
+ default=None,
198
217
  )
199
218
  @click.option(
200
- "--reflection", is_flag=True, help="Enable reflection step for higher reasoning", default=False
219
+ "--vision",
220
+ is_flag=True,
221
+ help="Enable vision capabilites by using screenshots",
222
+ default=False,
223
+ )
224
+ @click.option(
225
+ "--reasoning", is_flag=True, help="Enable planning with reasoning", default=False
226
+ )
227
+ @click.option(
228
+ "--reflection",
229
+ is_flag=True,
230
+ help="Enable reflection step for higher reasoning",
231
+ default=False,
201
232
  )
202
233
  @click.option(
203
234
  "--tracing", is_flag=True, help="Enable Arize Phoenix tracing", default=False
@@ -218,7 +249,9 @@ def cli(
218
249
  model: str,
219
250
  steps: int,
220
251
  base_url: str,
252
+ api_base: str,
221
253
  temperature: float,
254
+ vision: bool,
222
255
  reasoning: bool,
223
256
  reflection: bool,
224
257
  tracing: bool,
@@ -235,14 +268,14 @@ def cli(
235
268
  @click.option(
236
269
  "--provider",
237
270
  "-p",
238
- help="LLM provider (OpenAI, Ollama, Anthropic, Gemini, DeepSeek)",
239
- default="Gemini",
271
+ help="LLM provider (OpenAI, Ollama, Anthropic, GoogleGenAI, DeepSeek)",
272
+ default="GoogleGenAI",
240
273
  )
241
274
  @click.option(
242
275
  "--model",
243
276
  "-m",
244
277
  help="LLM model name",
245
- default="models/gemini-2.5-pro",
278
+ default="models/gemini-2.5-flash",
246
279
  )
247
280
  @click.option("--temperature", type=float, help="Temperature for LLM", default=0.2)
248
281
  @click.option("--steps", type=int, help="Maximum number of steps", default=15)
@@ -253,10 +286,24 @@ def cli(
253
286
  default=None,
254
287
  )
255
288
  @click.option(
256
- "--reasoning", is_flag=True, help="Enable/disable planning with reasoning", default=False
289
+ "--api_base",
290
+ help="Base URL for API (e.g., OpenAI or OpenAI-Like)",
291
+ default=None,
292
+ )
293
+ @click.option(
294
+ "--vision",
295
+ is_flag=True,
296
+ help="Enable vision capabilites by using screenshots",
297
+ default=False,
257
298
  )
258
299
  @click.option(
259
- "--reflection", is_flag=True, help="Enable reflection step for higher reasoning", default=False
300
+ "--reasoning", is_flag=True, help="Enable planning with reasoning", default=False
301
+ )
302
+ @click.option(
303
+ "--reflection",
304
+ is_flag=True,
305
+ help="Enable reflection step for higher reasoning",
306
+ default=False,
260
307
  )
261
308
  @click.option(
262
309
  "--tracing", is_flag=True, help="Enable Arize Phoenix tracing", default=False
@@ -270,9 +317,7 @@ def cli(
270
317
  help="Save agent trajectory to file",
271
318
  default=False,
272
319
  )
273
- @click.option(
274
- "--ios", is_flag=True, help="Run on iOS device", default=False
275
- )
320
+ @click.option("--ios", is_flag=True, help="Run on iOS device", default=False)
276
321
  def run(
277
322
  command: str,
278
323
  device: str | None,
@@ -280,7 +325,9 @@ def run(
280
325
  model: str,
281
326
  steps: int,
282
327
  base_url: str,
328
+ api_base: str,
283
329
  temperature: float,
330
+ vision: bool,
284
331
  reasoning: bool,
285
332
  reflection: bool,
286
333
  tracing: bool,
@@ -297,13 +344,15 @@ def run(
297
344
  model,
298
345
  steps,
299
346
  base_url,
347
+ api_base,
348
+ vision,
300
349
  reasoning,
301
350
  reflection,
302
351
  tracing,
303
352
  debug,
304
353
  temperature=temperature,
305
354
  save_trajectory=save_trajectory,
306
- ios=ios
355
+ ios=ios,
307
356
  )
308
357
 
309
358
 
@@ -325,17 +374,17 @@ async def devices():
325
374
 
326
375
 
327
376
  @cli.command()
328
- @click.argument("ip_address")
377
+ @click.argument("serial")
329
378
  @click.option("--port", "-p", default=5555, help="ADB port (default: 5555)")
330
379
  @coro
331
- async def connect(ip_address: str, port: int):
380
+ async def connect(serial: str, port: int):
332
381
  """Connect to a device over TCP/IP."""
333
382
  try:
334
- device = await device_manager.connect(ip_address, port)
383
+ device = await device_manager.connect(serial, port)
335
384
  if device:
336
- console.print(f"[green]Successfully connected to {ip_address}:{port}[/]")
385
+ console.print(f"[green]Successfully connected to {serial}:{port}[/]")
337
386
  else:
338
- console.print(f"[red]Failed to connect to {ip_address}:{port}[/]")
387
+ console.print(f"[red]Failed to connect to {serial}:{port}[/]")
339
388
  except Exception as e:
340
389
  console.print(f"[red]Error connecting to device: {e}[/]")
341
390
 
@@ -356,16 +405,15 @@ async def disconnect(serial: str):
356
405
 
357
406
 
358
407
  @cli.command()
359
- @click.option("--path", required=True, help="Path to the APK file to install")
360
408
  @click.option("--device", "-d", help="Device serial number or IP address", default=None)
409
+ @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)
410
+ @click.option(
411
+ "--debug", is_flag=True, help="Enable verbose debug logging", default=False
412
+ )
361
413
  @coro
362
- async def setup(path: str, device: str | None):
363
- """Install an APK file and enable it as an accessibility service."""
414
+ async def setup(path: str | None, device: str | None, debug: bool):
415
+ """Install and enable the DroidRun Portal on a device."""
364
416
  try:
365
- if not os.path.exists(path):
366
- console.print(f"[bold red]Error:[/] APK file not found at {path}")
367
- return
368
-
369
417
  if not device:
370
418
  devices = await device_manager.list_devices()
371
419
  if not devices:
@@ -381,66 +429,99 @@ async def setup(path: str, device: str | None):
381
429
  f"[bold red]Error:[/] Could not get device object for {device}"
382
430
  )
383
431
  return
384
- tools = Tools(serial=device)
385
- console.print(f"[bold blue]Step 1/2: Installing APK:[/] {path}")
386
- result = await tools.install_app(path, False, True)
387
432
 
388
- if "Error" in result:
389
- console.print(f"[bold red]Installation failed:[/] {result}")
390
- return
433
+ if not path:
434
+ console.print("[bold blue]Downloading DroidRun Portal APK...[/]")
435
+ apk_context = download_portal_apk(debug)
391
436
  else:
392
- console.print(f"[bold green]Installation successful![/]")
437
+ console.print(f"[bold blue]Using provided APK:[/] {path}")
438
+ apk_context = nullcontext(path)
393
439
 
394
- console.print(f"[bold blue]Step 2/2: Enabling accessibility service[/]")
440
+ with apk_context as apk_path:
441
+ if not os.path.exists(apk_path):
442
+ console.print(f"[bold red]Error:[/] APK file not found at {apk_path}")
443
+ return
395
444
 
396
- package = "com.droidrun.portal"
445
+ console.print(f"[bold blue]Step 1/2: Installing APK:[/] {apk_path}")
446
+ result = await device_obj.install_app(apk_path, True, True)
397
447
 
398
- try:
399
- await device_obj._adb.shell(
400
- device,
401
- "settings put secure enabled_accessibility_services com.droidrun.portal/com.droidrun.portal.DroidrunPortalService",
402
- )
448
+ if "Error" in result:
449
+ console.print(f"[bold red]Installation failed:[/] {result}")
450
+ return
451
+ else:
452
+ console.print(f"[bold green]Installation successful![/]")
403
453
 
404
- await device_obj._adb.shell(
405
- device, "settings put secure accessibility_enabled 1"
406
- )
454
+ console.print(f"[bold blue]Step 2/2: Enabling accessibility service[/]")
407
455
 
408
- console.print("[green]Accessibility service enabled successfully![/]")
409
- console.print(
410
- "\n[bold green]Setup complete![/] The DroidRun Portal is now installed and ready to use."
411
- )
456
+ try:
457
+ await enable_portal_accessibility(device_obj)
412
458
 
413
- except Exception as e:
414
- console.print(
415
- f"[yellow]Could not automatically enable accessibility service: {e}[/]"
416
- )
417
- console.print(
418
- "[yellow]Opening accessibility settings for manual configuration...[/]"
419
- )
459
+ console.print("[green]Accessibility service enabled successfully![/]")
460
+ console.print(
461
+ "\n[bold green]Setup complete![/] The DroidRun Portal is now installed and ready to use."
462
+ )
420
463
 
421
- await device_obj._adb.shell(
422
- device, "am start -a android.settings.ACCESSIBILITY_SETTINGS"
423
- )
464
+ except Exception as e:
465
+ console.print(
466
+ f"[yellow]Could not automatically enable accessibility service: {e}[/]"
467
+ )
468
+ console.print(
469
+ "[yellow]Opening accessibility settings for manual configuration...[/]"
470
+ )
471
+
472
+ await device_obj.shell(
473
+ "am start -a android.settings.ACCESSIBILITY_SETTINGS"
474
+ )
475
+
476
+ console.print(
477
+ "\n[yellow]Please complete the following steps on your device:[/]"
478
+ )
479
+ console.print(
480
+ f"1. Find [bold]{PORTAL_PACKAGE_NAME}[/] in the accessibility services list"
481
+ )
482
+ console.print("2. Tap on the service name")
483
+ console.print(
484
+ "3. Toggle the switch to [bold]ON[/] to enable the service"
485
+ )
486
+ console.print("4. Accept any permission dialogs that appear")
487
+
488
+ console.print(
489
+ "\n[bold green]APK installation complete![/] Please manually enable the accessibility service using the steps above."
490
+ )
424
491
 
425
- console.print(
426
- "\n[yellow]Please complete the following steps on your device:[/]"
427
- )
428
- console.print(
429
- f"1. Find [bold]{package}[/] in the accessibility services list"
430
- )
431
- console.print("2. Tap on the service name")
432
- console.print("3. Toggle the switch to [bold]ON[/] to enable the service")
433
- console.print("4. Accept any permission dialogs that appear")
492
+ except Exception as e:
493
+ console.print(f"[bold red]Error:[/] {e}")
434
494
 
435
- console.print(
436
- "\n[bold green]APK installation complete![/] Please manually enable the accessibility service using the steps above."
437
- )
495
+ if debug:
496
+ import traceback
497
+
498
+ traceback.print_exc()
499
+
500
+
501
+ @cli.command()
502
+ @click.option("--device", "-d", help="Device serial number or IP address", default=None)
503
+ @click.option(
504
+ "--debug", is_flag=True, help="Enable verbose debug logging", default=False
505
+ )
506
+ @coro
507
+ async def ping(device: str | None, debug: bool):
508
+ """Ping a device to check if it is ready and accessible."""
509
+ try:
510
+ device_obj = await device_manager.get_device(device)
511
+ if not device_obj:
512
+ console.print(f"[bold red]Error:[/] Could not find device {device}")
513
+ return
438
514
 
515
+ await ping_portal(device_obj, debug)
516
+ console.print(
517
+ "[bold green]Portal is installed and accessible. You're good to go![/]"
518
+ )
439
519
  except Exception as e:
440
520
  console.print(f"[bold red]Error:[/] {e}")
441
- import traceback
521
+ if debug:
522
+ import traceback
442
523
 
443
- traceback.print_exc()
524
+ traceback.print_exc()
444
525
 
445
526
 
446
527
  if __name__ == "__main__":
@@ -449,13 +530,15 @@ if __name__ == "__main__":
449
530
  provider = "GoogleGenAI"
450
531
  model = "models/gemini-2.5-flash"
451
532
  temperature = 0
452
- api_key = os.getenv("GEMINI_API_KEY")
533
+ api_key = os.getenv("GOOGLE_API_KEY")
453
534
  steps = 15
535
+ vision = True
454
536
  reasoning = True
455
537
  reflection = False
456
538
  tracing = True
457
539
  debug = True
458
540
  base_url = None
541
+ api_base = None
459
542
  ios = False
460
543
  run_command(
461
544
  command=command,
@@ -464,11 +547,13 @@ if __name__ == "__main__":
464
547
  model=model,
465
548
  steps=steps,
466
549
  temperature=temperature,
550
+ vision=vision,
467
551
  reasoning=reasoning,
468
552
  reflection=reflection,
469
553
  tracing=tracing,
470
554
  debug=debug,
471
555
  base_url=base_url,
556
+ api_base=api_base,
472
557
  api_key=api_key,
473
- ios=ios
558
+ ios=ios,
474
559
  )
droidrun/portal.py ADDED
@@ -0,0 +1,139 @@
1
+ import requests
2
+ import tempfile
3
+ import os
4
+ import contextlib
5
+ from droidrun.adb import Device, DeviceManager
6
+ import asyncio
7
+
8
+ REPO = "droidrun/droidrun-portal"
9
+ ASSET_NAME = "droidrun-portal"
10
+ GITHUB_API_HOSTS = ["https://api.github.com", "https://ungh.cc"]
11
+
12
+ PORTAL_PACKAGE_NAME = "com.droidrun.portal"
13
+ A11Y_SERVICE_NAME = (
14
+ f"{PORTAL_PACKAGE_NAME}/com.droidrun.portal.DroidrunAccessibilityService"
15
+ )
16
+
17
+
18
+ def get_latest_release_assets(debug: bool = False):
19
+ for host in GITHUB_API_HOSTS:
20
+ url = f"{host}/repos/{REPO}/releases/latest"
21
+ response = requests.get(url)
22
+ if response.status_code == 200:
23
+ if debug:
24
+ print(f"Using GitHub release on {host}")
25
+ break
26
+
27
+ response.raise_for_status()
28
+ latest_release = response.json()
29
+
30
+ if "release" in latest_release:
31
+ assets = latest_release["release"]["assets"]
32
+ else:
33
+ assets = latest_release.get("assets", [])
34
+
35
+ return assets
36
+
37
+
38
+ @contextlib.contextmanager
39
+ def download_portal_apk(debug: bool = False):
40
+ assets = get_latest_release_assets(debug)
41
+
42
+ asset_url = None
43
+ for asset in assets:
44
+ if (
45
+ "browser_download_url" in asset
46
+ and "name" in asset
47
+ and asset["name"].startswith(ASSET_NAME)
48
+ ):
49
+ asset_url = asset["browser_download_url"]
50
+ break
51
+ elif "downloadUrl" in asset and os.path.basename(
52
+ asset["downloadUrl"]
53
+ ).startswith(ASSET_NAME):
54
+ asset_url = asset["downloadUrl"]
55
+ break
56
+ else:
57
+ if debug:
58
+ print(asset)
59
+
60
+ if not asset_url:
61
+ raise Exception(f"Asset named '{ASSET_NAME}' not found in the latest release.")
62
+
63
+ tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".apk")
64
+ try:
65
+ r = requests.get(asset_url, stream=True)
66
+ r.raise_for_status()
67
+ for chunk in r.iter_content(chunk_size=8192):
68
+ if chunk:
69
+ tmp.write(chunk)
70
+ tmp.close()
71
+ yield tmp.name
72
+ finally:
73
+ if os.path.exists(tmp.name):
74
+ os.unlink(tmp.name)
75
+
76
+
77
+ async def enable_portal_accessibility(device: Device):
78
+ await device.shell(
79
+ f"settings put secure enabled_accessibility_services {A11Y_SERVICE_NAME}"
80
+ )
81
+ await device.shell("settings put secure accessibility_enabled 1")
82
+
83
+
84
+ async def check_portal_accessibility(device: Device, debug: bool = False) -> bool:
85
+ a11y_services = await device.shell(
86
+ "settings get secure enabled_accessibility_services"
87
+ )
88
+ if not A11Y_SERVICE_NAME in a11y_services:
89
+ if debug:
90
+ print(a11y_services)
91
+ return False
92
+
93
+ a11y_enabled = await device.shell("settings get secure accessibility_enabled")
94
+ if a11y_enabled != "1":
95
+ if debug:
96
+ print(a11y_enabled)
97
+ return False
98
+
99
+ return True
100
+
101
+
102
+ async def ping_portal(device: Device, debug: bool = False):
103
+ """
104
+ Ping the Droidrun Portal to check if it is installed and accessible.
105
+ """
106
+ try:
107
+ packages = await device.list_packages()
108
+ except Exception as e:
109
+ raise Exception(f"Failed to list packages: {e}")
110
+
111
+ if not PORTAL_PACKAGE_NAME in packages:
112
+ if debug:
113
+ print(packages)
114
+ raise Exception("Portal is not installed on the device")
115
+
116
+ if not await check_portal_accessibility(device, debug):
117
+ await device.shell("am start -a android.settings.ACCESSIBILITY_SETTINGS")
118
+ raise Exception(
119
+ "Droidrun Portal is not enabled as an accessibility service on the device"
120
+ )
121
+
122
+ try:
123
+ state = await device.shell(
124
+ "content query --uri content://com.droidrun.portal/state"
125
+ )
126
+ if not "Row: 0 result=" in state:
127
+ raise Exception("Failed to get state from Droidrun Portal")
128
+
129
+ except Exception as e:
130
+ raise Exception(f"Droidrun Portal is not reachable: {e}")
131
+
132
+
133
+ async def test():
134
+ device = await DeviceManager().get_device()
135
+ await ping_portal(device, debug=False)
136
+
137
+
138
+ if __name__ == "__main__":
139
+ asyncio.run(test())
@@ -0,0 +1,4 @@
1
+ from .tracker import capture, flush, print_telemetry_message
2
+ from .events import DroidAgentInitEvent, DroidAgentFinalizeEvent
3
+
4
+ __all__ = ["capture", "flush", "DroidAgentInitEvent", "DroidAgentFinalizeEvent", "print_telemetry_message"]
@@ -0,0 +1,27 @@
1
+ from typing import List
2
+ from droidrun.agent.context import Task
3
+ from pydantic import BaseModel
4
+
5
+ class TelemetryEvent(BaseModel):
6
+ pass
7
+
8
+ class DroidAgentInitEvent(TelemetryEvent):
9
+ goal: str
10
+ llm: str
11
+ tools: str
12
+ personas: str
13
+ max_steps: int
14
+ timeout: int
15
+ vision: bool
16
+ reasoning: bool
17
+ reflection: bool
18
+ enable_tracing: bool
19
+ debug: bool
20
+ save_trajectories: bool
21
+
22
+
23
+ class DroidAgentFinalizeEvent(TelemetryEvent):
24
+ tasks: str
25
+ success: bool
26
+ output: str
27
+ steps: int