Writing a chat bot quiz using the Microsoft Bot Framework

  • Tutorial

We have a tradition - every spring we participate in the Career Days of our beloved Novosibirsk State University, the main forge of our staff. And every year we come up with something interesting for students. This year we did a master class on how to write a chat bot. To register for the master class, they launched in Telegram their own bot "Academic" @academic_quiz_bot. He was gathered together at a master class.


image


If you haven't got a nice bot yet, now we’ll tell you how to choose a theme and, in fact, make a bot.



Step 1. Chatbot and Career Days, where is the connection?


It was not easy to come up with an interesting topic for the master class, because you have to compete with yourself. 2 years ago, in an hour and a half of a master class, we wrote a mobile application for an iPhone in front of students , which shows the weather near their alma mater at NSU (more than 5,000 downloads, and continues to be downloaded ). A year ago, we made an IoT solution in 1.5 hours and learned how to light a bulb from a smartphone. This year it was necessary to do something no less incendiary, and we decided to teach students how to make chat bots, because these virtual interlocutors are now experiencing explosive growth in popularity.


The idea to make a chat bot traditionally did not come from the ceiling - for one of our customers, we just developed a chat bot in Telegram, which allows territorial managers of the company to quickly manage projects — without assigning access to a stationary computer and fast Internet — assign, delegate tasks and monitor the status of their implementation.
In addition, the bot was a convenient way in a game form to gather students' contacts and invite to a master class.


image


Step 2. What are we going to chat about?


We definitely wanted to make Bot useful, so that he would continue to live and delight everyone after his Career Days. After much deliberation and controversy, such a topic was found! The basis was the popular online questionnaire of Alexei Kozionov in our city for knowledge of the Novosibirsk Academgorodok. And a week before the master class, we launched on the Telegram our chatbot “Academic” @academic_quiz_bot.


“Akademik” asks interesting questions (101 questions in total) with answer options about the life of his beloved district, such as: “What is called“ Toadstool ”in Akademgorodok,“ Which American president visited Akademgorodok? ”,“ Why is the neighborhood called “Щ”? ” , “How many steps does the staircase leading from the central beach to the bridge have?”, “Which of the Nobel Prize winners lived in Akademgorodok?” etc.


image


We supplemented the “Academics” with a sticker pack with encouraging phrases from our mascot - ebtman.


image


You can download sticker pack here.


In the first week of his life, the bot also worked as a registrar for a master class and carefully collected the participants' contacts. And now this is just an entertainment test. Anyone who knows Akademgorodok, Velkam pass the quiz.


Step. 3. Recipe chatbot quiz


Much has been written about the creation of a chat bot using the Microsoft Bot Framework on Habré. But the tutorials are few. Therefore, for everyone who has not yet washed down a virtual "talker", we tell our recipe.


image


First, create a project in Visual Studio using the Bot Application Template.


image


After creating, we immediately get the finished "Echo" bot, which repeats the message sent to him in the response. We can start the project with the F5 button. The browser will open the default page like this:


image


Copy the address and run the emulator.


image


Where we paste the copied address and append api / messages. Check that everything works.
Next, we will improve our bot. Make him ask his questions and check the answers.
First, we need data for questions. You can do this in many ways. Can be stored locally or downloaded from the cloud. We chose the second option.
Create a simple class for loading questions.


public class QuestService
{
    private static QuestService _instance;
    public static QuestService Instance => _instance ?? (_instance = new QuestService())
    private static string _url =                                                                
                   "https://academicquizbot.blob.core.windows.net/content/questions.txt";
    private readonly List _questions;
    public int QuestionsCount => _questions.Count;
    public QuestService()
    {
        var  root = DoRequest(_url);
        _questions = root.Questions;
         for (int i = 0; i < _questions.Count; i++)
         {
            _questions[i].Index = i;
         }
    }
    private T DoRequest(string requestStr) where T : class
    {
        var req = WebRequest.Create(new Uri(requestStr));
        req.Method = "GET";
        var response = req.GetResponse();
        var stream = response.GetResponseStream();
        if (stream == null)
        {
            return null;
        }
        using (var rstream = new StreamReader(stream))
        {
            var stringResult = rstream.ReadToEnd();
            var result = JsonConvert.DeserializeObject(stringResult);
            return result;
        }
    }
    public Question GetQuestion(List exceptIndexes)
    {
        var questions = _questions.Select(q => q)
                 .Where(q => !exceptIndexes.Contains(q.Index)).ToList();
        if (questions.Count == 0)
        {
            return null;
        }
        Random rand = new Random();
        return questions[rand.Next(questions.Count)];
    }
}

