diff --git a/metagpt/actions/action_node.py b/metagpt/actions/action_node.py index 092dd5755..4376e09ed 100644 --- a/metagpt/actions/action_node.py +++ b/metagpt/actions/action_node.py @@ -4,6 +4,9 @@ @Time : 2023/12/11 18:45 @Author : alexanderwu @File : action_node.py + +NOTE: You should use typing.List instead of list to do type annotation. Because in the markdown extraction process, + we can use typing to extract the type of the node, but we cannot use built-in list to extract. """ import json from typing import Any, Dict, Generic, List, Optional, Tuple, Type, TypeVar @@ -41,10 +44,10 @@ Fill in the above nodes based on the format example. """ -def dict_to_markdown(d, prefix="-", postfix="\n"): +def dict_to_markdown(d, prefix="##", kv_sep="\n", postfix="\n"): markdown_str = "" for key, value in d.items(): - markdown_str += f"{prefix} {key}: {value}{postfix}" + markdown_str += f"{prefix}{key}{kv_sep}{value}{postfix}" return markdown_str diff --git a/metagpt/actions/design_api_an.py b/metagpt/actions/design_api_an.py index 0a303cdd5..7d6802381 100644 --- a/metagpt/actions/design_api_an.py +++ b/metagpt/actions/design_api_an.py @@ -5,6 +5,8 @@ @Author : alexanderwu @File : design_api_an.py """ +from typing import List + from metagpt.actions.action_node import ActionNode from metagpt.logs import logger from metagpt.utils.mermaid import MMC1, MMC2 @@ -22,7 +24,7 @@ PROJECT_NAME = ActionNode( FILE_LIST = ActionNode( key="File list", - expected_type=list[str], + expected_type=List[str], instruction="Only need relative paths. ALWAYS write a main.py or app.py here", example=["main.py", "game.py"], ) diff --git a/metagpt/actions/project_management_an.py b/metagpt/actions/project_management_an.py index 6208c1051..215a67202 100644 --- a/metagpt/actions/project_management_an.py +++ b/metagpt/actions/project_management_an.py @@ -5,26 +5,28 @@ @Author : alexanderwu @File : project_management_an.py """ +from typing import List + from metagpt.actions.action_node import ActionNode from metagpt.logs import logger REQUIRED_PYTHON_PACKAGES = ActionNode( key="Required Python packages", - expected_type=list[str], + expected_type=List[str], instruction="Provide required Python packages in requirements.txt format.", example=["flask==1.1.2", "bcrypt==3.2.0"], ) REQUIRED_OTHER_LANGUAGE_PACKAGES = ActionNode( key="Required Other language third-party packages", - expected_type=list[str], + expected_type=List[str], instruction="List down the required packages for languages other than Python.", example=["No third-party dependencies required"], ) LOGIC_ANALYSIS = ActionNode( key="Logic Analysis", - expected_type=list[list[str]], + expected_type=List[List[str]], instruction="Provide a list of files with the classes/methods/functions to be implemented, " "including dependency analysis and imports.", example=[ @@ -35,7 +37,7 @@ LOGIC_ANALYSIS = ActionNode( TASK_LIST = ActionNode( key="Task list", - expected_type=list[str], + expected_type=List[str], instruction="Break down the tasks into a list of filenames, prioritized by dependency order.", example=["game.py", "main.py"], ) diff --git a/metagpt/actions/write_prd_an.py b/metagpt/actions/write_prd_an.py index 8698c739f..d58d72f64 100644 --- a/metagpt/actions/write_prd_an.py +++ b/metagpt/actions/write_prd_an.py @@ -5,6 +5,7 @@ @Author : alexanderwu @File : write_prd_an.py """ +from typing import List from metagpt.actions.action_node import ActionNode from metagpt.logs import logger @@ -39,14 +40,14 @@ PROJECT_NAME = ActionNode( PRODUCT_GOALS = ActionNode( key="Product Goals", - expected_type=list[str], + expected_type=List[str], instruction="Provide up to three clear, orthogonal product goals.", example=["Create an engaging user experience", "Improve accessibility, be responsive", "More beautiful UI"], ) USER_STORIES = ActionNode( key="User Stories", - expected_type=list[str], + expected_type=List[str], instruction="Provide up to 3 to 5 scenario-based user stories.", example=[ "As a player, I want to be able to choose difficulty levels", @@ -59,7 +60,7 @@ USER_STORIES = ActionNode( COMPETITIVE_ANALYSIS = ActionNode( key="Competitive Analysis", - expected_type=list[str], + expected_type=List[str], instruction="Provide 5 to 7 competitive products.", example=[ "2048 Game A: Simple interface, lacks responsive features", @@ -98,7 +99,7 @@ REQUIREMENT_ANALYSIS = ActionNode( REQUIREMENT_POOL = ActionNode( key="Requirement Pool", - expected_type=list[list[str]], + expected_type=List[List[str]], instruction="List down the top-5 requirements with their priority (P0, P1, P2).", example=[["P0", "The main code ..."], ["P0", "The game algorithm ..."]], ) diff --git a/metagpt/actions/write_review.py b/metagpt/actions/write_review.py new file mode 100644 index 000000000..13690a1a5 --- /dev/null +++ b/metagpt/actions/write_review.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +@Author : alexanderwu +@File : write_review.py +""" +from typing import List + +from metagpt.actions import Action +from metagpt.actions.action_node import ActionNode + +REVIEW = ActionNode( + key="Review", + expected_type=List[str], + instruction="Act as an experienced Reviewer and review the given output. Ask a series of critical questions, " + "concisely and clearly, to help the writer improve their work.", + example=[ + "This is a good PRD, but I think it can be improved by adding more details.", + ], +) + +LGTM = ActionNode( + key="LGTM", + expected_type=str, + instruction="LGTM/LBTM. If the output is good enough, give a LGTM (Looks Good To Me) to the writer, " + "else LBTM (Looks Bad To Me).", + example="LGTM", +) + +WRITE_REVIEW_NODE = ActionNode.from_children("WRITE_REVIEW_NODE", [REVIEW, LGTM]) + + +class WriteReview(Action): + """This class allows LLM to further mine noteworthy details based on specific "##TOPIC"(discussion topic) and + "##RECORD" (discussion records), thereby deepening the discussion.""" + + async def run(self, context): + return await WRITE_REVIEW_NODE.fill(context=context, llm=self.llm, schema="markdown") diff --git a/metagpt/utils/common.py b/metagpt/utils/common.py index ab7a3d99e..e123e8fd9 100644 --- a/metagpt/utils/common.py +++ b/metagpt/utils/common.py @@ -23,7 +23,7 @@ import traceback import typing from pathlib import Path from typing import Any -from typing import List, Tuple, Union +from typing import List, Tuple, Union, get_args, get_origin import aiofiles import loguru @@ -135,8 +135,31 @@ class OutputParser: parsed_data[block] = content return parsed_data + @staticmethod + def extract_content(text, tag="CONTENT"): + # Use regular expression to extract content between [CONTENT] and [/CONTENT] + extracted_content = re.search(rf"\[{tag}\](.*?)\[/{tag}\]", text, re.DOTALL) + + if extracted_content: + return extracted_content.group(1).strip() + else: + return "No content found between [CONTENT] and [/CONTENT] tags." + + @staticmethod + def is_supported_list_type(i): + origin = get_origin(i) + if origin is not List: + return False + + args = get_args(i) + if args == (str,) or args == (Tuple[str, str],) or args == (List[str],): + return True + + return False + @classmethod def parse_data_with_mapping(cls, data, mapping): + data = cls.extract_content(text=data) block_dict = cls.parse_blocks(data) parsed_data = {} for block, content in block_dict.items(): diff --git a/tests/metagpt/actions/test_write_review.py b/tests/metagpt/actions/test_write_review.py new file mode 100644 index 000000000..2d188b720 --- /dev/null +++ b/tests/metagpt/actions/test_write_review.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +@Time : 2023/12/20 15:01 +@Author : alexanderwu +@File : test_write_review.py +""" +import pytest + +from metagpt.actions.write_review import WriteReview + +CONTEXT = """ +{ + "Language": "zh_cn", + "Programming Language": "Python", + "Original Requirements": "写一个简单的2048", + "Project Name": "game_2048", + "Product Goals": [ + "创建一个引人入胜的用户体验", + "确保高性能", + "提供可定制的功能" + ], + "User Stories": [ + "作为用户,我希望能够选择不同的难度级别", + "作为玩家,我希望在每局游戏结束后能看到我的得分" + ], + "Competitive Analysis": [ + "Python Snake Game: 界面简单,缺乏高级功能" + ], + "Competitive Quadrant Chart": "quadrantChart\n title \"Reach and engagement of campaigns\"\n x-axis \"Low Reach\" --> \"High Reach\"\n y-axis \"Low Engagement\" --> \"High Engagement\"\n quadrant-1 \"我们应该扩展\"\n quadrant-2 \"需要推广\"\n quadrant-3 \"重新评估\"\n quadrant-4 \"可能需要改进\"\n \"Campaign A\": [0.3, 0.6]\n \"Campaign B\": [0.45, 0.23]\n \"Campaign C\": [0.57, 0.69]\n \"Campaign D\": [0.78, 0.34]\n \"Campaign E\": [0.40, 0.34]\n \"Campaign F\": [0.35, 0.78]\n \"Our Target Product\": [0.5, 0.6]", + "Requirement Analysis": "产品应该用户友好。", + "Requirement Pool": [ + [ + "P0", + "主要代码..." + ], + [ + "P0", + "游戏算法..." + ] + ], + "UI Design draft": "基本功能描述,简单的风格和布局。", + "Anything UNCLEAR": "..." +} +""" + + +@pytest.mark.asyncio +async def test_write_review(): + write_review = WriteReview() + review = await write_review.run(CONTEXT) + assert review.instruct_content + assert review.get("LGTM") in ["LGTM", "LBTM"]