builderbot icon indicating copy to clipboard operation
builderbot copied to clipboard

[🐛] Callback de addAnswer no se ejecuta al entrar a flujo con múltiples pasos via gotoFlow

Open marceloroig opened this issue 8 months ago • 6 comments

¿Que versión estas usando?

v2

¿Sobre que afecta?

Flujo de palabras (Flow)

Describe tu problema

Hola, estoy experimentando un comportamiento inesperado en mi flujo. Al usar gotoFlow para redirigir al usuario a un flujo (impresionFlow) que tiene al menos un addAnswer con una función callback, el bot envía el mensaje de ese addAnswer, pero el código dentro de la función callback no parece ejecutarse. Los console.log dentro de la callback no aparecen en la terminal.

Comportamiento Esperado: Después de seleccionar la opción 1 (Impresión) y que el bot envíe el primer mensaje del impresionFlow ('✅ Has seleccionado...'), la función callback de ese addAnswer debería ejecutarse. Los console.log dentro de la callback deberían aparecer en la terminal, y los flowDynamic subsiguientes deberían enviar mensajes al chat, y la función de notificación debería ser llamada.

Comportamiento Actual: Describe lo que realmente está pasando: "El bot envía el primer mensaje del impresionFlow ('✅ Has seleccionado...'). Sin embargo, la función callback del addAnswer no se ejecuta. No aparecen logs de la callback en la terminal. El flujo se detiene y no envía los mensajes subsiguientes (flowDynamic) ni llama a la función de notificación.

Fragmentos de Código Relevantes: Fragmento 1: Lógica de redirección a impresionFlow en serviciosSoporteFlow

Este snippet muestra el addAnswer que captura la opción del sub-menú (1 o 2) y redirige al flujo impresionFlow cuando se elige la opción 1. // En flows/asistenciaTecnicaFlow.js (dentro de const serviciosSoporteFlow = addKeyword(...) ...)

// Estado 3: Capturar la opción 1 o 2 y dirigir al sub-flujo correspondiente
.addAnswer(
    'Responde con *1* o *2*.', // Prompt que muestra 1 o 2
    { capture: true }, // Capturamos la respuesta
    async (ctx, { fallBack, gotoFlow }) => {
        const option = ctx.body.trim();
        if (option === '1') { // Si el usuario responde '1'
             console.log('--> LOG: Usuario seleccionó 1 (Impresion). Intentando gotoFlow(impresionFlow).'); // Log antes del gotoFlow
             return gotoFlow(impresionFlow); // Redirige al flujo de Impresión
        }
        if (option === '2') { // Si el usuario responde '2'
             // Aquí iría la lógica para ir al soporteSubmenuFlow
             console.log('--> LOG: Usuario seleccionó 2 (Soporte). Intentando gotoFlow(soporteSubmenuFlow).');
             return gotoFlow(soporteSubmenuFlow);
        }
        // Fallback si la respuesta no es '1' ni '2'
        return fallBack('Disculpa, no entendí. Por favor, responde con *1* o *2*.');
    }
);

// ... el resto de la definición de serviciosSoporteFlow ...

Fragmento 2: Definición COMPLETA del flujo impresionFlow Este snippet muestra la estructura completa del flujo impresionFlow que probamos al final, con dos addAnswers, donde el segundo no tiene texto pero sí la callback con toda la lógica y los logs de depuración. Este es el código que estás usando actualmente y donde la callback del primer addAnswer no se ejecuta.

// En flows/asistenciaTecnicaFlow.js (definiendo el flujo impresionFlow)

