Remove Twilio endpoints and dependencies

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-05 15:48:12 -07:00
parent 3961cfc9d4
commit 06f334359e
2 changed files with 21 additions and 154 deletions

View File

@@ -15,12 +15,6 @@ class Settings(BaseSettings):
openrouter_api_key: str = os.getenv("OPENROUTER_API_KEY", "") openrouter_api_key: str = os.getenv("OPENROUTER_API_KEY", "")
inworld_api_key: str = os.getenv("INWORLD_API_KEY", "") inworld_api_key: str = os.getenv("INWORLD_API_KEY", "")
# Twilio Settings
twilio_account_sid: str = os.getenv("TWILIO_ACCOUNT_SID", "")
twilio_auth_token: str = os.getenv("TWILIO_AUTH_TOKEN", "")
twilio_phone_number: str = os.getenv("TWILIO_PHONE_NUMBER", "")
twilio_webhook_base_url: str = os.getenv("TWILIO_WEBHOOK_BASE_URL", "")
# LLM Settings # LLM Settings
llm_provider: str = "openrouter" # "openrouter" or "ollama" llm_provider: str = "openrouter" # "openrouter" or "ollama"
openrouter_model: str = "anthropic/claude-3-haiku" openrouter_model: str = "anthropic/claude-3-haiku"

View File

