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}"}