Merge pull request #620 from iorisa/fixbug/role/teacher

Fixbug: 修复AgentStore使用的Teacher角色
This commit is contained in:
geekan 2023-12-25 16:41:50 +08:00 committed by GitHub
commit 4d831d9e47
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 155 additions and 199 deletions

View file

@ -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 <lesson filename> --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, Im not. Im 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! Im ... 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=<the language you are teaching> --language=<your native language>
```
If `lesson_filename` is not available, a demo lesson content will be used.
"""
fire.Fire(main)

View file

@ -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'

View file

@ -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

View file

@ -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, Im not. Im 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! Im ... 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"])

View file

@ -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"])