droidrun 0.2.0__py3-none-any.whl → 0.3.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 +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 +321 -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/context/todo.txt +4 -0
- droidrun/agent/droid/__init__.py +2 -2
- droidrun/agent/droid/droid_agent.py +264 -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 +268 -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 +268 -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 +333 -439
- droidrun/run.py +105 -0
- droidrun/tools/__init__.py +5 -10
- droidrun/tools/{actions.py → adb.py} +279 -238
- droidrun/tools/ios.py +594 -0
- droidrun/tools/tools.py +99 -0
- droidrun-0.3.0.dist-info/METADATA +149 -0
- droidrun-0.3.0.dist-info/RECORD +52 -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.0.dist-info}/WHEEL +0 -0
- {droidrun-0.2.0.dist-info → droidrun-0.3.0.dist-info}/entry_points.txt +0 -0
- {droidrun-0.2.0.dist-info → droidrun-0.3.0.dist-info}/licenses/LICENSE +0 -0
droidrun/cli/main.py
CHANGED
@@ -1,470 +1,311 @@
|
|
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, Tools
|
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
|
+
reasoning: bool,
|
63
|
+
reflection: bool,
|
64
|
+
tracing: bool,
|
65
|
+
debug: bool,
|
66
|
+
save_trajectory: bool = False,
|
67
|
+
ios: bool = False,
|
68
|
+
**kwargs,
|
69
|
+
):
|
117
70
|
"""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
|
-
|
71
|
+
log_handler = configure_logging(command, debug)
|
72
|
+
logger = logging.getLogger("droidrun")
|
73
|
+
|
74
|
+
log_handler.update_step("Initializing...")
|
75
|
+
|
76
|
+
with log_handler.render() as live:
|
200
77
|
try:
|
201
|
-
|
202
|
-
|
203
|
-
|
78
|
+
logger.info(f"🚀 Starting: {command}")
|
79
|
+
|
204
80
|
if not kwargs.get("temperature"):
|
205
81
|
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
82
|
|
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")
|
83
|
+
log_handler.update_step("Setting up tools...")
|
84
|
+
|
85
|
+
# Device setup
|
86
|
+
if device is None and not ios:
|
87
|
+
logger.info("🔍 Finding connected device...")
|
88
|
+
device_manager = DeviceManager()
|
89
|
+
devices = await device_manager.list_devices()
|
90
|
+
if not devices:
|
91
|
+
raise ValueError("No connected devices found.")
|
92
|
+
device = devices[0].serial
|
93
|
+
logger.info(f"📱 Using device: {device}")
|
94
|
+
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")
|
232
96
|
else:
|
233
|
-
|
234
|
-
|
97
|
+
logger.info(f"📱 Using device: {device}")
|
98
|
+
|
99
|
+
tools = AdbTools(serial=device) if not ios else IOSTools(url=device)
|
100
|
+
|
101
|
+
# LLM setup
|
102
|
+
log_handler.update_step("Initializing LLM...")
|
103
|
+
llm = load_llm(
|
104
|
+
provider_name=provider, model=model, base_url=base_url, **kwargs
|
105
|
+
)
|
106
|
+
logger.info(f"🧠 LLM ready: {provider}/{model}")
|
107
|
+
|
108
|
+
# Agent setup
|
109
|
+
log_handler.update_step("Initializing DroidAgent...")
|
110
|
+
|
111
|
+
mode = "planning with reasoning" if reasoning else "direct execution"
|
112
|
+
logger.info(f"🤖 Agent mode: {mode}")
|
113
|
+
|
235
114
|
if tracing:
|
236
|
-
|
237
|
-
|
238
|
-
update_display()
|
239
|
-
|
115
|
+
logger.info("🔍 Tracing enabled")
|
116
|
+
|
240
117
|
droid_agent = DroidAgent(
|
241
118
|
goal=command,
|
242
119
|
llm=llm,
|
243
|
-
|
244
|
-
tool_list=tool_list,
|
120
|
+
tools=tools,
|
245
121
|
max_steps=steps,
|
246
|
-
vision=vision,
|
247
122
|
timeout=1000,
|
248
|
-
max_retries=3,
|
249
123
|
reasoning=reasoning,
|
124
|
+
reflection=reflection,
|
250
125
|
enable_tracing=tracing,
|
251
|
-
debug=debug
|
126
|
+
debug=debug,
|
127
|
+
save_trajectories=save_trajectory
|
252
128
|
)
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
129
|
+
|
130
|
+
logger.info("▶️ Starting agent execution...")
|
131
|
+
logger.info("Press Ctrl+C to stop")
|
132
|
+
log_handler.update_step("Running agent...")
|
257
133
|
|
258
134
|
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)
|
135
|
+
handler = droid_agent.run()
|
136
|
+
|
137
|
+
async for event in handler.stream_events():
|
138
|
+
log_handler.handle_event(event)
|
139
|
+
result = await handler
|
315
140
|
|
316
141
|
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
|
-
|
142
|
+
log_handler.is_completed = True
|
143
|
+
log_handler.is_success = False
|
144
|
+
log_handler.current_step = "Stopped by user"
|
145
|
+
logger.info("⏹️ Stopped by user")
|
146
|
+
|
350
147
|
except Exception as e:
|
351
|
-
|
352
|
-
|
148
|
+
log_handler.is_completed = True
|
149
|
+
log_handler.is_success = False
|
150
|
+
log_handler.current_step = f"Error: {e}"
|
151
|
+
logger.error(f"💥 Error: {e}")
|
353
152
|
if debug:
|
354
153
|
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
|
-
|
154
|
+
|
155
|
+
logger.debug(traceback.format_exc())
|
156
|
+
|
390
157
|
except Exception as e:
|
391
|
-
|
392
|
-
|
158
|
+
log_handler.current_step = f"Error: {e}"
|
159
|
+
logger.error(f"💥 Setup error: {e}")
|
393
160
|
if debug:
|
394
161
|
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)}")
|
162
|
+
|
163
|
+
logger.debug(traceback.format_exc())
|
439
164
|
|
440
165
|
|
441
166
|
class DroidRunCLI(click.Group):
|
442
167
|
def parse_args(self, ctx, args):
|
443
|
-
|
444
|
-
|
168
|
+
# If the first arg is not an option and not a known command, treat as 'run'
|
169
|
+
if args and """not args[0].startswith("-")""" and args[0] not in self.commands:
|
170
|
+
args.insert(0, "run")
|
171
|
+
|
445
172
|
return super().parse_args(ctx, args)
|
446
173
|
|
174
|
+
|
175
|
+
@click.option("--device", "-d", help="Device serial number or IP address", default=None)
|
176
|
+
@click.option(
|
177
|
+
"--provider",
|
178
|
+
"-p",
|
179
|
+
help="LLM provider (OpenAI, Ollama, Anthropic, Gemini, DeepSeek)",
|
180
|
+
default="Gemini",
|
181
|
+
)
|
182
|
+
@click.option(
|
183
|
+
"--model",
|
184
|
+
"-m",
|
185
|
+
help="LLM model name",
|
186
|
+
default="models/gemini-2.5-pro",
|
187
|
+
)
|
188
|
+
@click.option("--temperature", type=float, help="Temperature for LLM", default=0.2)
|
189
|
+
@click.option("--steps", type=int, help="Maximum number of steps", default=15)
|
190
|
+
@click.option(
|
191
|
+
"--base_url",
|
192
|
+
"-u",
|
193
|
+
help="Base URL for API (e.g., OpenRouter or Ollama)",
|
194
|
+
default=None,
|
195
|
+
)
|
196
|
+
@click.option(
|
197
|
+
"--reasoning", is_flag=True, help="Enable/disable planning with reasoning", default=False
|
198
|
+
)
|
199
|
+
@click.option(
|
200
|
+
"--reflection", is_flag=True, help="Enable reflection step for higher reasoning", default=False
|
201
|
+
)
|
202
|
+
@click.option(
|
203
|
+
"--tracing", is_flag=True, help="Enable Arize Phoenix tracing", default=False
|
204
|
+
)
|
205
|
+
@click.option(
|
206
|
+
"--debug", is_flag=True, help="Enable verbose debug logging", default=False
|
207
|
+
)
|
208
|
+
@click.option(
|
209
|
+
"--save-trajectory",
|
210
|
+
is_flag=True,
|
211
|
+
help="Save agent trajectory to file",
|
212
|
+
default=False,
|
213
|
+
)
|
447
214
|
@click.group(cls=DroidRunCLI)
|
448
|
-
def cli(
|
215
|
+
def cli(
|
216
|
+
device: str | None,
|
217
|
+
provider: str,
|
218
|
+
model: str,
|
219
|
+
steps: int,
|
220
|
+
base_url: str,
|
221
|
+
temperature: float,
|
222
|
+
reasoning: bool,
|
223
|
+
reflection: bool,
|
224
|
+
tracing: bool,
|
225
|
+
debug: bool,
|
226
|
+
save_trajectory: bool,
|
227
|
+
):
|
449
228
|
"""DroidRun - Control your Android device through LLM agents."""
|
450
229
|
pass
|
451
230
|
|
231
|
+
|
452
232
|
@cli.command()
|
453
|
-
@click.argument(
|
454
|
-
@click.option(
|
455
|
-
@click.option(
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
@click.option(
|
462
|
-
|
463
|
-
|
464
|
-
|
233
|
+
@click.argument("command", type=str)
|
234
|
+
@click.option("--device", "-d", help="Device serial number or IP address", default=None)
|
235
|
+
@click.option(
|
236
|
+
"--provider",
|
237
|
+
"-p",
|
238
|
+
help="LLM provider (OpenAI, Ollama, Anthropic, Gemini, DeepSeek)",
|
239
|
+
default="Gemini",
|
240
|
+
)
|
241
|
+
@click.option(
|
242
|
+
"--model",
|
243
|
+
"-m",
|
244
|
+
help="LLM model name",
|
245
|
+
default="models/gemini-2.5-pro",
|
246
|
+
)
|
247
|
+
@click.option("--temperature", type=float, help="Temperature for LLM", default=0.2)
|
248
|
+
@click.option("--steps", type=int, help="Maximum number of steps", default=15)
|
249
|
+
@click.option(
|
250
|
+
"--base_url",
|
251
|
+
"-u",
|
252
|
+
help="Base URL for API (e.g., OpenRouter or Ollama)",
|
253
|
+
default=None,
|
254
|
+
)
|
255
|
+
@click.option(
|
256
|
+
"--reasoning", is_flag=True, help="Enable/disable planning with reasoning", default=False
|
257
|
+
)
|
258
|
+
@click.option(
|
259
|
+
"--reflection", is_flag=True, help="Enable reflection step for higher reasoning", default=False
|
260
|
+
)
|
261
|
+
@click.option(
|
262
|
+
"--tracing", is_flag=True, help="Enable Arize Phoenix tracing", default=False
|
263
|
+
)
|
264
|
+
@click.option(
|
265
|
+
"--debug", is_flag=True, help="Enable verbose debug logging", default=False
|
266
|
+
)
|
267
|
+
@click.option(
|
268
|
+
"--save-trajectory",
|
269
|
+
is_flag=True,
|
270
|
+
help="Save agent trajectory to file",
|
271
|
+
default=False,
|
272
|
+
)
|
273
|
+
@click.option(
|
274
|
+
"--ios", is_flag=True, help="Run on iOS device", default=False
|
275
|
+
)
|
276
|
+
def run(
|
277
|
+
command: str,
|
278
|
+
device: str | None,
|
279
|
+
provider: str,
|
280
|
+
model: str,
|
281
|
+
steps: int,
|
282
|
+
base_url: str,
|
283
|
+
temperature: float,
|
284
|
+
reasoning: bool,
|
285
|
+
reflection: bool,
|
286
|
+
tracing: bool,
|
287
|
+
debug: bool,
|
288
|
+
save_trajectory: bool,
|
289
|
+
ios: bool,
|
290
|
+
):
|
465
291
|
"""Run a command on your Android device using natural language."""
|
466
292
|
# Call our standalone function
|
467
|
-
return run_command(
|
293
|
+
return run_command(
|
294
|
+
command,
|
295
|
+
device,
|
296
|
+
provider,
|
297
|
+
model,
|
298
|
+
steps,
|
299
|
+
base_url,
|
300
|
+
reasoning,
|
301
|
+
reflection,
|
302
|
+
tracing,
|
303
|
+
debug,
|
304
|
+
temperature=temperature,
|
305
|
+
save_trajectory=save_trajectory,
|
306
|
+
ios=ios
|
307
|
+
)
|
308
|
+
|
468
309
|
|
469
310
|
@cli.command()
|
470
311
|
@coro
|
@@ -482,9 +323,10 @@ async def devices():
|
|
482
323
|
except Exception as e:
|
483
324
|
console.print(f"[red]Error listing devices: {e}[/]")
|
484
325
|
|
326
|
+
|
485
327
|
@cli.command()
|
486
|
-
@click.argument(
|
487
|
-
@click.option(
|
328
|
+
@click.argument("ip_address")
|
329
|
+
@click.option("--port", "-p", default=5555, help="ADB port (default: 5555)")
|
488
330
|
@coro
|
489
331
|
async def connect(ip_address: str, port: int):
|
490
332
|
"""Connect to a device over TCP/IP."""
|
@@ -497,8 +339,9 @@ async def connect(ip_address: str, port: int):
|
|
497
339
|
except Exception as e:
|
498
340
|
console.print(f"[red]Error connecting to device: {e}[/]")
|
499
341
|
|
342
|
+
|
500
343
|
@cli.command()
|
501
|
-
@click.argument(
|
344
|
+
@click.argument("serial")
|
502
345
|
@coro
|
503
346
|
async def disconnect(serial: str):
|
504
347
|
"""Disconnect from a device."""
|
@@ -511,9 +354,10 @@ async def disconnect(serial: str):
|
|
511
354
|
except Exception as e:
|
512
355
|
console.print(f"[red]Error disconnecting from device: {e}[/]")
|
513
356
|
|
357
|
+
|
514
358
|
@cli.command()
|
515
|
-
@click.option(
|
516
|
-
@click.option(
|
359
|
+
@click.option("--path", required=True, help="Path to the APK file to install")
|
360
|
+
@click.option("--device", "-d", help="Device serial number or IP address", default=None)
|
517
361
|
@coro
|
518
362
|
async def setup(path: str, device: str | None):
|
519
363
|
"""Install an APK file and enable it as an accessibility service."""
|
@@ -521,60 +365,110 @@ async def setup(path: str, device: str | None):
|
|
521
365
|
if not os.path.exists(path):
|
522
366
|
console.print(f"[bold red]Error:[/] APK file not found at {path}")
|
523
367
|
return
|
524
|
-
|
368
|
+
|
525
369
|
if not device:
|
526
370
|
devices = await device_manager.list_devices()
|
527
371
|
if not devices:
|
528
372
|
console.print("[yellow]No devices connected.[/]")
|
529
373
|
return
|
530
|
-
|
374
|
+
|
531
375
|
device = devices[0].serial
|
532
376
|
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
|
-
|
377
|
+
|
537
378
|
device_obj = await device_manager.get_device(device)
|
538
379
|
if not device_obj:
|
539
|
-
console.print(
|
380
|
+
console.print(
|
381
|
+
f"[bold red]Error:[/] Could not get device object for {device}"
|
382
|
+
)
|
540
383
|
return
|
541
384
|
tools = Tools(serial=device)
|
542
385
|
console.print(f"[bold blue]Step 1/2: Installing APK:[/] {path}")
|
543
386
|
result = await tools.install_app(path, False, True)
|
544
|
-
|
387
|
+
|
545
388
|
if "Error" in result:
|
546
389
|
console.print(f"[bold red]Installation failed:[/] {result}")
|
547
390
|
return
|
548
391
|
else:
|
549
392
|
console.print(f"[bold green]Installation successful![/]")
|
550
|
-
|
393
|
+
|
551
394
|
console.print(f"[bold blue]Step 2/2: Enabling accessibility service[/]")
|
552
|
-
|
395
|
+
|
553
396
|
package = "com.droidrun.portal"
|
554
|
-
|
397
|
+
|
555
398
|
try:
|
556
|
-
await device_obj._adb.shell(
|
557
|
-
|
558
|
-
|
559
|
-
|
399
|
+
await device_obj._adb.shell(
|
400
|
+
device,
|
401
|
+
"settings put secure enabled_accessibility_services com.droidrun.portal/com.droidrun.portal.DroidrunPortalService",
|
402
|
+
)
|
403
|
+
|
404
|
+
await device_obj._adb.shell(
|
405
|
+
device, "settings put secure accessibility_enabled 1"
|
406
|
+
)
|
407
|
+
|
560
408
|
console.print("[green]Accessibility service enabled successfully![/]")
|
561
|
-
console.print(
|
562
|
-
|
409
|
+
console.print(
|
410
|
+
"\n[bold green]Setup complete![/] The DroidRun Portal is now installed and ready to use."
|
411
|
+
)
|
412
|
+
|
563
413
|
except Exception as e:
|
564
|
-
console.print(
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
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
|
+
)
|
420
|
+
|
421
|
+
await device_obj._adb.shell(
|
422
|
+
device, "am start -a android.settings.ACCESSIBILITY_SETTINGS"
|
423
|
+
)
|
424
|
+
|
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
|
+
)
|
571
431
|
console.print("2. Tap on the service name")
|
572
432
|
console.print("3. Toggle the switch to [bold]ON[/] to enable the service")
|
573
433
|
console.print("4. Accept any permission dialogs that appear")
|
574
|
-
|
575
|
-
console.print(
|
576
|
-
|
434
|
+
|
435
|
+
console.print(
|
436
|
+
"\n[bold green]APK installation complete![/] Please manually enable the accessibility service using the steps above."
|
437
|
+
)
|
438
|
+
|
577
439
|
except Exception as e:
|
578
440
|
console.print(f"[bold red]Error:[/] {e}")
|
579
441
|
import traceback
|
580
|
-
|
442
|
+
|
443
|
+
traceback.print_exc()
|
444
|
+
|
445
|
+
|
446
|
+
if __name__ == "__main__":
|
447
|
+
command = "Open the settings app"
|
448
|
+
device = None
|
449
|
+
provider = "GoogleGenAI"
|
450
|
+
model = "models/gemini-2.5-flash"
|
451
|
+
temperature = 0
|
452
|
+
api_key = os.getenv("GEMINI_API_KEY")
|
453
|
+
steps = 15
|
454
|
+
reasoning = True
|
455
|
+
reflection = False
|
456
|
+
tracing = True
|
457
|
+
debug = True
|
458
|
+
base_url = None
|
459
|
+
ios = False
|
460
|
+
run_command(
|
461
|
+
command=command,
|
462
|
+
device=device,
|
463
|
+
provider=provider,
|
464
|
+
model=model,
|
465
|
+
steps=steps,
|
466
|
+
temperature=temperature,
|
467
|
+
reasoning=reasoning,
|
468
|
+
reflection=reflection,
|
469
|
+
tracing=tracing,
|
470
|
+
debug=debug,
|
471
|
+
base_url=base_url,
|
472
|
+
api_key=api_key,
|
473
|
+
ios=ios
|
474
|
+
)
|