Virtual AI Travel Planner

import os

import json

import time

import requests

import pandas as pd

import streamlit as st

from datetime import date, timedelta


# -----------------------------

# Setup: API Keys from env vars

# -----------------------------

OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "")

GOOGLE_MAPS_API_KEY = os.getenv("GOOGLE_MAPS_API_KEY", "")


# -----------------------------

# Google Places helpers

# -----------------------------

PLACES_TEXT_SEARCH_URL = "https://maps.googleapis.com/maps/api/place/textsearch/json"


def places_text_search(query, key, max_results=20):

    """

    Simple wrapper for Google Places 'Text Search' API.

    We page through results (up to ~60) but cap by max_results.

    """

    if not key:

        return []


    params = {

        "query": query,

        "key": key,

    }

    out = []

    next_page_token = None

    while True:

        if next_page_token:

            params["pagetoken"] = next_page_token

            # Google requires short delay before using next_page_token

            time.sleep(2)


        r = requests.get(PLACES_TEXT_SEARCH_URL, params=params, timeout=30)

        if r.status_code != 200:

            break

        data = r.json()

        out.extend(data.get("results", []))


        next_page_token = data.get("next_page_token")

        if not next_page_token or len(out) >= max_results:

            break


    return out[:max_results]



def fetch_hotels(destination: str, max_results=20):

    """

    Fetch hotels around destination. Uses price_level (0-4) and rating when available.

    """

    results = places_text_search(f"hotels in {destination}", GOOGLE_MAPS_API_KEY, max_results=max_results)

    rows = []

    for r in results:

        rows.append({

            "name": r.get("name"),

            "rating": r.get("rating"),

            "reviews": r.get("user_ratings_total"),

            "price_level(0-4)": r.get("price_level"),

            "address": r.get("formatted_address"),

            "lat": r.get("geometry", {}).get("location", {}).get("lat"),

            "lng": r.get("geometry", {}).get("location", {}).get("lng")

        })

    df = pd.DataFrame(rows)

    # sort: rating desc, then reviews desc

    if not df.empty:

        df = df.sort_values(by=["rating", "reviews"], ascending=[False, False], na_position="last")

    return df



def fetch_attractions(destination: str, max_results=20):

    """

    Fetch attractions/POIs.

    """

    query = f"top attractions in {destination}"

    results = places_text_search(query, GOOGLE_MAPS_API_KEY, max_results=max_results)

    rows = []

    for r in results:

        rows.append({

            "name": r.get("name"),

            "category": ", ".join(r.get("types", [])),

            "rating": r.get("rating"),

            "reviews": r.get("user_ratings_total"),

            "address": r.get("formatted_address"),

            "lat": r.get("geometry", {}).get("location", {}).get("lat"),

            "lng": r.get("geometry", {}).get("location", {}).get("lng")

        })

    df = pd.DataFrame(rows)

    if not df.empty:

        df = df.sort_values(by=["rating", "reviews"], ascending=[False, False], na_position="last")

    return df


# -----------------------------

# Budget helper

# -----------------------------

def simple_budget_breakdown(total_budget, days, travelers=1):

    """

    Very coarse split of a trip budget (adjust as needed).

    Returns per-trip & per-day guide.

    """

    total_budget = float(total_budget)

    days = max(1, int(days))

    travelers = max(1, int(travelers))


    # Example split: 45% stay, 30% food, 15% local transport, 10% activities

    stay = total_budget * 0.45

    food = total_budget * 0.30

    transport = total_budget * 0.15

    activities = total_budget * 0.10


    per_day = {

        "Stay": round(stay / days, 2),

        "Food": round(food / days, 2),

        "Local Transport": round(transport / days, 2),

        "Activities": round(activities / days, 2),

        "Total/day": round(total_budget / days, 2)

    }

    per_person = round(total_budget / travelers, 2)

    return per_day, per_person


# -----------------------------

# OpenAI itinerary helper

# -----------------------------

