
An AI that answers on WhatsApp: from conversation to a real order
For most small businesses, customer conversations happen on WhatsApp and Instagram. The problem is not that messages don't arrive — it's that nobody answers them in time. Someone asking "is this in stock?" at 9pm rarely waits for the reply that comes at 9am.
So I built an assistant for it. Not a bot that returns canned answers to a menu of questions, but a tool-using agent: while it talks, it does real work in the database — creates orders, books appointments, checks shipping status — and everything it does lands in the business's CRM panel in real time.

Why an agent rather than a chatbot
Rule-based flows trap the customer in a menu: "1 — Price list, 2 — Order status". The moment someone steps outside the menu, the flow breaks. At the other extreme is a plain LLM: it talks well but does nothing, and it invents whatever it doesn't know.
The useful middle is a model with a defined set of tools, tools that hit a real database, and a model that reports the result back in natural language. Asked about price, it reads the catalogue. Asked to place an order, it calls siparis_olustur. Unsure about anything, it hands the conversation to a human instead of guessing.
Architecture
The stack is deliberately small: one API, one frontend, one database, all in Docker.
- api — Python + FastAPI with async SQLAlchemy. The agent loop is built on LangGraph (agent → tools → agent).
- web — React + Vite + TypeScript + Tailwind. Two routes:
/admin(the CRM panel) and/demo(the WhatsApp/Instagram surface). - db — PostgreSQL: businesses, conversations, messages, orders, appointments, leads, catalogue.
- Live feed — the panel receives events over WebSocket, so an action shows up the instant the agent takes it.
- Model provider — one line in
.env: Google Gemini or Anthropic Claude. Cheap in development, good in production.
Tools: what the model can actually do
Each tool is a Pydantic schema, an async function and a description. The function does the work in the database, writes the result into the conversation as a role="tool" message, and publishes an event — that event is what makes the panel update live.
async def siparis_olustur(urun, musteri=None, telefon=None, tutar=None):
no = f"SP-{random.randint(1100, 9999)}"
db.add(Order(business_id=business.id, no=no, musteri=(musteri or "Müşteri"),
urun=urun, tutar=(tutar or "—"), durum="hazırlanıyor",
telefon=(telefon or ""), canli=True))
return await record("siparis_olustur",
{"urun": urun, "musteri": musteri, "telefon": telefon},
{"siparis_no": no, "durum": "hazırlanıyor"})
There are ten tools today: knowledge lookup, availability check, create/cancel appointment, order status, create order, product stock and price, save lead, share contact details, and hand off to a human.
One detail matters more than it looks: tools are enabled per business. Every business record stores which tools are switched on, and the agent only ever sees that subset. A dental clinic's assistant never sees the order tools; an e-commerce account never sees the appointment tools. Same engine, different storefront.
Confirm before writing
Every tool that writes to the database is bound by one rule in the system prompt: summarise, get confirmation, then call. In the conversation below the model repeats the product, quantity, name, phone number and total, and only calls the tool after the customer says yes.

That small rule removes almost every "I misunderstood but saved it anyway" failure. The cost profile of an agent is asymmetric: a wrong sentence is annoying, a wrong record creates work for the business.
The result doesn't stay in the chat
The order the assistant created appears in the panel's order list within seconds. Staff don't have to do anything; records created after hours are simply waiting in the morning.

A channel-agnostic core
WhatsApp and Instagram mean different webhooks, different payloads, different identity fields — but identical assistant logic. So each channel router only parses the Meta payload and then hands off to a shared core:
async def handle_user_message(db, business, channel, session_key, text,
demo_user, send):
conv = await get_or_create_conversation(db, business, channel, session_key,
user_id=uid)
if conv.ai_paused: # a human took over — the AI stays quiet
...
return
reply = await run_agent(db, business, conv, text)
await db.commit()
await send(session_key, reply)
session_key is the phone number on WhatsApp and the IGSID on Instagram. Conversation handling, human takeover, demo limits and the agent call all live in one place. Adding a channel means writing a router and a send function — nothing else.

Handing off to a human
One of the assistant's most valuable abilities is knowing when to stop talking. On a complaint, a refund, a negotiation or a question the model isn't sure about, it calls insana_devret; the conversation's ai_paused flag goes up and the AI never answers in that thread again. Further customer messages land straight in the panel and a person continues from there.
It works the other way too: a staff member who types into the conversation from the panel takes control automatically — which is what the panel's "type to take over (AI stops)" input means.
Keeping it from making things up
In this product hallucination isn't a theoretical risk, it's a business risk. I use three layers:
- Context — catalogue, opening hours, address, payment and policy details are embedded in the system prompt. No tool call needed; the model already knows.
- Lookup — anything outside that goes through
bilgi_ara, whose own result says "don't invent, hand off if needed" when nothing is found. - Handoff — when the model isn't sure, it escalates instead of producing an answer.
On top of that, one rule makes tool results mandatory: while holding a valid tool result, the model is forbidden from saying "let me connect you to a colleague". A tool returning a result is the success case; the model's job is to report it.
Multi-tenant demos
The part that turned out to matter commercially: every prospect can get their own demo account, with its own business profile, catalogue, enabled tool list and channel entitlement (whatsapp, instagram or both). The link a prospect receives opens an assistant already filled in for their industry.
Demo accounts carry a daily message limit; once it's reached the assistant politely points to the full version, which keeps API cost bounded.
What I took away
- Tools make an agent valuable, not the model. Model quality sets the tone; the tools do the work.
- Writes need confirmation. Be liberal with read tools, put a gate in front of write tools.
- Abstract the channel early. Adding Instagram later touched none of the agent code — just one router.
- "I don't know" is a feature. An assistant without a handoff path burns the business's reputation on the first hard question.
The product is at demo stage with the real WhatsApp Cloud API integration ready. If you'd like to see how it would behave for your own business, write to me from the contact page and I'll set up a demo filled in for you.