Building the “Alexa, write my quiz” Skill Part 3: Generating Canvas Quizzes with the Canvas API

Mary Gwozdz,
Software Engineer
 

Over the past few months, I have been working on a project to build an Alexa skill that could utilize PDF files that instructors already have to generate quizzes. First, I built the Alexa skill based off of a Lex chatbot that a previous group had created, using the Canvas API to retrieve a PDF test bank or glossary file that would get converted into a quiz database. That quiz database could then either be output directly to the user or uploaded to Canvas as a text file. Second, I explored how this skill could be improved by using machine learning to convert the PDF into a quiz database. So far, this work has been a great testament to how Alexa Skills and machine learning can be used to save time and enhance the learning experience.

As a final improvement, I have upgraded this Alexa skill to include the option for the user to generate a Canvas quiz in addition to the existing option of generating a text document that is uploaded to the Canvas file system. This update consisted of two parts. First, I updated my Java class CanvasService that I had already created for uploading and downloading files from Canvas so that it would now also utilize the Canvas API to generate Canvas quizzes. Second, I updated the Alexa Skill to have an additional slot for the user to specify whether the quiz should be generated within Canvas’s Quiz system or as a text file in Canvas’s file system, and I wired that to my new methods in theCanvasService class.

Using the Canvas API to Generate Canvas Quizzes

The Canvas API stores quizzes separately from quiz questions. Therefore, in order to generate a non-empty quiz, I had to first perform a POST to create the quiz itself, then loop through each of my questions to generate them and tie them to the quiz.

Creating the Quiz

The code that I used for this is as follows:

public void postNewQuiz(String quizTitle, Map<String, List<String>> questionToChoice, Map<String, String> questionToAnswer) {
  // Create Quiz
  HttpPost post = new HttpPost(createQuizUrl);
  post.addHeader("Authorization", "Bearer " + apiToken);
  ArrayList<NameValuePair> postParameters = new ArrayList<>();
  String quizId = "";

  try (CloseableHttpClient client = HttpClients.createDefault()) {
      postParameters.add(new BasicNameValuePair("quiz[title]", WordUtils.capitalize(quizTitle)));
      post.setEntity(new UrlEncodedFormEntity(postParameters, "UTF-8"));
      CloseableHttpResponse response = client.execute(post);
      String responseBody = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8.name());
      Map<String, String> retrieveMap = mapper.readValue(responseBody, Map.class);
      quizId = String.valueOf(retrieveMap.get("id"));
  } catch (Exception e) {
      e.printStackTrace();
  }

  // Create Quiz Questions
  if (StringUtils.isNotBlank(quizId)) {
      for (String question : questionAnswerMap.keySet()) {
          postNewQuizQuestion(quizId, question, questionChoicesMap.get(question), questionAnswerMap.get(question));
      }
  }
}

