2024.5组队学习——MetaGPT(0.8.1)智能体理论与实战(中):订阅智能体OSS实现

传送门:

订阅智能体OSS (Open Source Software)

  当我们持续关注某些感兴趣的信息时,Agent可以实时获取这些信息并进行处理,然后通过一些如邮件、微信、discord等通知渠道将处理后的信息发送给我们,我们将这类Agent称为订阅智能体。此时,Agent的Role可称为“资讯订阅员”的,其包含的Action则主要有两种:

  • 从外界信息源中搜集信息
  • 对搜集得到的信息进行总结

一、 SubscriptionRunner

1.1 源码解析

  我们还可以为开发一些额外功能,比如定时运行和发送通知。在MetaGPT中,metagpt.subscription模块提供了SubscriptionRunner类,它是一个简单的包装器,用于使用asyncio管理不同角色的订阅任务。其主要作用是定时触发运行一个Role,然后将运行结果通知给用户。

class SubscriptionRunner(BaseModel):
	model_config = ConfigDict(arbitrary_types_allowed=True)
	# tasks字典,用于存储每个角色对应的异步任务
	tasks: dict[Role, asyncio.Task] = Field(default_factory=dict)

    async def subscribe(
        self,
        role: Role,
        trigger: AsyncGenerator[Message, None],
        callback: Callable[
            [
                Message,
            ],
            Awaitable[None],
        ],
    ):
        loop = asyncio.get_running_loop()

        async def _start_role():
            async for msg in trigger:
                resp = await role.run(msg)
                await callback(resp)

        self.tasks[role] = loop.create_task(_start_role(), name=f"Subscription-{
     role}")

  subscribe 方法用于订阅一个角色,传入角色、触发器(trigger)和回调函数。它会创建一个异步任务——从触发器获取消息,并将角色处理后的响应传递给回调函数。

async def run(self, raise_exception: bool = True):
    """Runs all subscribed tasks and handles their completion or exception.

    Args:
        raise_exception: _description_. Defaults to True.

    Raises:
        task.exception: _description_
    """
    while True:
        for role, task in self.tasks.items():
            if task.done():
                if task.exception():
                    if raise_exception:
                        raise task.exception()
                    logger.opt(exception=task.exception()).error(f"Task {
     task.get_name()} run error")
                else:
                    logger.warning(
                        f"Task {
     task.get_name()} has completed. "
                        "If this is unexpected behavior, please check the trigger function."
                    )
                self.tasks.pop(role)
                break
        else:
            await asyncio.sleep(1)
  1. 使用一个无限的 while True 循环来持续检查所有任务的状态。
  2. 对于 self.tasks 字典中的每个 (role, task) 项:
    • 如果该任务已完成 (task.done() 为真):
      • 检查是否有异常 (task.exception())
        • 如果有异常,并且 raise_exception 参数为真,则抛出该异常
        • 如果有异常,并且 raise_exception 参数为假,则使用 logger 记录错误日志
      • 如果没有异常,则使用 logger 记录警告日志,提示任务已完成(可能是不期望的行为)
    • self.tasks 字典中移除该任务
    • 使用 break 语句退出循环,等待下一次循环
  3. 如果在当前循环中没有任何任务完成,则使用 await asyncio.sleep(1) 等待1秒,继续下一次循环。

  总的来说,run函数的作用是持续监视所有已订阅的任务,一旦有任务完成(无论是正常完成还是异常),就进行相应的处理,包括抛出异常、记录日志或移除已完成的任务。如果所有任务都还在运行中,它会等待一段时间后继续检查。这确保了所有已订阅的任务都可以持续运行,直到它们完成为止。

1.2 官方文档示例

import asyncio
from metagpt.subscription import SubscriptionRunner
from metagpt.roles import Searcher
from metagpt.schema import Message

async def trigger():
    while True:
        yield Message(content="the latest news about OpenAI")
        await asyncio.sleep(3600 * 24)

