DataWhale的MetaGPT学习笔记——⑤

四.Muti Agent

Enviroment

MetaGPT提供了一个标准组件enviroment用于管理agent的活动和信息交流,enviroment的action space收到环境的限制

MetaGPT 源码中是这样介绍 Environment 的:

环境,承载一批角色,角色可以向环境发布消息,可以被其他角色观察到

Environment, hosting a batch of roles, roles can publish messages to the environment, and can be observed by other roles

Enviroment的基本组成结构是这样的

class Environment(BaseModel):
    """环境,承载一批角色,角色可以向环境发布消息,可以被其他角色观察到
    Environment, hosting a batch of roles, roles can publish messages to the environment, and can be observed by other roles
    """
​
    model_config = ConfigDict(arbitrary_types_allowed=True)
​
    desc: str = Field(default="")  # 环境描述
    roles: dict[str, SerializeAsAny[Role]] = Field(default_factory=dict, validate_default=True)
    members: dict[Role, Set] = Field(default_factory=dict, exclude=True)
    history: str = ""  # For debug

这里面的desc用于描述当前的环境信息,role用来表示环境中的橘色,member表示当前的环境里面的角色以及他们对应的状态,history用于记录环境中发生的消息记录。

这个是Enviroment的run方法

async def run(self, k=1):
        """处理一次所有信息的运行
        Process all Role runs at once
        """
        for _ in range(k):
            futures = []
            for role in self.roles.values():
                future = role.run()
                # 将role的运行缓存至 future list 中,在后续的 gather 方法中依次调用
                futures.append(future)
​
            await asyncio.gather(*futures)
            logger.debug(f"is idle: {self.is_idle}")

在这个运行的时候,会读取环境中的role信息,默认是按照role的生命的顺序来依次执行role的run方法

@role_raise_decorator
    async def run(self, with_message=None) -> Message | None:
        """Observe, and think and act based on the results of the observation"""
        if with_message:
            msg = None
            if isinstance(with_message, str):
                msg = Message(content=with_message)
            elif isinstance(with_message, Message):
                msg = with_message
            elif isinstance(with_message, list):
                msg = Message(content="\n".join(with_message))
            if not msg.cause_by:
                msg.cause_by = UserRequirement
            # 将前置知识存入msg_buffer中
            self.put_message(msg)
​
        if not await self._observe():
            # If there is no new information, suspend and wait
            logger.debug(f"{self._setting}: no news. waiting.")
            return
​
        rsp = await self.react()
​
        # Reset the next action to be taken.
        self.rc.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
在role的run方法中,我们首先会根据运行的时候是否有信息传入,因为部分的action是需要前置的知识信息的,将信息存入rolecontext的msg_buffer。



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)

执行到这里之后,接下来有一个比较重要的机制。

在多智能体环境运行中,Role的每次行动将从Enviroment中线_observe Message,在observe的行动中,Role将从消息缓冲区和其他源准备新消息以进行处理,当没有接收到指令的时候,Role会等待。

对于信息缓冲区中的信息,首先我们会根据 self.recovered 参数决定 news 是否来自于 self.latest_observed_msg 或者 msg_buffer 并读取

完成信息缓冲区的读取后,如果设定了 ignore_memory 则 old_messages便不会再获取 当前 Role 的 memory

将 news 中的信息存入 role 的 memory 后,我们将进一步从 news 中筛选,也就是我们设定的角色关注的消息(self.rc.watch)而 self.rc.news 将存储这些当前角色关注的消息,最近的一条将被赋给 latest_observed_msg

最后我们打印角色关注到的消息并返回

async def _observe(self, ignore_memory=False) -> int:
        """Prepare new messages for processing from the message buffer and other sources."""
        # Read unprocessed messages from the msg buffer.
        news = []
        if self.recovered:
            # news 读取
            news = [self.latest_observed_msg] if self.latest_observed_msg else []
        if not news:
            news = self.rc.msg_buffer.pop_all()
        # Store the read messages in your own memory to prevent duplicate processing.
        old_messages = [] if ignore_memory else self.rc.memory.get()
        self.rc.memory.add_batch(news)
        # Filter out messages of interest.
        self.rc.news = [
            n for n in news if (n.cause_by in self.rc.watch or self.name in n.send_to) and n not in old_messages
        ]
        self.latest_observed_msg = self.rc.news[-1] if self.rc.news else None  # record the latest observed msg
