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
132 lines
3.6 KiB
Python
132 lines
3.6 KiB
Python
"""
|
|
Device Management API — Register and manage phones/softphones.
|
|
"""
|
|
|
|
import uuid
|
|
from datetime import datetime
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from api.deps import get_gateway
|
|
from core.gateway import AIPSTNGateway
|
|
from db.database import Device as DeviceDB
|
|
from db.database import get_db
|
|
from models.device import Device, DeviceCreate, DeviceStatus, DeviceUpdate
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
@router.post("/", response_model=Device)
|
|
async def register_device(
|
|
device: DeviceCreate,
|
|
gateway: AIPSTNGateway = Depends(get_gateway),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Register a new device with the gateway."""
|
|
device_id = f"dev_{uuid.uuid4().hex[:8]}"
|
|
|
|
# Save to DB
|
|
db_device = DeviceDB(
|
|
id=device_id,
|
|
name=device.name,
|
|
type=device.type.value,
|
|
sip_uri=device.sip_uri,
|
|
phone_number=device.phone_number,
|
|
priority=device.priority,
|
|
capabilities=device.capabilities,
|
|
is_online="false",
|
|
)
|
|
db.add(db_device)
|
|
await db.flush()
|
|
|
|
# Register with gateway
|
|
dev = Device(id=device_id, **device.model_dump())
|
|
gateway.register_device(dev)
|
|
|
|
return dev
|
|
|
|
|
|
@router.get("/", response_model=list[DeviceStatus])
|
|
async def list_devices(
|
|
gateway: AIPSTNGateway = Depends(get_gateway),
|
|
):
|
|
"""List all registered devices and their status."""
|
|
return [
|
|
DeviceStatus(
|
|
id=d.id,
|
|
name=d.name,
|
|
type=d.type,
|
|
is_online=d.is_online,
|
|
last_seen=d.last_seen,
|
|
can_receive_call=d.can_receive_call,
|
|
)
|
|
for d in gateway.devices.values()
|
|
]
|
|
|
|
|
|
@router.get("/{device_id}", response_model=Device)
|
|
async def get_device(
|
|
device_id: str,
|
|
gateway: AIPSTNGateway = Depends(get_gateway),
|
|
):
|
|
"""Get a specific device."""
|
|
device = gateway.devices.get(device_id)
|
|
if not device:
|
|
raise HTTPException(status_code=404, detail=f"Device {device_id} not found")
|
|
return device
|
|
|
|
|
|
@router.put("/{device_id}", response_model=Device)
|
|
async def update_device(
|
|
device_id: str,
|
|
update: DeviceUpdate,
|
|
gateway: AIPSTNGateway = Depends(get_gateway),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Update a device."""
|
|
device = gateway.devices.get(device_id)
|
|
if not device:
|
|
raise HTTPException(status_code=404, detail=f"Device {device_id} not found")
|
|
|
|
# Update in-memory
|
|
update_data = update.model_dump(exclude_unset=True)
|
|
for key, value in update_data.items():
|
|
setattr(device, key, value)
|
|
|
|
# Update in DB
|
|
result = await db.execute(
|
|
select(DeviceDB).where(DeviceDB.id == device_id)
|
|
)
|
|
db_device = result.scalar_one_or_none()
|
|
if db_device:
|
|
for key, value in update_data.items():
|
|
if key == "type" and value is not None:
|
|
value = value.value if hasattr(value, "value") else value
|
|
setattr(db_device, key, value)
|
|
|
|
return device
|
|
|
|
|
|
@router.delete("/{device_id}")
|
|
async def unregister_device(
|
|
device_id: str,
|
|
gateway: AIPSTNGateway = Depends(get_gateway),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Unregister a device."""
|
|
if device_id not in gateway.devices:
|
|
raise HTTPException(status_code=404, detail=f"Device {device_id} not found")
|
|
|
|
gateway.unregister_device(device_id)
|
|
|
|
result = await db.execute(
|
|
select(DeviceDB).where(DeviceDB.id == device_id)
|
|
)
|
|
db_device = result.scalar_one_or_none()
|
|
if db_device:
|
|
await db.delete(db_device)
|
|
|
|
return {"status": "unregistered", "device_id": device_id}
|