@@ -4,13 +4,10 @@ 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, Form, WebSocket, WebSocketDisconnect from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse, Response from fastapi.responses import FileResponse
from twilio.twiml.voice_response import VoiceResponse
import json import json
import base64
import audioop
import time import time
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel from pydantic import BaseModel
@@ -670,9 +667,9 @@ async def text_to_speech(request: TTSRequest):
# Also send to active real callers so they hear the AI # Also send to active real callers so they hear the AI
if session.active_real_caller: if session.active_real_caller:
call_sid = session.active_real_caller["call_sid"] caller_id = session.active_real_caller["caller_id"]
asyncio.create_task( asyncio.create_task(
caller_service.send_audio_to_caller(call_sid, audio_bytes, 24000) caller_service.send_audio_to_caller(caller_id, audio_bytes, 24000)
) )
return {"status": "playing", "duration": len(audio_bytes) / 2 / 24000} return {"status": "playing", "duration": len(audio_bytes) / 2 / 24000}
@@ -780,34 +777,7 @@ async def update_settings(data: dict):
return llm_service.get_settings() return llm_service.get_settings()
# --- Twilio Webhook & Queue Endpoints --- # --- 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"""
caller_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") @app.get("/api/queue")
async def get_call_queue(): async def get_call_queue():
@@ -815,32 +785,22 @@ async def get_call_queue():
return {"queue": caller_service.get_queue()} return {"queue": caller_service.get_queue()}
@app.post("/api/queue/take/{call_sid}") @app.post("/api/queue/take/{caller_id}")
async def take_call_from_queue(call_sid: str): async def take_call_from_queue(caller_id: str):
"""Take a caller off hold and put them on air""" """Take a caller off hold and put them on air"""
try: try:
call_info = caller_service.take_call(call_sid) call_info = caller_service.take_call(caller_id)
except ValueError as e: except ValueError as e:
raise HTTPException(404, str(e)) raise HTTPException(404, str(e))
session.active_real_caller = { session.active_real_caller = {
"call_sid": call_info["call_sid"], "caller_id": call_info["caller_id"],
"phone": call_info["phone"],
"channel": call_info["channel"], "channel": call_info["channel"],
"name": call_info["name"], "name": call_info["name"],
} }
# Connect Twilio media stream by updating the call # Notify caller they're on air via WebSocket
from twilio.rest import Client as TwilioClient await caller_service.notify_caller(caller_id, {"status": "on_air", "channel": call_info["channel"]})
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 { return {
"status": "on_air", "status": "on_air",
@@ -848,97 +808,17 @@ async def take_call_from_queue(call_sid: str):
} }
@app.post("/api/queue/drop/{call_sid}") @app.post("/api/queue/drop/{caller_id}")
async def drop_from_queue(call_sid: str): async def drop_from_queue(caller_id: str):
"""Drop a caller from the queue""" """Drop a caller from the queue"""
caller_service.remove_from_queue(call_sid) caller_service.remove_from_queue(caller_id)
await caller_service.disconnect_caller(caller_id)
# 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"} return {"status": "dropped"}
# --- Twilio WebSocket Media Stream --- async def _handle_real_caller_transcription(caller_id: str, pcm_data: bytes, sample_rate: int):
@app.websocket("/api/twilio/stream")
async def twilio_media_stream(websocket: WebSocket):
"""Handle Twilio Media Streams WebSocket — bidirectional audio"""
await websocket.accept()
print("[Twilio WS] Media stream connected")
call_sid = None
stream_sid = None
audio_buffer = bytearray()
CHUNK_DURATION_S = 3 # Transcribe every 3 seconds of audio
MULAW_SAMPLE_RATE = 8000
chunk_samples = CHUNK_DURATION_S * MULAW_SAMPLE_RATE
try:
while True:
data = await websocket.receive_text()
msg = json.loads(data)
event = msg.get("event")
if event == "start":
stream_sid = msg["start"]["streamSid"]
call_sid = msg["start"]["callSid"]
caller_service.register_websocket(call_sid, websocket)
if call_sid in caller_service.active_calls:
caller_service.active_calls[call_sid]["stream_sid"] = stream_sid
print(f"[Twilio WS] Stream started: {stream_sid} for call {call_sid}")
elif event == "media":
# Decode mulaw audio from base64
payload = base64.b64decode(msg["media"]["payload"])
# Convert mulaw to 16-bit PCM
pcm_data = audioop.ulaw2lin(payload, 2)
audio_buffer.extend(pcm_data)
# Get channel for this caller
call_info = caller_service.active_calls.get(call_sid)
if call_info:
channel = call_info["channel"]
# Route PCM to the caller's dedicated Loopback channel
audio_service.route_real_caller_audio(pcm_data, channel, MULAW_SAMPLE_RATE)
# When we have enough audio, transcribe
if len(audio_buffer) >= chunk_samples * 2: # 2 bytes per sample
pcm_chunk = bytes(audio_buffer[:chunk_samples * 2])
audio_buffer = audio_buffer[chunk_samples * 2:]
# Transcribe in background
asyncio.create_task(
_handle_real_caller_transcription(call_sid, pcm_chunk, MULAW_SAMPLE_RATE)
)
elif event == "stop":
print(f"[Twilio WS] Stream stopped: {stream_sid}")
break
except WebSocketDisconnect:
print(f"[Twilio WS] Disconnected: {call_sid}")
except Exception as e:
print(f"[Twilio WS] Error: {e}")
finally:
if call_sid:
caller_service.unregister_websocket(call_sid)
# Transcribe any remaining audio
if audio_buffer and call_sid:
asyncio.create_task(
_handle_real_caller_transcription(call_sid, bytes(audio_buffer), MULAW_SAMPLE_RATE)
)
async def _handle_real_caller_transcription(call_sid: str, pcm_data: bytes, sample_rate: int):
"""Transcribe a chunk of real caller audio and add to conversation""" """Transcribe a chunk of real caller audio and add to conversation"""
call_info = caller_service.active_calls.get(call_sid) call_info = caller_service.active_calls.get(caller_id)
if not call_info: if not call_info:
return return
@@ -1017,7 +897,7 @@ async def hangup_real_caller():
if not session.active_real_caller: if not session.active_real_caller:
raise HTTPException(400, "No active real caller") raise HTTPException(400, "No active real caller")
call_sid = session.active_real_caller["call_sid"] caller_id = session.active_real_caller["caller_id"]
caller_name = session.active_real_caller["name"] caller_name = session.active_real_caller["name"]
# Summarize the conversation # Summarize the conversation
@@ -1039,16 +919,9 @@ async def hangup_real_caller():
transcript=list(session.conversation), transcript=list(session.conversation),
)) ))
# End the Twilio call # Disconnect the caller
caller_service.hangup(call_sid) caller_service.hangup(caller_id)
await caller_service.disconnect_caller(caller_id)
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: {e}")
session.active_real_caller = None session.active_real_caller = None