Multi-agent AI automation system with shared message bus, specialized roles (coder/researcher/reviewer), and deny-by-default security. - Config system with Pydantic validation and YAML loading - Async message bus with inter-agent delegation - LLM providers: Anthropic (Claude) and LiteLLM (DeepSeek/Kimi/MiniMax) - Tool system: registry, builtins (file/bash/web), approval engine, MCP client - Agent engine with tool-calling loop and orchestrator for multi-agent management - CLI channel (REPL) and Discord channel - Docker + Dockge deployment config - Typer CLI: chat, serve, status, agents commands Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
93 lines
2.9 KiB
Python
93 lines
2.9 KiB
Python
"""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 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)
|
|
|
|
|
|
@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
|