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 # Configuración de Telegram
TELEGRAM_TOKEN=TU_TOKEN_NUEVO_AQUI TELEGRAM_TOKEN=TU_TOKEN_NUEVO_AQUI
TELEGRAM_ADMIN_CHAT_ID=TELEGRAM_ADMIN_CHAT_ID TELEGRAM_ADMIN_CHAT_ID=TELEGRAM_ADMIN_CHAT_ID
OPENAI_API_KEY=SK...... OPENAI_API_KEY=sk-proj-xxxx
GOOGLE_API_KEY=AIzaSyBqH5... 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 # WEBHOOKS
# ===============================
# 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í)
WEBHOOK_ONBOARDING=url WEBHOOK_ONBOARDING=url
# WEBHOOK_CONTRATO=url
WEBHOOK_PRINT=url
WEBHOOK_VACACIONES=url WEBHOOK_VACACIONES=url
WEBHOOK_PERMISOS=url WEBHOOK_PERMISOS=url
WEBHOOK_PRINTS=url
WEBHOOK_SCHEDULE=url
# --- DATABASE --- # ===============================
# Usado por el servicio de la base de datos en docker-compose.yml # DATABASE SETUP
MYSQL_DATABASE_USERS_ALMA=USERS_ALMA # ===============================
MYSQL_DATABASE_VANITY_HR=vanity_hr MYSQL_HOST=db
MYSQL_DATABASE_VANITY_ATTENDANCE=vanity_attendance
MYSQL_USER=user MYSQL_USER=user
MYSQL_PASSWORD=password MYSQL_PASSWORD=password
MYSQL_ROOT_PASSWORD=rootpassword MYSQL_ROOT_PASSWORD=rootpassword
# --- SMTP --- # Database Names
# Usado por el módulo de impresión para enviar correos 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_SERVER=smtp.hostinger.com
SMTP_PORT=465 SMTP_PORT=587
SMTP_USER=your_email@example.com SMTP_USER=your_email@example.com
SMTP_PASSWORD=your_password SMTP_PASSWORD=your_password
SMTP_RECIPIENT=your_email@example.com # También se acepta PRINTER_EMAIL como alias IMAP_SERVER=imap.hostinger.com
GOOGLE_CREDENTIALS_FILE=google_credentials.json 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 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. 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. Vanessa no es un chatbot genérico: es una interfaz conversacional para procesos reales de negocio.
- Onboarding completo de nuevas socias (`/welcome`) - **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`) - **Solicitud de vacaciones (`/vacaciones`)**: Flujo dinámico para gestionar días de descanso.
- Solicitud de permisos por horas (`/permiso`) - **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) ├── docker-compose.yml # Orquestación de servicios (bot + db)
├── README.md # Este documento ├── 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 └── modules/ # Habilidades del bot
├── __init__.py ├── ai.py # Clasificación de motivos con Gemini
├── database.py # Módulo de conexión a la base de datos ├── database.py # Conexión a DB y lógica de negocio (registro/verificación)
├── onboarding.py # Flujo /welcome (onboarding RH) ├── logger.py # Registro de auditoría
── rh_requests.py # /vacaciones y /permiso ── onboarding.py # Flujo /welcome
├── rh_requests.py # /vacaciones y /permiso
└── ui.py # Teclados y componentes de interfaz
``` ```
--- ---
## 🔐 Configuración (.env) ## 🔐 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 ---
TELEGRAM_TOKEN=TU_TOKEN_AQUI TELEGRAM_TOKEN=TU_TOKEN_AQUI
# --- AI (Gemini) ---
GOOGLE_API_KEY=AIzaSy...
# --- WEBHOOKS N8N --- # --- WEBHOOKS N8N ---
WEBHOOK_ONBOARDING=https://... # Alias aceptado: WEBHOOK_CONTRATO WEBHOOK_ONBOARDING=https://...
WEBHOOK_VACACIONES=https://... WEBHOOK_VACACIONES=https://...
WEBHOOK_PERMISOS=https://... WEBHOOK_PERMISOS=https://...
# --- DATABASE --- # --- DATABASE SETUP ---
# Usado por el servicio de la base de datos en docker-compose.yml MYSQL_HOST=db
MYSQL_DATABASE=vanessa_logs
MYSQL_USER=user MYSQL_USER=user
MYSQL_PASSWORD=password MYSQL_PASSWORD=password
MYSQL_ROOT_PASSWORD=rootpassword 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) ## 🐳 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 ### 1. Pre-requisitos
- Docker - Docker y Docker Compose instalaros.
- Docker Compose
### 2. Levantar los servicios ### 2. Levantar los servicios
Con el archivo `.env` ya configurado, simplemente ejecuta:
```bash ```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. 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.
### 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.
--- ---
## 🧩 Arquitectura Interna ## 🧩 Arquitectura Interna
### main.py (El Cerebro) ### main.py (El Cerebro)
- Inicializa el bot de Telegram - Inicializa el bot de Telegram y carga variables de entorno.
- Carga variables de entorno - Registra los handlers de cada módulo y define el menú principal y comandos persistentes.
- Registra los handlers de cada módulo
- Define el menú principal (`/start`, `/help`)
### modules/database.py ### modules/database.py
- Gestiona la conexión a la base de datos MySQL con SQLAlchemy. - Centraliza la conexión a las 3 bases de datos (`USERS_ALMA`, `vanity_hr`, `vanity_attendance`).
- Define el modelo `RequestLog` para la tabla de logs. - **Verificación de duplicados**: Ya no usa Google Sheets; ahora verifica el `telegram_id` directamente en la tabla `users`.
- Provee la función `log_request` para registrar interacciones. - **Registro de usuarias**: Función `register_user` para insertar candidatas tras el onboarding.
### modules/onboarding.py ### modules/onboarding.py
Flujo conversacional complejo que recolecta datos de nuevas empleadas y los envía a un webhook de n8n. Recolección exhaustiva de datos. Al finalizar:
Incluye derivadas útiles: `num_ext_texto` (número en letras, con interior) y `numero_empleado` (primeras 4 del CURP + fecha de ingreso). 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 ### modules/ai.py & modules/rh_requests.py
- Maneja solicitudes simples de RH (Vacaciones y Permisos) y las envía a un webhook de n8n. 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.
- 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.
--- ---
## 🗒️ Registro de versiones ## 🗒️ 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.users_alma_models import Base as BaseUsersAlma, User
from models.vanity_hr_models import Base as BaseVanityHr, DataEmpleadas, Vacaciones, Permisos from models.vanity_hr_models import Base as BaseVanityHr, DataEmpleadas, Vacaciones, Permisos
from models.vanity_attendance_models import Base as BaseVanityAttendance, AsistenciaRegistros, HorarioEmpleadas from models.vanity_attendance_models import Base as BaseVanityAttendance, AsistenciaRegistros, HorarioEmpleadas
import gspread
from google.oauth2.service_account import Credentials
# --- DATABASE (MySQL) SETUP --- # --- DATABASE (MySQL) SETUP ---
def _build_engine(db_name_env_var): 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 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 SessionVanityAttendance = sessionmaker(autocommit=False, autoflush=False, bind=engine_vanity_attendance) if engine_vanity_attendance else None
# --- GOOGLE SHEETS SETUP --- # --- GOOGLE SHEETS SETUP (REMOVED) ---
GSHEET_URL = os.getenv("GOOGLE_SHEET_URL") # Duplicate checking is now done via database.
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
def chat_id_exists(chat_id: int) -> bool: def chat_id_exists(chat_id: int) -> bool:
"""Checks if a Telegram chat_id already exists in the Google Sheet.""" """Checks if a Telegram chat_id already exists in the USERS_ALMA.users table."""
client = get_gsheet_client() if not SessionUsersAlma:
if not client: 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 return False
session = SessionUsersAlma()
try: try:
spreadsheet = client.open_by_url(GSHEET_URL) new_user = User(
worksheet = spreadsheet.get_worksheet(0) telegram_id=str(user_data.get("chat_id")),
chat_ids_in_sheet = worksheet.col_values(SHEET_COLUMN_INDEX) username=user_data.get("telegram_user"),
return str(chat_id) in chat_ids_in_sheet first_name=user_data.get("first_name"),
except gspread.exceptions.SpreadsheetNotFound: last_name=f"{user_data.get('apellido_paterno', '')} {user_data.get('apellido_materno', '')}".strip(),
logging.error("Could not find the spreadsheet at the provided URL.") email=user_data.get("email"),
return False 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: 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 return False
finally:
session.close()

View File

@@ -18,7 +18,7 @@ from telegram.ext import (
) )
from modules.logger import log_request 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 from modules.ui import main_actions_keyboard
# --- 1. CARGA DE ENTORNO --- # --- 1. CARGA DE ENTORNO ---
@@ -404,6 +404,19 @@ async def finalizar(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
except Exception as e: except Exception as e:
logging.error(f"Error enviando webhook a {url}: {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: if enviado:
await update.message.reply_text( await update.message.reply_text(
"✅ *¡Registro Exitoso!*\n\n" "✅ *¡Registro Exitoso!*\n\n"

View File

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