Building a Multi-Turn Tool-Calling WhatsApp AI Agent with Claude
2026-08-10 · 5 min read
The Problem
When we launched CodeMyFYP's WhatsApp support channel, conversations were handled manually. The dominant query types were:
- Pricing and INR/UPI payment questions
- Vacancy and job-placement queries
- Course and internship availability
Answering these manually meant slow response times and inconsistent answers. I needed an AI agent that could handle these at scale — including in Hindi and Hinglish, which a significant portion of our users write in.
Why Claude Haiku?
I chose Claude Haiku over GPT-3.5/GPT-4 for three reasons:
- Latency — Haiku is fast enough for real-time chat (sub-2s response for most turns)
- Tool-calling — Claude's tool-calling API is clean and reliable for structured lookups
- Instruction following — Haiku stays on-persona more consistently than smaller open models
Architecture
WhatsApp User
↓ message
WhatsApp Business API (webhook)
↓ POST /webhook
Flask Server
├── Language Detector → inject system prompt language instruction
├── Load conversation history (Redis TTL=30min)
├── Call Claude Haiku with:
│ - system prompt (persona + language instruction)
│ - messages (conversation history)
│ - tools (pricing, vacancy, faq)
│
├── If tool_call returned:
│ → Execute tool handler
│ → Append tool_result to messages
│ → Call Claude again for final response
│
└── Send reply via WhatsApp API
↓ log to monitoring dashboard
Tool Design
I defined three tools:
tools = [
{
"name": "get_pricing",
"description": "Look up current service pricing in INR. Use when user asks about fees, cost, or payment.",
"input_schema": {
"type": "object",
"properties": {
"service": {"type": "string", "description": "Service name, e.g. 'FYP development', 'internship'"},
},
"required": ["service"],
},
},
{
"name": "get_vacancies",
"description": "Retrieve current open positions and internship vacancies.",
"input_schema": {
"type": "object",
"properties": {
"role_filter": {"type": "string", "description": "Optional role type to filter by"},
},
},
},
{
"name": "get_faq_answer",
"description": "Look up a frequently asked question answer from the knowledge base.",
"input_schema": {
"type": "object",
"properties": {
"question": {"type": "string", "description": "The user's question verbatim"},
},
"required": ["question"],
},
},
]
Critical lesson: the description field is the most important part. Claude uses it to decide when to call the tool. Vague descriptions cause the model to call the wrong tool or skip tool-calling entirely.
Language Detection & Mirroring
Hindi/Hinglish mirroring was one of the most impactful features. I detect language on every incoming message and inject an instruction:
def detect_language(text: str) -> str:
hindi_chars = set("अआइईउऊएऐओऔकखगघचछजझटठडढणतथदधनपफबभमयरलवशषसह")
if any(c in hindi_chars for c in text):
return "hindi"
hinglish_keywords = ["kya", "hai", "mujhe", "chahiye", "bhai", "yaar", "karo", "batao"]
if any(kw in text.lower() for kw in hinglish_keywords):
return "hinglish"
return "english"
def build_system_prompt(lang: str) -> str:
base = "You are Cody, CodeMyFYP's friendly sales assistant..."
if lang == "hindi":
base += "\n\nIMPORTANT: The user is writing in Hindi. Reply in Hindi."
elif lang == "hinglish":
base += "\n\nIMPORTANT: The user is writing in Hinglish. Mirror their style — mix Hindi and English naturally."
return base
The Monitoring Dashboard
I built a Flask-served dashboard that shows:
- Active conversations with last message timestamp
- Tool invocation log — tool name, input, output, latency
- Failure events — tool errors, Claude refusals, timeout events
This was the feature that gave the team confidence to leave the agent running unsupervised. Without visibility, even a well-tested agent feels risky in production.
Lessons from Production
Multi-turn context is everything — without storing conversation history, the agent forgets what it already said and repeats itself. Store history server-side, not in the client.
Tool calls can fail silently — if a tool returns an error dict instead of raising, Claude still tries to respond and sometimes hallucinates an answer. Validate tool results before passing them back.
Rate limit WhatsApp webhook replies — if your Flask server takes too long, WhatsApp retries the webhook. Add idempotency checks on the message ID to prevent duplicate replies.
Test in Hinglish — my initial prompt didn't handle mixed-script inputs well. Testing with real Hinglish messages caught issues that English-only testing missed.
Result
Cody now handles the dominant query types autonomously — pricing, vacancies, and FAQs — with the correct language mirroring. The monitoring dashboard gives the team real-time visibility without requiring them to read every conversation.
The architecture scales: the Flask webhook is stateless (state lives in Redis), tool handlers are modular, and adding a new tool is a matter of defining the schema and handler function.