Skip to content

valtrof/curriculum-engine

Repository files navigation

Curriculum Engine

CI

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.

Why this matters

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

How it works

  1. A POST request with a subject and optional hour budget is sent to /plan
  2. Claude generates a structured curriculum (up to 5 phases, 4 resources each) using only search queries — no hallucinated URLs
  3. Each resource's search query is resolved against YouTube or Serper APIs to retrieve real, validated links
  4. The completed plan with live URLs is returned

Tech stack

  • 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

Prerequisites

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.

Quickstart

pip install -r requirements.txt
cp .env.example .env   # fill in your API keys
uvicorn api:app --reload

The API will be available at http://localhost:8000.

Docker

docker build -t curriculum-engine .
docker run -p 8000:8000 --env-file .env curriculum-engine

API

POST /plan

Generate a learning plan.

Request body:

{
  "subject": "Linear algebra",
  "hours": 20
}
  • subject — required, non-empty string
  • hours — 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

GET /health

{ "status": "ok" }

Project structure

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+)

Vector-index benchmark experiment

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:

  1. Generates 5,000 synthetic, unit-normalized, 384-dim float32 vectors (standing in for real sentence embeddings).
  2. Builds IndexFlatL2 (exact, ground truth), IndexHNSWFlat, and IndexIVFFlat on the same data; measures build time.
  3. Measures search latency and recall@10 (against Flat's exact results) for each index at default settings.
  4. Sweeps HNSW's efSearch and IVF's nprobe to chart the recall/latency tradeoff curve for each — chart.
  5. Reruns at 20,000 vectors (run_scale_test), adding IndexIVFPQ (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 nprobe and only matches Flat's exact recall when nprobe = 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).

Running tests

pytest
pytest tests/test_planner.py -v
pytest tests/test_retrieval.py -v

Tests use unittest.mock and pytest-asyncio. No live API calls are made during the test suite.

About

Hybrid RAG pipeline: Claude Haiku plans a curriculum via search queries; YouTube/Serper APIs retrieve and validate live links. Prompt caching cuts cost up to 90%. Standalone benchmarks: FAISS index tradeoffs (HNSW/IVF/IVFPQ, recall/latency/memory at scale), cross-encoder reranking (MRR 0.857-0.917), BM25+dense hybrid retrieval via RRF (MRR-0.935).

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages