update_engineer_and_editorprompt

This commit is contained in:
黄伟韬 2024-09-05 16:48:46 +08:00
parent 5dcde29d73
commit c2f9f010b7
10 changed files with 251 additions and 219 deletions

View file

@ -1,18 +1,14 @@
from __future__ import annotations
import os
from pathlib import Path
from pydantic import Field
from metagpt.config2 import Config
from metagpt.logs import logger
# from metagpt.actions.write_code_review import ValidateAndRewriteCode
from metagpt.prompts.di.engineer2 import (
CURRENT_EDITOR_STATE,
CURRENT_TERMINAL_STATE,
ENGINEER2_CMD_PROMPT,
CURRENT_STATE,
ENGINEER2_INSTRUCTION,
WRITE_CODE_PROMPT,
WRITE_CODE_SYSTEM_PROMPT,
@ -33,7 +29,6 @@ class Engineer2(RoleZero):
profile: str = "Engineer"
goal: str = "Take on game, app, and web development."
instruction: str = ENGINEER2_INSTRUCTION
cmd_prompt: str = ENGINEER2_CMD_PROMPT
terminal: Terminal = Field(default_factory=Terminal, exclude=True)
tools: list[str] = [
@ -58,52 +53,38 @@ class Engineer2(RoleZero):
async def _format_instruction(self):
"""
Formats the instruction message for the Engineer2.
Uses Editor's state to format the `_instruction` template.
Display the current terminal and editor state.
This information will be dynamically added to the command prompt.
"""
bash_working_dir = await self.terminal.run_command("pwd")
bash_state = {"working_dir": bash_working_dir}
editor_state = {"open_file": self.editor.current_file, "working_dir": self.editor.working_dir}
self.cmd_prompt_current_state = CURRENT_EDITOR_STATE.format(
**editor_state
).strip() + CURRENT_TERMINAL_STATE.format(**bash_state)
state = {
"editor_open_file": self.editor.current_file,
"editor_current_directory": self.editor.working_dir,
"terminal_current_directory": await self.terminal.run_command("pwd"),
}
self.cmd_prompt_current_state = CURRENT_STATE.format(**state).strip()
def _update_tool_execution(self):
self.tool_execution_map.update(
{
"Terminal.run_command": self.eval_terminal_run if self.run_eval else self.terminal.run_command,
"Terminal.run_command": self.terminal.run_command,
"git_create_pull": git_create_pull,
"Engineer2.write_new_code": self.write_new_code,
# "ValidateAndRewriteCode.run": validate.run,
# "ValidateAndRewriteCode": validate.run,
}
)
self.exclusive_tool_commands.append("Engineer2.write_new_code")
if self.run_eval:
self.tool_execution_map.update(
{
"Terminal.run_command": self._eval_terminal_run,
"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."""
if any([cmd_key_word in cmd for cmd_key_word in ["pull", "push", "commit"]]):
# The Engineer2 attempts to submit the repository after fixing the bug, thereby reaching the end of the fixing process.
# Set self.rc.todo to None to stop the engineer and then will trigger _save_git_diff funcion to save difference.
logger.info("Engineer2 use cmd:{cmd}")
logger.info("Current test case is finished.")
# stop the Engineer2
self._set_state(-1)
command_output = "Current test case is finished."
else:
command_output = await self.terminal.run_command(cmd)
return command_output
async def _act(self) -> Message:
message = await super()._act()
if self.run_eval:
await self._save_git_diff()
return message
def _retrieve_experience(self) -> str:
@ -119,44 +100,6 @@ class Engineer2(RoleZero):
command_output += await super()._run_special_command(cmd)
return command_output
async def _save_git_diff(self):
"""
Handles actions based on parsed commands.
When detecting engineer2 at the final action round, the process will stop immediately.
generates a patch using `git diff`.
Stores the cleaned patch in `output_diff`. Logs any exceptions.
This function is specifically added for SWE bench evaluation.
"""
# If todo switches to None, it indicates that this is the final round of reactions, and the Engineer2 will stop. Use git diff to store any changes made.
if not self.rc.todo:
from metagpt.tools.swe_agent_commands.swe_agent_utils import extract_patch
try:
logger.info(await self.submit())
diff_output = await self.terminal.run_command("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}")
async def submit(self):
if "SWE_CMD_WORK_DIR" not in os.environ:
os.environ["SWE_CMD_WORK_DIR"] = str(Config.default().workspace.path)
if os.path.exists(os.environ["SWE_CMD_WORK_DIR"] + "/test.patch"):
await self.terminal.run_command('git apply -R < "$SWE_CMD_WORK_DIR/test.patch"')
cmd = """
git add -A
echo "<<SUBMISSION START||"
git diff --cached
echo "||SUBMISSION DONE>>"
"""
diff_output = await self.terminal.run_command(cmd)
return diff_output
async def write_new_code(self, path: str, instruction: str = "") -> str:
"""Write a new code file.
Args:
@ -180,3 +123,14 @@ class Engineer2(RoleZero):
# TODO: Consider adding line no to be ready for editing.
return f"The file {path} has been successfully created, with content:\n{code}"
async def _eval_terminal_run(self, cmd):
"""change command pull/push/commit to end."""
if any([cmd_key_word in cmd for cmd_key_word in ["pull", "push", "commit"]]):
# The Engineer2 attempts to submit the repository after fixing the bug, thereby reaching the end of the fixing process.
logger.info("Engineer2 use cmd:{cmd}\nCurrent test case is finished.")
# Set self.rc.todo to None to stop the engineer.
self._set_state(-1)
else:
command_output = await self.terminal.run_command(cmd)
return command_output

View file

@ -73,6 +73,7 @@ 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"]
# List of exclusive tool commands
exclusive_tool_commands: list[str] = [
"Editor.edit_file_by_replace",
"Editor.insert_content_at_line",
@ -420,11 +421,13 @@ class RoleZero(Role):
# 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.
# Keep only the first exclusive command
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]
commands = [
cmd
for index, cmd in enumerate(commands)
if index == index_of_first_exclusive or cmd["command_name"] not in self.exclusive_tool_commands
]
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
@ -436,14 +439,14 @@ class RoleZero(Role):
for cmd in commands:
output = f"Command {cmd['command_name']} executed"
# handle special command first
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 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:
if inspect.iscoroutinefunction(tool_obj):
tool_output = await tool_obj(**cmd["args"])
else:
@ -451,14 +454,14 @@ class RoleZero(Role):
if tool_output:
output += f": {str(tool_output)}"
outputs.append(output)
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
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
outputs = "\n\n".join(outputs)
return outputs
@ -536,11 +539,8 @@ class RoleZero(Role):
from metagpt.environment.mgx.mgx_env import MGXEnv # avoid circular import
if not isinstance(self.rc.env, MGXEnv):
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
return "Not in MGXEnv, command will not be executed."
return await self.rc.env.reply_to_human(content, sent_from=self)
async def _end(self, **kwarg):
self._set_state(-1)