diff --git a/metagpt/actions/action_node.py b/metagpt/actions/action_node.py index 092dd5755..58688aefa 100644 --- a/metagpt/actions/action_node.py +++ b/metagpt/actions/action_node.py @@ -41,10 +41,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/write_review.py b/metagpt/actions/write_review.py new file mode 100644 index 000000000..94dd9951b --- /dev/null +++ b/metagpt/actions/write_review.py @@ -0,0 +1,40 @@ +#!/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 + +# from metagpt.llm import LLM + +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="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 fa18694e3..a290c7db7 100644 --- a/metagpt/utils/common.py +++ b/metagpt/utils/common.py @@ -18,7 +18,7 @@ import os import platform import re import typing -from typing import List, Tuple, Union +from typing import List, Tuple, Union, get_args, get_origin import aiofiles import loguru @@ -129,8 +129,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"]