Full-Stack · AI · Mobile

Menuto

Personalized restaurant dish recommendations powered by an LLM agent that learns your taste over time, using your favorites from other restaurants to inform what you'll love at new ones. Solo-built end to end: product, design, React Native frontend, FastAPI backend, and deployment.

Year
2026
Role
Solo — Product · Design · Full-Stack
Context
Personal Project · End-to-End Ownership
Tools
React Native · Expo 53 · FastAPI · Google Gemini 2.5 Flash · Supabase · PostgreSQL · Google Places API
I’m always indecisive at restaurants, and when I do decide, it’s always the wrong thing.
Why Not Just Ask an LLM?

You could send a model a photo of the menu and ask “what should I order?” You’d get a generic answer: no memory of what you’ve liked, no awareness of what reviewers say about this restaurant, no way to learn that you loved the cacio e pepe but hated the carbonara. Every conversation starts from zero. I wanted a system with state: one that tracks your favorites across restaurants, extracts taste signals from your ratings, and runs an 8-component scoring algorithm with Bayesian weight learning that adapts to how you decide.

~50
Dishes scored per request across 8 signal sources
3
Menu input modes: photo, URL, paste
4
LLM calls per recommendation (embeddings + agent reasoning)
6
LLM-analyzed dietary flags per dish (catches hidden ingredients)

The App

Find a restaurant and browse its full menu, then set your mood: how hungry you are, how adventurous, what you’re craving, and how you’re dining.

Restaurant search results
Find a restaurant
Restaurant detail with menu
Browse the full menu
Preference sliders
Hunger and taste sliders
Craving and dining style
Cravings and dining context

The agent reasons about your signals and returns personalized picks with explanations. Rate dishes after your meal; your favorites carry across restaurants for future visits.

Agent thinking
Browsing the kitchen
Recommended dishes
Your picks with reasons
Rate your dishes
Rate and save favorites
Saved restaurants
Your restaurant list

The Recommendation Engine

MENU PARSE3 input modesURL · Photo · TextDIETARY FILTERLLM-analyzed6 flags per dishSIGNAL ENRICH8 sourcesReviews · History · EmbeddingsAGENT SELECTLLM reasoningFull user narrativeFEEDBACK LOOPTaste extractionBayesian weight updateTASTE SIGNALS FEED BACK INTO SCORING
Agent-First Architecture

Rather than rigid scoring formulas, an LLM agent receives every signal about the user and reasons about what to recommend. An earlier version used 10 hand-tuned components (personal taste: 0.30, sentiment: 0.17, etc.), but the weights were identical for everyone and couldn't reason about context.

Research Foundations

Informed by Microsoft’s RecAI framework (Zhao et al., 2024): the “LLM-as-brain, models-as-tools” pattern where traditional signals generate candidates and the LLM handles final reasoning. The serendipity slot draws on SERAL (Chen et al., 2025) for filter-bubble mitigation, and the implicit negative feedback model follows Hu, Koren & Volinsky (2008) on collaborative filtering for implicit feedback.

The Pipeline
Data Gathering

8 signal sources per dish: parsed menu items, Google Places reviews (cached 14 days), review-based popularity, cross-user order counts, past ratings, behavioral signals (views/orders/favorites), LLM-extracted taste keywords, and embedding-based taste similarity (2 batch API calls).

Dietary Filtering

The only rigid step. LLM-generated dietary flags per dish, prompted to catch hidden ingredients (anchovy in Caesar dressing, fish sauce in Pad Thai). Falls back to a 30+ term keyword list for older menus.

Signal Enrichment

Each candidate gets readable flags: MATCHES YOUR TASTE, POPULAR (60%), WELL-REVIEWED, LOOKED AT BUT NEVER ORDERED. No numerical scoring, just facts the agent can reason about.

Agent Selection

The agent receives the full user narrative: taste profile, spice tolerance, hunger, cravings, the adventure-vs-safe slider, dining occasion, free-text mood, and history here. It reasons about meal composition, honors cravings, and writes a personal explanation per dish.

Feedback Loop

After ordering, the user rates dishes with quick-tap tags and optional notes. The LLM extracts taste signals from the text: “Loved the cream sauce” becomes liked: [“cream”, “rich sauce”], boosting similar dishes next time.

How It Learns
Thompson Sampling for Weight Learning

The 8-component algorithm doesn't use fixed weights. Each user has Bayesian priors (alpha/beta per component) that update on every rating, so the system learns whether they respond more to popularity, taste matching, or craving alignment, with no cold-start dataset. After ~10 ratings the weights diverge meaningfully from the uniform prior.

Embedding-Based Taste Compatibility

The user's taste profile and each dish description are embedded into the same vector space, then scored by cosine similarity. Someone who likes "creamy burrata" scores well on "stracciatella with olive oil" even with no keyword overlap. Two API calls total: one for the profile, one batch for all candidates.

Review Sentiment Decomposition

The LLM extracts per-dish sentiment from Google Places reviews. “The cacio e pepe was transcendent but the tiramisu was dry” decomposes into dish-level praise and criticism scores feeding the customer_praise component. Cached 14 days to stay within the Places API free tier.

System Design
Multi-modal menu ingestion

Three input paths (URL/HTML scraping, PDF via PyMuPDF, camera photo via LLM vision) normalize into the same ParsedDish schema. Content type is auto-detected from response headers with a byte-sniffing fallback.

Composite scoring, 8 components

Personal taste (embedding similarity), craving match, hunger fit, popularity/sentiment, dietary compliance, cuisine affinity, price fit, and friend boost. The system explains each recommendation by surfacing which components dominated.

Behavioral signals as separate tables

dish_views, dish_ratings, dish_orders, dish_favorites are separate normalized tables rather than a single interactions table. Enables efficient per-signal queries and signal-specific columns (hunger_level_when_ordered on orders, taste_signals JSONB on ratings).

Cold start via cross-user popularity

New users get recommendations weighted toward what others ordered and review sentiment. Free-text mood ("celebrating tonight") gives the agent context even without rating history.

Learning Et Al.Dishcovery