Hugging Muti Agent

0x01 前期环境准备

相关参考文档:

Python3.9 及以上环境

$ python --version
Python 3.10.13

安装 MetaGPT

$ pip install -i https://pypi.tuna.tsinghua.edu.cn/simple metagpt
$ pip list | grep metagpt
metagpt                      0.7.3

配置 MetaGPT

$ metagpt --init-config
$ vim ~/.metagpt/config2.yaml
llm:
  api_type: 'zhipuai'
  api_key: '从https://open.bigmodel.cn/usercenter/apikeys获取的api-key'
  model: 'glm-4'

测试示例

# main.py
import asyncio

from metagpt.actions import Action
from metagpt.environment import Environment
from metagpt.roles import Role
from metagpt.team import Team

action1 = Action(name="AlexSay", instruction="Express your opinion with emotion and don't repeat it")
action2 = Action(name="BobSay", instruction="Express your opinion with emotion and don't repeat it")
alex = Role(name="Alex", profile="Democratic candidate", goal="Win the election", actions=[action1], watch=[action2])
bob = Role(name="Bob", profile="Republican candidate", goal="Win the election", actions=[action2], watch=[action1])
env = Environment(desc="US election live broadcast")
team = Team(investment=10.0, env=env, roles=[alex, bob])

asyncio.run(team.run(idea="Topic: climate change. Under 80 words per message.", send_to="Alex", n_round=5))

0x02 智能体综述及多智能体框架

2.1 AI Agent 介绍

个人理解,AI Agent 是一种通过各种途径输入信息,并把这些信息进一步分解和规划,最后将结果依次馈入 LLM 大模型。或者说是具有多种不同模式的 LLM 聚合体,

https://e2b.dev/ai-agents

2.2 多智能体框架介绍

MetaGPT 是一种先进的元编程技术,它利用了大规模语言模型(LLM)和多智能体系统来模拟软件工程团队中的协作过程。通过编码标准操作程序(SOP)为提示,MetaGPT能够复制一个软件研发团队的结构和协作方式,以自动化和优化软件开发流程。旨在通过整合人类的程序知识来提高软件的鲁棒性,减少错误,并为复杂任务设计高效的软件解决方案。MetaGPT能够扮演不同的角色,如产品经理、架构师、项目经理和工程师,从而覆盖软件开发过程中的多个方面。只需要一个简单的需求输入,MetaGPT 就能够生成用户故事、竞品分析、需求文档、数据结构、API设计以及其他相关文档。它通过模拟不同角色的职责,高效地组织工作流程,并能够在给定任务的环境下主动观察和检索相关信息。

0x03 单智能体开发

3.1 RoleContext

v0.7.3 https://github.com/geekan/MetaGPT/blob/v0.7.3/metagpt/roles/role.py#L88

class RoleContext(BaseModel):
    """Role Runtime Context"""

    model_config = ConfigDict(arbitrary_types_allowed=True)

    # # env exclude=True to avoid `RecursionError: maximum recursion depth exceeded in comparison`
    env: "Environment" = Field(default=None, exclude=True)  # # avoid circular import
    # TODO judge if ser&deser
    msg_buffer: MessageQueue = Field(
        default_factory=MessageQueue, exclude=True
    )  # Message Buffer with Asynchronous Updates
    memory: Memory = Field(default_factory=Memory)
    # long_term_memory: LongTermMemory = Field(default_factory=LongTermMemory)
    working_memory: Memory = Field(default_factory=Memory)
    state: int = Field(default=-1)  # -1 indicates initial or termination state where todo is None
    todo: Action = Field(default=None, exclude=True)
    watch: set[str] = Field(default_factory=set)
    news: list[Type[Message]] = Field(default=[], exclude=True)  # TODO not used
    react_mode: RoleReactMode = (
        RoleReactMode.REACT
    )  # see `Role._set_react_mode` for definitions of the following two attributes
    max_react_loop: int = 1

    def check(self, role_id: str):
        # if hasattr(CONFIG, "enable_longterm_memory") and CONFIG.enable_longterm_memory:
        #     self.long_term_memory.recover_memory(role_id, self)
        #     self.memory = self.long_term_memory  # use memory to act as long_term_memory for unify operation
        pass

    @property
    def important_memory(self) -> list[Message]:
        """Retrieve information corresponding to the attention action."""
        return self.memory.get_by_actions(self.watch)

    @property
    def history(self) -> list[Message]:
        return self.memory.get()

    @classmethod
    def model_rebuild(cls, **kwargs):
        from metagpt.environment.base_env import Environment  # noqa: F401

        super().model_rebuild(**kwargs)

