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:
2026-03-21 19:23:26 +00:00
parent c9ff60702b
commit ecf37658ce
56 changed files with 11601 additions and 164 deletions

69
models/events.py Normal file
View File

@@ -0,0 +1,69 @@
"""
Event models — Real-time events published via WebSocket and event bus.
These events drive the dashboard, notifications, and MCP updates.
"""
from datetime import datetime
from enum import Enum
from typing import Any, Optional
from pydantic import BaseModel, Field
class EventType(str, Enum):
"""Types of events the gateway can emit."""
# Call lifecycle
CALL_INITIATED = "call.initiated"
CALL_RINGING = "call.ringing"
CALL_CONNECTED = "call.connected"
CALL_ENDED = "call.ended"
CALL_FAILED = "call.failed"
# Hold Slayer
IVR_STEP = "holdslayer.ivr_step"
IVR_DTMF_SENT = "holdslayer.dtmf_sent"
HOLD_DETECTED = "holdslayer.hold_detected"
HUMAN_DETECTED = "holdslayer.human_detected"
TRANSFER_STARTED = "holdslayer.transfer_started"
TRANSFER_COMPLETE = "holdslayer.transfer_complete"
# Audio
AUDIO_CLASSIFIED = "audio.classified"
TRANSCRIPT_CHUNK = "audio.transcript_chunk"
# Device
DEVICE_REGISTERED = "device.registered"
DEVICE_ONLINE = "device.online"
DEVICE_OFFLINE = "device.offline"
# System
GATEWAY_STARTED = "system.gateway_started"
GATEWAY_STOPPING = "system.gateway_stopping"
ERROR = "system.error"
# SIP Trunk
SIP_TRUNK_REGISTERED = "sip.trunk.registered"
SIP_TRUNK_REGISTRATION_FAILED = "sip.trunk.registration_failed"
SIP_TRUNK_UNREGISTERED = "sip.trunk.unregistered"
class GatewayEvent(BaseModel):
"""A real-time event from the gateway."""
type: EventType
call_id: Optional[str] = None
timestamp: datetime = Field(default_factory=datetime.now)
data: dict[str, Any] = Field(default_factory=dict)
message: Optional[str] = None # Human-readable description
def to_ws_message(self) -> dict:
"""Serialize for WebSocket transmission."""
return {
"type": self.type.value,
"call_id": self.call_id,
"timestamp": self.timestamp.isoformat(),
"data": self.data,
"message": self.message,
}