Search code examples
node.jsmongodbmongoosebotframeworkdirect-line-botframework

How to use MongoDB locally and directline-js for state management in Bot Framework using NodeJs and Mongoose?


I am maintaining the bot state in a local MongoDB storage. When I am trying to hand-off the conversation to an agent using directline-js, it shows an error of BotFrameworkAdapter.sendActivity(): Missing Conversation ID. The conversation ID is being saved in MongoDB

The issue is arising when I change the middle layer from Array to MongoDB. I have already successfully implemented the same bot-human hand-off using directline-js with an Array and the default Memory Storage.

MemoryStorage in BotFramework

const { BotFrameworkAdapter, MemoryStorage, ConversationState, UserState } = require('botbuilder')
const memoryStorage = new MemoryStorage();
conversationState = new ConversationState(memoryStorage);
userState = new UserState(memoryStorage);

Middle Layer for Hand-Off to Agent

case '#connect':
  const user = await this.provider.connectToAgent(conversationReference);
  if (user) {
             await turnContext.sendActivity(`You are connected to 
${ user.userReference.user.name }\n ${ JSON.stringify(user.messages) }`);

   await this.adapter.continueConversation(user.userReference, async 
   (userContext) => {
         await userContext.sendActivity('You are now connected to an agent!');
                    });
                    } 
   else {
 await turnContext.sendActivity('There are no users in the Queue right now.');
         }

The this.adapter.continueConversation throws the error when using MongoDB. While using Array it works fine. The MongoDB and Array object are both similar in structure.


Solution

  • Since this works with MemoryStorage and not your MongoDB implementation, I'm guessing that there's something wrong with your MongoDB implementation. This answer will focus on that. If this isn't the case, please provide your MongoDb implementation and/or a link to your repo and I can work off that.


    Mongoose is only necessary if you want to use custom models/types/interfaces. For storage that implements BotState, you just need to write a custom Storage adapter.

    The basics of this are documented here. Although written for C#, you can still apply the concepts to Node.

    1. Install mongodb

    npm i -S mongodb
    

    2. Create a MongoDbStorage class file

    MongoDbStorage.js

    var MongoClient = require('mongodb').MongoClient;
    
    module.exports = class MongoDbStorage {
        constructor(connectionUrl, db, collection) {
            this.url = connectionUrl;
            this.db = db;
            this.collection = collection;
    
            this.mongoOptions = {
                useNewUrlParser: true,
                useUnifiedTopology: true
            };
        }
    
        async read(keys) {
            const client = await this.getClient();
            try {
                var col = await this.getCollection(client);
    
                const data = {};
                await Promise.all(keys.map(async (key) => {
                    const doc = await col.findOne({ _id: key });
                    data[key] = doc ? doc.document : null;
                }));
                return data;
            } finally {
                client.close();
            }
        }
    
        async write(changes) {
            const client = await this.getClient();
            try {
                var col = await this.getCollection(client);
    
                await Promise.all(Object.keys(changes).map((key) => {
                    const changesCopy = { ...changes[key] };
                    const documentChange = {
                        _id: key,
                        document: changesCopy
                    };
                    const eTag = changes[key].eTag;
    
                    if (!eTag || eTag === '*') {
                        col.updateOne({ _id: key }, { $set: { ...documentChange } }, { upsert: true });
                    } else if (eTag.length > 0) {
                        col.replaceOne({ _id: eTag }, documentChange);
                    } else {
                        throw new Error('eTag empty');
                    }
                }));
            } finally {
                client.close();
            }
        }
    
        async delete(keys) {
            const client = await this.getClient();
            try {
                var col = await this.getCollection(client);
    
                await Promise.all(Object.keys(keys).map((key) => {
                    col.deleteOne({ _id: key });
                }));
            } finally {
                client.close();
            }
        }
    
        async getClient() {
            const client = await MongoClient.connect(this.url, this.mongoOptions)
                .catch(err => { throw err; });
    
            if (!client) throw new Error('Unable to create MongoDB client');
    
            return client;
        }
    
        async getCollection(client) {
            return client.db(this.db).collection(this.collection);
        }
    };
    

    Note: I've only done a little testing on this--enough to get it to work great with the Multi-Turn-Prompt Sample. Use at your own risk and modify as necessary.

    I based this off of a combination of these three storage implementations:

    3. Use it in your bot

    index.js

    const MongoDbStorage = require('./MongoDbStorage');
    
    const mongoDbStorage = new MongoDbStorage('mongodb://localhost:27017/', 'testDatabase', 'testCollection');
    
    const conversationState = new ConversationState(mongoDbStorage);
    const userState = new UserState(mongoDbStorage);