diff --git a/metagpt/actions/write_teaching_plan.py b/metagpt/actions/write_teaching_plan.py index 534f5ded9..d889fdbe3 100644 --- a/metagpt/actions/write_teaching_plan.py +++ b/metagpt/actions/write_teaching_plan.py @@ -5,51 +5,42 @@ @Author : mashenquan @File : write_teaching_plan.py """ +from typing import Optional + +from pydantic import Field + from metagpt.actions import Action from metagpt.config import CONFIG +from metagpt.llm import LLM from metagpt.logs import logger -from metagpt.schema import Message - - -class TeachingPlanRequirement(Action): - """Teaching Plan Requirement without any implementation details""" - - async def run(self, *args, **kwargs): - raise NotImplementedError +from metagpt.provider.base_gpt_api import BaseGPTAPI class WriteTeachingPlanPart(Action): """Write Teaching Plan Part""" - def __init__(self, name: str = "", context=None, llm=None, topic: str = "", language: str = "Chinese"): - """ + context: Optional[str] = None + llm: BaseGPTAPI = Field(default_factory=LLM) + topic: str = "" + language: str = "Chinese" + rsp: Optional[str] = None - :param name: action name - :param context: context - :param llm: object of :class:`LLM` - :param topic: topic part of teaching plan - :param language: A human language, such as Chinese, English, French, etc. - """ - super().__init__(name, context, llm) - self.topic = topic - self.language = language - self.rsp = None - - async def run(self, messages, *args, **kwargs): - if len(messages) < 1 or not isinstance(messages[0], Message): - raise ValueError("Invalid args, a tuple of List[Message] is expected") - - statement_patterns = self.TOPIC_STATEMENTS.get(self.topic, []) + async def run(self, with_message=None, **kwargs): + statement_patterns = TeachingPlanBlock.TOPIC_STATEMENTS.get(self.topic, []) statements = [] for p in statement_patterns: - s = format_value(p) + s = self.format_value(p) statements.append(s) - formatter = self.PROMPT_TITLE_TEMPLATE if self.topic == self.COURSE_TITLE else self.PROMPT_TEMPLATE + formatter = ( + TeachingPlanBlock.PROMPT_TITLE_TEMPLATE + if self.topic == TeachingPlanBlock.COURSE_TITLE + else TeachingPlanBlock.PROMPT_TEMPLATE + ) prompt = formatter.format( - formation=self.FORMATION, + formation=TeachingPlanBlock.FORMATION, role=self.prefix, statements="\n".join(statements), - lesson=messages[0].content, + lesson=self.context, topic=self.topic, language=self.language, ) @@ -61,14 +52,14 @@ class WriteTeachingPlanPart(Action): return self.rsp def _set_result(self, rsp): - if self.DATA_BEGIN_TAG in rsp: - ix = rsp.index(self.DATA_BEGIN_TAG) - rsp = rsp[ix + len(self.DATA_BEGIN_TAG) :] - if self.DATA_END_TAG in rsp: - ix = rsp.index(self.DATA_END_TAG) + if TeachingPlanBlock.DATA_BEGIN_TAG in rsp: + ix = rsp.index(TeachingPlanBlock.DATA_BEGIN_TAG) + rsp = rsp[ix + len(TeachingPlanBlock.DATA_BEGIN_TAG) :] + if TeachingPlanBlock.DATA_END_TAG in rsp: + ix = rsp.index(TeachingPlanBlock.DATA_END_TAG) rsp = rsp[0:ix] self.rsp = rsp.strip() - if self.topic != self.COURSE_TITLE: + if self.topic != TeachingPlanBlock.COURSE_TITLE: return if "#" not in self.rsp or self.rsp.index("#") != 0: self.rsp = "# " + self.rsp @@ -99,6 +90,8 @@ class WriteTeachingPlanPart(Action): value = value.replace("{" + f"{k}" + "}", str(v)) return value + +class TeachingPlanBlock: FORMATION = ( '"Capacity and role" defines the role you are currently playing;\n' '\t"[LESSON_BEGIN]" and "[LESSON_END]" tags enclose the content of textbook;\n' diff --git a/metagpt/roles/teacher.py b/metagpt/roles/teacher.py index 031ce94c9..3f70200ea 100644 --- a/metagpt/roles/teacher.py +++ b/metagpt/roles/teacher.py @@ -4,49 +4,54 @@ @Time : 2023/7/27 @Author : mashenquan @File : teacher.py +@Desc : Used by Agent Store @Modified By: mashenquan, 2023/8/22. A definition has been provided for the return value of _think: returning false indicates that further reasoning cannot continue. """ - import re import aiofiles -from metagpt.actions.write_teaching_plan import ( - TeachingPlanRequirement, - WriteTeachingPlanPart, -) +from metagpt.actions import UserRequirement +from metagpt.actions.write_teaching_plan import TeachingPlanBlock, WriteTeachingPlanPart from metagpt.config import CONFIG from metagpt.logs import logger from metagpt.roles import Role from metagpt.schema import Message +from metagpt.utils.common import any_to_str class Teacher(Role): """Support configurable teacher roles, with native and teaching languages being replaceable through configurations.""" - def __init__( - self, - name="Lily", - profile="{teaching_language} Teacher", - goal="writing a {language} teaching plan part by part", - constraints="writing in {language}", - desc="", - *args, - **kwargs, - ): - super().__init__(name=name, profile=profile, goal=goal, constraints=constraints, desc=desc, *args, **kwargs) - actions = [] - for topic in WriteTeachingPlanPart.TOPICS: - act = WriteTeachingPlanPart(topic=topic, llm=self._llm) - actions.append(act) - self._init_actions(actions) - self._watch({TeachingPlanRequirement}) + name: str = "Lily" + profile: str = "{teaching_language} Teacher" + goal: str = "writing a {language} teaching plan part by part" + constraints: str = "writing in {language}" + desc: str = "" + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.name = WriteTeachingPlanPart.format_value(self.name) + self.profile = WriteTeachingPlanPart.format_value(self.profile) + self.goal = WriteTeachingPlanPart.format_value(self.goal) + self.constraints = WriteTeachingPlanPart.format_value(self.constraints) + self.desc = WriteTeachingPlanPart.format_value(self.desc) async def _think(self) -> bool: """Everything will be done part by part.""" + if not self._actions: + if not self._rc.news or self._rc.news[0].cause_by != any_to_str(UserRequirement): + raise ValueError("Lesson content invalid.") + actions = [] + print(TeachingPlanBlock.TOPICS) + for topic in TeachingPlanBlock.TOPICS: + act = WriteTeachingPlanPart(context=self._rc.news[0].content, topic=topic, llm=self._llm) + actions.append(act) + self._init_actions(actions) + if self._rc.todo is None: self._set_state(0) return True @@ -76,7 +81,7 @@ class Teacher(Role): async def save(self, content): """Save teaching plan""" filename = Teacher.new_file_name(self.course_title) - pathname = CONFIG.workspace / "teaching_plan" + pathname = CONFIG.workspace_path / "teaching_plan" pathname.mkdir(exist_ok=True) pathname = pathname / filename try: @@ -100,7 +105,7 @@ class Teacher(Role): """Return course title of teaching plan""" default_title = "teaching_plan" for act in self._actions: - if act.topic != WriteTeachingPlanPart.COURSE_TITLE: + if act.topic != TeachingPlanBlock.COURSE_TITLE: continue if act.rsp is None: return default_title diff --git a/tests/metagpt/roles/test_teacher.py b/tests/metagpt/roles/test_teacher.py index 82d6c7052..0de50983f 100644 --- a/tests/metagpt/roles/test_teacher.py +++ b/tests/metagpt/roles/test_teacher.py @@ -5,15 +5,19 @@ @Author : mashenquan @File : test_teacher.py """ - +import os from typing import Dict, Optional +import pytest from pydantic import BaseModel +from metagpt.config import CONFIG, Config from metagpt.roles.teacher import Teacher +from metagpt.schema import Message -def test_init(): +@pytest.mark.asyncio +async def test_init(): class Inputs(BaseModel): name: str profile: str @@ -28,19 +32,6 @@ def test_init(): expect_desc: str inputs = [ - { - "name": "Lily{language}", - "expect_name": "LilyCN", - "profile": "X {teaching_language}", - "expect_profile": "X EN", - "goal": "Do {something_big}, {language}", - "expect_goal": "Do sleep, CN", - "constraints": "Do in {key1}, {language}", - "expect_constraints": "Do in HaHa, CN", - "kwargs": {"language": "CN", "key1": "HaHa", "something_big": "sleep", "teaching_language": "EN"}, - "desc": "aaa{language}", - "expect_desc": "aaaCN", - }, { "name": "Lily{language}", "expect_name": "Lily{language}", @@ -54,17 +45,37 @@ def test_init(): "desc": "aaa{language}", "expect_desc": "aaa{language}", }, + { + "name": "Lily{language}", + "expect_name": "LilyCN", + "profile": "X {teaching_language}", + "expect_profile": "X EN", + "goal": "Do {something_big}, {language}", + "expect_goal": "Do sleep, CN", + "constraints": "Do in {key1}, {language}", + "expect_constraints": "Do in HaHa, CN", + "kwargs": {"language": "CN", "key1": "HaHa", "something_big": "sleep", "teaching_language": "EN"}, + "desc": "aaa{language}", + "expect_desc": "aaaCN", + }, ] + env = os.environ.copy() for i in inputs: seed = Inputs(**i) + os.environ.clear() + os.environ.update(env) + CONFIG = Config() + CONFIG.set_context(seed.kwargs) + print(CONFIG.options) + assert bool("language" in seed.kwargs) == bool("language" in CONFIG.options) + teacher = Teacher( name=seed.name, profile=seed.profile, goal=seed.goal, constraints=seed.constraints, desc=seed.desc, - **seed.kwargs ) assert teacher.name == seed.expect_name assert teacher.desc == seed.expect_desc @@ -74,7 +85,8 @@ def test_init(): assert teacher.course_title == "teaching_plan" -def test_new_file_name(): +@pytest.mark.asyncio +async def test_new_file_name(): class Inputs(BaseModel): lesson_title: str ext: str @@ -90,6 +102,13 @@ def test_new_file_name(): assert result == seed.expect +@pytest.mark.asyncio +async def test_run(): + CONFIG.set_context({"language": "Chinese", "teaching_language": "English"}) + lesson = "Lesson 1: How to draw a tree. First step, buy a book." + teacher = Teacher() + await teacher.run(Message(content=lesson)) + + if __name__ == "__main__": - test_init() - test_new_file_name() + pytest.main([__file__, "-s"]) diff --git a/tests/metagpt/roles/test_tutorial_assistant.py b/tests/metagpt/roles/test_tutorial_assistant.py index 105f976c3..3158a5fc1 100644 --- a/tests/metagpt/roles/test_tutorial_assistant.py +++ b/tests/metagpt/roles/test_tutorial_assistant.py @@ -5,20 +5,30 @@ @Author : Stitch-z @File : test_tutorial_assistant.py """ -import aiofiles +import shutil + import pytest +from metagpt.const import TUTORIAL_PATH from metagpt.roles.tutorial_assistant import TutorialAssistant @pytest.mark.asyncio @pytest.mark.parametrize(("language", "topic"), [("Chinese", "Write a tutorial about Python")]) async def test_tutorial_assistant(language: str, topic: str): + shutil.rmtree(path=TUTORIAL_PATH, ignore_errors=True) + topic = "Write a tutorial about MySQL" role = TutorialAssistant(language=language) msg = await role.run(topic) - filename = msg.content - title = filename.split("/")[-1].split(".")[0] - async with aiofiles.open(filename, mode="r") as reader: - content = await reader.read() - assert content.startswith(f"# {title}") + assert "MySQL" in msg.content + assert TUTORIAL_PATH.exists() + # filename = msg.content + # title = filename.split("/")[-1].split(".")[0] + # async with aiofiles.open(filename, mode="r") as reader: + # content = await reader.read() + # assert content.startswith(f"# {title}") + + +if __name__ == "__main__": + pytest.main([__file__, "-s"])