codeact_agent.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. import re
  2. from agenthub.codeact_agent.prompt import (
  3. COMMAND_DOCS,
  4. EXAMPLES,
  5. GITHUB_MESSAGE,
  6. SYSTEM_PREFIX,
  7. SYSTEM_SUFFIX,
  8. )
  9. from opendevin.controller.agent import Agent
  10. from opendevin.controller.state.state import State
  11. from opendevin.core.logger import opendevin_logger as logger
  12. from opendevin.events.action import (
  13. Action,
  14. AgentFinishAction,
  15. CmdRunAction,
  16. IPythonRunCellAction,
  17. MessageAction,
  18. )
  19. from opendevin.events.observation import (
  20. CmdOutputObservation,
  21. IPythonRunCellObservation,
  22. )
  23. from opendevin.llm.llm import LLM
  24. from opendevin.runtime.plugins import (
  25. JupyterRequirement,
  26. PluginRequirement,
  27. SWEAgentCommandsRequirement,
  28. )
  29. ENABLE_GITHUB = True
  30. def parse_response(response) -> str:
  31. action = response.choices[0].message.content
  32. for lang in ['bash', 'ipython']:
  33. if f'<execute_{lang}>' in action and f'</execute_{lang}>' not in action:
  34. action += f'</execute_{lang}>'
  35. return action
  36. def truncate_observation(observation: str, max_chars: int = 10_000) -> str:
  37. """
  38. Truncate the middle of the observation if it is too long.
  39. """
  40. if len(observation) <= max_chars:
  41. return observation
  42. half = max_chars // 2
  43. return (
  44. observation[:half]
  45. + '\n[... Observation truncated due to length ...]\n'
  46. + observation[-half:]
  47. )
  48. def swe_agent_edit_hack(bash_command: str) -> str:
  49. """
  50. Hack to handle the SWE-agent edit command. The vanilla edit command will hang the SSHBox.
  51. REPLACE THIS:
  52. edit 683:693
  53. try:
  54. return list(urlsplit(url))
  55. except ValueError:
  56. raise ValidationError(self.error_messages['invalid'], code='invalid')
  57. end_of_edit
  58. WITH THIS:
  59. edit 683:693 <<EOF
  60. try:
  61. return list(urlsplit(url))
  62. except ValueError:
  63. raise ValidationError(self.error_messages['invalid'], code='invalid')
  64. EOF
  65. """
  66. if 'edit' in bash_command:
  67. # edit\s(\d+):(\d+)([\s\S]*)end_of_edit
  68. # replace
  69. bash_command = re.sub(
  70. r'edit\s(\d+):(\d+)([\s\S]*?)end_of_edit',
  71. r'edit \1:\2 <<EOF\3EOF',
  72. bash_command,
  73. )
  74. return bash_command
  75. class CodeActAgent(Agent):
  76. VERSION = '1.2'
  77. """
  78. The Code Act Agent is a minimalist agent.
  79. The agent works by passing the model a list of action-observation pairs and prompting the model to take the next step.
  80. ### Overview
  81. This agent implements the CodeAct idea ([paper](https://arxiv.org/abs/2402.13463), [tweet](https://twitter.com/xingyaow_/status/1754556835703751087)) that consolidates LLM agents’ **act**ions into a unified **code** action space for both *simplicity* and *performance* (see paper for more details).
  82. The conceptual idea is illustrated below. At each turn, the agent can:
  83. 1. **Converse**: Communicate with humans in natural language to ask for clarification, confirmation, etc.
  84. 2. **CodeAct**: Choose to perform the task by executing code
  85. - Execute any valid Linux `bash` command
  86. - Execute any valid `Python` code with [an interactive Python interpreter](https://ipython.org/). This is simulated through `bash` command, see plugin system below for more details.
  87. ![image](https://github.com/OpenDevin/OpenDevin/assets/38853559/92b622e3-72ad-4a61-8f41-8c040b6d5fb3)
  88. ### Plugin System
  89. To make the CodeAct agent more powerful with only access to `bash` action space, CodeAct agent leverages OpenDevin's plugin system:
  90. - [Jupyter plugin](https://github.com/OpenDevin/OpenDevin/tree/main/opendevin/runtime/plugins/jupyter): for IPython execution via bash command
  91. - [SWE-agent tool plugin](https://github.com/OpenDevin/OpenDevin/tree/main/opendevin/runtime/plugins/swe_agent_commands): Powerful bash command line tools for software development tasks introduced by [swe-agent](https://github.com/princeton-nlp/swe-agent).
  92. ### Demo
  93. https://github.com/OpenDevin/OpenDevin/assets/38853559/f592a192-e86c-4f48-ad31-d69282d5f6ac
  94. *Example of CodeActAgent with `gpt-4-turbo-2024-04-09` performing a data science task (linear regression)*
  95. ### Work-in-progress & Next step
  96. [] Support web-browsing
  97. [] Complete the workflow for CodeAct agent to submit Github PRs
  98. """
  99. sandbox_plugins: list[PluginRequirement] = [
  100. JupyterRequirement(),
  101. SWEAgentCommandsRequirement(),
  102. ]
  103. system_message: str = (
  104. f'{SYSTEM_PREFIX}\n{GITHUB_MESSAGE}\n\n{COMMAND_DOCS}\n\n{SYSTEM_SUFFIX}'
  105. if ENABLE_GITHUB
  106. else f'{SYSTEM_PREFIX}\n\n{COMMAND_DOCS}\n\n{SYSTEM_SUFFIX}'
  107. )
  108. def __init__(
  109. self,
  110. llm: LLM,
  111. ) -> None:
  112. """
  113. Initializes a new instance of the CodeActAgent class.
  114. Parameters:
  115. - llm (LLM): The llm to be used by this agent
  116. """
  117. super().__init__(llm)
  118. self.reset()
  119. def reset(self) -> None:
  120. """
  121. Resets the CodeAct Agent.
  122. """
  123. super().reset()
  124. self.messages: list[dict[str, str]] = [
  125. {'role': 'system', 'content': self.system_message},
  126. {
  127. 'role': 'user',
  128. 'content': f"Here is an example of how you can interact with the environment for task solving:\n{EXAMPLES}\n\nNOW, LET'S START!",
  129. },
  130. ]
  131. self.cost_accumulator = 0
  132. def step(self, state: State) -> Action:
  133. """
  134. Performs one step using the CodeAct Agent.
  135. This includes gathering info on previous steps and prompting the model to make a command to execute.
  136. Parameters:
  137. - state (State): used to get updated info and background commands
  138. Returns:
  139. - CmdRunAction(command) - bash command to run
  140. - IPythonRunCellAction(code) - IPython code to run
  141. - MessageAction(content) - Message action to run (e.g. ask for clarification)
  142. - AgentFinishAction() - end the interaction
  143. """
  144. updated_info = state.updated_info
  145. if updated_info:
  146. for prev_action, obs in updated_info:
  147. if (
  148. isinstance(prev_action, MessageAction)
  149. and prev_action.source == 'user'
  150. ):
  151. self.messages.append(
  152. {'role': 'user', 'content': prev_action.content}
  153. )
  154. if prev_action.content.strip() == '/exit':
  155. # User wants to exit
  156. return AgentFinishAction()
  157. if isinstance(obs, CmdOutputObservation):
  158. content = 'OBSERVATION:\n' + truncate_observation(obs.content)
  159. content += f'\n[Command {obs.command_id} finished with exit code {obs.exit_code}]]'
  160. self.messages.append({'role': 'user', 'content': content})
  161. elif isinstance(obs, IPythonRunCellObservation):
  162. content = 'OBSERVATION:\n' + obs.content
  163. # replace base64 images with a placeholder
  164. splitted = content.split('\n')
  165. for i, line in enumerate(splitted):
  166. if '![image](data:image/png;base64,' in line:
  167. splitted[i] = (
  168. '![image](data:image/png;base64, ...) already displayed to user'
  169. )
  170. content = '\n'.join(splitted)
  171. content = truncate_observation(content)
  172. self.messages.append({'role': 'user', 'content': content})
  173. latest_user_message = [m for m in self.messages if m['role'] == 'user'][-1]
  174. if latest_user_message:
  175. latest_user_message['content'] += (
  176. f'\n\nENVIRONMENT REMINDER: You have {state.max_iterations - state.iteration} turns left to complete the task.'
  177. )
  178. response = self.llm.completion(
  179. messages=self.messages,
  180. stop=[
  181. '</execute_ipython>',
  182. '</execute_bash>',
  183. ],
  184. temperature=0.0,
  185. )
  186. self.log_cost(response)
  187. action_str: str = parse_response(response)
  188. state.num_of_chars += sum(
  189. len(message['content']) for message in self.messages
  190. ) + len(action_str)
  191. self.messages.append({'role': 'assistant', 'content': action_str})
  192. if finish_command := re.search(r'<finish>.*</finish>', action_str, re.DOTALL):
  193. thought = action_str.replace(finish_command.group(0), '').strip()
  194. return AgentFinishAction(thought=thought)
  195. if bash_command := re.search(
  196. r'<execute_bash>(.*)</execute_bash>', action_str, re.DOTALL
  197. ):
  198. # remove the command from the action string to get thought
  199. thought = action_str.replace(bash_command.group(0), '').strip()
  200. # a command was found
  201. command_group = bash_command.group(1).strip()
  202. command_group = swe_agent_edit_hack(command_group)
  203. if command_group.strip() == 'exit':
  204. return AgentFinishAction()
  205. return CmdRunAction(command=command_group, thought=thought)
  206. elif python_code := re.search(
  207. r'<execute_ipython>(.*)</execute_ipython>', action_str, re.DOTALL
  208. ):
  209. # a code block was found
  210. code_group = python_code.group(1).strip()
  211. thought = action_str.replace(python_code.group(0), '').strip()
  212. return IPythonRunCellAction(code=code_group, thought=thought)
  213. else:
  214. # We assume the LLM is GOOD enough that when it returns pure natural language
  215. # it want to talk to the user
  216. return MessageAction(content=action_str, wait_for_response=True)
  217. def search_memory(self, query: str) -> list[str]:
  218. raise NotImplementedError('Implement this abstract method')
  219. def log_cost(self, response):
  220. cur_cost = self.llm.completion_cost(response)
  221. self.cost_accumulator += cur_cost
  222. logger.info(
  223. 'Cost: %.2f USD | Accumulated Cost: %.2f USD',
  224. cur_cost,
  225. self.cost_accumulator,
  226. )