feat: Implement direct MySQL database integration for onboarding and duplicate checks, add Gemini AI support, and update webhook and email configurations.

This commit is contained in:
Marco Gallegos
2025-12-18 15:58:01 -06:00
parent 8387a5851a
commit 1151d3af3d
5 changed files with 127 additions and 178 deletions

View File

@@ -1,49 +1,40 @@
# Configuración de Telegram
TELEGRAM_TOKEN=TU_TOKEN_NUEVO_AQUI
TELEGRAM_ADMIN_CHAT_ID=TELEGRAM_ADMIN_CHAT_ID
OPENAI_API_KEY=SK......
GOOGLE_API_KEY=AIzaSyBqH5...
OPENAI_API_KEY=sk-proj-xxxx
GOOGLE_API_KEY=AIzaSyBqH5... # Usado para Gemini AI en modules/ai.py
# URL de la hoja de cálculo de Google para verificar duplicados
GOOGLE_SHEET_URL=https://docs.google.com/spreadsheets/d/1iVHnNoAF4sVVhb2kcclthznYFUKetmhsM6b2ZUCXd-0/edit?gid=370216950#gid=370216950
# Opcional: Credenciales de Google como variables de entorno
# Si estas variables están definidas, se usarán en lugar del archivo JSON.
# Asegúrate de escapar correctamente el valor de GSA_PRIVATE_KEY (ej. reemplazando saltos de línea con \n)
GSA_TYPE=service_account
GSA_PROJECT_ID=
GSA_PRIVATE_KEY_ID=
GSA_PRIVATE_KEY=
GSA_CLIENT_EMAIL=
GSA_CLIENT_ID=
GSA_AUTH_URI=
GSA_TOKEN_URI=
GSA_AUTH_PROVIDER_X509_CERT_URL=
GSA_CLIENT_X509_CERT_URL=
# Webhooks de n8n (puedes agregar más aquí en el futuro)
# Usa WEBHOOK_ONBOARDING (o el alias WEBHOOK_CONTRATO si ya lo tienes así)
# ===============================
# WEBHOOKS
# ===============================
WEBHOOK_ONBOARDING=url
# WEBHOOK_CONTRATO=url
WEBHOOK_PRINT=url
WEBHOOK_VACACIONES=url
WEBHOOK_PERMISOS=url
WEBHOOK_PRINTS=url
WEBHOOK_SCHEDULE=url
# --- DATABASE ---
# Usado por el servicio de la base de datos en docker-compose.yml
MYSQL_DATABASE_USERS_ALMA=USERS_ALMA
MYSQL_DATABASE_VANITY_HR=vanity_hr
MYSQL_DATABASE_VANITY_ATTENDANCE=vanity_attendance
# ===============================
# DATABASE SETUP
# ===============================
MYSQL_HOST=db
MYSQL_USER=user
MYSQL_PASSWORD=password
MYSQL_ROOT_PASSWORD=rootpassword
# --- SMTP ---
# Usado por el módulo de impresión para enviar correos
# Database Names
MYSQL_DATABASE_USERS_ALMA=USERS_ALMA
MYSQL_DATABASE_VANITY_HR=vanity_hr
MYSQL_DATABASE_VANITY_ATTENDANCE=vanity_attendance
# ===============================
# EMAIL SETUP
# ===============================
SMTP_SERVER=smtp.hostinger.com
SMTP_PORT=465
SMTP_PORT=587
SMTP_USER=your_email@example.com
SMTP_PASSWORD=your_password
SMTP_RECIPIENT=your_email@example.com # También se acepta PRINTER_EMAIL como alias
GOOGLE_CREDENTIALS_FILE=google_credentials.json
IMAP_SERVER=imap.hostinger.com
IMAP_PORT=993
IMAP_USER=your_email@example.com
IMAP_PASSWORD=your_password
PRINTER_EMAIL=your_printer_email@example.com

125
Readme.md
View File

@@ -1,6 +1,6 @@
# 🤖 Vanessa Bot Asistente de RH para Vanity
Vanessa es un bot de Telegram escrito en Python que automatiza procesos internos de Recursos Humanos en Vanity. Su objetivo es eliminar fricción operativa: onboarding y solicitudes de RH, todo orquestado desde Telegram y conectado a flujos de n8n o servicios de correo.
Vanessa es un bot de Telegram escrito en Python que automatiza procesos internos de Recursos Humanos en Vanity. Su objetivo es eliminar fricción operativa: onboarding y solicitudes de RH, todo orquestado desde Telegram y conectado a flujos de n8n, servicios de correo y bases de datos MySQL.
Este repositorio está pensado como **proyecto Python profesional**, modular y listo para correr 24/7 en producción.
@@ -10,11 +10,11 @@ Este repositorio está pensado como **proyecto Python profesional**, modular y l
Vanessa no es un chatbot genérico: es una interfaz conversacional para procesos reales de negocio.
- Onboarding completo de nuevas socias (`/welcome`)
- Solicitud de vacaciones (`/vacaciones`)
- Solicitud de permisos por horas (`/permiso`)
- **Onboarding completo de nuevas socias (`/welcome`)**: Recolecta datos, valida que no existan duplicados en la DB, registra a la usuaria en `USERS_ALMA` y envía los datos a n8n.
- **Solicitud de vacaciones (`/vacaciones`)**: Flujo dinámico para gestionar días de descanso.
- **Solicitud de permisos por horas (`/permiso`)**: Incluye clasificación de motivos mediante IA (Gemini).
Cada flujo es un módulo independiente, y los datos se envían a **webhooks de n8n**.
Cada flujo es un módulo independiente que interactúa con la base de datos y flujos de **n8n**.
---
@@ -31,124 +31,91 @@ vanity_bot/
├── docker-compose.yml # Orquestación de servicios (bot + db)
├── README.md # Este documento
├── models/ # Modelos de base de datos (SQLAlchemy)
│ ├── users_alma_models.py
│ ├── vanity_hr_models.py
│ └── vanity_attendance_models.py
└── modules/ # Habilidades del bot
├── __init__.py
├── database.py # Módulo de conexión a la base de datos
├── onboarding.py # Flujo /welcome (onboarding RH)
── rh_requests.py # /vacaciones y /permiso
├── ai.py # Clasificación de motivos con Gemini
├── database.py # Conexión a DB y lógica de negocio (registro/verificación)
├── logger.py # Registro de auditoría
── onboarding.py # Flujo /welcome
├── rh_requests.py # /vacaciones y /permiso
└── ui.py # Teclados y componentes de interfaz
```
---
## 🔐 Configuración (.env)
Copia el archivo `.env.example` a `.env` y rellena los valores correspondientes. Este archivo es ignorado por Git para proteger tus credenciales.
Vanessa utiliza múltiples bases de datos y webhooks. Asegúrate de configurar correctamente los nombres de las bases de datos.
```
```ini
# --- TELEGRAM ---
TELEGRAM_TOKEN=TU_TOKEN_AQUI
# --- AI (Gemini) ---
GOOGLE_API_KEY=AIzaSy...
# --- WEBHOOKS N8N ---
WEBHOOK_ONBOARDING=https://... # Alias aceptado: WEBHOOK_CONTRATO
WEBHOOK_ONBOARDING=https://...
WEBHOOK_VACACIONES=https://...
WEBHOOK_PERMISOS=https://...
# --- DATABASE ---
# Usado por el servicio de la base de datos en docker-compose.yml
MYSQL_DATABASE=vanessa_logs
# --- DATABASE SETUP ---
MYSQL_HOST=db
MYSQL_USER=user
MYSQL_PASSWORD=password
MYSQL_ROOT_PASSWORD=rootpassword
# Nombres de las Bases de Datos
MYSQL_DATABASE_USERS_ALMA=USERS_ALMA
MYSQL_DATABASE_VANITY_HR=vanity_hr
MYSQL_DATABASE_VANITY_ATTENDANCE=vanity_attendance
```
---
## 🐳 Ejecución con Docker (Recomendado)
El proyecto está dockerizado para facilitar su despliegue.
El proyecto está dockerizado para facilitar su despliegue y aislamiento.
### 1. Pre-requisitos
- Docker
- Docker Compose
- Docker y Docker Compose instalaros.
### 2. Levantar los servicios
Con el archivo `.env` ya configurado, simplemente ejecuta:
```bash
docker-compose up --build
docker-compose up --build -d
```
Este comando construirá la imagen del bot, descargará la imagen de MySQL, y lanzará ambos servicios. `docker-compose` leerá las variables del archivo `.env` para configurar los contenedores.
### 3. Detener los servicios
Para detener los contenedores, presiona `Ctrl+C` en la terminal donde se están ejecutando, o ejecuta desde otro terminal:
```bash
docker-compose down
```
### 4. Despliegue con imagen pre-construida (Collify)
Si Collify solo consume imágenes ya publicadas, usa el archivo `docker-compose.collify.yml` que apunta a una imagen en registro (`DOCKER_IMAGE`).
1) Construir y publicar la imagen (ejemplo con Buildx y tag con timestamp):
```bash
export DOCKER_IMAGE=marcogll/vanessa-bot:prod-$(date +%Y%m%d%H%M)
docker buildx build --platform linux/amd64 -t $DOCKER_IMAGE . --push
```
2) Desplegar en el servidor (Collify) usando la imagen publicada:
```bash
export DOCKER_IMAGE=marcogll/vanessa-bot:prod-20240101
docker compose -f docker-compose.collify.yml pull
docker compose -f docker-compose.collify.yml up -d
```
`docker-compose.collify.yml` usa `env_file: .env`, así que carga las credenciales igual que en local o configúralas como variables de entorno en la plataforma.
Este comando levantará el bot y un contenedor de MySQL (si se usa el compose por defecto). El bot se reconectará automáticamente a la DB si esta tarda en iniciar.
---
## 🧩 Arquitectura Interna
### main.py (El Cerebro)
- Inicializa el bot de Telegram
- Carga variables de entorno
- Registra los handlers de cada módulo
- Define el menú principal (`/start`, `/help`)
- Inicializa el bot de Telegram y carga variables de entorno.
- Registra los handlers de cada módulo y define el menú principal y comandos persistentes.
### modules/database.py
- Gestiona la conexión a la base de datos MySQL con SQLAlchemy.
- Define el modelo `RequestLog` para la tabla de logs.
- Provee la función `log_request` para registrar interacciones.
- Centraliza la conexión a las 3 bases de datos (`USERS_ALMA`, `vanity_hr`, `vanity_attendance`).
- **Verificación de duplicados**: Ya no usa Google Sheets; ahora verifica el `telegram_id` directamente en la tabla `users`.
- **Registro de usuarias**: Función `register_user` para insertar candidatas tras el onboarding.
### modules/onboarding.py
Flujo conversacional complejo que recolecta datos de nuevas empleadas y los envía a un webhook de n8n.
Incluye derivadas útiles: `num_ext_texto` (número en letras, con interior) y `numero_empleado` (primeras 4 del CURP + fecha de ingreso).
Recolección exhaustiva de datos. Al finalizar:
1. Valida y formatea datos (RFC, CURP, fechas).
2. Registra a la empleada en la base de datos MySQL.
3. Envía el payload completo al webhook de n8n para generación de contratos.
### modules/rh_requests.py
- Maneja solicitudes simples de RH (Vacaciones y Permisos) y las envía a un webhook de n8n.
- Vacaciones: pregunta año (actual o siguiente), día/mes de inicio y fin, calcula métricas y aplica semáforo automático.
- Permisos: ofrece accesos rápidos (hoy/mañana/pasado) o fecha específica (año actual/siguiente, día/mes), pide horario, clasifica motivo con IA y envía al webhook.
---
## 🧠 Filosofía del Proyecto
- **Telegram como UI**: Interfaz conversacional accesible para todos.
- **Python como cerebro**: Lógica de negocio y orquestación.
- **Docker para despliegue**: Entornos consistentes y portátiles.
- **MySQL para persistencia**: Registro auditable de todas las interacciones.
- **Modularidad total**: Cada habilidad es un componente independiente.
---
## 🧪 Estado del Proyecto
✔ Funcional en producción
✔ Modular
✔ Escalable
✔ Auditable
Vanessa está viva. Y aprende con cada flujo nuevo.
### modules/ai.py & modules/rh_requests.py
Integración con **Google Gemini** para clasificar automáticamente los motivos de los permisos (Médico, Trámite, etc.) y envío sincronizado a webhooks de gestión humana.
---
## 🗒️ Registro de versiones
- **1.2 (2025-01-25)** — Onboarding: selector de año 20202026; `numero_empleado` incluye prefijo CURP (4 chars) + fecha de ingreso; vacaciones/permiso ajustan fin automático al siguiente año cuando aplica.
- **1.3 (2025-12-18)** — **Adiós Google Sheets**: Migración total a base de datos MySQL para verificación de existencia y registro de nuevas socias. Limpieza de `.env` y optimización de arquitectura de modelos.
- **1.2 (2025-01-25)** — Onboarding: selector de año 20202026; `numero_empleado` dinámico; mejoras en flujos de vacaciones/permiso.
- **1.1** — Implementación inicial de webhooks y Docker.

