This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Essay Feedback Writer is a full-stack web app that delivers AI-powered feedback on student essays across multiple domains (IELTS Writing, Korean university entrance exams / 수능 논술). It uses FastAPI + PostgreSQL on the backend and Svelte + Vite on the frontend, containerized via Docker Compose with Traefik as a reverse proxy. A multi-agent scoring system evaluates essays per criterion in parallel, with configurable YAML-based rubrics.
# Start all services with hot-reload (docker-compose.override.yaml applied automatically)
docker-compose up -d
# Run backend tests
docker-compose exec backend bash ./scripts/test.sh
# Open a shell inside the backend container
docker-compose exec backend bash
# Apply DB migrations inside the container
alembic upgrade head
# Create a new migration after modifying models.py
alembic revision --autogenerate -m "Description"cd backend
uv sync # Install all dependencies (including dev)
uv run pytest # Run all tests
uv run pytest app/tests/api/routes/ # Run a specific directory
uv run pytest app/tests/api/routes/test_users.py # Run a single test file
uv run ruff check --fix # Lint and formatcd frontend
npm install
npm run dev # Dev server at http://localhost:5173
npm run build # Production buildpre-commit install # Set up hooks after cloning
pre-commit run --all # Run all hooks manuallySvelte SPA (frontend/)
│ REST API calls via frontend/src/lib/api.js
▼
Traefik (reverse proxy, HTTPS, Let's Encrypt)
│
▼
FastAPI (backend/app/) [fully async — AsyncSession + asyncio]
│
├── JWT auth (python-jose + bcrypt)
├── LangChain LLM clients (OpenAI, Anthropic) for essay feedback
├── VLM-based OCR for handwriting input
├── SMTP email for password recovery
└── Async SQLAlchemy ORM
│
▼
PostgreSQL (separate DB for tests)
| Layer | Directory | Purpose |
|---|---|---|
| Entry | main.py |
FastAPI app init, CORS, routers |
| Models | models.py |
All SQLAlchemy ORM models |
| Schemas | schemas/ |
Pydantic validation (input/output) |
| CRUD | crud/ |
Async database query logic per entity |
| Routes | api/routes/ |
HTTP endpoints (4 routers) |
| Services | services/ |
Business logic (e.g., feedback_service.py) |
| Agents | agents/ |
Multi-agent scoring: schema, swarm, aggregator, builder, loader |
| Agent Configs | agents/configs/ |
YAML rubric definitions per domain (IELTS, KSAT) |
| Core | core/ |
config, security (JWT/Fernet), async DB session |
API base path: /api/v1
user_router.py— auth: login, register, password resetielts_router.py— IELTS-specific: prompts, essays, feedbacks, rubrics; alsoPOST /essays/handwritingandGET /essays/{id}/imageksat_router.py— KSAT-specific: exam browsing (university/year/track filters), essay submission per question, feedback generation, criteria & examplesshared_router.py— cross-domain: bots, AI providers, API models, user API key managementutil_router.py— health check
Dependency injection (api/deps.py): SessionDep (async DB session) and CurrentUser (JWT-validated user) are injected into route handlers via Annotated + Depends.
App.svelte— root component withsvelte-spa-routerroute definitionsroutes/DomainSelector.svelte— landing page with domain cards (IELTS, KSAT, future: Math, Science)routes/IELTSFeedbackWriter.svelte— IELTS essay submission and feedback UIroutes/KSATFeedbackWriter.svelte— KSAT exam browsing, per-question essay/feedback UIroutes/— also Auth, Password, ResetPassword pagescomponents/HandwritingCanvas.svelte— stylus/touch canvas for handwriting inputlib/api.js— fetch wrapper that attaches JWT Bearer token from the storelib/store.js— Svelte writable stores persisted tolocalStorage(isLogin,accessToken)
- User — email/password, superuser flag, linked to essays, feedbacks, API keys
- Essay — student submission, linked to a Prompt; includes
input_type(text/handwriting),image_path(uploaded image), andocr_text(VLM-extracted text) - Feedback — AI-generated feedback stored as JSONB, linked to Essay + Bot
- Bot — AI model configuration (name, version, deprecated flag)
- AIProvider / APIModel — provider registry (e.g., OpenAI, Anthropic) and their models
- UserAPIKey — user's personal API keys, encrypted with Fernet
- Prompt — essay assignment with
domainfield (DomainType: ielts / ksat) - Rubric / RubricCriterion — scoring criteria for prompts
- ExampleEssay — sample essays per prompt
- Exam — KSAT exam metadata (university, year, track: humanities/sciences), stores unified passage
content - ExamQuestion — links exam to prompt, with question_number, max_points, char_min/max, passage_refs
Multi-agent scoring pipeline (used by all domains):
- User submits essay → backend stores
Essayin DB - Backend retrieves user's encrypted API key (or falls back to superuser's key)
feedback_service.generate_feedback()loads the YAML rubric for the prompt's domainbuilder.pygenerates oneAgentConfigper rubric criterionScoringSwarmruns all criterion agents in parallel (semaphore-limited, max 4 concurrent)- Each agent scores its criterion using structured LLM output (
CriterionResult) Aggregatorcombines scores via weighted average, LLM holistic synthesis, or both- Result stored as JSONB:
{ feedback_by_criteria: {...}, overall_score, overall_feedback } - Frontend renders feedback using
marked.js
Handwriting path (IELTS only):
- User draws on
HandwritingCanvas.svelte→ image uploaded toPOST /essays/handwriting - Backend stores
Essay(input_type=handwriting) + image file inUPLOAD_DIR - VLM OCR extracts text → saved to
essay.ocr_text→ standard scoring pipeline runs
Rubric configuration:
- YAML files in
agents/configs/define criteria, weights, scales, band descriptors, and prompt templates - Loaded at runtime with caching via
loader.py - Registry maps rubric names → YAML paths (e.g.,
"KSAT 2025 CAU Humanities Q1"→ksat/cau_2025_humanities_q1.yaml)
Copy .env.example to .env and fill in:
SECRET_KEY— generate withpython -c "import secrets; print(secrets.token_urlsafe(64))"FERNET_SECRET— generate withpython -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"POSTGRES_SERVER=dbwhen using Docker Compose,localhostfor local devVITE_SERVER_URL— backend URL seen by the browser (not the container name)ENVIRONMENT=local|staging|production—productiondisables Swagger docs at/docsUPLOAD_DIR— directory for uploaded essay images (default:/app/uploads); mounted as theessay-uploadsDocker volumeMAX_UPLOAD_SIZE_MB— maximum allowed upload size in MB (default:10)
- Framework: pytest + pytest-asyncio
- Location:
backend/app/tests/withconftest.pyfixtures - Test DB: separate database configured via
TEST_POSTGRES_*env vars; runs on RAM-backedtmpfsin Docker for speed - HTTP client:
httpx.AsyncClientwithASGITransport(replaces synchronousTestClient) - Coverage: HTML report generated in
htmlcov/
# Inside backend container or locally with uv
pytest app/tests/crud/test_user.py # single file
pytest app/tests/ -k "test_login" # filter by nameModels are defined in backend/app/models.py. After changes:
alembic revision --autogenerate -m "Add X column to Y table"
alembic upgrade headSeed data (AI providers, bots, rubrics) is loaded via backend/app/initial_data.py from CSV files in backend/app/data/. KSAT exam data is seeded from backend/app/data/ksat/seed_ksat_data.py.
GitHub Actions workflows in .github/workflows/:
test-backend.yml— runs pytest on every pushtest-docker-compose.yml— integration test of the full stackdeploy-production.yml— builds and pushes Docker images, deploys to server
Docker images are published to ghcr.io/limjhyeok/.
| File | Purpose |
|---|---|
docker-compose.yaml |
Base service definitions |
docker-compose.override.yaml |
Dev overrides (volume mounts, hot-reload) |
docker-compose.test.yaml |
Test environment |
docker-compose.traefik.yaml |
Production Traefik configuration |
| Service | URL |
|---|---|
| Frontend | http://localhost:5173 |
| Backend API | http://localhost:8000/api/v1 |
| Swagger docs | http://localhost:8000/docs |
| Adminer (DB UI) | http://localhost:8080 |
| Traefik dashboard | http://localhost:8090 |