feat: + education industry

This commit is contained in:
莘权 马 2023-07-28 11:42:13 +08:00
parent 8cc8b80e49
commit 9d1a261bf6
9 changed files with 498 additions and 17 deletions

3
.gitignore vendored
View file

@ -163,3 +163,6 @@ workspace/*
*.mmd
tmp
output.wav
# output folder
output

View file

@ -0,0 +1,134 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
@Time : 2023/5/11 14:43
@Author : mashenquan
@File : write_teaching_plan.py
"""
from langchain.llms.base import LLM
from metagpt.logs import logger
from metagpt.actions import Action
from metagpt.schema import Message
class TeachingPlanRequirement(Action):
"""Teaching Plan Requirement without any implementation details"""
async def run(self, *args, **kwargs):
raise NotImplementedError
class WriteTeachingPlanPart(Action):
"""Write Teaching Plan Part"""
def __init__(self, name: str = '', context=None, llm: LLM = None, topic="", language="Chinese"):
"""
Args:
name: action name
context: context
llm: object of :class:`LLM`
topic: topic part of teaching plan
"""
super().__init__(name, context, llm)
self.topic = topic
self.language = language
self.rsp = None
async def run(self, *args, **kwargs):
if len(args) < 1 or len(args[0]) < 1 or not isinstance(args[0][0], Message):
raise ValueError("Invalid args, a tuple of List[Message] is expected")
statements = self.TOPIC_STATEMENTS.get(self.topic, [])
formatter = self.PROMPT_TITLE_TEMPLATE if self.topic == self.COURSE_TITLE else self.PROMPT_TEMPLATE
prompt = formatter.format(formation=self.FORMATION,
role=self.prefix,
statements="\n".join(statements),
lesson=args[0][0].content,
topic=self.topic,
language=self.language)
logger.debug(prompt)
rsp = await self._aask(prompt=prompt)
logger.debug(rsp)
self._set_result(rsp)
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)
rsp = rsp[0:ix]
self.rsp = rsp.strip()
if self.topic != self.COURSE_TITLE:
return
if '#' not in self.rsp or self.rsp.index('#') != 0:
self.rsp = "# " + self.rsp
def __str__(self):
"""str()时返回`topic`"""
return self.topic
def __repr__(self):
"""调试时返回`topic`"""
return self.topic
FORMATION = """
"\tCapacity and role" defines the role you are currently playing;
"\t[LESSON_BEGIN]" and "[LESSON_END]" tags enclose the content of textbook;
"\tStatement" defines the work detail you need to complete at this stage;
"\tAnswer options" defines the format requirements for your responses;
"\tConstraint" defines the conditions that your responses must comply with.
"""
COURSE_TITLE = "Title"
TOPICS = [COURSE_TITLE, "Teaching Hours", "Teaching Objectives", "Teaching Content",
"Teaching Methods and Strategies", "Learning Activities",
"Teaching Time Allocation", "Assessment and Feedback", "Teaching Summary and Improvement"]
TOPIC_STATEMENTS = {
COURSE_TITLE: ["Statement: Find and return the title of the lesson only in markdown first-level header format, "
"without anything else."],
"Teaching Content": [
"Statement: \"Teaching Content\" must include vocabulary, analysis, and examples of various grammar "
"structures that appear in the textbook, as well as the listening materials and key points.",
"Statement: \"Teaching Content\" must include more examples."],
"Teaching Time Allocation": [
"Statement: \"Teaching Time Allocation\" must include how much time is allocated to each "
"part of the textbook content."],
"Teaching Methods and Strategies": [
"Statement: \"Teaching Methods and Strategies\" must include teaching focus, difficulties, materials, "
"procedures, in detail."
]
}
# Teaching plan title
PROMPT_TITLE_TEMPLATE = """
Do not refer to the context of the previous conversation records, start the conversation anew.\n\n
Formation: {formation}\n\n
{statements}\n
Constraint: Writing in {language}.\n
Answer options: Encloses the lesson title with "[TEACHING_PLAN_BEGIN]" and "[TEACHING_PLAN_END]" tags.\n
[LESSON_BEGIN]\n
{lesson}\n
[LESSON_END]
"""
# Teaching plan parts:
PROMPT_TEMPLATE = """
Do not refer to the context of the previous conversation records, start the conversation anew.\n\n
Formation: {formation}\n\n
Capacity and role: {role}\n
Statement: Write the "{topic}" part of teaching plan, WITHOUT ANY content unrelated to "{topic}"!!\n
{statements}\n
Answer options: Enclose the teaching plan content with "[TEACHING_PLAN_BEGIN]" and "[TEACHING_PLAN_END]" tags.\n
Answer options: Using proper markdown format from second-level header format.\n
Constraint: Writing in {language}.\n
[LESSON_BEGIN]\n
{lesson}\n
[LESSON_END]
"""
DATA_BEGIN_TAG = "[TEACHING_PLAN_BEGIN]"
DATA_END_TAG = "[TEACHING_PLAN_END]"

View file

@ -4,12 +4,13 @@
@Time : 2023/5/5 23:08
@Author : alexanderwu
@File : openai.py
@Modified By: mashenquan, 2023-07-27, + try except.
"""
import asyncio
import time
from functools import wraps
from typing import NamedTuple
import traceback
import openai
from metagpt.config import CONFIG
@ -30,7 +31,9 @@ def retry(max_retries):
for i in range(max_retries):
try:
return await f(*args, **kwargs)
except Exception:
except Exception as e:
error_str = traceback.format_exc()
logger.warning(f"Exception occurred: {str(e)}, stack:{error_str}. Retrying...")
if i == max_retries - 1:
raise
await asyncio.sleep(2 ** i)
@ -148,10 +151,15 @@ class OpenAIGPTAPI(BaseGPTAPI, RateLimiter):
self.rpm = int(config.get("RPM", 10))
async def _achat_completion_stream(self, messages: list[dict]) -> str:
response = await openai.ChatCompletion.acreate(
**self._cons_kwargs(messages),
stream=True
)
try:
response = await openai.ChatCompletion.acreate(
**self._cons_kwargs(messages),
stream=True
)
except Exception as e:
error_str = traceback.format_exc()
logger.error(f"Exception:{e}, stack:{error_str}")
raise e
# create variables to collect the stream of chunks
collected_chunks = []

View file

@ -4,9 +4,11 @@
@Time : 2023/5/11 14:42
@Author : alexanderwu
@File : role.py
@Modified By: mashenquan, 2023-07-27, :class:`Role` + properties.
"""
from __future__ import annotations
import traceback
from typing import Iterable, Type
from pydantic import BaseModel, Field
@ -92,13 +94,22 @@ class RoleContext(BaseModel):
class Role:
"""角色/代理"""
def __init__(self, name="", profile="", goal="", constraints="", desc=""):
def __init__(self, name="", profile="", goal="", constraints="", desc="", *args, **kwargs):
# Enable parameter configurability
name = Role.format_value(name, kwargs)
profile = Role.format_value(profile, kwargs)
goal = Role.format_value(goal, kwargs)
constraints = Role.format_value(constraints, kwargs)
desc = Role.format_value(desc, kwargs)
# Initialize
self._llm = LLM()
self._setting = RoleSetting(name=name, profile=profile, goal=goal, constraints=constraints, desc=desc)
self._states = []
self._actions = []
self._role_id = str(self._setting)
self._rc = RoleContext()
self._options = Role.supply_options(kwargs)
def _reset(self):
self._states = []
@ -136,6 +147,26 @@ class Role:
"""获取角色描述(职位)"""
return self._setting.profile
@property
def name(self):
"""Return role `name`, read only"""
return self._setting.name
@property
def desc(self):
"""Return role `desc`, read only"""
return self._setting.desc
@property
def goal(self):
"""Return role `goal`, read only"""
return self._setting.goal
@property
def constraints(self):
"""Return role `constraints`, read only"""
return self._setting.constraints
def _get_prefix(self):
"""获取角色前缀"""
if self._setting.desc:
@ -164,7 +195,8 @@ class Role:
# history=self.history)
logger.info(f"{self._setting}: ready to {self._rc.todo}")
response = await self._rc.todo.run(self._rc.important_memory)
requirement = self._rc.important_memory
response = await self._rc.todo.run(requirement)
# logger.info(response)
if isinstance(response, ActionOutput):
msg = Message(content=response.content, instruct_content=response.instruct_content,
@ -238,3 +270,33 @@ class Role:
# 将回复发布到环境,等待下一个订阅者处理
self._publish_message(rsp)
return rsp
@staticmethod
def supply_options(options):
"""Supply missing options"""
ret = Role.__DEFAULT_OPTIONS__.copy()
if not options:
return ret
ret.update(options)
return ret
@staticmethod
def format_value(value, options):
"""Fill parameters inside `value` with `options`.
"""
if "{" not in value:
return value
options = Role.supply_options(options)
try:
return value.format(**options)
except KeyError as e:
logger.warning(f"Parameter is missing:{e}")
for k, v in options.items():
value = value.replace("{" + f"{k}" + "}", v)
return value
__DEFAULT_OPTIONS__ = {
"teaching_language": "English",
"language": "Chinese"
}

96
metagpt/roles/teacher.py Normal file
View file

@ -0,0 +1,96 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
@Time : 2023/5/23 17:25
@Author : mashenquan
@File : teacher.py
"""
from pathlib import Path
import aiofiles
from metagpt.actions.write_teaching_plan import WriteTeachingPlanPart, TeachingPlanRequirement
from metagpt.roles import Role
from metagpt.schema import Message
from metagpt.logs import logger
import re
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})
async def _think(self) -> None:
"""Everything will be done part by part."""
if self._rc.todo is None:
self._set_state(0)
return
if self._rc.state + 1 < len(self._states):
self._set_state(self._rc.state + 1)
else:
self._rc.todo = None
async def _react(self) -> Message:
ret = Message(content="")
while True:
await self._think()
if self._rc.todo is None:
break
logger.debug(f"{self._setting}: {self._rc.state=}, will do {self._rc.todo}")
msg = await self._act()
if ret.content != '':
ret.content += "\n\n\n"
ret.content += msg.content
logger.info(ret.content)
await self.save(ret.content)
return ret
async def save(self, content):
"""Save teaching plan"""
filename = Teacher.new_file_name(self.course_title)
pathname = Path(__file__).resolve().parent.parent.parent / "output"
pathname.mkdir(exist_ok=True)
pathname = pathname / filename
try:
async with aiofiles.open(str(pathname), mode='w', encoding='utf-8') as writer:
await writer.write(content)
except Exception as e:
logger.error(f'Save failed{e}')
logger.info(f"Save to:{pathname}")
@staticmethod
def new_file_name(lesson_title, ext=".md"):
"""Create a related file name based on `lesson_title` and `ext`."""
# 定义需要替换的特殊字符
illegal_chars = r'[#@$%!*&\\/:*?"<>|\n\t \']'
# 将特殊字符替换为下划线
filename = re.sub(illegal_chars, '_', lesson_title) + ext
return re.sub(r'_+', '_', filename)
@property
def course_title(self):
"""Return course title of teaching plan"""
default_title = "teaching_plan"
for act in self._actions:
if act.topic != WriteTeachingPlanPart.COURSE_TITLE:
continue
if act.rsp is None:
return default_title
title = act.rsp.lstrip("# \n")
if '\n' in title:
ix = title.index('\n')
title = title[0: ix]
return title
return default_title

View file

@ -4,6 +4,7 @@
@Time : 2023/5/12 00:30
@Author : alexanderwu
@File : software_company.py
@Modified By: mashenquan, 2023-07-27, Add `role` & `cause_by` parameters to `start_project()`.
"""
from pydantic import BaseModel, Field
@ -42,10 +43,10 @@ class SoftwareCompany(BaseModel):
if CONFIG.total_cost > CONFIG.max_budget:
raise NoMoneyException(CONFIG.total_cost, f'Insufficient funds: {CONFIG.max_budget}')
def start_project(self, idea):
def start_project(self, idea, role="BOSS", cause_by=BossRequirement):
"""Start a project from publishing boss requirement."""
self.idea = idea
self.environment.publish_message(Message(role="BOSS", content=idea, cause_by=BossRequirement))
self.environment.publish_message(Message(role=role, content=idea, cause_by=cause_by))
def _save(self):
logger.info(self.json())

View file

@ -35,3 +35,4 @@ tqdm==4.64.0
anthropic==0.3.6
typing-inspect==0.8.0
typing_extensions==4.5.0
aiofiles

View file

@ -1,15 +1,23 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
@Modified By: mashenquan, 2023-07-27, +industry concept
"""
import asyncio
from pathlib import Path
import aiofiles
import fire
from metagpt.logs import logger
from metagpt.actions.write_teaching_plan import TeachingPlanRequirement
from metagpt.roles import Architect, Engineer, ProductManager, ProjectManager
from metagpt.roles.teacher import Teacher
from metagpt.software_company import SoftwareCompany
async def startup(idea: str, investment: float = 3.0, n_round: int = 5, code_review: bool = False):
"""Run a startup. Be a boss."""
async def software_startup(investment: float = 3.0, n_round: int = 5, code_review: bool = False, *args, **kwargs):
"""Run a startup. Be a boss in software industry."""
idea = kwargs['idea'] # Your innovative idea, such as "Creating a snake game."
company = SoftwareCompany()
company.hire([ProductManager(),
Architect(),
@ -20,16 +28,90 @@ async def startup(idea: str, investment: float = 3.0, n_round: int = 5, code_rev
await company.run(n_round=n_round)
def main(idea: str, investment: float = 3.0, n_round: int = 5, code_review: bool = False):
async def education_startup(investment: float = 3.0, n_round: int = 5, code_review: bool = False, *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 withthe 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.
"""
lesson = ""
lesson_file = kwargs.get('lesson_file')
if lesson_file is not None 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 = SoftwareCompany()
company.hire([Teacher(*args, **kwargs)])
company.invest(investment)
company.start_project(lesson, role="Teacher", cause_by=TeachingPlanRequirement)
await company.run(n_round=1)
def main(investment: float = 3.0, n_round: int = 5, code_review: bool = False, *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: Your innovative idea, such as "Creating a snake game."
:param investment: As an investor, you have the opportunity to contribute a certain dollar amount to this AI company.
:param n_round:
:param code_review: Whether to use code review.
:param args: Parameters passed in format: `python your_script.py arg1 arg2 arg3`
:param kwargs: Parameters passed in format: `python your_script.py a--param1=value1 --param2=value2`
:return:
"""
asyncio.run(startup(idea, investment, n_round, code_review))
industry = kwargs.get("industry", "software")
industries = {
"software": software_startup,
"education": education_startup,
}
startup = industries.get(industry)
if startup is None:
print(f"Available industries:{list(industries.keys())}")
return
asyncio.run(startup(investment, n_round, code_review, *args, **kwargs))
if __name__ == '__main__':

View file

@ -0,0 +1,94 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
@Time : 2023/5/23 17:25
@Author : mashenquan
@File : teacher.py
"""
from typing import Dict, Optional
from pydantic import BaseModel
from metagpt.roles.teacher import Teacher
def test_init():
class Inputs(BaseModel):
name: str
profile: str
goal: str
constraints: str
desc: str
options: Optional[Dict] = None
expect_name: str
expect_profile: str
expect_goal: str
expect_constraints: str
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",
"options": {"language": "CN", "key1": "HaHa", "something_big": "sleep", "teaching_language": "EN"},
"desc": "aaa{language}",
"expect_desc": "aaaCN"
},
{
"name": "Lily{language}",
"expect_name": "LilyChinese",
"profile": "X {teaching_language}",
"expect_profile": "X English",
"goal": "Do {something_big}, {language}",
"expect_goal": "Do {something_big}, Chinese",
"constraints": "Do in {key1}, {language}",
"expect_constraints": "Do in {key1}, Chinese",
"desc": "aaa{language}",
"expect_desc": "aaaChinese"
},
]
for i in inputs:
seed = Inputs(**i)
teacher = Teacher(name=seed.name, profile=seed.profile, goal=seed.goal, constraints=seed.constraints,
desc=seed.desc, options=seed.options)
assert teacher.name == seed.expect_name
assert teacher.desc == seed.expect_desc
assert teacher.profile == seed.expect_profile
assert teacher.goal == seed.expect_goal
assert teacher.constraints == seed.expect_constraints
assert teacher.course_title == "teaching_plan"
def test_new_file_name():
class Inputs(BaseModel):
lesson_title: str
ext: str
expect: str
inputs = [
{
"lesson_title": "# @344\n12",
"ext": ".md",
"expect": "_344_12.md"
},
{
"lesson_title": "1#@$%!*&\\/:*?\"<>|\n\t \'1",
"ext": ".cc",
"expect": "1_1.cc"
}
]
for i in inputs:
seed = Inputs(**i)
result = Teacher.new_file_name(seed.lesson_title, seed.ext)
assert result == seed.expect
if __name__ == '__main__':
test_init()