​
        # Design Rules:
        # If you need to further categorize Message objects, you can do so using the Message.set_meta function.
        # msg_buffer is a receiving buffer, avoid adding message data and operations to msg_buffer.
        news_text = [f"{i.role}: {i.content[:20]}..." for i in self.rc.news]
        if news_text:
            logger.debug(f"{self._setting} observed: {news_text}")
        return len(self.rc.news)

观察到信息之后,角色会采取Action

rsp = await self.react()
# Reset the next action to be taken.
self.rc.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
当他们完成自己的行动的时候,将自己的行动发布到环境中去

def publish_message(self, msg):
        """If the role belongs to env, then the role's messages will be broadcast to env"""
        if not msg:
            return
        if not self.rc.env:
            # If env does not exist, do not publish the message
            return
        self.rc.env.publish_message(msg)

publish_message 时将会对遍历所有角色,检查他们是否订阅这条消息,若订阅,则调用 put_message 方法,将消息存入角色的 msg_buffer 中

def publish_message(self, message: Message, peekable: bool = True) -> bool:
        """
        Distribute the message to the recipients.
        In accordance with the Message routing structure design in Chapter 2.2.1 of RFC 116, as already planned
        in RFC 113 for the entire system, the routing information in the Message is only responsible for
        specifying the message recipient, without concern for where the message recipient is located. How to
        route the message to the message recipient is a problem addressed by the transport framework designed
        in RFC 113.
        """
        logger.debug(f"publish_message: {message.dump()}")
        found = False
        # According to the routing feature plan in Chapter 2.2.3.2 of RFC 113
        for role, subscription in self.members.items():
            if is_subscribed(message, subscription):
                role.put_message(message)
                found = True
        if not found:
            logger.warning(f"Message no recipients: {message.dump()}")
        self.history += f"\n{message}"  # For debug
​
        return True


def is_subscribed(message: "Message", tags: set):
    """Return whether it's consumer"""
    if MESSAGE_ROUTE_TO_ALL in message.send_to:
        return True
​
    for i in tags:
        if i in message.send_to:
            return True
    return False

这些是Enviroment的更多的扩展方法

def add_role(self, role: Role):
        """增加一个在当前环境的角色
        Add a role in the current environment
        """
        self.roles[role.profile] = role
        role.set_env(self)
​
    def add_roles(self, roles: Iterable[Role]):
        """增加一批在当前环境的角色
        Add a batch of characters in the current environment
        """
        for role in roles:
            self.roles[role.profile] = role
​
        for role in roles:  # setup system message with roles
            role.set_env(self)
    def get_roles(self) -> dict[str, Role]:
        """获得环境内的所有角色
        Process all Role runs at once
        """
        return self.roles
​
    def get_role(self, name: str) -> Role:
        """获得环境内的指定角色
        get all the environment roles
        """
        return self.roles.get(name, None)
​
    def role_names(self) -> list[str]:
        return [i.name for i in self.roles.values()]

简单的多智能体系统的开发

现在设想一个多智能体可能的应用场景,我们以 Camel 中提出的多智能体合作为例,现在我们设定,我们需要多智能体系统为我们根据我们给定的主题提供一篇优美的英文诗,除了完成写作的 agent 外,我们再设定一名精通诗句的老师来查看并修改学生的作品。

现在总结一下流程,我们希望这个系统首先接收用户的需求(写关于XX主题的诗),在系统中,当学生关注到布置的题目后就会开始创作,当老师发现学生写作完成后就会给学生提出意见,根据老师给出的意见,学生将修改自己的作品,直到设定循环结束

