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.
Files changed (57) 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 +321 -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/context/todo.txt +4 -0
  25. droidrun/agent/droid/__init__.py +2 -2
  26. droidrun/agent/droid/droid_agent.py +264 -325
  27. droidrun/agent/droid/events.py +28 -0
  28. droidrun/agent/oneflows/reflector.py +265 -0
  29. droidrun/agent/planner/__init__.py +2 -4
  30. droidrun/agent/planner/events.py +9 -13
  31. droidrun/agent/planner/planner_agent.py +268 -0
  32. droidrun/agent/planner/prompts.py +33 -53
  33. droidrun/agent/utils/__init__.py +3 -0
  34. droidrun/agent/utils/async_utils.py +1 -40
  35. droidrun/agent/utils/chat_utils.py +268 -48
  36. droidrun/agent/utils/executer.py +49 -14
  37. droidrun/agent/utils/llm_picker.py +14 -10
  38. droidrun/agent/utils/trajectory.py +184 -0
  39. droidrun/cli/__init__.py +1 -1
  40. droidrun/cli/logs.py +283 -0
  41. droidrun/cli/main.py +333 -439
  42. droidrun/run.py +105 -0
  43. droidrun/tools/__init__.py +5 -10
  44. droidrun/tools/{actions.py → adb.py} +279 -238
  45. droidrun/tools/ios.py +594 -0
  46. droidrun/tools/tools.py +99 -0
  47. droidrun-0.3.0.dist-info/METADATA +149 -0
  48. droidrun-0.3.0.dist-info/RECORD +52 -0
  49. droidrun/agent/planner/task_manager.py +0 -355
  50. droidrun/agent/planner/workflow.py +0 -371
  51. droidrun/tools/device.py +0 -29
  52. droidrun/tools/loader.py +0 -60
  53. droidrun-0.2.0.dist-info/METADATA +0 -373
  54. droidrun-0.2.0.dist-info/RECORD +0 -32
  55. {droidrun-0.2.0.dist-info → droidrun-0.3.0.dist-info}/WHEEL +0 -0
  56. {droidrun-0.2.0.dist-info → droidrun-0.3.0.dist-info}/entry_points.txt +0 -0
  57. {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 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, 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
- 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
+ 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
- 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
-
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
- update_display()
202
- logs.append(f"Executing command: {command}")
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
- 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")
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
- logs.append("Using direct execution mode without planning")
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
- logs.append("Arize Phoenix tracing enabled")
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
- tools_instance=tools_instance,
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
- logs.append("Press Ctrl+C to stop execution")
255
- current_step = "Running agent..."
256
- update_display()
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
- 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)
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
- 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
-
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
- logs.append(f"An unexpected error occurred during agent execution: {e}")
352
- current_step = f"Error: {e}"
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
- 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
-
154
+
155
+ logger.debug(traceback.format_exc())
156
+
390
157
  except Exception as e:
391
- logs.append(f"An unexpected error occurred during setup: {e}")
392
- current_step = f"Error: {e}"
158
+ log_handler.current_step = f"Error: {e}"
159
+ logger.error(f"💥 Setup error: {e}")
393
160
  if debug:
394
161
  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)}")
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
- if args and not args[0].startswith('-') and args[0] not in self.commands:
444
- args.insert(0, 'run')
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('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):
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(command, device, provider, model, steps, vision, base_url, reasoning, tracing, debug, temperature=temperature)
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('ip_address')
487
- @click.option('--port', '-p', default=5555, help='ADB port (default: 5555)')
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('serial')
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('--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)
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(f"[bold red]Error:[/] Could not get device object for {device}")
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(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
-
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("\n[bold green]Setup complete![/] The DroidRun Portal is now installed and ready to use.")
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(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")
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("\n[bold green]APK installation complete![/] Please manually enable the accessibility service using the steps above.")
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
- traceback.print_exc()
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
+ )