droidrun 0.3.1__py3-none-any.whl → 0.3.3__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 +7 -12
- droidrun/agent/codeact/codeact_agent.py +9 -7
- droidrun/agent/common/events.py +44 -1
- droidrun/agent/context/personas/__init__.py +2 -2
- droidrun/agent/context/personas/big_agent.py +96 -0
- droidrun/agent/context/personas/ui_expert.py +1 -0
- droidrun/agent/droid/droid_agent.py +63 -11
- droidrun/agent/droid/events.py +4 -0
- droidrun/agent/planner/planner_agent.py +2 -2
- droidrun/agent/utils/executer.py +10 -2
- droidrun/agent/utils/llm_picker.py +1 -0
- droidrun/agent/utils/trajectory.py +258 -11
- droidrun/cli/main.py +179 -86
- droidrun/macro/__init__.py +14 -0
- droidrun/macro/__main__.py +10 -0
- droidrun/macro/cli.py +228 -0
- droidrun/macro/replay.py +309 -0
- droidrun/portal.py +138 -0
- droidrun/telemetry/__init__.py +4 -0
- droidrun/telemetry/events.py +27 -0
- droidrun/telemetry/tracker.py +84 -0
- droidrun/tools/adb.py +704 -372
- droidrun/tools/ios.py +169 -166
- droidrun/tools/tools.py +70 -17
- {droidrun-0.3.1.dist-info → droidrun-0.3.3.dist-info}/METADATA +31 -29
- droidrun-0.3.3.dist-info/RECORD +54 -0
- droidrun/adb/__init__.py +0 -13
- droidrun/adb/device.py +0 -315
- droidrun/adb/manager.py +0 -93
- droidrun/adb/wrapper.py +0 -226
- droidrun/agent/context/personas/extractor.py +0 -52
- droidrun-0.3.1.dist-info/RECORD +0 -50
- {droidrun-0.3.1.dist-info → droidrun-0.3.3.dist-info}/WHEEL +0 -0
- {droidrun-0.3.1.dist-info → droidrun-0.3.3.dist-info}/entry_points.txt +0 -0
- {droidrun-0.3.1.dist-info → droidrun-0.3.3.dist-info}/licenses/LICENSE +0 -0
@@ -18,10 +18,24 @@ logger = logging.getLogger("droidrun")
|
|
18
18
|
|
19
19
|
class Trajectory:
|
20
20
|
|
21
|
-
def __init__(self):
|
22
|
-
"""Initializes an empty trajectory class.
|
21
|
+
def __init__(self, goal: str = None):
|
22
|
+
"""Initializes an empty trajectory class.
|
23
|
+
|
24
|
+
Args:
|
25
|
+
goal: The goal/prompt that this trajectory is trying to achieve
|
26
|
+
"""
|
23
27
|
self.events: List[Event] = []
|
24
28
|
self.screenshots: List[bytes] = []
|
29
|
+
self.macro: List[Event] = []
|
30
|
+
self.goal = goal or "DroidRun automation sequence"
|
31
|
+
|
32
|
+
def set_goal(self, goal: str) -> None:
|
33
|
+
"""Update the goal/description for this trajectory.
|
34
|
+
|
35
|
+
Args:
|
36
|
+
goal: The new goal/prompt description
|
37
|
+
"""
|
38
|
+
self.goal = goal
|
25
39
|
|
26
40
|
|
27
41
|
def create_screenshot_gif(self, output_path: str, duration: int = 1000) -> str:
|
@@ -33,9 +47,10 @@ class Trajectory:
|
|
33
47
|
duration: Duration for each frame in milliseconds
|
34
48
|
|
35
49
|
Returns:
|
36
|
-
Path to the created GIF file
|
50
|
+
Path to the created GIF file, or None if no screenshots available
|
37
51
|
"""
|
38
52
|
if len(self.screenshots) == 0:
|
53
|
+
logger.info("📷 No screenshots available for GIF creation")
|
39
54
|
return None
|
40
55
|
|
41
56
|
images = []
|
@@ -62,17 +77,20 @@ class Trajectory:
|
|
62
77
|
) -> str:
|
63
78
|
"""
|
64
79
|
Save trajectory steps to a JSON file and create a GIF of screenshots if available.
|
80
|
+
Also saves the macro sequence as a separate file for replay.
|
81
|
+
Creates a dedicated folder for each trajectory containing all related files.
|
65
82
|
|
66
83
|
Args:
|
67
|
-
directory:
|
84
|
+
directory: Base directory to save the trajectory files
|
68
85
|
|
69
86
|
Returns:
|
70
|
-
Path to the
|
87
|
+
Path to the trajectory folder
|
71
88
|
"""
|
72
89
|
os.makedirs(directory, exist_ok=True)
|
73
90
|
|
74
91
|
timestamp = time.strftime("%Y%m%d_%H%M%S")
|
75
|
-
|
92
|
+
trajectory_folder = os.path.join(directory, f"trajectory_{timestamp}")
|
93
|
+
os.makedirs(trajectory_folder, exist_ok=True)
|
76
94
|
|
77
95
|
def make_serializable(obj):
|
78
96
|
"""Recursively make objects JSON serializable."""
|
@@ -100,6 +118,7 @@ class Trajectory:
|
|
100
118
|
else:
|
101
119
|
return obj
|
102
120
|
|
121
|
+
# Save main trajectory events
|
103
122
|
serializable_events = []
|
104
123
|
for event in self.events:
|
105
124
|
event_dict = {
|
@@ -109,13 +128,209 @@ class Trajectory:
|
|
109
128
|
}
|
110
129
|
serializable_events.append(event_dict)
|
111
130
|
|
112
|
-
|
113
|
-
with open(
|
131
|
+
trajectory_json_path = os.path.join(trajectory_folder, "trajectory.json")
|
132
|
+
with open(trajectory_json_path, "w") as f:
|
114
133
|
json.dump(serializable_events, f, indent=2)
|
115
134
|
|
116
|
-
|
135
|
+
# Save macro sequence as a separate file for replay
|
136
|
+
if self.macro:
|
137
|
+
macro_data = []
|
138
|
+
for macro_event in self.macro:
|
139
|
+
macro_dict = {
|
140
|
+
"type": macro_event.__class__.__name__,
|
141
|
+
**{k: make_serializable(v) for k, v in macro_event.__dict__.items()
|
142
|
+
if not k.startswith('_')}
|
143
|
+
}
|
144
|
+
macro_data.append(macro_dict)
|
145
|
+
|
146
|
+
macro_json_path = os.path.join(trajectory_folder, "macro.json")
|
147
|
+
with open(macro_json_path, "w") as f:
|
148
|
+
json.dump({
|
149
|
+
"version": "1.0",
|
150
|
+
"description": self.goal,
|
151
|
+
"timestamp": timestamp,
|
152
|
+
"total_actions": len(macro_data),
|
153
|
+
"actions": macro_data
|
154
|
+
}, f, indent=2)
|
155
|
+
|
156
|
+
logger.info(f"💾 Saved macro sequence with {len(macro_data)} actions to {macro_json_path}")
|
157
|
+
|
158
|
+
# Create screenshot GIF
|
159
|
+
gif_path = self.create_screenshot_gif(os.path.join(trajectory_folder, "screenshots"))
|
160
|
+
if gif_path:
|
161
|
+
logger.info(f"🎬 Saved screenshot GIF to {gif_path}")
|
162
|
+
|
163
|
+
logger.info(f"📁 Trajectory saved to folder: {trajectory_folder}")
|
164
|
+
return trajectory_folder
|
165
|
+
|
166
|
+
@staticmethod
|
167
|
+
def load_trajectory_folder(trajectory_folder: str) -> Dict[str, Any]:
|
168
|
+
"""
|
169
|
+
Load trajectory data from a trajectory folder.
|
170
|
+
|
171
|
+
Args:
|
172
|
+
trajectory_folder: Path to the trajectory folder
|
173
|
+
|
174
|
+
Returns:
|
175
|
+
Dictionary containing trajectory data, macro data, and file paths
|
176
|
+
"""
|
177
|
+
result = {
|
178
|
+
"trajectory_data": None,
|
179
|
+
"macro_data": None,
|
180
|
+
"gif_path": None,
|
181
|
+
"folder_path": trajectory_folder
|
182
|
+
}
|
183
|
+
|
184
|
+
try:
|
185
|
+
# Load main trajectory
|
186
|
+
trajectory_json_path = os.path.join(trajectory_folder, "trajectory.json")
|
187
|
+
if os.path.exists(trajectory_json_path):
|
188
|
+
with open(trajectory_json_path, "r") as f:
|
189
|
+
result["trajectory_data"] = json.load(f)
|
190
|
+
logger.info(f"📖 Loaded trajectory data from {trajectory_json_path}")
|
191
|
+
|
192
|
+
# Load macro sequence
|
193
|
+
macro_json_path = os.path.join(trajectory_folder, "macro.json")
|
194
|
+
if os.path.exists(macro_json_path):
|
195
|
+
with open(macro_json_path, "r") as f:
|
196
|
+
result["macro_data"] = json.load(f)
|
197
|
+
logger.info(f"📖 Loaded macro data from {macro_json_path}")
|
198
|
+
|
199
|
+
# Check for GIF
|
200
|
+
gif_path = os.path.join(trajectory_folder, "screenshots.gif")
|
201
|
+
if os.path.exists(gif_path):
|
202
|
+
result["gif_path"] = gif_path
|
203
|
+
logger.info(f"🎬 Found screenshot GIF at {gif_path}")
|
204
|
+
|
205
|
+
return result
|
206
|
+
|
207
|
+
except Exception as e:
|
208
|
+
logger.error(f"❌ Error loading trajectory folder {trajectory_folder}: {e}")
|
209
|
+
return result
|
210
|
+
|
211
|
+
@staticmethod
|
212
|
+
def load_macro_sequence(macro_file_path: str) -> Dict[str, Any]:
|
213
|
+
"""
|
214
|
+
Load a macro sequence from a saved macro file.
|
215
|
+
|
216
|
+
Args:
|
217
|
+
macro_file_path: Path to the macro JSON file (can be full path or trajectory folder)
|
218
|
+
|
219
|
+
Returns:
|
220
|
+
Dictionary containing the macro sequence data
|
221
|
+
"""
|
222
|
+
# Check if it's a folder path - if so, look for macro.json inside
|
223
|
+
if os.path.isdir(macro_file_path):
|
224
|
+
macro_file_path = os.path.join(macro_file_path, "macro.json")
|
225
|
+
|
226
|
+
try:
|
227
|
+
with open(macro_file_path, "r") as f:
|
228
|
+
macro_data = json.load(f)
|
229
|
+
|
230
|
+
logger.info(f"📖 Loaded macro sequence with {macro_data.get('total_actions', 0)} actions from {macro_file_path}")
|
231
|
+
return macro_data
|
232
|
+
except FileNotFoundError:
|
233
|
+
logger.error(f"❌ Macro file not found: {macro_file_path}")
|
234
|
+
return {}
|
235
|
+
except json.JSONDecodeError as e:
|
236
|
+
logger.error(f"❌ Error parsing macro file {macro_file_path}: {e}")
|
237
|
+
return {}
|
238
|
+
|
239
|
+
@staticmethod
|
240
|
+
def get_macro_summary(macro_data: Dict[str, Any]) -> Dict[str, Any]:
|
241
|
+
"""
|
242
|
+
Get a summary of a macro sequence.
|
243
|
+
|
244
|
+
Args:
|
245
|
+
macro_data: The macro data dictionary
|
246
|
+
|
247
|
+
Returns:
|
248
|
+
Dictionary with statistics about the macro
|
249
|
+
"""
|
250
|
+
if not macro_data or "actions" not in macro_data:
|
251
|
+
return {"error": "Invalid macro data"}
|
252
|
+
|
253
|
+
actions = macro_data["actions"]
|
254
|
+
|
255
|
+
# Count action types
|
256
|
+
action_types = {}
|
257
|
+
for action in actions:
|
258
|
+
action_type = action.get("action_type", "unknown")
|
259
|
+
action_types[action_type] = action_types.get(action_type, 0) + 1
|
260
|
+
|
261
|
+
# Calculate duration if timestamps are available
|
262
|
+
timestamps = [action.get("timestamp") for action in actions if action.get("timestamp")]
|
263
|
+
duration = max(timestamps) - min(timestamps) if len(timestamps) > 1 else 0
|
264
|
+
|
265
|
+
return {
|
266
|
+
"version": macro_data.get("version", "unknown"),
|
267
|
+
"description": macro_data.get("description", "No description"),
|
268
|
+
"total_actions": len(actions),
|
269
|
+
"action_types": action_types,
|
270
|
+
"duration_seconds": round(duration, 2) if duration > 0 else None,
|
271
|
+
"timestamp": macro_data.get("timestamp", "unknown")
|
272
|
+
}
|
117
273
|
|
118
|
-
|
274
|
+
@staticmethod
|
275
|
+
def print_macro_summary(macro_file_path: str) -> None:
|
276
|
+
"""
|
277
|
+
Print a summary of a macro sequence.
|
278
|
+
|
279
|
+
Args:
|
280
|
+
macro_file_path: Path to the macro JSON file or trajectory folder
|
281
|
+
"""
|
282
|
+
macro_data = Trajectory.load_macro_sequence(macro_file_path)
|
283
|
+
if not macro_data:
|
284
|
+
print("❌ Could not load macro data")
|
285
|
+
return
|
286
|
+
|
287
|
+
summary = Trajectory.get_macro_summary(macro_data)
|
288
|
+
|
289
|
+
print("=== Macro Summary ===")
|
290
|
+
print(f"File: {macro_file_path}")
|
291
|
+
print(f"Version: {summary.get('version', 'unknown')}")
|
292
|
+
print(f"Description: {summary.get('description', 'No description')}")
|
293
|
+
print(f"Timestamp: {summary.get('timestamp', 'unknown')}")
|
294
|
+
print(f"Total actions: {summary.get('total_actions', 0)}")
|
295
|
+
if summary.get('duration_seconds'):
|
296
|
+
print(f"Duration: {summary['duration_seconds']} seconds")
|
297
|
+
print("Action breakdown:")
|
298
|
+
for action_type, count in summary.get('action_types', {}).items():
|
299
|
+
print(f" - {action_type}: {count}")
|
300
|
+
print("=====================")
|
301
|
+
|
302
|
+
@staticmethod
|
303
|
+
def print_trajectory_folder_summary(trajectory_folder: str) -> None:
|
304
|
+
"""
|
305
|
+
Print a comprehensive summary of a trajectory folder.
|
306
|
+
|
307
|
+
Args:
|
308
|
+
trajectory_folder: Path to the trajectory folder
|
309
|
+
"""
|
310
|
+
folder_data = Trajectory.load_trajectory_folder(trajectory_folder)
|
311
|
+
|
312
|
+
print("=== Trajectory Folder Summary ===")
|
313
|
+
print(f"Folder: {trajectory_folder}")
|
314
|
+
print(f"Trajectory data: {'✅ Available' if folder_data['trajectory_data'] else '❌ Missing'}")
|
315
|
+
print(f"Macro data: {'✅ Available' if folder_data['macro_data'] else '❌ Missing'}")
|
316
|
+
print(f"Screenshot GIF: {'✅ Available' if folder_data['gif_path'] else '❌ Missing'}")
|
317
|
+
|
318
|
+
if folder_data['macro_data']:
|
319
|
+
print("\n--- Macro Summary ---")
|
320
|
+
summary = Trajectory.get_macro_summary(folder_data['macro_data'])
|
321
|
+
print(f"Description: {summary.get('description', 'No description')}")
|
322
|
+
print(f"Total actions: {summary.get('total_actions', 0)}")
|
323
|
+
if summary.get('duration_seconds'):
|
324
|
+
print(f"Duration: {summary['duration_seconds']} seconds")
|
325
|
+
print("Action breakdown:")
|
326
|
+
for action_type, count in summary.get('action_types', {}).items():
|
327
|
+
print(f" - {action_type}: {count}")
|
328
|
+
|
329
|
+
if folder_data['trajectory_data']:
|
330
|
+
print(f"\n--- Trajectory Summary ---")
|
331
|
+
print(f"Total events: {len(folder_data['trajectory_data'])}")
|
332
|
+
|
333
|
+
print("=================================")
|
119
334
|
|
120
335
|
def get_trajectory_statistics(trajectory_data: Dict[str, Any]) -> Dict[str, Any]:
|
121
336
|
"""
|
@@ -181,4 +396,36 @@ class Trajectory:
|
|
181
396
|
print(f"Execution steps: {stats['execution_steps']}")
|
182
397
|
print(f"Successful executions: {stats['successful_executions']}")
|
183
398
|
print(f"Failed executions: {stats['failed_executions']}")
|
184
|
-
print("==========================")
|
399
|
+
print("==========================")
|
400
|
+
|
401
|
+
|
402
|
+
# Example usage:
|
403
|
+
"""
|
404
|
+
# Save a trajectory with a specific goal (automatically creates folder structure)
|
405
|
+
trajectory = Trajectory(goal="Open settings and check battery level")
|
406
|
+
# ... add events and screenshots to trajectory ...
|
407
|
+
folder_path = trajectory.save_trajectory()
|
408
|
+
|
409
|
+
# Or update the goal later
|
410
|
+
trajectory.set_goal("Navigate to Settings and find device info")
|
411
|
+
|
412
|
+
# Load entire trajectory folder
|
413
|
+
folder_data = Trajectory.load_trajectory_folder(folder_path)
|
414
|
+
trajectory_events = folder_data['trajectory_data']
|
415
|
+
macro_actions = folder_data['macro_data']
|
416
|
+
gif_path = folder_data['gif_path']
|
417
|
+
|
418
|
+
# Load just the macro from folder
|
419
|
+
macro_data = Trajectory.load_macro_sequence(folder_path)
|
420
|
+
|
421
|
+
# Print summaries
|
422
|
+
Trajectory.print_trajectory_folder_summary(folder_path)
|
423
|
+
Trajectory.print_macro_summary(folder_path)
|
424
|
+
|
425
|
+
# Example folder structure created:
|
426
|
+
# trajectories/
|
427
|
+
# └── trajectory_20250108_143052/
|
428
|
+
# ├── trajectory.json # Full trajectory events
|
429
|
+
# ├── macro.json # Macro sequence with goal as description
|
430
|
+
# └── screenshots.gif # Screenshot animation
|
431
|
+
"""
|