Escrito por Rodolfo Ferro en Planeta Chatbot.
Recientemente me uní a los Code Challenges de PyBites y construí a Disaster Attention Bot (DisAtBot), un chatbot que ayude a personas afectadas por desastres naturales. En este artículo muestro cómo construí el bot con Telegram y (por supuesto) Python.
“¿Quién convocó a tanto muchacho, de dónde salió tanto voluntario, cómo fue que la sangre sobró en los hospitales, quién organizó las brigadas que dirigieron el tránsito de vehículos y de peatones por toda la zona afectada? No hubo ninguna convocatoria, no se hizo ningún llamado y todos acudieron”
“El jueves negro que cambió a México”
– Emilio Viale, 1985.
Tabla de contenidos
Un poco de contexto…
Desde el 19 de septiembre de 2017, México ha sido sacudido por algunos terremotos (The Guardian, CNN). Esto hizo que me preguntara cómo podría mejorar el manejo de reportes en zonas de desastre, gente enterrada bajo escombros de edificios, gente herida en necesidad de atención médica y otras situaciones.
Verificado 19s fue una solución inmediata para dar seguimiento a reportes vía social media y para visualizar la info en un mapa online. Esto requirió muchísimo trabajo en tiempo real (24/7) de monitoreo de posts en redes sociales, de gente que se encontraba situada en las áreas afectadas. Y esos datos se actualizaban cada ~10 minutos.
Fue entonces que comencé a pensar en la manera de optimizar este proceso para futuras situaciones, no sólo en terremotos, sino situaciones de desastre en general. Esto me incentivó e trabajar en este bot para el Code Challenge 43 — Build a Chatbot Using Python de PyBytes.
Así nació DisAtBot
DisAtBot automatiza el proceso de reportar incidentes a través de plataformas de mensajería, como Telegram, Facebook Messenger, Twitter, etc. En este punto sólo cuenta con soporte para Telegram, pero espero expandirlo a otras plataformas de social media. Si te gustaría contribuir, ve la sección de contribución al final.
Puedes encontrar a DisAtBot en:
- Telegram: https://t.me/DisAtBot
- El repo oficial: https://github.com/RodolfoFerro/DisAtBot
La idea fue tener un flujo simple que permitiera a reportes de desastres ser rápidos y sencillos. El proceso general de DisAtBot es como sigue:
La idea es que cualquier usuario pueda interactuar con el bot seleccionando opciones de menús de botones en la conversación. Esto grandiosamente acelera el proceso de reporte de incidentes.
El siguiente paso sería abrir un ticket que se almacene en una base de datos, para la instancia gubernamental correspondiente/organización pública/ONG/etc. que sea quien valide y envíe asistencia. Cuando no se requiera más ayuda, o la situación está bajo control, el ticket se cerraría.
Configuración
Primero clona el repo. Yo usé Python 3.6 y los siguientes paquetes:
Para instalar las dependencias crea un entorno virtual (virtual env) y corre:
pip install -r requirements.txt
Luego ingresa en la carpeta de scripts y corre el bot como sigue:
python DisAtBot.py
Update: Actualmente el bot se encuentra activo para Telegram en servidores de PyBites. (¡Gracias PyBites!)
Diseño
El foco de la versión inicial fue la creación de menús de botones para una interacción sencilla con el usuario. El segundo –y principal– problema enfrentado fue el gestor de conversaciones (conversation handler). Una máquina finita de estados fue necesitada para preservar el flujo deseado y las respuestas para cada estado.
No voy a profundizar mucho en la explicación, pero el código debajo va a mostrar cómo me enfrenté a esto.
Lo primero de todo, la biblioteca de funciones de Telegram cuenta con algunos métodos para crear menús de botones para respuestas de usuarios durante el flujo de conversación. La idea es crear un Keyboard Markup para manejar las respuestas a través de botones. Ello puede ser Inline (los botones van a aparecer en la ventana de conversación) o como un Reply Keyboard (los botones serán desplegados debajo de la caja de texto para escribir mensajes).
Un ejemplo puede ser visto en la función de menú:
def menu(bot, update): """ Main menu function. This will display the options from the main menu. """ # Create buttons to select language: keyboard = [[send_report[LANG], view_map[LANG]], [view_faq[LANG], view_about[LANG]]] reply_markup = ReplyKeyboardMarkup(keyboard, one_time_keyboard=True, resize_keyboard=True) user = update.message.from_user logger.info("Menu command requested by {}.".format(user.first_name)) update.message.reply_text(main_menu[LANG], reply_markup=reply_markup) return SET_STAT
Como puede apreciarse, la variable keyboard
es una lista que contiene los cuatro botones a ser desplegados. La disposición de los botones se puede configurar anidando listas. En este caso los botones Report y Map se encuentran en la primera fila, mientras que los botones FAQ y About en la segunda. Esto se ve así:
Continuando con el código, se necesita unReplyMarkup
para manejar las respuestas y despliegue de los botones. Esto especifica el diseño del menú: si sólo un menú es desplegado, si necesita ser re-escalado, etc.
Para el bot utilizamos un logger (un registro), y la función update.message.reply(...)
es usada para actualizar el texto de acuerdo a la respuesta del usuario. La variable SET_STAT
retornada en esta función es un entero (predefinido) que devuelve el estado en ese momento, para poder continuar con el flujo principal.
Ahora entendemos la creación del menú y su manejo. La razón de usar botones es que queremos una interacción rápida porque el bot será usado en una situación de emergencia.
El gestor de conversaciones –ConversationHandler
– se hace cargo de configurar el estado o paso del flujo en el que nos encontremos en ese momento, máquina de estados finita que mencioné antes. Notemos que cada estado además necesita su propio gestor para manejar su respectiva información (respuestas de botones, etc.).
Este código muestra el conversation handler que creé:
def main(): """ Main function. This function handles the conversation flow by setting states on each step of the flow. Each state has its own handler for the interaction with the user. """ global LANG # Create the EventHandler and pass it your bot's token. updater = Updater(telegram_token) # Get the dispatcher to register handlers: dp = updater.dispatcher # Add conversation handler with predefined states: conv_handler = ConversationHandler( entry_points=[CommandHandler('start', start)], states={ SET_LANG: [RegexHandler('^(ES|EN)$', set_lang)], MENU: [CommandHandler('menu', menu)], SET_STAT: [RegexHandler( '^({}|{}|{}|{})$'.format( send_report['ES'], view_map['ES'], view_faq['ES'], view_about['ES']), set_state), RegexHandler( '^({}|{}|{}|{})$'.format( send_report['EN'], view_map['EN'], view_faq['EN'], view_about['EN']), set_state)], LOCATION: [MessageHandler(Filters.location, location), CommandHandler('menu', menu)] }, fallbacks=[CommandHandler('cancel', cancel), CommandHandler('help', help)] ) dp.add_handler(conv_handler) # Log all errors: dp.add_error_handler(error) # Start DisAtBot: updater.start_polling() # Run the bot until the user presses Ctrl-C or the process # receives SIGINT, SIGTERM or SIGABRT: updater.idle()
Puede parecer un poco confuso al inicio, pero básicamente se compone de lo siguiente:
- El gestor de la conversación (conversation handler) contiene los estados del flujo.
- También tiene los puntos de entrada (como la función
start
), y los fallbacks (como las funcionescancel
yhelp
). - También contiene funciones para manejar errores (error handlers).
- Una variable global
LANG
es usada, puesto que para esta implementación — olvidé mencionarlo–– ¡soporta interacción en inglés y español! - Para poder soportar esto, creé diccionarios para cada interacción con el bot en ambos idiomas.
Si quieres revisar el código completo del bot, checa el folder de scripts donde vas a encontrar el script principal y los diccionarios de idiomas.
Algunas otras características implementadas son el manejo de geolocalización y secciones About
/ FAQ
. Pero la mejor manera de conocer sobre este proyecto es verlo en acción (para una demo en tiempo real ve al minuto 8:30):
Trabajo a futuro
Para futuro desarrollo estoy pensando en añadir un mapa. El sistema actualmente crea un archivo GeoJSON de las ubicaciones adquiridas.
Como mencioné, estoy considerando expandirlo a otras plataformas como Facebook Messenger y Twitter. Otra cosa a añadir al proyecto sería un sitio web donde se expliquen los casos de uso principales del bot, ¿quizá un sitio tipo wiki?
Si tienes más ideas o sugerencias, siéntete libre de contactarme o:
Contribuye
Si estás interesado en contribuir a este proyecto, siéntete libre de echar un vistazo al archivo CONTRIBUTING del repo. Me sentiré muy complacido si este proyecto crece a algo que se use en la vida real para aliviar las dramáticas consecuencias de desastres naturales, las cuales siempre parecen golpear cuando son menos esperadas.
Me gustaría enormemente agradecer a Bob y Julian de PyBites por el reto y el premio, porque además, esto permitió hacerme acreedor del libro “Designing Bots: Creating Conversational Experiences” (O’Reilly Media, 2017) por Amir Shevat.
Si quisieras leer este post en inglés, puedes hacerlo aquí.
Keep Calm and Code in Python!
– Rod