mirror of
https://github.com/marcogll/talia_bot.git
synced 2026-01-13 13:25:19 +00:00
feat: Implement remote printing and sales RAG flow
Implement the first two items from the product roadmap:
1. **Remote Printing Service:**
* Create a new `printer.py` module to handle sending files via SMTP and checking status via IMAP.
* Add a document handler in `main.py` to allow admin users to send files to the bot for printing.
* Add a `/check_print_status` command for admins to monitor the print job status.
* Add SMTP/IMAP configuration variables to `config.py` and `.env.example`.
2. **Sales RAG Flow:**
* Implement a `sales_rag.py` module to generate personalized sales pitches.
* The sales flow uses a Retrieval-Augmented Generation (RAG) approach, retrieving relevant services from `services.json` to create a detailed prompt for the LLM.
* The existing `flow_engine.py` is modified to trigger the sales pitch generation upon completion of the `client_sales_funnel` flow.
* Update `main.py` to handle the flow engine's responses and send the generated pitch to the user.
* Update `client_sales_funnel.json` to be triggered by a button in the client menu.
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
|
||||
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",
|
||||
"role": "client",
|
||||
"trigger_button": "get_service_info",
|
||||
"trigger_automatic": true,
|
||||
"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.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))
|
||||
|
||||
|
||||
@@ -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."""
|
||||
|
||||
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