mirror of
https://github.com/marcogll/talia_bot.git
synced 2026-01-13 13:25:19 +00:00
Merge pull request #36 from marcogll/feat/cleanup-and-refactor-15004564199648452045
feat: Implement remote printing and sales RAG flow
This commit is contained in:
@@ -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
|
# Zona horaria por defecto para el manejo de fechas y horas
|
||||||
TIMEZONE = os.getenv("TIMEZONE", "America/Mexico_City")
|
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")
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"id": "client_sales_funnel",
|
"id": "client_sales_funnel",
|
||||||
"role": "client",
|
"role": "client",
|
||||||
|
"trigger_button": "get_service_info",
|
||||||
"trigger_automatic": true,
|
"trigger_automatic": true,
|
||||||
"steps": [
|
"steps": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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.aprobaciones import view_pending, handle_approval_action
|
||||||
from talia_bot.modules.servicios import get_service_info
|
from talia_bot.modules.servicios import get_service_info
|
||||||
from talia_bot.modules.admin import get_system_status
|
from talia_bot.modules.admin import get_system_status
|
||||||
|
import os
|
||||||
from talia_bot.modules.debug import print_handler
|
from talia_bot.modules.debug import print_handler
|
||||||
from talia_bot.modules.create_tag import create_tag_conv_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.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.db import setup_database
|
||||||
|
from talia_bot.modules.flow_engine import FlowEngine
|
||||||
|
|
||||||
from talia_bot.scheduler import schedule_daily_summary
|
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
|
# Respondemos al usuario
|
||||||
await update.message.reply_text(response_text, reply_markup=reply_markup)
|
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:
|
async def button_dispatcher(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
"""
|
"""
|
||||||
Esta función maneja los clics en los botones del menú.
|
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."
|
response_text = "❌ Ocurrió un error al procesar tu solicitud. Intenta de nuevo."
|
||||||
reply_markup = None
|
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')
|
await query.edit_message_text(text=response_text, reply_markup=reply_markup, parse_mode='Markdown')
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
@@ -128,6 +199,11 @@ def main() -> None:
|
|||||||
setup_database()
|
setup_database()
|
||||||
|
|
||||||
application = Application.builder().token(TELEGRAM_BOT_TOKEN).build()
|
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)
|
schedule_daily_summary(application)
|
||||||
|
|
||||||
# El orden de los handlers es crucial para que las conversaciones funcionen.
|
# 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("start", start))
|
||||||
application.add_handler(CommandHandler("print", print_handler))
|
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))
|
application.add_handler(CallbackQueryHandler(button_dispatcher))
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from talia_bot.db import get_db_connection
|
from talia_bot.db import get_db_connection
|
||||||
|
from talia_bot.modules.sales_rag import generate_sales_pitch
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
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'])
|
self.update_conversation_state(user_id, state['flow_id'], next_step_id, state['collected_data'])
|
||||||
return {"status": "in_progress", "step": next_step}
|
return {"status": "in_progress", "step": next_step}
|
||||||
else:
|
else:
|
||||||
|
final_data = state['collected_data']
|
||||||
self.end_flow(user_id)
|
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):
|
def end_flow(self, user_id):
|
||||||
"""Ends a flow for a user by deleting their conversation state."""
|
"""Ends a flow for a user by deleting their conversation state."""
|
||||||
|
|||||||
118
talia_bot/modules/printer.py
Normal file
118
talia_bot/modules/printer.py
Normal 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."
|
||||||
64
talia_bot/modules/sales_rag.py
Normal file
64
talia_bot/modules/sales_rag.py
Normal 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)
|
||||||
Reference in New Issue
Block a user