howdyai / botkit

Botkit is an open source developer tool for building chat bots, apps and custom integrations for major messaging platforms.
MIT License
11.44k stars 2.28k forks source link

Botkit with QnA Integration keeps repeating itself in Slack #2134

Closed mattw114862 closed 3 years ago

mattw114862 commented 3 years ago

Integrating QnA Maker with Slack through botkit seems to cause an issue where the response from the bot keeps repeating itself. I am unsure of how to resolve this. I thought maybe it was reading the messages of itself and responding back but I am not so sure. I also have the same integration for teams and twilio both which work fine.

// `require('dotenv').config();

const express = require("express");
const bodyParser = require("body-parser");
// Import Botkit's core features
const { Botkit } = require("botkit");
// Import a platform-specific adapter for slack.
const { SlackAdapter, SlackEventMiddleware, SlackMessageTypeMiddleware } = require('botbuilder-adapter-slack');
const { BotFrameworkAdapter } = require('botbuilder');
const { TwilioAdapter } = require('botbuilder-adapter-twilio-sms');
const { QnAMaker } = require('botbuilder-ai');

// Create an express server with json and urlencoded body parsing
const webserver = express();
webserver.use((req, res, next) => {
    req.rawBody = '';
    req.on('data', function (chunk) {
        req.rawBody += chunk;
    });
    next();
});
webserver.use(express.json());
webserver.use(express.urlencoded({ extended: true }));

 //Create Bot Framework Adapter.
// See https://aka.ms/about-bot-adapter to learn more about adapters and how bots work.
const botAdapter = new BotFrameworkAdapter({
    appId: process.env.MicrosoftAppId,
   appPassword: process.env.MicrosoftAppPassword
});

// Adaptor for Slack
const slackAdaptor = new SlackAdapter({
    // REMOVE THIS OPTION AFTER YOU HAVE CONFIGURED YOUR APP!
    //enable_incomplete: true,
    // The following options are sufficient for a single team application
    // parameters used to secure webhook endpoint
    verificationToken: process.env.SLACK_VERIFICATION_TOKEN,
    clientSigningSecret: process.env.SLACK_CLIENT_SIGNING_SECRET,
    // auth token for a single-team app
    botToken: process.env.SLACK_BOT_TOKEN
});

// QnA Integration
const qnaMaker = new QnAMaker({
    knowledgeBaseId: process.env.QnAKnowledgebaseId,
    endpointKey: process.env.QnAAuthKey,
    host: process.env.QnAEndpointHostName
});

// Use Slack Middleware to emit events that match their original Slack event types.
slackAdaptor.use(new SlackMessageTypeMiddleware());
slackAdaptor.use(new SlackEventMiddleware());

// Controller for Slack
const slackController = new Botkit({
    webhook_uri: process.env.SLACK_WEBHOOK_URI,
    adapter: slackAdaptor,
    webserver: webserver
});

// Controller for Teams
const teamsController = new Botkit({
    webhook_uri: process.env.TEAMS_WEBHOOK_URI,
    adapterConfig: {
        appId: process.env.TEAMS_APP_ID,
        appPassword: process.env.TEAMS_APP_PASSWORD,
    },
    webserver: webserver
});

// QnA Integration for Teams
teamsController.middleware.ingest.use(async (bot, message, next) => {
    if (message.incoming_message.type === 'message') {
        const qnaResults = await qnaMaker.getAnswers(message.context);
        if (qnaResults[0]) {
            // If we got an answer, send it directly
            //await message.context.sendActivity(qnaResults[0].answer);
            await bot.reply(message, qnaResults[0].answer); // also works
        } else {
            // If we have no other features, we could just say we didn't find any answers:
            //await message.context.sendActivity('No QnA Maker answers were found.');
            // Otherwise, just forward to the next BotHandler to see if there are any other matches
            next();
        }
    } else {
        next();
    }
});