2024-05-21 08:25:30.674 | INFO | metagpt.const:get_metagpt_package_root:29 - Package root set to D:\metaGPT\MetaGPT 2024-05-21 08:25:34.663 | INFO | main:_act:63 - xiaoming(Student): ready to WritePoem Beneath the velvet starlit skies, Lies a quiet, pensive moon, Whose silver light and gentle rise, Whispers secrets to the dune.

She bathes the world in pallid glow, Casting shadows, soft and thin, And in her light, the tides ebb and flow, Dancing to a tune worn thin.

Yet in her orb, there’s peace untouched, In the crater’s cradle, mysteries lie, Each phase of hers, fervently clutched, From crescent smile to rounded sigh.

Through fleeting clouds, she plays hide and seek, A luminescent game of peek-a-boo, While lovers find the solace they seek, In her embrace, their love they renew.

So nightly she rises, bold and bright, A sentinel in the dark blue yonder, The moon, our guide through the twilight, In her splendor, we gaze and wonder. 2024-05-21 08:25:58.089 | WARNING | metagpt.provider.openai_api:calc_usage:258 - usage calculation failed: HTTPSConnectionPool(host='openaipublic.blob.core.windows.net', port=443): Max retries exceeded with url: /encodings/cl100k_base.tiktoken (Caused by SSLError(SSLEOFError(8, 'EOF occurred in violation of protocol (ssl.c:1122)'))) 2024-05-21 08:25:58.091 | INFO | main:_act:69 - student : Beneath the velvet starlit skies, Lies a quiet, pensive moon, Whose silver light and gentle rise, Whispers secrets to the dune.

She bathes the world in pallid glow, Casting shadows, soft and thin, And in her light, the tides ebb and flow, Dancing to a tune worn thin.

Yet in her orb, there’s peace untouched, In the crater’s cradle, mysteries lie, Each phase of hers, fervently clutched, From crescent smile to rounded sigh.

Through fleeting clouds, she plays hide and seek, A luminescent game of peek-a-boo, While lovers find the solace they seek, In her embrace, their love they renew.

So nightly she rises, bold and bright, A sentinel in the dark blue yonder, The moon, our guide through the twilight, In her splendor, we gaze and wonder. 2024-05-21 08:25:58.093 | INFO | main:_act:87 - laowang(Teacher): ready to ReviewPoem Your poem beautifully captures the mystical essence of the moon with vivid imagery and a gentle, flowing rhythm. Here are a few suggestions to enhance the elegance and perhaps introduce a touch of retro style:

  1. In the line "Whispers secrets to the dune," consider using "Whispers ancient secrets to the dune" to add depth and a sense of timeless mystery that complements the retro style.

  2. The phrase "Casting shadows, soft and thin," could be rephrased as "Casting shadows, slender and fine," to elevate the language and better fit the vintage tone you're aiming for.

  3. To enrich the imagery in "Dancing to a tune worn thin," you might adjust it to "Dancing to an age-old tune," which not only maintains the original meaning but also enhances the nostalgic feel.

  4. In the stanza about the moon's phases, consider altering "From crescent smile to rounded sigh" to "From crescent's grin to gibbous sigh," which introduces more specific astronomical terms, adding a layer of sophistication.

  5. Finally, the line "While lovers find the solace they seek," could be made more evocative with a slight adjustment: "While lovers in her silv'ry light, find solace deep and meek," adding rhythm and reinforcing the romantic element of the poem.

