Skip to content

Plugin de IA ​

@botmesh/ai es un paquete separado que añade un autoresponder de IA multi-proveedor (Anthropic / OpenAI / Ollama / Gemini más nueve presets compatibles con OpenAI), y un cliente ctx.tools.ai que puedes usar desde tus propios handlers, flows y conversaciones.

Paquete separado

A diferencia de slashCommandsPlugin (incluido en el core), @botmesh/ai se distribuye como un paquete propio para que el core se mantenga libre de dependencias de IA.

Política de IA de WhatsApp (desde el 15-01-2026) ​

Los asistentes de IA de propósito general no están permitidos en la WhatsApp Business Platform

Desde el 15 de enero de 2026, los términos de Meta para la WhatsApp Business Platform (sección AI Providers) prohíben los asistentes de IA de propósito general: chatbots abiertos estilo ChatGPT cuya funcionalidad principal es la propia IA. Sigue estando permitido usar IA al servicio del negocio (atención al cliente, ventas, reservas, seguimiento de pedidos).

Qué significa para aiPlugin:

  • Escribe una persona acotada a tu negocio (qué vendes, horarios, políticas) que rechace con amabilidad lo que no tenga que ver con él — mira el quickstart de abajo.
  • Ofrece una salida hacia una persona y no presentes el bot como un asistente "para todo".
  • La regla viene de los términos de la Cloud API oficial (@botmesh/meta). Baileys es un cliente no oficial de WhatsApp Web y también está sujeto a los términos de WhatsApp.

Es una guía práctica, no asesoría legal: revisa los términos vigentes antes de salir a producción.

Instalación ​

Alpha — instalar con el tag alpha

@botmesh/ai está publicado como 0.2.0-alpha.0; los comandos de abajo usan @alpha, porque changeset publish no crea el tag latest.

bash
pnpm add @botmesh/ai@alpha @anthropic-ai/sdk

El segundo paquete es el SDK del proveedor que vayas a usar:

ProveedorSDK a instalar
anthropic (default)@anthropic-ai/sdk
openaiopenai
gemini@google/genai (el obsoleto @google/generative-ai sigue funcionando)
ollamaninguno — habla HTTP directo (fetch) contra un servidor Ollama local o remoto
deepseek, qwen, glm, kimi, groq, openrouter, mistral, grok, togetheropenai — son presets OpenAI-compatibles que se alcanzan vía el SDK openai con un baseURL propio

Quickstart (autoresponder) ​

ts
import { Bot } from '@botmesh/core'
import { BaileysProvider } from '@botmesh/baileys'
import { aiPlugin } from '@botmesh/ai'

const bot = new Bot({ provider: new BaileysProvider({ session: 'mi-bot' }) })

// Persona acotada al negocio (ver "Política de IA de WhatsApp" arriba).
const persona = [
  'Eres el asistente de atención al cliente de Panadería La Espiga.',
  'Solo respondes sobre sus productos, precios, horarios (lun-sáb 7:00-20:00), pedidos y encargos.',
  'Si te piden algo ajeno a la panadería, di con amabilidad que solo puedes ayudar con eso',
  'y ofrece pasar la conversación a una persona. Responde breve y en español; no inventes datos.',
].join(' ')

bot.plug(aiPlugin({
  provider: 'anthropic',
  apiKey: process.env.ANTHROPIC_API_KEY,
  persona,
}))

await bot.start()

Esto registra:

  • Un autoresponder catch-all (bot.onMessage) que contesta cualquier mensaje de texto que ningún otro handler/comando/conversación haya consumido, manteniendo un historial por chat (ventana acotada, 20 mensajes por defecto — ver context.maxMessages).
  • Un comando /reset que limpia el contexto de IA del chat y cancela la conversación activa (opt-out con resetCommand: false).

Modo avanzado: ctx.tools.ai ​

El plugin también hace bot.provide('ai', client), así que ctx.tools.ai está disponible en cualquier when(), flow o conversación. Desactiva el autoresponder (autoReply: false) si solo quieres esto:

ts
bot.plug(aiPlugin({
  provider: 'anthropic',
  apiKey: process.env.ANTHROPIC_API_KEY,
  autoReply: false,
}))

