"""Shared message bus — async queues for inter-component communication.""" from __future__ import annotations import asyncio from dataclasses import dataclass, field from datetime import datetime, timezone from typing import Any @dataclass class Attachment: """A text-extracted attachment from a user message.""" filename: str content: str @dataclass class InboundMessage: """Message from a channel (user) heading to an agent.""" channel: str sender_id: str chat_id: str content: str target_agent: str | None = None timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) metadata: dict[str, Any] = field(default_factory=dict) attachments: list[Attachment] = field(default_factory=list) @dataclass class OutboundMessage: """Message from an agent heading back to a channel.""" channel: str chat_id: str content: str reply_to: str | None = None metadata: dict[str, Any] = field(default_factory=dict) @dataclass class AgentMessage: """Inter-agent delegation message.""" from_agent: str to_agent: str task: str request_id: str = "" response: str | None = None class MessageBus: """Async queue-based message bus.""" def __init__(self) -> None: self.inbound: asyncio.Queue[InboundMessage] = asyncio.Queue() self.outbound: asyncio.Queue[OutboundMessage] = asyncio.Queue() self.agent_messages: asyncio.Queue[AgentMessage] = asyncio.Queue() self._outbound_subscribers: dict[str, list[asyncio.Queue[OutboundMessage]]] = {} async def publish_inbound(self, msg: InboundMessage) -> None: await self.inbound.put(msg) async def consume_inbound(self, timeout: float = 1.0) -> InboundMessage | None: try: return await asyncio.wait_for(self.inbound.get(), timeout=timeout) except asyncio.TimeoutError: return None async def publish_outbound(self, msg: OutboundMessage) -> None: await self.outbound.put(msg) # Also dispatch to channel-specific subscribers channel_queues = self._outbound_subscribers.get(msg.channel, []) for q in channel_queues: await q.put(msg) async def consume_outbound(self, timeout: float = 1.0) -> OutboundMessage | None: try: return await asyncio.wait_for(self.outbound.get(), timeout=timeout) except asyncio.TimeoutError: return None def subscribe_outbound(self, channel: str) -> asyncio.Queue[OutboundMessage]: """Subscribe to outbound messages for a specific channel.""" q: asyncio.Queue[OutboundMessage] = asyncio.Queue() self._outbound_subscribers.setdefault(channel, []).append(q) return q async def publish_agent_message(self, msg: AgentMessage) -> None: await self.agent_messages.put(msg) async def consume_agent_message( self, timeout: float = 1.0 ) -> AgentMessage | None: try: return await asyncio.wait_for(self.agent_messages.get(), timeout=timeout) except asyncio.TimeoutError: return None