env:Environment 对象,当在 Environment 添加 Role 时会同时设置 Role 对 Environment 的引用。
msg_buffer:一个 MessageQueue 对象,该对象是对 asyncio 的 Queue 进行简单封装,主要是提供了非阻塞的 pop / push 方法。Role 通过该对象与环境中的其他 Role 进行信息交互。
memory:记忆对象。当 Role 执行 _act 时,会将执行得到的响应转换为 Message 对象放入 memory 中。另外当 Role 执行 _observe 时,会把 msg_buffer 的所有消息转移到 memory 中。
state:记录 Role 的执行状态。初始状态值为 -1,当全部 Action 执行完成之后也会被重置为 -1。
todo:下一个待执行的 Action。当 state >= 0 时会指向最后一个 Action。
watch:用 str 表示的当前 Role 观察的 Action 列表,目前用在 _observe 获取 news 时进行消息过滤。
news:存储那些在本次执行 _observe 时读取到的与当前 Role 上下游相关的消息。
react_mode:ReAct 循环的模式,目前支持 REACT、BY_ORDER、PLAN_AND_ACT 3种模式,默认使用 REACT 模式。在 _set_react_mode 方法中有相关说明。简单来说,BY_ORDER 模式按照指定的 Action 顺序执行。PLAN_AND_ACT 则为一次思考后执行多个动作,即 _think -> _act -> act -> ...,而 REACT 模式按照 ReAct 论文中的思考——行动循环来执行,即 _think -> _act -> _think -> _act -> ...。
max_react_loop:在 react_mode 为 REACT 模式时生效,用于设置最大的思考-循环次数,超过后会停止 _react 执行。

class Role(SerializationMixin, ContextMixin, BaseModel):
    """Role/Agent"""

    model_config = ConfigDict(arbitrary_types_allowed=True, extra="allow")

    name: str = ""
    profile: str = ""
    goal: str = ""
    constraints: str = ""
    desc: str = ""
    is_human: bool = False

    role_id: str = ""
    states: list[str] = []

    # scenarios to set action system_prompt:
    #   1. `__init__` while using Role(actions=[...])
    #   2. add action to role while using `role.set_action(action)`
    #   3. set_todo while using `role.set_todo(action)`
    #   4. when role.system_prompt is being updated (e.g. by `role.system_prompt = "..."`)
    # Additional, if llm is not set, we will use role's llm
    actions: list[SerializeAsAny[Action]] = Field(default=[], validate_default=True)
    rc: RoleContext = Field(default_factory=RoleContext)
    addresses: set[str] = set()
    planner: Planner = Field(default_factory=Planner)

    # builtin variables
    recovered: bool = False  # to tag if a recovered role
    latest_observed_msg: Optional[Message] = None  # record the latest observed message when interrupted

    __hash__ = object.__hash__  # support Role as hashable type in `Environment.members`
    
    # ......

3.2 最简单的Agent

要自己实现一个最简单的Role,只需要重写Role基类的 _init_ 与 _act 方法。在 _init_ 方法中,我们需要声明 Agent 的name(名称)profile(类型)
我们使用 self._init_action 函数为其配备期望的动作 SimpleWriteCode 这个Action 应该能根据我们的需求生成我们期望的代码在 _act方法中,我们需要编写智能体具体的行动逻辑,智能体将从最新的记忆中获取人类指令,运行配备的动作,MetaGPT将其作为待办事项 (self.rc.todo) 在幕后处理,最后返回一个完整的消息。

