Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion froggy/envs/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,10 @@ def run(self):

def load_current_file(self, filepath: str) -> bool:
self.current_file = filepath
self.current_file_content = (self.working_dir / self.current_file).read_text()
self.current_file_content = self.load_file(filepath)

def load_file(self, filepath: str) -> str:
return (self.working_dir / filepath).read_text()

def directory_tree(self, root: str = None, editable_only: bool = False):
root = Path(root or self.path).absolute()
Expand Down
153 changes: 93 additions & 60 deletions froggy/tools/patchers.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,94 +139,127 @@ class SubstitutionPatcher(CodePatcher):
name = "substitution_patcher"
description = "Creates patches of code given start and end lines."
instructions = {
"template": "```rewrite head:tail <c>new_code</c>```",
"description": "Rewrite the code between lines [head, tail] with the new code. Line numbers are 1-based. When tail is not provided, it's assumed to be the same as head. When head is not provided, it's assumed to rewrite the whole code. The new code should be valid python code include proper indentation (can be determined from context), the special tokens <c> and </c> are used to wrap the new code. ",
"template": "```rewrite file/path.py head:tail <c>new_code</c>```",
"description": "Rewrite the code in file/path.py between lines [head, tail] with the new code. Line numbers are 1-based. When file path is not provided, it's assumed to rewrite the current file. When head and tail are not provided, it's assumed to rewrite the whole code. When only head is provided, it's assumed to rewrite that single line. The new code should be valid python code include proper indentation (can be determined from context), the special tokens <c> and </c> are used to wrap the new code. ",
"examples": [
"```rewrite <c>print('hola')</c>``` will rewrite the entire code to be print('hola'), because no line number is provided.",
"```rewrite 10 <c> print('bonjour')</c>``` will rewite line number 10 to be print('bonjour'), with the indents ahead (in this case, 4 spaces).",
"```rewrite 10:20 <c> print('hello')\\n print('hi again')</c>``` will replace the chunk of code between line number 10 and 20 by the two lines provided, both with indents ahead (in this case, 4 spaces).",
"```rewrite <c>print('hola')</c>``` will rewrite the current file (the entire code) to be print('hola'), because no line number is provided.",
"```rewrite 10 <c> print('bonjour')</c>``` will rewite line number 10 of the current file to be print('bonjour'), with the indents ahead (in this case, 4 spaces).",
"```rewrite 10:20 <c> print('hello')\\n print('hi again')</c>``` will replace the chunk of code between line number 10 and 20 in the current file by the two lines provided, both with indents ahead (in this case, 4 spaces).",
"```rewrite code/utils.py 4:6 <c> print('buongiorno')</c>``` will replace the chunk of code between line number 4 and 6 in the file code/utils.py by the single line provided, with the indent ahead (in this case, 8 spaces).",
],
}

def is_triggered(self, action):
return action.startswith(self.action)

def use(self, patch):
content = patch.split(self.action)[1].split("```")[0].strip()
self.rewrite_success = False
# rewrite the code given what's been discovered during the debugging
if self.environment.current_file is None:
return "No file is currently open."
if self.environment.current_file not in self.environment.editable_files:
return f"File {self.environment.current_file} is not editable."
def _rewrite_file(self, file_path, head, tail, new_code):
if file_path is None:
# by default, rewrite the current file
file_path = self.environment.current_file

if file_path is None:
return "No file is currently open.", False, None
if file_path.startswith(str(self.environment.working_dir)):
file_path = file_path[len(str(self.environment.working_dir)) + 1 :]
if file_path not in self.environment.all_files:
return f"File {file_path} does not exist or is not in the current repository.", False, None
if file_path not in self.environment.editable_files:
return f"File {file_path} is not editable.", False, None

