传送门:
- 《2024.5组队学习——MetaGPT(0.8.1)智能体理论与实战(上):MetaGPT安装、单智能体开发》
- 《2024.5组队学习——MetaGPT(0.8.1)智能体理论与实战(下):多智能体开发》
订阅智能体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)
- 使用一个无限的
while True
循环来持续检查所有任务的状态。 - 对于
self.tasks
字典中的每个(role, task)
项:- 如果该任务已完成 (
task.done()
为真):- 检查是否有异常 (
task.exception()
)- 如果有异常,并且
raise_exception
参数为真,则抛出该异常 - 如果有异常,并且
raise_exception
参数为假,则使用logger
记录错误日志
- 如果有异常,并且
- 如果没有异常,则使用
logger
记录警告日志,提示任务已完成(可能是不期望的行为)
- 检查是否有异常 (
- 从
self.tasks
字典中移除该任务 - 使用
break
语句退出循环,等待下一次循环
- 如果该任务已完成 (
- 如果在当前循环中没有任何任务完成,则使用
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()
方法时,生成器会从上次暂停的地方继续执行。
而异步生成器则是在异步的上下文中工作的。它使用async
和await
关键字,可以在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
的一些主要特性包括:
- Client/Server模式: 可以作为客户端或服务器使用
- Web框架集成: 可以与现有的web框架(如Django、Flask等)集成
- 中间件: 支持中间件功能,方便插入自定义行为
- WebSocket支持: 支持WebSocket协议
- Gunicorn工作: 可以与Gunicorn集成作为ASGI服务器运行
- 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(