实现 SimpleCoder

import asyncio
from metagpt.actions import Action
from metagpt.logs import logger
from metagpt.roles.role import Role
from metagpt.schema import Message


# 编写SimpleWriteCode动作
class SimpleWriteCode(Action):
    PROMPT_TEMPLATE: str = """
    Write a python function that can {instruction} and provide two runnnable test cases.
    Return ```python your_code_here ``` with NO other texts,
    your code:
    """

    name: str = "SimpleWriteCode"

    async def run(self, instruction: str):
        prompt = self.PROMPT_TEMPLATE.format(instruction=instruction)
        rsp = await self._aask(prompt)
        return rsp


# 设计SimpleCoder角色
class SimpleCoder(Role):
    name: str = "Alice"
    profile: str = "SimpleCoder"

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.set_actions([SimpleWriteCode])

    async def _act(self) -> Message:
        logger.info(f"{self._setting}: to do {self.rc.todo}({self.rc.todo.name})")
        todo = self.rc.todo  # todo will be SimpleWriteCode()
        msg = self.get_memories(k=1)[0]  # find the most recent messages
        code_text = await todo.run(msg.content)
        msg = Message(content=code_text, role=self.profile, cause_by=type(todo))
        # self.rc.memory.add(msg)
        return msg


async def main():
    msg="write a function that calculates the product of a list and run it"
    role = SimpleCoder()
    logger.info(msg)
    result = await role.run(msg)
    logger.info(result)


if __name__ == "__main__":
    asyncio.run(main())

3.3 实现一个多动作Agent

import asyncio
import re
import subprocess
from metagpt.actions import Action
from metagpt.logs import logger
from metagpt.roles.role import Role, RoleReactMode
from metagpt.schema import Message


class SimpleWriteCode(Action):
    PROMPT_TEMPLATE: str = """
    Write a python function that can {instruction} and provide two runnnable test cases.
    Return ```python your_code_here ``` with NO other texts,
    your code:
    """

    name: str = "SimpleWriteCode"

    async def run(self, instruction: str):
        prompt = self.PROMPT_TEMPLATE.format(instruction=instruction)

        rsp = await self._aask(prompt)

        code_text = SimpleWriteCode.parse_code(rsp)

        return code_text

    @staticmethod
    def parse_code(rsp):
        pattern = r"```python(.*)```"
        match = re.search(pattern, rsp, re.DOTALL)
        code_text = match.group(1) if match else rsp
        return code_text


class SimpleRunCode(Action):
    name: str = "SimpleRunCode"

    async def run(self, code_text: str):
        result = subprocess.run(["python3", "-c", code_text], capture_output=True, text=True)
        code_result = result.stdout
        logger.info(f"{code_result=}")
        return code_result


class SimpleCoder(Role):
    name: str = "Alice"
    profile: str = "SimpleCoder"

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.set_actions([SimpleWriteCode])

    async def _act(self) -> Message:
        logger.info(f"{self._setting}: to do {self.rc.todo}({self.rc.todo.name})")
        todo = self.rc.todo  # todo will be SimpleWriteCode()

        msg = self.get_memories(k=1)[0]  # find the most recent messages
        code_text = await todo.run(msg.content)
        msg = Message(content=code_text, role=self.profile, cause_by=type(todo))

        return msg


class RunnableCoder(Role):
    name: str = "Alice"
    profile: str = "RunnableCoder"

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.set_actions([SimpleWriteCode, SimpleRunCode])
        self._set_react_mode(react_mode=RoleReactMode.BY_ORDER.value)

    async def _act(self) -> Message:
        logger.info(f"{self._setting}: to do {self.rc.todo}({self.rc.todo.name})")
        # By choosing the Action by order under the hood
        # todo will be first SimpleWriteCode() then SimpleRunCode()
        todo = self.rc.todo

        msg = self.get_memories(k=1)[0]  # find the most k recent messages
        result = await todo.run(msg.content)

        msg = Message(content=result, role=self.profile, cause_by=type(todo))
        self.rc.memory.add(msg)
        return msg


