TL;DR: Al, düşün ve yanıtla döngüsünü tek bir Whapi.Cloud webhook'unda çalıştırın. LangGraph thread_id değerini bir checkpointer ile gönderenin telefon numarasına eşitleyin; böylece her kişi kendi belleğini korur. HTTP 200'ü bir saniyenin altında döndürün ve ajanı bir arka plan görevinde çalıştırın. ngrok yok, Meta doğrulaması yok, mesaj şablonu yok. MemorySaver ile başlayın, üretim için Postgres checkpointer'a geçin.
Her WhatsApp ajanının çalıştırdığı üç adımlı döngü
WhatsApp yapay zeka ajanı tek bir döngüdür: mesajı al, araç kullanan bir ajanla düşün ve yanıtı gönder. Geri kalan her şey bu üç adımın etrafındaki bağlantılardan ibarettir.
Bu kurulumda bir FastAPI rotası Whapi.Cloud'dan gelen mesajı alır, ChatOpenAI ile çalışan bir LangGraph ajanı ne yapacağına karar verir ve tek bir REST çağrısı yanıtı geri gönderir. Size mesaj atan telefon numarası, üç adımın hepsinde taşımanız gereken tek tanımlayıcıdır.
Numarayı bağlayın ve bir webhook'u uygulamanıza yönlendirin
Numarayı bağlamak için bir QR kodu okutun, ardından genel URL'nizi kanalın webhook ayarlarına yapıştırın. Gelen mesajlar, bir kişi size yazdığı anda JSON POST istekleri olarak ulaşır.
Resmi WhatsApp Business API'sinde, tek bir mesaj bile kodunuza ulaşmadan önce bir uygulama kaydetmeniz, Meta işletme doğrulamasından geçmeniz ve bir doğrulama el sıkışması yürütmeniz gerekirdi. Whapi.Cloud ile sıradan bir WhatsApp numarasını, WhatsApp Web'in kullandığı eşleştirme akışıyla aynı şekilde bir QR kodu okutarak bağlarsınız ve API yaklaşık iki dakikada çalışır hale gelir. Sizinle ilk gelen payload arasında bir Meta inceleme kuyruğu yoktur.
Webhook URL'sini dağıtılmış uygulamanızın /webhook rotasına ayarlayın ve kanal ayarlarında messages olayına abone olun. Whapi.Cloud bundan sonra gelen her mesajı bu rotaya POST eder. Aşağıdaki FastAPI işleyicisi, gönderenin telefon numarasını ve metin gövdesini payload içinden okur.

