Skip to content

Repository files navigation

Sapling

An AI-powered study companion that builds a live knowledge graph as you learn.

"preview"PythonTypeScriptNext.jsFastAPISupabaseGoogle GeminiD3.jsGit

Overview

Sapling is a study tool that adapts to how you learn. Chat with an AI tutor across three teaching modes, take adaptive quizzes, track assignments from your syllabus, and compare progress with classmates in study rooms. As you learn, a live knowledge graph maps your mastery in real time.

Features

  • Live Knowledge Graph — Your understanding is visualized as a growing node graph, with a 2D (D3/SVG, default) or 3D (WebGL) view toggle. Mastery scores update dynamically after every session and quiz, with per-course color shading and mastery-based opacity.
  • Three Teaching Modes — Socratic (guided reasoning), Expository (direct explanation), and TeachBack (you explain, Sapling corrects). Chat supports inline math (KaTeX), Mermaid diagrams, function plots, and theorem callouts.
  • Adaptive Quizzes — AI-generated quizzes targeting your weakest concepts, with difficulty scaling based on your performance and spaced-repetition scheduling that resurfaces concepts you've missed before.
  • Flashcards — Generate AI flashcards per course, or import them from paste, file (CSV/Markdown/Anki), URL, AI prompt, or photo. Study by topic with spaced-repetition ratings (Easy / Hard / Forgot).
  • Gradebook — Track your real-world grade per course. Categories + weights, per-assignment scores, per-course letter-scale overrides, and current grade calculation. Upload a syllabus and Sapling extracts categories and assignments automatically.
  • Gradescope Sync — Link a Sapling course to a Gradescope course and pull assignment grades in automatically. Sign in with a Gradescope email/password or, for BU accounts, a live SSO + Duo 2FA flow; credentials are stored encrypted and re-authenticated fresh on each sync.
  • Study Guide — Generate a Gemini-powered exam study guide from your uploaded course materials. Guides are cached per exam and can be regenerated at any time.
  • Class Intelligence — Aggregates anonymized class-wide patterns to surface common misconceptions and weak areas, personalizing your sessions.
  • Calendar & Syllabus Tracking — Paste your syllabus and Sapling extracts assignments, deadlines, and topics automatically.
  • Document Library — Upload PDFs and notes (up to 100 MB each); Sapling extracts summaries, key concept notes, and flashcard topics to enrich your knowledge graph and study guides. Uploads use a streaming SSE pipeline so the UI shows live per-phase progress ("Classifying..." → "Extracting summary, concepts, and syllabus..." → "Saved."). Concept notes can be re-scanned manually per doc or per course.
  • Notetaker — Write typed notes per course with debounced autosave and tags. Per note, Sapling can AI-summarize, extract concepts (merged into your knowledge graph and linked back to the note), answer questions in a note-grounded AI chat, send the note to the tutor, or generate a quiz targeting the note's weakest linked concept.
  • Study Rooms — Invite classmates, compare knowledge graphs, and track relative mastery across your group.
  • Room Chat — Real-time text chat with avatars inside each study room.
  • User Profiles — Public profiles with academic info, bio, featured achievements, and equipped cosmetics.
  • Achievements & Cosmetics — Unlock achievements by hitting milestones (sessions, quizzes, streaks). Equip cosmetic rewards like avatar frames, name colors, and title flairs.
  • Roles & Admin Panel — Role-based access control with an admin panel for user approval, role assignment, and content management.
  • Onboarding Flow — Multi-step onboarding that collects school, major, year, and courses after first sign-in. Sign-in itself is a popup-based Google OAuth flow launched from the landing page.
  • Newsletter — Beta-list signup directly from the landing page.
  • Feedback & Issue Reporting — Submit session feedback or report bugs directly from the app.

