feat: add initial Hold Slayer AI telephony gateway implementation
Complete project scaffolding and core implementation of an AI-powered telephony system that calls companies, navigates IVR menus, waits on hold, and transfers to the user when a human answers. Key components: - FastAPI server with REST API, WebSocket, and MCP (SSE) interfaces - SIP/VoIP call management via PJSUA2 with RTP audio streaming - LLM-powered IVR navigation using OpenAI/Anthropic with tool calling - Hold detection service combining audio analysis and silence detection - Real-time STT (Whisper/Deepgram) and TTS (OpenAI/Piper) pipelines - Call recording with per-channel and mixed audio capture - Event bus (asyncio pub/sub) for real-time client updates - Web dashboard with live call monitoring - SQLite persistence via SQLAlchemy with call history and analytics - Notification support (email, SMS, webhook, desktop) - Docker Compose deployment with Opal VoIP and Opal Media containers - Comprehensive test suite with unit, integration, and E2E tests - Simplified .gitignore and full project documentation in README
This commit is contained in:
120
core/event_bus.py
Normal file
120
core/event_bus.py
Normal file
@@ -0,0 +1,120 @@
|
||||
"""
|
||||
Event Bus — Async pub/sub for real-time gateway events.
|
||||
|
||||
WebSocket connections, MCP server, and internal services
|
||||
all subscribe to events here. Pure asyncio — no external deps.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from models.events import EventType, GatewayEvent
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EventBus:
|
||||
"""
|
||||
Async pub/sub event bus using asyncio.Queue per subscriber.
|
||||
|
||||
Features:
|
||||
- Non-blocking publish (put_nowait)
|
||||
- Automatic dead-subscriber cleanup (full queues are removed)
|
||||
- Event history (last N events for late joiners)
|
||||
- Typed event filtering on subscriptions
|
||||
- Async iteration via EventSubscription
|
||||
"""
|
||||
|
||||
def __init__(self, max_history: int = 1000):
|
||||
self._subscribers: list[tuple[asyncio.Queue[GatewayEvent], Optional[set[EventType]]]] = []
|
||||
self._history: list[GatewayEvent] = []
|
||||
self._max_history = max_history
|
||||
|
||||
async def publish(self, event: GatewayEvent) -> None:
|
||||
"""Publish an event to all subscribers."""
|
||||
self._history.append(event)
|
||||
if len(self._history) > self._max_history:
|
||||
self._history = self._history[-self._max_history :]
|
||||
|
||||
logger.info(f"📡 Event: {event.type.value} | {event.message or ''}")
|
||||
|
||||
dead_queues = []
|
||||
for queue, type_filter in self._subscribers:
|
||||
# Skip if subscriber has a type filter and this event doesn't match
|
||||
if type_filter and event.type not in type_filter:
|
||||
continue
|
||||
try:
|
||||
queue.put_nowait(event)
|
||||
except asyncio.QueueFull:
|
||||
dead_queues.append((queue, type_filter))
|
||||
|
||||
for entry in dead_queues:
|
||||
self._subscribers.remove(entry)
|
||||
|
||||
def subscribe(
|
||||
self,
|
||||
max_size: int = 100,
|
||||
event_types: Optional[set[EventType]] = None,
|
||||
) -> "EventSubscription":
|
||||
"""
|
||||
Create a new subscription.
|
||||
|
||||
Args:
|
||||
max_size: Queue depth before subscriber is considered dead.
|
||||
event_types: Optional filter — only receive these event types.
|
||||
None means receive everything.
|
||||
|
||||
Returns:
|
||||
An async iterator of GatewayEvents.
|
||||
"""
|
||||
queue: asyncio.Queue[GatewayEvent] = asyncio.Queue(maxsize=max_size)
|
||||
entry = (queue, event_types)
|
||||
self._subscribers.append(entry)
|
||||
return EventSubscription(queue, self, entry)
|
||||
|
||||
def unsubscribe(self, entry: tuple) -> None:
|
||||
"""Remove a subscriber."""
|
||||
if entry in self._subscribers:
|
||||
self._subscribers.remove(entry)
|
||||
|
||||
@property
|
||||
def recent_events(self) -> list[GatewayEvent]:
|
||||
"""Get recent event history."""
|
||||
return list(self._history)
|
||||
|
||||
@property
|
||||
def subscriber_count(self) -> int:
|
||||
return len(self._subscribers)
|
||||
|
||||
|
||||
class EventSubscription:
|
||||
"""An async iterator that yields events from the bus."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
queue: asyncio.Queue[GatewayEvent],
|
||||
bus: EventBus,
|
||||
entry: tuple,
|
||||
):
|
||||
self._queue = queue
|
||||
self._bus = bus
|
||||
self._entry = entry
|
||||
|
||||
def __aiter__(self):
|
||||
return self
|
||||
|
||||
async def __anext__(self) -> GatewayEvent:
|
||||
try:
|
||||
return await self._queue.get()
|
||||
except asyncio.CancelledError:
|
||||
self._bus.unsubscribe(self._entry)
|
||||
raise
|
||||
|
||||
async def get(self, timeout: Optional[float] = None) -> GatewayEvent:
|
||||
"""Get next event with optional timeout."""
|
||||
return await asyncio.wait_for(self._queue.get(), timeout=timeout)
|
||||
|
||||
def close(self):
|
||||
"""Unsubscribe from the event bus."""
|
||||
self._bus.unsubscribe(self._entry)
|
||||
Reference in New Issue
Block a user