mirror of
https://github.com/marcogll/talia_bot.git
synced 2026-01-13 21:35:19 +00:00
feat: Implement JSON-based conversational flow engine
This commit introduces a new `FlowEngine` to manage conversational flows based on JSON definitions. Key changes: - Created `talia_bot/modules/flow_engine.py` to handle the logic of parsing and executing flows. - Added a `conversations` table to the database to persist user state during flows. - Created the `talia_bot/data/flows` directory and added a sample `create_project.json` flow. - Integrated the `FlowEngine` into `main.py` with a `universal_handler` that routes user input to the engine or to legacy handlers.
This commit is contained in:
@@ -32,8 +32,18 @@ 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()
|
conn.commit()
|
||||||
logger.info("Database setup complete. 'users' table is ready.")
|
logger.info("Database setup complete. 'users' and 'conversations' tables are ready.")
|
||||||
except sqlite3.Error as e:
|
except sqlite3.Error as e:
|
||||||
logger.error(f"Database error during setup: {e}")
|
logger.error(f"Database error during setup: {e}")
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ from telegram.ext import (
|
|||||||
# Importamos las configuraciones y herramientas que creamos en otros archivos
|
# Importamos las configuraciones y herramientas que creamos en otros archivos
|
||||||
from talia_bot.config import TELEGRAM_BOT_TOKEN
|
from talia_bot.config import TELEGRAM_BOT_TOKEN
|
||||||
from talia_bot.modules.identity import get_user_role
|
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 handle_start as onboarding_handle_start
|
||||||
from talia_bot.modules.onboarding import get_admin_secondary_menu
|
from talia_bot.modules.onboarding import get_admin_secondary_menu
|
||||||
from talia_bot.modules.agenda import get_agenda
|
from talia_bot.modules.agenda import get_agenda
|
||||||
@@ -62,14 +63,56 @@ 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)
|
||||||
|
|
||||||
|
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:
|
async def button_dispatcher(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
"""
|
"""
|
||||||
Esta función maneja los clics en los botones del menú.
|
Legacy handler for menu button clicks that are not part of a flow.
|
||||||
Dependiendo de qué botón se presione, ejecuta una acción diferente.
|
|
||||||
"""
|
"""
|
||||||
query = update.callback_query
|
query = update.callback_query
|
||||||
await query.answer()
|
# No need to answer here as it's answered in the universal_handler
|
||||||
logger.info(f"El despachador recibió una consulta: {query.data}")
|
logger.info(f"El despachador legacy recibió una consulta: {query.data}")
|
||||||
|
|
||||||
response_text = "Acción no reconocida."
|
response_text = "Acción no reconocida."
|
||||||
reply_markup = None
|
reply_markup = None
|
||||||
@@ -91,27 +134,23 @@ async def button_dispatcher(update: Update, context: ContextTypes.DEFAULT_TYPE)
|
|||||||
try:
|
try:
|
||||||
if query.data in simple_handlers:
|
if query.data in simple_handlers:
|
||||||
handler = simple_handlers[query.data]
|
handler = simple_handlers[query.data]
|
||||||
logger.info(f"Ejecutando simple_handler para: {query.data}")
|
|
||||||
if asyncio.iscoroutinefunction(handler):
|
if asyncio.iscoroutinefunction(handler):
|
||||||
response_text = await handler()
|
response_text = await handler()
|
||||||
else:
|
else:
|
||||||
response_text = handler()
|
response_text = handler()
|
||||||
elif query.data in complex_handlers:
|
elif query.data in complex_handlers:
|
||||||
handler = complex_handlers[query.data]
|
handler = complex_handlers[query.data]
|
||||||
logger.info(f"Ejecutando complex_handler para: {query.data}")
|
|
||||||
if asyncio.iscoroutinefunction(handler):
|
if asyncio.iscoroutinefunction(handler):
|
||||||
response_text, reply_markup = await handler()
|
response_text, reply_markup = await handler()
|
||||||
else:
|
else:
|
||||||
response_text, reply_markup = handler()
|
response_text, reply_markup = handler()
|
||||||
elif query.data.startswith(('approve:', 'reject:')):
|
elif query.data.startswith(('approve:', 'reject:')):
|
||||||
logger.info(f"Ejecutando acción de aprobación: {query.data}")
|
|
||||||
response_text = handle_approval_action(query.data)
|
response_text = handle_approval_action(query.data)
|
||||||
elif query.data == 'start_create_tag':
|
elif query.data == 'start_create_tag':
|
||||||
response_text = "Para crear un tag, por favor usa el comando /create_tag."
|
response_text = "Para crear un tag, por favor usa el comando /create_tag."
|
||||||
else:
|
else:
|
||||||
logger.warning(f"Consulta no manejada por el despachador: {query.data}")
|
logger.warning(f"Consulta no manejada por el despachador: {query.data}")
|
||||||
await query.edit_message_text(text=response_text)
|
|
||||||
return
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.exception(f"Error al procesar la acción {query.data}: {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."
|
response_text = "❌ Ocurrió un error al procesar tu solicitud. Intenta de nuevo."
|
||||||
@@ -119,6 +158,7 @@ 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')
|
await query.edit_message_text(text=response_text, reply_markup=reply_markup, parse_mode='Markdown')
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
"""Función principal que arranca el bot."""
|
"""Función principal que arranca el bot."""
|
||||||
if not TELEGRAM_BOT_TOKEN:
|
if not TELEGRAM_BOT_TOKEN:
|
||||||
@@ -130,10 +170,9 @@ def main() -> None:
|
|||||||
application = Application.builder().token(TELEGRAM_BOT_TOKEN).build()
|
application = Application.builder().token(TELEGRAM_BOT_TOKEN).build()
|
||||||
schedule_daily_summary(application)
|
schedule_daily_summary(application)
|
||||||
|
|
||||||
# El orden de los handlers es crucial para que las conversaciones funcionen.
|
# Legacy ConversationHandlers
|
||||||
application.add_handler(create_tag_conv_handler())
|
application.add_handler(create_tag_conv_handler())
|
||||||
application.add_handler(vikunja_conv_handler())
|
application.add_handler(vikunja_conv_handler())
|
||||||
|
|
||||||
conv_handler = ConversationHandler(
|
conv_handler = ConversationHandler(
|
||||||
entry_points=[CallbackQueryHandler(propose_activity_start, pattern='^propose_activity$')],
|
entry_points=[CallbackQueryHandler(propose_activity_start, pattern='^propose_activity$')],
|
||||||
states={
|
states={
|
||||||
@@ -145,10 +184,13 @@ def main() -> None:
|
|||||||
)
|
)
|
||||||
application.add_handler(conv_handler)
|
application.add_handler(conv_handler)
|
||||||
|
|
||||||
|
# Command Handlers
|
||||||
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(CallbackQueryHandler(button_dispatcher))
|
# Universal Handler for flows and callbacks
|
||||||
|
application.add_handler(CallbackQueryHandler(universal_handler))
|
||||||
|
application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, universal_handler))
|
||||||
|
|
||||||
logger.info("Iniciando Talía Bot...")
|
logger.info("Iniciando Talía Bot...")
|
||||||
application.run_polling()
|
application.run_polling()
|
||||||
|
|||||||
Reference in New Issue
Block a user