Building GlamDesk — The AI Salon Receptionist -Voice Agent

glamdesk

How we built an AI voice receptionist for salons from scratch — every wrong turn, every breakthrough, and the final moment it all just worked.

By Techtogeek.com · 2025 · ~12 min read


The Idea — Every Salon Deserves a Brilliant Receptionist

It started with a simple observation. Walk into any small salon in Chennai and you’ll find the same scene — the stylist is mid-haircut, the phone is ringing, and someone at the door is asking if there’s an opening at 3 PM. The receptionist, if there even is one, is juggling all three at once.

Small businesses can’t always afford full-time reception staff. Missed calls mean missed bookings. Missed bookings mean lost revenue. And the problem compounds every single day.

What if a salon could have a warm, professional, always-on receptionist — that costs a fraction of a human salary and never takes a day off?

That was the seed of GlamDesk. An AI voice bot that handles appointment booking, answers FAQs, and routes calls for salons. Powered by Deepgram’s voice AI, backed by a SQLite database, and connected to the world through Twilio.

Simple idea. The execution, as always, was anything but.


We Didn’t Start from Zero

Rather than building completely from scratch, we found an excellent foundation — Tim’s DeepgramVoiceAgent project on GitHub. It demonstrated the core pattern beautifully: stream audio from Twilio into Deepgram’s Voice Agent API, let the LLM decide when to call functions, stream the response back.

The original was built around a pharmacy use case with a pharmacy_functions.py file containing hardcoded Python dictionaries — ORDERS_DB and DRUG_DB — standing in for a real database. Clean demo code, but not production-ready.

Our job was clear: swap out the pharmacy brain for a salon brain, and replace those hardcoded dicts with a real persistent database.

Decision: Use Tim’s architecture as the foundation. Replace pharmacy_functions.py with salon_functions.py. Replace hardcoded dicts with SQLite. Keep the three-task async pattern: sts_sender, sts_receiver, twilio_receiver.


Goodbye Hardcoded Dicts, Hello SQLite

The first real design decision was the database schema. A salon isn’t just a list of drugs and orders — it has customers, services, stylists, appointments, and FAQs, all woven together with foreign key relationships.

We designed five tables:

  • customers — stores the caller’s name and phone number. The phone number doubles as the unique identifier so the bot can look up a customer the moment they call.
  • services — every treatment with its duration and price.
  • stylists — who’s available on which days.
  • appointments — the heart of it all. Joins customers, services, and stylists with a datetime and a status field (booked or cancelled).
  • faqs — common questions about hours, location, parking, payment methods, and walk-in policy.

The salon_functions.py module exposed seven functions to the LLM:

  1. book_appointment — creates a booking after confirming no slot conflict
  2. lookup_appointment — fetches a booking by ID
  3. lookup_appointment_by_phone — fetches all bookings by phone number
  4. cancel_appointment — marks a booking as cancelled
  5. get_service_info — returns price, duration, and description for a service
  6. list_services — returns all available services
  7. get_available_slots — returns open time slots for a given date
  8. get_faq — answers common questions about the salon

Each function is a clean, self-contained SQLite operation. The FUNCTION_MAP dictionary ties them together so Deepgram’s LLM can call any of them by name with structured JSON arguments.

The database auto-initialises on first run, seeds itself with demo services, stylists, and FAQs, and is ready to use immediately. No setup scripts, no manual migrations needed — for a fresh install.


The Phone Number Lookup Debate

During testing, a question came up: how do you look up an appointment when a customer calls in? The original lookup_appointment function used an appointment ID — fine for a follow-up, but a real customer on the phone doesn’t know their booking number.

