How to Build a Personalized Unlimited Quiz App in Minutes: ChatGPT API Edition
Tags: chatgpt, tutorial, python, cloud
Published: 2023-03-11 07:02:52

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](https://www.youtube.com/@CodingIsFun) that introduced me to Streamlit. In [his video](https://youtu.be/Sb0A9i6d320), 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](https://www.engraved.blog/building-a-virtual-machine-inside/) or even a [Python interpreter](https://levelup.gitconnected.com/building-a-python-interpreter-inside-chatgpt-49251af35fea).

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](https://quizwhizai.streamlit.app/)
Or host your own using [this GitHub repo](https://github.com/dibakarroy1997/quizwhizai/blob/main/QuizWhizAI/main.py).

## 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](https://openai.com/blog/introducing-chatgpt-and-whisper-apis). They also have us covered with [python binding](https://pypi.org/project/openai/). 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:

```python
# 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)
```

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](https://i.imgur.com/tm6rqJ6.gif)

## Writing the Streamlit application

Streamlit provides you with many component and some thing which I used in the app are:
- [SideBar](https://docs.streamlit.io/library/api-reference/layout/st.sidebar)
- [Button](https://docs.streamlit.io/library/api-reference/widgets/st.button)
- [Radio buttons](https://docs.streamlit.io/library/api-reference/widgets/st.radio)
- [Text input](https://docs.streamlit.io/library/api-reference/widgets/st.text_input)
- [Download button](https://docs.streamlit.io/library/api-reference/widgets/st.download_button)

### Importing necessary modules and functions

```python
import json

import openai
import streamlit as st
from get_quiz import get_quiz_from_topic
```
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](https://i.imgur.com/vYXFY27.png)

```python
# 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()
```

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

```python
# 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
```

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](https://i.imgur.com/7vrQQY2.png)

```python
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}")
```

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](https://i.imgur.com/CjCHWHr.gif)

```python
# 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()
```

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](https://i.imgur.com/IM9V9zT.gif)

```python
# 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",
)
```

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](https://github.com/Dibakarroy1997/QuizWhizAI) and hosted in [Streamlit](https://quizwhizai.streamlit.app/).

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!