browsing_agent.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. import os
  2. from browsergym.core.action.highlevel import HighLevelActionSet
  3. from browsergym.utils.obs import flatten_axtree_to_str
  4. from agenthub.browsing_agent.response_parser import BrowsingResponseParser
  5. from opendevin.controller.agent import Agent
  6. from opendevin.controller.state.state import State
  7. from opendevin.core.logger import opendevin_logger as logger
  8. from opendevin.events.action import (
  9. Action,
  10. AgentFinishAction,
  11. BrowseInteractiveAction,
  12. MessageAction,
  13. )
  14. from opendevin.events.event import EventSource
  15. from opendevin.events.observation import BrowserOutputObservation
  16. from opendevin.llm.llm import LLM
  17. from opendevin.runtime.plugins import (
  18. PluginRequirement,
  19. )
  20. from opendevin.runtime.tools import RuntimeTool
  21. USE_NAV = (
  22. os.environ.get('USE_NAV', 'true') == 'true'
  23. ) # only disable NAV actions when running webarena and miniwob benchmarks
  24. USE_CONCISE_ANSWER = (
  25. os.environ.get('USE_CONCISE_ANSWER', 'false') == 'true'
  26. ) # only return concise answer when running webarena and miniwob benchmarks
  27. if not USE_NAV and USE_CONCISE_ANSWER:
  28. EVAL_MODE = True # disabled NAV actions and only return concise answer, for webarena and miniwob benchmarks\
  29. else:
  30. EVAL_MODE = False
  31. def get_error_prefix(last_browser_action: str) -> str:
  32. return f'IMPORTANT! Last action is incorrect:\n{last_browser_action}\nThink again with the current observation of the page.\n'
  33. def get_system_message(goal: str, action_space: str) -> str:
  34. return f"""\
  35. # Instructions
  36. Review the current state of the page and all other information to find the best
  37. possible next action to accomplish your goal. Your answer will be interpreted
  38. and executed by a program, make sure to follow the formatting instructions.
  39. # Goal:
  40. {goal}
  41. # Action Space
  42. {action_space}
  43. """
  44. CONCISE_INSTRUCTION = """\
  45. Here is another example with chain of thought of a valid action when providing a concise answer to user:
  46. "
  47. In order to accomplish my goal I need to send the information asked back to the user. This page list the information of HP Inkjet Fax Machine, which is the product identified in the objective. Its price is $279.49. I will send a message back to user with the answer.
  48. ```send_msg_to_user("$279.49")```
  49. "
  50. """
  51. def get_prompt(error_prefix: str, cur_axtree_txt: str, prev_action_str: str) -> str:
  52. prompt = f"""\
  53. {error_prefix}
  54. # Current Accessibility Tree:
  55. {cur_axtree_txt}
  56. # Previous Actions
  57. {prev_action_str}
  58. Here is an example with chain of thought of a valid action when clicking on a button:
  59. "
  60. In order to accomplish my goal I need to click on the button with bid 12
  61. ```click("12")```
  62. "
  63. """.strip()
  64. if USE_CONCISE_ANSWER:
  65. prompt += CONCISE_INSTRUCTION
  66. return prompt
  67. class BrowsingAgent(Agent):
  68. VERSION = '1.0'
  69. """
  70. An agent that interacts with the browser.
  71. """
  72. sandbox_plugins: list[PluginRequirement] = []
  73. runtime_tools: list[RuntimeTool] = [RuntimeTool.BROWSER]
  74. response_parser = BrowsingResponseParser()
  75. def __init__(
  76. self,
  77. llm: LLM,
  78. ) -> None:
  79. """
  80. Initializes a new instance of the BrowsingAgent class.
  81. Parameters:
  82. - llm (LLM): The llm to be used by this agent
  83. """
  84. super().__init__(llm)
  85. # define a configurable action space, with chat functionality, web navigation, and webpage grounding using accessibility tree and HTML.
  86. # see https://github.com/ServiceNow/BrowserGym/blob/main/core/src/browsergym/core/action/highlevel.py for more details
  87. action_subsets = ['chat', 'bid']
  88. if USE_NAV:
  89. action_subsets.append('nav')
  90. self.action_space = HighLevelActionSet(
  91. subsets=action_subsets,
  92. strict=False, # less strict on the parsing of the actions
  93. multiaction=True, # enable to agent to take multiple actions at once
  94. )
  95. self.reset()
  96. def reset(self) -> None:
  97. """
  98. Resets the Browsing Agent.
  99. """
  100. super().reset()
  101. self.cost_accumulator = 0
  102. self.error_accumulator = 0
  103. def step(self, state: State) -> Action:
  104. """
  105. Performs one step using the Browsing Agent.
  106. This includes gathering information on previous steps and prompting the model to make a browsing command to execute.
  107. Parameters:
  108. - state (State): used to get updated info
  109. Returns:
  110. - BrowseInteractiveAction(browsergym_command) - BrowserGym commands to run
  111. - MessageAction(content) - Message action to run (e.g. ask for clarification)
  112. - AgentFinishAction() - end the interaction
  113. """
  114. messages = []
  115. prev_actions = []
  116. cur_axtree_txt = ''
  117. error_prefix = ''
  118. last_obs = None
  119. last_action = None
  120. if EVAL_MODE and len(state.history) == 1:
  121. # for webarena and miniwob++ eval, we need to retrieve the initial observation already in browser env
  122. # initialize and retrieve the first observation by issuing an noop OP
  123. # For non-benchmark browsing, the browser env starts with a blank page, and the agent is expected to first navigate to desired websites
  124. return BrowseInteractiveAction(browser_actions='noop()')
  125. for prev_action, obs in state.history:
  126. if isinstance(prev_action, BrowseInteractiveAction):
  127. prev_actions.append(prev_action.browser_actions)
  128. last_obs = obs
  129. last_action = prev_action
  130. elif (
  131. isinstance(prev_action, MessageAction)
  132. and prev_action.source == EventSource.AGENT
  133. ):
  134. # agent has responded, task finish.
  135. return AgentFinishAction(outputs={'content': prev_action.content})
  136. if EVAL_MODE:
  137. prev_actions = prev_actions[1:] # remove the first noop action
  138. prev_action_str = '\n'.join(prev_actions)
  139. # if the final BrowserInteractiveAction exec BrowserGym's send_msg_to_user,
  140. # we should also send a message back to the user in OpenDevin and call it a day
  141. if (
  142. isinstance(last_action, BrowseInteractiveAction)
  143. and last_action.browsergym_send_msg_to_user
  144. ):
  145. return MessageAction(last_action.browsergym_send_msg_to_user)
  146. if isinstance(last_obs, BrowserOutputObservation):
  147. if last_obs.error:
  148. # add error recovery prompt prefix
  149. error_prefix = get_error_prefix(last_obs.last_browser_action)
  150. self.error_accumulator += 1
  151. if self.error_accumulator > 5:
  152. return MessageAction('Too many errors encountered. Task failed.')
  153. try:
  154. cur_axtree_txt = flatten_axtree_to_str(
  155. last_obs.axtree_object,
  156. extra_properties=last_obs.extra_element_properties,
  157. with_clickable=True,
  158. filter_visible_only=True,
  159. )
  160. except Exception as e:
  161. logger.error(
  162. 'Error when trying to process the accessibility tree: %s', e
  163. )
  164. return MessageAction('Error encountered when browsing.')
  165. if (goal := state.get_current_user_intent()) is None:
  166. goal = state.inputs['task']
  167. system_msg = get_system_message(
  168. goal,
  169. self.action_space.describe(with_long_description=False, with_examples=True),
  170. )
  171. messages.append({'role': 'system', 'content': system_msg})
  172. prompt = get_prompt(error_prefix, cur_axtree_txt, prev_action_str)
  173. messages.append({'role': 'user', 'content': prompt})
  174. logger.info(prompt)
  175. response = self.llm.completion(
  176. messages=messages,
  177. temperature=0.0,
  178. stop=[')```', ')\n```'],
  179. )
  180. return self.response_parser.parse(response)
  181. def search_memory(self, query: str) -> list[str]:
  182. raise NotImplementedError('Implement this abstract method')