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:
119
config.py
Normal file
119
config.py
Normal file
@@ -0,0 +1,119 @@
|
||||
"""
|
||||
Hold Slayer Gateway — Configuration
|
||||
|
||||
All settings loaded from environment variables / .env file.
|
||||
"""
|
||||
|
||||
from pydantic import Field
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
|
||||
class SIPTrunkSettings(BaseSettings):
|
||||
"""SIP trunk provider configuration."""
|
||||
|
||||
model_config = SettingsConfigDict(env_prefix="SIP_TRUNK_")
|
||||
|
||||
host: str = "sip.provider.com"
|
||||
port: int = 5060
|
||||
username: str = ""
|
||||
password: str = ""
|
||||
transport: str = "udp" # udp, tcp, tls
|
||||
did: str = "" # Your phone number (E.164)
|
||||
|
||||
|
||||
class GatewaySIPSettings(BaseSettings):
|
||||
"""Gateway SIP listener for device registration."""
|
||||
|
||||
model_config = SettingsConfigDict(env_prefix="GATEWAY_SIP_")
|
||||
|
||||
host: str = "0.0.0.0"
|
||||
port: int = 5060
|
||||
domain: str = "gateway.local"
|
||||
|
||||
|
||||
class SpeachesSettings(BaseSettings):
|
||||
"""Speaches STT service configuration."""
|
||||
|
||||
model_config = SettingsConfigDict(env_prefix="SPEACHES_")
|
||||
|
||||
url: str = "http://localhost:22070"
|
||||
prod_url: str = "http://localhost:22070"
|
||||
model: str = "whisper-large-v3"
|
||||
|
||||
|
||||
class ClassifierSettings(BaseSettings):
|
||||
"""Audio classifier thresholds."""
|
||||
|
||||
model_config = SettingsConfigDict(env_prefix="CLASSIFIER_")
|
||||
|
||||
music_threshold: float = 0.7
|
||||
speech_threshold: float = 0.6
|
||||
silence_threshold: float = 0.85
|
||||
window_seconds: float = 3.0
|
||||
|
||||
|
||||
class LLMSettings(BaseSettings):
|
||||
"""LLM service configuration (OpenAI-compatible API)."""
|
||||
|
||||
model_config = SettingsConfigDict(env_prefix="LLM_")
|
||||
|
||||
base_url: str = "http://localhost:11434/v1"
|
||||
model: str = "llama3"
|
||||
api_key: str = "not-needed"
|
||||
timeout: float = 30.0
|
||||
max_tokens: int = 1024
|
||||
temperature: float = 0.3
|
||||
|
||||
|
||||
class HoldSlayerSettings(BaseSettings):
|
||||
"""Hold Slayer behavior settings."""
|
||||
|
||||
model_config = SettingsConfigDict(env_prefix="HOLD_SLAYER_", env_prefix_allow_empty=True)
|
||||
|
||||
default_transfer_device: str = Field(
|
||||
default="sip_phone", validation_alias="DEFAULT_TRANSFER_DEVICE"
|
||||
)
|
||||
max_hold_time: int = Field(default=7200, validation_alias="MAX_HOLD_TIME")
|
||||
hold_check_interval: float = Field(default=2.0, validation_alias="HOLD_CHECK_INTERVAL")
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
"""Root application settings."""
|
||||
|
||||
model_config = SettingsConfigDict(
|
||||
env_file=".env",
|
||||
env_file_encoding="utf-8",
|
||||
extra="ignore",
|
||||
)
|
||||
|
||||
# Database
|
||||
database_url: str = "postgresql+asyncpg://holdslayer:changeme@localhost:5432/holdslayer"
|
||||
|
||||
# Server
|
||||
host: str = "0.0.0.0"
|
||||
port: int = 8000
|
||||
debug: bool = True
|
||||
log_level: str = "info"
|
||||
|
||||
# Notifications
|
||||
notify_sms_number: str = ""
|
||||
|
||||
# Sub-configs
|
||||
sip_trunk: SIPTrunkSettings = Field(default_factory=SIPTrunkSettings)
|
||||
gateway_sip: GatewaySIPSettings = Field(default_factory=GatewaySIPSettings)
|
||||
speaches: SpeachesSettings = Field(default_factory=SpeachesSettings)
|
||||
classifier: ClassifierSettings = Field(default_factory=ClassifierSettings)
|
||||
llm: LLMSettings = Field(default_factory=LLMSettings)
|
||||
hold_slayer: HoldSlayerSettings = Field(default_factory=HoldSlayerSettings)
|
||||
|
||||
|
||||
# Singleton
|
||||
_settings: Settings | None = None
|
||||
|
||||
|
||||
def get_settings() -> Settings:
|
||||
"""Get cached application settings."""
|
||||
global _settings
|
||||
if _settings is None:
|
||||
_settings = Settings()
|
||||
return _settings
|
||||
Reference in New Issue
Block a user