After-hours calls answered, zero hallucinated answers
Built for
SG CPA, a certified public accounting firm in Plano, Texas
Project
SG CPA voice receptionist
Twilio hands each utterance to FastAPI, Claude answers from a strict business-facts prompt, ElevenLabs speaks it, and Polly takes over if ElevenLabs fails.
Demo
Incoming call from Caller
Live signal
awaiting first turn…
- turn
- 0/11
- agent replies
- 0
Illustrative call, authored from the real system prompt.
Illustrative transcript authored from the agent's real system prompt; no model call.
How it works
01
Twilio Gather webhooks instead of media streams: simpler, cheaper, good enough for Q&A calls.
02
System prompt is templated from business_info.json so the same code serves any firm.
03
Claude called with max 256 tokens and temperature 0.3 to keep answers short and literal.
04
ElevenLabs TTS with automatic Polly fallback so a vendor outage never drops a call.
05
Health endpoint and Railway config for one-command deploys.
Stack and code
- Python
- FastAPI
- Twilio Voice
- Anthropic Claude
- ElevenLabs
- Amazon Polly
- pytest
- Railway
system_prompt.txt— The templated system prompt.
You are the AI phone receptionist for {business_name}, a {industry} firm.
Your job is to answer inbound phone calls and help callers with basic business information. You must be professional, warm, and concise.
BUSINESS INFORMATION:
- Business Name: {business_name}
- Address: {address}
- Hours of Operation: Monday through Friday, 9:00 AM to 5:00 PM CST. Closed Saturday and Sunday.
- Services: {services}
RESPONSE RULES:
- Keep answers to 1-2 sentences maximum. Callers are listening, not reading.
- Use a professional, friendly tone appropriate for a CPA firm.
- Only provide information listed above. Do not speculate or make up information.
- If asked about pricing, appointments, or specific tax questions, say: "I'd be happy to help with that. Please call us during business hours, Monday through Friday, 9 AM to 5 PM, and one of our team members can assist you."
- If the caller's question is unclear, politely ask them to repeat it.
- Do not discuss topics unrelated to the business.
~/Documents/SG_Receptionist/data/system_prompt.txt
claude_client.py— Claude client with a safe fallback response.
import logging
from typing import Optional
import anthropic
logger = logging.getLogger(__name__)
FALLBACK_RESPONSE = (
"I'm sorry, I'm having trouble right now. "
"Our office hours are Monday through Friday, 9 AM to 5 PM. "
"Please call back during business hours for assistance."
)
class ClaudeClient:
def __init__(self, api_key: str, system_prompt: str):
self.client = anthropic.AsyncAnthropic(api_key=api_key)
self.system_prompt = system_prompt
async def generate_response(
self,
user_message: str,
conversation_history: Optional[list] = None,
) -> str:
messages = list(conversation_history or [])
messages.append({"role": "user", "content": user_message})
try:
response = await self.client.messages.create(
model="claude-sonnet-4-6",
max_tokens=256,
temperature=0.3,
system=self.system_prompt,
messages=messages,
)
for block in response.content:
if block.type == "text":
return block.text
return FALLBACK_RESPONSE
except Exception:
logger.exception("Claude API error")
return FALLBACK_RESPONSE
~/Documents/SG_Receptionist/app/claude_client.py
voice_handler.py— TwiML generation and TTS fallback.
import logging
import hashlib
from pathlib import Path
from typing import Dict, List
from twilio.twiml.voice_response import VoiceResponse, Gather
logger = logging.getLogger(__name__)
AUDIO_DIR = Path("/tmp/sg_cpa_audio")
AUDIO_DIR.mkdir(exist_ok=True)
def save_audio_temp(audio_bytes: bytes, call_sid: str) -> str:
filename = f"resp_{hashlib.md5(call_sid.encode() + audio_bytes[:16]).hexdigest()[:12]}.mp3"
path = AUDIO_DIR / filename
path.write_bytes(audio_bytes)
return f"/audio/{filename}"
class VoiceHandler:
def __init__(self, app_base_url: str, claude_client, elevenlabs_client):
self.app_base_url = app_base_url
self.claude_client = claude_client
self.elevenlabs_client = elevenlabs_client
self.conversations: Dict[str, List[dict]] = {}
def build_greeting(self) -> VoiceResponse:
response = VoiceResponse()
gather = Gather(
input="speech",
action="/voice/respond",
method="POST",
speech_timeout="5",
language="en-US",
)
gather.say(
"Thank you for calling SG CPA. How can I help you today?",
voice="Polly.Joanna",
)
response.append(gather)
response.say(
"I didn't catch that. Thank you for calling SG CPA. Goodbye.",
voice="Polly.Joanna",
)
return response
async def handle_speech(self, speech_text: str, call_sid: str) -> VoiceResponse:
history = self.conversations.get(call_sid, [])
ai_response = await self.claude_client.generate_response(
speech_text, conversation_history=history
)
history.append({"role": "user", "content": speech_text})
history.append({"role": "assistant", "content": ai_response})
self.conversations[call_sid] = history
response = VoiceResponse()
audio_bytes = await self.elevenlabs_client.synthesize(ai_response)
if audio_bytes:
audio_path = save_audio_temp(audio_bytes, call_sid)
audio_url = f"{self.app_base_url}{audio_path}"
gather = Gather(
input="speech",
action="/voice/respond",
method="POST",
speech_timeout="5",
language="en-US",
)
gather.play(audio_url)
response.append(gather)
else:
logger.warning("ElevenLabs failed, falling back to Twilio TTS")
gather = Gather(
input="speech",
action="/voice/respond",
method="POST",
speech_timeout="5",
language="en-US",
)
gather.say(ai_response, voice="Polly.Joanna")
response.append(gather)
response.say(
"Thank you for calling SG CPA. Have a great day. Goodbye.",
voice="Polly.Joanna",
)
return response
def build_goodbye(self) -> VoiceResponse:
response = VoiceResponse()
response.say(
"Thank you for calling SG CPA. Have a great day. Goodbye.",
voice="Polly.Joanna",
)
response.hangup()
return response
def cleanup_call(self, call_sid: str) -> None:
self.conversations.pop(call_sid, None)
~/Documents/SG_Receptionist/app/voice_handler.py