Search code examples
javascriptnode.jsbotframework

Processing answer to reminder dialog


I would like to start a 'reminder dialog' when sending a proactive message to a user. The dialog is posted. but when processing the answer it goes back to the main dialog.

Currently i create my bot as followed:

const conversationState = new ConversationState(mongoStorage);
const userState = new UserState(memoryStorage);

const bot = new DialogAndWelcomeBot(conversationState, userState, logger);

// Listen for incoming activities and route them to your bot main dialog.
server.post("/api/messages", (req, res) => {
  adapter.use(new GraphQLMiddleware(bot.getAuthState()));

  // Route received a request to adapter for processing
  adapter.processActivity(req, res, async turnContext => {
    await bot.run(turnContext);
  });
});

and send the pro active message like this:

await adapter.createConversation(conversationReferenceAdapted, bot.remind);

where the DialogAndWelcomeBot has the following function:

remind = async turnContext => {
  const reminderDialog = new ReminderDialog(this.logger);
  await reminderDialog.run(turnContext, this.dialogState);
  await this.conversationState.saveChanges(turnContext, false);
  await this.userState.saveChanges(turnContext, false);
};

However, the ReminderDialog gets fired correctly (with a yes and no button). But when i press any of those buttons i get:

[onTurnError]: Error: DialogContext.continue(): Can't continue dialog. A dialog with an id of 'ReminderDialog' wasn't found.

Which makes me suspect it doesn't know the ReminderDialog in the original flow (through the constructor of DialogAndWelcomeBot where the MainDialog is instantiated and run).

Any ideas on how to approach this?

Desired functionality

I have a main flow that asks some details about the user. This flow can be called by texting anything to the bot. It will then reply asking for some inputs.

The flow I'm trying to implement is an alternative flow to the main one. It should check in with the user (for example every day). So it should start a (different from the main) conversation asking to input the amount of hours he did sports and then confirm.

in short:

  • main flow: User types hi -> Bot replies "what's your age" -> User types 28 -> Bot replies "Okay, thank you"

  • proactive flow: Bot asks the user "How long did you do sports today?" -> User types 1 hour -> Bot replies "Okay, is 1 hour correct?" -> User clicks the "yes" button -> Bot replies "Thanks, see you tomorrow"


