How to Build a Personalized Unlimited Quiz App in Minutes: ChatGPT API Edition

debakarroy

Debakar Roy

Posted on March 11, 2023

How to Build a Personalized Unlimited Quiz App in Minutes: ChatGPT API Edition

Folks are using the ChatGPT API for so many amazing ideas, and I couldn't resist the urge to try building something with it myself. A few months ago, YouTube recommended a video from Coding is Fun that introduced me to Streamlit. In his video, Sven built a dashboard using Streamlit from an existing Excel spreadsheet, and I was blown away. Although I've used tools like Splunk and PowerBI before, Streamlit seemed much more developer-friendly for some reason.

Then there's ChatGPT. When it was first introduced, people started using it for several activities, including making it act as a Virtual Machine or even a Python interpreter.

So, I had an idea: why not make use of the ChatGPT API and ask it to act as a REST server that sends back quiz questions on any topic in the world?

You can try out the final application here
Or host your own using this GitHub repo.

Using ChatGPT API

To start with, we first need to know how to use the ChatGPT API. OpenAI has a great article for that: Introducing ChatGPT and Whisper APIs. They also have us covered with python binding. Using it is quite easy. You just need to install the python binding, set up an API key, provide it some initial conversation, and then add the new user message.

To make it act like an API server for quiz, this was my approach:

# get_quiz.py
import json
from typing import Dict

import openai

# I have some chat history saved in a list, where each item is a dictionary representing a message with a role and content.
chat_history = [
    {
        "role": "system",
        "content": "You are a REST API server with an endpoint /generate-random-question/:topic, which generates unique random quiz question in json data.",
    },
    {"role": "user", "content": "GET /generate-random-question/devops"},
    {
        "role": "assistant",
        "content": '\n\n{\n    "question": "What is the difference between Docker and Kubernetes?",\n    "options": ["Docker is a containerization platform whereas Kubernetes is a container orchestration platform", " Kubernetes is a containerization platform whereas Docker is a container orchestration platform", "Both are containerization platforms", "Neither are containerization platforms"],\n    "answer": "Docker is a containerization platform whereas Kubernetes is a container orchestration platform",\n    "explanation": "Docker helps you create, deploy, and run applications within containers, while Kubernetes helps you manage collections of containers, automating their deployment, scaling, and more."\n}',
    },
    {"role": "user", "content": "GET /generate-random-question/jenkins"},
    {
        "role": "assistant",
        "content": '\n\n{\n    "question": "What is Jenkins?",\n    "options": ["A continuous integration server", "A database management system", "A programming language", "An operating system"],\n    "answer": "A continuous integration server",\n    "explanation": "Jenkins is an open source automation server that helps to automate parts of the software development process such as building, testing, and deploying code."\n}',
    },
]

# I define a function that takes a topic string and an API key, and returns a dictionary with a quiz question, options, answer, and explanation.
def get_quiz_from_topic(topic: str, api_key: str) -> Dict[str, str]:
    global chat_history

    # I set the OpenAI API key.
    openai.api_key = api_key

    # I make a copy of the chat history and add the user's message requesting a quiz question for the given topic.
    current_chat = chat_history[:]
    current_user_message = {
        "role": "user",
        "content": f"GET /generate-random-question/{topic}",
    }
    current_chat.append(current_user_message)
    chat_history.append(current_user_message)

    # I use the OpenAI API to generate a response based on the current chat history.
    response = openai.ChatCompletion.create(
        model="gpt-3.5-turbo", messages=current_chat
    )

    # I extract the quiz question from the response and add it to the chat history as an assistant message.
    quiz = response["choices"][0]["message"]["content"]
    current_assistent_message = {"role": "assistant", "content": quiz}
    chat_history.append(current_assistent_message)

    # I print the quiz question and return it as a dictionary.
    print(f"Response:\n{quiz}")
    return json.loads(quiz)
Enter fullscreen mode Exit fullscreen mode

