DINNERBOT
AI-Powered Meal Planning
Key Metrics
9
INTENT TYPES
5
RISEN PROMPTS
5
CONTEXT SOURCES
5
SERVICE MODULES
10 msgs
MEMORY WINDOW
3× selected
AUTO-FAVORITE
OVERVIEW
DinnerBot is a production-deployed serverless application that generates personalized weekly dinner menus, delivers them through Telegram, and produces aisle-grouped grocery lists — all through the persona of Gordon Ramsay. The project started from a simple question: what if you had a private executive chef who knew your family's dietary constraints, remembered what you've cooked recently, and could plan your dinners with a single message?
The system runs on Google Cloud Platform as a pair of Cloud Functions: one triggered weekly by Cloud Scheduler to generate three dinner options tailored to the family's profile (high protein, no wheat/mushrooms/olives/seed oils, hidden vegetables for a toddler), and a second serving as a Telegram webhook to handle all inbound interactions — button taps, text commands, conversational messages, and feedback. Google Gemini powers the language model, structured through the RISEN prompting framework with XML tags for consistent, high-quality outputs.
What elevates DinnerBot beyond a simple recipe generator is its conversational intelligence layer. The bot tracks conversation history with metadata enrichment, detects nine distinct intents from free-form text, maintains meal history with auto-favoriting, detects interaction staleness, and proactively solicits meal feedback — all while staying in character as a warm, practical Gordon Ramsay who remembers what the family loves.
ARCHITECTURE
DinnerBot follows a service-oriented architecture with five distinct modules, each responsible for a single domain:
LLMService — Google Gemini integration with five RISEN-framework prompts (recipe generation, grocery list, recipe detail, conversational, retry fallback). Handles intent detection via keyword/regex matching, JSON response parsing with validation, and a retry strategy with progressively simpler prompts. Falls back to hardcoded DEFAULT_RECIPES when all retries are exhausted.
DBService — Firestore CRUD layer managing three collections: meal_sessions (recipe options with selection state machine), meal_history (per-recipe tracking with selection counts, favorite status, and feedback), and conversation_history (message log with metadata, staleness timestamps, and pending feedback state). Documents are keyed by platform-prefixed user_id to support future multi-platform expansion.
TelegramService — Telegram Bot API wrapper handling outbound message formatting (HTML for recipes, plain text for grocery lists), inline keyboard construction with session-encoded callback data, callback query acknowledgment, and slash command registration with BotFather. Uses a persistent asyncio event loop to bridge Telegram's async API with Flask's synchronous request model.
Config — Two-tier configuration abstraction: environment variables for local development (.env via python-dotenv), Google Secret Manager for production. Properties are lazy-loaded with a sentinel pattern to distinguish "not initialized" from "initialization failed."
UserProfileService — Family profile management with Firestore-backed overrides merged on top of sensible defaults. Profiles include dietary restrictions, equipment availability, portion preferences, skill level, and special instructions (hidden vegetables, leftover-friendly meals, no-fuss weeknight cooking).
The entry points live in main.py — two Cloud Functions that serve as thin HTTP handlers, delegating all logic to the service modules. The cron trigger generates and sends recipes; the webhook handler validates, routes, and responds to all inbound Telegram updates.
CONVERSATIONAL AI
The conversational AI layer is what makes DinnerBot feel like an actual chef rather than a command-line tool. At its core is the RISEN (Role, Instructions, Steps, End goal, Narrowing) prompting framework — every prompt sent to Gemini is structured with XML tags (
Context Assembly — Each conversational response draws from five context sources injected into the prompt via dedicated XML sections:
Metadata Enrichment — Raw conversation logs are transformed into semantically rich context before being injected into prompts. A user message of "2" becomes "User selected Butter Chicken." A message of "favorites" becomes "User asked to see their favorites." This enrichment layer ensures Gemini understands the conversational arc rather than seeing a stream of cryptic numbers and keywords.
Memory Systems — The bot maintains two forms of memory: short-term conversation history (last 10 messages with metadata) and long-term meal history (every selection, with timestamps, frequency counts, and feedback). Meals selected three or more times are automatically flagged as favorites. This enables Gordon to naturally reference past meals — "You loved that Butter Chicken last week" or "We haven't done beef in a while."
Staleness Detection — When more than 24 hours pass between interactions, a
Feedback Loop — After sending a grocery list, the system sets a pending_feedback flag. On the next interaction, a
FEATURES
Intent Detection — The system routes free-form text to one of nine handlers through a keyword/regex matching pipeline: selection (meal pick), regenerate (new options), recipe_detail (expand a recipe), history (recent meals), generate_now (on-demand menu), favorites (go-to dishes), help (command list), feedback (meal review), and conversational (catch-all to Gemini). Slash commands (/start, /help, /menu, /favorites, /cancel) take priority over intent routing.
Meal Selection — Users pick meals via inline keyboard buttons (1, 2, 3) or text replies. The handler validates the selection against the pending session, marks it complete, saves to meal history (incrementing times_selected and checking the auto-favorite threshold), and triggers grocery list generation — all as a single atomic flow.
Aisle-Grouped Grocery Lists — After selection, Gemini generates a grocery list scaled for the family's portion size, grouped by store section (Protein, Produce, Dairy, Pantry). The prompt enforces practical quantities and respects dietary restrictions (no seed oils — butter, olive oil, avocado oil, or coconut oil only).
On-Demand Generation & Regeneration — Users can request new menus at any time ("what's for dinner", "plan dinner") or reject current options ("try again", "something else"). The system handles the full session lifecycle — expiring old sessions, generating fresh recipes, creating new Firestore documents, and sending updated inline keyboards.
Recipe Detail Expansion — Users can ask "tell me more about option 2" to get a full recipe breakdown: numbered cooking steps, the hidden-veggie strategy, protein content, and a Gordon tip — all generated on demand via a dedicated RISEN prompt.
Non-Text Handling — Photos, stickers, voice messages, and other non-text content are detected by content type and handled gracefully with an in-character response: "I can only read text messages, love. Type something or tap a button."
DEPLOYMENT
DinnerBot runs on Google Cloud Platform as a fully serverless stack with zero always-on infrastructure:
Cloud Functions (2nd Gen) — Two functions deployed to us-central1 with 512MB memory and 120-second timeout. The trigger function (cron_trigger_recipes) is HTTP-triggered with no public access, invoked by Cloud Scheduler with OIDC authentication. The webhook function (telegram_webhook) is publicly accessible — Telegram needs to reach it — but validated via a secret token header on every request.
Firestore — Three collections manage all persistent state. meal_sessions stores recipe options with a selection state machine (pending_selection → completed | expired | send_failed). meal_history tracks per-recipe data with selection counts, timestamps, favorite flags, and feedback values. conversation_history maintains the message log with metadata, last_interaction_at for staleness detection, and pending_feedback state.
Cloud Scheduler — A weekly cron job (Sundays at 10 AM Eastern) triggers the recipe generation function via authenticated HTTP POST, producing a new menu every week automatically.
Secret Manager — Stores GEMINI_API_KEY, TELEGRAM_BOT_TOKEN, and TELEGRAM_WEBHOOK_SECRET. Cloud Functions access secrets at runtime via IAM-bound service account permissions — no credentials are embedded in code or environment variables in production.
Cloud Build — A cloudbuild.yaml defines the CI/CD pipeline with parallel deployment of both Cloud Functions on push to main, with all secrets and environment variables configured to match the production deployment.
ENGINEERING DETAILS
Error Handling — LLM calls are wrapped in try/except with a two-tier retry strategy: the primary RISEN prompt first, then a simplified retry prompt that strips context to maximize JSON compliance. If both fail, hardcoded DEFAULT_RECIPES ensure the user always gets a response. Telegram webhook handlers always return HTTP 200 — even on internal errors — to prevent Telegram from retrying and causing duplicate processing. All user-facing error messages stay in character: "Bloody hell, something went wrong on my end. Give it another go in a few minutes."
Platform Abstraction — Every database operation uses a platform-prefixed user_id ("telegram_{chat_id}") rather than raw platform identifiers. This abstraction — carried through DBService, LLMService, UserProfileService, and all handler functions — means adding a new messaging platform (SMS, WhatsApp, Discord) requires only a new service module and webhook handler, with zero changes to the core logic. The Twilio SMS extension is architecturally ready but blocked pending phone number verification.
Async Bridge — Telegram's python-telegram-bot library is async-first, but Cloud Functions runs Flask (synchronous). TelegramService bridges this with a persistent asyncio event loop and a thread pool executor, avoiding the overhead of creating new loops per request while handling both async and synchronous calling contexts.
Session Lifecycle — Sessions follow a state machine (pending_selection → completed | expired | send_failed) with built-in guards: selection handlers validate session ownership, status, and option validity before processing. Expired or completed sessions are rejected gracefully with in-character messages guiding the user toward valid actions.
Conversation Pruning — Conversation history is capped at 20 stored messages (2× the 10-message context window) to prevent unbounded Firestore document growth while maintaining sufficient history for metadata-enriched context assembly.
Tech Stack
Details
Team
Steve Meadows
Timeline
2025