import math from dataclasses import dataclass from datetime import date, datetime, timedelta from typing import Dict, List, Tuple, Optional import gradio as gr from dateutil.parser import parse as parse_dt # Optional LLM (nice-to-have) LLM_AVAILABLE = True try: from transformers import pipeline except Exception: LLM_AVAILABLE = False # -------------------------- # Data + Helpers # -------------------------- VIBES = [ "Relax & Chill", "Adventure", "Food & Cafes", "Culture & Museums", "Nature & Scenic", "Nightlife", "Budget Backpacking", "Luxury", ] TRAVEL_STYLE_TEMPLATES = { "Relax & Chill": [ "Late breakfast at a local cafe", "Leisurely walk + scenic viewpoint", "Spa / beach / slow afternoon", "Sunset spot", "Early dinner + light dessert" ], "Adventure": [ "Early start + main adventure activity", "Local lunch (high-energy)", "Second activity (short hike / water sport)", "Rest + recovery", "Dinner near your stay" ], "Food & Cafes": [ "Famous breakfast spot", "Street food crawl / market", "Specialty cafe break", "Food experience (cooking class / tasting)", "Dinner at a highly rated local place" ], "Culture & Museums": [ "Main museum/heritage site", "Neighborhood walk (old town)", "Local lunch near cultural area", "Second museum/gallery/temple", "Evening performance / calm dinner" ], "Nature & Scenic": [ "Sunrise viewpoint / park", "Nature trail / lake / botanical garden", "Local lunch", "Scenic drive / cable car / waterfall", "Sunset + dinner" ], "Nightlife": [ "Late start brunch", "Light sightseeing", "Rest", "Evening bar/club district", "Late-night food" ], "Budget Backpacking": [ "Free walking tour / public park", "Local cheap eats", "Museum on discount day / free attractions", "Sunset viewpoint", "Affordable dinner" ], "Luxury": [ "Brunch + premium experience", "Private tour / signature attraction", "Fine dining lunch", "High-end shopping / spa", "Chef’s tasting / rooftop dinner" ], } PACKING_BASE = { "Essentials": [ "ID/passport, tickets, hotel confirmation", "Phone + charger + power bank", "Cards + some cash", "Reusable water bottle", "Basic medicines (painkiller, ORS, band-aids)", ], "Clothing": [ "Comfortable outfits (mix & match)", "Light jacket / hoodie", "Sleepwear, innerwear", "Comfortable walking shoes", ], "Extras": [ "Sunglasses / cap", "Small daypack", "Toiletries kit", ] } FOOD_SUGGESTIONS = { "India": [ "Try a local thali / meals-on-a-plate place", "Street food (only busy stalls + bottled water)", "Regional breakfast specialty (poha/idli/paratha etc.)", "One ‘famous’ dessert spot (jalebi, kulfi, etc.)" ], "Generic": [ "Find a local market/food hall for cheap + variety", "Try one signature dish of the region", "Do one fancy meal, keep others simple", "Carry snacks to avoid impulse spending" ] } def parse_start_date(s: str) -> date: if not s or not s.strip(): return date.today() try: return parse_dt(s).date() except Exception: return date.today() def money(x: float) -> str: return f"{x:,.0f}" if abs(x) >= 100 else f"{x:,.2f}" @dataclass class TripInputs: destination: str days: int start_date: date budget: float vibe: str people: int pace: str # Slow / Balanced / Packed food_pref: str # Veg/Non-veg/Any country_hint: str # India/Other # -------------------------- # Optional LLM (for nicer phrasing) # -------------------------- from functools import lru_cache @lru_cache(maxsize=1) def load_llm(): return pipeline("text2text-generation", model="google/flan-t5-base", device=-1) def maybe_polish_with_llm(text: str, use_llm: bool) -> str: if not use_llm or not LLM_AVAILABLE: return text try: gen = load_llm() prompt = ( "Rewrite this travel plan more clearly with headings and bullet points. " "Keep all details and don't add new places. Text:\n\n" + text ) out = gen(prompt, max_new_tokens=350, do_sample=False)[0]["generated_text"] return out.strip() if out else text except Exception: return text # -------------------------- # Core Planner (deterministic + fast) # -------------------------- def budget_breakdown(total: float, vibe: str, pace: str) -> Dict[str, float]: """ Simple envelope budget. Tune these as you like. """ # Defaults stay = 0.35 food = 0.25 local = 0.15 activities = 0.15 buffer = 0.10 if vibe in ["Luxury"]: stay, food, activities = 0.45, 0.22, 0.18 local, buffer = 0.10, 0.05 elif vibe in ["Budget Backpacking"]: stay, food, local = 0.25, 0.25, 0.20 activities, buffer = 0.15, 0.15 elif vibe in ["Nightlife"]: food, activities = 0.22, 0.22 stay, local, buffer = 0.33, 0.13, 0.10 if pace == "Packed": activities += 0.05 buffer -= 0.05 elif pace == "Slow": local += 0.03 buffer += 0.02 activities -= 0.05 # normalize parts = {"Stay": stay, "Food": food, "Local Transport": local, "Activities": activities, "Buffer": buffer} s = sum(parts.values()) for k in parts: parts[k] = (parts[k] / s) * total return parts def day_plan_blocks(vibe: str, pace: str) -> List[str]: blocks = TRAVEL_STYLE_TEMPLATES.get(vibe, TRAVEL_STYLE_TEMPLATES["Relax & Chill"]).copy() if pace == "Packed": # add one extra micro-block blocks.insert(2, "Quick extra stop (photo spot / short local experience)") elif pace == "Slow": # remove one block and add rest if len(blocks) > 4: blocks.pop(2) blocks.insert(3, "Rest / free time") return blocks def packing_list(days: int, vibe: str, food_pref: str) -> str: clothing_count = max(2, min(days, 7)) # suggest rotating laundry lines = [] lines.append("### Packing list\n") for section, items in PACKING_BASE.items(): lines.append(f"**{section}**") for it in items: lines.append(f"- {it}") lines.append("") lines.append("**Trip-specific**") lines.append(f"- Tops: ~{clothing_count}, Bottoms: ~{max(2, clothing_count//2)} (repeatable)") lines.append("- Socks/innerwear: enough for 4–5 days + quick wash plan") if vibe in ["Adventure", "Nature & Scenic"]: lines.append("- Light rain jacket / poncho") lines.append("- Sunscreen + insect repellent") lines.append("- Small first-aid kit upgrade (crepe bandage, antiseptic)") if vibe in ["Nightlife"]: lines.append("- One nice outfit + comfortable going-out shoes") if food_pref.lower().startswith("veg"): lines.append("- Dry snacks you trust (protein bars / nuts) for backup") lines.append("") return "\n".join(lines) def food_suggestions(country_hint: str, food_pref: str, vibe: str) -> str: tips = FOOD_SUGGESTIONS["India"] if country_hint == "India" else FOOD_SUGGESTIONS["Generic"] lines = ["### Food suggestions\n"] if food_pref.lower().startswith("veg"): lines.append("- Filter: pure-veg / veg-friendly places; check reviews for cross-contamination if needed.") elif food_pref.lower().startswith("non"): lines.append("- Try regional non-veg specialties, but keep one simple meal/day to manage budget.") else: lines.append("- Mix local specialties + simple meals to keep spending stable.") if vibe == "Food & Cafes": lines.append("- Do one food crawl day: 4–6 small items instead of 1 big expensive meal.") lines.append("- Save cafe-hopping for afternoons; mornings are better for popular breakfast spots.") for t in tips: lines.append(f"- {t}") lines.append("") return "\n".join(lines) def build_itinerary(inp: TripInputs) -> str: per_day = inp.budget / max(inp.days, 1) env = budget_breakdown(inp.budget, inp.vibe, inp.pace) lines = [] lines.append(f"# ✈️ Travel Itinerary Bot\n") lines.append(f"**Destination:** {inp.destination}") lines.append(f"**Days:** {inp.days} | **Start:** {inp.start_date.isoformat()} | **People:** {inp.people}") lines.append(f"**Vibe:** {inp.vibe} | **Pace:** {inp.pace} | **Food preference:** {inp.food_pref}") lines.append(f"**Total budget:** {money(inp.budget)} | **Budget/day (avg):** {money(per_day)}") lines.append("") lines.append("## Budget breakdown (envelopes)") for k, v in env.items(): lines.append(f"- **{k}**: {money(v)}") lines.append("") blocks = day_plan_blocks(inp.vibe, inp.pace) lines.append("## Day-by-day plan") for d in range(inp.days): day_date = inp.start_date + timedelta(days=d) lines.append(f"### Day {d+1} — {day_date.strftime('%a, %d %b %Y')}") lines.append(f"- Morning: {blocks[0]}") lines.append(f"- Late morning: {blocks[1]}") lines.append(f"- Afternoon: {blocks[2]}") lines.append(f"- Evening: {blocks[3]}") lines.append(f"- Night: {blocks[4]}") lines.append(f"- **Spend target today:** ~{money(per_day)} (adjust using envelopes above)") lines.append("") lines.append(packing_list(inp.days, inp.vibe, inp.food_pref)) lines.append(food_suggestions(inp.country_hint, inp.food_pref, inp.vibe)) lines.append("## Quick checklist") lines.append("- Book stays near transit / main area to save time & money.") lines.append("- Keep 10% buffer for surprises (cabs, tickets, snacks).") lines.append("- If a day goes over budget: compensate next day by choosing free attractions + simpler meals.") lines.append("") lines.append("> Note: This is a planning assistant. Always verify opening hours, local rules, and safety advisories.") return "\n".join(lines) # -------------------------- # Gradio App # -------------------------- def generate_plan(destination, days, start_date_str, budget, vibe, people, pace, food_pref, country_hint, use_llm): days = int(days) people = int(people) budget = float(budget) start = parse_start_date(start_date_str) inp = TripInputs( destination=destination.strip() or "Your destination", days=max(1, min(days, 30)), start_date=start, budget=max(0.0, budget), vibe=vibe, people=max(1, min(people, 12)), pace=pace, food_pref=food_pref, country_hint=country_hint, ) raw = build_itinerary(inp) final = maybe_polish_with_llm(raw, use_llm=use_llm) return final with gr.Blocks(title="Travel Itinerary Bot") as demo: gr.Markdown( "# 🧳 Travel Itinerary Bot\n" "Enter **destination + days + budget + vibe** → get a **day-by-day itinerary**, **packing list**, and **food suggestions**.\n" ) with gr.Row(): with gr.Column(scale=1): destination = gr.Textbox(label="Destination", placeholder="Goa / Manali / Jaipur / Bangkok ...", value="Goa") days = gr.Slider(1, 14, value=5, step=1, label="Number of days") start_date_str = gr.Textbox(label="Start date (optional)", placeholder="2025-12-20 or 'next Monday'", value="") budget = gr.Number(label="Total budget", value=20000) vibe = gr.Dropdown(VIBES, value="Relax & Chill", label="Vibe") pace = gr.Radio(["Slow", "Balanced", "Packed"], value="Balanced", label="Pace") people = gr.Slider(1, 6, value=1, step=1, label="People") food_pref = gr.Radio(["Veg", "Non-veg", "Any"], value="Any", label="Food preference") country_hint = gr.Radio(["India", "Other"], value="India", label="Country hint (for food tips)") use_llm = gr.Checkbox(label="Use FLAN-T5 to polish text (optional, slower)", value=False) btn = gr.Button("Generate itinerary", variant="primary") with gr.Column(scale=2): output = gr.Markdown() btn.click( generate_plan, inputs=[destination, days, start_date_str, budget, vibe, people, pace, food_pref, country_hint, use_llm], outputs=[output], ) demo.launch(share=True)