diff --git a/metagpt/actions/debug_error.py b/metagpt/actions/debug_error.py index a55f13dad..7a12e18f8 100644 --- a/metagpt/actions/debug_error.py +++ b/metagpt/actions/debug_error.py @@ -5,6 +5,7 @@ @Author : alexanderwu @File : debug_error.py """ +import re from metagpt.actions.action import Action from metagpt.logs import logger @@ -36,7 +37,9 @@ class DebugError(Action): # return fixed_code async def run(self, *args, **kwargs) -> str: - if "PASS" in self.context.output: + pattern = r"Ran (\d+) tests in ([\d.]+)s\n\nOK" + matches = re.search(pattern, self.context.output) + if matches: return "", "the original code works fine, no need to debug" file_name = self.context.code_filename diff --git a/metagpt/actions/run_code.py b/metagpt/actions/run_code.py index f2d323f06..b244577a7 100644 --- a/metagpt/actions/run_code.py +++ b/metagpt/actions/run_code.py @@ -51,8 +51,14 @@ CONTEXT = """ ## Running Command {command} ## Running Output -standard output: {outs}; -standard errors: {errs}; +standard output: +```text +{outs} +``` +standard errors: +```text +{errs} +``` """ @@ -84,10 +90,19 @@ class RunCode(Action): additional_python_paths = ":".join(additional_python_paths) env["PYTHONPATH"] = additional_python_paths + ":" + env.get("PYTHONPATH", "") + install_command = ["python", "-m", "pip", "install", "-r", "requirements.txt"] + logger.info(" ".join(install_command)) + subprocess.run(install_command, check=True, cwd=working_directory, env=env) + + install_pytest_command = ["python", "-m", "pip", "install", "pytest"] + logger.info(" ".join(install_pytest_command)) + subprocess.run(install_pytest_command, check=True, cwd=working_directory, env=env) + # Start the subprocess process = subprocess.Popen( command, cwd=working_directory, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env ) + logger.info(" ".join(command)) try: # Wait for the process to complete, with a timeout @@ -101,7 +116,11 @@ class RunCode(Action): async def run(self, *args, **kwargs) -> str: logger.info(f"Running {' '.join(self.context.command)}") if self.context.mode == "script": - outs, errs = await self.run_script(command=self.context.command, **kwargs) + outs, errs = await self.run_script( + command=self.context.command, + working_directory=self.context.working_directory, + additional_python_paths=self.context.additional_python_paths, + ) elif self.context.mode == "text": outs, errs = await self.run_text(code=self.context.code) diff --git a/metagpt/roles/qa_engineer.py b/metagpt/roles/qa_engineer.py index eac30413a..f950efef4 100644 --- a/metagpt/roles/qa_engineer.py +++ b/metagpt/roles/qa_engineer.py @@ -7,15 +7,13 @@ @Modified By: mashenquan, 2023-11-1. In accordance with Chapter 2.2.1 and 2.2.2 of RFC 116, modify the data type of the `cause_by` value in the `Message` to a string, and utilize the new message filtering feature. """ -import json - from metagpt.actions import DebugError, RunCode, WriteCode, WriteCodeReview, WriteTest from metagpt.config import CONFIG from metagpt.const import MESSAGE_ROUTE_TO_NONE, OUTPUTS_FILE_REPO, TEST_CODES_FILE_REPO from metagpt.logs import logger from metagpt.roles import Role from metagpt.schema import Document, Message, RunCodeContext, TestingContext -from metagpt.utils.common import any_to_str_set +from metagpt.utils.common import any_to_str_set, parse_recipient class QaEngineer(Role): @@ -64,68 +62,76 @@ class QaEngineer(Role): code_filename=context.code_doc.filename, test_filename=context.test_doc.filename, working_directory=str(CONFIG.git_repo.workdir), - additional_python_paths=[CONFIG.src_workspace], + additional_python_paths=[str(CONFIG.src_workspace)], ) - msg = Message( - content=run_code_context.json(), - role=self.profile, - cause_by=WriteTest, - sent_from=self, - send_to=self, + self.publish_message( + Message( + content=run_code_context.json(), + role=self.profile, + cause_by=WriteTest, + sent_from=self, + send_to=self, + ) ) - self.publish_message(msg) logger.info(f"Done {str(tests_file_repo.workdir)} generating.") async def _run_code(self, msg): - m = json.loads(msg.content) - run_code_context = RunCodeContext(**m) - src_file_repo = CONFIG.git_repo.new_file_repository(CONFIG.src_workspace) - src_doc = await src_file_repo.get(run_code_context.code_filename) + run_code_context = RunCodeContext.loads(msg.content) + src_doc = await CONFIG.git_repo.new_file_repository(CONFIG.src_workspace).get(run_code_context.code_filename) if not src_doc: return - test_file_repo = CONFIG.git_repo.new_file_repository(TEST_CODES_FILE_REPO) - test_doc = await test_file_repo.get(run_code_context.test_filename) + test_doc = await CONFIG.git_repo.new_file_repository(TEST_CODES_FILE_REPO).get(run_code_context.test_filename) if not test_doc: return run_code_context.code = src_doc.content run_code_context.test_code = test_doc.content result_msg = await RunCode(context=run_code_context, llm=self._llm).run() - outputs_file_repo = CONFIG.git_repo.new_file_repository(OUTPUTS_FILE_REPO) - run_code_context.output_filename = run_code_context.test_filename + ".log" - await outputs_file_repo.save( + run_code_context.output_filename = run_code_context.test_filename + ".md" + await CONFIG.git_repo.new_file_repository(OUTPUTS_FILE_REPO).save( filename=run_code_context.output_filename, content=result_msg, dependencies={src_doc.root_relative_path, test_doc.root_relative_path}, ) run_code_context.code = None run_code_context.test_code = None - msg = Message( - content=run_code_context.json(), role=self.profile, cause_by=RunCode, sent_from=self, send_to=self + recipient = parse_recipient(result_msg) # the recipient might be Engineer or myself + mappings = { + "Engineer": "Alex", + "QaEngineer": "Edward", + } + self.publish_message( + Message( + content=run_code_context.json(), + role=self.profile, + cause_by=RunCode, + sent_from=self, + send_to=mappings.get(recipient, MESSAGE_ROUTE_TO_NONE), + ) ) - self.publish_message(msg) async def _debug_error(self, msg): - m = json.loads(msg.context) - run_code_context = RunCodeContext(**m) + run_code_context = RunCodeContext.loads(msg.content) output_file_repo = CONFIG.git_repo.new_file_repository(OUTPUTS_FILE_REPO) output_doc = await output_file_repo.get(run_code_context.output_filename) if not output_doc: return run_code_context.output = output_doc.content code = await DebugError(context=run_code_context, llm=self._llm).run() - src_file_repo = CONFIG.git_repo.new_file_repository(CONFIG.src_workspace) - await src_file_repo.save(filename=run_code_context.code_filename, content=code) + await CONFIG.git_repo.new_file_repository(CONFIG.src_workspace).save( + filename=run_code_context.code_filename, content=code + ) run_code_context.output = None run_code_context.output_filename = None - msg = Message( - content=run_code_context.json(), - role=self.profile, - cause_by=DebugError, - sent_from=self, - send_to=self, + self.publish_message( + Message( + content=run_code_context.json(), + role=self.profile, + cause_by=DebugError, + sent_from=self, + send_to=self, + ) ) - self.publish_message(msg) async def _act(self) -> Message: if self.test_round > self.test_round_allowed: @@ -154,11 +160,10 @@ class QaEngineer(Role): # I ran my test code, time to fix bugs, if any await self._debug_error(msg) self.test_round += 1 - result_msg = Message( + return Message( content=f"Round {self.test_round} of tests done", role=self.profile, cause_by=WriteTest, sent_from=self.profile, send_to=MESSAGE_ROUTE_TO_NONE, ) - return result_msg diff --git a/metagpt/schema.py b/metagpt/schema.py index 5cc7cdb2d..53a22f0e6 100644 --- a/metagpt/schema.py +++ b/metagpt/schema.py @@ -253,12 +253,28 @@ class CodingContext(BaseModel): task_doc: Document code_doc: Document + @staticmethod + def loads(val: str) -> CodingContext | None: + try: + m = json.loads(val) + return CodingContext(**m) + except Exception: + return None + class TestingContext(BaseModel): filename: str code_doc: Document test_doc: Document + @staticmethod + def loads(val: str) -> TestingContext | None: + try: + m = json.loads(val) + return TestingContext(**m) + except Exception: + return None + class RunCodeContext(BaseModel): mode: str = "script" @@ -271,3 +287,11 @@ class RunCodeContext(BaseModel): additional_python_paths: List[str] = Field(default_factory=list) output_filename: Optional[str] output: Optional[str] + + @staticmethod + def loads(val: str) -> RunCodeContext | None: + try: + m = json.loads(val) + return RunCodeContext(**m) + except Exception: + return None diff --git a/metagpt/utils/common.py b/metagpt/utils/common.py index 798acf214..9002a8dfb 100644 --- a/metagpt/utils/common.py +++ b/metagpt/utils/common.py @@ -304,7 +304,13 @@ def print_members(module, indent=0): def parse_recipient(text): pattern = r"## Send To:\s*([A-Za-z]+)\s*?" # hard code for now recipient = re.search(pattern, text) - return recipient.group(1) if recipient else "" + if recipient: + return recipient.group(1) + pattern = r"Send To:\s*([A-Za-z]+)\s*?" + recipient = re.search(pattern, text) + if recipient: + return recipient.group(1) + return "" def get_class_name(cls) -> str: diff --git a/metagpt/utils/file_repository.py b/metagpt/utils/file_repository.py index 62ba99d42..8de4bdf5b 100644 --- a/metagpt/utils/file_repository.py +++ b/metagpt/utils/file_repository.py @@ -96,8 +96,15 @@ class FileRepository: path_name = self.workdir / filename if not path_name.exists(): return None - async with aiofiles.open(str(path_name), mode="r") as reader: - doc.content = await reader.read() + try: + async with aiofiles.open(str(path_name), mode="r") as reader: + doc.content = await reader.read() + except FileNotFoundError as e: + logger.info(f"open {str(path_name)} failed:{e}") + return None + except Exception as e: + logger.info(f"open {str(path_name)} failed:{e}") + return None return doc async def get_all(self) -> List[Document]: