Adds read-only access to persisted call records for the dashboard and implements a client for the Rhema text-to-speech service. - api/call_history.py: New router providing paged call lists and detailed call records with transcript metadata. - services/tts.py: Async client for OpenAI-compatible TTS endpoints (Rhema/Kokoro) used for call-flow steps.
85 lines
2.3 KiB
Python
85 lines
2.3 KiB
Python
"""
|
|
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
|
|
dnd: 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.dnd:
|
|
return False
|
|
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
|