diff --git a/metagpt/prompts/di/engineer2.py b/metagpt/prompts/di/engineer2.py index 016884d2b..8a3860742 100644 --- a/metagpt/prompts/di/engineer2.py +++ b/metagpt/prompts/di/engineer2.py @@ -80,7 +80,8 @@ Note: 21. When using the editor, pay attention to the editor's current directory. When you use editor tools, the paths must be either absolute or relative to the editor's current directory. 22. The default programming languages are Native HTML. 23. When planning, consider whether images are needed. If you are developing a showcase website, start by using ImageGetter.get_image to obtain the necessary images. -24. If you finish all the tasks, use the command "end" to end. +24. When planning, merge multiple tasks that operate on the same file into a single task. For example, create one task for writing unit tests for all functions in a class. Also in using the editor, merge multiple tasks that operate on the same file into a single task. +25. When create unit tests for a code file, use Editor.read() to read the code file before planing. And create one plan to writing the unit test for the whole file. """ CURRENT_STATE = """ The current editor state is: @@ -108,8 +109,11 @@ WRITE_CODE_PROMPT = """ # Current Coding File {file_path} -# Further Instruction -{instruction} +# File Description +{file_description} + +# Instruction +Your task is to write the {file_name} according to the User Requirement. # Output While some concise thoughts are helpful, code is absolutely required. Always output one and only one code block in your response. Output code in the following format: diff --git a/metagpt/prompts/di/role_zero.py b/metagpt/prompts/di/role_zero.py index 2697ecef4..2ccb2bed0 100644 --- a/metagpt/prompts/di/role_zero.py +++ b/metagpt/prompts/di/role_zero.py @@ -71,7 +71,7 @@ you must respond in {respond_language}. Pay close attention to the Example provided, you can reuse the example for your current situation if it fits. If you open a file, the line number is displayed at the front of each line. You may use any of the available commands to create a plan or update the plan. You may output mutiple commands, they will be executed sequentially. -If you finish current task, you will automatically take the next task in the existing plan, use Plan.finish_task, DON'T append a new task. +If you finish current task, you will automatically take the next task in the existing plan, use Plan.finish_current_task, DON'T append a new task. Review the latest plan's outcome, focusing on achievements. If your completed task matches the current, consider it finished. Using 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. In your response, include at least one command. @@ -119,18 +119,14 @@ END_COMMAND = """ ``` """ -ASK_HUMAN_COMMAND = """ -```json -[ - { - "command_name": "RoleZero.ask_human", - "args": { - "question": "I'm a little uncertain about the next step, could you provide me with some guidance?" - } - } -] -``` +SUMMARY_PROBLEM_WHEN_DUPLICATE = """You has meet a problem and cause duplicate command.Please directly tell me what is confusing or troubling you. Do Not output any command.Ouput you problem in {language} and within 30 words.""" +ASK_HUMAN_GUIDANCE_FORMAT = """ +I am facing the following problem: +{problem} +Could you please provide me with some guidance?If you want to stop, please include "" in your guidance. """ +ASK_HUMAN_COMMAND = [{"command_name": "RoleZero.ask_human", "args": {"question": ""}}] + JSON_REPAIR_PROMPT = """ ## json data {json_data} @@ -226,8 +222,10 @@ Response Category: TASK. Thought: The request is vague and lacks specifics, requiring clarification on the process to optimize. Response Category: AMBIGUOUS. +9. Request: "Change the color of the text to blue in styles.css, add a new button in web page, delete the old background image." +Thought: The request is an incremental development task that requires modifying one or more files. +Response Category: TASK. """ - QUICK_RESPONSE_SYSTEM_PROMPT = """ {role_info} However, you MUST respond to the user message by yourself directly, DON'T ask your team members. @@ -237,11 +235,11 @@ REPORT_TO_HUMAN_PROMPT = """ ## Examlpe example 1: User requirement: create a 2048 game -reply: The development of the 2048 game has been completed. All files (index.html, style.css, and script.js) have been created and reviewed. +Reply: The development of the 2048 game has been completed. All files (index.html, style.css, and script.js) have been created and reviewed. example 2: User requirement: Crawl and extract all the herb names from the website, Tell me the number of herbs. -reply : The herb names have been successfully extracted. A total of 8 herb names were extracted. +Reply : The herb names have been successfully extracted. A total of 8 herb names were extracted. ------------ diff --git a/metagpt/prompts/di/team_leader.py b/metagpt/prompts/di/team_leader.py index 139021ae1..9b0163710 100644 --- a/metagpt/prompts/di/team_leader.py +++ b/metagpt/prompts/di/team_leader.py @@ -36,6 +36,7 @@ Note: 13. Instructions and reply must be in the same language. 14. Default technology stack is HTML (.html), CSS (.css), and Pure JavaScript (.js). Web app is the default option when developing software. 15. You are the only one who decides the programming language for the software, so the instruction must contain the programming language. +16. Data collection and web/software development are two separate tasks. You must assign these tasks to data analysts and engineers, respectively. Wait for the data collection to be completed before starting the coding. """ TL_THOUGHT_GUIDANCE = ( THOUGHT_GUIDANCE diff --git a/metagpt/roles/di/engineer2.py b/metagpt/roles/di/engineer2.py index 3d80e6a26..6352a217f 100644 --- a/metagpt/roles/di/engineer2.py +++ b/metagpt/roles/di/engineer2.py @@ -1,5 +1,6 @@ from __future__ import annotations +import os from pathlib import Path from pydantic import Field @@ -106,26 +107,23 @@ class Engineer2(RoleZero): async def _run_special_command(self, cmd) -> str: """command requiring special check or parsing.""" # finish current task before end. - command_output = "" - if cmd["command_name"] == "end" and not self.planner.plan.is_plan_finished(): - self.planner.plan.finish_all_tasks() - command_output += "All tasks are finished.\n" - command_output += await super()._run_special_command(cmd) + command_output = await super()._run_special_command(cmd) return command_output - async def write_new_code(self, path: str, instruction: str = "Write code for the current file.") -> str: + async def write_new_code(self, path: str, file_description: str = "") -> str: """Write a new code file. Args: path (str): The absolute path of the file to be created. - instruction (optional, str): Further hints or notice other than the current task instruction, must be very concise and can be empty. Defaults to "". + file_description (optional, str): "Brief description and important notes of the file content, must be very concise and can be empty. Defaults to "". """ plan_status, _ = self._get_plan_status() prompt = WRITE_CODE_PROMPT.format( user_requirement=self.planner.plan.goal, - file_path=path, plan_status=plan_status, - instruction=instruction, + file_path=path, + file_description=file_description, + file_name=os.path.basename(path), ) # Sometimes the Engineer repeats the last command to respond. # Replace the last command with a manual prompt to guide the Engineer to write new code. @@ -152,3 +150,8 @@ class Engineer2(RoleZero): else: command_output = await self.terminal.run_command(cmd) return command_output + + async def _end(self): + if not self.planner.plan.is_plan_finished(): + self.planner.plan.finish_all_tasks() + return await super()._end() diff --git a/metagpt/roles/di/role_zero.py b/metagpt/roles/di/role_zero.py index f636ecbc3..7c89a7e66 100644 --- a/metagpt/roles/di/role_zero.py +++ b/metagpt/roles/di/role_zero.py @@ -20,6 +20,7 @@ from metagpt.logs import logger from metagpt.memory.role_zero_memory import RoleZeroLongTermMemory from metagpt.prompts.di.role_zero import ( ASK_HUMAN_COMMAND, + ASK_HUMAN_GUIDANCE_FORMAT, CMD_PROMPT, DETECT_LANGUAGE_PROMPT, END_COMMAND, @@ -31,6 +32,7 @@ from metagpt.prompts.di.role_zero import ( REGENERATE_PROMPT, REPORT_TO_HUMAN_PROMPT, ROLE_INSTRUCTION, + SUMMARY_PROBLEM_WHEN_DUPLICATE, SUMMARY_PROMPT, SYSTEM_PROMPT, ) @@ -74,7 +76,7 @@ class RoleZero(Role): tools: list[str] = [] # Use special symbol [""] to indicate use of all registered tools 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"] + special_tool_commands: list[str] = ["Plan.finish_current_task", "end", "Bash.run", "RoleZero.ask_human"] # List of exclusive tool commands. # If multiple instances of these commands appear, only the first occurrence will be retained. exclusive_tool_commands: list[str] = [ @@ -247,7 +249,6 @@ class RoleZero(Role): 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) - return True @exp_cache(context_builder=RoleZeroContextBuilder(), serializer=RoleZeroSerializer()) @@ -406,7 +407,7 @@ class RoleZero(Role): async def _check_duplicates(self, req: list[dict], command_rsp: str, check_window: int = 10): past_rsp = [mem.content for mem in self.rc.memory.get(check_window)] - if command_rsp in past_rsp: + if command_rsp in past_rsp and '"command_name": "end"' not in command_rsp: # Normal response with thought contents are highly unlikely to reproduce # If an identical response is detected, it is a bad response, mostly due to LLM repeating generated content # In this case, ask human for help and regenerate @@ -415,10 +416,15 @@ class RoleZero(Role): # Hard rule to ask human for help if past_rsp.count(command_rsp) >= 3: if '"command_name": "Plan.finish_current_task",' in command_rsp: - # Detect the deplicate of "Plan.finish_current_task" command, use command "end" to finish the task + # Detect the duplicate of the 'Plan.finish_current_task' command, and use the 'end' command to finish the task. logger.warning(f"Duplicate response detected: {command_rsp}") return END_COMMAND - return ASK_HUMAN_COMMAND + problem = await self.llm.aask( + req + [UserMessage(content=SUMMARY_PROBLEM_WHEN_DUPLICATE.format(language=self.respond_language))] + ) + ASK_HUMAN_COMMAND[0]["args"]["question"] = ASK_HUMAN_GUIDANCE_FORMAT.format(problem=problem).strip() + ask_human_command = "```json\n" + json.dumps(ASK_HUMAN_COMMAND, indent=4, ensure_ascii=False) + "\n```" + return ask_human_command # Try correction by self logger.warning(f"Duplicate response detected: {command_rsp}") regenerate_req = req + [UserMessage(content=REGENERATE_PROMPT)] @@ -526,7 +532,15 @@ class RoleZero(Role): elif cmd["command_name"] == "end": command_output = await self._end() - + elif cmd["command_name"] == "RoleZero.ask_human": + human_response = await self.ask_human(**cmd["args"]) + if "" in human_response: + human_response += "The user has asked me to stop because I have encountered a problem." + self.rc.memory.add(UserMessage(content=human_response, cause_by=RunCommand)) + end_output = "\nCommand end executed:" + end_output += await self._end() + return end_output + return human_response # output from bash.run may be empty, add decorations to the output to ensure visibility. elif cmd["command_name"] == "Bash.run": tool_obj = self.tool_execution_map[cmd["command_name"]] diff --git a/metagpt/tools/libs/editor.py b/metagpt/tools/libs/editor.py index a054d3773..ac92f31b6 100644 --- a/metagpt/tools/libs/editor.py +++ b/metagpt/tools/libs/editor.py @@ -783,8 +783,8 @@ class Editor(BaseModel): lines = content.splitlines(True) total_lines = len(lines) check_list = [ - ("first_replaced", first_replaced_line_number, first_replaced_line_content), - ("last_replaced", last_replaced_line_number, last_replaced_line_content), + ("first", first_replaced_line_number, first_replaced_line_content), + ("last", last_replaced_line_number, last_replaced_line_content), ] for position, line_number, line_content in check_list: if lines[line_number - 1].rstrip() != line_content: @@ -947,7 +947,7 @@ class Editor(BaseModel): Args: file_name: (str): The name of the file to edit. - line_number (int): The line number (starting from 1) to insert the content after.the insert content will be add between the line of line_number-1 and line_number + line_number (int): The line number (starting from 1) to insert the content after. The insert content will be add between the line of line_number-1 and line_number insert_content (str): The content to insert betweed the previous_line_content and current_line_content.The insert_content must be a complete block of code at. NOTE: diff --git a/tests/metagpt/tools/libs/test_editor.py b/tests/metagpt/tools/libs/test_editor.py index f00dca554..81964440a 100644 --- a/tests/metagpt/tools/libs/test_editor.py +++ b/tests/metagpt/tools/libs/test_editor.py @@ -550,9 +550,9 @@ def test_edit_file_by_replace(temp_py_file): MISMATCH_ERROR = """ -Error: The `first_replaced_replaced_line_number` does not match the `first_replaced_replaced_line_content`. Please correct the parameters. -The `first_replaced_replaced_line_number` is 5 and the corresponding content is " b = 2". -But the `first_replaced_replaced_line_content ` is "". +Error: The `first_replaced_line_number` does not match the `first_replaced_line_content`. Please correct the parameters. +The `first_replaced_line_number` is 5 and the corresponding content is " b = 2". +But the `first_replaced_line_content ` is "". The content around the specified line is: The 002 line is "def test_function_for_fm():" The 003 line is " "some docstring"" @@ -561,9 +561,9 @@ The 005 line is " b = 2" The 006 line is " c = 3" The 007 line is " # this is the 7th line" Pay attention to the new content. Ensure that it aligns with the new parameters. -Error: The `last_replaced_replaced_line_number` does not match the `last_replaced_replaced_line_content`. Please correct the parameters. -The `last_replaced_replaced_line_number` is 5 and the corresponding content is " b = 2". -But the `last_replaced_replaced_line_content ` is "". +Error: The `last_replaced_line_number` does not match the `last_replaced_line_content`. Please correct the parameters. +The `last_replaced_line_number` is 5 and the corresponding content is " b = 2". +But the `last_replaced_line_content ` is "". The content around the specified line is: The 002 line is "def test_function_for_fm():" The 003 line is " "some docstring""