async def callback(msg: Message):
    print(msg.content)

async def main():
    pb = SubscriptionRunner()
    await pb.subscribe(Searcher(), trigger(), callback)
    await pb.run()

await main()

  从例子可以知道订阅智能体的实现主要有3个要素,分别是Role、Trigger、Callback,即智能体本身、触发器、数据回调。其中,trigger 是一个无限循环的异步函数:

  • 在循环中,它首先 yield 一个 Message 对象,其 content 属性设置为 “the latest news about OpenAI”。
  • 使用 await asyncio.sleep(3600 * 24) 暂停一天执行
  • 循环以上过程

  所以,trigger 函数的作用是每隔24小时产生一个包含 “the latest news about OpenAI” 内容的 Message 对象。在 main 函数中,trigger() 被传递给 subscribe 方法,作为 Searcher 角色的触发器。每当 trigger 生成一个新的 Message,Searcher 角色就会处理该消息,并将响应传递给 callback 函数打印出来。

  要注意的是,我们虽然不对订阅智能体的Role做限制,但是不是所有的Role都适合用来做订阅智能体,比如MetaGPT软件公司中的几个角色,例如产品经理、架构师、工程师等,因为当给这些Role一个需求输入时,它们的产出往往是类似的,并没有定时执行然后发送给我们能的必要。
  从应用的角度出发,订阅智能体的输出应该具有实时性,相同的一个需求描述输入,输出的内容一般是会随着时间的变化而不同,例如新闻资讯、技术前沿进展、热门的开源项目等。

1.3 Trigger(异步生成器)

  Trigger是个异步生成器(Asynchronous Generators)。在Python中,生成器(Generators)是一种特殊的迭代器,区别于list、dict等类型,它是在需要时才生成数据,而不是一次性把所有数据加载到内存中。这种按需生成的方式可以提高内存使用效率,特别是在处理大量数据或无限数据流时非常有用。

def generate_data(n):
    """生成包含n个整数的生成器"""
    for i in range(n):
        yield i

def process_data(data):
    """对每个数据进行平方运算"""
    for item in data:
        print(item**2)

# 生成一百万个整数的生成器
data_generator = generate_data(1000000)

# 处理生成器中的数据
process_data(data_generator)

  在上面的代码中,generate_data函数是一个生成器,它每次被调用时只生成一个整数,而不是一次性将所有数据生成到内存中,这样就大大减少了内存使用量。

  传统生成器是在同步模式下使用yield关键字来产生值,当生成器函数遇到yield语句时,它会暂停执行并将值产出,下次再次调用next()方法时,生成器会从上次暂停的地方继续执行。

  而异步生成器则是在异步的上下文中工作的。它使用asyncawait关键字,可以在yield语句处暂停执行并等待一个潜在的耗时异步操作完成后再继续。这使得它特别适合于处理异步数据流,比如从网络套接字接收数据、从异步API获取数据等。

import asyncio

async def counter(start, stop):
    n = start
    while n < stop:
        yield n
        n += 1
        await asyncio.sleep(0.5)  # 模拟异步操作

async def main():
    async for i in counter(3, 8):
        print(i)

asyncio.run(main())

# 输出:
# 3
# (暂停0.5秒)
# 4 
# (暂停0.5秒)  
# 5
# ...

  在这个异步生成器中,每次产出一个数值后,它会通过await asyncio.sleep(0.5)模拟一个耗时0.5秒的异步操作,然后再继续执行。这样的异步方式可以避免阻塞主线程,提高程序的响应能力。

1.4 设置aiohttp代理

  aiohttp是一个用于异步 HTTP 客户端/服务器的非常流行的Python库,它基于 asyncio 库构建,可以让你方便地编写单线程并发代码。

  作为一个异步HTTP客户端,aiohttp 允许你发送对服务器的请求而不阻塞事件循环。这意味着你的脚本可以高效地发送多个HTTP请求,无需为每个请求启动新线程。这在需要发起大量HTTP请求时特别有用,比如爬虫、API客户端等。如果你需要编写高度可扩展、高并发的网络应用程序,使用 aiohttp 将是一个不错的选择。