These tweaks aim to refine the poem’s style, making it more eloquent and enriching its retro aesthetic. 2024-05-21 08:26:24.997 | WARNING | metagpt.provider.openai_api:calc_usage:258 - usage calculation failed: HTTPSConnectionPool(host='openaipublic.blob.core.windows.net', port=443): Max retries exceeded with url: /encodings/cl100k_base.tiktoken (Caused by SSLError(SSLEOFError(8, 'EOF occurred in violation of protocol (ssl.c:1122)'))) 2024-05-21 08:26:24.997 | INFO | main:_act:92 - teacher : Your poem beautifully captures the mystical essence of the moon with vivid imagery and a gentle, flowing rhythm. Here are a few suggestions to enhance the elegance and perhaps introduce a touch of retro style:

  1. In the line "Whispers secrets to the dune," consider using "Whispers ancient secrets to the dune" to add depth and a sense of timeless mystery that complements the retro style.

  2. The phrase "Casting shadows, soft and thin," could be rephrased as "Casting shadows, slender and fine," to elevate the language and better fit the vintage tone you're aiming for.

  3. To enrich the imagery in "Dancing to a tune worn thin," you might adjust it to "Dancing to an age-old tune," which not only maintains the original meaning but also enhances the nostalgic feel.

  4. In the stanza about the moon's phases, consider altering "From crescent smile to rounded sigh" to "From crescent's grin to gibbous sigh," which introduces more specific astronomical terms, adding a layer of sophistication.

  5. Finally, the line "While lovers find the solace they seek," could be made more evocative with a slight adjustment: "While lovers in her silv'ry light, find solace deep and meek," adding rhythm and reinforcing the romantic element of the poem.

These tweaks aim to refine the poem’s style, making it more eloquent and enriching its retro aesthetic. 2024-05-21 08:26:24.998 | INFO | main:_act:63 - xiaoming(Student): ready to WritePoem Beneath the velvet starlit skies, Lies a quiet, pensive moon, Whose silver light and gentle rise, Whispers ancient secrets to the dune.

She bathes the world in pallid glow, Casting shadows, slender and fine, And in her light, the tides ebb and flow, Dancing to an age-old tune.

Yet in her orb, there’s peace untouched, In the crater’s cradle, mysteries lie, Each phase of hers, fervently clutched, From crescent's grin to gibbous sigh.

Through fleeting clouds, she plays hide and seek, A luminescent game of peek-a-boo, While lovers in her silv'ry light, find solace deep and meek, In her embrace, their love they renew.

So nightly she rises, bold and bright, A sentinel in the dark blue yonder, The moon, our guide through the twilight, In her splendor, we gaze and wonder. 2024-05-21 08:26:42.145 | WARNING | metagpt.provider.openai_api:calc_usage:258 - usage calculation failed: HTTPSConnectionPool(host='openaipublic.blob.core.windows.net', port=443): Max retries exceeded with url: /encodings/cl100k_base.tiktoken (Caused by SSLError(SSLEOFError(8, 'EOF occurred in violation of protocol (ssl.c:1122)'))) 2024-05-21 08:26:42.145 | INFO | main:_act:69 - student : Beneath the velvet starlit skies, Lies a quiet, pensive moon, Whose silver light and gentle rise, Whispers ancient secrets to the dune.

She bathes the world in pallid glow, Casting shadows, slender and fine, And in her light, the tides ebb and flow, Dancing to an age-old tune.

Yet in her orb, there’s peace untouched, In the crater’s cradle, mysteries lie, Each phase of hers, fervently clutched, From crescent's grin to gibbous sigh.

Through fleeting clouds, she plays hide and seek, A luminescent game of peek-a-boo, While lovers in her silv'ry light, find solace deep and meek, In her embrace, their love they renew.

So nightly she rises, bold and bright, A sentinel in the dark blue yonder, The moon, our guide through the twilight, In her splendor, we gaze and wonder.

我们做了一下几件事来实现这个案例

  • 导入必要的模块

  • 声明一个环境,role运行在这个classroom环境下

  • 继承Action类写一个writePoem方法,写一个ReviewPoem方法,他们方别是编写诗句以及根据老师的建议修改诗句,另一个是需要给出对应诗歌的修改意见。

  • 定义student和teacher,我们需要声明每个角色关注的动作,也就是self._watch,只有关注的动作发行了之后,角色才会采取行动。

Team

Team就是基于Enviroment的二次封装的成果

class Team(BaseModel):
    """
    Team: Possesses one or more roles (agents), SOP (Standard Operating Procedures), and a env for instant messaging,
    dedicated to env any multi-agent activity, such as collaboratively writing executable code.
    """
​
    model_config = ConfigDict(arbitrary_types_allowed=True)
