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.
- droidrun/__init__.py +15 -8
- droidrun/__main__.py +2 -3
- droidrun/adb/device.py +1 -1
- droidrun/agent/codeact/__init__.py +13 -0
- droidrun/agent/codeact/codeact_agent.py +334 -0
- droidrun/agent/codeact/events.py +36 -0
- droidrun/agent/codeact/prompts.py +78 -0
- droidrun/agent/droid/__init__.py +13 -0
- droidrun/agent/droid/droid_agent.py +418 -0
- droidrun/agent/planner/__init__.py +15 -0
- droidrun/agent/planner/events.py +20 -0
- droidrun/agent/planner/prompts.py +144 -0
- droidrun/agent/planner/task_manager.py +355 -0
- droidrun/agent/planner/workflow.py +371 -0
- droidrun/agent/utils/async_utils.py +56 -0
- droidrun/agent/utils/chat_utils.py +92 -0
- droidrun/agent/utils/executer.py +97 -0
- droidrun/agent/utils/llm_picker.py +143 -0
- droidrun/cli/main.py +422 -107
- droidrun/tools/__init__.py +4 -25
- droidrun/tools/actions.py +767 -783
- droidrun/tools/device.py +1 -1
- droidrun/tools/loader.py +60 -0
- {droidrun-0.1.0.dist-info → droidrun-0.2.0.dist-info}/METADATA +134 -37
- droidrun-0.2.0.dist-info/RECORD +32 -0
- droidrun/agent/__init__.py +0 -16
- droidrun/agent/llm_reasoning.py +0 -567
- droidrun/agent/react_agent.py +0 -556
- droidrun/llm/__init__.py +0 -24
- droidrun-0.1.0.dist-info/RECORD +0 -20
- {droidrun-0.1.0.dist-info → droidrun-0.2.0.dist-info}/WHEEL +0 -0
- {droidrun-0.1.0.dist-info → droidrun-0.2.0.dist-info}/entry_points.txt +0 -0
- {droidrun-0.1.0.dist-info → droidrun-0.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -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
|