cicboy commited on
Commit
7d0a287
·
1 Parent(s): 7682476

Update tools and itinerary generation

Browse files
app.py CHANGED
@@ -12,11 +12,10 @@ load_dotenv()
12
  print("🔍 GOOGLE_MAPS_API_KEY found:", bool(os.getenv("GOOGLE_MAPS_API_KEY")))
13
  print("🔍 OPENAI_API_KEY found:", bool(os.getenv("OPENAI_API_KEY")))
14
 
15
- # ✅ Ensure LiteLLM knows you’re using OpenAI models
16
  os.environ["LITELLM_PROVIDER"] = "openai"
17
  os.environ["OPENAI_API_BASE"] = "https://api.openai.com/v1"
18
 
19
- # Confirm key is available
20
  if not os.getenv("OPENAI_API_KEY"):
21
  raise ValueError("Missing OPENAI_API_KEY")
22
  if not os.getenv("GOOGLE_MAPS_API_KEY"):
@@ -56,7 +55,7 @@ retriever_agent = Agent(
56
  tools=[maps_tool],
57
  llm="gpt-4o-mini",
58
  temperature=0.2,
59
- verbose=True
60
  )
61
 
62
  weather_agent = Agent(
@@ -66,7 +65,7 @@ weather_agent = Agent(
66
  tools=[weather_tool],
67
  llm="gpt-4o-mini",
68
  temperature=0.2,
69
- verbose=True
70
  )
71
 
72
  route_agent = Agent(
@@ -74,9 +73,9 @@ route_agent = Agent(
74
  goal="Calculate walking, driving, cycling, public transport times between trip locations to optimize daily routes.",
75
  backstory="A navigtion expert skilled at minimizing time and building efficient routes.",
76
  tools=[route_tool],
77
- llm="gpt-5-mini",
78
  temperature=0.25,
79
- verbose=True
80
  )
81
 
82
  planner_agent = Agent(
@@ -91,7 +90,7 @@ planner_agent = Agent(
91
  temperature=0.3,
92
  output_schema=ItineraryModel,
93
  reasoning=True,
94
- verbose=True
95
  )
96
 
97
  writer_agent = Agent(
@@ -108,15 +107,20 @@ writer_agent = Agent(
108
  ),
109
  llm="gpt-4o",
110
  temperature=0.7,
111
- verbose=True
112
  )
113
 
114
  # Core logic
115
  def generate_itinerary(location, start_date, end_date, preferences, transport_modes ):
116
- trip_duration_days = len(expand_dates(start_date, end_date))
117
- transport_modes_str = ', '.join(transport_modes) if isinstance(transport_modes, list) else transport_modes
 
 
 
 
 
118
 
119
- #Define the tasks
120
 
121
  retrieval_task = Task(
122
  description=f"Gather restaurants, landmarks, and activities for the trip in {location}.",
@@ -133,13 +137,18 @@ def generate_itinerary(location, start_date, end_date, preferences, transport_mo
133
  )
134
 
135
  route_task = Task(
136
- description=(f"Using the candidate places retrieved, compute optimal routes using the following transport modes: {transport_modes_str}. "
137
- "Use walking for short distances (<2km) and public transport for longer ones. "
138
- "Return route data including mode, distance, and estimated time."
 
 
 
 
 
139
  ),
140
- expected_output="A list of routes with mode, distance and duration in minutes.",
141
  agent=route_agent,
142
- inputs = {"mode": {transport_modes_str}}
143
  )
144
 
145
  planning_task = Task(
@@ -174,9 +183,40 @@ def generate_itinerary(location, start_date, end_date, preferences, transport_mo
174
  " - weather_forecast (if applicable)\n"
175
  " - distance_from_prev (km)\n"
176
  " - travel_duration_min (if applicable)\n\n"
177
- "🔟 Return a structured JSON object grouped by day with weather, schedule, and reasoning "
178
- "following the ItineraryModel format. Ensure the JSON output strictly follows the ItineraryModel schema fields "
179
- "(days[], events[], metadata, reasoning)."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
  ),
181
  expected_output="A structured JSON itinerary with complete metadata and travel-aware timestamps for each activity.",
182
  context=[retrieval_task, weather_task, route_task],
@@ -190,9 +230,9 @@ def generate_itinerary(location, start_date, end_date, preferences, transport_mo
190
  description=(
191
  "You are a professional travel writer. Given a structured itinerary JSON with detailed fields "
192
  "(rating, reasoning, distance_from_prev, weather_forecast), write an engaging Markdown itinerary.\n\n"
193
- "Make sure the itinerary is covering the period {start_date} to {end_date}.\n\n"
194
  "At the top of the Markdown output, include a summary line like:\n"
195
- "'**Trip Dates:** {start_date} → {end_date} \n"
196
  "Each day must include:\n"
197
  "- The date and weather summary from the JSON.\n"
198
  "- Chronological itinerary entries with start and end times.\n"
@@ -229,15 +269,10 @@ def generate_itinerary(location, start_date, end_date, preferences, transport_mo
229
  })
230
 
231
  # Safely extract the result
232
- if hasattr(writing_task.output, "result"):
233
- markdown_itinerary = writing_task.output.result
234
- else:
235
- markdown_itinerary = str(writing_task.output) if writing_task.output else "⚠️ No Markdown itinerary generated."
236
-
237
- # Ensure final output is always a string
238
- if not isinstance(markdown_itinerary, str):
239
- markdown_itinerary = str(markdown_itinerary)
240
-
241
  return markdown_itinerary
242
 
243
  # Gradio UI
 
12
  print("🔍 GOOGLE_MAPS_API_KEY found:", bool(os.getenv("GOOGLE_MAPS_API_KEY")))
13
  print("🔍 OPENAI_API_KEY found:", bool(os.getenv("OPENAI_API_KEY")))
14
 
 
15
  os.environ["LITELLM_PROVIDER"] = "openai"
16
  os.environ["OPENAI_API_BASE"] = "https://api.openai.com/v1"
17
 
18
+ # Confirm key is available
19
  if not os.getenv("OPENAI_API_KEY"):
20
  raise ValueError("Missing OPENAI_API_KEY")
21
  if not os.getenv("GOOGLE_MAPS_API_KEY"):
 
55
  tools=[maps_tool],
56
  llm="gpt-4o-mini",
57
  temperature=0.2,
58
+ verbose=False
59
  )
60
 
61
  weather_agent = Agent(
 
65
  tools=[weather_tool],
66
  llm="gpt-4o-mini",
67
  temperature=0.2,
68
+ verbose=False
69
  )
70
 
71
  route_agent = Agent(
 
73
  goal="Calculate walking, driving, cycling, public transport times between trip locations to optimize daily routes.",
74
  backstory="A navigtion expert skilled at minimizing time and building efficient routes.",
75
  tools=[route_tool],
76
+ llm="gpt-4o-mini",
77
  temperature=0.25,
78
+ verbose=False
79
  )
80
 
81
  planner_agent = Agent(
 
90
  temperature=0.3,
91
  output_schema=ItineraryModel,
92
  reasoning=True,
93
+ verbose=False
94
  )
95
 
96
  writer_agent = Agent(
 
107
  ),
108
  llm="gpt-4o",
109
  temperature=0.7,
110
+ verbose=False
111
  )
112
 
113
  # Core logic
114
  def generate_itinerary(location, start_date, end_date, preferences, transport_modes ):
115
+ days, date_list = expand_dates(start_date, end_date)
116
+ trip_duration_days = days
117
+
118
+ if isinstance(transport_modes, str):
119
+ transport_modes = [transport_modes]
120
+
121
+ transport_modes_str = ", ".join(transport_modes)
122
 
123
+ #Define the tasks
124
 
125
  retrieval_task = Task(
126
  description=f"Gather restaurants, landmarks, and activities for the trip in {location}.",
 
137
  )
138
 
139
  route_task = Task(
140
+ description=(
141
+ f"From the RetrieverTask output, build a destinations list (strings) using the "
142
+ f"top 10 places across categories (prefer formatted address if present, else name + city). "
143
+ f"Use origin='{location}'. Then call Route Planner Tool exactly once with:\n"
144
+ f"- origin: '{location}'\n"
145
+ f"- destinations: <that list>\n"
146
+ f"- modes: {transport_modes}\n"
147
+ f"Return the tool output as JSON."
148
  ),
149
+ expected_output="A JSON list of routes with mode, distance_km, duration_min, destination.",
150
  agent=route_agent,
151
+ context=[retrieval_task]
152
  )
153
 
154
  planning_task = Task(
 
183
  " - weather_forecast (if applicable)\n"
184
  " - distance_from_prev (km)\n"
185
  " - travel_duration_min (if applicable)\n\n"
186
+ "🔟 Output format (MUST match ItineraryModel exactly):\n"
187
+ "{\n"
188
+ ' "destination": "<city/region>",\n'
189
+ ' "trip_duration_days": <int>,\n'
190
+ ' "transport_mode": ["walking", "public_transport", "driving", "cycling"],\n'
191
+ ' "start_date": "YYYY-MM-DD",\n'
192
+ ' "end_date": "YYYY-MM-DD",\n'
193
+ ' "traveler_profile": "<short preference summary>",\n'
194
+ ' "days": [\n'
195
+ " {\n"
196
+ ' "date": "YYYY-MM-DD",\n'
197
+ ' "weather_summary": "<string>",\n'
198
+ ' "summary": "<string>",\n'
199
+ ' "activities": [\n'
200
+ " {\n"
201
+ ' "name": "<place name>",\n'
202
+ ' "category": "<breakfast|lunch|dinner|museum|park|landmark|...>",\n'
203
+ ' "start_time": "HH:MM",\n'
204
+ ' "end_time": "HH:MM",\n'
205
+ ' "location": "<address>",\n'
206
+ ' "map_url": "<optional>",\n'
207
+ ' "rating": <optional float>,\n'
208
+ ' "reasoning": "<optional>",\n'
209
+ ' "weather_forecast": "<optional>",\n'
210
+ ' "distance_from_prev": <optional float>,\n'
211
+ ' "duration_minutes": <optional int>\n'
212
+ " }\n"
213
+ " ]\n"
214
+ " }\n"
215
+ " ],\n"
216
+ ' "total_distance_km": <optional float>,\n'
217
+ ' "notes": "<optional>"\n'
218
+ "}\n"
219
+ "⚠️ Use field name 'activities' (NOT events). Use 'duration_minutes' (NOT travel_duration_min)."
220
  ),
221
  expected_output="A structured JSON itinerary with complete metadata and travel-aware timestamps for each activity.",
222
  context=[retrieval_task, weather_task, route_task],
 
230
  description=(
231
  "You are a professional travel writer. Given a structured itinerary JSON with detailed fields "
232
  "(rating, reasoning, distance_from_prev, weather_forecast), write an engaging Markdown itinerary.\n\n"
233
+ f"Make sure the itinerary is covering the period {start_date} to {end_date}.\n\n"
234
  "At the top of the Markdown output, include a summary line like:\n"
235
+ f"'**Trip Dates:** {start_date} → {end_date} \n"
236
  "Each day must include:\n"
237
  "- The date and weather summary from the JSON.\n"
238
  "- Chronological itinerary entries with start and end times.\n"
 
269
  })
270
 
271
  # Safely extract the result
272
+ markdown_itinerary = (
273
+ result if isinstance(result, str)
274
+ else getattr(result, "raw", None) or str(result)
275
+ )
 
 
 
 
 
276
  return markdown_itinerary
277
 
278
  # Gradio UI
models/itinerary_model.py CHANGED
@@ -14,10 +14,10 @@ class Activity(BaseModel):
14
  rating: Optional[float] = Field(None, description="Average rating score (if available)")
15
  reasoning: Optional[str] = Field(None, description="Reason why this activity was selected")
16
  weather_forecast: Optional[str] = Field(None, description="Expected weather at this time/location")
 
17
  distance_from_prev: Optional[float] = Field(None, description="Distance from previous activity in kilometers")
18
  duration_minutes: Optional[int] = Field(None, description="Automatically computed duration in minutes.")
19
 
20
-
21
  # 📅 Daily itinerary
22
  class DayPlan(BaseModel):
23
  date: str = Field(..., description="Date of the plan, ISO format YYYY-MM-DD")
@@ -25,12 +25,11 @@ class DayPlan(BaseModel):
25
  summary: Optional[str] = Field(None, description="High-level overview of the day")
26
  activities: List[Activity] = Field(..., description="Ordered list of daily activities and meals")
27
 
28
-
29
  # 🌍 Entire trip plan
30
  class ItineraryModel(BaseModel):
31
  destination: str = Field(..., description="City or region of the trip")
32
  trip_duration_days: int = Field(..., description="Number of days in the itinerary")
33
- transport_mode: Optional[str] = Field(None, description="User's preferred mode of transport (walking, public_transport, driving)")
34
  start_date: Optional[str] = Field(None, description="Trip start date (ISO format)")
35
  end_date: Optional[str] = Field(None, description="End date (YYYY-MM-DD).")
36
  traveler_profile: Optional[str] = Field(None, description="Short description of traveler preferences.")
 
14
  rating: Optional[float] = Field(None, description="Average rating score (if available)")
15
  reasoning: Optional[str] = Field(None, description="Reason why this activity was selected")
16
  weather_forecast: Optional[str] = Field(None, description="Expected weather at this time/location")
17
+ travel_mode: Optional[str] = Field(None, description="Mode used to reach this activity (walking/public_transport/driving/cycling)")
18
  distance_from_prev: Optional[float] = Field(None, description="Distance from previous activity in kilometers")
19
  duration_minutes: Optional[int] = Field(None, description="Automatically computed duration in minutes.")
20
 
 
21
  # 📅 Daily itinerary
22
  class DayPlan(BaseModel):
23
  date: str = Field(..., description="Date of the plan, ISO format YYYY-MM-DD")
 
25
  summary: Optional[str] = Field(None, description="High-level overview of the day")
26
  activities: List[Activity] = Field(..., description="Ordered list of daily activities and meals")
27
 
 
28
  # 🌍 Entire trip plan
29
  class ItineraryModel(BaseModel):
30
  destination: str = Field(..., description="City or region of the trip")
31
  trip_duration_days: int = Field(..., description="Number of days in the itinerary")
32
+ transport_modes: List[str] = Field(default_factory=list, description="Allowed modes: walking, public_transport, driving, cycling")
33
  start_date: Optional[str] = Field(None, description="Trip start date (ISO format)")
34
  end_date: Optional[str] = Field(None, description="End date (YYYY-MM-DD).")
35
  traveler_profile: Optional[str] = Field(None, description="Short description of traveler preferences.")
tools/google_maps_tool.py CHANGED
@@ -1,9 +1,9 @@
1
  import os
2
  import httpx
3
  from typing import List, Dict, Optional
4
- from crewai_tools import RagTool
5
 
6
- class GoogleMapsTool(RagTool):
7
  """
8
  CrewAI compatible toool for querying Google Places Api
9
  """
 
1
  import os
2
  import httpx
3
  from typing import List, Dict, Optional
4
+ from crewai.tools import BaseTool
5
 
6
+ class GoogleMapsTool(BaseTool):
7
  """
8
  CrewAI compatible toool for querying Google Places Api
9
  """
tools/route_planner_tool.py CHANGED
@@ -1,10 +1,10 @@
1
  import os
2
  import httpx
3
  from typing import List, Dict, Union, Optional
4
- from crewai_tools.tools import RagTool
5
 
6
 
7
- class RoutePlannerTool(RagTool):
8
  """
9
  CrewAI-compatible tool for computing optimal routes between locations
10
  using Google Directions API with multi-mode transport support.
@@ -47,49 +47,57 @@ class RoutePlannerTool(RagTool):
47
  base_url = "https://maps.googleapis.com/maps/api/directions/json"
48
  results = []
49
 
50
- for dest in destinations[:max_results]:
51
- best_route = None
52
-
53
- # Try walking first
54
- if "walking" in modes:
55
- walk_params = {"origin": origin, "destination": dest, "mode": "walking", "key": api_key}
56
- walk_data = self._fetch_route(base_url, walk_params)
57
- if walk_data:
58
- best_route = {**walk_data, "mode": "walking"}
59
-
60
- # Try public transport if walking route > 2km or not available
61
- if ("public_transport" in modes or "transit" in modes) and (
62
- not best_route or best_route["distance_km"] > 2
63
- ):
64
- transit_params = {
65
- "origin": origin,
66
- "destination": dest,
67
- "mode": "transit",
68
- "transit_mode": "bus|subway|train|tram",
69
- "key": api_key
70
- }
71
- transit_data = self._fetch_route(base_url, transit_params)
72
- if transit_data:
73
- if not best_route or transit_data["duration_min"] < best_route["duration_min"]:
74
- best_route = {**transit_data, "mode": "public_transport"}
75
-
76
- # Try driving if included and others unavailable
77
- if "driving" in modes and not best_route:
78
- drive_params = {"origin": origin, "destination": dest, "mode": "driving", "key": api_key}
79
- drive_data = self._fetch_route(base_url, drive_params)
80
- if drive_data:
81
- best_route = {**drive_data, "mode": "driving"}
82
-
83
- if best_route:
84
- best_route["destination"] = dest
85
- results.append(best_route)
86
-
87
- return results
88
-
89
- def _fetch_route(self, base_url: str, params: Dict) -> Optional[Dict]:
 
 
 
 
 
 
 
 
90
  """Fetch and parse route data from Google Directions API."""
91
  try:
92
- response = httpx.get(base_url, params=params, timeout=20.0)
93
  data = response.json()
94
 
95
  if data["status"] != "OK" or not data.get("routes"):
 
1
  import os
2
  import httpx
3
  from typing import List, Dict, Union, Optional
4
+ from crewai.tools import BaseTool
5
 
6
 
7
+ class RoutePlannerTool(BaseTool):
8
  """
9
  CrewAI-compatible tool for computing optimal routes between locations
10
  using Google Directions API with multi-mode transport support.
 
47
  base_url = "https://maps.googleapis.com/maps/api/directions/json"
48
  results = []
49
 
50
+ with httpx.Client(timeout=20.0) as client:
51
+ for dest in destinations[:max_results]:
52
+ best_route = None
53
+
54
+ # Try walking first
55
+ if "walking" in modes:
56
+ walk_params = {"origin": origin, "destination": dest, "mode": "walking", "key": api_key}
57
+ walk_data = self._fetch_route(base_url, walk_params)
58
+ if walk_data:
59
+ best_route = {**walk_data, "mode": "walking"}
60
+
61
+ # Try public transport if walking route > 2km or not available
62
+ if ("public_transport" in modes or "transit" in modes) and (
63
+ not best_route or best_route["distance_km"] > 2
64
+ ):
65
+ transit_params = {
66
+ "origin": origin,
67
+ "destination": dest,
68
+ "mode": "transit",
69
+ "transit_mode": "bus|subway|train|tram",
70
+ "key": api_key
71
+ }
72
+ transit_data = self._fetch_route(base_url, transit_params)
73
+ if transit_data:
74
+ if not best_route or transit_data["duration_min"] < best_route["duration_min"]:
75
+ best_route = {**transit_data, "mode": "public_transport"}
76
+
77
+ # Cycling if included
78
+ if "cycling" in modes and not best_route:
79
+ bike_params = {"origin": origin, "destination": dest, "mode": "bicycling", "key": api_key}
80
+ bike_data = self._fetch_route(client, base_url, bike_params)
81
+ if bike_data:
82
+ best_route = {**bike_data, "mode": "cycling"}
83
+
84
+ # Try driving if included and others unavailable
85
+ if "driving" in modes and not best_route:
86
+ drive_params = {"origin": origin, "destination": dest, "mode": "driving", "key": api_key}
87
+ drive_data = self._fetch_route(base_url, drive_params)
88
+ if drive_data:
89
+ best_route = {**drive_data, "mode": "driving"}
90
+
91
+ if best_route:
92
+ best_route["destination"] = dest
93
+ results.append(best_route)
94
+
95
+ return results
96
+
97
+ def _fetch_route(self, client: httpx.Client, base_url: str, params: Dict) -> Optional[Dict]:
98
  """Fetch and parse route data from Google Directions API."""
99
  try:
100
+ response = client.get(base_url, params=params, timeout=20.0)
101
  data = response.json()
102
 
103
  if data["status"] != "OK" or not data.get("routes"):
tools/semantic_ranking_tool.py CHANGED
@@ -2,13 +2,13 @@ import os
2
  import numpy as np
3
  from typing import List, Dict, Any, Optional
4
  from openai import OpenAI
5
- from crewai_tools import RagTool
6
  from dotenv import load_dotenv
7
 
8
  load_dotenv()
9
  os.environ["CHROMA_OPENAI_API_KEY"] = os.getenv("OPENAI_API_KEY")
10
 
11
- class SemanticRankingTool(RagTool):
12
  """
13
  CrewAI-compatible tool that semantically ranks and filters candidate places
14
  based on user preferences, ratings, popularity, and optional distance.
 
2
  import numpy as np
3
  from typing import List, Dict, Any, Optional
4
  from openai import OpenAI
5
+ from crewai.tools import BaseTool
6
  from dotenv import load_dotenv
7
 
8
  load_dotenv()
9
  os.environ["CHROMA_OPENAI_API_KEY"] = os.getenv("OPENAI_API_KEY")
10
 
11
+ class SemanticRankingTool(BaseTool):
12
  """
13
  CrewAI-compatible tool that semantically ranks and filters candidate places
14
  based on user preferences, ratings, popularity, and optional distance.
tools/weather_tool.py CHANGED
@@ -1,5 +1,5 @@
1
  import httpx
2
- from crewai_tools import RagTool
3
  from pydantic import BaseModel, Field
4
  from typing import Dict, Optional, Any, Type
5
  from datetime import date, timedelta
@@ -11,10 +11,11 @@ class WeatherToolInput(BaseModel):
11
  city: Optional[str] = Field(None, description="City name, e.g. 'Florence, Italy'.")
12
  latitude: Optional[float] = Field(None, description="Latitude in decimal degrees.")
13
  longitude: Optional[float] = Field(None, description="Longitude in decimal degrees.")
14
-
 
15
 
16
  # 🌤️ CrewAI Tool
17
- class WeatherTool(RagTool):
18
  """Fetches multi-day weather forecasts using the Open-Meteo API."""
19
 
20
  name: str = "Weather Forecast Tool"
@@ -65,6 +66,10 @@ class WeatherTool(RagTool):
65
  "timezone": "auto",
66
  }
67
 
 
 
 
 
68
  resp = httpx.get(base_url, params=params, timeout=10.0)
69
  data = resp.json()
70
  days = data.get("daily", {})
 
1
  import httpx
2
+ from crewai.tools import BaseTool
3
  from pydantic import BaseModel, Field
4
  from typing import Dict, Optional, Any, Type
5
  from datetime import date, timedelta
 
11
  city: Optional[str] = Field(None, description="City name, e.g. 'Florence, Italy'.")
12
  latitude: Optional[float] = Field(None, description="Latitude in decimal degrees.")
13
  longitude: Optional[float] = Field(None, description="Longitude in decimal degrees.")
14
+ start_date: Optional[str] = Field(None, description="YYYY-MM-DD")
15
+ end_date: Optional[str] = Field(None, description="YYYY-MM-DD")
16
 
17
  # 🌤️ CrewAI Tool
18
+ class WeatherTool(BaseTool):
19
  """Fetches multi-day weather forecasts using the Open-Meteo API."""
20
 
21
  name: str = "Weather Forecast Tool"
 
66
  "timezone": "auto",
67
  }
68
 
69
+ if start_date and end_date:
70
+ params["start_date"] = start_date
71
+ params["end_date"] = end_date
72
+
73
  resp = httpx.get(base_url, params=params, timeout=10.0)
74
  data = resp.json()
75
  days = data.get("daily", {})