task.py 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. from openhands.core.exceptions import (
  2. LLMMalformedActionError,
  3. TaskInvalidStateError,
  4. )
  5. from openhands.core.logger import openhands_logger as logger
  6. OPEN_STATE = 'open'
  7. COMPLETED_STATE = 'completed'
  8. ABANDONED_STATE = 'abandoned'
  9. IN_PROGRESS_STATE = 'in_progress'
  10. VERIFIED_STATE = 'verified'
  11. STATES = [
  12. OPEN_STATE,
  13. COMPLETED_STATE,
  14. ABANDONED_STATE,
  15. IN_PROGRESS_STATE,
  16. VERIFIED_STATE,
  17. ]
  18. class Task:
  19. id: str
  20. goal: str
  21. parent: 'Task | None'
  22. subtasks: list['Task']
  23. def __init__(
  24. self,
  25. parent: 'Task',
  26. goal: str,
  27. state: str = OPEN_STATE,
  28. subtasks=None, # noqa: B006
  29. ):
  30. """Initializes a new instance of the Task class.
  31. Args:
  32. parent: The parent task, or None if it is the root task.
  33. goal: The goal of the task.
  34. state: The initial state of the task.
  35. subtasks: A list of subtasks associated with this task.
  36. """
  37. if subtasks is None:
  38. subtasks = []
  39. if parent.id:
  40. self.id = parent.id + '.' + str(len(parent.subtasks))
  41. else:
  42. self.id = str(len(parent.subtasks))
  43. self.parent = parent
  44. self.goal = goal
  45. logger.debug(f'Creating task {self.id} with parent={parent.id}, goal={goal}')
  46. self.subtasks = []
  47. for subtask in subtasks or []:
  48. if isinstance(subtask, Task):
  49. self.subtasks.append(subtask)
  50. else:
  51. goal = subtask.get('goal')
  52. state = subtask.get('state')
  53. subtasks = subtask.get('subtasks')
  54. logger.debug(f'Reading: {goal}, {state}, {subtasks}')
  55. self.subtasks.append(Task(self, goal, state, subtasks))
  56. self.state = OPEN_STATE
  57. def to_string(self, indent=''):
  58. """Returns a string representation of the task and its subtasks.
  59. Args:
  60. indent: The indentation string for formatting the output.
  61. Returns:
  62. A string representation of the task and its subtasks.
  63. """
  64. emoji = ''
  65. if self.state == VERIFIED_STATE:
  66. emoji = '✅'
  67. elif self.state == COMPLETED_STATE:
  68. emoji = '🟢'
  69. elif self.state == ABANDONED_STATE:
  70. emoji = '❌'
  71. elif self.state == IN_PROGRESS_STATE:
  72. emoji = '💪'
  73. elif self.state == OPEN_STATE:
  74. emoji = '🔵'
  75. result = indent + emoji + ' ' + self.id + ' ' + self.goal + '\n'
  76. for subtask in self.subtasks:
  77. result += subtask.to_string(indent + ' ')
  78. return result
  79. def to_dict(self):
  80. """Returns a dictionary representation of the task.
  81. Returns:
  82. A dictionary containing the task's attributes.
  83. """
  84. return {
  85. 'id': self.id,
  86. 'goal': self.goal,
  87. 'state': self.state,
  88. 'subtasks': [t.to_dict() for t in self.subtasks],
  89. }
  90. def set_state(self, state):
  91. """Sets the state of the task and its subtasks.
  92. Args: state: The new state of the task.
  93. Raises:
  94. TaskInvalidStateError: If the provided state is invalid.
  95. """
  96. if state not in STATES:
  97. logger.error('Invalid state: %s', state)
  98. raise TaskInvalidStateError(state)
  99. self.state = state
  100. if (
  101. state == COMPLETED_STATE
  102. or state == ABANDONED_STATE
  103. or state == VERIFIED_STATE
  104. ):
  105. for subtask in self.subtasks:
  106. if subtask.state != ABANDONED_STATE:
  107. subtask.set_state(state)
  108. elif state == IN_PROGRESS_STATE:
  109. if self.parent is not None:
  110. self.parent.set_state(state)
  111. def get_current_task(self) -> 'Task | None':
  112. """Retrieves the current task in progress.
  113. Returns:
  114. The current task in progress, or None if no task is in progress.
  115. """
  116. for subtask in self.subtasks:
  117. if subtask.state == IN_PROGRESS_STATE:
  118. return subtask.get_current_task()
  119. if self.state == IN_PROGRESS_STATE:
  120. return self
  121. return None
  122. class RootTask(Task):
  123. """Serves as the root node in a tree of tasks.
  124. Because we want the top-level of the root_task to be a list of tasks (1, 2, 3, etc.),
  125. the "root node" of the data structure is kind of invisible--it just
  126. holds references to the top-level tasks.
  127. Attributes:
  128. id: Kept blank for root_task
  129. goal: Kept blank for root_task
  130. parent: None for root_task
  131. subtasks: The top-level list of tasks associated with the root_task.
  132. state: The state of the root_task.
  133. """
  134. id: str = ''
  135. goal: str = ''
  136. parent: None = None
  137. def __init__(self):
  138. self.subtasks = []
  139. self.state = OPEN_STATE
  140. def __str__(self):
  141. """Returns a string representation of the root_task.
  142. Returns:
  143. A string representation of the root_task.
  144. """
  145. return self.to_string()
  146. def get_task_by_id(self, id: str) -> Task:
  147. """Retrieves a task by its ID.
  148. Args:
  149. id: The ID of the task.
  150. Returns:
  151. The task with the specified ID.
  152. Raises:
  153. AgentMalformedActionError: If the provided task ID is invalid or does not exist.
  154. """
  155. if id == '':
  156. return self
  157. if len(self.subtasks) == 0:
  158. raise LLMMalformedActionError('Task does not exist:' + id)
  159. try:
  160. parts = [int(p) for p in id.split('.')]
  161. except ValueError:
  162. raise LLMMalformedActionError('Invalid task id:' + id)
  163. task: Task = self
  164. for part in parts:
  165. if part >= len(task.subtasks):
  166. raise LLMMalformedActionError('Task does not exist:' + id)
  167. task = task.subtasks[part]
  168. return task
  169. def add_subtask(self, parent_id: str, goal: str, subtasks: list | None = None):
  170. """Adds a subtask to a parent task.
  171. Args:
  172. parent_id: The ID of the parent task.
  173. goal: The goal of the subtask.
  174. subtasks: A list of subtasks associated with the new subtask.
  175. """
  176. subtasks = subtasks or []
  177. parent = self.get_task_by_id(parent_id)
  178. child = Task(parent=parent, goal=goal, subtasks=subtasks)
  179. parent.subtasks.append(child)
  180. def set_subtask_state(self, id: str, state: str):
  181. """Sets the state of a subtask.
  182. Args:
  183. id: The ID of the subtask.
  184. state: The new state of the subtask.
  185. """
  186. task = self.get_task_by_id(id)
  187. logger.debug('Setting task {task.id} from state {task.state} to {state}')
  188. task.set_state(state)
  189. unfinished_tasks = [
  190. t
  191. for t in self.subtasks
  192. if t.state not in [COMPLETED_STATE, VERIFIED_STATE, ABANDONED_STATE]
  193. ]
  194. if len(unfinished_tasks) == 0:
  195. self.set_state(COMPLETED_STATE)