const impresionFlow = addKeyword(utils.setEvent('IMPRESION_FLOW')) .addAnswer( // PRIMER addAnswer: Envía mensaje de confirmación, tiene callback con log '✅ Has seleccionado 2.1 Quiero Imprimir unos Documentos.', // Mensaje visible en chat null, // No capture, opciones null async (ctx, { state, flowDynamic, provider }) => { // <-- Callback del PRIMER addAnswer // <<-- ESTE LOG NO APARECE EN LA TERMINAL -->> console.log('--> LOG: Entrando a la callback del PRIMER addAnswer de impresionFlow (Solo Confirmación).'); // No ponemos flowDynamic aquí para no enviar otro mensaje desde este paso } ) .addAnswer( // SEGUNDO addAnswer: No tiene texto, callback con lógica, flowDynamic y notificación null, // <<-- SIN TEXTO AQUÍ -->> null, // Sin opciones/capture async (ctx, { state, flowDynamic, provider }) => { // <-- Callback del SEGUNDO addAnswer // <<-- NINGUNO DE LOS LOGS DENTRO DE ESTA CALLBACK APARECEN -->> console.log('--> LOG: Entrando a la callback del SEGUNDO addAnswer de impresionFlow (Lógica principal).'); // Este log NO APARECE

        // Obtener datos del usuario del state
        console.log('--> LOG: Obteniendo datos del state en la segunda callback.');
        const nombreDefault = state.get('nombre_default') || 'N/A';
        const nombreContacto = state.get('nombre_contacto') || 'N/A';
        const telefonoContacto = state.get('telefono_contacto') || 'N/A';
        console.log(`--> LOG: Datos obtenidos: Nombre=${nombreContacto}, Tel=${telefonoContacto}`);

        // Enviar el SEGUNDO mensaje (la instrucción de enviar documentos)
        console.log('--> LOG: Intentando enviar mensaje de instrucción desde la segunda callback.');
        await flowDynamic('Por favor, envía los documentos que deseas imprimir a este número de WhatsApp: *0981113363*, en atencion al cliente tendran listo para imprimir.');
        console.log('--> LOG: Mensaje de instrucción enviado desde la segunda callback.');

        // Llamar a la función de notificación (asegúrate de incluir el código de esta función en el issue también)
        console.log('--> LOG: Llamando a enviarNotificacionImpresion desde la segunda callback.');
        try {
            await enviarNotificacionImpresion(provider, {
                telefono: telefonoContacto,
                nombrePerfil: nombreDefault,
                nombreContacto: nombreContacto
            });
            console.log('--> LOG: enviarNotificacionImpresion finalizada desde la segunda callback.');
        } catch (error) {
            console.error('--> LOG: Error capturado al llamar a enviarNotificacionImpresion desde la segunda callback:', error);
        }

        // Enviar el mensaje final de confirmación
        console.log('--> LOG: Intentando enviar mensaje final al usuario desde la segunda callback.');
        await flowDynamic('Un Vendedor será avisado de tu pedido de impresión y se pondrá en contacto contigo.');
        console.log('--> LOG: Mensaje final enviado desde la segunda callback.');

        console.log('--> LOG: Callback del SEGUNDO addAnswer de impresionFlow finalizada.');
    }
);

Reproducir error

Para reproducir el problema: 1.Iniciar el bot. 2.En el chat, enviar 'hola' para iniciar el welcomeFlow. 3.Seleccionar la opción '2' para ir a serviciosSoporteFlow. 4.Ingresar el nombre cuando se solicite. 5.Seleccionar la opción '1' en el sub-menú (para ir a impresionFlow). 6.Observar el chat (el bot envía el primer mensaje del impresionFlow, pero no los siguientes). 7.Observar la terminal (no aparece ningún log de las callbacks del impresionFlow).

Información Adicional

package.json: { "name": "base-bailey-json", "version": "1.0.0", "description": "", "main": "src/app.js", "type": "module", "scripts": { "lint": "eslint . --no-ignore", "dev": "npm run lint && nodemon ./src/app.js", "start": "node ./src/app.js" }, "keywords": [], "dependencies": { "@builderbot/bot": "1.2.6", "@builderbot/provider-baileys": "1.2.6", "@builderbot/database-json": "1.2.6" }, "devDependencies": { "eslint-plugin-builderbot": "latest", "eslint": "^8.57.0", "nodemon": "^3.1.0" }, "author": "", "license": "ISC", "packageManager": "[email protected]+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39" }

Ojo, mi version de Node.js v20.19.1

Provider y Database: uso @builderbot/provider-baileys y @builderbot/database-json.

marceloroig avatar May 09 '25 01:05 marceloroig

Error con el addAnswer

