diff --git a/metagpt/prompts/di/role_zero.py b/metagpt/prompts/di/role_zero.py index 3c47ebf5f..f85128b38 100644 --- a/metagpt/prompts/di/role_zero.py +++ b/metagpt/prompts/di/role_zero.py @@ -218,3 +218,22 @@ QUICK_RESPONSE_SYSTEM_PROMPT = """ {role_info} However, you MUST respond to the user message by yourself directly, DON'T ask your team members. """ + +REPORT_TO_HUMAN_PROMPT = """ +# Current Plan +{plan_status} + +Your have just finish a task, Use "RoleZero.reply_to_human" to report what you have done. +The output format is : +```json +[ + {{ + "command_name": "RoleZero.reply_to_human", + "args": {{ + "content": "" + }} + }} +] +``` + +""" diff --git a/metagpt/roles/di/role_zero.py b/metagpt/roles/di/role_zero.py index c6cbfdfa8..727360ae9 100644 --- a/metagpt/roles/di/role_zero.py +++ b/metagpt/roles/di/role_zero.py @@ -26,6 +26,7 @@ from metagpt.prompts.di.role_zero import ( QUICK_THINK_PROMPT, QUICK_THINK_SYSTEM_PROMPT, REGENERATE_PROMPT, + REPORT_TO_HUMAN_PROMPT, ROLE_INSTRUCTION, SYSTEM_PROMPT, THOUGHT_GUIDANCE, @@ -86,6 +87,8 @@ class RoleZero(Role): use_fixed_sop: bool = False requirements_constraints: str = "" # the constraints in user requirements + command_history: list[str] = [] + @model_validator(mode="after") def set_plan_and_tool(self) -> "RoleZero": # We force using this parameter for DataAnalyst @@ -234,21 +237,57 @@ class RoleZero(Role): if self.use_fixed_sop: return await super()._act() - commands, ok = await self._parse_commands() + commands, ok = await self._parse_commands(self.command_rsp) if not ok: error_msg = commands + self.rc.memory.add(UserMessage(content=error_msg)) return error_msg logger.info(f"Commands: \n{commands}") outputs = await self._run_commands(commands) logger.info(f"Commands outputs: \n{outputs}") self.rc.memory.add(UserMessage(content=outputs)) + # Report what is done when finishing the task. + current_command_list = [command["command_name"] for command in commands] + self.command_history.extend(current_command_list) + if self.check_whether_report_to_human(): + memory = self.rc.memory.get(self.memory_k) + memory = await self.parse_browser_actions(memory) + memory = self.parse_images(memory) + plan_status, _ = self._get_plan_status() + prompt = REPORT_TO_HUMAN_PROMPT.format(plan_status=plan_status) + req = self.llm.format_msg(memory + [UserMessage(content=prompt)]) + respond_command = await self.llm.aask(msg=req) + commands, ok = await self._parse_commands(respond_command) + if ok and len(commands) == 1 and commands[0]["command_name"] == "RoleZero.reply_to_human": + cmd = commands[0] + report_result = await self.reply_to_human(cmd["args"]) + self.rc.memory.add(AIMessage(content=respond_command)) + self.rc.memory.add(UserMessage(content=report_result)) + self.command_history.append("RoleZero.reply_to_human") + logger.info(f"Commands outputs: \n{report_result}") + outputs += report_result + return AIMessage( content=f"I have finished the task, please mark my task as finished. Outputs: {outputs}", sent_from=self.name, cause_by=RunCommand, ) + def check_whether_report_to_human(self): + """ "Check whether add reply to human command when finish current task""" + interaction_with_human = ["RoleZero.ask_human", "RoleZero.reply_to_human"] + plan_end_action = ["Plan.finish_current_task", "end"] + flag = 0 + for command_name in self.command_history[::-1]: + if command_name in plan_end_action: + flag |= 1 + elif command_name in interaction_with_human: + flag |= 2 + else: + break + return flag == 1 + async def _react(self) -> Message: # NOTE: Diff 1: Each time landing here means news is observed, set todo to allow news processing in _think self._set_state(0) @@ -332,7 +371,7 @@ class RoleZero(Role): command_rsp = await self.llm.aask(regenerate_req) return command_rsp - async def _parse_commands(self) -> Tuple[List[Dict], bool]: + async def _parse_commands(self, command_rsp) -> Tuple[List[Dict], bool]: """Retrieves commands from the Large Language Model (LLM). This function attempts to retrieve a list of commands from the LLM by @@ -344,20 +383,20 @@ class RoleZero(Role): - A boolean flag indicating success (True) or failure (False). """ try: - commands = CodeParser.parse_code(block=None, lang="json", text=self.command_rsp) + commands = CodeParser.parse_code(block=None, lang="json", text=command_rsp) if commands.endswith("]") and not commands.startswith("["): commands = "[" + commands commands = json.loads(repair_llm_raw_output(output=commands, req_keys=[None], repair_type=RepairType.JSON)) except json.JSONDecodeError as e: - logger.warning(f"Failed to parse JSON for: {self.command_rsp}. Trying to repair...") + logger.warning(f"Failed to parse JSON for: {command_rsp}. Trying to repair...") commands = await self.llm.aask( - msg=JSON_REPAIR_PROMPT.format(json_data=self.command_rsp, json_decode_error=str(e)) + msg=JSON_REPAIR_PROMPT.format(json_data=command_rsp, json_decode_error=str(e)) ) try: commands = json.loads(CodeParser.parse_code(block=None, lang="json", text=commands)) except json.JSONDecodeError: # repair escape error of code and math - commands = CodeParser.parse_code(block=None, lang="json", text=self.command_rsp) + commands = CodeParser.parse_code(block=None, lang="json", text=command_rsp) new_command = repair_escape_error(commands) commands = json.loads( repair_llm_raw_output(output=new_command, req_keys=[None], repair_type=RepairType.JSON) @@ -365,8 +404,7 @@ class RoleZero(Role): except Exception as e: tb = traceback.format_exc() print(tb) - error_msg = UserMessage(content=str(e)) - self.rc.memory.add(error_msg) + error_msg = str(e) return error_msg, False # 为了对LLM不按格式生成进行容错 diff --git a/metagpt/roles/di/swe_agent.py b/metagpt/roles/di/swe_agent.py index e1d2c9613..e90fc3045 100644 --- a/metagpt/roles/di/swe_agent.py +++ b/metagpt/roles/di/swe_agent.py @@ -9,6 +9,7 @@ from metagpt.prompts.di.swe_agent import ( NEXT_STEP_TEMPLATE, ) from metagpt.roles.di.role_zero import RoleZero +from metagpt.schema import Message from metagpt.tools.libs.git import git_create_pull from metagpt.tools.libs.terminal import Bash @@ -32,8 +33,6 @@ class SWEAgent(RoleZero): async def _think(self) -> bool: await self._format_instruction() res = await super()._think() - if self.run_eval: - await self._parse_commands_for_eval() return res def _update_tool_execution(self): @@ -55,6 +54,12 @@ class SWEAgent(RoleZero): bash_state = json.loads(state_output) self.cmd_prompt_current_state = CURRENT_BASH_STATE.format(**bash_state).strip() + async def _act(self) -> Message: + message = await super()._act() + if self.run_eval: + self._parse_commands_for_eval() + return message + async def _parse_commands_for_eval(self): """ Handles actions based on parsed commands. @@ -65,23 +70,19 @@ class SWEAgent(RoleZero): This function is specifically added for SWE bench evaluation. """ # only import when evaluation is needed - from metagpt.tools.swe_agent_commands.swe_agent_utils import extract_patch + if not self.rc.todo: + from metagpt.tools.swe_agent_commands.swe_agent_utils import extract_patch - commands, ok = await self._parse_commands() - if not ok: - return - for cmd in commands: - if "end" != cmd.get("command_name", ""): - return - try: - diff_output = await self.terminal.run("git diff --cached") - clear_diff = extract_patch(diff_output) - logger.info(f"Diff output: \n{clear_diff}") - if clear_diff: - self.output_diff = clear_diff + # swe agent have been stop. it means 'end' or other command which can stop the swe agent have been executed + try: + diff_output = await self.terminal.run("git diff --cached") + clear_diff = extract_patch(diff_output) + logger.info(f"Diff output: \n{clear_diff}") + if clear_diff: + self.output_diff = clear_diff - except Exception as e: - logger.error(f"Error during submission: {e}") + except Exception as e: + logger.error(f"Error during submission: {e}") def _retrieve_experience(self) -> str: return MINIMAL_EXAMPLE