diff --git a/examples/write_teaching_plan.py b/examples/write_teaching_plan.py deleted file mode 100644 index 01181dc2b..000000000 --- a/examples/write_teaching_plan.py +++ /dev/null @@ -1,114 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -""" -@Time : 2023-07-27 -@Author : mashenquan -@File : write_teaching_plan.py -@Desc: Write teaching plan demo - ``` - export PYTHONPATH=$PYTHONPATH:$PWD - python examples/write_teaching_plan.py --language=Chinese --teaching_language=English - - ``` -""" - -import asyncio -from pathlib import Path - -import aiofiles -import fire - -from metagpt.actions.write_teaching_plan import TeachingPlanRequirement -from metagpt.config import CONFIG -from metagpt.logs import logger -from metagpt.roles.teacher import Teacher -from metagpt.schema import Message -from metagpt.team import Team - - -async def startup(lesson_file: str, investment: float = 3.0, n_round: int = 1, *args, **kwargs): - """Run a startup. Be a teacher in education industry.""" - - demo_lesson = """ - UNIT 1 Making New Friends - TOPIC 1 Welcome to China! - Section A - - 1a Listen and number the following names. - Jane Mari Kangkang Michael - Look, listen and understand. Then practice the conversation. - Work in groups. Introduce yourself using - I ’m ... Then practice 1a - with your own hometown or the following places. - - 1b Listen and number the following names - Jane Michael Maria Kangkang - 1c Work in groups. Introduce yourself using I ’m ... Then practice 1a with your own hometown or the following places. - China the USA the UK Hong Kong Beijing - - 2a Look, listen and understand. Then practice the conversation - Hello! - Hello! - Hello! - Hello! Are you Maria? - No, I’m not. I’m Jane. - Oh, nice to meet you, Jane - Nice to meet you, too. - Hi, Maria! - Hi, Kangkang! - Welcome to China! - Thanks. - - 2b Work in groups. Make up a conversation with your own name and the - following structures. - A: Hello! / Good morning! / Hi! I’m ... Are you ... ? - B: ... - - 3a Listen, say and trace - Aa Bb Cc Dd Ee Ff Gg - - 3b Listen and number the following letters. Then circle the letters with the same sound as Bb. - Aa Bb Cc Dd Ee Ff Gg - - 3c Match the big letters with the small ones. Then write them on the lines. - """ - CONFIG.set_context(kwargs) - - lesson = "" - if lesson_file and Path(lesson_file).exists(): - async with aiofiles.open(lesson_file, mode="r", encoding="utf-8") as reader: - lesson = await reader.read() - logger.info(f"Course content: {lesson}") - if not lesson: - logger.info("No course content provided, using the demo course.") - lesson = demo_lesson - - company = Team() - company.hire([Teacher(*args, **kwargs)]) - company.invest(investment) - company.env.publish_message(Message(content=lesson, cause_by=TeachingPlanRequirement)) - await company.run(n_round=1) - - -def main(idea: str, investment: float = 3.0, n_round: int = 5, *args, **kwargs): - """ - We are a software startup comprised of AI. By investing in us, you are empowering a future filled with limitless possibilities. - :param idea: lesson filename. - :param investment: As an investor, you have the opportunity to contribute a certain dollar amount to this AI company. - :param n_round: Reserved. - :param args: Parameters passed in format: `python your_script.py arg1 arg2 arg3` - :param kwargs: Parameters passed in format: `python your_script.py --param1=value1 --param2=value2` - :return: - """ - asyncio.run(startup(idea, investment, n_round, *args, **kwargs)) - - -if __name__ == "__main__": - """ - Formats: - ``` - python write_teaching_plan.py lesson_filename --teaching_language= --language= - ``` - If `lesson_filename` is not available, a demo lesson content will be used. - """ - fire.Fire(main) 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..521e59c96 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,56 @@ 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 = """ + UNIT 1 Making New Friends + TOPIC 1 Welcome to China! + Section A + + 1a Listen and number the following names. + Jane Mari Kangkang Michael + Look, listen and understand. Then practice the conversation. + Work in groups. Introduce yourself using + I ’m ... Then practice 1a + with your own hometown or the following places. + + 1b Listen and number the following names + Jane Michael Maria Kangkang + 1c Work in groups. Introduce yourself using I ’m ... Then practice 1a with your own hometown or the following places. + China the USA the UK Hong Kong Beijing + + 2a Look, listen and understand. Then practice the conversation + Hello! + Hello! + Hello! + Hello! Are you Maria? + No, I’m not. I’m Jane. + Oh, nice to meet you, Jane + Nice to meet you, too. + Hi, Maria! + Hi, Kangkang! + Welcome to China! + Thanks. + + 2b Work in groups. Make up a conversation with your own name and the + following structures. + A: Hello! / Good morning! / Hi! I’m ... Are you ... ? + B: ... + + 3a Listen, say and trace + Aa Bb Cc Dd Ee Ff Gg + + 3b Listen and number the following letters. Then circle the letters with the same sound as Bb. + Aa Bb Cc Dd Ee Ff Gg + + 3c Match the big letters with the small ones. Then write them on the lines. + """ + teacher = Teacher() + rsp = await teacher.run(Message(content=lesson)) + assert rsp + + 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"])