With just 20-30 lines of code, we can now have an API server that generates quizzes on any topic we want in the required format.
Generate Quiz on Random topic

Writing the Streamlit application

Streamlit provides you with many component and some thing which I used in the app are:

Importing necessary modules and functions

import json

import openai
import streamlit as st
from get_quiz import get_quiz_from_topic
Enter fullscreen mode Exit fullscreen mode

The json module is used at the end where I want to export the questions in current session to a file which user can download and use later.

The openai module is used to catch exception if the user enters a wrong API key.

We have streamlit library which we will use extensively to build our application.

Next is the get_quiz_from_topic function which we created to get unlimited quiz on any topic.

Sidebar for user input

User input

# Input box to enter the topic of the quiz
topic = st.sidebar.text_input(
    "To change topic just enter in below. From next new quiz question the topic entered here will be used.",
    value="devops",
)

# Input box for OpenAI API key
api_key = st.sidebar.text_input("OpenAI API key", type="password").strip()
Enter fullscreen mode Exit fullscreen mode

The second section of the code defines the topic and api_key variables by getting input from the user through the Streamlit sidebar. The topic variable is a text input field where the user can enter the topic of the quiz they want to take. The api_key variable is a password input field where the user can enter their OpenAI API key to access the GPT-3 API.

Later in this sidebar we will add a download button to download all the questions in the current session.

Initilizing the Session State

# Initialize session state variables if they don't exist yet
if "current_question" not in st.session_state:
    st.session_state.answers = {}
    st.session_state.current_question = 0
    st.session_state.questions = []
    st.session_state.right_answers = 0
    st.session_state.wrong_answers = 0
Enter fullscreen mode Exit fullscreen mode

The third section of the code initializes the session state variables used by the application. The session state is a built-in feature of Streamlit that allows developers to store and retrieve user data across multiple user interactions. The session state variables are used to keep track of the user's quiz progress and answers.

Displaying Quiz Questions

Display Question

def display_question():
    # Handle first case
    if len(st.session_state.questions) == 0:
        try:
            first_question = get_quiz_from_topic(topic, api_key)
        except openai.error.AuthenticationError:
            st.error(
                "Please enter a valid OpenAI API key in the left sidebar to proceed. "
                "To know how to obtain the key checkout readme for this project here: https://github.com/Dibakarroy1997/QuizWhizAI/blob/main/README.md"
            )
            return
        st.session_state.questions.append(first_question)

    # Disable the submit button if the user has already answered this question
    submit_button_disabled = st.session_state.current_question in st.session_state.answers

    # Get the current question from the questions list
    question = st.session_state.questions[st.session_state.current_question]

    # Display the question prompt
    st.write(f"{st.session_state.current_question + 1}. {question['question']}")

    # Use an empty placeholder to display the radio button options
    options = st.empty()

    # Display the radio button options and wait for the user to select an answer
    user_answer = options.radio("Your answer:", question["options"], key=st.session_state.current_question)

    # Display the submit button and disable it if necessary
    submit_button = st.button("Submit", disabled=submit_button_disabled)

    # If the user has already answered this question, display their previous answer
    if st.session_state.current_question in st.session_state.answers:
        index = st.session_state.answers[st.session_state.current_question]
        options.radio(
            "Your answer:",
            question["options"],
            key=float(st.session_state.current_question),
            index=index,
        )

    # If the user clicks the submit button, check their answer and show the explanation
    if submit_button:
        # Record the user's answer in the session state
        st.session_state.answers[st.session_state.current_question] = question["options"].index(user_answer)

        # Check if the user's answer is correct and update the score
        if user_answer == question["answer"]:
            st.write("Correct!")
            st.session_state.right_answers += 1
        else:
            st.write(f"Sorry, the correct answer was {question['answer']}.")
            st.session_state.wrong_answers += 1

        # Show an expander with the explanation of the correct answer
        with st.expander("Explanation"):
            st.write(question["explanation"])

    # Display the current score
    st.write(f"Right answers: {st.session_state.right_answers}")
    st.write(f"Wrong answers: {st.session_state.wrong_answers}")