The answer was to look up by phone number. The JOIN query was almost right — filter through the customers table by phone to reach the appointments table — but there was one small mistake: a missing closing parenthesis on conn.execute(, and fetchone() instead of fetchall(), since one customer can have multiple bookings.

The fix was straightforward. We added lookup_appointment_by_phone(phone) as a separate function, used fetchall(), and registered it in FUNCTION_MAP. The bot can now identify a returning customer the moment they say their phone number.


The Column We Forgot to Build

The next feature was ambitious: place an automated outbound call to remind customers one hour before their appointment. Twilio can make outbound calls. Deepgram can handle the conversation. But how do you make sure you don’t call the same customer twice?

We needed a flag. A single column on the appointments table — reminder_sent — an integer defaulting to 0, set to 1 once the call goes out.

The problem: the database had already been created without this column. This is where many small projects break down — schema changes mid-development with live data already in the database.

We wrote a proper migration script: migrate_add_reminder_sent.py.

It uses PRAGMA table_info(appointments) to check whether the column already exists before trying to add it. It prints the full schema before and after the migration so you can visually confirm the change. It counts rows with reminder_sent = 0 versus 1 to confirm everything looks clean. And it is safe to re-run — if the column already exists, it skips silently.

We also updated the CREATE TABLE statement in salon_functions.py so any fresh database created from scratch already includes the column. Two scenarios covered: existing databases use the migration, new databases get it automatically.

The reminder_scheduler.py process runs separately alongside the main server. Every 60 seconds it queries for appointments in the now+55min → now+65min window with reminder_sent = 0. When it finds one, it calls Twilio’s REST API to place the outbound call, then marks the flag to 1.

The outbound handler builds a custom Deepgram config on the fly — personalised with the customer’s name, service, and appointment time — so the greeting sounds specific and human, not robotic.


The Crash — Opening Handshake Failed

This was the moment that nearly broke everything.

The server was running. Twilio was configured. The ngrok tunnel was live. We made a test call. Twilio connected — and then the terminal printed this:

opening handshake failed
websockets.exceptions.InvalidUpgrade: missing Connection header

The call came through. The bot stayed silent. Twilio’s debugger logged a stream error. After digging through the websockets library source code and Twilio’s Media Streams documentation side by side, the root cause became clear.

Raw websockets.serve is strict about the WebSocket upgrade handshake. It validates headers strictly according to RFC 6455. Twilio’s Media Streams sends its WebSocket upgrade request with slightly different header formatting — specifically how it constructs the Connection header — which the raw library rejects before the handshake even completes. The connection is terminated before a single byte of audio is exchanged.

The fix was architectural. We replaced websockets.serve with FastAPI and uvicorn. FastAPI’s WebSocket endpoint uses Starlette under the hood, which is far more permissive about upgrade headers and handles exactly the kind of HTTP-to-WebSocket upgrade that Twilio sends.

Here is what changed in main.py:

BeforeAfter
websockets.serve(handler, host, port)@app.websocket("/media-stream")
asyncio.run(main())uvicorn main:app --host 0.0.0.0 --port 5000
twilio_ws.send(...)twilio_ws.send_text(...)
No TwiML route/incoming-call route returns TwiML <Stream>

We also added an /incoming-call route that returns a TwiML response telling Twilio where to stream the audio. Twilio calls /incoming-call, receives the <Stream> TwiML pointing to /media-stream, then opens the WebSocket — and this time the handshake succeeds.

There is no feeling quite like hearing your code speak for the first time.

The bot said: “Hello, thank you for calling GlamDesk! I’m your AI salon receptionist. How can I assist you today?”


What We Actually Built — The Full Stack

After all the iterations, here is what GlamDesk looks like end to end:

Layer 1 — Telephony: Twilio
Provides the phone number. Streams µ-law 8kHz audio bidirectionally over WebSocket. The /incoming-call TwiML webhook connects the call to the server.

Layer 2 — Server: FastAPI + uvicorn
Runs on port 5000. Accepts Twilio’s WebSocket upgrade correctly. Manages three concurrent async tasks per call: sts_sender (forwards audio to Deepgram), sts_receiver (receives audio and events back from Deepgram), and twilio_receiver (buffers incoming audio from Twilio).

Layer 3 — Voice AI: Deepgram Voice Agent
Speech-to-text with nova-3. LLM reasoning with gpt-4o-mini. Text-to-speech with aura-2-thalia-en. Sends FunctionCallRequest events when it needs to read or write data.

Layer 4 — Business Logic: salon_functions.py
Eight clean functions mapped by name in FUNCTION_MAP. Each one a self-contained SQLite operation. The LLM calls them by name with structured JSON arguments and receives structured JSON back.

Layer 5 — Persistence: SQLite (glamdesk.db)
Five tables. Auto-initialised on first run. Seeded with demo data. Managed interactively via db_manager.py CLI — a fully menu-driven terminal tool for adding, editing, and deleting records across all five tables.

Layer 6 — Scheduler: reminder_scheduler.py
Polls every 60 seconds. Places outbound Twilio calls one hour before appointments. Marks reminder_sent = 1 to prevent duplicate calls.

Package manager: UV with pyproject.toml
Replaced pip and requirements.txt. UV manages the virtual environment automatically, generates a lockfile, and runs scripts cleanly with uv run.


What This Build Taught Us

Every build teaches something. GlamDesk taught us several things we won’t forget.

WebSocket libraries are not all equal. The difference between websockets.serve and FastAPI’s WebSocket handler seems cosmetic until Twilio’s handshake hits a strict RFC validator. Always test your WebSocket server with the actual client — not a browser substitute.

Schema migrations are inevitable. No matter how carefully you design a database upfront, you will need to add a column you didn’t think of. Write migration scripts from day one. The reminder_sent column taught us this the hard way.

A great prompt is half the product. The Deepgram agent config — specifically the system prompt — determines whether GlamDesk sounds like a confused robot or a professional receptionist. Getting the tone, the instructions, and the function descriptions right took as much iteration as the Python code itself.

Tooling matters at every scale. Switching from pip and requirements.txt to UV and pyproject.toml felt minor but made dependency management dramatically cleaner. Good tools reduce friction. Reduced friction means more time actually building.

Separate your concerns. The inbound call server, the outbound reminder scheduler, and the TwiML webhook handler are three separate processes with three separate responsibilities. Keeping them separate made each one independently debuggable.

The hardest bugs are never in the logic. They’re in the layer between two systems that each assume the other is behaving correctly.


What’s Next for GlamDesk

The foundation is solid. The bot books appointments, answers FAQs, handles cancellations, and calls customers to remind them one hour before they’re due. For a small salon, that is transformative.

But there is so much more it could do:

  • WhatsApp integration so customers can book via chat without making a call
  • Owner dashboard showing today’s bookings, revenue, and reminder status at a glance
  • Multi-language support — Tamil, Hindi, and English in the same conversation
  • Stylist-specific availability so the bot can say “Priya is free at 3 PM but Kavitha can take you at 4 PM”
  • Post-visit follow-up calls asking for feedback after the appointment

Every one of these is an extension of what already exists — not a rewrite. The infrastructure is there.

At its core, GlamDesk is proving something important: AI voice infrastructure is no longer out of reach for small businesses. With open APIs, a lightweight database, and a few hundred lines of Python, a Chennai salon can have the same quality of automated booking experience as a national spa chain.

Never miss a booking. Always on. Always polished.


Tech Stack: Python · Deepgram nova-3 · gpt-4o-mini · aura-2-thalia-en · Twilio · FastAPI · uvicorn · SQLite · UV

GitHub: https://github.com/subasen85/GlamDesk.git

If you are interested in any of my article or want to collaborate, feel free to get in touch,I am available in contact us.
Thank you for reading,TechtoGeek.com

Leave a Reply

Your email address will not be published. Required fields are marked *