Imported from suncloudsmoon/quizzer (
AGENTS.md). Install upstream withnpx skills add suncloudsmoon/quizzer. Copyright stays with the author.
AGENTS.md
Project overview
A Duolingo-style quiz system that generates interactive courses from PDFs and serves them with spaced repetition, adaptive difficulty, and gamification. Built on the five pillars of the Duolingo Method (learn by doing, personalized learning, focus on what matters, stay motivated, feel the delight).
Architecture
src/quizzer/
app.py CLI entry point, Config model, LLM setup
quiz_creator.py PDF → Course JSON (offline, run once per document)
quiz_handler.py Serves questions at runtime via serve_course()
learner_state.py All per-learner runtime state (single JSON file)
paths.py Platform-specific paths via platformdirs
File layout (via platformdirs):
<config_dir>/config.json — LLM config (base_url, api_key, model)
<data_dir>/courses/<course-id>/course.json — read-only course content
<data_dir>/courses/<course-id>/images/ — page images
<data_dir>/progress/<course-id>.json — per-course learner state
Data flow: quiz_creator.py produces a read-only course.json. quiz_handler.py reads the course and reads/writes learner state in progress/. The course is never mutated after generation.
Serialization: All models use Pydantic BaseModel. Save via model_dump_json(), load via model_validate_json(). No dataclasses, no dacite, no custom to_dict/from_dict.
Key models
app.py:
Config— LLM connection settings (base_url,api_key,model), persisted toconfig.jsonQuizzer— CLI loop, wires together creator/handler/state
quiz_creator.py (Pydantic, frozen):
- 7 question types:
TrueFalseQuestion,MultipleChoiceQuestion,MatchingQuestion,SortingQuestion,FillInBlankQuestion,OrderingQuestion,FreeRecallQuestion Question= Union of all 7 typesLessonhasid,summary,questions,pages,page_numsCoursehasid,lessonsQuizCreatorhandles PDF parsing, VLM triage, question generation, scaffolding, chunking
learner_state.py (Pydantic, mutable):
SM2Stats— spaced repetition per question. Easiness seeded from question difficulty.LearnerModel— adaptive difficulty via exponential moving average (Birdbrain-lite)LearnerProgress— streaks, XP, levelsLessonProgress— per-lesson correctness tracking + completion status (replaces Lesson.got_correct/got_incorrect/status)Feedback— return type of FeedbackEngine.generate()ProcessResult— return type of process_answer()FullLearnerState— top-level container, single JSON file
Conventions
- All data-related classes are Pydantic
BaseModel, even if they have no fields. Never use@dataclassfor anything that gets serialized or holds data. - Use
Field(default_factory=...)for mutable defaults, notfield()from dataclasses. - Question types in quiz_creator.py are
frozen=True(immutable content). - Learner state models are mutable (get_sm2 returns a live reference you can mutate in place).
TYPE_ORDERdict in quiz_creator.py defines the scaffold ordering: receptive types first (TrueFalse=1) → productive types last (FreeRecall=7).- LLM calls go through
Settings.llmfrom llama_index. Structured output viaSettings.llm.as_structured_llm(OutputModel). - The
Confidenceenum has 4 levels: guessing, unsure, confident, certain. - The
QUALITY_MAPmaps (correct, confidence) → SM-2 quality score (0–5). Quality 2 = wrong but self-aware.
Dependencies
pydantic— all data modelsllama-index-core— LLM interface (Settings.llm,ChatMessage, structured output)llama-index-llms-openai-like— OpenAI-compatible LLM backendPyMuPDF(imported asfitz) — PDF page renderingplatformdirs— platform-specific config/data directoriestqdm— progress bars during course generation
Note: uuid7 is used from Python's stdlib uuid module (requires Python ≥3.14).
Commands
quizzer # Start the CLI (console script via pyproject.toml)
/create # Generate a course from a PDF
/study # Study an existing course (shows in-progress first)
/settings # View or change settings (LLM endpoint, API key, model)
/help # List available commands
# In-quiz commands (available after answering a question)
/chat # Open a multi-turn tutor chat about the current question
/done # Exit tutor chat and return to quiz
Testing
tests/test_regressions.pycontains lightweight regression tests for quiz flow edge cases.- Run them with
PYTHONDONTWRITEBYTECODE=1 python3 -m unittest tests.test_regressions -v. - The test module stubs external LLM/PDF dependencies so these checks can run without the full runtime stack installed.
- Current coverage includes continued-page triage merging, praise fallback behavior, true/false input validation, and review-session sampling without duplicate questions.
How spaced repetition works
SM-2 algorithm. Each question has a static difficulty (0–1, set by VLM at generation). On first encounter, SM2Stats.from_question_difficulty() seeds the easiness factor: difficulty 0.0 → easiness 2.5, difficulty 1.0 → easiness 1.3. After each answer, easiness drifts based on performance. Quality < 3 resets interval to 1 day. Quality ≥ 3 grows the interval (1 → 6 → easiness×previous).
How adaptive difficulty works
LearnerModel.theta tracks ability via exponential moving average. select_questions() picks questions where difficulty is within ±0.15 of theta (the zone of proximal development). When more questions are due than max_questions, the session filters to this range.
How lessons are served
build_session(): get due questions → filter by difficulty → scaffold (receptive first, easy first)- Main loop: present each question → collect answer + confidence →
process_answer()updates SM2, ability, XP, lesson progress → offer/chattutor - Resurface mistakes at end of lesson (second attempt, quality=3 if correct)
- End-of-lesson praise (effort-based, LLM-generated)
update_lesson_statuses(): linear unlock, ≥70% accuracy to complete- Review mode:
build_review_session()interleaves due questions across all completed lessons, mistakes weighted 2x
How tutor chat works
After answering any question the user can type /chat to open a multi-turn conversation with the LLM. The session is scoped to the current question — context does not carry across questions.
- System prompt includes: question details, the user's answer, correct/incorrect result, lesson summary, and behavioral rules (concise, encouraging, no new quiz questions).
- Page images from the lesson are attached as image blocks in the first user message, giving the LLM visual context of the source material.
- The LLM auto-generates an opening explanation via
TutorResponsestructured output, then the user can ask follow-up questions. - The session ends on
/done, empty input, or afterMAX_TUTOR_TURNS(5) exchanges. - Implemented via
tutor_chat()andprompt_continue_or_chat()inquiz_handler.py. UsesSettings.llm.as_structured_llm(TutorResponse)(structured output, not free-form chat).