AI-powered lunch recommendations for Stanford students — right place, right meal, right now.
Team: Lynn Tong · Andrew Lim · Patrick Crouch
Course: MLOps — Spring 2026
Meal information across Stanford's campus is scattered. Students waste time bouncing between dining hall websites before deciding where and what to eat. Lunch Buddy fixes that.
Lunch Buddy is a web app that recommends the best dining hall and dish for you based on:
- Your location (closest cafeterias within a set radius)
- Your preference profile (tastes, dietary restrictions, allergies)
- Today's menus (scraped fresh each morning from Stanford's dining site)
An LLM agent surfaces a ranked top-3 recommendation, personalized to you and updated daily.
| Layer | Stack |
|---|---|
| Data ingestion | Web scraper → runs daily at 8am |
| Storage | PostgreSQL (menus, dishes, cafeterias) + vector store (RAG) |
| User profiles | Preferences, allergies, feedback history |
| Recommendation | LLM + RAG pipeline, geolocation-aware ranking |
| Deployment | Docker · GCP · Vertex AI |
| ML Pipeline | MLFlow (experiment tracking) · Metaflow (CD) |
| Monitoring | Evidently (data + prediction drift) |
| CI/CD | GitHub Actions · Pytest |
Requires the lunch_buddy conda env and models/.env configured.
conda activate lunch_buddy
cd models/gemini_flash_rag| Command | What it does |
|---|---|
python demo.py |
One round with today's live menu |
python demo.py --table backfill_menu |
10-day EMA learning loop (safe for experiments) |
python demo.py --mood "something light" |
Mood blending - mood leads as primary constraint |
python demo.py --user_id <supabase-uuid> |
Loads your stored profile, saves after each pick |
- Stanford dining hall menus — scraped daily, stored progressively as training data
- Geospatial data — user location + dining hall coordinates for proximity ranking
- User preference surveys — manually collected onboarding data
| Week | Dates | Milestone |
|---|---|---|
| 1 | Mar 27 – Apr 2 | Scraper MVP, PostgreSQL schema, MLFlow tracking, FastAPI wrapper |
| 2 | Apr 3 – Apr 9 | Dockerize, GCP Artifact Registry, system design, Vertex AI deploy |
| 3 | Apr 10 – Apr 17 | 9 Pytest test cases, CI via GitHub Actions |
| 4 | Apr 18 – Apr 25 | Full CI/CD pipeline, Metaflow, product pitch video, app demo video |
| 5 | Apr 26 – May 1 | Evidently monitoring, Ruff cleanup, final technical slides |
The milestone API loads a pyfunc model from the MLflow Model Registry at startup and exposes JSON endpoints for health checks and predictions.
All HTTP routes (/, /health, /predict) live in:
fastapi/main.py
- Python 3.11+ recommended (matches the Docker image).
- MLflow tracking server reachable from your machine (URL goes in
fastapi/.env— see below). - Model artifacts in GCS: loading the real model requires Google credentials that can read the artifact bucket, plus a GCP project ID for the client libraries.
From the repo root:
cd fastapi
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt-
Environment file (
fastapi/.env). The real.envis not in Git (it is listed in.gitignore), so you create it locally.From
fastapi/:touch .env
Then open
fastapi/.envand paste the following, or merge these lines with your own values:MLFLOW_TRACKING_URI=http://35.232.122.64:5000 MLFLOW_MODEL_URI=models:/dummy_model/1 USE_STUB_MODEL=0
Change
MLFLOW_MODEL_URIif your registry name, version, or stage differs (e.g.models:/dummy_model/Production). UseUSE_STUB_MODEL=1only for quick tests without MLflow/GCS.main.pyloads this file automatically viapython-dotenv. -
Authenticate for GCS (artifacts) and set the project the libraries should use:
gcloud auth application-default login gcloud auth application-default set-quota-project lunch-buddy-491800 export GOOGLE_CLOUD_PROJECT=lunch-buddy-491800Your Google account also needs Storage Object Viewer (or equivalent) on the MLflow artifact bucket.
-
Start the server:
uvicorn main:app --reload --host 0.0.0.0 --port 8000
-
Open interactive docs: http://127.0.0.1:8000/docs
From the repo root:
cd fastapi
touch .envAdd the following variables to fastapi/.env:
MLFLOW_TRACKING_URI=http://35.232.122.64:5000
MLFLOW_MODEL_URI=models:/dummy_model/1
USE_STUB_MODEL=0
SUPABASE_URL=https://<your-project-ref>.supabase.co
SUPABASE_JWT_AUDIENCE=authenticated
GOOGLE_CLOUD_PROJECT=lunch-buddy-491800
GOOGLE_APPLICATION_CREDENTIALS=/gcloud/adc.jsonThen start the API:
docker compose up --buildOpen:
- Docs: http://127.0.0.1:8000/docs
- Health: http://127.0.0.1:8000/health
Stop:
docker compose downRequest (JSON body) — fields match PredictRequest in fastapi/main.py:
| Field | Type | Required |
|---|---|---|
preferences |
array of strings | yes |
constraints |
array of strings | no (default []) |
user_id |
string | no |
Example:
{
"preferences": ["vegetarian", "quick"],
"constraints": ["no nuts"],
"user_id": "student-01"
}Response (200) — shape matches PredictResponse:
{
"suggestions": ["sandwich", "salad"],
"rationale": "dummy"
}(Exact strings depend on the registered model; the dummy model returns fixed suggestions.)
curl example:
curl -s -X POST "http://127.0.0.1:8000/predict" \
-H "Content-Type: application/json" \
-d '{"preferences": ["vegetarian"], "constraints": []}'From the repo root:
cd frontend
touch .envAdd the following variables to frontend/.env:
VITE_API_URL=http://127.0.0.1:8000
VITE_SUPABASE_URL=https://<your-project-ref>.supabase.co
VITE_SUPABASE_PUBLISHABLE_KEY=<your-key>Then start the frontend:
docker compose up --buildOpen: http://127.0.0.1:5173
Stop:
docker compose downBroader Lunch Buddy app setup (database, scraper, LLM) — TBD.
Overview: An automated daily ETL (Extract, Transform, Load) pipeline that pulls scraped university dining menus from Google Cloud Storage, generates semantic vector embeddings using Vertex AI, and syncs the data to a Supabase PostgreSQL database for hybrid search.
- Database Connection Pooling (Supabase): * Configured via a Transaction Pooler (Port 6543) rather than a direct connection. This protects the database from connection exhaustion during spiky API traffic.
- Enabled IPv4 Shared Pooling to bypass silent IPv6 networking failures common in ephemeral CI/CD environments.
- Batch API Processing: We hit the Vertex AI
text-embedding-004model in batches of 50 to optimize network latency and strictly adhere to rate limits. - Idempotent Database Uploads:
- Data is pushed via
psycopg2.extras.execute_valuesfor high-performance bulk inserts (one database trip instead of hundreds). - The database enforces a strict
UNIQUE (dish_name, dining_hall, meal_time, date_served)constraint. - By using an
ON CONFLICT DO UPDATEclause, the pipeline is fully idempotent. If the GitHub Action fails and restarts, or runs twice in one day, it will simply update existing records rather than duplicating data.
- Data is pushed via
- CI/CD Automation (GitHub Actions):
- Runs automatically on a daily cron schedule, with a manual
workflow_dispatchtrigger for testing. - Utilizes Google's official
auth@v2action for secure Service Account authentication (Vertex AI User role), keeping GCP keys safely in GitHub Secrets.
- Runs automatically on a daily cron schedule, with a manual
The Python ingestion script acts as the bridge between raw cloud storage and our vectorized database. Here is the exact lifecycle of the data during a single run:
The script authenticates with Google Cloud Storage and downloads the scraped JSON file for the current day. It reads this raw, highly nested JSON directly into memory.
Before touching any external APIs, the script processes the JSON into a flat structure.
- Search Text Generation: It combines the dish name and ingredients into a single, clean
search_textstring. This is the exact text the AI will interpret. - Application-Layer Deduplication: Scraped menus frequently contain duplicate entries (e.g., the same pizza served for both Lunch and Dinner). The script creates a unique fingerprint for every item:
(dish_name, dining_hall, meal_time). If it detects duplicates within the daily JSON, it safely overwrites them, ensuring only one clean record remains.
The script iterates through the flattened menu items in batches of 50. It sends the search_text for each batch to Google's Vertex AI (text-embedding-004 model). Vertex AI returns a 768-dimensional mathematical array (a vector) for each item, which the script attaches to the respective Python dictionary.
To maximize database insert speeds, the script uses the psycopg2 library's execute_values function. This requires converting our list of Python dictionaries into a strict list of tuples that map perfectly to our Postgres database columns.
The script opens a connection to the Supabase Transaction Pooler and executes a bulk INSERT query. It utilizes an ON CONFLICT clause tied to our database's unique constraint. If a menu item already exists for that day, meal, and hall, the script simply updates the vector embedding instead of crashing, guaranteeing data integrity.
For academic use only — Stanford MLOps Spring 2026.