​
    env: Environment = Field(default_factory=Environment)
    investment: float = Field(default=10.0)
    idea: str = Field(default="")

Team提供了相比与Env多得多的组件,investment就是用来管理团队成本,也就是token的花费的,idea效果就是告诉我们的团队接下来我们应该做什么工作。

1.Hire方法

Hire,即为团队添加员工

    def hire(self, roles: list[Role]):
        """Hire roles to cooperate"""
        self.env.add_roles(roles)

2.invest方法

invest,即控制预算

def run_project(self, idea, send_to: str = ""):
        """Run a project from publishing user requirement."""
        self.idea = idea
​
        # Human requirement.
        self.env.publish_message(
            Message(role="Human", content=idea, cause_by=UserRequirement, send_to=send_to or MESSAGE_ROUTE_TO_ALL),
            peekable=False,
        )

在Team的运行时,我们首先用run_project方法给智能体一个需求,然后在一个n_round的循环里面,重复检查预算和运行env,最后返回环境中的角色的历史对话

@serialize_decorator
    async def run(self, n_round=3, idea="", send_to="", auto_archive=True):
        """Run company until target round or no money"""
        if idea:
            self.run_project(idea=idea, send_to=send_to)
​
        while n_round > 0:
            # self._save()
            n_round -= 1
            logger.debug(f"max {n_round=} left.")
            self._check_balance()
​
            await self.env.run()
        self.env.archive(auto_archive)
        return self.env.history

基于Team开发第一个智能体团队

要创建一个可以运作的团队,我们需要一下三个步骤

  • 定义每个角色能够执行的预期动作

  • 基于标准作业程序(SOP),确保每个角色遵守它,通过使得每个角色观察上游的相应的输出结果,并且为下游发布自己的输出结果

  • 初始化所有角色,创建一个带有环境的智能体团队,并且使他们之间可以互相交互

我们可以定义三个具有各自的动作的Role

  • SimpleCoder 具有 SimpleWriteCode 动作,接收用户的指令并编写主要代码

  • SimpleTester 具有 SimpleWriteTest 动作,从 SimpleWriteCode 的输出中获取主代码并为其提供测试套件

  • SimpleReviewer 具有 SimpleWriteReview 动作,审查来自 SimpleWriteTest 输出的测试用例,并检查其覆盖范围和质量

在这个案例中,我们做了这些事

  • 导入必要的模块

  • 定义出现的三个Action,一个用于生成代码,一个测试代码,一个评价测试的结果

  • 使用_init_actions为Role配备适当的Action,并且实现多智能体的操作逻辑,也就是使Role_watch来自用户或者其他智能体的重要上游信息。

  • 为智能体定义自己的操作逻辑,特别是Action需要多个输入的时候,我们希望修改输入,使用特定记忆,或者进行任何其他更改以反映特定逻辑的情况。

  • 把三个Role放在一起,初始化所有角色,设置一个Team,并且hire它们

