A FastAPI service that generates verified, structured learning plans using Claude AI. The engine designs multi-phase curricula with live-validated resource links sourced from YouTube and web search APIs.
Ask an LLM for learning resources and it will confidently invent URLs that don't exist. This project solves that with a hybrid RAG design that splits the work by what each side is good at: Claude does the reasoning (curriculum structure, sequencing, time budgeting) and emits only search queries — never URLs — while live YouTube and Serper APIs resolve those queries into real, validated links. The same boundary-drawing applies to any production LLM system: let the model reason, let deterministic systems supply the facts.
At a glance
- Zero hallucinated URLs by construction — the LLM never generates links, retrieval APIs do
- Resource lookups run concurrently with
asyncio; graceful degradation when optional APIs are unavailable - Prompt caching on the fixed system prompt (Anthropic
cache_control: ephemeral) cuts repeated input-token costs by up to 90% - Typed, validated responses end-to-end with Pydantic; 18+ tests with fully mocked APIs, run in CI
- Design details in ARCHITECTURE.md
- A POST request with a subject and optional hour budget is sent to
/plan - Claude generates a structured curriculum (up to 5 phases, 4 resources each) using only search queries — no hallucinated URLs
- Each resource's search query is resolved against YouTube or Serper APIs to retrieve real, validated links
- The completed plan with live URLs is returned
- Python 3.12 / FastAPI / Uvicorn
- Anthropic Claude (claude-haiku-4-5) — curriculum generation with prompt caching
- YouTube Data API v3 — video resource discovery
- Serper API — article and web resource discovery
- httpx — async HTTP for all external calls
- Pydantic — response validation
| Variable | Required | Purpose |
|---|---|---|
ANTHROPIC_API_KEY |
Yes | Claude API access |
YOUTUBE_API_KEY |
No | Video resource lookup |
SERPER_API_KEY |
No | Article/web resource lookup |
The service degrades gracefully when optional API keys are absent — resources will be returned without enriched URLs.
pip install -r requirements.txt
cp .env.example .env # fill in your API keys
uvicorn api:app --reloadThe API will be available at http://localhost:8000.
docker build -t curriculum-engine .
docker run -p 8000:8000 --env-file .env curriculum-engineGenerate a learning plan.
Request body:
{
"subject": "Linear algebra",
"hours": 20
}subject— required, non-empty stringhours— optional integer, 1–200 (default: 10)
Response: a LearningPlan object with phases and validated resources.
{
"subject": "Linear algebra",
"total_hours": 20,
"overview": "...",
"phases": [
{
"phase": 1,
"title": "Foundations",
"hours": 5,
"description": "...",
"milestone": "...",
"resources": [
{
"title": "Essence of Linear Algebra",
"resource_type": "video",
"estimated_minutes": 60,
"url": "https://youtube.com/...",
"retrieved_title": "Essence of linear algebra - 3Blue1Brown",
"channel": "3Blue1Brown"
}
]
}
]
}Status codes: 200 success · 400 validation error · 502 upstream API error · 500 internal error
{ "status": "ok" }curriculum-engine/
├── api.py # FastAPI app, endpoints, lifespan management
├── curriculum_engine/
│ ├── models.py # Pydantic models: LearningPlan, Phase, Resource
│ ├── planner.py # Claude-based curriculum generation
│ └── retrieval.py # Resource enrichment via YouTube & Serper
├── experiments/
│ └── vector_index_benchmark.py # HNSW/IVF/IVFPQ benchmark (see below) — standalone, not wired into the API
└── tests/
├── test_planner.py # Curriculum generation tests (8)
└── test_retrieval.py # Resource enrichment tests (10+)
experiments/vector_index_benchmark.py is a standalone FAISS benchmark — a preparation step for adding a real embedding-based retrieval layer to this project, which should improve resource relevance (matching a phase's actual learning goal semantically, not just keyword search), let curated resources be ranked and reused instead of re-querying live search APIs every time, and support reranking/hybrid (BM25 + dense) retrieval for better precision. The current retrieval path (above) is live web search only, so this experiment doesn't touch curriculum_engine/ yet.
Setup: python -m venv .venv (Python 3.12 — faiss-cpu wheels weren't yet available for newer Python versions at time of writing), then pip install -r requirements.txt, then python experiments/vector_index_benchmark.py.
What it does:
- Generates 5,000 synthetic, unit-normalized, 384-dim
float32vectors (standing in for real sentence embeddings). - Builds
IndexFlatL2(exact, ground truth),IndexHNSWFlat, andIndexIVFFlaton the same data; measures build time. - Measures search latency and recall@10 (against Flat's exact results) for each index at default settings.
- Sweeps HNSW's
efSearchand IVF'snprobeto chart the recall/latency tradeoff curve for each — chart. - Reruns at 20,000 vectors (
run_scale_test), addingIndexIVFPQ(product-quantized) to also measure memory footprint, then linearly extrapolates memory to 1M/10M/1B vectors.
Findings:
- HNSW gets strong recall (~80%) with no tuning and needs far less tuning than IVF to reach near-exact recall; IVF's recall is highly sensitive to
nprobeand only matches Flat's exact recall whennprobe=nlist(searching every cluster — at which point its speed advantage is gone too). - Memory, not latency, is the real constraint at large scale: uncompressed Flat/HNSW would need ~1.5TB of RAM at 1 billion vectors — infeasible on a single machine. IVFPQ's ~32x compression (48 compressed bytes/vector vs. 1,536 raw bytes for a 384-dim float32 vector) brings that down to ~48GB, which is why PQ-based (or PQ + HNSW hybrid) indexes are the default at real production scale rather than plain HNSW.
- IVFPQ trades some search latency for that memory win (decoding/approximating distances from quantized codes costs more per comparison than comparing raw floats).
pytest
pytest tests/test_planner.py -v
pytest tests/test_retrieval.py -vTests use unittest.mock and pytest-asyncio. No live API calls are made during the test suite.