diff --git a/.gitignore b/.gitignore index 60c768f..682258b 100644 --- a/.gitignore +++ b/.gitignore @@ -158,7 +158,6 @@ cython_debug/ .vscode/ # Google Service Account Credentials -*.json !credentials.example.json google_key.json diff --git a/README.md b/README.md index 206299f..8c2c895 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,9 @@ El sistema sigue un flujo modular: --- -## 📋 Flujos de Trabajo (Features) +## 📋 Flujos de Trabajo Modulares (Features) + +El comportamiento del bot se define a través de **flujos de conversación modulares** gestionados por un motor central (`flow_engine.py`). Cada flujo es un archivo `.json` independiente ubicado en `talia_bot/data/flows/`, lo que permite modificar o crear nuevas conversaciones sin alterar el código principal. ### 1. 👑 Gestión Admin (Proyectos & Identidad) @@ -118,10 +120,11 @@ IMAP_SERVER=imap.hostinger.com ### 3. Estructura de Datos -Asegúrate de tener los archivos base en `talia_bot/data/`: +Asegúrate de tener los archivos y directorios base en `talia_bot/data/`: * `servicios.json`: Catálogo de servicios para el RAG de ventas. * `credentials.json`: Credenciales de Google Cloud. -* `users.db`: Base de datos SQLite. +* `users.db`: Base de datos SQLite que almacena los roles de los usuarios. +* `flows/`: Directorio que contiene las definiciones de los flujos de conversación en formato JSON. Cada archivo representa una conversación completa para un rol específico. --- @@ -130,10 +133,11 @@ Asegúrate de tener los archivos base en `talia_bot/data/`: ```text talia_bot_mg/ ├── talia_bot/ -│ ├── main.py # Entry Point y Router de Identidad -│ ├── db.py # Gestión de la base de datos +│ ├── main.py # Entry Point y dispatcher principal +│ ├── db.py # Gestión de la base de datos SQLite │ ├── config.py # Carga de variables de entorno │ ├── modules/ +│ │ ├── flow_engine.py # Motor de flujos de conversación (lee los JSON) │ │ ├── identity.py # Lógica de Roles y Permisos │ │ ├── llm_engine.py # Cliente OpenAI/Gemini │ │ ├── vikunja.py # API Manager para Tareas @@ -141,7 +145,8 @@ talia_bot_mg/ │ │ ├── printer.py # SMTP/IMAP Loop │ │ └── sales_rag.py # Lógica de Ventas y Servicios │ └── data/ -│ ├── servicios.json # Base de conocimiento +│ ├── flows/ # Directorio con los flujos de conversación en JSON +│ ├── servicios.json # Base de conocimiento para ventas │ ├── credentials.json # Credenciales de Google │ └── users.db # Base de datos de usuarios ├── .env.example # Plantilla de variables de entorno diff --git a/talia_bot/data/flows/admin_block_agenda.json b/talia_bot/data/flows/admin_block_agenda.json new file mode 100644 index 0000000..5598962 --- /dev/null +++ b/talia_bot/data/flows/admin_block_agenda.json @@ -0,0 +1,25 @@ +{ + "id": "admin_block_agenda", + "role": "admin", + "trigger_button": "🛑 Bloquear Agenda", + "steps": [ + { + "step_id": 0, + "variable": "BLOCK_DATE", + "question": "Necesito bloquear la agenda. ¿Para cuándo?", + "options": ["Hoy", "Mañana"] + }, + { + "step_id": 1, + "variable": "BLOCK_TIME", + "question": "Dame el horario exacto que necesitas bloquear (ej. 'de 2pm a 4pm').", + "input_type": "text" + }, + { + "step_id": 2, + "variable": "BLOCK_TITLE", + "question": "Finalmente, dame una breve descripción o motivo del bloqueo.", + "input_type": "text" + } + ] +} diff --git a/talia_bot/data/flows/admin_check_agenda.json b/talia_bot/data/flows/admin_check_agenda.json new file mode 100644 index 0000000..01300c3 --- /dev/null +++ b/talia_bot/data/flows/admin_check_agenda.json @@ -0,0 +1,19 @@ +{ + "id": "admin_check_agenda", + "role": "admin", + "trigger_button": "📅 Revisar Agenda", + "steps": [ + { + "step_id": 0, + "variable": "AGENDA_TIMEFRAME", + "question": "Consultando el oráculo del tiempo... ⏳", + "options": ["📅 Hoy", "🔮 Mañana"] + }, + { + "step_id": 1, + "variable": "AGENDA_ACTION", + "question": "Aquí tienes tu realidad: {CALENDAR_DATA}", + "options": ["✅ Todo bien", "🛑 Bloquear Espacio"] + } + ] +} diff --git a/talia_bot/data/flows/admin_create_nfc_tag.json b/talia_bot/data/flows/admin_create_nfc_tag.json new file mode 100644 index 0000000..56e3d92 --- /dev/null +++ b/talia_bot/data/flows/admin_create_nfc_tag.json @@ -0,0 +1,25 @@ +{ + "id": "admin_create_nfc_tag", + "role": "admin", + "trigger_button": "➕ Crear Tag NFC", + "steps": [ + { + "step_id": 0, + "variable": "NFC_ACTION_TYPE", + "question": "Creemos un nuevo tag NFC. ¿Qué acción quieres que dispare?", + "options": ["Iniciar Flujo", "URL Estática"] + }, + { + "step_id": 1, + "variable": "NFC_FLOW_CHOICE", + "question": "Okay, ¿qué flujo debería iniciar este tag?", + "input_type": "dynamic_keyboard_flows" + }, + { + "step_id": 2, + "variable": "NFC_CONFIRM", + "question": "Perfecto. Cuando acerques tu teléfono a este tag, se iniciará el flujo '{flow_name}'. Aquí tienes los datos para escribir en el tag: {NFC_DATA}", + "options": ["✅ Hecho"] + } + ] +} diff --git a/talia_bot/data/flows/admin_idea_capture.json b/talia_bot/data/flows/admin_idea_capture.json new file mode 100644 index 0000000..bd564d9 --- /dev/null +++ b/talia_bot/data/flows/admin_idea_capture.json @@ -0,0 +1,25 @@ +{ + "id": "admin_idea_capture", + "role": "admin", + "trigger_button": "💡 Capturar Idea", + "steps": [ + { + "step_id": 0, + "variable": "IDEA_CONTENT", + "question": "Te escucho. 💡 Las ideas vuelan...", + "input_type": "text_or_audio" + }, + { + "step_id": 1, + "variable": "IDEA_CATEGORY", + "question": "¿En qué cajón mental guardamos esto?", + "options": ["💰 Negocio", "📹 Contenido", "👤 Personal"] + }, + { + "step_id": 2, + "variable": "IDEA_ACTION", + "question": "¿Cuál es el plan de ataque?", + "options": ["✅ Crear Tarea", "📓 Guardar Nota"] + } + ] +} diff --git a/talia_bot/data/flows/admin_print_file.json b/talia_bot/data/flows/admin_print_file.json new file mode 100644 index 0000000..cdf9a63 --- /dev/null +++ b/talia_bot/data/flows/admin_print_file.json @@ -0,0 +1,13 @@ +{ + "id": "admin_print_file", + "role": "admin", + "trigger_button": "🖨️ Imprimir", + "steps": [ + { + "step_id": 0, + "variable": "UPLOAD_FILE", + "question": "Por favor, envíame el archivo que quieres imprimir.", + "input_type": "document" + } + ] +} diff --git a/talia_bot/data/flows/admin_project_management.json b/talia_bot/data/flows/admin_project_management.json new file mode 100644 index 0000000..63bd775 --- /dev/null +++ b/talia_bot/data/flows/admin_project_management.json @@ -0,0 +1,31 @@ +{ + "id": "admin_project_management", + "role": "admin", + "trigger_button": "🏗️ Ver Proyectos", + "steps": [ + { + "step_id": 0, + "variable": "PROJECT_SELECT", + "question": "Aquí está el tablero de ajedrez...", + "input_type": "dynamic_keyboard_vikunja_projects" + }, + { + "step_id": 1, + "variable": "TASK_SELECT", + "question": "Has seleccionado el proyecto {project_name}. ¿Qué quieres hacer?", + "input_type": "dynamic_keyboard_vikunja_tasks" + }, + { + "step_id": 2, + "variable": "ACTION_TYPE", + "question": "¿Cuál es la jugada?", + "options": ["🔄 Actualizar Estatus", "💬 Agregar Comentario"] + }, + { + "step_id": 3, + "variable": "UPDATE_CONTENT", + "question": "Adelante. Soy todo oídos.", + "input_type": "text_or_audio" + } + ] +} diff --git a/talia_bot/data/flows/client_sales_funnel.json b/talia_bot/data/flows/client_sales_funnel.json new file mode 100644 index 0000000..daaa074 --- /dev/null +++ b/talia_bot/data/flows/client_sales_funnel.json @@ -0,0 +1,25 @@ +{ + "id": "client_sales_funnel", + "role": "client", + "trigger_automatic": true, + "steps": [ + { + "step_id": 0, + "variable": "CLIENT_NAME", + "question": "Hola. Soy Talia, la mano derecha de Armando. ✨Él está ocupado creando, pero yo soy la puerta de entrada. ¿Con quién tengo el gusto?", + "input_type": "text" + }, + { + "step_id": 1, + "variable": "CLIENT_INDUSTRY", + "question": "Mucho gusto, {user_name}. Para entender mejor tus necesidades, ¿cuál es el giro de tu negocio o tu industria?", + "options": ["🍽️ Restaurantes", "🩺 Salud", "🛍️ Retail", "อื่น ๆ"] + }, + { + "step_id": 2, + "variable": "IDEA_PITCH", + "question": "Excelente. Ahora, el escenario es tuyo. 🎤 Cuéntame sobre tu proyecto o la idea que tienes en mente. No te guardes nada. Puedes escribirlo o, si prefieres, enviarme una nota de voz.", + "input_type": "text_or_audio" + } + ] +} diff --git a/talia_bot/data/flows/crew_print_file.json b/talia_bot/data/flows/crew_print_file.json new file mode 100644 index 0000000..ed9d376 --- /dev/null +++ b/talia_bot/data/flows/crew_print_file.json @@ -0,0 +1,13 @@ +{ + "id": "crew_print_file", + "role": "crew", + "trigger_button": "🖨️ Imprimir", + "steps": [ + { + "step_id": 0, + "variable": "UPLOAD_FILE", + "question": "Claro, envíame el archivo que necesitas imprimir y yo me encargo.", + "input_type": "document" + } + ] +} diff --git a/talia_bot/data/flows/crew_request_time.json b/talia_bot/data/flows/crew_request_time.json new file mode 100644 index 0000000..0e2a706 --- /dev/null +++ b/talia_bot/data/flows/crew_request_time.json @@ -0,0 +1,31 @@ +{ + "id": "crew_request_time", + "role": "crew", + "trigger_button": "📅 Solicitar Agenda", + "steps": [ + { + "step_id": 0, + "variable": "REQUEST_TYPE", + "question": "Para usar la agenda del estudio, necesito que seas preciso.", + "options": ["🎥 Grabación", "🎙️ Locución", "🎬 Edición", "🛠️ Mantenimiento"] + }, + { + "step_id": 1, + "variable": "REQUEST_DATE", + "question": "¿Para cuándo necesitas el espacio?", + "options": ["Hoy", "Mañana", "Esta Semana"] + }, + { + "step_id": 2, + "variable": "REQUEST_TIME", + "question": "Dame el horario exacto que necesitas (ej. 'de 10am a 2pm').", + "input_type": "text" + }, + { + "step_id": 3, + "variable": "REQUEST_JUSTIFICATION", + "question": "Entendido. Antes de confirmar, necesito que me expliques brevemente el plan o el motivo para justificar el bloqueo del espacio. Puedes escribirlo o enviarme un audio.", + "input_type": "text_or_audio" + } + ] +} diff --git a/talia_bot/data/flows/crew_secret_onboarding.json b/talia_bot/data/flows/crew_secret_onboarding.json new file mode 100644 index 0000000..b5b39dd --- /dev/null +++ b/talia_bot/data/flows/crew_secret_onboarding.json @@ -0,0 +1,37 @@ +{ + "id": "crew_secret_onboarding", + "role": "crew", + "trigger_command": "/abracadabra", + "steps": [ + { + "step_id": 0, + "variable": "ONBOARD_START", + "question": "Vaya, vaya... Parece que conoces el comando secreto. 🎩. Antes de continuar, necesito saber tu nombre completo.", + "input_type": "text" + }, + { + "step_id": 1, + "variable": "ONBOARD_ORIGIN", + "question": "Un placer, {user_name}. ¿Cuál es tu base de operaciones principal?", + "options": ["🏢 Office", "✨ Aura"] + }, + { + "step_id": 2, + "variable": "ONBOARD_EMAIL", + "question": "Perfecto. Ahora necesito tu correo electrónico de la empresa.", + "input_type": "text" + }, + { + "step_id": 3, + "variable": "ONBOARD_PHONE", + "question": "Y por último, tu número de teléfono.", + "input_type": "text" + }, + { + "step_id": 4, + "variable": "ONBOARD_CONFIRM", + "question": "Gracias. He enviado una notificación al Administrador para que apruebe tu acceso. En cuanto lo haga, tendrás acceso completo. ¡Bienvenido a bordo!", + "options": ["✅ Entendido"] + } + ] +} diff --git a/talia_bot/data/services.json b/talia_bot/data/services.json new file mode 100644 index 0000000..bfe4943 --- /dev/null +++ b/talia_bot/data/services.json @@ -0,0 +1,22 @@ +[ + { + "service_name": "Web Development for Restaurants", + "description": "Custom websites and online ordering systems for restaurants, helping you reach more customers and streamline your operations.", + "keywords": ["restaurant", "food", "online ordering", "website", "restaurantes", "comida"] + }, + { + "service_name": "Patient Management Systems for Healthcare", + "description": "A secure and efficient software solution for managing patient records, appointments, and billing in medical clinics.", + "keywords": ["healthcare", "medical", "patient", "clinic", "salud", "médico", "pacientes"] + }, + { + "service_name": "Content Creation & Social Media Strategy", + "description": "Engaging content packages and social media management to build your brand's online presence and connect with your audience.", + "keywords": ["content creation", "social media", "marketing", "branding", "contenido", "redes sociales"] + }, + { + "service_name": "General Business Consulting", + "description": "Strategic consulting to help you optimize business processes, identify growth opportunities, and improve overall performance.", + "keywords": ["business", "consulting", "strategy", "growth", "negocio", "consultoría"] + } +] diff --git a/talia_bot/db.py b/talia_bot/db.py index acbd19c..a4fe64c 100644 --- a/talia_bot/db.py +++ b/talia_bot/db.py @@ -32,18 +32,8 @@ def setup_database(): ) """) - # Create the conversations table for the flow engine - cursor.execute(""" - CREATE TABLE IF NOT EXISTS conversations ( - user_id INTEGER PRIMARY KEY, - flow_id TEXT NOT NULL, - current_step_id INTEGER NOT NULL, - collected_data TEXT - ) - """) - conn.commit() - logger.info("Database setup complete. 'users' and 'conversations' tables are ready.") + logger.info("Database setup complete. 'users' table is ready.") except sqlite3.Error as e: logger.error(f"Database error during setup: {e}") finally: diff --git a/talia_bot/main.py b/talia_bot/main.py index f583a54..8a453aa 100644 --- a/talia_bot/main.py +++ b/talia_bot/main.py @@ -17,7 +17,6 @@ from telegram.ext import ( # Importamos las configuraciones y herramientas que creamos en otros archivos from talia_bot.config import TELEGRAM_BOT_TOKEN from talia_bot.modules.identity import get_user_role -from talia_bot.modules.flow_engine import FlowEngine from talia_bot.modules.onboarding import handle_start as onboarding_handle_start from talia_bot.modules.onboarding import get_admin_secondary_menu from talia_bot.modules.agenda import get_agenda @@ -63,56 +62,14 @@ async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: # Respondemos al usuario await update.message.reply_text(response_text, reply_markup=reply_markup) -flow_engine = FlowEngine() - -async def universal_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: - """ - Handles all user interactions (text, callbacks, voice, documents). - Routes them to the flow engine or legacy handlers. - """ - user_id = update.effective_user.id - conversation_state = flow_engine.get_conversation_state(user_id) - - if conversation_state: - # User is in an active flow, so we process the response. - response_text = update.message.text if update.message else None - result = flow_engine.handle_response(user_id, response_text) - - if result['status'] == 'in_progress': - await update.message.reply_text(result['step']['message']) - elif result['status'] == 'complete': - summary = "\n".join([f"- {key}: {value}" for key, value in result['data'].items()]) - await update.message.reply_text(f"Flow '{result['flow_id']}' completado.\n\nResumen:\n{summary}") - else: - await update.message.reply_text(result.get('message', 'Ocurrió un error.')) - else: - # No active flow, check for a callback query to start a new flow or use legacy dispatcher. - if update.callback_query: - query = update.callback_query - await query.answer() - flow_to_start = query.data - - # Check if the callback is intended to start a known flow. - if flow_engine.get_flow(flow_to_start): - initial_step = flow_engine.start_flow(user_id, flow_to_start) - if initial_step: - await query.message.reply_text(initial_step['message']) - else: - # Fallback to the old button dispatcher for legacy actions. - await button_dispatcher(update, context) - elif update.message and update.message.text: - # Handle regular text messages that are not part of a flow (e.g., commands). - # For now, we just ignore them if they are not commands. - logger.info(f"Received non-flow text message from {user_id}: {update.message.text}") - - async def button_dispatcher(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """ - Legacy handler for menu button clicks that are not part of a flow. + Esta función maneja los clics en los botones del menú. + Dependiendo de qué botón se presione, ejecuta una acción diferente. """ query = update.callback_query - # No need to answer here as it's answered in the universal_handler - logger.info(f"El despachador legacy recibió una consulta: {query.data}") + await query.answer() + logger.info(f"El despachador recibió una consulta: {query.data}") response_text = "Acción no reconocida." reply_markup = None @@ -134,23 +91,27 @@ async def button_dispatcher(update: Update, context: ContextTypes.DEFAULT_TYPE) try: if query.data in simple_handlers: handler = simple_handlers[query.data] + logger.info(f"Ejecutando simple_handler para: {query.data}") if asyncio.iscoroutinefunction(handler): response_text = await handler() else: response_text = handler() elif query.data in complex_handlers: handler = complex_handlers[query.data] + logger.info(f"Ejecutando complex_handler para: {query.data}") if asyncio.iscoroutinefunction(handler): response_text, reply_markup = await handler() else: response_text, reply_markup = handler() elif query.data.startswith(('approve:', 'reject:')): + logger.info(f"Ejecutando acción de aprobación: {query.data}") response_text = handle_approval_action(query.data) elif query.data == 'start_create_tag': response_text = "Para crear un tag, por favor usa el comando /create_tag." else: logger.warning(f"Consulta no manejada por el despachador: {query.data}") - + await query.edit_message_text(text=response_text) + return except Exception as exc: logger.exception(f"Error al procesar la acción {query.data}: {exc}") response_text = "❌ Ocurrió un error al procesar tu solicitud. Intenta de nuevo." @@ -158,7 +119,6 @@ async def button_dispatcher(update: Update, context: ContextTypes.DEFAULT_TYPE) await query.edit_message_text(text=response_text, reply_markup=reply_markup, parse_mode='Markdown') - def main() -> None: """Función principal que arranca el bot.""" if not TELEGRAM_BOT_TOKEN: @@ -170,9 +130,10 @@ def main() -> None: application = Application.builder().token(TELEGRAM_BOT_TOKEN).build() schedule_daily_summary(application) - # Legacy ConversationHandlers + # El orden de los handlers es crucial para que las conversaciones funcionen. application.add_handler(create_tag_conv_handler()) application.add_handler(vikunja_conv_handler()) + conv_handler = ConversationHandler( entry_points=[CallbackQueryHandler(propose_activity_start, pattern='^propose_activity$')], states={ @@ -184,13 +145,10 @@ def main() -> None: ) application.add_handler(conv_handler) - # Command Handlers application.add_handler(CommandHandler("start", start)) application.add_handler(CommandHandler("print", print_handler)) - # Universal Handler for flows and callbacks - application.add_handler(CallbackQueryHandler(universal_handler)) - application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, universal_handler)) + application.add_handler(CallbackQueryHandler(button_dispatcher)) logger.info("Iniciando Talía Bot...") application.run_polling()