Creating an AI-Powered Quiz Generator with FastAPI and AWS Bedrock

This article will guide beginner Python developers on creating a web application that generates quizzes using the power of AI. You will learn about FastAPI, SQLModel, and the AWS Bedrock API while building a quiz generator. The application will allow users to input text content, specify the number of questions and options, and then automatically generate a quiz using a large language model (LLM) from Amazon Bedrock.

Development Environment

To build your quiz generator, you first need to set up our Python development environment and install the required packages.

In addition to Python 3, you will be using:

  • virtualenv – a tool for creating isolated Python environments

  • FastAPI – a modern, fast web framework for building APIs with Python 3.7+

  • SQLModel – a library for interacting with SQL databases from Python code, based on Python type hints

  • Jinja2 – a fast, expressive, extensible templating engine

  • boto3 - AWS SDK for Python to interact with AWS services, including Bedrock

  • langchain - a framework for developing applications with large language models

  • python-decouple - a library for managing settings and separating sensitive information from the codebase

  • uvicorn - an ASGI web server implementation for Python.

  • alembic - a lightweight database migration tool for usage with the SQLAlchemy Database Toolkit for Python.

First, create a virtual environment using virtualenv. Virtual environments isolate your Python setup on a per-project basis, preventing conflicts between project dependencies.

On Windows, Linux, or macOS:

Code snippet

mkdir quiz_generator
cd quiz_generator

The next step is to make our virtual environment. This will be called environment. Make sure the name you choose for your virtualenv is in lower case with no special characters and spaces.

Code snippet

python3 -m venv environment

Enter the quiz_generator directory:

Code snippet

cd quiz_generator

Activate the virtual environment:

Code snippet

environment\Scripts\activate

For Linux and macOS:

Enter the quiz_generator directory:

Code snippet

cd quiz_generator

Activate the virtual environment:

Code snippet

source environment/bin/activate

You will know that you have virtualenv started when you see that the prompt in your console is prefixed with (environment).

Next, you will be installing dependencies. Create a requirements.txt file. This file contains a list of items to be installed using the pip install command.

For your use case, you need to create a new file using the text editor of our choice in the quiz_generator directory, and save the file as requirements.txt. In your requirements.txt file, type the following:

Code snippet

alembic==1.14.0
email-validator==2.2.0
fastapi==0.115.6
fastapi-login==1.10.2
jinja2==3.1.5
boto3==1.34.13
langchain==0.3.13
langchain_aws
botocore==1.34.13
passlib==1.7.4
playwright==1.49.1
pydantic==2.10.4
python-decouple==3.8
python-multipart==0.0.19
sqlmodel==0.0.22
starlette==0.41.3
uvicorn==0.32.1

On the command line, use the following command to install the dependencies:

Code snippet

pip install -r requirements.txt

This will install the updated versions of the listed libraries.

Creating our Quiz Generator Application

The code for your quiz generator will be broken apart into several files, each with a specific purpose. The main files are: main.py, models.py, prompts.py, schemas.py, and database.py.

models.py

This file defines the SQLModel models for your database: Quiz, Question, and Option.

Python

from sqlmodel import Field, SQLModel, Relationship

class Quiz(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    content: str = Field(max_length=100000)
    number_of_questions: int
    number_of_options: int

    questions: list["Question"] = Relationship(back_populates="quiz")


class Question(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    quiz_id: int | None = Field(default=None, foreign_key="quiz.id")
    quiz: Quiz = Relationship(back_populates="questions")
    question: str

    options: list["Option"] = Relationship(back_populates="question") 


class Option(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    question_id: int | None = Field(default=None, foreign_key="question.id")
    question: Question = Relationship(back_populates="options")        
    option: str
    is_correct: bool = Field(default=False)

schemas.py

This file defines Pydantic schemas for data validation and response models. These will be used for the output parser in prompts.py.

Python

from typing import List, Dict, Optional, Union

from pydantic import BaseModel, Field


class QuizSchema(BaseModel):
    questions: Dict[str, List[str]] = Field(description='Dictionary of questions and their list of options. The key is question and the value is a '
                                                        'List of options')

You need to create a .env file in the root directory of your project and add your AWS credentials:

Plaintext

AWS_ACCESS_KEY_ID=YOUR_ACCESS_KEY
AWS_SECRET_ACCESS_KEY=YOUR_SECRET_KEY
AWS_REGION_NAME=YOUR_REGION # e.g., us-west-2

prompts.py

This file contains the logic for interacting with the AWS Bedrock to generate quiz questions and options.

Python

import asyncio
import boto3
import json

from langchain_core.prompts import PromptTemplate
from langchain_aws import ChatBedrock
from langchain.output_parsers import PydanticOutputParser
from decouple import config

from .schemas import QuizSchema

async def generate_quizzes(number_of_questions, number_of_options, text):
    generated_questions_output_parser = PydanticOutputParser(pydantic_object=QuizSchema)
    generated_questions_format_instructions = generated_questions_output_parser.get_format_instructions()

    # Define prompt template
    generate_questions_template = """
                    This is a text below. Generate {number_of_questions} questions based on this. 
                    Each question should return {number_of_options} options. One of the option should be an answer. 
                    The answer is in UPPERCASE. The remaining options are in lowercases. 

                    The text is {text} 
                    The number_of_questions is: {number_of_questions}
                    The number_of_options is: {number_of_options}  

                    Format instructions: {format_instructions}
                    """
    generate_questions_prompt = PromptTemplate(
        template=generate_questions_template,
        input_variables=["number_of_questions", "number_of_options", 'text'],
        partial_variables={"format_instructions": generated_questions_format_instructions})

    # Initialize Bedrock client using boto3
    bedrock_runtime = boto3.client(
        service_name="bedrock-runtime",
        region_name=config("AWS_REGION_NAME"),  # Replace with your desired AWS region
        aws_access_key_id=config("AWS_ACCESS_KEY_ID"),
        aws_secret_access_key=config("AWS_SECRET_ACCESS_KEY"),
    )

    # We will use Claude model here as an example
    llm = ChatBedrock(
        model_id="anthropic.claude-v2", client=bedrock_runtime, model_kwargs={"max_tokens_to_sample": 500}
    )

    generated_questions = generate_questions_prompt | llm | generated_questions_output_parser

    tasks = [
        generated_questions.ainvoke({"number_of_questions": number_of_questions,
                                     "number_of_options": number_of_options,
                                     'text': text})
    ]
    list_of_tasks = await asyncio.gather(*tasks)

    return list_of_tasks

Changes in prompts.py:

  1. Import boto3: You have added import boto3 to use the AWS SDK for Python.

  2. Initialize Bedrock Client: You initialize a bedrock_runtime client using boto3.client(). You'll need to configure your AWS credentials and region. You can replace anthropic.claude-v2 with your chosen model ID.

  3. Use ChatBedrock from langchain_aws: You initialize a ChatBedrock LLM object from langchain, providing the model ID and the Bedrock client.

  4. Configure model_kwargs: If needed, you can adjust parameters like max_tokens_to_sample in model_kwargs when creating the ChatBedrock object.

database.py

This file handles database connection and session management using SQLModel.

Python

from sqlmodel import create_engine, Session, SQLModel

from decouple import config


sqlite_file_name = config("QUIZ_GENERATOR_SQLITE_FILE_NAME")
sqlite_url = f"sqlite:///{sqlite_file_name}"

connect_args = {"check_same_thread": False}
engine = create_engine(sqlite_url, echo=True)


def create_db_and_tables():
    SQLModel.metadata.create_all(engine)


def get_session():
    with Session(engine) as session:
        yield session

This code creates an SQLite database engine and defines functions to create database tables and get a database session.

You will need to add QUIZ_GENERATOR_SQLITE_FILE_NAME=database.db to your .env file.

main.py

This file contains the main FastAPI application logic, including routes and integration with other modules.

Let's start by creating the FastAPI application and defining the startup event to create the database tables:

Python

from fastapi import FastAPI, Depends, status, HTTPException, Form, Request
from fastapi.responses import HTMLResponse, RedirectResponse
from sqlmodel import Session, select   
from sqlalchemy.orm import selectinload
from starlette.templating import Jinja2Templates
from starlette.staticfiles import StaticFiles

from . import models
from .database import create_db_and_tables, get_session
from .prompts import generate_quizzes

app = FastAPI()

# ... (code for static files and templates)

@app.on_event("startup")
def on_startup():
    create_db_and_tables()

# ... (other routes will be added here)

You also need to configure static files and templates. Add the following code to main.py:

Python

script_dir = os.path.dirname(__file__)
st_abs_file_path = os.path.join(script_dir, "static/")
app.mount("/static", StaticFiles(directory=st_abs_file_path), name="static")

templates = Jinja2Templates(directory="app/templates")

Adding Routes to main.py

Now, add the routes to our main.py file to handle the different pages and API endpoints:

Python

# ... (previous code in main.py)

@app.get("/", response_class=HTMLResponse)
async def home(request: Request):
    return RedirectResponse(url="/quizzes")

@app.get("/quizzes", response_class=HTMLResponse)
async def list_quizzes_page(request: Request, session: SessionDep):
    statement = (
        select(models.Quiz)
        .options(
            selectinload(models.Quiz.questions)
            .selectinload(models.Question.options)
        )
    )
    quizzes = session.exec(statement).all()
    return templates.TemplateResponse("list_quizzes.html", {"request": request, "quizzes": quizzes})


@app.get("/quizzes/new", response_class=HTMLResponse)
async def create_quiz_page(request: Request):
    return templates.TemplateResponse("create_quiz.html", {"request": request})


@app.post("/quizzes/")
async def create_quiz(request: Request, session: SessionDep, content: str = Form(...), number_of_questions: int = Form(...), number_of_options: int = Form(...)):
    # Create quiz object
    quiz = models.Quiz(content=content, number_of_questions=number_of_questions, number_of_options=number_of_options)

    # Add to database
    session.add(quiz)
    session.commit()
    session.refresh(quiz)

    # Generate questions using your existing function
    generated_content = await generate_quizzes(number_of_questions=number_of_questions, 
                                         number_of_options=number_of_options, 
                                         text=content)

    generated_content = generated_content[0]

    # Create questions and options
    for question_text, options_list in generated_content.questions.items():
        db_question = models.Question(
            quiz_id=quiz.id,
            question=question_text
        )
        session.add(db_question)
        session.commit()

        for option_text in options_list:
            is_correct = option_text.isupper()
            db_option = models.Option(
                question_id=db_question.id,
                option=option_text.lower(),
                is_correct=is_correct
            )
            session.add(db_option)
        session.commit()

    return RedirectResponse(url=f"/quizzes/{quiz.id}", status_code=303)


@app.get("/quizzes/{quiz_id}", response_class=HTMLResponse)
async def quiz_detail_page(quiz_id: int, request: Request, session: SessionDep):
    statement = (
        select(models.Quiz)
        .options(
            selectinload(models.Quiz.questions)
            .selectinload(models.Question.options)
        )
        .where(models.Quiz.id == quiz_id)
    )

    quiz = session.exec(statement).first()
    if not quiz:
        raise HTTPException(status_code=404, detail="Quiz not found")

    return templates.TemplateResponse("quiz_detail.html",  {"request": request, "quiz": quiz})

These routes handle:

  • /: Redirects to the /quizzes route.

  • /quizzes: Displays a list of all quizzes.

  • /quizzes/new: Displays the form to create a new quiz.

  • /quizzes/: Handles the creation of a new quiz (POST request).

  • /quizzes/{quiz_id}: Displays the details of a specific quiz.

Creating the User Interface

The user interface consists of HTML templates using Jinja2 templating. We have four main templates:

  • base.html: Base template with common elements like navigation.

  • create_quiz.html: Form for creating a new quiz.

  • list_quizzes.html: Page for listing all quizzes.

  • quiz_detail.html: Page for viewing details of a specific quiz.

base.html

HTML

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% block title %}Quiz App{% endblock %}</title>
    <script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-gray-50">
    <nav class="bg-white shadow-lg">
        <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
            <div class="flex justify-between h-16">
                <div class="flex">
                    <a href="/" class="flex items-center text-xl font-bold text-indigo-600">
                        Quiz App
                    </a>
                </div>
                <div class="flex space-x-4">
                    <a href="/quizzes" class="inline-flex items-center px-3 py-2 text-sm font-medium text-gray-700 hover:text-indigo-600">
                        All Quizzes
                    </a>
                    <a href="/quizzes/new" class="inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 rounded-md">
                        Create Quiz
                    </a>
                </div>
            </div>
        </div>
    </nav>

    <main class="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
        {% block content %}{% endblock %}
    </main>
</body>
</html>

create_quiz.html

{% extends "base.html" %}

{% block title %}Create New Quiz{% endblock %}

{% block content %}
<div class="md:grid md:grid-cols-3 md:gap-6">
    <div class="md:col-span-1">
        <div class="px-4 sm:px-0">
            <h3 class="text-lg font-medium leading-6 text-gray-900">Create New Quiz</h3>
            <p class="mt-1 text-sm text-gray-600">
                Provide the content for your quiz and specify the number of questions and options.
            </p>
        </div>
    </div>
    <div class="mt-5 md:mt-0 md:col-span-2">
        <form method="POST" action="/quizzes/">
            <div class="shadow sm:rounded-md sm:overflow-hidden">
                <div class="px-4 py-5 bg-white space-y-6 sm:p-6">
                    <div>
                        <label for="content" class="block text-sm font-medium text-gray-700">Quiz Content</label>
                        <div class="mt-1">
                            <textarea id="content" name="content" rows="10" 
                                class="shadow-sm focus:ring-indigo-500 focus:border-indigo-500 mt-1 block w-full sm:text-sm border border-gray-300 rounded-md" 
                                required></textarea>
                        </div>
                    </div>

                    <div class="grid grid-cols-2 gap-6">
                        <div>
                            <label for="number_of_questions" class="block text-sm font-medium text-gray-700">
                                Number of Questions
                            </label>
                            <input type="number" name="number_of_questions" id="number_of_questions" min="1"
                                class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md"
                                required>
                        </div>
                        <div>
                            <label for="number_of_options" class="block text-sm font-medium text-gray-700">
                                Options per Question
                            </label>
                            <input type="number" name="number_of_options" id="number_of_options" min="2"
                                class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md"
                                required>
                        </div>
                    </div>
                </div>
                <div class="px-4 py-3 bg-gray-50 text-right sm:px-6">
                    <button type="submit" 
                        class="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-7                        <div>
                            <label for="number_of_options" class="block text-sm font-medium text-gray-700">
                                Options per Question
                            </label>
                            <input type="number" name="number_of_options" id="number_of_options" min="2"
                                class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md"
                                required>
                        </div>
                    </div>
                </div>
                <div class="px-4 py-3 bg-gray-50 text-right sm:px-6">
                    <button type="submit" 
                        class="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
                        Create Quiz
                    </button>
                </div>
            </div>
        </form>
    </div>
</div>
{% endblock %}

list_quizzes.html


{% extends "base.html" %}

{% block title %}All Quizzes{% endblock %}

{% block content %}
<div class="px-4 sm:px-6 lg:px-8">
    <div class="sm:flex sm:items-center">
        <div class="sm:flex-auto">
            <h1 class="text-xl font-semibold text-gray-900">All Quizzes</h1>
        </div>
    </div>

    <div class="mt-8 grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
        {% for quiz in quizzes %}
        <div class="bg-white overflow-hidden shadow rounded-lg">
            <div class="px-4 py-5 sm:p-6">
                <h3 class="text-lg leading-6 font-medium text-gray-900">Quiz #{{ quiz.id }}</h3>
                <div class="mt-2 max-w-xl text-sm text-gray-500">
                    <p>{{ quiz.content[:200] }}...</p>
                </div>
                <div class="mt-3 text-sm text-gray-500">
                    <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800 mr-2">
                        {{ quiz.number_of_questions }} Questions
                    </span>
                    <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
                        {{ quiz.number_of_options }} Options
                    </span>
                </div>
            </div>
            <div class="bg-gray-50 px-4 py-4 sm:px-6">
                <a href="/quizzes/{{ quiz.id }}" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-indigo-600 bg-white hover:bg-gray-50">
                    View Quiz
                </a>
            </div>
        </div>
        {% endfor %}
    </div>
</div>
{% endblock %}

quiz_detail.html

{% extends "base.html" %}

{% block title %}Quiz Detail{% endblock %}

{% block content %}
<div class="bg-white shadow overflow-hidden sm:rounded-lg">
    <div class="px-4 py-5 sm:px-6">
        <h3 class="text-lg leading-6 font-medium text-gray-900">Quiz #{{ quiz.id }}</h3>
    </div>
    <div class="border-t border-gray-200 px-4 py-5 sm:px-6">
        <div class="prose max-w-none">
            {{ quiz.content }}
        </div>
    </div>
</div>

<div class="mt-8">
    <h2 class="text-lg font-medium text-gray-900 mb-4">Questions</h2>

    {% for question in quiz.questions %}
    <div class="bg-white shadow overflow-hidden sm:rounded-lg mb-6">
        <div class="px-4 py-5 sm:px-6 bg-gray-50">
            <h3 class="text-lg leading-6 font-medium text-gray-900">
                Question {{ loop.index }}
            </h3>
            <p class="mt-1 max-w-2xl text-sm text-gray-500">
                {{ question.question }}
            </p>
        </div>
        <div class="border-t border-gray-200">
            <ul class="divide-y divide-gray-200">
                {% for option in question.options %}
                <li class="px-4 py-4 sm:px-6 {% if option.is_correct %}bg-green-50{% endif %}">
                    <div class="flex items-center justify-between">
                        <span class="text-sm font-medium text-gray-900">
                            {{ option.option }}
                        </span>
                        {% if option.is_correct %}
                        <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
                            Correct Answer
                        </span>
                        {% endif %}
                    </div>
                </li>
                {% endfor %}
            </ul>
        </div>
    </div>
    {% endfor %}

    <div class="mt-6">
        <a href="/quizzes" class="inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
            Back to All Quizzes
        </a>
    </div>
</div>
{% endblock %}

Testing and Running the Application

To test our quiz generator, we can run the application using uvicorn:

Code snippet

uvicorn app.main:app --reload

This will start the development server. You can then access the application in your browser, typically at http://127.0.0.1:8000.

You should be able to:

  1. Navigate to /quizzes/new.

  2. Enter quiz content, the number of questions, and the number of options.

  3. Click "Create Quiz."

  4. You will be redirected to the quiz detail page, where you can see the generated questions and options.

  5. Navigate to /quizzes to see a list of all created quizzes.

What's Next

Congratulations! You now have a functioning AI-powered quiz generator that uses AWS Bedrock. You can improve this application by:

  • Adding User Authentication: Implement user accounts and authentication to restrict quiz creation and management.

  • Improving Error Handling: Handle potential errors, such as failures in AI generation or database operations.

  • Enhancing the User Interface: Improve the UI with features like searching, filtering, and pagination for quizzes.

  • Adding More Features: Consider adding features like editing quizzes, deleting quizzes, or taking quizzes online.

  • Deploying the Application: Deploy the application to a server so it can be accessed by others. You can use platforms like AWS, or other cloud providers.

  • Trying Different Bedrock Models: Experiment with other models available in Amazon Bedrock to see how they perform for quiz generation.

I encourage you to play around with the code, experiment with different features of FastAPI and the AWS Bedrock API, and explore other interesting things you can build!