Enrutamiento en chatbots complejos con el marco Hobot



Después de haber comenzado a desarrollar bots para Telegram hace varios años, descubrí el rendimiento, la simplicidad y la flexibilidad de trabajar con ellos como un caso especial de la interfaz de línea de comandos. Estas características, disponibles para muchos hoy en día, se deben en gran parte al popular marco telegraf.js y similares, que proporcionan métodos simplificados para trabajar con la API de Telegram.

Al mismo tiempo, la arquitectura del proyecto recae completamente en los hombros del desarrollador, y a juzgar por la modesta cantidad de bots complejos y multifuncionales, todavía tenemos espacio para crecer en este sentido.

En este artículo quiero hablar sobre un pequeño marco de enrutamiento de chatbot, sin el cual el desarrollo de nuestro proyecto sería imposible.

Alguna información básica


En chatbots y CLI, la ejecución de una sola acción lógica a menudo consiste en varios pasos de refinamiento o pasos de ramificación. Esto requiere que el programa almacene una determinada coordenada para recordar en qué parte del flujo se encuentra el usuario y ejecutar su comando de acuerdo con esta coordenada.

La ilustración más simple es la ejecución del comando npm init , durante el cual el programa le pide que especifique uno u otro dato para package.json a su vez.

En el primer paso, le dice al usuario que está esperando la entrada de texto del nombre del paquete, y lo que el usuario le envía con el siguiente comando se guardará como el nombre del paquete gracias a la variable en la que se escribe esta espera.

Llamamos a esta ruta variable: la ruta a la que este o aquel código está lógicamente adjunto. Siempre es necesario si el bot tiene navegación o comandos emitidos en unos pocos pasos.

La práctica de hoy en arquitectura bot


El enfoque que vi por primera vez por otros desarrolladores se veía así: para cualquier actualización que provenga del usuario, se escribe una lista de verificaciones para un valor particular de la variable de ruta y se coloca lógica de negocios y navegación adicional dentro de estas verificaciones en la forma más elemental:

onUserInput(ctx, input) {
    switch(ctx.session.path) {
        case 'firstPath':
	    if (input === '!') {
               // -  
	        ctx.reply('!');
	        ctx.session.path = 'secondPath';
	    } else {
	        ctx.reply(' "!"');
	    }
	    break;
        case '...':
       	    //   
    }
}

Si solo tiene un par de equipos y un par de pasos para cada equipo, esta solución es óptima. Llegar al tercer equipo y al séptimo si comienzas a pensar que algo va mal.

En uno de los bots con los que tuvimos la oportunidad de trabajar en una etapa tardía, el núcleo de lo funcional era una hoja de 4000 líneas y ~ 70 condiciones de solo el nivel superior que surgió de dos ifs, con un control de lo que se tomó en serio, a veces rutas, a veces comandos, a veces caminos y comandos. Se verificaron todas estas condiciones para cada acción del usuario y se accedió a funciones auxiliares desde un objeto de hoja adyacente, que también creció a partir de varias líneas. No hace falta decir, ¿qué tan lento y tedioso iba este proyecto?

Marco Hobot


Comenzando ActualizeBot, ya imaginamos cuán grande sería, y nuestra primera tarea fue preservar la extensibilidad y la velocidad del desarrollo.

Para hacer esto, dividimos la lógica del cliente en controladores asignados a las rutas y escribimos una pequeña abstracción para navegar entre estos controladores y procesar los mensajes recibidos del usuario en ellos.

Todo esto, basado en grandes proyectos, fue escrito en TypeScript y recibió el elegante nombre de Hobot, aludiendo, por supuesto, a las tuberías de navegación.

Un controlador es un objeto simple de tres propiedades:

  • ruta : identificador de cadena de la ruta utilizada para inicializar y navegar por las rutas inicializadas
  • get — , , hobot.gotoPath(ctx, path, data?). — data ,
  • post — . , , . updateType — , : text, callback_query . updateType ctx

Ejemplo de controlador:

const anotherController = {
    path: 'firstPath',
    get: async (ctx, data) => 
        await ctx.reply('Welcome to this path! Say "Hi"'),
    post: async (ctx, updateType) => {
        //     : text / callback_query / etc...
        if (updateType === updateTypes.text && ctx.update.message.text === 'Hi') {
            await ctx.reply("Thank you!");
            // hobot       this:
            this.hobot.gotoPath(ctx, 'secondPath', { userJustSaid: "Hi" });
        } else {
            //     ,       
            await ctx.reply('We expect "Hi" text message here');
        }
    }
}

Parece un poco más complicado que al principio, pero la dificultad seguirá siendo la misma cuando tengas 100 o 200 caminos.

La lógica interna es trivial y es sorprendente que nadie haya hecho esto todavía :
estos controladores se agregan a un objeto cuyas claves son los valores de la propiedad de ruta y estas teclas los invocan durante las acciones del usuario o al navegar usando hobot.gotoPath (ctx, ruta, datos? ) .

La navegación se asigna en un método separado para no tocar la ruta variable y la lógica de navegación, sino para pensar solo en la lógica de negocios, aunque siempre puede cambiar ctx.session.path con las manos, lo que, por supuesto, no se recomienda.

Todo lo que necesita hacer para que su nuevo barco con una estructura indestructible funcione es lanzar un bot de telegrafo regular y pasarlo junto con el objeto de configuración al constructor Hobot. El objeto de configuración consta de los controladores que desea inicializar, la ruta predeterminada y los pares de comando / controlador.

//   telegraf-
const bot = new Telegraf('_');

//  
export const hobot = new Hobot(bot, {
    defaultPath: 'firstPath',
    commands: [
        //  ,     
        // get    :
        { command: 'start', path: 'firstPath' }
    ],
    controllers: [
        // -,    
        startController,
        nextController
    ]
});

// C telegraf-,       
bot.launch();

En conclusión


Ventajas implícitas de dividir una hoja en controladores:

  • La capacidad de poner funciones aisladas, métodos e interfaces que están bloqueados a la lógica de este controlador en archivos separados al lado de los controladores
  • Riesgo significativamente reducido de romper todo accidentalmente
  • Modularidad: activar / desactivar / dar a un determinado segmento de la audiencia esta o aquella lógica se puede hacer simplemente agregando y eliminando controladores de la matriz, incluso actualizando la configuración sin programación; para esto, por supuesto, necesitamos escribir un par de cartas, ya que aún no hemos alcanzado
  • La capacidad de decirle claramente al usuario qué se espera exactamente de él cuando él (lo que sucede a menudo) hace algo mal, y hacerlo donde este es el lugar, al final del procesamiento del método posterior

Nuestros planes :

este artículo describe la secuencia de comandos básica para trabajar con mensajes de texto Hobot.
Si el tema resulta relevante, compartiremos otras sutilezas técnicas del marco y nuestras observaciones de la práctica de desarrollar y usar bots de telegramas en futuros artículos.

Enlaces : La

instalación del bot se ve así: npm i -s hobot
Repositorio con tutorial en README.MD y el sandbox
Ready bot trabajando en producción basado en Hobot.

¡Gracias por su atención, me complacerá escuchar sus preguntas, deseos o ideas para nuevos bots!

All Articles