Spaces:
Sleeping
Sleeping
File size: 12,884 Bytes
5dd9976 6fcb466 f597c94 29009fc 5dd9976 29009fc 5dd9976 29009fc 5dd9976 29009fc 5dd9976 f597c94 5dd9976 19d2ed6 aa34e82 5dd9976 29009fc 5dd9976 29009fc 5dd9976 29009fc 5dd9976 29009fc 5dd9976 29009fc 5dd9976 29009fc 5dd9976 29009fc 5dd9976 29009fc 5dd9976 29009fc 5dd9976 29009fc 97f20a2 29009fc f597c94 adcda7d f597c94 adcda7d f597c94 adcda7d f597c94 adcda7d f597c94 29009fc 5dd9976 29009fc 5dd9976 29009fc 5dd9976 29009fc 97f20a2 5dd9976 97f20a2 29009fc 5dd9976 97f20a2 29009fc 97f20a2 29009fc | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 | from fastapi import FastAPI, HTTPException, Query
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import HTMLResponse
from pydantic import BaseModel
import requests
import pandas as pd
from datetime import datetime, date
import google.generativeai as genai
import os
import json
from dotenv import load_dotenv
from sheets_client import add_subscriber_to_sheet
import random
load_dotenv()
app = FastAPI(
title="Aussie Backpacker Flow API",
description="Provides flight demand analysis and AI-powered insights for Australian hostels.",
version="1.0.0"
)
origins = ["*"]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
FLIGHT_API_KEY = os.getenv("FLIGHT_API_KEY")
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
if GEMINI_API_KEY:
try:
genai.configure(api_key=GEMINI_API_KEY)
except Exception as e:
print(f"Could not configure Gemini API: {e}")
AUSTRALIAN_CITY_CODES = {
"Sydney": "SYD", "Melbourne": "MEL", "Brisbane": "BNE", "Perth": "PER",
"Adelaide": "ADL", "Canberra": "CBR", "Gold Coast": "OOL", "Cairns": "CNS",
"Hobart": "HBA", "Darwin": "DRW"
}
class NewsletterPayload(BaseModel):
email: str
favDestinations: list[str] = []
travelOrigin: str = ""
dob: str = ""
class ChatPayload(BaseModel):
message: str
def fetch_flight_data(api_key, departure_airport, arrival_airport, date_str):
url = f"https://api.flightapi.io/onewaytrip/{api_key}/{departure_airport}/{arrival_airport}/{date_str}/1/0/0/Economy/AUD"
try:
response = requests.get(url, timeout=30)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
print(f"Error fetching flight data: {e}")
return None
def parse_and_process_data(data):
if not data or 'itineraries' not in data or not data.get('itineraries'):
return pd.DataFrame()
carriers = {c['id']: c for c in data.get('carriers', [])}
flight_options = []
for i, itinerary in enumerate(data.get('itineraries', [])):
price_info = itinerary.get('pricing_options', [{}])[0].get('price', {})
price = price_info.get('amount')
leg_id = itinerary.get('leg_ids', [None])[0]
leg = next((l for l in data.get('legs', []) if l['id'] == leg_id), None)
if not all([price, leg]):
continue
marketing_carrier_id = leg.get('marketing_carrier_ids', [None])[0]
carrier_info = carriers.get(marketing_carrier_id, {})
airline_name = carrier_info.get('name', "Unknown Airline")
flight_number = "N/A"
if leg.get('segment_ids'):
segment_id = leg['segment_ids'][0]
segment = next((s for s in data.get('segments', []) if s['id'] == segment_id), None)
if segment:
carrier_code = carrier_info.get('code', "XX")
flight_num_part = segment.get('marketing_flight_number', leg['id'][:3])
flight_number = f"{carrier_code}{flight_num_part}"
flight_options.append({
"id": i, "airline": airline_name, "flight": flight_number,
"departure": pd.to_datetime(leg['departure']).strftime('%H:%M'),
"arrival": pd.to_datetime(leg['arrival']).strftime('%H:%M'), "price": price
})
return pd.DataFrame(flight_options).sort_values(by="price").reset_index(drop=True)
def get_dashboard_ai_analysis(df, origin, destination, date_str):
if not GEMINI_API_KEY or df.empty:
return "AI analysis could not be performed due to a configuration issue or lack of data."
cheapest_flight = df.iloc[0]
data_summary = f"""Context: Flight search from {origin} to {destination} for {date_str}. Total flights: {len(df)}. Price Range: ${df['price'].min():.2f} to ${df['price'].max():.2f}. Cheapest: {cheapest_flight['airline']} for ${cheapest_flight['price']:.2f}."""
prompt = f"You are a sharp, concise market analyst for 'Aussie Backpacker Flow'. Based on the following summary, provide actionable insights in markdown bullet points: {data_summary}. Focus on: a one-sentence market snapshot, top budget carriers, demand interpretation, and one specific marketing tip for a hostel manager."
try:
model = genai.GenerativeModel('gemini-1.5-flash-latest')
response = model.generate_content(prompt)
report_header = f"Based on the data for **{origin} to {destination}** on **{date_str}**, here are the key insights:"
return f"{report_header}\n\n{response.text}"
except Exception as e:
return f"An error occurred during AI analysis: {e}"
def generate_dynamic_trend_data(origin, destination):
price_tiers = {
"short": (90, 180),
"medium": (150, 300),
"long": (250, 450)
}
long_haul_cities = ["Perth"]
tier = "long" if origin in long_haul_cities or destination in long_haul_cities else "short"
if origin in ["Sydney", "Melbourne", "Brisbane"] and destination in ["Cairns", "Adelaide", "Hobart"]:
tier = "medium"
min_base, max_base = price_tiers[tier]
data = {}
routes_to_generate = {f"{origin[:3].upper()}-{destination[:3].upper()}": (min_base, max_base)}
other_routes = list(price_tiers.keys())
random.shuffle(other_routes)
for i in range(2):
tier_key = other_routes[i]
mock_origin, mock_dest = ("SYD","MEL") if tier_key == "short" else ("BNE","CNS") if tier_key == "medium" else ("PER","SYD")
routes_to_generate[f"{mock_origin}-{mock_dest}"] = price_tiers[tier_key]
for route, (min_b, max_b) in routes_to_generate.items():
route_data = []
for i in range(1, 31):
day_factor = (i % 7 - 3) * 5
min_price = round(min_b + day_factor + random.uniform(-10, 10))
max_price = round(max_b + day_factor + random.uniform(-10, 20))
avg_price = round((min_price + max_price) / 2)
route_data.append({"day": i, "minPrice": min_price, "avgPrice": avg_price, "maxPrice": max_price})
data[route] = route_data
return data
def generate_mock_heatmap_data():
airports = list(AUSTRALIAN_CITY_CODES.keys())
routes = []
for _ in range(10):
from_city, to_city = random.sample(airports, 2)
price = random.randint(70, 400)
status = 'green' if price < 150 else 'yellow' if price < 280 else 'red'
routes.append({"from": from_city, "to": to_city, "status": status, "price": f"${price}"})
return routes
def generate_mock_topmovers_data():
routes = ["SYD β MEL", "BNE β CNS", "MEL β ADL", "SYD β CBR", "SYD β OOL"]
cities = ["Gold Coast (OOL)", "Cairns (CNS)", "Sydney (SYD)"]
price_drops = [{"route": r, "airline": random.choice(["Jetstar", "Rex", "Virgin"]), "drop": f"{random.randint(15, 35)}%", "price": f"${random.randint(70, 120)}"} for r in random.sample(routes, 5)]
high_demand = [{"city": c, "flights": random.randint(30, 120), "change": f"+{random.randint(8, 25)}%"} for c in cities]
return {"priceDrops": price_drops, "highDemand": high_demand}
@app.get("/", response_class=HTMLResponse)
def read_root():
html_content = """
<html>
<head>
<title>Aussie Backpacker Flow API</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background-color: #0d1117; color: #c9d1d9; }
.container { max-width: 700px; text-align: center; background-color: #161b22; border: 1px solid #30363d; border-radius: 12px; padding: 50px; }
h1 { color: #58a6ff; font-size: 2.5rem; margin-bottom: 1rem; }
code { background-color: #30363d; padding: 4px 8px; border-radius: 6px; font-family: "SF Mono", "Consolas", monospace; }
a { color: #58a6ff; text-decoration: none; }
.status { display: inline-block; background-color: #238636; color: white; padding: 8px 18px; border-radius: 20px; font-weight: bold; margin-bottom: 1.5rem; }
</style>
</head>
<body>
<div class="container">
<p class="status">API is Online</p>
<h1>Aussie Backpacker Flow API</h1>
<p>This is the backend server for the flight demand analysis tool.</p>
<p>To see the interactive API documentation, visit <a href="/docs">/docs</a>.</p>
</div>
</body>
</html>
"""
return HTMLResponse(content=html_content)
@app.post("/api/newsletter")
def handle_newsletter(payload: NewsletterPayload):
success, message = add_subscriber_to_sheet(
email=payload.email,
fav_destinations=payload.favDestinations,
origin=payload.travelOrigin,
dob=payload.dob
)
if not success:
raise HTTPException(status_code=400, detail=message)
return {"message": message}
@app.get("/api/dashboard-data")
def get_dashboard_data(origin: str, destination: str, date: str):
origin_code = AUSTRALIAN_CITY_CODES.get(origin)
dest_code = AUSTRALIAN_CITY_CODES.get(destination)
if not origin_code or not dest_code:
raise HTTPException(status_code=400, detail="Invalid city name provided.")
raw_data = fetch_flight_data(FLIGHT_API_KEY, origin_code, dest_code, date)
flight_df = parse_and_process_data(raw_data)
if flight_df.empty:
raise HTTPException(status_code=404, detail=f"No live flight data could be found for {origin} to {destination} on {date}. Please try another route or date.")
cheapest_flight_row = flight_df.loc[flight_df['price'].idxmin()]
dashboard_payload = {
"insightCards": {
"cheapestFlight": {"price": cheapest_flight_row['price'], "airline": cheapest_flight_row['airline'], "flightNumber": cheapest_flight_row['flight']},
"busiestAirline": {"name": flight_df['airline'].mode()[0], "flightCount": len(flight_df)},
"bestDeal": {"name": cheapest_flight_row['airline'], "savings": "Top Value"}
},
"aiAnalystReport": get_dashboard_ai_analysis(flight_df, origin, destination, date),
"flightPriceChart": flight_df.groupby('airline')['price'].min().reset_index().rename(columns={'airline': 'name'}).to_dict('records'),
"flightDataTable": flight_df.to_dict('records'),
"detailedTrendChart": generate_dynamic_trend_data(origin, destination),
"airfareHeatmap": generate_mock_heatmap_data(),
"topMovers": generate_mock_topmovers_data(),
}
return dashboard_payload
@app.post("/api/chat")
def handle_chat(payload: ChatPayload):
user_message = payload.message
if not GEMINI_API_KEY:
return {"reply": "Chatbot is disabled. Backend needs a Gemini API key."}
try:
model = genai.GenerativeModel('gemini-1.5-flash-latest')
intent_prompt = f"From the user's message, extract origin city, destination city, and a date (today is {date.today().strftime('%Y-%m-%d')}). Respond ONLY with a valid JSON object. Keys: 'origin', 'destination', 'date'. Missing values should be null. Message: '{user_message}'"
response = model.generate_content(intent_prompt)
json_str = response.text.strip().replace("```json", "").replace("```", "")
params = json.loads(json_str)
if not all(params.get(k) for k in ['origin', 'destination', 'date']):
return {"reply": "I can help with that! To give you the best info, I need the origin city, destination city, and the date you're interested in."}
origin_code = next((code for name, code in AUSTRALIAN_CITY_CODES.items() if name.lower() in params['origin'].lower()), None)
dest_code = next((code for name, code in AUSTRALIAN_CITY_CODES.items() if name.lower() in params['destination'].lower()), None)
if not origin_code or not dest_code:
return {"reply": "Sorry, I couldn't recognise those city names. Please use major Australian cities."}
raw_data = fetch_flight_data(FLIGHT_API_KEY, origin_code, dest_code, params['date'])
flight_df = parse_and_process_data(raw_data)
if flight_df.empty:
return {"reply": f"I couldn't find any flights from {params['origin']} to {params['destination']} on {params['date']}."}
summary_prompt = f"You are a helpful travel assistant. Based on this flight data, write a short, conversational summary of the cheapest option, and maybe one other good one. Data: {flight_df.head(3).to_markdown()}"
summary_response = model.generate_content(summary_prompt)
return {"reply": summary_response.text}
except Exception as e:
return {"reply": f"I had a little trouble with that. My apologies. Error: {e}"} |