FRANZKAFKA
FRANZKAFKA

Reputation: 53

Access CosmosDB from different bots

I'm trying to read and write from/to an Azure Cosmos DB with two different bots (js, v4, ms botframework).

Chatbot 1: - Chat with user, save user data and use it later

Chatbot 2: - Read and display some user data

I use the following client: https://github.com/Microsoft/BotFramework-WebChat

Scenario:

  1. I fixate my userID in the client (which has a directline to bot 1) to let's say "123"
  2. I use Bot 1 and enter my username in the dialog (prompted by bot)
  3. I refresh the Website on which Bot 1 is running with the same id "123"
  4. I see that the bot still has my data stored
  5. I change the ID in my client to "124"
  6. I use Bot 1 and see there is no stored data (which is expected since ID "124" has never chatted with Bot 1)
  7. I change the ID back to "123"
  8. I use bot 1 and see that data from step 2 is still there
  9. I use bot 2 with the id "123"
  10. I see that there is no data ("undefined")
  11. I use bot with ID "123" again
  12. I see that the data from step 2 is gone

Which means that whenever I access the database with my second bot it seems like the data is cleared / deleted.

This is how I access the DB in index.js:

//Add CosmosDB (info in .env file)
const memoryStorage = new CosmosDbStorage({
    serviceEndpoint: process.env.ACTUAL_SERVICE_ENDPOINT, 
    authKey: process.env.ACTUAL_AUTH_KEY, 
    databaseId: process.env.DATABASE,
    collectionId: process.env.COLLECTION
})

// ConversationState and UserState
const conversationState = new ConversationState(memoryStorage);
const userState = new UserState(memoryStorage);

// Use middleware to write/read from DB
adapter.use(new AutoSaveStateMiddleware(conversationState));
adapter.use(new AutoSaveStateMiddleware(userState));

This is how I use the DB in bot.js:

constructor(conversationState, userState, dialogSet, memoryStorage) {
        // Creates a new state accessor property.
        // See https://aka.ms/about-bot-state-accessors to learn more about the bot state and state accessors
        this.conversationState = conversationState;
        this.userState = userState;

        // Memory storage
        this.memoryStorage = memoryStorage;

        // Conversation Data Property for ConversationState
        this.conversationData = conversationState.createProperty(CONVERSATION_DATA_PROPERTY);
        // Properties for UserState
        this.userData = userState.createProperty(USER_DATA_PROPERTY);
        this.investmentData = userState.createProperty(INVESTMENT_DATA_PROPERTY);

}

    async displayPayout (step) {
            console.log("Display Payout");
            // Retrieve user object from UserState storage
            const userInvestData = await this.investmentData.get(step.context, {});
            const user = await this.userData.get(step.context, {});

            await step.context.sendActivity(`Hallo ${user.name}. Am Ausgang kannst du dir deine Bezahlung von ${userInvestData.payout} abholen.` );
    }

The code snipped is from bot 2. Bot 1 saves the data in the same way. You can find the repos here:

Bot 1: https://github.com/FRANZKAFKA13/roboadvisoryBot

Bot 2: https://github.com/FRANZKAFKA13/displayBot

Client for Bot 1: https://github.com/FRANZKAFKA13/ra-bot-website-c

Client for Bot 2: https://github.com/FRANZKAFKA13/ra-bot-website-display

I also tried to use the "readOnly" key from CosmosDB in bot 2, which throws an error:

     [onTurnError]: [object Object]
(node:1640) UnhandledPromiseRejectionWarning: TypeError: Cannot perform 'set' on a proxy that has been revoked
    at adapter.sendActivities.then (C:\Users\X\Implementierung\display_bot\node_modules\botbuilder-core\lib\turnContext.js:175:36)
    at <anonymous>
    at process._tickDomainCallback (internal/process/next_tick.js:229:7)
(node:1640) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 3)

Another behavior that I have noticed: When I trigger a "join event" through my client with a redux store, the userdata is not saved as well (every time I refresh the page, the data is gone, despite using the same id "123" all the time)

dispatch({
              type: 'WEB_CHAT/SEND_EVENT',
              payload: {
                // Event starting bot's conversation
                name: 'webchat/join',
                value: {}
              }

Any ideas? Thanks in advance

Upvotes: 1

Views: 298

Answers (2)

FRANZKAFKA
FRANZKAFKA

Reputation: 53

Edit: Solved it by adding "[this.userID]" after each "user" call.

I tried your method and whenever I write the data, a new eTag is created which leads to the object being split apart:

"document": {
    "25781dc4-805d-4e69-bf89-da1f4d72e7cb": {
        "25781dc4-805d-4e69-bf89-da1f4d72e7cb": {
            "25781dc4-805d-4e69-bf89-da1f4d72e7cb": {
                "25781dc4-805d-4e69-bf89-da1f4d72e7cb": {
                    "25781dc4-805d-4e69-bf89-da1f4d72e7cb": {
                        "name": "",
                        "age": "",
                        "gender": "",
                        "education": "",
                        "major": "",
                        "eTag": "\"00003998-0000-0000-0000-5c797fff0000\""
                    },
                    "name": "Jane Doe",
                    "eTag": "\"00003e98-0000-0000-0000-5c7980080000\""
                },
                "age": 22,
                "eTag": "\"00004898-0000-0000-0000-5c7980150000\""
            },
            "gender": "female",
            "eTag": "\"00004d98-0000-0000-0000-5c79801b0000\""
        },
        "education": "Bachelor",
        "eTag": "\"00005498-0000-0000-0000-5c7980200000\""
    },
    "major": "Business Administration",
    "complete": true

How can I prevent this?

My Code:

In Constructor:

this.changes = {};
this.userID = "";
this.userDatax = {
    name: "",
    age: "",
    gender: "",
    education: "",
    major: "",

    eTag: '*',
}

In Dialogs:

async welcomeUser (step) {
    console.log("Welcome User Dialog");
    //step.context.sendActivity({ type: ActivityTypes.Typing});

    // Initialize UserData Object and save it to DB
    this.changes[this.userID] = this.userDatax;
    await this.memoryStorage.write(this.changes);
}

async promptForAge (step) {
    console.log("Age Prompt");
    // Read UserData from DB
    var user = await this.memoryStorage.read([this.userID]);
    console.log(user);

    // Before saving entry, check if it already exists
    if(!user.name) {
        user.name = step.result;
        user.eTag = '*';
        // Write userData to DB
        this.changes[this.userID] = user;
        await this.memoryStorage.write(this.changes);
    }
}

Upvotes: 0

mdrichardson
mdrichardson

Reputation: 7241

Since storage ids (see image) are created automatically based off of user ids (that may also be created automatically and varies by channel) and channel ids, this can be very difficult to do. It can make it very difficult to persist user and conversation data, particularly across bots and channels.

Example ID:

enter image description here

Here's more on how IDs work.

Personally, I would write my own, custom storage, instead of (or in addition to) saving it with UserState.

To write your data, do something like this:

const changes = {};
const userDataToWrite = {
    name: step.result,
    eTag: '*',
}
// Replace 'UserId' with however you want to set the UserId
changes['UserId'] = userDataToWrite;
this.memoryStorage.write(changes);

This will store a document that looks like this (I set 'UserId' to 'user123':

enter image description here

To read:

const userDataFromStorage = await this.memoryStorage.read(['UserId']);

userDataFromStorage will look like this:

{ UserId:
   { name: 'myName',
     eTag: '"0000c700-0000-0000-0000-5c7879d30000"' } }

You'll have to manage userIds yourself, but this will ensure that the data can be read across bots, channels, and users.

Upvotes: 1

Related Questions