prompts.py 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. import re
  2. from json import JSONDecodeError
  3. from opendevin.core.config import config
  4. from opendevin.core.exceptions import LLMOutputError
  5. from opendevin.core.utils import json
  6. from opendevin.events.action import (
  7. Action,
  8. action_from_dict,
  9. )
  10. from opendevin.events.observation import (
  11. CmdOutputObservation,
  12. )
  13. ACTION_PROMPT = """
  14. You're a thoughtful robot. Your main task is this:
  15. %(task)s
  16. Don't expand the scope of your task--just complete it as written.
  17. This is your internal monologue, in JSON format:
  18. %(monologue)s
  19. Your most recent thought is at the bottom of that monologue. Continue your train of thought.
  20. What is your next single thought or action? Your response must be in JSON format.
  21. It must be a single object, and it must contain two fields:
  22. * `action`, which is one of the actions below
  23. * `args`, which is a map of key-value pairs, specifying the arguments for that action
  24. Here are the possible actions:
  25. * `read` - reads the content of a file. Arguments:
  26. * `path` - the path of the file to read
  27. * `write` - writes the content to a file. Arguments:
  28. * `path` - the path of the file to write
  29. * `content` - the content to write to the file
  30. * `run` - runs a command. Arguments:
  31. * `command` - the command to run
  32. * `background` - if true, run the command in the background, so that other commands can be run concurrently. Useful for e.g. starting a server. You won't be able to see the logs. You don't need to end the command with `&`, just set this to true.
  33. * `kill` - kills a background command
  34. * `id` - the ID of the background command to kill
  35. * `browse` - opens a web page. Arguments:
  36. * `url` - the URL to open
  37. * `push` - Push a branch from the current repo to github:
  38. * `owner` - the owner of the repo to push to
  39. * `repo` - the name of the repo to push to
  40. * `branch` - the name of the branch to push
  41. * `recall` - recalls a past memory. Arguments:
  42. * `query` - the query to search for
  43. * `message` - make a plan, set a goal, or record your thoughts. Arguments:
  44. * `content` - the message to record
  45. * `finish` - if you're absolutely certain that you've completed your task and have tested your work, use the finish action to stop working.
  46. %(background_commands)s
  47. You MUST take time to think in between read, write, run, browse, push, and recall actions--do this with the `message` action.
  48. You should never act twice in a row without thinking. But if your last several
  49. actions are all `message` actions, you should consider taking a different action.
  50. Notes:
  51. * you are logged in as %(user)s, but sudo will always work without a password.
  52. * all non-background commands will be forcibly stopped if they remain running for over %(timeout)s seconds.
  53. * your environment is Debian Linux. You can install software with `sudo apt-get`, but remember to use -y.
  54. * don't run interactive commands, or commands that don't return (e.g. `node server.js`). You may run commands in the background (e.g. `node server.js &`)
  55. * don't run interactive text editors (e.g. `nano` or 'vim'), instead use the 'write' or 'read' action.
  56. * don't run gui applications (e.g. software IDEs (like vs code or codium), web browsers (like firefox or chromium), or other complex software packages). Use non-interactive cli applications, or special actions instead.
  57. * whenever an action fails, always send a `message` about why it may have happened before acting again.
  58. What is your next single thought or action? Again, you must reply with JSON, and only with JSON. You must respond with exactly one 'action' object.
  59. %(hint)s
  60. """
  61. MONOLOGUE_SUMMARY_PROMPT = """
  62. Below is the internal monologue of an automated LLM agent. Each
  63. thought is an item in a JSON array. The thoughts may be memories,
  64. actions taken by the agent, or outputs from those actions.
  65. Please return a new, smaller JSON array, which summarizes the
  66. internal monologue. You can summarize individual thoughts, and
  67. you can condense related thoughts together with a description
  68. of their content.
  69. %(monologue)s
  70. Make the summaries as pithy and informative as possible.
  71. Be specific about what happened and what was learned. The summary
  72. will be used as keywords for searching for the original memory.
  73. Be sure to preserve any key words or important information.
  74. Your response must be in JSON format. It must be an object with the
  75. key `new_monologue`, which is a JSON array containing the summarized monologue.
  76. Each entry in the array must have an `action` key, and an `args` key.
  77. The action key may be `summarize`, and `args.summary` should contain the summary.
  78. You can also use the same action and args from the source monologue.
  79. """
  80. def get_summarize_monologue_prompt(thoughts: list[dict]):
  81. """
  82. Gets the prompt for summarizing the monologue
  83. Returns:
  84. - str: A formatted string with the current monologue within the prompt
  85. """
  86. return MONOLOGUE_SUMMARY_PROMPT % {
  87. 'monologue': json.dumps({'old_monologue': thoughts}, indent=2),
  88. }
  89. def get_request_action_prompt(
  90. task: str,
  91. thoughts: list[dict],
  92. background_commands_obs: list[CmdOutputObservation] = [],
  93. ):
  94. """
  95. Gets the action prompt formatted with appropriate values.
  96. Parameters:
  97. - task (str): The current task the agent is trying to accomplish
  98. - thoughts (list[dict]): The agent's current thoughts
  99. - background_commands_obs (list[CmdOutputObservation]): list of all observed background commands running
  100. Returns:
  101. - str: Formatted prompt string with hint, task, monologue, and background included
  102. """
  103. hint = ''
  104. if len(thoughts) > 0:
  105. latest_thought = thoughts[-1]
  106. if 'action' in latest_thought:
  107. if latest_thought['action'] == 'message':
  108. if latest_thought['args']['content'].startswith('OK so my task is'):
  109. hint = "You're just getting started! What should you do first?"
  110. else:
  111. hint = "You've been thinking a lot lately. Maybe it's time to take action?"
  112. elif latest_thought['action'] == 'error':
  113. hint = 'Looks like that last command failed. Maybe you need to fix it, or try something else.'
  114. bg_commands_message = ''
  115. if len(background_commands_obs) > 0:
  116. bg_commands_message = 'The following commands are running in the background:'
  117. for command_obs in background_commands_obs:
  118. bg_commands_message += (
  119. f'\n`{command_obs.command_id}`: {command_obs.command}'
  120. )
  121. bg_commands_message += '\nYou can end any process by sending a `kill` action with the numerical `id` above.'
  122. user = 'opendevin' if config.run_as_devin else 'root'
  123. return ACTION_PROMPT % {
  124. 'task': task,
  125. 'monologue': json.dumps(thoughts, indent=2),
  126. 'background_commands': bg_commands_message,
  127. 'hint': hint,
  128. 'user': user,
  129. 'timeout': config.sandbox_timeout,
  130. 'WORKSPACE_MOUNT_PATH_IN_SANDBOX': config.workspace_mount_path_in_sandbox,
  131. }
  132. def parse_action_response(response: str) -> Action:
  133. """
  134. Parses a string to find an action within it
  135. Parameters:
  136. - response (str): The string to be parsed
  137. Returns:
  138. - Action: The action that was found in the response string
  139. """
  140. try:
  141. action_dict = json.loads(response)
  142. except JSONDecodeError:
  143. # Find response-looking json in the output and use the more promising one. Helps with weak llms
  144. response_json_matches = re.finditer(
  145. r"""{\s*\"action\":\s?\"(\w+)\"(?:,?|,\s*\"args\":\s?{((?:.|\s)*?)})\s*}""",
  146. response,
  147. ) # Find all response-looking strings
  148. def rank(match):
  149. return (
  150. len(match[2]) if match[1] == 'message' else 130
  151. ) # Crudely rank multiple responses by length
  152. try:
  153. action_dict = json.loads(
  154. max(response_json_matches, key=rank)[0]
  155. ) # Use the highest ranked response
  156. except (ValueError, JSONDecodeError):
  157. raise LLMOutputError(
  158. 'Invalid JSON, the response must be well-formed JSON as specified in the prompt.'
  159. )
  160. except (ValueError, TypeError):
  161. raise LLMOutputError(
  162. 'Invalid JSON, the response must be well-formed JSON as specified in the prompt.'
  163. )
  164. if 'content' in action_dict:
  165. # The LLM gets confused here. Might as well be robust
  166. action_dict['contents'] = action_dict.pop('content')
  167. return action_from_dict(action_dict)
  168. def parse_summary_response(response: str) -> list[dict]:
  169. """
  170. Parses a summary of the monologue
  171. Parameters:
  172. - response (str): The response string to be parsed
  173. Returns:
  174. - list[dict]: The list of summaries output by the model
  175. """
  176. parsed = json.loads(response)
  177. return parsed['new_monologue']