Skip to content

Latest commit

 

History

History
154 lines (131 loc) · 8.56 KB

File metadata and controls

154 lines (131 loc) · 8.56 KB

Arquitectura

Qué hace el proyecto

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 .json en data/. Ú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_reviewapproved_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.

Estructura de carpetas

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

Flujo de datos

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
Loading

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.

Por qué está organizado así

  • 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.json separado de app/ — 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.py centraliza rutas — todos los módulos resuelven config/companies.json, data/ y app/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 que python -m app.cli y uvicorn app.web:app funcionen igual sin importar el cwd.
  • data/ gitignored por completo — tanto el dashboard.db local como los .json que 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.

Puntos débiles conocidos (no bloqueantes, documentados para la siguiente iteración)

  • 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.py corre 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() en app/webhooks.py está ajustado a eso (ver ADR 004). Falta confirmar el payload de un botón (Confirmar/Redactar) tocado — se asume el mismo formato interactive.button_reply que ya usa whatsapp_client.py para enviarlos, pero puede necesitar ajuste en el primer tap real.