Tech Stack

  • Frontend — Next.js 16 (TypeScript, App Router). The knowledge graph renders in 2D via D3.js (default) or 3D via react-force-graph-3d (three.js/WebGL), lazy-loaded so the 3D stack only enters the bundle when toggled on. Vitest with jsdom + React Testing Library for unit + component tests.
  • Backend — FastAPI (Python) serving a REST API. Document ingestion runs through a Pydantic AI agentic pipeline (4 typed worker agents fanned out in parallel via asyncio.gather). Quiz generation, the chat tutor, syllabus extraction, and the notetaker (summary / concepts / chat) are also Pydantic AI agents — every LLM call in the backend goes through these agents; the legacy structured-prompt helper (services/gemini_service.py) was retired in ADR 0024.
  • AI — Google Gemini, with per-task model routing configurable via env vars. Defaults: gemini-2.5-flash-lite for classifier, summary, quiz generation, and note summary/concepts; gemini-2.5-flash for concept extraction, syllabus parsing, and note chat; gemini-2.5-pro for the chat tutor. Override per task via SAPLING_MODEL_<TASK>.
  • Streamingsse-starlette Server-Sent Events on POST /api/documents/upload for live per-phase progress. The frontend SSE consumer (frontend/src/lib/sse.ts) parses the wire format from a fetch ReadableStream so it works with multipart POSTs (which EventSource can't do).
  • ObservabilityLogfire auto-instruments Pydantic AI agent runs, tool calls, and FastAPI requests. A custom span scrubber (backend/services/logfire_scrubber.py) truncates and SHA-256-fingerprints risky attribute paths (prompt text, model output, message content) before egress so user-uploaded document text never ships verbatim. genai-prices provides per-call cost telemetry. Per-request structured logging includes a correlation ID, status, and duration.
  • OCR — Docling (layout-aware PDF → Markdown) with GOT-OCR 2.0 fallback for math/handwriting; Tesseract retained as a legacy fallback.
  • Gradescope sync — Per-user grade import via the unofficial gradescopeapi client, plus a Playwright headless-Chromium flow for BU SSO + Duo 2FA sign-in (run playwright install chromium after pip install). Credentials are stored encrypted and the app re-authenticates fresh on each sync.
  • Database — Supabase (PostgreSQL) for all persistent data
  • Encryption — AES-256-GCM column-level encryption (via the cryptography library) for user PII, document summaries/concept notes, OAuth tokens, chat messages, and gradebook notes
  • Deploy — Frontend on Cloudflare Workers via @opennextjs/cloudflare

Usage

Backend

cd backend
python3 -m venv venv
source venv/bin/activate # fish: source venv/bin/activate.fish
pip install -r requirements.txt
cp .env.example .env # fill in GEMINI_API_KEY, SUPABASE_URL, SUPABASE_SERVICE_KEY, ENCRYPTION_KEY
python3 main.py # → http://localhost:5000

Frontend

cd frontend
npm install
echo"NEXT_PUBLIC_API_URL=http://localhost:5000"> .env.local
npm run dev # → http://localhost:3000

API Endpoints

Learn

  • POST/api/learn/start-session — Start a tutoring session
  • POST/api/learn/chat — Send a chat message
  • POST/api/learn/action — Send a structured action (e.g. quiz, recap)
  • POST/api/learn/end-session — End a session
  • GET/api/learn/sessions/{user_id} — List past sessions

Graph

  • GET/api/graph/{user_id} — Fetch the user's knowledge graph
  • GET/api/graph/{user_id}/recommendations — Get next-concept recommendations
  • GET/api/graph/{user_id}/courses — List courses

Quiz

  • POST/api/quiz/generate — Generate an adaptive quiz
  • POST/api/quiz/submit — Submit answers and update mastery

Flashcards

  • POST/api/flashcards/generate — Generate flashcards for a topic
  • POST/api/flashcards/import/parse — Parse cards from paste, file, URL, or photo (no save)
  • POST/api/flashcards/import/generate — AI-generate cards from a topic / prompt
  • POST/api/flashcards/import/cleanup — Clean up parsed cards before commit
  • POST/api/flashcards/import/cloze — Convert sentences into cloze-deletion cards
  • POST/api/flashcards/import/commit — Commit parsed cards to the user's deck
  • GET/api/flashcards/user/{user_id} — Fetch a user's flashcards
  • POST/api/flashcards/rate — Rate a card (Easy / Hard / Forgot)
  • DELETE/api/flashcards/{card_id} — Delete a card

Gradebook

  • GET/api/gradebook/summary — Per-course grade summary across the user's courses (filter by semester)
  • GET/api/gradebook/courses/{course_id} — Full gradebook for a course (categories, assignments, current grade)
  • POST/api/gradebook/courses/{course_id}/categories — Create a category
  • PATCH/api/gradebook/courses/{course_id}/categories — Bulk-update categories (weights, names)
  • DELETE/api/gradebook/categories/{category_id} — Delete a category
  • POST/api/gradebook/assignments — Create an assignment
  • PATCH/api/gradebook/assignments/{assignment_id} — Update an assignment (grade, weight, due date)
  • DELETE/api/gradebook/assignments/{assignment_id} — Delete an assignment
  • PATCH/api/gradebook/courses/{course_id}/scale — Override the per-course letter-grade scale
  • POST/api/gradebook/syllabus/apply — Apply a parsed syllabus (replaces categories, dedupes assignments)

Gradescope

  • POST/api/gradescope/credentials — Test a Gradescope login, then save encrypted email/password credentials
  • POST/api/gradescope/credentials/bu-sso — Live BU SSO sign-in via headless Chromium (WebLogin + Duo), storing session cookies
  • DELETE/api/gradescope/credentials — Remove stored credentials
  • GET/api/gradescope/status — Whether credentials are saved and when the user last synced
  • GET/api/gradescope/courses — List the user's Gradescope student courses (live)
  • GET/api/gradescope/links — List Sapling-course → Gradescope-course mappings
  • POST/api/gradescope/link — Create or update a course mapping
  • DELETE/api/gradescope/link/{sapling_course_id} — Remove a course mapping
  • POST/api/gradescope/sync/{sapling_course_id} — Pull assignments from the linked Gradescope course and upsert grades into the gradebook

Study Guide

  • GET/api/study-guide/{user_id}/guide — Get (or generate) a study guide for an exam
  • GET/api/study-guide/{user_id}/cached — List all cached study guides
  • GET/api/study-guide/{user_id}/courses — List courses for guide generation
  • GET/api/study-guide/{user_id}/exams — List exam-type assignments
  • POST/api/study-guide/regenerate — Invalidate cache and regenerate a guide

Calendar

  • POST/api/calendar/extract — Extract assignments from a syllabus
  • GET/api/calendar/upcoming/{user_id} — Fetch upcoming assignments
  • POST/api/calendar/save — Save extracted assignments

Documents

  • POST/api/documents/uploadStreaming SSE upload. Runs the agentic pipeline (classifier → parallel summary/concepts/syllabus → graph merge) and emits typed SSE events the client renders as live progress: status:start, progress:classify, progress:classified, progress:extract, progress:extracted, progress:graph_update, progress:graph_updated, result:finalize, status:done. Errors emit error:failed (terminal). Idempotent on X-Request-ID — a retry with the same ID returns the previously persisted document without re-running the pipeline.
  • POST/api/documents/upload/sync — Non-streaming JSON upload. Same orchestrator under the hood, returns the persisted document as a single JSON response. Used by callers that don't need progress events.
  • GET/api/documents/user/{user_id} — List a user's documents
  • DELETE/api/documents/doc/{doc_id} — Delete a document
  • POST/api/documents/doc/{doc_id}/scan-concepts — Re-extract concepts from a stored document into the course graph
  • POST/api/documents/course/{course_id}/scan-concepts — Extend a course's concept graph from its label alone

Notes

  • GET/api/notes/user/{user_id} — List a user's notes (filter by course_id)
  • POST/api/notes — Create a note
  • GET/api/notes/{note_id} — Fetch a single note
  • PATCH/api/notes/{note_id} — Update a note (title, body, tags, course)
  • DELETE/api/notes/{note_id} — Delete a note
  • GET/api/notes/{note_id}/concepts — List concepts linked to a note
  • POST/api/notes/{note_id}/concepts — Link a graph concept to a note
  • DELETE/api/notes/{note_id}/concepts/{concept_node_id} — Unlink a concept
  • POST/api/notes/{note_id}/summarize — AI-summarize the note (agent-backed)
  • POST/api/notes/{note_id}/extract-concepts — Extract concepts, merge into the graph, link back to the note
  • POST/api/notes/{note_id}/chat — Ask a question grounded in the note (agent-backed)
  • POST/api/notes/{note_id}/send-to-tutor — Build a tutor handoff (topic + preface) from the note
  • POST/api/notes/{note_id}/generate-quiz — Pick the note's weakest linked concept to quiz on

Social

  • POST/api/social/rooms/create — Create a study room
  • POST/api/social/rooms/join — Join a study room by invite code
  • GET/api/social/rooms/{user_id} — List a user's rooms
  • GET/api/social/rooms/{room_id}/overview — Room overview with AI-generated group summary
  • GET/api/social/rooms/{room_id}/activity — Recent activity feed for a room
  • POST/api/social/rooms/{room_id}/match — Find study partners within a room
  • POST/api/social/rooms/{room_id}/leave — Leave a room
  • DELETE/api/social/rooms/{room_id}/members/{member_id} — Kick a member (room leader only)
  • GET/api/social/rooms/{room_id}/messages — Fetch room chat messages
  • POST/api/social/rooms/{room_id}/messages — Send a chat message
  • POST/api/social/school-match — Find study partners school-wide
  • GET/api/social/students — List all students with mastery stats

Auth

  • GET/api/auth/google — Redirect to Google OAuth consent screen
  • GET/api/auth/google/callback — OAuth callback, issues session token
  • GET/api/auth/me — Get current user from session token

Onboarding

  • GET/api/onboarding/courses — Search courses by name or code
  • POST/api/onboarding/profile — Save onboarding profile data

Profile

  • GET/api/profile/{user_id} — Public profile with roles, achievements, cosmetics
  • PUT/api/profile/{user_id} — Update profile fields (bio, major, links, etc.)
  • PUT/api/profile/{user_id}/settings — Update user settings
  • POST/api/profile/{user_id}/avatar — Upload a profile avatar
  • POST/api/profile/{user_id}/equip — Equip or unequip a cosmetic item
  • PUT/api/profile/{user_id}/featured-role — Set featured role on profile
  • PUT/api/profile/{user_id}/featured-achievements — Set featured achievements
  • DELETE/api/profile/{user_id} — Delete account

Admin

  • GET/POST/api/admin/roles — List / create roles
  • PATCH/DELETE/api/admin/roles/{role_id} — Update / delete a role
  • POST/DELETE/api/admin/roles/assign · /api/admin/roles/revoke — Assign / revoke a role
  • GET/POST/DELETE/api/admin/roles/cosmetics (+ /api/admin/roles/{role_id}/cosmetics) — Link cosmetics to roles
  • GET/POST/api/admin/achievements — List / create achievements
  • PATCH/DELETE/api/admin/achievements/{achievement_id} — Update / delete an achievement
  • POST/api/admin/achievements/grant — Manually grant an achievement
  • GET/POST/PATCH/DELETE achievement triggers (/api/admin/achievements/{achievement_id}/triggers, /api/admin/achievements/triggers, /api/admin/achievements/triggers/{trigger_id})
  • GET/POST/DELETE/api/admin/achievements/cosmetics (+ /api/admin/achievements/{achievement_id}/cosmetics) — Link cosmetics to achievements
  • GET/POST/api/admin/cosmetics — List / create cosmetic items
  • PATCH/DELETE/api/admin/cosmetics/{cosmetic_id} — Update / delete a cosmetic
  • GET/api/admin/users — Paginated, searchable user list
  • PATCH/api/admin/users/{user_id}/approve · /unapprove — Approve / unapprove a user
  • POST/api/admin/allowlist/approve · /api/admin/allowlist/revoke — Manage the email allowlist
  • GET/api/admin/audit — Paginated admin audit log (filter by action / target)
  • GET/api/admin/analytics/overview — Totals, 30-day series, and role counts

Feedback

  • POST/api/feedback/feedback — Submit session or general feedback
  • POST/api/feedback/issue-reports — Submit a bug/issue report

Newsletter

  • POST/api/newsletter/subscribe — Add an email to the beta / newsletter list

Environment Variables

backend/.env

VariableRequiredDescription
GEMINI_API_KEYGoogle Gemini API key
SUPABASE_URLYour Supabase project URL
SUPABASE_SERVICE_KEYSupabase service role key
ENCRYPTION_KEYAES-256-GCM key for column-level encryption (32 bytes as 64 hex chars; generate with python -c "import secrets; print(secrets.token_hex(32))")
PORTBackend port (default 5000)
FRONTEND_URLAllowed CORS origin (default http://localhost:3000)
APP_ENVDeployment environment (default production, fail-closed checks). Set local for local dev (relaxes SESSION_SECRET); set staging on the staging deploy (adds a noindex header, still fail-closed).
GOOGLE_CLIENT_IDGoogle OAuth client ID (for sign-in and Calendar)
GOOGLE_CLIENT_SECRETGoogle OAuth client secret
SESSION_SECRETHMAC secret for session tokens (min 32 bytes)
ALLOWED_EMAIL_DOMAINSComma-separated sign-in email-domain allowlist (default bu.edu). Empty value disables the check (any domain may sign in).
SUPABASE_DB_URLSupabase session-mode pooler URI (port 5432, user postgres.<ref>) — used only by the db.migrate migration runner, never at app runtime. Not the direct db.<ref> host (IPv6-only, unreachable from most networks); not port 6543 (transaction mode, breaks DDL)
LOGFIRE_TOKENIf set, traces ship to logfire.pydantic.dev. Without it, Logfire stays local-only. The Sapling scrubber redacts prompt/output content before egress regardless.
SAPLING_MODEL_CLASSIFIEROverride classifier-agent model (default gemini-2.5-flash-lite)
SAPLING_MODEL_SUMMARYOverride summary-agent model (default gemini-2.5-flash-lite)
SAPLING_MODEL_CONCEPTSOverride concept-extraction-agent model (default gemini-2.5-flash)
SAPLING_MODEL_SYLLABUSOverride syllabus-extraction-agent model (default gemini-2.5-flash)
SAPLING_MODEL_QUIZOverride quiz-generation-agent model (default gemini-2.5-flash-lite)
SAPLING_MODEL_CHAT_TUTOROverride chat-tutor-agent model (default gemini-2.5-pro)
SAPLING_MODEL_NOTE_SUMMARYOverride note-summary-agent model (default gemini-2.5-flash-lite)
SAPLING_MODEL_NOTE_CONCEPTSOverride note-concepts-agent model (default gemini-2.5-flash-lite)
SAPLING_MODEL_NOTE_CHATOverride note-chat-agent model (default gemini-2.5-flash)
OCR_ASYNC_ENABLEDWhen true, the streaming /upload route runs OCR off the request critical path with a progress:extracting_text SSE event. Default false.
DBOS_ENABLEDWhen true AND dbos is installed AND DBOS_DATABASE_URL is set, process_document runs as a checkpointed DBOS workflow with per-step resume on crash. Default false (decorators are no-op passthroughs). See docs/decisions/0011-durable-execution-dbos.md.
DBOS_DATABASE_URLPostgres connection string for DBOS metadata (separate from Supabase). Required only when DBOS_ENABLED=true.

frontend/.env.local

VariableRequiredDescription
NEXT_PUBLIC_API_URLBackend base URL (e.g. http://localhost:5000)
SESSION_SECRETSame HMAC secret as backend (for middleware token verification)

Tests

Backend — pytest, mocked Gemini + Supabase. ~660 tests.

cd backend
python -m pytest tests/ -q --ignore=tests/evals

Frontend — Vitest. Pure-logic tests (sse.ts, api.ts) run in node; component tests (e.g. DocumentUploadModal, TopNav, KnowledgeGraph3D) use jsdom + React Testing Library + @testing-library/jest-dom. Per-file // @vitest-environment jsdom directive keeps the lib tests fast.

cd frontend
npm install
npm run typecheck
npm test# vitest run
npm run test:watch # vitest watch

Evals — extraction-accuracy harness for the migrated agents. Five offline datasets (document_classification, document_summary, concept_extraction, syllabus_extraction, quiz_generation) run in replay mode against committed cassettes — no network, fully deterministic. Each task reports a per-evaluator accuracy score; a task fails only when it regresses below the committed baseline in tests/evals/baselines.json or a cassette is missing. One command runs them all:

cd backend
python tests/evals/run_all.py # replay (default), gate on baselines
SAPLING_EVAL_MODE=record python tests/evals/run_all.py # refresh cassettes (live Gemini)
SAPLING_EVAL_UPDATE_BASELINES=1 python tests/evals/run_all.py # refresh baselines from current scores

.github/workflows/evals.yml runs run_all.py in replay mode on every PR that touches backend/agents/** or the harness. The chat_tutor dataset is excluded from the offline harness because its retrieval tool reads a live Supabase (tracked under #149). See backend/tests/evals/README.md for the record/refresh workflow.

Architecture & Dev Context

Live architecture overviewdocs/architecture.md.

Architectural Decision Recordsdocs/decisions/ (append-only, MADR-minimal format). Seventeen ADRs as of merge:

  • 0001 — Adopt Pydantic AI as the agent framework
  • 0002 — Markdown-based dev-context vault structure
  • 0003 — Per-call usage_limits= and inline system prompts
  • 0004graph_service as the next agent-tool surface
  • 0005 — Quiz generation as the next agentic refactor
  • 0006 — SSE protocol choice (sse-starlette + custom mapper, not VercelAIAdapter)
  • 0007 — Drop the document orchestrator agent (saves a Gemini Pro call per upload)
  • 0008 — Per-task model routing
  • 0009 — Request correlation IDs (X-Request-ID)
  • 0010 — OCR async / two-phase upload (partial — feature flag shipped, full design deferred)
  • 0011 — Durable execution via DBOS (partial — optional shim shipped, real DBOS opt-in)
  • 0012 — Concept-by-concept streaming (deferred — needs eval data on Gemini's emission ordering first)
  • 0013 — Refactor #2 (quiz generation) shipped as quiz_agent
  • 0014 — Adaptive quiz iteration (spaced repetition + history + difficulty)
  • 0015 — Refactor #3 (chat tutor) shipped as chat_tutor_agent
  • 0016 — Refactor #4 (syllabus extraction) unified onto the agent
  • 0017 — Notetaker dynamic implementation (CRUD + four agent-backed actions)

Things that didn't workdocs/attempts/ (each entry has a mandatory "What I'd try next" section).

Slash commands for Claude Code sessions.claude/commands/log-decision.md, log-attempt.md, recall.md, sync-context.md. Run /sync-context at session start to load the most relevant ADRs as a digest.

Read-only context curator subagent.claude/agents/context-curator.md keeps the main session's context window lean by forking off vault searches into a separate subagent.

Migrations

Schema lives as ordered SQL files in backend/db/migrations/, applied in filename order. New migrations use a UTC timestamp prefix (date -u +%Y%m%d%H%M%S); the legacy NNNN_ files are frozen and must never be renamed, since the ledger keys on basename and a rename re-runs the migration. See backend/db/migrations/README.md. A minimal runner (backend/db/migrate.py) applies pending files in order and records each in a tracking table, so it's idempotent — re-running only applies what's new. The runner connects with psycopg over the session-mode pooler URI (SUPABASE_DB_URL, port 5432, user postgres.<ref>); this is the one sanctioned exception to the db/connection.py::table()-only convention, since runtime PostgREST can't execute DDL.

cd backend
SUPABASE_DB_URL=postgresql://... python -m db.migrate # apply pending
SUPABASE_DB_URL=postgresql://... python -m db.migrate --baseline # record all as applied without running (adopting an existing DB)

Migrations 00190028 are the modular schema redesign: courses split into an abstract courses table plus course_offerings and terms, user_courses became enrollments, identity split into users + user_profiles, the gradebook re-keyed onto enrollment_id, analytics re-keyed onto offerings, and the graph gained append-only node_mastery_events. The public API boundary still keys on the abstract course_id.

For staging, after applying migrations you can lay down a self-contained fake demo dataset (graph + gradebook + courses-with-term) with python -m db.seed_staging — idempotent and staging-only, never run it against production. See docs/staging/setup-checklist.md for the full staging bring-up.

License

Copyright (c) 2026 Andres Lopez, Jack He, Luke Cooper, and Jose Gael Cruz-Lopez

About

Resources

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
GitHub - SaplingLearn/Sapling · GitHub
Skip to content

Repository files navigation

Sapling

An AI-powered study companion that builds a live knowledge graph as you learn.

"preview"PythonTypeScriptNext.jsFastAPISupabaseGoogle GeminiD3.jsGit

Overview

Sapling is a study tool that adapts to how you learn. Chat with an AI tutor across three teaching modes, take adaptive quizzes, track assignments from your syllabus, and compare progress with classmates in study rooms. As you learn, a live knowledge graph maps your mastery in real time.

Features

  • Live Knowledge Graph — Your understanding is visualized as a growing node graph, with a 2D (D3/SVG, default) or 3D (WebGL) view toggle. Mastery scores update dynamically after every session and quiz, with per-course color shading and mastery-based opacity.
  • Three Teaching Modes — Socratic (guided reasoning), Expository (direct explanation), and TeachBack (you explain, Sapling corrects). Chat supports inline math (KaTeX), Mermaid diagrams, function plots, and theorem callouts.
  • Adaptive Quizzes — AI-generated quizzes targeting your weakest concepts, with difficulty scaling based on your performance and spaced-repetition scheduling that resurfaces concepts you've missed before.
  • Flashcards — Generate AI flashcards per course, or import them from paste, file (CSV/Markdown/Anki), URL, AI prompt, or photo. Study by topic with spaced-repetition ratings (Easy / Hard / Forgot).
  • Gradebook — Track your real-world grade per course. Categories + weights, per-assignment scores, per-course letter-scale overrides, and current grade calculation. Upload a syllabus and Sapling extracts categories and assignments automatically.
  • Gradescope Sync — Link a Sapling course to a Gradescope course and pull assignment grades in automatically. Sign in with a Gradescope email/password or, for BU accounts, a live SSO + Duo 2FA flow; credentials are stored encrypted and re-authenticated fresh on each sync.
  • Study Guide — Generate a Gemini-powered exam study guide from your uploaded course materials. Guides are cached per exam and can be regenerated at any time.
  • Class Intelligence — Aggregates anonymized class-wide patterns to surface common misconceptions and weak areas, personalizing your sessions.
  • Calendar & Syllabus Tracking — Paste your syllabus and Sapling extracts assignments, deadlines, and topics automatically.
  • Document Library — Upload PDFs and notes (up to 100 MB each); Sapling extracts summaries, key concept notes, and flashcard topics to enrich your knowledge graph and study guides. Uploads use a streaming SSE pipeline so the UI shows live per-phase progress ("Classifying..." → "Extracting summary, concepts, and syllabus..." → "Saved."). Concept notes can be re-scanned manually per doc or per course.
  • Notetaker — Write typed notes per course with debounced autosave and tags. Per note, Sapling can AI-summarize, extract concepts (merged into your knowledge graph and linked back to the note), answer questions in a note-grounded AI chat, send the note to the tutor, or generate a quiz targeting the note's weakest linked concept.
  • Study Rooms — Invite classmates, compare knowledge graphs, and track relative mastery across your group.
  • Room Chat — Real-time text chat with avatars inside each study room.
  • User Profiles — Public profiles with academic info, bio, featured achievements, and equipped cosmetics.
  • Achievements & Cosmetics — Unlock achievements by hitting milestones (sessions, quizzes, streaks). Equip cosmetic rewards like avatar frames, name colors, and title flairs.
  • Roles & Admin Panel — Role-based access control with an admin panel for user approval, role assignment, and content management.
  • Onboarding Flow — Multi-step onboarding that collects school, major, year, and courses after first sign-in. Sign-in itself is a popup-based Google OAuth flow launched from the landing page.
  • Newsletter — Beta-list signup directly from the landing page.
  • Feedback & Issue Reporting — Submit session feedback or report bugs directly from the app.

Tech Stack

  • Frontend — Next.js 16 (TypeScript, App Router). The knowledge graph renders in 2D via D3.js (default) or 3D via react-force-graph-3d (three.js/WebGL), lazy-loaded so the 3D stack only enters the bundle when toggled on. Vitest with jsdom + React Testing Library for unit + component tests.
  • Backend — FastAPI (Python) serving a REST API. Document ingestion runs through a Pydantic AI agentic pipeline (4 typed worker agents fanned out in parallel via asyncio.gather). Quiz generation, the chat tutor, syllabus extraction, and the notetaker (summary / concepts / chat) are also Pydantic AI agents — every LLM call in the backend goes through these agents; the legacy structured-prompt helper (services/gemini_service.py) was retired in ADR 0024.
  • AI — Google Gemini, with per-task model routing configurable via env vars. Defaults: gemini-2.5-flash-lite for classifier, summary, quiz generation, and note summary/concepts; gemini-2.5-flash for concept extraction, syllabus parsing, and note chat; gemini-2.5-pro for the chat tutor. Override per task via SAPLING_MODEL_<TASK>.
  • Streamingsse-starlette Server-Sent Events on POST /api/documents/upload for live per-phase progress. The frontend SSE consumer (frontend/src/lib/sse.ts) parses the wire format from a fetch ReadableStream so it works with multipart POSTs (which EventSource can't do).
  • ObservabilityLogfire auto-instruments Pydantic AI agent runs, tool calls, and FastAPI requests. A custom span scrubber (backend/services/logfire_scrubber.py) truncates and SHA-256-fingerprints risky attribute paths (prompt text, model output, message content) before egress so user-uploaded document text never ships verbatim. genai-prices provides per-call cost telemetry. Per-request structured logging includes a correlation ID, status, and duration.
  • OCR — Docling (layout-aware PDF → Markdown) with GOT-OCR 2.0 fallback for math/handwriting; Tesseract retained as a legacy fallback.
  • Gradescope sync — Per-user grade import via the unofficial gradescopeapi client, plus a Playwright headless-Chromium flow for BU SSO + Duo 2FA sign-in (run playwright install chromium after pip install). Credentials are stored encrypted and the app re-authenticates fresh on each sync.
  • Database — Supabase (PostgreSQL) for all persistent data
  • Encryption — AES-256-GCM column-level encryption (via the cryptography library) for user PII, document summaries/concept notes, OAuth tokens, chat messages, and gradebook notes
  • Deploy — Frontend on Cloudflare Workers via @opennextjs/cloudflare

Usage

Backend

cd backend
python3 -m venv venv
source venv/bin/activate # fish: source venv/bin/activate.fish
pip install -r requirements.txt
cp .env.example .env # fill in GEMINI_API_KEY, SUPABASE_URL, SUPABASE_SERVICE_KEY, ENCRYPTION_KEY
python3 main.py # → http://localhost:5000

Frontend

cd frontend
npm install
echo"NEXT_PUBLIC_API_URL=http://localhost:5000"> .env.local
npm run dev # → http://localhost:3000

API Endpoints

Learn

  • POST/api/learn/start-session — Start a tutoring session
  • POST/api/learn/chat — Send a chat message
  • POST/api/learn/action — Send a structured action (e.g. quiz, recap)
  • POST/api/learn/end-session — End a session
  • GET/api/learn/sessions/{user_id} — List past sessions

Graph

  • GET/api/graph/{user_id} — Fetch the user's knowledge graph
  • GET/api/graph/{user_id}/recommendations — Get next-concept recommendations
  • GET/api/graph/{user_id}/courses — List courses

Quiz

  • POST/api/quiz/generate — Generate an adaptive quiz
  • POST/api/quiz/submit — Submit answers and update mastery

Flashcards

  • POST/api/flashcards/generate — Generate flashcards for a topic
  • POST/api/flashcards/import/parse — Parse cards from paste, file, URL, or photo (no save)
  • POST/api/flashcards/import/generate — AI-generate cards from a topic / prompt
  • POST/api/flashcards/import/cleanup — Clean up parsed cards before commit
  • POST/api/flashcards/import/cloze — Convert sentences into cloze-deletion cards
  • POST/api/flashcards/import/commit — Commit parsed cards to the user's deck
  • GET/api/flashcards/user/{user_id} — Fetch a user's flashcards
  • POST/api/flashcards/rate — Rate a card (Easy / Hard / Forgot)
  • DELETE/api/flashcards/{card_id} — Delete a card

Gradebook

  • GET/api/gradebook/summary — Per-course grade summary across the user's courses (filter by semester)
  • GET/api/gradebook/courses/{course_id} — Full gradebook for a course (categories, assignments, current grade)
  • POST/api/gradebook/courses/{course_id}/categories — Create a category
  • PATCH/api/gradebook/courses/{course_id}/categories — Bulk-update categories (weights, names)
  • DELETE/api/gradebook/categories/{category_id} — Delete a category
  • POST/api/gradebook/assignments — Create an assignment
  • PATCH/api/gradebook/assignments/{assignment_id} — Update an assignment (grade, weight, due date)
  • DELETE/api/gradebook/assignments/{assignment_id} — Delete an assignment
  • PATCH/api/gradebook/courses/{course_id}/scale — Override the per-course letter-grade scale
  • POST/api/gradebook/syllabus/apply — Apply a parsed syllabus (replaces categories, dedupes assignments)

Gradescope

  • POST/api/gradescope/credentials — Test a Gradescope login, then save encrypted email/password credentials
  • POST/api/gradescope/credentials/bu-sso — Live BU SSO sign-in via headless Chromium (WebLogin + Duo), storing session cookies
  • DELETE/api/gradescope/credentials — Remove stored credentials
  • GET/api/gradescope/status — Whether credentials are saved and when the user last synced
  • GET/api/gradescope/courses — List the user's Gradescope student courses (live)
  • GET/api/gradescope/links — List Sapling-course → Gradescope-course mappings
  • POST/api/gradescope/link — Create or update a course mapping
  • DELETE/api/gradescope/link/{sapling_course_id} — Remove a course mapping
  • POST/api/gradescope/sync/{sapling_course_id} — Pull assignments from the linked Gradescope course and upsert grades into the gradebook

Study Guide

  • GET/api/study-guide/{user_id}/guide — Get (or generate) a study guide for an exam
  • GET/api/study-guide/{user_id}/cached — List all cached study guides
  • GET/api/study-guide/{user_id}/courses — List courses for guide generation
  • GET/api/study-guide/{user_id}/exams — List exam-type assignments
  • POST/api/study-guide/regenerate — Invalidate cache and regenerate a guide

Calendar

  • POST/api/calendar/extract — Extract assignments from a syllabus
  • GET/api/calendar/upcoming/{user_id} — Fetch upcoming assignments
  • POST/api/calendar/save — Save extracted assignments

Documents

  • POST/api/documents/uploadStreaming SSE upload. Runs the agentic pipeline (classifier → parallel summary/concepts/syllabus → graph merge) and emits typed SSE events the client renders as live progress: status:start, progress:classify, progress:classified, progress:extract, progress:extracted, progress:graph_update, progress:graph_updated, result:finalize, status:done. Errors emit error:failed (terminal). Idempotent on X-Request-ID — a retry with the same ID returns the previously persisted document without re-running the pipeline.
  • POST/api/documents/upload/sync — Non-streaming JSON upload. Same orchestrator under the hood, returns the persisted document as a single JSON response. Used by callers that don't need progress events.
  • GET/api/documents/user/{user_id} — List a user's documents
  • DELETE/api/documents/doc/{doc_id} — Delete a document
  • POST/api/documents/doc/{doc_id}/scan-concepts — Re-extract concepts from a stored document into the course graph
  • POST/api/documents/course/{course_id}/scan-concepts — Extend a course's concept graph from its label alone

Notes

  • GET/api/notes/user/{user_id} — List a user's notes (filter by course_id)
  • POST/api/notes — Create a note
  • GET/api/notes/{note_id} — Fetch a single note
  • PATCH/api/notes/{note_id} — Update a note (title, body, tags, course)
  • DELETE/api/notes/{note_id} — Delete a note
  • GET/api/notes/{note_id}/concepts — List concepts linked to a note
  • POST/api/notes/{note_id}/concepts — Link a graph concept to a note
  • DELETE/api/notes/{note_id}/concepts/{concept_node_id} — Unlink a concept
  • POST/api/notes/{note_id}/summarize — AI-summarize the note (agent-backed)
  • POST/api/notes/{note_id}/extract-concepts — Extract concepts, merge into the graph, link back to the note
  • POST/api/notes/{note_id}/chat — Ask a question grounded in the note (agent-backed)
  • POST/api/notes/{note_id}/send-to-tutor — Build a tutor handoff (topic + preface) from the note
  • POST/api/notes/{note_id}/generate-quiz — Pick the note's weakest linked concept to quiz on

Social

  • POST/api/social/rooms/create — Create a study room
  • POST/api/social/rooms/join — Join a study room by invite code
  • GET/api/social/rooms/{user_id} — List a user's rooms
  • GET/api/social/rooms/{room_id}/overview — Room overview with AI-generated group summary
  • GET/api/social/rooms/{room_id}/activity — Recent activity feed for a room
  • POST/api/social/rooms/{room_id}/match — Find study partners within a room
  • POST/api/social/rooms/{room_id}/leave — Leave a room
  • DELETE/api/social/rooms/{room_id}/members/{member_id} — Kick a member (room leader only)
  • GET/api/social/rooms/{room_id}/messages — Fetch room chat messages
  • POST/api/social/rooms/{room_id}/messages — Send a chat message
  • POST/api/social/school-match — Find study partners school-wide
  • GET/api/social/students — List all students with mastery stats

Auth

  • GET/api/auth/google — Redirect to Google OAuth consent screen
  • GET/api/auth/google/callback — OAuth callback, issues session token
  • GET/api/auth/me — Get current user from session token

Onboarding

  • GET/api/onboarding/courses — Search courses by name or code
  • POST/api/onboarding/profile — Save onboarding profile data

Profile

  • GET/api/profile/{user_id} — Public profile with roles, achievements, cosmetics
  • PUT/api/profile/{user_id} — Update profile fields (bio, major, links, etc.)
  • PUT/api/profile/{user_id}/settings — Update user settings
  • POST/api/profile/{user_id}/avatar — Upload a profile avatar
  • POST/api/profile/{user_id}/equip — Equip or unequip a cosmetic item
  • PUT/api/profile/{user_id}/featured-role — Set featured role on profile
  • PUT/api/profile/{user_id}/featured-achievements — Set featured achievements
  • DELETE/api/profile/{user_id} — Delete account

Admin

  • GET/POST/api/admin/roles — List / create roles
  • PATCH/DELETE/api/admin/roles/{role_id} — Update / delete a role
  • POST/DELETE/api/admin/roles/assign · /api/admin/roles/revoke — Assign / revoke a role
  • GET/POST/DELETE/api/admin/roles/cosmetics (+ /api/admin/roles/{role_id}/cosmetics) — Link cosmetics to roles
  • GET/POST/api/admin/achievements — List / create achievements
  • PATCH/DELETE/api/admin/achievements/{achievement_id} — Update / delete an achievement
  • POST/api/admin/achievements/grant — Manually grant an achievement
  • GET/POST/PATCH/DELETE achievement triggers (/api/admin/achievements/{achievement_id}/triggers, /api/admin/achievements/triggers, /api/admin/achievements/triggers/{trigger_id})
  • GET/POST/DELETE/api/admin/achievements/cosmetics (+ /api/admin/achievements/{achievement_id}/cosmetics) — Link cosmetics to achievements
  • GET/POST/api/admin/cosmetics — List / create cosmetic items
  • PATCH/DELETE/api/admin/cosmetics/{cosmetic_id} — Update / delete a cosmetic
  • GET/api/admin/users — Paginated, searchable user list
  • PATCH/api/admin/users/{user_id}/approve · /unapprove — Approve / unapprove a user
  • POST/api/admin/allowlist/approve · /api/admin/allowlist/revoke — Manage the email allowlist
  • GET/api/admin/audit — Paginated admin audit log (filter by action / target)
  • GET/api/admin/analytics/overview — Totals, 30-day series, and role counts

Feedback

  • POST/api/feedback/feedback — Submit session or general feedback
  • POST/api/feedback/issue-reports — Submit a bug/issue report

Newsletter

  • POST/api/newsletter/subscribe — Add an email to the beta / newsletter list

Environment Variables

backend/.env

VariableRequiredDescription
GEMINI_API_KEYGoogle Gemini API key
SUPABASE_URLYour Supabase project URL
SUPABASE_SERVICE_KEYSupabase service role key
ENCRYPTION_KEYAES-256-GCM key for column-level encryption (32 bytes as 64 hex chars; generate with python -c "import secrets; print(secrets.token_hex(32))")
PORTBackend port (default 5000)
FRONTEND_URLAllowed CORS origin (default http://localhost:3000)
APP_ENVDeployment environment (default production, fail-closed checks). Set local for local dev (relaxes SESSION_SECRET); set staging on the staging deploy (adds a noindex header, still fail-closed).
GOOGLE_CLIENT_IDGoogle OAuth client ID (for sign-in and Calendar)
GOOGLE_CLIENT_SECRETGoogle OAuth client secret
SESSION_SECRETHMAC secret for session tokens (min 32 bytes)
ALLOWED_EMAIL_DOMAINSComma-separated sign-in email-domain allowlist (default bu.edu). Empty value disables the check (any domain may sign in).
SUPABASE_DB_URLSupabase session-mode pooler URI (port 5432, user postgres.<ref>) — used only by the db.migrate migration runner, never at app runtime. Not the direct db.<ref> host (IPv6-only, unreachable from most networks); not port 6543 (transaction mode, breaks DDL)
LOGFIRE_TOKENIf set, traces ship to logfire.pydantic.dev. Without it, Logfire stays local-only. The Sapling scrubber redacts prompt/output content before egress regardless.
SAPLING_MODEL_CLASSIFIEROverride classifier-agent model (default gemini-2.5-flash-lite)
SAPLING_MODEL_SUMMARYOverride summary-agent model (default gemini-2.5-flash-lite)
SAPLING_MODEL_CONCEPTSOverride concept-extraction-agent model (default gemini-2.5-flash)
SAPLING_MODEL_SYLLABUSOverride syllabus-extraction-agent model (default gemini-2.5-flash)
SAPLING_MODEL_QUIZOverride quiz-generation-agent model (default gemini-2.5-flash-lite)
SAPLING_MODEL_CHAT_TUTOROverride chat-tutor-agent model (default gemini-2.5-pro)
SAPLING_MODEL_NOTE_SUMMARYOverride note-summary-agent model (default gemini-2.5-flash-lite)
SAPLING_MODEL_NOTE_CONCEPTSOverride note-concepts-agent model (default gemini-2.5-flash-lite)
SAPLING_MODEL_NOTE_CHATOverride note-chat-agent model (default gemini-2.5-flash)
OCR_ASYNC_ENABLEDWhen true, the streaming /upload route runs OCR off the request critical path with a progress:extracting_text SSE event. Default false.
DBOS_ENABLEDWhen true AND dbos is installed AND DBOS_DATABASE_URL is set, process_document runs as a checkpointed DBOS workflow with per-step resume on crash. Default false (decorators are no-op passthroughs). See docs/decisions/0011-durable-execution-dbos.md.
DBOS_DATABASE_URLPostgres connection string for DBOS metadata (separate from Supabase). Required only when DBOS_ENABLED=true.

frontend/.env.local

VariableRequiredDescription
NEXT_PUBLIC_API_URLBackend base URL (e.g. http://localhost:5000)
SESSION_SECRETSame HMAC secret as backend (for middleware token verification)

Tests

Backend — pytest, mocked Gemini + Supabase. ~660 tests.

cd backend
python -m pytest tests/ -q --ignore=tests/evals

Frontend — Vitest. Pure-logic tests (sse.ts, api.ts) run in node; component tests (e.g. DocumentUploadModal, TopNav, KnowledgeGraph3D) use jsdom + React Testing Library + @testing-library/jest-dom. Per-file // @vitest-environment jsdom directive keeps the lib tests fast.

cd frontend
npm install
npm run typecheck
npm test# vitest run
npm run test:watch # vitest watch

Evals — extraction-accuracy harness for the migrated agents. Five offline datasets (document_classification, document_summary, concept_extraction, syllabus_extraction, quiz_generation) run in replay mode against committed cassettes — no network, fully deterministic. Each task reports a per-evaluator accuracy score; a task fails only when it regresses below the committed baseline in tests/evals/baselines.json or a cassette is missing. One command runs them all:

cd backend
python tests/evals/run_all.py # replay (default), gate on baselines
SAPLING_EVAL_MODE=record python tests/evals/run_all.py # refresh cassettes (live Gemini)
SAPLING_EVAL_UPDATE_BASELINES=1 python tests/evals/run_all.py # refresh baselines from current scores

.github/workflows/evals.yml runs run_all.py in replay mode on every PR that touches backend/agents/** or the harness. The chat_tutor dataset is excluded from the offline harness because its retrieval tool reads a live Supabase (tracked under #149). See backend/tests/evals/README.md for the record/refresh workflow.

Architecture & Dev Context

Live architecture overviewdocs/architecture.md.

Architectural Decision Recordsdocs/decisions/ (append-only, MADR-minimal format). Seventeen ADRs as of merge:

  • 0001 — Adopt Pydantic AI as the agent framework
  • 0002 — Markdown-based dev-context vault structure
  • 0003 — Per-call usage_limits= and inline system prompts
  • 0004graph_service as the next agent-tool surface
  • 0005 — Quiz generation as the next agentic refactor
  • 0006 — SSE protocol choice (sse-starlette + custom mapper, not VercelAIAdapter)
  • 0007 — Drop the document orchestrator agent (saves a Gemini Pro call per upload)
  • 0008 — Per-task model routing
  • 0009 — Request correlation IDs (X-Request-ID)
  • 0010 — OCR async / two-phase upload (partial — feature flag shipped, full design deferred)
  • 0011 — Durable execution via DBOS (partial — optional shim shipped, real DBOS opt-in)
  • 0012 — Concept-by-concept streaming (deferred — needs eval data on Gemini's emission ordering first)
  • 0013 — Refactor #2 (quiz generation) shipped as quiz_agent
  • 0014 — Adaptive quiz iteration (spaced repetition + history + difficulty)
  • 0015 — Refactor #3 (chat tutor) shipped as chat_tutor_agent
  • 0016 — Refactor #4 (syllabus extraction) unified onto the agent
  • 0017 — Notetaker dynamic implementation (CRUD + four agent-backed actions)

Things that didn't workdocs/attempts/ (each entry has a mandatory "What I'd try next" section).

Slash commands for Claude Code sessions.claude/commands/log-decision.md, log-attempt.md, recall.md, sync-context.md. Run /sync-context at session start to load the most relevant ADRs as a digest.

Read-only context curator subagent.claude/agents/context-curator.md keeps the main session's context window lean by forking off vault searches into a separate subagent.

Migrations

Schema lives as ordered SQL files in backend/db/migrations/, applied in filename order. New migrations use a UTC timestamp prefix (date -u +%Y%m%d%H%M%S); the legacy NNNN_ files are frozen and must never be renamed, since the ledger keys on basename and a rename re-runs the migration. See backend/db/migrations/README.md. A minimal runner (backend/db/migrate.py) applies pending files in order and records each in a tracking table, so it's idempotent — re-running only applies what's new. The runner connects with psycopg over the session-mode pooler URI (SUPABASE_DB_URL, port 5432, user postgres.<ref>); this is the one sanctioned exception to the db/connection.py::table()-only convention, since runtime PostgREST can't execute DDL.

cd backend
SUPABASE_DB_URL=postgresql://... python -m db.migrate # apply pending
SUPABASE_DB_URL=postgresql://... python -m db.migrate --baseline # record all as applied without running (adopting an existing DB)

Migrations 00190028 are the modular schema redesign: courses split into an abstract courses table plus course_offerings and terms, user_courses became enrollments, identity split into users + user_profiles, the gradebook re-keyed onto enrollment_id, analytics re-keyed onto offerings, and the graph gained append-only node_mastery_events. The public API boundary still keys on the abstract course_id.

For staging, after applying migrations you can lay down a self-contained fake demo dataset (graph + gradebook + courses-with-term) with python -m db.seed_staging — idempotent and staging-only, never run it against production. See docs/staging/setup-checklist.md for the full staging bring-up.

License

Copyright (c) 2026 Andres Lopez, Jack He, Luke Cooper, and Jose Gael Cruz-Lopez

About

Resources

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - SaplingLearn/Sapling · GitHub
Skip to content

Repository files navigation

Sapling

An AI-powered study companion that builds a live knowledge graph as you learn.

"preview"PythonTypeScriptNext.jsFastAPISupabaseGoogle GeminiD3.jsGit

Overview

Sapling is a study tool that adapts to how you learn. Chat with an AI tutor across three teaching modes, take adaptive quizzes, track assignments from your syllabus, and compare progress with classmates in study rooms. As you learn, a live knowledge graph maps your mastery in real time.

Features

  • Live Knowledge Graph — Your understanding is visualized as a growing node graph, with a 2D (D3/SVG, default) or 3D (WebGL) view toggle. Mastery scores update dynamically after every session and quiz, with per-course color shading and mastery-based opacity.
  • Three Teaching Modes — Socratic (guided reasoning), Expository (direct explanation), and TeachBack (you explain, Sapling corrects). Chat supports inline math (KaTeX), Mermaid diagrams, function plots, and theorem callouts.
  • Adaptive Quizzes — AI-generated quizzes targeting your weakest concepts, with difficulty scaling based on your performance and spaced-repetition scheduling that resurfaces concepts you've missed before.
  • Flashcards — Generate AI flashcards per course, or import them from paste, file (CSV/Markdown/Anki), URL, AI prompt, or photo. Study by topic with spaced-repetition ratings (Easy / Hard / Forgot).
  • Gradebook — Track your real-world grade per course. Categories + weights, per-assignment scores, per-course letter-scale overrides, and current grade calculation. Upload a syllabus and Sapling extracts categories and assignments automatically.
  • Gradescope Sync — Link a Sapling course to a Gradescope course and pull assignment grades in automatically. Sign in with a Gradescope email/password or, for BU accounts, a live SSO + Duo 2FA flow; credentials are stored encrypted and re-authenticated fresh on each sync.
  • Study Guide — Generate a Gemini-powered exam study guide from your uploaded course materials. Guides are cached per exam and can be regenerated at any time.
  • Class Intelligence — Aggregates anonymized class-wide patterns to surface common misconceptions and weak areas, personalizing your sessions.
  • Calendar & Syllabus Tracking — Paste your syllabus and Sapling extracts assignments, deadlines, and topics automatically.
  • Document Library — Upload PDFs and notes (up to 100 MB each); Sapling extracts summaries, key concept notes, and flashcard topics to enrich your knowledge graph and study guides. Uploads use a streaming SSE pipeline so the UI shows live per-phase progress ("Classifying..." → "Extracting summary, concepts, and syllabus..." → "Saved."). Concept notes can be re-scanned manually per doc or per course.
  • Notetaker — Write typed notes per course with debounced autosave and tags. Per note, Sapling can AI-summarize, extract concepts (merged into your knowledge graph and linked back to the note), answer questions in a note-grounded AI chat, send the note to the tutor, or generate a quiz targeting the note's weakest linked concept.
  • Study Rooms — Invite classmates, compare knowledge graphs, and track relative mastery across your group.
  • Room Chat — Real-time text chat with avatars inside each study room.
  • User Profiles — Public profiles with academic info, bio, featured achievements, and equipped cosmetics.
  • Achievements & Cosmetics — Unlock achievements by hitting milestones (sessions, quizzes, streaks). Equip cosmetic rewards like avatar frames, name colors, and title flairs.
  • Roles & Admin Panel — Role-based access control with an admin panel for user approval, role assignment, and content management.
  • Onboarding Flow — Multi-step onboarding that collects school, major, year, and courses after first sign-in. Sign-in itself is a popup-based Google OAuth flow launched from the landing page.
  • Newsletter — Beta-list signup directly from the landing page.
  • Feedback & Issue Reporting — Submit session feedback or report bugs directly from the app.

Tech Stack

  • Frontend — Next.js 16 (TypeScript, App Router). The knowledge graph renders in 2D via D3.js (default) or 3D via react-force-graph-3d (three.js/WebGL), lazy-loaded so the 3D stack only enters the bundle when toggled on. Vitest with jsdom + React Testing Library for unit + component tests.
  • Backend — FastAPI (Python) serving a REST API. Document ingestion runs through a Pydantic AI agentic pipeline (4 typed worker agents fanned out in parallel via asyncio.gather). Quiz generation, the chat tutor, syllabus extraction, and the notetaker (summary / concepts / chat) are also Pydantic AI agents — every LLM call in the backend goes through these agents; the legacy structured-prompt helper (services/gemini_service.py) was retired in ADR 0024.
  • AI — Google Gemini, with per-task model routing configurable via env vars. Defaults: gemini-2.5-flash-lite for classifier, summary, quiz generation, and note summary/concepts; gemini-2.5-flash for concept extraction, syllabus parsing, and note chat; gemini-2.5-pro for the chat tutor. Override per task via SAPLING_MODEL_<TASK>.
  • Streamingsse-starlette Server-Sent Events on POST /api/documents/upload for live per-phase progress. The frontend SSE consumer (frontend/src/lib/sse.ts) parses the wire format from a fetch ReadableStream so it works with multipart POSTs (which EventSource can't do).
  • ObservabilityLogfire auto-instruments Pydantic AI agent runs, tool calls, and FastAPI requests. A custom span scrubber (backend/services/logfire_scrubber.py) truncates and SHA-256-fingerprints risky attribute paths (prompt text, model output, message content) before egress so user-uploaded document text never ships verbatim. genai-prices provides per-call cost telemetry. Per-request structured logging includes a correlation ID, status, and duration.
  • OCR — Docling (layout-aware PDF → Markdown) with GOT-OCR 2.0 fallback for math/handwriting; Tesseract retained as a legacy fallback.
  • Gradescope sync — Per-user grade import via the unofficial gradescopeapi client, plus a Playwright headless-Chromium flow for BU SSO + Duo 2FA sign-in (run playwright install chromium after pip install). Credentials are stored encrypted and the app re-authenticates fresh on each sync.
  • Database — Supabase (PostgreSQL) for all persistent data
  • Encryption — AES-256-GCM column-level encryption (via the cryptography library) for user PII, document summaries/concept notes, OAuth tokens, chat messages, and gradebook notes
  • Deploy — Frontend on Cloudflare Workers via @opennextjs/cloudflare

Usage

Backend

cd backend
python3 -m venv venv
source venv/bin/activate # fish: source venv/bin/activate.fish
pip install -r requirements.txt
cp .env.example .env # fill in GEMINI_API_KEY, SUPABASE_URL, SUPABASE_SERVICE_KEY, ENCRYPTION_KEY
python3 main.py # → http://localhost:5000

Frontend

cd frontend
npm install
echo"NEXT_PUBLIC_API_URL=http://localhost:5000"> .env.local
npm run dev # → http://localhost:3000

API Endpoints

Learn

  • POST/api/learn/start-session — Start a tutoring session
  • POST/api/learn/chat — Send a chat message
  • POST/api/learn/action — Send a structured action (e.g. quiz, recap)
  • POST/api/learn/end-session — End a session
  • GET/api/learn/sessions/{user_id} — List past sessions

Graph

  • GET/api/graph/{user_id} — Fetch the user's knowledge graph
  • GET/api/graph/{user_id}/recommendations — Get next-concept recommendations
  • GET/api/graph/{user_id}/courses — List courses

Quiz

  • POST/api/quiz/generate — Generate an adaptive quiz
  • POST/api/quiz/submit — Submit answers and update mastery

Flashcards

  • POST/api/flashcards/generate — Generate flashcards for a topic
  • POST/api/flashcards/import/parse — Parse cards from paste, file, URL, or photo (no save)
  • POST/api/flashcards/import/generate — AI-generate cards from a topic / prompt
  • POST/api/flashcards/import/cleanup — Clean up parsed cards before commit
  • POST/api/flashcards/import/cloze — Convert sentences into cloze-deletion cards
  • POST/api/flashcards/import/commit — Commit parsed cards to the user's deck
  • GET/api/flashcards/user/{user_id} — Fetch a user's flashcards
  • POST/api/flashcards/rate — Rate a card (Easy / Hard / Forgot)
  • DELETE/api/flashcards/{card_id} — Delete a card

Gradebook

  • GET/api/gradebook/summary — Per-course grade summary across the user's courses (filter by semester)
  • GET/api/gradebook/courses/{course_id} — Full gradebook for a course (categories, assignments, current grade)
  • POST/api/gradebook/courses/{course_id}/categories — Create a category
  • PATCH/api/gradebook/courses/{course_id}/categories — Bulk-update categories (weights, names)
  • DELETE/api/gradebook/categories/{category_id} — Delete a category
  • POST/api/gradebook/assignments — Create an assignment
  • PATCH/api/gradebook/assignments/{assignment_id} — Update an assignment (grade, weight, due date)
  • DELETE/api/gradebook/assignments/{assignment_id} — Delete an assignment
  • PATCH/api/gradebook/courses/{course_id}/scale — Override the per-course letter-grade scale
  • POST/api/gradebook/syllabus/apply — Apply a parsed syllabus (replaces categories, dedupes assignments)

Gradescope

  • POST/api/gradescope/credentials — Test a Gradescope login, then save encrypted email/password credentials
  • POST/api/gradescope/credentials/bu-sso — Live BU SSO sign-in via headless Chromium (WebLogin + Duo), storing session cookies
  • DELETE/api/gradescope/credentials — Remove stored credentials
  • GET/api/gradescope/status — Whether credentials are saved and when the user last synced
  • GET/api/gradescope/courses — List the user's Gradescope student courses (live)
  • GET/api/gradescope/links — List Sapling-course → Gradescope-course mappings
  • POST/api/gradescope/link — Create or update a course mapping
  • DELETE/api/gradescope/link/{sapling_course_id} — Remove a course mapping
  • POST/api/gradescope/sync/{sapling_course_id} — Pull assignments from the linked Gradescope course and upsert grades into the gradebook

Study Guide

  • GET/api/study-guide/{user_id}/guide — Get (or generate) a study guide for an exam
  • GET/api/study-guide/{user_id}/cached — List all cached study guides
  • GET/api/study-guide/{user_id}/courses — List courses for guide generation
  • GET/api/study-guide/{user_id}/exams — List exam-type assignments
  • POST/api/study-guide/regenerate — Invalidate cache and regenerate a guide

Calendar

  • POST/api/calendar/extract — Extract assignments from a syllabus
  • GET/api/calendar/upcoming/{user_id} — Fetch upcoming assignments
  • POST/api/calendar/save — Save extracted assignments

Documents

  • POST/api/documents/uploadStreaming SSE upload. Runs the agentic pipeline (classifier → parallel summary/concepts/syllabus → graph merge) and emits typed SSE events the client renders as live progress: status:start, progress:classify, progress:classified, progress:extract, progress:extracted, progress:graph_update, progress:graph_updated, result:finalize, status:done. Errors emit error:failed (terminal). Idempotent on X-Request-ID — a retry with the same ID returns the previously persisted document without re-running the pipeline.
  • POST/api/documents/upload/sync — Non-streaming JSON upload. Same orchestrator under the hood, returns the persisted document as a single JSON response. Used by callers that don't need progress events.
  • GET/api/documents/user/{user_id} — List a user's documents
  • DELETE/api/documents/doc/{doc_id} — Delete a document
  • POST/api/documents/doc/{doc_id}/scan-concepts — Re-extract concepts from a stored document into the course graph
  • POST/api/documents/course/{course_id}/scan-concepts — Extend a course's concept graph from its label alone

Notes

  • GET/api/notes/user/{user_id} — List a user's notes (filter by course_id)
  • POST/api/notes — Create a note
  • GET/api/notes/{note_id} — Fetch a single note
  • PATCH/api/notes/{note_id} — Update a note (title, body, tags, course)
  • DELETE/api/notes/{note_id} — Delete a note
  • GET/api/notes/{note_id}/concepts — List concepts linked to a note
  • POST/api/notes/{note_id}/concepts — Link a graph concept to a note
  • DELETE/api/notes/{note_id}/concepts/{concept_node_id} — Unlink a concept
  • POST/api/notes/{note_id}/summarize — AI-summarize the note (agent-backed)
  • POST/api/notes/{note_id}/extract-concepts — Extract concepts, merge into the graph, link back to the note
  • POST/api/notes/{note_id}/chat — Ask a question grounded in the note (agent-backed)
  • POST/api/notes/{note_id}/send-to-tutor — Build a tutor handoff (topic + preface) from the note
  • POST/api/notes/{note_id}/generate-quiz — Pick the note's weakest linked concept to quiz on

Social

  • POST/api/social/rooms/create — Create a study room
  • POST/api/social/rooms/join — Join a study room by invite code
  • GET/api/social/rooms/{user_id} — List a user's rooms
  • GET/api/social/rooms/{room_id}/overview — Room overview with AI-generated group summary
  • GET/api/social/rooms/{room_id}/activity — Recent activity feed for a room
  • POST/api/social/rooms/{room_id}/match — Find study partners within a room
  • POST/api/social/rooms/{room_id}/leave — Leave a room
  • DELETE/api/social/rooms/{room_id}/members/{member_id} — Kick a member (room leader only)
  • GET/api/social/rooms/{room_id}/messages — Fetch room chat messages
  • POST/api/social/rooms/{room_id}/messages — Send a chat message
  • POST/api/social/school-match — Find study partners school-wide
  • GET/api/social/students — List all students with mastery stats

Auth

  • GET/api/auth/google — Redirect to Google OAuth consent screen
  • GET/api/auth/google/callback — OAuth callback, issues session token
  • GET/api/auth/me — Get current user from session token

Onboarding

  • GET/api/onboarding/courses — Search courses by name or code
  • POST/api/onboarding/profile — Save onboarding profile data

Profile

  • GET/api/profile/{user_id} — Public profile with roles, achievements, cosmetics
  • PUT/api/profile/{user_id} — Update profile fields (bio, major, links, etc.)
  • PUT/api/profile/{user_id}/settings — Update user settings
  • POST/api/profile/{user_id}/avatar — Upload a profile avatar
  • POST/api/profile/{user_id}/equip — Equip or unequip a cosmetic item
  • PUT/api/profile/{user_id}/featured-role — Set featured role on profile
  • PUT/api/profile/{user_id}/featured-achievements — Set featured achievements
  • DELETE/api/profile/{user_id} — Delete account

Admin

  • GET/POST/api/admin/roles — List / create roles
  • PATCH/DELETE/api/admin/roles/{role_id} — Update / delete a role
  • POST/DELETE/api/admin/roles/assign · /api/admin/roles/revoke — Assign / revoke a role
  • GET/POST/DELETE/api/admin/roles/cosmetics (+ /api/admin/roles/{role_id}/cosmetics) — Link cosmetics to roles
  • GET/POST/api/admin/achievements — List / create achievements
  • PATCH/DELETE/api/admin/achievements/{achievement_id} — Update / delete an achievement
  • POST/api/admin/achievements/grant — Manually grant an achievement
  • GET/POST/PATCH/DELETE achievement triggers (/api/admin/achievements/{achievement_id}/triggers, /api/admin/achievements/triggers, /api/admin/achievements/triggers/{trigger_id})
  • GET/POST/DELETE/api/admin/achievements/cosmetics (+ /api/admin/achievements/{achievement_id}/cosmetics) — Link cosmetics to achievements
  • GET/POST/api/admin/cosmetics — List / create cosmetic items
  • PATCH/DELETE/api/admin/cosmetics/{cosmetic_id} — Update / delete a cosmetic
  • GET/api/admin/users — Paginated, searchable user list
  • PATCH/api/admin/users/{user_id}/approve · /unapprove — Approve / unapprove a user
  • POST/api/admin/allowlist/approve · /api/admin/allowlist/revoke — Manage the email allowlist
  • GET/api/admin/audit — Paginated admin audit log (filter by action / target)
  • GET/api/admin/analytics/overview — Totals, 30-day series, and role counts

Feedback

  • POST/api/feedback/feedback — Submit session or general feedback
  • POST/api/feedback/issue-reports — Submit a bug/issue report

Newsletter

  • POST/api/newsletter/subscribe — Add an email to the beta / newsletter list

Environment Variables

backend/.env

VariableRequiredDescription
GEMINI_API_KEYGoogle Gemini API key
SUPABASE_URLYour Supabase project URL
SUPABASE_SERVICE_KEYSupabase service role key
ENCRYPTION_KEYAES-256-GCM key for column-level encryption (32 bytes as 64 hex chars; generate with python -c "import secrets; print(secrets.token_hex(32))")
PORTBackend port (default 5000)
FRONTEND_URLAllowed CORS origin (default http://localhost:3000)
APP_ENVDeployment environment (default production, fail-closed checks). Set local for local dev (relaxes SESSION_SECRET); set staging on the staging deploy (adds a noindex header, still fail-closed).
GOOGLE_CLIENT_IDGoogle OAuth client ID (for sign-in and Calendar)
GOOGLE_CLIENT_SECRETGoogle OAuth client secret
SESSION_SECRETHMAC secret for session tokens (min 32 bytes)
ALLOWED_EMAIL_DOMAINSComma-separated sign-in email-domain allowlist (default bu.edu). Empty value disables the check (any domain may sign in).
SUPABASE_DB_URLSupabase session-mode pooler URI (port 5432, user postgres.<ref>) — used only by the db.migrate migration runner, never at app runtime. Not the direct db.<ref> host (IPv6-only, unreachable from most networks); not port 6543 (transaction mode, breaks DDL)
LOGFIRE_TOKENIf set, traces ship to logfire.pydantic.dev. Without it, Logfire stays local-only. The Sapling scrubber redacts prompt/output content before egress regardless.
SAPLING_MODEL_CLASSIFIEROverride classifier-agent model (default gemini-2.5-flash-lite)
SAPLING_MODEL_SUMMARYOverride summary-agent model (default gemini-2.5-flash-lite)
SAPLING_MODEL_CONCEPTSOverride concept-extraction-agent model (default gemini-2.5-flash)
SAPLING_MODEL_SYLLABUSOverride syllabus-extraction-agent model (default gemini-2.5-flash)
SAPLING_MODEL_QUIZOverride quiz-generation-agent model (default gemini-2.5-flash-lite)
SAPLING_MODEL_CHAT_TUTOROverride chat-tutor-agent model (default gemini-2.5-pro)
SAPLING_MODEL_NOTE_SUMMARYOverride note-summary-agent model (default gemini-2.5-flash-lite)
SAPLING_MODEL_NOTE_CONCEPTSOverride note-concepts-agent model (default gemini-2.5-flash-lite)
SAPLING_MODEL_NOTE_CHATOverride note-chat-agent model (default gemini-2.5-flash)
OCR_ASYNC_ENABLEDWhen true, the streaming /upload route runs OCR off the request critical path with a progress:extracting_text SSE event. Default false.
DBOS_ENABLEDWhen true AND dbos is installed AND DBOS_DATABASE_URL is set, process_document runs as a checkpointed DBOS workflow with per-step resume on crash. Default false (decorators are no-op passthroughs). See docs/decisions/0011-durable-execution-dbos.md.
DBOS_DATABASE_URLPostgres connection string for DBOS metadata (separate from Supabase). Required only when DBOS_ENABLED=true.

frontend/.env.local

VariableRequiredDescription
NEXT_PUBLIC_API_URLBackend base URL (e.g. http://localhost:5000)
SESSION_SECRETSame HMAC secret as backend (for middleware token verification)

Tests

Backend — pytest, mocked Gemini + Supabase. ~660 tests.

cd backend
python -m pytest tests/ -q --ignore=tests/evals

Frontend — Vitest. Pure-logic tests (sse.ts, api.ts) run in node; component tests (e.g. DocumentUploadModal, TopNav, KnowledgeGraph3D) use jsdom + React Testing Library + @testing-library/jest-dom. Per-file // @vitest-environment jsdom directive keeps the lib tests fast.

cd frontend
npm install
npm run typecheck
npm test# vitest run
npm run test:watch # vitest watch

Evals — extraction-accuracy harness for the migrated agents. Five offline datasets (document_classification, document_summary, concept_extraction, syllabus_extraction, quiz_generation) run in replay mode against committed cassettes — no network, fully deterministic. Each task reports a per-evaluator accuracy score; a task fails only when it regresses below the committed baseline in tests/evals/baselines.json or a cassette is missing. One command runs them all:

cd backend
python tests/evals/run_all.py # replay (default), gate on baselines
SAPLING_EVAL_MODE=record python tests/evals/run_all.py # refresh cassettes (live Gemini)
SAPLING_EVAL_UPDATE_BASELINES=1 python tests/evals/run_all.py # refresh baselines from current scores

.github/workflows/evals.yml runs run_all.py in replay mode on every PR that touches backend/agents/** or the harness. The chat_tutor dataset is excluded from the offline harness because its retrieval tool reads a live Supabase (tracked under #149). See backend/tests/evals/README.md for the record/refresh workflow.

Architecture & Dev Context

Live architecture overviewdocs/architecture.md.

Architectural Decision Recordsdocs/decisions/ (append-only, MADR-minimal format). Seventeen ADRs as of merge:

  • 0001 — Adopt Pydantic AI as the agent framework
  • 0002 — Markdown-based dev-context vault structure
  • 0003 — Per-call usage_limits= and inline system prompts
  • 0004graph_service as the next agent-tool surface
  • 0005 — Quiz generation as the next agentic refactor
  • 0006 — SSE protocol choice (sse-starlette + custom mapper, not VercelAIAdapter)
  • 0007 — Drop the document orchestrator agent (saves a Gemini Pro call per upload)
  • 0008 — Per-task model routing
  • 0009 — Request correlation IDs (X-Request-ID)
  • 0010 — OCR async / two-phase upload (partial — feature flag shipped, full design deferred)
  • 0011 — Durable execution via DBOS (partial — optional shim shipped, real DBOS opt-in)
  • 0012 — Concept-by-concept streaming (deferred — needs eval data on Gemini's emission ordering first)
  • 0013 — Refactor #2 (quiz generation) shipped as quiz_agent
  • 0014 — Adaptive quiz iteration (spaced repetition + history + difficulty)
  • 0015 — Refactor #3 (chat tutor) shipped as chat_tutor_agent
  • 0016 — Refactor #4 (syllabus extraction) unified onto the agent
  • 0017 — Notetaker dynamic implementation (CRUD + four agent-backed actions)

Things that didn't workdocs/attempts/ (each entry has a mandatory "What I'd try next" section).

Slash commands for Claude Code sessions.claude/commands/log-decision.md, log-attempt.md, recall.md, sync-context.md. Run /sync-context at session start to load the most relevant ADRs as a digest.

Read-only context curator subagent.claude/agents/context-curator.md keeps the main session's context window lean by forking off vault searches into a separate subagent.

Migrations

Schema lives as ordered SQL files in backend/db/migrations/, applied in filename order. New migrations use a UTC timestamp prefix (date -u +%Y%m%d%H%M%S); the legacy NNNN_ files are frozen and must never be renamed, since the ledger keys on basename and a rename re-runs the migration. See backend/db/migrations/README.md. A minimal runner (backend/db/migrate.py) applies pending files in order and records each in a tracking table, so it's idempotent — re-running only applies what's new. The runner connects with psycopg over the session-mode pooler URI (SUPABASE_DB_URL, port 5432, user postgres.<ref>); this is the one sanctioned exception to the db/connection.py::table()-only convention, since runtime PostgREST can't execute DDL.

cd backend
SUPABASE_DB_URL=postgresql://... python -m db.migrate # apply pending
SUPABASE_DB_URL=postgresql://... python -m db.migrate --baseline # record all as applied without running (adopting an existing DB)

Migrations 00190028 are the modular schema redesign: courses split into an abstract courses table plus course_offerings and terms, user_courses became enrollments, identity split into users + user_profiles, the gradebook re-keyed onto enrollment_id, analytics re-keyed onto offerings, and the graph gained append-only node_mastery_events. The public API boundary still keys on the abstract course_id.

For staging, after applying migrations you can lay down a self-contained fake demo dataset (graph + gradebook + courses-with-term) with python -m db.seed_staging — idempotent and staging-only, never run it against production. See docs/staging/setup-checklist.md for the full staging bring-up.

License

Copyright (c) 2026 Andres Lopez, Jack He, Luke Cooper, and Jose Gael Cruz-Lopez

About

Resources

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - SaplingLearn/Sapling · GitHub
Skip to content

Repository files navigation

Sapling

An AI-powered study companion that builds a live knowledge graph as you learn.

"preview"PythonTypeScriptNext.jsFastAPISupabaseGoogle GeminiD3.jsGit

Overview

Sapling is a study tool that adapts to how you learn. Chat with an AI tutor across three teaching modes, take adaptive quizzes, track assignments from your syllabus, and compare progress with classmates in study rooms. As you learn, a live knowledge graph maps your mastery in real time.

Features

  • Live Knowledge Graph — Your understanding is visualized as a growing node graph, with a 2D (D3/SVG, default) or 3D (WebGL) view toggle. Mastery scores update dynamically after every session and quiz, with per-course color shading and mastery-based opacity.
  • Three Teaching Modes — Socratic (guided reasoning), Expository (direct explanation), and TeachBack (you explain, Sapling corrects). Chat supports inline math (KaTeX), Mermaid diagrams, function plots, and theorem callouts.
  • Adaptive Quizzes — AI-generated quizzes targeting your weakest concepts, with difficulty scaling based on your performance and spaced-repetition scheduling that resurfaces concepts you've missed before.
  • Flashcards — Generate AI flashcards per course, or import them from paste, file (CSV/Markdown/Anki), URL, AI prompt, or photo. Study by topic with spaced-repetition ratings (Easy / Hard / Forgot).
  • Gradebook — Track your real-world grade per course. Categories + weights, per-assignment scores, per-course letter-scale overrides, and current grade calculation. Upload a syllabus and Sapling extracts categories and assignments automatically.
  • Gradescope Sync — Link a Sapling course to a Gradescope course and pull assignment grades in automatically. Sign in with a Gradescope email/password or, for BU accounts, a live SSO + Duo 2FA flow; credentials are stored encrypted and re-authenticated fresh on each sync.
  • Study Guide — Generate a Gemini-powered exam study guide from your uploaded course materials. Guides are cached per exam and can be regenerated at any time.
  • Class Intelligence — Aggregates anonymized class-wide patterns to surface common misconceptions and weak areas, personalizing your sessions.
  • Calendar & Syllabus Tracking — Paste your syllabus and Sapling extracts assignments, deadlines, and topics automatically.
  • Document Library — Upload PDFs and notes (up to 100 MB each); Sapling extracts summaries, key concept notes, and flashcard topics to enrich your knowledge graph and study guides. Uploads use a streaming SSE pipeline so the UI shows live per-phase progress ("Classifying..." → "Extracting summary, concepts, and syllabus..." → "Saved."). Concept notes can be re-scanned manually per doc or per course.
  • Notetaker — Write typed notes per course with debounced autosave and tags. Per note, Sapling can AI-summarize, extract concepts (merged into your knowledge graph and linked back to the note), answer questions in a note-grounded AI chat, send the note to the tutor, or generate a quiz targeting the note's weakest linked concept.
  • Study Rooms — Invite classmates, compare knowledge graphs, and track relative mastery across your group.
  • Room Chat — Real-time text chat with avatars inside each study room.
  • User Profiles — Public profiles with academic info, bio, featured achievements, and equipped cosmetics.
  • Achievements & Cosmetics — Unlock achievements by hitting milestones (sessions, quizzes, streaks). Equip cosmetic rewards like avatar frames, name colors, and title flairs.
  • Roles & Admin Panel — Role-based access control with an admin panel for user approval, role assignment, and content management.
  • Onboarding Flow — Multi-step onboarding that collects school, major, year, and courses after first sign-in. Sign-in itself is a popup-based Google OAuth flow launched from the landing page.
  • Newsletter — Beta-list signup directly from the landing page.
  • Feedback & Issue Reporting — Submit session feedback or report bugs directly from the app.

Tech Stack

  • Frontend — Next.js 16 (TypeScript, App Router). The knowledge graph renders in 2D via D3.js (default) or 3D via react-force-graph-3d (three.js/WebGL), lazy-loaded so the 3D stack only enters the bundle when toggled on. Vitest with jsdom + React Testing Library for unit + component tests.
  • Backend — FastAPI (Python) serving a REST API. Document ingestion runs through a Pydantic AI agentic pipeline (4 typed worker agents fanned out in parallel via asyncio.gather). Quiz generation, the chat tutor, syllabus extraction, and the notetaker (summary / concepts / chat) are also Pydantic AI agents — every LLM call in the backend goes through these agents; the legacy structured-prompt helper (services/gemini_service.py) was retired in ADR 0024.
  • AI — Google Gemini, with per-task model routing configurable via env vars. Defaults: gemini-2.5-flash-lite for classifier, summary, quiz generation, and note summary/concepts; gemini-2.5-flash for concept extraction, syllabus parsing, and note chat; gemini-2.5-pro for the chat tutor. Override per task via SAPLING_MODEL_<TASK>.
  • Streamingsse-starlette Server-Sent Events on POST /api/documents/upload for live per-phase progress. The frontend SSE consumer (frontend/src/lib/sse.ts) parses the wire format from a fetch ReadableStream so it works with multipart POSTs (which EventSource can't do).
  • ObservabilityLogfire auto-instruments Pydantic AI agent runs, tool calls, and FastAPI requests. A custom span scrubber (backend/services/logfire_scrubber.py) truncates and SHA-256-fingerprints risky attribute paths (prompt text, model output, message content) before egress so user-uploaded document text never ships verbatim. genai-prices provides per-call cost telemetry. Per-request structured logging includes a correlation ID, status, and duration.
  • OCR — Docling (layout-aware PDF → Markdown) with GOT-OCR 2.0 fallback for math/handwriting; Tesseract retained as a legacy fallback.
  • Gradescope sync — Per-user grade import via the unofficial gradescopeapi client, plus a Playwright headless-Chromium flow for BU SSO + Duo 2FA sign-in (run playwright install chromium after pip install). Credentials are stored encrypted and the app re-authenticates fresh on each sync.
  • Database — Supabase (PostgreSQL) for all persistent data
  • Encryption — AES-256-GCM column-level encryption (via the cryptography library) for user PII, document summaries/concept notes, OAuth tokens, chat messages, and gradebook notes
  • Deploy — Frontend on Cloudflare Workers via @opennextjs/cloudflare

Usage

Backend

cd backend
python3 -m venv venv
source venv/bin/activate # fish: source venv/bin/activate.fish
pip install -r requirements.txt
cp .env.example .env # fill in GEMINI_API_KEY, SUPABASE_URL, SUPABASE_SERVICE_KEY, ENCRYPTION_KEY
python3 main.py # → http://localhost:5000

Frontend

cd frontend
npm install
echo"NEXT_PUBLIC_API_URL=http://localhost:5000"> .env.local
npm run dev # → http://localhost:3000

API Endpoints

Learn

  • POST/api/learn/start-session — Start a tutoring session
  • POST/api/learn/chat — Send a chat message
  • POST/api/learn/action — Send a structured action (e.g. quiz, recap)
  • POST/api/learn/end-session — End a session
  • GET/api/learn/sessions/{user_id} — List past sessions

Graph

  • GET/api/graph/{user_id} — Fetch the user's knowledge graph
  • GET/api/graph/{user_id}/recommendations — Get next-concept recommendations
  • GET/api/graph/{user_id}/courses — List courses

Quiz

  • POST/api/quiz/generate — Generate an adaptive quiz
  • POST/api/quiz/submit — Submit answers and update mastery

Flashcards

  • POST/api/flashcards/generate — Generate flashcards for a topic
  • POST/api/flashcards/import/parse — Parse cards from paste, file, URL, or photo (no save)
  • POST/api/flashcards/import/generate — AI-generate cards from a topic / prompt
  • POST/api/flashcards/import/cleanup — Clean up parsed cards before commit
  • POST/api/flashcards/import/cloze — Convert sentences into cloze-deletion cards
  • POST/api/flashcards/import/commit — Commit parsed cards to the user's deck
  • GET/api/flashcards/user/{user_id} — Fetch a user's flashcards
  • POST/api/flashcards/rate — Rate a card (Easy / Hard / Forgot)
  • DELETE/api/flashcards/{card_id} — Delete a card

Gradebook

  • GET/api/gradebook/summary — Per-course grade summary across the user's courses (filter by semester)
  • GET/api/gradebook/courses/{course_id} — Full gradebook for a course (categories, assignments, current grade)
  • POST/api/gradebook/courses/{course_id}/categories — Create a category
  • PATCH/api/gradebook/courses/{course_id}/categories — Bulk-update categories (weights, names)
  • DELETE/api/gradebook/categories/{category_id} — Delete a category
  • POST/api/gradebook/assignments — Create an assignment
  • PATCH/api/gradebook/assignments/{assignment_id} — Update an assignment (grade, weight, due date)
  • DELETE/api/gradebook/assignments/{assignment_id} — Delete an assignment
  • PATCH/api/gradebook/courses/{course_id}/scale — Override the per-course letter-grade scale
  • POST/api/gradebook/syllabus/apply — Apply a parsed syllabus (replaces categories, dedupes assignments)

Gradescope

  • POST/api/gradescope/credentials — Test a Gradescope login, then save encrypted email/password credentials
  • POST/api/gradescope/credentials/bu-sso — Live BU SSO sign-in via headless Chromium (WebLogin + Duo), storing session cookies
  • DELETE/api/gradescope/credentials — Remove stored credentials
  • GET/api/gradescope/status — Whether credentials are saved and when the user last synced
  • GET/api/gradescope/courses — List the user's Gradescope student courses (live)
  • GET/api/gradescope/links — List Sapling-course → Gradescope-course mappings
  • POST/api/gradescope/link — Create or update a course mapping
  • DELETE/api/gradescope/link/{sapling_course_id} — Remove a course mapping
  • POST/api/gradescope/sync/{sapling_course_id} — Pull assignments from the linked Gradescope course and upsert grades into the gradebook

Study Guide

  • GET/api/study-guide/{user_id}/guide — Get (or generate) a study guide for an exam
  • GET/api/study-guide/{user_id}/cached — List all cached study guides
  • GET/api/study-guide/{user_id}/courses — List courses for guide generation
  • GET/api/study-guide/{user_id}/exams — List exam-type assignments
  • POST/api/study-guide/regenerate — Invalidate cache and regenerate a guide

Calendar

  • POST/api/calendar/extract — Extract assignments from a syllabus
  • GET/api/calendar/upcoming/{user_id} — Fetch upcoming assignments
  • POST/api/calendar/save — Save extracted assignments

Documents

  • POST/api/documents/uploadStreaming SSE upload. Runs the agentic pipeline (classifier → parallel summary/concepts/syllabus → graph merge) and emits typed SSE events the client renders as live progress: status:start, progress:classify, progress:classified, progress:extract, progress:extracted, progress:graph_update, progress:graph_updated, result:finalize, status:done. Errors emit error:failed (terminal). Idempotent on X-Request-ID — a retry with the same ID returns the previously persisted document without re-running the pipeline.
  • POST/api/documents/upload/sync — Non-streaming JSON upload. Same orchestrator under the hood, returns the persisted document as a single JSON response. Used by callers that don't need progress events.
  • GET/api/documents/user/{user_id} — List a user's documents
  • DELETE/api/documents/doc/{doc_id} — Delete a document
  • POST/api/documents/doc/{doc_id}/scan-concepts — Re-extract concepts from a stored document into the course graph
  • POST/api/documents/course/{course_id}/scan-concepts — Extend a course's concept graph from its label alone

Notes

  • GET/api/notes/user/{user_id} — List a user's notes (filter by course_id)
  • POST/api/notes — Create a note
  • GET/api/notes/{note_id} — Fetch a single note
  • PATCH/api/notes/{note_id} — Update a note (title, body, tags, course)
  • DELETE/api/notes/{note_id} — Delete a note
  • GET/api/notes/{note_id}/concepts — List concepts linked to a note
  • POST/api/notes/{note_id}/concepts — Link a graph concept to a note
  • DELETE/api/notes/{note_id}/concepts/{concept_node_id} — Unlink a concept
  • POST/api/notes/{note_id}/summarize — AI-summarize the note (agent-backed)
  • POST/api/notes/{note_id}/extract-concepts — Extract concepts, merge into the graph, link back to the note
  • POST/api/notes/{note_id}/chat — Ask a question grounded in the note (agent-backed)
  • POST/api/notes/{note_id}/send-to-tutor — Build a tutor handoff (topic + preface) from the note
  • POST/api/notes/{note_id}/generate-quiz — Pick the note's weakest linked concept to quiz on

Social

  • POST/api/social/rooms/create — Create a study room
  • POST/api/social/rooms/join — Join a study room by invite code
  • GET/api/social/rooms/{user_id} — List a user's rooms
  • GET/api/social/rooms/{room_id}/overview — Room overview with AI-generated group summary
  • GET/api/social/rooms/{room_id}/activity — Recent activity feed for a room
  • POST/api/social/rooms/{room_id}/match — Find study partners within a room
  • POST/api/social/rooms/{room_id}/leave — Leave a room
  • DELETE/api/social/rooms/{room_id}/members/{member_id} — Kick a member (room leader only)
  • GET/api/social/rooms/{room_id}/messages — Fetch room chat messages
  • POST/api/social/rooms/{room_id}/messages — Send a chat message
  • POST/api/social/school-match — Find study partners school-wide
  • GET/api/social/students — List all students with mastery stats

Auth

  • GET/api/auth/google — Redirect to Google OAuth consent screen
  • GET/api/auth/google/callback — OAuth callback, issues session token
  • GET/api/auth/me — Get current user from session token

Onboarding

  • GET/api/onboarding/courses — Search courses by name or code
  • POST/api/onboarding/profile — Save onboarding profile data

Profile

  • GET/api/profile/{user_id} — Public profile with roles, achievements, cosmetics
  • PUT/api/profile/{user_id} — Update profile fields (bio, major, links, etc.)
  • PUT/api/profile/{user_id}/settings — Update user settings
  • POST/api/profile/{user_id}/avatar — Upload a profile avatar
  • POST/api/profile/{user_id}/equip — Equip or unequip a cosmetic item
  • PUT/api/profile/{user_id}/featured-role — Set featured role on profile
  • PUT/api/profile/{user_id}/featured-achievements — Set featured achievements
  • DELETE/api/profile/{user_id} — Delete account

Admin

  • GET/POST/api/admin/roles — List / create roles
  • PATCH/DELETE/api/admin/roles/{role_id} — Update / delete a role
  • POST/DELETE/api/admin/roles/assign · /api/admin/roles/revoke — Assign / revoke a role
  • GET/POST/DELETE/api/admin/roles/cosmetics (+ /api/admin/roles/{role_id}/cosmetics) — Link cosmetics to roles
  • GET/POST/api/admin/achievements — List / create achievements
  • PATCH/DELETE/api/admin/achievements/{achievement_id} — Update / delete an achievement
  • POST/api/admin/achievements/grant — Manually grant an achievement
  • GET/POST/PATCH/DELETE achievement triggers (/api/admin/achievements/{achievement_id}/triggers, /api/admin/achievements/triggers, /api/admin/achievements/triggers/{trigger_id})
  • GET/POST/DELETE/api/admin/achievements/cosmetics (+ /api/admin/achievements/{achievement_id}/cosmetics) — Link cosmetics to achievements
  • GET/POST/api/admin/cosmetics — List / create cosmetic items
  • PATCH/DELETE/api/admin/cosmetics/{cosmetic_id} — Update / delete a cosmetic
  • GET/api/admin/users — Paginated, searchable user list
  • PATCH/api/admin/users/{user_id}/approve · /unapprove — Approve / unapprove a user
  • POST/api/admin/allowlist/approve · /api/admin/allowlist/revoke — Manage the email allowlist
  • GET/api/admin/audit — Paginated admin audit log (filter by action / target)
  • GET/api/admin/analytics/overview — Totals, 30-day series, and role counts

Feedback

  • POST/api/feedback/feedback — Submit session or general feedback
  • POST/api/feedback/issue-reports — Submit a bug/issue report

Newsletter

  • POST/api/newsletter/subscribe — Add an email to the beta / newsletter list

Environment Variables

backend/.env

VariableRequiredDescription
GEMINI_API_KEYGoogle Gemini API key
SUPABASE_URLYour Supabase project URL
SUPABASE_SERVICE_KEYSupabase service role key
ENCRYPTION_KEYAES-256-GCM key for column-level encryption (32 bytes as 64 hex chars; generate with python -c "import secrets; print(secrets.token_hex(32))")
PORTBackend port (default 5000)
FRONTEND_URLAllowed CORS origin (default http://localhost:3000)
APP_ENVDeployment environment (default production, fail-closed checks). Set local for local dev (relaxes SESSION_SECRET); set staging on the staging deploy (adds a noindex header, still fail-closed).
GOOGLE_CLIENT_IDGoogle OAuth client ID (for sign-in and Calendar)
GOOGLE_CLIENT_SECRETGoogle OAuth client secret
SESSION_SECRETHMAC secret for session tokens (min 32 bytes)
ALLOWED_EMAIL_DOMAINSComma-separated sign-in email-domain allowlist (default bu.edu). Empty value disables the check (any domain may sign in).
SUPABASE_DB_URLSupabase session-mode pooler URI (port 5432, user postgres.<ref>) — used only by the db.migrate migration runner, never at app runtime. Not the direct db.<ref> host (IPv6-only, unreachable from most networks); not port 6543 (transaction mode, breaks DDL)
LOGFIRE_TOKENIf set, traces ship to logfire.pydantic.dev. Without it, Logfire stays local-only. The Sapling scrubber redacts prompt/output content before egress regardless.
SAPLING_MODEL_CLASSIFIEROverride classifier-agent model (default gemini-2.5-flash-lite)
SAPLING_MODEL_SUMMARYOverride summary-agent model (default gemini-2.5-flash-lite)
SAPLING_MODEL_CONCEPTSOverride concept-extraction-agent model (default gemini-2.5-flash)
SAPLING_MODEL_SYLLABUSOverride syllabus-extraction-agent model (default gemini-2.5-flash)
SAPLING_MODEL_QUIZOverride quiz-generation-agent model (default gemini-2.5-flash-lite)
SAPLING_MODEL_CHAT_TUTOROverride chat-tutor-agent model (default gemini-2.5-pro)
SAPLING_MODEL_NOTE_SUMMARYOverride note-summary-agent model (default gemini-2.5-flash-lite)
SAPLING_MODEL_NOTE_CONCEPTSOverride note-concepts-agent model (default gemini-2.5-flash-lite)
SAPLING_MODEL_NOTE_CHATOverride note-chat-agent model (default gemini-2.5-flash)
OCR_ASYNC_ENABLEDWhen true, the streaming /upload route runs OCR off the request critical path with a progress:extracting_text SSE event. Default false.
DBOS_ENABLEDWhen true AND dbos is installed AND DBOS_DATABASE_URL is set, process_document runs as a checkpointed DBOS workflow with per-step resume on crash. Default false (decorators are no-op passthroughs). See docs/decisions/0011-durable-execution-dbos.md.
DBOS_DATABASE_URLPostgres connection string for DBOS metadata (separate from Supabase). Required only when DBOS_ENABLED=true.

frontend/.env.local

VariableRequiredDescription
NEXT_PUBLIC_API_URLBackend base URL (e.g. http://localhost:5000)
SESSION_SECRETSame HMAC secret as backend (for middleware token verification)

Tests

Backend — pytest, mocked Gemini + Supabase. ~660 tests.

cd backend
python -m pytest tests/ -q --ignore=tests/evals

Frontend — Vitest. Pure-logic tests (sse.ts, api.ts) run in node; component tests (e.g. DocumentUploadModal, TopNav, KnowledgeGraph3D) use jsdom + React Testing Library + @testing-library/jest-dom. Per-file // @vitest-environment jsdom directive keeps the lib tests fast.

cd frontend
npm install
npm run typecheck
npm test# vitest run
npm run test:watch # vitest watch

Evals — extraction-accuracy harness for the migrated agents. Five offline datasets (document_classification, document_summary, concept_extraction, syllabus_extraction, quiz_generation) run in replay mode against committed cassettes — no network, fully deterministic. Each task reports a per-evaluator accuracy score; a task fails only when it regresses below the committed baseline in tests/evals/baselines.json or a cassette is missing. One command runs them all:

cd backend
python tests/evals/run_all.py # replay (default), gate on baselines
SAPLING_EVAL_MODE=record python tests/evals/run_all.py # refresh cassettes (live Gemini)
SAPLING_EVAL_UPDATE_BASELINES=1 python tests/evals/run_all.py # refresh baselines from current scores

.github/workflows/evals.yml runs run_all.py in replay mode on every PR that touches backend/agents/** or the harness. The chat_tutor dataset is excluded from the offline harness because its retrieval tool reads a live Supabase (tracked under #149). See backend/tests/evals/README.md for the record/refresh workflow.

Architecture & Dev Context

Live architecture overviewdocs/architecture.md.

Architectural Decision Recordsdocs/decisions/ (append-only, MADR-minimal format). Seventeen ADRs as of merge:

  • 0001 — Adopt Pydantic AI as the agent framework
  • 0002 — Markdown-based dev-context vault structure
  • 0003 — Per-call usage_limits= and inline system prompts
  • 0004graph_service as the next agent-tool surface
  • 0005 — Quiz generation as the next agentic refactor
  • 0006 — SSE protocol choice (sse-starlette + custom mapper, not VercelAIAdapter)
  • 0007 — Drop the document orchestrator agent (saves a Gemini Pro call per upload)
  • 0008 — Per-task model routing
  • 0009 — Request correlation IDs (X-Request-ID)
  • 0010 — OCR async / two-phase upload (partial — feature flag shipped, full design deferred)
  • 0011 — Durable execution via DBOS (partial — optional shim shipped, real DBOS opt-in)
  • 0012 — Concept-by-concept streaming (deferred — needs eval data on Gemini's emission ordering first)
  • 0013 — Refactor #2 (quiz generation) shipped as quiz_agent
  • 0014 — Adaptive quiz iteration (spaced repetition + history + difficulty)
  • 0015 — Refactor #3 (chat tutor) shipped as chat_tutor_agent
  • 0016 — Refactor #4 (syllabus extraction) unified onto the agent
  • 0017 — Notetaker dynamic implementation (CRUD + four agent-backed actions)

Things that didn't workdocs/attempts/ (each entry has a mandatory "What I'd try next" section).

Slash commands for Claude Code sessions.claude/commands/log-decision.md, log-attempt.md, recall.md, sync-context.md. Run /sync-context at session start to load the most relevant ADRs as a digest.

Read-only context curator subagent.claude/agents/context-curator.md keeps the main session's context window lean by forking off vault searches into a separate subagent.

Migrations

Schema lives as ordered SQL files in backend/db/migrations/, applied in filename order. New migrations use a UTC timestamp prefix (date -u +%Y%m%d%H%M%S); the legacy NNNN_ files are frozen and must never be renamed, since the ledger keys on basename and a rename re-runs the migration. See backend/db/migrations/README.md. A minimal runner (backend/db/migrate.py) applies pending files in order and records each in a tracking table, so it's idempotent — re-running only applies what's new. The runner connects with psycopg over the session-mode pooler URI (SUPABASE_DB_URL, port 5432, user postgres.<ref>); this is the one sanctioned exception to the db/connection.py::table()-only convention, since runtime PostgREST can't execute DDL.

cd backend
SUPABASE_DB_URL=postgresql://... python -m db.migrate # apply pending
SUPABASE_DB_URL=postgresql://... python -m db.migrate --baseline # record all as applied without running (adopting an existing DB)

Migrations 00190028 are the modular schema redesign: courses split into an abstract courses table plus course_offerings and terms, user_courses became enrollments, identity split into users + user_profiles, the gradebook re-keyed onto enrollment_id, analytics re-keyed onto offerings, and the graph gained append-only node_mastery_events. The public API boundary still keys on the abstract course_id.

For staging, after applying migrations you can lay down a self-contained fake demo dataset (graph + gradebook + courses-with-term) with python -m db.seed_staging — idempotent and staging-only, never run it against production. See docs/staging/setup-checklist.md for the full staging bring-up.

License

Copyright (c) 2026 Andres Lopez, Jack He, Luke Cooper, and Jose Gael Cruz-Lopez

About

Resources

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' GitHub - SaplingLearn/Sapling · GitHub
Skip to content

Repository files navigation

Sapling

An AI-powered study companion that builds a live knowledge graph as you learn.

"preview"PythonTypeScriptNext.jsFastAPISupabaseGoogle GeminiD3.jsGit

Overview

Sapling is a study tool that adapts to how you learn. Chat with an AI tutor across three teaching modes, take adaptive quizzes, track assignments from your syllabus, and compare progress with classmates in study rooms. As you learn, a live knowledge graph maps your mastery in real time.

Features

  • Live Knowledge Graph — Your understanding is visualized as a growing node graph, with a 2D (D3/SVG, default) or 3D (WebGL) view toggle. Mastery scores update dynamically after every session and quiz, with per-course color shading and mastery-based opacity.
  • Three Teaching Modes — Socratic (guided reasoning), Expository (direct explanation), and TeachBack (you explain, Sapling corrects). Chat supports inline math (KaTeX), Mermaid diagrams, function plots, and theorem callouts.
  • Adaptive Quizzes — AI-generated quizzes targeting your weakest concepts, with difficulty scaling based on your performance and spaced-repetition scheduling that resurfaces concepts you've missed before.
  • Flashcards — Generate AI flashcards per course, or import them from paste, file (CSV/Markdown/Anki), URL, AI prompt, or photo. Study by topic with spaced-repetition ratings (Easy / Hard / Forgot).
  • Gradebook — Track your real-world grade per course. Categories + weights, per-assignment scores, per-course letter-scale overrides, and current grade calculation. Upload a syllabus and Sapling extracts categories and assignments automatically.
  • Gradescope Sync — Link a Sapling course to a Gradescope course and pull assignment grades in automatically. Sign in with a Gradescope email/password or, for BU accounts, a live SSO + Duo 2FA flow; credentials are stored encrypted and re-authenticated fresh on each sync.
  • Study Guide — Generate a Gemini-powered exam study guide from your uploaded course materials. Guides are cached per exam and can be regenerated at any time.
  • Class Intelligence — Aggregates anonymized class-wide patterns to surface common misconceptions and weak areas, personalizing your sessions.
  • Calendar & Syllabus Tracking — Paste your syllabus and Sapling extracts assignments, deadlines, and topics automatically.
  • Document Library — Upload PDFs and notes (up to 100 MB each); Sapling extracts summaries, key concept notes, and flashcard topics to enrich your knowledge graph and study guides. Uploads use a streaming SSE pipeline so the UI shows live per-phase progress ("Classifying..." → "Extracting summary, concepts, and syllabus..." → "Saved."). Concept notes can be re-scanned manually per doc or per course.
  • Notetaker — Write typed notes per course with debounced autosave and tags. Per note, Sapling can AI-summarize, extract concepts (merged into your knowledge graph and linked back to the note), answer questions in a note-grounded AI chat, send the note to the tutor, or generate a quiz targeting the note's weakest linked concept.
  • Study Rooms — Invite classmates, compare knowledge graphs, and track relative mastery across your group.
  • Room Chat — Real-time text chat with avatars inside each study room.
  • User Profiles — Public profiles with academic info, bio, featured achievements, and equipped cosmetics.
  • Achievements & Cosmetics — Unlock achievements by hitting milestones (sessions, quizzes, streaks). Equip cosmetic rewards like avatar frames, name colors, and title flairs.
  • Roles & Admin Panel — Role-based access control with an admin panel for user approval, role assignment, and content management.
  • Onboarding Flow — Multi-step onboarding that collects school, major, year, and courses after first sign-in. Sign-in itself is a popup-based Google OAuth flow launched from the landing page.
  • Newsletter — Beta-list signup directly from the landing page.
  • Feedback & Issue Reporting — Submit session feedback or report bugs directly from the app.

Tech Stack

  • Frontend — Next.js 16 (TypeScript, App Router). The knowledge graph renders in 2D via D3.js (default) or 3D via react-force-graph-3d (three.js/WebGL), lazy-loaded so the 3D stack only enters the bundle when toggled on. Vitest with jsdom + React Testing Library for unit + component tests.
  • Backend — FastAPI (Python) serving a REST API. Document ingestion runs through a Pydantic AI agentic pipeline (4 typed worker agents fanned out in parallel via asyncio.gather). Quiz generation, the chat tutor, syllabus extraction, and the notetaker (summary / concepts / chat) are also Pydantic AI agents — every LLM call in the backend goes through these agents; the legacy structured-prompt helper (services/gemini_service.py) was retired in ADR 0024.
  • AI — Google Gemini, with per-task model routing configurable via env vars. Defaults: gemini-2.5-flash-lite for classifier, summary, quiz generation, and note summary/concepts; gemini-2.5-flash for concept extraction, syllabus parsing, and note chat; gemini-2.5-pro for the chat tutor. Override per task via SAPLING_MODEL_<TASK>.
  • Streamingsse-starlette Server-Sent Events on POST /api/documents/upload for live per-phase progress. The frontend SSE consumer (frontend/src/lib/sse.ts) parses the wire format from a fetch ReadableStream so it works with multipart POSTs (which EventSource can't do).
  • ObservabilityLogfire auto-instruments Pydantic AI agent runs, tool calls, and FastAPI requests. A custom span scrubber (backend/services/logfire_scrubber.py) truncates and SHA-256-fingerprints risky attribute paths (prompt text, model output, message content) before egress so user-uploaded document text never ships verbatim. genai-prices provides per-call cost telemetry. Per-request structured logging includes a correlation ID, status, and duration.
  • OCR — Docling (layout-aware PDF → Markdown) with GOT-OCR 2.0 fallback for math/handwriting; Tesseract retained as a legacy fallback.
  • Gradescope sync — Per-user grade import via the unofficial gradescopeapi client, plus a Playwright headless-Chromium flow for BU SSO + Duo 2FA sign-in (run playwright install chromium after pip install). Credentials are stored encrypted and the app re-authenticates fresh on each sync.
  • Database — Supabase (PostgreSQL) for all persistent data
  • Encryption — AES-256-GCM column-level encryption (via the cryptography library) for user PII, document summaries/concept notes, OAuth tokens, chat messages, and gradebook notes
  • Deploy — Frontend on Cloudflare Workers via @opennextjs/cloudflare

Usage

Backend

cd backend
python3 -m venv venv
source venv/bin/activate # fish: source venv/bin/activate.fish
pip install -r requirements.txt
cp .env.example .env # fill in GEMINI_API_KEY, SUPABASE_URL, SUPABASE_SERVICE_KEY, ENCRYPTION_KEY
python3 main.py # → http://localhost:5000

Frontend

cd frontend
npm install
echo"NEXT_PUBLIC_API_URL=http://localhost:5000"> .env.local
npm run dev # → http://localhost:3000

API Endpoints

Learn

  • POST/api/learn/start-session — Start a tutoring session
  • POST/api/learn/chat — Send a chat message
  • POST/api/learn/action — Send a structured action (e.g. quiz, recap)
  • POST/api/learn/end-session — End a session
  • GET/api/learn/sessions/{user_id} — List past sessions

Graph

  • GET/api/graph/{user_id} — Fetch the user's knowledge graph
  • GET/api/graph/{user_id}/recommendations — Get next-concept recommendations
  • GET/api/graph/{user_id}/courses — List courses

Quiz

  • POST/api/quiz/generate — Generate an adaptive quiz
  • POST/api/quiz/submit — Submit answers and update mastery

Flashcards

  • POST/api/flashcards/generate — Generate flashcards for a topic
  • POST/api/flashcards/import/parse — Parse cards from paste, file, URL, or photo (no save)
  • POST/api/flashcards/import/generate — AI-generate cards from a topic / prompt
  • POST/api/flashcards/import/cleanup — Clean up parsed cards before commit
  • POST/api/flashcards/import/cloze — Convert sentences into cloze-deletion cards
  • POST/api/flashcards/import/commit — Commit parsed cards to the user's deck
  • GET/api/flashcards/user/{user_id} — Fetch a user's flashcards
  • POST/api/flashcards/rate — Rate a card (Easy / Hard / Forgot)
  • DELETE/api/flashcards/{card_id} — Delete a card

Gradebook

  • GET/api/gradebook/summary — Per-course grade summary across the user's courses (filter by semester)
  • GET/api/gradebook/courses/{course_id} — Full gradebook for a course (categories, assignments, current grade)
  • POST/api/gradebook/courses/{course_id}/categories — Create a category
  • PATCH/api/gradebook/courses/{course_id}/categories — Bulk-update categories (weights, names)
  • DELETE/api/gradebook/categories/{category_id} — Delete a category
  • POST/api/gradebook/assignments — Create an assignment
  • PATCH/api/gradebook/assignments/{assignment_id} — Update an assignment (grade, weight, due date)
  • DELETE/api/gradebook/assignments/{assignment_id} — Delete an assignment
  • PATCH/api/gradebook/courses/{course_id}/scale — Override the per-course letter-grade scale
  • POST/api/gradebook/syllabus/apply — Apply a parsed syllabus (replaces categories, dedupes assignments)

Gradescope

  • POST/api/gradescope/credentials — Test a Gradescope login, then save encrypted email/password credentials
  • POST/api/gradescope/credentials/bu-sso — Live BU SSO sign-in via headless Chromium (WebLogin + Duo), storing session cookies
  • DELETE/api/gradescope/credentials — Remove stored credentials
  • GET/api/gradescope/status — Whether credentials are saved and when the user last synced
  • GET/api/gradescope/courses — List the user's Gradescope student courses (live)
  • GET/api/gradescope/links — List Sapling-course → Gradescope-course mappings
  • POST/api/gradescope/link — Create or update a course mapping
  • DELETE/api/gradescope/link/{sapling_course_id} — Remove a course mapping
  • POST/api/gradescope/sync/{sapling_course_id} — Pull assignments from the linked Gradescope course and upsert grades into the gradebook

Study Guide

  • GET/api/study-guide/{user_id}/guide — Get (or generate) a study guide for an exam
  • GET/api/study-guide/{user_id}/cached — List all cached study guides
  • GET/api/study-guide/{user_id}/courses — List courses for guide generation
  • GET/api/study-guide/{user_id}/exams — List exam-type assignments
  • POST/api/study-guide/regenerate — Invalidate cache and regenerate a guide

Calendar

  • POST/api/calendar/extract — Extract assignments from a syllabus
  • GET/api/calendar/upcoming/{user_id} — Fetch upcoming assignments
  • POST/api/calendar/save — Save extracted assignments

Documents

  • POST/api/documents/uploadStreaming SSE upload. Runs the agentic pipeline (classifier → parallel summary/concepts/syllabus → graph merge) and emits typed SSE events the client renders as live progress: status:start, progress:classify, progress:classified, progress:extract, progress:extracted, progress:graph_update, progress:graph_updated, result:finalize, status:done. Errors emit error:failed (terminal). Idempotent on X-Request-ID — a retry with the same ID returns the previously persisted document without re-running the pipeline.
  • POST/api/documents/upload/sync — Non-streaming JSON upload. Same orchestrator under the hood, returns the persisted document as a single JSON response. Used by callers that don't need progress events.
  • GET/api/documents/user/{user_id} — List a user's documents
  • DELETE/api/documents/doc/{doc_id} — Delete a document
  • POST/api/documents/doc/{doc_id}/scan-concepts — Re-extract concepts from a stored document into the course graph
  • POST/api/documents/course/{course_id}/scan-concepts — Extend a course's concept graph from its label alone

Notes

  • GET/api/notes/user/{user_id} — List a user's notes (filter by course_id)
  • POST/api/notes — Create a note
  • GET/api/notes/{note_id} — Fetch a single note
  • PATCH/api/notes/{note_id} — Update a note (title, body, tags, course)
  • DELETE/api/notes/{note_id} — Delete a note
  • GET/api/notes/{note_id}/concepts — List concepts linked to a note
  • POST/api/notes/{note_id}/concepts — Link a graph concept to a note
  • DELETE/api/notes/{note_id}/concepts/{concept_node_id} — Unlink a concept
  • POST/api/notes/{note_id}/summarize — AI-summarize the note (agent-backed)
  • POST/api/notes/{note_id}/extract-concepts — Extract concepts, merge into the graph, link back to the note
  • POST/api/notes/{note_id}/chat — Ask a question grounded in the note (agent-backed)
  • POST/api/notes/{note_id}/send-to-tutor — Build a tutor handoff (topic + preface) from the note
  • POST/api/notes/{note_id}/generate-quiz — Pick the note's weakest linked concept to quiz on

Social

  • POST/api/social/rooms/create — Create a study room
  • POST/api/social/rooms/join — Join a study room by invite code
  • GET/api/social/rooms/{user_id} — List a user's rooms
  • GET/api/social/rooms/{room_id}/overview — Room overview with AI-generated group summary
  • GET/api/social/rooms/{room_id}/activity — Recent activity feed for a room
  • POST/api/social/rooms/{room_id}/match — Find study partners within a room
  • POST/api/social/rooms/{room_id}/leave — Leave a room
  • DELETE/api/social/rooms/{room_id}/members/{member_id} — Kick a member (room leader only)
  • GET/api/social/rooms/{room_id}/messages — Fetch room chat messages
  • POST/api/social/rooms/{room_id}/messages — Send a chat message
  • POST/api/social/school-match — Find study partners school-wide
  • GET/api/social/students — List all students with mastery stats

Auth

  • GET/api/auth/google — Redirect to Google OAuth consent screen
  • GET/api/auth/google/callback — OAuth callback, issues session token
  • GET/api/auth/me — Get current user from session token

Onboarding

  • GET/api/onboarding/courses — Search courses by name or code
  • POST/api/onboarding/profile — Save onboarding profile data

Profile

  • GET/api/profile/{user_id} — Public profile with roles, achievements, cosmetics
  • PUT/api/profile/{user_id} — Update profile fields (bio, major, links, etc.)
  • PUT/api/profile/{user_id}/settings — Update user settings
  • POST/api/profile/{user_id}/avatar — Upload a profile avatar
  • POST/api/profile/{user_id}/equip — Equip or unequip a cosmetic item
  • PUT/api/profile/{user_id}/featured-role — Set featured role on profile
  • PUT/api/profile/{user_id}/featured-achievements — Set featured achievements
  • DELETE/api/profile/{user_id} — Delete account

Admin

  • GET/POST/api/admin/roles — List / create roles
  • PATCH/DELETE/api/admin/roles/{role_id} — Update / delete a role
  • POST/DELETE/api/admin/roles/assign · /api/admin/roles/revoke — Assign / revoke a role
  • GET/POST/DELETE/api/admin/roles/cosmetics (+ /api/admin/roles/{role_id}/cosmetics) — Link cosmetics to roles
  • GET/POST/api/admin/achievements — List / create achievements
  • PATCH/DELETE/api/admin/achievements/{achievement_id} — Update / delete an achievement
  • POST/api/admin/achievements/grant — Manually grant an achievement
  • GET/POST/PATCH/DELETE achievement triggers (/api/admin/achievements/{achievement_id}/triggers, /api/admin/achievements/triggers, /api/admin/achievements/triggers/{trigger_id})
  • GET/POST/DELETE/api/admin/achievements/cosmetics (+ /api/admin/achievements/{achievement_id}/cosmetics) — Link cosmetics to achievements
  • GET/POST/api/admin/cosmetics — List / create cosmetic items
  • PATCH/DELETE/api/admin/cosmetics/{cosmetic_id} — Update / delete a cosmetic
  • GET/api/admin/users — Paginated, searchable user list
  • PATCH/api/admin/users/{user_id}/approve · /unapprove — Approve / unapprove a user
  • POST/api/admin/allowlist/approve · /api/admin/allowlist/revoke — Manage the email allowlist
  • GET/api/admin/audit — Paginated admin audit log (filter by action / target)
  • GET/api/admin/analytics/overview — Totals, 30-day series, and role counts

Feedback

  • POST/api/feedback/feedback — Submit session or general feedback
  • POST/api/feedback/issue-reports — Submit a bug/issue report

Newsletter

  • POST/api/newsletter/subscribe — Add an email to the beta / newsletter list

Environment Variables

backend/.env

VariableRequiredDescription
GEMINI_API_KEYGoogle Gemini API key
SUPABASE_URLYour Supabase project URL
SUPABASE_SERVICE_KEYSupabase service role key
ENCRYPTION_KEYAES-256-GCM key for column-level encryption (32 bytes as 64 hex chars; generate with python -c "import secrets; print(secrets.token_hex(32))")
PORTBackend port (default 5000)
FRONTEND_URLAllowed CORS origin (default http://localhost:3000)
APP_ENVDeployment environment (default production, fail-closed checks). Set local for local dev (relaxes SESSION_SECRET); set staging on the staging deploy (adds a noindex header, still fail-closed).
GOOGLE_CLIENT_IDGoogle OAuth client ID (for sign-in and Calendar)
GOOGLE_CLIENT_SECRETGoogle OAuth client secret
SESSION_SECRETHMAC secret for session tokens (min 32 bytes)
ALLOWED_EMAIL_DOMAINSComma-separated sign-in email-domain allowlist (default bu.edu). Empty value disables the check (any domain may sign in).
SUPABASE_DB_URLSupabase session-mode pooler URI (port 5432, user postgres.<ref>) — used only by the db.migrate migration runner, never at app runtime. Not the direct db.<ref> host (IPv6-only, unreachable from most networks); not port 6543 (transaction mode, breaks DDL)
LOGFIRE_TOKENIf set, traces ship to logfire.pydantic.dev. Without it, Logfire stays local-only. The Sapling scrubber redacts prompt/output content before egress regardless.
SAPLING_MODEL_CLASSIFIEROverride classifier-agent model (default gemini-2.5-flash-lite)
SAPLING_MODEL_SUMMARYOverride summary-agent model (default gemini-2.5-flash-lite)
SAPLING_MODEL_CONCEPTSOverride concept-extraction-agent model (default gemini-2.5-flash)
SAPLING_MODEL_SYLLABUSOverride syllabus-extraction-agent model (default gemini-2.5-flash)
SAPLING_MODEL_QUIZOverride quiz-generation-agent model (default gemini-2.5-flash-lite)
SAPLING_MODEL_CHAT_TUTOROverride chat-tutor-agent model (default gemini-2.5-pro)
SAPLING_MODEL_NOTE_SUMMARYOverride note-summary-agent model (default gemini-2.5-flash-lite)
SAPLING_MODEL_NOTE_CONCEPTSOverride note-concepts-agent model (default gemini-2.5-flash-lite)
SAPLING_MODEL_NOTE_CHATOverride note-chat-agent model (default gemini-2.5-flash)
OCR_ASYNC_ENABLEDWhen true, the streaming /upload route runs OCR off the request critical path with a progress:extracting_text SSE event. Default false.
DBOS_ENABLEDWhen true AND dbos is installed AND DBOS_DATABASE_URL is set, process_document runs as a checkpointed DBOS workflow with per-step resume on crash. Default false (decorators are no-op passthroughs). See docs/decisions/0011-durable-execution-dbos.md.
DBOS_DATABASE_URLPostgres connection string for DBOS metadata (separate from Supabase). Required only when DBOS_ENABLED=true.

frontend/.env.local

VariableRequiredDescription
NEXT_PUBLIC_API_URLBackend base URL (e.g. http://localhost:5000)
SESSION_SECRETSame HMAC secret as backend (for middleware token verification)

Tests

Backend — pytest, mocked Gemini + Supabase. ~660 tests.

cd backend
python -m pytest tests/ -q --ignore=tests/evals

Frontend — Vitest. Pure-logic tests (sse.ts, api.ts) run in node; component tests (e.g. DocumentUploadModal, TopNav, KnowledgeGraph3D) use jsdom + React Testing Library + @testing-library/jest-dom. Per-file // @vitest-environment jsdom directive keeps the lib tests fast.

cd frontend
npm install
npm run typecheck
npm test# vitest run
npm run test:watch # vitest watch

Evals — extraction-accuracy harness for the migrated agents. Five offline datasets (document_classification, document_summary, concept_extraction, syllabus_extraction, quiz_generation) run in replay mode against committed cassettes — no network, fully deterministic. Each task reports a per-evaluator accuracy score; a task fails only when it regresses below the committed baseline in tests/evals/baselines.json or a cassette is missing. One command runs them all:

cd backend
python tests/evals/run_all.py # replay (default), gate on baselines
SAPLING_EVAL_MODE=record python tests/evals/run_all.py # refresh cassettes (live Gemini)
SAPLING_EVAL_UPDATE_BASELINES=1 python tests/evals/run_all.py # refresh baselines from current scores

.github/workflows/evals.yml runs run_all.py in replay mode on every PR that touches backend/agents/** or the harness. The chat_tutor dataset is excluded from the offline harness because its retrieval tool reads a live Supabase (tracked under #149). See backend/tests/evals/README.md for the record/refresh workflow.

Architecture & Dev Context

Live architecture overviewdocs/architecture.md.

Architectural Decision Recordsdocs/decisions/ (append-only, MADR-minimal format). Seventeen ADRs as of merge:

  • 0001 — Adopt Pydantic AI as the agent framework
  • 0002 — Markdown-based dev-context vault structure
  • 0003 — Per-call usage_limits= and inline system prompts
  • 0004graph_service as the next agent-tool surface
  • 0005 — Quiz generation as the next agentic refactor
  • 0006 — SSE protocol choice (sse-starlette + custom mapper, not VercelAIAdapter)
  • 0007 — Drop the document orchestrator agent (saves a Gemini Pro call per upload)
  • 0008 — Per-task model routing
  • 0009 — Request correlation IDs (X-Request-ID)
  • 0010 — OCR async / two-phase upload (partial — feature flag shipped, full design deferred)
  • 0011 — Durable execution via DBOS (partial — optional shim shipped, real DBOS opt-in)
  • 0012 — Concept-by-concept streaming (deferred — needs eval data on Gemini's emission ordering first)
  • 0013 — Refactor #2 (quiz generation) shipped as quiz_agent
  • 0014 — Adaptive quiz iteration (spaced repetition + history + difficulty)
  • 0015 — Refactor #3 (chat tutor) shipped as chat_tutor_agent
  • 0016 — Refactor #4 (syllabus extraction) unified onto the agent
  • 0017 — Notetaker dynamic implementation (CRUD + four agent-backed actions)

Things that didn't workdocs/attempts/ (each entry has a mandatory "What I'd try next" section).

Slash commands for Claude Code sessions.claude/commands/log-decision.md, log-attempt.md, recall.md, sync-context.md. Run /sync-context at session start to load the most relevant ADRs as a digest.

Read-only context curator subagent.claude/agents/context-curator.md keeps the main session's context window lean by forking off vault searches into a separate subagent.

Migrations

Schema lives as ordered SQL files in backend/db/migrations/, applied in filename order. New migrations use a UTC timestamp prefix (date -u +%Y%m%d%H%M%S); the legacy NNNN_ files are frozen and must never be renamed, since the ledger keys on basename and a rename re-runs the migration. See backend/db/migrations/README.md. A minimal runner (backend/db/migrate.py) applies pending files in order and records each in a tracking table, so it's idempotent — re-running only applies what's new. The runner connects with psycopg over the session-mode pooler URI (SUPABASE_DB_URL, port 5432, user postgres.<ref>); this is the one sanctioned exception to the db/connection.py::table()-only convention, since runtime PostgREST can't execute DDL.

cd backend
SUPABASE_DB_URL=postgresql://... python -m db.migrate # apply pending
SUPABASE_DB_URL=postgresql://... python -m db.migrate --baseline # record all as applied without running (adopting an existing DB)

Migrations 00190028 are the modular schema redesign: courses split into an abstract courses table plus course_offerings and terms, user_courses became enrollments, identity split into users + user_profiles, the gradebook re-keyed onto enrollment_id, analytics re-keyed onto offerings, and the graph gained append-only node_mastery_events. The public API boundary still keys on the abstract course_id.

For staging, after applying migrations you can lay down a self-contained fake demo dataset (graph + gradebook + courses-with-term) with python -m db.seed_staging — idempotent and staging-only, never run it against production. See docs/staging/setup-checklist.md for the full staging bring-up.

License

Copyright (c) 2026 Andres Lopez, Jack He, Luke Cooper, and Jose Gael Cruz-Lopez

About

Resources

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - SaplingLearn/Sapling · GitHub
Skip to content

Repository files navigation

Sapling

An AI-powered study companion that builds a live knowledge graph as you learn.

"preview"PythonTypeScriptNext.jsFastAPISupabaseGoogle GeminiD3.jsGit

Overview

Sapling is a study tool that adapts to how you learn. Chat with an AI tutor across three teaching modes, take adaptive quizzes, track assignments from your syllabus, and compare progress with classmates in study rooms. As you learn, a live knowledge graph maps your mastery in real time.

Features

  • Live Knowledge Graph — Your understanding is visualized as a growing node graph, with a 2D (D3/SVG, default) or 3D (WebGL) view toggle. Mastery scores update dynamically after every session and quiz, with per-course color shading and mastery-based opacity.
  • Three Teaching Modes — Socratic (guided reasoning), Expository (direct explanation), and TeachBack (you explain, Sapling corrects). Chat supports inline math (KaTeX), Mermaid diagrams, function plots, and theorem callouts.
  • Adaptive Quizzes — AI-generated quizzes targeting your weakest concepts, with difficulty scaling based on your performance and spaced-repetition scheduling that resurfaces concepts you've missed before.
  • Flashcards — Generate AI flashcards per course, or import them from paste, file (CSV/Markdown/Anki), URL, AI prompt, or photo. Study by topic with spaced-repetition ratings (Easy / Hard / Forgot).
  • Gradebook — Track your real-world grade per course. Categories + weights, per-assignment scores, per-course letter-scale overrides, and current grade calculation. Upload a syllabus and Sapling extracts categories and assignments automatically.
  • Gradescope Sync — Link a Sapling course to a Gradescope course and pull assignment grades in automatically. Sign in with a Gradescope email/password or, for BU accounts, a live SSO + Duo 2FA flow; credentials are stored encrypted and re-authenticated fresh on each sync.
  • Study Guide — Generate a Gemini-powered exam study guide from your uploaded course materials. Guides are cached per exam and can be regenerated at any time.
  • Class Intelligence — Aggregates anonymized class-wide patterns to surface common misconceptions and weak areas, personalizing your sessions.
  • Calendar & Syllabus Tracking — Paste your syllabus and Sapling extracts assignments, deadlines, and topics automatically.
  • Document Library — Upload PDFs and notes (up to 100 MB each); Sapling extracts summaries, key concept notes, and flashcard topics to enrich your knowledge graph and study guides. Uploads use a streaming SSE pipeline so the UI shows live per-phase progress ("Classifying..." → "Extracting summary, concepts, and syllabus..." → "Saved."). Concept notes can be re-scanned manually per doc or per course.
  • Notetaker — Write typed notes per course with debounced autosave and tags. Per note, Sapling can AI-summarize, extract concepts (merged into your knowledge graph and linked back to the note), answer questions in a note-grounded AI chat, send the note to the tutor, or generate a quiz targeting the note's weakest linked concept.
  • Study Rooms — Invite classmates, compare knowledge graphs, and track relative mastery across your group.
  • Room Chat — Real-time text chat with avatars inside each study room.
  • User Profiles — Public profiles with academic info, bio, featured achievements, and equipped cosmetics.
  • Achievements & Cosmetics — Unlock achievements by hitting milestones (sessions, quizzes, streaks). Equip cosmetic rewards like avatar frames, name colors, and title flairs.
  • Roles & Admin Panel — Role-based access control with an admin panel for user approval, role assignment, and content management.
  • Onboarding Flow — Multi-step onboarding that collects school, major, year, and courses after first sign-in. Sign-in itself is a popup-based Google OAuth flow launched from the landing page.
  • Newsletter — Beta-list signup directly from the landing page.
  • Feedback & Issue Reporting — Submit session feedback or report bugs directly from the app.

Tech Stack

  • Frontend — Next.js 16 (TypeScript, App Router). The knowledge graph renders in 2D via D3.js (default) or 3D via react-force-graph-3d (three.js/WebGL), lazy-loaded so the 3D stack only enters the bundle when toggled on. Vitest with jsdom + React Testing Library for unit + component tests.
  • Backend — FastAPI (Python) serving a REST API. Document ingestion runs through a Pydantic AI agentic pipeline (4 typed worker agents fanned out in parallel via asyncio.gather). Quiz generation, the chat tutor, syllabus extraction, and the notetaker (summary / concepts / chat) are also Pydantic AI agents — every LLM call in the backend goes through these agents; the legacy structured-prompt helper (services/gemini_service.py) was retired in ADR 0024.
  • AI — Google Gemini, with per-task model routing configurable via env vars. Defaults: gemini-2.5-flash-lite for classifier, summary, quiz generation, and note summary/concepts; gemini-2.5-flash for concept extraction, syllabus parsing, and note chat; gemini-2.5-pro for the chat tutor. Override per task via SAPLING_MODEL_<TASK>.
  • Streamingsse-starlette Server-Sent Events on POST /api/documents/upload for live per-phase progress. The frontend SSE consumer (frontend/src/lib/sse.ts) parses the wire format from a fetch ReadableStream so it works with multipart POSTs (which EventSource can't do).
  • ObservabilityLogfire auto-instruments Pydantic AI agent runs, tool calls, and FastAPI requests. A custom span scrubber (backend/services/logfire_scrubber.py) truncates and SHA-256-fingerprints risky attribute paths (prompt text, model output, message content) before egress so user-uploaded document text never ships verbatim. genai-prices provides per-call cost telemetry. Per-request structured logging includes a correlation ID, status, and duration.
  • OCR — Docling (layout-aware PDF → Markdown) with GOT-OCR 2.0 fallback for math/handwriting; Tesseract retained as a legacy fallback.
  • Gradescope sync — Per-user grade import via the unofficial gradescopeapi client, plus a Playwright headless-Chromium flow for BU SSO + Duo 2FA sign-in (run playwright install chromium after pip install). Credentials are stored encrypted and the app re-authenticates fresh on each sync.
  • Database — Supabase (PostgreSQL) for all persistent data
  • Encryption — AES-256-GCM column-level encryption (via the cryptography library) for user PII, document summaries/concept notes, OAuth tokens, chat messages, and gradebook notes
  • Deploy — Frontend on Cloudflare Workers via @opennextjs/cloudflare

Usage

Backend

cd backend
python3 -m venv venv
source venv/bin/activate # fish: source venv/bin/activate.fish
pip install -r requirements.txt
cp .env.example .env # fill in GEMINI_API_KEY, SUPABASE_URL, SUPABASE_SERVICE_KEY, ENCRYPTION_KEY
python3 main.py # → http://localhost:5000

Frontend

cd frontend
npm install
echo"NEXT_PUBLIC_API_URL=http://localhost:5000"> .env.local
npm run dev # → http://localhost:3000

API Endpoints

Learn

  • POST/api/learn/start-session — Start a tutoring session
  • POST/api/learn/chat — Send a chat message
  • POST/api/learn/action — Send a structured action (e.g. quiz, recap)
  • POST/api/learn/end-session — End a session
  • GET/api/learn/sessions/{user_id} — List past sessions

Graph

  • GET/api/graph/{user_id} — Fetch the user's knowledge graph
  • GET/api/graph/{user_id}/recommendations — Get next-concept recommendations
  • GET/api/graph/{user_id}/courses — List courses

Quiz

  • POST/api/quiz/generate — Generate an adaptive quiz
  • POST/api/quiz/submit — Submit answers and update mastery

Flashcards

  • POST/api/flashcards/generate — Generate flashcards for a topic
  • POST/api/flashcards/import/parse — Parse cards from paste, file, URL, or photo (no save)
  • POST/api/flashcards/import/generate — AI-generate cards from a topic / prompt
  • POST/api/flashcards/import/cleanup — Clean up parsed cards before commit
  • POST/api/flashcards/import/cloze — Convert sentences into cloze-deletion cards
  • POST/api/flashcards/import/commit — Commit parsed cards to the user's deck
  • GET/api/flashcards/user/{user_id} — Fetch a user's flashcards
  • POST/api/flashcards/rate — Rate a card (Easy / Hard / Forgot)
  • DELETE/api/flashcards/{card_id} — Delete a card

Gradebook

  • GET/api/gradebook/summary — Per-course grade summary across the user's courses (filter by semester)
  • GET/api/gradebook/courses/{course_id} — Full gradebook for a course (categories, assignments, current grade)
  • POST/api/gradebook/courses/{course_id}/categories — Create a category
  • PATCH/api/gradebook/courses/{course_id}/categories — Bulk-update categories (weights, names)
  • DELETE/api/gradebook/categories/{category_id} — Delete a category
  • POST/api/gradebook/assignments — Create an assignment
  • PATCH/api/gradebook/assignments/{assignment_id} — Update an assignment (grade, weight, due date)
  • DELETE/api/gradebook/assignments/{assignment_id} — Delete an assignment
  • PATCH/api/gradebook/courses/{course_id}/scale — Override the per-course letter-grade scale
  • POST/api/gradebook/syllabus/apply — Apply a parsed syllabus (replaces categories, dedupes assignments)

Gradescope

  • POST/api/gradescope/credentials — Test a Gradescope login, then save encrypted email/password credentials
  • POST/api/gradescope/credentials/bu-sso — Live BU SSO sign-in via headless Chromium (WebLogin + Duo), storing session cookies
  • DELETE/api/gradescope/credentials — Remove stored credentials
  • GET/api/gradescope/status — Whether credentials are saved and when the user last synced
  • GET/api/gradescope/courses — List the user's Gradescope student courses (live)
  • GET/api/gradescope/links — List Sapling-course → Gradescope-course mappings
  • POST/api/gradescope/link — Create or update a course mapping
  • DELETE/api/gradescope/link/{sapling_course_id} — Remove a course mapping
  • POST/api/gradescope/sync/{sapling_course_id} — Pull assignments from the linked Gradescope course and upsert grades into the gradebook

Study Guide

  • GET/api/study-guide/{user_id}/guide — Get (or generate) a study guide for an exam
  • GET/api/study-guide/{user_id}/cached — List all cached study guides
  • GET/api/study-guide/{user_id}/courses — List courses for guide generation
  • GET/api/study-guide/{user_id}/exams — List exam-type assignments
  • POST/api/study-guide/regenerate — Invalidate cache and regenerate a guide

Calendar

  • POST/api/calendar/extract — Extract assignments from a syllabus
  • GET/api/calendar/upcoming/{user_id} — Fetch upcoming assignments
  • POST/api/calendar/save — Save extracted assignments

Documents

  • POST/api/documents/uploadStreaming SSE upload. Runs the agentic pipeline (classifier → parallel summary/concepts/syllabus → graph merge) and emits typed SSE events the client renders as live progress: status:start, progress:classify, progress:classified, progress:extract, progress:extracted, progress:graph_update, progress:graph_updated, result:finalize, status:done. Errors emit error:failed (terminal). Idempotent on X-Request-ID — a retry with the same ID returns the previously persisted document without re-running the pipeline.
  • POST/api/documents/upload/sync — Non-streaming JSON upload. Same orchestrator under the hood, returns the persisted document as a single JSON response. Used by callers that don't need progress events.
  • GET/api/documents/user/{user_id} — List a user's documents
  • DELETE/api/documents/doc/{doc_id} — Delete a document
  • POST/api/documents/doc/{doc_id}/scan-concepts — Re-extract concepts from a stored document into the course graph
  • POST/api/documents/course/{course_id}/scan-concepts — Extend a course's concept graph from its label alone

Notes

  • GET/api/notes/user/{user_id} — List a user's notes (filter by course_id)
  • POST/api/notes — Create a note
  • GET/api/notes/{note_id} — Fetch a single note
  • PATCH/api/notes/{note_id} — Update a note (title, body, tags, course)
  • DELETE/api/notes/{note_id} — Delete a note
  • GET/api/notes/{note_id}/concepts — List concepts linked to a note
  • POST/api/notes/{note_id}/concepts — Link a graph concept to a note
  • DELETE/api/notes/{note_id}/concepts/{concept_node_id} — Unlink a concept
  • POST/api/notes/{note_id}/summarize — AI-summarize the note (agent-backed)
  • POST/api/notes/{note_id}/extract-concepts — Extract concepts, merge into the graph, link back to the note
  • POST/api/notes/{note_id}/chat — Ask a question grounded in the note (agent-backed)
  • POST/api/notes/{note_id}/send-to-tutor — Build a tutor handoff (topic + preface) from the note
  • POST/api/notes/{note_id}/generate-quiz — Pick the note's weakest linked concept to quiz on

Social

  • POST/api/social/rooms/create — Create a study room
  • POST/api/social/rooms/join — Join a study room by invite code
  • GET/api/social/rooms/{user_id} — List a user's rooms
  • GET/api/social/rooms/{room_id}/overview — Room overview with AI-generated group summary
  • GET/api/social/rooms/{room_id}/activity — Recent activity feed for a room
  • POST/api/social/rooms/{room_id}/match — Find study partners within a room
  • POST/api/social/rooms/{room_id}/leave — Leave a room
  • DELETE/api/social/rooms/{room_id}/members/{member_id} — Kick a member (room leader only)
  • GET/api/social/rooms/{room_id}/messages — Fetch room chat messages
  • POST/api/social/rooms/{room_id}/messages — Send a chat message
  • POST/api/social/school-match — Find study partners school-wide
  • GET/api/social/students — List all students with mastery stats

Auth

  • GET/api/auth/google — Redirect to Google OAuth consent screen
  • GET/api/auth/google/callback — OAuth callback, issues session token
  • GET/api/auth/me — Get current user from session token

Onboarding

  • GET/api/onboarding/courses — Search courses by name or code
  • POST/api/onboarding/profile — Save onboarding profile data

Profile

  • GET/api/profile/{user_id} — Public profile with roles, achievements, cosmetics
  • PUT/api/profile/{user_id} — Update profile fields (bio, major, links, etc.)
  • PUT/api/profile/{user_id}/settings — Update user settings
  • POST/api/profile/{user_id}/avatar — Upload a profile avatar
  • POST/api/profile/{user_id}/equip — Equip or unequip a cosmetic item
  • PUT/api/profile/{user_id}/featured-role — Set featured role on profile
  • PUT/api/profile/{user_id}/featured-achievements — Set featured achievements
  • DELETE/api/profile/{user_id} — Delete account

Admin

  • GET/POST/api/admin/roles — List / create roles
  • PATCH/DELETE/api/admin/roles/{role_id} — Update / delete a role
  • POST/DELETE/api/admin/roles/assign · /api/admin/roles/revoke — Assign / revoke a role
  • GET/POST/DELETE/api/admin/roles/cosmetics (+ /api/admin/roles/{role_id}/cosmetics) — Link cosmetics to roles
  • GET/POST/api/admin/achievements — List / create achievements
  • PATCH/DELETE/api/admin/achievements/{achievement_id} — Update / delete an achievement
  • POST/api/admin/achievements/grant — Manually grant an achievement
  • GET/POST/PATCH/DELETE achievement triggers (/api/admin/achievements/{achievement_id}/triggers, /api/admin/achievements/triggers, /api/admin/achievements/triggers/{trigger_id})
  • GET/POST/DELETE/api/admin/achievements/cosmetics (+ /api/admin/achievements/{achievement_id}/cosmetics) — Link cosmetics to achievements
  • GET/POST/api/admin/cosmetics — List / create cosmetic items
  • PATCH/DELETE/api/admin/cosmetics/{cosmetic_id} — Update / delete a cosmetic
  • GET/api/admin/users — Paginated, searchable user list
  • PATCH/api/admin/users/{user_id}/approve · /unapprove — Approve / unapprove a user
  • POST/api/admin/allowlist/approve · /api/admin/allowlist/revoke — Manage the email allowlist
  • GET/api/admin/audit — Paginated admin audit log (filter by action / target)
  • GET/api/admin/analytics/overview — Totals, 30-day series, and role counts

Feedback

  • POST/api/feedback/feedback — Submit session or general feedback
  • POST/api/feedback/issue-reports — Submit a bug/issue report

Newsletter

  • POST/api/newsletter/subscribe — Add an email to the beta / newsletter list

Environment Variables

backend/.env

VariableRequiredDescription
GEMINI_API_KEYGoogle Gemini API key
SUPABASE_URLYour Supabase project URL
SUPABASE_SERVICE_KEYSupabase service role key
ENCRYPTION_KEYAES-256-GCM key for column-level encryption (32 bytes as 64 hex chars; generate with python -c "import secrets; print(secrets.token_hex(32))")
PORTBackend port (default 5000)
FRONTEND_URLAllowed CORS origin (default http://localhost:3000)
APP_ENVDeployment environment (default production, fail-closed checks). Set local for local dev (relaxes SESSION_SECRET); set staging on the staging deploy (adds a noindex header, still fail-closed).
GOOGLE_CLIENT_IDGoogle OAuth client ID (for sign-in and Calendar)
GOOGLE_CLIENT_SECRETGoogle OAuth client secret
SESSION_SECRETHMAC secret for session tokens (min 32 bytes)
ALLOWED_EMAIL_DOMAINSComma-separated sign-in email-domain allowlist (default bu.edu). Empty value disables the check (any domain may sign in).
SUPABASE_DB_URLSupabase session-mode pooler URI (port 5432, user postgres.<ref>) — used only by the db.migrate migration runner, never at app runtime. Not the direct db.<ref> host (IPv6-only, unreachable from most networks); not port 6543 (transaction mode, breaks DDL)
LOGFIRE_TOKENIf set, traces ship to logfire.pydantic.dev. Without it, Logfire stays local-only. The Sapling scrubber redacts prompt/output content before egress regardless.
SAPLING_MODEL_CLASSIFIEROverride classifier-agent model (default gemini-2.5-flash-lite)
SAPLING_MODEL_SUMMARYOverride summary-agent model (default gemini-2.5-flash-lite)
SAPLING_MODEL_CONCEPTSOverride concept-extraction-agent model (default gemini-2.5-flash)
SAPLING_MODEL_SYLLABUSOverride syllabus-extraction-agent model (default gemini-2.5-flash)
SAPLING_MODEL_QUIZOverride quiz-generation-agent model (default gemini-2.5-flash-lite)
SAPLING_MODEL_CHAT_TUTOROverride chat-tutor-agent model (default gemini-2.5-pro)
SAPLING_MODEL_NOTE_SUMMARYOverride note-summary-agent model (default gemini-2.5-flash-lite)
SAPLING_MODEL_NOTE_CONCEPTSOverride note-concepts-agent model (default gemini-2.5-flash-lite)
SAPLING_MODEL_NOTE_CHATOverride note-chat-agent model (default gemini-2.5-flash)
OCR_ASYNC_ENABLEDWhen true, the streaming /upload route runs OCR off the request critical path with a progress:extracting_text SSE event. Default false.
DBOS_ENABLEDWhen true AND dbos is installed AND DBOS_DATABASE_URL is set, process_document runs as a checkpointed DBOS workflow with per-step resume on crash. Default false (decorators are no-op passthroughs). See docs/decisions/0011-durable-execution-dbos.md.
DBOS_DATABASE_URLPostgres connection string for DBOS metadata (separate from Supabase). Required only when DBOS_ENABLED=true.

frontend/.env.local

VariableRequiredDescription
NEXT_PUBLIC_API_URLBackend base URL (e.g. http://localhost:5000)
SESSION_SECRETSame HMAC secret as backend (for middleware token verification)

Tests

Backend — pytest, mocked Gemini + Supabase. ~660 tests.

cd backend
python -m pytest tests/ -q --ignore=tests/evals

Frontend — Vitest. Pure-logic tests (sse.ts, api.ts) run in node; component tests (e.g. DocumentUploadModal, TopNav, KnowledgeGraph3D) use jsdom + React Testing Library + @testing-library/jest-dom. Per-file // @vitest-environment jsdom directive keeps the lib tests fast.

cd frontend
npm install
npm run typecheck
npm test# vitest run
npm run test:watch # vitest watch

Evals — extraction-accuracy harness for the migrated agents. Five offline datasets (document_classification, document_summary, concept_extraction, syllabus_extraction, quiz_generation) run in replay mode against committed cassettes — no network, fully deterministic. Each task reports a per-evaluator accuracy score; a task fails only when it regresses below the committed baseline in tests/evals/baselines.json or a cassette is missing. One command runs them all:

cd backend
python tests/evals/run_all.py # replay (default), gate on baselines
SAPLING_EVAL_MODE=record python tests/evals/run_all.py # refresh cassettes (live Gemini)
SAPLING_EVAL_UPDATE_BASELINES=1 python tests/evals/run_all.py # refresh baselines from current scores

.github/workflows/evals.yml runs run_all.py in replay mode on every PR that touches backend/agents/** or the harness. The chat_tutor dataset is excluded from the offline harness because its retrieval tool reads a live Supabase (tracked under #149). See backend/tests/evals/README.md for the record/refresh workflow.

Architecture & Dev Context

Live architecture overviewdocs/architecture.md.

Architectural Decision Recordsdocs/decisions/ (append-only, MADR-minimal format). Seventeen ADRs as of merge:

  • 0001 — Adopt Pydantic AI as the agent framework
  • 0002 — Markdown-based dev-context vault structure
  • 0003 — Per-call usage_limits= and inline system prompts
  • 0004graph_service as the next agent-tool surface
  • 0005 — Quiz generation as the next agentic refactor
  • 0006 — SSE protocol choice (sse-starlette + custom mapper, not VercelAIAdapter)
  • 0007 — Drop the document orchestrator agent (saves a Gemini Pro call per upload)
  • 0008 — Per-task model routing
  • 0009 — Request correlation IDs (X-Request-ID)
  • 0010 — OCR async / two-phase upload (partial — feature flag shipped, full design deferred)
  • 0011 — Durable execution via DBOS (partial — optional shim shipped, real DBOS opt-in)
  • 0012 — Concept-by-concept streaming (deferred — needs eval data on Gemini's emission ordering first)
  • 0013 — Refactor #2 (quiz generation) shipped as quiz_agent
  • 0014 — Adaptive quiz iteration (spaced repetition + history + difficulty)
  • 0015 — Refactor #3 (chat tutor) shipped as chat_tutor_agent
  • 0016 — Refactor #4 (syllabus extraction) unified onto the agent
  • 0017 — Notetaker dynamic implementation (CRUD + four agent-backed actions)

Things that didn't workdocs/attempts/ (each entry has a mandatory "What I'd try next" section).

Slash commands for Claude Code sessions.claude/commands/log-decision.md, log-attempt.md, recall.md, sync-context.md. Run /sync-context at session start to load the most relevant ADRs as a digest.

Read-only context curator subagent.claude/agents/context-curator.md keeps the main session's context window lean by forking off vault searches into a separate subagent.

Migrations

Schema lives as ordered SQL files in backend/db/migrations/, applied in filename order. New migrations use a UTC timestamp prefix (date -u +%Y%m%d%H%M%S); the legacy NNNN_ files are frozen and must never be renamed, since the ledger keys on basename and a rename re-runs the migration. See backend/db/migrations/README.md. A minimal runner (backend/db/migrate.py) applies pending files in order and records each in a tracking table, so it's idempotent — re-running only applies what's new. The runner connects with psycopg over the session-mode pooler URI (SUPABASE_DB_URL, port 5432, user postgres.<ref>); this is the one sanctioned exception to the db/connection.py::table()-only convention, since runtime PostgREST can't execute DDL.

cd backend
SUPABASE_DB_URL=postgresql://... python -m db.migrate # apply pending
SUPABASE_DB_URL=postgresql://... python -m db.migrate --baseline # record all as applied without running (adopting an existing DB)

Migrations 00190028 are the modular schema redesign: courses split into an abstract courses table plus course_offerings and terms, user_courses became enrollments, identity split into users + user_profiles, the gradebook re-keyed onto enrollment_id, analytics re-keyed onto offerings, and the graph gained append-only node_mastery_events. The public API boundary still keys on the abstract course_id.

For staging, after applying migrations you can lay down a self-contained fake demo dataset (graph + gradebook + courses-with-term) with python -m db.seed_staging — idempotent and staging-only, never run it against production. See docs/staging/setup-checklist.md for the full staging bring-up.

License

Copyright (c) 2026 Andres Lopez, Jack He, Luke Cooper, and Jose Gael Cruz-Lopez

About

Resources

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); GitHub - SaplingLearn/Sapling · GitHub
Skip to content

Repository files navigation

Sapling

An AI-powered study companion that builds a live knowledge graph as you learn.

"preview"PythonTypeScriptNext.jsFastAPISupabaseGoogle GeminiD3.jsGit

Overview

Sapling is a study tool that adapts to how you learn. Chat with an AI tutor across three teaching modes, take adaptive quizzes, track assignments from your syllabus, and compare progress with classmates in study rooms. As you learn, a live knowledge graph maps your mastery in real time.

Features

  • Live Knowledge Graph — Your understanding is visualized as a growing node graph, with a 2D (D3/SVG, default) or 3D (WebGL) view toggle. Mastery scores update dynamically after every session and quiz, with per-course color shading and mastery-based opacity.
  • Three Teaching Modes — Socratic (guided reasoning), Expository (direct explanation), and TeachBack (you explain, Sapling corrects). Chat supports inline math (KaTeX), Mermaid diagrams, function plots, and theorem callouts.
  • Adaptive Quizzes — AI-generated quizzes targeting your weakest concepts, with difficulty scaling based on your performance and spaced-repetition scheduling that resurfaces concepts you've missed before.
  • Flashcards — Generate AI flashcards per course, or import them from paste, file (CSV/Markdown/Anki), URL, AI prompt, or photo. Study by topic with spaced-repetition ratings (Easy / Hard / Forgot).
  • Gradebook — Track your real-world grade per course. Categories + weights, per-assignment scores, per-course letter-scale overrides, and current grade calculation. Upload a syllabus and Sapling extracts categories and assignments automatically.
  • Gradescope Sync — Link a Sapling course to a Gradescope course and pull assignment grades in automatically. Sign in with a Gradescope email/password or, for BU accounts, a live SSO + Duo 2FA flow; credentials are stored encrypted and re-authenticated fresh on each sync.
  • Study Guide — Generate a Gemini-powered exam study guide from your uploaded course materials. Guides are cached per exam and can be regenerated at any time.
  • Class Intelligence — Aggregates anonymized class-wide patterns to surface common misconceptions and weak areas, personalizing your sessions.
  • Calendar & Syllabus Tracking — Paste your syllabus and Sapling extracts assignments, deadlines, and topics automatically.
  • Document Library — Upload PDFs and notes (up to 100 MB each); Sapling extracts summaries, key concept notes, and flashcard topics to enrich your knowledge graph and study guides. Uploads use a streaming SSE pipeline so the UI shows live per-phase progress ("Classifying..." → "Extracting summary, concepts, and syllabus..." → "Saved."). Concept notes can be re-scanned manually per doc or per course.
  • Notetaker — Write typed notes per course with debounced autosave and tags. Per note, Sapling can AI-summarize, extract concepts (merged into your knowledge graph and linked back to the note), answer questions in a note-grounded AI chat, send the note to the tutor, or generate a quiz targeting the note's weakest linked concept.
  • Study Rooms — Invite classmates, compare knowledge graphs, and track relative mastery across your group.
  • Room Chat — Real-time text chat with avatars inside each study room.
  • User Profiles — Public profiles with academic info, bio, featured achievements, and equipped cosmetics.
  • Achievements & Cosmetics — Unlock achievements by hitting milestones (sessions, quizzes, streaks). Equip cosmetic rewards like avatar frames, name colors, and title flairs.
  • Roles & Admin Panel — Role-based access control with an admin panel for user approval, role assignment, and content management.
  • Onboarding Flow — Multi-step onboarding that collects school, major, year, and courses after first sign-in. Sign-in itself is a popup-based Google OAuth flow launched from the landing page.
  • Newsletter — Beta-list signup directly from the landing page.
  • Feedback & Issue Reporting — Submit session feedback or report bugs directly from the app.

Tech Stack

  • Frontend — Next.js 16 (TypeScript, App Router). The knowledge graph renders in 2D via D3.js (default) or 3D via react-force-graph-3d (three.js/WebGL), lazy-loaded so the 3D stack only enters the bundle when toggled on. Vitest with jsdom + React Testing Library for unit + component tests.
  • Backend — FastAPI (Python) serving a REST API. Document ingestion runs through a Pydantic AI agentic pipeline (4 typed worker agents fanned out in parallel via asyncio.gather). Quiz generation, the chat tutor, syllabus extraction, and the notetaker (summary / concepts / chat) are also Pydantic AI agents — every LLM call in the backend goes through these agents; the legacy structured-prompt helper (services/gemini_service.py) was retired in ADR 0024.
  • AI — Google Gemini, with per-task model routing configurable via env vars. Defaults: gemini-2.5-flash-lite for classifier, summary, quiz generation, and note summary/concepts; gemini-2.5-flash for concept extraction, syllabus parsing, and note chat; gemini-2.5-pro for the chat tutor. Override per task via SAPLING_MODEL_<TASK>.
  • Streamingsse-starlette Server-Sent Events on POST /api/documents/upload for live per-phase progress. The frontend SSE consumer (frontend/src/lib/sse.ts) parses the wire format from a fetch ReadableStream so it works with multipart POSTs (which EventSource can't do).
  • ObservabilityLogfire auto-instruments Pydantic AI agent runs, tool calls, and FastAPI requests. A custom span scrubber (backend/services/logfire_scrubber.py) truncates and SHA-256-fingerprints risky attribute paths (prompt text, model output, message content) before egress so user-uploaded document text never ships verbatim. genai-prices provides per-call cost telemetry. Per-request structured logging includes a correlation ID, status, and duration.
  • OCR — Docling (layout-aware PDF → Markdown) with GOT-OCR 2.0 fallback for math/handwriting; Tesseract retained as a legacy fallback.
  • Gradescope sync — Per-user grade import via the unofficial gradescopeapi client, plus a Playwright headless-Chromium flow for BU SSO + Duo 2FA sign-in (run playwright install chromium after pip install). Credentials are stored encrypted and the app re-authenticates fresh on each sync.
  • Database — Supabase (PostgreSQL) for all persistent data
  • Encryption — AES-256-GCM column-level encryption (via the cryptography library) for user PII, document summaries/concept notes, OAuth tokens, chat messages, and gradebook notes
  • Deploy — Frontend on Cloudflare Workers via @opennextjs/cloudflare

Usage

Backend

cd backend
python3 -m venv venv
source venv/bin/activate # fish: source venv/bin/activate.fish
pip install -r requirements.txt
cp .env.example .env # fill in GEMINI_API_KEY, SUPABASE_URL, SUPABASE_SERVICE_KEY, ENCRYPTION_KEY
python3 main.py # → http://localhost:5000

Frontend

cd frontend
npm install
echo"NEXT_PUBLIC_API_URL=http://localhost:5000"> .env.local
npm run dev # → http://localhost:3000

API Endpoints

Learn

  • POST/api/learn/start-session — Start a tutoring session
  • POST/api/learn/chat — Send a chat message
  • POST/api/learn/action — Send a structured action (e.g. quiz, recap)
  • POST/api/learn/end-session — End a session
  • GET/api/learn/sessions/{user_id} — List past sessions

Graph

  • GET/api/graph/{user_id} — Fetch the user's knowledge graph
  • GET/api/graph/{user_id}/recommendations — Get next-concept recommendations
  • GET/api/graph/{user_id}/courses — List courses

Quiz

  • POST/api/quiz/generate — Generate an adaptive quiz
  • POST/api/quiz/submit — Submit answers and update mastery

Flashcards

  • POST/api/flashcards/generate — Generate flashcards for a topic
  • POST/api/flashcards/import/parse — Parse cards from paste, file, URL, or photo (no save)
  • POST/api/flashcards/import/generate — AI-generate cards from a topic / prompt
  • POST/api/flashcards/import/cleanup — Clean up parsed cards before commit
  • POST/api/flashcards/import/cloze — Convert sentences into cloze-deletion cards
  • POST/api/flashcards/import/commit — Commit parsed cards to the user's deck
  • GET/api/flashcards/user/{user_id} — Fetch a user's flashcards
  • POST/api/flashcards/rate — Rate a card (Easy / Hard / Forgot)
  • DELETE/api/flashcards/{card_id} — Delete a card

Gradebook

  • GET/api/gradebook/summary — Per-course grade summary across the user's courses (filter by semester)
  • GET/api/gradebook/courses/{course_id} — Full gradebook for a course (categories, assignments, current grade)
  • POST/api/gradebook/courses/{course_id}/categories — Create a category
  • PATCH/api/gradebook/courses/{course_id}/categories — Bulk-update categories (weights, names)
  • DELETE/api/gradebook/categories/{category_id} — Delete a category
  • POST/api/gradebook/assignments — Create an assignment
  • PATCH/api/gradebook/assignments/{assignment_id} — Update an assignment (grade, weight, due date)
  • DELETE/api/gradebook/assignments/{assignment_id} — Delete an assignment
  • PATCH/api/gradebook/courses/{course_id}/scale — Override the per-course letter-grade scale
  • POST/api/gradebook/syllabus/apply — Apply a parsed syllabus (replaces categories, dedupes assignments)

Gradescope

  • POST/api/gradescope/credentials — Test a Gradescope login, then save encrypted email/password credentials
  • POST/api/gradescope/credentials/bu-sso — Live BU SSO sign-in via headless Chromium (WebLogin + Duo), storing session cookies
  • DELETE/api/gradescope/credentials — Remove stored credentials
  • GET/api/gradescope/status — Whether credentials are saved and when the user last synced
  • GET/api/gradescope/courses — List the user's Gradescope student courses (live)
  • GET/api/gradescope/links — List Sapling-course → Gradescope-course mappings
  • POST/api/gradescope/link — Create or update a course mapping
  • DELETE/api/gradescope/link/{sapling_course_id} — Remove a course mapping
  • POST/api/gradescope/sync/{sapling_course_id} — Pull assignments from the linked Gradescope course and upsert grades into the gradebook

Study Guide

  • GET/api/study-guide/{user_id}/guide — Get (or generate) a study guide for an exam
  • GET/api/study-guide/{user_id}/cached — List all cached study guides
  • GET/api/study-guide/{user_id}/courses — List courses for guide generation
  • GET/api/study-guide/{user_id}/exams — List exam-type assignments
  • POST/api/study-guide/regenerate — Invalidate cache and regenerate a guide

Calendar

  • POST/api/calendar/extract — Extract assignments from a syllabus
  • GET/api/calendar/upcoming/{user_id} — Fetch upcoming assignments
  • POST/api/calendar/save — Save extracted assignments

Documents

  • POST/api/documents/uploadStreaming SSE upload. Runs the agentic pipeline (classifier → parallel summary/concepts/syllabus → graph merge) and emits typed SSE events the client renders as live progress: status:start, progress:classify, progress:classified, progress:extract, progress:extracted, progress:graph_update, progress:graph_updated, result:finalize, status:done. Errors emit error:failed (terminal). Idempotent on X-Request-ID — a retry with the same ID returns the previously persisted document without re-running the pipeline.
  • POST/api/documents/upload/sync — Non-streaming JSON upload. Same orchestrator under the hood, returns the persisted document as a single JSON response. Used by callers that don't need progress events.
  • GET/api/documents/user/{user_id} — List a user's documents
  • DELETE/api/documents/doc/{doc_id} — Delete a document
  • POST/api/documents/doc/{doc_id}/scan-concepts — Re-extract concepts from a stored document into the course graph
  • POST/api/documents/course/{course_id}/scan-concepts — Extend a course's concept graph from its label alone

Notes

  • GET/api/notes/user/{user_id} — List a user's notes (filter by course_id)
  • POST/api/notes — Create a note
  • GET/api/notes/{note_id} — Fetch a single note
  • PATCH/api/notes/{note_id} — Update a note (title, body, tags, course)
  • DELETE/api/notes/{note_id} — Delete a note
  • GET/api/notes/{note_id}/concepts — List concepts linked to a note
  • POST/api/notes/{note_id}/concepts — Link a graph concept to a note
  • DELETE/api/notes/{note_id}/concepts/{concept_node_id} — Unlink a concept
  • POST/api/notes/{note_id}/summarize — AI-summarize the note (agent-backed)
  • POST/api/notes/{note_id}/extract-concepts — Extract concepts, merge into the graph, link back to the note
  • POST/api/notes/{note_id}/chat — Ask a question grounded in the note (agent-backed)
  • POST/api/notes/{note_id}/send-to-tutor — Build a tutor handoff (topic + preface) from the note
  • POST/api/notes/{note_id}/generate-quiz — Pick the note's weakest linked concept to quiz on

Social

  • POST/api/social/rooms/create — Create a study room
  • POST/api/social/rooms/join — Join a study room by invite code
  • GET/api/social/rooms/{user_id} — List a user's rooms
  • GET/api/social/rooms/{room_id}/overview — Room overview with AI-generated group summary
  • GET/api/social/rooms/{room_id}/activity — Recent activity feed for a room
  • POST/api/social/rooms/{room_id}/match — Find study partners within a room
  • POST/api/social/rooms/{room_id}/leave — Leave a room
  • DELETE/api/social/rooms/{room_id}/members/{member_id} — Kick a member (room leader only)
  • GET/api/social/rooms/{room_id}/messages — Fetch room chat messages
  • POST/api/social/rooms/{room_id}/messages — Send a chat message
  • POST/api/social/school-match — Find study partners school-wide
  • GET/api/social/students — List all students with mastery stats

Auth

  • GET/api/auth/google — Redirect to Google OAuth consent screen
  • GET/api/auth/google/callback — OAuth callback, issues session token
  • GET/api/auth/me — Get current user from session token

Onboarding

  • GET/api/onboarding/courses — Search courses by name or code
  • POST/api/onboarding/profile — Save onboarding profile data

Profile

  • GET/api/profile/{user_id} — Public profile with roles, achievements, cosmetics
  • PUT/api/profile/{user_id} — Update profile fields (bio, major, links, etc.)
  • PUT/api/profile/{user_id}/settings — Update user settings
  • POST/api/profile/{user_id}/avatar — Upload a profile avatar
  • POST/api/profile/{user_id}/equip — Equip or unequip a cosmetic item
  • PUT/api/profile/{user_id}/featured-role — Set featured role on profile
  • PUT/api/profile/{user_id}/featured-achievements — Set featured achievements
  • DELETE/api/profile/{user_id} — Delete account

Admin

  • GET/POST/api/admin/roles — List / create roles
  • PATCH/DELETE/api/admin/roles/{role_id} — Update / delete a role
  • POST/DELETE/api/admin/roles/assign · /api/admin/roles/revoke — Assign / revoke a role
  • GET/POST/DELETE/api/admin/roles/cosmetics (+ /api/admin/roles/{role_id}/cosmetics) — Link cosmetics to roles
  • GET/POST/api/admin/achievements — List / create achievements
  • PATCH/DELETE/api/admin/achievements/{achievement_id} — Update / delete an achievement
  • POST/api/admin/achievements/grant — Manually grant an achievement
  • GET/POST/PATCH/DELETE achievement triggers (/api/admin/achievements/{achievement_id}/triggers, /api/admin/achievements/triggers, /api/admin/achievements/triggers/{trigger_id})
  • GET/POST/DELETE/api/admin/achievements/cosmetics (+ /api/admin/achievements/{achievement_id}/cosmetics) — Link cosmetics to achievements
  • GET/POST/api/admin/cosmetics — List / create cosmetic items
  • PATCH/DELETE/api/admin/cosmetics/{cosmetic_id} — Update / delete a cosmetic
  • GET/api/admin/users — Paginated, searchable user list
  • PATCH/api/admin/users/{user_id}/approve · /unapprove — Approve / unapprove a user
  • POST/api/admin/allowlist/approve · /api/admin/allowlist/revoke — Manage the email allowlist
  • GET/api/admin/audit — Paginated admin audit log (filter by action / target)
  • GET/api/admin/analytics/overview — Totals, 30-day series, and role counts

Feedback

  • POST/api/feedback/feedback — Submit session or general feedback
  • POST/api/feedback/issue-reports — Submit a bug/issue report

Newsletter

  • POST/api/newsletter/subscribe — Add an email to the beta / newsletter list

Environment Variables

backend/.env

VariableRequiredDescription
GEMINI_API_KEYGoogle Gemini API key
SUPABASE_URLYour Supabase project URL
SUPABASE_SERVICE_KEYSupabase service role key
ENCRYPTION_KEYAES-256-GCM key for column-level encryption (32 bytes as 64 hex chars; generate with python -c "import secrets; print(secrets.token_hex(32))")
PORTBackend port (default 5000)
FRONTEND_URLAllowed CORS origin (default http://localhost:3000)
APP_ENVDeployment environment (default production, fail-closed checks). Set local for local dev (relaxes SESSION_SECRET); set staging on the staging deploy (adds a noindex header, still fail-closed).
GOOGLE_CLIENT_IDGoogle OAuth client ID (for sign-in and Calendar)
GOOGLE_CLIENT_SECRETGoogle OAuth client secret
SESSION_SECRETHMAC secret for session tokens (min 32 bytes)
ALLOWED_EMAIL_DOMAINSComma-separated sign-in email-domain allowlist (default bu.edu). Empty value disables the check (any domain may sign in).
SUPABASE_DB_URLSupabase session-mode pooler URI (port 5432, user postgres.<ref>) — used only by the db.migrate migration runner, never at app runtime. Not the direct db.<ref> host (IPv6-only, unreachable from most networks); not port 6543 (transaction mode, breaks DDL)
LOGFIRE_TOKENIf set, traces ship to logfire.pydantic.dev. Without it, Logfire stays local-only. The Sapling scrubber redacts prompt/output content before egress regardless.
SAPLING_MODEL_CLASSIFIEROverride classifier-agent model (default gemini-2.5-flash-lite)
SAPLING_MODEL_SUMMARYOverride summary-agent model (default gemini-2.5-flash-lite)
SAPLING_MODEL_CONCEPTSOverride concept-extraction-agent model (default gemini-2.5-flash)
SAPLING_MODEL_SYLLABUSOverride syllabus-extraction-agent model (default gemini-2.5-flash)
SAPLING_MODEL_QUIZOverride quiz-generation-agent model (default gemini-2.5-flash-lite)
SAPLING_MODEL_CHAT_TUTOROverride chat-tutor-agent model (default gemini-2.5-pro)
SAPLING_MODEL_NOTE_SUMMARYOverride note-summary-agent model (default gemini-2.5-flash-lite)
SAPLING_MODEL_NOTE_CONCEPTSOverride note-concepts-agent model (default gemini-2.5-flash-lite)
SAPLING_MODEL_NOTE_CHATOverride note-chat-agent model (default gemini-2.5-flash)
OCR_ASYNC_ENABLEDWhen true, the streaming /upload route runs OCR off the request critical path with a progress:extracting_text SSE event. Default false.
DBOS_ENABLEDWhen true AND dbos is installed AND DBOS_DATABASE_URL is set, process_document runs as a checkpointed DBOS workflow with per-step resume on crash. Default false (decorators are no-op passthroughs). See docs/decisions/0011-durable-execution-dbos.md.
DBOS_DATABASE_URLPostgres connection string for DBOS metadata (separate from Supabase). Required only when DBOS_ENABLED=true.

frontend/.env.local

VariableRequiredDescription
NEXT_PUBLIC_API_URLBackend base URL (e.g. http://localhost:5000)
SESSION_SECRETSame HMAC secret as backend (for middleware token verification)

Tests

Backend — pytest, mocked Gemini + Supabase. ~660 tests.

cd backend
python -m pytest tests/ -q --ignore=tests/evals

Frontend — Vitest. Pure-logic tests (sse.ts, api.ts) run in node; component tests (e.g. DocumentUploadModal, TopNav, KnowledgeGraph3D) use jsdom + React Testing Library + @testing-library/jest-dom. Per-file // @vitest-environment jsdom directive keeps the lib tests fast.

cd frontend
npm install
npm run typecheck
npm test# vitest run
npm run test:watch # vitest watch

Evals — extraction-accuracy harness for the migrated agents. Five offline datasets (document_classification, document_summary, concept_extraction, syllabus_extraction, quiz_generation) run in replay mode against committed cassettes — no network, fully deterministic. Each task reports a per-evaluator accuracy score; a task fails only when it regresses below the committed baseline in tests/evals/baselines.json or a cassette is missing. One command runs them all:

cd backend
python tests/evals/run_all.py # replay (default), gate on baselines
SAPLING_EVAL_MODE=record python tests/evals/run_all.py # refresh cassettes (live Gemini)
SAPLING_EVAL_UPDATE_BASELINES=1 python tests/evals/run_all.py # refresh baselines from current scores

.github/workflows/evals.yml runs run_all.py in replay mode on every PR that touches backend/agents/** or the harness. The chat_tutor dataset is excluded from the offline harness because its retrieval tool reads a live Supabase (tracked under #149). See backend/tests/evals/README.md for the record/refresh workflow.

Architecture & Dev Context

Live architecture overviewdocs/architecture.md.

Architectural Decision Recordsdocs/decisions/ (append-only, MADR-minimal format). Seventeen ADRs as of merge:

  • 0001 — Adopt Pydantic AI as the agent framework
  • 0002 — Markdown-based dev-context vault structure
  • 0003 — Per-call usage_limits= and inline system prompts
  • 0004graph_service as the next agent-tool surface
  • 0005 — Quiz generation as the next agentic refactor
  • 0006 — SSE protocol choice (sse-starlette + custom mapper, not VercelAIAdapter)
  • 0007 — Drop the document orchestrator agent (saves a Gemini Pro call per upload)
  • 0008 — Per-task model routing
  • 0009 — Request correlation IDs (X-Request-ID)
  • 0010 — OCR async / two-phase upload (partial — feature flag shipped, full design deferred)
  • 0011 — Durable execution via DBOS (partial — optional shim shipped, real DBOS opt-in)
  • 0012 — Concept-by-concept streaming (deferred — needs eval data on Gemini's emission ordering first)
  • 0013 — Refactor #2 (quiz generation) shipped as quiz_agent
  • 0014 — Adaptive quiz iteration (spaced repetition + history + difficulty)
  • 0015 — Refactor #3 (chat tutor) shipped as chat_tutor_agent
  • 0016 — Refactor #4 (syllabus extraction) unified onto the agent
  • 0017 — Notetaker dynamic implementation (CRUD + four agent-backed actions)

Things that didn't workdocs/attempts/ (each entry has a mandatory "What I'd try next" section).

Slash commands for Claude Code sessions.claude/commands/log-decision.md, log-attempt.md, recall.md, sync-context.md. Run /sync-context at session start to load the most relevant ADRs as a digest.

Read-only context curator subagent.claude/agents/context-curator.md keeps the main session's context window lean by forking off vault searches into a separate subagent.

Migrations

Schema lives as ordered SQL files in backend/db/migrations/, applied in filename order. New migrations use a UTC timestamp prefix (date -u +%Y%m%d%H%M%S); the legacy NNNN_ files are frozen and must never be renamed, since the ledger keys on basename and a rename re-runs the migration. See backend/db/migrations/README.md. A minimal runner (backend/db/migrate.py) applies pending files in order and records each in a tracking table, so it's idempotent — re-running only applies what's new. The runner connects with psycopg over the session-mode pooler URI (SUPABASE_DB_URL, port 5432, user postgres.<ref>); this is the one sanctioned exception to the db/connection.py::table()-only convention, since runtime PostgREST can't execute DDL.

cd backend
SUPABASE_DB_URL=postgresql://... python -m db.migrate # apply pending
SUPABASE_DB_URL=postgresql://... python -m db.migrate --baseline # record all as applied without running (adopting an existing DB)

Migrations 00190028 are the modular schema redesign: courses split into an abstract courses table plus course_offerings and terms, user_courses became enrollments, identity split into users + user_profiles, the gradebook re-keyed onto enrollment_id, analytics re-keyed onto offerings, and the graph gained append-only node_mastery_events. The public API boundary still keys on the abstract course_id.

For staging, after applying migrations you can lay down a self-contained fake demo dataset (graph + gradebook + courses-with-term) with python -m db.seed_staging — idempotent and staging-only, never run it against production. See docs/staging/setup-checklist.md for the full staging bring-up.

License

Copyright (c) 2026 Andres Lopez, Jack He, Luke Cooper, and Jose Gael Cruz-Lopez

About

Resources

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages