diff --git a/metagpt/prompts/di/engineer2.py b/metagpt/prompts/di/engineer2.py index 559bb89e4..c597146a5 100644 --- a/metagpt/prompts/di/engineer2.py +++ b/metagpt/prompts/di/engineer2.py @@ -21,10 +21,9 @@ Note: 3. When using Editor.edit_file_by_replace, if there is no exact match, take the difference in indentation into consideration. 4. After editing, verify the changes to ensure correct line numbers and proper indentation. Adhere to PEP8 standards for Python code. 5. NOTE ABOUT THE EDIT COMMAND: Indentation really matters! When editing a file, make sure to insert appropriate indentation before each line! Ensuring the code adheres to PEP8 standards. If a edit command fails, you can try to edit the file again to correct the indentation, but don't repeat the same command without changes. -6. YOU CAN ONLY ENTER ONE COMMAND AT A TIME and must wait for feedback, plan your commands carefully. -7. To avoid syntax errors when editing files multiple times, consider opening the file to view the surrounding code related to the error line and make modifications based on this context. -8. Ensure to observe the currently open file and the current working directory, which is displayed right after the open file. The open file might be in a different directory than the working directory. Remember, commands like 'create' open files and might alter the current open file. -9. Effectively using Use search commands (`search_dir`, `search_file`, `find_file`) and navigation commands (`open_file`, `goto_line`) to locate and modify files efficiently. The Editor tool can fully satisfy the requirements. Follow these steps and considerations for optimal results: +6. To avoid syntax errors when editing files multiple times, consider opening the file to view the surrounding code related to the error line and make modifications based on this context. +7. Ensure to observe the currently open file and the current working directory, which is displayed right after the open file. The open file might be in a different directory than the working directory. Remember, commands like 'create' open files and might alter the current open file. +8. Effectively using Use search commands (`search_dir`, `search_file`, `find_file`) and navigation commands (`open_file`, `goto_line`) to locate and modify files efficiently. The Editor tool can fully satisfy the requirements. Follow these steps and considerations for optimal results: **General Search Guidelines:** - Ensure you are in the repository's root directory before starting your search. - Always double-check the current working directory and the currently open file to avoid confusion. @@ -61,24 +60,26 @@ Note: - If a search command fails, modify the search criteria and check for typos or incorrect paths, then try again. - Based on feedback of observation or Terminal command in trajectory to guide adjustments in your search strategy. -10. When the edit fails, try to enlarge the starting line. -11. You must use the Editor.open_file command to open a file before using the Editor tool's edit command to modify it. When you open a file, any currently open file will be automatically closed. -12. Remember, when you use Editor.insert_content_at_line or Editor.edit_file_by_replace, the line numbers will change after the operation. Therefore, if there are multiple operations, perform only the first operation in the current response, and defer the subsequent operations to the next turn. -13. If you choose Editor.insert_content_at_line, you must ensure that there is no duplication between the inserted content and the original code. If there is overlap between the new code and the original code, use Editor.edit_file_by_replace instead. -14. If you choose Editor.edit_file_by_replace, the original code that needs to be replaced must start at the beginning of the line and end at the end of the line +9. When the edit fails, try to enlarge the range of code. +10. You must use the Editor.open_file command to open a file before using the Editor tool's edit command to modify it. When you open a file, any currently open file will be automatically closed. +11. Remember, when you use Editor.insert_content_at_line or Editor.edit_file_by_replace, the line numbers will change after the operation. Therefore, if there are multiple operations, perform only the first operation in the current response, and defer the subsequent operations to the next turn. +11.1 Using Editor.insert_content_at_line and Editor.edit_file_by_replace more than once in the current command list is forbidden. +12. If you choose Editor.insert_content_at_line, you must ensure that there is no duplication between the inserted content and the original code. If there is overlap between the new code and the original code, use Editor.edit_file_by_replace instead. +13. If you choose Editor.edit_file_by_replace, the original code that needs to be replaced must start at the beginning of the line and end at the end of the line -15. When not specified, you should write files in a folder named "src". If you know the project path, then write in a "src" folder under the project path. -16. When provided system design or project schedule, you MUST read them first before making a plan, then adhere to them in your implementation, especially in the programming language, package, or framework. You MUST implement all code files prescribed in the system design or project schedule. You can create a plan first with each task corresponding to implementing one code file. -17. When planning, initially list the files for coding, then outline all coding and review tasks in your first response. -18. If you plan to read a file, do not include other plans in the same response. -19. Use Engineer2.write_new_code to create or modify a file. Write only one code file each time. -20. When the requirement is simple, you don't need to create a plan, just do it right away. -21. If the code exists, use the Editor tool's open and edit commands to modify it. Since it is not a new code, do not use write_new_code. -22. Aways user absolute path as parameter. if no specific root path given, use "workspace/'project_name'" as default work space. +14. When not specified, you should write files in a folder named "src". If you know the project path, then write in a "src" folder under the project path. +15. When provided system design or project schedule, you MUST read them first before making a plan, then adhere to them in your implementation, especially in the programming language, package, or framework. You MUST implement all code files prescribed in the system design or project schedule. You can create a plan first with each task corresponding to implementing one code file. +16. When planning, initially list the files for coding, then outline all coding tasks based on the file organization in your first response. +17. If you plan to read a file, do not include other plans in the same response. +18. Use Engineer2.write_new_code to create or modify a file. Write only one code file each time. If you only need to code one file, provide all the necessary information in one response. +19. When the requirement is simple, you don't need to create a plan, just do it right away. +20. If the code exists, use the Editor tool's open and edit commands to modify it. Since it is not a new code, do not use write_new_code. +21. Aways user absolute path as parameter. if no specific root path given, use "workspace/'project_name'" as default work space. +22. Running the Python code in the terminal is strictly forbidden. """ ENGINEER2_CMD_PROMPT = ( CMD_PROMPT - + "\nWhen using the Editor tool, the command list must contain a single command. Because the command is mutually exclusive." + + "\nUsing Editor.insert_content_at_line and Editor.edit_file_by_replace more than once in the current command list is forbidden. Because the command is mutually exclusive and will change the line number after execution." ) CURRENT_EDITOR_STATE = """ @@ -99,7 +100,7 @@ You are a world-class engineer, your goal is to write google-style, elegant, mod Pay attention to the conversation history and the following constraints: 1. When provided system design, YOU MUST FOLLOW "Data structures and interfaces". DONT CHANGE ANY DESIGN. Do not use public member functions that do not exist in your design. 2. When modifying a code, rewrite the full code instead of updating or inserting a snippet. -3. Write out EVERY CODE DETAIL, DON'T LEAVE TODO. +3. Write out EVERY CODE DETAIL, DON'T LEAVE TODO OR PLACEHOLDER. """ WRITE_CODE_PROMPT = """ diff --git a/metagpt/roles/di/role_zero.py b/metagpt/roles/di/role_zero.py index 94b21a76f..ed84d24d4 100644 --- a/metagpt/roles/di/role_zero.py +++ b/metagpt/roles/di/role_zero.py @@ -75,8 +75,14 @@ class RoleZero(Role): tool_recommender: Optional[ToolRecommender] = None tool_execution_map: Annotated[dict[str, Callable], Field(exclude=True)] = {} special_tool_commands: list[str] = ["Plan.finish_current_task", "end", "Bash.run"] + exclusive_tool_commands: list[str] = [ + "Editor.edit_file_by_replace", + "Editor.insert_content_at_line", + "Editor.append_file", + ] + exclusive_command_enable_flag: bool = True # Equipped with three basic tools by default for optional use - editor: Editor = Editor() + editor: Editor = Editor(enable_auto_lint=True) browser: Browser = Browser() # Experience @@ -149,7 +155,7 @@ class RoleZero(Role): "scroll_up", "search_dir", "search_file", - "set_workdir", + # "set_workdir", "write", ] } @@ -221,6 +227,7 @@ class RoleZero(Role): self.command_rsp = await self._check_duplicates(req, self.command_rsp) self.rc.memory.add(AIMessage(content=self.command_rsp)) + self.exclusive_command_enable_flag = True return True @exp_cache(context_builder=RoleZeroContextBuilder(), serializer=RoleZeroSerializer()) @@ -420,14 +427,14 @@ class RoleZero(Role): for cmd in commands: output = f"Command {cmd['command_name']} executed" # handle special command first - if self._is_special_command(cmd): - special_command_output = await self._run_special_command(cmd) - outputs.append(output + ":" + special_command_output) - continue - # run command as specified by tool_execute_map - if cmd["command_name"] in self.tool_execution_map: - tool_obj = self.tool_execution_map[cmd["command_name"]] - try: + try: + if self._is_special_command(cmd): + special_command_output = await self._run_special_command(cmd) + outputs.append(output + ":" + special_command_output) + continue + # run command as specified by tool_execute_map + if cmd["command_name"] in self.tool_execution_map: + tool_obj = self.tool_execution_map[cmd["command_name"]] if inspect.iscoroutinefunction(tool_obj): tool_output = await tool_obj(**cmd["args"]) else: @@ -435,20 +442,20 @@ class RoleZero(Role): if tool_output: output += f": {str(tool_output)}" outputs.append(output) - except Exception as e: - tb = traceback.format_exc() - logger.exception(str(e) + tb) - outputs.append(output + f": {tb}") - break # Stop executing if any command fails - else: - outputs.append(f"Command {cmd['command_name']} not found.") - break + else: + outputs.append(f"Command {cmd['command_name']} not found.") + break + except Exception as e: + tb = traceback.format_exc() + logger.exception(str(e) + tb) + outputs.append(output + f": {tb}") + break # Stop executing if any command fails outputs = "\n\n".join(outputs) return outputs def _is_special_command(self, cmd) -> bool: - return cmd["command_name"] in self.special_tool_commands + return cmd["command_name"] in self.special_tool_commands or cmd["command_name"] in self.exclusive_tool_commands async def _run_special_command(self, cmd) -> str: """command requiring special check or parsing""" @@ -472,6 +479,14 @@ class RoleZero(Role): ) else: command_output += f"\n[command]: {cmd['args']['cmd']} \n[command output] : {tool_output}" + + elif cmd["command_name"] in self.exclusive_tool_commands: + if self.exclusive_command_enable_flag is True: + tool_obj = self.tool_execution_map[cmd["command_name"]] + command_output += tool_obj(**cmd["args"]) + else: + command_output += "This command has not been executed." + self.exclusive_command_enable_flag = False return command_output def _get_plan_status(self) -> Tuple[str, str]: @@ -518,7 +533,9 @@ class RoleZero(Role): if not isinstance(self.rc.env, MGXEnv): return "Not in MGXEnv, command will not be executed." - return await self.rc.env.reply_to_human(content, sent_from=self) + rsp = await self.rc.env.reply_to_human(content, sent_from=self) + rsp += " If all tasks are finished, use 'end' to stop." + return async def _end(self): self._set_state(-1) diff --git a/metagpt/strategy/experience_retriever.py b/metagpt/strategy/experience_retriever.py index a6fe44368..a751a0810 100644 --- a/metagpt/strategy/experience_retriever.py +++ b/metagpt/strategy/experience_retriever.py @@ -842,7 +842,7 @@ Explanation: I will first need to read the system design document and the projec ## example 2 Consider this example only after you have obtained the content of system design and project schedule documents. -Suppose the system design and project schedule prescribes three files index.html, style.css, script.js, to follow the design and schedule, I will create a plan consisting of three tasks, each corresponding to the creation of one of the required files: `index.html`, `style.css`, and `script.js`. Following the completion of these tasks, I will add a code review task for each file to ensure the implementation aligns with the provided system design and project schedule documents. +Suppose the system design and project schedule prescribes three files index.html, style.css, script.js, to follow the design and schedule, I will create a plan consisting of three tasks, each corresponding to the creation of one of the required files: `index.html`, `style.css`, and `script.js`. Here's the plan: @@ -916,19 +916,6 @@ I will use browser to review the detailed information of this issue in order to ] ``` -## example 5 -The target working directory is "/workspace/MetaGPT/provider/", but the current working directory is different. I will use the set_workdir command to change the working directory. -```json -[ - { - "command_name": "Editor.set_workdir", - "args": { - "path": "/workspace/MetaGPT/provider" - } - } -] -``` - ## example 6 I need to locating the `openai_api.py` file, so I will search for the `openai_api.py` file. ```json diff --git a/metagpt/tools/libs/editor.py b/metagpt/tools/libs/editor.py index 46f0d5c0d..ca23984de 100644 --- a/metagpt/tools/libs/editor.py +++ b/metagpt/tools/libs/editor.py @@ -277,7 +277,7 @@ class Editor(BaseModel): return "" return f"[File: {current_file.resolve()} ({total_lines} lines total)]\n" - def set_workdir(self, path: str) -> None: + def _set_workdir(self, path: str) -> None: """ Sets the working directory to the given path. eg: repo directory. You MUST to set it up before open the file. @@ -499,6 +499,17 @@ class Editor(BaseModel): content = "".join(new_lines) return content, n_added_lines + def get_indentation_infromation(self, content, first_error_line): + content_lines = content.split("\n") + previous_line = content_lines[first_error_line - 2] if first_error_line - 2 >= 0 else "" + first_insert_line = content_lines[first_error_line - 1] + ret_str = f'the privous line is "{previous_line}", the indentation has {len(previous_line)-len(previous_line.lstrip())} space\n' + insert_line_indentation = len(first_insert_line) - len(first_insert_line.lstrip()) + ret_str += f'the error line is "{first_insert_line}", the indentation has {insert_line_indentation} space\n' + ret_str += "Please check the indentation of the code to ensure that it is not causing any errors.\n" + ret_str += f"Try to use indentation that has {insert_line_indentation-4 if insert_line_indentation-4 >0 else 0} or {insert_line_indentation+4} space" + return ret_str + def _edit_file_impl( self, file_name: Path, @@ -655,6 +666,8 @@ class Editor(BaseModel): ) ret_str += "-------------------------------------------------\n" + ret_str += "\n" + self.get_indentation_infromation(content, first_error_line) + ret_str += ( "Your changes have NOT been applied. Please fix your edit command and try again.\n" "You either need to 1) Specify the correct start/end line arguments or 2) Correct your edit code.\n" @@ -674,11 +687,21 @@ class Editor(BaseModel): except ValueError as e: ret_str += f"Invalid input: {e}\n" except Exception as e: + error_str = "" + if is_append: + error_str += self.get_indentation_infromation(content, len(lines)) + else: + # insert or replace + error_str += self.get_indentation_infromation(content, start) # Clean up the temporary file if an error occurs + with original_file_backup_path.open() as fin, file_name.open("w") as fout: + fout.write(fin.read()) if temp_file_path and Path(temp_file_path).exists(): Path(temp_file_path).unlink() + logger.warning(f"An unexpected error occurred: {e}") - raise e + raise Exception(f"{error_str}") from e + # raise e # Update the file information and print the updated content with file_name.open("r", encoding="utf-8") as file: