From 0c2c9fc524a4e2117ad2c1650586a88357c435a6 Mon Sep 17 00:00:00 2001 From: Marco Gallegos Date: Sun, 21 Dec 2025 11:42:08 -0600 Subject: [PATCH] feat: Implement multi-step conversation flows with dynamic UI, persistent state, and improved path handling. --- .gitignore | 1 + .../data/flows/admin_project_management.json | 2 +- talia_bot/data/users.db | Bin 16384 -> 20480 bytes talia_bot/db.py | 14 +- talia_bot/debug.md | 70 ++++++++++ talia_bot/main.py | 124 +++++++++++++++--- talia_bot/modules/equipo.py | 1 + talia_bot/modules/flow_engine.py | 5 +- talia_bot/modules/vikunja.py | 24 ++++ talia_bot/start_bot.sh | 11 ++ 10 files changed, 231 insertions(+), 21 deletions(-) create mode 100644 talia_bot/debug.md create mode 100755 talia_bot/start_bot.sh diff --git a/.gitignore b/.gitignore index 682258b..0f08cc3 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ __pycache__/ *.py[cod] *$py.class + # C extensions *.so diff --git a/talia_bot/data/flows/admin_project_management.json b/talia_bot/data/flows/admin_project_management.json index 63bd775..ed52d12 100644 --- a/talia_bot/data/flows/admin_project_management.json +++ b/talia_bot/data/flows/admin_project_management.json @@ -1,7 +1,7 @@ { "id": "admin_project_management", "role": "admin", - "trigger_button": "🏗️ Ver Proyectos", + "trigger_button": "manage_vikunja", "steps": [ { "step_id": 0, diff --git a/talia_bot/data/users.db b/talia_bot/data/users.db index 9f8d355391cafa8022413608d8d9a403a15332cb..cfbe3d608f037f1a7f3608ece46fcb78efb723d3 100644 GIT binary patch delta 275 zcmZo@U~E{xI6+#FiGhKE6^LPgX`+s?C=-KTmk2Na4+a)qJqEry{P^5ylYordcVnMLJw4D5Mkm?ZEh>*#={L%yvku=C!$k%H04w%Mx&QzG delta 59 zcmZozz}V2hI6+#Fk%57M1&CpQaiWf~Fe8IrStl?54+dsF9R|KS{= 2: + keyboard.append(row) + row = [] + if row: + keyboard.append(row) + reply_markup = InlineKeyboardMarkup(keyboard) + + if update.callback_query: + await update.callback_query.edit_message_text(text=text, reply_markup=reply_markup) + else: + await update.message.reply_text(text=text, reply_markup=reply_markup) + async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """ Se ejecuta cuando el usuario escribe /start. @@ -73,7 +120,7 @@ async def text_and_voice_handler(update: Update, context: ContextTypes.DEFAULT_T 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) + # await start(update, context) # Changed behavior: Don't auto-start, might be annoying return user_response = update.message.text @@ -85,7 +132,7 @@ async def text_and_voice_handler(update: Update, context: ContextTypes.DEFAULT_T result = flow_engine.handle_response(user_id, user_response) if result["status"] == "in_progress": - await update.message.reply_text(result["step"]["question"]) + await send_step_message(update, result["step"]) elif result["status"] == "complete": if "sales_pitch" in result: await update.message.reply_text(result["sales_pitch"]) @@ -124,7 +171,17 @@ async def check_print_status_command(update: Update, context: ContextTypes.DEFAU await update.message.reply_text(response) +async def reset_conversation(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Resets the conversation state for the user.""" + user_id = update.effective_user.id + flow_engine = context.bot_data["flow_engine"] + flow_engine.end_flow(user_id) + await update.message.reply_text("🔄 Conversación reiniciada. Puedes empezar de nuevo.") + logger.info(f"User {user_id} reset their conversation.") + + async def button_dispatcher(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + print("--- BUTTON DISPATCHER CALLED ---") """ Esta función maneja los clics en los botones del menú. Dependiendo de qué botón se presione, ejecuta una acción diferente. @@ -169,24 +226,51 @@ async def button_dispatcher(update: Update, context: ContextTypes.DEFAULT_TYPE) logger.info(f"Ejecutando acción de aprobación: {query.data}") response_text = handle_approval_action(query.data) else: + # 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: + logger.info(f"Iniciando flujo: {flow_to_start['id']}") + initial_step = flow_engine.start_flow(update.effective_user.id, flow_to_start["id"]) + if initial_step: + await send_step_message(update, initial_step) + else: + logger.error("No se pudo iniciar el flujo (paso inicial vacío).") + return + + # Check if the user is in a flow and clicked an option + state = flow_engine.get_conversation_state(update.effective_user.id) + if state: + logger.info(f"Procesando paso de flujo para usuario {update.effective_user.id}. Data: {query.data}") + result = flow_engine.handle_response(update.effective_user.id, query.data) + + if result["status"] == "in_progress": + logger.info("Flujo en progreso, enviando siguiente paso.") + await send_step_message(update, result["step"]) + elif result["status"] == "complete": + logger.info("Flujo completado.") + if "sales_pitch" in result: + await query.edit_message_text(result["sales_pitch"]) + elif "nfc_tag" in result: + await query.edit_message_text(result["nfc_tag"], parse_mode='Markdown') + else: + await query.edit_message_text("Gracias por completar el flujo.") + elif result["status"] == "error": + logger.error(f"Error en el flujo: {result['message']}") + await query.edit_message_text(f"Error: {result['message']}") + return + logger.warning(f"Consulta no manejada por el despachador: {query.data}") + # Only update text if no flow was started 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." 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: @@ -205,9 +289,7 @@ def main() -> None: schedule_daily_summary(application) - # El orden de los handlers es crucial para que las conversaciones funcionen. - application.add_handler(vikunja_conv_handler()) - + # Conversation handler for proposing activities conv_handler = ConversationHandler( entry_points=[CallbackQueryHandler(propose_activity_start, pattern='^propose_activity$')], states={ @@ -218,8 +300,12 @@ def main() -> None: per_message=False ) application.add_handler(conv_handler) + + # El orden de los handlers es crucial para que las conversaciones funcionen. + # application.add_handler(vikunja_conv_handler()) application.add_handler(CommandHandler("start", start)) + application.add_handler(CommandHandler("reset", reset_conversation)) # Added reset command application.add_handler(CommandHandler("print", print_handler)) application.add_handler(CommandHandler("check_print_status", check_print_status_command)) @@ -229,8 +315,10 @@ def main() -> None: application.add_handler(CallbackQueryHandler(button_dispatcher)) + application.add_handler(TypeHandler(object, catch_all_handler)) + logger.info("Iniciando Talía Bot...") application.run_polling() if __name__ == "__main__": - main() + main() \ No newline at end of file diff --git a/talia_bot/modules/equipo.py b/talia_bot/modules/equipo.py index 7efbc10..0b67ab8 100644 --- a/talia_bot/modules/equipo.py +++ b/talia_bot/modules/equipo.py @@ -9,6 +9,7 @@ from telegram.ext import ContextTypes, ConversationHandler DESCRIPTION, DURATION = range(2) async def propose_activity_start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: + print("--- PROPOSE ACTIVITY START CALLED ---") """ Inicia el proceso para que un miembro del equipo proponga una actividad. Se activa cuando se pulsa el botón correspondiente. diff --git a/talia_bot/modules/flow_engine.py b/talia_bot/modules/flow_engine.py index 5727d3b..190df5a 100644 --- a/talia_bot/modules/flow_engine.py +++ b/talia_bot/modules/flow_engine.py @@ -14,7 +14,10 @@ class FlowEngine: def _load_flows(self): """Loads all individual flow JSON files from the flows directory.""" - flows_dir = 'talia_bot/data/flows' + # flows_dir = 'talia_bot/data/flows' # OLD + base_dir = os.path.dirname(os.path.abspath(__file__)) + flows_dir = os.path.join(base_dir, '..', 'data', 'flows') + loaded_flows = [] try: if not os.path.exists(flows_dir): diff --git a/talia_bot/modules/vikunja.py b/talia_bot/modules/vikunja.py index 20e107e..6081be4 100644 --- a/talia_bot/modules/vikunja.py +++ b/talia_bot/modules/vikunja.py @@ -29,6 +29,30 @@ def get_vikunja_headers(): "Content-Type": "application/json", } +def get_projects_list(): + """Returns a list of projects from Vikunja.""" + if not VIKUNJA_API_TOKEN: + return [] + try: + response = requests.get(f"{VIKUNJA_API_URL}/projects", headers=get_vikunja_headers()) + response.raise_for_status() + return response.json() + except Exception as e: + logger.error(f"Error fetching projects: {e}") + return [] + +def get_tasks_list(project_id=1): + """Returns a list of tasks for a project.""" + if not VIKUNJA_API_TOKEN: + return [] + try: + response = requests.get(f"{VIKUNJA_API_URL}/projects/{project_id}/tasks", headers=get_vikunja_headers()) + response.raise_for_status() + return response.json() + except Exception as e: + logger.error(f"Error fetching tasks: {e}") + return [] + def get_tasks(): """ Obtiene y formatea la lista de tareas de Vikunja. diff --git a/talia_bot/start_bot.sh b/talia_bot/start_bot.sh new file mode 100755 index 0000000..cbf80c5 --- /dev/null +++ b/talia_bot/start_bot.sh @@ -0,0 +1,11 @@ +#!/bin/bash +# Script to start the Talia Bot with correct PYTHONPATH + +# Get the directory of the script +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +# Set PYTHONPATH to include the parent directory +export PYTHONPATH="$PYTHONPATH:$DIR/.." + +# Run the bot +python3 "$DIR/main.py"