bot.conversation('reclamo', async (c, ctx) => {
  await ctx.reply('📝 Cuéntanos qué pasó con tu pedido:')
  const texto = await c.waitText()
  const resumen = await ctx.tools.ai.ask(
    `Resume en 3 líneas este reclamo de un cliente para el equipo de soporte: ${texto}`,
  )
  await ctx.reply(`✅ Registramos tu reclamo:\n${resumen}`)
})

ctx.tools.ai (un AIClient) expone:

  • ask(prompt, opts?): Promise<string> — una pregunta suelta, sin historial de chat (no lee ni escribe el contexto guardado). Usa la persona/model/maxTokens/effort configurados en aiPlugin (y las herramientas, si las hay).
  • replyInChat(chatKey, text, opts?): Promise<string> — responde dentro del historial de un chat (lo lee y guarda el turno), igual que el autoresponder. opts acepta { userId, isGroup } para la memoria y { ctx, confirm, signal } para las herramientas; usa chatKeyOf(ctx) como clave. replyInChatDetailed() devuelve además las citas y los hechos del turno.
  • streamReplyInChat(chatKey, text, opts?) — el gemelo en streaming de replyInChat, para pasárselo a ctx.stream().
  • runTools(input, opts?) — ejecuta el bucle de herramientas y devuelve su resultado completo.
  • memory(), memoryFacts(), remember(), forget(), extractMemory() — la API de memoria a largo plazo; idle() espera a la extracción en segundo plano.
  • generate(input: { messages, system?, model?, maxTokens?, effort?, signal? }): Promise<GenerateResult> — control total: mensajes explícitos y overrides puntuales.
  • stream(input): AsyncIterable<string> — igual que generate pero entrega el texto en fragmentos (for await (const delta of ...)).

Tipado (TypeScript). Importar @botmesh/ai amplía la interfaz DefaultTools del core, así que ctx.tools.ai ya es un AIClient en los handlers de when(), onMessage() y command() (y en bot.injectTools.ai), sin casts. El tipo no comprueba que el plugin esté instalado: sin bot.plug(aiPlugin(...)) (o después de unplug()), ctx.tools.ai es undefined en tiempo de ejecución.

Los proveedores ​

Anthropic (default) ​

ts
bot.plug(aiPlugin({
  provider: 'anthropic',                     // opcional, es el default
  apiKey: process.env.ANTHROPIC_API_KEY,     // o deja que el SDK lea ANTHROPIC_API_KEY del entorno
  // model: 'claude-opus-5-5',               // default; considera 'claude-sonnet-5-5' o
                                              // 'claude-haiku-5-5' para chats de alto volumen (más baratos)
}))

OpenAI ​

ts
bot.plug(aiPlugin({
  provider: 'openai',
  apiKey: process.env.OPENAI_API_KEY,
  model: 'gpt-5.1',   // OBLIGATORIO: OpenAIProvider no trae modelo por defecto
}))

baseUrl es opcional (endpoints compatibles con la API de OpenAI). El tope de tokens de salida va en max_completion_tokens para OpenAI y Azure OpenAI (sin baseUrl, api.openai.com, *.openai.azure.com, *.cognitiveservices.azure.com o *.services.ai.azure.com bajo /openai) y en max_tokens para cualquier otro baseUrl (vLLM, LM Studio, gateways), que es lo que esos servidores esperan. maxTokensField lo fuerza.

Gemini ​

Requiere @google/genai (peer opcional, carga lazy). Google declaró obsoleto el SDK anterior, @google/generative-ai; se sigue usando si @google/genai no está instalado, con un DeprecationWarning la primera vez. baseUrl pasa a ser el httpOptions.baseUrl del SDK.

ts
bot.plug(aiPlugin({
  provider: 'gemini',
  apiKey: process.env.GEMINI_API_KEY,  // o GOOGLE_API_KEY en el entorno
  // model: 'gemini-3.8-flash',        // default si se omite
}))

Para Vertex AI, o cualquier opción del cliente que aiPlugin no expone, crea el provider tú mismo y adapta el cliente con fromGoogleGenAI:

ts
import { GoogleGenAI } from '@google/genai'
import { GeminiProvider, fromGoogleGenAI } from '@botmesh/ai'

const ai = new GoogleGenAI({ vertexai: true, project: 'mi-proyecto', location: 'global' })
bot.plug(aiPlugin({ instance: new GeminiProvider(fromGoogleGenAI(ai), { model: 'gemini-3.8-flash' }) }))

Ollama (local, sin API key) ​

ts
bot.plug(aiPlugin({
  provider: 'ollama',
  baseUrl: 'http://localhost:11434',   // default si se omite
  model: 'llama3.1',                   // OBLIGATORIO: OllamaProvider no trae modelo por defecto
}))

Presets — DeepSeek, Qwen, GLM, Kimi, Groq, OpenRouter, Mistral, Grok, Together ​

Cada preset es un endpoint compatible con OpenAI que se alcanza vía el SDK openai con un baseURL propio — cero dependencias extra más allá de openai, y cada uno trae un modelo por defecto y su variable de entorno convencional para la API key:

ts
bot.plug(aiPlugin({ provider: 'deepseek' }))  // lee DEEPSEEK_API_KEY, modelo deepseek-chat
PresetVariable de entornoModelo por defecto
deepseekDEEPSEEK_API_KEYdeepseek-chat
qwenDASHSCOPE_API_KEYqwen-plus
glmZHIPUAI_API_KEYglm-5.3
kimiMOONSHOT_API_KEYmoonshot-v1-8k
groqGROQ_API_KEYllama-3.3-70b-versatile
openrouterOPENROUTER_API_KEYopenrouter/auto
mistralMISTRAL_API_KEYmistral-small-latest
grokXAI_API_KEYgrok-4.7
togetherTOGETHER_API_KEYmeta-llama/Llama-3.3-70B-Instruct-Turbo

apiKey y model sobreescriben los defaults:

ts
bot.plug(aiPlugin({
  provider: 'groq',
  apiKey: process.env.GROQ_API_KEY,
  model: 'llama-3.3-70b-versatile',
}))

model es obligatorio en openai y ollama

Ninguno de los dos trae un default razonable (hay demasiados modelos válidos y cambian seguido) — si lo omites, el provider lanza un error explícito en el primer generate/stream. anthropic (claude-opus-5-5), gemini (gemini-3.8-flash) y los presets sí traen default.

Los modelos por defecto no entran en semver

Cada proveedor retira modelos a su ritmo, así que los modelos por defecto siguen a los actuales y pueden cambiar en una versión menor. Fija model si la elección te importa (coste, latencia, estilo de respuesta).

Versiones de SDK soportadas ​

Los SDK son dependencias peer opcionales; instala el que necesite tu proveedor:

SDKVersiones aceptadasProbadas
@anthropic-ai/sdk^0.68.0 || ^1.0.00.68.0 y 1.x
openai^6.48.0 || ^7.0.06.48.0 y 7.x
@google/genai^2.0.02.0.0 y 2.x
@google/generative-ai (obsoleto)^0.21.0 a ^0.24.00.24

Los tests usan cada SDK en los dos extremos de su rango contra una imitación local de la API HTTP: respuestas simples, llamadas a herramientas con su resultado y streaming.

Si el proveedor está mal configurado (SDK sin instalar, proveedor desconocido, API key ausente), aiPlugin no revienta el proceso: install es síncrono y resuelve el proveedor de forma perezosa. Al arrancar se hace un "warmup" que registra el problema en el log de errores (con la pista de qué pnpm add correr cuando falta un SDK), y a partir de ahí cada mensaje responde el errorMessage configurado en vez de tumbar el bot.

Una configuración que nunca puede funcionar falla al construirla: aiPlugin() lanza un RangeError / TypeError que nombra la opción ante un timeout que no es un número finito de ms (un NaN de Number(process.env.X) saltaría a 1 ms), un maxTokens que no es un entero positivo, un context.maxMessages negativo o NaN, un toolConfirm.timeout que no se puede leer o una herramienta sin handler. openai / ollama sin model dejan un aviso en el log al instalar.

bot.stop() aborta todas las llamadas al modelo en curso: las respuestas automáticas, la extracción de memoria en segundo plano y las llamadas que hacen tus propios handlers con ctx.tools.ai (ask, generate, stream, replyInChat, streamReplyInChat, runTools), que rechazan con un AbortError. Un proveedor lento no mantiene vivo el proceso, ni siquiera con timeoutMs: 0.

Config completa de aiPlugin(config) ​

CampoTipoDefaultDescripción
provider'anthropic' | 'openai' | 'ollama' | 'gemini' | 'deepseek' | 'qwen' | 'glm' | 'kimi' | 'groq' | 'openrouter' | 'mistral' | 'grok' | 'together''anthropic'proveedor a resolver
modelstringsegún proveedormodelo a usar (obligatorio en openai/ollama)
apiKeystringfallback a env varAPI key (si se omite, cada proveedor intenta su variable de entorno convencional)
baseUrlstringsegún presetoverride de endpoint (baseURL de openai/anthropic / host de ollama)
instanceAIProvider—proveedor ya construido (MockAIProvider en tests, o el tuyo): se salta la resolución de provider/apiKey/model y la carga perezosa del SDK
personastring—system prompt
maxTokensnumber4096 (anthropic) / 1024 (openai, presets, gemini)límite de tokens de salida (en ollama solo si lo fijas, como options.num_predict; el tope de 1024 de la extracción de memoria no se aplica ahí). En modelos de razonamiento incluye los tokens de pensamiento
effort'low' | 'medium' | 'high' | 'xhigh' | 'max'—esfuerzo de razonamiento (solo Anthropic; no se envía a modelos que lo rechazan, como Claude Haiku 4.5 / Sonnet 4.5)
thinking'auto' | 'adaptive' | 'off' | { budgetTokens }'auto'pensamiento extendido de Anthropic, ver Pensamiento (Anthropic)
timeoutMsnumber60000timeout de cada llamada al modelo. Sin streaming: plazo total; al vencer aborta la petición y rechaza, para que un proveedor colgado no bloquee la cola del chat. Con streaming: timeout de inactividad que se reinicia con cada fragmento, así que una respuesta larga nunca se corta. 0 lo desactiva (no recomendado)
maxTokensField'max_tokens' | 'max_completion_tokens'según baseUrlproveedores compatibles con OpenAI: el campo del tope de tokens de salida (ver OpenAI arriba)
context.maxMessagesnumber20ventana de historial guardada por chat (siempre empieza por un turno del usuario; las respuestas vacías no se guardan)
autoReplybooleantrueregistra el autoresponder catch-all
resetCommandbooleantrueregistra /reset
errorMessagestringai.error localizado (es: '⚠️ Ahora no puedo responder, intenta luego.')respuesta si el proveedor falla o el modelo responde vacío
refusalMessagestringai.refusal localizado (es: 'No puedo ayudar con eso.')respuesta cuando el proveedor rehúsa (stopReason === 'refusal': rechazo de Anthropic, refusal de OpenAI, bloqueo de seguridad de Gemini)
humanizeTypingbooleantrueenvía "escribiendo…" (ctx.typing()) mientras se genera la respuesta; la respuesta se salta entonces el BotConfig.typing, así que el indicador sale una sola vez
toolsRecord<string, AITool>—funciones que el modelo puede llamar (ver Herramientas)
maxStepsnumber5máximo de llamadas al modelo por turno en el bucle de herramientas
toolTimeoutMsnumber15000timeout por defecto de cada herramienta (0 lo desactiva)
maxStepsMessagestringmensaje localizado ai.tools.maxSteps (es: 'No pude completar la solicitud. Intenta de nuevo con más detalle.')respuesta cuando el bucle llega a maxSteps
toolConfirm{ yes?: string[]; no?: string[]; timeout?: number | string; buttons?: boolean; answeredNotice?: boolean; expiredNotice?: boolean; systemNote?: boolean | string }{ timeout: '2m', buttons: true }diálogo de confirmación de las herramientas confirm: true (ver Confirmación humana)
format'auto' | 'whatsapp' | 'markdown' | 'html' | 'plain' | false'auto'convierte el Markdown del modelo al formato del canal (ver Formato por canal)
onToolEvent(event: AIToolEvent) => void—auditoría de cada evento del bucle (también se emite como bot.on('ai.tool'))
memoryboolean | AIMemoryConfig—memoria a largo plazo por usuario (ver Memoria y RAG)
ragAIRetriever | AIRagConfig—respuestas basadas en tus documentos (ver Memoria y RAG)
streamboolean | AIStreamConfigapagadotransmite el autoresponder con ctx.stream() (ver Streaming)

