From 1d4cecf05e8b6cd5ce5a01305d20cb8264159c83 Mon Sep 17 00:00:00 2001 From: eteka Date: Sun, 12 Oct 2025 22:22:53 +0100 Subject: [PATCH] Create Streamlit AI travel agent app --- .env.example | 2 + .gitignore | 4 + README.md | 63 +++++++- app.py | 371 +++++++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 3 + 5 files changed, 441 insertions(+), 2 deletions(-) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 app.py create mode 100644 requirements.txt diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..d6a8d9e --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +# Copy this file to .env and set your OpenAI credentials +OPENAI_API_KEY=sk-your-key-here diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..095ff8a --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +__pycache__/ +.env +.streamlit/ +*.pyc diff --git a/README.md b/README.md index d723190..68f1fa1 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,61 @@ -# hello-world -first project +# 🧭 AI Travel Agent + +A Streamlit application that crafts personalized travel itineraries using OpenAI's generative models. The project is inspired by [ashumishra2104/AI_Travel_agent_Streamlit](https://github.com/ashumishra2104/AI_Travel_agent_Streamlit) and reimagined with a modern OpenAI workflow, fallback itineraries, and downloadable trip plans. + +## Features + +- Guided sidebar to collect trip preferences such as destination, trip length, travel style, and special requests. +- Generates multi-day itineraries, highlights, packing lists, and travel tips with OpenAI's `gpt-4o-mini` model. +- Automatic fallback itinerary when an API key is not configured so the UI can be previewed locally. +- Expandable day-by-day schedule view with dedicated tabs for packing and travel tips. +- Download buttons for both JSON and Markdown versions of the generated itinerary. + +## Prerequisites + +- Python 3.9+ +- An OpenAI API key with access to the Responses API. + +## Getting started + +1. **Install dependencies** + + ```bash + pip install -r requirements.txt + ``` + +2. **Configure environment variables** + + ```bash + cp .env.example .env + # edit .env and add your OpenAI API key + ``` + + Alternatively, export `OPENAI_API_KEY` directly in your shell or hosting platform. + +3. **Run the Streamlit app** + + ```bash + streamlit run app.py + ``` + + The app will open in your browser at `http://localhost:8501`. + +## Usage notes + +- When an API key is available, the app requests a structured JSON itinerary from OpenAI. If a network or authentication issue occurs, an informative error is shown and a helpful fallback plan is displayed. +- Use the download buttons to save the generated itinerary as JSON for integrations or Markdown for sharing with travelers. +- Feel free to customize the prompt or add new sections (e.g., budgeting, maps) depending on your needs. + +## Project structure + +``` +. +├── app.py # Streamlit UI and OpenAI integration +├── requirements.txt # Python dependencies +├── .env.example # Environment variable template +└── README.md # Project documentation +``` + +## License + +This project inherits the MIT License from the original repository. diff --git a/app.py b/app.py new file mode 100644 index 0000000..380625e --- /dev/null +++ b/app.py @@ -0,0 +1,371 @@ +"""Streamlit AI travel agent application.""" +from __future__ import annotations + +import json +import os +from dataclasses import dataclass, asdict +from typing import Any, Dict, List, Optional + +import streamlit as st + +try: + from openai import OpenAI +except ImportError: # pragma: no cover - optional dependency for local preview + OpenAI = None # type: ignore + + +@dataclass +class DailyItem: + day: int + summary: str + morning: str + afternoon: str + evening: str + dining: Optional[str] = None + + +@dataclass +class TravelPlan: + destination: str + days: int + overview: str + highlights: List[str] + daily_schedule: List[DailyItem] + packing_list: List[str] + travel_tips: List[str] + + def to_markdown(self) -> str: + """Return a markdown representation of the travel plan.""" + lines = [f"# {self.destination} Travel Plan", "", "## Overview", self.overview, ""] + if self.highlights: + lines.append("## Trip Highlights") + for item in self.highlights: + lines.append(f"- {item}") + lines.append("") + + lines.append("## Detailed Itinerary") + for day in self.daily_schedule: + lines.extend( + [ + f"### Day {day.day}: {day.summary}", + f"- **Morning:** {day.morning}", + f"- **Afternoon:** {day.afternoon}", + f"- **Evening:** {day.evening}", + ] + ) + if day.dining: + lines.append(f"- **Dining:** {day.dining}") + lines.append("") + + if self.packing_list: + lines.append("## Packing List") + for item in self.packing_list: + lines.append(f"- {item}") + lines.append("") + + if self.travel_tips: + lines.append("## Travel Tips") + for tip in self.travel_tips: + lines.append(f"- {tip}") + + return "\n".join(lines) + + +SYSTEM_PROMPT = """You are a seasoned travel agent that crafts bespoke travel itineraries. +Always respond with valid JSON that matches the provided schema. +Include realistic, regionally appropriate suggestions. +""" + + +def _client_from_env() -> Optional[OpenAI]: + api_key = os.environ.get("OPENAI_API_KEY") + if not api_key or OpenAI is None: + return None + return OpenAI(api_key=api_key) + + +def _generate_prompt(user_inputs: Dict[str, Any]) -> str: + interests = ", ".join(user_inputs.get("interests", [])) or "general sightseeing" + styles = ", ".join(user_inputs.get("styles", [])) or "balanced" + companions = user_inputs.get("companions") or "Solo" + budget = user_inputs.get("budget") or "moderate" + special = user_inputs.get("special") or "None" + + return ( + "Create a detailed travel plan.\n" + f"Destination: {user_inputs['destination']}\n" + f"Start date: {user_inputs['start_date']}\n" + f"Number of days: {user_inputs['days']}\n" + f"Budget level: {budget}\n" + f"Travel styles: {styles}\n" + f"Interests: {interests}\n" + f"Travel companions: {companions}\n" + f"Special requests: {special}\n" + "Respond with itinerary, daily schedule (morning/afternoon/evening/dining), packing list, and travel tips." + ) + + +def _fallback_plan(user_inputs: Dict[str, Any]) -> TravelPlan: + destination = user_inputs["destination"] + days = int(user_inputs["days"]) + daily_items = [] + for day in range(1, days + 1): + daily_items.append( + DailyItem( + day=day, + summary=f"Memorable experiences in {destination}", + morning="Begin the day with a locally inspired breakfast and a guided walking tour.", + afternoon="Visit a landmark museum or cultural site tailored to your interests.", + evening="Enjoy a sunset viewpoint followed by a relaxed dinner in a neighborhood eatery.", + dining="Sample regional specialties and ask for seasonal recommendations from the chef.", + ) + ) + + return TravelPlan( + destination=destination, + days=days, + overview=( + f"A curated {days}-day adventure in {destination} highlighting local culture, cuisine, and hidden gems. " + "This placeholder itinerary is shown because no OpenAI API key was detected." + ), + highlights=[ + "Discover iconic attractions with tailored recommendations", + "Taste signature dishes at beloved restaurants", + "Enjoy balanced pacing with both structured and free time", + ], + daily_schedule=daily_items, + packing_list=[ + "Comfortable walking shoes", + "Weather-appropriate layers", + "Portable phone charger", + "Reusable water bottle", + ], + travel_tips=[ + "Book popular experiences ahead of time to secure preferred slots.", + "Carry a small amount of local currency for markets and taxis.", + "Learn a few local phrases to enhance your interactions.", + ], + ) + + +def _call_openai(user_inputs: Dict[str, Any]) -> Optional[TravelPlan]: + client = _client_from_env() + if client is None: + return None + + prompt = _generate_prompt(user_inputs) + try: + response = client.responses.create( + model="gpt-4o-mini", + input=[ + { + "role": "system", + "content": [ + {"type": "text", "text": SYSTEM_PROMPT}, + { + "type": "input_text", + "text": json.dumps( + { + "type": "object", + "properties": { + "overview": {"type": "string"}, + "highlights": {"type": "array", "items": {"type": "string"}}, + "daily_schedule": { + "type": "array", + "items": { + "type": "object", + "properties": { + "day": {"type": "integer"}, + "summary": {"type": "string"}, + "morning": {"type": "string"}, + "afternoon": {"type": "string"}, + "evening": {"type": "string"}, + "dining": {"type": "string"}, + }, + "required": [ + "day", + "summary", + "morning", + "afternoon", + "evening", + ], + }, + }, + "packing_list": {"type": "array", "items": {"type": "string"}}, + "travel_tips": {"type": "array", "items": {"type": "string"}}, + }, + "required": [ + "overview", + "highlights", + "daily_schedule", + "packing_list", + "travel_tips", + ], + } + ), + }, + ], + }, + {"role": "user", "content": [{"type": "text", "text": prompt}]}, + ], + response_format={"type": "json_object"}, + max_output_tokens=1200, + ) + except Exception as exc: # pragma: no cover - network/API failure fallback + st.error(f"Failed to generate itinerary: {exc}") + return None + + try: + content = response.output[0].content[0].text # type: ignore[index] + data = json.loads(content) + except Exception as exc: # pragma: no cover - guard parsing + st.error(f"Unexpected response format: {exc}") + return None + + daily_schedule = [ + DailyItem( + day=item.get("day", index + 1), + summary=item.get("summary", ""), + morning=item.get("morning", ""), + afternoon=item.get("afternoon", ""), + evening=item.get("evening", ""), + dining=item.get("dining"), + ) + for index, item in enumerate(data.get("daily_schedule", [])) + ] + + return TravelPlan( + destination=user_inputs["destination"], + days=user_inputs["days"], + overview=data.get("overview", ""), + highlights=list(data.get("highlights", [])), + daily_schedule=daily_schedule, + packing_list=list(data.get("packing_list", [])), + travel_tips=list(data.get("travel_tips", [])), + ) + + +def _collect_user_inputs() -> Dict[str, Any]: + st.sidebar.header("Trip Preferences") + destination = st.sidebar.text_input("Destination", placeholder="e.g. Kyoto, Japan") + start_date = st.sidebar.date_input("Start date") + days = st.sidebar.slider("Trip length (days)", min_value=1, max_value=21, value=5) + budget = st.sidebar.selectbox("Budget", ["Shoestring", "Moderate", "Upscale", "Luxury"]) + styles = st.sidebar.multiselect( + "Travel style", + options=["Relaxed", "Adventure", "Cultural", "Foodie", "Family", "Romantic"], + ) + interests = st.sidebar.multiselect( + "Interests", + options=[ + "Museums", + "Local cuisine", + "Outdoor adventures", + "Nightlife", + "History", + "Shopping", + "Wellness", + ], + ) + companions = st.sidebar.selectbox( + "Who are you traveling with?", + options=["Solo", "Partner", "Friends", "Family", "Colleagues"], + ) + special = st.sidebar.text_area("Special requests", placeholder="e.g. Vegetarian-friendly, accessible rooms") + + return { + "destination": destination.strip(), + "start_date": start_date.isoformat(), + "days": days, + "budget": budget, + "styles": styles, + "interests": interests, + "companions": companions, + "special": special.strip(), + } + + +def _render_plan(plan: TravelPlan) -> None: + st.subheader("Trip Overview") + st.write(plan.overview) + + if plan.highlights: + st.subheader("Highlights") + st.markdown("\n".join(f"- {item}" for item in plan.highlights)) + + itinerary_tab, packing_tab, tips_tab = st.tabs(["Daily Schedule", "Packing List", "Travel Tips"]) + + with itinerary_tab: + for day in plan.daily_schedule: + with st.expander(f"Day {day.day}: {day.summary}", expanded=day.day == 1): + st.markdown( + "\n".join( + [ + f"**Morning:** {day.morning}", + f"**Afternoon:** {day.afternoon}", + f"**Evening:** {day.evening}", + ] + ) + ) + if day.dining: + st.markdown(f"**Dining:** {day.dining}") + + with packing_tab: + if plan.packing_list: + st.markdown("\n".join(f"- {item}" for item in plan.packing_list)) + else: + st.info("No packing suggestions were provided.") + + with tips_tab: + if plan.travel_tips: + st.markdown("\n".join(f"- {tip}" for tip in plan.travel_tips)) + else: + st.info("No travel tips were provided.") + + st.download_button( + label="Download itinerary as JSON", + data=json.dumps(asdict(plan), indent=2), + file_name=f"{plan.destination.lower().replace(' ', '_')}_itinerary.json", + mime="application/json", + ) + st.download_button( + label="Download itinerary as Markdown", + data=plan.to_markdown(), + file_name=f"{plan.destination.lower().replace(' ', '_')}_itinerary.md", + mime="text/markdown", + ) + + +def main() -> None: + st.set_page_config(page_title="AI Travel Agent", page_icon="🧭", layout="wide") + st.title("🧭 AI Travel Agent") + st.caption( + "Plan unforgettable trips powered by OpenAI. Adjust preferences in the sidebar and generate a personalized itinerary." + ) + + user_inputs = _collect_user_inputs() + + st.markdown( + """ + ### How it works + 1. Fill in your destination and preferences using the sidebar. + 2. Click **Generate itinerary** to request a custom travel plan. + 3. Review the daily schedule, packing list, and travel tips. Download the plan for later. + """ + ) + + if st.button("Generate itinerary", type="primary"): + if not user_inputs["destination"]: + st.error("Please provide a destination to plan your trip.") + return + + plan = _call_openai(user_inputs) + if plan is None: + plan = _fallback_plan(user_inputs) + _render_plan(plan) + else: + st.info("Enter your preferences and click **Generate itinerary** to see a personalized trip plan.") + + +if __name__ == "__main__": + main() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..ed9c8d3 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +streamlit>=1.32.0 +openai>=1.35.3 +python-dotenv>=1.0.1