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:
69
models/events.py
Normal file
69
models/events.py
Normal 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,
|
||||
}
|
||||
Reference in New Issue
Block a user