jigar-dhulla / laravel-whatsapp-ai-agent
A Laravel package for building WhatsApp AI agents on top of wacli.
Package info
github.com/jigar-dhulla/laravel-whatsapp-ai-agent
pkg:composer/jigar-dhulla/laravel-whatsapp-ai-agent
Requires
- php: ^8.3
- laravel/ai: ^0.6
- laravel/framework: ^13.0
Requires (Dev)
- laravel/pint: ^1.0
- orchestra/testbench: ^11.0
- phpunit/phpunit: ^12.0
README
An extension to the Laravel AI SDK that adds WhatsApp as an agent interface. It polls a wacli SQLite database for new messages, routes them through your configured agents (any class that implements Laravel\Ai\Contracts\Agent), and sends replies back via the wacli binary.
Requirements
- PHP 8.3+
- Laravel 13.0+
laravel/ai^0.6wacli0.6.0+ installed and authenticated on the host
Installation
composer require jigar-dhulla/laravel-whatsapp-ai-agent
The package registers itself automatically via Laravel's package auto-discovery.
Setup
Run the setup command to detect your wacli binary and write paths to .env:
php artisan wa:setup
This will:
- Locate your wacli binary
- Run
wacli doctorto detect your store directory - Prompt for the wacli SQLite database path
- Write
WA_WACLI_BINARY,WA_WACLI_STORE, andWA_WACLI_DATABASEto.env
Then publish the config to customize your agents:
php artisan vendor:publish --tag=whatsapp-agent-config
This publishes config/whatsapp-agent.php, which includes a default WhatsAppAgent ready to use. Edit the config to add your own agents or customize scopes and triggers.
Configuration
config/whatsapp-agent.php
return [ 'wacli' => [ 'binary' => env('WA_WACLI_BINARY', 'wacli'), 'database' => env('WA_WACLI_DATABASE'), 'store' => env('WA_WACLI_STORE'), ], /* | One entry per agent. The agent key must be a class that implements | Laravel\Ai\Contracts\Agent (use the Promptable trait for the full SDK | feature set). Provider and model are optional runtime overrides; if null, | the agent class resolves them via its own #[Provider]/#[Model] attributes | or config/ai.php defaults. | | - triggers: [] matches every message in this agent's scope | - chats/groups: at least one entry total — empty scope = inactive agent */ 'agents' => [ [ 'agent' => \JigarDhulla\LaravelWhatsApp\Agents\WhatsAppAgent::class, 'triggers' => [], 'chats' => [], 'groups' => [], ], ], 'polling' => [ 'interval_seconds' => (int) env('WA_POLLING_INTERVAL', 60), ], ];
AI provider configuration (API keys, default models) belongs in config/ai.php, managed by laravel/ai — not in this package.
Environment Variables
| Variable | Default | Description |
|---|---|---|
WA_WACLI_BINARY |
wacli |
Path to the wacli binary (0.6.0+ required) |
WA_WACLI_DATABASE |
— | Path to the wacli SQLite database (set by wa:setup) |
WA_WACLI_STORE |
— | Path to the wacli store directory (set by wa:setup) |
WA_AGENT_PROVIDER |
— | Optional: AI provider override for the default agent (e.g. anthropic, openai) |
WA_AGENT_MODEL |
— | Optional: Model identifier override for the default agent |
WA_POLLING_INTERVAL |
60 |
Seconds between database polls |
WA_HISTORY_LIMIT |
100 |
Max past messages included in agent context |
Creating Custom Agents
Generate a new agent with make:agent:
php artisan make:agent CustomAgent
This creates app/Ai/Agents/CustomAgent.php. Then add it to your published config/whatsapp-agent.php:
'agents' => [ [ 'agent' => \App\Ai\Agents\CustomAgent::class, 'triggers' => ['@custom'], 'chats' => ['111@s.whatsapp.net'], 'groups' => [], ], ],
You can keep the default WhatsAppAgent in the config alongside your custom agents, or remove it entirely.
Conversation memory
Add the RemembersWhatsAppConversations trait to your agent to give it access to the full message history from the wacli SQLite database. The trait implements the Laravel AI SDK's conversation interface, so previous messages in the chat are automatically injected as context on every prompt:
use JigarDhulla\LaravelWhatsApp\Traits\RemembersWhatsAppConversations; use Laravel\Ai\Contracts\Agent; use Laravel\Ai\Contracts\Conversational; use Laravel\Ai\Promptable; class CustomAgent implements Agent, Conversational { use Promptable; use RemembersWhatsAppConversations; }
The number of historical messages included is controlled by WA_HISTORY_LIMIT (default 100). Override maxConversationMessages() in your agent class to set a per-agent limit.
Multiple Agents
One message can match multiple agents simultaneously. Each match dispatches an independent queued job, so agents process in parallel:
// config/whatsapp-agent.php 'agents' => [ [ 'agent' => \App\Ai\Agents\SupportAgent::class, 'triggers' => ['@support'], 'chats' => ['111@s.whatsapp.net'], 'groups' => ['group1@g.us'], ], [ 'agent' => \App\Ai\Agents\SalesAgent::class, 'triggers' => ['@sales'], 'chats' => ['111@s.whatsapp.net'], 'groups' => [], ], [ 'agent' => \App\Ai\Agents\BroadcastAgent::class, 'triggers' => [], // empty = matches all messages in scope 'chats' => [], 'groups' => ['announcements@g.us'], ], ],
Routing rules:
- A message matches an agent when its chat/group JID is in the agent's
chatsorgroupslist and the body contains at least one trigger phrase (case-insensitive). - An empty
triggersarray matches every message in scope. - An agent with empty
chatsand emptygroupsis inactive and never fires.
Usage
Start the listener
php artisan wa:listen
Runs an infinite polling loop. Each iteration syncs new wacli messages, routes them via AgentRouter, and dispatches one ProcessWhatsAppMessage job per matched agent.
php artisan wa:listen --once # single iteration, then exit php artisan wa:listen --max-iterations=10 php artisan wa:listen -vv # show startup config summary php artisan wa:listen -vvv # show each message scanned
Check agent status
php artisan wa:status
Shows wacli auth/connection state and a summary of every configured agent — class name, provider/model, triggers, and scope.
Queue worker
Each matched message dispatches a ProcessWhatsAppMessage job. Run a queue worker alongside the listener:
php artisan queue:work
How It Works
wacli sync → SQLite DB → WhatsAppMessageReader → AgentRouter
↓ (one job per match)
ProcessWhatsAppMessage (queued)
↓
$agent->prompt($body)
↓
wacli send text
wa:listencallswacli sync --once --idle-exit 5seach iteration.WhatsAppMessageReaderfetches rows newer than the last processed rowid, filtered to the union of all agents' JIDs.AgentRouter::match($chatJid, $body)returns every agent config entry whose scope contains the chat and whose triggers match the message.- One
ProcessWhatsAppMessagejob is dispatched per matched entry. - The job resolves the agent class from the container, calls
$agent->prompt($body), and sends the reply viawacli send text.
Known Limitations
Sync lock blocks replies — wacli sync holds an exclusive lock, so wacli send cannot run concurrently. This package works around it by calling waitUntilUnlocked() before each send, but under heavy message load the reply may be delayed until the sync finishes. Native support for concurrent sync and send is being tracked in steipete/wacli#6.
Testing
composer install vendor/bin/phpunit vendor/bin/pint
License
MIT — see LICENSE.