agent_controller.py 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  1. import asyncio
  2. from typing import Callable, List, Type
  3. from opendevin import config
  4. from opendevin.schema.config import ConfigType
  5. from opendevin.action import (
  6. Action,
  7. AgentFinishAction,
  8. AgentDelegateAction,
  9. NullAction,
  10. )
  11. from opendevin.observation import (
  12. Observation,
  13. AgentErrorObservation,
  14. AgentDelegateObservation,
  15. NullObservation,
  16. )
  17. from opendevin.agent import Agent
  18. from opendevin.exceptions import AgentMalformedActionError, AgentNoActionError, MaxCharsExceedError, LLMOutputError
  19. from opendevin.logger import opendevin_logger as logger
  20. from opendevin.plan import Plan
  21. from opendevin.state import State
  22. from opendevin.action.tasks import TaskStateChangedAction
  23. from opendevin.schema import TaskState
  24. from opendevin.controller.action_manager import ActionManager
  25. MAX_ITERATIONS = config.get(ConfigType.MAX_ITERATIONS)
  26. MAX_CHARS = config.get(ConfigType.MAX_CHARS)
  27. class AgentController:
  28. id: str
  29. agent: Agent
  30. max_iterations: int
  31. action_manager: ActionManager
  32. callbacks: List[Callable]
  33. delegate: 'AgentController | None' = None
  34. state: State | None = None
  35. _task_state: TaskState = TaskState.INIT
  36. _cur_step: int = 0
  37. def __init__(
  38. self,
  39. agent: Agent,
  40. inputs: dict = {},
  41. sid: str = 'default',
  42. max_iterations: int = MAX_ITERATIONS,
  43. max_chars: int = MAX_CHARS,
  44. callbacks: List[Callable] = [],
  45. ):
  46. self.id = sid
  47. self.agent = agent
  48. self.max_iterations = max_iterations
  49. self.action_manager = ActionManager(self.id)
  50. self.max_chars = max_chars
  51. self.callbacks = callbacks
  52. # Initialize agent-required plugins for sandbox (if any)
  53. self.action_manager.init_sandbox_plugins(agent.sandbox_plugins)
  54. def update_state_for_step(self, i):
  55. if self.state is None:
  56. return
  57. self.state.iteration = i
  58. self.state.background_commands_obs = self.action_manager.get_background_obs()
  59. def update_state_after_step(self):
  60. if self.state is None:
  61. return
  62. self.state.updated_info = []
  63. def add_history(self, action: Action, observation: Observation):
  64. if self.state is None:
  65. return
  66. if not isinstance(action, Action):
  67. raise TypeError(
  68. f'action must be an instance of Action, got {type(action).__name__} instead'
  69. )
  70. if not isinstance(observation, Observation):
  71. raise TypeError(
  72. f'observation must be an instance of Observation, got {type(observation).__name__} instead'
  73. )
  74. self.state.history.append((action, observation))
  75. self.state.updated_info.append((action, observation))
  76. async def _run(self):
  77. if self.state is None:
  78. return
  79. if self._task_state != TaskState.RUNNING:
  80. raise ValueError('Task is not in running state')
  81. for i in range(self._cur_step, self.max_iterations):
  82. self._cur_step = i
  83. try:
  84. finished = await self.step(i)
  85. if finished:
  86. self._task_state = TaskState.FINISHED
  87. except Exception as e:
  88. logger.error('Error in loop', exc_info=True)
  89. raise e
  90. if self._task_state == TaskState.FINISHED:
  91. logger.info('Task finished by agent')
  92. await self.reset_task()
  93. break
  94. elif self._task_state == TaskState.STOPPED:
  95. logger.info('Task stopped by user')
  96. await self.reset_task()
  97. break
  98. elif self._task_state == TaskState.PAUSED:
  99. logger.info('Task paused')
  100. self._cur_step = i + 1
  101. await self.notify_task_state_changed()
  102. break
  103. if self._is_stuck():
  104. logger.info('Loop detected, stopping task')
  105. observation = AgentErrorObservation('I got stuck into a loop, the task has stopped.')
  106. await self._run_callbacks(observation)
  107. await self.set_task_state_to(TaskState.STOPPED)
  108. break
  109. async def setup_task(self, task: str, inputs: dict = {}):
  110. """Sets up the agent controller with a task.
  111. """
  112. self._task_state = TaskState.RUNNING
  113. await self.notify_task_state_changed()
  114. self.state = State(Plan(task))
  115. self.state.inputs = inputs
  116. async def start(self, task: str):
  117. """Starts the agent controller with a task.
  118. If task already run before, it will continue from the last step.
  119. """
  120. await self.setup_task(task)
  121. await self._run()
  122. async def resume(self):
  123. if self.state is None:
  124. raise ValueError('No task to resume')
  125. self._task_state = TaskState.RUNNING
  126. await self.notify_task_state_changed()
  127. await self._run()
  128. async def reset_task(self):
  129. self.state = None
  130. self._cur_step = 0
  131. self._task_state = TaskState.INIT
  132. self.agent.reset()
  133. await self.notify_task_state_changed()
  134. async def set_task_state_to(self, state: TaskState):
  135. self._task_state = state
  136. if state == TaskState.STOPPED:
  137. await self.reset_task()
  138. logger.info(f'Task state set to {state}')
  139. def get_task_state(self):
  140. """Returns the current state of the agent task."""
  141. return self._task_state
  142. async def notify_task_state_changed(self):
  143. await self._run_callbacks(TaskStateChangedAction(self._task_state))
  144. async def start_delegate(self, action: AgentDelegateAction):
  145. AgentCls: Type[Agent] = Agent.get_cls(action.agent)
  146. agent = AgentCls(llm=self.agent.llm)
  147. self.delegate = AgentController(
  148. sid=self.id + '-delegate',
  149. agent=agent,
  150. max_iterations=self.max_iterations,
  151. max_chars=self.max_chars,
  152. callbacks=self.callbacks,
  153. )
  154. task = action.inputs.get('task') or ''
  155. await self.delegate.setup_task(task, action.inputs)
  156. async def step(self, i: int) -> bool:
  157. if self.state is None:
  158. raise ValueError('No task to run')
  159. if self.delegate is not None:
  160. delegate_done = await self.delegate.step(i)
  161. if delegate_done:
  162. outputs = self.delegate.state.outputs if self.delegate.state else {}
  163. obs: Observation = AgentDelegateObservation(content='', outputs=outputs)
  164. self.add_history(NullAction(), obs)
  165. self.delegate = None
  166. self.delegateAction = None
  167. return False
  168. logger.info(f'STEP {i}', extra={'msg_type': 'STEP'})
  169. logger.info(self.state.plan.main_goal, extra={'msg_type': 'PLAN'})
  170. if self.state.num_of_chars > self.max_chars:
  171. raise MaxCharsExceedError(self.state.num_of_chars, self.max_chars)
  172. log_obs = self.action_manager.get_background_obs()
  173. for obs in log_obs:
  174. self.add_history(NullAction(), obs)
  175. await self._run_callbacks(obs)
  176. logger.info(obs, extra={'msg_type': 'BACKGROUND LOG'})
  177. self.update_state_for_step(i)
  178. action: Action = NullAction()
  179. observation: Observation = NullObservation('')
  180. try:
  181. action = self.agent.step(self.state)
  182. if action is None:
  183. raise AgentNoActionError('No action was returned')
  184. except (AgentMalformedActionError, AgentNoActionError, LLMOutputError) as e:
  185. observation = AgentErrorObservation(str(e))
  186. logger.info(action, extra={'msg_type': 'ACTION'})
  187. self.update_state_after_step()
  188. await self._run_callbacks(action)
  189. finished = isinstance(action, AgentFinishAction)
  190. if finished:
  191. self.state.outputs = action.outputs # type: ignore[attr-defined]
  192. logger.info(action, extra={'msg_type': 'INFO'})
  193. return True
  194. if isinstance(observation, NullObservation):
  195. observation = await self.action_manager.run_action(action, self)
  196. if not isinstance(observation, NullObservation):
  197. logger.info(observation, extra={'msg_type': 'OBSERVATION'})
  198. self.add_history(action, observation)
  199. await self._run_callbacks(observation)
  200. return False
  201. async def _run_callbacks(self, event):
  202. if event is None:
  203. return
  204. for callback in self.callbacks:
  205. idx = self.callbacks.index(callback)
  206. try:
  207. await callback(event)
  208. except Exception as e:
  209. logger.exception(f'Callback error: {e}, idx: {idx}')
  210. await asyncio.sleep(
  211. 0.001
  212. ) # Give back control for a tick, so we can await in callbacks
  213. def get_state(self):
  214. return self.state
  215. def _is_stuck(self):
  216. if self.state is None or self.state.history is None or len(self.state.history) < 3:
  217. return False
  218. # if the last three (Action, Observation) tuples are too repetitive
  219. # the agent got stuck in a loop
  220. if all(
  221. [self.state.history[-i][0] == self.state.history[-3][0] for i in range(1, 3)]
  222. ):
  223. # it repeats same action, give it a chance, but not if:
  224. if (all
  225. (isinstance(self.state.history[-i][1], NullObservation) for i in range(1, 4))):
  226. # same (Action, NullObservation): like 'think' the same thought over and over
  227. logger.debug('Action, NullObservation loop detected')
  228. return True
  229. elif (all
  230. (isinstance(self.state.history[-i][1], AgentErrorObservation) for i in range(1, 4))):
  231. # (NullAction, AgentErrorObservation): errors coming from an exception
  232. # (Action, AgentErrorObservation): the same action getting an error, even if not necessarily the same error
  233. logger.debug('Action, AgentErrorObservation loop detected')
  234. return True
  235. return False