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.
Files changed (55) hide show
  1. droidrun/__init__.py +16 -11
  2. droidrun/__main__.py +1 -1
  3. droidrun/adb/__init__.py +3 -3
  4. droidrun/adb/device.py +1 -1
  5. droidrun/adb/manager.py +2 -2
  6. droidrun/agent/__init__.py +6 -0
  7. droidrun/agent/codeact/__init__.py +2 -4
  8. droidrun/agent/codeact/codeact_agent.py +330 -235
  9. droidrun/agent/codeact/events.py +12 -20
  10. droidrun/agent/codeact/prompts.py +0 -52
  11. droidrun/agent/common/default.py +5 -0
  12. droidrun/agent/common/events.py +4 -0
  13. droidrun/agent/context/__init__.py +23 -0
  14. droidrun/agent/context/agent_persona.py +15 -0
  15. droidrun/agent/context/context_injection_manager.py +66 -0
  16. droidrun/agent/context/episodic_memory.py +15 -0
  17. droidrun/agent/context/personas/__init__.py +11 -0
  18. droidrun/agent/context/personas/app_starter.py +44 -0
  19. droidrun/agent/context/personas/default.py +95 -0
  20. droidrun/agent/context/personas/extractor.py +52 -0
  21. droidrun/agent/context/personas/ui_expert.py +107 -0
  22. droidrun/agent/context/reflection.py +20 -0
  23. droidrun/agent/context/task_manager.py +124 -0
  24. droidrun/agent/droid/__init__.py +2 -2
  25. droidrun/agent/droid/droid_agent.py +269 -325
  26. droidrun/agent/droid/events.py +28 -0
  27. droidrun/agent/oneflows/reflector.py +265 -0
  28. droidrun/agent/planner/__init__.py +2 -4
  29. droidrun/agent/planner/events.py +9 -13
  30. droidrun/agent/planner/planner_agent.py +288 -0
  31. droidrun/agent/planner/prompts.py +33 -53
  32. droidrun/agent/utils/__init__.py +3 -0
  33. droidrun/agent/utils/async_utils.py +1 -40
  34. droidrun/agent/utils/chat_utils.py +265 -48
  35. droidrun/agent/utils/executer.py +49 -14
  36. droidrun/agent/utils/llm_picker.py +14 -10
  37. droidrun/agent/utils/trajectory.py +184 -0
  38. droidrun/cli/__init__.py +1 -1
  39. droidrun/cli/logs.py +283 -0
  40. droidrun/cli/main.py +364 -441
  41. droidrun/tools/__init__.py +5 -10
  42. droidrun/tools/{actions.py → adb.py} +381 -412
  43. droidrun/tools/ios.py +596 -0
  44. droidrun/tools/tools.py +95 -0
  45. droidrun-0.3.1.dist-info/METADATA +150 -0
  46. droidrun-0.3.1.dist-info/RECORD +50 -0
  47. droidrun/agent/planner/task_manager.py +0 -355
  48. droidrun/agent/planner/workflow.py +0 -371
  49. droidrun/tools/device.py +0 -29
  50. droidrun/tools/loader.py +0 -60
  51. droidrun-0.2.0.dist-info/METADATA +0 -373
  52. droidrun-0.2.0.dist-info/RECORD +0 -32
  53. {droidrun-0.2.0.dist-info → droidrun-0.3.1.dist-info}/WHEEL +0 -0
  54. {droidrun-0.2.0.dist-info → droidrun-0.3.1.dist-info}/entry_points.txt +0 -0
  55. {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 time
17
- import queue
9
+ import warnings
18
10
  from rich.console import Console
19
- from rich.live import Live
20
- from rich.panel import Panel
21
- from rich.layout import Layout
22
- from rich.text import Text
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
- class RichHandler(logging.Handler):
37
- def emit(self, record):
38
- log_record = self.format(record)
39
- log_queue.put(log_record)
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(command: str, device: str | None, provider: str, model: str, steps: int, vision: bool, base_url: str, reasoning: bool, tracing: bool, debug: bool, **kwargs):
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
- global current_step
121
- current_step = "Initializing..."
122
- logs = []
123
- max_log_history = 1000
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
- update_display()
202
- logs.append(f"Executing command: {command}")
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
- if debug:
213
- logs.append(f"Tools: {list(tool_list.keys())}")
214
- update_display()
215
-
216
- device_serial = tools_instance.serial
217
- logs.append(f"Using device: {device_serial}")
218
- update_display()
219
-
220
- os.environ["DROIDRUN_DEVICE_SERIAL"] = device_serial
221
-
222
- current_step = "Initializing LLM..."
223
- update_display()
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
- logs.append("Using direct execution mode without planning")
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
- logs.append("Arize Phoenix tracing enabled")
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
- tools_instance=tools_instance,
244
- tool_list=tool_list,
122
+ tools=tools,
245
123
  max_steps=steps,
246
- vision=vision,
247
124
  timeout=1000,
248
- max_retries=3,
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
- logs.append("Press Ctrl+C to stop execution")
255
- current_step = "Running agent..."
256
- update_display()
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
- log_task = asyncio.create_task(process_logs())
260
- result = None
261
- try:
262
- result = await droid_agent.run()
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
- logs.append("Execution stopped by user.")
318
- current_step = "Stopped by user"
319
-
320
- is_completed = True
321
- is_success = False
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
- logs.append(f"An unexpected error occurred during agent execution: {e}")
352
- current_step = f"Error: {e}"
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
- logs.append(traceback.format_exc())
356
-
357
- is_completed = True
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
- logs.append(f"An unexpected error occurred during setup: {e}")
392
- current_step = f"Error: {e}"
161
+ log_handler.current_step = f"Error: {e}"
162
+ logger.error(f"💥 Setup error: {e}")
393
163
  if debug:
394
164
  import traceback
395
- logs.append(traceback.format_exc())
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
- if args and not args[0].startswith('-') and args[0] not in self.commands:
444
- args.insert(0, 'run')
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('command', type=str)
454
- @click.option('--device', '-d', help='Device serial number or IP address', default=None)
455
- @click.option('--provider', '-p', help='LLM provider (openai, ollama, anthropic, gemini, deepseek)', default='Gemini')
456
- @click.option('--model', '-m', help='LLM model name', default="models/gemini-2.5-pro-preview-05-06")
457
- @click.option('--temperature', type=float, help='Temperature for LLM', default=0.2)
458
- @click.option('--steps', type=int, help='Maximum number of steps', default=15)
459
- @click.option('--vision', is_flag=True, help='Enable vision capabilities', default=True)
460
- @click.option('--base_url', '-u', help='Base URL for API (e.g., OpenRouter or Ollama)', default=None)
461
- @click.option('--reasoning/--no-reasoning', is_flag=True, help='Enable/disable planning with reasoning', default=False)
462
- @click.option('--tracing', is_flag=True, help='Enable Arize Phoenix tracing', default=False)
463
- @click.option('--debug', is_flag=True, help='Enable verbose debug logging', default=False)
464
- def run(command: str, device: str | None, provider: str, model: str, steps: int, vision: bool, base_url: str, temperature: float, reasoning: bool, tracing: bool, debug: bool):
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(command, device, provider, model, steps, vision, base_url, reasoning, tracing, debug, temperature=temperature)
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('ip_address')
487
- @click.option('--port', '-p', default=5555, help='ADB port (default: 5555)')
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('serial')
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('--path', required=True, help='Path to the APK file to install')
516
- @click.option('--device', '-d', help='Device serial number or IP address', default=None)
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(f"[bold red]Error:[/] Could not get device object for {device}")
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(device, "settings put secure enabled_accessibility_services com.droidrun.portal/com.droidrun.portal.DroidrunPortalService")
557
-
558
- await device_obj._adb.shell(device, "settings put secure accessibility_enabled 1")
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("\n[bold green]Setup complete![/] The DroidRun Portal is now installed and ready to use.")
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(f"[yellow]Could not automatically enable accessibility service: {e}[/]")
565
- console.print("[yellow]Opening accessibility settings for manual configuration...[/]")
566
-
567
- await device_obj._adb.shell(device, "am start -a android.settings.ACCESSIBILITY_SETTINGS")
568
-
569
- console.print("\n[yellow]Please complete the following steps on your device:[/]")
570
- console.print(f"1. Find [bold]{package}[/] in the accessibility services list")
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("\n[bold green]APK installation complete![/] Please manually enable the accessibility service using the steps above.")
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
- traceback.print_exc()
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
+ )