Repository navigation
Releases: kevinrivm/nea-agent
Release list
Nea 2.0.1 — ALLOWED_WA_IDS acepta el teléfono con +
Nea 2.0.0 — el despacho es la única entrada
Versión mayor: hay algo que hacer a mano al actualizar (abajo, «Actualizar
desde 1.x»). Spec 031 de vocero-crm (specs/031-nea-por-despacho).
Cambió
- Nea ya no recibe el webhook de Meta: el CRM se lo despacha. Meta (y
Zernio) le escriben al CRM; el CRM guarda el mensaje, junta la ráfaga y se la
entrega a Nea firmada enPOST /vocero/dispatch. Contra Vocero raíz la firma
esCRM_BOT_API_KEY, la misma llave con la que Nea ya le contesta: no hay un
secreto nuevo. Es la entrada que Vocero Cloud ya usaba. - Un CRM que no contesta al empezar el turno ya no se reintenta aquí. Nea
1.x repetía la ráfaga hasta diez minutos porque el relay podía no haberle
entregado el mensaje al CRM todavía. Con el despacho, el CRM acaba de
mandarlo: si no contesta, la cola del despacho le pasa la conversación a
una persona (motivoerror) en cuanto vuelva.
Nuevo
- Instagram, Messenger y TikTok. Todo lo que el CRM despacha le llega a
Nea, y el prompt nombra el canal (conversation.channel): un DM de
Instagram no se contesta «por WhatsApp». En WhatsApp el chasis no cambia ni
un byte. - Lo que el webhook daba, ahora llega del CRM: el anuncio de origen
(adOrigendel contexto), la ubicación, los nombres de las tarjetas de
contacto y el nombre del documento (llaves nuevas del despacho, vocero-crm
031). La nota de voz se reconoce por su tipo (audio/ogg). DATABASE_SCHEMA: connea, Nea guarda sus tablas en ese esquema y
puede usar la misma base del CRM. Vacía,public, como siempre.- Un mensaje repetido en dos despachos se lee una vez también contra la
raíz (antes, solo en cloud). /healthganawebhookDeMeta.ultimoEn: cuándo le llegó a Nea un webhook de
Meta, que desde la 2.0 ya no debería llegarle.- El Laboratorio del CRM prueba a Nea (vocero-crm 033). Un despacho con
conversation.test: truees una corrida del Laboratorio: pasa aunque la
identidad (sintética) no esté enALLOWED_WA_IDSy no programa seguimiento.
La respuesta la guarda el CRM sin enviarla. - Roles como datos (vocero-crm 034). El perfil del CRM trae
roley
tools: Nea le enseña al modelo solo las herramientas encendidas (ficha,
agendar, reprogramar, salida; escalar va siempre). Sin «agendar», se porta
como contra un CRM sin agenda. El cambio llega cuando Nea relee el perfil
(cada 5 minutos), sin redesplegar. Sintools, todas, como antes. LLM_API_KEYes opcional junto a Vocero 2.0 (vocero-crm 036). Vacía,
Nea piensa por el CRM: la llave vive en Ajustes → IA del CRM y no viaja
hasta aquí; Nea llama a{CRM_BASE_URL}/api/bot/llmconCRM_BOT_API_KEY
y usa el modelo que el perfil dice (llm.model), oLLM_MODELsi no trae.
Cambiar el modelo en el CRM llega al releer el perfil (cada 5 minutos), sin
redesplegar. Sin IA configurada en el CRM (402sin_credencial), Nea calla
y le pasa la conversación a una persona (motivoerror). ConLLM_API_KEY
puesta no cambia nada. Las notas de voz van por el mismo camino con
LLM_TRANSCRIBE_MODEL(un modelo que oiga, nowhisper-1). En modo cloud
no cambia nada./healthganallm:"crm"o"propia"(sin llaves ni URLs).- Nea mueve al lead de etapa (vocero-crm 038): la herramienta
move_stagellama aPOST /api/bot/stage. Se le enseña al modelo solo con
la claveetapaencendida en el perfil y si el contexto trae
lead.nextStages, y solo hacia esas etapas. El prompt dice cuándo: en el
mismo turno de la respuesta, en cuanto el lead dijo qué necesita o pidió
avanzar (precio, contratar, empezar, agendar); nunca por un saludo o un
«ok». Contra un CRM sin la ruta, la herramienta no existe.
Se quitó
app/webhook.py,app/relay.py,app/coalesce.py, el bloquerelayde
/healthy las variablesMETA_APP_SECRET,VERIFY_TOKEN,
CRM_WEBHOOK_URL,RELAY_BACKOFF_CAP_SECONDS,COALESCE_SECONDS,
TYPING_DELAY_SECONDSyTURN_RETRY_DELAYS. Si siguen en el entorno, Nea
lo avisa al arrancar (solo los nombres, nunca los valores).- La tabla
relay_queuese queda, sin uso, por si guardaba algo sin entregar
al actualizar. Se borra en una versión siguiente. POST /webhookresponde 503 (no 404): si Meta todavía le escribe a Nea,
reintenta en vez de dar el mensaje por entregado, y el log dice qué hacer.
Actualizar desde 1.x
En este orden, para no perder mensajes:
- CRM (Vocero 2.0):
BRAIN_DISPATCH_URL=https://<nea>/vocero/dispatch
(ohttp://nea:8000/vocero/dispatchen la misma red) y redeploy.
BOT_API_KEYya lo tiene. Después, enciende el agente en la pantalla
Agente del CRM: desde la 2.0 ese interruptor decide si el CRM le despacha
a Nea. - Meta: quita el override del número que apuntaba a Nea
(POST /{PHONE_NUMBER_ID}con"override_callback_uri": ""). Los webhooks
vuelven al override de la WABA, el del CRM. Compruébalo con
GET /{PHONE_NUMBER_ID}?fields=webhook_configuration. - Nea: la imagen
2.0.0. Puedes quitar las variables de «Se quitó» y,
si quieres que piense con la llave de Ajustes → IA del CRM, también
LLM_API_KEY.
Si haces el 3 antes que el 2, no se pierde nada: Nea le contesta 503 a Meta,
Meta reintenta, y /health enseña webhookDeMeta.ultimoEn hasta que muevas
el webhook.
Nea 1.x frente a un Vocero 2.0 sigue funcionando mientras no actualices:
/api/bot/* no cambió.
Arreglado
- La hora que ve el agente es la del negocio que atiende, no la de la
instancia. Una Nea en modo cloud atiende a negocios de zonas distintas,
pero armaba el prompt conAGENT_TIMEZONE(la de México) para todos. Los
horarios los calcula el CRM en la zona de cada negocio, así que la agenda
estaba bien; lo que fallaba era lo que el agente decía de ella: a un lead de
un negocio en Colombia le dijo «es de México el huso». Ahora la zona sale
del bloquebookingdel contexto (la de Ajustes → Agenda) para el prompt,
el seguimiento y las herramientas; «hoy», «mañana» y el calendario también
son los del negocio. El prompt la nombra como lugar («hora de Colombia»),
nunca con su nombre técnico, y el agente no le calcula al lead la
diferencia con su hora.AGENT_TIMEZONEqueda de respaldo para un CRM que
no mande la zona. Sin migraciones ni variables nuevas. - Un mensaje del CRM se lee una sola vez, aunque venga en dos despachos.
En modo cloud, el CRM entrega la ráfaga completa cada vez que despacha: si
el cliente escribe mientras un despacho va en vuelo o mientras Nea piensa,
el siguiente despacho —con otrodispatchId— repite los mensajes
anteriores. Nea deduplicaba solo por despacho y el repetido entraba otra vez
a su historial («hola», y luego «hola» + la pregunta nueva). Ahora anota
cadamessages[].idcuando lo lee y descarta los que ya leyó; un despacho
que no trae nada nuevo no gasta turno. Sin migraciones ni variables nuevas.
Nea 1.1.0
Dos PR sobre 1.0.0: #36 y #38. Nada se rompe al actualizar.
Nuevo
- Nea olvida las conversaciones que el CRM elimina (#36). En modo cloud,
POST /vocero/dispatchacepta el eventoconversation.deleted(misma
firma que un despacho) y borra el historial, la fase, los horarios
ofrecidos, los envíos pendientes y los despachos guardados de esa
conversación, solo en la organización del aviso. El aviso pasa por la cola
de despachos: un turno que esperaba termina antes, y un mensaje nuevo de la
misma persona empieza después. - Una conversación nueva del CRM no hereda memoria (#36), en los dos modos. Si
la misma persona llega con unconversation.iddistinto del que Nea
recordaba, el turno empieza de cero. Es lo que cubre un aviso que no llegó. - Webhook con el secreto en la ruta:
/webhook/<VERIFY_TOKEN>(#38). Para
instalar Nea en un servidor que no debe guardar el App Secret de la app de
Meta (un Tech Provider que despliega para sus clientes: con ese secreto se
administran los webhooks de TODA la app). Funciona como
/api/webhooks/wa/<token>del CRM: ruta equivocada → 404, y la firma se
verifica solo siMETA_APP_SECRETestá puesta. Exige unVERIFY_TOKENde
32 caracteres o más; con uno más corto esa entrada no abre./webhookno
cambia: sigue exigiendo la firma. - El
VERIFY_TOKENya no queda en el log de accesos (ni en la ruta ni en el
hub.verify_tokendel handshake) (#38).
Actualizar
Basta con correr la versión nueva (ghcr.io/kevinrivm/nea-agent:1.1.0): no
hay migraciones ni variables nuevas, y /webhook se comporta igual que en
1.0.0.
- Si tu Nea ya recibe por
/webhookconMETA_APP_SECRET, no tienes que
cambiar nada. - Si quieres quitar el App Secret de ese servidor (modo estándar): pon un
VERIFY_TOKENde 32 caracteres o más (openssl rand -hex 32), redespliega,
cambia el override del número ahttps://nea.tu-dominio.com/webhook/<VERIFY_TOKEN>
(README, «El webhook de Meta, a Nea») y, cuando un mensaje de prueba llegue,
vacíaMETA_APP_SECRETy redespliega otra vez. El proxy puede guardar la
URL completa en sus propios logs, igual que con el webhook del CRM. - Comprueba:
/healthcon"version": "1.1.0"; un mensaje de prueba
llega a la bandeja del CRM y Nea contesta.
Nea 1.0.0
Primera versión con número y primera con imagen publicada. Hasta aquí Nea se
instalaba construyendo main. Lo de abajo cuenta desde el main del 18-sep
(c90ec87): los PR #27, #28, #29, #30 y #32. Si tu copia es más vieja, lee también
el final de «Actualizar».
¿Corres el modo estándar con un
mainde entre el 31-ago y el 21-sep?
Tu Nea no le está pasando los mensajes al CRM. Actualiza (ver «Actualizar»).
Modo estándar:
META_APP_SECRETahora es obligatoria. Sin ella,
POST /webhookresponde 401 y Nea deja de recibir mensajes. Ponla antes de
actualizar.
Nuevo
- Imagen en GHCR (#29). Cada tag
vX.Y.Zpublica
ghcr.io/kevinrivm/nea-agent:X.Y.Z, además deX.Yylatest, para
linux/amd64. Coolify la descarga en vez de construirla en el VPS. El CI
construye la imagen en cada PR y en cada push amain, sin publicarla. /healthdice qué Nea corre y cómo va el relay (#28):version,
commitycommitVerified,mode(estándar,cloudomultiorg) y, en
modo estándar,relayconpendientes,masViejoSegundosy
ultimoErrorEn. El código HTTP sigue dependiendo solo de la base.- El turno aguanta una caída del CRM (#30). Si al empezar un turno el CRM
no contesta (red, timeout, 5xx o 429), la misma ráfaga se reintenta tras
TURN_RETRY_DELAYS(15 s, 45 s, 2 min y 5 min). Lo que el lead escriba
mientras tanto sale en una sola respuesta. Si el CRM no vuelve a tiempo, la
conversación pasa a un humano (handofferror) en cuanto conteste. Los
reintentos viven en memoria: un reinicio de Nea los pierde; el mensaje no,
porque el relay lo guarda en Postgres. - El cierre sin rumbo se ve en el CRM (#28). Mientras dura, la ficha del
contacto llevacierre_sin_rumbocon la hora (el CRM raíz lo enseña como
«Cierre sin rumbo»). Se borra al reabrir. - Prueba de punta a punta contra Vocero raíz (#30).
scripts/e2e_contra_raiz.pymonta el par en modo estándar, hace de Meta y
de cliente, y comprueba nueve historias en el CRM. Cuesta unos centavos de
modelo. - Pruebas de
PgStorecontra un Postgres de verdad (#27):
tests/test_pg_store.py, con su propio job de CI (postgres:16).
Cambió
META_APP_SECRETes obligatoria en modo estándar (#32). Antes, vacía,
/webhookaceptaba cualquier POST sin firma: quien conociera la URL podía
hacerse pasar por un lead y hacer que Nea le contestara y gastara modelo.
Ahora, sin secreto o con la firma ausente o inválida, responde 401. En modo
cloud/webhookya no se monta: la única entrada es el despacho firmado del
CRM.- La imagen corre sin root (#32): usuario
nea, UID y GID 10001. - Límites de entrada (#32). El cuerpo de
/webhooky del despacho, hasta
2 MiB (más, 413). La descarga de un adjunto, hasta 16 MiB y 30 s (antes,
60 s). El texto de un PDF se extrae en un proceso aparte, con 6 s de tope y
dos a la vez, para que un PDF pesado no frene los turnos de los demás. - El candado de cierre se reabre (#28). Antes, tras la despedida había
24 h de silencio pasara lo que pasara: un «¿cuánto cuesta?» se quedaba sin
respuesta. Ahora el relleno («gracias», «ok 👍») se sigue contestando con
silencio duranteSTALL_COOLDOWN_HOURS, pero un mensaje con contenido
reabre en el acto y los contadores vuelven a cero. Los umbrales se ajustan
conSTALL_MAX_TURNS,STALL_FILLER_STREAKySTALL_COOLDOWN_HOURS, con
los valores de siempre por defecto. Y «ok 👍» o «va, gracias 🙏» ahora
cuentan como relleno. - Tope por intento al modelo (#27):
LLM_TIMEOUT_SECONDS(45 s), al
conversar y al transcribir, y el SDK ya no reintenta por su cuenta. Antes un
proveedor colgado podía tener al lead media hora en «escribiendo…» antes
del handofferror. - La agenda se vuelve a preguntar cada
AGENDA_PROBE_TTL_SECONDS(60 s)
(#27). Encender o apagarAGENDAen el CRM ya no exige reiniciar Nea. - El relay se pone al día en un minuto (#27, #30). La espera entre
intentos tiene tope deRELAY_BACKOFF_CAP_SECONDS(60 s; antes, 15 min), y
un barrido vacía toda la cola vencida, no solo 50 filas. - Sin agenda v2, Nea no ofrece recordatorios (#30). El CRM raíz no los
manda:book_sessionya no piderecordatorios_aceptadosy el prompt dice
que por aquí no hay recordatorios. Con la agenda v2 de Vocero Cloud todo
sigue igual. - Un NUNCA más en
app/prompt.py(#30): no ofrecer ni prometer una acción
que Nea no puede hacer con sus herramientas (llamar, mandar un correo,
escribir más tarde, avisar, apartar un lugar). Ninguno de los NUNCA de antes
se relajó. - La confirmación de la cita dice «Enlace de la reunión:» (#27), no
«Enlace de Zoom:»: el CRM no dice de qué proveedor es la sala. - Dependencias con versión exacta en
requirements*.txt(#27): lo que
pasó las pruebas es lo que se despliega.
Corregido
- Modo estándar: el relay no le pasaba nada al CRM (#27). Desde
95549c8
(31-ago, #12),PgStore.due_relaysleía columnas querelay_queueno tiene
y el worker se tragaba el error cada 5 s. Sin relay, ningún mensaje entrante
llegaba a la bandeja, los estados de entrega no avanzaban, a un contacto
nuevo Nea no le contestaba nunca (/api/bot/contextdaba 404) y a los
conocidos solo mientras su último mensaje relevado tuviera menos de 24 h. El
modo cloud no usa relay y no se vio afectado. - Markdown en WhatsApp (#28). El lead veía
**$800**,## Precios,
tablas con barras y[Agenda](https://…). Ahoraapp/formato.pylo
convierte al formato de WhatsApp antes de enviar, en el turno y en el
seguimiento, sin tocar URL ni correos. El historial guarda lo convertido. - Quien escribe sin compartir su número no recibía respuesta (#28). Nea
mandaba el BSUID pelón y el CRM lo guarda comobsuid:<id>: el contexto
daba 404.ALLOWED_WA_IDSyTESTER_WA_IDSaceptan el BSUID con o sin el
prefijo. - Un turno que revienta por algo inesperado deja, además del log, un
handofferroren la conversación (#27). - Un 404 del CRM ya no apaga la agenda de todo el proceso (#27). Solo el
404 vacío quiere decir «agenda apagada»; un 404 con el sobre de error del
CRM es una conversación o una cita que no existe. - Contra un CRM que ignora la fecha pedida, Nea no niega la tarde (#30).
Dice qué horarios ve, avisa que es solo una parte y ofrece que el equipo
confirme la hora o revisar otro día, en vez de «mañana solo tengo por la
mañana». - El token del webhook del CRM ya no queda en el log de Nea (#30). httpx
escribía la URL de cada relay, con el token en la ruta. - Modo cloud: un despacho confirmado ya no se pierde si Nea se cae (#32).
Antes Nea respondía 200 al CRM con el turno solo en memoria. Ahora lo guarda
endispatch_inboxantes de confirmar y un worker lo retoma al arrancar. Un
turno que se cortó a medias pasa a un humano en vez de repetirse, para no
mandarle dos veces lo mismo al lead.
Actualizar
Basta con correr la versión nueva: las migraciones se aplican solas y todas
las variables nuevas traen default. En modo cloud no hay relay: los puntos 2
y 6 no aplican.
Antes, en modo estándar: comprueba que META_APP_SECRET tenga el App
Secret de tu app de Meta (la misma del token). Si estaba vacía, desde esta
versión POST /webhook responde 401 y en el log de Nea aparece
firma inválida o ausente en el webhook — 401.
-
¿Qué versión corres? Si tu
/healthno traeversion, tu Nea es de
antes del 22-sep. Si dicedev, se construyó sin los build args. -
Modo estándar con un
mainde entre el 31-ago y el 21-sep (de
95549c8a antes de #27): el CRM no está recibiendo tus mensajes. En el
log de Nea se ve comorelay: fallo procesando la colacon
KeyError: 'organization_id', cada 5 s. Al actualizar, el primer barrido
le entrega al CRM lo que tenga menos de 24 h y abandona lo más viejo (su
cuerpo crudo se queda enrelay_queue). A los leads que escribieron esos
días y no recibieron respuesta nadie les contesta solo: revísalos en la
bandeja. -
Pásate a la imagen (recomendado). En Coolify, lo más limpio es una app
nueva: + New → Docker Image,ghcr.io/kevinrivm/nea-agent:1.0.0, con las
mismas variables, la misma base y el mismo dominio. Detén la vieja antes de
arrancar la nueva: dos Nea contra la misma base reenvían dos veces lo
pendiente. No pongasNEA_VERSIONen las variables y dejaPORTen 8000.
La imagen corre como UID 10001: si montas elBRIEF_PATHdesde un volumen,
que ese usuario pueda leerlo.
Si prefieres construir, hazlo desde el tagv1.0.0con los build args del
README. -
Migraciones. Se aplican solas al arrancar, antes de atender:
006_stall_reapertura.sql:bot_conversation.stall_since_message_id
(BIGINT NOT NULL DEFAULT 0).007_relay_ultimo_error.sql:relay_queue.last_error_aty su índice
parcial.008_dispatch_inbox.sql: la tabladispatch_inbox. Solo la usa el modo
cloud; en modo estándar se queda vacía.
Son aditivas (
ADD COLUMN IF NOT EXISTS,CREATE TABLE IF NOT EXISTS): no
borran ni reescriben datos. -
Variables nuevas. Ninguna es obligatoria (la que cambió es
META_APP_SECRET, arriba):Variable Default Antes LLM_TIMEOUT_SECONDS45Hasta 600 s por petición, y el SDK reintentaba dos veces más AGENDA_PROBE_TTL_SECONDS60Se preguntaba solo al arrancar STALL_MAX_TURNS1414, fijo STALL_FILLER_STREAK33, fijo STALL_COOLDOWN_HOURS2424, fijo TURN_RETRY_DELAYS15,45,120,300Sin reintentos: el turno se perdía RELAY_BACKOFF_CAP_SECONDS60900 (15 min) -
Si Meta le manda los webhooks a Nea por un override de la WABA, pásalo
al del número (README, «El webhook de Meta, a Nea»). Vocero raíz hasta
1.3.0 lo borra cada vez que guardas la conexión ...