DEV Community

AIbuddy_il
AIbuddy_il

Posted on

How We Built a Multi-Industry WhatsApp AI Agent in Israel

How We Built a Multi-Industry WhatsApp AI Agent in Israel

At AI Buddy, we've deployed AI agents for 200+ Israeli businesses — real estate agencies, dental clinics, law firms, gyms, and beauty salons. This is the technical story of how we built a WhatsApp AI agent that works across all of them.

The Problem: WhatsApp Is Israel's Business Channel

In Israel, over 90% of business communication happens on WhatsApp. Not email. Not web chat. WhatsApp. This means:

  • Leads come in at 11pm asking about services
  • Customers expect responses in minutes, not hours
  • A 3-hour response time loses 80%+ of conversion opportunities
  • One person can't respond fast enough at scale

The solution isn't "hire someone to watch WhatsApp 24/7." It's an AI agent that handles the entire conversation flow autonomously.

Architecture Overview

┌─────────────────┐     ┌──────────────────┐     ┌─────────────────┐
│   WhatsApp API  │────▶│   Agent Layer    │────▶│   Integrations  │
│  (Green API /   │     │  (LLM + Tools)   │     │  CRM / Calendar │
│   360Dialog)    │◀────│                  │◀────│  / Booking      │
└─────────────────┘     └──────────────────┘     └─────────────────┘
Enter fullscreen mode Exit fullscreen mode

The core components:

  1. WhatsApp ingress — Webhook receives messages, normalizes them
  2. Context manager — Maintains conversation history per phone number
  3. LLM orchestrator — Decides what to do next
  4. Tool executor — Takes real-world actions (book, CRM update, etc.)
  5. Response generator — Crafts natural Hebrew responses

Step 1: WhatsApp Integration

We use Green API for WhatsApp Business API access (approved provider). Here's the webhook handler:

// webhook.js
const express = require('express');
const app = express();

app.post('/webhook', express.json(), async (req, res) => {
  const { typeWebhook, messageData } = req.body;

  if (typeWebhook !== 'incomingMessageReceived') {
    return res.sendStatus(200);
  }

  const { idMessage, chatId, textMessageData } = messageData;
  const phoneNumber = chatId.replace('@c.us', '');
  const messageText = textMessageData?.textMessage;

  if (!messageText) return res.sendStatus(200);

  // Process asynchronously, respond immediately to webhook
  res.sendStatus(200);

  await processIncomingMessage({
    phoneNumber,
    message: messageText,
    messageId: idMessage
  });
});
Enter fullscreen mode Exit fullscreen mode

Step 2: Context Manager

Each phone number gets its own conversation context. We store this in Redis with a 24-hour TTL:

// context.js
const Redis = require('ioredis');
const redis = new Redis(process.env.REDIS_URL);

const CONTEXT_TTL = 86400; // 24 hours

async function getContext(phoneNumber) {
  const raw = await redis.get(`ctx:${phoneNumber}`);
  if (!raw) return { messages: [], leadData: {}, stage: 'initial' };
  return JSON.parse(raw);
}

async function updateContext(phoneNumber, context) {
  await redis.setex(
    `ctx:${phoneNumber}`,
    CONTEXT_TTL,
    JSON.stringify(context)
  );
}

async function appendMessage(phoneNumber, role, content) {
  const ctx = await getContext(phoneNumber);
  ctx.messages.push({ role, content, timestamp: Date.now() });

  // Keep last 20 messages to stay within token limits
  if (ctx.messages.length > 20) {
    ctx.messages = ctx.messages.slice(-20);
  }

  await updateContext(phoneNumber, ctx);
  return ctx;
}
Enter fullscreen mode Exit fullscreen mode

Step 3: The LLM Orchestrator

This is where the intelligence lives. We use function calling (tool use) to allow the LLM to take actions:

// orchestrator.js
const Anthropic = require('@anthropic-ai/sdk');
const client = new Anthropic();

const TOOLS = [
  {
    name: 'check_availability',
    description: 'Check available appointment slots',
    input_schema: {
      type: 'object',
      properties: {
        date: { type: 'string', description: 'Date in YYYY-MM-DD format' },
        service_type: { type: 'string' }
      },
      required: ['service_type']
    }
  },
  {
    name: 'book_appointment',
    description: 'Book an appointment for the customer',
    input_schema: {
      type: 'object',
      properties: {
        name: { type: 'string' },
        phone: { type: 'string' },
        service: { type: 'string' },
        datetime: { type: 'string' }
      },
      required: ['name', 'phone', 'service', 'datetime']
    }
  },
  {
    name: 'update_crm',
    description: 'Update the CRM with lead information',
    input_schema: {
      type: 'object',
      properties: {
        phone: { type: 'string' },
        name: { type: 'string' },
        interest: { type: 'string' },
        budget: { type: 'string' },
        status: { type: 'string', enum: ['new', 'qualified', 'booked', 'lost'] }
      },
      required: ['phone', 'status']
    }
  },
  {
    name: 'escalate_to_human',
    description: 'Flag this conversation for human review',
    input_schema: {
      type: 'object',
      properties: {
        reason: { type: 'string' },
        priority: { type: 'string', enum: ['low', 'medium', 'high'] }
      },
      required: ['reason', 'priority']
    }
  }
];

async function runAgent(businessConfig, phoneNumber, conversationHistory) {
  const systemPrompt = buildSystemPrompt(businessConfig);

  const response = await client.messages.create({
    model: 'claude-opus-4-5',
    max_tokens: 1024,
    system: systemPrompt,
    messages: conversationHistory,
    tools: TOOLS
  });

  return response;
}

