diff --git a/aider/coders/__init__.py b/aider/coders/__init__.py index 96b5ba2bd..7f85a5622 100644 --- a/aider/coders/__init__.py +++ b/aider/coders/__init__.py @@ -1,6 +1,7 @@ from .base_coder import Coder from .editblock_coder import EditBlockCoder +from .editblock_func_coder import EditBlockFunctionCoder from .wholefile_coder import WholeFileCoder from .wholefile_func_coder import WholeFileFunctionCoder -__all__ = [Coder, EditBlockCoder, WholeFileCoder, WholeFileFunctionCoder] +__all__ = [Coder, EditBlockCoder, WholeFileCoder, WholeFileFunctionCoder, EditBlockFunctionCoder] diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index a21cb420e..349a88418 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -55,7 +55,12 @@ class Coder: openai_api_base="https://api.openai.com/v1", **kwargs, ): - from . import EditBlockCoder, WholeFileCoder, WholeFileFunctionCoder + from . import ( + EditBlockCoder, + EditBlockFunctionCoder, + WholeFileCoder, + WholeFileFunctionCoder, + ) openai.api_key = openai_api_key openai.api_base = openai_api_base @@ -81,6 +86,8 @@ class Coder: return WholeFileCoder(main_model, io, **kwargs) elif edit_format == "whole-func": return WholeFileFunctionCoder(main_model, io, **kwargs) + elif edit_format == "diff-func": + return EditBlockFunctionCoder(main_model, io, **kwargs) else: raise ValueError(f"Unknown edit format {edit_format}") @@ -559,6 +566,8 @@ class Coder: if functions is not None: kwargs["functions"] = self.functions + dump(kwargs) + # Generate SHA1 hash of kwargs and append it to chat_completion_call_hashes hash_object = hashlib.sha1(json.dumps(kwargs, sort_keys=True).encode()) self.chat_completion_call_hashes.append(hash_object.hexdigest()) diff --git a/aider/coders/editblock_func_coder.py b/aider/coders/editblock_func_coder.py new file mode 100644 index 000000000..6937d5e76 --- /dev/null +++ b/aider/coders/editblock_func_coder.py @@ -0,0 +1,153 @@ +import os + +from aider import diffs + +from ..dump import dump # noqa: F401 +from .base_coder import Coder +from .editblock_func_prompts import EditBlockFunctionPrompts + + +class EditBlockFunctionCoder(Coder): + functions = [ + dict( + name="replace_lines", + description="create or update one or more files", + parameters=dict( + type="object", + required=["explanation", "edits"], + properties=dict( + explanation=dict( + type="string", + description=( + "Step by step plan for the changes to be made to the code (future" + " tense, markdown format)" + ), + ), + edits=dict( + type="array", + items=dict( + parameters=dict( + type="object", + required=["path", "original_lines", "updated_lines"], + properties=dict( + file_path=dict( + type="string", + description="Path of file to edit", + ), + original_lines=dict( + type="string", + description=( + ( + "Portion of the original file, including all" + " whitespace, newlines, without skipping any lines" + ), + ), + ), + updated_lines=dict( + type="string", + description=( + "New content to replace the `original_lines` with" + ), + ), + ), + ), + ), + ), + ), + ), + ), + ] + + def __init__(self, *args, **kwargs): + self.gpt_prompts = EditBlockFunctionPrompts() + super().__init__(*args, **kwargs) + + def update_cur_messages(self, content, edited): + if edited: + self.cur_messages += [ + dict(role="assistant", content=self.gpt_prompts.redacted_edit_message) + ] + else: + self.cur_messages += [dict(role="assistant", content=content)] + + def get_context_from_history(self, history): + context = "" + if history: + context += "# Context:\n" + for msg in history: + if msg["role"] == "user": + context += msg["role"].upper() + ": " + msg["content"] + "\n" + return context + + def render_incremental_response(self, final=False): + if self.partial_response_content: + return self.partial_response_content + + args = self.parse_partial_args() + + if not args: + return + + explanation = args.get("explanation") + files = args.get("files", []) + + res = "" + if explanation: + res += f"{explanation}\n\n" + + for i, file_upd in enumerate(files): + path = file_upd.get("path") + if not path: + continue + content = file_upd.get("content") + if not content: + continue + + this_final = (i < len(files) - 1) or final + res += self.live_diffs(path, content, this_final) + + return res + + def live_diffs(self, fname, content, final): + lines = content.splitlines(keepends=True) + + # ending an existing block + full_path = os.path.abspath(os.path.join(self.root, fname)) + + with open(full_path, "r") as f: + orig_lines = f.readlines() + + show_diff = diffs.diff_partial_update( + orig_lines, + lines, + final, + fname=fname, + ).splitlines() + + return "\n".join(show_diff) + + def update_files(self): + name = self.partial_response_function_call.get("name") + if name and name != "replace_lines": + raise ValueError(f'Unknown function_call name="{name}", use name="write_file"') + + args = self.parse_partial_args() + if not args: + return + + files = args.get("files", []) + + edited = set() + for file_upd in files: + path = file_upd.get("path") + if not path: + raise ValueError(f"Missing path parameter: {file_upd}") + + content = file_upd.get("content") + if not content: + raise ValueError(f"Missing content parameter: {file_upd}") + + if self.allowed_to_edit(path, content): + edited.add(path) + + return edited diff --git a/aider/coders/editblock_func_prompts.py b/aider/coders/editblock_func_prompts.py new file mode 100644 index 000000000..c7f18b868 --- /dev/null +++ b/aider/coders/editblock_func_prompts.py @@ -0,0 +1,27 @@ +# flake8: noqa: E501 + +from .base_prompts import CoderPrompts + + +class EditBlockFunctionPrompts(CoderPrompts): + main_system = """Act as an expert software developer. +Take requests for changes to the supplied code. +If the request is ambiguous, ask questions. + +Once you understand the request you MUST use the `replace_lines` function to edit the files to make the needed changes. +""" + + system_reminder = """ +ONLY return code using the `replace_lines` function. +NEVER return code outside the `replace_lines` function. +""" + + files_content_prefix = "Here is the current content of the files:\n" + files_no_full_files = "I am not sharing any files yet." + + redacted_edit_message = "No changes are needed." + + repo_content_prefix = ( + "Below here are summaries of other files! Do not propose changes to these *read-only*" + " files without asking me first.\n" + ) diff --git a/aider/coders/wholefile_func_prompts.py b/aider/coders/wholefile_func_prompts.py index 6434a042d..69be4aa74 100644 --- a/aider/coders/wholefile_func_prompts.py +++ b/aider/coders/wholefile_func_prompts.py @@ -21,4 +21,5 @@ NEVER return code outside the `write_file` function. redacted_edit_message = "No changes are needed." + # TODO: should this be present for using this with gpt-4? repo_content_prefix = None