Lee comentarios reales de una página de Facebook (Graph API), usa Claude para clasificar cuáles son preguntas/consultas (y de qué tipo: técnica, comercial, soporte, otro), genera un borrador de respuesta con el tono de la marca, y permite que ese borrador se publique — automáticamente si es de bajo riesgo (comercial + confianza alta, ver ADR 002) o tras revisión humana en un dashboard web para todo lo demás.
Dos formas de entrada al mismo motor:
- CLI (
app/cli.py) — corridas puntuales/manuales, imprime en consola y guarda un.jsonendata/. Útil para explorar un post específico o correr ad-hoc. - Dashboard web (
app/web.py, FastAPI + HTMX) — pensado para uso en equipo: sincroniza en background, persiste estado en base de datos (pending_review→approved_published/rejected/auto_published) y da una cola de revisión editable. Ver ADR 003.
Ambos reutilizan el mismo core sin duplicar lógica: facebook_client.py
(Graph API), classifier.py (clasificación con Claude) y
response_generator.py (generación de borradores con few-shot de respuestas
reales de la marca) no saben si quien los llama es el CLI o el dashboard.
fb-question-detector/
├── app/ # paquete de la aplicación
│ ├── cli.py # entrypoint CLI → python -m app.cli
│ ├── web.py # entrypoint web → uvicorn app.web:app
│ ├── daemon.py # entrypoint demonio → python -m app.daemon (Cron Job: una pasada de sync + notificación WhatsApp, ver ADR 006)
│ ├── config.py # rutas centralizadas (config/, data/, templates/)
│ ├── facebook_client.py # cliente Graph API (leer posts/comentarios, publicar respuestas)
│ ├── facebook_webhook.py # POST /webhooks/facebook — ingesta en tiempo real (vía principal)
│ ├── classifier.py # clasifica comentarios con LLM (es_pregunta, tipo, confianza) + retry/backoff
│ ├── response_generator.py # genera borradores de respuesta (few-shot con tono de marca)
│ ├── llm_client.py # cliente LLM agnóstico de proveedor (Anthropic/OpenAI/DeepSeek/compatible)
│ ├── whatsapp_client.py # cliente REST de Kapso (envío de mensajes/botones de WhatsApp)
│ ├── whatsapp_notifier.py # notifica por WhatsApp los comentarios en pending_review
│ ├── webhooks.py # POST /webhooks/whatsapp — botones Confirmar/Redactar y texto libre
│ ├── database.py # modelos SQLAlchemy (Comment + estado de WhatsApp + LLMCallLog) + sesión (SQLite local / Postgres prod)
│ ├── sync.py # orquesta: trae comentarios nuevos → clasifica → genera → guarda/auto-publica
│ ├── cost_report.py # agrega LLMCallLog → GET /costos y `cli.py --cost-report` (ver ADR 007)
│ └── templates/ # HTML (Jinja2 + HTMX) que sirve app/web.py
├── config/
│ └── companies.json # qué páginas de Facebook gestiona el proyecto (sin secretos)
├── docs/
│ ├── CONTEXT.md # glosario y decisiones de negocio
│ ├── DASHBOARD_DEPLOY.md # despliegue completo (dashboard + demonio, Railway/Render) y cuentas necesarias
│ ├── FACEBOOK_WEBHOOK_SETUP.md # trámite de suscripción del webhook de Meta
│ ├── WHATSAPP_SETUP.md # cómo configurar notificaciones por WhatsApp (Kapso)
│ ├── GUIA_USUARIO.md # guía para el dueño del negocio (dashboard + WhatsApp), no técnica
│ ├── ANALISIS_COSTOS.md # costos de producción (LLM, WhatsApp, hosting) — ver ADR 007 para datos reales medidos
│ └── adr/ # Architecture Decision Records numerados
├── data/ # generado en runtime, gitignored (dashboard.db, *.json de salida del CLI)
├── .env.example # variables de entorno requeridas, sin valores reales
├── requirements.txt
└── README.md
flowchart LR
FB[Facebook Graph API]
FBWH[facebook_webhook.py\ntiempo real, vía principal]
DAEMON[daemon.py\ncron 2x/día, red de seguridad\nfast_sync + deep_scan]
CLIRUN[cli.py\ncorrida puntual]
FB -->|evento comentario| FBWH
FB -->|posts + comentarios, vía facebook_client.py| DAEMON
FB -->|posts + comentarios, vía facebook_client.py| CLIRUN
FBWH --> SYNC[sync.py: process_comment]
DAEMON --> SYNC
SYNC --> CL[classifier.py\nClaude: es_pregunta / tipo / confianza]
CLIRUN --> CL
CL -->|es_pregunta=true| RG[response_generator.py\nClaude: borrador con tono de marca]
CL -->|es_pregunta=false, solo vía sync.py| DB
RG --> DB[(database.py\nComment)]
RG --> JSONOUT[data/*.json — solo CLI]
DB --> DASH[dashboard HTML/HTMX]
DASH -->|aprobar manual| PUB[fb.post_reply]
DB -->|nuevo pending_review| WAN[whatsapp_notifier.py]
WAN --> WAC[whatsapp_client.py]
WAC --> KAPSO[Kapso API]
KAPSO --> OWNER[WhatsApp del dueño]
OWNER -->|tap Confirmar / Redactar + texto| KAPSO
KAPSO --> WAWH[webhooks.py\nPOST /webhooks/whatsapp]
WAWH -->|Confirmar| PUB
WAWH -->|Redactar: guarda el texto del dueño directo, sin volver a llamar al LLM| DB
PUB -->|respuesta publicada| FB
sync.py es el único camino que persiste en database.py — el CLI comparte
classifier.py/response_generator.py pero no pasa por sync.py ni por la
base de datos, así que sus resultados no disparan notificación de WhatsApp ni
aparecen en el dashboard (quedan solo en el .json que exporta). El loop de
WhatsApp (whatsapp_notifier.py → Kapso → dueño → webhooks.py) es una vía
alternativa de aprobación al dashboard, no un reemplazo: ambos actúan sobre el
mismo Comment.status, así que aprobar por cualquiera de los dos lo saca de
la cola del otro.
- Un solo paquete
app/, no un monorepo con frontend separado — para un tool interno de pocos usuarios, HTMX dentro de FastAPI da la interactividad necesaria sin duplicar infraestructura de un SPA. Detalle y trade-offs en ADR 003. config/companies.jsonseparado deapp/— es configuración de negocio (qué páginas se gestionan), no código; se edita sin tocar Python al agregar una empresa nueva (ver README).app/config.pycentraliza rutas — todos los módulos resuelvenconfig/companies.json,data/yapp/templates/de forma absoluta (relativa a la ubicación del archivo, no al directorio desde donde se ejecuta el comando). Esto es lo que permite quepython -m app.cliyuvicorn app.web:appfuncionen igual sin importar elcwd.data/gitignored por completo — tanto eldashboard.dblocal como los.jsonque exporta el CLI pueden contener datos reales de clientes (nombres, comentarios). Nunca se suben al repo.- El clasificador no filtra, solo etiqueta — decisión de diseño explícita documentada en docs/CONTEXT.md: filtrar por tipo de interés es responsabilidad de quien consume la salida (CLI o dashboard), no del clasificador.
- La auto-publicación es intencionalmente angosta — solo comercial +
confianza alta se publica sin revisión humana; todo lo demás cae a
pending_review. Razonamiento completo en ADR 002.
- Auth del dashboard es un solo usuario/contraseña compartido — no apto para más de un puñado de personas sin fricción (ver docs/DASHBOARD_DEPLOY.md).
- El dashboard en sí no sincroniza en background (sigue requiriendo apretar
"Sincronizar" para una corrida manual); el webhook de Facebook cubre el
caso en tiempo real, y
app/daemon.pycorre como Cron Job aparte (red de seguridad, no vía principal) — ver ADR 006. - El payload de webhook de Kapso para mensajes de texto ya se confirmó contra
un caso real y
_extract_event()enapp/webhooks.pyestá ajustado a eso (ver ADR 004). Falta confirmar el payload de un botón (Confirmar/Redactar) tocado — se asume el mismo formatointeractive.button_replyque ya usawhatsapp_client.pypara enviarlos, pero puede necesitar ajuste en el primer tap real.