Skip to content
Learn Agentic AI
Learn Agentic AI13 min read3 views

Building a Menu Recommendation Agent: Personalized Suggestions Based on Preferences

Learn how to build an AI agent that provides personalized menu recommendations based on guest preferences, dietary restrictions, allergen awareness, and intelligent food and drink pairings.

Why Personalized Menu Recommendations Matter

Restaurant guests face decision fatigue when presented with extensive menus. Studies show that diners who receive personalized recommendations order 15 to 20 percent more and report higher satisfaction. An AI menu recommendation agent learns guest preferences through conversation, filters for dietary restrictions and allergens, and suggests items with intelligent pairing logic — acting as a knowledgeable server for every guest.

The key challenge is balancing personalization with discovery. A great recommendation agent does not just echo past preferences; it introduces guests to new dishes they are likely to enjoy based on flavor profile similarity.

The recommendation engine needs rich item metadata beyond name and price — it needs flavor profiles, allergens, and pairing relationships.

flowchart TD
    START["Building a Menu Recommendation Agent: Personalize…"] --> A
    A["Why Personalized Menu Recommendations M…"]
    A --> B
    B["Menu Knowledge Model"]
    B --> C
    C["Recommendation Engine"]
    C --> D
    D["Building the Recommendation Agent Tools"]
    D --> E
    E["FAQ"]
    E --> DONE["Key Takeaways"]
    style START fill:#4f46e5,stroke:#4338ca,color:#fff
    style DONE fill:#059669,stroke:#047857,color:#fff
from dataclasses import dataclass, field
from enum import Enum

class Allergen(Enum):
    GLUTEN = "gluten"
    DAIRY = "dairy"
    NUTS = "nuts"
    SHELLFISH = "shellfish"
    EGGS = "eggs"
    SOY = "soy"
    FISH = "fish"
    SESAME = "sesame"

class FlavorProfile(Enum):
    SAVORY = "savory"
    SPICY = "spicy"
    SWEET = "sweet"
    UMAMI = "umami"
    ACIDIC = "acidic"
    SMOKY = "smoky"
    HERBACEOUS = "herbaceous"
    RICH = "rich"

@dataclass
class DetailedMenuItem:
    item_id: str
    name: str
    price: float
    course: str
    description: str
    allergens: list[Allergen] = field(default_factory=list)
    dietary_flags: list[str] = field(default_factory=list)  # vegan, vegetarian, gf
    flavor_profiles: list[FlavorProfile] = field(default_factory=list)
    pairs_with: list[str] = field(default_factory=list)  # item_ids
    spice_level: int = 0  # 0-5
    popularity_score: float = 0.0  # 0-1 based on order frequency
    seasonal: bool = False

@dataclass
class GuestPreferences:
    allergens: list[Allergen] = field(default_factory=list)
    dietary_restrictions: list[str] = field(default_factory=list)
    flavor_preferences: list[FlavorProfile] = field(default_factory=list)
    spice_tolerance: int = 3  # 0-5
    disliked_ingredients: list[str] = field(default_factory=list)
    past_orders: list[str] = field(default_factory=list)
    budget_per_person: float = 0.0  # 0 means no budget constraint

Recommendation Engine

The core recommendation logic scores each menu item against the guest's preference profile.

See AI Voice Agents Handle Real Calls

Book a free demo or calculate how much you can save with AI voice automation.

def score_item(item: DetailedMenuItem, prefs: GuestPreferences) -> float:
    # Hard filters: allergens and dietary restrictions
    for allergen in prefs.allergens:
        if allergen in item.allergens:
            return -1.0  # Completely excluded

    if prefs.dietary_restrictions:
        if not any(flag in item.dietary_flags for flag in prefs.dietary_restrictions):
            if prefs.dietary_restrictions != []:
                return -1.0

    if prefs.budget_per_person > 0 and item.price > prefs.budget_per_person:
        return -0.5

    score = 0.0

    # Flavor profile match
    flavor_overlap = set(prefs.flavor_preferences) & set(item.flavor_profiles)
    score += len(flavor_overlap) * 2.0

    # Spice tolerance alignment
    spice_diff = abs(item.spice_level - prefs.spice_tolerance)
    score -= spice_diff * 0.5

    # Popularity bonus
    score += item.popularity_score * 1.5

    # Novelty bonus: items not previously ordered
    if item.item_id not in prefs.past_orders:
        score += 1.0

    # Seasonal bonus
    if item.seasonal:
        score += 0.5

    return score

