plan.py 6.3 KB

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