Skip to content

devlooped/WhatsApp

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Icon WhatsApp agents for Azure Functions

Version Downloads License Build

Create agents for WhatsApp using Azure Functions.

Usage

var builder = FunctionsApplication.CreateBuilder(args);
builder.ConfigureFunctionsWebApplication();

builder.Services.AddWhatsApp<MyWhatsAppHandler>();

builder.Build().Run();

Instead of providing an IWhatsAppHandler implementation, you can also register an inline handler using minimal API style:

builder.Services.AddWhatsApp((messages, cancellation) =>
{
    foreach (var message in messages)
    {
        // MessageType: Content | Error | Interactive | Reaction | Status
        Console.WriteLine($"Got message type {message.Type}");
        switch (message)
        {
            case ContentMessage content:
                // ContentType = Text | Contact | Document | Image | Audio | Video | Location | Unknown (raw JSON)
                Console.WriteLine($"Got content type {content.Content.Type}");
                break;
            case ErrorMessage error:
                Console.WriteLine($"Error: {error.Error.Message} ({error.Error.Code})");
                break;
            case InteractiveMessage interactive:
                Console.WriteLine($"Interactive: {interactive.Button.Title} ({interactive.Button.Id})");
                break;
            case StatusMessage status:
                Console.WriteLine($"Status: {status.Status}");
                break;
        }
    }

    return AsyncEnumerable.Empty<Response>();
});```

If the handler needs additional services, they can be provided directly 
as generic parameters of the `UseWhatsApp` method, such as:

```csharp
builder.Services.AddWhatsApp<ILogger<Program>>((logger, message, cancellation) =>
{
    logger.LogInformation($"Got messages!");

    return messages.OfType<ContentMessage>()
        .Select(content => content.Reply($"โ˜‘๏ธ Got your {content.Content.Type}"))
        .ToAsyncEnumerable();
}

You can also specify the parameter types in the delegate itself and avoid the generic parameters:

builder.Services.AddWhatsApp(async (ILogger<Program> logger, IEnumerable<Message> messages, CancellationToken cancellation) =>

Handlers generate responses by returning an IAsyncEnumerable<Response>, and the responses are typically created by calling extension methods on the incoming messages, such as Reply or React:

if (message is ContentMessage content)
{
    yield return message.React(message, "๐Ÿง ");
    // simulate some hard work at hand, like doing some LLM-stuff :)
    await Task.Delay(2000);
    var json = JsonSerializer.Serialize(content, options);
    yield return message.Reply($"โ˜‘๏ธ Got your {content.Content.Type}:\r\n{json}");
}

This allows the handler to remain decoupled from the actual sending of messages, making it easier to unit test.

If sending messages outside the handler pipeline is needed, you can use the provided IWhatsAppClient, which is a very thin abstraction allowing you to send arbitrary payloads to WhatsApp for Business:

public interface IWhatsAppClient
{
    /// Payloads from https://developers.facebook.com/docs/whatsapp/cloud-api/reference/messages
    Task SendAync(string from, object payload);
}

Extensions methods for this interface take care of simplifying usage for some common scenarios, such as reacting to a message and replying with plain text:

if (message is ContentMessage content)
{
    await client.ReactAsync(message, "๐Ÿง ");
    // simulate some hard work at hand, like doing some LLM-stuff :)
    await Task.Delay(2000);
    var json = JsonSerializer.Serialize(content, options);
    await client.ReplyAsync(message, $"โ˜‘๏ธ Got your {content.Content.Type}:\r\n{json}");
}

The above code would render as follows in WhatsApp:

Conversations

WhatsApp does not provide a way to keep track of conversations, at most providing the related message ID of a message that was replied to. In many agents, however, keeping track of conversations is crucial for maintaining context and continuity.

This library provides a simple built-in functionality for this based on some simple heuristics:

  • If a message is sent in response to another message, it is considered part of the same conversation.
  • Messages sent within a short time frame (default: 5 minutes) are considered part of the same conversation.
  • Individual messages, conversations and the active conversations are stored in an Azure storage account

Usage:

builder.Services
    .AddWhatsApp<MyWhatsAppHandler>()
    .UseConversation(conversationWindowSeconds: 300 /* default */);

Unless you provide a CloudStorageAccount in the service collection, the library will use the AzureWebJobsStorage connection string automatically for this, so things will just work out of the box.

An example of providing storage to a different account than the functions runtime one:

builder.Services.AddSingleton(services => builder.Environment.IsDevelopment() ?
    // Always local emulator in development
    CloudStorageAccount.DevelopmentStorageAccount :
    // First try with custom connection string
    CloudStorageAccount.TryParse(builder.Configuration["App:Storage"] ?? "", out var storage) ?
    storage :
    // Fallback to built-in functions storage (default behavior).
    CloudStorageAccount.Parse(builder.Configuration["AzureWebJobsStorage"]));

Configuration

You need to register an app in the Meta App Dashboard. The app must then be configured to use the WhatsApp Business API, and the webhook and verification token (an arbitrary value) must be set up in the app settings under WhatsApp. The webhook URL is /whatsapp under your Azure Functions app.

Make sure you subscribe the webhook to the messages field, with API version v22.0 or later.

Configuration on the Azure Functions side is done via the ASP.NET options pattern and the MetaOptions type. When you call UseWhatsApp, the options will be bound by default to the Meta section in the configuration. You can also configure it programmatically as follows:

builder.Services.Configure<MetaOptions>(options =>
{
    options.VerifyToken = "my-webhook-1234";
    options.Numbers["12345678"] = "asff=";
});

Via configuration:

{
  "Meta": {
    "VerifyToken": "my-webhook-1234",
    "Numbers": {
      "12345678": "asff="
    }
  }
}

The Numbers dictionary is a map of WhatsApp phone identifiers and the corresponding access token for it. To get a permanent access token for use, you'd need to create a system user with full control permissions to the WhatsApp Business API (app).

Functionality pipelines

IWhatsAppHandler instances can be layered to form a pipeline of components, each contributing unique capabilities. These components may originate from Devlooped.WhatsApp, external NuGet libraries, or custom implementations. This mechanism enables flexible enhancement of the WhatsApp handler's functionality to suit specific requirements. Below is an example that wraps a WhatsApp handler with logging, OpenTelemetry tracing, message storage and conversation management:

var builder = FunctionsApplication.CreateBuilder(args);
builder.ConfigureFunctionsWebApplication();

builder.Services.AddWhatsApp<MyWhatsAppHandler>()
    .UseOpenTelemetry(builder.Environment.ApplicationName)
    .UseLogging()
    .UseStorage()
    .UseConversation();

builder.Build().Run();

Creating additional cross-cutting behaviors to keep handlers clean and focused on a single responsibility is straightforward. For example, let's say you don't want to perform any processing for status messages (these are VERY noisy, sent by whatsApp foreach thing that happens to a message, such as when it is sent, delivered, read, etc.). You could easily create a custom component that filters out these messages:

static class IgnoreMessagesExtensions
{
    public static WhatsAppHandlerBuilder UseIgnore(this WhatsAppHandlerBuilder builder)
        => Throw.IfNull(builder).Use((inner, services) => new IgnoreMessagesHandler(inner,
            message => message.Type != MessageType.Status && message.Type != MessageType.Unsupported));

    public static WhatsAppHandlerBuilder UseIgnore(this WhatsAppHandlerBuilder builder, Func<IMessage, bool> filter)
        => Throw.IfNull(builder).Use((inner, services) => new IgnoreMessagesHandler(inner, filter));

    class IgnoreMessagesHandler(IWhatsAppHandler inner, Func<IMessage, bool> filter) : DelegatingWhatsAppHandler(inner)
    {
        public override IAsyncEnumerable<Response> HandleAsync(IEnumerable<IMessage> messages, CancellationToken cancellation = default)
        {
            var filtered = messages.Where(filter).ToArray();
            // Skip inner handler altogether if no messages pass the filter.
            if (filtered.Length == 0)
                return AsyncEnumerable.Empty<Response>();

            return base.HandleAsync(filtered, cancellation).WithExecutionFlow();
        }
    }
}

This new extension method can now be used in the pipeline without changing any of the existing handlers:

builder.Services.AddWhatsApp<MyWhatsAppHandler>()
    .UseOpenTelemetry(builder.Environment.ApplicationName)
    .UseLogging()
    .UseIgnore()  // ๐Ÿ‘ˆ Ignore status+unsupported messages. We do log them.
    .UseStorage()
    .UseConversation();

OpenTelemetry

The configurable built-in support for OpenTelemetry shown above allows tracking of key metrics such as message processing time and the number of messages processed.

This is a rendering of the telemetry data in Aspire in the sample app provided in this repository:

Scalability and Performance

In order to quickly and efficiently process incoming messages, the library uses Azure Storage Queues to queue incoming messages from WhatsApp, which provides a reliable and scalable way to handle incoming messages. It also uses Azure Table Storage to detect duplicate messages and avoid processing the same message multiple times.

If QueueServiceClient and TableServiceClient are registered in the DI container before invoking UseWhatsApp, the library will automatically use them. Otherwise, it will register both services using the AzureWebJobsStorage connection string, therefore sharing storage with the Azure Functions runtime.

License

We offer this project under a dual licensing model, tailored to the needs of commercial distributors and open-source projects.

For open-source projects and free software developers:

If you develop free software (FOSS) and want to leverage this project, the open-source version under AGPLv3 is ideal. If you use a FOSS license other than AGPLv3, Devlooped offers an exception, allowing usage without requiring the entire derivative work to fall under AGPLv3, under certain conditions.

See AGPLv3 and Universal FOSS Exception.

For OEMs, ISVs, VARs, and other commercial users:

If you use this project and distribute or host commercial software without sharing the code under AGPLv3, you must obtain a commercial license from Devlooped. Alternatively, you can sponsor on GitHub Sponsors at the AGPLv3 tier or above per developer, which grants you a commercial license for the duration of the sponsorship. You can sponsor through each individual developer's account or through your GitHub organization.

WhatsApp CLI

Version Downloads

Provides a command-line interface for the WhatsApp library and its backend functions. This allows you to interact with your WhatsApp pipeline without having to set up your WhatsApp for Business app for local development.

The backend functions are only enabled if the hosting environment is set to Development so that in production, the CLI endpoint is not available. Example:

The console will automatically remember the last used WhatsApp endpoint, output format and simulated user phone number.

Usage: whatsapp [OPTIONS]+
Options:
  -u, --url                  WhatsApp functions endpoint
  -n, --number=VALUE         Your WhatsApp user phone number
  -j, --json                 Format output as JSON
  -t, --text                 Format output as text
  -y, --yaml                 Format output as YAML
  -?, -h, --help             Display this help.
  -v, --version              Render tool version and updates.

to render the responses since it provides a more readable format than JSON.

Dogfooding

CI Version Build

We also produce CI packages from branches and pull requests so you can dogfood builds as quickly as they are produced.

The CI feed is https://pkg.kzu.app/index.json.

The versioning scheme for packages is:

  • PR builds: 42.42.42-pr[NUMBER]
  • Branch builds: 42.42.42-[BRANCH].[COMMITS]

Sponsors

Clarius Org MFB Technologies, Inc. Torutek DRIVE.NET, Inc. Keith Pickford Thomas Bolon Kori Francis Toni Wenzel Uno Platform Reuben Swartz Jacob Foshee Eric Johnson David JENNI Jonathan Charley Wu Ken Bonny Simon Cropp agileworks-eu sorahex Zheyu Shen Vezel ChilliCream 4OTC Vincent Limo Jordan S. Jones domischell Justin Wendlandt Adrian Alonso Michael Hagedorn Matt Frear

Sponsor this project ย 

Learn more about GitHub Sponsors

About

WhatsApp agents for Azure Functions

Topics

Resources

License

Code of conduct

Security policy

Stars

Watchers

Forks

Sponsor this project

 

Contributors 4

  •  
  •  
  •  
  •