# 运行SimpleCoder角色
async def main():
    msg = "write a function that calculates the product of a list and run it"
    role = RunnableCoder()
    logger.info(msg)
    result = await role.run(msg)
    logger.info(result)


if __name__ == "__main__":
    asyncio.run(main())

3.4 实现一个更复杂的Agent:技术文档助手(TutorialAssistant)

因为 token 限制的原因,我们先通过 LLM 大模型生成教程的目录(WriteDirectory),再对目录按照二级标题进行分块,对于每块目录按照标题生成详细内容(WriteContent),最后再将标题和内容进行拼接,解决 LLM 大模型长文本的限制问题。

3.4.1 编写 WriteDirectory 动作

# 编写WriteDirectory动作
class WriteDirectory(Action):

    name: str = "WriteDirectory"
    language: str = "chinese"

    COMMON_PROMPT = """
    You are now a seasoned technical professional in the field of the internet. 
    We need you to write a technical tutorial with the topic "{topic}".
    您现在是互联网领域的经验丰富的技术专业人员。
    我们需要您撰写一个关于"{topic}"的技术教程。
    """
    DIRECTORY_PROMPT = COMMON_PROMPT + """
    Please provide the specific table of contents for this tutorial, strictly following the following requirements:
    1. The output must be strictly in the specified language, {language}.
    2. Answer strictly in the dictionary format like {{"title": "xxx", "directory": [{{"dir 1": ["sub dir 1", "sub dir 2"]}}, {{"dir 2": ["sub dir 3", "sub dir 4"]}}]}}.
    3. The directory should be as specific and sufficient as possible, with a primary and secondary directory.The secondary directory is in the array.
    4. Do not have extra spaces or line breaks.
    5. Each directory title has practical significance.
    请按照以下要求提供本教程的具体目录:
    1. 输出必须严格符合指定语言,{language}。
    2. 回答必须严格按照字典格式,如{{"title": "xxx", "directory": [{{"dir 1": ["sub dir 1", "sub dir 2"]}}, {{"dir 2": ["sub dir 3", "sub dir 4"]}}]}}。
    3. 目录应尽可能具体和充分,包括一级和二级目录。二级目录在数组中。
    4. 不要有额外的空格或换行符。
    5. 每个目录标题都具有实际意义。
    """

    async def run(self, topic: str):
        prompt = self.DIRECTORY_PROMPT.format(topic=topic, language=language)
        rsp = await self._aask(prompt)
        return rsp

3.4.2 编写WriteContent动作

# 编写WriteContent动作
class WriteContent(Action):

    name: str = "WriteContent"
    language: str = "chinese"
    directory: dict = {}

    COMMON_PROMPT = """
    You are now a seasoned technical professional in the field of the internet. 
    We need you to write a technical tutorial with the topic "{topic}".
    """

    CONTENT_PROMPT = COMMON_PROMPT + """
    Now I will give you the module directory titles for the topic. 
    Please output the detailed principle content of this title in detail. 
    If there are code examples, please provide them according to standard code specifications. 
    Without a code example, it is not necessary.

    The module directory titles for the topic is as follows:
    {directory}

    Strictly limit output according to the following requirements:
    1. Follow the Markdown syntax format for layout.
    2. If there are code examples, they must follow standard syntax specifications, have document annotations, and be displayed in code blocks.
    3. The output must be strictly in the specified language, {language}.
    4. Do not have redundant output, including concluding remarks.
    5. Strict requirement not to output the topic "{topic}".
    现在我将为您提供该主题的模块目录标题。
    请详细输出此标题的详细原理内容。
    如果有代码示例,请按照标准代码规范提供。
    没有代码示例则不需要提供。
    
    该主题的模块目录标题如下:
    {directory}
    
    严格按照以下要求限制输出:
    1. 遵循Markdown语法格式进行布局。
    2. 如果有代码示例,必须遵循标准语法规范,具备文档注释,并以代码块形式显示。
    3. 输出必须严格使用指定语言{language}。
    4. 不得有冗余输出,包括总结性陈述。
    5. 严禁输出主题"{topic}"。
    """

    async def run(self, topic: str):
        prompt = self.DIRECTORY_PROMPT.format(
            topic=topic, language=language, directory=self.directory)
        rsp = await self._aask(prompt)
        return rsp

