droidrun 0.3.5__py3-none-any.whl → 0.3.6__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/agent/codeact/codeact_agent.py +16 -3
- droidrun/agent/codeact/events.py +3 -0
- droidrun/agent/common/events.py +5 -1
- droidrun/agent/droid/droid_agent.py +137 -84
- droidrun/agent/planner/events.py +2 -0
- droidrun/agent/planner/planner_agent.py +15 -5
- droidrun/agent/usage.py +213 -0
- droidrun/agent/utils/executer.py +1 -1
- droidrun/agent/utils/llm_picker.py +91 -54
- droidrun/agent/utils/trajectory.py +256 -154
- droidrun/cli/logs.py +4 -1
- droidrun/cli/main.py +3 -1
- droidrun/portal.py +20 -7
- droidrun/telemetry/events.py +1 -1
- droidrun/tools/adb.py +99 -167
- droidrun/tools/tools.py +0 -1
- {droidrun-0.3.5.dist-info → droidrun-0.3.6.dist-info}/METADATA +17 -23
- {droidrun-0.3.5.dist-info → droidrun-0.3.6.dist-info}/RECORD +21 -20
- {droidrun-0.3.5.dist-info → droidrun-0.3.6.dist-info}/WHEEL +0 -0
- {droidrun-0.3.5.dist-info → droidrun-0.3.6.dist-info}/entry_points.txt +0 -0
- {droidrun-0.3.5.dist-info → droidrun-0.3.6.dist-info}/licenses/LICENSE +0 -0
@@ -9,6 +9,7 @@ import json
|
|
9
9
|
import logging
|
10
10
|
import os
|
11
11
|
import time
|
12
|
+
import uuid
|
12
13
|
from typing import Dict, List, Any
|
13
14
|
from PIL import Image
|
14
15
|
import io
|
@@ -16,61 +17,114 @@ from llama_index.core.workflow import Event
|
|
16
17
|
|
17
18
|
logger = logging.getLogger("droidrun")
|
18
19
|
|
20
|
+
|
21
|
+
def make_serializable(obj):
|
22
|
+
"""Recursively make objects JSON serializable."""
|
23
|
+
if hasattr(obj, "__class__") and obj.__class__.__name__ == "ChatMessage":
|
24
|
+
# Extract the text content from the ChatMessage
|
25
|
+
if hasattr(obj, "content") and obj.content is not None:
|
26
|
+
return {"role": obj.role.value, "content": obj.content}
|
27
|
+
# If content is not available, try extracting from blocks
|
28
|
+
elif hasattr(obj, "blocks") and obj.blocks:
|
29
|
+
text_content = ""
|
30
|
+
for block in obj.blocks:
|
31
|
+
if hasattr(block, "text"):
|
32
|
+
text_content += block.text
|
33
|
+
return {"role": obj.role.value, "content": text_content}
|
34
|
+
else:
|
35
|
+
return str(obj)
|
36
|
+
elif isinstance(obj, dict):
|
37
|
+
return {k: make_serializable(v) for k, v in obj.items()}
|
38
|
+
elif isinstance(obj, list):
|
39
|
+
return [make_serializable(item) for item in obj]
|
40
|
+
elif hasattr(obj, "__dict__"):
|
41
|
+
# Handle other custom objects by converting to dict
|
42
|
+
result = {}
|
43
|
+
for k, v in obj.__dict__.items():
|
44
|
+
if not k.startswith("_"):
|
45
|
+
try:
|
46
|
+
result[k] = make_serializable(v)
|
47
|
+
except (TypeError, ValueError) as e:
|
48
|
+
# If serialization fails, convert to string representation
|
49
|
+
logger.warning(f"Failed to serialize attribute {k}: {e}")
|
50
|
+
result[k] = str(v)
|
51
|
+
return result
|
52
|
+
else:
|
53
|
+
try:
|
54
|
+
# Test if the object is JSON serializable
|
55
|
+
json.dumps(obj)
|
56
|
+
return obj
|
57
|
+
except (TypeError, ValueError):
|
58
|
+
# If not serializable, convert to string
|
59
|
+
return str(obj)
|
60
|
+
|
19
61
|
class Trajectory:
|
20
62
|
|
21
63
|
def __init__(self, goal: str = None):
|
22
64
|
"""Initializes an empty trajectory class.
|
23
|
-
|
65
|
+
|
24
66
|
Args:
|
25
67
|
goal: The goal/prompt that this trajectory is trying to achieve
|
26
68
|
"""
|
27
69
|
self.events: List[Event] = []
|
28
|
-
self.screenshots: List[bytes] = []
|
70
|
+
self.screenshots: List[bytes] = []
|
71
|
+
self.ui_states: List[Dict[str, Any]] = []
|
29
72
|
self.macro: List[Event] = []
|
30
73
|
self.goal = goal or "DroidRun automation sequence"
|
31
74
|
|
32
75
|
def set_goal(self, goal: str) -> None:
|
33
76
|
"""Update the goal/description for this trajectory.
|
34
|
-
|
77
|
+
|
35
78
|
Args:
|
36
79
|
goal: The new goal/prompt description
|
37
80
|
"""
|
38
81
|
self.goal = goal
|
39
82
|
|
40
|
-
|
41
83
|
def create_screenshot_gif(self, output_path: str, duration: int = 1000) -> str:
|
42
84
|
"""
|
43
85
|
Create a GIF from a list of screenshots.
|
44
|
-
|
86
|
+
|
45
87
|
Args:
|
46
88
|
output_path: Base path for the GIF (without extension)
|
47
89
|
duration: Duration for each frame in milliseconds
|
48
|
-
|
90
|
+
|
49
91
|
Returns:
|
50
92
|
Path to the created GIF file, or None if no screenshots available
|
51
93
|
"""
|
52
94
|
if len(self.screenshots) == 0:
|
53
95
|
logger.info("📷 No screenshots available for GIF creation")
|
54
96
|
return None
|
55
|
-
|
97
|
+
|
56
98
|
images = []
|
57
99
|
for screenshot in self.screenshots:
|
58
100
|
img_data = screenshot
|
59
101
|
img = Image.open(io.BytesIO(img_data))
|
60
102
|
images.append(img)
|
61
|
-
|
103
|
+
|
62
104
|
# Save as GIF
|
63
|
-
gif_path =
|
105
|
+
gif_path = os.path.join(output_path, "trajectory.gif")
|
64
106
|
images[0].save(
|
65
|
-
gif_path,
|
66
|
-
save_all=True,
|
67
|
-
append_images=images[1:],
|
68
|
-
duration=duration,
|
69
|
-
loop=0
|
107
|
+
gif_path, save_all=True, append_images=images[1:], duration=duration, loop=0
|
70
108
|
)
|
71
|
-
|
109
|
+
|
72
110
|
return gif_path
|
73
111
|
|
112
|
+
def get_trajectory(self) -> List[Dict[str, Any]]:
|
113
|
+
# Save main trajectory events
|
114
|
+
serializable_events = []
|
115
|
+
for event in self.events:
|
116
|
+
event_dict = {
|
117
|
+
"type": event.__class__.__name__,
|
118
|
+
**{
|
119
|
+
k: make_serializable(v)
|
120
|
+
for k, v in event.__dict__.items()
|
121
|
+
if not k.startswith("_")
|
122
|
+
},
|
123
|
+
}
|
124
|
+
serializable_events.append(event_dict)
|
125
|
+
|
126
|
+
return serializable_events
|
127
|
+
|
74
128
|
def save_trajectory(
|
75
129
|
self,
|
76
130
|
directory: str = "trajectories",
|
@@ -79,55 +133,61 @@ class Trajectory:
|
|
79
133
|
Save trajectory steps to a JSON file and create a GIF of screenshots if available.
|
80
134
|
Also saves the macro sequence as a separate file for replay.
|
81
135
|
Creates a dedicated folder for each trajectory containing all related files.
|
82
|
-
|
136
|
+
|
83
137
|
Args:
|
84
138
|
directory: Base directory to save the trajectory files
|
85
|
-
|
139
|
+
|
86
140
|
Returns:
|
87
141
|
Path to the trajectory folder
|
88
142
|
"""
|
89
143
|
os.makedirs(directory, exist_ok=True)
|
90
|
-
|
91
144
|
timestamp = time.strftime("%Y%m%d_%H%M%S")
|
92
|
-
|
145
|
+
unique_id = str(uuid.uuid4())[:8]
|
146
|
+
trajectory_folder = os.path.join(directory, f"{timestamp}_{unique_id}")
|
93
147
|
os.makedirs(trajectory_folder, exist_ok=True)
|
94
|
-
|
95
|
-
def make_serializable(obj):
|
96
|
-
"""Recursively make objects JSON serializable."""
|
97
|
-
if hasattr(obj, "__class__") and obj.__class__.__name__ == "ChatMessage":
|
98
|
-
# Extract the text content from the ChatMessage
|
99
|
-
if hasattr(obj, "content") and obj.content is not None:
|
100
|
-
return {"role": obj.role.value, "content": obj.content}
|
101
|
-
# If content is not available, try extracting from blocks
|
102
|
-
elif hasattr(obj, "blocks") and obj.blocks:
|
103
|
-
text_content = ""
|
104
|
-
for block in obj.blocks:
|
105
|
-
if hasattr(block, "text"):
|
106
|
-
text_content += block.text
|
107
|
-
return {"role": obj.role.value, "content": text_content}
|
108
|
-
else:
|
109
|
-
return str(obj)
|
110
|
-
elif isinstance(obj, dict):
|
111
|
-
return {k: make_serializable(v) for k, v in obj.items()}
|
112
|
-
elif isinstance(obj, list):
|
113
|
-
return [make_serializable(item) for item in obj]
|
114
|
-
elif hasattr(obj, "__dict__"):
|
115
|
-
# Handle other custom objects by converting to dict
|
116
|
-
return {k: make_serializable(v) for k, v in obj.__dict__.items()
|
117
|
-
if not k.startswith('_')}
|
118
|
-
else:
|
119
|
-
return obj
|
120
|
-
|
121
|
-
# Save main trajectory events
|
148
|
+
|
122
149
|
serializable_events = []
|
123
150
|
for event in self.events:
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
151
|
+
# Debug: Check if tokens attribute exists
|
152
|
+
if hasattr(event, "tokens"):
|
153
|
+
logger.debug(
|
154
|
+
f"Event {event.__class__.__name__} has tokens: {event.tokens}"
|
155
|
+
)
|
156
|
+
else:
|
157
|
+
logger.debug(
|
158
|
+
f"Event {event.__class__.__name__} does NOT have tokens attribute"
|
159
|
+
)
|
160
|
+
|
161
|
+
# Start with the basic event structure
|
162
|
+
event_dict = {"type": event.__class__.__name__}
|
163
|
+
|
164
|
+
# Add all attributes from __dict__
|
165
|
+
for k, v in event.__dict__.items():
|
166
|
+
if not k.startswith("_"):
|
167
|
+
try:
|
168
|
+
event_dict[k] = make_serializable(v)
|
169
|
+
except (TypeError, ValueError) as e:
|
170
|
+
logger.warning(f"Failed to serialize attribute {k}: {e}")
|
171
|
+
event_dict[k] = str(v)
|
172
|
+
|
173
|
+
# Explicitly check for and add tokens attribute if it exists
|
174
|
+
if hasattr(event, "tokens") and "tokens" not in event_dict:
|
175
|
+
logger.debug(
|
176
|
+
f"Manually adding tokens attribute for {event.__class__.__name__}"
|
177
|
+
)
|
178
|
+
event_dict["tokens"] = make_serializable(event.tokens)
|
179
|
+
|
180
|
+
# Debug: Check if tokens is in the serialized event
|
181
|
+
if "tokens" in event_dict:
|
182
|
+
logger.debug(
|
183
|
+
f"Serialized event contains tokens: {event_dict['tokens']}"
|
184
|
+
)
|
185
|
+
else:
|
186
|
+
logger.debug(f"Serialized event does NOT contain tokens")
|
187
|
+
|
129
188
|
serializable_events.append(event_dict)
|
130
|
-
|
189
|
+
|
190
|
+
|
131
191
|
trajectory_json_path = os.path.join(trajectory_folder, "trajectory.json")
|
132
192
|
with open(trajectory_json_path, "w") as f:
|
133
193
|
json.dump(serializable_events, f, indent=2)
|
@@ -138,39 +198,60 @@ class Trajectory:
|
|
138
198
|
for macro_event in self.macro:
|
139
199
|
macro_dict = {
|
140
200
|
"type": macro_event.__class__.__name__,
|
141
|
-
**{
|
142
|
-
|
201
|
+
**{
|
202
|
+
k: make_serializable(v)
|
203
|
+
for k, v in macro_event.__dict__.items()
|
204
|
+
if not k.startswith("_")
|
205
|
+
},
|
143
206
|
}
|
144
207
|
macro_data.append(macro_dict)
|
145
|
-
|
208
|
+
|
146
209
|
macro_json_path = os.path.join(trajectory_folder, "macro.json")
|
147
210
|
with open(macro_json_path, "w") as f:
|
148
|
-
json.dump(
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
211
|
+
json.dump(
|
212
|
+
{
|
213
|
+
"version": "1.0",
|
214
|
+
"description": self.goal,
|
215
|
+
"timestamp": timestamp,
|
216
|
+
"total_actions": len(macro_data),
|
217
|
+
"actions": macro_data,
|
218
|
+
},
|
219
|
+
f,
|
220
|
+
indent=2,
|
221
|
+
)
|
222
|
+
|
223
|
+
logger.info(
|
224
|
+
f"💾 Saved macro sequence with {len(macro_data)} actions to {macro_json_path}"
|
225
|
+
)
|
226
|
+
screenshots_folder = os.path.join(trajectory_folder, "screenshots");
|
227
|
+
os.makedirs(screenshots_folder, exist_ok=True)
|
228
|
+
|
229
|
+
gif_path = self.create_screenshot_gif(
|
230
|
+
screenshots_folder
|
231
|
+
)
|
160
232
|
if gif_path:
|
161
233
|
logger.info(f"🎬 Saved screenshot GIF to {gif_path}")
|
162
234
|
|
163
235
|
logger.info(f"📁 Trajectory saved to folder: {trajectory_folder}")
|
236
|
+
|
237
|
+
if len(self.ui_states) != len(self.screenshots):
|
238
|
+
logger.warning("UI states and screenshots are not the same length!")
|
239
|
+
|
240
|
+
os.makedirs(os.path.join(trajectory_folder, "ui_states"), exist_ok=True)
|
241
|
+
for idx, ui_state in enumerate(self.ui_states):
|
242
|
+
ui_states_path = os.path.join(trajectory_folder, "ui_states", f"{idx}.json")
|
243
|
+
with open(ui_states_path, "w", encoding="utf-8") as f:
|
244
|
+
json.dump(ui_state, f, ensure_ascii=False, indent=2)
|
164
245
|
return trajectory_folder
|
165
246
|
|
166
247
|
@staticmethod
|
167
248
|
def load_trajectory_folder(trajectory_folder: str) -> Dict[str, Any]:
|
168
249
|
"""
|
169
250
|
Load trajectory data from a trajectory folder.
|
170
|
-
|
251
|
+
|
171
252
|
Args:
|
172
253
|
trajectory_folder: Path to the trajectory folder
|
173
|
-
|
254
|
+
|
174
255
|
Returns:
|
175
256
|
Dictionary containing trajectory data, macro data, and file paths
|
176
257
|
"""
|
@@ -178,9 +259,9 @@ class Trajectory:
|
|
178
259
|
"trajectory_data": None,
|
179
260
|
"macro_data": None,
|
180
261
|
"gif_path": None,
|
181
|
-
"folder_path": trajectory_folder
|
262
|
+
"folder_path": trajectory_folder,
|
182
263
|
}
|
183
|
-
|
264
|
+
|
184
265
|
try:
|
185
266
|
# Load main trajectory
|
186
267
|
trajectory_json_path = os.path.join(trajectory_folder, "trajectory.json")
|
@@ -188,22 +269,22 @@ class Trajectory:
|
|
188
269
|
with open(trajectory_json_path, "r") as f:
|
189
270
|
result["trajectory_data"] = json.load(f)
|
190
271
|
logger.info(f"📖 Loaded trajectory data from {trajectory_json_path}")
|
191
|
-
|
272
|
+
|
192
273
|
# Load macro sequence
|
193
274
|
macro_json_path = os.path.join(trajectory_folder, "macro.json")
|
194
275
|
if os.path.exists(macro_json_path):
|
195
276
|
with open(macro_json_path, "r") as f:
|
196
277
|
result["macro_data"] = json.load(f)
|
197
278
|
logger.info(f"📖 Loaded macro data from {macro_json_path}")
|
198
|
-
|
279
|
+
|
199
280
|
# Check for GIF
|
200
281
|
gif_path = os.path.join(trajectory_folder, "screenshots.gif")
|
201
282
|
if os.path.exists(gif_path):
|
202
283
|
result["gif_path"] = gif_path
|
203
284
|
logger.info(f"🎬 Found screenshot GIF at {gif_path}")
|
204
|
-
|
285
|
+
|
205
286
|
return result
|
206
|
-
|
287
|
+
|
207
288
|
except Exception as e:
|
208
289
|
logger.error(f"❌ Error loading trajectory folder {trajectory_folder}: {e}")
|
209
290
|
return result
|
@@ -212,22 +293,24 @@ class Trajectory:
|
|
212
293
|
def load_macro_sequence(macro_file_path: str) -> Dict[str, Any]:
|
213
294
|
"""
|
214
295
|
Load a macro sequence from a saved macro file.
|
215
|
-
|
296
|
+
|
216
297
|
Args:
|
217
298
|
macro_file_path: Path to the macro JSON file (can be full path or trajectory folder)
|
218
|
-
|
299
|
+
|
219
300
|
Returns:
|
220
301
|
Dictionary containing the macro sequence data
|
221
302
|
"""
|
222
303
|
# Check if it's a folder path - if so, look for macro.json inside
|
223
304
|
if os.path.isdir(macro_file_path):
|
224
305
|
macro_file_path = os.path.join(macro_file_path, "macro.json")
|
225
|
-
|
306
|
+
|
226
307
|
try:
|
227
308
|
with open(macro_file_path, "r") as f:
|
228
309
|
macro_data = json.load(f)
|
229
|
-
|
230
|
-
logger.info(
|
310
|
+
|
311
|
+
logger.info(
|
312
|
+
f"📖 Loaded macro sequence with {macro_data.get('total_actions', 0)} actions from {macro_file_path}"
|
313
|
+
)
|
231
314
|
return macro_data
|
232
315
|
except FileNotFoundError:
|
233
316
|
logger.error(f"❌ Macro file not found: {macro_file_path}")
|
@@ -240,42 +323,44 @@ class Trajectory:
|
|
240
323
|
def get_macro_summary(macro_data: Dict[str, Any]) -> Dict[str, Any]:
|
241
324
|
"""
|
242
325
|
Get a summary of a macro sequence.
|
243
|
-
|
326
|
+
|
244
327
|
Args:
|
245
328
|
macro_data: The macro data dictionary
|
246
|
-
|
329
|
+
|
247
330
|
Returns:
|
248
331
|
Dictionary with statistics about the macro
|
249
332
|
"""
|
250
333
|
if not macro_data or "actions" not in macro_data:
|
251
334
|
return {"error": "Invalid macro data"}
|
252
|
-
|
335
|
+
|
253
336
|
actions = macro_data["actions"]
|
254
|
-
|
337
|
+
|
255
338
|
# Count action types
|
256
339
|
action_types = {}
|
257
340
|
for action in actions:
|
258
341
|
action_type = action.get("action_type", "unknown")
|
259
342
|
action_types[action_type] = action_types.get(action_type, 0) + 1
|
260
|
-
|
343
|
+
|
261
344
|
# Calculate duration if timestamps are available
|
262
|
-
timestamps = [
|
345
|
+
timestamps = [
|
346
|
+
action.get("timestamp") for action in actions if action.get("timestamp")
|
347
|
+
]
|
263
348
|
duration = max(timestamps) - min(timestamps) if len(timestamps) > 1 else 0
|
264
|
-
|
349
|
+
|
265
350
|
return {
|
266
351
|
"version": macro_data.get("version", "unknown"),
|
267
352
|
"description": macro_data.get("description", "No description"),
|
268
353
|
"total_actions": len(actions),
|
269
354
|
"action_types": action_types,
|
270
355
|
"duration_seconds": round(duration, 2) if duration > 0 else None,
|
271
|
-
"timestamp": macro_data.get("timestamp", "unknown")
|
356
|
+
"timestamp": macro_data.get("timestamp", "unknown"),
|
272
357
|
}
|
273
358
|
|
274
359
|
@staticmethod
|
275
360
|
def print_macro_summary(macro_file_path: str) -> None:
|
276
361
|
"""
|
277
362
|
Print a summary of a macro sequence.
|
278
|
-
|
363
|
+
|
279
364
|
Args:
|
280
365
|
macro_file_path: Path to the macro JSON file or trajectory folder
|
281
366
|
"""
|
@@ -283,19 +368,19 @@ class Trajectory:
|
|
283
368
|
if not macro_data:
|
284
369
|
print("❌ Could not load macro data")
|
285
370
|
return
|
286
|
-
|
371
|
+
|
287
372
|
summary = Trajectory.get_macro_summary(macro_data)
|
288
|
-
|
373
|
+
|
289
374
|
print("=== Macro Summary ===")
|
290
375
|
print(f"File: {macro_file_path}")
|
291
376
|
print(f"Version: {summary.get('version', 'unknown')}")
|
292
377
|
print(f"Description: {summary.get('description', 'No description')}")
|
293
378
|
print(f"Timestamp: {summary.get('timestamp', 'unknown')}")
|
294
379
|
print(f"Total actions: {summary.get('total_actions', 0)}")
|
295
|
-
if summary.get(
|
380
|
+
if summary.get("duration_seconds"):
|
296
381
|
print(f"Duration: {summary['duration_seconds']} seconds")
|
297
382
|
print("Action breakdown:")
|
298
|
-
for action_type, count in summary.get(
|
383
|
+
for action_type, count in summary.get("action_types", {}).items():
|
299
384
|
print(f" - {action_type}: {count}")
|
300
385
|
print("=====================")
|
301
386
|
|
@@ -303,94 +388,57 @@ class Trajectory:
|
|
303
388
|
def print_trajectory_folder_summary(trajectory_folder: str) -> None:
|
304
389
|
"""
|
305
390
|
Print a comprehensive summary of a trajectory folder.
|
306
|
-
|
391
|
+
|
307
392
|
Args:
|
308
393
|
trajectory_folder: Path to the trajectory folder
|
309
394
|
"""
|
310
395
|
folder_data = Trajectory.load_trajectory_folder(trajectory_folder)
|
311
|
-
|
396
|
+
|
312
397
|
print("=== Trajectory Folder Summary ===")
|
313
398
|
print(f"Folder: {trajectory_folder}")
|
314
|
-
print(
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
399
|
+
print(
|
400
|
+
f"Trajectory data: {'✅ Available' if folder_data['trajectory_data'] else '❌ Missing'}"
|
401
|
+
)
|
402
|
+
print(
|
403
|
+
f"Macro data: {'✅ Available' if folder_data['macro_data'] else '❌ Missing'}"
|
404
|
+
)
|
405
|
+
print(
|
406
|
+
f"Screenshot GIF: {'✅ Available' if folder_data['gif_path'] else '❌ Missing'}"
|
407
|
+
)
|
408
|
+
|
409
|
+
if folder_data["macro_data"]:
|
319
410
|
print("\n--- Macro Summary ---")
|
320
|
-
summary = Trajectory.get_macro_summary(folder_data[
|
411
|
+
summary = Trajectory.get_macro_summary(folder_data["macro_data"])
|
321
412
|
print(f"Description: {summary.get('description', 'No description')}")
|
322
413
|
print(f"Total actions: {summary.get('total_actions', 0)}")
|
323
|
-
if summary.get(
|
414
|
+
if summary.get("duration_seconds"):
|
324
415
|
print(f"Duration: {summary['duration_seconds']} seconds")
|
325
416
|
print("Action breakdown:")
|
326
|
-
for action_type, count in summary.get(
|
417
|
+
for action_type, count in summary.get("action_types", {}).items():
|
327
418
|
print(f" - {action_type}: {count}")
|
328
|
-
|
329
|
-
if folder_data[
|
419
|
+
|
420
|
+
if folder_data["trajectory_data"]:
|
330
421
|
print(f"\n--- Trajectory Summary ---")
|
331
422
|
print(f"Total events: {len(folder_data['trajectory_data'])}")
|
332
|
-
|
333
|
-
print("=================================")
|
334
423
|
|
335
|
-
|
336
|
-
"""
|
337
|
-
Get statistics about a trajectory.
|
338
|
-
|
339
|
-
Args:
|
340
|
-
trajectory_data: The trajectory data dictionary
|
341
|
-
|
342
|
-
Returns:
|
343
|
-
Dictionary with statistics about the trajectory
|
344
|
-
"""
|
345
|
-
trajectory_steps = trajectory_data.get("trajectory_steps", [])
|
346
|
-
|
347
|
-
# Count different types of steps
|
348
|
-
step_types = {}
|
349
|
-
for step in trajectory_steps:
|
350
|
-
step_type = step.get("type", "unknown")
|
351
|
-
step_types[step_type] = step_types.get(step_type, 0) + 1
|
352
|
-
|
353
|
-
# Count planning vs execution steps
|
354
|
-
planning_steps = sum(count for step_type, count in step_types.items()
|
355
|
-
if step_type.startswith("planner_"))
|
356
|
-
execution_steps = sum(count for step_type, count in step_types.items()
|
357
|
-
if step_type.startswith("codeact_"))
|
358
|
-
|
359
|
-
# Count successful vs failed executions
|
360
|
-
successful_executions = sum(1 for step in trajectory_steps
|
361
|
-
if step.get("type") == "codeact_execution"
|
362
|
-
and step.get("success", False))
|
363
|
-
failed_executions = sum(1 for step in trajectory_steps
|
364
|
-
if step.get("type") == "codeact_execution"
|
365
|
-
and not step.get("success", True))
|
366
|
-
|
367
|
-
# Return statistics
|
368
|
-
return {
|
369
|
-
"total_steps": len(trajectory_steps),
|
370
|
-
"step_types": step_types,
|
371
|
-
"planning_steps": planning_steps,
|
372
|
-
"execution_steps": execution_steps,
|
373
|
-
"successful_executions": successful_executions,
|
374
|
-
"failed_executions": failed_executions,
|
375
|
-
"goal_achieved": trajectory_data.get("success", False)
|
376
|
-
}
|
424
|
+
print("=================================")
|
377
425
|
|
378
426
|
def print_trajectory_summary(self, trajectory_data: Dict[str, Any]) -> None:
|
379
427
|
"""
|
380
428
|
Print a summary of a trajectory.
|
381
|
-
|
429
|
+
|
382
430
|
Args:
|
383
431
|
trajectory_data: The trajectory data dictionary
|
384
432
|
"""
|
385
433
|
stats = self.get_trajectory_statistics(trajectory_data)
|
386
|
-
|
434
|
+
|
387
435
|
print("=== Trajectory Summary ===")
|
388
436
|
print(f"Goal: {trajectory_data.get('goal', 'Unknown')}")
|
389
437
|
print(f"Success: {trajectory_data.get('success', False)}")
|
390
438
|
print(f"Reason: {trajectory_data.get('reason', 'Unknown')}")
|
391
439
|
print(f"Total steps: {stats['total_steps']}")
|
392
440
|
print("Step breakdown:")
|
393
|
-
for step_type, count in stats[
|
441
|
+
for step_type, count in stats["step_types"].items():
|
394
442
|
print(f" - {step_type}: {count}")
|
395
443
|
print(f"Planning steps: {stats['planning_steps']}")
|
396
444
|
print(f"Execution steps: {stats['execution_steps']}")
|
@@ -399,6 +447,60 @@ class Trajectory:
|
|
399
447
|
print("==========================")
|
400
448
|
|
401
449
|
|
450
|
+
def get_trajectory_statistics(
|
451
|
+
trajectory_steps: List[Dict[str, Any]],
|
452
|
+
) -> Dict[str, Any]:
|
453
|
+
"""
|
454
|
+
Get statistics about a trajectory.
|
455
|
+
|
456
|
+
Args:
|
457
|
+
trajectory_steps: The trajectory list of steps
|
458
|
+
|
459
|
+
Returns:
|
460
|
+
Dictionary with statistics about the trajectory
|
461
|
+
"""
|
462
|
+
|
463
|
+
# Count different types of steps
|
464
|
+
step_types = {}
|
465
|
+
for step in trajectory_steps:
|
466
|
+
step_type = step.get("type", "unknown")
|
467
|
+
step_types[step_type] = step_types.get(step_type, 0) + 1
|
468
|
+
|
469
|
+
# Count planning vs execution steps
|
470
|
+
planning_steps = sum(
|
471
|
+
count
|
472
|
+
for step_type, count in step_types.items()
|
473
|
+
if step_type.startswith("planner_")
|
474
|
+
)
|
475
|
+
execution_steps = sum(
|
476
|
+
count
|
477
|
+
for step_type, count in step_types.items()
|
478
|
+
if step_type.startswith("codeact_")
|
479
|
+
)
|
480
|
+
|
481
|
+
# Count successful vs failed executions
|
482
|
+
successful_executions = sum(
|
483
|
+
1
|
484
|
+
for step in trajectory_steps
|
485
|
+
if step.get("type") == "codeact_execution" and step.get("success", False)
|
486
|
+
)
|
487
|
+
failed_executions = sum(
|
488
|
+
1
|
489
|
+
for step in trajectory_steps
|
490
|
+
if step.get("type") == "codeact_execution" and not step.get("success", True)
|
491
|
+
)
|
492
|
+
|
493
|
+
# Return statistics
|
494
|
+
return {
|
495
|
+
"total_steps": len(trajectory_steps),
|
496
|
+
"step_types": step_types,
|
497
|
+
"planning_steps": planning_steps,
|
498
|
+
"execution_steps": execution_steps,
|
499
|
+
"successful_executions": successful_executions,
|
500
|
+
"failed_executions": failed_executions,
|
501
|
+
}
|
502
|
+
|
503
|
+
|
402
504
|
# Example usage:
|
403
505
|
"""
|
404
506
|
# Save a trajectory with a specific goal (automatically creates folder structure)
|
@@ -428,4 +530,4 @@ Trajectory.print_macro_summary(folder_path)
|
|
428
530
|
# ├── trajectory.json # Full trajectory events
|
429
531
|
# ├── macro.json # Macro sequence with goal as description
|
430
532
|
# └── screenshots.gif # Screenshot animation
|
431
|
-
"""
|
533
|
+
"""
|
droidrun/cli/logs.py
CHANGED
@@ -6,7 +6,7 @@ from rich.console import Console
|
|
6
6
|
from rich.live import Live
|
7
7
|
from typing import List
|
8
8
|
|
9
|
-
from droidrun.agent.common.events import ScreenshotEvent
|
9
|
+
from droidrun.agent.common.events import ScreenshotEvent, RecordUIStateEvent
|
10
10
|
from droidrun.agent.planner.events import (
|
11
11
|
PlanInputEvent,
|
12
12
|
PlanThinkingEvent,
|
@@ -177,6 +177,9 @@ class LogHandler(logging.Handler):
|
|
177
177
|
if isinstance(event, ScreenshotEvent):
|
178
178
|
logger.debug("📸 Taking screenshot...")
|
179
179
|
|
180
|
+
elif isinstance(event, RecordUIStateEvent):
|
181
|
+
logger.debug(f"✏️ Recording UI state")
|
182
|
+
|
180
183
|
# Planner events
|
181
184
|
elif isinstance(event, PlanInputEvent):
|
182
185
|
self.current_step = "Planning..."
|