From 33b9c57756d0993b660e6ee5b623a6a5707e8d11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E4=BC=9F=E9=9F=AC?= Date: Wed, 4 Sep 2024 20:36:49 +0800 Subject: [PATCH] fix/insert_more_than_one_pre_rsp --- metagpt/prompts/di/engineer2.py | 7 ++-- metagpt/roles/di/engineer2.py | 7 ++++ metagpt/roles/di/role_zero.py | 48 +++++++++++++----------- metagpt/strategy/experience_retriever.py | 25 +++++++++--- metagpt/tools/libs/editor.py | 37 ++++++++++++------ 5 files changed, 83 insertions(+), 41 deletions(-) diff --git a/metagpt/prompts/di/engineer2.py b/metagpt/prompts/di/engineer2.py index c597146a5..3108a7cbd 100644 --- a/metagpt/prompts/di/engineer2.py +++ b/metagpt/prompts/di/engineer2.py @@ -5,7 +5,8 @@ You are an autonomous programmer The special interface consists of a file editor that shows you 100 lines of a file at a time. -You can use any terminal commands you want (e.g., find, grep, cat, ls, cd) by calling Terminal.run_command. +You can use terminal commands (e.g., cat, ls, cd) by calling Terminal.run_command. +Do Not run the code. You should carefully observe the behavior and results of the previous action, and avoid triggering repeated errors. @@ -63,7 +64,7 @@ Note: 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. +11.1 Do not use Editor.insert_content_at_line or Editor.edit_file_by_replace more than once per command list. 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 @@ -75,7 +76,7 @@ Note: 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. +22. Forbidden to run code in the terminal. """ ENGINEER2_CMD_PROMPT = ( CMD_PROMPT diff --git a/metagpt/roles/di/engineer2.py b/metagpt/roles/di/engineer2.py index 56d520b36..bee5aa04d 100644 --- a/metagpt/roles/di/engineer2.py +++ b/metagpt/roles/di/engineer2.py @@ -77,6 +77,13 @@ class Engineer2(RoleZero): # "ValidateAndRewriteCode": validate.run, } ) + if self.run_eval: + self.tool_execution_map.update( + { + "RoleZero.ask_human": self._end, + "RoleZero.reply_to_human": self._end, + } + ) async def eval_terminal_run(self, cmd): """change command pull/push/commit to end.""" diff --git a/metagpt/roles/di/role_zero.py b/metagpt/roles/di/role_zero.py index ed84d24d4..d28f27138 100644 --- a/metagpt/roles/di/role_zero.py +++ b/metagpt/roles/di/role_zero.py @@ -80,7 +80,6 @@ class RoleZero(Role): "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(enable_auto_lint=True) browser: Browser = Browser() @@ -223,11 +222,8 @@ class RoleZero(Role): async with ThoughtReporter(enable_llm_stream=True) as reporter: await reporter.async_report({"type": "react"}) self.command_rsp = await self.llm_cached_aask(req=req, system_msgs=[system_prompt], state_data=state_data) - 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()) @@ -267,7 +263,8 @@ class RoleZero(Role): if self.use_fixed_sop: return await super()._act() - commands, ok = await self._parse_commands(self.command_rsp) + commands, ok, self.command_rsp = await self._parse_commands(self.command_rsp) + self.rc.memory.add(AIMessage(content=self.command_rsp)) if not ok: error_msg = commands self.rc.memory.add(UserMessage(content=error_msg)) @@ -415,12 +412,25 @@ class RoleZero(Role): tb = traceback.format_exc() print(tb) error_msg = str(e) - return error_msg, False + return error_msg, False, command_rsp # 为了对LLM不按格式生成进行容错 if isinstance(commands, dict): commands = commands["commands"] if "commands" in commands else [commands] - return commands, True + + # Set the exclusive command flag to False. + command_flag = [command["command_name"] not in self.exclusive_tool_commands for command in commands] + if command_flag.count(False) > 1: + # Set the flag of the first exclusive command to True. + index_of_first_exclusive = command_flag.index(False) + command_flag[index_of_first_exclusive] = True + # Select command which flag is True. + commands = [commands[index] for index, flag in enumerate(command_flag) if flag is True] + command_rsp = "```json\n" + json.dumps(commands, indent=4, ensure_ascii=False) + "\n```json" + logger.info( + "exclusive command more than one in current command list. change the command list.\n" + command_rsp + ) + return commands, True, command_rsp async def _run_commands(self, commands) -> str: outputs = [] @@ -455,7 +465,7 @@ class RoleZero(Role): return outputs def _is_special_command(self, cmd) -> bool: - return cmd["command_name"] in self.special_tool_commands or cmd["command_name"] in self.exclusive_tool_commands + return cmd["command_name"] in self.special_tool_commands async def _run_special_command(self, cmd) -> str: """command requiring special check or parsing""" @@ -464,7 +474,9 @@ class RoleZero(Role): if cmd["command_name"] == "Plan.finish_current_task": if not self.planner.plan.is_plan_finished(): self.planner.plan.finish_current_task() - command_output = "Current task is finished. If all tasks are finished, use 'end' to stop." + command_output = ( + "Current task is finished. If you no longer need to take action, use the command ‘end’ to stop." + ) elif cmd["command_name"] == "end": command_output = await self._end() @@ -480,13 +492,6 @@ 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]: @@ -532,12 +537,13 @@ class RoleZero(Role): from metagpt.environment.mgx.mgx_env import MGXEnv # avoid circular import if not isinstance(self.rc.env, MGXEnv): - return "Not in MGXEnv, command will not be executed." - rsp = await self.rc.env.reply_to_human(content, sent_from=self) - rsp += " If all tasks are finished, use 'end' to stop." - return + rsp = "Not in MGXEnv, command will not be executed." + else: + rsp = await self.rc.env.reply_to_human(content, sent_from=self) + rsp += " If you no longer need to take action, use the command ‘end’ to stop." + return rsp - async def _end(self): + async def _end(self, **kwarg): self._set_state(-1) memory = self.rc.memory.get(self.memory_k) # Ensure reply to the human before the "end" command is executed. Hard code k=5 for checking. diff --git a/metagpt/strategy/experience_retriever.py b/metagpt/strategy/experience_retriever.py index a751a0810..65fb0c53a 100644 --- a/metagpt/strategy/experience_retriever.py +++ b/metagpt/strategy/experience_retriever.py @@ -988,6 +988,24 @@ Note that the edit command must be executed in a single response, so this step w ] ``` +## example 10.1 +To enhance the functionality of the 2048 game, including game end detection and score tracking, we need to add these features to the existing game_2048.py file. First, we will add a score tracking feature, and then we will insert game end detection logic into the game loop. +We will use the Editor.insert_content_at_line command to insert new code into the file for adding score tracking and game end detection. +Since Editor.insert_content_at_line can only be used once per response, this time I will use it to create the variable self.score +```json +[ + { + "command_name": "Editor.insert_content_at_line", + "args": { + "file_name": "/home/mgx/mgx/MetaGPT/workspace/2048_game_py/game_2048.py", + "line_number": 4, + "content": " self.score = 0\n" + } + } +] +``` +In the next turn, I will try to add another code snippet + ## example 11 ``` #### Save the changes and commit them to the remote repository. @@ -1028,14 +1046,9 @@ Thought: Now that the changes have been pushed to the remote repository, due to """ ## example 11 -I have finish all the tasks, so I will use 'Plan.finish_current_task' and then fellowing the command "end" to stop. +I have finished all the tasks, so I will use Plan.finish_current_task and then follow the command ‘end’ to stop. ```json [ - { - "command_name": "Plan.finish_current_task", - "args": { - } - }, { "command_name": "end", "args": { diff --git a/metagpt/tools/libs/editor.py b/metagpt/tools/libs/editor.py index ca23984de..12af8611f 100644 --- a/metagpt/tools/libs/editor.py +++ b/metagpt/tools/libs/editor.py @@ -633,9 +633,11 @@ class Editor(BaseModel): first_error_line = None if lint_error is not None: - if first_error_line is not None: - show_line = int(first_error_line) - elif is_append: + # if first_error_line is not None: + # show_line = int(first_error_line) + + # show the first insert line. + if is_append: # original end-of-file show_line = len(lines) # insert OR edit WILL provide meaningful line numbers @@ -666,7 +668,7 @@ class Editor(BaseModel): ) ret_str += "-------------------------------------------------\n" - ret_str += "\n" + self.get_indentation_infromation(content, first_error_line) + ret_str += self.get_indentation_infromation(content, start or len(lines)) ret_str += ( "Your changes have NOT been applied. Please fix your edit command and try again.\n" @@ -687,12 +689,14 @@ 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) + error_str = "[This is how your edit would have looked if applied]\n" + error_str += "-------------------------------------------------\n" + error_str += self._print_window(file_name, start or len(lines), 40) + "\n" + error_str += "-------------------------------------------------\n" + error_str += self.get_indentation_infromation(content, start or len(lines)) + if not is_insert and not is_append: + error_str += "enlarge the range of original code." + error_str += "\nTry to enlarge the range of the orginal code" # 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()) @@ -720,7 +724,8 @@ class Editor(BaseModel): return ret_str def edit_file_by_replace(self, file_name: str, to_replace: str, new_content: str) -> str: - """Edit a file. This will search for `to_replace` in the given file and replace it with `new_content`. + """ + Edit a file. This will search for `to_replace` in the given file and replace it with `new_content`. Every *to_replace* must *EXACTLY MATCH* the existing source code, character for character, including all comments, docstrings, etc. @@ -764,6 +769,10 @@ class Editor(BaseModel): file_name: str: The name of the file to edit. to_replace: str: The content to search for and replace. new_content: str: The new content to replace the old content with. + + NOTE: + This tool is exclusive. If you use this tool, you cannot use any other commands in the current response. + If you need to use it multiple times, wait for the next turn. """ # FIXME: support replacing *all* occurrences if to_replace.strip() == "": @@ -839,6 +848,9 @@ class Editor(BaseModel): file_name: str: The name of the file to edit. line_number: int: The line number (starting from 1) to insert the content after. content: str: The content to insert. + NOTE: + This tool is exclusive. If you use this tool, you cannot use any other commands in the current response. + If you need to use it multiple times, wait for the next turn. """ file_name = self._try_fix_path(file_name) @@ -859,6 +871,9 @@ class Editor(BaseModel): Args: file_name: str: The name of the file to edit. content: str: The content to insert. + NOTE: + This tool is exclusive. If you use this tool, you cannot use any other commands in the current response. + If you need to use it multiple times, wait for the next turn. """ file_name = self._try_fix_path(file_name)