sonuprasad23 commited on
Commit
29009fc
·
1 Parent(s): 19d2ed6

Done some major changes

Browse files
Files changed (4) hide show
  1. Dockerfile +0 -7
  2. main.py +105 -130
  3. requirements.txt +3 -1
  4. sheets_client.py +52 -0
Dockerfile CHANGED
@@ -1,18 +1,11 @@
1
- # Use an official Python runtime as a parent image
2
  FROM python:3.11-slim
3
 
4
- # Set the working directory in the container
5
  WORKDIR /code
6
 
7
- # Copy the requirements file into the container at /code
8
  COPY ./requirements.txt /code/requirements.txt
9
 
10
- # Install any needed packages specified in requirements.txt
11
  RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
12
 
13
- # Copy the rest of the application's code into the container at /code
14
  COPY . /code/
15
 
16
- # Tell uvicorn to run on all available network interfaces (0.0.0.0)
17
- # and on the port Hugging Face Spaces expects (7860).
18
  CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
 
 
1
  FROM python:3.11-slim
2
 
 
3
  WORKDIR /code
4
 
 
5
  COPY ./requirements.txt /code/requirements.txt
6
 
 
7
  RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
8
 
 
9
  COPY . /code/
10
 
 
 
11
  CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
main.py CHANGED
@@ -1,24 +1,21 @@
1
  from fastapi import FastAPI, HTTPException, Query
2
  from fastapi.middleware.cors import CORSMiddleware
 
3
  import requests
4
  import pandas as pd
5
- from datetime import datetime
6
  import google.generativeai as genai
7
  import os
 
8
  from dotenv import load_dotenv
 
 
9
 
10
  load_dotenv()
11
  app = FastAPI()
12
 
13
  origins = ["*"]
14
-
15
- app.add_middleware(
16
- CORSMiddleware,
17
- allow_origins=origins,
18
- allow_credentials=True,
19
- allow_methods=["*"],
20
- allow_headers=["*"],
21
- )
22
 
23
  FLIGHT_API_KEY = os.getenv("FLIGHT_API_KEY")
24
  GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
@@ -35,6 +32,15 @@ AUSTRALIAN_CITY_CODES = {
35
  "Hobart": "HBA", "Darwin": "DRW"
36
  }
37
 
 
 
 
 
 
 
 
 
 
38
  def fetch_flight_data(api_key, departure_airport, arrival_airport, date_str):
39
  url = f"https://api.flightapi.io/onewaytrip/{api_key}/{departure_airport}/{arrival_airport}/{date_str}/1/0/0/Economy/AUD"
40
  try:
@@ -45,38 +51,21 @@ def fetch_flight_data(api_key, departure_airport, arrival_airport, date_str):
45
  print(f"Error fetching flight data: {e}")
46
  return None
47
 
48
- def parse_and_process_data(data, origin_code, dest_code, search_date):
49
- if not data or 'itineraries' not in data:
50
  return pd.DataFrame()
51
-
52
  carriers = {c['id']: c for c in data.get('carriers', [])}
53
- places = {p['id']: p['name'] for p in data.get('places', [])}
54
-
55
  flight_options = []
56
-
57
- if not data.get('itineraries'):
58
- return pd.DataFrame()
59
-
60
  for i, itinerary in enumerate(data.get('itineraries', [])):
61
- if not itinerary.get('pricing_options'):
62
- continue
63
-
64
- price_info = itinerary['pricing_options'][0].get('price', {})
65
  price = price_info.get('amount')
66
-
67
- if not itinerary.get('leg_ids'):
68
- continue
69
-
70
- leg_id = itinerary['leg_ids'][0]
71
  leg = next((l for l in data.get('legs', []) if l['id'] == leg_id), None)
72
-
73
- if not leg or not price:
74
  continue
75
-
76
  marketing_carrier_id = leg.get('marketing_carrier_ids', [None])[0]
77
  carrier_info = carriers.get(marketing_carrier_id, {})
78
  airline_name = carrier_info.get('name', "Unknown Airline")
79
-
80
  flight_number = "N/A"
81
  if leg.get('segment_ids'):
82
  segment_id = leg['segment_ids'][0]
@@ -85,119 +74,105 @@ def parse_and_process_data(data, origin_code, dest_code, search_date):
85
  carrier_code = carrier_info.get('code', "XX")