function buildSystemPrompt(config) {
  return `You are an AI agent for ${config.businessName}, a ${config.businessType} in Israel.

Your job is to:
1. Respond to incoming WhatsApp messages in natural Hebrew
2. Qualify leads by understanding their needs
3. Book appointments when appropriate
4. Update the CRM with relevant information
5. Escalate to a human when the situation requires it

Business information:
- Services: ${config.services.join(', ')}
- Operating hours: ${config.operatingHours}
- Pricing: ${config.pricingInfo}
- Tone: ${config.tone}

IMPORTANT:
- Always respond in Hebrew
- Be helpful and conversational, not robotic
- Don't make up information about services or prices
- If unsure, say you'll check and get back to them
- Never be pushy about booking

Current date/time in Israel: ${new Date().toLocaleString('he-IL', {timeZone: 'Asia/Jerusalem'})}`;
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Tool Execution

// tools.js
async function executeTool(toolName, toolInput, businessConfig) {
  switch (toolName) {
    case 'check_availability':
      return await checkCalendarAvailability(toolInput, businessConfig.calendarId);

    case 'book_appointment':
      const booking = await createCalendarEvent(toolInput, businessConfig.calendarId);
      await sendConfirmationSMS(toolInput.phone, booking);
      return { success: true, bookingId: booking.id, datetime: booking.start };

    case 'update_crm':
      return await upsertCRMLead(toolInput, businessConfig.crmId);

    case 'escalate_to_human':
      await notifyHumanAgent(toolInput, businessConfig.alertPhone);
      return { escalated: true };

    default:
      throw new Error(`Unknown tool: ${toolName}`);
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 5: The Main Processing Loop

// process.js
async function processIncomingMessage({ phoneNumber, message, messageId }) {
  // Get or create conversation context
  const ctx = await appendMessage(phoneNumber, 'user', message);

  // Get business config (could be from DB based on which WhatsApp number received the message)
  const businessConfig = await getBusinessConfig(phoneNumber);

  let response = await runAgent(businessConfig, phoneNumber, ctx.messages);

  // Handle tool calls in a loop (agent may call multiple tools)
  while (response.stop_reason === 'tool_use') {
    const toolResults = [];

    for (const block of response.content) {
      if (block.type === 'tool_use') {
        const result = await executeTool(block.name, block.input, businessConfig);
        toolResults.push({
          type: 'tool_result',
          tool_use_id: block.id,
          content: JSON.stringify(result)
        });
      }
    }

    // Add assistant response and tool results to history
    ctx.messages.push({ role: 'assistant', content: response.content });
    ctx.messages.push({ role: 'user', content: toolResults });

    // Run agent again with tool results
    response = await runAgent(businessConfig, phoneNumber, ctx.messages);
  }

  // Extract the final text response
  const textResponse = response.content
    .filter(b => b.type === 'text')
    .map(b => b.text)
    .join('');

  if (textResponse) {
    // Save to context
    ctx.messages.push({ role: 'assistant', content: textResponse });
    await updateContext(phoneNumber, ctx);

    // Send WhatsApp response
    await sendWhatsAppMessage(phoneNumber, textResponse);
  }
}
Enter fullscreen mode Exit fullscreen mode

Industry-Specific Customization

The businessConfig object is where vertical customization happens. Here's how it differs by industry:

// Real estate agency config
const realEstateConfig = {
  businessType: 'real estate agency',
  qualificationQuestions: ['budget', 'area', 'property_type', 'timeline'],
  keyQualifiers: {
    minBudget: 1500000, // 1.5M NIS minimum
    seriousBuyer: ['mortgage_approved', 'cash_buyer']
  },
  escalationTriggers: ['over_5M', 'commercial_property', 'negative']
};

// Dental clinic config
const dentalConfig = {
  businessType: 'dental clinic',
  qualificationQuestions: ['treatment_type', 'urgency', 'insurance'],
  services: ['checkup', 'cleaning', 'whitening', 'implants', 'orthodontics'],
  urgencyTriggers: ['pain', 'כאב', 'broken', 'שבור', 'emergency', 'חירום']
};
Enter fullscreen mode Exit fullscreen mode

Performance in Production

After 6 months running across 200+ businesses:

  • Average response time: < 3 seconds (vs 3-12 hours manually)
  • Lead response rate: 97% (vs 40-60% manually)
  • Booking conversion: +35-60% depending on industry
  • Human escalation rate: 8-15% of conversations
  • False escalations (unnecessary human involvement): < 2%

What We Learned the Hard Way

1. Context window management is critical. An LLM that can see 100 past messages will cost 10x more than one seeing 20. We learned to summarize and compress old context.

2. Hebrew prompting needs extra care. LLMs are trained mostly on English. Hebrew system prompts need to be more explicit about tone and formality levels.

3. Tool call loops need safeguards. Occasionally an agent would loop in tool calls. We added a max-iterations limit (5) and a fallback to human escalation.

4. Test edge cases with real WhatsApp messages. Users send voice notes (which we now transcribe), images, PDFs of documents. The agent needs to handle all of these gracefully.

5. Shabbat and Jewish holidays require explicit handling. The agent needs to know not to book appointments on Shabbat/holidays and to handle Friday afternoon lead surges appropriately.

Try It

If you're building WhatsApp AI agents or want to see one in action for your Israeli business, visit aibuddy.co.il. We've open-sourced some of the utility functions from this stack — reach out via the site.

Happy to answer questions in the comments about any specific part of the architecture.

Top comments (0)