droidrun 0.3.1__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/__init__.py +1 -10
- droidrun/adb/device.py +101 -71
- droidrun/adb/manager.py +3 -3
- droidrun/agent/codeact/codeact_agent.py +2 -1
- droidrun/agent/context/personas/__init__.py +0 -2
- droidrun/agent/droid/droid_agent.py +50 -7
- droidrun/agent/droid/events.py +4 -0
- droidrun/agent/utils/llm_picker.py +1 -0
- droidrun/cli/main.py +126 -70
- droidrun/portal.py +139 -0
- droidrun/telemetry/__init__.py +4 -0
- droidrun/telemetry/events.py +27 -0
- droidrun/telemetry/tracker.py +83 -0
- droidrun/tools/adb.py +82 -218
- droidrun/tools/ios.py +6 -3
- droidrun/tools/tools.py +41 -6
- {droidrun-0.3.1.dist-info → droidrun-0.3.2.dist-info}/METADATA +17 -28
- {droidrun-0.3.1.dist-info → droidrun-0.3.2.dist-info}/RECORD +21 -18
- droidrun/agent/context/personas/extractor.py +0 -52
- {droidrun-0.3.1.dist-info → droidrun-0.3.2.dist-info}/WHEEL +0 -0
- {droidrun-0.3.1.dist-info → droidrun-0.3.2.dist-info}/entry_points.txt +0 -0
- {droidrun-0.3.1.dist-info → droidrun-0.3.2.dist-info}/licenses/LICENSE +0 -0
droidrun/cli/main.py
CHANGED
@@ -7,6 +7,7 @@ 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
|
@@ -14,6 +15,13 @@ from droidrun.adb import DeviceManager
|
|
14
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
|
|
@@ -78,6 +85,7 @@ async def run_command(
|
|
78
85
|
with log_handler.render() as live:
|
79
86
|
try:
|
80
87
|
logger.info(f"🚀 Starting: {command}")
|
88
|
+
print_telemetry_message()
|
81
89
|
|
82
90
|
if not kwargs.get("temperature"):
|
83
91
|
kwargs["temperature"] = 0
|
@@ -94,7 +102,9 @@ async def run_command(
|
|
94
102
|
device = devices[0].serial
|
95
103
|
logger.info(f"📱 Using device: {device}")
|
96
104
|
elif device is None and ios:
|
97
|
-
raise ValueError(
|
105
|
+
raise ValueError(
|
106
|
+
"iOS device not specified. Please specify the device base url (http://device-ip:6643) via --device"
|
107
|
+
)
|
98
108
|
else:
|
99
109
|
logger.info(f"📱 Using device: {device}")
|
100
110
|
|
@@ -103,7 +113,11 @@ async def run_command(
|
|
103
113
|
# LLM setup
|
104
114
|
log_handler.update_step("Initializing LLM...")
|
105
115
|
llm = load_llm(
|
106
|
-
provider_name=provider,
|
116
|
+
provider_name=provider,
|
117
|
+
model=model,
|
118
|
+
base_url=base_url,
|
119
|
+
api_base=api_base,
|
120
|
+
**kwargs,
|
107
121
|
)
|
108
122
|
logger.info(f"🧠 LLM ready: {provider}/{model}")
|
109
123
|
|
@@ -127,7 +141,7 @@ async def run_command(
|
|
127
141
|
reflection=reflection,
|
128
142
|
enable_tracing=tracing,
|
129
143
|
debug=debug,
|
130
|
-
save_trajectories=save_trajectory
|
144
|
+
save_trajectories=save_trajectory,
|
131
145
|
)
|
132
146
|
|
133
147
|
logger.info("▶️ Starting agent execution...")
|
@@ -202,13 +216,19 @@ class DroidRunCLI(click.Group):
|
|
202
216
|
default=None,
|
203
217
|
)
|
204
218
|
@click.option(
|
205
|
-
"--vision",
|
219
|
+
"--vision",
|
220
|
+
is_flag=True,
|
221
|
+
help="Enable vision capabilites by using screenshots",
|
222
|
+
default=False,
|
206
223
|
)
|
207
224
|
@click.option(
|
208
225
|
"--reasoning", is_flag=True, help="Enable planning with reasoning", default=False
|
209
226
|
)
|
210
227
|
@click.option(
|
211
|
-
"--reflection",
|
228
|
+
"--reflection",
|
229
|
+
is_flag=True,
|
230
|
+
help="Enable reflection step for higher reasoning",
|
231
|
+
default=False,
|
212
232
|
)
|
213
233
|
@click.option(
|
214
234
|
"--tracing", is_flag=True, help="Enable Arize Phoenix tracing", default=False
|
@@ -271,13 +291,19 @@ def cli(
|
|
271
291
|
default=None,
|
272
292
|
)
|
273
293
|
@click.option(
|
274
|
-
"--vision",
|
294
|
+
"--vision",
|
295
|
+
is_flag=True,
|
296
|
+
help="Enable vision capabilites by using screenshots",
|
297
|
+
default=False,
|
275
298
|
)
|
276
299
|
@click.option(
|
277
300
|
"--reasoning", is_flag=True, help="Enable planning with reasoning", default=False
|
278
301
|
)
|
279
302
|
@click.option(
|
280
|
-
"--reflection",
|
303
|
+
"--reflection",
|
304
|
+
is_flag=True,
|
305
|
+
help="Enable reflection step for higher reasoning",
|
306
|
+
default=False,
|
281
307
|
)
|
282
308
|
@click.option(
|
283
309
|
"--tracing", is_flag=True, help="Enable Arize Phoenix tracing", default=False
|
@@ -291,9 +317,7 @@ def cli(
|
|
291
317
|
help="Save agent trajectory to file",
|
292
318
|
default=False,
|
293
319
|
)
|
294
|
-
@click.option(
|
295
|
-
"--ios", is_flag=True, help="Run on iOS device", default=False
|
296
|
-
)
|
320
|
+
@click.option("--ios", is_flag=True, help="Run on iOS device", default=False)
|
297
321
|
def run(
|
298
322
|
command: str,
|
299
323
|
device: str | None,
|
@@ -328,7 +352,7 @@ def run(
|
|
328
352
|
debug,
|
329
353
|
temperature=temperature,
|
330
354
|
save_trajectory=save_trajectory,
|
331
|
-
ios=ios
|
355
|
+
ios=ios,
|
332
356
|
)
|
333
357
|
|
334
358
|
|
@@ -350,17 +374,17 @@ async def devices():
|
|
350
374
|
|
351
375
|
|
352
376
|
@cli.command()
|
353
|
-
@click.argument("
|
377
|
+
@click.argument("serial")
|
354
378
|
@click.option("--port", "-p", default=5555, help="ADB port (default: 5555)")
|
355
379
|
@coro
|
356
|
-
async def connect(
|
380
|
+
async def connect(serial: str, port: int):
|
357
381
|
"""Connect to a device over TCP/IP."""
|
358
382
|
try:
|
359
|
-
device = await device_manager.connect(
|
383
|
+
device = await device_manager.connect(serial, port)
|
360
384
|
if device:
|
361
|
-
console.print(f"[green]Successfully connected to {
|
385
|
+
console.print(f"[green]Successfully connected to {serial}:{port}[/]")
|
362
386
|
else:
|
363
|
-
console.print(f"[red]Failed to connect to {
|
387
|
+
console.print(f"[red]Failed to connect to {serial}:{port}[/]")
|
364
388
|
except Exception as e:
|
365
389
|
console.print(f"[red]Error connecting to device: {e}[/]")
|
366
390
|
|
@@ -381,16 +405,15 @@ async def disconnect(serial: str):
|
|
381
405
|
|
382
406
|
|
383
407
|
@cli.command()
|
384
|
-
@click.option("--path", required=True, help="Path to the APK file to install")
|
385
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
|
+
)
|
386
413
|
@coro
|
387
|
-
async def setup(path: str, device: str | None):
|
388
|
-
"""Install
|
414
|
+
async def setup(path: str | None, device: str | None, debug: bool):
|
415
|
+
"""Install and enable the DroidRun Portal on a device."""
|
389
416
|
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
417
|
if not device:
|
395
418
|
devices = await device_manager.list_devices()
|
396
419
|
if not devices:
|
@@ -406,66 +429,99 @@ async def setup(path: str, device: str | None):
|
|
406
429
|
f"[bold red]Error:[/] Could not get device object for {device}"
|
407
430
|
)
|
408
431
|
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
432
|
|
413
|
-
if
|
414
|
-
console.print(
|
415
|
-
|
433
|
+
if not path:
|
434
|
+
console.print("[bold blue]Downloading DroidRun Portal APK...[/]")
|
435
|
+
apk_context = download_portal_apk(debug)
|
416
436
|
else:
|
417
|
-
console.print(f"[bold
|
437
|
+
console.print(f"[bold blue]Using provided APK:[/] {path}")
|
438
|
+
apk_context = nullcontext(path)
|
418
439
|
|
419
|
-
|
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
|
420
444
|
|
421
|
-
|
445
|
+
console.print(f"[bold blue]Step 1/2: Installing APK:[/] {apk_path}")
|
446
|
+
result = await device_obj.install_app(apk_path, True, True)
|
422
447
|
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
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![/]")
|
428
453
|
|
429
|
-
|
430
|
-
device, "settings put secure accessibility_enabled 1"
|
431
|
-
)
|
454
|
+
console.print(f"[bold blue]Step 2/2: Enabling accessibility service[/]")
|
432
455
|
|
433
|
-
|
434
|
-
|
435
|
-
"\n[bold green]Setup complete![/] The DroidRun Portal is now installed and ready to use."
|
436
|
-
)
|
456
|
+
try:
|
457
|
+
await enable_portal_accessibility(device_obj)
|
437
458
|
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
console.print(
|
443
|
-
"[yellow]Opening accessibility settings for manual configuration...[/]"
|
444
|
-
)
|
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
|
+
)
|
445
463
|
|
446
|
-
|
447
|
-
|
448
|
-
|
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
|
+
)
|
449
491
|
|
450
|
-
|
451
|
-
|
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")
|
492
|
+
except Exception as e:
|
493
|
+
console.print(f"[bold red]Error:[/] {e}")
|
459
494
|
|
460
|
-
|
461
|
-
|
462
|
-
|
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
|
463
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
|
+
)
|
464
519
|
except Exception as e:
|
465
520
|
console.print(f"[bold red]Error:[/] {e}")
|
466
|
-
|
521
|
+
if debug:
|
522
|
+
import traceback
|
467
523
|
|
468
|
-
|
524
|
+
traceback.print_exc()
|
469
525
|
|
470
526
|
|
471
527
|
if __name__ == "__main__":
|
@@ -499,5 +555,5 @@ if __name__ == "__main__":
|
|
499
555
|
base_url=base_url,
|
500
556
|
api_base=api_base,
|
501
557
|
api_key=api_key,
|
502
|
-
ios=ios
|
558
|
+
ios=ios,
|
503
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,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
|
@@ -0,0 +1,83 @@
|
|
1
|
+
from posthog import Posthog
|
2
|
+
from pathlib import Path
|
3
|
+
from uuid import uuid4
|
4
|
+
import os
|
5
|
+
import logging
|
6
|
+
from .events import TelemetryEvent
|
7
|
+
|
8
|
+
logger = logging.getLogger("droidrun-telemetry")
|
9
|
+
droidrun_logger = logging.getLogger("droidrun")
|
10
|
+
|
11
|
+
PROJECT_API_KEY = "phc_XyD3HKIsetZeRkmnfaBughs8fXWYArSUFc30C0HmRiO"
|
12
|
+
HOST = "https://eu.i.posthog.com"
|
13
|
+
USER_ID_PATH = Path.home() / ".droidrun" / "user_id"
|
14
|
+
RUN_ID = str(uuid4())
|
15
|
+
|
16
|
+
TELEMETRY_ENABLED_MESSAGE = "🕵️ Anonymized telemetry enabled. See https://docs.droidrun.ai/v3/guides/telemetry for more information."
|
17
|
+
TELEMETRY_DISABLED_MESSAGE = "🛑 Anonymized telemetry disabled. Consider setting the DROIDRUN_TELEMETRY_ENABLED environment variable to 'true' to enable telemetry and help us improve DroidRun."
|
18
|
+
|
19
|
+
posthog = Posthog(
|
20
|
+
project_api_key=PROJECT_API_KEY,
|
21
|
+
host=HOST,
|
22
|
+
disable_geoip=False,
|
23
|
+
)
|
24
|
+
|
25
|
+
|
26
|
+
def is_telemetry_enabled():
|
27
|
+
telemetry_enabled = os.environ.get("DROIDRUN_TELEMETRY_ENABLED", "true")
|
28
|
+
enabled = telemetry_enabled.lower() in ["true", "1", "yes", "y"]
|
29
|
+
logger.debug(f"Telemetry enabled: {enabled}")
|
30
|
+
return enabled
|
31
|
+
|
32
|
+
|
33
|
+
def print_telemetry_message():
|
34
|
+
if is_telemetry_enabled():
|
35
|
+
droidrun_logger.info(TELEMETRY_ENABLED_MESSAGE)
|
36
|
+
|
37
|
+
else:
|
38
|
+
droidrun_logger.info(TELEMETRY_DISABLED_MESSAGE)
|
39
|
+
|
40
|
+
|
41
|
+
# Print telemetry message on import
|
42
|
+
print_telemetry_message()
|
43
|
+
|
44
|
+
|
45
|
+
def get_user_id() -> str:
|
46
|
+
try:
|
47
|
+
if not USER_ID_PATH.exists():
|
48
|
+
USER_ID_PATH.touch()
|
49
|
+
USER_ID_PATH.write_text(str(uuid4()))
|
50
|
+
logger.debug(f"User ID: {USER_ID_PATH.read_text()}")
|
51
|
+
return USER_ID_PATH.read_text()
|
52
|
+
except Exception as e:
|
53
|
+
logger.error(f"Error getting user ID: {e}")
|
54
|
+
return "unknown"
|
55
|
+
|
56
|
+
|
57
|
+
def capture(event: TelemetryEvent):
|
58
|
+
try:
|
59
|
+
if not is_telemetry_enabled():
|
60
|
+
logger.debug(f"Telemetry disabled, skipping capture of {event}")
|
61
|
+
return
|
62
|
+
event_name = type(event).__name__
|
63
|
+
event_data = event.model_dump()
|
64
|
+
properties = {
|
65
|
+
"run_id": RUN_ID,
|
66
|
+
**event_data,
|
67
|
+
}
|
68
|
+
|
69
|
+
posthog.capture(event_name, distinct_id=get_user_id(), properties=properties)
|
70
|
+
logger.debug(f"Captured event: {event_name} with properties: {event}")
|
71
|
+
except Exception as e:
|
72
|
+
logger.error(f"Error capturing event: {e}")
|
73
|
+
|
74
|
+
|
75
|
+
def flush():
|
76
|
+
try:
|
77
|
+
if not is_telemetry_enabled():
|
78
|
+
logger.debug(f"Telemetry disabled, skipping flush")
|
79
|
+
return
|
80
|
+
posthog.flush()
|
81
|
+
logger.debug(f"Flushed telemetry data")
|
82
|
+
except Exception as e:
|
83
|
+
logger.error(f"Error flushing telemetry data: {e}")
|