# webhook.py -- receives inbound WhatsApp messages from Whapi.Cloud
from fastapi import FastAPI, Request
app = FastAPI()
@app.post("/webhook")
async def webhook(request: Request):
data = await request.json()
# Whapi delivers inbound messages in a "messages" array.
for msg in data.get("messages", []):
if msg.get("from_me"):
continue # skip your own outgoing messages echoed back
sender = msg["from"] # the contact's phone number, e.g. "14155551234"
text = msg.get("text", {}).get("body", "")
print(f"{sender}: {text}")
return {"status": "ok"}
Bu sender değeri, ajanın tamamının belkemiğidir. Kime yanıt vereceğinizi söyler ve birazdan her konuşmayı ayrı tutan anahtara dönüşür. Gelen mesajın tam şeması için Whapi.Cloud API belgelerine bakın.
Araçları seçen, harekete geçen ve sonra gözlemleyen ReAct ajanını oluşturun
LangGraph ReAct ajanı, bir araç seçen, onu çalıştıran, sonucu okuyan ve yanıt verebilene kadar bunu tekrarlayan bir LLM'dir. Döngüyü LangGraph sağlar; modeli ve araçları siz sağlarsınız.
LangChain size model sarmalayıcısını ve araç soyutlamalarını verir. LangGraph'in create_react_agent işlevi bunları durum tutan bir grafa bağlar; böylece ajan bir aracı çağırabilir, çıktıyı gözlemleyebilir ve sonraki adımına karar verebilir. Her yeteneği @tool ile süslenmiş sade bir işlev olarak tanımlar, ardından listeyi ajana verirsiniz.
# agent.py -- a tool-using ReAct agent
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
from langgraph.prebuilt import create_react_agent
# Define tools as standalone functions.
# Decorating a bound method (def check_slots(self, ...)) raises a
# duplicate "self" argument error at agent-build time -- keep tools module-level.
@tool
def check_appointment_slots(day: str) -> str:
"""Return free appointment slots for a given day."""
return "09:00, 11:30, 16:00"
model = ChatOpenAI(model="gpt-4o", temperature=0)
agent = create_react_agent(
model,
tools=[check_appointment_slots],
prompt="You are a clinic's WhatsApp assistant. Keep replies short.",
)
prompt argümanı ajanın kalıcı talimatlarını belirler ve her turda yeniden uygulanır; böylece konuşma büyüse bile asistanın rolü sabit kalır. Araçları küçük ve tek amaçlı tutun: biri randevu saatlerini okusun, biri bir siparişi sorgulasın, biri de konuşmayı bir insana aktarsın. Model hangisini çağıracağına aracın adından ve docstring'inden karar verdiği için, ikisini de bir API belgesi gibi yazın.
Bu ajan zaten düşünebiliyor ve bir aracı çağırabiliyor. Henüz yapamadığı şey hiçbir şeyi hatırlamak. Onu iki kez çağırın; ikinci mesaj sıfırdan başlar, çünkü bir çağrıyı bir sonrakine bağlayan hiçbir şey yoktur. Pratikte çoğu ilk kurulumun bozuk görünmesinin nedeni tam da bu boşluktur.
thread_id'yi telefon numarasına ayarlayın, her kullanıcı kendi belleğini korusun
Bir checkpointer ekleyin ve her çağrıda gönderenin telefon numarasına eşit bir thread_id geçirin. Bu tek satır, paylaşılan tek bir beyin ile kişi başına bir bellek arasındaki farktır.
Bir checkpointer olmadan tüm kullanıcılar tek bir konuşma durumunu paylaşır; bu yüzden mesaj atan ikinci kişi, ilk kişinin bağlamını devralır. Bir checkpointer ve kullanıcı başına bir thread_id ile her kişi yalıtılmış bir akış elde eder. Webhook'tan gelen telefon numarasını bu thread_id olarak kullanın; yönlendirme kendiliğinden çözülür. Buna telefonu thread_id yapma kuralı diyoruz ve tüm kurulumun yükünü taşıyan karar budur.
# memory.py -- one isolated conversation per phone number
from langgraph.checkpoint.memory import MemorySaver
from langgraph.prebuilt import create_react_agent
checkpointer = MemorySaver() # in-memory; resets on restart
# For production, swap one line:
# from langgraph.checkpoint.postgres import PostgresSaver
# checkpointer = PostgresSaver.from_conn_string("postgresql://...")
agent = create_react_agent(model, tools=tools, checkpointer=checkpointer)
def reply_for(sender: str, text: str) -> str:
# thread_id = phone number -> each contact keeps a separate conversation
config = {"configurable": {"thread_id": sender}}
result = agent.invoke({"messages": [("user", text)]}, config=config)
return result["messages"][-1].content
MemorySaver her akışı bellekte tutar ve prototip için mükemmeldir. Yeniden başlatmada her şeyi unutur; bu da dağıtıma geçene kadar sorun değildir. Telefonu thread_id yapma kuralı üretime geçtiğinizde değişmez; yalnızca checkpointer'ın arkasındaki depolama değişir. Üretimde çalışan açık kaynaklı WhatsApp ajanları tam olarak bu deseni, telefon numarasına göre anahtarlanmış biçimde kullanır.
Neden «sadece ngrok ve Business API'yi kullanın» yaklaşımı önce bozulur
Yaygın eğitim yolu, herhangi bir yapay zeka mantığı çalışmadan önce yerel bir tünel, mesaj şablonları ve 24 saatlik bir yanıt penceresi getirir. Tek webhook yolu bu üçünü de atlar.
Büyük olasılıkla önce standart kuruluma uzanırsınız: Meta'nın dizüstünüze ulaşabilmesi için bir ngrok tüneli ve gönderim için resmi Business API. İşte tam burada bozulur. Tünel URL'si her yeniden başlatmada değişir ve uyarı vermeden düşer; bu yüzden kodunuz düzgün görünürken webhook'unuz sessizce mesaj almayı bırakır. Sonra gönderim tarafı da kendi sürtünmesini ekler.
Resmi WhatsApp Business API'sinde, 24 saatlik bir pencerenin dışında başlattığınız her mesajın önceden onaylanmış bir şablon olması gerekir ve şablonların kabaca üçte biri biçim veya kategori sorunları yüzünden ilk incelemede reddedilir. 1 Temmuz 2025'ten beri Meta ayrıca teslim edilen her şablon mesajı başına kategoriye ve ülkeye göre ücret alıyor; bu, geliştiriciler için birçok gizli maliyeti olan bir modeldir. Whapi.Cloud ile ajan tek bir API çağrısıyla serbest metin yanıtı verir; bu yüzden ne şablon onay kuyruğu ne de denetlenecek şablon başı ücretlendirme vardır. Maliyet öngörülebilirliği argümanı tek cümlede şudur: serbestçe gönderebileceğiniz bir yanıt ne reddedilebilir ne de ek ücretlendirilebilir.
| Kurulumun ihtiyacı olan | Tek bir Whapi.Cloud webhook'u | ngrok + resmi Business API |
|---|---|---|
| Mesajları yerelde alma | Barındırılan webhook URL'si, tünel yok | URL'leri değiştiren ve düşen ngrok tüneli |
| Hesap kurulumu | QR taraması, ~2 dakikada hazır | Meta işletme doğrulaması, günler ila haftalar |
| Yanıt gönderme | Serbest metin, tek bir REST çağrısı | Önceden onaylı şablon, ~3'te 1'i reddedilir |
| Yanıt zamanlaması | 24 saatlik hizmet penceresi yok | Serbest metin yalnızca 24 saatlik pencerede |
| Gönderim maliyet modeli | Abonelik, mesaj başına şablon ücreti yok | Temmuz 2025'ten beri teslim edilen şablon başına ücret |
200'ü hızlıca döndürün, ajanı arka planda çalıştırın
Webhook'u hemen HTTP 200 ile onaylayın, ardından yavaş LLM çağrısını bir arka plan görevinde çalıştırın. Yavaş bir yanıt asla webhook bağlantısını açık tutmamalıdır.
Rotanızın durum güncellemeleri ve okundu bilgileriyle uyandırılmaması için kanal ayarlarında yalnızca messages olayına abone olun. Gelen her webhook payload'ı gönderenin numarasını, mesaj türünü ve metin gövdesini taşır; botun kendi mesajlarına yanıt vermemesi için from_me değeri true olan her şeyi yok sayın.
Bir LLM çağrısı birkaç saniye sürer; bir webhook onayı ise milisaniye sürmelidir. İşleyiciniz dönmeden önce ajanı beklerse, teslimat zaman aşımına uğrayabilir ve aynı mesaj yeniden teslim edilir; böylece kullanıcı yanıtı iki kez alır. FastAPI'nin BackgroundTasks özelliği hemen dönmenize ve işlemi sonradan yapmanıza olanak tanır.
# webhook_async.py -- fast 200, then reason and reply in the background
import os, requests
from fastapi import FastAPI, Request, BackgroundTasks
app = FastAPI()
def handle(sender: str, text: str):
answer = reply_for(sender, text) # the slow part: agent + LLM
# POST https://gate.whapi.cloud/messages/text
# If you block the webhook waiting for this, Whapi retries the
# delivery and the contact gets the same answer twice.
requests.post(
"https://gate.whapi.cloud/messages/text",
headers={"Authorization": f"Bearer {os.environ['WHAPI_TOKEN']}"},
json={"to": sender, "body": answer},
timeout=30,
)
@app.post("/webhook")
async def webhook(request: Request, background: BackgroundTasks):
data = await request.json()
for msg in data.get("messages", []):
if msg.get("from_me"):
continue
background.add_task(handle, msg["from"], msg.get("text", {}).get("body", ""))
return {"status": "ok"} # returned in milliseconds
Yanıt, gönderenin numarası to alanında ve ajanın yanıtı body içinde olacak şekilde POST /messages/text üzerinden geri gider. Rota ajan bitmeden döndüğü için teslimatlar hızlı kalır ve yinelenen yanıt hataları ortadan kalkar. Bozuk ilk kurulumlarda en sık karşılaştığımız desen, modelde takılan ve farkında olmadan ağ geçidini yeniden denemeye alıştıran eşzamanlı bir işleyicidir.
Prototipten üretime: MemorySaver'ı Postgres ile değiştirin
MemorySaver bir sonraki yeniden başlatmaya kadar hatırlar; Postgres checkpointer ise dağıtımlar ve çökmeler boyunca hatırlar. Değişiklik tek satırdır, çünkü telefonu thread_id yapma kuralı aynı kalır.
Webhook URL'sinin erişilebilir olması için FastAPI uygulamasını herhangi bir genel sunucuya dağıtın, WHAPI_TOKEN ve model anahtarınızı ortam değişkenleri olarak ayarlayın ve MemorySaver'ı PostgresSaver ile değiştirin. Konuşma durumu bundan sonra yeniden başlatmaları atlatır ve aynı kullanıcı başına akışlar, ajanın kendi kodunda hiçbir değişiklik yapmadan çalışmaya devam eder.
İlk kez kuranların karşılaştığı yaygın hatalar (ve çözümü)
İki hata neredeyse her ilk kurulumu sekteye uğratır: yanlış OpenAI sarmalayıcısı ve sınıf yöntemi olarak tanımlanmış bir araç. İkisi de başlangıçta kafa karıştırıcı mesajlarla başarısız olur.
gpt-4o gibi bir sohbet modeli adını eski completion sarmalayıcısına geçirirseniz, OpenAI This is a chat model and not supported in the v1/completions endpoint döndürür. Çözüm, completion tarzı OpenAI sınıfını değil ChatOpenAI sınıfını kullanmaktır.
# Wrong: completion wrapper rejects chat models
# from langchain_openai import OpenAI
# model = OpenAI(model="gpt-4o") # -> v1/completions endpoint error
# Right: chat model wrapper
from langchain_openai import ChatOpenAI
model = ChatOpenAI(model="gpt-4o")
İkinci hata, bir örnek yöntemini @tool ile süslediğinizde ortaya çıkar. LangChain self değerini zorunlu bir araç argümanı olarak okur ve ajan derlemesi başarısız olur. Araçları modül düzeyinde işlevler olarak tanımlayın ve paylaşılan her durumu bir closure veya genel bir istemci aracılığıyla geçirin.
Beklenmedik bir davranışla Python tarafında değil de WhatsApp tarafında karşılaşırsanız, whapi.cloud üzerindeki sohbet aracılığıyla Whapi.Cloud destek ekibine ulaşın; ekip, müşterilere üretim sorunlarını çözmede etkin biçimde yardımcı olur. Sesli not dökümünü veya vektör tabanlı getirmeyi burada ele almıyoruz; ikisi de aynı döngünün üzerine oturur ve kendi rehberini hak eder.
Kurulumun tamamı bu: tek bir Whapi.Cloud webhook'u mesajı alır, bir LangGraph ajanı her kişinin telefon numarasına göre anahtarlanmış bellekle düşünür ve tek bir REST çağrısı yanıtı gönderir. Bunu bu kadar kısa tutan şey, tünelleri, Meta doğrulamasını ve şablon onaylarını atlamaktır. Bu döngüde randevu almayı otomatikleştiren ekipler, gelmeyenlerin dörtte bir veya daha fazla azaldığını bildiriyor; kullanıcı başına bellek desenini doğru kurmaya değmesinin nedeni de bu. Üç adımı gösterildiği gibi bağlayın; ajan, mesajlar boyunca gerçek bir konuşma yürütür.