This class, when created, will load questions from the server and, if necessary, return the next question, excluding those to which the user has already answered.


Create a new class in Visual Studio called QuizDialog. And we implement the IDialog interface in it.


[Serializable]
public class QuizDialog : IDialog
{
    async Task IDialog.StartAsync(IDialogContext context)
    {
        context.Wait(MessageReceived);
    }
    private async Task MessageReceived(IDialogContext context, IAwaitable message)
    {
       await ShowQuestion(context);
    }
    private async Task ShowQuestion(IDialogContext context)
    {
    }
}

We implement the ShowQuestion function. First, upload a list of questions you have already completed from the repository.


var history = DataProviderService.Instance.GetAnswerStatistic(context.Activity.From.Id); 

And ask the next question


var exceptIndexes = history.Select(h => h.Index).ToList();
_question = QuestService.Instance.GetQuestion(exceptIndexes);
if (_question == null)
{
    context.Done("");
    return;
}

If there are no questions, then we consider the quiz passed and return control to the parent dialogue.
Next, we send the text of the question to the user.


StringBuilder questionText = new StringBuilder();
questionText.AppendLine(BoldString($"Вопрос №{history.Count + 1}:"));
questionText.AppendLine(_question.Text);
questionText.AppendLine(_question.ImageUrl);
var reply = context.MakeMessage();
reply.Text = questionText.ToString();
reply.TextFormat = "xml";
await context.PostAsync(reply);

Pictures can also be transmitted not through the url (although I believe that telegram is more reliable in that way), but through Attachment.


if (!string.IsNullOrWhiteSpace(_question.ImageUrl))
{
    reply.Attachments.Add(new Attachment("image/jpg", _question.ImageUrl));
}

BoldString Function Code:


private string BoldString(string text)
{
    if (string.IsNullOrEmpty(text))
    {
       return text;
    }
    return $"{text}";
}

We format the text in “xml” to divide the text into lines and highlight the line with question numbers in bold. It should be borne in mind that not all messengers support the same formats, here it is worth reading the documentation.


Далее нам нужно вывести список ответов. Будем выводить двумя способами. Так как у нас бывают ответы с длинным текстом. Такие мы будем выводить просто под номерами и кнопочки с номерами ответов после. А если ответы короткие (до 30 символов), то будем выводить их сразу в виде кнопочек.


bool isLongAnswer = _question.Answers.Any(a => a.Length > LongTextLength);
var answers = _question.Answers;
if (isLongAnswer)
{
     var answersReply = context.MakeMessage();
     StringBuilder text = new StringBuilder();
     answers = new List();
     for (int i = 0; i < _question.Answers.Count; i++)
     {
         text.AppendLine($"{i+1}) {_question.Answers[i]}");
         answers.Add($"{i + 1}");
     }
     answersReply.Text = text.ToString();
     answersReply.TextFormat = "xml";
     await context.PostAsync(answersReply);
}
ResumeAfter answerRecived = AnswerReceived;
if (isLongAnswer)
{
    answerRecived = LongAnswerReceived;
}
PromptDialog.Choice(context, answerRecived,
      new PromptOptions(
          prompt: "Выберите один из вариантов. \n /exit - для отмены",
          retry: null,
          tooManyAttempts: "Неверно",
          options: answers,
          attempts: 0,
          promptStyler: new PromptStyler(),
          descriptions: answers));

Собственно, функция PromptDialog.Choice показывает диалог с кнопочками. Далее нам понадобятся две функции для обработки выбора пользователя.