aiohttp 的一些主要特性包括:

  1. Client/Server模式: 可以作为客户端或服务器使用
  2. Web框架集成: 可以与现有的web框架(如Django、Flask等)集成
  3. 中间件: 支持中间件功能,方便插入自定义行为
  4. WebSocket支持: 支持WebSocket协议
  5. Gunicorn工作: 可以与Gunicorn集成作为ASGI服务器运行
  6. URLs构建: 方便构造查询参数或URL编码

  教程涉中需要访问到一些国外的网站,可能会遇到网络问题,因为 aiohttp 默认不走系统代理,所以需要做下代理配置。MetaGPT中已经提供了GLOBAL_PROXY参数用来表示全局代理配置,教程中遇到使用aiohttp进行请求的地方,都会将代理设置为GLOBAL_PROXY的值,所以可以通过在config/key.yaml配置文件中,添加自己代理服务器的配置,以解决网络问题:

GLOBAL_PROXY: http://127.0.0.1:8118  # 改成自己的代理服务器地址

我是在AutoDL上跑的项目,代理设置可参考帖子:《AutoDL 使用代理加速》,讲解了如何在一台服务器命令行中启用 clash 代理。

mkdir mihomo
cd mihomo

# 下载lash 二进制文件并解压
# 最原始的 clash 项目已经删库,这个是目前找到的规模比较大的继任 fork ,二进制文件也更名为 mihomo 
wget https://github.com/MetaCubeX/mihomo/releases/download/v1.18.1/mihomo-linux-amd64-v1.18.1.gz
gzip -d  mihomo-linux-amd64-v1.18.1.gz

# 下载Country.mmdb文件
wget https://github.com/Dreamacro/maxmind-geoip/releases/download/20240512/Country.mmdb
vim config.yaml  # 配置config文件

# 授予执行权限
chmod +x ./mihomo-linux-amd64-v1.18.1

  config文件需要订阅clash服务商获取,比如v2net
  打开Clash Verge v1.3.8,下载clash-verge_1.3.8_amd64.AppImage,然后运行sudo apt install fuse安装FUSE。之后运行chmod +x clash-verge_1.3.8_amd64.AppImage修改此文件权限,最后运行./clash-verge_1.3.8_amd64.AppImage直接启动clash-verge软件。然后参考Set Up V2NET on Linux,就可以配置正确的config.yaml(文章中是apt安装clash-verge_x.x.x_amd64.deb文件,但是我都失败了,.AppImage文件无需安装,可以直接启动。。

最后直接执行

./mihomo-linux-amd64-v1.18.1 -d .   
#这里的 -d 选项很重要,用于设置工作目录为当前所在目录,否则找不到 config.yaml

看到类似如下输出就成功了
在这里插入图片描述
保留这个终端,以使得 mihomo 能持续运行并且监听服务端口。然后新开其他终端,并在新开终端中配置环境变量:

export https_proxy=http://127.0.0.1:7890/
export http_proxy=http://127.0.0.1:7890/

到这一步就能顺利访问到目标网址。测试:

# 如果返回结果中包含HTTP状态码(如200 OK),则表示通过代理访问成功。
curl -I https://www.google.com

也可以使用aiohttp库检测代理:

import aiohttp
import asyncio

async def fetch(url):
    proxy = "http://127.0.0.1:7890"
    async with aiohttp.ClientSession() as session:
        async with session.get(url, proxy=proxy) as response:
            return await response.text()

url = "https://api.github.com"
result = await.fetch(url)  				# notebook中运行
# result = asyncio.run(fetch(url)) 		# .py文件运行
print(
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

神洛华

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值