alaa wael commited on
Commit
cb32952
·
1 Parent(s): f6b59b5

add travel utils

Browse files
Files changed (2) hide show
  1. app/api/routes.py +18 -2
  2. app/utils/travel_utils.py +125 -0
app/api/routes.py CHANGED
@@ -32,6 +32,7 @@ from app.services.preprocessing import (
32
  from app.services.postprocessing import agent_response_to_dict
33
  from app.utils.helpers import utc_now_iso
34
  from app.core.config import settings
 
35
 
36
  logger = logging.getLogger(__name__)
37
 
@@ -179,10 +180,25 @@ async def chat_structured(request: ChatRequest) -> Dict[str, Any]:
179
  @router.post("/plan", response_model=AgentResponse, tags=["Planning"])
180
  async def plan(request: PlanRequest) -> AgentResponse:
181
  """
182
- Generate a day-by-day itinerary from a fixed list of places.
 
183
  """
184
  try:
185
- return generate_plan(request)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
186
 
187
  except Exception as exc:
188
  logger.exception("Error in /plan: %s", exc)
 
32
  from app.services.postprocessing import agent_response_to_dict
33
  from app.utils.helpers import utc_now_iso
34
  from app.core.config import settings
35
+ from app.utils.travel_utils import adjust_itinerary_times
36
 
37
  logger = logging.getLogger(__name__)
38
 
 
180
  @router.post("/plan", response_model=AgentResponse, tags=["Planning"])
181
  async def plan(request: PlanRequest) -> AgentResponse:
182
  """
183
+ Generate a day-by-day itinerary from a fixed list of places,
184
+ calculate travel times, and adjust the schedules accordingly.
185
  """
186
  try:
187
+ # 1. Run the core planning logic/agents
188
+ agent_response = generate_plan(request)
189
+
190
+ # 2. Convert response to a dictionary for modifications
191
+ # Use model_dump() for Pydantic v2, or dict() for Pydantic v1
192
+ if hasattr(agent_response, "model_dump"):
193
+ response_dict = agent_response.model_dump()
194
+ else:
195
+ response_dict = agent_response.dict()
196
+
197
+ # 3. Adjust time ranges based on spatial travel times
198
+ adjusted_dict = adjust_itinerary_times(response_dict)
199
+
200
+ # 4. Reconstruct the response model with modified data
201
+ return AgentResponse(**adjusted_dict)
202
 
203
  except Exception as exc:
204
  logger.exception("Error in /plan: %s", exc)
app/utils/travel_utils.py ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+ from datetime import datetime, timedelta
3
+ import requests
4
+
5
+
6
+ # ----------------------------------------------------
7
+ # 1. TRAVEL TIME CALCULATION
8
+ # ----------------------------------------------------
9
+ def get_travel_time_minutes(lat1, lon1, lat2, lon2):
10
+ """
11
+ Calculates travel time using OSRM Routing API, with a fallback
12
+ to Haversine (straight-line) distance if the API call fails.
13
+ """
14
+ # Try using OSRM API (Free road-routing service)
15
+ url = f"http://router.project-osrm.org/route/v1/driving/{lon1},{lat1};{lon2},{lat2}?overview=false"
16
+ try:
17
+ response = requests.get(url, timeout=3)
18
+ if response.status_code == 200:
19
+ data = response.json()
20
+ if data.get("code") == "Ok":
21
+ duration_seconds = data["routes"][0]["duration"]
22
+ return round(duration_seconds / 60)
23
+ except Exception:
24
+ pass # Fail silently to trigger the mathematical fallback
25
+
26
+ # Fallback: Haversine Formula with an assumed average speed of 30 km/h
27
+ R = 6371.0 # Earth's radius in km
28
+ phi1, phi2 = math.radians(lat1), math.radians(lat2)
29
+ delta_phi = math.radians(lat2 - lat1)
30
+ delta_lambda = math.radians(lon2 - lon1)
31
+
32
+ a = (
33
+ math.sin(delta_phi / 2.0) ** 2
34
+ + math.cos(phi1) * math.cos(phi2) * math.sin(delta_lambda / 2.0) ** 2
35
+ )
36
+
37
+ c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
38
+ distance_km = R * c
39
+
40
+ avg_speed_kmh = 30.0
41
+ time_minutes = (distance_km / avg_speed_kmh) * 60
42
+ return max(5, round(time_minutes)) # Ensure a minimum of 5 minutes travel time
43
+
44
+
45
+ # ----------------------------------------------------
46
+ # 2. TIME FORMATTING AND PARSING HELPERS
47
+ # ----------------------------------------------------
48
+ def parse_time_str(time_str: str) -> datetime:
49
+ """Parses time strings like '9:00 PM' or '10:30 AM' into a datetime object."""
50
+ clean_str = time_str.strip().upper()
51
+ return datetime.strptime(clean_str, "%I:%M %p")
52
+
53
+
54
+ def format_time_obj(dt: datetime) -> str:
55
+ """Formats datetime objects back to '9:00 PM' style string."""
56
+ return dt.strftime("%I:%M %p").lstrip("0")
57
+
58
+
59
+ # ----------------------------------------------------
60
+ # 3. ITINERARY ADJUSTMENT ENGINE
61
+ # ----------------------------------------------------
62
+ def adjust_itinerary_times(itinerary_dict: dict) -> dict:
63
+ """
64
+ Iterates through each day in the itinerary, calculates travel times,
65
+ and updates the 'time_range' fields for all activities.
66
+ """
67
+ daily_itinerary = (
68
+ itinerary_dict.get("active_trip", {})
69
+ .get("itinerary", {})
70
+ .get("daily_itinerary", [])
71
+ )
72
+
73
+ for day in daily_itinerary:
74
+ activities = day.get("activities", [])
75
+ if not activities:
76
+ continue
77
+
78
+ # Step 1: Process the first activity
79
+ first_act = activities[0]
80
+ duration_hours = first_act.get("estimated_duration_hours", 1.0)
81
+
82
+ # Extract initial start time
83
+ time_range = first_act.get("time_range", "09:00 AM - 10:00 AM")
84
+ try:
85
+ start_time_str = time_range.split("-")[0].strip()
86
+ current_start = parse_time_str(start_time_str)
87
+ except Exception:
88
+ current_start = parse_time_str("09:00 AM")
89
+
90
+ current_end = current_start + timedelta(hours=duration_hours)
91
+ first_act["time_range"] = (
92
+ f"{format_time_obj(current_start)} - {format_time_obj(current_end)}"
93
+ )
94
+
95
+ # Step 2: Sequentially process subsequent activities
96
+ for i in range(1, len(activities)):
97
+ prev_act = activities[i - 1]
98
+ curr_act = activities[i]
99
+
100
+ # Extract coordinates (Note spelling: 'late' and 'long' from your JSON structure)
101
+ prev_loc = prev_act.get("place", {}).get("location", {})
102
+ curr_loc = curr_act.get("place", {}).get("location", {})
103
+
104
+ lat1, lon1 = prev_loc.get("late"), prev_loc.get("long")
105
+ lat2, lon2 = curr_loc.get("late"), curr_loc.get("long")
106
+
107
+ # Calculate travel time (default to 15 minutes if coordinates are missing)
108
+ if None not in (lat1, lon1, lat2, lon2):
109
+ travel_minutes = get_travel_time_minutes(lat1, lon1, lat2, lon2)
110
+ else:
111
+ travel_minutes = 15
112
+
113
+ # Current activity starts after previous ends + travel time
114
+ curr_start = current_end + timedelta(minutes=travel_minutes)
115
+ curr_duration = curr_act.get("estimated_duration_hours", 1.0)
116
+ curr_end = curr_start + timedelta(hours=curr_duration)
117
+
118
+ curr_act["time_range"] = (
119
+ f"{format_time_obj(curr_start)} - {format_time_obj(curr_end)}"
120
+ )
121
+
122
+ # Advance pointers
123
+ current_end = curr_end
124
+
125
+ return itinerary_dict