Try the live demo → http://18.190.241.69:8080/course/view.php?id=2 Click "Log in as a guest" on the login page to browse the course without creating an account.
A content architecture for training courses designed to be maintained by one person and adapted by AI.
MTAT separates training content into atomic, typed modules — concept, demo, exercise, assessment — so that a single base module can be AI-adapted into unlimited audience-specific variants without manual rework per variant. A CLI tool (generate-variant.py) drives the adaptation using Claude. A versioned system prompt (prompts/adapt.md) governs how Claude adapts content and what it must never change.
Training content decays. Product capabilities change. Audiences diverge. Teams multiply.
The traditional response is manual: update the deck, rework the exercises, brief the trainers, repeat for every audience and locale. At small scale, this works. At scale, it collapses — the content library drifts, variants become inconsistent, and the single person who owns the material becomes a bottleneck for every adaptation request.
MTAT is built around a different assumption: if content is structured correctly, an LLM can handle audience adaptation reliably and consistently, and one content owner can operate at the output level that previously required a full team.
Every unit of content is one of four types, with a defined pedagogical role:
| Type | Role | Typical Length |
|---|---|---|
concept |
Introduces and explains a new idea | 10–20 min |
demo |
Shows the concept applied to a real scenario | 15–25 min |
exercise |
Learner applies the concept hands-on | 20–40 min |
assessment |
Checks comprehension and surfaces gaps | 5–15 min |
Modules are sequenced by course, not embedded in a deck. The separation means any module can be reused across courses, replaced without touching adjacent content, and adapted independently.
Each module is a directory containing exactly two files:
01-concept/
├── base.md # The content — markdown, audience-agnostic
└── metadata.yaml # Structured data — ID, objectives, type, version, tags
base.md contains no front matter. It is plain Markdown that can be read by humans, processed by scripts, or fed to an LLM without parsing complexity.
metadata.yaml is the source of truth for all structured facts about the module. The adaptation tool reads it and injects a variant-specific front matter block into each generated output.
Module IDs follow the pattern: <course-id>-<sequence-number>
pe-fundamentals-01 # Course: prompt-engineering-fundamentals, Module 1
pe-fundamentals-02 # Course: prompt-engineering-fundamentals, Module 2
IDs are:
- Stable across versions (the ID never changes even when content does)
- Referenced in
prerequisitesfields to express sequencing - Used in
manifest.yamlto trace every generated variant back to its source
Every call to generate-variant.py appends a record to variants/manifest.yaml:
- module_id: pe-fundamentals-01
module_path: example-course/01-concept
audience: developer
locale: en-US
output_file: variants/01-concept/developer-en-US.md
generated_at: 2025-01-15T14:32:00Z
model: claude-opus-4-6
input_tokens: 1842
output_tokens: 2103The manifest is the audit trail. It answers: what variants exist, when they were generated, and what model produced them. When source content changes, the manifest tells you exactly which variants are now stale.
mtat/
├── README.md # This file
├── RUBRIC.md # Content quality rubric (score new modules before adding)
├── requirements.txt # Python dependencies
├── .gitignore
│
├── generate-variant.py # CLI tool: adapts any module for any audience
├── upload-to-moodle.py # CLI tool: uploads variants to local Moodle via Docker
├── docker-compose.yml # Moodle + MariaDB stack for local LMS preview
│
├── prompts/
│ └── adapt.md # System prompt governing Claude's adaptation behavior
│ # Version-controlled — changes here change all future variants
│
├── example-course/ # Complete working example: Prompt Engineering Fundamentals
│ ├── README.md
│ ├── 01-concept/
│ │ ├── base.md
│ │ └── metadata.yaml
│ ├── 02-demo/
│ │ ├── base.md
│ │ └── metadata.yaml
│ ├── 03-exercise/
│ │ ├── base.md
│ │ └── metadata.yaml
│ └── 04-assessment/
│ ├── base.md
│ └── metadata.yaml
│
└── variants/ # Generated output (gitignored except manifest)
└── manifest.yaml # Audit trail of all generated variants
Create and activate a virtual environment, then install:
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txtOn Windows:
.venv\Scripts\activateYou'll need to run
source .venv/bin/activateat the start of each new terminal session before using the script.
export ANTHROPIC_API_KEY=your_key_hereOr add it to a .env file in the repo root:
ANTHROPIC_API_KEY=your_key_here
python3 generate-variant.py --module example-course/01-concept --audience developerOutput is written to variants/01-concept/developer-en-US.md. The manifest is updated at variants/manifest.yaml.
upload-to-moodle.py spins up a local Moodle LMS via Docker and uploads your generated variants as Page resources inside a course — one page per audience variant. This gives you a realistic learner view of how the content renders and reads in an actual LMS.
- Docker Desktop installed and running
- Virtual env active with dependencies installed (see Quickstart step 1)
- At least one set of generated variants in
variants/manifest.yaml
docker compose up -dFirst run builds a custom Moodle image (downloads the base PHP/Apache image and Moodle 4.3 source from GitHub — expect 5–10 minutes and several hundred MB). Watch progress with:
docker compose logs -f moodleOnce you see Apache log lines, open http://localhost:8080 — you'll land on the Moodle web installer.
Complete the installer (one-time, ~3 minutes):
- Confirm the detected paths and click Next
- Choose MariaDB as the database type
- Database settings — fill in exactly:
Field Value Database host mariadbDatabase name moodleDatabase user moodleDatabase password moodlepassword - Accept the license, let it run the environment checks, then install
- Create the admin account — use whatever username/password you like (note it down — you'll need it for
--passwordwhen uploading variants) - Set the site name to anything (e.g.,
MTAT Preview) and finish
After the installer completes you'll be logged in as admin.
# Generate all four audience variants for the example concept module
python3 generate-variant.py --module example-course/01-concept --audience developer
python3 generate-variant.py --module example-course/01-concept --audience executive
python3 generate-variant.py --module example-course/01-concept --audience champion
python3 generate-variant.py --module example-course/01-concept --audience technical-writerOn first run, the script enables the Moodle REST API via docker exec and caches the token locally. Pass the admin password you set during the installer:
python3 upload-to-moodle.py --setup-moodle --password <your-admin-password>On subsequent runs (token already cached):
python3 upload-to-moodle.py --password <your-admin-password>To upload only one course's variants:
python3 upload-to-moodle.py --password <your-admin-password> --course-id prompt-engineering-fundamentalsYou can also set the password via environment variable to avoid typing it each time:
export MOODLE_ADMIN_PASS=<your-admin-password>
- Go to http://localhost:8080/my/courses.php
- Open the course that matches your
course_id(e.g., "Prompt Engineering Fundamentals") - Each audience variant appears as a Page resource:
Module Title [developer / en-US],Module Title [executive / en-US], etc. - Click through each to compare how the same learning objective reads for different audiences
# Stop (data is preserved in Docker volumes)
docker compose stop
# Restart
docker compose start
# Wipe everything and start fresh
docker compose down -vThe
.moodle-tokenfile (gitignored) caches your API token locally. Delete it to force re-authentication.
python3 generate-variant.py [OPTIONS]
Options:
--module PATH Path to the module directory. Required.
Must contain base.md and metadata.yaml.
--audience TEXT Target audience. Required.
Built-in presets: developer, executive, champion, technical-writer
Any other string is passed to Claude as a custom audience description.
--locale TEXT Output locale as a BCP 47 tag. Default: en-US
Examples: es-MX, fr-FR, ja-JP, pt-BR
--output DIR Output directory. Default: variants/
| Preset | Who they are | Adaptation focus |
|---|---|---|
developer |
Engineers building with LLMs and APIs | Technical precision, code examples, mechanism over metaphor |
executive |
Senior leaders making investment decisions | ROI framing, business outcomes, strategic context |
champion |
Internal trainers running team rollouts | Facilitation notes, timing, discussion prompts, debrief guides |
technical-writer |
Doc and content professionals | Analogies to content workflows, structured authoring patterns |
Custom audiences are supported — pass any descriptive string and Claude will adapt accordingly:
python3 generate-variant.py --module example-course/01-concept \
--audience "healthcare compliance officers with no prior AI experience"The --locale flag triggers full translation of all text content. Code blocks are preserved exactly; only inline comments and string values within code are translated.
python3 generate-variant.py --module example-course/02-demo \
--audience developer --locale es-MX- Create a subdirectory under the repo root (e.g.,
my-new-course/). - Create one subdirectory per module, numbered sequentially.
- In each module directory, create
base.mdandmetadata.yaml. - Score the module against
RUBRIC.mdbefore treating it as production-ready. - Generate your first variants:
python3 generate-variant.py --module my-new-course/01-concept --audience developer
id: <course-id>-<sequence> # e.g., api-essentials-01
title: "Module Title"
module_type: concept # concept | demo | exercise | assessment
course_id: <course-id> # e.g., api-essentials
version: "1.0"
audience: general
locale: en-US
estimated_minutes: 15
prerequisites: [] # list of module IDs this module depends on
learning_objectives:
- Objective phrased as a learner outcome (starts with a verb)
- Another objective
tags:
- relevant-tag
last_updated: "YYYY-MM-DD"base.md contains prose. metadata.yaml contains structured data. They serve different consumers: humans and editors read base.md; scripts, LLMs, and quality checks read metadata.yaml. Mixing them (e.g., front matter in base.md) creates parsing friction and makes the metadata harder to query programmatically.
prompts/adapt.md governs what Claude does with every module. A change to the system prompt changes all future variants. Keeping it in version control means you can:
- See exactly what changed between prompt versions
- Attribute quality differences to specific prompt changes
- Roll back if a prompt update degrades output quality
- Require review before prompt changes merge
Token counts in manifest.yaml make cost visible. Over time, the manifest tells you the average cost per variant type, which audiences require the most tokens (and why), and whether cost is trending up as modules grow. Cost-blindness is how content operations quietly become expensive.
Generated variants are outputs, not source. Committing them creates merge conflicts with no resolution strategy and inflates repo history with content that can be regenerated at any time. The manifest provides the index; the source modules provide the canonical content; the CLI provides the generation.
A monolithic course document (one big deck or document) is difficult to adapt by part, reuse across courses, or update without reviewing the whole. Atomic modules can be updated individually, recombined into different course sequences, adapted independently for different audiences, and quality-checked at the unit level. The overhead of the directory structure pays for itself the first time you need to update one module without touching the others.
Add a key-value pair to AUDIENCE_PROFILES in generate-variant.py. The value should describe who the audience is and how to adapt for them in 3–5 sentences.
Edit prompts/adapt.md. Commit the change with a clear message describing what behavior you are changing and why. Run a test generation and compare before/after outputs before merging.
Pipe base.md content to Claude with the rubric from RUBRIC.md before accepting a new module. A simple scoring script that calls the API with the rubric as a system prompt and the module content as the user message can automate this check.
See Previewing Variants in Moodle for a full local Moodle setup. The variants/manifest.yaml is designed to be machine-readable. A sync script can read the manifest, find variants newer than the LMS's last-known version, and push updated content to the platform's API. Each module's stable ID provides the mapping key.