propuesta de arquitectura · v1.0
Plataforma · el cerebro del bot · agents/mogui

El bot ya usa OpenRouter.
Lo que no tiene es memoria.

Mogui responde WhatsApp desde un runtime artesanal dentro del API: un prompt de 4.500 tokens, memoria de 120 minutos, conocimiento congelado en el código y cero evals. La propuesta: mover el cerebro a eve — autohospedado en Render, sin Vercel — con sesiones durables, retrieval sobre pgvector y evals en CI. Con el corte desaparecen tres cosas: el deploy para corregir un precio, el olvido a las dos horas, y el adivinar si el bot mejoró. La tubería no se toca: el admin sigue respondiendo por la misma conversación.

eve autohospedado · Render OpenRouter · gemini-2.5-flash · temp 0.2 Inbox: 0 líneas cambiadas RAG pgvector · Supabase Evals en CI −2.000 líneas de runtime propio
Hallazgo Desde julio el bot cotiza el flete aéreo con una tarifa inventada: «~USD 750 (referencia provisional)» con un TODO(ops) en packages/ai-knowledge/src/topics/precios.ts. Los 21 temas de conocimiento están sin validar por ops. Ningún runtime arregla eso: la fase 1 es contenido.
Hoy: el cerebro vive enredado en la tubería 01 · El flujo actual

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:

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.

El corte: cerebro y tubería 02 · Dos casas, un contrato

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.

Meta Cloud API · WhatsApp — el webhook y el envío apuntan únicamente a apps/api, como hoy
apps/api · la tuberíase queda
  • 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
agents/mogui · el cerebronace en eve
  • 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
Supabase Postgres · Message/Conversation siguen siendo la verdad del inbox · schema propio para las sesiones de eve (@workflow/world-postgres) · tablas pgvector del conocimiento
La regla de oro

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.

La casa nueva: agents/mogui 03 · Un agente que es un directorio

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.

La regla de oro escala

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 contrato entre las dos casas 04 · Asíncrono, tipado, con red

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.

El conocimiento deja de ser un prompt 05 · RAG con pgvector, como la clase

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.

Gobernanza primero

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.

Medir antes de creer 06 · Evals en CI, sombra antes del corte

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.

Qué se borra, qué se queda, qué nace 07 · El inventario del corte
DestinoPiezaDetalle
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.
Infra: Render, sin Vercel 08 · Autohospedado, junto a lo demás

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.

Riesgos con nombre y salvaguarda 09 · Lo que puede salir mal
RiesgoSalvaguarda
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.
Qué NO hacemos 10 · Los límites de la propuesta

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).

Las fases, con sus compuertas 11 · Para subdividir después

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.

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.