Introduction

Learn how to create a WhatsApp bot that acts similar to ChatGPT using A1Base. This simple bot demonstrates the capabilities of A1Base as an identity layer for AI agents.

Key Features

  • Uses conversation history to produce contextually relevant responses

  • Responds to both individual and group messages

  • Message collection and storage

Use Cases

  • Answering questions and providing conversation similar to ChatGPT

  • Boilerplate for other A1Base chat-based agents

Implementation

Below is a complete Express.js implementation that demonstrates how to:

  1. Listen to incoming WhatsApp messages with webhooks

  2. Process and store responses split by threads

  3. Generate AI responses with the OpenAI API using the full conversation context

  4. Send responses to individual and group chats with the A1Base API

chat-agent.ts
import { A1BaseAPI, type WhatsAppIncomingData } from "a1base-node";
import express, { Express, Request, Response } from "express";
import OpenAI from "openai";

const AGENT_NUMBER = "YOUR_AGENT_NUMBER"; //e.g. +12345678900
const API_KEY = "YOUR_A1BASE_API_KEY";
const API_SECRET = "YOUR_A1BASE_API_SECRET";
const ACCOUNT_ID = "YOUR_A1BASE_ACCOUNT_ID";
const OPENAI_API_KEY = "YOUR_OPENAI_API_KEY";

interface ThreadMessage {
  message_id: string;
  content: string;
  sender_number: string;
  sender_name: string;
  timestamp: string;
}

const openai = new OpenAI({
  apiKey: OPENAI_API_KEY,
});

// Initialize A1Base client
const credentials = {
  apiKey: API_KEY,
  apiSecret: API_SECRET,
};
const client = new A1BaseAPI({
  credentials
});

// Store messages by thread ID
const messagesByThread = new Map<string, ThreadMessage[]>();

const getSystemPrompt = (userName: string) => `
You are a friendly AI chat assistant. Keep your responses concise and engaging.

The user's name is ${userName}.
You should address them by name in the first message, and in future messages where appropriate (but not too often, keep it natural).

Keep messages relatively short as this conversation is occurring over WhatsApp.

Try to mirror their mannerisms. If they use lowercase, you should too. Etc.
`;

async function generateAgentResponse(threadId: string): Promise<string> {
  const threadMessages = messagesByThread.get(threadId) || [];
  const messages = threadMessages.map((msg) => ({
    role:
      msg.sender_number === AGENT_NUMBER
        ? ("assistant" as const)
        : ("user" as const),
    content: msg.content,
  }));

  // Get the user's name from the thread
  const userName =
    [...threadMessages]
      .reverse()
      .find((msg: ThreadMessage) => msg.sender_number !== AGENT_NUMBER)
      ?.sender_name || "";

  console.log(messages);

  const completion = await openai.chat.completions.create({
    model: "gpt-4o",
    messages: [
      { role: "system", content: getSystemPrompt(userName) },
      ...messages,
    ],
  });

  return (
    completion.choices[0].message.content ||
    "Sorry, I couldn't generate a response"
  );
}

const saveMessage = (threadId: string, message: ThreadMessage) => {
  const threadMessages = messagesByThread.get(threadId) || [];
  threadMessages.push(message);
  messagesByThread.set(threadId, threadMessages);
  console.log(`[Save] Stored new message in thread:`, threadId, message);
};

const app: Express = express();
const port = 3000;

// Middleware to parse JSON bodies
app.use(express.json());

// Endpoint to receive WhatsApp replies
app.post(
  "/whatsapp/incoming",
  async (req: Request, res: Response): Promise<any> => {
    console.log("[Receive] Incoming message:", req.body);

    let {
      thread_id,
      message_id,
      thread_type,
      content,
      sender_number,
      sender_name,
      a1_account_id,
      timestamp,
    } = req.body as WhatsAppIncomingData;

    // Patch bug where sender_number is not prefixed with +
    sender_number = "+" + sender_number;

    // Patch bug where group message sender number is missing if sender is a1base agent
    if (thread_type === "group" && sender_number === "+") {
      sender_number = AGENT_NUMBER;
    }

    // Store message in thread history regardless of sender
    saveMessage(thread_id, {
      message_id,
      content,
      sender_number,
      sender_name,
      timestamp,
    });

    // Don't respond to messages from the agent itself
    if (sender_number === AGENT_NUMBER) {
      console.log("[Receive] Message from agent, not generating response");
      return res.json({ success: true });
    }

    const aiResponse = await generateAgentResponse(thread_id);

    if (thread_type === "group") {
      try {
        await client.sendGroupMessage(ACCOUNT_ID, {
          content: aiResponse,
          from: AGENT_NUMBER,
          thread_id: thread_id,
          service: "whatsapp",
        });
      } catch (error) {
        console.error("[Chat] Error generating/sending AI response:", error);
        await client.sendGroupMessage(ACCOUNT_ID, {
          content: "Sorry, I encountered an error processing your message",
          from: AGENT_NUMBER,
          thread_id: thread_id,
          service: "whatsapp",
        });
      }
    } else {
      try {
        await client.sendIndividualMessage(ACCOUNT_ID, {
          content: aiResponse,
          from: AGENT_NUMBER,
          to: sender_number,
          service: "whatsapp",
        });
      } catch (error) {
        console.error("[Chat] Error generating/sending AI response:", error);
        await client.sendIndividualMessage(ACCOUNT_ID, {
          content: "Sorry, I encountered an error processing your message",
          from: AGENT_NUMBER,
          to: sender_number,
          service: "whatsapp",
        });
      }
    }

    return res.json({ success: true });
  },
);

// Start the server
app.listen(port, () => {
  console.log(`[Server] My bot running at http://localhost:${port}`);
  if (!API_KEY || !API_SECRET || !ACCOUNT_ID || !AGENT_NUMBER) {
    console.warn(
      "[Server] Warning: Missing environment variables for WhatsApp integration",
    );
  }
});