def get_recommendations(
    menu: list[DetailedMenuItem],
    prefs: GuestPreferences,
    course: str = "",
    limit: int = 3,
) -> list[tuple[DetailedMenuItem, float]]:
    candidates = menu if not course else [m for m in menu if m.course == course]
    scored = [(item, score_item(item, prefs)) for item in candidates]
    # Filter out excluded items
    scored = [(item, s) for item, s in scored if s >= 0]
    scored.sort(key=lambda x: x[1], reverse=True)
    return scored[:limit]

Building the Recommendation Agent Tools

from agents import Agent, function_tool

full_menu: list[DetailedMenuItem] = [
    DetailedMenuItem("A1", "Crispy Calamari", 14.0, "appetizer",
                     "Lightly battered with marinara and lemon aioli",
                     [Allergen.GLUTEN, Allergen.SHELLFISH], [],
                     [FlavorProfile.SAVORY, FlavorProfile.ACIDIC],
                     ["W1"], 2, 0.85),
    DetailedMenuItem("A2", "Burrata & Heirloom Tomato", 16.0, "appetizer",
                     "Fresh burrata, seasonal tomatoes, basil oil",
                     [Allergen.DAIRY], ["vegetarian"],
                     [FlavorProfile.HERBACEOUS, FlavorProfile.RICH],
                     ["W2"], 0, 0.78, True),
    DetailedMenuItem("M1", "Grilled Salmon", 32.0, "main",
                     "Atlantic salmon with lemon herb butter and asparagus",
                     [Allergen.FISH, Allergen.DAIRY], [],
                     [FlavorProfile.SAVORY, FlavorProfile.HERBACEOUS],
                     ["W2", "A2"], 0, 0.92),
    DetailedMenuItem("M2", "Mushroom Risotto", 24.0, "main",
                     "Arborio rice with wild mushrooms and truffle oil",
                     [Allergen.DAIRY], ["vegetarian"],
                     [FlavorProfile.UMAMI, FlavorProfile.RICH],
                     ["W2", "A2"], 0, 0.88),
    DetailedMenuItem("M3", "Spicy Thai Basil Chicken", 22.0, "main",
                     "Wok-fired chicken with Thai basil and chili",
                     [Allergen.SOY, Allergen.EGGS], [],
                     [FlavorProfile.SPICY, FlavorProfile.HERBACEOUS],
                     ["W3"], 4, 0.75),
]

guest_prefs = GuestPreferences()

@function_tool
def set_guest_preferences(
    allergens: list[str] = [],
    dietary: list[str] = [],
    flavor_likes: list[str] = [],
    spice_tolerance: int = 3,
    dislikes: list[str] = [],
    budget: float = 0.0,
) -> str:
    guest_prefs.allergens = [Allergen(a) for a in allergens if a in [e.value for e in Allergen]]
    guest_prefs.dietary_restrictions = dietary
    guest_prefs.flavor_preferences = [
        FlavorProfile(f) for f in flavor_likes if f in [e.value for e in FlavorProfile]
    ]
    guest_prefs.spice_tolerance = spice_tolerance
    guest_prefs.disliked_ingredients = dislikes
    guest_prefs.budget_per_person = budget
    return (
        f"Preferences set: allergens={allergens}, dietary={dietary}, "
        f"flavors={flavor_likes}, spice={spice_tolerance}/5, budget=${budget:.2f}"
    )

@function_tool
def recommend_dishes(course: str = "", count: int = 3) -> str:
    recs = get_recommendations(full_menu, guest_prefs, course, count)
    if not recs:
        return f"No suitable {course or 'menu'} items match your preferences."
    lines = []
    for item, score in recs:
        flags = ", ".join(item.dietary_flags) if item.dietary_flags else ""
        flag_str = f" [{flags}]" if flags else ""
        seasonal_str = " (Seasonal)" if item.seasonal else ""
        lines.append(
            f"- **{item.name}** (${item.price:.2f}){flag_str}{seasonal_str}\n"
            f"  {item.description}"
        )
    return "\n".join(lines)

