droidrun 0.1.0__py3-none-any.whl → 0.2.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.
@@ -0,0 +1,355 @@
1
+ import json # For potential saving/loading later (optional)
2
+ import os
3
+ from typing import List, Dict, Any, Optional
4
+
5
+ class TaskManager:
6
+ """
7
+ Manages a list of tasks for an agent, each with a status.
8
+ """
9
+ # --- Define Status Constants ---
10
+ STATUS_PENDING = "pending" # Task hasn't been attempted yet
11
+ STATUS_ATTEMPTING = "attempting" # Task is currently being worked on
12
+ STATUS_COMPLETED = "completed" # Task was finished successfully
13
+ STATUS_FAILED = "failed" # Task attempt resulted in failure
14
+
15
+ VALID_STATUSES = {
16
+ STATUS_PENDING,
17
+ STATUS_ATTEMPTING,
18
+ STATUS_COMPLETED,
19
+ STATUS_FAILED
20
+ }
21
+ # desktop path/todo.md
22
+ file_path = os.path.join(os.path.expanduser("~"), "Desktop", "todo.txt")
23
+ def __init__(self):
24
+ """Initializes an empty task list."""
25
+ self.tasks = [] # List to store task dictionaries
26
+ self.task_completed = False
27
+ self.message = None
28
+ self.start_execution = False
29
+ self.task_history = [] # List to store historical task information
30
+ self.persistent_completed_tasks = [] # Persistent list of completed tasks
31
+ self.persistent_failed_tasks = [] # Persistent list of failed tasks
32
+
33
+ # self.tasks is a property, make a getter and setter for it
34
+ def set_tasks(self, tasks: List[str], task_contexts: Optional[List[Dict[str, Any]]] = None):
35
+ """
36
+ Clears the current task list and sets new tasks from a list.
37
+ Each task should be a string.
38
+
39
+ Args:
40
+ tasks: A list of strings, each representing a task.
41
+ task_contexts: Optional list of context dictionaries for each task.
42
+ """
43
+ try:
44
+ # Save any completed or failed tasks before clearing the list
45
+ for task in self.tasks:
46
+ if task["status"] == self.STATUS_COMPLETED and task not in self.persistent_completed_tasks:
47
+ # Store a copy to prevent modifications
48
+ self.persistent_completed_tasks.append(task.copy())
49
+ elif task["status"] == self.STATUS_FAILED and task not in self.persistent_failed_tasks:
50
+ # Store a copy to prevent modifications
51
+ self.persistent_failed_tasks.append(task.copy())
52
+
53
+ # Now clear the task list and add new tasks
54
+ self.tasks = []
55
+ for i, task in enumerate(tasks):
56
+ if not isinstance(task, str) or not task.strip():
57
+ raise ValueError("Each task must be a non-empty string.")
58
+
59
+ task_dict = {
60
+ "description": task.strip(),
61
+ "status": self.STATUS_PENDING
62
+ }
63
+
64
+ # Add context if provided
65
+ if task_contexts and i < len(task_contexts):
66
+ task_dict["context"] = task_contexts[i]
67
+
68
+ self.tasks.append(task_dict)
69
+
70
+ print(f"Tasks set: {len(self.tasks)} tasks added.")
71
+ self.save_to_file()
72
+ except Exception as e:
73
+ print(f"Error setting tasks: {e}")
74
+
75
+ def add_task(self, task_description: str, task_context: Optional[Dict[str, Any]] = None):
76
+ """
77
+ Adds a new task to the list with a 'pending' status.
78
+
79
+ Args:
80
+ task_description: The string describing the task.
81
+ task_context: Optional dictionary with context for the task.
82
+
83
+ Returns:
84
+ int: The index of the newly added task.
85
+
86
+ Raises:
87
+ ValueError: If the task_description is empty or not a string.
88
+ """
89
+ if not isinstance(task_description, str) or not task_description.strip():
90
+ raise ValueError("Task description must be a non-empty string.")
91
+
92
+ task = {
93
+ "description": task_description.strip(),
94
+ "status": self.STATUS_PENDING
95
+ }
96
+
97
+ # Add context if provided
98
+ if task_context:
99
+ task["context"] = task_context
100
+
101
+ self.tasks.append(task)
102
+ self.save_to_file()
103
+ print(f"Task added: {task_description} (Status: {self.STATUS_PENDING})")
104
+
105
+ return len(self.tasks) - 1 # Return the index of the new task
106
+
107
+ def get_task(self, index: int):
108
+ """
109
+ Retrieves a specific task by its index.
110
+
111
+ Args:
112
+ index: The integer index of the task.
113
+
114
+ Returns:
115
+ dict: The task dictionary {'description': str, 'status': str}.
116
+
117
+ Raises:
118
+ IndexError: If the index is out of bounds.
119
+ """
120
+ if 0 <= index < len(self.tasks):
121
+ return self.tasks[index]
122
+ else:
123
+ raise IndexError(f"Task index {index} out of bounds.")
124
+
125
+ def get_all_tasks(self):
126
+ """
127
+ Returns a copy of the entire list of tasks.
128
+
129
+ Returns:
130
+ list[dict]: A list containing all task dictionaries.
131
+ Returns an empty list if no tasks exist.
132
+ """
133
+ return list(self.tasks) # Return a copy to prevent external modification
134
+
135
+ def update_status(self, index: int, new_status: str, result_info: Optional[Dict[str, Any]] = None):
136
+ """
137
+ Updates the status of a specific task.
138
+
139
+ Args:
140
+ index: The index of the task to update.
141
+ new_status: The new status string (must be one of VALID_STATUSES).
142
+ result_info: Optional dictionary with additional information about the task result.
143
+
144
+ Raises:
145
+ IndexError: If the index is out of bounds.
146
+ ValueError: If the new_status is not a valid status.
147
+ """
148
+ if new_status not in self.VALID_STATUSES:
149
+ raise ValueError(f"Invalid status '{new_status}'. Valid statuses are: {', '.join(self.VALID_STATUSES)}")
150
+
151
+ # get_task will raise IndexError if index is invalid
152
+ task = self.get_task(index)
153
+ old_status = task["status"]
154
+ task["status"] = new_status
155
+
156
+ # Add result information if provided
157
+ if result_info:
158
+ for key, value in result_info.items():
159
+ task[key] = value
160
+
161
+ # Store task history when status changes
162
+ if old_status != new_status:
163
+ history_entry = {
164
+ "index": index,
165
+ "description": task["description"],
166
+ "old_status": old_status,
167
+ "new_status": new_status,
168
+ "result_info": result_info
169
+ }
170
+ self.task_history.append(history_entry)
171
+
172
+ # If the task is now completed or failed, add it to our persistent lists
173
+ if new_status == self.STATUS_COMPLETED:
174
+ # Make a copy to ensure it doesn't change if the original task is modified
175
+ task_copy = task.copy()
176
+ if task_copy not in self.persistent_completed_tasks:
177
+ self.persistent_completed_tasks.append(task_copy)
178
+ elif new_status == self.STATUS_FAILED:
179
+ # Make a copy to ensure it doesn't change if the original task is modified
180
+ task_copy = task.copy()
181
+ if task_copy not in self.persistent_failed_tasks:
182
+ self.persistent_failed_tasks.append(task_copy)
183
+
184
+ self.save_to_file()
185
+ # No need to re-assign task to self.tasks[index] as dictionaries are mutable
186
+
187
+ def delete_task(self, index: int):
188
+ """
189
+ Deletes a task by its index.
190
+
191
+ Args:
192
+ index: The index of the task to delete.
193
+
194
+ Raises:
195
+ IndexError: If the index is out of bounds.
196
+ """
197
+ if 0 <= index < len(self.tasks):
198
+ del self.tasks[index]
199
+ self.save_to_file()
200
+ else:
201
+ raise IndexError(f"Task index {index} out of bounds.")
202
+
203
+ def clear_tasks(self):
204
+ """Removes all tasks from the list."""
205
+ self.tasks = []
206
+ print("All tasks cleared.")
207
+ self.save_to_file()
208
+
209
+ def get_tasks_by_status(self, status: str):
210
+ """
211
+ Filters and returns tasks matching a specific status.
212
+
213
+ Args:
214
+ status: The status string to filter by.
215
+
216
+ Returns:
217
+ list[dict]: A list of tasks matching the status.
218
+
219
+ Raises:
220
+ ValueError: If the status is not a valid status.
221
+ """
222
+ if status not in self.VALID_STATUSES:
223
+ raise ValueError(f"Invalid status '{status}'. Valid statuses are: {', '.join(self.VALID_STATUSES)}")
224
+ return [task for task in self.tasks if task["status"] == status]
225
+
226
+ # --- Convenience methods for specific statuses ---
227
+ def get_pending_tasks(self) -> list[dict]:
228
+ return self.get_tasks_by_status(self.STATUS_PENDING)
229
+
230
+ def get_attempting_task(self) -> dict | None:
231
+ attempting_tasks = self.get_tasks_by_status(self.STATUS_ATTEMPTING)
232
+ if attempting_tasks:
233
+ return attempting_tasks[0]
234
+ else:
235
+ return None
236
+
237
+ def get_completed_tasks(self) -> list[dict]:
238
+ return self.get_tasks_by_status(self.STATUS_COMPLETED)
239
+
240
+ def get_failed_tasks(self) -> dict | None:
241
+ attempting_tasks = self.get_tasks_by_status(self.STATUS_FAILED)
242
+ if attempting_tasks:
243
+ return attempting_tasks[0]
244
+ else:
245
+ return None
246
+
247
+ # --- Utility methods ---
248
+ def __len__(self):
249
+ """Returns the total number of tasks."""
250
+ return len(self.tasks)
251
+
252
+ def __str__(self):
253
+ """Provides a user-friendly string representation of the task list."""
254
+ if not self.tasks:
255
+ return "Task List (empty)"
256
+
257
+ output = "Task List:\n"
258
+ output += "----------\n"
259
+ for i, task in enumerate(self.tasks):
260
+ output += f"{i}: [{task['status'].upper():<10}] {task['description']}\n"
261
+ output += "----------"
262
+ return output
263
+
264
+ def __repr__(self):
265
+ """Provides a developer-friendly representation."""
266
+ return f"<TaskManager(task_count={len(self.tasks)}, completed={self.get_completed_tasks()}, attempting={self.get_attempting_task()}, pending={self.get_pending_tasks()})>"
267
+ def save_to_file(self, filename=file_path):
268
+ """Saves the current task list to a Markdown file."""
269
+ try:
270
+ with open(filename, 'w', encoding='utf-8') as f:
271
+ f.write(str(self))
272
+ #print(f"Tasks saved to {filename}.")
273
+ except Exception as e:
274
+ print(f"Error saving tasks to file: {e}")
275
+ def complete_goal(self, message: str):
276
+ """
277
+ Marks the goal as completed, use this whether the task completion was successful or on failure.
278
+ This method should be called when the task is finished, regardless of the outcome.
279
+
280
+ Args:
281
+ message: The message to be logged.
282
+ """
283
+ self.task_completed = True
284
+ self.message = message
285
+ print(f"Goal completed: {message}")
286
+ def start_agent(self):
287
+ """Starts the sub-agent to perform the tasks if there are any tasks to perform.
288
+ Use this function after setting the tasks.
289
+ Args:
290
+ None"""
291
+ if len(self.tasks) == 0:
292
+ print("No tasks to perform.")
293
+ return
294
+ self.start_execution = True
295
+
296
+ def get_task_history(self):
297
+ """
298
+ Returns the history of task status changes.
299
+
300
+ Returns:
301
+ list: A list of dictionaries with historical task information.
302
+ """
303
+ return self.task_history
304
+
305
+ def get_all_completed_tasks(self) -> List[Dict]:
306
+ """
307
+ Returns all completed tasks, including those from previous planning cycles.
308
+
309
+ Returns:
310
+ List of completed task dictionaries
311
+ """
312
+ # Get currently active completed tasks
313
+ current_completed = self.get_completed_tasks()
314
+
315
+ # Create a combined list, ensuring no duplicates
316
+ all_completed = []
317
+
318
+ # Add current completed tasks
319
+ for task in current_completed:
320
+ if task not in all_completed:
321
+ all_completed.append(task)
322
+
323
+ # Add historical completed tasks
324
+ for task in self.persistent_completed_tasks:
325
+ # Check if task is already in the list based on description
326
+ if not any(t["description"] == task["description"] for t in all_completed):
327
+ all_completed.append(task)
328
+
329
+ return all_completed
330
+
331
+ def get_all_failed_tasks(self) -> List[Dict]:
332
+ """
333
+ Returns all failed tasks, including those from previous planning cycles.
334
+
335
+ Returns:
336
+ List of failed task dictionaries
337
+ """
338
+ # Get currently active failed tasks
339
+ current_failed = self.get_tasks_by_status(self.STATUS_FAILED)
340
+
341
+ # Create a combined list, ensuring no duplicates
342
+ all_failed = []
343
+
344
+ # Add current failed tasks
345
+ for task in current_failed:
346
+ if task not in all_failed:
347
+ all_failed.append(task)
348
+
349
+ # Add historical failed tasks
350
+ for task in self.persistent_failed_tasks:
351
+ # Check if task is already in the list based on description
352
+ if not any(t["description"] == task["description"] for t in all_failed):
353
+ all_failed.append(task)
354
+
355
+ return all_failed