Pensamiento (Anthropic) ​

Los modelos Claude piensan antes de responder. @botmesh/ai elige la petición adecuada para el modelo que configures (thinking: 'auto', el valor por defecto):

ModeloQué se envía
Claude Opus / Sonnet 4.6 o posteriores, Haiku 5 o posteriores, Fable, Mythos, ids desconocidosthinking: { type: 'adaptive' } (+ effort si lo fijas)
Claude Haiku 4.5, Sonnet 4.5, Opus 4.5 y anterioressin thinking (y sin effort, salvo en Opus 4.5)
ts
aiPlugin({ provider: 'anthropic', model: 'claude-haiku-4-5', thinking: { budgetTokens: 2048 } })
  • 'adaptive' envía siempre pensamiento adaptativo; 'off' no envía nunca el parámetro (los modelos que siempre piensan, como Claude Opus 5.5, lo siguen haciendo: baja effort en su lugar).
  • { budgetTokens } (un entero positivo; cualquier otro valor lanza un RangeError al construir la configuración) activa el pensamiento con presupuesto (budget_tokens, elevado al mínimo de 1024) en los modelos sin pensamiento adaptativo (Haiku 4.5, Sonnet 4.5, Opus 4.5 y anteriores). Los modelos adaptativos reciben pensamiento adaptativo: Opus / Sonnet 4.6 aún aceptan presupuestos pero los marcan como obsoletos, y Opus 4.7+, Sonnet 5+, Haiku 5+, Fable y Mythos los rechazan. Si maxTokens no deja sitio para la respuesta, pasa a ser budgetTokens + maxTokens.
  • Si aun así la API rechaza la petición con un 400 que menciona thinking (o effort), la llamada se reintenta una vez sin ese parámetro y el modelo queda recordado, así que las siguientes llamadas ya no lo envían. anthropicModelSupport(model) expone las reglas.

Herramientas ​

Con tools, el modelo puede llamar a funciones de tu bot mediante el tool / function calling nativo (Anthropic, OpenAI y los presets compatibles, Gemini, Ollama), en un bucle de varios pasos con timeout por herramienta, errores devueltos al modelo, confirmación humana para las herramientas confirm: true y eventos de auditoría:

ts
bot.plug(aiPlugin({
  provider: 'openai',
  model: 'gpt-5-mini',
  tools: {
    buscar_producto: {
      description: 'Busca productos del catálogo por texto',
      schema: { type: 'object', properties: { q: { type: 'string' } }, required: ['q'] },
      handler: async ({ q }: { q: string }) => db.products.search(q),
    },
  },
}))

Guía completa: Herramientas de IA.

Memoria y RAG ​

memory recuerda hechos de cada usuario entre conversaciones (se guardan en ctx.user, se inyectan en el autoresponder y, opcionalmente, el modelo los extrae en segundo plano). rag responde a partir de tus documentos con citas numeradas, usando cualquier retriever, como createRag() del paquete experimental @botmesh/rag.

ts
bot.plug(aiPlugin({
  provider: 'openai',
  model: 'gpt-5-mini',
  memory: { autoExtract: true, ttl: '90d' },
  rag: { retriever: createRag({ documents: ['./docs/faq.md'] }), appendSources: true },
}))

Guía completa: Memoria y RAG.

Streaming ​

stream: true envía la respuesta del autoresponder mientras se genera: borradores nativos en Telegram, ediciones en vivo en Discord y trozos por frases con "escribiendo…" en WhatsApp. Los turnos con herramientas no se transmiten. Guía completa: Respuestas en streaming.

Formato por canal ​