private async Task AnswerReceived(IDialogContext context, IAwaitable message)
{
    bool isCorrect = false;
    try
    {
        var answer = await message;
        isCorrect = answer == _question.CorrectAnswer;
    }
    catch
    {
        // ignored
    }
    await ShowResult(context, isCorrect);
}
private async Task LongAnswerReceived(IDialogContext context, IAwaitable message)
{
    bool isCorrect = false;
    try
    {
        var answer = await message;
        var answerNumber = int.Parse(answer) - 1;
        isCorrect = _question.Answers[answerNumber] == _question.CorrectAnswer;
    }
    catch
    {
        // ignored
    }
    await ShowResult(context, isCorrect);
}

В случае если пользователь написал что-то некорректное считаем его ответ неправильным. Вот такие мы злодеи.


Далее, нужно показать правильный ответ и статистику. И небольшим бонусов если пользователь общается с ботом через «telegram» подбодрить его стикером.


private async Task ShowResult(IDialogContext context, bool isCorrect)
{
    string isCorrectStr;
    Random rand = new Random();
    string sticker = "";
    if (isCorrect)
    {
        var index = rand.Next(Consts.CorrectAnswers.Length);
        isCorrectStr = Consts.CorrectAnswers[index];
        var stickerIndex = rand.Next(Consts.GoodStickers.Length);
        sticker = Consts.GoodStickers[stickerIndex];
    }
    else
    {
        var index = rand.Next(Consts.BadAnswers.Length);
        isCorrectStr = Consts.BadAnswers[index];
        var stickerIndex = rand.Next(Consts.BadStickers.Length);
        sticker = Consts.BadStickers[stickerIndex];
    }
    bool isTelegram = context.Activity.ChannelId == "telegram";
    if (isTelegram && rand.Next(100) > 30)
    {
        var bot = new TelegramBotClient(Consts.Telegram.Token);
        await bot.SendStickerAsync(context.Activity.From.Id, sticker);
    }
    var reply = context.MakeMessage();
    reply.Text = BoldString($"{isCorrectStr}");
    reply.TextFormat = "xml";
    await context.PostAsync(reply);
    var answer =  new AnswerStatistic { Index = _question.Index, IsCorrect = isCorrect };
    DataProviderService.Instance.AddAnswerStatistics(context.Activity.From.Id, answer);
    await ShowAnswerDescribe(context);
    await ShowStatistic(context);
    await ShowQuestion(context);
}
private async Task ShowAnswerDescribe(IDialogContext context)
{
    var reply = context.MakeMessage();
    reply.Text = _question.DescribeAnswer + _question.DescribeAnswerImageUrl;
    await context.PostAsync(reply);
}
private async Task ShowStatistic(IDialogContext context)
{
    var user = DataProviderService.Instance.GetUser(context.Activity.From.Id);
    var history = DataProviderService.Instance.GetAnswerStatistic(context.Activity.From.Id);
    var reply = context.MakeMessage();
    int correctCount = history.Count(h => h.IsCorrect);
    int questionsCount = QuestService.Instance.QuestionsCount;
    string text = $"{history.Count}/{questionsCount} Из них верно: {correctCount}";
    reply.Text = text;
    await context.PostAsync(reply);
}

Далее в MessagesController нужно вызвать наш диалог.


public async Task Post([FromBody]Activity activity)
{
    if (activity.Type == ActivityTypes.Message)
    {
        await Conversation.SendAsync(activity, () => new QuizDialog());
    }
    else
    {
        HandleSystemMessage(activity);
    }
    var response = Request.CreateResponse(HttpStatusCode.OK);
    return response;
}

После отладки в эмуляторе можно опубликовать бота. Вот тут это уже описано.


And our project lies entirely here.


Step 4. Summarizing


This student spring, we taught young people how to write their own chat bots and gathered a record number of participants for us at the master class. We met again with the fans  We have guys, in the sense of a guy and a girl, who have been coming to our master classes for the third year in a row and, finally, have grown to 4 courses. They say they will write a diploma and immediately to us (we still do not take students before the 4th year students).


And also, we ourselves enjoyed the communication with youth and inspiration for new projects! Well, we really love our quiz and recommend it to everyone who is familiar with the Academy - @academic_quiz_bot.


image


Also popular now: