agent.py 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. from typing import TypedDict, Union
  2. from opendevin.controller.agent import Agent
  3. from opendevin.controller.state.state import State
  4. from opendevin.core.schema import AgentState
  5. from opendevin.events.action import (
  6. Action,
  7. AddTaskAction,
  8. AgentFinishAction,
  9. AgentRejectAction,
  10. BrowseInteractiveAction,
  11. BrowseURLAction,
  12. CmdRunAction,
  13. FileReadAction,
  14. FileWriteAction,
  15. MessageAction,
  16. ModifyTaskAction,
  17. )
  18. from opendevin.events.observation import (
  19. AgentStateChangedObservation,
  20. CmdOutputObservation,
  21. FileReadObservation,
  22. FileWriteObservation,
  23. NullObservation,
  24. Observation,
  25. )
  26. from opendevin.events.serialization.event import event_to_dict
  27. from opendevin.llm.llm import LLM
  28. """
  29. FIXME: There are a few problems this surfaced
  30. * FileWrites seem to add an unintended newline at the end of the file
  31. * Browser not working
  32. """
  33. ActionObs = TypedDict(
  34. 'ActionObs', {'action': Action, 'observations': list[Observation]}
  35. )
  36. class DummyAgent(Agent):
  37. VERSION = '1.0'
  38. """
  39. The DummyAgent is used for e2e testing. It just sends the same set of actions deterministically,
  40. without making any LLM calls.
  41. """
  42. def __init__(self, llm: LLM):
  43. super().__init__(llm)
  44. self.steps: list[ActionObs] = [
  45. {
  46. 'action': AddTaskAction(
  47. parent='None', goal='check the current directory'
  48. ),
  49. 'observations': [],
  50. },
  51. {
  52. 'action': AddTaskAction(parent='0', goal='run ls'),
  53. 'observations': [],
  54. },
  55. {
  56. 'action': ModifyTaskAction(task_id='0', state='in_progress'),
  57. 'observations': [],
  58. },
  59. {
  60. 'action': MessageAction('Time to get started!'),
  61. 'observations': [],
  62. },
  63. {
  64. 'action': CmdRunAction(command='echo "foo"'),
  65. 'observations': [
  66. CmdOutputObservation(
  67. 'foo', command_id=-1, command='echo "foo"', exit_code=0
  68. )
  69. ],
  70. },
  71. {
  72. 'action': FileWriteAction(
  73. content='echo "Hello, World!"', path='hello.sh'
  74. ),
  75. 'observations': [
  76. FileWriteObservation(
  77. content='echo "Hello, World!"', path='hello.sh'
  78. )
  79. ],
  80. },
  81. {
  82. 'action': FileReadAction(path='hello.sh'),
  83. 'observations': [
  84. FileReadObservation('echo "Hello, World!"\n', path='hello.sh')
  85. ],
  86. },
  87. {
  88. 'action': CmdRunAction(command='bash hello.sh'),
  89. 'observations': [
  90. CmdOutputObservation(
  91. 'bash: hello.sh: No such file or directory',
  92. command_id=-1,
  93. command='bash workspace/hello.sh',
  94. exit_code=127,
  95. )
  96. ],
  97. },
  98. {
  99. 'action': BrowseURLAction(url='https://google.com'),
  100. 'observations': [
  101. # BrowserOutputObservation('<html><body>Simulated Google page</body></html>',url='https://google.com',screenshot=''),
  102. ],
  103. },
  104. {
  105. 'action': BrowseInteractiveAction(
  106. browser_actions='goto("https://google.com")'
  107. ),
  108. 'observations': [
  109. # BrowserOutputObservation('<html><body>Simulated Google page after interaction</body></html>',url='https://google.com',screenshot=''),
  110. ],
  111. },
  112. {
  113. 'action': AgentRejectAction(),
  114. 'observations': [NullObservation('')],
  115. },
  116. {
  117. 'action': AgentFinishAction(
  118. outputs={}, thought='Task completed', action='finish'
  119. ),
  120. 'observations': [AgentStateChangedObservation('', AgentState.FINISHED)],
  121. },
  122. ]
  123. def step(self, state: State) -> Action:
  124. if state.iteration >= len(self.steps):
  125. return AgentFinishAction()
  126. current_step = self.steps[state.iteration]
  127. action = current_step['action']
  128. # If the action is AddTaskAction or ModifyTaskAction, update the parent ID or task_id
  129. if isinstance(action, AddTaskAction):
  130. if action.parent == 'None':
  131. action.parent = '' # Root task has no parent
  132. elif action.parent == '0':
  133. action.parent = state.root_task.id
  134. elif action.parent.startswith('0.'):
  135. action.parent = f'{state.root_task.id}{action.parent[1:]}'
  136. elif isinstance(action, ModifyTaskAction):
  137. if action.task_id == '0':
  138. action.task_id = state.root_task.id
  139. elif action.task_id.startswith('0.'):
  140. action.task_id = f'{state.root_task.id}{action.task_id[1:]}'
  141. # Ensure the task_id doesn't start with a dot
  142. if action.task_id.startswith('.'):
  143. action.task_id = action.task_id[1:]
  144. elif isinstance(action, (BrowseURLAction, BrowseInteractiveAction)):
  145. try:
  146. return self.simulate_browser_action(action)
  147. except (
  148. Exception
  149. ): # This could be a specific exception for browser unavailability
  150. return self.handle_browser_unavailable(action)
  151. if state.iteration > 0:
  152. prev_step = self.steps[state.iteration - 1]
  153. if 'observations' in prev_step and prev_step['observations']:
  154. expected_observations = prev_step['observations']
  155. hist_events = state.history.get_last_events(len(expected_observations))
  156. if len(hist_events) < len(expected_observations):
  157. print(
  158. f'Warning: Expected {len(expected_observations)} observations, but got {len(hist_events)}'
  159. )
  160. for i in range(min(len(expected_observations), len(hist_events))):
  161. hist_obs = event_to_dict(hist_events[i])
  162. expected_obs = event_to_dict(expected_observations[i])
  163. # Remove dynamic fields for comparison
  164. for obs in [hist_obs, expected_obs]:
  165. obs.pop('id', None)
  166. obs.pop('timestamp', None)
  167. obs.pop('cause', None)
  168. obs.pop('source', None)
  169. if 'extras' in obs:
  170. obs['extras'].pop('command_id', None)
  171. if hist_obs != expected_obs:
  172. print(
  173. f'Warning: Observation mismatch. Expected {expected_obs}, got {hist_obs}'
  174. )
  175. return action
  176. def simulate_browser_action(
  177. self, action: Union[BrowseURLAction, BrowseInteractiveAction]
  178. ) -> Action:
  179. # Instead of simulating, we'll reject the browser action
  180. return self.handle_browser_unavailable(action)
  181. def handle_browser_unavailable(
  182. self, action: Union[BrowseURLAction, BrowseInteractiveAction]
  183. ) -> Action:
  184. # Create a message action to inform that browsing is not available
  185. message = 'Browser actions are not available in the DummyAgent environment.'
  186. if isinstance(action, BrowseURLAction):
  187. message += f' Unable to browse URL: {action.url}'
  188. elif isinstance(action, BrowseInteractiveAction):
  189. message += (
  190. f' Unable to perform interactive browsing: {action.browser_actions}'
  191. )
  192. return MessageAction(content=message)
  193. async def get_working_directory(self, state: State) -> str:
  194. # Implement this method to return the current working directory
  195. # This might involve accessing state information or making an async call
  196. # For now, we'll return a placeholder value
  197. return './workspace'