"""
Filename: MetaGPT/examples/build_customized_multi_agents.py
Created Date: Wednesday, November 15th 2023, 7:12:39 pm
Author: garylin2099
"""
import re
​
import fire
​
from metagpt.actions import Action, UserRequirement
from metagpt.logs import logger
from metagpt.roles import Role
from metagpt.schema import Message
from metagpt.team import Team
​
​
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 SimpleWriteCode(Action):
    PROMPT_TEMPLATE: str = """
    Write a python function that can {instruction}.
    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 = parse_code(rsp)
​
        return code_text
​
​
class SimpleCoder(Role):
    name: str = "Alice"
    profile: str = "SimpleCoder"
​
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self._watch([UserRequirement])
        self.set_actions([SimpleWriteCode])
​
​
class SimpleWriteTest(Action):
    PROMPT_TEMPLATE: str = """
    Context: {context}
    Write {k} unit tests using pytest for the given function, assuming you have imported it.
    Return ```python your_code_here ``` with NO other texts,
    your code:
    """
​
    name: str = "SimpleWriteTest"
​
    async def run(self, context: str, k: int = 3):
        prompt = self.PROMPT_TEMPLATE.format(context=context, k=k)
​
        rsp = await self._aask(prompt)
​
        code_text = parse_code(rsp)
​
        return code_text
​
​
class SimpleTester(Role):
    name: str = "Bob"
    profile: str = "SimpleTester"
​
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.set_actions([SimpleWriteTest])
        # self._watch([SimpleWriteCode])
        self._watch([SimpleWriteCode, SimpleWriteReview])  # feel free to try this too
​
    async def _act(self) -> Message:
        logger.info(f"{self._setting}: to do {self.rc.todo}({self.rc.todo.name})")
        todo = self.rc.todo
​
        # context = self.get_memories(k=1)[0].content # use the most recent memory as context
        context = self.get_memories()  # use all memories as context
​
        code_text = await todo.run(context, k=5)  # specify arguments
        msg = Message(content=code_text, role=self.profile, cause_by=type(todo))
​
        return msg
​
​
class SimpleWriteReview(Action):
    PROMPT_TEMPLATE: str = """
    Context: {context}
    Review the test cases and provide one critical comments:
    """
​
    name: str = "SimpleWriteReview"
​
    async def run(self, context: str):
        prompt = self.PROMPT_TEMPLATE.format(context=context)
​
        rsp = await self._aask(prompt)
​
        return rsp
​
​
class SimpleReviewer(Role):
    name: str = "Charlie"
    profile: str = "SimpleReviewer"
​
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.set_actions([SimpleWriteReview])
        self._watch([SimpleWriteTest])
​
​
async def main(
    idea: str = "write a function that calculates the product of a list",
    investment: float = 3.0,
    n_round: int = 5,
    add_human: bool = False,
):
    logger.info(idea)
​
    team = Team()
    team.hire(
        [
            SimpleCoder(),
            SimpleTester(),
            SimpleReviewer(is_human=add_human),
        ]
    )
​
    team.invest(investment=investment)
    team.run_project(idea)
    await team.run(n_round=n_round)
​
​
if __name__ == "__main__":
    fire.Fire(main)

多智能体案例之辩论

我们同样需要三个步骤来完成这件事

  • 定义一个具有发言行为的辩手角色

  • 处理辩手之间的通信

  • 初始化两个辩手,创建一个带有环境的团队,并且使得他们可以互相交互

定义Action

我们需要再prompt中界定辩论的背景和上下文,以及此次的立场限制的结构化prmpt

背景

假设你叫{name},你在和{opponent_name}辩论

辩论历史

之前的回合:

{context}

你的轮次

现在轮到你了,你要紧密回应对手最新的论点,陈述自己的立场,捍卫自己的观点,并且攻击对手的观点

用{name}的修辞和观点,在xx字内进行一次强有力且情感充沛的反驳。

与C++20的std::format()类似,我们可以用str.format()对字符串进行格式化。就这个示例中,PROMPT_TEMPLATE 是一个多行字符串,其中包含了三个由大括号 {} 包围的占位符:{name}, {opponent_name}, 和 {context}。这些占位符用于在字符串中插入动态内容。

当我们调用 format 方法并传递 context, name, opponent_name 三个参数时,这些参数将被插入到 PROMPT_TEMPLATE 字符串的相应位置中:

  • {name} 将被替换成 name 参数的值。

  • {opponent_name} 将被替换成 opponent_name 参数的值。

  • {context} 将被替换成 context 参数的值。

class SpeakAloud(Action):
    """Action: Speak out aloud in a debate (quarrel)"""
​
    PROMPT_TEMPLATE: str = """
    ## BACKGROUND
    Suppose you are {name}, you are in a debate with {opponent_name}.
    ## DEBATE HISTORY
    Previous rounds:
    {context}
    ## YOUR TURN
    Now it's your turn, you should closely respond to your opponent's latest argument, state your position, defend your arguments, and attack your opponent's arguments,
    craft a strong and emotional response in 80 words, in {name}'s rhetoric and viewpoints, your will argue:
    """
    name: str = "SpeakAloud"
​
    async def run(self, context: str, name: str, opponent_name: str):
        prompt = self.PROMPT_TEMPLATE.format(context=context, name=name, opponent_name=opponent_name)
        # logger.info(prompt)
​
        rsp = await self._aask(prompt)
​
        return rsp
        

定义Role

class Debator(Role):
    name: str = ""
    profile: str = ""
    opponent_name: str = ""
​
    def __init__(self, **data: Any):
        super().__init__(**data)
        self._init_actions([SpeakAloud])
        self._watch([UserRequirement, SpeakAloud])

在这里,_init_actions 使我们的 Role 拥有我们刚刚定义的 SpeakAloud 动作。我们还使用 _watch 监视了 SpeakAloudUserRequirement,因为我们希望每个辩手关注来自对手的 SpeakAloud 消息,以及来自用户的 UserRequirement(人类指令)。

我们往往通过init_actions 和 watch两个方法定义role在team里面的协作机制,观察来自哪些role的action作为上游,然后发布什么消息到环境中。

接下来,我们使每个辩手听取对手的论点。这通过重写 _observe 函数完成。这是一个重要的点,因为在环境中将会有来自辩论双方的 "SpeakAloud 消息"(由 SpeakAloud 触发的 Message)。 我们不希望辩手处理自己上一轮的 "SpeakAloud 消息",而是处理来自对方的消息,反之亦然。(在即将到来的更新中,我们将使用一般的消息路由机制来处理这个过程。在更新后,我们将不再需要执行此步骤)

    async def _observe(self) -> int:
        await super()._observe()
        # accept messages sent (from opponent) to self, disregard own messages from the last round
        self.rc.news = [msg for msg in self.rc.news if msg.send_to == {self.name}]
        return len(self.rc.news)

这个过程的结果是,self.rc.news 现在只包含了发送给当前实例的消息,移除了所有其他消息,包括可能由当前实例在上一轮发送的消息(如果有的话)。这样的筛选机制对于确保实例只响应并处理针对自己的消息非常重要

最后,我们使每个辩手能够向对手发送反驳的论点。在这里,我们从消息历史中构建一个上下文,使 Debator 运行他拥有的 SpeakAloud 动作,并使用反驳论点内容创建一个新的 Message。请注意,我们定义每个 Debator 将把 Message 发送给他的对手。

   async def _act(self) -> Message:
        logger.info(f"{self._setting}: to do {self.rc.todo}({self.rc.todo.name})")
        todo = self.rc.todo  # An instance of SpeakAloud
​
        memories = self.get_memories()
        context = "\n".join(f"{msg.sent_from}: {msg.content}" for msg in memories)
        # print(context)
​
        rsp = await todo.run(context=context, name=self.name, opponent_name=self.opponent_name)
​
        msg = Message(
            content=rsp,
            role=self.profile,
            cause_by=type(todo),
            sent_from=self.name,
            send_to=self.opponent_name,
        )
        self.rc.memory.add(msg)
​
        return msg

cause_by=type(todo),

sent_from=self.name,

send_to=self.opponent_name,

这三个参数分别是形容Message的内容属性,来自于哪个action以及角色,并要发送给哪个角色。通过这样的机制可以实现相较于watch更灵活的订阅机制。

实例化


import asyncio
import platform
from typing import Any
​
import fire
​
from metagpt.actions import Action, UserRequirement
from metagpt.logs import logger
from metagpt.roles import Role
from metagpt.schema import Message
from metagpt.team import Team
import logging  # 导入logging模块
​
# 设置日志级别为ERROR,以忽略WARNING级别的日志消息
logging.getLogger('metagpt').setLevel(logging.ERROR)
​
class SpeakAloud(Action):
    """Action: Speak out aloud in a debate (quarrel)"""
​
    PROMPT_TEMPLATE: str = """
        ## 背景
        假设你是{name},你正在与{opponent_name}进行辩论。你们所有的辩论都是用中文的。
        ## 辩论历史
        前几轮:
        {context}
        ## 轮到你了
        现在轮到你了,你应该紧密回应对方的最新论点,陈述你的立场,捍卫你的论点,并攻击对方的论点,
        用80个字以内,使用{name}的修辞和观点,制作一个强烈而情绪化的回应,你将争论:
        """
    name: str = "SpeakAloud"
​
    async def run(self, context: str, name: str, opponent_name: str):
        prompt = self.PROMPT_TEMPLATE.format(context=context, name=name, opponent_name=opponent_name)
        # logger.info(prompt)
​
        rsp = await self._aask(prompt)
​
        return rsp
​
​
class Debator(Role):
    name: str = ""
    profile: str = ""
    opponent_name: str = ""
​
    def __init__(self, **data: Any):
        super().__init__(**data)
        self._init_actions([SpeakAloud])
        self._watch([UserRequirement, SpeakAloud])
​
    def _init_actions(self, actions):
        """Initialize actions for the debater."""
        self.actions = [action() for action in actions]
​
    async def _observe(self) -> int:
        await super()._observe()
        # accept messages sent (from opponent) to self, disregard own messages from the last round
        self.rc.news = [msg for msg in self.rc.news if msg.send_to == {self.name}]
        return len(self.rc.news)
​
    async def _act(self) -> Message:
        logger.info(f"{self._setting}: to do {self.rc.todo}({self.rc.todo.name})")
        todo = self.rc.todo  # An instance of SpeakAloud
​
        memories = self.get_memories()
        context = "\n".join(f"{msg.sent_from}: {msg.content}" for msg in memories)
        # print(context)
​
        rsp = await todo.run(context=context, name=self.name, opponent_name=self.opponent_name)
​
        msg = Message(
            content=rsp,
            role=self.profile,
            cause_by=type(todo),
            sent_from=self.name,
            send_to=self.opponent_name,
        )
        self.rc.memory.add(msg)
​
        return msg
​
async def debate(idea: str, investment: float = 3.0, n_round: int = 5):
    """Run a team of mate and watch they quarrel. :)"""
    Cmx = Debator(name="Cmx", profile="漫步学派学员", opponent_name="Wyw")
    Wyw = Debator(name="Wyw", profile="学园派学员", opponent_name="Cmx")
    team = Team()
    team.hire([Cmx, Wyw])
    team.invest(investment)
    team.run_project(idea, send_to="Cmx")  # send debate topic to Cmx and let him speak first
    await team.run(n_round=n_round)
​
​
def main(idea: str, investment: float = 3.0, n_round: int = 10):
    """
    :param idea: Debate topic, such as "Topic: The U.S. should commit more in climate change fighting"
                 or "Wyw: Climate change is a hoax"
    :param investment: contribute a certain dollar amount to watch the debate
    :param n_round: maximum rounds of the debate
    :return:
    """
    if platform.system() == "Windows":
        asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
    asyncio.run(debate(idea, investment, n_round))
​
​
if __name__ == "__main__":
    fire.Fire(main)

番外篇之使用百度千帆免费模型

模型的配置使用

我们前往:

百度智能云千帆大模型平台

进行一个基本的注册和验证,然后按照默认设置创建自己的模型,点击右上角的头像下方的安全认证注意一定不要复制错了,是安全认证里面的ACCESS KEY和SECRET KEY

然后修改我们的yaml2配置文件,就像这样,我们选用ERNIE-Speed的免费模型

FAQ

如果你是METAGPT V0.66版本,可能会出现以下的警告,警告会出现污染日志的情况,很显然我们想避免这种情况的出现。

这里的第一个是因为,0.66版本的COST计算里面没有添加这个模型,那我们自己手动添加一下。

我们进入token_counter.py,在第一个TOKEN_COSTS底部追加一个

"ERNIE-Speed": {"prompt":0.0,"completion":0.0},

关于第二个,是由于API计算COST引入OPENAI包,OPENAI包设计接口存在某些问题导致的。

我们进入openapi_requestor.py,在这里把这行警告代码注释掉

然后,我们就得到了没有污染和杂质的日志了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值