droidrun 0.2.0__py3-none-any.whl → 0.3.1__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 +16 -11
- droidrun/__main__.py +1 -1
- droidrun/adb/__init__.py +3 -3
- droidrun/adb/device.py +1 -1
- droidrun/adb/manager.py +2 -2
- droidrun/agent/__init__.py +6 -0
- droidrun/agent/codeact/__init__.py +2 -4
- droidrun/agent/codeact/codeact_agent.py +330 -235
- droidrun/agent/codeact/events.py +12 -20
- droidrun/agent/codeact/prompts.py +0 -52
- droidrun/agent/common/default.py +5 -0
- droidrun/agent/common/events.py +4 -0
- droidrun/agent/context/__init__.py +23 -0
- droidrun/agent/context/agent_persona.py +15 -0
- droidrun/agent/context/context_injection_manager.py +66 -0
- droidrun/agent/context/episodic_memory.py +15 -0
- droidrun/agent/context/personas/__init__.py +11 -0
- droidrun/agent/context/personas/app_starter.py +44 -0
- droidrun/agent/context/personas/default.py +95 -0
- droidrun/agent/context/personas/extractor.py +52 -0
- droidrun/agent/context/personas/ui_expert.py +107 -0
- droidrun/agent/context/reflection.py +20 -0
- droidrun/agent/context/task_manager.py +124 -0
- droidrun/agent/droid/__init__.py +2 -2
- droidrun/agent/droid/droid_agent.py +269 -325
- droidrun/agent/droid/events.py +28 -0
- droidrun/agent/oneflows/reflector.py +265 -0
- droidrun/agent/planner/__init__.py +2 -4
- droidrun/agent/planner/events.py +9 -13
- droidrun/agent/planner/planner_agent.py +288 -0
- droidrun/agent/planner/prompts.py +33 -53
- droidrun/agent/utils/__init__.py +3 -0
- droidrun/agent/utils/async_utils.py +1 -40
- droidrun/agent/utils/chat_utils.py +265 -48
- droidrun/agent/utils/executer.py +49 -14
- droidrun/agent/utils/llm_picker.py +14 -10
- droidrun/agent/utils/trajectory.py +184 -0
- droidrun/cli/__init__.py +1 -1
- droidrun/cli/logs.py +283 -0
- droidrun/cli/main.py +364 -441
- droidrun/tools/__init__.py +5 -10
- droidrun/tools/{actions.py → adb.py} +381 -412
- droidrun/tools/ios.py +596 -0
- droidrun/tools/tools.py +95 -0
- droidrun-0.3.1.dist-info/METADATA +150 -0
- droidrun-0.3.1.dist-info/RECORD +50 -0
- droidrun/agent/planner/task_manager.py +0 -355
- droidrun/agent/planner/workflow.py +0 -371
- droidrun/tools/device.py +0 -29
- droidrun/tools/loader.py +0 -60
- droidrun-0.2.0.dist-info/METADATA +0 -373
- droidrun-0.2.0.dist-info/RECORD +0 -32
- {droidrun-0.2.0.dist-info → droidrun-0.3.1.dist-info}/WHEEL +0 -0
- {droidrun-0.2.0.dist-info → droidrun-0.3.1.dist-info}/entry_points.txt +0 -0
- {droidrun-0.2.0.dist-info → droidrun-0.3.1.dist-info}/licenses/LICENSE +0 -0
droidrun/cli/main.py
CHANGED
@@ -1,470 +1,336 @@
|
|
1
1
|
"""
|
2
2
|
DroidRun CLI - Command line interface for controlling Android devices through LLM agents.
|
3
3
|
"""
|
4
|
-
if __name__ == "__main__":
|
5
|
-
import sys
|
6
|
-
import os
|
7
|
-
_project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
|
8
|
-
sys.path.insert(0, _project_root)
|
9
|
-
__package__ = "droidrun.cli"
|
10
|
-
|
11
4
|
|
12
5
|
import asyncio
|
13
6
|
import click
|
14
7
|
import os
|
15
8
|
import logging
|
16
|
-
import
|
17
|
-
import queue
|
9
|
+
import warnings
|
18
10
|
from rich.console import Console
|
19
|
-
from
|
20
|
-
from
|
21
|
-
from
|
22
|
-
from
|
23
|
-
from rich.spinner import Spinner
|
24
|
-
from rich.align import Align
|
25
|
-
from ..tools import DeviceManager, Tools, load_tools
|
26
|
-
from ..agent.droid import DroidAgent
|
27
|
-
from ..agent.utils.llm_picker import load_llm
|
11
|
+
from droidrun.agent.droid import DroidAgent
|
12
|
+
from droidrun.agent.utils.llm_picker import load_llm
|
13
|
+
from droidrun.adb import DeviceManager
|
14
|
+
from droidrun.tools import AdbTools, IOSTools
|
28
15
|
from functools import wraps
|
16
|
+
from droidrun.cli.logs import LogHandler
|
17
|
+
|
18
|
+
# Suppress all warnings
|
19
|
+
warnings.filterwarnings("ignore")
|
20
|
+
os.environ["TOKENIZERS_PARALLELISM"] = "false"
|
21
|
+
os.environ["GRPC_ENABLE_FORK_SUPPORT"] = "false"
|
22
|
+
|
29
23
|
console = Console()
|
30
24
|
device_manager = DeviceManager()
|
31
25
|
|
32
|
-
log_queue = queue.Queue()
|
33
|
-
current_step = "Initializing..."
|
34
|
-
spinner = Spinner("dots")
|
35
26
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
27
|
+
def configure_logging(goal: str, debug: bool):
|
28
|
+
logger = logging.getLogger("droidrun")
|
29
|
+
logger.handlers = []
|
30
|
+
|
31
|
+
handler = LogHandler(goal)
|
32
|
+
handler.setFormatter(
|
33
|
+
logging.Formatter("%(levelname)s %(message)s", "%H:%M:%S")
|
34
|
+
if debug
|
35
|
+
else logging.Formatter("%(message)s", "%H:%M:%S")
|
36
|
+
)
|
37
|
+
logger.addHandler(handler)
|
38
|
+
|
39
|
+
|
40
|
+
logger.setLevel(logging.DEBUG if debug else logging.INFO)
|
41
|
+
logger.propagate = False
|
42
|
+
|
43
|
+
return handler
|
44
|
+
|
40
45
|
|
41
46
|
def coro(f):
|
42
47
|
@wraps(f)
|
43
48
|
def wrapper(*args, **kwargs):
|
44
49
|
return asyncio.run(f(*args, **kwargs))
|
50
|
+
|
45
51
|
return wrapper
|
46
52
|
|
47
|
-
def create_layout():
|
48
|
-
"""Create a layout with logs at top and status at bottom"""
|
49
|
-
layout = Layout()
|
50
|
-
layout.split(
|
51
|
-
Layout(name="logs"),
|
52
|
-
Layout(name="goal", size=3),
|
53
|
-
Layout(name="status", size=3)
|
54
|
-
)
|
55
|
-
return layout
|
56
|
-
|
57
|
-
def update_layout(layout, log_list, step_message, current_time, goal=None, completed=False, success=None):
|
58
|
-
"""Update the layout with current logs and step information"""
|
59
|
-
from rich.text import Text
|
60
|
-
import shutil
|
61
|
-
|
62
|
-
terminal_height = shutil.get_terminal_size().lines
|
63
|
-
other_components_height = 3 + 3 + 4 + 1 + 4
|
64
|
-
available_log_lines = max(5, terminal_height - other_components_height)
|
65
|
-
|
66
|
-
visible_logs = log_list[-available_log_lines:] if len(log_list) > available_log_lines else log_list
|
67
|
-
|
68
|
-
log_content = "\n".join(visible_logs)
|
69
|
-
|
70
|
-
layout["logs"].update(Panel(
|
71
|
-
log_content,
|
72
|
-
title=f"Logs (showing {len(visible_logs)} most recent of {len(log_list)} total)",
|
73
|
-
border_style="blue",
|
74
|
-
title_align="left",
|
75
|
-
padding=(0, 1),
|
76
|
-
))
|
77
|
-
|
78
|
-
if goal:
|
79
|
-
goal_text = Text(goal, style="bold")
|
80
|
-
layout["goal"].update(Panel(
|
81
|
-
goal_text,
|
82
|
-
title="Goal",
|
83
|
-
border_style="magenta",
|
84
|
-
title_align="left",
|
85
|
-
padding=(0, 1)
|
86
|
-
))
|
87
|
-
|
88
|
-
step_display = Text()
|
89
|
-
|
90
|
-
if completed:
|
91
|
-
if success:
|
92
|
-
step_display.append("✓ ", style="bold green")
|
93
|
-
panel_title = "Completed Successfully"
|
94
|
-
panel_style = "green"
|
95
|
-
else:
|
96
|
-
step_display.append("✗ ", style="bold red")
|
97
|
-
panel_title = "Failed"
|
98
|
-
panel_style = "red"
|
99
|
-
else:
|
100
|
-
step_display.append(spinner.render(current_time))
|
101
|
-
step_display.append(" ")
|
102
|
-
panel_title = "Current Action"
|
103
|
-
panel_style = "green"
|
104
|
-
|
105
|
-
step_display.append(step_message)
|
106
|
-
|
107
|
-
layout["status"].update(Panel(
|
108
|
-
step_display,
|
109
|
-
title=panel_title,
|
110
|
-
border_style=panel_style,
|
111
|
-
title_align="left",
|
112
|
-
padding=(0, 1)
|
113
|
-
))
|
114
53
|
|
115
54
|
@coro
|
116
|
-
async def run_command(
|
55
|
+
async def run_command(
|
56
|
+
command: str,
|
57
|
+
device: str | None,
|
58
|
+
provider: str,
|
59
|
+
model: str,
|
60
|
+
steps: int,
|
61
|
+
base_url: str,
|
62
|
+
api_base: str,
|
63
|
+
vision: bool,
|
64
|
+
reasoning: bool,
|
65
|
+
reflection: bool,
|
66
|
+
tracing: bool,
|
67
|
+
debug: bool,
|
68
|
+
save_trajectory: bool = False,
|
69
|
+
ios: bool = False,
|
70
|
+
**kwargs,
|
71
|
+
):
|
117
72
|
"""Run a command on your Android device using natural language."""
|
118
|
-
configure_logging(debug)
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
is_completed = False
|
125
|
-
is_success = None
|
126
|
-
|
127
|
-
layout = create_layout()
|
128
|
-
|
129
|
-
with Live(layout, refresh_per_second=20, console=console) as live:
|
130
|
-
def update_display():
|
131
|
-
current_time = time.time()
|
132
|
-
update_layout(
|
133
|
-
layout,
|
134
|
-
logs,
|
135
|
-
current_step,
|
136
|
-
current_time,
|
137
|
-
goal=command,
|
138
|
-
completed=is_completed,
|
139
|
-
success=is_success
|
140
|
-
)
|
141
|
-
live.refresh()
|
142
|
-
|
143
|
-
def process_new_logs():
|
144
|
-
log_count = 0
|
145
|
-
while not log_queue.empty():
|
146
|
-
try:
|
147
|
-
log = log_queue.get_nowait()
|
148
|
-
logs.append(log)
|
149
|
-
log_count += 1
|
150
|
-
if len(logs) > max_log_history:
|
151
|
-
logs.pop(0)
|
152
|
-
except queue.Empty:
|
153
|
-
break
|
154
|
-
return log_count > 0
|
155
|
-
|
156
|
-
async def process_logs():
|
157
|
-
global current_step
|
158
|
-
iteration = 0
|
159
|
-
while True:
|
160
|
-
if is_completed:
|
161
|
-
process_new_logs()
|
162
|
-
if iteration % 10 == 0:
|
163
|
-
update_display()
|
164
|
-
iteration += 1
|
165
|
-
await asyncio.sleep(0.1)
|
166
|
-
continue
|
167
|
-
|
168
|
-
new_logs_added = process_new_logs()
|
169
|
-
|
170
|
-
# Improve detection of the latest action from logs
|
171
|
-
latest_task = None
|
172
|
-
for log in reversed(logs[-50:]): # Search from most recent logs first
|
173
|
-
if "🔧 Executing task:" in log:
|
174
|
-
task_desc = log.split("🔧 Executing task:", 1)[1].strip()
|
175
|
-
|
176
|
-
if "Goal:" in task_desc:
|
177
|
-
goal_part = task_desc.split("Goal:", 1)[1].strip()
|
178
|
-
latest_task = goal_part
|
179
|
-
else:
|
180
|
-
latest_task = task_desc
|
181
|
-
break # Stop at the most recent task
|
182
|
-
|
183
|
-
if latest_task:
|
184
|
-
current_step = f"Executing: {latest_task}"
|
185
|
-
|
186
|
-
if new_logs_added or iteration % 5 == 0:
|
187
|
-
update_layout(
|
188
|
-
layout,
|
189
|
-
logs,
|
190
|
-
current_step,
|
191
|
-
time.time(),
|
192
|
-
goal=command,
|
193
|
-
completed=is_completed,
|
194
|
-
success=is_success
|
195
|
-
)
|
196
|
-
|
197
|
-
iteration += 1
|
198
|
-
await asyncio.sleep(0.05)
|
199
|
-
|
73
|
+
log_handler = configure_logging(command, debug)
|
74
|
+
logger = logging.getLogger("droidrun")
|
75
|
+
|
76
|
+
log_handler.update_step("Initializing...")
|
77
|
+
|
78
|
+
with log_handler.render() as live:
|
200
79
|
try:
|
201
|
-
|
202
|
-
|
203
|
-
|
80
|
+
logger.info(f"🚀 Starting: {command}")
|
81
|
+
|
204
82
|
if not kwargs.get("temperature"):
|
205
83
|
kwargs["temperature"] = 0
|
206
|
-
|
207
|
-
current_step = "Setting up tools..."
|
208
|
-
update_display()
|
209
|
-
|
210
|
-
tool_list, tools_instance = await load_tools(serial=device)
|
211
84
|
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
llm = load_llm(provider_name=provider, model=model, base_url=base_url, **kwargs)
|
226
|
-
|
227
|
-
current_step = "Initializing DroidAgent..."
|
228
|
-
update_display()
|
229
|
-
|
230
|
-
if reasoning:
|
231
|
-
logs.append("Using planning mode with reasoning")
|
85
|
+
log_handler.update_step("Setting up tools...")
|
86
|
+
|
87
|
+
# Device setup
|
88
|
+
if device is None and not ios:
|
89
|
+
logger.info("🔍 Finding connected device...")
|
90
|
+
device_manager = DeviceManager()
|
91
|
+
devices = await device_manager.list_devices()
|
92
|
+
if not devices:
|
93
|
+
raise ValueError("No connected devices found.")
|
94
|
+
device = devices[0].serial
|
95
|
+
logger.info(f"📱 Using device: {device}")
|
96
|
+
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")
|
232
98
|
else:
|
233
|
-
|
234
|
-
|
99
|
+
logger.info(f"📱 Using device: {device}")
|
100
|
+
|
101
|
+
tools = AdbTools(serial=device) if not ios else IOSTools(url=device)
|
102
|
+
|
103
|
+
# LLM setup
|
104
|
+
log_handler.update_step("Initializing LLM...")
|
105
|
+
llm = load_llm(
|
106
|
+
provider_name=provider, model=model, base_url=base_url, api_base=api_base, **kwargs
|
107
|
+
)
|
108
|
+
logger.info(f"🧠 LLM ready: {provider}/{model}")
|
109
|
+
|
110
|
+
# Agent setup
|
111
|
+
log_handler.update_step("Initializing DroidAgent...")
|
112
|
+
|
113
|
+
mode = "planning with reasoning" if reasoning else "direct execution"
|
114
|
+
logger.info(f"🤖 Agent mode: {mode}")
|
115
|
+
|
235
116
|
if tracing:
|
236
|
-
|
237
|
-
|
238
|
-
update_display()
|
239
|
-
|
117
|
+
logger.info("🔍 Tracing enabled")
|
118
|
+
|
240
119
|
droid_agent = DroidAgent(
|
241
120
|
goal=command,
|
242
121
|
llm=llm,
|
243
|
-
|
244
|
-
tool_list=tool_list,
|
122
|
+
tools=tools,
|
245
123
|
max_steps=steps,
|
246
|
-
vision=vision,
|
247
124
|
timeout=1000,
|
248
|
-
|
125
|
+
vision=vision,
|
249
126
|
reasoning=reasoning,
|
127
|
+
reflection=reflection,
|
250
128
|
enable_tracing=tracing,
|
251
|
-
debug=debug
|
129
|
+
debug=debug,
|
130
|
+
save_trajectories=save_trajectory
|
252
131
|
)
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
132
|
+
|
133
|
+
logger.info("▶️ Starting agent execution...")
|
134
|
+
logger.info("Press Ctrl+C to stop")
|
135
|
+
log_handler.update_step("Running agent...")
|
257
136
|
|
258
137
|
try:
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
if result.get("success", False):
|
265
|
-
is_completed = True
|
266
|
-
is_success = True
|
267
|
-
|
268
|
-
if result.get("output"):
|
269
|
-
success_output = f"🎯 FINAL ANSWER: {result.get('output')}"
|
270
|
-
logs.append(success_output)
|
271
|
-
current_step = f"{result.get('output')}"
|
272
|
-
else:
|
273
|
-
current_step = result.get("reason", "Success")
|
274
|
-
else:
|
275
|
-
is_completed = True
|
276
|
-
is_success = False
|
277
|
-
|
278
|
-
current_step = result.get("reason", "Failed") if result else "Failed"
|
279
|
-
|
280
|
-
update_layout(
|
281
|
-
layout,
|
282
|
-
logs,
|
283
|
-
current_step,
|
284
|
-
time.time(),
|
285
|
-
goal=command,
|
286
|
-
completed=is_completed,
|
287
|
-
success=is_success
|
288
|
-
)
|
289
|
-
|
290
|
-
await asyncio.sleep(2)
|
291
|
-
finally:
|
292
|
-
log_task.cancel()
|
293
|
-
try:
|
294
|
-
await log_task
|
295
|
-
except asyncio.CancelledError:
|
296
|
-
pass
|
297
|
-
|
298
|
-
for _ in range(20):
|
299
|
-
process_new_logs()
|
300
|
-
await asyncio.sleep(0.05)
|
301
|
-
|
302
|
-
update_layout(
|
303
|
-
layout,
|
304
|
-
logs,
|
305
|
-
current_step,
|
306
|
-
time.time(),
|
307
|
-
goal=command,
|
308
|
-
completed=is_completed,
|
309
|
-
success=is_success
|
310
|
-
)
|
311
|
-
|
312
|
-
live.refresh()
|
313
|
-
|
314
|
-
await asyncio.sleep(3)
|
138
|
+
handler = droid_agent.run()
|
139
|
+
|
140
|
+
async for event in handler.stream_events():
|
141
|
+
log_handler.handle_event(event)
|
142
|
+
result = await handler
|
315
143
|
|
316
144
|
except KeyboardInterrupt:
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
update_layout(
|
324
|
-
layout,
|
325
|
-
logs,
|
326
|
-
current_step,
|
327
|
-
time.time(),
|
328
|
-
goal=command,
|
329
|
-
completed=is_completed,
|
330
|
-
success=is_success
|
331
|
-
)
|
332
|
-
|
333
|
-
except ValueError as e:
|
334
|
-
logs.append(f"Configuration Error: {e}")
|
335
|
-
current_step = f"Error: {e}"
|
336
|
-
|
337
|
-
is_completed = True
|
338
|
-
is_success = False
|
339
|
-
|
340
|
-
update_layout(
|
341
|
-
layout,
|
342
|
-
logs,
|
343
|
-
current_step,
|
344
|
-
time.time(),
|
345
|
-
goal=command,
|
346
|
-
completed=is_completed,
|
347
|
-
success=is_success
|
348
|
-
)
|
349
|
-
|
145
|
+
log_handler.is_completed = True
|
146
|
+
log_handler.is_success = False
|
147
|
+
log_handler.current_step = "Stopped by user"
|
148
|
+
logger.info("⏹️ Stopped by user")
|
149
|
+
|
350
150
|
except Exception as e:
|
351
|
-
|
352
|
-
|
151
|
+
log_handler.is_completed = True
|
152
|
+
log_handler.is_success = False
|
153
|
+
log_handler.current_step = f"Error: {e}"
|
154
|
+
logger.error(f"💥 Error: {e}")
|
353
155
|
if debug:
|
354
156
|
import traceback
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
is_success = False
|
359
|
-
|
360
|
-
update_layout(
|
361
|
-
layout,
|
362
|
-
logs,
|
363
|
-
current_step,
|
364
|
-
time.time(),
|
365
|
-
goal=command,
|
366
|
-
completed=is_completed,
|
367
|
-
success=is_success
|
368
|
-
)
|
369
|
-
|
370
|
-
update_display()
|
371
|
-
await asyncio.sleep(1)
|
372
|
-
|
373
|
-
except ValueError as e:
|
374
|
-
logs.append(f"Error: {e}")
|
375
|
-
current_step = f"Error: {e}"
|
376
|
-
|
377
|
-
step_display = Text()
|
378
|
-
step_display.append("⚠ ", style="bold red")
|
379
|
-
step_display.append(current_step)
|
380
|
-
|
381
|
-
layout["status"].update(Panel(
|
382
|
-
step_display,
|
383
|
-
title="Error",
|
384
|
-
border_style="red",
|
385
|
-
title_align="left",
|
386
|
-
padding=(0, 1)
|
387
|
-
))
|
388
|
-
update_display()
|
389
|
-
|
157
|
+
|
158
|
+
logger.debug(traceback.format_exc())
|
159
|
+
|
390
160
|
except Exception as e:
|
391
|
-
|
392
|
-
|
161
|
+
log_handler.current_step = f"Error: {e}"
|
162
|
+
logger.error(f"💥 Setup error: {e}")
|
393
163
|
if debug:
|
394
164
|
import traceback
|
395
|
-
|
396
|
-
|
397
|
-
step_display = Text()
|
398
|
-
step_display.append("⚠ ", style="bold red")
|
399
|
-
step_display.append(current_step)
|
400
|
-
|
401
|
-
layout["status"].update(Panel(
|
402
|
-
step_display,
|
403
|
-
title="Error",
|
404
|
-
border_style="red",
|
405
|
-
title_align="left",
|
406
|
-
padding=(0, 1)
|
407
|
-
))
|
408
|
-
update_display()
|
409
|
-
await asyncio.sleep(1)
|
410
|
-
|
411
|
-
def configure_logging(debug: bool):
|
412
|
-
"""Configure logging verbosity based on debug flag."""
|
413
|
-
root_logger = logging.getLogger()
|
414
|
-
droidrun_logger = logging.getLogger("droidrun")
|
415
|
-
|
416
|
-
# Clear existing handlers
|
417
|
-
for handler in root_logger.handlers[:]:
|
418
|
-
root_logger.removeHandler(handler)
|
419
|
-
for handler in droidrun_logger.handlers[:]:
|
420
|
-
droidrun_logger.removeHandler(handler)
|
421
|
-
|
422
|
-
rich_handler = RichHandler()
|
423
|
-
|
424
|
-
formatter = logging.Formatter('%(message)s')
|
425
|
-
rich_handler.setFormatter(formatter)
|
426
|
-
|
427
|
-
if debug:
|
428
|
-
rich_handler.setLevel(logging.DEBUG)
|
429
|
-
droidrun_logger.setLevel(logging.DEBUG)
|
430
|
-
root_logger.setLevel(logging.INFO)
|
431
|
-
else:
|
432
|
-
rich_handler.setLevel(logging.INFO)
|
433
|
-
droidrun_logger.setLevel(logging.INFO)
|
434
|
-
root_logger.setLevel(logging.WARNING)
|
435
|
-
|
436
|
-
droidrun_logger.addHandler(rich_handler)
|
437
|
-
|
438
|
-
log_queue.put(f"Logging level set to: {logging.getLevelName(droidrun_logger.level)}")
|
165
|
+
|
166
|
+
logger.debug(traceback.format_exc())
|
439
167
|
|
440
168
|
|
441
169
|
class DroidRunCLI(click.Group):
|
442
170
|
def parse_args(self, ctx, args):
|
443
|
-
|
444
|
-
|
171
|
+
# If the first arg is not an option and not a known command, treat as 'run'
|
172
|
+
if args and """not args[0].startswith("-")""" and args[0] not in self.commands:
|
173
|
+
args.insert(0, "run")
|
174
|
+
|
445
175
|
return super().parse_args(ctx, args)
|
446
176
|
|
177
|
+
|
178
|
+
@click.option("--device", "-d", help="Device serial number or IP address", default=None)
|
179
|
+
@click.option(
|
180
|
+
"--provider",
|
181
|
+
"-p",
|
182
|
+
help="LLM provider (OpenAI, Ollama, Anthropic, GoogleGenAI, DeepSeek)",
|
183
|
+
default="GoogleGenAI",
|
184
|
+
)
|
185
|
+
@click.option(
|
186
|
+
"--model",
|
187
|
+
"-m",
|
188
|
+
help="LLM model name",
|
189
|
+
default="models/gemini-2.5-flash",
|
190
|
+
)
|
191
|
+
@click.option("--temperature", type=float, help="Temperature for LLM", default=0.2)
|
192
|
+
@click.option("--steps", type=int, help="Maximum number of steps", default=15)
|
193
|
+
@click.option(
|
194
|
+
"--base_url",
|
195
|
+
"-u",
|
196
|
+
help="Base URL for API (e.g., OpenRouter or Ollama)",
|
197
|
+
default=None,
|
198
|
+
)
|
199
|
+
@click.option(
|
200
|
+
"--api_base",
|
201
|
+
help="Base URL for API (e.g., OpenAI, OpenAI-Like)",
|
202
|
+
default=None,
|
203
|
+
)
|
204
|
+
@click.option(
|
205
|
+
"--vision", is_flag=True, help="Enable vision capabilites by using screenshots", default=False
|
206
|
+
)
|
207
|
+
@click.option(
|
208
|
+
"--reasoning", is_flag=True, help="Enable planning with reasoning", default=False
|
209
|
+
)
|
210
|
+
@click.option(
|
211
|
+
"--reflection", is_flag=True, help="Enable reflection step for higher reasoning", default=False
|
212
|
+
)
|
213
|
+
@click.option(
|
214
|
+
"--tracing", is_flag=True, help="Enable Arize Phoenix tracing", default=False
|
215
|
+
)
|
216
|
+
@click.option(
|
217
|
+
"--debug", is_flag=True, help="Enable verbose debug logging", default=False
|
218
|
+
)
|
219
|
+
@click.option(
|
220
|
+
"--save-trajectory",
|
221
|
+
is_flag=True,
|
222
|
+
help="Save agent trajectory to file",
|
223
|
+
default=False,
|
224
|
+
)
|
447
225
|
@click.group(cls=DroidRunCLI)
|
448
|
-
def cli(
|
226
|
+
def cli(
|
227
|
+
device: str | None,
|
228
|
+
provider: str,
|
229
|
+
model: str,
|
230
|
+
steps: int,
|
231
|
+
base_url: str,
|
232
|
+
api_base: str,
|
233
|
+
temperature: float,
|
234
|
+
vision: bool,
|
235
|
+
reasoning: bool,
|
236
|
+
reflection: bool,
|
237
|
+
tracing: bool,
|
238
|
+
debug: bool,
|
239
|
+
save_trajectory: bool,
|
240
|
+
):
|
449
241
|
"""DroidRun - Control your Android device through LLM agents."""
|
450
242
|
pass
|
451
243
|
|
244
|
+
|
452
245
|
@cli.command()
|
453
|
-
@click.argument(
|
454
|
-
@click.option(
|
455
|
-
@click.option(
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
@click.option(
|
462
|
-
|
463
|
-
|
464
|
-
|
246
|
+
@click.argument("command", type=str)
|
247
|
+
@click.option("--device", "-d", help="Device serial number or IP address", default=None)
|
248
|
+
@click.option(
|
249
|
+
"--provider",
|
250
|
+
"-p",
|
251
|
+
help="LLM provider (OpenAI, Ollama, Anthropic, GoogleGenAI, DeepSeek)",
|
252
|
+
default="GoogleGenAI",
|
253
|
+
)
|
254
|
+
@click.option(
|
255
|
+
"--model",
|
256
|
+
"-m",
|
257
|
+
help="LLM model name",
|
258
|
+
default="models/gemini-2.5-flash",
|
259
|
+
)
|
260
|
+
@click.option("--temperature", type=float, help="Temperature for LLM", default=0.2)
|
261
|
+
@click.option("--steps", type=int, help="Maximum number of steps", default=15)
|
262
|
+
@click.option(
|
263
|
+
"--base_url",
|
264
|
+
"-u",
|
265
|
+
help="Base URL for API (e.g., OpenRouter or Ollama)",
|
266
|
+
default=None,
|
267
|
+
)
|
268
|
+
@click.option(
|
269
|
+
"--api_base",
|
270
|
+
help="Base URL for API (e.g., OpenAI or OpenAI-Like)",
|
271
|
+
default=None,
|
272
|
+
)
|
273
|
+
@click.option(
|
274
|
+
"--vision", is_flag=True, help="Enable vision capabilites by using screenshots", default=False
|
275
|
+
)
|
276
|
+
@click.option(
|
277
|
+
"--reasoning", is_flag=True, help="Enable planning with reasoning", default=False
|
278
|
+
)
|
279
|
+
@click.option(
|
280
|
+
"--reflection", is_flag=True, help="Enable reflection step for higher reasoning", default=False
|
281
|
+
)
|
282
|
+
@click.option(
|
283
|
+
"--tracing", is_flag=True, help="Enable Arize Phoenix tracing", default=False
|
284
|
+
)
|
285
|
+
@click.option(
|
286
|
+
"--debug", is_flag=True, help="Enable verbose debug logging", default=False
|
287
|
+
)
|
288
|
+
@click.option(
|
289
|
+
"--save-trajectory",
|
290
|
+
is_flag=True,
|
291
|
+
help="Save agent trajectory to file",
|
292
|
+
default=False,
|
293
|
+
)
|
294
|
+
@click.option(
|
295
|
+
"--ios", is_flag=True, help="Run on iOS device", default=False
|
296
|
+
)
|
297
|
+
def run(
|
298
|
+
command: str,
|
299
|
+
device: str | None,
|
300
|
+
provider: str,
|
301
|
+
model: str,
|
302
|
+
steps: int,
|
303
|
+
base_url: str,
|
304
|
+
api_base: str,
|
305
|
+
temperature: float,
|
306
|
+
vision: bool,
|
307
|
+
reasoning: bool,
|
308
|
+
reflection: bool,
|
309
|
+
tracing: bool,
|
310
|
+
debug: bool,
|
311
|
+
save_trajectory: bool,
|
312
|
+
ios: bool,
|
313
|
+
):
|
465
314
|
"""Run a command on your Android device using natural language."""
|
466
315
|
# Call our standalone function
|
467
|
-
return run_command(
|
316
|
+
return run_command(
|
317
|
+
command,
|
318
|
+
device,
|
319
|
+
provider,
|
320
|
+
model,
|
321
|
+
steps,
|
322
|
+
base_url,
|
323
|
+
api_base,
|
324
|
+
vision,
|
325
|
+
reasoning,
|
326
|
+
reflection,
|
327
|
+
tracing,
|
328
|
+
debug,
|
329
|
+
temperature=temperature,
|
330
|
+
save_trajectory=save_trajectory,
|
331
|
+
ios=ios
|
332
|
+
)
|
333
|
+
|
468
334
|
|
469
335
|
@cli.command()
|
470
336
|
@coro
|
@@ -482,9 +348,10 @@ async def devices():
|
|
482
348
|
except Exception as e:
|
483
349
|
console.print(f"[red]Error listing devices: {e}[/]")
|
484
350
|
|
351
|
+
|
485
352
|
@cli.command()
|
486
|
-
@click.argument(
|
487
|
-
@click.option(
|
353
|
+
@click.argument("ip_address")
|
354
|
+
@click.option("--port", "-p", default=5555, help="ADB port (default: 5555)")
|
488
355
|
@coro
|
489
356
|
async def connect(ip_address: str, port: int):
|
490
357
|
"""Connect to a device over TCP/IP."""
|
@@ -497,8 +364,9 @@ async def connect(ip_address: str, port: int):
|
|
497
364
|
except Exception as e:
|
498
365
|
console.print(f"[red]Error connecting to device: {e}[/]")
|
499
366
|
|
367
|
+
|
500
368
|
@cli.command()
|
501
|
-
@click.argument(
|
369
|
+
@click.argument("serial")
|
502
370
|
@coro
|
503
371
|
async def disconnect(serial: str):
|
504
372
|
"""Disconnect from a device."""
|
@@ -511,9 +379,10 @@ async def disconnect(serial: str):
|
|
511
379
|
except Exception as e:
|
512
380
|
console.print(f"[red]Error disconnecting from device: {e}[/]")
|
513
381
|
|
382
|
+
|
514
383
|
@cli.command()
|
515
|
-
@click.option(
|
516
|
-
@click.option(
|
384
|
+
@click.option("--path", required=True, help="Path to the APK file to install")
|
385
|
+
@click.option("--device", "-d", help="Device serial number or IP address", default=None)
|
517
386
|
@coro
|
518
387
|
async def setup(path: str, device: str | None):
|
519
388
|
"""Install an APK file and enable it as an accessibility service."""
|
@@ -521,60 +390,114 @@ async def setup(path: str, device: str | None):
|
|
521
390
|
if not os.path.exists(path):
|
522
391
|
console.print(f"[bold red]Error:[/] APK file not found at {path}")
|
523
392
|
return
|
524
|
-
|
393
|
+
|
525
394
|
if not device:
|
526
395
|
devices = await device_manager.list_devices()
|
527
396
|
if not devices:
|
528
397
|
console.print("[yellow]No devices connected.[/]")
|
529
398
|
return
|
530
|
-
|
399
|
+
|
531
400
|
device = devices[0].serial
|
532
401
|
console.print(f"[blue]Using device:[/] {device}")
|
533
|
-
|
534
|
-
os.environ["DROIDRUN_DEVICE_SERIAL"] = device
|
535
|
-
console.print(f"[blue]Set DROIDRUN_DEVICE_SERIAL to:[/] {device}")
|
536
|
-
|
402
|
+
|
537
403
|
device_obj = await device_manager.get_device(device)
|
538
404
|
if not device_obj:
|
539
|
-
console.print(
|
405
|
+
console.print(
|
406
|
+
f"[bold red]Error:[/] Could not get device object for {device}"
|
407
|
+
)
|
540
408
|
return
|
541
|
-
tools = Tools(serial=device)
|
542
|
-
console.print(f"[bold blue]Step 1/2: Installing APK:[/] {path}")
|
543
|
-
result = await tools.install_app(path, False, True)
|
544
409
|
|
410
|
+
console.print(f"[bold blue]Step 1/2: Installing APK:[/] {path}")
|
411
|
+
result = await device_obj.install_app(path, False, True)
|
412
|
+
|
545
413
|
if "Error" in result:
|
546
414
|
console.print(f"[bold red]Installation failed:[/] {result}")
|
547
415
|
return
|
548
416
|
else:
|
549
417
|
console.print(f"[bold green]Installation successful![/]")
|
550
|
-
|
418
|
+
|
551
419
|
console.print(f"[bold blue]Step 2/2: Enabling accessibility service[/]")
|
552
|
-
|
420
|
+
|
553
421
|
package = "com.droidrun.portal"
|
554
|
-
|
422
|
+
|
555
423
|
try:
|
556
|
-
await device_obj._adb.shell(
|
557
|
-
|
558
|
-
|
559
|
-
|
424
|
+
await device_obj._adb.shell(
|
425
|
+
device,
|
426
|
+
"settings put secure enabled_accessibility_services com.droidrun.portal/com.droidrun.portal.DroidrunPortalService",
|
427
|
+
)
|
428
|
+
|
429
|
+
await device_obj._adb.shell(
|
430
|
+
device, "settings put secure accessibility_enabled 1"
|
431
|
+
)
|
432
|
+
|
560
433
|
console.print("[green]Accessibility service enabled successfully![/]")
|
561
|
-
console.print(
|
562
|
-
|
434
|
+
console.print(
|
435
|
+
"\n[bold green]Setup complete![/] The DroidRun Portal is now installed and ready to use."
|
436
|
+
)
|
437
|
+
|
563
438
|
except Exception as e:
|
564
|
-
console.print(
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
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
|
+
)
|
445
|
+
|
446
|
+
await device_obj._adb.shell(
|
447
|
+
device, "am start -a android.settings.ACCESSIBILITY_SETTINGS"
|
448
|
+
)
|
449
|
+
|
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
|
+
)
|
571
456
|
console.print("2. Tap on the service name")
|
572
457
|
console.print("3. Toggle the switch to [bold]ON[/] to enable the service")
|
573
458
|
console.print("4. Accept any permission dialogs that appear")
|
574
|
-
|
575
|
-
console.print(
|
576
|
-
|
459
|
+
|
460
|
+
console.print(
|
461
|
+
"\n[bold green]APK installation complete![/] Please manually enable the accessibility service using the steps above."
|
462
|
+
)
|
463
|
+
|
577
464
|
except Exception as e:
|
578
465
|
console.print(f"[bold red]Error:[/] {e}")
|
579
466
|
import traceback
|
580
|
-
|
467
|
+
|
468
|
+
traceback.print_exc()
|
469
|
+
|
470
|
+
|
471
|
+
if __name__ == "__main__":
|
472
|
+
command = "Open the settings app"
|
473
|
+
device = None
|
474
|
+
provider = "GoogleGenAI"
|
475
|
+
model = "models/gemini-2.5-flash"
|
476
|
+
temperature = 0
|
477
|
+
api_key = os.getenv("GOOGLE_API_KEY")
|
478
|
+
steps = 15
|
479
|
+
vision = True
|
480
|
+
reasoning = True
|
481
|
+
reflection = False
|
482
|
+
tracing = True
|
483
|
+
debug = True
|
484
|
+
base_url = None
|
485
|
+
api_base = None
|
486
|
+
ios = False
|
487
|
+
run_command(
|
488
|
+
command=command,
|
489
|
+
device=device,
|
490
|
+
provider=provider,
|
491
|
+
model=model,
|
492
|
+
steps=steps,
|
493
|
+
temperature=temperature,
|
494
|
+
vision=vision,
|
495
|
+
reasoning=reasoning,
|
496
|
+
reflection=reflection,
|
497
|
+
tracing=tracing,
|
498
|
+
debug=debug,
|
499
|
+
base_url=base_url,
|
500
|
+
api_base=api_base,
|
501
|
+
api_key=api_key,
|
502
|
+
ios=ios
|
503
|
+
)
|