Ctrl K
ComunidadGuías…

ai-responder/README.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# AI Responder

> Answers inbound WhatsApp messages with a reply from any OpenAI-compatible Chat Completions API, as the
> last responder in the chain.

![type: extension](https://img.shields.io/badge/type-extension-blue.svg)
![license: MIT](https://img.shields.io/badge/license-MIT-green.svg)
![built for OpenWA](https://img.shields.io/badge/OpenWA-%E2%89%A5%200.8.0-25D366.svg)
[![downloads](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Frmyndharis%2FOpenWA-plugins%2Fbadges%2Fdownloads%2Fai-responder.json)](https://github.com/rmyndharis/OpenWA-plugins/releases?q=ai-responder)

## Details

<!-- BEGIN DETAILS (generated by scripts/catalog.mjs — do not edit by hand) -->
| Field | Value |
| ----- | ----- |
| **Identifier** | `ai-responder` |
| **Version** | 0.1.6 |
| **Released** | 2026-11-10 |
| **Status** | beta |
| **Author** | Abhinav Raj |
| **License** | MIT |
| **Type** | `extension` |
| **Requires OpenWA** | ≥ 0.8.0 (tested 0.30.0) |
| **Keywords** | ai, openai, llm, auto-reply, chatbot, whatsapp, openwa |
| **Repository** | [OpenWA-plugins/ai-responder](https://github.com/rmyndharis/OpenWA-plugins/tree/main/ai-responder) |
<!-- END DETAILS -->

## Features

- **Any OpenAI-compatible provider.** OpenAI by default; Groq, OpenRouter, or a local Ollama, vLLM or
  LM Studio server through `apiBaseUrl` and `model`.
- **Last in line.** Runs at hook priority 97, after every other responder in this catalog, so a
  command, a menu, a keyword rule or the away message always answers first. It only sees what none of
  them claimed.
- **Single-turn.** Each message is sent on its own, with the system prompt. There is no conversation
  memory: the model never sees earlier messages or its own earlier replies.
- **WhatsApp formatting.** The model's Markdown (bold, italic, strikethrough, links, headings) is
  converted before sending, and the answer goes out as a quoted reply.
- **Bounded cost.** Hourly limits per chat and per session, input cut to `maxInputChars`, output capped
  by `maxOutputTokens`, and a request timeout.
- **Off the delivery path.** The provider call runs after the hook has returned, so a slow provider
  never delays message delivery or the other plugins.

## What it does and what it logs

Each inbound message is checked first. These are passed down the chain untouched, never claimed and
never sent to the provider:

- anything not from the engine, sent by this account, or without text (a sticker, a voice note, media
  without a caption);
- shared contact cards, polls, catalog orders, product cards and locations, whose text nobody typed
  at the bot. Tapped business buttons, list replies and media captions are answered;
- group messages, unless `respondInGroups` is on, and a group message with no identifiable sender;
- WhatsApp Channels (`@newsletter`), broadcast lists and status updates;
- a message sent more than five minutes before it arrived;
- a message over the per-chat or per-session hourly limit, or one that arrives while eight provider
  calls are already open.

Every other message is claimed (`{ continue: false }`) at once, before the provider answers, and the
provider call runs in the background. Its text, cut to `maxInputChars`, is sent with the system prompt;
the reply is converted to WhatsApp formatting and sent as a quoted reply. If the provider fails or times
out, the error is logged and the contact gets no reply: the message was claimed, so no other plugin
answers it either. A redelivery of a message already claimed is claimed again and not sent twice.

Log lines:

- at enable, the plugin version and model;
- a warning the first time in an hour that a chat hits the per-chat limit (naming the chat id), that a
  session hits the per-session cap (naming the session), and that eight provider calls are already
  open;
- a warning when the config resolved for a session is invalid (the message is skipped);
- an error when a reply fails, carrying the HTTP status and at most 200 characters of the provider's
  response body, with the API key redacted.

The plugin never logs message text or replies itself; the provider's error body is the only provider
text it logs, and a provider may quote part of the request there. Nothing is stored: the plugin
declares `net:fetch` and `messages:send`, and no `storage:use`.

## Setup

1. Get an API key from OpenAI or another OpenAI-compatible provider. For a local model, start the
   server (for example Ollama on `http://127.0.0.1:11434/v1`) and add `127.0.0.1` to
   `SSRF_ALLOWED_HOSTS` on the gateway. A local server that ignores the key still needs a non-empty
   `apiKey`.
2. Install the plugin (see [Install](#install)).
3. Configure it (see [Configuration](#configuration)): set `apiKey`, and `apiBaseUrl` and `model` when
   not using OpenAI. Write an optional `systemPrompt` for tone and scope; it is not confidential (see
   [Security](#security)). Adjust the limits to the expected traffic.
4. Enable it for the sessions it should answer on.
5. Check the other responders enabled on those sessions (see [Known interactions](#known-interactions)).

## Install

```bash
node package.mjs ai-responder    # produces ai-responder.zip at the repo root

curl -X POST "https://your-openwa-host/api/plugins/install" \
  -H "X-API-Key: <ADMIN_API_KEY>" -F "file=@ai-responder.zip"

curl -X PUT "https://your-openwa-host/api/plugins/ai-responder/config" \
  -H "X-API-Key: <ADMIN_API_KEY>" -H "Content-Type: application/json" \
  -d '{ "config": { "apiBaseUrl": "https://api.openai.com/v1", "apiKey": "<YOUR_API_KEY>", "model": "gpt-4o-mini" } }'

curl -X POST "https://your-openwa-host/api/plugins/ai-responder/enable" \
  -H "X-API-Key: <ADMIN_API_KEY>"
```

Or download the packaged `.zip` from [Releases](https://github.com/rmyndharis/OpenWA-plugins/releases)
and upload it in the dashboard **Plugins → Install** (or the **Catalog** tab).

## Configuration

| Key | Required | Default | Description |
| --- | -------- | ------- | ----------- |
| `apiBaseUrl` | yes | `https://api.openai.com/v1` | Base URL of an OpenAI-compatible API; the plugin posts to `<apiBaseUrl>/chat/completions`. Must be https, or http on `localhost`, `127.0.0.1` or `[::1]`, with no embedded credentials; anything else fails enable. |
| `apiKey` | yes | none | Bearer key for the provider. Stored as a secret. |
| `model` | yes | `gpt-4o-mini` | Model id sent with every request. |
| `systemPrompt` | no | none | Instructions for tone and scope, sent with every message. Not confidential. |
| `respondInGroups` | no | `false` | Also answer in group chats. Every message in every group then goes to the provider. |
| `maxRepliesPerChatPerHour` | no | `20` | Messages answered per chat per clock hour. |
| `maxRepliesPerSessionPerHour` | no | `200` | Messages answered per WhatsApp session per clock hour, across all chats. |
| `maxInputChars` | no | `2000` | The message text sent to the provider is cut to this many characters. |
| `maxOutputTokens` | no | `500` | Sent as `max_tokens`, bounding the length and cost of each reply. |
| `timeoutMs` | no | `15000` | Provider request timeout, clamped to 1000-25000 ms. |

Both limits count every message claimed in the hour, answered or failed, and are kept in memory: a
gateway restart or a re-enable starts a fresh hour. A message over a limit is not claimed, so a later
plugin in the chain may still answer it. The same holds for a message that arrives while eight provider
calls are already open.

## Compatibility

Checked against the OpenWA v0.30.0 worker runtime with isolated services. This covers plugin lifecycle, hooks or ingress dispatch, capability calls and per-session configuration. Live WhatsApp sessions, provider authentication and dashboard rendering require separate deployment checks.

Targets OpenWA **≥ 0.8.0**, which admits the configured `apiBaseUrl` host through
`net.allowConfigHosts`. Status is beta: it has not yet been smoke-tested on a live gateway.

- The request always carries `max_tokens`. Models that accept only `max_completion_tokens`, such as
  OpenAI's reasoning models, reject it; use a chat model such as the default `gpt-4o-mini`.
- A message is answered only within five minutes of when it was sent. From OpenWA 0.23.6 a Baileys
  session delivers the messages WhatsApp queued during a disconnect once it reconnects; those are left
  unanswered instead of drawing a burst of replies to old messages. The age is measured against the
  gateway's clock, so keep it in sync.
- Shared contact cards, polls, catalog orders and product cards are never answered. From OpenWA 0.23.2
  contact cards and polls carry text in the message body, and from 0.23.5 so do Baileys orders and
  product cards. A Baileys whole-catalog share arrives as type `unknown` with the catalog title as its
  body, cannot be told apart from a button reply, and is answered.
- Shared locations are never answered: on whatsapp-web.js their body is the base64 map thumbnail.

### Known interactions

This plugin runs at priority 97, after `http-action` (70), `chat-flow` (75), `faq-bot` (80),
`typebot-connector` (85) and `after-hours` (95), and answers only what none of them claimed.

- **`typebot-connector` (85) fully starves this plugin.** It claims every message in its scope, so on a
  session running both, this plugin never answers a direct chat (nor a group while the connector's
  `respondInGroups` is on, its default). Enable one or the other per session.
- **`after-hours` (95) partially starves this plugin.** Outside business hours it claims the first
  message in each chat per `cooldownSec` window and sends the away message; later messages in that
  window reach this plugin and get a model answer. A contact therefore gets the away notice and then an
  answer. If the model should answer around the clock, do not enable `after-hours` on the same session.
- **`faq-bot` (80)** claims every message a rule matches, so those never reach the model. With
  `fallbackReply` set it also claims the first unmatched message per `fallbackCooldownSec` window, and
  the rest reach this plugin: the contact gets the fallback and then a model answer. Leave
  `fallbackReply` empty to let the model answer whatever the rules miss.
- **`http-action` (70) and `chat-flow` (75)** claim only messages addressed to them (a command, a
  trigger word, a reply inside an active menu); everything else reaches this plugin.
- A plugin registered without an explicit priority runs at 100, after this one, and never sees a
  message this plugin claimed.

### Per-session config

**Supported.** Every config field may be overridden per WhatsApp session via the dashboard; an override
takes effect on the next inbound message (config is re-read per event). For example, two sessions can
use different providers, models and system prompts. A per-session https `apiBaseUrl` host is admitted
through `net.allowConfigHosts` like the base one. Rate-limit counters are kept per session and per
chat, and each session's limits come from its own resolved config.

**Host overrides and the API key.** From OpenWA 0.26.0 a per-session override that points
`apiBaseUrl` at another origin than the base config does not inherit the stored `apiKey`; set `apiKey`
in that override too. Without it the plugin skips the message and logs that the key is required.

## Security

- **Every in-scope message is sent to the provider.** The text of every message this plugin answers
  (up to `maxInputChars`), with the system prompt, goes to the configured provider: `api.openai.com` by
  default. Contacts are not told. The operator is responsible for having their consent and a legal basis
  to share their messages with that provider, and for its data retention terms. Group messages are
  included only with `respondInGroups`.
- **The system prompt is not secret.** Any contact can try to make the model reveal, ignore or override
  it. Never put confidential data (credentials, internal URLs, customer records, private pricing rules)
  in `systemPrompt`, and do not rely on it to enforce a policy.
- **Contacts can steer the replies.** A message is model input, so a contact can talk the model into
  writing whatever they want (a false promise, a price, a link, offensive text), and it is sent from
  the account as a reply. The reply only ever goes to the chat the message came from, which with
  `respondInGroups` on is the whole group, and the model gets no tools: a contact cannot make the
  plugin message another chat, read other conversations or call anything but the provider. Nothing
  reviews a reply before it is sent, so do not enable the plugin where a wrong answer in the account's
  name is unacceptable.
- **Loop and cost bounds.** The plugin never answers its own messages, a channel or a broadcast. A loop
  with another autoresponder is capped at `maxRepliesPerChatPerHour` replies per chat per hour, and all
  traffic on a session at `maxRepliesPerSessionPerHour`. Each call is bounded by `maxInputChars` in,
  `maxOutputTokens` out and `timeoutMs`, and at most eight calls are open at once, so a burst cannot
  take every one of the gateway's shared outbound request slots from the other plugins. The limits are
  in memory, so a restart grants a fresh hour.
- **Outbound HTTP is allow-listed.** Calls go through the host's SSRF-guarded `ctx.net.fetch`. Reachable
  hosts are the https host of `apiBaseUrl` (`net.allowConfigHosts`) and the loopback entries
  `localhost`, `127.0.0.1` and `[::1]` in `net.allow`, which exist for a local model. Plain http is
  accepted only for those loopback hosts and never with embedded credentials; the plugin refuses to
  enable otherwise. The gateway's SSRF guard still blocks loopback until the operator adds the host to
  `SSRF_ALLOWED_HOSTS`, which a local model therefore needs.
- **The API key** is a secret field (masked on read), sent only as a Bearer token to `apiBaseUrl`, and
  redacted from any provider error body before it is logged.

## Changelog

See [CHANGELOG.md](./CHANGELOG.md).

## License

[MIT](../LICENSE) © Abhinav Raj.