fischer-agentkit/src/agentkit/core/handoff_transport.py

253 lines
9.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""HandoffTransport - Agent 间 Handoff 传输层抽象
提供统一的传输接口,支持:
- InProcessHandoffTransport: 进程内 asyncio.Queue 传输Expert Team 模式)
- RedisHandoffTransport: Redis Pub/Sub 传输(分布式模式)
"""
from __future__ import annotations
import asyncio
import json
import logging
import os
from typing import AsyncIterator, Awaitable, Callable
import redis.asyncio as aioredis
logger = logging.getLogger(__name__)
class HandoffTransport:
"""Handoff 传输层协议
定义 Agent 间 Handoff 消息的传输接口,支持多种底层实现。
"""
async def send(self, channel: str, message: dict) -> None:
"""发送消息到指定频道"""
...
async def listen(self, channel: str) -> AsyncIterator[dict]:
"""监听指定频道的消息(异步生成器)"""
...
yield
def register_handler(self, channel: str, handler: Callable[[dict], Awaitable[None]]) -> None:
"""注册消息处理器"""
...
class InProcessHandoffTransport:
"""进程内 Handoff 传输
基于 asyncio.Queue 实现的进程内消息传输,适用于 Expert Team 模式下
同一进程内的 Agent 间 Handoff无需 Redis 依赖。
支持广播模式:同一频道可有多个消费者,每条消息会投递到所有消费者。
"""
_CLOSED_SENTINEL: dict = {"_closed": True}
_MAX_QUEUE_SIZE: int = 1024
def __init__(self) -> None:
self._channels: dict[str, list[asyncio.Queue[dict]]] = {}
self._handlers: dict[str, list[Callable[[dict], Awaitable[None]]]] = {}
self._closed = False
async def send(self, channel: str, message: dict) -> None:
"""发送消息到指定频道
将消息放入该频道所有消费者的队列中,并调用所有已注册的处理器。
如果频道不存在(无消费者),消息将被丢弃。
"""
queues = self._channels.get(channel, [])
for queue in queues:
await queue.put(message)
handlers = self._handlers.get(channel, [])
for handler in handlers:
try:
await handler(message)
except Exception as e:
logger.error(f"InProcessHandoffTransport handler error on channel '{channel}': {e}")
async def listen(self, channel: str) -> AsyncIterator[dict]: # type: ignore[override]
"""监听指定频道的消息
为调用者创建一个独立的队列,通过异步生成器持续产出消息。
每个调用者获得独立的队列,实现广播语义。
"""
queue: asyncio.Queue[dict] = asyncio.Queue(maxsize=self._MAX_QUEUE_SIZE)
if channel not in self._channels:
self._channels[channel] = []
self._channels[channel].append(queue)
try:
while True:
message = await queue.get()
if message is self._CLOSED_SENTINEL:
break
yield message
finally:
# 清理:移除当前消费者的队列
if channel in self._channels:
try:
self._channels[channel].remove(queue)
except ValueError:
pass
if not self._channels[channel]:
del self._channels[channel]
def register_handler(self, channel: str, handler: Callable[[dict], Awaitable[None]]) -> None:
"""注册消息处理器
当频道收到消息时,处理器将被调用。
"""
if channel not in self._handlers:
self._handlers[channel] = []
self._handlers[channel].append(handler)
def close(self) -> None:
"""关闭传输,清理所有频道和处理器。
向所有活跃的监听器队列放入哨兵值,使其能够优雅退出。
"""
self._closed = True
# Put sentinel into every queue so active listeners unblock and exit
for channel_queues in self._channels.values():
for queue in channel_queues:
try:
queue.put_nowait(self._CLOSED_SENTINEL)
except asyncio.QueueFull:
pass
self._channels.clear()
self._handlers.clear()
class RedisHandoffTransport:
"""Redis Pub/Sub Handoff 传输
基于 Redis Pub/Sub 实现的分布式消息传输,适用于跨进程、跨节点的
Agent 间 Handoff。
使用懒连接:在首次使用时才建立 Redis 连接,而非初始化时。
"""
def __init__(self, redis_url: str) -> None:
self._redis_url = redis_url
self._redis: aioredis.Redis | None = None
self._active_pubsubs: list[aioredis.client.PubSub] = []
self._handlers: dict[str, list[Callable[[dict], Awaitable[None]]]] = {}
self._listen_tasks: dict[str, asyncio.Task] = {}
async def _ensure_connection(self) -> aioredis.Redis:
"""确保 Redis 连接已建立(懒初始化)"""
if self._redis is None:
try:
# DEPLOY-2: 显式传入 REDIS_PASSWORD env varHelm 注入但 redis-py 不自动读取)。
self._redis = aioredis.from_url(
self._redis_url,
decode_responses=True,
password=os.environ.get("REDIS_PASSWORD") or None,
)
await self._redis.ping()
logger.info("RedisHandoffTransport connected to Redis")
except Exception:
# Reset on failure so future calls retry
self._redis = None
raise
return self._redis
async def send(self, channel: str, message: dict) -> None:
"""发送消息到指定频道
通过 Redis PUBLISH 命令将消息发布到频道。
"""
redis = await self._ensure_connection()
await redis.publish(channel, json.dumps(message))
logger.debug(f"RedisHandoffTransport sent message to channel '{channel}'")
async def listen(self, channel: str) -> AsyncIterator[dict]: # type: ignore[override]
"""监听指定频道的消息
订阅 Redis 频道,通过异步生成器持续产出消息。
"""
redis = await self._ensure_connection()
pubsub = redis.pubsub()
await pubsub.subscribe(channel)
self._active_pubsubs.append(pubsub)
try:
async for message in pubsub.listen():
if message["type"] == "message":
data = message["data"]
if isinstance(data, str):
yield json.loads(data)
else:
yield json.loads(data.decode())
except asyncio.CancelledError:
pass
finally:
await pubsub.unsubscribe(channel)
await pubsub.aclose()
if pubsub in self._active_pubsubs:
self._active_pubsubs.remove(pubsub)
def register_handler(self, channel: str, handler: Callable[[dict], Awaitable[None]]) -> None:
"""注册消息处理器并启动后台监听任务
注册处理器后,自动启动一个后台 asyncio.Task 来监听该频道的消息。
"""
if channel not in self._handlers:
self._handlers[channel] = []
self._handlers[channel].append(handler)
# 启动后台监听任务
if channel not in self._listen_tasks:
self._listen_tasks[channel] = asyncio.create_task(self._background_listen(channel))
async def _background_listen(self, channel: str) -> None:
"""后台监听频道消息并调用处理器"""
try:
async for message in self.listen(channel):
handlers = self._handlers.get(channel, [])
for handler in handlers:
try:
await handler(message)
except Exception as e:
logger.error(
f"RedisHandoffTransport handler error on channel '{channel}': {e}"
)
except asyncio.CancelledError:
pass
except Exception as e:
logger.error(f"RedisHandoffTransport listen error on channel '{channel}': {e}")
async def close(self) -> None:
"""关闭传输,取消所有监听任务并关闭 Redis 连接"""
# 取消所有后台监听任务
for task in self._listen_tasks.values():
if not task.done():
task.cancel()
try:
await task
except asyncio.CancelledError:
pass
self._listen_tasks.clear()
self._handlers.clear()
# 关闭所有活跃的 PubSub 连接
for pubsub in self._active_pubsubs:
try:
await pubsub.unsubscribe()
await pubsub.aclose()
except Exception:
pass
self._active_pubsubs.clear()
# 关闭 Redis 连接
if self._redis is not None:
await self._redis.close()
self._redis = None