diff --git a/metagpt/actions/design_api.py b/metagpt/actions/design_api.py index b0a6a2861..981dde53f 100644 --- a/metagpt/actions/design_api.py +++ b/metagpt/actions/design_api.py @@ -26,7 +26,11 @@ from metagpt.actions.design_api_an import ( REFINED_DESIGN_NODE, REFINED_PROGRAM_CALL_FLOW, ) -from metagpt.const import DATA_API_DESIGN_FILE_REPO, SEQ_FLOW_FILE_REPO +from metagpt.const import ( + DATA_API_DESIGN_FILE_REPO, + DEFAULT_WORKSPACE_ROOT, + SEQ_FLOW_FILE_REPO, +) from metagpt.logs import logger from metagpt.schema import AIMessage, Document, Documents, Message from metagpt.tools.tool_registry import register_tool @@ -64,7 +68,7 @@ class WriteDesign(Action): prd_filename: str = "", legacy_design_filename: str = "", extra_info: str = "", - output_path: str = "", + output_pathname: str = "", **kwargs, ) -> AIMessage: """ @@ -75,7 +79,7 @@ class WriteDesign(Action): prd_filename (str, optional): The filename of the Product Requirement Document (PRD). legacy_design_filename (str, optional): The filename of the legacy design document. extra_info (str, optional): Additional information to be included in the system design. - output_path (str, optional): The output path where the system design should be saved. + output_pathname (str, optional): The output path name of file that the system design should be saved to. Returns: AIMessage: An AIMessage object containing the system design. @@ -87,7 +91,7 @@ class WriteDesign(Action): >>> action = WriteDesign() >>> result = await action.run(user_requirement=user_requirement, extra_info=extra_info) >>> print(result.content) - The design is balabala... + System Design filename: "/path/to/design/filename" # Modify an exists system design. >>> user_requirement = "Your user requirements" @@ -96,7 +100,7 @@ class WriteDesign(Action): >>> action = WriteDesign() >>> result = await action.run(user_requirement=user_requirement, extra_info=extra_info, legacy_design_filename=legacy_design_filename) >>> print(result.content) - The design is balabala... + System Design filename: "/path/to/design/filename" # Write a new system design with the given PRD(Product Requirement Document). >>> user_requirement = "Your user requirements" @@ -105,7 +109,7 @@ class WriteDesign(Action): >>> action = WriteDesign() >>> result = await action.run(user_requirement=user_requirement, extra_info=extra_info, prd_filename=prd_filename) >>> print(result.content) - The design is balabala... + System Design filename: "/path/to/design/filename" # Modify an exists system design with the given PRD(Product Requirement Document). >>> user_requirement = "Your user requirements" @@ -115,45 +119,45 @@ class WriteDesign(Action): >>> action = WriteDesign() >>> result = await action.run(user_requirement=user_requirement, extra_info=extra_info, legacy_design_filename=legacy_design_filename, prd_filename=prd_filename) >>> print(result.content) - The design is balabala... + TSystem Design filename: "/path/to/design/filename" - # Write a new system design and save to the directory. + # Write a new system design and save to the path name. >>> user_requirement = "Your user requirements" >>> extra_info = "Your extra information" - >>> output_path = "/path/to/save/" + >>> output_pathname = "/path/to/design/filename" >>> action = WriteDesign() - >>> result = await action.run(user_requirement=user_requirement, extra_info=extra_info, output_path=output_path) + >>> result = await action.run(user_requirement=user_requirement, extra_info=extra_info, output_pathname=output_pathname) >>> print(result.content) System Design filename: "/path/to/design/filename" - # Modify an exists system design and save to the directory. + # Modify an exists system design and save to the path name. >>> user_requirement = "Your user requirements" >>> extra_info = "Your extra information" >>> legacy_design_filename = "/path/to/exists/design/filename" - >>> output_path = "/path/to/save/" + >>> output_pathname = "/path/to/design/filename" >>> action = WriteDesign() - >>> result = await action.run(user_requirement=user_requirement, extra_info=extra_info, legacy_design_filename=legacy_design_filename) + >>> result = await action.run(user_requirement=user_requirement, extra_info=extra_info, legacy_design_filename=legacy_design_filename, output_pathname=output_pathname) >>> print(result.content) System Design filename: "/path/to/design/filename" - # Write a new system design with the given PRD(Product Requirement Document) and save to the directory. + # Write a new system design with the given PRD(Product Requirement Document) and save to the path name. >>> user_requirement = "Your user requirements" >>> extra_info = "Your extra information" >>> prd_filename = "/path/to/prd/filename" - >>> output_path = "/path/to/save/" + >>> output_pathname = "/path/to/design/filename" >>> action = WriteDesign() - >>> result = await action.run(user_requirement=user_requirement, extra_info=extra_info, prd_filename=prd_filename) + >>> result = await action.run(user_requirement=user_requirement, extra_info=extra_info, prd_filename=prd_filename, output_pathname=output_pathname) >>> print(result.content) System Design filename: "/path/to/design/filename" - # Modify an exists system design with the given PRD(Product Requirement Document) and save to the directory. + # Modify an exists system design with the given PRD(Product Requirement Document) and save to the path name. >>> user_requirement = "Your user requirements" >>> extra_info = "Your extra information" >>> prd_filename = "/path/to/prd/filename" >>> legacy_design_filename = "/path/to/exists/design/filename" - >>> output_path = "/path/to/save/" + >>> output_pathname = "/path/to/design/filename" >>> action = WriteDesign() - >>> result = await action.run(user_requirement=user_requirement, extra_info=extra_info, legacy_design_filename=legacy_design_filename, prd_filename=prd_filename) + >>> result = await action.run(user_requirement=user_requirement, extra_info=extra_info, legacy_design_filename=legacy_design_filename, prd_filename=prd_filename, output_pathname=output_pathname) >>> print(result.content) System Design filename: "/path/to/design/filename" """ @@ -163,7 +167,7 @@ class WriteDesign(Action): prd_filename=prd_filename, legacy_design_filename=legacy_design_filename, extra_info=extra_info, - output_path=output_path, + output_pathname=output_pathname, ) self.input_args = with_messages[-1].instruct_content @@ -268,14 +272,16 @@ class WriteDesign(Action): prd_filename: str = "", legacy_design_filename: str = "", extra_info: str = "", - output_path: str = "", + output_pathname: str = "", ) -> AIMessage: - context = to_markdown_code_block(user_requirement) - if extra_info: - context = to_markdown_code_block(extra_info) + prd_content = "" if prd_filename: prd_content = await aread(filename=prd_filename) - context += to_markdown_code_block(prd_content) + context = "### User Requirements\n{user_requirement}\n### Extra_info\n{extra_info}\n### PRD\n{prd}\n".format( + user_requirement=to_markdown_code_block(user_requirement), + extra_info=to_markdown_code_block(extra_info), + prd=to_markdown_code_block(prd_content), + ) if not legacy_design_filename: node = await self._new_system_design(context=context) design = Document(content=node.instruct_content.model_dump_json()) @@ -285,13 +291,14 @@ class WriteDesign(Action): prd_doc=Document(content=context), system_design_doc=Document(content=old_design_content) ) - if not output_path: - return AIMessage(content=design.content) - output_filename = Path(output_path) / f"{uuid.uuid4().hex}.json" - await awrite(filename=output_filename, data=design.content) - kvs = {"changed_system_design_filenames": [output_filename]} + if not output_pathname: + output_path = DEFAULT_WORKSPACE_ROOT + output_path.mkdir(parents=True, exist_ok=True) + output_pathname = Path(output_path) / f"{uuid.uuid4().hex}.json" + await awrite(filename=output_pathname, data=design.content) + kvs = {"changed_system_design_filenames": [output_pathname]} return AIMessage( - content=f'System Design filename: "{str(output_filename)}"', + content=f'System Design filename: "{str(output_pathname)}"', instruct_content=AIMessage.create_instruct_value(kvs=kvs), ) diff --git a/metagpt/actions/write_prd.py b/metagpt/actions/write_prd.py index 58db97c75..5ed45cab4 100644 --- a/metagpt/actions/write_prd.py +++ b/metagpt/actions/write_prd.py @@ -35,6 +35,7 @@ from metagpt.actions.write_prd_an import ( from metagpt.const import ( BUGFIX_FILENAME, COMPETITIVE_ANALYSIS_FILE_REPO, + DEFAULT_WORKSPACE_ROOT, REQUIREMENT_FILENAME, ) from metagpt.logs import logger @@ -82,7 +83,7 @@ class WritePRD(Action): with_messages: List[Message] = None, *, user_requirement: str = "", - output_path: str = "", + output_pathname: str = "", legacy_prd_filename: str = "", extra_info: str = "", **kwargs, @@ -92,7 +93,7 @@ class WritePRD(Action): Args: user_requirement (str): A string detailing the user's requirements. - output_path (str, optional): The file path where the output document should be saved. Defaults to "". + output_pathname (str, optional): The path name of file that the output document should be saved to. Defaults to "". legacy_prd_filename (str, optional): The file path of the legacy Product Requirement Document to use as a reference. Defaults to "". extra_info (str, optional): Additional information to include in the document. Defaults to "". **kwargs: Additional keyword arguments. @@ -107,7 +108,7 @@ class WritePRD(Action): >>> write_prd = WritePRD() >>> result = await write_prd.run(user_requirement=user_requirement, extra_info=extra_info) >>> print(result.content) - The PRD is about balabala... + PRD filename: "/path/to/prd/directory/213434ad.json" # Modify a exists PRD(Product Requirement Document) >>> user_requirement = "YOUR REQUIREMENTS" @@ -116,24 +117,24 @@ class WritePRD(Action): >>> write_prd = WritePRD() >>> result = await write_prd.run(user_requirement=user_requirement, extra_info=extra_info, legacy_prd_filename=legacy_prd_filename) >>> print(result.content) - The PRD is about balabala... + PRD filename: "/path/to/prd/directory/213434ad.json" - # Write and save a new PRD(Product Requirement Document) to the directory. + # Write and save a new PRD(Product Requirement Document) to the path name. >>> user_requirement = "YOUR REQUIREMENTS" >>> extra_info = "YOUR EXTRA INFO" - >>> output_path = "/path/to/prd/directory/" + >>> output_pathname = "/path/to/prd/directory/213434ad.json" >>> write_prd = WritePRD() - >>> result = await write_prd.run(user_requirement=user_requirement, extra_info=extra_info, output_path=output_path) + >>> result = await write_prd.run(user_requirement=user_requirement, extra_info=extra_info, output_pathname=output_pathname) >>> print(result.content) PRD filename: "/path/to/prd/directory/213434ad.json" - # Modify a exists PRD(Product Requirement Document) and save to the directory. + # Modify a exists PRD(Product Requirement Document) and save to the path name. >>> user_requirement = "YOUR REQUIREMENTS" >>> extra_info = "YOUR EXTRA INFO" >>> legacy_prd_filename = "/path/to/exists/prd_filename" - >>> output_path = "/path/to/prd/directory/" + >>> output_pathname = "/path/to/prd/directory/213434ad.json" >>> write_prd = WritePRD() - >>> result = await write_prd.run(user_requirement=user_requirement, extra_info=extra_info, legacy_prd_filename=legacy_prd_filename, output_path=output_path) + >>> result = await write_prd.run(user_requirement=user_requirement, extra_info=extra_info, legacy_prd_filename=legacy_prd_filename, output_pathname=output_pathname) >>> print(result.content) PRD filename: "/path/to/prd/directory/213434ad.json" @@ -141,7 +142,7 @@ class WritePRD(Action): if not with_messages: return await self._execute_api( user_requirement=user_requirement, - output_path=output_path, + output_pathname=output_pathname, legacy_prd_filename=legacy_prd_filename, extra_info=extra_info, ) @@ -306,12 +307,12 @@ class WritePRD(Action): self.repo.git_repo.rename_root(self.project_name) async def _execute_api( - self, user_requirement: str, output_path: str, legacy_prd_filename: str, extra_info: str + self, user_requirement: str, output_pathname: str, legacy_prd_filename: str, extra_info: str ) -> AIMessage: - content = to_markdown_code_block(val=user_requirement, type_="text") - if extra_info: - content += to_markdown_code_block(val=extra_info) - + content = "#### User Requirements\n{user_requirement}\n#### Extra Info\n{extra_info}\n".format( + user_requirement=to_markdown_code_block(val=user_requirement), + extra_info=to_markdown_code_block(val=extra_info), + ) req = Document(content=content) if not legacy_prd_filename: node = await self._new_prd(requirement=req.content) @@ -321,10 +322,10 @@ class WritePRD(Action): old_prd = Document(content=content) new_prd = await self._merge(req=req, related_doc=old_prd) - if not output_path: - return AIMessage(content=new_prd.content) - - output_filename = Path(output_path) / f"{uuid.uuid4().hex}.json" - await awrite(filename=output_filename, data=new_prd.content) - kvs = AIMessage.create_instruct_value({"changed_prd_filenames": [str(output_filename)]}) - return AIMessage(content=f'PRD filename: "{str(output_filename)}"', instruct_content=kvs) + if not output_pathname: + output_path = DEFAULT_WORKSPACE_ROOT + output_path.mkdir(parents=True, exist_ok=True) + output_pathname = Path(output_path) / f"{uuid.uuid4().hex}.json" + await awrite(filename=output_pathname, data=new_prd.content) + kvs = AIMessage.create_instruct_value({"changed_prd_filenames": [str(output_pathname)]}) + return AIMessage(content=f'PRD filename: "{str(output_pathname)}"', instruct_content=kvs) diff --git a/metagpt/tools/libs/browser.py b/metagpt/tools/libs/browser.py index 8d6daec11..955058ea0 100644 --- a/metagpt/tools/libs/browser.py +++ b/metagpt/tools/libs/browser.py @@ -1,11 +1,13 @@ from __future__ import annotations + import contextlib +from uuid import uuid4 from playwright.async_api import async_playwright -from metagpt.utils.file import MemoryFileSystem -from uuid import uuid4 + from metagpt.const import DEFAULT_WORKSPACE_ROOT from metagpt.tools.tool_registry import register_tool +from metagpt.utils.file import MemoryFileSystem from metagpt.utils.parse_html import simplify_html from metagpt.utils.report import BrowserReporter @@ -64,7 +66,6 @@ class Browser: # Since RAG is an optional optimization, if it fails, the simplified HTML can be used as a fallback. with contextlib.suppress(Exception): - from metagpt.rag.engines import SimpleEngine # avoid circular import # TODO make `from_docs` asynchronous diff --git a/metagpt/utils/common.py b/metagpt/utils/common.py index 7303d1f47..28bfe623e 100644 --- a/metagpt/utils/common.py +++ b/metagpt/utils/common.py @@ -945,5 +945,7 @@ def get_markdown_code_block_type(filename: str) -> str: def to_markdown_code_block(val: str, type_: str = "") -> str: + if not val: + return val or "" val = val.replace("```", "\\`\\`\\`") return f"\n```{type_}\n{val}\n```\n" diff --git a/metagpt/utils/file.py b/metagpt/utils/file.py index a8ed482d9..8861f65dc 100644 --- a/metagpt/utils/file.py +++ b/metagpt/utils/file.py @@ -72,7 +72,6 @@ class File: class MemoryFileSystem(_MemoryFileSystem): - @classmethod def _strip_protocol(cls, path): return super()._strip_protocol(str(path)) diff --git a/metagpt/utils/parse_html.py b/metagpt/utils/parse_html.py index 3aac8ca6c..1ed3a620c 100644 --- a/metagpt/utils/parse_html.py +++ b/metagpt/utils/parse_html.py @@ -4,11 +4,10 @@ from __future__ import annotations from typing import Generator, Optional from urllib.parse import urljoin, urlparse +import htmlmin from bs4 import BeautifulSoup from pydantic import BaseModel, PrivateAttr -import htmlmin - class WebPage(BaseModel): inner_text: str diff --git a/tests/metagpt/actions/test_design_api.py b/tests/metagpt/actions/test_design_api.py index 0a792fb15..3398be5e6 100644 --- a/tests/metagpt/actions/test_design_api.py +++ b/tests/metagpt/actions/test_design_api.py @@ -6,12 +6,12 @@ @File : test_design_api.py @Modifiled By: mashenquan, 2023-12-6. According to RFC 135 """ -import json +from pathlib import Path import pytest from metagpt.actions.design_api import WriteDesign -from metagpt.const import METAGPT_ROOT +from metagpt.const import DEFAULT_WORKSPACE_ROOT, METAGPT_ROOT from metagpt.logs import logger from metagpt.schema import AIMessage, Message from metagpt.utils.project_repo import ProjectRepo @@ -74,8 +74,7 @@ async def test_design_api(context, user_requirement, prd_filename, legacy_design ) assert isinstance(result, AIMessage) assert result.content - m = json.loads(result.content) - assert m + assert str(DEFAULT_WORKSPACE_ROOT) in result.content @pytest.mark.parametrize( @@ -97,7 +96,7 @@ async def test_design_api_dir(context, user_requirement, prd_filename, legacy_de user_requirement=user_requirement, prd_filename=prd_filename, legacy_design_filename=legacy_design_filename, - output_path=context.config.project_path, + output_pathname=str(Path(context.config.project_path) / "1.txt"), ) assert isinstance(result, AIMessage) assert result.content diff --git a/tests/metagpt/actions/test_write_prd.py b/tests/metagpt/actions/test_write_prd.py index 93a1b150c..6cf1da1dc 100644 --- a/tests/metagpt/actions/test_write_prd.py +++ b/tests/metagpt/actions/test_write_prd.py @@ -6,12 +6,13 @@ @File : test_write_prd.py @Modified By: mashenquan, 2023-11-1. According to Chapter 2.2.1 and 2.2.2 of RFC 116, replace `handle` with `run`. """ -import json +import uuid +from pathlib import Path import pytest from metagpt.actions import UserRequirement, WritePRD -from metagpt.const import REQUIREMENT_FILENAME +from metagpt.const import DEFAULT_WORKSPACE_ROOT, REQUIREMENT_FILENAME from metagpt.logs import logger from metagpt.roles.product_manager import ProductManager from metagpt.roles.role import RoleReactMode @@ -80,10 +81,12 @@ async def test_write_prd_api(context): result = await action.run(user_requirement="write a snake game.") assert isinstance(result, AIMessage) assert result.content - m = json.loads(result.content) - assert m + assert str(DEFAULT_WORKSPACE_ROOT) in result.content - result = await action.run(user_requirement="write a snake game.", output_path=str(context.config.project_path)) + result = await action.run( + user_requirement="write a snake game.", + output_pathname=str(Path(context.config.project_path) / f"{uuid.uuid4().hex}.json"), + ) assert isinstance(result, AIMessage) assert result.content assert result.instruct_content @@ -94,12 +97,11 @@ async def test_write_prd_api(context): result = await action.run(user_requirement="Add moving enemy.", legacy_prd_filename=legacy_prd_filename) assert isinstance(result, AIMessage) assert result.content - m = json.loads(result.content) - assert m + assert str(DEFAULT_WORKSPACE_ROOT) in result.content result = await action.run( user_requirement="Add moving enemy.", - output_path=str(context.config.project_path), + output_pathname=str(Path(context.config.project_path) / f"{uuid.uuid4().hex}.json"), legacy_prd_filename=legacy_prd_filename, ) assert isinstance(result, AIMessage)