droidrun 0.3.5__py3-none-any.whl → 0.3.7__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.
@@ -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 = f"{output_path}.gif"
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
- trajectory_folder = os.path.join(directory, f"trajectory_{timestamp}")
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
- event_dict = {
125
- "type": event.__class__.__name__,
126
- **{k: make_serializable(v) for k, v in event.__dict__.items()
127
- if not k.startswith('_')}
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
- **{k: make_serializable(v) for k, v in macro_event.__dict__.items()
142
- if not k.startswith('_')}
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
- "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"))
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(f"📖 Loaded macro sequence with {macro_data.get('total_actions', 0)} actions from {macro_file_path}")
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 = [action.get("timestamp") for action in actions if action.get("timestamp")]
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('duration_seconds'):
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('action_types', {}).items():
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(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']:
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['macro_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('duration_seconds'):
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('action_types', {}).items():
417
+ for action_type, count in summary.get("action_types", {}).items():
327
418
  print(f" - {action_type}: {count}")
328
-
329
- if folder_data['trajectory_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
- def get_trajectory_statistics(trajectory_data: Dict[str, Any]) -> Dict[str, Any]:
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['step_types'].items():
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..."