Tutor
Drop a book in. Get a course out.
Tutor
Drop a book in. Get a course out.
Tutor turns books, papers, and documents into structured, navigable courses with quizzes and progress tracking. Hand it a PDF, EPUB, DOCX, or a Project Gutenberg ID — a multi-LLM pipeline extracts concepts, correlates them into a knowledge graph, and writes the learning path on the other side. RAG retrieval keeps every quiz and section grounded in the actual source material.
Full architecture rationale, invariants, and the non-negotiable rules live in docs/BIBLE.md — this file is the practical "how to build, run, and use it" tour. Where the two disagree, the bible wins.
Table of contents
- What it does
- How it works
- Stack
- Project layout
- Domain model
- Getting started
- CLI reference (
Tutor.Cli) - The
Courses/content model - Configuration
- Export.ps1
- Tests
- Documentation canon (
docs/) - Code style
- Known limitations
What it does
- You bring the source. Tutor builds the course. A PDF, EPUB, DOCX, HTML, legacy
.doc/.rtf/.odt,.mobi/.azw/.azw3, or a Project Gutenberg ID goes in; a hierarchical course (Lessons → Sections → concepts, with quizzes) comes out. No manual authoring required. - The knowledge graph is the product, not the source bytes. Concepts are extracted per chunk, correlated across resources into one graph, and the course is generated from the graph. The source is retained for grounding, never replayed verbatim.
- Answers are grounded. Every quiz prompt and section fill is retrieved from the actual source via embeddings + RAG — Tutor does not invent content that isn't on the page.
- Courses travel. A finished course is a self-contained
.tutorbundle — structure, concepts, quizzes, and pre-computed embeddings — that installs without re-running the (slow, paid) LLM pipeline. - Same engine, two front doors.
Tutor.CliandTutor.Blazorregister the identical service graph, so anything the CLI builds, the Blazor app can read with zero translation, and vice versa.
How it works
SOURCE FILE -> PARSE -> CHUNK -> EMBED -> EXTRACT CONCEPTS -> CONCEPT MAP
|
KNOWLEDGE GRAPH <- CORRELATE <------+
|
COURSE STRUCTURE <------+
|
.tutor BUNDLE <---------+ (share / load / unload)
- Eight input formats. Phase A (managed C#, NuGet-based):
.txt,.md,.html/.htm,.epub,.pdf,.docx. Phase B (shell-out):.doc/.rtf/.odtvia LibreOffice;.mobi/.azw/.azw3via Calibre'sebook-convert. Each Phase B parser fails cleanly with install instructions if the external tool isn't on the machine. - OCR fallback.
TesseractPdfOcrServicehandles scanned PDFs; if the native Tesseract libs or trained data aren't available it goes silent and falls back to text-only extraction rather than crashing the run. - Pluggable LLMs.
LlmServiceRouterroutes chat/reasoning calls toOpenAIService,ClaudeService,DeepSeekService, orGeminiServicebased on aSELECTED_MODELpreference (defaultClaude, overridable per CLI invocation with--llm). Embeddings always go through OpenAI regardless of the selected chat provider. Transport (auth, retry, circuit breaker) is owned byMindAttic.Legion; no pipeline code may reference a vendor SDK directly. AKimiServicealso exists inTutor.Core/Services/KimiService.csbut is not currently wired intoLlmServiceRouter's switch. - Knowledge graph.
ConceptExtractionService/ConceptMapServiceextract a concept JSON per chunk;ConceptCorrelationService+KnowledgeGraphServicecorrelate concepts across resources via LSH + SimHash;OrphanConceptLinkerServicereattaches strays;DynamicConceptExpansionServiceandConceptMergeServicerefine the graph further. The course is generated from the graph, not authored by hand. - RAG.
EmbeddingService+VectorStoreServiceprovide semantic search overContentChunks;SimHashService+LSHServicedo near-duplicate detection. - Progress and quizzes.
UserProgressServiceandLearningPathServicedrive per-user mastery-gated lesson unlocking;QuizGenerationService/QuizServiceback an LLM-generated (or pre-baked) quiz controller;FinalExamService+CertificateService/CertificateAuthorityhandle course completion.
Stack
| Layer | Technology |
|---|---|
| Host | ASP.NET Core Blazor Server (net10.0) |
| Headless | tutor CLI (Tutor.Cli) — same DI graph as the Blazor host |
| LLM transport | MindAttic.Legion (OpenAI / Claude / DeepSeek / Gemini) |
| Credentials | MindAttic.Vault — %APPDATA%\MindAttic\LLM\providers.json |
| Parsing | UglyToad.PdfPig (PDF), VersOne.Epub (EPUB), AngleSharp (HTML), Open-XML (DOCX) + LibreOffice / Calibre shell-outs |
| OCR | Tesseract (TesseractPdfOcrService), trained data downloaded to Tutor.Core/tessdata on first build |
| RAG | In-process vector store (VectorStoreService) + LSH/SimHash |
| Auth | MindAttic.Authentication v1.0.0 — SQL Server (TutorAuthDbContext), Argon2id + pepper, idle/absolute session expiry |
| Tests | NUnit (Tutor.Tests) + Cypress (Tutor.Cypress) |
Project layout
| Project / folder | Purpose |
|---|---|
Tutor.Core |
Parsers, the full pipeline, domain models, storage services. Everything substantive lives here. |
Tutor.Shared |
Razor components shared by the host — layout, pages (Home, Library, Learn, Courses, ConceptGraph, Settings, Users, Login, …), quiz/exam/certificate tabs, chat. |
Tutor.Blazor |
Blazor Server host. Composition root (Program.cs), DI wiring, middleware, appsettings.json. |
Tutor.Cli |
tutor headless binary. Mirrors Blazor's DI graph; owns bundle export/import and Gutenberg fetch. |
Tutor.Tests |
NUnit unit/component suite (parsers, services, packaging, models — see Tests). |
Tutor.Cypress |
End-to-end browser tests (sibling Node project, not part of Tutor.slnx). |
Courses/ |
Pre-built .tutor bundles shipped with the repo — see content model. |
dist/ |
Local scratch output from CLI runs (smoke tests, batch logs, exported bundles) — not part of the build. |
docs/ |
Codex documentation canon — see below. |
tools/ |
codex.ps1 (docs digest/doctor) and build-readme.ps1 (README → README.htm renderer, see below). |
scripts/ |
Landing-page build scripts referenced by package.json (see note — currently empty; kept for the npm script wiring). |
Export.ps1 |
Ad-hoc source-export utility — see below. |
index.htm |
The mindattic.com/tutor landing page (built separately from README.md; do not confuse with README.htm). |
Tutor.Core references MindAttic.Legion (..\..\MindAttic.Legion\MindAttic.Legion) and MindAttic.Vault as sibling-repo project references — Tutor is not standalone-buildable outside the MindAttic workspace layout.
Domain model
The nouns the pipeline and storage layer operate on (Tutor.Core/Models):
| Model | Meaning |
|---|---|
CourseResource |
One ingested source (book/paper/doc). Has original + AI-formatted content. Produces exactly one ConceptMap (1:1). |
ConceptMap |
Concepts + relationships + complexity ordering for a single resource. |
ConceptMapCollection |
The aggregated graph across all of a course's resources (collection_{courseId}). |
Course |
Lightweight metadata + references only — resource IDs, the collection ID, the structure ID. Carries no content directly. |
CourseStructure |
The learning path: ordered Lessons → hierarchical Sections → concept IDs, plus baked PreGeneratedQuestions (quiz). |
ContentChunk |
A text snippet + its embedding + LSH/SimHash signatures. The RAG substrate. |
UserProgress |
Per-user, per-course mastery state driving lesson gating. |
CourseCertificate |
Issued on passing the final exam. |
Invariant: a
Coursenever embeds content. Content lives in resources, structure, concept maps, and chunks; the course just points at them — this keeps bundles composable and storage deduplicated (seedocs/BIBLE.md§4.2).
Getting started
Prerequisites: .NET 10 SDK. Optional: LibreOffice (.doc/.rtf/.odt) and Calibre (.mobi/.azw/.azw3). At least one LLM provider API key registered with MindAttic.Vault.
dotnet build Tutor.slnx
dotnet run --project Tutor.Blazor
# -> https://localhost:7200 (HTTPS) or http://localhost:5200 (HTTP)
# see Tutor.Blazor/Properties/launchSettings.json
dotnet run --project Tutor.Cli -- help
LLM API keys resolve through MindAttic.Vault's standard chain: %APPDATA%\MindAttic\LLM\providers.json, layered under environment variables, so the CLI uses the same working keys as every other MindAttic app. Configure them from the Blazor UI's Settings page before running the CLI.
CLI reference (Tutor.Cli)
The tutor binary shares Tutor.Core's full DI graph with the Blazor host, so a course built by one is readable by the other with zero translation. Verbs are dispatched in Tutor.Cli/Program.cs; each is backed by a class under Tutor.Cli/Commands/.
| Command | What it does |
|---|---|
tutor gutenberg <book-id> [--course "Name"] [--description "..."] [--allow-duplicate] |
Download a Project Gutenberg work by ID and import it as a new course. |
tutor gutenberg-top10 [--dry-run] [--allow-duplicate] [--export-dir <dir>] [--quiz-mode baked\|dynamic\|both] |
Drive the curated top-10 (Moby Dick, Pride and Prejudice, Frankenstein, Sherlock Holmes, Alice, Dorian Gray, Tom Sawyer, Treasure Island, Gulliver's Travels, Dracula) sequentially. Skips books whose course name already exists, so a re-run after a partial failure resumes naturally. With --export-dir, writes each course to <dir>/<Title>.tutor. Long-running: roughly 2 hours per book and meaningful API spend. |
tutor import <path> --course "Name" [--description "..."] [--author "..."] [--title "..."] [--quiz-mode ...] [--allow-duplicate] |
Import one local file as a new course. Parser is picked from the file extension. |
tutor build-course <dir-or-zip> [--course "Override Name"] [--quiz-mode ...] [--export <out.tutor>] [--allow-duplicate] |
Build one course out of many source files in a single command — the headless equivalent of adding each resource in the UI and clicking "Build Course". Point it at a directory or .zip containing a manifest.json plus the files it lists (see shape below). With --export, also writes the redistributable .tutor bundle. |
tutor export <course-id> <output.tutor> |
Export a course (resources, concept map, structure, embeddings) to a single shareable .tutor bundle. |
tutor import-bundle <file.tutor> [--course "Override Name"] [--allow-duplicate] / tutor install <file.tutor> [...] |
Restore a course from a .tutor bundle (alias: install). Legacy .tutorcourse files are also accepted. All IDs are rewritten so a re-import never collides with existing data. Skips the LLM pipeline entirely (embeddings ride along) — typically under 1 second regardless of book size. |
tutor list |
List all courses on this machine. |
tutor delete <course-id> [--dry-run] |
Remove a course and cascade-delete its resources, concept maps, course structure, embeddings, and ConceptMapCollection file. --dry-run prints what would be removed without touching anything. |
tutor fetch <...> |
FetchOnlyCommand — download a remote source without parsing it. |
tutor parse <...> |
ParseOnlyCommand — parse a source without running the LLM pipeline. |
tutor diag-keys |
Print masked LLM key/preference diagnostics (Vault-resolved key lengths/suffixes, current SELECTED_MODEL). |
tutor help |
Show CLI usage. |
Global flags (stripped before the verb dispatch, so any command accepts them):
| Flag | Effect |
|---|---|
--verbose |
Turns on Tutor.Core's trace-level logging, forwarded to stderr as [LEVEL] message. |
--llm <openai\|claude\|deepseek\|gemini> |
Overrides the chat/reasoning provider for this run (default claude). Embeddings always use OpenAI regardless. Persisted to the shared SELECTED_MODEL preference that LlmServiceRouter reads. |
--quiz-mode baked\|dynamic\|both |
Controls section quizzes: baked/both (default) pre-generate and bundle questions for offline play; dynamic skips pre-generation and lets the runtime generate them live from the bundled concept maps. |
--allow-duplicate |
By default a duplicate-name course is rejected; pass this to intentionally create a second copy (e.g. a different translation/printing). |
build-course's manifest.json shape:
{
"name": "...",
"description": "...",
"quizMode": "both",
"items": [
{ "file": "ch1.epub", "title": "...", "author": "..." }
]
}
Course data is stored at %LocalAppData%\Tutor\... and shared with the Blazor UI — anything imported via the CLI shows up there immediately.
Note on drift: the CLI verbs above (
gutenberg-top10,import,build-course,fetch,parse,diag-keys) are read directly fromProgram.cs's verb switch and its embeddedPrintHelp()text. If you editProgram.cs's command surface, update this table in the same change.
The Courses/ content model
Courses/ ships ten pre-built .tutor bundles — self-contained zips carrying course metadata, the learning structure (lessons → topics → sections), concept maps, baked quiz questions, and RAG embeddings, so they install without re-running the (slow, paid) LLM pipeline. One file per course; bundles are independent and are never merged (TUT-LAW-3).
Included (all Project Gutenberg sources): Alice's Adventures in Wonderland, Dracula, Frankenstein, Gulliver's Travels, Moby Dick; Or, The Whale, Pride and Prejudice, The Adventures of Sherlock Holmes, The Adventures of Tom Sawyer, The Picture of Dorian Gray, Treasure Island. See Courses/README.md for the authoritative list.
Install a bundled course:
tutor install "Courses/Moby Dick; Or, The Whale.tutor"
(tutor install is an alias of tutor import-bundle.) All IDs are rewritten on import, so installing the same bundle twice yields two distinct courses rather than overwriting one. Once installed, the course appears in the Tutor Blazor UI automatically.
Remove a course:
tutor list # find the course id
tutor delete <course-id> # cascades resources, structure, concept maps, embeddings
Add a new bundle to Courses/ — build it with the CLI, then commit the resulting .tutor file:
tutor gutenberg-top10 --export-dir Courses --quiz-mode both
# or, for an arbitrary source directory/zip:
tutor build-course <dir-or-zip> --export "Courses/My Course.tutor"
Bundles carry embeddings and can be several MB each; they are committed to the repo so courses travel with it. See RFC 0001 — Course Packaging & Sharing for the archive's internal layout (manifest.json, course.json, courseStructure.json, per-resource JSON/text, concept maps, chunks.json) and the roadmap toward an in-app load/unload library.
Configuration
Connection string resolution priority (Tutor.Blazor's data store):
ConnectionStrings__Tutorenvironment variableConnectionStrings:Tutorinappsettings.json- LocalDB fallback
The auth database (TutorAuthDbContext) resolves its own connection string the same way, keyed TutorAuth (ConnectionStrings__TutorAuth env var, else ConnectionStrings:TutorAuth, else LocalDB).
LLM credentials follow MindAttic.Vault's standard resolution chain — %APPDATA%\MindAttic\LLM\providers.json or MindAttic:Vault:LLM:* in IConfiguration.
Export.ps1
Export.ps1 at the repo root is a generic source-bundling utility, not Tutor-specific tooling — its header comments describe a Unity project export ("Export Unity project sources... Default: ONLY .cs files") and it does not reference any Tutor concept. In this repo it walks the tree from wherever it's invoked, collects .cs files (extra extensions like .prefab/.meta/.unity are commented out and unused here), skips noisy directories (.git, .vs, obj, bin, etc.), and writes a single ExportedScripts.txt containing a JSON manifest (path/SHA-256/size/line count per file) followed by the full text of every file, delimited by <<<FILE START>>> / <<<FILE END>>> markers. It appears to be a shared personal script reused across repos (including non-.NET ones) rather than a maintained part of the Tutor build. Run it with:
powershell -File Export.ps1
ExportedScripts.txt (checked into the repo, ~860 KB) is its most recent output.
Tests
dotnet test Tutor.Tests
Tutor.Tests (NUnit) is organized into Fakes/, Models/, Packaging/, Parsers/, and Services/ subfolders, covering parsers, the concept-map JSON shape, packaging (export/import round-trips), auth import/admin contracts, and the full course lifecycle (lock → unlock → final exam → certificate → unload) via FullCourseLifecycleTests. As of the last recorded bible update (2026-06-07): 380 passed, 0 failed, 0 skipped, build clean. The end-to-end LLM pipeline itself (extraction, section fill) is deliberately not automated — it's paid and non-deterministic — so those stories are marked 🟡 partial in docs/USER_STORIES.md even though the surrounding primitives are pinned.
cd Tutor.Cypress
npm install
npm run cypress:run # headless; Tutor.Blazor must be running first
# or: npm run cypress:open (interactive runner)
Tutor.Blazor must be reachable at http://localhost:5200 (default) before running Cypress — start it with dotnet run --project Tutor.Blazor. Override the base URL with $env:CYPRESS_BASE_URL = "https://localhost:7200". Authenticated specs use a cy.login() custom command; provide credentials via $env:CYPRESS_username / $env:CYPRESS_password.
Specs in Tutor.Cypress/e2e/:
| Spec | Covers |
|---|---|
smoke.cy.ts |
Basic app-is-alive check. |
auth.cy.ts |
Login/redirect guards. |
courses.cy.ts |
Course library listing/navigation. |
course-flow.cy.ts |
Deterministic course-flow wiring (lock/unlock, navigation). |
concept-graph.cy.ts |
Concept graph page rendering. |
quiz.cy.ts |
Quiz UI wiring (not the LLM-generated content itself — that's pinned by FullCourseLifecycleTests instead). |
Definition of done for a feature: clean dotnet build Tutor.slnx, green Tutor.Tests, and — for anything user-facing — a Cypress guard or a lifecycle assertion. No vendor lock-in introduced; whole-number versioning respected (HOUSE-LAW-8).
Documentation canon (docs/)
Tutor has adopted the MindAttic "Codex" documentation standard — a fact lives in exactly one layer, linked by stable ID, never by line number:
| Layer | File | Role |
|---|---|---|
| L0 | docs/BIBLE.md |
What Tutor is/is not, architecture canon, the Laws ({#TUT-LAW-n}), verified state, active frontier, glossary. |
| L1 | docs/AMENDMENTS.md |
Append-only change log (TUT-A<n>); an amendment wins over the bible. Currently empty — bible is epoch 0. |
| L2 | docs/USER_STORIES.md |
Acceptance stories (TUT-US-<Epic><n>), each ✅ citing its verifying test. |
| rfc | docs/rfc/0001-course-packaging.md |
Design note for the in-app course load/unload/share roadmap — compares .tutor to MindAttic.Ideas's .idea packaging and lays out the upgrade path. |
| generated | docs/BIBLE.digest.md |
Produced by tools/codex.ps1 digest; injected as session context. Never hand-edit. |
Org-wide rules (whole-number versioning, soft-disable-never-delete, credentials-through-Vault, provider-agnostic LLMs, guarded-zip packaging, one-engine-many-front-doors, MindAttic.Authentication, verified-not-asserted done) live once in ../MindAttic.HouseRules.md and are inherited by reference from docs/BIBLE.md §5 — see that section for how each maps onto Tutor specifically.
Before editing anything under docs/, run pwsh tools/codex.ps1 doctor (must pass); run pwsh tools/codex.ps1 digest after touching BIBLE.md.
Regenerating this file as HTML: tools/build-readme.ps1 renders this README.md to README.htm at the repo root, using the shared engine at ../codex-standard/build-readme.ps1 (one engine for every MindAttic repo, so every README.htm looks and behaves identically). Run it with:
powershell -NoProfile -ExecutionPolicy Bypass -File tools\build-readme.ps1
README.htm is a separate artifact from index.htm at the repo root — index.htm is the mindattic.com landing page (built by a different, currently-retired pipeline; see the note under Project layout) and should not be confused with, or overwritten by, the README renderer.
Code style
- Private fields:
camelCasewithout underscore prefix. - Constructors:
this.x = xto disambiguate.
Known limitations
- Quiz attribution during mid-circuit starts.
QuizServicereads the user ID fromIHttpContextAccessor, which isnullafter the initial Blazor Server render, so mid-circuit quiz starts attribute to"anonymous". Tracked as a TODO inTutor.Core/Services/Quiz/QuizService.csand as story B5. Fix is to thread the ID fromAuthenticationStateinstead. - Not multi-tenant. Single-host Blazor Server app with SQL-backed auth; designs should not assume horizontal scale-out or per-tenant isolation (
docs/BIBLE.md§3). - Landing-page build scripts are stale.
package.json'sbuild/deploynpm scripts point atscripts/cli/build-html.jsandscripts/cli/deploy.ps1, but that directory is currently empty — the local FTP landing-page machinery was retired in favor of routing/deploythroughMindAttic.Deploy(see git history onscripts/cli). The npm script entries were left in place but do not currently resolve to files in this repo.