Me pasa algo similar pero sin nisiquiera pasar por un goToFlow, en este caso tome el ejemplo de la documentación oficial: https://builderbot.vercel.app/en/add-functions#message-with-callback y solo le agregué un if para saber si el fallBack funcionaba.

En efecto funciona, pero luego del mensaje del flowDynamic no salta al siguiente addAnswer.

const mainFlow2 = addKeyword('pepe')
    .addAnswer('Hi!, Do you know 4+4?', {capture:true}, async (_, {flowDynamic, fallBack}) => {
        const sum = 4 + 4
        const value = _.body;
        if (Number(value) !== sum) {
            return fallBack(`No, ${value} no es correcto, intenta de nuevo`);
        }
        await flowDynamic(`Total: ${sum} pero tu dijiste ${value}`)
    })
    .addAnswer('¿Quieres saber algo más?', {capture:true}, async (_, {flowDynamic, fallBack}) => {
        const value = _.body;
        if (value !== 'si' && value !== 'no') {
            return fallBack(`No entiendo, por favor responde con "si" o "no"`);
        }
        await flowDynamic(`Tu respuesta fue ${value}`)
    })
    .addAction(async (_, {flowDynamic}) => {
        await flowDynamic(`Other message`)
    })

Este es el resultado, en lugar de saltar a la pregunta '¿Quieres saber algo más?', se finaliza el flujo. Image

En un ejemplo más complejo se puede ver el mismo comportamiento:

import { addKeyword, EVENTS } from '@builderbot/bot';
import { ApiResponse } from '../services/api/interfaces/api-response.interface';
import { ClientRequestDto, ClientResponseDto } from '../services/api/dto/client.dto';
import { ApiService } from '../services/api/api.service';

const apiService = new ApiService();



const registerFlow = addKeyword(EVENTS.ACTION)
    .addAnswer('👋 Hola, parece que eres nuevo por aquí.\n\n¿Quieres registrarte?',
        { capture: true,
            buttons: [
                { body: 'Si, quiero'},
                { body: 'No, gracias'}
            ]
        },
        async (ctx, ctxFn) => {
            if(ctx.body === 'Si, quiero'){
                await ctxFn.flowDynamic('✅ ¡Perfecto! Vamos a comenzar con tu registro 📝');
            }else if(ctx.body === 'No, gracias'){
                return ctxFn.endFlow('❌ El registro fue cancelado, puedes volver a escribirle al bot para registrarte cuando lo desees');
            }else{
                return ctxFn.fallBack('⚠️ Por favor, escoge una de las opciones');
            }
        }
    )
    .addAnswer('📝 ¿Cuál es tu nombre completo?',
        { capture: true },
        async (ctx, ctxFn) => {
            await ctxFn.flowDynamic(`👤 Gracias ${ctx.body}!`);
            await ctxFn.state.update({ firstName: ctx.body });
        }
    )
    .addAnswer('📧 ¿Cuál es tu correo electrónico?',
        { capture: true },
        async (ctx, ctxFn) => {
            // Validar formato de correo electrónico
            const emailRegex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;

            if (!emailRegex.test(ctx.body)) {
                return ctxFn.fallBack('⚠️ Por favor, ingresa un correo electrónico válido');
            }

            await ctxFn.state.update({ email: ctx.body });
        }
    )
    .addAnswer('🔢 ¿Cuál es tu número de identificación? solo escribe números',
        { capture: true },
        async (ctx, ctxFn) => {
            // Validar que sea un número de teléfono (simplificado)
            const identificacionRegex = /^\d{7,15}$/;
            const identificationNumber = ctx.body.replace(/\D/g, '');
            if (!identificacionRegex.test(identificationNumber)) {
                return ctxFn.fallBack('⚠️ Por favor, ingresa un número de teléfono válido (solo números)');
            }

            const state = ctxFn.state.getMyState();

            // Preparar objeto para enviar a la API
            const clientData: ClientRequestDto = {
                VcIdentificationNumber: identificationNumber,
                VcFirstName: state.firstName,
                VcEmail: state.email,
                VcPhone: ctx.body,
                VcFirstLastName: ' ',
            };

            try {
                // Llamar a la API para registrar el cliente
                const response = await apiService.request<ApiResponse<ClientResponseDto>>(
                    '/clients',
                    'POST',
                    clientData
                );

                if(response.status === 'success'){
                    await ctxFn.flowDynamic('🎉 ¡Excelente! Tus datos ya fueron registrados, ya puedes comenzar a utilizar nuestros servicios');
                }else{
                    await ctxFn.flowDynamic('❌ Lo siento, hubo un error al registrar tus datos. Por favor, intenta nuevamente más tarde.');
                }
            } catch (error) {
                console.error('Error al registrar cliente:', error);
                await ctxFn.flowDynamic('❌ Lo siento, hubo un error al registrar tus datos. Por favor, intenta nuevamente más tarde.');
            }
        }
    );

