microsoft / botframework-sdk

Bot Framework provides the most comprehensive experience for building conversation applications.
MIT License
7.47k stars 2.44k forks source link

[Question] Why is DialogData not serialized to state in replyReceived when creating a custom dialog? #3814

Closed srozga closed 6 years ago

srozga commented 6 years ago

Issue Description

I am trying to create a custom dialog by subclassing Dialog. I handle the begin and replyReceived methods. The dialogData or any other state is never saved after replyReceived is hit. It is only saved when begin is called. SimpleDialog doesn't seem to do anything special and behaves the same way. Waterfalls in general seem to work. Thanks!

Code Example

require('dotenv-extended').load();

const builder = require('botbuilder');
const restify = require('restify');

const server = restify.createServer();
server.listen(process.env.port || process.env.PORT || 3978, () => {
    console.log('%s listening to %s', server.name, server.url);
});

const connector = new builder.ChatConnector({
    appId: process.env.MICROSOFT_APP_ID,
    appPassword: process.env.MICROSOFT_APP_PASSWORD
});
server.post('/api/messages', connector.listen());

const bot = new builder.UniversalBot(connector, [
    session => {
        session.beginDialog('test');
    },
    (session, arg) => {
        console.log('ok done');
        session.endConversation();
    }
]);

class TestDialog extends builder.Dialog {
    begin(session, args) {
        session.dialogData.eph = { step: 1 };
        console.log(session.dialogData.eph.step);
    }

    replyReceived(session) {
        session.dialogData.eph.step++;
        console.log(session.dialogData.eph.step);
        if (session.dialogData.eph.step > 3) {
            session.endDialog();
        }
    }
}

bot.dialog('test', new TestDialog());

bot.dialog('test2', new builder.SimpleDialog((session, arg) => {
    if(!session.dialogData.eph) session.dialogData.eph = { step: 1 };
    else session.dialogData.eph.step++;

    console.log(session.dialogData.eph.step);
    if (session.dialogData.eph.step > 3) {
        session.endDialog();
    }
}));

Reproduction Steps

  1. Run the code above.

Expected Behavior

After sending the bot 3 messages I expect the dialog to end and my root waterfall to end the conversation.

Actual Results

Instead, after the dialogData is set in begin, anything I do to the dialogData in repyReceived is not stored. When looking at the emulator log, the setPrivateConversationData call is never made when my code hits replyReceived.

nwhitmont commented 6 years ago

HI @srozga - Here's what's going on with the dialog you described: The dialogData doesn't persist, because there are no additional turns in the dialog and dialogData isn't scoped to persist across all conversations. Your custom class receives the message, increments your custom var, and then since the dialog is over, the dialogData is disposed, aka reset next time you send a message, so the counter will never reach >=3 in this scenario.

If you want to persist data outside of a single-turn dialog, use session.conversationData instead. ;-)

In the Node.js storage containers reference, dialog data is defined as:

Contains data that is saved for the current dialog only. Each dialog maintains its own copy of this property. The property is cleared when the dialog is removed from the dialog stack.

Let me know if you have any other questions.

srozga commented 6 years ago

I’m not sure I understand what you mean by single turn dialog. I guess I’m coming wtbit from the perspective of dialoga in the C# SDK. I can place the dialog on top of the stack, StartAsync is analogous to begin and I can do context.Wait(handlee), where handler is analogous to my replyReceived. In C# I would have to call context.Wait again inside of the handler to keep the dialog on top of the stack and get more messages. Is there something similar in Node? How does the WaterfallDialog or PromotDialog do it.

nwhitmont commented 6 years ago

Can you share some more context about your specific use case? What are you trying to accomplish here exactly?

Single turn is one message from user, one response from bot.

User: hello bot
Bot: goodbye user

Multi-turn is anything beyond that.

// turn 1
User: order sandwich
Bot: great, what kind of bread do you want? (choices here)
// turn 2
User: whole wheat
Bot: what kind of veggies do you want?
// turn 3
User: tomatoes, lettuce
Bot: what kind of protein do you want (choices)?
// etc...

In Node.js SDK for BotBuilder, multi-turn dialogs are composed with "waterfall" dialogs and prompts.

For more details, see: https://docs.microsoft.com/en-us/bot-framework/nodejs/bot-builder-nodejs-dialog-overview#using-waterfalls-and-prompts

srozga commented 6 years ago

Trying to understand a few things about the underlying functions of the dialog abstraction in Node. Structuring every multi turn interaction as a waterfall is a bit contrived; I want to have more control over flow, retries, step branching, etc. Initially this came up because I wanted to see what it would take to implement my own PromptDialog for academic purposes.

Currently, I am trying to prove out some integrations with Slack, specifically their Ephemeral messages. It appears, that this would involve me using their chat.postEphemeral method directly, and I have all the necessary information to do so in the message from the Bot Framework. So, there will be cases in which I will receive a message from the Slack Connector, and will send a message back using a mechanism other than session.send.

The challenge, it seems, is that state is not saved between dialog replyReceived calls, unless my dialogs use a session.send. Case in point, my original dialog exhibited behavior in which beginDialog state was saved correctly but after replyReceived, nothing was saved. I confirmed that the dialog was not being reinitialized, contrary to your assertion that the dialog is over.

Here's that code again:

class TestDialog extends builder.Dialog {
    begin(session, args) {
        session.dialogData.eph = { step: 1 };
        console.log(session.dialogData.eph.step);
    }

    replyReceived(session) {
        session.dialogData.eph.step++;
        console.log(session.dialogData.eph.step);
        if (session.dialogData.eph.step > 3) {
            session.endDialog();
        }
    }
}

However, if I change the code to this, it works as expected.

class TestDialog2 extends builder.Dialog {
    begin(session, args) {
        session.dialogData.eph = { step: 1 };
        session.send(session.dialogData.eph.step + '');
    }

    replyReceived(session) {
        session.dialogData.eph.step++;
        session.send(session.dialogData.eph.step + '');
        if (session.dialogData.eph.step > 3) {
            session.endDialog();
        }
    }
}

The only difference is the fact that I use session.send instead of a console.log. For a vanilla Bot Framework bot this is makes sense. If I want to take advantage of some of the native features in Slack using this approach, it is not possible.

My understanding is that using sending the Slack message format to the Connector works, but only in so far as it calls chat.postMessage. Calling chat.postEphemeral, chat.update and chat.delete directly is perfectly possible, but I can get into this strange scenario in which conversation data is not saved.

Thanks! -s

nwhitmont commented 6 years ago

If you want to send a full Slack message, you need to put the extra Slack-specific message data in the message object's channelData as described in this article:

https://docs.microsoft.com/en-us/bot-framework/dotnet/bot-builder-dotnet-channeldata#create-a-full-fidelity-slack-message

Another thing to note, is the new SDK4 line currently in development, which will be a complete re-write of the framework. There will be some changes to how dialogs work, based on our community feedback from v3 SDK. Look out for a developer preview next month.

srozga commented 6 years ago

Of course, but this interface doesn't support ephemeral messages, right? In that case, I need to either call chat.postEphemeral or use an interactive message's response_url. In both cases, the dialog state will not save because I'm not using session.send.

The ephemeral use case, ok. But responding to response_url is a big part of responding interactive message flows in Slack. Using the above code, I can't save the state.

Is there a way I could force a save of the user's privateConversationData?