droidrun 0.1.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.
- droidrun/__init__.py +19 -0
- droidrun/__main__.py +8 -0
- droidrun/adb/__init__.py +13 -0
- droidrun/adb/device.py +315 -0
- droidrun/adb/manager.py +93 -0
- droidrun/adb/wrapper.py +226 -0
- droidrun/agent/__init__.py +16 -0
- droidrun/agent/llm_reasoning.py +567 -0
- droidrun/agent/react_agent.py +556 -0
- droidrun/cli/__init__.py +9 -0
- droidrun/cli/main.py +265 -0
- droidrun/llm/__init__.py +24 -0
- droidrun/tools/__init__.py +35 -0
- droidrun/tools/actions.py +854 -0
- droidrun/tools/device.py +29 -0
- droidrun-0.1.0.dist-info/METADATA +276 -0
- droidrun-0.1.0.dist-info/RECORD +20 -0
- droidrun-0.1.0.dist-info/WHEEL +4 -0
- droidrun-0.1.0.dist-info/entry_points.txt +2 -0
- droidrun-0.1.0.dist-info/licenses/LICENSE +21 -0
droidrun/cli/main.py
ADDED
@@ -0,0 +1,265 @@
|
|
1
|
+
"""
|
2
|
+
DroidRun CLI - Command line interface for controlling Android devices through LLM agents.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import asyncio
|
6
|
+
import click
|
7
|
+
import os
|
8
|
+
from rich.console import Console
|
9
|
+
from rich import print as rprint
|
10
|
+
from droidrun.tools import DeviceManager
|
11
|
+
from droidrun.agent import run_agent
|
12
|
+
from functools import wraps
|
13
|
+
|
14
|
+
# Import the install_app function directly for the setup command
|
15
|
+
from droidrun.tools.actions import install_app
|
16
|
+
|
17
|
+
console = Console()
|
18
|
+
device_manager = DeviceManager()
|
19
|
+
|
20
|
+
def coro(f):
|
21
|
+
@wraps(f)
|
22
|
+
def wrapper(*args, **kwargs):
|
23
|
+
return asyncio.run(f(*args, **kwargs))
|
24
|
+
return wrapper
|
25
|
+
|
26
|
+
# Define the run command as a standalone function to be used as both a command and default
|
27
|
+
@coro
|
28
|
+
async def run_command(command: str, device: str | None, provider: str, model: str, steps: int, vision: bool):
|
29
|
+
"""Run a command on your Android device using natural language."""
|
30
|
+
console.print(f"[bold blue]Executing command:[/] {command}")
|
31
|
+
|
32
|
+
# Auto-detect Gemini if model starts with "gemini-"
|
33
|
+
if model and model.startswith("gemini-"):
|
34
|
+
provider = "gemini"
|
35
|
+
|
36
|
+
# Print vision status
|
37
|
+
if vision:
|
38
|
+
console.print("[blue]Vision capabilities are enabled.[/]")
|
39
|
+
else:
|
40
|
+
console.print("[blue]Vision capabilities are disabled.[/]")
|
41
|
+
|
42
|
+
# Get API keys from environment variables
|
43
|
+
api_key = None
|
44
|
+
if provider.lower() == 'openai':
|
45
|
+
api_key = os.environ.get('OPENAI_API_KEY')
|
46
|
+
if not api_key:
|
47
|
+
console.print("[bold red]Error:[/] OPENAI_API_KEY environment variable not set")
|
48
|
+
return
|
49
|
+
if not model:
|
50
|
+
model = "gpt-4o-mini"
|
51
|
+
elif provider.lower() == 'anthropic':
|
52
|
+
api_key = os.environ.get('ANTHROPIC_API_KEY')
|
53
|
+
if not api_key:
|
54
|
+
console.print("[bold red]Error:[/] ANTHROPIC_API_KEY environment variable not set")
|
55
|
+
return
|
56
|
+
if not model:
|
57
|
+
model = "claude-3-sonnet-20240229"
|
58
|
+
elif provider.lower() == 'gemini':
|
59
|
+
api_key = os.environ.get('GEMINI_API_KEY')
|
60
|
+
if not api_key:
|
61
|
+
console.print("[bold red]Error:[/] GEMINI_API_KEY environment variable not set")
|
62
|
+
return
|
63
|
+
if not model:
|
64
|
+
model = "gemini-2.0-flash"
|
65
|
+
else:
|
66
|
+
console.print(f"[bold red]Error:[/] Unsupported provider: {provider}")
|
67
|
+
return
|
68
|
+
|
69
|
+
try:
|
70
|
+
# Try to find a device if none specified
|
71
|
+
if not device:
|
72
|
+
devices = await device_manager.list_devices()
|
73
|
+
if not devices:
|
74
|
+
console.print("[yellow]No devices connected.[/]")
|
75
|
+
return
|
76
|
+
|
77
|
+
device = devices[0].serial
|
78
|
+
console.print(f"[blue]Using device:[/] {device}")
|
79
|
+
|
80
|
+
# Set the device serial in the environment variable
|
81
|
+
os.environ["DROIDRUN_DEVICE_SERIAL"] = device
|
82
|
+
console.print(f"[blue]Set DROIDRUN_DEVICE_SERIAL to:[/] {device}")
|
83
|
+
|
84
|
+
# Run the agent
|
85
|
+
console.print("[bold blue]Running ReAct agent...[/]")
|
86
|
+
console.print("[yellow]Press Ctrl+C to stop execution[/]")
|
87
|
+
|
88
|
+
try:
|
89
|
+
steps = await run_agent(
|
90
|
+
task=command,
|
91
|
+
device_serial=device, # Still pass for backward compatibility
|
92
|
+
llm_provider=provider,
|
93
|
+
model_name=model,
|
94
|
+
api_key=api_key,
|
95
|
+
vision=vision
|
96
|
+
)
|
97
|
+
|
98
|
+
# Final message
|
99
|
+
console.print(f"[bold green]Execution completed with {len(steps)} steps[/]")
|
100
|
+
except ValueError as e:
|
101
|
+
if "does not support vision" in str(e):
|
102
|
+
console.print(f"[bold red]Vision Error:[/] {e}")
|
103
|
+
console.print("[yellow]Please specify a vision-capable model with the --model flag.[/]")
|
104
|
+
console.print("[blue]Recommended models:[/]")
|
105
|
+
console.print(" - OpenAI: gpt-4o or gpt-4-vision")
|
106
|
+
console.print(" - Anthropic: claude-3-opus-20240229 or claude-3-sonnet-20240229")
|
107
|
+
console.print(" - Gemini: gemini-pro-vision")
|
108
|
+
return
|
109
|
+
else:
|
110
|
+
raise # Re-raise other ValueError exceptions
|
111
|
+
|
112
|
+
except Exception as e:
|
113
|
+
console.print(f"[bold red]Error:[/] {e}")
|
114
|
+
|
115
|
+
# Custom Click multi-command class to handle both subcommands and default behavior
|
116
|
+
class DroidRunCLI(click.Group):
|
117
|
+
def parse_args(self, ctx, args):
|
118
|
+
# Check if the first argument might be a task rather than a command
|
119
|
+
if args and not args[0].startswith('-') and args[0] not in self.commands:
|
120
|
+
# Insert the 'run' command before the first argument if it's not a known command
|
121
|
+
args.insert(0, 'run')
|
122
|
+
return super().parse_args(ctx, args)
|
123
|
+
|
124
|
+
@click.group(cls=DroidRunCLI)
|
125
|
+
def cli():
|
126
|
+
"""DroidRun - Control your Android device through LLM agents."""
|
127
|
+
pass
|
128
|
+
|
129
|
+
@cli.command()
|
130
|
+
@click.argument('command', type=str)
|
131
|
+
@click.option('--device', '-d', help='Device serial number or IP address', default=None)
|
132
|
+
@click.option('--provider', '-p', help='LLM provider (openai, anthropic, or gemini)', default='openai')
|
133
|
+
@click.option('--model', '-m', help='LLM model name', default=None)
|
134
|
+
@click.option('--steps', type=int, help='Maximum number of steps', default=15)
|
135
|
+
@click.option('--vision', is_flag=True, help='Enable vision capabilities')
|
136
|
+
def run(command: str, device: str | None, provider: str, model: str, steps: int, vision: bool):
|
137
|
+
"""Run a command on your Android device using natural language."""
|
138
|
+
# Call our standalone function
|
139
|
+
return run_command(command, device, provider, model, steps, vision)
|
140
|
+
|
141
|
+
@cli.command()
|
142
|
+
@coro
|
143
|
+
async def devices():
|
144
|
+
"""List connected Android devices."""
|
145
|
+
try:
|
146
|
+
devices = await device_manager.list_devices()
|
147
|
+
if not devices:
|
148
|
+
console.print("[yellow]No devices connected.[/]")
|
149
|
+
return
|
150
|
+
|
151
|
+
console.print(f"[green]Found {len(devices)} connected device(s):[/]")
|
152
|
+
for device in devices:
|
153
|
+
console.print(f" • [bold]{device.serial}[/]")
|
154
|
+
except Exception as e:
|
155
|
+
console.print(f"[red]Error listing devices: {e}[/]")
|
156
|
+
|
157
|
+
@cli.command()
|
158
|
+
@click.argument('ip_address')
|
159
|
+
@click.option('--port', '-p', default=5555, help='ADB port (default: 5555)')
|
160
|
+
@coro
|
161
|
+
async def connect(ip_address: str, port: int):
|
162
|
+
"""Connect to a device over TCP/IP."""
|
163
|
+
try:
|
164
|
+
device = await device_manager.connect(ip_address, port)
|
165
|
+
if device:
|
166
|
+
console.print(f"[green]Successfully connected to {ip_address}:{port}[/]")
|
167
|
+
else:
|
168
|
+
console.print(f"[red]Failed to connect to {ip_address}:{port}[/]")
|
169
|
+
except Exception as e:
|
170
|
+
console.print(f"[red]Error connecting to device: {e}[/]")
|
171
|
+
|
172
|
+
@cli.command()
|
173
|
+
@click.argument('serial')
|
174
|
+
@coro
|
175
|
+
async def disconnect(serial: str):
|
176
|
+
"""Disconnect from a device."""
|
177
|
+
try:
|
178
|
+
success = await device_manager.disconnect(serial)
|
179
|
+
if success:
|
180
|
+
console.print(f"[green]Successfully disconnected from {serial}[/]")
|
181
|
+
else:
|
182
|
+
console.print(f"[yellow]Device {serial} was not connected[/]")
|
183
|
+
except Exception as e:
|
184
|
+
console.print(f"[red]Error disconnecting from device: {e}[/]")
|
185
|
+
|
186
|
+
@cli.command()
|
187
|
+
@click.option('--path', required=True, help='Path to the APK file to install')
|
188
|
+
@click.option('--device', '-d', help='Device serial number or IP address', default=None)
|
189
|
+
@coro
|
190
|
+
async def setup(path: str, device: str | None):
|
191
|
+
"""Install an APK file and enable it as an accessibility service."""
|
192
|
+
try:
|
193
|
+
# Check if APK file exists
|
194
|
+
if not os.path.exists(path):
|
195
|
+
console.print(f"[bold red]Error:[/] APK file not found at {path}")
|
196
|
+
return
|
197
|
+
|
198
|
+
# Try to find a device if none specified
|
199
|
+
if not device:
|
200
|
+
devices = await device_manager.list_devices()
|
201
|
+
if not devices:
|
202
|
+
console.print("[yellow]No devices connected.[/]")
|
203
|
+
return
|
204
|
+
|
205
|
+
device = devices[0].serial
|
206
|
+
console.print(f"[blue]Using device:[/] {device}")
|
207
|
+
|
208
|
+
# Set the device serial in the environment variable
|
209
|
+
os.environ["DROIDRUN_DEVICE_SERIAL"] = device
|
210
|
+
console.print(f"[blue]Set DROIDRUN_DEVICE_SERIAL to:[/] {device}")
|
211
|
+
|
212
|
+
# Get a device object for ADB commands
|
213
|
+
device_obj = await device_manager.get_device(device)
|
214
|
+
if not device_obj:
|
215
|
+
console.print(f"[bold red]Error:[/] Could not get device object for {device}")
|
216
|
+
return
|
217
|
+
|
218
|
+
# Step 1: Install the APK file
|
219
|
+
console.print(f"[bold blue]Step 1/2: Installing APK:[/] {path}")
|
220
|
+
result = await install_app(path, False, True, device)
|
221
|
+
|
222
|
+
if "Error" in result:
|
223
|
+
console.print(f"[bold red]Installation failed:[/] {result}")
|
224
|
+
return
|
225
|
+
else:
|
226
|
+
console.print(f"[bold green]Installation successful![/]")
|
227
|
+
|
228
|
+
# Step 2: Enable the accessibility service with the specific command
|
229
|
+
console.print(f"[bold blue]Step 2/2: Enabling accessibility service[/]")
|
230
|
+
|
231
|
+
# Package name for reference in error message
|
232
|
+
package = "com.droidrun.portal"
|
233
|
+
|
234
|
+
try:
|
235
|
+
# Use the exact command provided
|
236
|
+
await device_obj._adb.shell(device, "settings put secure enabled_accessibility_services com.droidrun.portal/com.droidrun.portal.DroidrunPortalService")
|
237
|
+
|
238
|
+
# Also enable accessibility services globally
|
239
|
+
await device_obj._adb.shell(device, "settings put secure accessibility_enabled 1")
|
240
|
+
|
241
|
+
console.print("[green]Accessibility service enabled successfully![/]")
|
242
|
+
console.print("\n[bold green]Setup complete![/] The DroidRun Portal is now installed and ready to use.")
|
243
|
+
|
244
|
+
except Exception as e:
|
245
|
+
console.print(f"[yellow]Could not automatically enable accessibility service: {e}[/]")
|
246
|
+
console.print("[yellow]Opening accessibility settings for manual configuration...[/]")
|
247
|
+
|
248
|
+
# Fallback: Open the accessibility settings page
|
249
|
+
await device_obj._adb.shell(device, "am start -a android.settings.ACCESSIBILITY_SETTINGS")
|
250
|
+
|
251
|
+
console.print("\n[yellow]Please complete the following steps on your device:[/]")
|
252
|
+
console.print(f"1. Find [bold]{package}[/] in the accessibility services list")
|
253
|
+
console.print("2. Tap on the service name")
|
254
|
+
console.print("3. Toggle the switch to [bold]ON[/] to enable the service")
|
255
|
+
console.print("4. Accept any permission dialogs that appear")
|
256
|
+
|
257
|
+
console.print("\n[bold green]APK installation complete![/] Please manually enable the accessibility service using the steps above.")
|
258
|
+
|
259
|
+
except Exception as e:
|
260
|
+
console.print(f"[bold red]Error:[/] {e}")
|
261
|
+
import traceback
|
262
|
+
traceback.print_exc()
|
263
|
+
|
264
|
+
if __name__ == '__main__':
|
265
|
+
cli()
|
droidrun/llm/__init__.py
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
"""
|
2
|
+
DroidRun LLM Module.
|
3
|
+
|
4
|
+
This module provides LLM providers for the Agent to use for reasoning.
|
5
|
+
"""
|
6
|
+
|
7
|
+
from droidrun.agent.llm_reasoning import LLMReasoner as BaseLLM
|
8
|
+
|
9
|
+
# Alias OpenAILLM and AnthropicLLM for backward compatibility
|
10
|
+
class OpenAILLM(BaseLLM):
|
11
|
+
"""
|
12
|
+
OpenAI-based LLM provider.
|
13
|
+
"""
|
14
|
+
def __init__(self, model="gpt-4o", **kwargs):
|
15
|
+
super().__init__(provider="openai", model=model, **kwargs)
|
16
|
+
|
17
|
+
class AnthropicLLM(BaseLLM):
|
18
|
+
"""
|
19
|
+
Anthropic-based LLM provider.
|
20
|
+
"""
|
21
|
+
def __init__(self, model="claude-3-opus-20240229", **kwargs):
|
22
|
+
super().__init__(provider="anthropic", model=model, **kwargs)
|
23
|
+
|
24
|
+
__all__ = ["BaseLLM", "OpenAILLM", "AnthropicLLM"]
|
@@ -0,0 +1,35 @@
|
|
1
|
+
"""
|
2
|
+
DroidRun Tools - Core functionality for Android device control.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from .device import DeviceManager
|
6
|
+
from .actions import (
|
7
|
+
tap,
|
8
|
+
swipe,
|
9
|
+
input_text,
|
10
|
+
press_key,
|
11
|
+
start_app,
|
12
|
+
install_app,
|
13
|
+
uninstall_app,
|
14
|
+
take_screenshot,
|
15
|
+
list_packages,
|
16
|
+
get_clickables,
|
17
|
+
complete,
|
18
|
+
extract,
|
19
|
+
)
|
20
|
+
|
21
|
+
__all__ = [
|
22
|
+
'DeviceManager',
|
23
|
+
'tap',
|
24
|
+
'swipe',
|
25
|
+
'input_text',
|
26
|
+
'press_key',
|
27
|
+
'start_app',
|
28
|
+
'install_app',
|
29
|
+
'uninstall_app',
|
30
|
+
'take_screenshot',
|
31
|
+
'list_packages',
|
32
|
+
'get_clickables',
|
33
|
+
'complete',
|
34
|
+
'extract',
|
35
|
+
]
|