Los modelos escriben Markdown (**negrita**, # título, [enlace](url), - elemento), pero la mayoría de las apps de chat no lo muestran. El autoresponder convierte cada respuesta (también en streaming) al formato del chat, ctx.textFormat: el textFormatFor(chatId) del provider cuando sirve varios canales y, si no, su capabilities.textFormat:

textFormatProvidersResultado
'whatsapp'baileys, meta, evolution, wppconnect, gupshup; chats whatsapp: de twilio; contactos de WhatsApp en highlevel*negrita*, _cursiva_, ~tachado~, viñetas •, títulos en negrita, enlaces como texto (url)
'plain'telegram (se envía sin parse_mode), messenger, instagram, tiktok; chats SMS de twilio; contactos de SMS, Email, FB, IG, Live chat y GMB en highlevelsin marcas
'markdown'discordsin cambios
sin declararel resto (email)sin cambios

Nunca se tocan los bloques ni los spans de código, ni las palabras con snake_case o un 2 * 3. El historial guarda el texto original del modelo. format: 'whatsapp' (o cualquier formato) fija uno; format: false envía el texto tal cual. formatMarkdown(text, format) y formatMarkdownStream(source, format) se exportan para tus propios handlers. Con 'html' solo las URLs http(s), mailto: y tel: se convierten en enlaces <a href>: cualquier otra (un enlace javascript: que podría colar una inyección de prompt) queda como texto.

Notas de voz ​

El autoresponder solo contesta mensajes de texto. Con el plugin experimental @botmesh/voice, las notas de voz se transcriben antes de enrutarse (usa autoTranscribe: { asText: true } para que la IA las conteste) y autoReplyWithVoice puede devolver la respuesta como audio.

/reset y slashCommandsPlugin ​

aiPlugin registra su propio /reset: limpia el contexto de IA del chat y cancela la conversación activa. El registro de comandos es "último plug() gana" (comportamiento documentado del core), así que si también usas slashCommandsPlugin (que trae su propio /reset para cancelar conversaciones), instala aiPlugin después para que su /reset prevalezca:

ts
bot.plug(slashCommandsPlugin, { greeting: '🤖 ¡Hola!' })
bot.plug(aiPlugin({ apiKey: process.env.ANTHROPIC_API_KEY })) // su /reset gana

Testing ​

MockAIProvider implementa AIProvider con respuestas fijas o programables y cero red — úsalo para testear tu propia lógica alrededor de AIClient sin llamar a un proveedor real:

ts
import { AIClient, ContextManager, type ConvStore, type AIMessage } from '@botmesh/ai'
import { MockAIProvider } from '@botmesh/ai/testing'

const memory = new Map<string, AIMessage[]>()
const store: ConvStore = {
  async getConversation(k) { return memory.get(k) },
  async setConversation(k, m) { memory.set(k, m) },
  async clearConversation(k) { memory.delete(k) },
}

const client = new AIClient(new MockAIProvider({ reply: 'respuesta simulada' }), new ContextManager(store))

const respuesta = await client.ask('¿qué hora es?')
// respuesta === 'respuesta simulada'

reply también puede ser una función de la petición que devuelva { toolCalls } para simular llamadas a herramientas (ver Testing en la guía de herramientas), y mock.calls registra cada petición que envió el cliente.

MockAIProvider y validateArgs (comprueba los argumentos de una herramienta contra su esquema, como el bucle de herramientas) vienen de @botmesh/ai/testing; importar MockAIProvider de @botmesh/ai sigue funcionando pero está obsoleto. El paquete tiene tres entradas: @botmesh/ai (API de aplicación), @botmesh/ai/testing (dobles de test; ambas con semver) y @botmesh/ai/internal (helpers del plugin y nombres de los tipos de los SDK, sin semver).

Un AIProvider propio que haga streaming debe respetar GenerateInput.onStreamEnd: llamarlo una vez, tras el último fragmento, solo si el stream termina con normalidad (no ante un error, un aborto o un break temprano), con un AIStreamEndInfo (stopReason, usage si se conoce). Comprueba signal.aborted antes de llamarlo: si no, un cliente que ignora la señal terminaría "con normalidad" tras un aborto.

Roadmap ​

No-objetivos de esta primera versión, ya contemplados en el diseño de AIProvider/ContextManager para poder añadirlos después sin romper la API:

  • Auto-summarize / compaction del historial (en vez de recortar por ventana fija de mensajes).
  • Multi-persona por número.

Siguientes pasos ​

Released under the MIT License.