Create upwards of 60 hours of educational content, complete with practice questions, given a topic phrase.
- AI Course Creator
- Ensure Python version of
3.12
or higher. (I personally recommend pyenv with the virtualenv extension). Repo contains pyenv.python-version
file with preferred version. - Ensure Node is installed with version
18.x
or higher.app/client
contains.nvmrc
file with preferred version.
git clone https://github.com/AlextheYounga/ai-course-creator.git
pip install -r requirements.txt
Use the example sqlite database which contains 13 courses on "Calculus From the Perspective of Python Programming"
unzip storage/example/python-calculus.db.zip && mv database.db db/database.db
Run python run.py
Running this command will automatically create default config files including a default .env
file, which is where we will store variables like our OpenAI API keys. Check this file and add your keys there.
APP_ENV=development
LLM_CLIENT=OPENAI
OPENAI_API_KEY=your-open-api-key
DB_URL='sqlite:///db/database.db'
OUTPUT_DIRECTORY='out'
(ai-course-creator) M1:ai-course-creator alexyounger$ python run.py
[?] Select command category:
Start Course Creator
Utilities
> Run App Server
This will install the required node packages, build the app, and then open the browser towards the correct localhost server.
(ai-course-creator) ai-course-creator$ python run.py
[?] Select command category:
> Start Course Creator
Utilities
Run App Server
[?] Which topic would you like to generate course material for?:
Create New
All
> Ruby on Rails
[?] Select task:
> Generate Outline
Generate Page Material With Interactives
Generate Page Material Only
Generate Interactives
Compile Interactives
Resume Job
You can also run new generations from the frontend, although bear in mind that the CLI has been prioritized over the frontend application, and there may be some instabilities in this form. There is also currently no way to view the progress of a new generation from the frontend; this can only be viewed from the terminal.
The jobs system was developed with tremendous help from the legend Billy W. Conn. I had never built a job queue from scratch, and I had trouble finding a simple Python jobs queue package. Although he didn't commit directly to this repo, he wrote most of the code in
src/jobs
, and I only adjusted the code in a few places, (and by adjusted I mean butchered his beautiful work). The code in that folder generally looks better than all other code in this application, but it looked even better before I got ahold of it. The comments in those files are also his.
Generate an outline of courses, their chapters, and pages, given a single topic. This will create a random number of courses, but can be as high as 25 courses.
Generate page material for each page in the course, as well as create interactive question for practice challenges. Interactives will be generated based on the material in each page. Kinds of interactive questions:
- Multiple Choice
- Fill in the Blank (this may be broken)
- Code Editor
- These are currently not runnable. One item on the TODO list is to add Judge0 code execution to handle this. All of the information exists to make this runnable, and testable.
- Codepen Embed
- Is not runnable but allows for interactive components within programming course content
Generate page material only, without interactives.
This will generate only interactives, but this is only possible if page material has been generated prior to this. Interactives are generated for each page.
Associate interactives with page material. This process happens automatically when generating page material with interactives, but there were times when I needed to run this process by itself, so I kept it.
Cut a job short? Here's how you can keep it running where you last left off. (May contain a few bugs)
Topics can be added in two places:
- Topics can be created either from the CLI by doing
Start Course Creator
->Create New
- From the frontend application by going to the
Topics
tab and hitting theCreate New
button
There was a specific emphasis on programming topics, but this can handle technically any topic you can think of. Ideally, the topic is at most a partial sentence. Something like
Good topics:
- "Sailing"
- "Advanced Sailing"
- "Sailing Around the World"
- "sailing around the world"
Bad Topics:
- "I want to learn sailing"
- "Yo lemme get that Jack Sparrow aura!"
Topics can also have their own configurations. For instance, maybe you don't want any coding related interactives on a topic about "Sailing". Currently, the only place we can add those topics settings are the in the configs/topics.yaml
.
Please see the topics.example.yaml
file to see an example of the available topic settings.
This is not ideal, and I have already built out the ability to change these settings from the Run New Generation
page, but I have yet to hook this up throughout the app. Currently, those settings do nothing. Sorry, it's a WIP people!
Had a ton of help with the prompts from JohnGaltjr, who also helped discover some interesting phenomenon with ChatGPT. Thanks!
Prompts are located in the storage/prompts
folder, and are broken up into distinct prompt "collections". You can assign a collection to each topic in the topic settings located in topics.yaml
. If a prompt does not exist in a particular collection, the prompt from storage/prompts/core
will be used.
If you want to change prompts, you can add a custom prompt collection by making a new folder in the storage/prompts
folder and then assign that collection to a topic in the configs/topics.yaml
file. Copy the specific prompt you want to
edit into this folder.
All course generations require an initial outline. The LLM will output a yaml structure of courses, with chapters and pages.
Example output from the LLM that will be used to create courses, chapters, and pages. By default, you'll get an output with approximately 25 objects shaped like the following.
- courseName: "An Intriguing Course Name"
chapters:
- name: "An Interesting Chapter Name"
pages:
- "First Page of Chapter"
- "Second Page of Chapter"
- "Third Page of Chapter, and so on"
- name: "Another Interesting Chapter Name"
pages:
- "First Page of Chapter"
These outlines can also be edited from the frontend App Mode under the Outlines
tab. You can also create a new outline from scratch.
[?] Select utility command:
> Backup Database
Dump Content From Existing Outline
Run DB Migrations
- Backup Database
- Will create a zipped backup of your database in the storage folder. Very useful
- Dump Content From Existing Outline
- Will dump all content from a particular outline in the
out/
folder in markdown format.
- Will dump all content from a particular outline in the
- Run DB Migrations
- This is used for making database changes. I had to write my own SQLite migrations system; it actually works surprisingly well.
The Course Creator uses an event/handler architecture, events are associated with event handlers, and all managed in Redis queues. All event handlers are processed by Redis as if they are a distinct job with no knowledge of each other. This creates a kind of "state machine" system, where every single action can be accounted for. This is a very practical system for any complex AI-based architecture, and allows for easy maintenance as well as a near-infinite number of configurations and flows.
The one tradeoff of this system: more code. More code is generally considered a no-no, but if you value *clarity above all, as I do, then this is an acceptable tradeoff. There is a lot of code, but it is very simple. If you want to create a new event-handler flow in order to facilitate a new feature, you can copy 80% of the code from another handler.
Example Event:
# src/events/events.py
class GenerateSomethingFromLLMRequested(Event):
def __init__(self, data, id=cuid()):
super().__init__()
self.id = id
self.data = data
class ProcessResponseFromLLM(Event):
def __init__(self, data, id=cuid()):
super().__init__()
self.id = id
self.data = data
Example Handler:
# src/handlers/generate_something_from_llm_handler.py
from src.events.events import ProcessResponseFromLLM
class GenerateSomethingFromLLMHandler:
def __init__(self, data: dict):
self.data = data
self.db = DB()
def handle(self) -> Outline:
response = self._get_response_from_llm()
# call another event
return ProcessResponseFromLLM(self.data)
def _get_response_from_llm(self) -> dict:
pass
Event Registry:
# src/events/events_registry.py
# EventRegistry.register(event, handler)
EventRegistry.register(GenerateSomethingFromLLMRequested, GenerateSomethingFromLLMHandler)
EventRegistry.register(ProcessResponseFromLLM, ProcessResponseFromLLMHandler)
Excalidraw of Event Handler Flow
I keep a running TODO list (docs/todo.md) of things I'd like to do. I am also willing to add to that todo list if there's something you want to see.
I will always read pull requests. I may not always merge pull requests. I definitely would love some help improving this. I will attempt to improve the documentation as time goes on.