@function_tool
def get_pairing_suggestions(item_id: str) -> str:
    item = next((m for m in full_menu if m.item_id == item_id), None)
    if not item:
        return f"Item {item_id} not found."
    pairings = [m for m in full_menu if m.item_id in item.pairs_with]
    if not pairings:
        return f"No specific pairing suggestions for {item.name}."
    lines = [f"Great pairings with {item.name}:"]
    for p in pairings:
        lines.append(f"- {p.name} (${p.price:.2f}): {p.description}")
    return "\n".join(lines)

@function_tool
def check_allergens(item_id: str) -> str:
    item = next((m for m in full_menu if m.item_id == item_id), None)
    if not item:
        return f"Item {item_id} not found."
    if not item.allergens:
        return f"{item.name} contains no major allergens."
    allergen_names = ", ".join(a.value for a in item.allergens)
    return f"{item.name} contains: {allergen_names}. Please inform kitchen of any allergies."

recommendation_agent = Agent(
    name="Menu Recommendation Agent",
    instructions="""You are a knowledgeable restaurant recommendation agent.
    Start by learning the guest's dietary needs, allergies, and flavor
    preferences. Then suggest dishes course by course. Always check
    allergens before confirming recommendations. Suggest pairings to
    enhance the dining experience.""",
    tools=[set_guest_preferences, recommend_dishes, get_pairing_suggestions, check_allergens],
)

FAQ

How does the agent handle guests who say "surprise me" with no stated preferences?

When a guest has no explicit preferences, the agent defaults to the popularity-based ranking and highlights seasonal specials first. It also asks one or two quick qualifying questions — "Any allergies I should know about?" and "Do you enjoy spicy food?" — to establish safety constraints before making its top picks. The novelty bonus in the scoring ensures it suggests a diverse mix rather than the same three popular dishes.

Can the recommendation engine learn from a guest's dining history over time?

Yes. The past_orders field in GuestPreferences builds over time. The scoring function uses this history in two ways: it applies a novelty bonus for items the guest has never tried, and it can infer flavor preferences from historically ordered items. If a guest consistently orders umami-heavy and rich dishes, the engine upweights those flavor profiles even if the guest never explicitly stated a preference.

How does the agent handle allergen cross-contamination concerns?

The allergen check provides the listed allergens for each dish, but the agent also adds a standard advisory that the kitchen should be informed of all allergies since shared cooking surfaces may cause cross-contamination. For severe allergies (anaphylaxis risk), the agent recommends speaking with the kitchen manager directly and flags the order for special handling.


#MenuRecommendation #PersonalizationAI #AllergenDetection #AgenticAI #Python #LearnAI #AIEngineering

Share
C

Written by

CallSphere Team

Expert insights on AI voice agents and customer communication automation.

Try CallSphere AI Voice Agents

See how AI voice agents work for your industry. Live demo available -- no signup required.

Related Articles You May Like

AI Interview Prep

7 AI Coding Interview Questions From Anthropic, Meta & OpenAI (2026 Edition)

Real AI coding interview questions from Anthropic, Meta, and OpenAI in 2026. Includes implementing attention from scratch, Anthropic's progressive coding screens, Meta's AI-assisted round, and vector search — with solution approaches.

Learn Agentic AI

Fine-Tuning LLMs for Agentic Tasks: When and How to Customize Foundation Models

When fine-tuning beats prompting for AI agents: dataset creation from agent traces, SFT and DPO training approaches, evaluation methodology, and cost-benefit analysis for agentic fine-tuning.

AI Interview Prep

7 Agentic AI & Multi-Agent System Interview Questions for 2026

Real agentic AI and multi-agent system interview questions from Anthropic, OpenAI, and Microsoft in 2026. Covers agent design patterns, memory systems, safety, orchestration frameworks, tool calling, and evaluation.

Learn Agentic AI

Building a Multi-Agent Data Pipeline: Ingestion, Transformation, and Analysis Agents

Build a three-agent data pipeline with ingestion, transformation, and analysis agents that process data from APIs, CSVs, and databases using Python.

Learn Agentic AI

Adaptive Thinking in Claude 4.6: How AI Agents Decide When and How Much to Reason

Technical exploration of adaptive thinking in Claude 4.6 — how the model dynamically adjusts reasoning depth, its impact on agent architectures, and practical implementation patterns.

Learn Agentic AI

How NVIDIA Vera CPU Solves the Agentic AI Bottleneck: Architecture Deep Dive

Technical analysis of NVIDIA's Vera CPU designed for agentic AI workloads — why the CPU is the bottleneck, how Vera's architecture addresses it, and what it means for agent performance.