The createQuizUrl variable is set to a value with the format <canvas_url>/api/v1/courses/<course_id>/quizzes via my properties file. With a few more updates, this could easily work with a variety of Canvas courses (by switching out the course_id or even a variety of Canvas URLs for multiple institutions or departments, but for my proof of concept, I just hardcoded both of them.

Next, I added the Authorization header to the POST, with its value having been generated in accordance with the Canvas documentation. For my proof of concept, I merely kept this value hardcoded in my local properties file; however, in a more robust implementation, this value would be encrypted and stored elsewhere, such as within a Spring Cloud Config Server.

Within the try-with-resources block, I added the only parameter necessary to create a Canvas quiz, which is the quiz title. This value is already retrieved in an Alexa Skill slot from the user and is passed to this method as one of the parameters. I utilized WordUtils from Apache Commons Text to capitalize the first letter of each word in the title.

Next, I set the parameters as a UTF-8 entity within the post and finally executed it, generating the quiz.

Generating the Quiz Questions

In order to generate the questions associated with this quiz, I needed to retrieve the quizId. I did this by using Apache’s IOUtils to convert the output json response from the POST into a string and then converting that string into a map using Jackson’s ObjectMapper.

After those conversions were complete, I could simply retrieve the id value from the map as shown in this line in the code above:

quizId = String.valueOf(retrieveMap.get("id"));

Then, I utilized the remaining parameters that were passed to this method to loop through all of the questions for the quiz by calling the next method that I wrote, which is shown below:

private void postNewQuizQuestion(String quizId, String question, List<String> choices, String answer) {
  HttpPost post = new HttpPost(createQuizUrl + "/" + quizId + "/questions");
  post.addHeader("Authorization", "Bearer " + apiToken);
  post.setHeader("Accept", "application/json");
  JSONArray answersJson = convertChoicesListToAnswersJson(choices, answer);
  JSONObject questionJson = new JSONObject()
          .put("question_text", "<p>" + question + "</p>")
          .put("question_type", "multiple_choice_question")
          .put("points_possible", 1)
          .put("answers", answersJson);
  String jsonBody = new JSONObject().put("question", questionJson).toString();

  try (CloseableHttpClient client = HttpClients.createDefault()) {
      StringEntity questionEntity = new StringEntity(jsonBody);
      post.setEntity(questionEntity);
      post.addHeader("content-type", "application/json");
      client.execute(post);
  } catch (Exception e) {
      e.printStackTrace();
  }
}

Within this method, I first created the POST with the format <canvas_url>/api/v1/courses/<course_id>/quizzes/<quiz_id>/questions and I added the Authorization header in the same manner as described above. Then I added the Accept header to indicate that I will be including a json body in my POST request.

The question json body must contain at least four fields: “question_text”, “question_type”, “points_possible”, and “answers”.

  • For the “question_text”, I simply wrapped the question parameter that was passed to the method inside a “<p></p>” tag as that was how Canvas would format questions when I created them in the Canvas UI.
  • For “question_type”, I put “multiple_choice_question” as all of my questions are multiple choice. However, Canvas has a large variety of question types that may be utilized listed in their documentation.
  • For “points_possible”, I simply hardcoded this value to 1; however, this could also be added as an additional slot in the Alexa Skill if users were interested in making this value customizable.
  • Finally, the “answers” field must consist of a json array with a specific format. In order to generate this json array, I wrote an additional method:
private JSONArray convertChoicesListToAnswersJson(List<String> choices, String answer) {
  JSONArray jsonAnswers = new JSONArray();
  for (String choice: choices) {
      JSONObject choiceJson = new JSONObject().put("text", choice);
      if (StringUtils.equals(choice, answer)) {
          choiceJson.put("weight", 100);
      } else {
          choiceJson.put("weight", 0);
      }
      jsonAnswers.put(choiceJson);
  }
  return jsonAnswers;
}

This method generates a json array with each array element containing two fields: “text” and “weight.” The “text” field consists of the text for the specific answer choice, and the “weight” field consists of the value 100 if this choice is the correct answer and the value 0 if this choice is incorrect. The json array is returned back to the postNewQuizQuestion method and placed as the value for the “answers” field in the question json object.

The question json object must be wrapped within a “question” field to complete the json body for generating the question as shown in the final json body:

"question": {
    "question_text": "<p>What is the best fruit?</p>",
    "question_type": "multiple_choice_question",
    "points_possible": 1,
    "answers": [
        {
            "text": "orange",
            "weight": 0
        },
        {
            "text": "apple",
            "weight": 100
        },
        {
            "text": "banana",
            "weight": 0
        },
        {
            "text": "pear",
            "weight": 0
        },
    ]
}

The json body is added as a string entity to the POST with an additional “content-type” header before being executed, generating the question as shown on these lines from above:

StringEntity questionEntity = new StringEntity(jsonBody);
post.setEntity(questionEntity);
post.addHeader("content-type", "application/json");
client.execute(post);

This process is repeated for all of the questions passed to the initial postNewQuiz method.

Adding Quiz Destination Slot to Alexa Skill

In order for Alexa to know where the user wants the quiz to be generated, I needed to add a quiz destination slot to the skill. For both Alexa Skill and Lex Chatbot creation, the word “slot” refers to a variable whose value is elicited from the user. The Alexa Skill Intent Handler that I had already written for my Alexa Skill described in Part 1 of this series included a method for validating that all of the necessary slots of information from the user consist of the expected formats. I named the new slot for the quiz destination "quizDest," in which the user will provide a value of either “Canvas quiz” or “file.” I added one more value to my map of slot question phrases such that if the slot "quizDest" is empty, then Alexa will ask the user, “What is the location you want the quiz to go to (file or Canvas quiz)?” I also added a list of acceptable values for "quizDest," including “canvasquiz” and “file” since I had defaulted to removing all spaces from user input. If a user does not provide one of the expected answers, then they will continuously be asked to answer the question.

Next, within the Alexa Developer Console, I needed to add the slot as well. Once I logged into the developer console, I clicked on “Slot Types” and then “+ Add Slot Type” as shown in Figure 1.

A3-Figure1

Figure 1: Viewing Slot Types in the Alexa Developer Console.

 

Then I entered “quizDest” as the name for a custom slot type and clicked “Next” as shown in Figure 2.

A3-Figure2

Figure 2: Adding Slot Type in the Alexa Developer Console

 

For “Slot Values” I entered “file” and “canvasquiz” and for “canvasquiz” I entered “canvas quiz” as a possible synonym as shown in Figure 3. Then I clicked on the “Save Model” button.

A3-Figure3

Figure 3: Adding Slot Values for the “quizDest” slot in the Alexa Developer Console.


Next, I clicked on “Interaction Model” then “Intents” and selected my “writeQuiz” intent. After scrolling to the bottom, I could see where to add “quizDest” as my sixth slot and select “quizDest” as the type for the slot as shown in Figure 4.

A3-Figure4

Figure 4: Adding the “quizDest” slot to the “writeQuiz” intent in the Alexa Developer Console.


Finally, I clicked “Save Model” and “Build Model” to complete this change.

After adding the "quizDest" slot, I needed to update all of the lambda functions in my pipeline to propagate the "quizDest" slot value down the line so that the quiz would go to the correct place. This consisted of being sure to include “quizDest” and its value in each request that triggered the next lambda function.

Finally, I updated my write-exam-lambda (described in Part 1 of this series) to have a separate method to run in the event that the user requests that the quiz destination be a “canvas quiz” instead of a “file.” This method retrieves the quiz questions from Athena in the exact same manner as the existing method for generating the file, with the only difference being that instead of formatting them like a quiz and placing them in a string, it now creates a question-to-answer map and a question-to-choices map, which are then passed to the postNewQuiz method described above.

Once all the lambdas were updated in AWS, the Alexa Skill was ready to be tested again as shown in Figure 5. It worked, and the resulting Canvas quiz is shown in Figure 6.

A3-Figure5

Figure 5: Testing updated Alexa Skill in the Alexa Developer Console.

A3-Figure6

Figure 6: Canvas Quiz generated from Alexa Skill test shown in Figure 5.

 

In conclusion, this project has demonstrated that the Canvas API can be utilized to automate quiz generation. I have explained how to accomplish this utilizing my existing Alexa Skill as the trigger to retrieve the required information from the user, then pulling the questions from an existing test bank or generating the test bank from a glossary file that had been uploaded to Canvas, then converting the test bank data into a Canvas quiz. I have also explained how to implement an additional slot within the Alexa Developer Console and tie up all of the loose ends within my AWS pipeline.

This mechanism could easily be further customizable to provide more flexibility to users, including a larger variety of question types, point values, due dates, and all of the other features that are available via the Canvas UI. In addition, this functionality could be separated from my Alexa Skill and instead added to a custom CMS dashboard or a custom Canvas app or a variety of other places to support automated quiz generation. Regardless, the progress made between these three segments has been only a small peek into what can possibly be done with Alexa and the Canvas API to help students and instructors save time and enhance the learning experience.

Useful Reading:

 

Mary Gwozdz

Mary Gwozdz

Software Engineer

Mary Gwozdz is a Software Engineer at Unicon, joining first as an intern in 2016 before becoming a full-time developer in 2018.

While at Unicon, Ms. Gwozdz has contributed numerous updates to the California Community Colleges (CCC) Application websites in efforts to modernize and improve its content and efficiency. She created Vagrant scripts to replace the environment setup process, decreasing the setup time from 1 month to 1 day. She also used Groovy and AWS to reconcile and improve the movement of encrypted data between its AWS architecture and other applications. Additionally, she wrote multiple Spring Boot REST web services for handling application data and deployed them as Docker containers to Rancher with Jenkins.

Before joining Unicon, Ms. Gwozdz did biomedical engineering research at the University of Texas. Her research resulted in two publications, one for her work in detecting genetic anomalies on a molecular level, and one for her development of a machine learning algorithm for detecting swallows in dysphagia patients.

Ms. Gwozdz graduated magna cum laude from the University of Texas at Austin with a BS in biomedical engineering with a technical emphasis in software engineering.

Top