86
  flight_num_part = segment.get('marketing_flight_number', leg['id'][:3])
87
  flight_number = f"{carrier_code}{flight_num_part}"
88
-
89
  flight_options.append({
90
- "id": i,
91
- "airline": airline_name,
92
- "flight": flight_number,
93
  "departure": pd.to_datetime(leg['departure']).strftime('%H:%M'),
94
- "arrival": pd.to_datetime(leg['arrival']).strftime('%H:%M'),
95
- "price": price
96
  })
97
-
98
- if not flight_options:
99
- return pd.DataFrame()
100
-
101
- df = pd.DataFrame(flight_options)
102
- return df.sort_values(by="price").reset_index(drop=True)
103
-
104
- def get_ai_insights(df, origin, destination, date):
105
- if df.empty:
106
- return "No flight data was available to analyze."
107
-
108
- if not GEMINI_API_KEY:
109
- return "**AI Analysis Skipped:** Gemini API key not configured on the backend."
110
-
111
- summary_df = df.head(10)
112
- data_summary = f"""
113
- Flight Price Data Summary:
114
- - Route: {origin} to {destination}
115
- - Date: {date}
116
- - Number of flights found: {len(df)}
117
- - Cheapest flight: AUD {df['price'].min()} on {df.loc[df['price'].idxmin(), 'airline']}
118
- - Average price of top 10 cheapest: AUD {summary_df['price'].mean():.2f}
119
- """
120
-
121
- prompt = f"""
122
- You are a market analyst for a chain of Australian hostels. Your goal is to provide a brief, actionable report based on flight data.
123
-
124
- **Data Provided:**
125
- {data_summary}
126
-
127
- **Your Task:**
128
- Provide a bullet-point summary in Markdown format for a hostel manager. The tone should be concise and professional. Focus on:
129
- - **Top Insight:** What is the single most important takeaway? (e.g., "The route is highly competitive today, driving prices down.")
130
- - **Price Trends:** Is it a good day to travel for budget-conscious guests? What is the cheapest price found?
131
- - **Key Airlines:** Which 1-2 airlines are dominating the budget-friendly options?
132
- - **Actionable Advice:** Give one concrete recommendation. (e.g., "Target marketing efforts towards customers arriving on Rex or Jetstar flights.")
133
- """
134
-
135
  try:
136
  model = genai.GenerativeModel('gemini-1.5-flash-latest')
137
  response = model.generate_content(prompt)
138
- report_header = f"Based on the data for **{origin} to {destination}** on **{date}**, here are the key insights:"
139
  return f"{report_header}\n\n{response.text}"
140
  except Exception as e:
141
- return f"Could not generate AI insights. Error: {e}"
142
-
143
- @app.get("/")
144
- def read_root():
145
- return {
146
- "message": "Welcome to the Airline Demand Analyzer API!",
147
- "status": "ok",
148
- "endpoints": {
149
- "analyze_demand": "/api/analyze-demand?origin=<city>&destination=<city>&date=YYYY-MM-DD"
150
- }
151
- }
152
-
153
- @app.get("/api/analyze-demand")
154
- def analyze_demand(
155
- origin: str = Query(..., description="Origin city name, e.g., Sydney"),
156
- destination: str = Query(..., description="Destination city name, e.g., Melbourne"),
157
- date: str = Query(..., description="Departure date in YYYY-MM-DD format")
158
- ):
159
- if not FLIGHT_API_KEY:
160
- raise HTTPException(status_code=500, detail="Flight API key not configured on the server.")
161
-
 
 
 
 
 
 
 
 
 
 
 
 
162
  origin_code = AUSTRALIAN_CITY_CODES.get(origin)
163
  dest_code = AUSTRALIAN_CITY_CODES.get(destination)
164
-
165
  if not origin_code or not dest_code:
166
- raise HTTPException(status_code=400, detail="Invalid origin or destination city name.")
167
-
168
  raw_data = fetch_flight_data(FLIGHT_API_KEY, origin_code, dest_code, date)
169
- if not raw_data:
170
- raise HTTPException(status_code=404, detail="No flight data found for the specified route and date.")
171
-
172
- flight_df = parse_and_process_data(raw_data, origin_code, dest_code, date)
173
  if flight_df.empty:
174
- raise HTTPException(status_code=404, detail="No flight options could be parsed for this route.")
175
-
176
- ai_report = get_ai_insights(flight_df, origin, destination, date)
177
-
178
  cheapest_flight_row = flight_df.loc[flight_df['price'].idxmin()]
179
- insight_cards = {
180
- "cheapestFlight": {
181
- "price": cheapest_flight_row['price'],
182
- "airline": cheapest_flight_row['airline'],
183
- "flightNumber": cheapest_flight_row['flight']
184
- },
185
- "busiestAirline": {
186
- "name": flight_df['airline'].mode()[0],
187
- "flightCount": len(flight_df)
188
  },
189
- "bestDeal": {
190
- "name": cheapest_flight_row['airline'],
191
- "savings": "Top Value"
192
- }
193
- }
194
-
195
- chart_data = flight_df.groupby('airline')['price'].min().reset_index()
196
- chart_data.rename(columns={'airline': 'name', 'price': 'price'}, inplace=True)
197
-
198
- return {
199
- "insightCards": insight_cards,
200
  "aiAnalystReport": ai_report,
201
- "flightPriceChart": chart_data.to_dict('records'),
202
  "flightDataTable": flight_df.to_dict('records')
203
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  from fastapi import FastAPI, HTTPException, Query
2
  from fastapi.middleware.cors import CORSMiddleware
3
+ from pydantic import BaseModel
4
  import requests
5
  import pandas as pd
6
+ from datetime import datetime, date
7
  import google.generativeai as genai
8
  import os
9
+ import json
10
  from dotenv import load_dotenv
11
+ from sheets_client import add_subscriber_to_sheet
12
+ import random
13
 
14
  load_dotenv()
15
  app = FastAPI()
16
 
17
  origins = ["*"]
18
+ app.add_middleware(CORSMiddleware, allow_origins=origins, allow_credentials=True, allow_methods=["*"], allow_headers=["*"])
 
 
 
 
 
 
 
19
 
20
  FLIGHT_API_KEY = os.getenv("FLIGHT_API_KEY")
21
  GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
 
32
  "Hobart": "HBA", "Darwin": "DRW"
33
  }
34
 
35
+ class NewsletterPayload(BaseModel):
36
+ email: str
37
+ favDestinations: list[str] = []
38
+ travelOrigin: str = ""
39
+ dob: str = ""
40
+
41
+ class ChatPayload(BaseModel):
42
+ message: str
43
+
44
  def fetch_flight_data(api_key, departure_airport, arrival_airport, date_str):
45
  url = f"https://api.flightapi.io/onewaytrip/{api_key}/{departure_airport}/{arrival_airport}/{date_str}/1/0/0/Economy/AUD"
46
  try:
 
51
  print(f"Error fetching flight data: {e}")
52
  return None
53
 
54
+ def parse_and_process_data(data):
55
+ if not data or 'itineraries' not in data or not data.get('itineraries'):
56
  return pd.DataFrame()
 
57
  carriers = {c['id']: c for c in data.get('carriers', [])}
 
 
58
  flight_options = []
 
 
 
 
59
  for i, itinerary in enumerate(data.get('itineraries', [])):
60
+ price_info = itinerary.get('pricing_options', [{}])[0].get('price', {})
 
 
 
61
  price = price_info.get('amount')
62
+ leg_id = itinerary.get('leg_ids', [None])[0]
 
 
 
 
63
  leg = next((l for l in data.get('legs', []) if l['id'] == leg_id), None)
64
+ if not all([price, leg]):
 
65
  continue
 
66
  marketing_carrier_id = leg.get('marketing_carrier_ids', [None])[0]
67
  carrier_info = carriers.get(marketing_carrier_id, {})
68
  airline_name = carrier_info.get('name', "Unknown Airline")
 
69
  flight_number = "N/A"
70
  if leg.get('segment_ids'):
71
  segment_id = leg['segment_ids'][0]
 
74
  carrier_code = carrier_info.get('code', "XX")
75
  flight_num_part = segment.get('marketing_flight_number', leg['id'][:3])
76
  flight_number = f"{carrier_code}{flight_num_part}"
 
77
  flight_options.append({
78
+ "id": i, "airline": airline_name, "flight": flight_number,
 
 
79
  "departure": pd.to_datetime(leg['departure']).strftime('%H:%M'),
80
+ "arrival": pd.to_datetime(leg['arrival']).strftime('%H:%M'), "price": price
 
81
  })
82
+ return pd.DataFrame(flight_options).sort_values(by="price").reset_index(drop=True)
83
+
84
+ def get_dashboard_ai_analysis(df, origin, destination, date_str):
85
+ if not GEMINI_API_KEY or df.empty:
86
+ return "AI analysis could not be performed due to a configuration issue or lack of data."
87
+ cheapest_flight = df.iloc[0]
88
+ 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}."""
89
+ 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."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  try:
91
  model = genai.GenerativeModel('gemini-1.5-flash-latest')
92
  response = model.generate_content(prompt)
93
+ report_header = f"Based on the data for **{origin} to {destination}** on **{date_str}**, here are the key insights:"
94
  return f"{report_header}\n\n{response.text}"
95
  except Exception as e:
96
+ return f"An error occurred during AI analysis: {e}"
97
+
98
+ def generate_mock_heatmap_data():
99
+ airports = list(AUSTRALIAN_CITY_CODES.keys())
100
+ routes = []
101
+ for _ in range(10):
102
+ from_city, to_city = random.sample(airports, 2)
103
+ price = random.randint(70, 400)
104
+ status = 'green' if price < 150 else 'yellow' if price < 280 else 'red'
105
+ routes.append({"from": from_city, "to": to_city, "status": status, "price": f"${price}"})
106
+ return routes
107
+
108
+ def generate_mock_topmovers_data():
109
+ routes = ["SYD → MEL", "BNE → CNS", "MEL → ADL", "SYD → CBR", "SYD → OOL"]
110
+ cities = ["Gold Coast (OOL)", "Cairns (CNS)", "Sydney (SYD)"]
111
+ 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)]
112
+ high_demand = [{"city": c, "flights": random.randint(30, 120), "change": f"+{random.randint(8, 25)}%"} for c in cities]
113
+ return {"priceDrops": price_drops, "highDemand": high_demand}
114
+
115
+ @app.post("/api/newsletter")
116
+ def handle_newsletter(payload: NewsletterPayload):
117
+ success, message = add_subscriber_to_sheet(
118
+ email=payload.email,
119
+ fav_destinations=payload.favDestinations,
120
+ origin=payload.travelOrigin,
121
+ dob=payload.dob
122
+ )
123
+ if not success:
124
+ raise HTTPException(status_code=400, detail=message)
125
+ return {"message": message}
126
+
127
+ @app.get("/api/dashboard-data")
128
+ def get_dashboard_data(origin: str, destination: str, date: str):
129
  origin_code = AUSTRALIAN_CITY_CODES.get(origin)
130
  dest_code = AUSTRALIAN_CITY_CODES.get(destination)
 
131
  if not origin_code or not dest_code:
132
+ raise HTTPException(status_code=400, detail="Invalid city name provided.")
 
133
  raw_data = fetch_flight_data(FLIGHT_API_KEY, origin_code, dest_code, date)
134
+ flight_df = parse_and_process_data(raw_data)
 
 
 
135
  if flight_df.empty:
136
+ 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.")
137
+ ai_report = get_dashboard_ai_analysis(flight_df, origin, destination, date)
 
 
138
  cheapest_flight_row = flight_df.loc[flight_df['price'].idxmin()]
139
+ dashboard_payload = {
140
+ "airfareHeatmap": generate_mock_heatmap_data(),
141
+ "topMovers": generate_mock_topmovers_data(),
142
+ "insightCards": {
143
+ "cheapestFlight": {"price": cheapest_flight_row['price'], "airline": cheapest_flight_row['airline'], "flightNumber": cheapest_flight_row['flight']},
144
+ "busiestAirline": {"name": flight_df['airline'].mode()[0], "flightCount": len(flight_df)},
145
+ "bestDeal": {"name": cheapest_flight_row['airline'], "savings": "Top Value"}
 
 
146
  },
 
 
 
 
 
 
 
 
 
 
 
147
  "aiAnalystReport": ai_report,
148
+ "flightPriceChart": flight_df.groupby('airline')['price'].min().reset_index().rename(columns={'airline': 'name'}).to_dict('records'),
149
  "flightDataTable": flight_df.to_dict('records')
150
+ }
151
+ return dashboard_payload
152
+
153
+ @app.post("/api/chat")
154
+ def handle_chat(payload: ChatPayload):
155
+ user_message = payload.message
156
+ if not GEMINI_API_KEY:
157
+ return {"reply": "Chatbot is disabled. Backend needs a Gemini API key."}
158
+ try:
159
+ model = genai.GenerativeModel('gemini-1.5-flash-latest')
160
+ 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}'"
161
+ response = model.generate_content(intent_prompt)
162
+ json_str = response.text.strip().replace("```json", "").replace("```", "")
163
+ params = json.loads(json_str)
164
+ if not all(params.get(k) for k in ['origin', 'destination', 'date']):
165
+ 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."}
166
+ origin_code = next((code for name, code in AUSTRALIAN_CITY_CODES.items() if name.lower() in params['origin'].lower()), None)
167
+ dest_code = next((code for name, code in AUSTRALIAN_CITY_CODES.items() if name.lower() in params['destination'].lower()), None)
168
+ if not origin_code or not dest_code:
169
+ return {"reply": "Sorry, I couldn't recognise those city names. Please use major Australian cities."}
170
+ raw_data = fetch_flight_data(FLIGHT_API_KEY, origin_code, dest_code, params['date'])
171
+ flight_df = parse_and_process_data(raw_data)
172
+ if flight_df.empty:
173
+ return {"reply": f"I couldn't find any flights from {params['origin']} to {params['destination']} on {params['date']}."}
174
+ 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()}"
175
+ summary_response = model.generate_content(summary_prompt)
176
+ return {"reply": summary_response.text}
177
+ except Exception as e:
178
+ return {"reply": f"I had a little trouble with that. My apologies. Error: {e}"}
requirements.txt CHANGED
@@ -3,4 +3,6 @@ uvicorn[standard]
3
  python-dotenv
4
  requests
5
  pandas
6
- google-generativeai
 
 
 
3
  python-dotenv
4
  requests
5
  pandas
6
+ google-generativeai
7
+ gspread
8
+ oauth2client
sheets_client.py ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gspread
2
+ from oauth2client.service_account import ServiceAccountCredentials
3
+ import os
4
+ from datetime import datetime
5
+
6
+ SCOPE = [
7
+ "https://www.googleapis.com/auth/spreadsheets",
8
+ "https://www.googleapis.com/auth/drive.file"
9
+ ]
10
+
11
+ def get_sheet():
12
+ try:
13
+ # We'll use the content of the credentials file passed as a secret
14
+ creds_json = os.getenv("GOOGLE_CREDENTIALS_JSON")
15
+ if not creds_json:
16
+ print("Error: GOOGLE_CREDENTIALS_JSON secret not set.")
17
+ return None
18
+
19
+ creds_dict = json.loads(creds_json)
20
+ creds = ServiceAccountCredentials.from_json_keyfile_dict(creds_dict, SCOPE)
21
+ client = gspread.authorize(creds)
22
+ sheet_id = os.getenv("GOOGLE_SHEET_ID")
23
+ if not sheet_id:
24
+ print("Error: GOOGLE_SHEET_ID secret not set.")
25
+ return None
26
+ sheet = client.open_by_key(sheet_id).sheet1
27
+ return sheet
28
+ except Exception as e:
29
+ print(f"Error connecting to Google Sheets: {e}")
30
+ return None
31
+
32
+ def add_subscriber_to_sheet(email: str, fav_destinations: list, origin: str, dob: str):
33
+ sheet = get_sheet()
34
+ if sheet is None:
35
+ return False, "Could not connect to the subscribers sheet."
36
+
37
+ try:
38
+ all_values = sheet.get_all_values()
39
+ if not all_values or all_values[0] != ["timestamp", "email", "favorite_destinations", "usual_origin", "dob"]:
40
+ sheet.insert_row(["timestamp", "email", "favorite_destinations", "usual_origin", "dob"], 1)
41
+
42
+ timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
43
+ destinations_str = ", ".join(fav_destinations)
44
+
45
+ email_list = sheet.col_values(2)
46
+ if email in email_list:
47
+ return False, "This email is already subscribed."
48
+
49
+ sheet.append_row([timestamp, email, destinations_str, origin, dob])
50
+ return True, "Successfully subscribed!"
51
+ except Exception as e:
52
+ return False, f"An error occurred: {e}"