Solution

  • You will want to set up both a cron job as well as proactive messaging for this work. Fortunately, there are already posts you can reference that go over how to do both.

    The cron job will allow you to set a time of day for the proactive message to be executed. This Stack Overflow post discusses how to create a simple project that can run alongside your bot. Alternatively, you can also run cron jobs inside an Azure Function which, similarly, would make a call to your proactive message api on a set schedule.

    Regarding the proactive message, look over this Stack Overflow post that goes into detail on setting up this service. Some points specific to that user's issue won't apply to you and can be ignored. This sample from the BotBuilder-Samples repo can also serve as a good reference point.

    Hope of help!


    [Edit]

    Here's a basic setup for calling an API to send a proactive message that also initiates a particular dialog flow. Obviously, you will need to alter to fit your needs, but this should get you on the right path.

    In short, a call is made against an API exposed by your bot that includes the conversationId as a param. When the API is hit, a conversationReference is created and used for sending a Hero card. The Hero card asks the user if they have trained (yes/no) which sends a PostBack when they respond. The PostBack is the trigger which is monitored for in a component dialog via the interruption method. When a match is made, the "proactive" dialog is begun. When the user is finished, the "proactive" dialog is popped off the stack and the user is then returned to where the conversation left off (if they were in one).

    Please note, proactive messages require a token and conversationId. Either the user will need to have conversed with the bot previously or you will need to generate the token and conversationId prior, via the bot, and send those with the user's Slack user.id to begin.

    Index.js

    const conversationReferences = {};
    
    const dialog = new MainDialog( 'MainDialog', userState, conversationState );
    const bot = new WelcomeBot( conversationState, userState, dialog, conversationReferences );
    
    server.post( '/api/message', async ( req, res ) => {
    [...]
    }
    
    server.get( '/api/notify/:conversationID', async ( req, res ) => {
      const { conversationID, query } = req.params;
      const conversationReference = conversationReferences[ conversationID ];
    
      await adapter.continueConversation( conversationReference, async turnContext => {  
        var reply = { type: ActivityTypes.Message };
    
        const yesBtn = { type: ActionTypes.PostBack, title: 'Yes', value: 'Yes' };
        const noBtn = { type: ActionTypes.PostBack, title: 'No', value: 'No' };
    
        const card = CardFactory.heroCard(
          'Have you trained today?',
          null,
          [ yesBtn, noBtn ]
        );
    
        reply.attachments = [ card ];
    
        await turnContext.sendActivity( reply );
        return { status: DialogTurnStatus.waiting };
      } );
    
    
      res.setHeader( 'Content-Type', 'text/html' );
      res.writeHead( 200 );
      res.write( '<html><body><h1>Proactive messages have been sent.</h1></body></html>' );
      res.end();
    } );
    

    mainDialog.js

    const { DialogSet } = require( 'botbuilder-dialogs' );
    const { InterruptionDialog} = require( './interruptionDialog' );
    
    const MAIN_WATERFALL_DIALOG = 'MainWaterfallDialog';
    const ADAPTIVE_CARD = 'AdaptiveCard';
    
    class MainDialog extends CancelAndHelpDialog {
      constructor ( id, userState, conversationState ) {
        this.mainId = id;
        this.userState = userState;
        this.conversationState = conversationState;
        [...]
      };
    
      async run ( turnContext, accessor ) {
        const dialogSet = new DialogSet( accessor );
        this.id = this.mainId;
        dialogSet.add( this );
    
        const dialogContext = await dialogSet.createContext( turnContext );
        const results = await dialogContext.continueDialog();
        if ( results.status === DialogTurnStatus.empty ) {
          return await dialogContext.beginDialog( this.id );
        }
      };
    
      [...]
    };
    

    interruptionDialog.js

    const { ProactiveDialog, PROACTIVE_DIALOG } = require( './proactiveDialog' );
    
    class InterruptionDialog extends ComponentDialog {
      constructor ( id ) {
        super( id );
        this.addDialog( new ConfirmPrompt( 'ConfirmPrompt' ) );
        this.addDialog( new ProactiveDialog() );
      }
    
      async onBeginDialog ( innerDc, options ) {
        const result = await this.interrupt( innerDc );
        if ( result ) {
          return result;
        }
        return await super.onBeginDialog( innerDc, options );
      }
    
      async onContinueDialog ( innerDc ) {
        const result = await this.interrupt( innerDc );
        if ( result ) {
          return result;
        }
        return await super.onContinueDialog( innerDc );
      }
    
      async onEndDialog ( innerDc ) {
        const result = await this.interrupt( innerDc );
        if ( result ) {
          return result;
        }
        return await super.onEndDialog( innerDc );
      }
    
      async interrupt ( innerDc, next ) {
        if ( innerDc.context.activity.type === 'message' ) {
          if ( activity.channelId === 'slack' && activity.channelData.Payload ) {
            if ( activity.channelData.Payload.actions[ 0 ].name === 'postBack' ) {
              return await innerDc.beginDialog( PROACTIVE_DIALOG );          
            }
          } 
        }
      }
    }
    
    module.exports.InterruptionDialog = InterruptionDialog;
    

    proactiveDialog.js

    const {
      NumberPrompt,
      ComponentDialog,
      DialogTurnStatus,
      WaterfallDialog
    } = require( 'botbuilder-dialogs' );
    
    const PROACTIVE_DIALOG = 'proactiveDialog';
    const WATERFALL_DIALOG = 'WATERFALL_DIALOG';
    const NUMBER_PROMPT = 'NUMBER_PROMPT';
    
    class ProactiveDialog extends ComponentDialog {
      constructor () {
        super( PROACTIVE_DIALOG );
    
        this.addDialog( new NumberPrompt( NUMBER_PROMPT ) );
        this.addDialog( new WaterfallDialog( WATERFALL_DIALOG, [
          this.didTrainStep.bind( this ),
          this.trainingStep.bind( this )
        ] ) );
        this.initialDialogId = WATERFALL_DIALOG;
      }
    
      async didTrainStep ( stepContext ) {
        const activity = stepContext.context.activity;
        const response = activity.channelData.Payload.actions[ 0 ].value.toLowerCase();
        if ( response === 'yes' ) {
          return await stepContext.prompt( NUMBER_PROMPT, 'Fantastic! How many minutes?' )
        } else if ( response === 'no' ) {
          await stepContext.context.sendActivity( 'Rubbish...serious rubbish.' )
        }
        return await stepContext.next();
      }
    
      async trainingStep ( stepContext ) {
        const activity = stepContext.context.activity;
        const stepResult = stepContext.result;
        const textResponse = activity.text.toLowerCase();
    
        if ( textResponse === 'no' ) {
          await stepContext.context.sendActivity( "I would recommend at least 5-10 mins of training." )
        } else
          if ( typeof ( stepResult ) === 'number' && stepResult > 0 ) {
            await stepContext.context.sendActivity( "I'll log that for you." );
          } else if ( stepResult <= 0 ) {
            await stepContext.context.sendActivity( "I can't log that value." )
          }
        return { status: DialogTurnStatus.complete }
      }
    }
    module.exports.ProactiveDialog = ProactiveDialog;
    module.exports.PROACTIVE_DIALOG = PROACTIVE_DIALOG;