Caspian Meal-Planning Workflow
Weekly/monthly Indian meal charts, generated per household from a recipe universe + diet profile, sent to the family's cook by text every morning with ingredients, quantities, and portion sizes - and updated from the cook's replies.
What Caspian actually does here
Caspian is a messaging transport
layer, not a planning or knowledge engine: one on_message handler, one
message.reply() / send_message() / initiate(), routed to whichever
channel (SMS, WhatsApp, Telegram, Slack, email, ...) the message came from.
It has no concept of recipes, diets, or meal plans. All of that - the
recipe universe, the household's diet profile, the weekly/monthly
generation, and the text that goes out - is this app; Caspian only carries
the final message to the cook's phone and carries their reply back.
Channel status: Caspian's docs list WhatsApp as "coming soon" (SMS,
Telegram, Slack, Discord, email are live and free; X and iMessage are paid).
The installed caspian-sdk package already ships connect_whatsapp() and a
hosted start_whatsapp_onboarding() flow that hits a real API endpoint and
is billed like the other paid channels - so it may already work for some
accounts even though the docs site hasn't caught up. This project defaults
to SMS via your own Twilio number (confirmed working) and treats
WhatsApp as a one-env-var swap (CASPIAN_CHANNEL=whatsapp) once you've
confirmed it's live on your account - see
scripts/start_whatsapp_onboarding.py.
The handler, formatter, and scheduler never change either way.
Architecture
Recipe universe (app/data/recipes_seed.json, 36 dishes)
Household diet profile (app/models.py: Household)
|
v
Meal-plan generator (app/meal_planner/generator.py, Claude)
- filters recipes by diet/allergy/dislikes
- avoids repeats from the last 14 days
- writes a MealPlan + MealPlanItem per day/slot
|
v
Scheduler (app/scheduler/jobs.py, APScheduler)
- Sun 20:00 -> generate next week's chart per household
- 1st, 08:00 -> aggregate the month into one ingredient shopping list
- daily, per household send_time -> format + send today's meals
|
v
Caspian CommClient (app/messaging/handler.py)
- send_message() / initiate() pushes today's meals to the cook
- on_message() reads the cook's reply, classifies it with Claude
(dislike / swap request / ingredient issue / confirmation / question),
logs it as Feedback, and acks it
Data model
- Household - one row per family: cook's phone, diet type, allergies,
dislikes, preferred cuisines, spice level, family size, kids count,
free-text notes (fed straight into the LLM prompt), and the
caspian_conversation_idonce the cook has texted in. - Recipe - one dish: region, cuisine, meal types, diet, spice level,
tags, and ingredients scaled to 1 serving (multiplied by
portion_servingsat send time). This is the "training data" from the brief: retrieval + context handed to Claude on every generation call, not a fine-tuned model. - MealPlan / MealPlanItem - a week (or four weeks, for "monthly") of
date x meal_slot -> [recipe_ids], e.g. lunch = dal + rice + roti. - Feedback - every inbound cook reply plus Claude's read on what it meant.
- RegionCuisineMap - state/UT -> locally popular
cuisine_styleweights (app/data/region_cuisine_seed.json), hand-authored. Not wired into the planner yet - lands with Phase 3's region-popularity weighting.
Recipe dataset
The recipe universe is seeded two ways:
- Fixture (default,
scripts/seed_db.py): 36 hand-curated recipes inapp/data/recipes_seed.json- fast, guaranteed-clean, used for local dev/tests. - Ingested (
scripts/seed_db.py --ingested): 4,262 recipes parsed from the 6000+ Indian Food Recipes Dataset (Kaggle). To regenerate:- Download the dataset from Kaggle (needs a Kaggle account) and drop the
CSV at
app/data/raw/indian_food_dataset.csv(gitignored - it carries copyrighted recipe instructions text, not something to redistribute even privately; a one-time manual step, not fetched automatically). python scripts/ingest_recipes.py- parses it intoapp/data/recipes_ingested.json(tracked in git: structured facts only- names, ingredients, tags - no instructions text) and prints a row-count/parse-failure report.
python scripts/seed_db.py --ingestedto load it into the DB.
- Download the dataset from Kaggle (needs a Kaggle account) and drop the
CSV at
Known limitations of the parsing (heuristic, not exact - see
app/data/ingest.py): ~2,080 non-Indian-cuisine rows and ~529
never-translated-to-English rows are dropped entirely; ingredient units
are approximated into a g/ml/pc/cup/tbsp/tsp vocabulary (e.g. "1 clove"
becomes 1 pc, losing the "clove" specificity); a line with no explicit
quantity is marked "to taste" rather than guessing a number; season_tags
default to all-season (the source data carries no season signal).
Setup
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env # fill in CASPIAN_API_KEY, GEMINI_API_KEY, Twilio creds
alembic upgrade head # creates/updates the schema for whatever DATABASE_URL points at
Get CASPIAN_API_KEY from dashboard.trycaspianai.com
and GEMINI_API_KEY from aistudio.google.com
(Google AI Studio - free tier, no card needed; set LLM_PROVIDER=anthropic
ANTHROPIC_API_KEYinstead if you'd rather use Claude - seeapp/meal_planner/llm_client.py). For SMS you need your own Twilio number + Account SID + Auth Token (console.twilio.com), and its inbound webhook pointed at Caspian per the SMS docs (connect_phoneprints the exact webhook URL to set).
Schema changes go through Alembic (alembic revision --autogenerate -m "...",
then alembic upgrade head) rather than editing the DB directly - this is
what lets a populated Postgres DB pick up model changes safely. init_db()
(plain Base.metadata.create_all) still exists for quick local scripts/tests
that just need a working SQLite file, not a real migration history.
Running it
python scripts/seed_db.py # load the recipe universe
python scripts/onboard_household.py --name "Malani" \
--cook-name "Radha" --cook-phone "+919812345678" \
--diet-type veg --family-size 4 --kids-count 1 \
--spice-level mild --allergies peanut \
--preferred-cuisines gujarati punjabi
python scripts/generate_week.py 1 # weekly chart for household 1
python scripts/send_today.py 1 # send today's meals now (manual test)
python -m app.main # run the real service:
# listens for cook replies
# + runs the weekly/monthly/daily cron jobs
Known simplifications (buildathon scope, not production)
_extract_phone()inapp/messaging/handler.pyguesses the shape ofmessage.senderfor the phone channel - confirm the exact keys against a real inbound Twilio message once it's wired up.- The daily cron groups households by
Household.send_time, but jobs are only registered atstart_scheduler()startup - restart the process after adding a household with a send_time that doesn't already have a job. - No auth/API layer for managing households - it's CLI scripts + direct DB access. Fine for one buildathon demo, not for multiple operators.