droidrun 0.1.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 (58) hide show
  1. droidrun/__init__.py +22 -10
  2. droidrun/__main__.py +1 -2
  3. droidrun/adb/__init__.py +3 -3
  4. droidrun/adb/device.py +2 -2
  5. droidrun/adb/manager.py +2 -2
  6. droidrun/agent/__init__.py +5 -15
  7. droidrun/agent/codeact/__init__.py +11 -0
  8. droidrun/agent/codeact/codeact_agent.py +420 -0
  9. droidrun/agent/codeact/events.py +28 -0
  10. droidrun/agent/codeact/prompts.py +26 -0
  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 +13 -0
  26. droidrun/agent/droid/droid_agent.py +357 -0
  27. droidrun/agent/droid/events.py +28 -0
  28. droidrun/agent/oneflows/reflector.py +265 -0
  29. droidrun/agent/planner/__init__.py +13 -0
  30. droidrun/agent/planner/events.py +16 -0
  31. droidrun/agent/planner/planner_agent.py +268 -0
  32. droidrun/agent/planner/prompts.py +124 -0
  33. droidrun/agent/utils/__init__.py +3 -0
  34. droidrun/agent/utils/async_utils.py +17 -0
  35. droidrun/agent/utils/chat_utils.py +312 -0
  36. droidrun/agent/utils/executer.py +132 -0
  37. droidrun/agent/utils/llm_picker.py +147 -0
  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 +358 -149
  42. droidrun/run.py +105 -0
  43. droidrun/tools/__init__.py +4 -30
  44. droidrun/tools/adb.py +879 -0
  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/llm_reasoning.py +0 -567
  50. droidrun/agent/react_agent.py +0 -556
  51. droidrun/llm/__init__.py +0 -24
  52. droidrun/tools/actions.py +0 -854
  53. droidrun/tools/device.py +0 -29
  54. droidrun-0.1.0.dist-info/METADATA +0 -276
  55. droidrun-0.1.0.dist-info/RECORD +0 -20
  56. {droidrun-0.1.0.dist-info → droidrun-0.3.0.dist-info}/WHEEL +0 -0
  57. {droidrun-0.1.0.dist-info → droidrun-0.3.0.dist-info}/entry_points.txt +0 -0
  58. {droidrun-0.1.0.dist-info → droidrun-0.3.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,184 @@
1
+ """
2
+ Trajectory utilities for DroidRun agents.
3
+
4
+ This module provides helper functions for working with agent trajectories,
5
+ including saving, loading, and analyzing them.
6
+ """
7
+
8
+ import json
9
+ import logging
10
+ import os
11
+ import time
12
+ from typing import Dict, List, Any
13
+ from PIL import Image
14
+ import io
15
+ from llama_index.core.workflow import Event
16
+
17
+ logger = logging.getLogger("droidrun")
18
+
19
+ class Trajectory:
20
+
21
+ def __init__(self):
22
+ """Initializes an empty trajectory class."""
23
+ self.events: List[Event] = []
24
+ self.screenshots: List[bytes] = []
25
+
26
+
27
+ def create_screenshot_gif(self, output_path: str, duration: int = 1000) -> str:
28
+ """
29
+ Create a GIF from a list of screenshots.
30
+
31
+ Args:
32
+ output_path: Base path for the GIF (without extension)
33
+ duration: Duration for each frame in milliseconds
34
+
35
+ Returns:
36
+ Path to the created GIF file
37
+ """
38
+ if len(self.screenshots) == 0:
39
+ return None
40
+
41
+ images = []
42
+ for screenshot in self.screenshots:
43
+ img_data = screenshot
44
+ img = Image.open(io.BytesIO(img_data))
45
+ images.append(img)
46
+
47
+ # Save as GIF
48
+ gif_path = f"{output_path}.gif"
49
+ images[0].save(
50
+ gif_path,
51
+ save_all=True,
52
+ append_images=images[1:],
53
+ duration=duration,
54
+ loop=0
55
+ )
56
+
57
+ return gif_path
58
+
59
+ def save_trajectory(
60
+ self,
61
+ directory: str = "trajectories",
62
+ ) -> str:
63
+ """
64
+ Save trajectory steps to a JSON file and create a GIF of screenshots if available.
65
+
66
+ Args:
67
+ directory: Directory to save the trajectory files
68
+
69
+ Returns:
70
+ Path to the saved trajectory file
71
+ """
72
+ os.makedirs(directory, exist_ok=True)
73
+
74
+ timestamp = time.strftime("%Y%m%d_%H%M%S")
75
+ base_path = os.path.join(directory, f"trajectory_{timestamp}")
76
+
77
+ def make_serializable(obj):
78
+ """Recursively make objects JSON serializable."""
79
+ if hasattr(obj, "__class__") and obj.__class__.__name__ == "ChatMessage":
80
+ # Extract the text content from the ChatMessage
81
+ if hasattr(obj, "content") and obj.content is not None:
82
+ return {"role": obj.role.value, "content": obj.content}
83
+ # If content is not available, try extracting from blocks
84
+ elif hasattr(obj, "blocks") and obj.blocks:
85
+ text_content = ""
86
+ for block in obj.blocks:
87
+ if hasattr(block, "text"):
88
+ text_content += block.text
89
+ return {"role": obj.role.value, "content": text_content}
90
+ else:
91
+ return str(obj)
92
+ elif isinstance(obj, dict):
93
+ return {k: make_serializable(v) for k, v in obj.items()}
94
+ elif isinstance(obj, list):
95
+ return [make_serializable(item) for item in obj]
96
+ elif hasattr(obj, "__dict__"):
97
+ # Handle other custom objects by converting to dict
98
+ return {k: make_serializable(v) for k, v in obj.__dict__.items()
99
+ if not k.startswith('_')}
100
+ else:
101
+ return obj
102
+
103
+ serializable_events = []
104
+ for event in self.events:
105
+ event_dict = {
106
+ "type": event.__class__.__name__,
107
+ **{k: make_serializable(v) for k, v in event.__dict__.items()
108
+ if not k.startswith('_')}
109
+ }
110
+ serializable_events.append(event_dict)
111
+
112
+ json_path = f"{base_path}.json"
113
+ with open(json_path, "w") as f:
114
+ json.dump(serializable_events, f, indent=2)
115
+
116
+ self.create_screenshot_gif(base_path)
117
+
118
+ return json_path
119
+
120
+ def get_trajectory_statistics(trajectory_data: Dict[str, Any]) -> Dict[str, Any]:
121
+ """
122
+ Get statistics about a trajectory.
123
+
124
+ Args:
125
+ trajectory_data: The trajectory data dictionary
126
+
127
+ Returns:
128
+ Dictionary with statistics about the trajectory
129
+ """
130
+ trajectory_steps = trajectory_data.get("trajectory_steps", [])
131
+
132
+ # Count different types of steps
133
+ step_types = {}
134
+ for step in trajectory_steps:
135
+ step_type = step.get("type", "unknown")
136
+ step_types[step_type] = step_types.get(step_type, 0) + 1
137
+
138
+ # Count planning vs execution steps
139
+ planning_steps = sum(count for step_type, count in step_types.items()
140
+ if step_type.startswith("planner_"))
141
+ execution_steps = sum(count for step_type, count in step_types.items()
142
+ if step_type.startswith("codeact_"))
143
+
144
+ # Count successful vs failed executions
145
+ successful_executions = sum(1 for step in trajectory_steps
146
+ if step.get("type") == "codeact_execution"
147
+ and step.get("success", False))
148
+ failed_executions = sum(1 for step in trajectory_steps
149
+ if step.get("type") == "codeact_execution"
150
+ and not step.get("success", True))
151
+
152
+ # Return statistics
153
+ return {
154
+ "total_steps": len(trajectory_steps),
155
+ "step_types": step_types,
156
+ "planning_steps": planning_steps,
157
+ "execution_steps": execution_steps,
158
+ "successful_executions": successful_executions,
159
+ "failed_executions": failed_executions,
160
+ "goal_achieved": trajectory_data.get("success", False)
161
+ }
162
+
163
+ def print_trajectory_summary(self, trajectory_data: Dict[str, Any]) -> None:
164
+ """
165
+ Print a summary of a trajectory.
166
+
167
+ Args:
168
+ trajectory_data: The trajectory data dictionary
169
+ """
170
+ stats = self.get_trajectory_statistics(trajectory_data)
171
+
172
+ print("=== Trajectory Summary ===")
173
+ print(f"Goal: {trajectory_data.get('goal', 'Unknown')}")
174
+ print(f"Success: {trajectory_data.get('success', False)}")
175
+ print(f"Reason: {trajectory_data.get('reason', 'Unknown')}")
176
+ print(f"Total steps: {stats['total_steps']}")
177
+ print("Step breakdown:")
178
+ for step_type, count in stats['step_types'].items():
179
+ print(f" - {step_type}: {count}")
180
+ print(f"Planning steps: {stats['planning_steps']}")
181
+ print(f"Execution steps: {stats['execution_steps']}")
182
+ print(f"Successful executions: {stats['successful_executions']}")
183
+ print(f"Failed executions: {stats['failed_executions']}")
184
+ print("==========================")
droidrun/cli/__init__.py CHANGED
@@ -4,6 +4,6 @@ DroidRun CLI Module.
4
4
  This module provides command-line interfaces for interacting with Android devices.
5
5
  """
6
6
 
7
- from .main import cli
7
+ from droidrun.cli.main import cli
8
8
 
9
9
  __all__ = ["cli"]
droidrun/cli/logs.py ADDED
@@ -0,0 +1,283 @@
1
+ import logging
2
+ from rich.layout import Layout
3
+ from rich.panel import Panel
4
+ from rich.spinner import Spinner
5
+ from rich.console import Console
6
+ from rich.live import Live
7
+ from typing import List
8
+
9
+ from droidrun.agent.common.events import ScreenshotEvent
10
+ from droidrun.agent.planner.events import (
11
+ PlanInputEvent,
12
+ PlanThinkingEvent,
13
+ PlanCreatedEvent,
14
+ )
15
+ from droidrun.agent.codeact.events import (
16
+ TaskInputEvent,
17
+ TaskThinkingEvent,
18
+ TaskExecutionEvent,
19
+ TaskExecutionResultEvent,
20
+ TaskEndEvent,
21
+ )
22
+ from droidrun.agent.droid.events import (
23
+ CodeActExecuteEvent,
24
+ CodeActResultEvent,
25
+ ReasoningLogicEvent,
26
+ TaskRunnerEvent,
27
+ FinalizeEvent,
28
+ )
29
+
30
+
31
+ class LogHandler(logging.Handler):
32
+ def __init__(self, goal: str, current_step: str = "Initializing..."):
33
+ super().__init__()
34
+
35
+ self.goal = goal
36
+ self.current_step = current_step
37
+ self.is_completed = False
38
+ self.is_success = False
39
+ self.spinner = Spinner("dots")
40
+ self.console = Console()
41
+ self.layout = self._create_layout()
42
+ self.logs: List[str] = []
43
+
44
+ def emit(self, record):
45
+ msg = self.format(record)
46
+ lines = msg.splitlines()
47
+
48
+ for line in lines:
49
+ self.logs.append(line)
50
+ # Optionally, limit the log list size
51
+ if len(self.logs) > 100:
52
+ self.logs.pop(0)
53
+
54
+ self.rerender()
55
+
56
+ def render(self):
57
+ return Live(self.layout, refresh_per_second=4, console=self.console)
58
+
59
+ def rerender(self):
60
+ self._update_layout(
61
+ self.layout,
62
+ self.logs,
63
+ self.current_step,
64
+ self.goal,
65
+ self.is_completed,
66
+ self.is_success,
67
+ )
68
+
69
+ def update_step(self, step: str):
70
+ self.current_step = step
71
+ self.rerender()
72
+
73
+ def _create_layout(self):
74
+ """Create a layout with logs at top and status at bottom"""
75
+ layout = Layout()
76
+ layout.split(
77
+ Layout(name="logs"),
78
+ Layout(name="goal", size=3),
79
+ Layout(name="status", size=3),
80
+ )
81
+ return layout
82
+
83
+ def _update_layout(
84
+ self,
85
+ layout: Layout,
86
+ log_list: List[str],
87
+ step_message: str,
88
+ goal: str = None,
89
+ completed: bool = False,
90
+ success: bool = False,
91
+ ):
92
+ """Update the layout with current logs and step information"""
93
+ from rich.text import Text
94
+ import shutil
95
+
96
+ # Cache terminal size to avoid frequent recalculation
97
+ try:
98
+ terminal_height = shutil.get_terminal_size().lines
99
+ except:
100
+ terminal_height = 24 # fallback
101
+
102
+ # Reserve space for panels and borders (more conservative estimate)
103
+ other_components_height = 10 # goal panel + status panel + borders + padding
104
+ available_log_lines = max(8, terminal_height - other_components_height)
105
+
106
+ # Only show recent logs, but ensure we don't flicker
107
+ visible_logs = (
108
+ log_list[-available_log_lines:]
109
+ if len(log_list) > available_log_lines
110
+ else log_list
111
+ )
112
+
113
+ # Ensure we always have some content to prevent panel collapse
114
+ if not visible_logs:
115
+ visible_logs = ["Initializing..."]
116
+
117
+ log_content = "\n".join(visible_logs)
118
+
119
+ layout["logs"].update(
120
+ Panel(
121
+ log_content,
122
+ title=f"Activity Log ({len(log_list)} entries)",
123
+ border_style="blue",
124
+ title_align="left",
125
+ padding=(0, 1),
126
+ height=available_log_lines + 2,
127
+ )
128
+ )
129
+
130
+ if goal:
131
+ goal_text = Text(goal, style="bold")
132
+ layout["goal"].update(
133
+ Panel(
134
+ goal_text,
135
+ title="Goal",
136
+ border_style="magenta",
137
+ title_align="left",
138
+ padding=(0, 1),
139
+ height=3,
140
+ )
141
+ )
142
+
143
+ step_display = Text()
144
+
145
+ if completed:
146
+ if success:
147
+ step_display.append("✓ ", style="bold green")
148
+ panel_title = "Completed"
149
+ panel_style = "green"
150
+ else:
151
+ step_display.append("✗ ", style="bold red")
152
+ panel_title = "Failed"
153
+ panel_style = "red"
154
+ else:
155
+ step_display.append("⚡ ", style="bold yellow")
156
+ panel_title = "Status"
157
+ panel_style = "yellow"
158
+
159
+ step_display.append(step_message)
160
+
161
+ layout["status"].update(
162
+ Panel(
163
+ step_display,
164
+ title=panel_title,
165
+ border_style=panel_style,
166
+ title_align="left",
167
+ padding=(0, 1),
168
+ height=3,
169
+ )
170
+ )
171
+
172
+ def handle_event(self, event):
173
+ """Handle streaming events from the agent workflow."""
174
+ logger = logging.getLogger("droidrun")
175
+
176
+ # Log different event types with proper names
177
+ if isinstance(event, ScreenshotEvent):
178
+ logger.debug("📸 Taking screenshot...")
179
+
180
+ # Planner events
181
+ elif isinstance(event, PlanInputEvent):
182
+ self.current_step = "Planning..."
183
+ logger.info("💭 Planner receiving input...")
184
+
185
+ elif isinstance(event, PlanThinkingEvent):
186
+ if event.thoughts:
187
+ thoughts_preview = (
188
+ event.thoughts[:150] + "..."
189
+ if len(event.thoughts) > 150
190
+ else event.thoughts
191
+ )
192
+ logger.info(f"🧠 Planning: {thoughts_preview}")
193
+ if event.code:
194
+ logger.info(f"📝 Generated plan code")
195
+
196
+ elif isinstance(event, PlanCreatedEvent):
197
+ if event.tasks:
198
+ task_count = len(event.tasks) if event.tasks else 0
199
+ self.current_step = f"Plan ready ({task_count} tasks)"
200
+ logger.info(f"📋 Plan created with {task_count} tasks")
201
+ for task in event.tasks:
202
+ desc = task.description
203
+ logger.info(f"- {desc}")
204
+
205
+ # CodeAct events
206
+ elif isinstance(event, TaskInputEvent):
207
+ self.current_step = "Processing task input..."
208
+ logger.info("💬 Task input received...")
209
+
210
+ elif isinstance(event, TaskThinkingEvent):
211
+ if hasattr(event, "thoughts") and event.thoughts:
212
+ thoughts_preview = (
213
+ event.thoughts[:150] + "..."
214
+ if len(event.thoughts) > 150
215
+ else event.thoughts
216
+ )
217
+ logger.info(f"🧠 Thinking: {thoughts_preview}")
218
+ if hasattr(event, "code") and event.code:
219
+ logger.info(f"💻 Executing action code")
220
+ logger.debug(f"{event.code}")
221
+
222
+ elif isinstance(event, TaskExecutionEvent):
223
+ self.current_step = "Executing action..."
224
+ logger.info(f"⚡ Executing action...")
225
+
226
+ elif isinstance(event, TaskExecutionResultEvent):
227
+ if hasattr(event, "output") and event.output:
228
+ output = str(event.output)
229
+ if "Error" in output or "Exception" in output:
230
+ output_preview = (
231
+ output[:100] + "..." if len(output) > 100 else output
232
+ )
233
+ logger.info(f"❌ Action error: {output_preview}")
234
+ else:
235
+ output_preview = (
236
+ output[:100] + "..." if len(output) > 100 else output
237
+ )
238
+ logger.info(f"⚡ Action result: {output_preview}")
239
+
240
+ elif isinstance(event, TaskEndEvent):
241
+ if hasattr(event, "success") and hasattr(event, "reason"):
242
+ if event.success:
243
+ self.current_step = event.reason
244
+ logger.info(f"✅ Task completed: {event.reason}")
245
+ else:
246
+ self.current_step = f"Task failed"
247
+ logger.info(f"❌ Task failed: {event.reason}")
248
+
249
+ # Droid coordination events
250
+ elif isinstance(event, CodeActExecuteEvent):
251
+ self.current_step = "Executing task..."
252
+ logger.info(f"🔧 Starting task execution...")
253
+
254
+ elif isinstance(event, CodeActResultEvent):
255
+ if hasattr(event, "success") and hasattr(event, "reason"):
256
+ if event.success:
257
+ self.current_step = event.reason
258
+ logger.info(f"✅ Task completed: {event.reason}")
259
+ else:
260
+ self.current_step = f"Task failed"
261
+ logger.info(f"❌ Task failed: {event.reason}")
262
+
263
+ elif isinstance(event, ReasoningLogicEvent):
264
+ self.current_step = "Planning..."
265
+ logger.info(f"🤔 Planning next steps...")
266
+
267
+ elif isinstance(event, TaskRunnerEvent):
268
+ self.current_step = "Processing tasks..."
269
+ logger.info(f"🏃 Processing task queue...")
270
+
271
+ elif isinstance(event, FinalizeEvent):
272
+ if hasattr(event, "success") and hasattr(event, "reason"):
273
+ self.is_completed = True
274
+ self.is_success = event.success
275
+ if event.success:
276
+ self.current_step = f"Success: {event.reason}"
277
+ logger.info(f"🎉 Goal achieved: {event.reason}")
278
+ else:
279
+ self.current_step = f"Failed: {event.reason}"
280
+ logger.info(f"❌ Goal failed: {event.reason}")
281
+
282
+ else:
283
+ logger.debug(f"🔄 {event.__class__.__name__}")