Enter fullscreen mode Exit fullscreen mode

The fourth section of the code defines the display_question() function, which is responsible for displaying the quiz question to the user. The function checks if there are any questions already loaded in the session state. If there are no questions, it calls the get_quiz_from_topic() function to retrieve a new question from the OpenAI API based on the user's selected topic.

Once the question is retrieved, the function displays it to the user using the st.write() method. The function also displays the multiple-choice options using the options.radio() method and allows the user to select their answer. If the user has already answered the current question, the function preselects the previously selected answer.

When the user submits their answer, the function checks if the answer is correct and displays feedback to the user. If the answer is correct, the function increments the right_answers session state variable. If the answer is incorrect, the function increments the wrong_answers session state variable. The function also displays an explanation of the correct answer using the st.expander() method.

The code also has error handling to ensure that it can handle certain types of errors and provide helpful messages to the user.

For example, if the user enters an invalid API key or fails to enter one altogether, an AuthenticationError is raised when trying to access OpenAI's API. The code catches this error and displays an error message to the user, asking them to enter a valid API key in the left sidebar.

Navigating Quiz Questions

Navigating Quiz Questions

# Define a function to go to the next question
def next_question():
    # Move to the next question in the questions list
    st.session_state.current_question += 1

    # If we've reached the end of the questions list, get a new question
    if st.session_state.current_question > len(st.session_state.questions) - 1:
        try:
            next_question = get_quiz_from_topic(topic, api_key)
        except openai.error.AuthenticationError:
            st.session_state.current_question -= 1
            return
        st.session_state.questions.append(next_question)


# Define a function to go to the previous question
def prev_question():
    # Move to the previous question in the questions list
    if st.session_state.current_question > 0:
        st.session_state.current_question -= 1
        st.session_state.explanation = None


# Create a 3-column layout for the Prev/Next buttons and the question display
col1, col2, col3 = st.columns([1, 6, 1])

# Add a Prev button to the left column that goes to the previous question
with col1:
    if col1.button("Prev"):
        prev_question()

# Add a Next button to the right column that goes to the next question
with col3:
    if col3.button("Next"):
        next_question()

# Display the actual quiz question
with col2:
    display_question()
Enter fullscreen mode Exit fullscreen mode

The fifth section of the code defines the next_question() and prev_question() functions, which are responsible for navigating between quiz questions. The next_question() function increments the current_question session state variable, and if there are no more questions loaded in the session state, it retrieves a new question from the OpenAI API based on the user's selected topic. The prev_question() function decrements the current_question session state variable.

The code also defines three columns using the st.columns() method to display the navigation buttons for the previous and next question. The display_question() function is displayed in the center column.

The order in which these methods are called is also important. You can see in-spite of display_question() funtion being part of column 2, is called after next_question() and prev_question() is called.

Download Quiz Data

Download Button

# Add download buttons to sidebar which download current questions
download_button = st.sidebar.download_button(
    "Download Quiz Data",
    data=json.dumps(st.session_state.questions, indent=4),
    file_name="quiz_session.json",
    mime="application/json",
)
Enter fullscreen mode Exit fullscreen mode

At the bottom of the sidebar, there is a download button that allows the user to download the data for the current quiz session in JSON format. The data includes the questions, answer options, correct answers, and explanations for each question.

The download_button() function from Streamlit is used to create the download button. This function takes in several arguments, including the text to display on the button, the data to be downloaded (in this case, the quiz session data), the file name, and the MIME type of the file.

The end result is then pushed to GitHub repository and hosted in Streamlit.

I hope you found this article insightful and inspiring, and that it has sparked your creativity to build something truly remarkable. Remember, the only limit is your imagination! So go out there and create something amazing of your own. Thank you for reading!

💖 💪 🙅 🚩
debakarroy
Debakar Roy

Posted on March 11, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related