Merge pull request #36 from marcogll/feat/cleanup-and-refactor-15004564199648452045

feat: Implement remote printing and sales RAG flow
This commit is contained in:
Marco Gallegos
2025-12-21 02:56:43 -06:00
committed by GitHub
6 changed files with 282 additions and 1 deletions

View File

@@ -46,3 +46,10 @@ CALENDLY_LINK = os.getenv("CALENDLY_LINK", "https://calendly.com/user/appointmen
# Zona horaria por defecto para el manejo de fechas y horas
TIMEZONE = os.getenv("TIMEZONE", "America/Mexico_City")
# --- PRINT SERVICE ---
SMTP_SERVER = os.getenv("SMTP_SERVER")
SMTP_PORT = os.getenv("SMTP_PORT")
SMTP_USER = os.getenv("SMTP_USER")
SMTP_PASS = os.getenv("SMTP_PASS")
IMAP_SERVER = os.getenv("IMAP_SERVER")

View File

@@ -1,6 +1,7 @@
{
"id": "client_sales_funnel",
"role": "client",
"trigger_button": "get_service_info",
"trigger_automatic": true,
"steps": [
{

View File

@@ -33,10 +33,13 @@ from talia_bot.modules.equipo import (
from talia_bot.modules.aprobaciones import view_pending, handle_approval_action
from talia_bot.modules.servicios import get_service_info
from talia_bot.modules.admin import get_system_status
import os
from talia_bot.modules.debug import print_handler
from talia_bot.modules.create_tag import create_tag_conv_handler
from talia_bot.modules.vikunja import vikunja_conv_handler
from talia_bot.modules.printer import send_file_to_printer, check_print_status
from talia_bot.db import setup_database
from talia_bot.modules.flow_engine import FlowEngine
from talia_bot.scheduler import schedule_daily_summary
@@ -62,6 +65,64 @@ async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
# Respondemos al usuario
await update.message.reply_text(response_text, reply_markup=reply_markup)
async def text_and_voice_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Handles text and voice messages for the flow engine."""
user_id = update.effective_user.id
flow_engine = context.bot_data["flow_engine"]
state = flow_engine.get_conversation_state(user_id)
if not state:
# If there's no active conversation, treat it as a start command
await start(update, context)
return
user_response = update.message.text
if update.message.voice:
# Here you would add the logic to transcribe the voice message
# For now, we'll just use a placeholder
user_response = "Voice message received (transcription not implemented yet)."
result = flow_engine.handle_response(user_id, user_response)
if result["status"] == "in_progress":
await update.message.reply_text(result["step"]["question"])
elif result["status"] == "complete":
if "sales_pitch" in result:
await update.message.reply_text(result["sales_pitch"])
else:
await update.message.reply_text("Gracias por completar el flujo.")
elif result["status"] == "error":
await update.message.reply_text(result["message"])
async def handle_document(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Handles documents sent to the bot for printing."""
document = update.message.document
user_id = update.effective_user.id
file = await context.bot.get_file(document.file_id)
# Create a directory for temporary files if it doesn't exist
temp_dir = 'temp_files'
os.makedirs(temp_dir, exist_ok=True)
file_path = os.path.join(temp_dir, document.file_name)
await file.download_to_drive(file_path)
response = await send_file_to_printer(file_path, user_id, document.file_name)
await update.message.reply_text(response)
# Clean up the downloaded file
os.remove(file_path)
async def check_print_status_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Command to check print status."""
user_id = update.effective_user.id
response = await check_print_status(user_id)
await update.message.reply_text(response)
async def button_dispatcher(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""
Esta función maneja los clics en los botones del menú.
@@ -117,6 +178,16 @@ async def button_dispatcher(update: Update, context: ContextTypes.DEFAULT_TYPE)
response_text = "❌ Ocurrió un error al procesar tu solicitud. Intenta de nuevo."
reply_markup = None
# Check if the button is a flow trigger
flow_engine = context.bot_data["flow_engine"]
flow_to_start = next((flow for flow in flow_engine.flows if flow.get("trigger_button") == query.data), None)
if flow_to_start:
initial_step = flow_engine.start_flow(update.effective_user.id, flow_to_start["id"])
if initial_step:
await query.edit_message_text(text=initial_step["question"])
return
await query.edit_message_text(text=response_text, reply_markup=reply_markup, parse_mode='Markdown')
def main() -> None:
@@ -128,6 +199,11 @@ def main() -> None:
setup_database()
application = Application.builder().token(TELEGRAM_BOT_TOKEN).build()
# Instantiate and store the flow engine in bot_data
flow_engine = FlowEngine()
application.bot_data["flow_engine"] = flow_engine
schedule_daily_summary(application)
# El orden de los handlers es crucial para que las conversaciones funcionen.
@@ -147,6 +223,11 @@ def main() -> None:
application.add_handler(CommandHandler("start", start))
application.add_handler(CommandHandler("print", print_handler))
application.add_handler(CommandHandler("check_print_status", check_print_status_command))
application.add_handler(MessageHandler(filters.Document.ALL, handle_document))
application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND | filters.VOICE, text_and_voice_handler))
application.add_handler(CallbackQueryHandler(button_dispatcher))

View File

@@ -3,6 +3,7 @@ import json
import logging
import os
from talia_bot.db import get_db_connection
from talia_bot.modules.sales_rag import generate_sales_pitch
logger = logging.getLogger(__name__)
@@ -117,8 +118,17 @@ class FlowEngine:
self.update_conversation_state(user_id, state['flow_id'], next_step_id, state['collected_data'])
return {"status": "in_progress", "step": next_step}
else:
final_data = state['collected_data']
self.end_flow(user_id)
return {"status": "complete", "flow_id": flow['id'], "data": state['collected_data']}
response = {"status": "complete", "flow_id": flow['id'], "data": final_data}
if flow['id'] == 'client_sales_funnel':
user_query = final_data.get('IDEA_PITCH', '')
sales_pitch = generate_sales_pitch(user_query, final_data)
response['sales_pitch'] = sales_pitch
return response
def end_flow(self, user_id):
"""Ends a flow for a user by deleting their conversation state."""

View File

@@ -0,0 +1,118 @@
# talia_bot/modules/printer.py
# This module will contain the SMTP/IMAP loop for the remote printing service.
import smtplib
import imaplib
import email
import logging
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.base import MIMEBase
from email import encoders
from talia_bot.config import (
SMTP_SERVER,
SMTP_PORT,
SMTP_USER,
SMTP_PASS,
IMAP_SERVER,
)
from talia_bot.modules.identity import is_admin
logger = logging.getLogger(__name__)
async def send_file_to_printer(file_path: str, user_id: int, file_name: str):
"""
Sends a file to the printer via email.
"""
if not is_admin(user_id):
return "No tienes permiso para usar este comando."
if not all([SMTP_SERVER, SMTP_PORT, SMTP_USER, SMTP_PASS]):
logger.error("Faltan una o más variables de entorno SMTP.")
return "El servicio de impresión no está configurado correctamente."
try:
msg = MIMEMultipart()
msg["From"] = SMTP_USER
msg["To"] = SMTP_USER # Sending to the printer's email address
msg["Subject"] = f"Print Job from {user_id}: {file_name}"
body = f"Nuevo trabajo de impresión enviado por el usuario {user_id}.\nNombre del archivo: {file_name}"
msg.attach(MIMEText(body, "plain"))
with open(file_path, "rb") as attachment:
part = MIMEBase("application", "octet-stream")
part.set_payload(attachment.read())
encoders.encode_base64(part)
part.add_header(
"Content-Disposition",
f"attachment; filename= {file_name}",
)
msg.attach(part)
server = smtplib.SMTP_SSL(SMTP_SERVER, SMTP_PORT)
server.login(SMTP_USER, SMTP_PASS)
text = msg.as_string()
server.sendmail(SMTP_USER, SMTP_USER, text)
server.quit()
logger.info(f"Archivo {file_name} enviado a la impresora por el usuario {user_id}.")
return f"Tu archivo '{file_name}' ha sido enviado a la impresora. Recibirás una notificación cuando el estado del trabajo cambie."
except Exception as e:
logger.error(f"Error al enviar el correo de impresión: {e}")
return "Ocurrió un error al enviar el archivo a la impresora. Por favor, inténtalo de nuevo más tarde."
async def check_print_status(user_id: int):
"""
Checks the status of print jobs by reading the inbox.
"""
if not is_admin(user_id):
return "No tienes permiso para usar este comando."
if not all([IMAP_SERVER, SMTP_USER, SMTP_PASS]):
logger.error("Faltan una o más variables de entorno IMAP.")
return "El servicio de monitoreo de impresión no está configurado correctamente."
try:
mail = imaplib.IMAP4_SSL(IMAP_SERVER)
mail.login(SMTP_USER, SMTP_PASS)
mail.select("inbox")
status, messages = mail.search(None, "UNSEEN")
if status != "OK":
return "No se pudieron buscar los correos."
email_ids = messages[0].split()
if not email_ids:
return "No hay actualizaciones de estado de impresión."
statuses = []
for e_id in email_ids:
_, msg_data = mail.fetch(e_id, "(RFC822)")
for response_part in msg_data:
if isinstance(response_part, tuple):
msg = email.message_from_bytes(response_part[1])
subject = msg["subject"].lower()
if "completed" in subject:
statuses.append(f"Trabajo de impresión completado: {msg['subject']}")
elif "failed" in subject:
statuses.append(f"Trabajo de impresión fallido: {msg['subject']}")
elif "received" in subject:
statuses.append(f"Trabajo de impresión recibido: {msg['subject']}")
else:
statuses.append(f"Nuevo correo: {msg['subject']}")
mail.logout()
if not statuses:
return "No se encontraron actualizaciones de estado relevantes."
return "\n".join(statuses)
except Exception as e:
logger.error(f"Error al revisar el estado de la impresión: {e}")
return "Ocurrió un error al revisar el estado de la impresión."

View File

@@ -0,0 +1,64 @@
# talia_bot/modules/sales_rag.py
# This module will contain the sales RAG flow for new clients.
import json
import logging
from talia_bot.modules.llm_engine import get_smart_response
logger = logging.getLogger(__name__)
def load_services_data():
"""Loads the services data from the JSON file."""
try:
with open("talia_bot/data/services.json", "r", encoding="utf-8") as f:
return json.load(f)
except FileNotFoundError:
logger.error("El archivo services.json no fue encontrado.")
return []
except json.JSONDecodeError:
logger.error("Error al decodificar el archivo services.json.")
return []
def find_relevant_services(user_query, services):
"""
Finds relevant services based on the user's query.
A simple keyword matching approach is used here.
"""
query = user_query.lower()
relevant_services = []
for service in services:
for keyword in service.get("keywords", []):
if keyword in query:
relevant_services.append(service)
break # Avoid adding the same service multiple times
return relevant_services
def generate_sales_pitch(user_query, collected_data):
"""
Generates a personalized sales pitch using the RAG approach.
"""
services = load_services_data()
relevant_services = find_relevant_services(user_query, services)
if not relevant_services:
# Fallback if no specific services match
context_str = "No specific services match the user's request, but we can offer general business consulting."
else:
context_str = "Based on your needs, here are some services we offer:\n"
for service in relevant_services:
context_str += f"- **{service['service_name']}**: {service['description']}\n"
prompt = (
f"El cliente {collected_data.get('CLIENT_NAME', 'un cliente')} "
f"del sector {collected_data.get('CLIENT_INDUSTRY', 'no especificado')} "
f"ha descrito su proyecto de la siguiente manera: '{user_query}'.\n\n"
f"Aquí hay información sobre nuestros servicios que podría ser relevante para ellos:\n{context_str}\n\n"
"Actúa como un asistente de ventas amigable y experto llamado Talia. "
"Tu objetivo es conectar su idea con nuestros servicios y proponer los siguientes pasos. "
"Genera una respuesta personalizada que:\n"
"1. Muestre que has entendido su idea.\n"
"2. Destaque cómo nuestros servicios pueden ayudarles a alcanzar sus objetivos.\n"
"3. Termine con una llamada a la acción clara, como sugerir una llamada o una reunión para discutirlo más a fondo."
)
return get_smart_response(prompt)