3.4.3 Role 基类

首先我们根据role基类中定义的_init_actions方法来看,当我们初始化一个动作时,这个动作将被加入到self.actions 中,而self.actions为一个列表,其中存储了我们所有的动作。

class Role(SerializationMixin, ContextMixin, BaseModel):

    rc: RoleContext = Field(default_factory=RoleContext)
    # ......
    
    async def _act(self) -> Message:
        logger.info(f"{self._setting}: to do {self.rc.todo}({self.rc.todo.name})")
        response = await self.rc.todo.run(self.rc.history)
        if isinstance(response, (ActionOutput, ActionNode)):
            msg = Message(
                content=response.content,
                instruct_content=response.instruct_content,
                role=self._setting,
                cause_by=self.rc.todo,
                sent_from=self,
            )
        elif isinstance(response, Message):
            msg = response
        else:
            msg = Message(content=response, role=self.profile, cause_by=self.rc.todo, sent_from=self)
        self.rc.memory.add(msg)

        return msg
    
    def put_message(self, message):
        """Place the message into the Role object's private message buffer."""
        if not message:
            return
        self.rc.msg_buffer.push(message)

    async def _think(self) -> bool:
        """Consider what to do and decide on the next course of action. Return false if nothing can be done."""
        if len(self.actions) == 1:
            # If there is only one action, then only this one can be performed
            self._set_state(0)

            return True

        if self.recovered and self.rc.state >= 0:
            self._set_state(self.rc.state)  # action to run from recovered state
            self.recovered = False  # avoid max_react_loop out of work
            return True

        prompt = self._get_prefix()
        prompt += STATE_TEMPLATE.format(
            history=self.rc.history,
            states="\n".join(self.states),
            n_states=len(self.states) - 1,
            previous_state=self.rc.state,
        )

        next_state = await self.llm.aask(prompt)
        next_state = extract_state_value_from_output(next_state)
        logger.debug(f"{prompt=}")

        if (not next_state.isdigit() and next_state != "-1") or int(next_state) not in range(-1, len(self.states)):
            logger.warning(f"Invalid answer of state, {next_state=}, will be set to -1")
            next_state = -1
        else:
            next_state = int(next_state)
            if next_state == -1:
                logger.info(f"End actions with {next_state=}")
        self._set_state(next_state)
        return True

    async def _react(self) -> Message:
        """Think first, then act, until the Role _think it is time to stop and requires no more todo.
        This is the standard think-act loop in the ReAct paper, which alternates thinking and acting in task solving, i.e. _think -> _act -> _think -> _act -> ...
        Use llm to select actions in _think dynamically
        """
        actions_taken = 0
        rsp = Message(content="No actions taken yet", cause_by=Action)  # will be overwritten after Role _act
        # max_react_loop默认为1
        while actions_taken < self.rc.max_react_loop:
            # think
            await self._think()
            if self.rc.todo is None:
                break
            # act
            logger.debug(f"{self._setting}: {self.rc.state=}, will do {self.rc.todo}")
            rsp = await self._act()
            actions_taken += 1
        return rsp  # return output from the last action

    async def react(self) -> Message:
        """Entry to one of three strategies by which Role reacts to the observed Message"""
        # 在RoleContext中react_mode值默认为RoleReactMode.REACT
        if self.rc.react_mode == RoleReactMode.REACT:
            rsp = await self._react()
        elif self.rc.react_mode == RoleReactMode.BY_ORDER:
            rsp = await self._act_by_order()
        elif self.rc.react_mode == RoleReactMode.PLAN_AND_ACT:
            rsp = await self._plan_and_act()
        self._set_state(state=-1)  # current reaction is complete, reset state to -1 and todo back to None
        return rsp

    def get_memories(self, k=0) -> list[Message]:
        """A wrapper to return the most recent k memories of this role, return all when k=0"""
        return self.rc.memory.get(k=k)

    async def run(self, with_message=None) -> Message | None:
        """Observe, and think and act based on the results of the observation"""
        if with_message:
            # ......检查入参with_message,处理后赋值给msg
            self.put_message(msg)
        # ......
        # _react()
        rsp = await self.react()

        # Reset the next action to be taken.
        self.set_todo(None)
        # Send the response message to the Environment object to have it relay the message to the subscribers.
        self.publish_message(rsp)
        return rsp

  1. run() 方法中,将入参 with_message 加入到 self.rc.msg_buffer 中,然后调用 self.react()。
  2. _react() 方法中,react_mode 默认为 RoleReactMode.REACT,调用 self._react()。
  3. self._react() 方法中,会先调用 self._think() 进行思考,然后调用 self._act() 执行动作。

