Skip to content
Tommy CardeLo
Go back

Deepy 2.0 — Reconstruir un asistente personal alrededor de un solo Postgres, un núcleo de WhatsApp y un dashboard privado

Deepy 2.0 reemplaza un pipeline de Sheets y markdown por un núcleo guiado por WhatsApp (FastAPI) y un dashboard web de un solo usuario (Next.js), ambos sobre el mismo Postgres con roles distintos y de mínimo privilegio.

Los repositorios son privados, porque los datos son míos: tareas, dinero, hábitos.


El problema

La primera versión de Deepy fue un asistente de voz local. Lo que lo alimentaba era una cadena de piezas poco acopladas: una nota de WhatsApp llegaba a una hoja de Google Sheets, un script en mi PC la consultaba cada dos horas, le pedía a un LLM que clasificara cada fila y reescribía un archivo markdown que el asistente leía como contexto.

Funcionaba, pero tenía tres problemas estructurales:

Así que dejé de parchearlo y reconstruí alrededor de una idea: un solo Postgres, con la estructura en el esquema, y dos clientes encima.

Qué hace

El núcleo (deepy_core) recibe mis mensajes personales de WhatsApp y los convierte en filas:

El dashboard (deepy_web) es una app Next.js de un solo usuario con secciones de tareas (tablero de tres columnas más una vista Eisenhower), finanzas (cuentas, presupuesto por categoría, ciclo de la tarjeta de crédito), hábitos (rachas), entrenamientos y calendario. Es instalable como PWA, así que vive en la pantalla de inicio de mi celular.

Arquitectura

WhatsApp ──► Evolution API ──► bot del gym (fan-out) ──► deepy_core (FastAPI)
                                                              │
                         router (modelo pequeño) ──► extractor (por módulo)
                                                              │
                                                              ▼
                                   ┌───────────── PostgreSQL ─────────────┐
                                   │ core · tareas · habitos · financiero │
                                   │ ideas · monitoreo · gymbros (RO)     │
                                   └──────────────────────────────────────┘
                                                              ▲
                                deepy_web (Next.js, sin ORM, rol propio)

Cron del host ──► jobs /internal/* ──► correo con enlaces firmados

El bot del gym, que tiene su propio post, sigue siendo dueño del webhook de Evolution. WhatsApp solo permite un webhook por instancia, así que ese bot reenvía mis mensajes directos a Deepy y se queda con el grupo.

Decisiones de diseño clave

1. Reemplazar archivos de texto por esquemas

Cada dominio tiene su propio esquema de Postgres (tareas, habitos, financiero, ideas) y las migraciones son archivos SQL numerados. Una tarea es una fila con estado, fecha límite y dominio; un gasto es una fila con monto, moneda y cuenta. Todo lo que muestra el dashboard es una consulta simple, y el LLM ya no hace de motor de base de datos.

2. Una base de datos, dos roles, poderes distintos

El dashboard y el núcleo comparten los datos, pero no los privilegios. deepy_web es un rol propio con GRANT explícitos por tabla: puede editar tareas y gastos, pero no tiene ningún acceso al esquema core (capturas crudas y trazas). El panel de entrenamientos lee las tablas del bot del gym mediante un permiso de solo lectura sobre una única tabla.

Compartir la base es la forma más simple de no tener que sincronizar dos sistemas. Los permisos por tabla son lo que lo hace seguro: si la web se viera comprometida, igual no podría leer mi historial crudo de mensajes.

3. Sin ORM en el lado web

deepy_web usa pg con consultas escritas a mano. Con pocas tablas y un solo usuario, un ORM agrega sobre todo una segunda descripción del esquema que puede desincronizarse de la real. Un script check:db corre contra la base real y verifica que las columnas numéricas llegan como números y que el rol no puede leer lo que no debe.

4. El LLM extrae; el código decide

Igual que en el bot del gym, el modelo es una función, no un agente. El router devuelve un módulo y una operación; Python valida el resultado con un esquema de Pydantic y hace la escritura. Los modelos se reparten por costo: uno pequeño y rápido para el router y el guardrail, y uno más grande para extraer y redactar respuestas.

Cuando el router no está seguro, pregunta. El mensaje original se guarda en Redis cinco minutos, de modo que mi respuesta se combina con él en vez de tratarse como un mensaje nuevo.

5. Las preguntas se responden con SQL de solo lectura, tras un guardrail

Ante una pregunta, el modelo escribe un único SELECT sobre una sola vista, elegida por el router. Luego dos capas lo revisan: una regex bloquea verbos de escritura y funciones peligrosas, y una segunda llamada al modelo confirma que solo toca la vista de ese módulo. La consulta se ejecuta con un rol de solo lectura, y cualquier fallo cuenta como “bloqueado”.

6. Cron en el host, no un scheduler dentro de la app

Las tareas programadas son endpoints HTTP internos que dispara el crontab del host, versionado en el repositorio. Es menos elegante que un scheduler dentro del proceso, pero es fácil de leer y de ejecutar a mano. Un chequeo de salud corre dos veces por hora: compara lo que debió ejecutarse con lo que se ejecutó, y muestra el resultado como correo (deduplicado, para que un problema persistente no me llene la bandeja) y como aviso en el dashboard.

7. Enlaces firmados en lugar de un login para los botones del correo

Los correos de recordatorio traen un botón “Cumplir”. Cada botón es un enlace firmado con HMAC y con vencimiento, generado en Python y verificado en TypeScript. La página muestra primero una confirmación y solo escribe cuando se pulsa el botón, de modo que un escáner de enlaces que abra la URL no pueda marcar nada como hecho. El E2E genera un token con el firmador de Python y se lo pasa al verificador de TypeScript, así los dos lados no pueden desviarse sin que nadie se entere.

Qué aprendí

Qué sigue

Exploré convertir esto en un producto multiusuario y lo pausé; la versión personal es la que uso a diario. Por ahora la meta es mantenerlo aburrido: menos funciones, más confiabilidad, y el chequeo de salud como lo primero que reviso.

Stack

Python · FastAPI · Redis · PostgreSQL · API de Anthropic · OpenAI Whisper · Next.js 15 · React 19 · TypeScript · Tailwind · pg (sin ORM) · Docker + Compose · GitHub Actions · Playwright


Share this post on:

Artículo siguiente
Neo Tamagotchi — Una mascota de bolsillo para mis perros, en C sobre un RP2350