export { registerFlow };

Resultado con versiones: 1.2.3, 1.2.4 , 1.2.5 y 1.2.7-alpha.5 En este caso no pasa al siguiente bloque:

Image

Resultado con versiones: 1.2.6 En este caso, aunque se logra conectar al provider de meta, no envia mensajes, no muestra nada en consola por lo que es imposible hacer un debug.

Error con el fallBack con addAction

Buscando alternativas, se intentó usar addAction en lugar de addAnswer pero el comportamiento es igual errático En este caso luego de un goToFlow, el encadenamiento de los mensajes funciona, pero el fallback no realiza las validaciones correctas:


import { addKeyword, EVENTS } from '@builderbot/bot';
import { ApiResponse } from '../services/api/interfaces/api-response.interface';
import { ClientRequestDto, ClientResponseDto } from '../services/api/dto/client.dto';
import { ApiService } from '../services/api/api.service';

const apiService = new ApiService();



const registerFlow = addKeyword(EVENTS.ACTION)
.addAnswer('👋 Hola, parece que eres nuevo por aquí.\n\n¿Quieres registrarte?\n\nResponde *SI* o *NO* (también puedes usar 1 o 0)',
    { capture: true }
)
.addAction(async (ctx, ctxFn) => {
    // Convertir respuesta a minúsculas para facilitar validación
    const response = ctx.body.toLowerCase();

    // Validar que la respuesta sea una de las opciones permitidas
    if (response === 'si' || response === '1') {
        await ctxFn.flowDynamic('✅ ¡Perfecto! Vamos a comenzar con tu registro 📝');
    } else if (response === 'no' || response === '0') {
        return ctxFn.endFlow('❌ El registro fue cancelado, puedes volver a escribirle al bot para registrarte cuando lo desees');
    } else {
        return ctxFn.fallBack('⚠️ Por favor, responde solo con *SI* o *NO* (o 1 o 0)');
    }
})
.addAnswer('📝 ¿Cuál es tu nombre completo?',
    { capture: true }
)
.addAction(async (ctx, ctxFn) => {
    await ctxFn.flowDynamic(`👤 Gracias ${ctx.body}!`);
    await ctxFn.state.update({ firstName: ctx.body });
})
.addAnswer('📧 ¿Cuál es tu correo electrónico?',
    { capture: true }
)
.addAction(async (ctx, ctxFn) => {
    // Validar formato de correo electrónico
    const emailRegex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;

    if (!emailRegex.test(ctx.body)) {
        return ctxFn.fallBack('⚠️ Por favor, ingresa un correo electrónico válido');
    }

    await ctxFn.state.update({ email: ctx.body });
})
.addAnswer('🔢 ¿Cuál es tu número de identificación? solo escribe números',
    { capture: true }
)
.addAction(async (ctx, ctxFn) => {
    // Validar que sea un número de teléfono (simplificado)
    const identificacionRegex = /^\d{7,15}$/;
    const identificationNumber = ctx.body.replace(/\D/g, '');
    if (!identificacionRegex.test(identificationNumber)) {
        return ctxFn.fallBack('⚠️ Por favor, ingresa un número de teléfono válido (solo números)');
    }

    const state = ctxFn.state.getMyState();

    // Preparar objeto para enviar a la API
    const clientData: ClientRequestDto = {
        VcIdentificationNumber: identificationNumber,
        VcFirstName: state.firstName,
        VcEmail: state.email,
        VcPhone: ctx.body,
        VcFirstLastName: ' ',
    };

    try {
        // Llamar a la API para registrar el cliente
        const response = await apiService.request<ApiResponse<ClientResponseDto>>(
            '/clients',
            'POST',
            clientData
        );

        if(response.status === 'success'){
            await ctxFn.flowDynamic('🎉 ¡Excelente! Tus datos ya fueron registrados, ya puedes comenzar a utilizar nuestros servicios');
        }else{
            await ctxFn.flowDynamic('❌ Lo siento, hubo un error al registrar tus datos. Por favor, intenta nuevamente más tarde.');
        }
    } catch (error) {
        console.error('Error al registrar cliente:', error);
        await ctxFn.flowDynamic('❌ Lo siento, hubo un error al registrar tus datos. Por favor, intenta nuevamente más tarde.');
    }
});


