AI front desk for HVAC
hvacX
A chat and email front desk that books HVAC visits with zero double bookings and answers questions with 99% correctness on its evaluation set.

Problem
An HVAC contractor in South Florida fields the same requests all day. Customers ask questions, book technician visits, reschedule and cancel them, by chat and by email. The office needs to follow every booking, thank customers when a job is done and remind them the day before. An AI can carry that load, but a front desk that books real appointments cannot be allowed to guess.
Constraints
What made it hard.
- Bookings, emails and Slack posts have real consequences, so none of them can depend on free-form model output.
- Two customers can ask for the same slot at the same moment, and both cannot get it.
- Answers must come only from the company’s own information, and the system must refuse when it does not know.
- Customers write whatever they like, including prompt-injection attempts and genuine emergencies such as a gas smell.
- It had to run the same on a laptop as on a server, with no public URL, Slack app or Gmail push setup.
Solution
hvacX is four independent LangGraph runs that share one booking core and one knowledge base. A customer can ask a question or book a visit in the website chat or by email. The office follows every booking in Slack. Finished jobs reported in Slack get a thank-you email, and every visit gets a reminder the day before. The runs never call each other. They share state only through PostgreSQL.
The design rule is “LLM classifies, code acts.” The model returns a structured analysis of each message. Everything that changes the world is ordinary, tested code, and it reports success only after the database confirms it.
Run A and B: chat and email
- System: Chat message, or email polled every 90 s
- Code: Emergency guard
- Model output: Classify the turn
- Code: Collect details, offer up to 3 open times
- Code: Book after a clear yes
- System: Confirmation email and Slack post
Questions
- Model output: Rewrite into standalone queries
- Code: Search ChromaDB
- Model output: Answer only from passages, or say so
Run C: finished jobs, every 12 h
- System: “case 123 status done” in Slack
- Code: Thank-you email, once
- Code: Case closed
Run D: reminders, daily at 6 PM
- Code: Visits tomorrow, not yet reminded
- System: Reminder email
Every status change goes through one transition table with an audit row, and the model never sets a status. Simplified diagram.


Email has its own rules, enforced by a code-only poller before the graph sees anything. Changes are accepted only from the booking’s own address. A change within two hours of the visit goes to a person. An unclear email gets a fixed acknowledgement, a Slack alert, and silence from the AI on that case. Every email is stored by its Gmail message ID, so it is handled once, and staff opening the inbox cannot hide one from the agent.


Phase 1 is complete. Next are an embeddable chat widget for the company’s website and a WhatsApp channel that reuses the same agent and tools.
Technology and why
- LangGraph
- Each of the four runs is an explicit graph with a Postgres checkpointer, so a visitor or an email thread can pick up where it stopped days later.
- Pydantic
- The classifier returns a typed analysis (intent, details, chosen option) that code validates before anything happens.
- PostgreSQL
- The system of record. Row locks and partial unique indexes inside one transaction make a double booking impossible.
- ChromaDB
- Stores the 118 knowledge-base chunks, searched by cosine similarity with a tuned minimum score.
- OpenAI
- gpt-4.1-mini classifies and words replies, gpt-4.1 chunks and reviews the knowledge base, and text-embedding-3-large embeds it.
- MCP
- Gmail and Slack are reached through Klavis MCP servers, and only the seven tools the system needs are loaded.
- APScheduler
- Runs the inbox poll every 90 seconds, the Slack status check every 12 hours and reminders daily at 6 PM, inside the app.
- pytest
- 65 offline tests with a fake model, database and MCP cover routing, booking, guardrails and every run.
Hard problems
Keeping the model away from critical actions
The model only classifies a message and words the answer. Code performs every booking, status change, email and Slack post. Confirmation texts are templates filled in after the database write succeeds, so the model can never confirm a booking that does not exist.
Identity comes from code, never from the model
Tools read the case, the customer and the recipient address from graph state that code sets. A fooled model can only send a bad reply. It cannot act on another customer’s case or email an address it chose.
Guaranteeing zero double bookings
Chat and email call the same booking core. Each booking runs in one transaction with row locks and partial unique indexes, one active booking per slot and one per case.
Retrieval worth trusting
An LLM writes self-contained chunks, then code checks that every price, phone number, URL, percentage and licence number in the source survives unchanged and that every line is covered. Retrieval keeps at most two chunks per source, and nothing above the threshold means no answer.
Hostile and urgent messages
A code-side guard catches emergency words in English and Roman Urdu before any model call and sends the approved safety message plus an urgent Slack alert. Customer text is treated as data, and more than five AI replies per thread per hour hands the case to a person.
Results
- Answer correctness (LLM judge)
- 99%
- Faithfulness to retrieved passages
- 98%
- Mean reciprocal rank (MRR)
- 0.846
- Expected source retrieved in top-k
- 100%
- Correct refusals on unanswerable questions
- 100%
- Invented facts on unanswerable questions
- 0%
- Median (p50) latency, 4.49s at p95
- 2.96s
- Double bookings, guaranteed by row locks
- 0
Model and evaluation
- Model
- OpenAI gpt-4.1-mini for classification and replies (Groq supported), gpt-4.1 for chunking and review, text-embedding-3-large for embeddings.
- Evaluation
- 51 labelled questions, 40 answerable and 11 that must be refused, run end to end through the same path as the chat. Routing 98%, must-include facts in passages 98%, 0% false “not sure” answers.
- Latency
- 2.96 s at p50 and 4.49 s at p95 on the evaluation set.