codeact_agent.py 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. import re
  2. from typing import List, Mapping
  3. from agenthub.codeact_agent.prompt import EXAMPLES, SYSTEM_MESSAGE
  4. from opendevin.controller.agent import Agent
  5. from opendevin.controller.state.state import State
  6. from opendevin.events.action import (
  7. Action,
  8. AgentEchoAction,
  9. AgentFinishAction,
  10. AgentTalkAction,
  11. CmdRunAction,
  12. IPythonRunCellAction,
  13. NullAction,
  14. )
  15. from opendevin.events.observation import (
  16. AgentMessageObservation,
  17. CmdOutputObservation,
  18. IPythonRunCellObservation,
  19. UserMessageObservation,
  20. )
  21. from opendevin.llm.llm import LLM
  22. from opendevin.runtime.plugins import (
  23. JupyterRequirement,
  24. PluginRequirement,
  25. SWEAgentCommandsRequirement,
  26. )
  27. def parse_response(response) -> str:
  28. action = response.choices[0].message.content
  29. for lang in ['bash', 'ipython']:
  30. if f'<execute_{lang}>' in action and f'</execute_{lang}>' not in action:
  31. action += f'</execute_{lang}>'
  32. return action
  33. def truncate_observation(observation: str, max_chars: int = 5000) -> str:
  34. """
  35. Truncate the middle of the observation if it is too long.
  36. """
  37. if len(observation) <= max_chars:
  38. return observation
  39. half = max_chars // 2
  40. return (
  41. observation[:half]
  42. + '\n[... Observation truncated due to length ...]\n'
  43. + observation[-half:]
  44. )
  45. class CodeActAgent(Agent):
  46. """
  47. The Code Act Agent is a minimalist agent.
  48. The agent works by passing the model a list of action-observation pairs and prompting the model to take the next step.
  49. ### Overview
  50. 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).
  51. The conceptual idea is illustrated below. At each turn, the agent can:
  52. 1. **Converse**: Communicate with humans in natural language to ask for clarification, confirmation, etc.
  53. 2. **CodeAct**: Choose to perform the task by executing code
  54. - Execute any valid Linux `bash` command
  55. - 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.
  56. ![image](https://github.com/OpenDevin/OpenDevin/assets/38853559/92b622e3-72ad-4a61-8f41-8c040b6d5fb3)
  57. ### Plugin System
  58. To make the CodeAct agent more powerful with only access to `bash` action space, CodeAct agent leverages OpenDevin's plugin system:
  59. - [Jupyter plugin](https://github.com/OpenDevin/OpenDevin/tree/main/opendevin/runtime/plugins/jupyter): for IPython execution via bash command
  60. - [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).
  61. ### Demo
  62. https://github.com/OpenDevin/OpenDevin/assets/38853559/f592a192-e86c-4f48-ad31-d69282d5f6ac
  63. *Example of CodeActAgent with `gpt-4-turbo-2024-04-09` performing a data science task (linear regression)*
  64. ### Work-in-progress & Next step
  65. [] Support web-browsing
  66. [] Complete the workflow for CodeAct agent to submit Github PRs
  67. """
  68. sandbox_plugins: List[PluginRequirement] = [
  69. JupyterRequirement(),
  70. SWEAgentCommandsRequirement(),
  71. ]
  72. SUPPORTED_ACTIONS = (
  73. CmdRunAction,
  74. IPythonRunCellAction,
  75. AgentEchoAction,
  76. AgentTalkAction,
  77. NullAction,
  78. )
  79. SUPPORTED_OBSERVATIONS = (
  80. AgentMessageObservation,
  81. UserMessageObservation,
  82. CmdOutputObservation,
  83. IPythonRunCellObservation,
  84. )
  85. def __init__(
  86. self,
  87. llm: LLM,
  88. ) -> None:
  89. """
  90. Initializes a new instance of the CodeActAgent class.
  91. Parameters:
  92. - llm (LLM): The llm to be used by this agent
  93. """
  94. super().__init__(llm)
  95. self.messages: List[Mapping[str, str]] = []
  96. def step(self, state: State) -> Action:
  97. """
  98. Performs one step using the CodeAct Agent.
  99. This includes gathering info on previous steps and prompting the model to make a command to execute.
  100. Parameters:
  101. - state (State): used to get updated info and background commands
  102. Returns:
  103. - CmdRunAction(command) - bash command to run
  104. - IPythonRunCellAction(code) - IPython code to run
  105. - AgentTalkAction(content) - Talk action to run (e.g. ask for clarification)
  106. - AgentFinishAction() - end the interaction
  107. """
  108. if len(self.messages) == 0:
  109. assert state.plan.main_goal, 'Expecting instruction to be set'
  110. self.messages = [
  111. {'role': 'system', 'content': SYSTEM_MESSAGE},
  112. {
  113. 'role': 'user',
  114. 'content': (
  115. f'Here is an example of how you can interact with the environment for task solving:\n{EXAMPLES}\n\n'
  116. f"NOW, LET'S START!\n\n{state.plan.main_goal}"
  117. ),
  118. },
  119. ]
  120. updated_info = state.updated_info
  121. if updated_info:
  122. for prev_action, obs in updated_info:
  123. assert isinstance(
  124. prev_action, self.SUPPORTED_ACTIONS
  125. ), f'{prev_action.__class__} is not supported (supported: {self.SUPPORTED_ACTIONS})'
  126. # prev_action is already added to self.messages when returned
  127. # handle observations
  128. assert isinstance(
  129. obs, self.SUPPORTED_OBSERVATIONS
  130. ), f'{obs.__class__} is not supported (supported: {self.SUPPORTED_OBSERVATIONS})'
  131. if isinstance(obs, (AgentMessageObservation, UserMessageObservation)):
  132. self.messages.append({'role': 'user', 'content': obs.content})
  133. # User wants to exit
  134. if obs.content.strip() == '/exit':
  135. return AgentFinishAction()
  136. elif isinstance(obs, CmdOutputObservation):
  137. content = 'OBSERVATION:\n' + truncate_observation(obs.content)
  138. content += f'\n[Command {obs.command_id} finished with exit code {obs.exit_code}]]'
  139. self.messages.append({'role': 'user', 'content': content})
  140. elif isinstance(obs, IPythonRunCellObservation):
  141. content = 'OBSERVATION:\n' + obs.content
  142. # replace base64 images with a placeholder
  143. splited = content.split('\n')
  144. for i, line in enumerate(splited):
  145. if '![image](data:image/png;base64,' in line:
  146. splited[i] = (
  147. '![image](data:image/png;base64, ...) already displayed to user'
  148. )
  149. content = '\n'.join(splited)
  150. content = truncate_observation(content)
  151. self.messages.append({'role': 'user', 'content': content})
  152. else:
  153. raise NotImplementedError(
  154. f'Unknown observation type: {obs.__class__}'
  155. )
  156. response = self.llm.completion(
  157. messages=self.messages,
  158. stop=[
  159. '</execute_ipython>',
  160. '</execute_bash>',
  161. ],
  162. temperature=0.0,
  163. )
  164. action_str: str = parse_response(response)
  165. state.num_of_chars += sum(
  166. len(message['content']) for message in self.messages
  167. ) + len(action_str)
  168. self.messages.append({'role': 'assistant', 'content': action_str})
  169. if bash_command := re.search(
  170. r'<execute_bash>(.*)</execute_bash>', action_str, re.DOTALL
  171. ):
  172. # remove the command from the action string to get thought
  173. thought = action_str.replace(bash_command.group(0), '').strip()
  174. # a command was found
  175. command_group = bash_command.group(1).strip()
  176. if command_group.strip() == 'exit':
  177. return AgentFinishAction()
  178. return CmdRunAction(command=command_group, thought=thought)
  179. elif python_code := re.search(
  180. r'<execute_ipython>(.*)</execute_ipython>', action_str, re.DOTALL
  181. ):
  182. # a code block was found
  183. code_group = python_code.group(1).strip()
  184. thought = action_str.replace(python_code.group(0), '').strip()
  185. return IPythonRunCellAction(code=code_group, thought=thought)
  186. else:
  187. # We assume the LLM is GOOD enough that when it returns pure natural language
  188. # it want to talk to the user
  189. return AgentTalkAction(content=action_str)
  190. def search_memory(self, query: str) -> List[str]:
  191. raise NotImplementedError('Implement this abstract method')