export { registerFlow };

Este es el resultado:

Image

camilocalderont avatar May 11 '25 02:05 camilocalderont

entonces.... toca programarlo manualmente?

Z3R0GT avatar Jun 05 '25 22:06 Z3R0GT

entonces.... toca programarlo manualmente?

Para serte sincero, dure más de 2 semanas intentando estabilizar cambiando entre versiones. En Youtube hay mucha documentación, pero son de versiones anteriores. Claramente cada versión nueva debe ofrecer una funcionalidad igual o superior, pero en mi caso no encontré esto.

Yo tomé la decisión de hacer un bot desde cero en python, tomando como base la lógica aquí usada y me he enfrentado a diferentes inconvenientes como entender la interacción de estados de mensajes con el API de meta, guardar el estado entre los pasos del flujo entre otros.

De ahí destaco y valoro el esfuerzo de publicar esta librería que acorta muchos caminos y tiene buenas prácticas. Quiza el talón de aquiles es la manera en que almacena la información de las conversaciones para tener en cuenta el estado por flujo.

camilocalderont avatar Jun 06 '25 00:06 camilocalderont

Recomiendo usar el provider de memory (es un Map<>) y que gestiónen las conversaciones puede ser ya de manera externa

leifermendez avatar Jun 06 '25 07:06 leifermendez

Tambien los invito a pasar por el discord http://link.codigoencasa.com/DISCORD tenemos más casos de uso y expertos en este tema

leifermendez avatar Jun 06 '25 07:06 leifermendez

Por el momento, la solución que he encontrado es básicamente usar como loco gotoFlow y evaluar cada entrada manualmente, el usuario final no vera mucha diferencia en como funcione lo pienso como si:

-> entrada usuario .* evaluó :: resultado :: mando el usuario a algún flujo según la entrada -> usuario pasa a la siguiente parte

por ejemplo:

const flowAlgoMain = addKeyword("hola")
    .addAnswer(
        "hola",
        { capture : true},
        async (ctx, {gotoFlow }) => {
            const msg = filter(ctx.body)
            if (msg == "algo")
            {
                return gotoFlow(flowAlgo1 )
            }
            else if (msg == "joteria")
            {
                return gotoFlow(flowAlgo2 )
            }
        }
    )
    

const flowAlgo1 = addKeyword(EVENTS.ACTION)
    .addAnswer(
        "algo1",
        {capture : true },
        async (ctx, { provider }) => {
            await provider.sendText(ctx.from+"@s.whatsapp.net", "algo1", {options:{}} )
        }
    )

const flowAlgo2 = addKeyword(EVENTS.ACTION)
    .addAnswer(
        "algo2",
        {capture : true },
        async (ctx, { provider }) => {
            await provider.sendText(ctx.from+"@s.whatsapp.net", "algo2", {options:{}} )
        }
    )

siendo filter() su función o algo asi para manejar las entradas del usuario, y los flujo adyacentes podrían meterlos en sus propias carpetas o algo así (siguiendo la recomendación )

Y para diferenciarlos, podrían usar esta forma para nombrar las variables:

flow + (nombre del flujo) + Main || patrón para diferenciarlos (mi caso, número XD)

Z3R0GT avatar Jun 07 '25 01:06 Z3R0GT

¿Alguna novedad sobre esta ISSUE?

github-actions[bot] avatar Aug 06 '25 22:08 github-actions[bot]