self._react() 方法基本决定了 agent 的行动路线,这里需要思考的是要实现我们期望的 agent,他应该怎样行动?
因此,开发复杂的agent,需要 _react() 方法、_think() 方法和 _act() 方法。

3.4.4 编写 TutorialAssistant 角色

async def _react(self) -> Message:
    """Execute the assistant's think and actions.

    Returns:
        A message containing the final result of the assistant's actions.
    执行助手的思考和行动。
    返回:
    包含助手行动最终结果的消息。
    """
    while True:
        await self._think()
        if self.rc.todo is None:
            break
        msg = await self._act()
    root_path = TUTORIAL_PATH / datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
    await File.write(root_path, f"{self.main_title}.md", self.total_content.encode('utf-8'))
    return msg


async def _think(self) -> None:
    """Determine the next action to be taken by the role."""
    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

_act 方法中我们将目前的 todo 内容按照 action 的类型分开处理:

  • 当目前需要生成目录时
    • 获取用户的输入,传入 WriteDirectory action 内 生成对应的目录;
    • 最后,在 _handle_directory 方法中根据目录内容,我们生成子任务,也就是根据标题题目来生成内容;
    • 子任务生成结束后,我们使用 self._init_actions 更新目前的任务列表;
  • 当下次运行 _act 方法时
    • 将执行 WriteContent 动作,来生成指定目录中的内容;
async def _act(self) -> Message:
    """Perform an action as determined by the role.

    Returns:
            A message containing the result of the action.
    """
    todo = self.rc.todo
    if type(todo) is WriteDirectory:
        msg = self.rc.memory.get(k=1)[0]
        self.topic = msg.content
        resp = await todo.run(topic=self.topic)
        logger.info(resp)
        # 将writedirector生成的目录一级标题actions添加到actions列表中。
        return await self._handle_directory(resp)
    resp = await todo.run(topic=self.topic)
    logger.info(resp)
    if self.total_content != "":
        self.total_content += "\n\n\n"
    self.total_content += resp
    return Message(content=resp, role=self.profile)


async def _handle_directory(self, titles: Dict) -> Message:
    """Handle the directories for the tutorial document.

    Args:
        titles: A dictionary containing the titles and directory structure,
                such as {"title": "xxx", "directory": [{"dir 1": ["sub dir 1", "sub dir 2"]}]}

    Returns:
        A message containing information about the directory.
        处理教程文档的目录。
        参数:
        titles:包含标题和目录结构的字典,
        例如{"title": "xxx", "directory": [{"dir 1": ["sub dir 1", "sub dir 2"]}]}。
        返回值:
        包含目录信息的消息。
    """
    # 当生成目录后记录目录标题(因为最后要输出完整文档)
    self.main_title = titles.get("title")
    directory = f"{self.main_title}\n"
    # self.total_content用来存储最好要输出的所有内容
    self.total_content += f"# {self.main_title}"
    actions = list()
    for first_dir in titles.get("directory"):
        # 根据目录结构来生成新的需要行动的action(目前只设计了两级目录)
        actions.append(WriteContent(language=self.language, directory=first_dir))
        key = list(first_dir.keys())[0]
        directory += f"- {key}\n"
        for second_dir in first_dir[key]:
            directory += f"  - {second_dir}\n"
    self.set_actions(actions)
    self.rc.todo = None
    return Message(content=directory)