// QnA Integration for Slack
slackController.middleware.ingest.use(async (bot, message, next) => {
    if (message.incoming_message.type === 'message') {
        const qnaResults = await qnaMaker.getAnswers(message.context);
        if (qnaResults[0]) {
            // If we got an answer, send it directly
            //await message.context.sendActivity(qnaResults[0].answer);
            await bot.reply(message, qnaResults[0].answer); // also works
        } else {
            // If we have no other features, we could just say we didn't find any answers:
            //await message.context.sendActivity('No QnA Maker answers were found.');
            // Otherwise, just forward to the next BotHandler to see if there are any other matches
           next();
        }
    } else {
      next();
    }
});

//Adapter and Controller for Twilio
const smsadapter = new TwilioAdapter({
    twilio_number: process.env.TWILIO_NUMBER,
    account_sid: process.env.TWILIO_ACCOUNT_SID,
    auth_token: process.env.TWILIO_AUTH_TOKEN,
});

const smsController = new Botkit({
    adapter: smsadapter,
    webserver: webserver
});

// QnA Integration for Twilio
smsController.middleware.ingest.use(async (bot, message, next) => {
    if (message.incoming_message.type === 'message') {
        const qnaResults = await qnaMaker.getAnswers(message.context);
        if (qnaResults[0]) {
            // If we got an answer, send it directly
            //await message.context.sendActivity(qnaResults[0].answer);
            await bot.reply(message, qnaResults[0].answer); // also works
        } else {
            // If we have no other features, we could just say we didn't find any answers:
            //await message.context.sendActivity('No QnA Maker answers were found.');
            // Otherwise, just forward to the next BotHandler to see if there are any other matches
            next();
        }
    } else {
        next();
    }
});

// Catch-all for errors.
botAdapter.onTurnError = async (context, error) => {
    // This check writes out errors to console log
    // NOTE: In production environment, you should consider logging this to Azure
    //       application insights.
    console.error(`\n [onTurnError]: ${ error }`);
    // Send a message to the user
    await context.sendActivity(`Oops. Something went wrong!`);
    // Clear out state
    await conversationState.delete(context);
};

// load developer-created local custom feature modules.
// Load common features for slack and teams
slackController.loadModules(__dirname + '/features/common');
teamsController.loadModules(__dirname + '/features/common');
// Load platform specific features seperately
slackController.loadModules(__dirname + '/features/slack');
teamsController.loadModules(__dirname + '/features/teams');
smsController.loadModules(__dirname + '/features/twilio');

// For running server locally for development purposes
if (require.main === module) {
    const http = require("http");
    const port = process.env.PORT || 3000;
    const httpserver = http.createServer(webserver);
    httpserver.listen(port, function () {
        console.log("Slack Webhook endpoint online:  http://127.0.0.1:" + port + process.env.SLACK_WEBHOOK_URI);
        console.log("Teams Webhook endpoint online:  http://127.0.0.1:" + port + process.env.TEAMS_WEBHOOK_URI);
    });
};`

image

benbrown commented 3 years ago

This is most likely due to the fact that your bot is "hearing" messages from itself. Make sure you are not processing messages FROM the bot.

mattw114862 commented 3 years ago

After playing around with it I was able to get it fixed. It seems to be an issue with the slack API and how it handles retries in POST. I noticed on Ngrok that it was posting multiple 200 OK responses. I can't really say for sure if it is because it was the bot trying to respond to it's on query, however, I did notice that it only seemed to do this when I had Microsoft's QnA Maker integrated into the bot's responses.

Doing some googling on other people reporting the same issue I was finally able to stop the repeats by adding a few snippets:

// Create an express server with json and urlencoded body parsing
const webserver = express();
webserver.use((req, res, next) => {
    res.set("X-Slack-No-Retry", "1"); // disable retries
    res.status(200).json();
    req.rawBody = '';
    req.on('data', function (chunk) {
        req.rawBody += chunk;
    });
    next();
    return;
});
webserver.use(express.json());
webserver.use(express.urlencoded({ extended: true }));

Hope this helps any one else who experiences the same issue.

benbrown commented 3 years ago

Ah! This happens when the 200 response takes too long to be sent, which can happen when using external services like QNA Maker.

Thanks for the follow-up!