parser.py 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. import re
  2. from opendevin.events.action import (
  3. Action,
  4. AgentFinishAction,
  5. BrowseURLAction,
  6. CmdRunAction,
  7. FileReadAction,
  8. FileWriteAction,
  9. MessageAction,
  10. )
  11. from .prompts import COMMAND_USAGE, CUSTOM_DOCS
  12. # commands: exit, read, write, browse, kill, search_file, search_dir
  13. no_open_file_error = MessageAction(
  14. 'You are not currently in a file. You can use the read command to open a file and then use goto to navigate through it.'
  15. )
  16. def invalid_error(cmd, docs):
  17. return f"""ERROR:
  18. Invalid command structure for
  19. ```
  20. {cmd}
  21. ```
  22. You may have caused this error by having multiple commands within your command block.
  23. If so, try again by running only one of the commands:
  24. Try again using this format:
  25. {COMMAND_USAGE[docs]}
  26. """
  27. def get_action_from_string(
  28. command_string: str, path: str, line: int, thoughts: str = ''
  29. ) -> Action | None:
  30. """
  31. Parses the command string to find which command the agent wants to run
  32. Converts the command into a proper Action and returns
  33. """
  34. vars = command_string.split(' ')
  35. cmd = vars[0]
  36. args = [] if len(vars) == 1 else vars[1:]
  37. if 'exit' == cmd:
  38. return AgentFinishAction()
  39. elif 'think' == cmd:
  40. return MessageAction(' '.join(args))
  41. elif 'scroll_up' == cmd:
  42. if not path:
  43. return no_open_file_error
  44. return FileReadAction(path, line + 100, line + 200, thoughts)
  45. elif 'scroll_down' == cmd:
  46. if not path:
  47. return no_open_file_error
  48. return FileReadAction(path, line - 100, line, thoughts)
  49. elif 'goto' == cmd:
  50. if not path:
  51. return no_open_file_error
  52. rex = r'^goto\s+(\d+)$'
  53. valid = re.match(rex, command_string)
  54. if valid:
  55. start = int(valid.group(1))
  56. end = start + 100
  57. return FileReadAction(path, start, end, thoughts)
  58. else:
  59. return MessageAction(invalid_error(command_string, 'goto'))
  60. elif 'edit' == cmd:
  61. if not path:
  62. return no_open_file_error
  63. rex = r'^edit\s+(\d+)\s+(-?\d+)\s+(\S.*)$'
  64. valid = re.match(rex, command_string, re.DOTALL)
  65. if valid:
  66. start = int(valid.group(1))
  67. end = int(valid.group(2))
  68. change = valid.group(3)
  69. if '"' == change[-1] and '"' == change[0]:
  70. change = change[1:-1]
  71. return FileWriteAction(path, change, start, end, thoughts)
  72. else:
  73. return MessageAction(invalid_error(command_string, 'edit'))
  74. elif 'read' == cmd:
  75. rex = r'^read\s+(\S+)(?:\s+(\d+))?(?:\s+(-?\d+))?$'
  76. valid = re.match(rex, command_string, re.DOTALL)
  77. if valid:
  78. file = valid.group(1)
  79. start_str = valid.group(2)
  80. end_str = valid.group(3)
  81. start = 0 if not start_str else int(start_str)
  82. end = -1 if not end_str else int(end_str)
  83. return FileReadAction(file, start, end, thoughts)
  84. else:
  85. return MessageAction(invalid_error(command_string, 'read'))
  86. elif 'write' == cmd:
  87. rex = r'^write\s+(\S+)\s+(.*?)\s*(\d+)?\s*(-?\d+)?$'
  88. valid = re.match(rex, command_string, re.DOTALL)
  89. if valid:
  90. file = valid.group(1)
  91. content = valid.group(2)
  92. start_str = valid.group(3)
  93. end_str = valid.group(4)
  94. start = 0 if not start_str else int(start_str)
  95. end = -1 if not end_str else int(end_str)
  96. if '"' == content[-1] and '"' == content[0]:
  97. content = content[1:-1]
  98. return FileWriteAction(file, content, start, end, thoughts)
  99. else:
  100. return MessageAction(invalid_error(command_string, 'write'))
  101. elif 'browse' == cmd:
  102. return BrowseURLAction(args[0].strip())
  103. elif cmd in ['search_file', 'search_dir', 'find_file']:
  104. rex = r'^(search_file|search_dir|find_file)\s+(\S+)(?:\s+(\S+))?$'
  105. valid = re.match(rex, command_string, re.DOTALL)
  106. if valid:
  107. return CmdRunAction(command_string)
  108. else:
  109. return MessageAction(
  110. f'Invalid command structure for\n ```\n{command_string}\n```.\nTry again using this format:\n{CUSTOM_DOCS}'
  111. )
  112. else:
  113. # check bash command
  114. obs = str(CmdRunAction(f'type {cmd}'))
  115. if obs.split(':')[-1].strip() == 'not found':
  116. # echo not found error for llm
  117. return MessageAction(content=obs)
  118. else:
  119. # run valid command
  120. return CmdRunAction(command_string)
  121. def parse_command(input_str: str, path: str, line: int):
  122. """
  123. Parses a given string and separates the command (enclosed in triple backticks) from any accompanying text.
  124. Args:
  125. input_str (str): The input string to be parsed.
  126. Returns:
  127. tuple: A tuple containing the command and the accompanying text (if any).
  128. """
  129. input_str = input_str.strip()
  130. if '```' in input_str:
  131. parts = input_str.split('```')
  132. command_str = parts[1].strip()
  133. ind = 2 if len(parts) > 2 else 1
  134. accompanying_text = ''.join(parts[:-ind]).strip()
  135. action = get_action_from_string(command_str, path, line, accompanying_text)
  136. if action:
  137. return action, accompanying_text
  138. return None, input_str # used for retry