def generate_itinerary_with_openai(destination, start_date, days, interests, budget_hint, travelers):

    """

    Calls OpenAI Chat Completions to generate an itinerary.

    Expects OPENAI_API_KEY in env. Uses the Chat Completions HTTP endpoint.

    """

    if not OPENAI_API_KEY:

        return "OpenAI API key not set. Please set OPENAI_API_KEY in your environment."


    # Build system & user prompts

    sys_prompt = (

        "You are a helpful travel planner. Create practical, walkable day-by-day itineraries with morning, afternoon, and evening blocks, "

        "add short logistic hints, and keep it realistic for the location. Keep each day concise and bulleted."

    )

    user_prompt = (

        f"Destination: {destination}\n"

        f"Start date: {start_date}\n"

        f"Trip length: {days} days\n"

        f"Travelers: {travelers}\n"

        f"Interests: {', '.join(interests) if interests else 'general sightseeing'}\n"

        f"Budget guidance: {budget_hint}\n\n"

        "Please produce:\n"

        "1) A short overview paragraph (tone: friendly & practical)\n"

        "2) Day-by-day plan, each day with 3 bullet sections: Morning / Afternoon / Evening\n"

        "3) A compact list of neighborhood/area suggestions for dining\n"

        "4) 6 packing tips relevant to weather & activities\n"

    )


    # Minimal direct HTTP call to OpenAI Chat Completions (compatible with current API).

    url = "https://api.openai.com/v1/chat/completions"

    headers = {

        "Authorization": f"Bearer {OPENAI_API_KEY}",

        "Content-Type": "application/json",

    }

    payload = {

        "model": "gpt-4o-mini",   # pick a chat-capable model available to your account

        "messages": [

            {"role": "system", "content": sys_prompt},

            {"role": "user", "content": user_prompt}

        ],

        "temperature": 0.7,

        "max_tokens": 1200

    }


    try:

        resp = requests.post(url, headers=headers, data=json.dumps(payload), timeout=60)

        resp.raise_for_status()

        data = resp.json()

        return data["choices"][0]["message"]["content"].strip()

    except Exception as e:

        return f"Failed to generate itinerary: {e}"


# -----------------------------

# Streamlit UI

# -----------------------------

st.set_page_config(page_title="Virtual AI Travel Planner", page_icon="🧭", layout="wide")


st.title("🧭 Virtual AI Travel Planner")

st.caption("Enter your destination & budget → get hotels, attractions, and an AI-built itinerary.\n\n*Educational demo. Always verify details before booking.*")


with st.sidebar:

    st.header("🔑 API Keys")

    st.write("Set these as environment variables before running:\n- `OPENAI_API_KEY`\n- `GOOGLE_MAPS_API_KEY`")

    st.write("Detected:")

    st.code(f"OPENAI_API_KEY set: {bool(OPENAI_API_KEY)}\nGOOGLE_MAPS_API_KEY set: {bool(GOOGLE_MAPS_API_KEY)}")


col1, col2, col3 = st.columns([1.2,1,1])


with col1:

    destination = st.text_input("Destination (city/country)", placeholder="e.g., Tokyo, Japan")

    start = st.date_input("Start date", value=date.today() + timedelta(days=14))

    days = st.number_input("Trip length (days)", min_value=1, max_value=21, value=5, step=1)


with col2:

    budget = st.number_input("Total budget (your currency)", min_value=0.0, value=1000.0, step=100.0, help="Rough trip budget total")

    travelers = st.number_input("Travelers", min_value=1, value=2, step=1)

    interests = st.multiselect("Interests", [

        "Food", "Museums", "Nature", "Architecture", "Shopping", "Nightlife", "Adventure", "History", "Beaches", "Hiking"

    ], default=["Food", "Museums"])


with col3:

    max_hotels = st.slider("Max hotels to fetch", 5, 40, 15)

    max_attractions = st.slider("Max attractions to fetch", 5, 40, 20)

    run = st.button("✨ Plan my trip")


if run:

    if not destination.strip():

        st.error("Please enter a destination.")

        st.stop()


    # Budget breakdown

    per_day, per_person = simple_budget_breakdown(budget, days, travelers)

    st.subheader("💸 Budget Guide")

    c1, c2 = st.columns(2)

    with c1:

        st.write("**Per-day guide** (rough):")

        st.table(pd.DataFrame([per_day]))

    with c2:

        st.metric("Per-person total", f"{per_person:.2f}")


    # Google results

    st.subheader("🏨 Hotels (Google Places)")

    hotels_df = fetch_hotels(destination, max_results=max_hotels)

    if hotels_df.empty:

        st.info("No hotel data (check your Google API key & billing).")

    else:

        st.dataframe(hotels_df, use_container_width=True)


    st.subheader("📍 Attractions / Things to do (Google Places)")

    attractions_df = fetch_attractions(destination, max_results=max_attractions)

    if attractions_df.empty:

        st.info("No attractions data (check your Google API key & billing).")

    else:

        st.dataframe(attractions_df, use_container_width=True)


    # AI itinerary

    st.subheader("🧠 AI Itinerary")

    budget_hint = f"Total budget approx {budget} for {travelers} travelers over {days} days. Per-day guide: {per_day}."

    itinerary = generate_itinerary_with_openai(destination, start, days, interests, budget_hint, travelers)

    st.write(itinerary)


    # Optional: CSV downloads

    st.download_button(

        "⬇️ Download hotels CSV",

        data=hotels_df.to_csv(index=False).encode("utf-8"),

        file_name=f"{destination.replace(' ','_').lower()}_hotels.csv",

        mime="text/csv"

    )

    st.download_button(

        "⬇️ Download attractions CSV",

        data=attractions_df.to_csv(index=False).encode("utf-8"),

        file_name=f"{destination.replace(' ','_').lower()}_attractions.csv",

        mime="text/csv"

    )


    st.success("Done! Scroll up to view tables and itinerary. 🌍✈️")

else:

    st.info("Fill the form and click **Plan my trip**.")


No comments: