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
70 lines
2.0 KiB
Python
70 lines
2.0 KiB
Python
"""
|
|
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,
|
|
}
|