success = True
if content[0].isnumeric():
# the line number is provided
new_code = clean_code(new_code) # str
new_code_lines = new_code.split("\n")
new_code_length = len(new_code_lines) # number of lines in the newly generated code
if head is None and tail is None:
# no line number is provided, rewrite the whole code
try:
line_numbers = content.split("<c>")[0].strip()
content = (
content[len(line_numbers) :]
.strip()
.split("<c>")[1]
.split("</c>")[0]
) # get the new code
line_numbers = line_numbers.split(":")
line_numbers = [item.strip() for item in line_numbers]
if len(line_numbers) == 1:
# only head is provided (rewrite that line)
head = int(line_numbers[0]) - 1 # 1-based to 0-based
tail = head
elif len(line_numbers) == 2:
# both head and tail are provided
head = int(line_numbers[0]) - 1 # 1-based to 0-based
tail = int(line_numbers[1]) - 1 # 1-based to 0-based
else:
success = False
self.environment.overwrite_file(filepath=file_path, content=new_code)
if file_path == self.environment.current_file:
self.environment.load_current_file(file_path)
except:
success = False
if success is True:
# rewrite the code
content = clean_code(content) # str
content_lines = content.split("\n")
content_length = len(
content_lines
) # number of lines in the newly generated code
full_code_lines = self.environment.current_file_content.split("\n")
full_code_lines[head : tail + 1] = content_lines # list
self.environment.overwrite_file(
filepath=self.environment.current_file,
content="\n".join(full_code_lines),
)
self.environment.load_current_file(self.environment.current_file)
else:
# no line number is provided, rewrite the whole code
# rewrite the code given the provided line numbers
if tail is None:
# only head is provided (rewrite that line)
tail = head
try:
content = content.split("<c>")[1].split("</c>")[0] # get the new code
content = clean_code(content) # str
content_length = len(
content.split("\n")
) # number of lines in the newly generated code
# rewrite the code
full_code_lines = self.environment.load_file(file_path).split("\n")
full_code_lines[head : tail + 1] = new_code_lines # list
self.environment.overwrite_file(
filepath=self.environment.current_file, content=content
filepath=file_path,
content="\n".join(full_code_lines)
)
self.environment.load_current_file(self.environment.current_file)
head, tail = None, None
if file_path == self.environment.current_file:
self.environment.load_current_file(file_path)
except:
success = False
return "", success, new_code_length

def parse_line_numbers(self, line_number_string):

# only line number is provided
line_numbers = line_number_string.split(":")
line_numbers = [item.strip() for item in line_numbers]
assert len(line_numbers) in [1, 2], "Invalid line number format."
if len(line_numbers) == 1:
# only head is provided (rewrite that line)
head = int(line_numbers[0]) - 1 # 1-based to 0-based
tail = head
else:
# len(line_numbers) == 2:
# both head and tail are provided
head = int(line_numbers[0]) - 1 # 1-based to 0-based
tail = int(line_numbers[1]) - 1 # 1-based to 0-based
return head, tail

def use(self, patch):
content = patch.split(self.action)[1].split("```")[0].strip()
# parse content to get file_path, head, tail, and new_code
# code/utils.py 4:6 <c> print('buongiorno')</c>
file_path, head, tail = None, None, None
try:
new_code = content.split("<c>", 1)[1].split("</c>", 1)[0]
content = content.split("<c>", 1)[0].strip()
# code/utils.py 4:6
content_list = content.split()
if len(content_list) == 0:
# no file path and line number is provided
pass
elif len(content_list) == 1:
# either file path or line number is provided
if content_list[0][0].isnumeric():
# only line number is provided
head, tail = self.parse_line_numbers(content_list[0])
else:
# only file path is provided
file_path = content_list[0]
elif len(content_list) == 2:
# both file path and line number are provided
file_path = content_list[0]
head, tail = self.parse_line_numbers(content_list[1])
else:
raise ValueError("Invalid content format.")
except:
self.rewrite_success = False
return "Rewrite failed."

message, success, new_code_length = self._rewrite_file(file_path, head, tail, new_code)
if success is True:
if (
hasattr(self.environment, "tools")
and isinstance(self.environment.tools, dict)
and "pdb" in self.environment.tools
):
self.environment.tools["pdb"].breakpoint_modify(
file_path,
head + 1 if isinstance(head, int) else None,
tail + 1 if isinstance(tail, int) else None,
content_length,
new_code_length,
) # converting head/tail back to 1-based index for breakpoint management
self.rewrite_success = True
return "Rewriting done."

return "Rewrite failed."

self.rewrite_success = False
return "\n".join([message, "Rewrite failed."])
8 changes: 6 additions & 2 deletions froggy/tools/pdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ def breakpoint_add_clear(self, action: str, which_file=None):

return success, output

def breakpoint_modify(self, rewrite_head, rewrite_tail, new_code_length):
def breakpoint_modify(self, rewrite_file, rewrite_head, rewrite_tail, new_code_length):
# handle breakpoints line number changes caused by rewriting
# this is a wrapper that manages the self.breakpoints_state, which does not reset at each pseudo terminal start
# self.breakpoints_state is a dict, the keys are "|||".join([file_path, str(line_number)]) and values are breakpoint_command
Expand All @@ -249,9 +249,13 @@ def breakpoint_modify(self, rewrite_head, rewrite_tail, new_code_length):
current_breakpoints_state_copy = copy.copy(
self.environment.current_breakpoints_state
)
if rewrite_file is None:
rewrite_file = self.environment.current_file
if rewrite_file.startswith(str(self.environment.working_dir)):
rewrite_file = rewrite_file[len(str(self.environment.working_dir)) + 1 :]
for _key in self.environment.current_breakpoints_state.keys():
_file_path, _line_number = _key.split("|||")
if _file_path != self.environment.current_file:
if _file_path != rewrite_file:
# the breakpoints are not in the current file, no need to modify
continue
_line_number = int(_line_number)
Expand Down