完整代码

import asyncio
from datetime import datetime
from typing import Dict
from metagpt.const import TUTORIAL_PATH
from metagpt.actions import Action
from metagpt.logs import logger
from metagpt.roles.role import Role, RoleReactMode
from metagpt.schema import Message
from metagpt.utils.file import File
from metagpt.utils.common import OutputParser


# 编写WriteDirectory动作
class WriteDirectory(Action):

    name: str = "WriteDirectory"
    language: str = "chinese"

    COMMON_PROMPT: str = """
    You are now a seasoned technical professional in the field of the internet. 
    We need you to write a technical tutorial with the topic "{topic}".
    您现在是互联网领域的经验丰富的技术专业人员。
    我们需要您撰写一个关于"{topic}"的技术教程。
    """
    DIRECTORY_PROMPT: str = COMMON_PROMPT + """
    Please provide the specific table of contents for this tutorial, strictly following the following requirements:
    1. The output must be strictly in the specified language, {language}.
    2. Answer strictly in the dictionary format like {{"title": "xxx", "directory": [{{"dir 1": ["sub dir 1", "sub dir 2"]}}, {{"dir 2": ["sub dir 3", "sub dir 4"]}}]}}.
    3. The directory should be as specific and sufficient as possible, with a primary and secondary directory.The secondary directory is in the array.
    4. Do not have extra spaces or line breaks.
    5. Each directory title has practical significance.
    请按照以下要求提供本教程的具体目录:
    1. 输出必须严格符合指定语言,{language}。
    2. 回答必须严格按照字典格式,如{{"title": "xxx", "directory": [{{"dir 1": ["sub dir 1", "sub dir 2"]}}, {{"dir 2": ["sub dir 3", "sub dir 4"]}}]}}。
    3. 目录应尽可能具体和充分,包括一级和二级目录。二级目录在数组中。
    4. 不要有额外的空格或换行符。
    5. 每个目录标题都具有实际意义。
    """

    async def run(self, topic: str) -> Dict:
        prompt = self.DIRECTORY_PROMPT.format(topic=topic, language=self.language)
        rsp = await self._aask(prompt)
        return OutputParser.extract_struct(rsp, dict)


# 编写WriteContent动作
class WriteContent(Action):

    name: str = "WriteContent"
    language: str = "chinese"
    directory: Dict = {}

    COMMON_PROMPT: str = """
    You are now a seasoned technical professional in the field of the internet. 
    We need you to write a technical tutorial with the topic "{topic}".
    """

    CONTENT_PROMPT: str = COMMON_PROMPT + """
    Now I will give you the module directory titles for the topic. 
    Please output the detailed principle content of this title in detail. 
    If there are code examples, please provide them according to standard code specifications. 
    Without a code example, it is not necessary.

    The module directory titles for the topic is as follows:
    {directory}

    Strictly limit output according to the following requirements:
    1. Follow the Markdown syntax format for layout.
    2. If there are code examples, they must follow standard syntax specifications, have document annotations, and be displayed in code blocks.
    3. The output must be strictly in the specified language, {language}.
    4. Do not have redundant output, including concluding remarks.
    5. Strict requirement not to output the topic "{topic}".
    现在我将为您提供该主题的模块目录标题。
    请详细输出此标题的详细原理内容。
    如果有代码示例,请按照标准代码规范提供。
    没有代码示例则不需要提供。
    
    该主题的模块目录标题如下:
    {directory}
    
    严格按照以下要求限制输出:
    1. 遵循Markdown语法格式进行布局。
    2. 如果有代码示例,必须遵循标准语法规范,具备文档注释,并以代码块形式显示。
    3. 输出必须严格使用指定语言{language}。
    4. 不得有冗余输出,包括总结性陈述。
    5. 严禁输出主题"{topic}"。
    """

    async def run(self, topic: str):
        prompt = self.CONTENT_PROMPT.format(
            topic=topic, language=self.language, directory=self.directory)
        rsp = await self._aask(prompt)
        return rsp


