A production-ready, containerized RAG (Retrieval-Augmented Generation) service for generating structured Social-Emotional Learning (SEL) lesson plans. Built with FastAPI, PostgreSQL (with optional pgvector), and OpenAI embeddings.
- Hybrid Retrieval: Combines BM25 (PostgreSQL tsvector) and vector similarity search with Reciprocal Rank Fusion (RRF)
- Structured Lesson Plans: Generates classroom-ready plans with 5 sections:
- Objective
- Activities (with time allocations)
- Reflection
- Assessment
- Resources
- CASEL Alignment: Automatically aligns content to CASEL's five core competencies
- Grade Band Support: K-2, 3-5, 6-8, 9-12, or "all"
- Safety Validation: Rule-based + LLM safety checks with fail-open/fail-closed modes
- Timeboxed Activities: Explicit time allocations for all activities
- Differentiation Notes: Includes support strategies when applicable
- Citations: Source citations with metadata and snippets
- Works Without pgvector: Falls back to text search only if pgvector is not available
- Python 3.11+
- PostgreSQL 16+ (or Docker for database)
- OpenAI API key
- (Optional) Docker and Docker Compose for containerized setup
git clone <your-repo-url>
cd SELCoach# Create virtual environment
python3 -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
# Install dependencies
pip install -e ".[dev]"# Copy sample env file
cp .env.sample .env
# Edit .env and add your OpenAI API key
nano .env # or use your preferred editorRequired variables in .env:
DB_DSN=postgresql+psycopg://seluser:selpass@localhost:5432/seldb
OPENAI_API_KEY=sk-your-actual-key-here
LLM_MODEL=gpt-4-turbo-preview
EMBED_MODEL=text-embedding-3-large
SAFETY_MODE=fail-open
LOG_LEVEL=INFO
ADMIN_ENABLED=falseOption A: Install PostgreSQL Locally
# Install PostgreSQL 16 via Homebrew (macOS)
brew install postgresql@16
# Start PostgreSQL service
brew services start postgresql@16
# Create database and user
createdb seldb
createuser seluser
psql -U $(whoami) -d seldb -c "ALTER USER seluser WITH PASSWORD 'selpass';"
psql -U $(whoami) -d seldb -c "GRANT ALL PRIVILEGES ON DATABASE seldb TO seluser;"
psql -U $(whoami) -d seldb -c "GRANT ALL ON SCHEMA public TO seluser;"Option B: Use Docker for Database Only
# Start only the database container
docker compose -f docker/docker-compose.yml up -d db# Using Python script (no psql required)
python scripts/apply_migrations.py
# Or manually with psql
export PATH="/opt/homebrew/opt/postgresql@16/bin:$PATH" # Add to your shell config
psql -U seluser -d seldb -f db/migrations/0001_extensions.sql
psql -U seluser -d seldb -f db/migrations/0002_schema.sql
psql -U seluser -d seldb -f db/migrations/0003_indexes.sqlpython -m rag.ingestion.ingest_cli --path data/teacheruvicorn api.main:app --reload --port 8001The API will be available at:
- Main: http://localhost:8001
- API Docs: http://localhost:8001/docs
- Health Check: http://localhost:8001/healthz
git clone <your-repo-url>
cd SELCoach
cp .env.sample .env
# Edit .env and add your OPENAI_API_KEYdocker compose -f docker/docker-compose.yml up -d --build# Using bootstrap script (requires local psql)
./scripts/bootstrap.sh
# Or using Docker exec
docker compose -f docker/docker-compose.yml exec db psql -U seluser -d seldb -f /app/db/migrations/0001_extensions.sql
docker compose -f docker/docker-compose.yml exec db psql -U seluser -d seldb -f /app/db/migrations/0002_schema.sql
docker compose -f docker/docker-compose.yml exec db psql -U seluser -d seldb -f /app/db/migrations/0003_indexes.sqldocker compose -f docker/docker-compose.yml exec api python -m rag.ingestion.ingest_cli --path data/teacher- Main: http://localhost:8000
- API Docs: http://localhost:8000/docs
curl http://localhost:8001/healthzcurl -X POST http://localhost:8001/api/chat \
-H "Content-Type: application/json" \
-d '{
"query": "Create a 30-minute lesson for 5th grade on coping with test anxiety.",
"context": {
"grade_band": "3-5",
"casel": ["Self-Management"],
"duration_minutes": 30
}
}'{
"role_routed": "teacher",
"safety": {
"label": "safe",
"notes": null
},
"answer": {
"format": "lesson_plan",
"content": "# Lesson Plan\n\n## Objective\n...",
"disclaimer": "This is not medical or diagnostic advice."
},
"citations": [
{
"source_id": "seed_src_2.md-chunk-0",
"source_title": "Managing Test Anxiety",
"snippet": "Test anxiety is a common challenge..."
}
],
"why_this_answer": "Retrieved 3 relevant chunks. Aligned to CASEL: Self-Management. Grade band: 3-5. Duration: 30 minutes."
}SELCoach/
├── api/ # FastAPI application
│ ├── main.py # App entry point
│ ├── routers/ # API endpoints
│ │ ├── chat.py # Chat endpoint
│ │ └── admin.py # Admin endpoints
│ ├── schemas.py # Pydantic schemas
│ ├── deps.py # Dependency injection
│ └── config.py # Configuration
├── rag/ # RAG pipeline
│ ├── ingestion/ # Document ingestion
│ │ ├── loaders.py
│ │ ├── clean.py
│ │ ├── chunk.py
│ │ ├── labelers.py
│ │ ├── embed.py
│ │ └── ingest_cli.py
│ ├── retrieval/ # Hybrid retrieval
│ │ ├── filters.py
│ │ ├── hybrid.py
│ │ ├── rrf.py
│ │ └── store_pg.py
│ ├── generation/ # Lesson plan generation
│ │ ├── prompts_teacher.py
│ │ ├── planner.py
│ │ └── generator.py
│ └── safety/ # Safety validation
│ ├── rules.py
│ └── validator.py
├── db/
│ └── migrations/ # Database migrations
├── tests/ # Test suite
├── docker/ # Docker configuration
├── scripts/ # Utility scripts
├── data/
│ └── teacher/ # Seed data
└── pyproject.toml # Dependencies
Environment variables (see .env.sample):
| Variable | Description | Default |
|---|---|---|
DB_DSN |
PostgreSQL connection string | postgresql+psycopg://seluser:selpass@localhost:5432/seldb |
OPENAI_API_KEY |
OpenAI API key (required) | - |
LLM_MODEL |
LLM model name | gpt-4-turbo-preview |
EMBED_MODEL |
Embedding model | text-embedding-3-large |
SAFETY_MODE |
Safety mode: fail-open or fail-closed |
fail-open |
LOG_LEVEL |
Logging level | INFO |
ADMIN_ENABLED |
Enable admin endpoints | false |
Ingest documents from a directory:
python -m rag.ingestion.ingest_cli \
--path data/teacher \
--default-grade "3-5" \
--default-casel "Self-Awareness,Self-Management"Options:
--path: Path to directory containing markdown/text files--default-grade: Default grade band if inference fails--default-casel: Default CASEL competencies (comma-separated)
Run the test suite:
# Install dev dependencies
pip install -e ".[dev]"
# Run all tests
pytest
# Run with coverage
pytest --cov=rag --cov=api
# Run specific test file
pytest tests/test_ingestion.pyIf you see errors about the vector extension:
Local PostgreSQL:
# Install pgvector
brew install pgvector
# Copy extension files (requires sudo)
sudo mkdir -p /opt/homebrew/opt/postgresql@16/share/postgresql@16/extension
sudo cp /opt/homebrew/Cellar/pgvector/*/share/postgresql@17/extension/* /opt/homebrew/opt/postgresql@16/share/postgresql@16/extension/
sudo cp /opt/homebrew/Cellar/pgvector/*/lib/postgresql@17/* /opt/homebrew/opt/postgresql@16/lib/postgresql@16/
# Create extension
psql -U seluser -d seldb -c "CREATE EXTENSION IF NOT EXISTS vector;"Note: The application works without pgvector - it will use text search only.
# Check PostgreSQL is running
brew services list | grep postgresql
# Test connection
psql -U seluser -d seldb -c "SELECT version();"
# Check database exists
psql -U $(whoami) -lqt | grep seldbThis usually means:
- Documents haven't been ingested - run ingestion CLI
- Query doesn't match content - try broader queries like "emotions" or "test anxiety"
- Filters too strict - the search will automatically relax filters if no results
If port 8000 is in use:
# Use a different port
uvicorn api.main:app --reload --port 8001- Verify your API key is set correctly in
.env - Check you have sufficient API credits
- Verify the model names are correct
- Query Processing: Normalize filters (grade_band, CASEL)
- Hybrid Search:
- Lexical: PostgreSQL
tsvectorwithplainto_tsquery - Vector: pgvector cosine similarity (if available)
- Lexical: PostgreSQL
- Fusion: Reciprocal Rank Fusion (RRF) with k=60
- Top-K: Return top 10 results with snippets
- Planner: LLM creates structured outline (JSON)
- Generator: LLM expands outline into full lesson plan
- Safety: Rule-based + LLM validation
- Post-processing: Add citations, ensure disclaimer
- Rules: Pattern matching for reject/needs_review keywords
- LLM Check: Classification prompt for final label
- Fail Modes:
fail-open(allow on error) orfail-closed(reject on error)
CREATE TABLE documents (
id UUID PRIMARY KEY,
source_id TEXT,
source_title TEXT,
publisher TEXT,
published_at TIMESTAMPTZ,
grade_band TEXT, -- 'K-2'|'3-5'|'6-8'|'9-12'|'all'
casel_competencies TEXT[], -- CASEL 5 subset
tags TEXT[],
content TEXT NOT NULL,
content_tsv tsvector, -- For text search
embedding vector(1536), -- Optional: only if pgvector available
doc_hash TEXT UNIQUE,
created_at TIMESTAMPTZ,
updated_at TIMESTAMPTZ
);- Never commit
.envfile (already in.gitignore) - Admin endpoints are disabled by default (
ADMIN_ENABLED=false) - API keys should be kept secure
- In production, use proper authentication/authorization
See LICENSE file.
- Fork the repository
- Create a feature branch
- Make changes with tests
- Submit a pull request
For issues and questions, please open a GitHub issue.
The system aligns to CASEL's five core competencies:
- Self-Awareness: Understanding one's emotions, thoughts, and values
- Self-Management: Managing emotions, behaviors, and goals
- Social Awareness: Understanding others' perspectives and showing empathy
- Relationship Skills: Building and maintaining healthy relationships
- Responsible Decision-Making: Making ethical, constructive choices
- Support for PDF document ingestion
- Multi-language support
- User authentication and session management
- Lesson plan templates and customization
- Analytics and usage tracking
- Integration with learning management systems
Built with ❤️ for educators