View File

@@ -5,8 +5,7 @@ from sqlalchemy.orm import sessionmaker
from models.users_alma_models import Base as BaseUsersAlma, User
from models.vanity_hr_models import Base as BaseVanityHr, DataEmpleadas, Vacaciones, Permisos
from models.vanity_attendance_models import Base as BaseVanityAttendance, AsistenciaRegistros, HorarioEmpleadas
import gspread
from google.oauth2.service_account import Credentials
# --- DATABASE (MySQL) SETUP ---
def _build_engine(db_name_env_var):
@@ -36,69 +35,50 @@ SessionUsersAlma = sessionmaker(autocommit=False, autoflush=False, bind=engine_u
SessionVanityHr = sessionmaker(autocommit=False, autoflush=False, bind=engine_vanity_hr) if engine_vanity_hr else None
SessionVanityAttendance = sessionmaker(autocommit=False, autoflush=False, bind=engine_vanity_attendance) if engine_vanity_attendance else None
# --- GOOGLE SHEETS SETUP ---
GSHEET_URL = os.getenv("GOOGLE_SHEET_URL")
GOOGLE_CREDENTIALS_FILE = os.getenv("GOOGLE_CREDENTIALS_FILE", "google_credentials.json")
SHEET_COLUMN_INDEX = 40 # AN is the 40th column
def get_gsheet_client():
"""Returns an authenticated gspread client or None if it fails."""
if not GSHEET_URL:
logging.warning("GOOGLE_SHEET_URL is not configured. Duplicate checking is disabled.")
return None
creds = None
scopes = ["https://www.googleapis.com/auth/spreadsheets.readonly"]
gsa_creds_dict = {
"type": os.getenv("GSA_TYPE"),
"project_id": os.getenv("GSA_PROJECT_ID"),
"private_key_id": os.getenv("GSA_PRIVATE_KEY_ID"),
"private_key": (os.getenv("GSA_PRIVATE_KEY") or "").replace("\\n", "\n"),
"client_email": os.getenv("GSA_CLIENT_EMAIL"),
"client_id": os.getenv("GSA_CLIENT_ID"),
"auth_uri": os.getenv("GSA_AUTH_URI"),
"token_uri": os.getenv("GSA_TOKEN_URI"),
"auth_provider_x509_cert_url": os.getenv("GSA_AUTH_PROVIDER_X509_CERT_URL"),
"client_x509_cert_url": os.getenv("GSA_CLIENT_X509_CERT_URL"),
}
if all(gsa_creds_dict.values()):
try:
creds = Credentials.from_service_account_info(gsa_creds_dict, scopes=scopes)
except Exception as e:
logging.error(f"Error processing Google credentials from environment: {e}")
return None
elif os.path.exists(GOOGLE_CREDENTIALS_FILE):
try:
creds = Credentials.from_service_account_file(GOOGLE_CREDENTIALS_FILE, scopes=scopes)
except Exception as e:
logging.error(f"Error processing credentials file '{GOOGLE_CREDENTIALS_FILE}': {e}")
return None
else:
logging.warning("Google credentials not found (neither environment variables nor file). Duplicate checking is disabled.")
return None
try:
return gspread.authorize(creds)
except Exception as e:
logging.error(f"Error authorizing gspread client: {e}")
return None
# --- GOOGLE SHEETS SETUP (REMOVED) ---
# Duplicate checking is now done via database.
def chat_id_exists(chat_id: int) -> bool:
"""Checks if a Telegram chat_id already exists in the Google Sheet."""
client = get_gsheet_client()
if not client:
"""Checks if a Telegram chat_id already exists in the USERS_ALMA.users table."""
if not SessionUsersAlma:
logging.warning("SessionUsersAlma not initialized. Cannot check if chat_id exists.")
return False
session = SessionUsersAlma()
try:
exists = session.query(User).filter(User.telegram_id == str(chat_id)).first() is not None
return exists
except Exception as e:
logging.error(f"Error checking if chat_id exists in DB: {e}")
return False
finally:
session.close()
def register_user(user_data: dict) -> bool:
"""Registers a new user in the USERS_ALMA.users table."""
if not SessionUsersAlma:
logging.warning("SessionUsersAlma not initialized. Cannot register user.")
return False
session = SessionUsersAlma()
try:
spreadsheet = client.open_by_url(GSHEET_URL)
worksheet = spreadsheet.get_worksheet(0)
chat_ids_in_sheet = worksheet.col_values(SHEET_COLUMN_INDEX)
return str(chat_id) in chat_ids_in_sheet
except gspread.exceptions.SpreadsheetNotFound:
logging.error("Could not find the spreadsheet at the provided URL.")
return False
new_user = User(
telegram_id=str(user_data.get("chat_id")),
username=user_data.get("telegram_user"),
first_name=user_data.get("first_name"),
last_name=f"{user_data.get('apellido_paterno', '')} {user_data.get('apellido_materno', '')}".strip(),
email=user_data.get("email"),
cell_phone=user_data.get("celular"),
role='user' # Default role
)
session.add(new_user)
session.commit()
logging.info(f"User {user_data.get('chat_id')} registered successfully in DB.")
return True
except Exception as e:
logging.error(f"Error reading the spreadsheet: {e}")
session.rollback()
logging.error(f"Error registering user in DB: {e}")
return False
finally:
session.close()

View File

@@ -18,7 +18,7 @@ from telegram.ext import (
)
from modules.logger import log_request
from modules.database import chat_id_exists
from modules.database import chat_id_exists, register_user
from modules.ui import main_actions_keyboard
# --- 1. CARGA DE ENTORNO ---
@@ -404,6 +404,19 @@ async def finalizar(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
except Exception as e:
logging.error(f"Error enviando webhook a {url}: {e}")
# --- REGISTRO EN BASE DE DATOS ---
db_ok = register_user({
**meta,
**payload["metadata"],
**payload["candidato"],
**payload["contacto"]
})
if db_ok:
logging.info(f"Usuario {meta['chat_id']} registrado en la base de datos.")
else:
logging.error(f"Fallo al registrar usuario {meta['chat_id']} en la base de datos.")
if enviado:
await update.message.reply_text(
"✅ *¡Registro Exitoso!*\n\n"

View File

@@ -4,6 +4,4 @@ requests
SQLAlchemy
mysql-connector-python
google-generativeai
openai
gspread
google-auth-oauthlib
openai