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
:
Import
boto3
: You have addedimport boto3
to use the AWS SDK for Python.Initialize Bedrock Client: You initialize a
bedrock_runtime
client usingboto3.client()
. You'll need to configure your AWS credentials and region. You can replaceanthropic.claude-v2
with your chosen model ID.Use
ChatBedrock
fromlangchain_aws
: You initialize aChatBedrock
LLM object fromlangchain
, providing the model ID and the Bedrock client.Configure
model_kwargs
: If needed, you can adjust parameters likemax_tokens_to_sample
inmodel_kwargs
when creating theChatBedrock
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:
Navigate to
/quizzes/new
.Enter quiz content, the number of questions, and the number of options.
Click "Create Quiz."
You will be redirected to the quiz detail page, where you can see the generated questions and options.
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!