Spaces:
Sleeping
Sleeping
| """ | |
| SkyGuardian AI - Real-Time Flight Price Intelligence Engine | |
| ============================================================ | |
| Production-ready flight price monitoring and optimization system. | |
| Architecture: | |
| - Modular provider-agnostic design | |
| - Real API integration (RapidAPI Skyscanner, AviationStack, Kiwi) | |
| - SQLite historical tracking | |
| - Surge detection and volatility analysis | |
| - Nearby airport arbitrage | |
| - Emergency booking mode | |
| - Gradio UI for HuggingFace Spaces deployment | |
| """ | |
| import os | |
| import sqlite3 | |
| import requests | |
| import json | |
| import math | |
| from datetime import datetime, timedelta | |
| from typing import Dict, List, Tuple, Optional | |
| from dataclasses import dataclass | |
| import gradio as gr | |
| # ========================================================= | |
| # DATA MODELS | |
| # ========================================================= | |
| class Airport: | |
| """Airport data structure""" | |
| code: str | |
| name: str | |
| city: str | |
| country: str | |
| lat: float | |
| lon: float | |
| class PriceData: | |
| """Flight price data structure""" | |
| origin: str | |
| destination: str | |
| price: float | |
| currency: str | |
| departure_date: str | |
| carrier: Optional[str] = None | |
| route_type: str = "direct" | |
| class AnalysisResult: | |
| """Analysis result structure""" | |
| current_price: float | |
| historical_avg: float | |
| surge_index: float | |
| volatility: float | |
| drop_probability: float | |
| nearby_savings: Optional[Dict] = None | |
| recommendation: str = "" | |
| explanation: str = "" | |
| # ========================================================= | |
| # AIRPORT INTELLIGENCE AGENT | |
| # ========================================================= | |
| class AirportAgent: | |
| """Manages airport data and geographic calculations""" | |
| def __init__(self): | |
| self.airports = self._load_airports() | |
| def _load_airports(self) -> Dict[str, Airport]: | |
| """Load airports for Singapore, India, Malaysia, Thailand, Sri Lanka""" | |
| airports_data = { | |
| # Singapore | |
| "SIN": Airport("SIN", "Singapore Changi", "Singapore", "Singapore", 1.3644, 103.9915), | |
| # India - Major Cities | |
| "DEL": Airport("DEL", "Indira Gandhi Intl", "New Delhi", "India", 28.5665, 77.1031), | |
| "BOM": Airport("BOM", "Chhatrapati Shivaji Intl", "Mumbai", "India", 19.0896, 72.8656), | |
| "BLR": Airport("BLR", "Kempegowda Intl", "Bangalore", "India", 13.1986, 77.7066), | |
| "MAA": Airport("MAA", "Chennai Intl", "Chennai", "India", 12.9941, 80.1709), | |
| "HYD": Airport("HYD", "Rajiv Gandhi Intl", "Hyderabad", "India", 17.2403, 78.4294), | |
| "CCU": Airport("CCU", "Netaji Subhas Chandra Bose Intl", "Kolkata", "India", 22.6547, 88.4467), | |
| "GOI": Airport("GOI", "Dabolim Airport", "Goa", "India", 15.3808, 73.8314), | |
| "COK": Airport("COK", "Cochin Intl", "Kochi", "India", 10.1520, 76.4019), | |
| # Malaysia | |
| "KUL": Airport("KUL", "Kuala Lumpur Intl", "Kuala Lumpur", "Malaysia", 2.7456, 101.7072), | |
| "PEN": Airport("PEN", "Penang Intl", "Penang", "Malaysia", 5.2971, 100.2769), | |
| "JHB": Airport("JHB", "Senai Intl", "Johor Bahru", "Malaysia", 1.6411, 103.6697), | |
| # Thailand | |
| "BKK": Airport("BKK", "Suvarnabhumi", "Bangkok", "Thailand", 13.6900, 100.7501), | |
| "DMK": Airport("DMK", "Don Mueang Intl", "Bangkok", "Thailand", 13.9126, 100.6067), | |
| "HKT": Airport("HKT", "Phuket Intl", "Phuket", "Thailand", 8.1132, 98.3169), | |
| "CNX": Airport("CNX", "Chiang Mai Intl", "Chiang Mai", "Thailand", 18.7668, 98.9628), | |
| # Sri Lanka | |
| "CMB": Airport("CMB", "Bandaranaike Intl", "Colombo", "Sri Lanka", 7.1808, 79.8841), | |
| } | |
| return airports_data | |
| def get_airport_list(self) -> List[Tuple[str, str]]: | |
| """Get formatted airport list for dropdown""" | |
| return [(f"{apt.code} - {apt.city}, {apt.country}", apt.code) | |
| for apt in sorted(self.airports.values(), key=lambda x: x.city)] | |
| def haversine_distance(self, lat1: float, lon1: float, lat2: float, lon2: float) -> float: | |
| """Calculate distance between two points in kilometers""" | |
| R = 6371 # Earth's radius in km | |
| lat1_rad = math.radians(lat1) | |
| lat2_rad = math.radians(lat2) | |
| delta_lat = math.radians(lat2 - lat1) | |
| delta_lon = math.radians(lon2 - lon1) | |
| a = math.sin(delta_lat/2)**2 + math.cos(lat1_rad) * math.cos(lat2_rad) * math.sin(delta_lon/2)**2 | |
| c = 2 * math.asin(math.sqrt(a)) | |
| return R * c | |
| def find_nearby_airports(self, airport_code: str, radius_km: int = 300) -> List[str]: | |
| """Find airports within specified radius""" | |
| if airport_code not in self.airports: | |
| return [] | |
| origin = self.airports[airport_code] | |
| nearby = [] | |
| for code, apt in self.airports.items(): | |
| if code != airport_code: | |
| distance = self.haversine_distance(origin.lat, origin.lon, apt.lat, apt.lon) | |
| if distance <= radius_km: | |
| nearby.append(code) | |
| return nearby | |
| # ========================================================= | |
| # PRICE PROVIDER INTERFACE | |
| # ========================================================= | |
| class PriceProvider: | |
| """Abstract base class for price providers""" | |
| def get_prices(self, origin: str, destination: str, departure_date: str, **kwargs) -> Optional[PriceData]: | |
| """Fetch flight prices - to be implemented by subclasses""" | |
| raise NotImplementedError | |
| class SkyscannerProvider(PriceProvider): | |
| """Skyscanner API via RapidAPI""" | |
| def __init__(self, api_key: str): | |
| self.api_key = api_key | |
| self.base_url = "https://skyscanner-api.p.rapidapi.com/v3" | |
| def get_prices(self, origin: str, destination: str, departure_date: str, **kwargs) -> Optional[PriceData]: | |
| """Fetch prices from Skyscanner""" | |
| try: | |
| headers = { | |
| "X-RapidAPI-Key": self.api_key, | |
| "X-RapidAPI-Host": "skyscanner-api.p.rapidapi.com" | |
| } | |
| # Search for flights | |
| url = f"{self.base_url}/flights/live/search/create" | |
| payload = { | |
| "query": { | |
| "market": "SG", | |
| "locale": "en-GB", | |
| "currency": "USD", | |
| "queryLegs": [{ | |
| "originPlaceId": {"iata": origin}, | |
| "destinationPlaceId": {"iata": destination}, | |
| "date": {"year": int(departure_date[:4]), | |
| "month": int(departure_date[5:7]), | |
| "day": int(departure_date[8:10])} | |
| }], | |
| "adults": 1, | |
| "cabinClass": "CABIN_CLASS_ECONOMY" | |
| } | |
| } | |
| response = requests.post(url, json=payload, headers=headers, timeout=10) | |
| if response.status_code == 200: | |
| data = response.json() | |
| # Extract cheapest price from response | |
| if "content" in data and "results" in data["content"]: | |
| results = data["content"]["results"] | |
| if "itineraries" in results and results["itineraries"]: | |
| cheapest = min(results["itineraries"].values(), | |
| key=lambda x: x["pricingOptions"][0]["price"]["amount"]) | |
| price_info = cheapest["pricingOptions"][0]["price"] | |
| return PriceData( | |
| origin=origin, | |
| destination=destination, | |
| price=price_info["amount"] / 1000, # Convert from smallest unit | |
| currency=price_info.get("unit", "USD"), | |
| departure_date=departure_date, | |
| carrier=cheapest["legs"][0]["carriers"]["marketing"][0]["name"] if cheapest["legs"][0]["carriers"]["marketing"] else None | |
| ) | |
| return None | |
| except Exception as e: | |
| print(f"Skyscanner API error: {str(e)}") | |
| return None | |
| class AviationStackProvider(PriceProvider): | |
| """AviationStack free API (flight data, not pricing - fallback)""" | |
| def __init__(self, api_key: str): | |
| self.api_key = api_key | |
| self.base_url = "http://api.aviationstack.com/v1" | |
| def get_prices(self, origin: str, destination: str, departure_date: str, **kwargs) -> Optional[PriceData]: | |
| """Note: AviationStack doesn't provide pricing, returns estimated baseline""" | |
| try: | |
| # Check if route exists | |
| params = { | |
| "access_key": self.api_key, | |
| "dep_iata": origin, | |
| "arr_iata": destination | |
| } | |
| response = requests.get(f"{self.base_url}/routes", params=params, timeout=10) | |
| if response.status_code == 200: | |
| data = response.json() | |
| if data.get("data"): | |
| # Return baseline estimated price (not real-time pricing) | |
| return PriceData( | |
| origin=origin, | |
| destination=destination, | |
| price=self._estimate_baseline_price(origin, destination), | |
| currency="USD", | |
| departure_date=departure_date, | |
| route_type="estimated" | |
| ) | |
| return None | |
| except Exception as e: | |
| print(f"AviationStack API error: {str(e)}") | |
| return None | |
| def _estimate_baseline_price(self, origin: str, destination: str) -> float: | |
| """Rough baseline estimate based on distance (fallback only)""" | |
| # This is a fallback estimation - real providers should be used | |
| airport_agent = AirportAgent() | |
| airports = airport_agent.airports | |
| if origin in airports and destination in airports: | |
| dist = airport_agent.haversine_distance( | |
| airports[origin].lat, airports[origin].lon, | |
| airports[destination].lat, airports[destination].lon | |
| ) | |
| # Rough estimate: $0.10-0.15 per km | |
| return round(dist * 0.12, 2) | |
| return 300.0 # Default fallback | |
| class KiwiProvider(PriceProvider): | |
| """Kiwi.com Tequila API""" | |
| def __init__(self, api_key: str): | |
| self.api_key = api_key | |
| self.base_url = "https://api.tequila.kiwi.com" | |
| def get_prices(self, origin: str, destination: str, departure_date: str, **kwargs) -> Optional[PriceData]: | |
| """Fetch prices from Kiwi""" | |
| try: | |
| headers = {"apikey": self.api_key} | |
| params = { | |
| "fly_from": origin, | |
| "fly_to": destination, | |
| "date_from": departure_date, | |
| "date_to": departure_date, | |
| "adults": 1, | |
| "curr": "USD", | |
| "limit": 1 | |
| } | |
| response = requests.get(f"{self.base_url}/v2/search", | |
| headers=headers, params=params, timeout=10) | |
| if response.status_code == 200: | |
| data = response.json() | |
| if data.get("data"): | |
| flight = data["data"][0] | |
| return PriceData( | |
| origin=origin, | |
| destination=destination, | |
| price=flight["price"], | |
| currency=flight.get("currency", "USD"), | |
| departure_date=departure_date, | |
| carrier=flight["airlines"][0] if flight.get("airlines") else None | |
| ) | |
| return None | |
| except Exception as e: | |
| print(f"Kiwi API error: {str(e)}") | |
| return None | |
| # ========================================================= | |
| # PRICE LOGGER | |
| # ========================================================= | |
| class PriceLogger: | |
| """SQLite-based historical price logging""" | |
| def __init__(self, db_path: str = "flight_prices.db"): | |
| self.db_path = db_path | |
| self._init_database() | |
| def _init_database(self): | |
| """Initialize SQLite database""" | |
| conn = sqlite3.connect(self.db_path) | |
| cursor = conn.cursor() | |
| cursor.execute(""" | |
| CREATE TABLE IF NOT EXISTS price_history ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| origin TEXT NOT NULL, | |
| destination TEXT NOT NULL, | |
| departure_date TEXT NOT NULL, | |
| timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, | |
| price REAL NOT NULL, | |
| currency TEXT DEFAULT 'USD', | |
| carrier TEXT, | |
| route_type TEXT DEFAULT 'direct' | |
| ) | |
| """) | |
| cursor.execute(""" | |
| CREATE INDEX IF NOT EXISTS idx_route | |
| ON price_history(origin, destination, departure_date) | |
| """) | |
| conn.commit() | |
| conn.close() | |
| def log_price(self, price_data: PriceData): | |
| """Log a price query""" | |
| conn = sqlite3.connect(self.db_path) | |
| cursor = conn.cursor() | |
| cursor.execute(""" | |
| INSERT INTO price_history | |
| (origin, destination, departure_date, price, currency, carrier, route_type) | |
| VALUES (?, ?, ?, ?, ?, ?, ?) | |
| """, ( | |
| price_data.origin, | |
| price_data.destination, | |
| price_data.departure_date, | |
| price_data.price, | |
| price_data.currency, | |
| price_data.carrier, | |
| price_data.route_type | |
| )) | |
| conn.commit() | |
| conn.close() | |
| def get_historical_prices(self, origin: str, destination: str, | |
| departure_date: str, days_lookback: int = 30) -> List[Tuple]: | |
| """Retrieve historical prices for route""" | |
| conn = sqlite3.connect(self.db_path) | |
| cursor = conn.cursor() | |
| lookback_date = (datetime.now() - timedelta(days=days_lookback)).strftime("%Y-%m-%d %H:%M:%S") | |
| cursor.execute(""" | |
| SELECT price, timestamp | |
| FROM price_history | |
| WHERE origin = ? AND destination = ? | |
| AND departure_date = ? | |
| AND timestamp >= ? | |
| ORDER BY timestamp DESC | |
| """, (origin, destination, departure_date, lookback_date)) | |
| results = cursor.fetchall() | |
| conn.close() | |
| return results | |
| # ========================================================= | |
| # SURGE ANALYZER | |
| # ========================================================= | |
| class SurgeAnalyzer: | |
| """Analyzes price surges and volatility""" | |
| def analyze(self, current_price: float, historical_prices: List[Tuple]) -> Dict: | |
| """Analyze surge behavior""" | |
| if not historical_prices: | |
| return { | |
| "surge_index": 0.0, | |
| "volatility": 0.0, | |
| "historical_avg": current_price, | |
| "is_surge": False | |
| } | |
| prices = [p[0] for p in historical_prices] | |
| # Calculate statistics | |
| avg_price = sum(prices) / len(prices) | |
| variance = sum((p - avg_price) ** 2 for p in prices) / len(prices) | |
| std_dev = math.sqrt(variance) | |
| # Surge index: how many standard deviations above mean | |
| surge_index = (current_price - avg_price) / std_dev if std_dev > 0 else 0.0 | |
| # Volatility: coefficient of variation | |
| volatility = (std_dev / avg_price) if avg_price > 0 else 0.0 | |
| # Detect surge (>1.5 standard deviations above mean) | |
| is_surge = surge_index > 1.5 | |
| return { | |
| "surge_index": round(surge_index, 2), | |
| "volatility": round(volatility, 3), | |
| "historical_avg": round(avg_price, 2), | |
| "is_surge": is_surge, | |
| "std_dev": round(std_dev, 2) | |
| } | |
| def estimate_drop_probability(self, surge_index: float, volatility: float, | |
| days_to_departure: int) -> float: | |
| """Estimate probability of price drop (deterministic logic)""" | |
| # Base probability on surge level | |
| if surge_index < 0.5: | |
| base_prob = 0.2 # Already low | |
| elif surge_index < 1.5: | |
| base_prob = 0.4 # Moderate | |
| else: | |
| base_prob = 0.6 # High surge | |
| # Adjust for volatility (higher volatility = more likely to drop) | |
| volatility_factor = min(volatility * 0.3, 0.25) | |
| # Adjust for days to departure (closer = less likely to drop significantly) | |
| if days_to_departure < 3: | |
| time_penalty = -0.3 | |
| elif days_to_departure < 7: | |
| time_penalty = -0.15 | |
| elif days_to_departure < 14: | |
| time_penalty = 0.0 | |
| else: | |
| time_penalty = 0.1 | |
| probability = base_prob + volatility_factor + time_penalty | |
| return max(0.0, min(1.0, probability)) # Clamp between 0 and 1 | |
| # ========================================================= | |
| # ARBITRAGE AGENT | |
| # ========================================================= | |
| class ArbitrageAgent: | |
| """Detects nearby airport savings opportunities""" | |
| def __init__(self, airport_agent: AirportAgent, price_provider: PriceProvider): | |
| self.airport_agent = airport_agent | |
| self.price_provider = price_provider | |
| def find_arbitrage(self, destination: str, departure_date: str, | |
| current_price: float, origin: str) -> Optional[Dict]: | |
| """Find cheaper nearby airports""" | |
| nearby = self.airport_agent.find_nearby_airports(destination, radius_km=300) | |
| if not nearby: | |
| return None | |
| best_saving = None | |
| for nearby_code in nearby[:3]: # Check top 3 nearest | |
| try: | |
| nearby_price_data = self.price_provider.get_prices( | |
| origin, nearby_code, departure_date | |
| ) | |
| if nearby_price_data and nearby_price_data.price < current_price: | |
| savings = current_price - nearby_price_data.price | |
| savings_pct = (savings / current_price) * 100 | |
| if not best_saving or savings > best_saving["savings"]: | |
| airport_info = self.airport_agent.airports.get(nearby_code) | |
| best_saving = { | |
| "airport_code": nearby_code, | |
| "airport_name": airport_info.name if airport_info else nearby_code, | |
| "city": airport_info.city if airport_info else "Unknown", | |
| "price": nearby_price_data.price, | |
| "savings": round(savings, 2), | |
| "savings_pct": round(savings_pct, 1) | |
| } | |
| except Exception as e: | |
| print(f"Error checking nearby airport {nearby_code}: {str(e)}") | |
| continue | |
| return best_saving | |
| # ========================================================= | |
| # DECISION ENGINE | |
| # ========================================================= | |
| class DecisionEngine: | |
| """Makes booking recommendations based on analysis""" | |
| def decide(self, analysis: AnalysisResult, days_to_departure: int, | |
| emergency_mode: bool = False) -> Tuple[str, str]: | |
| """Generate recommendation and explanation""" | |
| if emergency_mode: | |
| return self._emergency_decision(analysis) | |
| # Decision logic | |
| if analysis.surge_index > 2.0 and analysis.drop_probability > 0.5 and days_to_departure > 7: | |
| recommendation = "⏳ WAIT 24-48H" | |
| explanation = f""" | |
| **High surge detected** (Surge Index: {analysis.surge_index}) | |
| Current price is {analysis.surge_index:.1f} standard deviations above historical average. | |
| Drop probability: {analysis.drop_probability*100:.0f}% | |
| **Recommendation:** Wait 24-48 hours. Price likely to normalize. | |
| Check again before {days_to_departure - 2} days to departure. | |
| """ | |
| elif analysis.nearby_savings and analysis.nearby_savings["savings_pct"] > 20: | |
| recommendation = "🛫 TRY NEARBY AIRPORT" | |
| nb = analysis.nearby_savings | |
| explanation = f""" | |
| **Significant arbitrage opportunity detected** | |
| Fly to {nb['city']} ({nb['airport_code']}) instead: ${nb['price']:.2f} | |
| **Save ${nb['savings']:.2f} ({nb['savings_pct']:.0f}%)** | |
| Distance: ~{self._estimate_distance(nb)}km from destination | |
| Consider: Ground transport cost + time vs savings | |
| """ | |
| elif analysis.surge_index < 0.5: | |
| recommendation = "✅ BOOK NOW" | |
| explanation = f""" | |
| **Excellent price detected** | |
| Current price: ${analysis.current_price:.2f} | |
| Below historical average: ${analysis.historical_avg:.2f} | |
| **Recommendation:** Book immediately. Price is at historical low. | |
| Volatility: {analysis.volatility:.1%} - stable pricing | |
| """ | |
| elif days_to_departure <= 3: | |
| recommendation = "⚠️ BOOK NOW (URGENT)" | |
| explanation = f""" | |
| **Time-sensitive booking** | |
| {days_to_departure} days to departure - prices typically spike closer to date. | |
| Current price: ${analysis.current_price:.2f} | |
| Waiting risk: High | |
| **Recommendation:** Book now to avoid last-minute surge. | |
| """ | |
| elif analysis.surge_index > 1.5 and analysis.volatility > 0.15: | |
| recommendation = "📊 MONITOR CLOSELY" | |
| explanation = f""" | |
| **Moderate surge with high volatility** | |
| Surge Index: {analysis.surge_index} | |
| Volatility: {analysis.volatility:.1%} | |
| Drop probability: {analysis.drop_probability*100:.0f}% | |
| **Recommendation:** Check back in 12-24 hours. | |
| Set price alert if available. | |
| """ | |
| else: | |
| recommendation = "✅ BOOK NOW" | |
| explanation = f""" | |
| **Fair market price** | |
| Current price: ${analysis.current_price:.2f} | |
| Historical average: ${analysis.historical_avg:.2f} | |
| Surge Index: {analysis.surge_index} | |
| **Recommendation:** Price is reasonable. Book when ready. | |
| """ | |
| return recommendation, explanation.strip() | |
| def _emergency_decision(self, analysis: AnalysisResult) -> Tuple[str, str]: | |
| """Emergency mode - always recommend booking""" | |
| if analysis.nearby_savings and analysis.nearby_savings["savings_pct"] > 15: | |
| nb = analysis.nearby_savings | |
| recommendation = "🛫 BOOK NEARBY AIRPORT" | |
| explanation = f""" | |
| **EMERGENCY MODE: Best available option** | |
| Cheapest confirmed option: {nb['city']} ({nb['airport_code']}) | |
| Price: ${nb['price']:.2f} (Save ${nb['savings']:.2f}) | |
| Book immediately. Arrange ground transport separately. | |
| """ | |
| else: | |
| recommendation = "✅ BOOK NOW" | |
| explanation = f""" | |
| **EMERGENCY MODE: Book immediately** | |
| Direct flight price: ${analysis.current_price:.2f} | |
| No time for price monitoring. | |
| This is your confirmed option - book now. | |
| """ | |
| return recommendation, explanation.strip() | |
| def _estimate_distance(self, nearby_savings: Dict) -> int: | |
| """Estimate distance to nearby airport""" | |
| return 150 # Placeholder - could calculate actual distance | |
| # ========================================================= | |
| # ORCHESTRATOR | |
| # ========================================================= | |
| class Orchestrator: | |
| """Main orchestration engine""" | |
| def __init__(self, api_key: str = None, provider_type: str = "skyscanner"): | |
| self.airport_agent = AirportAgent() | |
| self.price_logger = PriceLogger() | |
| self.surge_analyzer = SurgeAnalyzer() | |
| # Initialize price provider | |
| if api_key: | |
| if provider_type == "skyscanner": | |
| self.price_provider = SkyscannerProvider(api_key) | |
| elif provider_type == "kiwi": | |
| self.price_provider = KiwiProvider(api_key) | |
| elif provider_type == "aviationstack": | |
| self.price_provider = AviationStackProvider(api_key) | |
| else: | |
| self.price_provider = AviationStackProvider(api_key) | |
| else: | |
| # Fallback provider for demo | |
| self.price_provider = AviationStackProvider("demo_key") | |
| self.arbitrage_agent = ArbitrageAgent(self.airport_agent, self.price_provider) | |
| self.decision_engine = DecisionEngine() | |
| def analyze_flight(self, origin: str, destination: str, departure_date: str, | |
| emergency_mode: bool = False) -> Dict: | |
| """Main analysis pipeline""" | |
| # Validation | |
| if origin not in self.airport_agent.airports: | |
| return {"error": f"Unknown origin airport: {origin}"} | |
| if destination not in self.airport_agent.airports: | |
| return {"error": f"Unknown destination airport: {destination}"} | |
| try: | |
| departure_dt = datetime.strptime(departure_date, "%Y-%m-%d") | |
| days_to_departure = (departure_dt - datetime.now()).days | |
| if days_to_departure < 0: | |
| return {"error": "Departure date is in the past"} | |
| except ValueError: | |
| return {"error": "Invalid date format. Use YYYY-MM-DD"} | |
| # Fetch current price | |
| price_data = self.price_provider.get_prices(origin, destination, departure_date) | |
| if not price_data: | |
| return { | |
| "error": "Unable to fetch price data. Please check API key or try again later.", | |
| "suggestion": "Verify RapidAPI key for Skyscanner or use alternative provider." | |
| } | |
| # Log price | |
| self.price_logger.log_price(price_data) | |
| # Get historical data | |
| historical = self.price_logger.get_historical_prices( | |
| origin, destination, departure_date, days_lookback=30 | |
| ) | |
| # Analyze surge | |
| surge_analysis = self.surge_analyzer.analyze(price_data.price, historical) | |
| # Estimate drop probability | |
| drop_prob = self.surge_analyzer.estimate_drop_probability( | |
| surge_analysis["surge_index"], | |
| surge_analysis["volatility"], | |
| days_to_departure | |
| ) | |
| # Check arbitrage (unless emergency mode) | |
| nearby_savings = None | |
| if not emergency_mode: | |
| nearby_savings = self.arbitrage_agent.find_arbitrage( | |
| destination, departure_date, price_data.price, origin | |
| ) | |
| # Create analysis result | |
| analysis_result = AnalysisResult( | |
| current_price=price_data.price, | |
| historical_avg=surge_analysis["historical_avg"], | |
| surge_index=surge_analysis["surge_index"], | |
| volatility=surge_analysis["volatility"], | |
| drop_probability=drop_prob, | |
| nearby_savings=nearby_savings | |
| ) | |
| # Generate decision | |
| recommendation, explanation = self.decision_engine.decide( | |
| analysis_result, days_to_departure, emergency_mode | |
| ) | |
| # Return comprehensive result | |
| return { | |
| "success": True, | |
| "origin": f"{self.airport_agent.airports[origin].city} ({origin})", | |
| "destination": f"{self.airport_agent.airports[destination].city} ({destination})", | |
| "departure_date": departure_date, | |
| "days_to_departure": days_to_departure, | |
| "current_price": f"${price_data.price:.2f}", | |
| "currency": price_data.currency, | |
| "carrier": price_data.carrier or "Multiple carriers", | |
| "historical_avg": f"${analysis_result.historical_avg:.2f}", | |
| "surge_index": f"{analysis_result.surge_index:.2f}", | |
| "volatility": f"{analysis_result.volatility:.1%}", | |
| "drop_probability": f"{analysis_result.drop_probability*100:.0f}%", | |
| "nearby_savings": self._format_nearby_savings(nearby_savings), | |
| "recommendation": recommendation, | |
| "explanation": explanation, | |
| "data_points": len(historical), | |
| "emergency_mode": emergency_mode | |
| } | |
| def _format_nearby_savings(self, nearby_savings: Optional[Dict]) -> str: | |
| """Format nearby savings for display""" | |
| if not nearby_savings: | |
| return "None detected" | |
| return f"{nearby_savings['city']} ({nearby_savings['airport_code']}): ${nearby_savings['price']:.2f} - Save ${nearby_savings['savings']:.2f} ({nearby_savings['savings_pct']:.0f}%)" | |
| # ========================================================= | |
| # GRADIO UI | |
| # ========================================================= | |
| def create_ui(): | |
| """Create Gradio interface""" | |
| # Global orchestrator (will be reinitialized with API key) | |
| orchestrator = None | |
| def analyze_flight_wrapper(origin, destination, departure_date, emergency_mode, api_key, provider_type): | |
| """Wrapper for Gradio interface""" | |
| nonlocal orchestrator | |
| # Initialize orchestrator with API key | |
| if api_key and api_key.strip(): | |
| orchestrator = Orchestrator(api_key=api_key.strip(), provider_type=provider_type) | |
| else: | |
| orchestrator = Orchestrator() # Use fallback | |
| result = orchestrator.analyze_flight(origin, destination, departure_date, emergency_mode) | |
| if "error" in result: | |
| error_msg = result["error"] | |
| if "suggestion" in result: | |
| error_msg += f"\n\n💡 {result['suggestion']}" | |
| return ( | |
| "❌ Error", | |
| error_msg, | |
| "", "", "", "", "", "", "" | |
| ) | |
| # Format output | |
| route_info = f"**{result['origin']}** → **{result['destination']}**\n" | |
| route_info += f"Departure: {result['departure_date']} ({result['days_to_departure']} days)\n" | |
| route_info += f"Carrier: {result['carrier']}" | |
| price_info = f"💰 **${result['current_price']}** {result['currency']}" | |
| stats = f""" | |
| **Historical Average:** {result['historical_avg']} | |
| **Surge Index:** {result['surge_index']} | |
| **Volatility:** {result['volatility']} | |
| **Drop Probability:** {result['drop_probability']} | |
| **Data Points:** {result['data_points']} historical records | |
| """ | |
| return ( | |
| result['recommendation'], | |
| route_info, | |
| price_info, | |
| stats, | |
| result['nearby_savings'], | |
| result['explanation'], | |
| f"✅ Analysis complete. Emergency Mode: {'ON' if result['emergency_mode'] else 'OFF'}", | |
| "", | |
| "" | |
| ) | |
| # Build UI | |
| with gr.Blocks(title="SkyGuardian AI - Flight Price Intelligence", theme=gr.themes.Soft()) as app: | |
| gr.Markdown(""" | |
| # 🛫 SkyGuardian AI | |
| ## Real-Time Flight Price Intelligence Engine | |
| Monitor flight prices, detect surges, find arbitrage opportunities, and get AI-powered booking recommendations. | |
| """) | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| gr.Markdown("### Flight Details") | |
| # Get airport choices | |
| temp_agent = AirportAgent() | |
| airport_choices = temp_agent.get_airport_list() | |
| origin = gr.Dropdown( | |
| choices=airport_choices, | |
| label="Origin Airport", | |
| value="SIN", | |
| interactive=True | |
| ) | |
| destination = gr.Dropdown( | |
| choices=airport_choices, | |
| label="Destination Airport", | |
| value="BOM", | |
| interactive=True | |
| ) | |
| departure_date = gr.Textbox( | |
| label="Departure Date (YYYY-MM-DD)", | |
| value=(datetime.now() + timedelta(days=30)).strftime("%Y-%m-%d"), | |
| placeholder="2024-03-15" | |
| ) | |
| emergency_mode = gr.Checkbox( | |
| label="🚨 Emergency Mode (Book Immediately)", | |
| value=False | |
| ) | |
| gr.Markdown("### API Configuration") | |
| provider_type = gr.Radio( | |
| choices=["skyscanner", "kiwi", "aviationstack"], | |
| label="Price Provider", | |
| value="skyscanner", | |
| info="Skyscanner (RapidAPI) recommended for real pricing" | |
| ) | |
| api_key = gr.Textbox( | |
| label="API Key (RapidAPI for Skyscanner/Kiwi)", | |
| placeholder="Enter your RapidAPI key", | |
| type="password", | |
| info="Get free key at rapidapi.com" | |
| ) | |
| analyze_btn = gr.Button("🔍 Analyze Flight", variant="primary", size="lg") | |
| with gr.Column(scale=2): | |
| gr.Markdown("### Analysis Results") | |
| recommendation_box = gr.Textbox( | |
| label="AI Recommendation", | |
| lines=1, | |
| interactive=False | |
| ) | |
| route_box = gr.Markdown(label="Route Information") | |
| price_box = gr.Markdown(label="Current Price") | |
| stats_box = gr.Markdown(label="Price Statistics") | |
| nearby_box = gr.Textbox( | |
| label="Nearby Airport Savings", | |
| lines=2, | |
| interactive=False | |
| ) | |
| explanation_box = gr.Markdown(label="Detailed Explanation") | |
| status_box = gr.Textbox( | |
| label="Status", | |
| lines=1, | |
| interactive=False | |
| ) | |
| gr.Markdown(""" | |
| --- | |
| ### How It Works | |
| 1. **Real-Time Pricing**: Fetches current flight prices from live APIs | |
| 2. **Historical Tracking**: Logs all queries to build price history database | |
| 3. **Surge Detection**: Analyzes current price vs historical average | |
| 4. **Volatility Analysis**: Calculates price stability and drop probability | |
| 5. **Arbitrage Detection**: Finds cheaper nearby airports within 300km | |
| 6. **Smart Recommendations**: AI-powered booking strategy based on data | |
| ### Emergency Mode | |
| When enabled, system recommends **immediate booking** with best available option (no wait suggestions). | |
| ### API Setup | |
| - **Skyscanner**: Get free RapidAPI key at [rapidapi.com/skyscanner](https://rapidapi.com/skyscanner) | |
| - **Kiwi**: Sign up at [tequila.kiwi.com](https://tequila.kiwi.com) | |
| - **Fallback**: System uses estimation if no API key provided (demo only) | |
| """) | |
| # Connect components | |
| analyze_btn.click( | |
| fn=analyze_flight_wrapper, | |
| inputs=[origin, destination, departure_date, emergency_mode, api_key, provider_type], | |
| outputs=[ | |
| recommendation_box, | |
| route_box, | |
| price_box, | |
| stats_box, | |
| nearby_box, | |
| explanation_box, | |
| status_box, | |
| gr.Textbox(visible=False), # Hidden outputs | |
| gr.Textbox(visible=False) | |
| ] | |
| ) | |
| return app | |
| # ========================================================= | |
| # MAIN ENTRY POINT | |
| # ========================================================= | |
| if __name__ == "__main__": | |
| app = create_ui() | |
| app.launch(share=False) |