From 1151d3af3d00f6fd446c917c14fcdab777b29ae9 Mon Sep 17 00:00:00 2001 From: Marco Gallegos Date: Thu, 18 Dec 2025 15:58:01 -0600 Subject: [PATCH] feat: Implement direct MySQL database integration for onboarding and duplicate checks, add Gemini AI support, and update webhook and email configurations. --- .env.example | 59 +++++++++----------- Readme.md | 125 ++++++++++++++++-------------------------- modules/database.py | 102 ++++++++++++++-------------------- modules/onboarding.py | 15 ++++- requirements.txt | 4 +- 5 files changed, 127 insertions(+), 178 deletions(-) diff --git a/.env.example b/.env.example index 688fea0..f98de24 100644 --- a/.env.example +++ b/.env.example @@ -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 diff --git a/Readme.md b/Readme.md index d32b83a..ecc22fd 100644 --- a/Readme.md +++ b/Readme.md @@ -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 2020–2026; `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 2020–2026; `numero_empleado` dinámico; mejoras en flujos de vacaciones/permiso. +- **1.1** — Implementación inicial de webhooks y Docker. diff --git a/modules/database.py b/modules/database.py index f5b1845..25fd837 100644 --- a/modules/database.py +++ b/modules/database.py @@ -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() + diff --git a/modules/onboarding.py b/modules/onboarding.py index 2af4d0a..d5401a9 100644 --- a/modules/onboarding.py +++ b/modules/onboarding.py @@ -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" diff --git a/requirements.txt b/requirements.txt index 71bd7d6..46fc452 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,6 +4,4 @@ requests SQLAlchemy mysql-connector-python google-generativeai -openai -gspread -google-auth-oauthlib \ No newline at end of file +openai \ No newline at end of file