fischer-agentkit/src/agentkit/bus/memory_bus.py

144 lines
4.8 KiB
Python

"""InMemoryMessageBus — 基于 asyncio.Queue 的内存消息总线。
用于开发和测试,行为与 Redis 实现一致。
"""
from __future__ import annotations
import asyncio
import logging
from typing import Any, Callable, Awaitable
from agentkit.bus.message import AgentMessage
logger = logging.getLogger(__name__)
class InMemoryMessageBus:
"""基于 asyncio.Queue 的内存消息总线。"""
def __init__(self) -> None:
self._subscribers: dict[str, list[Callable[[AgentMessage], Awaitable[None]]]] = {}
self._pending_requests: dict[str, asyncio.Future[AgentMessage]] = {}
self._queues: dict[str, asyncio.Queue[AgentMessage]] = {}
async def publish(self, message: AgentMessage) -> None:
"""发布消息。"""
if message.is_broadcast:
await self.broadcast(message)
return
# Point-to-point: deliver to recipient's queue
recipient = message.recipient
if recipient and recipient in self._queues:
await self._queues[recipient].put(message)
elif recipient and recipient in self._subscribers:
# No queue, call handlers directly
for handler in self._subscribers[recipient]:
try:
await handler(message)
except Exception as e:
logger.warning(f"Handler error for {recipient}: {e}")
# Check if this is a response to a pending request
# Only resolve if this is a reply (message_id != correlation_id),
# not the original request itself
if (
message.correlation_id
and message.correlation_id in self._pending_requests
and message.message_id != message.correlation_id
):
future = self._pending_requests[message.correlation_id]
if not future.done():
future.set_result(message)
async def subscribe(
self,
agent_name: str,
handler: Callable[[AgentMessage], Awaitable[None]],
) -> None:
"""订阅消息。"""
if agent_name not in self._subscribers:
self._subscribers[agent_name] = []
self._queues[agent_name] = asyncio.Queue()
self._subscribers[agent_name].append(handler)
# Start consumer task
asyncio.create_task(self._consume_queue(agent_name, handler))
async def _consume_queue(
self,
agent_name: str,
handler: Callable[[AgentMessage], Awaitable[None]],
) -> None:
"""消费队列中的消息。"""
queue = self._queues.get(agent_name)
if queue is None:
return
while True:
try:
message = await queue.get()
try:
await handler(message)
except Exception as e:
logger.warning(f"Handler error for {agent_name}: {e}")
except asyncio.CancelledError:
break
async def unsubscribe(self, agent_name: str) -> None:
"""取消订阅。"""
self._subscribers.pop(agent_name, None)
self._queues.pop(agent_name, None)
async def request(
self,
message: AgentMessage,
timeout: float = 30.0,
) -> AgentMessage:
"""请求-响应模式。"""
if not message.correlation_id:
message.correlation_id = message.message_id
loop = asyncio.get_event_loop()
future: asyncio.Future[AgentMessage] = loop.create_future()
self._pending_requests[message.correlation_id] = future
try:
await self.publish(message)
return await asyncio.wait_for(future, timeout=timeout)
except asyncio.TimeoutError:
raise TimeoutError(
f"Request {message.correlation_id} timed out after {timeout}s"
)
finally:
self._pending_requests.pop(message.correlation_id, None)
async def broadcast(self, message: AgentMessage) -> None:
"""广播消息。"""
# Ensure recipient is None for broadcast
message.recipient = None
for agent_name, handlers in self._subscribers.items():
for handler in handlers:
try:
await handler(message)
except Exception as e:
logger.warning(f"Broadcast handler error for {agent_name}: {e}")
# Check pending requests (only for replies)
if (
message.correlation_id
and message.correlation_id in self._pending_requests
and message.message_id != message.correlation_id
):
future = self._pending_requests[message.correlation_id]
if not future.done():
future.set_result(message)
async def health_check(self) -> bool:
return True
@property
def backend_type(self) -> str:
return "memory"