No existe una «app del bot». El agente completo — loop, prompts, memoria, tools — vive dentro del monolito NestJS, entrelazado con el webhook, el envío y el inbox. Así viaja un mensaje hoy:
POST /whatsapp/webhook — firma HMAC verificada y 200 inmediato para
que Meta no reintente.
MessagingService.recordInboundMessage — la tabla Message es la
verdad del inbox. Aquí también vive el gate: si Conversation.isHuman es
true, el bot calla y el hilo es del admin.
Lock por conversación, typing, contexto de 24 mensajes / 120 minutos con
summary que escribe «[N mensajes resumidos]» sin contenido. Los taps
(track_*, menús) se resuelven sin modelo — eso está bien y se queda.
Loop de 5 iteraciones con fetch crudo a OpenRouter: gemini-2.5-flash-lite
a temperatura 0.7 para tarea factual, fallbacks que cambian de familia
(Mistral → GPT → Claude) a mitad de hilo, resultados de tools truncados a
1.500 caracteres, y un prompt monolítico de ~4.500 tokens con 12 reglas-parche.
finalizeTurn → MessagingService → Meta. Dos fallos seguidos
→ disculpa, isHuman = true y cola de handoff con SLA de 5 minutos.
Este circuito de seguridad es bueno — sobrevive intacto.
causa 1Contenido sin validar
21 temas draft-pending-review, tarifa aérea placeholder, tres formatos
de código de flete que no coinciden entre el conocimiento, el prompt y
freights.service.ts. El bot repite lo que le escribieron en julio.
causa 2Conocimiento congelado
Solo 12 de 25 temas entran al prompt — completos, en cada turno. Actualizar un
precio es un deploy. Y el API ya tiene la verdad viva (exchange-rates/,
shipping-pricing/) que el bot no consulta.
causa 3Canales contradictorios
Instagram y TikTok llevan su propio mini-negocio hardcodeado: prometen transporte «terrestre» y rutas que WhatsApp niega. Tres prompts, tres versiones de Mogos.
causa 4Modelo con creatividad
El modelo más barato de la familia, a temperatura 0.7, con fallbacks multifamilia: el estilo y la obediencia de tools cambian dentro de la misma conversación.
causa 5Memoria corta y recortada
24 mensajes son 4-6 turnos agénticos; a las 2 horas el hilo se borra; y si el resultado de una tool no cabe en 1.500 caracteres, el modelo razona sin los datos. El bot olvida lo que acordó contigo esta mañana.
y ninguna medidaCero evals
No hay set de pruebas, ni feedback 👍/👎, ni métrica de accuracy. Cada cambio de
prompt se lanza a fe. La semilla ya existe: Message.metadata guarda intent y
tools usadas de cada turno real.
Todo lo que habla con Meta, con Postgres y con el admin se queda donde está.
Todo lo que piensa se muda a agents/mogui, un proyecto eve autohospedado como
servicio privado de Render — hermano del API en el mismo Blueprint, solo
alcanzable por la red interna.
- Webhooks + firma HMAC de los 3 canales intacto
- Gate isHuman — la verdad del mute vive en Postgres intacto
- Capa determinista — taps
track_*y menús, cero LLM, cero hop - Despachador — lo que queda de
chatbot.service.ts: ~200 líneas que llaman a eve y aplican su veredicto - MessagingService — ventana de 24h, plantillas Meta, límites de formato intacto
- Inbox + handoff + realtime — el admin responde por la misma conversación 0 líneas
- Sesión durable por conversación — 30 días, compactación real; se acabó la ventana de 24 mensajes
- instructions.md corta + skills a demanda — mueren las 12 reglas-parche
- 11 tools tipadas que pegan por HTTP autenticado al API — 8 migran, 3 nacen vivas
- search_knowledge — retrieval pgvector sobre el corpus completo rag
- Evals — el set dorado corre en CI antes de cada deploy medible
- OpenRouter vía AI SDK — económico, temperatura 0.2, fallbacks de una sola familia
eve jamás habla con Meta. Toda respuesta vuelve por
MessagingService, donde la ventana de 24 horas, las plantillas aprobadas y los
límites de formato (3 botones, 10 filas) se validan en el único lugar donde siempre se han
validado. Por eso el inbox, el handoff y el realtime no se enteran de la migración.
En eve, el agente es un directorio: las instrucciones, las tools, los
procedimientos y los evals son archivos versionados en el monorepo — mismo repo, mismos
PRs, misma disciplina de revisión que el resto de Mogos. Vive en agents/*, una
categoría de workspace nueva junto a apps/* y packages/* (una línea en
el package.json raíz): los agentes de IA en su propia fila, sin confundirse con
apps/agent, que es el portal de referidores.
agents/mogui/ ├── package.json · eve (pineado) + @openrouter/ai-sdk-provider + zod ├── agent/ │ ├── agent.ts · modelo + sesiones durables en Postgres │ ├── instructions.md · la persona Mogui — corta; el conocimiento ya no vive aquí │ ├── tools/ · tipadas con Zod · HTTP autenticado al API │ │ ├── get_user_freights.ts get_user_orders.ts │ │ ├── get_freight_status.ts get_freight_eta.ts │ │ ├── create_lead.ts register_account.ts │ │ ├── request_human_agent.ts show_main_menu.ts │ │ ├── get_shipping_rates.ts · NUEVA — tarifas vivas, mata el placeholder │ │ ├── get_exchange_rate.ts · NUEVA — tasa BCV del módulo real │ │ └── search_knowledge.ts · NUEVA — retrieval pgvector │ ├── skills/ · procedimientos que reemplazan las reglas-parche │ │ ├── cotizar-flete/ primer-envio/ registro-magico/ │ ├── subagents/ · especialistas aislados, opcionales — ver «¿Y cuando sean varios?» │ └── channels/ │ └── mogos-api.ts · canal custom · continuationToken = conversationId └── evals/ · el set dorado · corre en CI antes de cada deploy ├── evals.config.ts └── …casos sembrados desde Message.metadata
// agent/agent.ts — el modelo es un parámetro, no una apuesta import { defineAgent } from "eve"; import { createOpenRouter } from "@openrouter/ai-sdk-provider"; const openrouter = createOpenRouter({ apiKey: process.env.OPENROUTER_API_KEY }); export default defineAgent({ model: openrouter("google/gemini-2.5-flash"), // económico · sin cambiar de familia modelOptions: { temperature: 0.2 }, // tarea factual, no creativa limits: { sessionTimeoutMs: 30 * 24 * 60 * 60 * 1000 }, experimental: { workflow: { world: "@workflow/world-postgres" } }, // sesiones en Supabase });
migranLas 8 tools de siempre
Mismos nombres, mismos contratos — pero pegan por HTTP con token de servicio a
endpoints internos nuevos del API, que heredan el contrato guestGuard
(una acción user-scoped sin usuario se bloquea, nunca devuelve datos de todos). El
relleno determinista de create_lead y el registro mágico completo se quedan
del lado del API como endpoints; eve solo los invoca.
nacenLas 3 tools vivas
get_shipping_rates y get_exchange_rate conectan los módulos que
ya existen y el bot ignoraba — se acabó cotizar con un número de julio.
search_knowledge recupera del corpus completo: los 13 temas que hoy nunca
llegan a WhatsApp (seguro, aduana, peso volumétrico…) se vuelven respondibles.
¿Y cuando sean varios agentes?
Un app eve compila un agente raíz — no existe un agents/ con
varios raíces dentro del mismo servicio. Lo que sí existe, en la fuente de verdad de eve,
son tres escalones para crecer — y esta estructura ya los acomoda sin moverse:
escalón 1Subagentes, dentro de la casa
Especialistas declarados en agent/subagents/<id>/ — cada uno con su
agent.ts, prompt, tools y skills propios y aislados (no heredan nada
del raíz). Mogui delega leyendo su description. Un cotizador fino o un
traductor es/zh entran aquí sin nuevo servicio.
escalón 2Casas hermanas, un proyecto por agente
Un agente con vida propia — canales, schedules y ritmo de deploy suyos — es
otro proyecto eve: agents/copiloto, agents/ops… Cada uno su
servicio privado en Render, en fila dentro de agents/*. Canales y
schedules son del raíz: un subagente no puede recibir webhooks.
escalón 3Federados, entre servicios
defineRemoteAgent bajo agent/subagents/: otra instalación eve se
usa como si fuera un subagente local — URL desde env en runtime, auth saliente. Mogui
podría delegar en el agente de ops (y al revés) sin compartir proceso ni base.
En los tres escalones, los agentes de cara al cliente siguen hablando por la misma boca: el API. Más cerebros nunca significa más caminos hacia Meta.
El despachador entrega el mensaje al canal custom de eve y sigue con su vida. Cuando el turno durable termina, eve hace callback con un veredicto tipado — nunca con un envío directo.
// ida · despachador → eve (red interna de Render) POST http://mogui:10000/eve/channels/mogos-api/message Authorization: Bearer MOGOS_EVE_SERVICE_TOKEN { "conversationId": "cmf4k…", // continuationToken → una sesión durable por conversación "channel": "WHATSAPP", "text": "¿cuánto cuesta traer 2 CBM desde Yiwu?", "contact": { "name": "María", "phone": "+58…" }, "userId": "usr_…" | null // null = invitado → guestGuard en cada endpoint }
// vuelta · message.completed → callback firmado al API POST https://api…/messaging/bot/reply { "conversationId": "cmf4k…", "kind": "reply" | "escalate" | "degraded", "messages": [ { "type": "text", "text": "…" }, { "type": "interactive", … } ], // límites Meta se validan al enviar "intent": "quote_freight", "toolsUsed": ["get_shipping_rates", "search_knowledge"] }
carreraEl API revalida isHuman al recibir
Si un admin tomó el chat mientras eve pensaba, el callback se descarta. El humano siempre gana — la verdad del mute vive en Postgres, nunca en la sesión de eve.
timeoutSin callback, actúa el circuito de siempre
Si eve no responde en N segundos, corre el mismo camino de fallo que hoy: disculpa,
dos strikes, isHuman = true y cola de handoff con SLA de 5 minutos. eve caído
nunca deja a un cliente en silencio.
escalarequest_human_agent devuelve un veredicto
kind: "escalate" dispara el escalamiento existente — la fila
ConversationHandoff, la notificación, el SLA. El flujo del admin no cambia.
historialMessage es la verdad; la sesión, caché
Al reactivar el bot después de un turno humano, el despachador reconstruye contexto
desde la tabla Message (reconstructFromMessages ya existe) para que
eve sepa lo que el admin acordó con el cliente.
Hoy el conocimiento se inyecta completo en cada turno y se actualiza con deploys. La disciplina correcta es la de la clase: embeddings en pgvector, indexado automático por hash, y retrieval medible. Todo lo que hace falta ya existe en el repo: el corpus está tipado (25 temas + 8 packs por ruta, es/zh — chunking natural), la base ya es Supabase Postgres, y el patrón de proteger índices especiales con guarda de prebuild ya se usa para los GIN de búsqueda.
Un schedule de eve recorre el corpus cada N minutos: hash por tema — nuevo o modificado → marcado pendiente en la tabla de estado. Actualizar conocimiento deja de ser un deploy.
Chunks por tema/FAQ con solape, un modelo de embedding fijo (el mismo para indexar y consultar — regla de la clase), vectores a la tabla pgvector con índice HNSW protegido por la guarda de prebuild.
search_knowledge busca por distancia coseno con umbral, top-4. El prompt
baja de ~4.500 tokens fijos a instrucciones cortas + solo lo relevante al turno.
Pares pregunta → tema esperado en el set de evals: recall y precisión del retrieval, comparables entre modelos de embedding, umbrales y chunking — igual que la clase.
El RAG distribuye conocimiento; no lo corrige. Antes de indexar: ops valida
los 21 temas, la tarifa aérea sale de shipping-pricing (viva, no
escrita), queda un solo formato de código de flete, y los prompts de
Instagram/TikTok se alinean con WhatsApp. Indexar el corpus de hoy sería servir las
mismas mentiras, más rápido.
Cada cambio de prompt, tool o modelo corre contra un set dorado antes de
llegar a un cliente. La semilla no hay que inventarla: Message.metadata ya
guarda el intent y las tools usadas de cada turno real — las conversaciones de estos
meses son los casos de prueba.
// evals/cotizar/aereo-yiwu.eval.ts — un caso del set dorado export default defineEval({ description: "Cotización aérea usa la tarifa viva, nunca el placeholder", async test(t) { await t.send("¿cuánto cuesta traer 40 kg desde Yiwu por aéreo?"); t.succeeded(); t.calledTool("get_shipping_rates"); // la verdad viva, obligatoria t.judge.autoevals.closedQA("responde en USD con la tarifa vigente"); }, });
antes del corteeve corre en sombra
En la fase 2, cada mensaje real se responde por el bot actual y por eve en paralelo — pero solo el actual envía. Se comparan veredictos contra el set dorado: el corte se hace cuando eve gana con números, no con fe.
modeloEconómico, elegido con datos
Arranca gemini-2.5-flash a temperatura 0.2. Si otro candidato económico
(haiku, gpt-5-mini) rinde mejor en el set dorado, cambiar el modelo es una línea de
agent.ts — y los evals lo demuestran antes del deploy.
| Destino | Pieza | Detalle |
|---|---|---|
| se borra | ~2.000 líneas de runtime propio | ai.service.ts (674), prompt.builder.ts (297),
cache.service.ts, la ventana manual de conversation.manager.ts
y la inyección de 12 temas al prompt. eve hace todo eso de serie. |
| se adelgaza | chatbot.service.ts | De 861 líneas a ~200 de despachador: lock, gate isHuman, capa
determinista, contrato con eve, circuito de fallo. |
| intacto | Toda la tubería y el inbox | Webhooks y firmas, adapters de los 3 canales, messaging.service.ts,
handoff/ con su SLA, admin/, broadcast realtime,
templates/, media, OTP, MCP. El inbox no cambia ni una línea. |
| nace | agents/mogui + los bordes | El proyecto eve, el controller interno de tools (hereda guestGuard),
el callback POST /messaging/bot/reply, las tablas pgvector y el servicio
privado en Render (prod + staging). |
| decisión aparte | El copiloto in-app (⌘J) | Comparte ai/providers/ y packages/ai-knowledge. Mientras no
migre, esos módulos no se borran — el corte del bot no lo obliga. |
eve compila a un servidor Node normal (eve build && eve start) — exactamente lo
que Render ya corre. No hay dependencia de Vercel: ni para el runtime, ni para el modelo,
ni para el estado.
servicioPrivado, hermano del API
Mismo Blueprint, prod y staging como siempre. Solo el API le habla por la red interna; cero superficie pública nueva. Meta sigue apuntando únicamente al API.
estadoSesiones en Postgres, no en disco
@workflow/world-postgres sobre un schema propio del mismo Supabase: las
sesiones durables sobreviven cada deploy sin disco persistente ni instancia fija.
envCuatro variables
OPENROUTER_API_KEY, MOGOS_API_URL,
MOGOS_EVE_SERVICE_TOKEN y la conexión del world. Nombres nuevos, patrón de
siempre — grupos de entorno de Render.
previewVersiones pineadas, red abajo
eve está en preview y el world en línea 5.0.0-beta: se pinean ambas. Si el
cerebro no responde, el despachador tiene el circuito de disculpa + handoff — el
cliente nunca queda en silencio.
| Riesgo | Salvaguarda |
|---|---|
| Ventana de 24h — un turno durable «despierta» horas después y la ventana ya cerró. | Toda salida pasa por MessagingService: fuera de ventana, el callback
se descarta o se convierte a plantilla aprobada. eve nunca envía. |
| Carrera con el admin — eve piensa mientras un humano toma el chat. | isHuman se lee al despachar y al recibir el callback. El
veredicto tardío de eve se descarta; el humano siempre gana. |
| Plantillas Meta — texto creativo donde va copy aprobado rompe el WABA. | Las plantillas viven 100% en el API. El cerebro solo produce texto libre e interactivos; el registry Zod posicional no se toca. |
Doble historial — la sesión de eve (30 días) y la tabla
Message podrían divergir. |
Declarado: Message es la verdad para humanos; la sesión es caché del
modelo. Al reactivar el bot tras turno humano, se reconstruye desde
Message. |
| Latencia y costo del hop — un HTTP + checkpoint por turno. | Los taps y menús deterministas nunca salen del API (cero LLM, cero hop). Solo los turnos que de verdad piensan pagan el viaje. |
| Tools por HTTP — de Prisma directo a una superficie autenticada nueva. | Token de servicio + el contrato guestGuard heredado en cada endpoint:
acción user-scoped sin usuario se bloquea, jamás devuelve datos de todos. |
eve no habla con Meta. Ni envía, ni elige plantillas, ni conoce el token del WABA. Un solo lugar valida la ventana y el formato: el de siempre.
No migramos el copiloto in-app todavía. Es otra decisión con su propio corte; mientras tanto sus módulos compartidos no se borran.
No indexamos el corpus sin validarlo. RAG sobre contenido con placeholders es servir las mismas mentiras con mejor tecnología.
No cambiamos de familia de modelo a mitad de conversación. Fallbacks solo dentro de Gemini; cambiar de modelo es una decisión de evals, no de un balanceador.
No tocamos el circuito de handoff. El SLA de 5 minutos, la cola, el «responder es atender» y el switch Bot/Agente quedan exactamente como están.
No hacemos big-bang. El corte llega en fase 3, después de que la sombra demuestre paridad con números — y con vuelta atrás de una línea (el despachador re-apunta al loop viejo hasta que se borra).
Cada fase entrega valor sola y tiene una compuerta medible antes de la siguiente. La palanca más grande — el contenido — va primero porque no depende de ninguna infra nueva.
Ops valida los 21 temas; tarifa aérea real; un formato de código; IG/TikTok alineados. Se exponen los endpoints internos de tools. Compuerta: ops firma el corpus; el bot actual ya responde mejor sin tocar el runtime.
El proyecto eve nace con sus 11 tools y el set dorado v1. Cada mensaje real se responde en paralelo, sin enviar. Compuerta: eve iguala o supera al bot actual en el set dorado.
El despachador re-apunta del loop viejo a eve. El circuito de fallo y el rollback de una línea quedan armados. Compuerta: una semana estable — luego se borran las ~2.000 líneas.
Tablas, indexado automático por hash, search_knowledge al aire y el
prompt adelgaza. Compuerta: recall/precisión del retrieval medidos en el set —
y el conocimiento se actualiza sin deploy.
Instagram y TikTok entran al mismo cerebro (muere el mini-negocio hardcodeado), y se decide el destino del copiloto in-app con los números de la nueva casa a la vista.
El inbox no cambia ni una línea.
El cerebro cambia de casa.
eve en Render con OpenRouter, retrieval sobre pgvector, evals antes de cada deploy — y la tubería que ya funciona, funcionando igual. Importamos futuro.