init commit
This commit is contained in:
0
mcp_servers/data/__init__.py
Normal file
0
mcp_servers/data/__init__.py
Normal file
0
mcp_servers/data/mock/__init__.py
Normal file
0
mcp_servers/data/mock/__init__.py
Normal file
137
mcp_servers/data/models.py
Normal file
137
mcp_servers/data/models.py
Normal file
@@ -0,0 +1,137 @@
|
||||
"""Pydantic models for all operational data."""
|
||||
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class FlightStatus(str, Enum):
|
||||
ON_TIME = "ON_TIME"
|
||||
DELAYED = "DELAYED"
|
||||
CANCELLED = "CANCELLED"
|
||||
DIVERTED = "DIVERTED"
|
||||
|
||||
|
||||
class DelayCause(str, Enum):
|
||||
WEATHER = "WEATHER"
|
||||
MAINTENANCE = "MAINTENANCE"
|
||||
CREW = "CREW"
|
||||
ATC = "ATC"
|
||||
LATE_AIRCRAFT = "LATE_AIRCRAFT"
|
||||
|
||||
|
||||
class CrewRole(str, Enum):
|
||||
CAPTAIN = "CAPTAIN"
|
||||
FIRST_OFFICER = "FIRST_OFFICER"
|
||||
FA = "FA"
|
||||
|
||||
|
||||
class MPStatus(str, Enum):
|
||||
GLOBAL_SERVICES = "GLOBAL_SERVICES"
|
||||
K1 = "1K"
|
||||
PLATINUM = "PLATINUM"
|
||||
GOLD = "GOLD"
|
||||
SILVER = "SILVER"
|
||||
GENERAL = "GENERAL"
|
||||
|
||||
|
||||
class FlightData(BaseModel):
|
||||
flight_id: str
|
||||
origin: str
|
||||
destination: str
|
||||
scheduled_departure: datetime
|
||||
actual_departure: datetime | None = None
|
||||
scheduled_arrival: datetime
|
||||
actual_arrival: datetime | None = None
|
||||
status: FlightStatus
|
||||
delay_minutes: int = 0
|
||||
delay_cause: DelayCause | None = None
|
||||
aircraft_tail: str
|
||||
gate: str
|
||||
inbound_flight: str | None = None
|
||||
crew_ids: list[str] = []
|
||||
passenger_count: int = 0
|
||||
|
||||
|
||||
class CrewMember(BaseModel):
|
||||
crew_id: str
|
||||
name: str
|
||||
role: CrewRole
|
||||
duty_hours_elapsed: float
|
||||
duty_hours_limit: float
|
||||
rest_hours_since_last: float
|
||||
next_scheduled_flight: str | None = None
|
||||
base_hub: str
|
||||
|
||||
|
||||
class Passenger(BaseModel):
|
||||
pax_id: str
|
||||
name: str
|
||||
mileage_plus_status: MPStatus
|
||||
flight_id: str
|
||||
destination: str
|
||||
connection_flight: str | None = None
|
||||
connection_deadline: datetime | None = None
|
||||
special_needs: list[str] = []
|
||||
|
||||
|
||||
class MELItem(BaseModel):
|
||||
mel_id: str
|
||||
aircraft_tail: str
|
||||
system: str
|
||||
description: str
|
||||
restriction: str | None = None
|
||||
expires: datetime
|
||||
|
||||
|
||||
class RebookingCase(BaseModel):
|
||||
pax_id: str
|
||||
name: str
|
||||
original_flight: str
|
||||
mileage_plus_status: MPStatus
|
||||
destination: str
|
||||
next_available: str | None = None
|
||||
urgency: str # HIGH | MEDIUM | LOW
|
||||
|
||||
|
||||
class HubInfo(BaseModel):
|
||||
code: str
|
||||
name: str
|
||||
city: str
|
||||
timezone: str
|
||||
latitude: float
|
||||
longitude: float
|
||||
terminals: int
|
||||
gates: int
|
||||
runways: int
|
||||
|
||||
|
||||
# Hub reference data
|
||||
HUBS: dict[str, HubInfo] = {
|
||||
"ORD": HubInfo(
|
||||
code="ORD", name="O'Hare International", city="Chicago",
|
||||
timezone="America/Chicago", latitude=41.9742, longitude=-87.9073,
|
||||
terminals=4, gates=191, runways=8,
|
||||
),
|
||||
"EWR": HubInfo(
|
||||
code="EWR", name="Newark Liberty International", city="Newark",
|
||||
timezone="America/New_York", latitude=40.6895, longitude=-74.1745,
|
||||
terminals=3, gates=120, runways=3,
|
||||
),
|
||||
"IAH": HubInfo(
|
||||
code="IAH", name="George Bush Intercontinental", city="Houston",
|
||||
timezone="America/Chicago", latitude=29.9902, longitude=-95.3368,
|
||||
terminals=5, gates=130, runways=5,
|
||||
),
|
||||
"SFO": HubInfo(
|
||||
code="SFO", name="San Francisco International", city="San Francisco",
|
||||
timezone="America/Los_Angeles", latitude=37.6213, longitude=-122.3790,
|
||||
terminals=4, gates=115, runways=4,
|
||||
),
|
||||
"DEN": HubInfo(
|
||||
code="DEN", name="Denver International", city="Denver",
|
||||
timezone="America/Denver", latitude=39.8561, longitude=-104.6737,
|
||||
terminals=1, gates=95, runways=6,
|
||||
),
|
||||
}
|
||||
0
mcp_servers/data/real/__init__.py
Normal file
0
mcp_servers/data/real/__init__.py
Normal file
124
mcp_servers/data/real/faa.py
Normal file
124
mcp_servers/data/real/faa.py
Normal file
@@ -0,0 +1,124 @@
|
||||
"""FAA NASSTATUS API client — live airport delay/status data, no API key required.
|
||||
|
||||
Endpoint: https://nasstatus.faa.gov/api/airport-status-information
|
||||
Returns XML with ground stops, ground delay programs, arrival/departure delays, closures.
|
||||
"""
|
||||
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
import httpx
|
||||
|
||||
BASE_URL = "https://nasstatus.faa.gov/api/airport-status-information"
|
||||
|
||||
# Cache parsed data to avoid hitting the API for every airport query
|
||||
_cache: dict | None = None
|
||||
_cache_raw: str = ""
|
||||
|
||||
|
||||
async def _fetch_all_status() -> dict[str, list[dict]]:
|
||||
"""Fetch and parse all airport status information from FAA."""
|
||||
global _cache, _cache_raw
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10.0, follow_redirects=True) as client:
|
||||
resp = await client.get(BASE_URL)
|
||||
resp.raise_for_status()
|
||||
xml_text = resp.text
|
||||
|
||||
# Don't re-parse if unchanged
|
||||
if xml_text == _cache_raw and _cache is not None:
|
||||
return _cache
|
||||
|
||||
_cache_raw = xml_text
|
||||
_cache = _parse_status_xml(xml_text)
|
||||
return _cache
|
||||
|
||||
except Exception as e:
|
||||
return {"_error": [{"error": str(e)}]}
|
||||
|
||||
|
||||
def _parse_status_xml(xml_text: str) -> dict[str, list[dict]]:
|
||||
"""Parse FAA airport status XML into a dict of airport → delays."""
|
||||
delays_by_airport: dict[str, list[dict]] = {}
|
||||
root = ET.fromstring(xml_text)
|
||||
|
||||
for delay_type in root.findall("Delay_type"):
|
||||
name = delay_type.findtext("Name", "").strip()
|
||||
|
||||
# Ground Stop Programs
|
||||
for program in delay_type.findall(".//Program"):
|
||||
arpt = program.findtext("ARPT", "").strip()
|
||||
if not arpt:
|
||||
continue
|
||||
delays_by_airport.setdefault(arpt, []).append({
|
||||
"type": "ground_stop",
|
||||
"reason": program.findtext("Reason", "unknown").strip(),
|
||||
"end_time": program.findtext("End_Time", "").strip() or None,
|
||||
})
|
||||
|
||||
# Ground Delay Programs
|
||||
for gdp in delay_type.findall(".//Ground_Delay"):
|
||||
arpt = gdp.findtext("ARPT", "").strip()
|
||||
if not arpt:
|
||||
continue
|
||||
delays_by_airport.setdefault(arpt, []).append({
|
||||
"type": "ground_delay_program",
|
||||
"reason": gdp.findtext("Reason", "unknown").strip(),
|
||||
"average_delay": gdp.findtext("Avg", "").strip() or None,
|
||||
"max_delay": gdp.findtext("Max", "").strip() or None,
|
||||
})
|
||||
|
||||
# Arrival/Departure Delays
|
||||
for delay in delay_type.findall(".//Delay"):
|
||||
arpt = delay.findtext("ARPT", "").strip()
|
||||
if not arpt:
|
||||
continue
|
||||
delays_by_airport.setdefault(arpt, []).append({
|
||||
"type": name.lower().replace(" ", "_") if name else "delay",
|
||||
"reason": delay.findtext("Reason", "unknown").strip(),
|
||||
"average_delay": delay.findtext("Avg", "").strip() or None,
|
||||
"max_delay": delay.findtext("Max", "").strip() or None,
|
||||
"trend": delay.findtext("Trend", "").strip() or None,
|
||||
})
|
||||
|
||||
# Closures
|
||||
for closure in delay_type.findall(".//Closure"):
|
||||
arpt = closure.findtext("ARPT", "").strip()
|
||||
if not arpt:
|
||||
continue
|
||||
delays_by_airport.setdefault(arpt, []).append({
|
||||
"type": "closure",
|
||||
"reason": closure.findtext("Reason", "unknown").strip(),
|
||||
"begin": closure.findtext("Begin", "").strip() or None,
|
||||
"end": closure.findtext("End", "").strip() or None,
|
||||
})
|
||||
|
||||
return delays_by_airport
|
||||
|
||||
|
||||
async def get_airport_status(airport_code: str) -> dict:
|
||||
"""Fetch live FAA airport status for a single airport.
|
||||
|
||||
Returns ground delay programs, ground stops, closures.
|
||||
Falls back to 'status_unavailable' on any error.
|
||||
"""
|
||||
all_status = await _fetch_all_status()
|
||||
|
||||
if "_error" in all_status:
|
||||
return {
|
||||
"airport": airport_code.upper(),
|
||||
"status": "status_unavailable",
|
||||
"error": all_status["_error"][0]["error"],
|
||||
"source": "faa_nasstatus_live",
|
||||
}
|
||||
|
||||
airport = airport_code.upper()
|
||||
delays = all_status.get(airport, [])
|
||||
|
||||
return {
|
||||
"airport": airport,
|
||||
"has_delays": len(delays) > 0,
|
||||
"status": "delays_active" if delays else "normal_operations",
|
||||
"delays": delays,
|
||||
"source": "faa_nasstatus_live",
|
||||
}
|
||||
197
mcp_servers/data/real/openmeteo.py
Normal file
197
mcp_servers/data/real/openmeteo.py
Normal file
@@ -0,0 +1,197 @@
|
||||
"""OpenMeteo API client — live weather data, no API key required."""
|
||||
|
||||
import httpx
|
||||
|
||||
BASE_URL = "https://api.open-meteo.com/v1/forecast"
|
||||
|
||||
# United hub coordinates
|
||||
HUB_COORDS: dict[str, tuple[float, float]] = {
|
||||
"ORD": (41.9742, -87.9073), # Chicago O'Hare
|
||||
"EWR": (40.6895, -74.1745), # Newark
|
||||
"IAH": (29.9902, -95.3368), # Houston Intercontinental
|
||||
"SFO": (37.6213, -122.3790), # San Francisco
|
||||
"DEN": (39.8561, -104.6737), # Denver
|
||||
"LAX": (33.9425, -118.4081), # Los Angeles
|
||||
"IAD": (38.9531, -77.4565), # Washington Dulles
|
||||
}
|
||||
|
||||
# WMO weather interpretation codes
|
||||
WMO_CODES: dict[int, str] = {
|
||||
0: "Clear sky",
|
||||
1: "Mainly clear",
|
||||
2: "Partly cloudy",
|
||||
3: "Overcast",
|
||||
45: "Fog",
|
||||
48: "Depositing rime fog",
|
||||
51: "Light drizzle",
|
||||
53: "Moderate drizzle",
|
||||
55: "Dense drizzle",
|
||||
61: "Slight rain",
|
||||
63: "Moderate rain",
|
||||
65: "Heavy rain",
|
||||
71: "Slight snow",
|
||||
73: "Moderate snow",
|
||||
75: "Heavy snow",
|
||||
77: "Snow grains",
|
||||
80: "Slight rain showers",
|
||||
81: "Moderate rain showers",
|
||||
82: "Violent rain showers",
|
||||
85: "Slight snow showers",
|
||||
86: "Heavy snow showers",
|
||||
95: "Thunderstorm",
|
||||
96: "Thunderstorm with slight hail",
|
||||
99: "Thunderstorm with heavy hail",
|
||||
}
|
||||
|
||||
SIGNIFICANT_CODES = {45, 48, 65, 75, 82, 86, 95, 96, 99}
|
||||
|
||||
|
||||
def _interpolate_waypoints(
|
||||
origin: tuple[float, float], dest: tuple[float, float], n: int = 2
|
||||
) -> list[tuple[float, float]]:
|
||||
"""Generate n intermediate waypoints along a great-circle approximation."""
|
||||
points = []
|
||||
for i in range(1, n + 1):
|
||||
frac = i / (n + 1)
|
||||
lat = origin[0] + frac * (dest[0] - origin[0])
|
||||
lon = origin[1] + frac * (dest[1] - origin[1])
|
||||
points.append((round(lat, 4), round(lon, 4)))
|
||||
return points
|
||||
|
||||
|
||||
def _interpret_weather(code: int) -> dict:
|
||||
return {
|
||||
"code": code,
|
||||
"condition": WMO_CODES.get(code, f"Unknown ({code})"),
|
||||
"is_significant": code in SIGNIFICANT_CODES,
|
||||
}
|
||||
|
||||
|
||||
async def fetch_weather_at_point(
|
||||
lat: float, lon: float, client: httpx.AsyncClient
|
||||
) -> dict:
|
||||
"""Fetch current weather at a single point."""
|
||||
params = {
|
||||
"latitude": lat,
|
||||
"longitude": lon,
|
||||
"current": "temperature_2m,wind_speed_10m,wind_direction_10m,weather_code,visibility,precipitation",
|
||||
"timezone": "UTC",
|
||||
}
|
||||
resp = await client.get(BASE_URL, params=params)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
current = data["current"]
|
||||
weather = _interpret_weather(current["weather_code"])
|
||||
return {
|
||||
"latitude": lat,
|
||||
"longitude": lon,
|
||||
"temperature_c": current["temperature_2m"],
|
||||
"wind_speed_kmh": current["wind_speed_10m"],
|
||||
"wind_direction_deg": current["wind_direction_10m"],
|
||||
"visibility_m": current.get("visibility"),
|
||||
"precipitation_mm": current.get("precipitation", 0),
|
||||
"weather": weather,
|
||||
}
|
||||
|
||||
|
||||
async def get_weather_along_route(origin: str, destination: str) -> dict:
|
||||
"""Fetch weather at origin, destination, and en-route waypoints."""
|
||||
origin_coords = HUB_COORDS.get(origin.upper())
|
||||
dest_coords = HUB_COORDS.get(destination.upper())
|
||||
|
||||
if not origin_coords or not dest_coords:
|
||||
return {
|
||||
"error": f"Unknown airport code. Known: {', '.join(HUB_COORDS.keys())}",
|
||||
"origin": origin,
|
||||
"destination": destination,
|
||||
}
|
||||
|
||||
waypoints = _interpolate_waypoints(origin_coords, dest_coords)
|
||||
all_points = [
|
||||
("origin", origin.upper(), origin_coords),
|
||||
*[(f"waypoint_{i+1}", f"WP{i+1}", wp) for i, wp in enumerate(waypoints)],
|
||||
("destination", destination.upper(), dest_coords),
|
||||
]
|
||||
|
||||
results = {}
|
||||
significant_events = []
|
||||
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
for label, name, (lat, lon) in all_points:
|
||||
try:
|
||||
weather = await fetch_weather_at_point(lat, lon, client)
|
||||
weather["label"] = label
|
||||
weather["name"] = name
|
||||
results[label] = weather
|
||||
if weather["weather"]["is_significant"]:
|
||||
significant_events.append(
|
||||
{
|
||||
"location": name,
|
||||
"condition": weather["weather"]["condition"],
|
||||
"code": weather["weather"]["code"],
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
results[label] = {"label": label, "name": name, "error": str(e)}
|
||||
|
||||
return {
|
||||
"origin": origin.upper(),
|
||||
"destination": destination.upper(),
|
||||
"waypoints": results,
|
||||
"significant_events": significant_events,
|
||||
"source": "openmeteo_live",
|
||||
}
|
||||
|
||||
|
||||
async def get_weather_forecast_hubs() -> dict:
|
||||
"""Fetch 4-hour forecast for all United hubs."""
|
||||
hubs = ["ORD", "EWR", "IAH", "SFO", "DEN"]
|
||||
results = {}
|
||||
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
for hub in hubs:
|
||||
lat, lon = HUB_COORDS[hub]
|
||||
params = {
|
||||
"latitude": lat,
|
||||
"longitude": lon,
|
||||
"hourly": "temperature_2m,wind_speed_10m,weather_code,visibility,precipitation_probability",
|
||||
"forecast_hours": 4,
|
||||
"timezone": "UTC",
|
||||
}
|
||||
try:
|
||||
resp = await client.get(BASE_URL, params=params)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
hourly = data["hourly"]
|
||||
|
||||
hours = []
|
||||
risk_flag = False
|
||||
for i in range(len(hourly["time"])):
|
||||
code = hourly["weather_code"][i]
|
||||
weather = _interpret_weather(code)
|
||||
if weather["is_significant"]:
|
||||
risk_flag = True
|
||||
hours.append(
|
||||
{
|
||||
"time": hourly["time"][i],
|
||||
"temperature_c": hourly["temperature_2m"][i],
|
||||
"wind_speed_kmh": hourly["wind_speed_10m"][i],
|
||||
"weather": weather,
|
||||
"visibility_m": hourly.get("visibility", [None])[i]
|
||||
if "visibility" in hourly
|
||||
else None,
|
||||
"precipitation_probability": hourly[
|
||||
"precipitation_probability"
|
||||
][i],
|
||||
}
|
||||
)
|
||||
|
||||
results[hub] = {
|
||||
"hub": hub,
|
||||
"forecast": hours,
|
||||
"risk_flag": risk_flag,
|
||||
}
|
||||
except Exception as e:
|
||||
results[hub] = {"hub": hub, "error": str(e)}
|
||||
|
||||
return {"hubs": results, "source": "openmeteo_live"}
|
||||
0
mcp_servers/data/scenarios/__init__.py
Normal file
0
mcp_servers/data/scenarios/__init__.py
Normal file
123
mcp_servers/data/scenarios/crew_swap_ewr.py
Normal file
123
mcp_servers/data/scenarios/crew_swap_ewr.py
Normal file
@@ -0,0 +1,123 @@
|
||||
"""Scenario: EWR Crew Duty Limit — complex Part 117 crew swap needed.
|
||||
|
||||
Captain hitting duty limit in 2h, backup crew needed. Tests Handover IMMEDIATE action.
|
||||
"""
|
||||
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from mcp_servers.data.models import (
|
||||
CrewMember,
|
||||
CrewRole,
|
||||
DelayCause,
|
||||
FlightData,
|
||||
FlightStatus,
|
||||
MELItem,
|
||||
MPStatus,
|
||||
Passenger,
|
||||
RebookingCase,
|
||||
)
|
||||
|
||||
SCENARIO_ID = "crew_swap_ewr"
|
||||
SCENARIO_NAME = "EWR Crew Duty Limit"
|
||||
SCENARIO_DESCRIPTION = (
|
||||
"Captain on UA2180 hitting Part 117 duty limit in 1h 45min. "
|
||||
"Delay cascading. 2 crew swaps needed."
|
||||
)
|
||||
SCENARIO_HUBS = ["EWR"]
|
||||
|
||||
FLIGHTS: list[FlightData] = [
|
||||
FlightData(
|
||||
flight_id="UA2180", origin="EWR", destination="LAX",
|
||||
scheduled_departure=datetime(2026, 4, 11, 20, 0, tzinfo=timezone.utc),
|
||||
actual_departure=None,
|
||||
scheduled_arrival=datetime(2026, 4, 11, 23, 30, tzinfo=timezone.utc),
|
||||
status=FlightStatus.DELAYED,
|
||||
delay_minutes=90,
|
||||
delay_cause=DelayCause.CREW,
|
||||
aircraft_tail="N81201",
|
||||
gate="C72",
|
||||
crew_ids=["CR-3001", "CR-3002", "CR-3010", "CR-3011"],
|
||||
passenger_count=210,
|
||||
),
|
||||
FlightData(
|
||||
flight_id="UA2244", origin="EWR", destination="ORD",
|
||||
scheduled_departure=datetime(2026, 4, 11, 20, 30, tzinfo=timezone.utc),
|
||||
actual_departure=None,
|
||||
scheduled_arrival=datetime(2026, 4, 11, 22, 0, tzinfo=timezone.utc),
|
||||
status=FlightStatus.DELAYED,
|
||||
delay_minutes=45,
|
||||
delay_cause=DelayCause.LATE_AIRCRAFT,
|
||||
aircraft_tail="N81202",
|
||||
gate="C87",
|
||||
crew_ids=["CR-3003", "CR-3004", "CR-3012"],
|
||||
passenger_count=178,
|
||||
),
|
||||
FlightData(
|
||||
flight_id="UA2310", origin="EWR", destination="SFO",
|
||||
scheduled_departure=datetime(2026, 4, 11, 21, 0, tzinfo=timezone.utc),
|
||||
scheduled_arrival=datetime(2026, 4, 12, 0, 15, tzinfo=timezone.utc),
|
||||
status=FlightStatus.ON_TIME,
|
||||
aircraft_tail="N81203",
|
||||
gate="C90",
|
||||
crew_ids=["CR-3005", "CR-3006"],
|
||||
passenger_count=195,
|
||||
),
|
||||
]
|
||||
|
||||
CREW: list[CrewMember] = [
|
||||
# UA2180 — captain at limit
|
||||
CrewMember(crew_id="CR-3001", name="Capt. Mitchell", role=CrewRole.CAPTAIN,
|
||||
duty_hours_elapsed=12.25, duty_hours_limit=14.0,
|
||||
rest_hours_since_last=10.0, next_scheduled_flight="UA2180", base_hub="EWR"),
|
||||
CrewMember(crew_id="CR-3002", name="FO Vasquez", role=CrewRole.FIRST_OFFICER,
|
||||
duty_hours_elapsed=11.0, duty_hours_limit=14.0,
|
||||
rest_hours_since_last=11.0, next_scheduled_flight="UA2180", base_hub="EWR"),
|
||||
# UA2244
|
||||
CrewMember(crew_id="CR-3003", name="Capt. Ali", role=CrewRole.CAPTAIN,
|
||||
duty_hours_elapsed=8.0, duty_hours_limit=14.0,
|
||||
rest_hours_since_last=14.0, next_scheduled_flight="UA2244", base_hub="EWR"),
|
||||
CrewMember(crew_id="CR-3004", name="FO Johansson", role=CrewRole.FIRST_OFFICER,
|
||||
duty_hours_elapsed=8.0, duty_hours_limit=14.0,
|
||||
rest_hours_since_last=13.0, next_scheduled_flight="UA2244", base_hub="EWR"),
|
||||
# UA2310
|
||||
CrewMember(crew_id="CR-3005", name="Capt. Reed", role=CrewRole.CAPTAIN,
|
||||
duty_hours_elapsed=4.0, duty_hours_limit=14.0,
|
||||
rest_hours_since_last=20.0, next_scheduled_flight="UA2310", base_hub="EWR"),
|
||||
CrewMember(crew_id="CR-3006", name="FO Torres", role=CrewRole.FIRST_OFFICER,
|
||||
duty_hours_elapsed=4.0, duty_hours_limit=14.0,
|
||||
rest_hours_since_last=18.0, next_scheduled_flight="UA2310", base_hub="EWR"),
|
||||
# FAs
|
||||
CrewMember(crew_id="CR-3010", name="FA Collins", role=CrewRole.FA,
|
||||
duty_hours_elapsed=11.5, duty_hours_limit=14.0,
|
||||
rest_hours_since_last=10.0, next_scheduled_flight="UA2180", base_hub="EWR"),
|
||||
CrewMember(crew_id="CR-3011", name="FA Yamamoto", role=CrewRole.FA,
|
||||
duty_hours_elapsed=11.5, duty_hours_limit=14.0,
|
||||
rest_hours_since_last=10.0, next_scheduled_flight="UA2180", base_hub="EWR"),
|
||||
CrewMember(crew_id="CR-3012", name="FA Petrov", role=CrewRole.FA,
|
||||
duty_hours_elapsed=7.0, duty_hours_limit=14.0,
|
||||
rest_hours_since_last=15.0, next_scheduled_flight="UA2244", base_hub="EWR"),
|
||||
# Backup crew
|
||||
CrewMember(crew_id="CR-8812", name="Capt. Foster", role=CrewRole.CAPTAIN,
|
||||
duty_hours_elapsed=0.0, duty_hours_limit=14.0,
|
||||
rest_hours_since_last=28.0, next_scheduled_flight=None, base_hub="EWR"),
|
||||
CrewMember(crew_id="CR-8813", name="FO Chang", role=CrewRole.FIRST_OFFICER,
|
||||
duty_hours_elapsed=0.0, duty_hours_limit=14.0,
|
||||
rest_hours_since_last=24.0, next_scheduled_flight=None, base_hub="EWR"),
|
||||
]
|
||||
|
||||
CREW_NOTES: dict[str, list[str]] = {
|
||||
"UA2180": [
|
||||
"Capt. Mitchell duty limit approaching — 1h 45min remaining.",
|
||||
"If departure slips past 22:15 ET, mandatory crew swap per Part 117 §117.19.",
|
||||
"Backup Capt. Foster (CR-8812) on standby at crew lounge, cleared and rested.",
|
||||
"FO Vasquez also approaching limit but has buffer until 23:00.",
|
||||
],
|
||||
"UA2244": [
|
||||
"Delay is cascading from late inbound aircraft, not crew-related.",
|
||||
"Gate conflict resolved — moved to C87.",
|
||||
],
|
||||
}
|
||||
|
||||
MAINTENANCE: dict[str, list[MELItem]] = {}
|
||||
REBOOKINGS: list[RebookingCase] = []
|
||||
PASSENGERS: list[Passenger] = []
|
||||
117
mcp_servers/data/scenarios/maintenance_delay_sfo.py
Normal file
117
mcp_servers/data/scenarios/maintenance_delay_sfo.py
Normal file
@@ -0,0 +1,117 @@
|
||||
"""Scenario: SFO Maintenance Delay — MEL issue cascading to 2 flights.
|
||||
|
||||
Aircraft N82301 has APU MEL, delay cascading. Tests FCE maintenance-caused
|
||||
delay explanation and Handover MEL flag section.
|
||||
"""
|
||||
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from mcp_servers.data.models import (
|
||||
CrewMember,
|
||||
CrewRole,
|
||||
DelayCause,
|
||||
FlightData,
|
||||
FlightStatus,
|
||||
MELItem,
|
||||
MPStatus,
|
||||
Passenger,
|
||||
RebookingCase,
|
||||
)
|
||||
|
||||
SCENARIO_ID = "maintenance_delay_sfo"
|
||||
SCENARIO_NAME = "SFO MEL Issue"
|
||||
SCENARIO_DESCRIPTION = (
|
||||
"Aircraft N82301 APU inoperative (MEL). Delay cascading to 2 flights. "
|
||||
"Route restriction if divert needed."
|
||||
)
|
||||
SCENARIO_HUBS = ["SFO"]
|
||||
|
||||
FLIGHTS: list[FlightData] = [
|
||||
FlightData(
|
||||
flight_id="UA712", origin="SFO", destination="EWR",
|
||||
scheduled_departure=datetime(2026, 4, 11, 19, 0, tzinfo=timezone.utc),
|
||||
actual_departure=None,
|
||||
scheduled_arrival=datetime(2026, 4, 12, 3, 30, tzinfo=timezone.utc),
|
||||
status=FlightStatus.DELAYED,
|
||||
delay_minutes=65,
|
||||
delay_cause=DelayCause.MAINTENANCE,
|
||||
aircraft_tail="N82301",
|
||||
gate="G4",
|
||||
crew_ids=["CR-4001", "CR-4002", "CR-4010"],
|
||||
passenger_count=198,
|
||||
),
|
||||
FlightData(
|
||||
flight_id="UA724", origin="SFO", destination="DEN",
|
||||
scheduled_departure=datetime(2026, 4, 11, 20, 30, tzinfo=timezone.utc),
|
||||
scheduled_arrival=datetime(2026, 4, 11, 23, 45, tzinfo=timezone.utc),
|
||||
status=FlightStatus.DELAYED,
|
||||
delay_minutes=30,
|
||||
delay_cause=DelayCause.LATE_AIRCRAFT,
|
||||
aircraft_tail="N82302",
|
||||
gate="G8",
|
||||
crew_ids=["CR-4003", "CR-4004"],
|
||||
passenger_count=165,
|
||||
),
|
||||
FlightData(
|
||||
flight_id="UA760", origin="SFO", destination="LAX",
|
||||
scheduled_departure=datetime(2026, 4, 11, 18, 30, tzinfo=timezone.utc),
|
||||
scheduled_arrival=datetime(2026, 4, 11, 19, 45, tzinfo=timezone.utc),
|
||||
status=FlightStatus.ON_TIME,
|
||||
aircraft_tail="N82303",
|
||||
gate="G12",
|
||||
crew_ids=["CR-4005", "CR-4006"],
|
||||
passenger_count=140,
|
||||
),
|
||||
]
|
||||
|
||||
CREW: list[CrewMember] = [
|
||||
CrewMember(crew_id="CR-4001", name="Capt. Novak", role=CrewRole.CAPTAIN,
|
||||
duty_hours_elapsed=6.0, duty_hours_limit=14.0,
|
||||
rest_hours_since_last=16.0, next_scheduled_flight="UA712", base_hub="SFO"),
|
||||
CrewMember(crew_id="CR-4002", name="FO Agrawal", role=CrewRole.FIRST_OFFICER,
|
||||
duty_hours_elapsed=6.0, duty_hours_limit=14.0,
|
||||
rest_hours_since_last=15.0, next_scheduled_flight="UA712", base_hub="SFO"),
|
||||
CrewMember(crew_id="CR-4003", name="Capt. Svensson", role=CrewRole.CAPTAIN,
|
||||
duty_hours_elapsed=5.0, duty_hours_limit=14.0,
|
||||
rest_hours_since_last=18.0, next_scheduled_flight="UA724", base_hub="SFO"),
|
||||
CrewMember(crew_id="CR-4004", name="FO Rivera", role=CrewRole.FIRST_OFFICER,
|
||||
duty_hours_elapsed=5.0, duty_hours_limit=14.0,
|
||||
rest_hours_since_last=17.0, next_scheduled_flight="UA724", base_hub="SFO"),
|
||||
CrewMember(crew_id="CR-4005", name="Capt. Wallace", role=CrewRole.CAPTAIN,
|
||||
duty_hours_elapsed=3.0, duty_hours_limit=14.0,
|
||||
rest_hours_since_last=22.0, next_scheduled_flight="UA760", base_hub="SFO"),
|
||||
CrewMember(crew_id="CR-4006", name="FO Zhao", role=CrewRole.FIRST_OFFICER,
|
||||
duty_hours_elapsed=3.0, duty_hours_limit=14.0,
|
||||
rest_hours_since_last=20.0, next_scheduled_flight="UA760", base_hub="SFO"),
|
||||
CrewMember(crew_id="CR-4010", name="FA Douglas", role=CrewRole.FA,
|
||||
duty_hours_elapsed=5.5, duty_hours_limit=14.0,
|
||||
rest_hours_since_last=16.0, next_scheduled_flight="UA712", base_hub="SFO"),
|
||||
]
|
||||
|
||||
CREW_NOTES: dict[str, list[str]] = {
|
||||
"UA712": [
|
||||
"MEL item #47-3: APU inoperative on N82301.",
|
||||
"Maintenance team inspecting — estimated release in 45 min.",
|
||||
"APU MEL restricts routing to airports with ground power only.",
|
||||
"Current SFO→EWR route unaffected, but flag if divert to BDL or HPN needed.",
|
||||
],
|
||||
"UA724": [
|
||||
"Delay cascading from UA712 — shared gate G8 not available until UA712 pushes.",
|
||||
],
|
||||
}
|
||||
|
||||
MAINTENANCE: dict[str, list[MELItem]] = {
|
||||
"N82301": [
|
||||
MELItem(
|
||||
mel_id="MEL-SFO-473",
|
||||
aircraft_tail="N82301",
|
||||
system="APU",
|
||||
description="Auxiliary Power Unit inoperative. Aircraft requires external ground power for engine start and gate operations.",
|
||||
restriction="Routing restricted to airports with ground power availability. Cannot divert to airports without ground power units (e.g., BDL, HPN).",
|
||||
expires=datetime(2026, 4, 18, 0, 0, tzinfo=timezone.utc),
|
||||
),
|
||||
],
|
||||
}
|
||||
|
||||
REBOOKINGS: list[RebookingCase] = []
|
||||
PASSENGERS: list[Passenger] = []
|
||||
85
mcp_servers/data/scenarios/manager.py
Normal file
85
mcp_servers/data/scenarios/manager.py
Normal file
@@ -0,0 +1,85 @@
|
||||
"""Scenario manager — loads and switches between operational scenarios."""
|
||||
|
||||
from importlib import import_module
|
||||
from typing import Any
|
||||
|
||||
SCENARIO_MODULES = {
|
||||
"normal_ops": "mcp_servers.data.scenarios.normal_ops",
|
||||
"weather_disruption_ord": "mcp_servers.data.scenarios.weather_disruption_ord",
|
||||
"maintenance_delay_sfo": "mcp_servers.data.scenarios.maintenance_delay_sfo",
|
||||
"crew_swap_ewr": "mcp_servers.data.scenarios.crew_swap_ewr",
|
||||
}
|
||||
|
||||
|
||||
class ScenarioManager:
|
||||
"""Manages the active scenario. Singleton — all MCP servers share one instance."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._active_id: str = "weather_disruption_ord"
|
||||
self._cache: dict[str, Any] = {}
|
||||
|
||||
@property
|
||||
def active_id(self) -> str:
|
||||
return self._active_id
|
||||
|
||||
def set_active(self, scenario_id: str) -> dict:
|
||||
if scenario_id not in SCENARIO_MODULES:
|
||||
raise ValueError(
|
||||
f"Unknown scenario: {scenario_id}. "
|
||||
f"Available: {', '.join(SCENARIO_MODULES.keys())}"
|
||||
)
|
||||
self._active_id = scenario_id
|
||||
return self.get_metadata()
|
||||
|
||||
def get_metadata(self, scenario_id: str | None = None) -> dict:
|
||||
mod = self._load(scenario_id or self._active_id)
|
||||
return {
|
||||
"scenario_id": mod.SCENARIO_ID,
|
||||
"name": mod.SCENARIO_NAME,
|
||||
"description": mod.SCENARIO_DESCRIPTION,
|
||||
"hubs": mod.SCENARIO_HUBS,
|
||||
"flight_count": len(mod.FLIGHTS),
|
||||
"disrupted_flights": sum(
|
||||
1 for f in mod.FLIGHTS if f.status != "ON_TIME"
|
||||
),
|
||||
}
|
||||
|
||||
def list_scenarios(self) -> list[dict]:
|
||||
return [self.get_metadata(sid) for sid in SCENARIO_MODULES]
|
||||
|
||||
def _load(self, scenario_id: str) -> Any:
|
||||
if scenario_id not in self._cache:
|
||||
self._cache[scenario_id] = import_module(SCENARIO_MODULES[scenario_id])
|
||||
return self._cache[scenario_id]
|
||||
|
||||
@property
|
||||
def _module(self) -> Any:
|
||||
return self._load(self._active_id)
|
||||
|
||||
@property
|
||||
def flights(self):
|
||||
return self._module.FLIGHTS
|
||||
|
||||
@property
|
||||
def crew(self):
|
||||
return self._module.CREW
|
||||
|
||||
@property
|
||||
def crew_notes(self) -> dict[str, list[str]]:
|
||||
return self._module.CREW_NOTES
|
||||
|
||||
@property
|
||||
def maintenance(self):
|
||||
return self._module.MAINTENANCE
|
||||
|
||||
@property
|
||||
def rebookings(self):
|
||||
return self._module.REBOOKINGS
|
||||
|
||||
@property
|
||||
def passengers(self):
|
||||
return self._module.PASSENGERS
|
||||
|
||||
|
||||
# Singleton
|
||||
scenario_manager = ScenarioManager()
|
||||
72
mcp_servers/data/scenarios/normal_ops.py
Normal file
72
mcp_servers/data/scenarios/normal_ops.py
Normal file
@@ -0,0 +1,72 @@
|
||||
"""Scenario: Normal Operations — all flights on time, clear weather, full crew.
|
||||
|
||||
Baseline scenario. Agents should produce minimal/calm output.
|
||||
"""
|
||||
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from mcp_servers.data.models import (
|
||||
CrewMember,
|
||||
CrewRole,
|
||||
FlightData,
|
||||
FlightStatus,
|
||||
MPStatus,
|
||||
Passenger,
|
||||
RebookingCase,
|
||||
MELItem,
|
||||
)
|
||||
|
||||
SCENARIO_ID = "normal_ops"
|
||||
SCENARIO_NAME = "Normal Operations"
|
||||
SCENARIO_DESCRIPTION = "All flights on time at ORD. Clear weather. No disruptions."
|
||||
SCENARIO_HUBS = ["ORD"]
|
||||
|
||||
FLIGHTS: list[FlightData] = [
|
||||
FlightData(
|
||||
flight_id="UA1440", origin="ORD", destination="SFO",
|
||||
scheduled_departure=datetime(2026, 4, 11, 18, 0, tzinfo=timezone.utc),
|
||||
scheduled_arrival=datetime(2026, 4, 11, 20, 30, tzinfo=timezone.utc),
|
||||
status=FlightStatus.ON_TIME, aircraft_tail="N79001", gate="H10",
|
||||
crew_ids=["CR-2001", "CR-2002"], passenger_count=175,
|
||||
),
|
||||
FlightData(
|
||||
flight_id="UA1552", origin="ORD", destination="EWR",
|
||||
scheduled_departure=datetime(2026, 4, 11, 18, 30, tzinfo=timezone.utc),
|
||||
scheduled_arrival=datetime(2026, 4, 11, 21, 45, tzinfo=timezone.utc),
|
||||
status=FlightStatus.ON_TIME, aircraft_tail="N79002", gate="B12",
|
||||
crew_ids=["CR-2003", "CR-2004"], passenger_count=190,
|
||||
),
|
||||
FlightData(
|
||||
flight_id="UA1678", origin="ORD", destination="DEN",
|
||||
scheduled_departure=datetime(2026, 4, 11, 19, 0, tzinfo=timezone.utc),
|
||||
scheduled_arrival=datetime(2026, 4, 11, 20, 30, tzinfo=timezone.utc),
|
||||
status=FlightStatus.ON_TIME, aircraft_tail="N79003", gate="C8",
|
||||
crew_ids=["CR-2005", "CR-2006"], passenger_count=160,
|
||||
),
|
||||
]
|
||||
|
||||
CREW: list[CrewMember] = [
|
||||
CrewMember(crew_id="CR-2001", name="Capt. Hayes", role=CrewRole.CAPTAIN,
|
||||
duty_hours_elapsed=4.0, duty_hours_limit=14.0,
|
||||
rest_hours_since_last=18.0, next_scheduled_flight="UA1440", base_hub="ORD"),
|
||||
CrewMember(crew_id="CR-2002", name="FO Park", role=CrewRole.FIRST_OFFICER,
|
||||
duty_hours_elapsed=4.0, duty_hours_limit=14.0,
|
||||
rest_hours_since_last=16.0, next_scheduled_flight="UA1440", base_hub="ORD"),
|
||||
CrewMember(crew_id="CR-2003", name="Capt. Lewis", role=CrewRole.CAPTAIN,
|
||||
duty_hours_elapsed=5.0, duty_hours_limit=14.0,
|
||||
rest_hours_since_last=15.0, next_scheduled_flight="UA1552", base_hub="ORD"),
|
||||
CrewMember(crew_id="CR-2004", name="FO Sharma", role=CrewRole.FIRST_OFFICER,
|
||||
duty_hours_elapsed=5.0, duty_hours_limit=14.0,
|
||||
rest_hours_since_last=14.0, next_scheduled_flight="UA1552", base_hub="ORD"),
|
||||
CrewMember(crew_id="CR-2005", name="Capt. Brown", role=CrewRole.CAPTAIN,
|
||||
duty_hours_elapsed=3.0, duty_hours_limit=14.0,
|
||||
rest_hours_since_last=20.0, next_scheduled_flight="UA1678", base_hub="ORD"),
|
||||
CrewMember(crew_id="CR-2006", name="FO Nguyen", role=CrewRole.FIRST_OFFICER,
|
||||
duty_hours_elapsed=3.0, duty_hours_limit=14.0,
|
||||
rest_hours_since_last=19.0, next_scheduled_flight="UA1678", base_hub="ORD"),
|
||||
]
|
||||
|
||||
CREW_NOTES: dict[str, list[str]] = {}
|
||||
MAINTENANCE: dict[str, list[MELItem]] = {}
|
||||
REBOOKINGS: list[RebookingCase] = []
|
||||
PASSENGERS: list[Passenger] = []
|
||||
281
mcp_servers/data/scenarios/weather_disruption_ord.py
Normal file
281
mcp_servers/data/scenarios/weather_disruption_ord.py
Normal file
@@ -0,0 +1,281 @@
|
||||
"""Scenario: ORD Thunderstorms — active disruption at Chicago O'Hare.
|
||||
|
||||
Thunderstorm line causing GDP, 4 flights delayed, 1 cancelled.
|
||||
Tests FCE multi-flight notifications and Handover weather risk section.
|
||||
"""
|
||||
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from mcp_servers.data.models import (
|
||||
CrewMember,
|
||||
CrewRole,
|
||||
DelayCause,
|
||||
FlightData,
|
||||
FlightStatus,
|
||||
MELItem,
|
||||
MPStatus,
|
||||
Passenger,
|
||||
RebookingCase,
|
||||
)
|
||||
|
||||
SCENARIO_ID = "weather_disruption_ord"
|
||||
SCENARIO_NAME = "ORD Thunderstorms"
|
||||
SCENARIO_DESCRIPTION = (
|
||||
"Active thunderstorm line at ORD. GDP in effect. "
|
||||
"4 delays, 1 cancellation. 47 passengers need rebooking."
|
||||
)
|
||||
SCENARIO_HUBS = ["ORD"]
|
||||
|
||||
# --- Flights ---
|
||||
|
||||
FLIGHTS: list[FlightData] = [
|
||||
# Delayed flights
|
||||
FlightData(
|
||||
flight_id="UA432",
|
||||
origin="ORD", destination="SFO",
|
||||
scheduled_departure=datetime(2026, 4, 11, 17, 45, tzinfo=timezone.utc),
|
||||
actual_departure=None,
|
||||
scheduled_arrival=datetime(2026, 4, 11, 20, 15, tzinfo=timezone.utc),
|
||||
actual_arrival=None,
|
||||
status=FlightStatus.DELAYED,
|
||||
delay_minutes=55,
|
||||
delay_cause=DelayCause.WEATHER,
|
||||
aircraft_tail="N78501",
|
||||
gate="H14",
|
||||
inbound_flight="UA219",
|
||||
crew_ids=["CR-1001", "CR-1002", "CR-1010", "CR-1011"],
|
||||
passenger_count=186,
|
||||
),
|
||||
FlightData(
|
||||
flight_id="UA881",
|
||||
origin="ORD", destination="LAX",
|
||||
scheduled_departure=datetime(2026, 4, 11, 18, 30, tzinfo=timezone.utc),
|
||||
actual_departure=None,
|
||||
scheduled_arrival=datetime(2026, 4, 11, 21, 0, tzinfo=timezone.utc),
|
||||
actual_arrival=None,
|
||||
status=FlightStatus.DELAYED,
|
||||
delay_minutes=40,
|
||||
delay_cause=DelayCause.WEATHER,
|
||||
aircraft_tail="N78502",
|
||||
gate="H22",
|
||||
crew_ids=["CR-1003", "CR-1004", "CR-1012", "CR-1013"],
|
||||
passenger_count=204,
|
||||
),
|
||||
FlightData(
|
||||
flight_id="UA233",
|
||||
origin="ORD", destination="DEN",
|
||||
scheduled_departure=datetime(2026, 4, 11, 18, 0, tzinfo=timezone.utc),
|
||||
actual_departure=None,
|
||||
scheduled_arrival=datetime(2026, 4, 11, 19, 45, tzinfo=timezone.utc),
|
||||
actual_arrival=None,
|
||||
status=FlightStatus.DELAYED,
|
||||
delay_minutes=70,
|
||||
delay_cause=DelayCause.WEATHER,
|
||||
aircraft_tail="N78503",
|
||||
gate="C12",
|
||||
crew_ids=["CR-1005", "CR-1006", "CR-1014"],
|
||||
passenger_count=152,
|
||||
),
|
||||
FlightData(
|
||||
flight_id="UA094",
|
||||
origin="ORD", destination="EWR",
|
||||
scheduled_departure=datetime(2026, 4, 11, 17, 15, tzinfo=timezone.utc),
|
||||
actual_departure=None,
|
||||
scheduled_arrival=datetime(2026, 4, 11, 20, 30, tzinfo=timezone.utc),
|
||||
actual_arrival=None,
|
||||
status=FlightStatus.DELAYED,
|
||||
delay_minutes=35,
|
||||
delay_cause=DelayCause.WEATHER,
|
||||
aircraft_tail="N78504",
|
||||
gate="B8",
|
||||
crew_ids=["CR-1007", "CR-1008", "CR-1015"],
|
||||
passenger_count=178,
|
||||
),
|
||||
# Cancelled flight
|
||||
FlightData(
|
||||
flight_id="UA517",
|
||||
origin="ORD", destination="IAH",
|
||||
scheduled_departure=datetime(2026, 4, 11, 19, 0, tzinfo=timezone.utc),
|
||||
actual_departure=None,
|
||||
scheduled_arrival=datetime(2026, 4, 11, 21, 30, tzinfo=timezone.utc),
|
||||
actual_arrival=None,
|
||||
status=FlightStatus.CANCELLED,
|
||||
delay_minutes=0,
|
||||
delay_cause=DelayCause.WEATHER,
|
||||
aircraft_tail="N78505",
|
||||
gate="C18",
|
||||
crew_ids=["CR-1009", "CR-1016"],
|
||||
passenger_count=47,
|
||||
),
|
||||
# On-time flights (normal ops happening alongside disruption)
|
||||
FlightData(
|
||||
flight_id="UA1220",
|
||||
origin="ORD", destination="IAD",
|
||||
scheduled_departure=datetime(2026, 4, 11, 20, 30, tzinfo=timezone.utc),
|
||||
actual_departure=None,
|
||||
scheduled_arrival=datetime(2026, 4, 11, 23, 15, tzinfo=timezone.utc),
|
||||
actual_arrival=None,
|
||||
status=FlightStatus.ON_TIME,
|
||||
delay_minutes=0,
|
||||
delay_cause=None,
|
||||
aircraft_tail="N78506",
|
||||
gate="B15",
|
||||
crew_ids=["CR-1017", "CR-1018"],
|
||||
passenger_count=145,
|
||||
),
|
||||
FlightData(
|
||||
flight_id="UA788",
|
||||
origin="ORD", destination="SFO",
|
||||
scheduled_departure=datetime(2026, 4, 11, 21, 0, tzinfo=timezone.utc),
|
||||
actual_departure=None,
|
||||
scheduled_arrival=datetime(2026, 4, 11, 23, 30, tzinfo=timezone.utc),
|
||||
actual_arrival=None,
|
||||
status=FlightStatus.ON_TIME,
|
||||
delay_minutes=0,
|
||||
delay_cause=None,
|
||||
aircraft_tail="N78507",
|
||||
gate="H18",
|
||||
crew_ids=["CR-1019", "CR-1020"],
|
||||
passenger_count=192,
|
||||
),
|
||||
]
|
||||
|
||||
# --- Crew ---
|
||||
|
||||
CREW: list[CrewMember] = [
|
||||
# UA432 crew
|
||||
CrewMember(crew_id="CR-1001", name="Capt. Rodriguez", role=CrewRole.CAPTAIN,
|
||||
duty_hours_elapsed=10.5, duty_hours_limit=14.0,
|
||||
rest_hours_since_last=12.0, next_scheduled_flight="UA432", base_hub="ORD"),
|
||||
CrewMember(crew_id="CR-1002", name="FO Chen", role=CrewRole.FIRST_OFFICER,
|
||||
duty_hours_elapsed=10.5, duty_hours_limit=14.0,
|
||||
rest_hours_since_last=11.0, next_scheduled_flight="UA432", base_hub="ORD"),
|
||||
# UA881 crew — captain approaching duty limit
|
||||
CrewMember(crew_id="CR-1003", name="Capt. Williams", role=CrewRole.CAPTAIN,
|
||||
duty_hours_elapsed=12.25, duty_hours_limit=14.0,
|
||||
rest_hours_since_last=10.0, next_scheduled_flight="UA881", base_hub="ORD"),
|
||||
CrewMember(crew_id="CR-1004", name="FO Patel", role=CrewRole.FIRST_OFFICER,
|
||||
duty_hours_elapsed=9.0, duty_hours_limit=14.0,
|
||||
rest_hours_since_last=14.0, next_scheduled_flight="UA881", base_hub="ORD"),
|
||||
# UA233 crew
|
||||
CrewMember(crew_id="CR-1005", name="Capt. Johnson", role=CrewRole.CAPTAIN,
|
||||
duty_hours_elapsed=8.0, duty_hours_limit=14.0,
|
||||
rest_hours_since_last=16.0, next_scheduled_flight="UA233", base_hub="ORD"),
|
||||
CrewMember(crew_id="CR-1006", name="FO Kim", role=CrewRole.FIRST_OFFICER,
|
||||
duty_hours_elapsed=7.5, duty_hours_limit=14.0,
|
||||
rest_hours_since_last=15.0, next_scheduled_flight="UA233", base_hub="ORD"),
|
||||
# UA094 crew
|
||||
CrewMember(crew_id="CR-1007", name="Capt. Davis", role=CrewRole.CAPTAIN,
|
||||
duty_hours_elapsed=6.0, duty_hours_limit=14.0,
|
||||
rest_hours_since_last=18.0, next_scheduled_flight="UA094", base_hub="ORD"),
|
||||
CrewMember(crew_id="CR-1008", name="FO Martinez", role=CrewRole.FIRST_OFFICER,
|
||||
duty_hours_elapsed=6.0, duty_hours_limit=14.0,
|
||||
rest_hours_since_last=17.0, next_scheduled_flight="UA094", base_hub="ORD"),
|
||||
# UA517 crew (cancelled flight — available for reassignment)
|
||||
CrewMember(crew_id="CR-1009", name="Capt. Thompson", role=CrewRole.CAPTAIN,
|
||||
duty_hours_elapsed=4.0, duty_hours_limit=14.0,
|
||||
rest_hours_since_last=20.0, next_scheduled_flight=None, base_hub="ORD"),
|
||||
# Flight attendants
|
||||
CrewMember(crew_id="CR-1010", name="FA Brooks", role=CrewRole.FA,
|
||||
duty_hours_elapsed=10.0, duty_hours_limit=14.0,
|
||||
rest_hours_since_last=12.0, next_scheduled_flight="UA432", base_hub="ORD"),
|
||||
CrewMember(crew_id="CR-1011", name="FA Lee", role=CrewRole.FA,
|
||||
duty_hours_elapsed=10.0, duty_hours_limit=14.0,
|
||||
rest_hours_since_last=13.0, next_scheduled_flight="UA432", base_hub="ORD"),
|
||||
CrewMember(crew_id="CR-1012", name="FA Garcia", role=CrewRole.FA,
|
||||
duty_hours_elapsed=11.5, duty_hours_limit=14.0,
|
||||
rest_hours_since_last=10.0, next_scheduled_flight="UA881", base_hub="ORD"),
|
||||
# Backup crew
|
||||
CrewMember(crew_id="CR-4421", name="Capt. Okafor", role=CrewRole.CAPTAIN,
|
||||
duty_hours_elapsed=0.0, duty_hours_limit=14.0,
|
||||
rest_hours_since_last=24.0, next_scheduled_flight=None, base_hub="ORD"),
|
||||
]
|
||||
|
||||
# --- Crew Notes ---
|
||||
|
||||
CREW_NOTES: dict[str, list[str]] = {
|
||||
"UA432": [
|
||||
"Ground stop at ORD — thunderstorm line moving NE across field.",
|
||||
"Inbound aircraft N78501 on ground, boarding paused at gate H14.",
|
||||
"Fresh crew on board, catering complete. Ready to board on release.",
|
||||
"Gate hold — no gate change expected per ops.",
|
||||
],
|
||||
"UA881": [
|
||||
"Capt. Williams approaching duty limit — monitor closely.",
|
||||
"If departure slips past 22:15 CT, mandatory crew swap required.",
|
||||
"Backup crew CR-4421 (Capt. Okafor) on standby, cleared and rested.",
|
||||
],
|
||||
"UA233": [
|
||||
"Extended delay due to weather at ORD and congestion at DEN.",
|
||||
"8 connecting passengers with tight connections at DEN — rebooking may be needed.",
|
||||
],
|
||||
"UA094": [
|
||||
"Moderate delay. EWR reporting clear conditions, delay is ORD-side only.",
|
||||
],
|
||||
"UA517": [
|
||||
"CANCELLED — thunderstorm forecast through departure window.",
|
||||
"47 passengers need rebooking. 8 Global Services pax — priority handling.",
|
||||
"Rebooking options loaded in system — supervisor approval pending.",
|
||||
],
|
||||
}
|
||||
|
||||
# --- Maintenance ---
|
||||
|
||||
MAINTENANCE: dict[str, list[MELItem]] = {
|
||||
"N78501": [
|
||||
MELItem(
|
||||
mel_id="MEL-ORD-001",
|
||||
aircraft_tail="N78501",
|
||||
system="WEATHER_RADAR",
|
||||
description="Weather radar intermittent — functions normally after reset cycle.",
|
||||
restriction=None,
|
||||
expires=datetime(2026, 4, 15, 0, 0, tzinfo=timezone.utc),
|
||||
),
|
||||
],
|
||||
}
|
||||
|
||||
# --- Passengers needing rebooking (from cancelled UA517) ---
|
||||
|
||||
REBOOKINGS: list[RebookingCase] = [
|
||||
RebookingCase(pax_id="PAX-00341", name="J. Morrison",
|
||||
original_flight="UA517", mileage_plus_status=MPStatus.GLOBAL_SERVICES,
|
||||
destination="IAH", next_available="UA519", urgency="HIGH"),
|
||||
RebookingCase(pax_id="PAX-00342", name="S. Nakamura",
|
||||
original_flight="UA517", mileage_plus_status=MPStatus.GLOBAL_SERVICES,
|
||||
destination="IAH", next_available="UA519", urgency="HIGH"),
|
||||
RebookingCase(pax_id="PAX-00355", name="R. Okonkwo",
|
||||
original_flight="UA517", mileage_plus_status=MPStatus.K1,
|
||||
destination="IAH", next_available="UA519", urgency="HIGH"),
|
||||
RebookingCase(pax_id="PAX-00360", name="M. Fernandez",
|
||||
original_flight="UA517", mileage_plus_status=MPStatus.PLATINUM,
|
||||
destination="IAH", next_available=None, urgency="MEDIUM"),
|
||||
RebookingCase(pax_id="PAX-00371", name="K. Singh",
|
||||
original_flight="UA517", mileage_plus_status=MPStatus.GOLD,
|
||||
destination="IAH", next_available=None, urgency="MEDIUM"),
|
||||
RebookingCase(pax_id="PAX-00380", name="L. Anderson",
|
||||
original_flight="UA517", mileage_plus_status=MPStatus.GENERAL,
|
||||
destination="IAH", next_available=None, urgency="LOW"),
|
||||
]
|
||||
|
||||
# --- Passengers on disrupted flights (sample) ---
|
||||
|
||||
PASSENGERS: list[Passenger] = [
|
||||
# UA432 passengers
|
||||
Passenger(pax_id="PAX-10001", name="A. Kowalski",
|
||||
mileage_plus_status=MPStatus.GLOBAL_SERVICES, flight_id="UA432",
|
||||
destination="SFO", connection_flight="UA1892",
|
||||
connection_deadline=datetime(2026, 4, 11, 21, 30, tzinfo=timezone.utc)),
|
||||
Passenger(pax_id="PAX-10002", name="B. Tanaka",
|
||||
mileage_plus_status=MPStatus.K1, flight_id="UA432",
|
||||
destination="SFO"),
|
||||
Passenger(pax_id="PAX-10003", name="C. Dubois",
|
||||
mileage_plus_status=MPStatus.GENERAL, flight_id="UA432",
|
||||
destination="SFO", special_needs=["WCHR"]),
|
||||
# UA517 passengers (cancelled)
|
||||
Passenger(pax_id="PAX-00341", name="J. Morrison",
|
||||
mileage_plus_status=MPStatus.GLOBAL_SERVICES, flight_id="UA517",
|
||||
destination="IAH"),
|
||||
Passenger(pax_id="PAX-00342", name="S. Nakamura",
|
||||
mileage_plus_status=MPStatus.GLOBAL_SERVICES, flight_id="UA517",
|
||||
destination="IAH"),
|
||||
]
|
||||
Reference in New Issue
Block a user