# 设计TutorialAssistant角色
class TutorialAssistant(Role):

    name: str = "Alice"
    profile: str = "Tutorial Assistant"
    goal: str = "Generate tutorial documents"
    constraints: str = "Strictly follow Markdown's syntax, with neat and standardized layout"
    language: str = "Chinese"

    topic: str = ""
    main_title: str = ""
    total_content: str = ""

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.set_actions([WriteDirectory(language=self.language)])
        self._set_react_mode(react_mode=RoleReactMode.REACT)

    async def _act(self) -> Message:
        """Perform an action as determined by the role.

        Returns:
                A message containing the result of the action.
        """
        todo = self.rc.todo
        if type(todo) is WriteDirectory:
            msg = self.rc.memory.get(k=1)[0]
            self.topic = msg.content
            resp = await todo.run(topic=self.topic)
            logger.info(resp)
            # 将writedirector生成的目录一级标题actions添加到actions列表中。
            return await self._handle_directory(resp)
        resp = await todo.run(topic=self.topic)
        logger.info(resp)
        if self.total_content != "":
            self.total_content += "\n\n\n"
        self.total_content += resp
        return Message(content=resp, role=self.profile)


    async def _think(self) -> None:
        """Determine the next action to be taken by the role."""
        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:
        """Execute the assistant's think and actions.

        Returns:
            A message containing the final result of the assistant's actions.
        执行助手的思考和行动。
        返回:
        包含助手行动最终结果的消息。
        """
        while True:
            await self._think()
            if self.rc.todo is None:
                break
            msg = await self._act()
        root_path = TUTORIAL_PATH / datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
        await File.write(root_path, f"{self.main_title}.md", self.total_content.encode('utf-8'))
        return msg

    async def _handle_directory(self, titles: Dict) -> Message:
        """Handle the directories for the tutorial document.

        Args:
            titles: A dictionary containing the titles and directory structure,
                    such as {"title": "xxx", "directory": [{"dir 1": ["sub dir 1", "sub dir 2"]}]}

        Returns:
            A message containing information about the directory.
            处理教程文档的目录。
            参数:
            titles:包含标题和目录结构的字典,
            例如{"title": "xxx", "directory": [{"dir 1": ["sub dir 1", "sub dir 2"]}]}。
            返回值:
            包含目录信息的消息。
        """
        # 当生成目录后记录目录标题(因为最后要输出完整文档)
        self.main_title = titles.get("title")
        directory = f"{self.main_title}\n"
        # self.total_content用来存储最好要输出的所有内容
        self.total_content += f"# {self.main_title}"
        actions = list()
        for first_dir in titles.get("directory"):
            # 根据目录结构来生成新的需要行动的action(目前只设计了两级目录)
            actions.append(WriteContent(language=self.language, directory=first_dir))
            key = list(first_dir.keys())[0]
            directory += f"- {key}\n"
            for second_dir in first_dir[key]:
                directory += f"  - {second_dir}\n"
        self.set_actions(actions)
        self.rc.todo = None
        return Message(content=directory)


async def main():
    msg="Git 教程"
    role = TutorialAssistant()
    logger.info(msg)
    result = await role.run(msg)
    logger.info(result)


if __name__ == "__main__":
    asyncio.run(main())

3.5 单智能体案例 OSS - 订阅智能体

在MetaGPT中,metagpt.subscription模块提供了SubscriptionRunner类,与metagpt.team.Team类似,SubscriptionRunner类是提供了一个Role的运行方式,基于SubscriptionRunner类,我们可以定时触发运行一个Role,然后将Role的执行输出通知给用户,例如,以下的示例:

posted @   geyashi  阅读(33)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
点击右上角即可分享
微信分享提示