Add Twilio webhook and queue management endpoints
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,14 +4,16 @@ import uuid
|
|||||||
import asyncio
|
import asyncio
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from fastapi import FastAPI, HTTPException, UploadFile, File
|
from fastapi import FastAPI, HTTPException, UploadFile, File, Form
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from fastapi.responses import FileResponse
|
from fastapi.responses import FileResponse, Response
|
||||||
|
from twilio.twiml.voice_response import VoiceResponse
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from .config import settings
|
from .config import settings
|
||||||
|
from .services.twilio_service import TwilioService
|
||||||
from .services.transcription import transcribe_audio
|
from .services.transcription import transcribe_audio
|
||||||
from .services.llm import llm_service
|
from .services.llm import llm_service
|
||||||
from .services.tts import generate_speech
|
from .services.tts import generate_speech
|
||||||
@@ -395,6 +397,7 @@ class Session:
|
|||||||
|
|
||||||
|
|
||||||
session = Session()
|
session = Session()
|
||||||
|
twilio_service = TwilioService()
|
||||||
|
|
||||||
|
|
||||||
# --- Static Files ---
|
# --- Static Files ---
|
||||||
@@ -761,6 +764,91 @@ async def update_settings(data: dict):
|
|||||||
return llm_service.get_settings()
|
return llm_service.get_settings()
|
||||||
|
|
||||||
|
|
||||||
|
# --- Twilio Webhook & Queue Endpoints ---
|
||||||
|
|
||||||
|
@app.post("/api/twilio/voice")
|
||||||
|
async def twilio_voice_webhook(
|
||||||
|
CallSid: str = Form(...),
|
||||||
|
From: str = Form(...),
|
||||||
|
):
|
||||||
|
"""Handle incoming Twilio call — greet and enqueue"""
|
||||||
|
twilio_service.add_to_queue(CallSid, From)
|
||||||
|
|
||||||
|
response = VoiceResponse()
|
||||||
|
response.say("You're calling Luke at the Roost. Hold tight, we'll get to you.", voice="alice")
|
||||||
|
response.enqueue(
|
||||||
|
"radio_show",
|
||||||
|
wait_url="/api/twilio/hold-music",
|
||||||
|
wait_url_method="POST",
|
||||||
|
)
|
||||||
|
return Response(content=str(response), media_type="application/xml")
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/twilio/hold-music")
|
||||||
|
async def twilio_hold_music():
|
||||||
|
"""Serve hold music TwiML for queued callers"""
|
||||||
|
response = VoiceResponse()
|
||||||
|
response.say("Please hold, you'll be on air shortly.", voice="alice")
|
||||||
|
response.pause(length=30)
|
||||||
|
return Response(content=str(response), media_type="application/xml")
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/queue")
|
||||||
|
async def get_call_queue():
|
||||||
|
"""Get list of callers waiting in queue"""
|
||||||
|
return {"queue": twilio_service.get_queue()}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/queue/take/{call_sid}")
|
||||||
|
async def take_call_from_queue(call_sid: str):
|
||||||
|
"""Take a caller off hold and put them on air"""
|
||||||
|
try:
|
||||||
|
call_info = twilio_service.take_call(call_sid)
|
||||||
|
except ValueError as e:
|
||||||
|
raise HTTPException(404, str(e))
|
||||||
|
|
||||||
|
session.active_real_caller = {
|
||||||
|
"call_sid": call_info["call_sid"],
|
||||||
|
"phone": call_info["phone"],
|
||||||
|
"channel": call_info["channel"],
|
||||||
|
"name": call_info["name"],
|
||||||
|
}
|
||||||
|
|
||||||
|
# Connect Twilio media stream by updating the call
|
||||||
|
from twilio.rest import Client as TwilioClient
|
||||||
|
if settings.twilio_account_sid and settings.twilio_auth_token:
|
||||||
|
client = TwilioClient(settings.twilio_account_sid, settings.twilio_auth_token)
|
||||||
|
twiml = VoiceResponse()
|
||||||
|
connect = twiml.connect()
|
||||||
|
connect.stream(
|
||||||
|
url=f"wss://{settings.twilio_webhook_base_url.replace('https://', '')}/api/twilio/stream",
|
||||||
|
name=call_sid,
|
||||||
|
)
|
||||||
|
client.calls(call_sid).update(twiml=str(twiml))
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "on_air",
|
||||||
|
"caller": call_info,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/queue/drop/{call_sid}")
|
||||||
|
async def drop_from_queue(call_sid: str):
|
||||||
|
"""Drop a caller from the queue"""
|
||||||
|
twilio_service.remove_from_queue(call_sid)
|
||||||
|
|
||||||
|
# Hang up the Twilio call
|
||||||
|
from twilio.rest import Client as TwilioClient
|
||||||
|
if settings.twilio_account_sid and settings.twilio_auth_token:
|
||||||
|
try:
|
||||||
|
client = TwilioClient(settings.twilio_account_sid, settings.twilio_auth_token)
|
||||||
|
client.calls(call_sid).update(status="completed")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[Twilio] Failed to end call {call_sid}: {e}")
|
||||||
|
|
||||||
|
return {"status": "dropped"}
|
||||||
|
|
||||||
|
|
||||||
# --- Server Control Endpoints ---
|
# --- Server Control Endpoints ---
|
||||||
|
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|||||||
Reference in New Issue
Block a user