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

81
models/device.py Normal file
View File

@@ -0,0 +1,81 @@
"""
Device models — SIP phones, softphones, cell phones.
Devices register with the gateway and can receive transferred calls.
"""
from datetime import datetime
from enum import Enum
from typing import Optional
from pydantic import BaseModel, Field
class DeviceType(str, Enum):
"""Types of devices that can connect to the gateway."""
SIP_PHONE = "sip_phone" # Hardware SIP phone
SOFTPHONE = "softphone" # Software SIP client
CELL = "cell" # Cell phone (reached via PSTN trunk)
TABLET = "tablet" # Tablet with SIP client
WEBRTC = "webrtc" # Browser-based WebRTC client
class DeviceBase(BaseModel):
"""Shared device fields."""
name: str # "Office SIP Phone"
type: DeviceType
extension: Optional[int] = None # 221-299, auto-assigned if omitted
sip_uri: Optional[str] = None # sip:robert@gateway.helu.ca
phone_number: Optional[str] = None # For PSTN devices (E.164)
priority: int = 10 # Routing priority (lower = higher priority)
capabilities: list[str] = Field(default_factory=lambda: ["voice"])
class Device(DeviceBase):
"""Full device model."""
id: str
is_online: bool = False
last_seen: Optional[datetime] = None
created_at: Optional[datetime] = None
updated_at: Optional[datetime] = None
@property
def can_receive_call(self) -> bool:
"""Can this device receive a call right now?"""
if self.type in (DeviceType.SIP_PHONE, DeviceType.SOFTPHONE, DeviceType.WEBRTC):
return self.is_online and self.sip_uri is not None
if self.type == DeviceType.CELL:
return self.phone_number is not None
return False
class DeviceCreate(DeviceBase):
"""Request model for registering a new device."""
pass
class DeviceUpdate(BaseModel):
"""Request model for updating a device."""
name: Optional[str] = None
type: Optional[DeviceType] = None
extension: Optional[int] = None
sip_uri: Optional[str] = None
phone_number: Optional[str] = None
priority: Optional[int] = None
capabilities: Optional[list[str]] = None
class DeviceStatus(BaseModel):
"""Lightweight device status for list views."""
id: str
name: str
type: DeviceType
is_online: bool
last_seen: Optional[datetime] = None
can_receive_call: bool