dpaul93 commited on
Commit
45d9925
·
verified ·
1 Parent(s): eca8184

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +801 -0
app.py ADDED
@@ -0,0 +1,801 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Multi-Agent Travel Planning System
3
+ A LangGraph-based travel assistant with specialized agents for flights, hotels, and itineraries.
4
+ """
5
+
6
+ import os
7
+ import json
8
+ from typing import TypedDict, Annotated, List, Optional, Union
9
+ import operator
10
+ from dotenv import load_dotenv
11
+ import gradio as gr
12
+ import uuid
13
+
14
+ # Load environment variables
15
+ load_dotenv()
16
+
17
+ # Core imports
18
+ from langchain_google_genai import ChatGoogleGenerativeAI
19
+ from langchain_core.messages import HumanMessage, AIMessage, BaseMessage, ToolMessage
20
+ from langchain_core.output_parsers import StrOutputParser
21
+ from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
22
+
23
+ # LangGraph imports
24
+ from langgraph.graph import StateGraph, END
25
+ from langgraph.checkpoint.memory import InMemorySaver
26
+
27
+ # Tool imports
28
+ from langchain_tavily import TavilySearch
29
+ from langchain_core.tools import tool
30
+ import serpapi
31
+
32
+
33
+ class TravelPlannerState(TypedDict):
34
+ """State schema for travel multiagent system"""
35
+ messages: Annotated[List[BaseMessage], operator.add]
36
+ next_agent: Optional[str]
37
+ user_query: Optional[str]
38
+
39
+
40
+ class TravelPlannerApp:
41
+ """Main travel planner application class"""
42
+
43
+ def __init__(self):
44
+ # Check for required environment variables
45
+ required_vars = ['GOOGLE_API_KEY', 'TAVILY_API_KEY', 'SERPAPI_API_KEY']
46
+ missing_vars = [var for var in required_vars if not os.environ.get(var)]
47
+
48
+ if missing_vars:
49
+ raise ValueError(f"Missing required environment variables: {', '.join(missing_vars)}")
50
+
51
+ self.llm = self._setup_llm()
52
+ self.tools = self._setup_tools()
53
+ self.agents = self._setup_agents()
54
+ self.router = self._create_router()
55
+ self.workflow = self._build_workflow()
56
+
57
+ def _setup_llm(self):
58
+ """Initialize the LLM"""
59
+ return ChatGoogleGenerativeAI(
60
+ model="gemini-2.0-flash-exp",
61
+ temperature=0.2,
62
+ google_api_key=os.environ.get("GOOGLE_API_KEY")
63
+ )
64
+
65
+ def _setup_tools(self):
66
+ """Setup external tools"""
67
+ # Tavily search tool
68
+ tavily_tool = TavilySearch(max_results=2)
69
+
70
+ # Define SERP API tools using @tool decorator
71
+ @tool
72
+ def search_flights(departure_airport: str, arrival_airport: str,
73
+ outbound_date: str, return_date: str = None,
74
+ adults: int = 1, children: int = 0) -> str:
75
+ """Search for flights using Google Flights engine via SERP API"""
76
+ return self._search_flights(departure_airport, arrival_airport,
77
+ outbound_date, return_date, adults, children)
78
+
79
+ @tool
80
+ def search_hotels(location: str, check_in_date: str, check_out_date: str,
81
+ adults: int = 1, children: int = 0, rooms: int = 1,
82
+ hotel_class: str = None, sort_by: int = 8) -> str:
83
+ """Search for hotels using Google Hotels engine via SERP API"""
84
+ return self._search_hotels(location, check_in_date, check_out_date,
85
+ adults, children, rooms, hotel_class, sort_by)
86
+
87
+ return {
88
+ "tavily": tavily_tool,
89
+ "search_flights": search_flights,
90
+ "search_hotels": search_hotels
91
+ }
92
+
93
+ def _search_flights(self, departure_airport: str, arrival_airport: str,
94
+ outbound_date: str, return_date: str = None,
95
+ adults: int = 1, children: int = 0) -> str:
96
+ """Search for flights using Google Flights engine via SERP API"""
97
+ try:
98
+ params = {
99
+ 'api_key': os.environ.get('SERPAPI_API_KEY'),
100
+ 'engine': 'google_flights',
101
+ 'hl': 'en',
102
+ 'gl': 'us',
103
+ 'departure_id': departure_airport,
104
+ 'arrival_id': arrival_airport,
105
+ 'outbound_date': outbound_date,
106
+ 'currency': 'USD',
107
+ 'adults': adults,
108
+ 'children': children,
109
+ }
110
+
111
+ # Set trip type based on return_date
112
+ if return_date:
113
+ params['return_date'] = return_date
114
+ params['type'] = '1' # Round trip
115
+ else:
116
+ params['type'] = '2' # One way
117
+
118
+ print(f"🔍 Searching flights with params: {params}")
119
+
120
+ # Add timeout to prevent hanging
121
+ import time
122
+ start_time = time.time()
123
+
124
+ search = serpapi.search(params)
125
+
126
+ elapsed = time.time() - start_time
127
+ print(f"⏱️ Search completed in {elapsed:.2f} seconds")
128
+
129
+ if not search.data:
130
+ return "No search results returned from SERP API"
131
+
132
+ # Try different result keys depending on trip type
133
+ possible_keys = ['best_flights', 'other_flights', 'flights']
134
+ results = None
135
+
136
+ for key in possible_keys:
137
+ if key in search.data and search.data[key]:
138
+ results = search.data[key]
139
+ break
140
+
141
+ if not results:
142
+ available_keys = list(search.data.keys())
143
+ return f"No flights found. Available data keys: {available_keys}"
144
+
145
+ return json.dumps(results, indent=2)
146
+
147
+ except Exception as e:
148
+ error_msg = f"Flight search failed: {str(e)}"
149
+ print(f"❌ {error_msg}")
150
+ return error_msg
151
+
152
+ def _search_hotels(self, location: str, check_in_date: str, check_out_date: str,
153
+ adults: int = 1, children: int = 0, rooms: int = 1,
154
+ hotel_class: str = None, sort_by: int = 8) -> str:
155
+ """Search for hotels using Google Hotels engine via SERP API"""
156
+ try:
157
+ adults = int(float(adults)) if adults else 1
158
+ children = int(float(children)) if children else 0
159
+ rooms = int(float(rooms)) if rooms else 1
160
+ sort_by = int(float(sort_by)) if sort_by else 8
161
+
162
+ params = {
163
+ 'api_key': os.environ.get('SERPAPI_API_KEY'),
164
+ 'engine': 'google_hotels',
165
+ 'hl': 'en',
166
+ 'gl': 'us',
167
+ 'q': location,
168
+ 'check_in_date': check_in_date,
169
+ 'check_out_date': check_out_date,
170
+ 'currency': 'USD',
171
+ 'adults': adults,
172
+ 'children': children,
173
+ 'rooms': rooms,
174
+ 'sort_by': sort_by
175
+ }
176
+
177
+ if hotel_class:
178
+ params['hotel_class'] = hotel_class
179
+
180
+ print(f"🔍 Searching hotels with params: {params}")
181
+
182
+ # Add timeout to prevent hanging
183
+ import time
184
+ start_time = time.time()
185
+
186
+ search = serpapi.search(params)
187
+
188
+ elapsed = time.time() - start_time
189
+ print(f"⏱️ Search completed in {elapsed:.2f} seconds")
190
+
191
+ if not search.data:
192
+ return "No search results returned from SERP API"
193
+
194
+ properties = search.data.get('properties', [])
195
+
196
+ if not properties:
197
+ available_keys = list(search.data.keys())
198
+ return f"No hotels found in results. Available data keys: {available_keys}"
199
+
200
+ # Return formatted results
201
+ results = []
202
+ for hotel in properties[:5]: # Top 5 results
203
+ hotel_info = {
204
+ 'name': hotel.get('name', 'Unknown'),
205
+ 'price': hotel.get('rate_per_night', 'Price not available'),
206
+ 'rating': hotel.get('overall_rating', 'No rating'),
207
+ 'description': hotel.get('description', 'No description'),
208
+ 'amenities': hotel.get('amenities', [])
209
+ }
210
+ results.append(hotel_info)
211
+
212
+ return json.dumps(results, indent=2)
213
+
214
+ except Exception as e:
215
+ error_msg = f"Hotel search failed: {str(e)}"
216
+ print(f"❌ {error_msg}")
217
+ return error_msg
218
+
219
+ def _setup_agents(self):
220
+ """Setup all specialized agents"""
221
+
222
+ # Itinerary Agent
223
+ itinerary_prompt = ChatPromptTemplate.from_messages([
224
+ ("system", """You are an expert travel itinerary planner. ONLY respond to travel planning and itinerary-related questions.
225
+
226
+ IMPORTANT RULES:
227
+ - If asked about non-travel topics (weather, math, general questions), politely decline and redirect to travel planning
228
+ - Always provide complete, well-formatted itineraries with specific details
229
+ - Include timing, locations, transportation, and practical tips
230
+
231
+ Use the ReAct approach:
232
+ 1. THOUGHT: Analyze what travel information is needed
233
+ 2. ACTION: Search for current information about destinations, attractions, prices, hours
234
+ 3. OBSERVATION: Process the search results
235
+ 4. Provide a comprehensive, formatted response
236
+
237
+ Available tools:
238
+ - tavily_search_results_json: Search for current travel information
239
+
240
+ Format your itineraries with:
241
+ - Clear day-by-day breakdown
242
+ - Specific times and locations
243
+ - Transportation between locations
244
+ - Estimated costs when possible
245
+ - Practical tips and recommendations"""),
246
+ MessagesPlaceholder(variable_name="messages"),
247
+ ])
248
+
249
+ # Flight Agent
250
+ flight_prompt = ChatPromptTemplate.from_messages([
251
+ ("system", """You are a flight booking expert. ONLY respond to flight-related queries.
252
+
253
+ IMPORTANT RULES:
254
+ - If asked about non-flight topics, politely decline and redirect to flight booking
255
+ - Always use the search_flights tool to find current flight information
256
+ - For one-way flights: only provide departure_airport, arrival_airport, and outbound_date
257
+ - For round-trip flights: include return_date parameter
258
+ - CRITICAL: When parsing dates, pay attention to the year mentioned by the user
259
+ - If no year is specified, assume the current year (2025)
260
+ - Format dates as YYYY-MM-DD (e.g., 2025-07-15 for July 15, 2025)
261
+
262
+ Available tools:
263
+ - search_flights: Search for comprehensive flight data
264
+
265
+ Parameters for search_flights:
266
+ - departure_airport: 3-letter airport code (e.g., "DEL", "JFK")
267
+ - arrival_airport: 3-letter airport code (e.g., "LHR", "LAX", "DXB")
268
+ - outbound_date: Date in YYYY-MM-DD format (IMPORTANT: Use correct year!)
269
+ - return_date: Optional, only for round-trip flights
270
+ - adults: Number of adult passengers (default: 1)
271
+ - children: Number of child passengers (default: 0)
272
+
273
+ Examples:
274
+ - "15 Jul 2025" → "2025-07-15"
275
+ - "July 15, 2025" → "2025-07-15"
276
+ - "15th July 2025" → "2025-07-15"
277
+ - "15 Jul" (no year specified) → "2025-07-15"
278
+
279
+ Process:
280
+ 1. ALWAYS search for flights first using the tool
281
+ 2. Analyze the results to find flights matching user preferences
282
+ 3. Present organized results with clear recommendations
283
+
284
+ Airport code mapping:
285
+ - Delhi: DEL
286
+ - London Heathrow: LHR
287
+ - London Gatwick: LGW
288
+ - Dubai: DXB
289
+ - New York JFK: JFK
290
+ - New York LaGuardia: LGA
291
+ - New York Newark: EWR
292
+ - etc."""),
293
+ MessagesPlaceholder(variable_name="messages"),
294
+ ])
295
+
296
+ # Hotel Agent
297
+ hotel_prompt = ChatPromptTemplate.from_messages([
298
+ ("system", """You are a hotel booking expert. ONLY respond to hotel and accommodation-related queries.
299
+
300
+ IMPORTANT RULES:
301
+ - If asked about non-hotel topics, politely decline and redirect to hotel booking
302
+ - Always use the search_hotels tool to find current hotel information
303
+ - Provide detailed hotel options with prices, ratings, amenities, and location details
304
+ - Include practical booking advice and tips
305
+ - You CAN search and analyze results for different criteria like star ratings, price ranges, amenities
306
+
307
+ Available tools:
308
+ - search_hotels: Search for hotels using Google Hotels engine
309
+
310
+ When searching hotels:
311
+ - If check-out date is not provided in the initial request, assume a 1-night stay (add 1 day to check-in date)
312
+ - Always proceed with the search even if some details are missing
313
+ - Format dates as YYYY-MM-DD
314
+
315
+ For hotel searches, you need:
316
+ - Location/destination
317
+ - Check-in date (YYYY-MM-DD format)
318
+ - Check-out date (YYYY-MM-DD format)
319
+ - Number of guests (adults, children)
320
+ - Number of rooms
321
+ - Hotel preferences (star rating, amenities, etc.)
322
+
323
+ Present results with:
324
+ - Hotel name and star rating
325
+ - Price per night and total cost
326
+ - Key amenities and features
327
+ - Location and nearby attractions
328
+ - Booking recommendations
329
+
330
+ If user provides a follow-up response after asking for clarification, immediately proceed with the hotel search using all available information."""),
331
+ MessagesPlaceholder(variable_name="messages"),
332
+ ])
333
+
334
+ # Bind tools to agents
335
+ itinerary_agent = itinerary_prompt | self.llm.bind_tools([self.tools["tavily"]])
336
+ flight_agent = flight_prompt | self.llm.bind_tools([self.tools["search_flights"]])
337
+ hotel_agent = hotel_prompt | self.llm.bind_tools([self.tools["search_hotels"]])
338
+
339
+ return {
340
+ "itinerary": itinerary_agent,
341
+ "flight": flight_agent,
342
+ "hotel": hotel_agent
343
+ }
344
+
345
+ def _create_router(self):
346
+ """Create routing logic for agent selection"""
347
+ router_prompt = ChatPromptTemplate.from_messages([
348
+ ("system", """You are a routing expert for a travel planning system.
349
+
350
+ Analyze the user's query and decide which specialist agent should handle it:
351
+
352
+ - FLIGHT: Flight bookings, airlines, air travel, flight search, tickets, airports, departures, arrivals, airline prices
353
+ - HOTEL: Hotels, accommodations, stays, rooms, hotel bookings, lodging, resorts, hotel search, hotel prices
354
+ - ITINERARY: Travel itineraries, trip planning, destinations, activities, attractions, sightseeing, travel advice, weather, culture, food, general travel questions
355
+
356
+ Respond with ONLY one word: FLIGHT, HOTEL, or ITINERARY
357
+
358
+ Examples:
359
+ "Book me a flight to Paris" → FLIGHT
360
+ "Find hotels in Tokyo" → HOTEL
361
+ "Plan my 5-day trip to Italy" → ITINERARY
362
+ "Search flights from NYC to London" → FLIGHT
363
+ "Where should I stay in Bali?" → HOTEL
364
+ "What are the best attractions in Rome?" → ITINERARY
365
+ "I need airline tickets" → FLIGHT
366
+ "Show me hotel options" → HOTEL
367
+ "Create an itinerary for Japan" → ITINERARY"""),
368
+ ("user", "Query: {query}")
369
+ ])
370
+
371
+ router_chain = router_prompt | self.llm | StrOutputParser()
372
+
373
+ def route_query(state):
374
+ """Router function - decides which agent to call next"""
375
+ user_message = state["messages"][-1].content
376
+
377
+ try:
378
+ decision = router_chain.invoke({"query": user_message}).strip().upper()
379
+ agent_mapping = {
380
+ "FLIGHT": "flight_agent",
381
+ "HOTEL": "hotel_agent",
382
+ "ITINERARY": "itinerary_agent"
383
+ }
384
+ next_agent = agent_mapping.get(decision, "itinerary_agent")
385
+ return next_agent
386
+ except Exception:
387
+ return "itinerary_agent"
388
+
389
+ return route_query
390
+
391
+ def _ensure_valid_content(self, content):
392
+ """Ensure content is valid and not empty for Gemini API"""
393
+ if not content:
394
+ return "No results available"
395
+
396
+ # Convert to string if not already
397
+ content_str = str(content)
398
+
399
+ # Check if empty or whitespace only
400
+ if not content_str or not content_str.strip():
401
+ return "No results available"
402
+
403
+ # Ensure minimum length
404
+ if len(content_str.strip()) < 3:
405
+ return f"Limited results: {content_str.strip()}"
406
+
407
+ return content_str
408
+
409
+ def _itinerary_agent_node(self, state: TravelPlannerState):
410
+ """Itinerary planning agent node"""
411
+ messages = state["messages"]
412
+ response = self.agents["itinerary"].invoke({"messages": messages})
413
+
414
+ if hasattr(response, 'tool_calls') and response.tool_calls:
415
+ tool_messages = []
416
+ for tool_call in response.tool_calls:
417
+ if tool_call['name'] == 'tavily_search_results_json':
418
+ try:
419
+ print(f"🔍 Tavily search query: {tool_call['args'].get('query', 'No query')}")
420
+
421
+ # Use the direct search method instead of invoke
422
+ search_query = tool_call['args'].get('query', '')
423
+ if search_query:
424
+ tool_result = self.tools["tavily"].search(search_query, max_results=2)
425
+ else:
426
+ tool_result = "No search query provided"
427
+
428
+ print(f"📋 Tavily raw result: {type(tool_result)} - {str(tool_result)[:200]}...")
429
+
430
+ # Handle different response types
431
+ if isinstance(tool_result, list):
432
+ if len(tool_result) == 0:
433
+ tool_result = "No search results found"
434
+ else:
435
+ tool_result = json.dumps(tool_result, indent=2)
436
+ elif isinstance(tool_result, dict):
437
+ tool_result = json.dumps(tool_result, indent=2)
438
+
439
+ # Ensure valid content for Gemini API
440
+ tool_result = self._ensure_valid_content(tool_result)
441
+
442
+ print(f"✅ Processed tool result length: {len(tool_result)}")
443
+
444
+ except Exception as e:
445
+ print(f"❌ Tavily search error: {e}")
446
+ tool_result = f"Search failed: {str(e)}"
447
+
448
+ tool_messages.append(ToolMessage(
449
+ content=tool_result,
450
+ tool_call_id=tool_call['id']
451
+ ))
452
+
453
+ if tool_messages:
454
+ all_messages = messages + [response] + tool_messages
455
+ try:
456
+ final_response = self.agents["itinerary"].invoke({"messages": all_messages})
457
+ return {"messages": [response] + tool_messages + [final_response]}
458
+ except Exception as e:
459
+ print(f"❌ Error in final response: {e}")
460
+ # Return a fallback response
461
+ fallback_response = self.agents["itinerary"].invoke({"messages": messages})
462
+ return {"messages": [fallback_response]}
463
+
464
+ return {"messages": [response]}
465
+
466
+ def _flight_agent_node(self, state: TravelPlannerState):
467
+ """Flight booking agent node"""
468
+ messages = state["messages"]
469
+ try:
470
+ response = self.agents["flight"].invoke({"messages": messages})
471
+
472
+ if hasattr(response, 'tool_calls') and response.tool_calls:
473
+ tool_messages = []
474
+ for tool_call in response.tool_calls:
475
+ if tool_call['name'] == 'search_flights':
476
+ try:
477
+ print(f"✈️ Flight search with args: {tool_call['args']}")
478
+ tool_result = self.tools["search_flights"].invoke(tool_call['args'])
479
+ # Ensure valid content for Gemini API
480
+ tool_result = self._ensure_valid_content(tool_result)
481
+ print(f"✅ Flight search completed, result length: {len(tool_result)}")
482
+ except Exception as e:
483
+ print(f"❌ Flight search error: {e}")
484
+ tool_result = f"Flight search failed: {str(e)}"
485
+
486
+ tool_messages.append(ToolMessage(
487
+ content=tool_result,
488
+ tool_call_id=tool_call['id']
489
+ ))
490
+
491
+ if tool_messages:
492
+ all_messages = messages + [response] + tool_messages
493
+ try:
494
+ final_response = self.agents["flight"].invoke({"messages": all_messages})
495
+ return {"messages": [response] + tool_messages + [final_response]}
496
+ except Exception as e:
497
+ print(f"❌ Error in flight final response: {e}")
498
+ # Return a fallback response
499
+ fallback_response = self.agents["flight"].invoke({"messages": messages})
500
+ return {"messages": [fallback_response]}
501
+
502
+ return {"messages": [response]}
503
+ except Exception as e:
504
+ print(f"❌ Error in flight agent node: {e}")
505
+ # Create a fallback response
506
+ from langchain_core.messages import AIMessage
507
+ fallback_msg = AIMessage(content=f"I apologize, but I encountered an error while processing your flight request. Please try again with your flight search query.")
508
+ return {"messages": [fallback_msg]}
509
+
510
+ def _hotel_agent_node(self, state: TravelPlannerState):
511
+ """Hotel booking agent node"""
512
+ messages = state["messages"]
513
+ try:
514
+ response = self.agents["hotel"].invoke({"messages": messages})
515
+
516
+ if hasattr(response, 'tool_calls') and response.tool_calls:
517
+ tool_messages = []
518
+ for tool_call in response.tool_calls:
519
+ if tool_call['name'] == 'search_hotels':
520
+ try:
521
+ print(f"🏨 Hotel search with args: {tool_call['args']}")
522
+ tool_result = self.tools["search_hotels"].invoke(tool_call['args'])
523
+ # Ensure valid content for Gemini API
524
+ tool_result = self._ensure_valid_content(tool_result)
525
+ print(f"✅ Hotel search completed, result length: {len(tool_result)}")
526
+ except Exception as e:
527
+ print(f"❌ Hotel search error: {e}")
528
+ tool_result = f"Hotel search failed: {str(e)}"
529
+
530
+ tool_messages.append(ToolMessage(
531
+ content=tool_result,
532
+ tool_call_id=tool_call['id']
533
+ ))
534
+
535
+ if tool_messages:
536
+ all_messages = messages + [response] + tool_messages
537
+ try:
538
+ final_response = self.agents["hotel"].invoke({"messages": all_messages})
539
+ return {"messages": [response] + tool_messages + [final_response]}
540
+ except Exception as e:
541
+ print(f"❌ Error in hotel final response: {e}")
542
+ # Return a fallback response
543
+ fallback_response = self.agents["hotel"].invoke({"messages": messages})
544
+ return {"messages": [fallback_response]}
545
+
546
+ return {"messages": [response]}
547
+ except Exception as e:
548
+ print(f"❌ Error in hotel agent node: {e}")
549
+ # Create a fallback response
550
+ from langchain_core.messages import AIMessage
551
+ fallback_msg = AIMessage(content=f"I apologize, but I encountered an error while processing your hotel request. Please try again with your hotel search query.")
552
+ return {"messages": [fallback_msg]}
553
+
554
+ def _router_node(self, state: TravelPlannerState):
555
+ """Router node - determines which agent should handle the query"""
556
+ user_message = state["messages"][-1].content
557
+ next_agent = self.router(state)
558
+
559
+ return {
560
+ "next_agent": next_agent,
561
+ "user_query": user_message
562
+ }
563
+
564
+ def _route_to_agent(self, state: TravelPlannerState):
565
+ """Conditional edge function - routes to appropriate agent"""
566
+ next_agent = state.get("next_agent")
567
+
568
+ if next_agent == "flight_agent":
569
+ return "flight_agent"
570
+ elif next_agent == "hotel_agent":
571
+ return "hotel_agent"
572
+ elif next_agent == "itinerary_agent":
573
+ return "itinerary_agent"
574
+ else:
575
+ return "itinerary_agent"
576
+
577
+ def _build_workflow(self):
578
+ """Build the complete LangGraph workflow"""
579
+ workflow = StateGraph(TravelPlannerState)
580
+
581
+ # Add nodes
582
+ workflow.add_node("router", self._router_node)
583
+ workflow.add_node("flight_agent", self._flight_agent_node)
584
+ workflow.add_node("hotel_agent", self._hotel_agent_node)
585
+ workflow.add_node("itinerary_agent", self._itinerary_agent_node)
586
+
587
+ # Set entry point
588
+ workflow.set_entry_point("router")
589
+
590
+ # Add conditional edges
591
+ workflow.add_conditional_edges(
592
+ "router",
593
+ self._route_to_agent,
594
+ {
595
+ "flight_agent": "flight_agent",
596
+ "hotel_agent": "hotel_agent",
597
+ "itinerary_agent": "itinerary_agent"
598
+ }
599
+ )
600
+
601
+ # Add edges to END
602
+ workflow.add_edge("flight_agent", END)
603
+ workflow.add_edge("hotel_agent", END)
604
+ workflow.add_edge("itinerary_agent", END)
605
+
606
+ # Compile with memory
607
+ checkpointer = InMemorySaver()
608
+ return workflow.compile(checkpointer=checkpointer)
609
+
610
+ def chat(self, message: str, thread_id: str = "default"):
611
+ """Process a single message and return response"""
612
+ try:
613
+ config = {"configurable": {"thread_id": thread_id}}
614
+
615
+ result = self.workflow.invoke(
616
+ {"messages": [HumanMessage(content=message)]},
617
+ config
618
+ )
619
+
620
+ # Ensure we have a valid response
621
+ if not result.get("messages") or len(result["messages"]) == 0:
622
+ return "I apologize, but I didn't receive a proper response. Please try your request again."
623
+
624
+ last_message = result["messages"][-1]
625
+
626
+ # Check if the last message has content
627
+ if hasattr(last_message, 'content') and last_message.content:
628
+ return last_message.content
629
+ else:
630
+ return "I apologize, but I didn't generate a proper response. Please try your request again."
631
+
632
+ except Exception as e:
633
+ print(f"❌ Error in chat method: {e}")
634
+ return f"I encountered an error while processing your request: {str(e)}. Please try again."
635
+
636
+ def chat_stream(self, message: str, thread_id: str = "default"):
637
+ """Stream response for a message"""
638
+ config = {"configurable": {"thread_id": thread_id}}
639
+
640
+ for chunk in self.workflow.stream(
641
+ {"messages": [HumanMessage(content=message)]},
642
+ config
643
+ ):
644
+ yield chunk
645
+
646
+
647
+ # For LangGraph Cloud deployment
648
+ app = TravelPlannerApp()
649
+
650
+ # Gradio Interface Functions
651
+ def create_gradio_interface():
652
+ """Create and configure the Gradio interface"""
653
+
654
+ def chat_function(message, history, session_id):
655
+ """Handle chat messages with session memory"""
656
+ try:
657
+ # Use session_id as thread_id for maintaining conversation context
658
+ response = app.chat(message, thread_id=session_id)
659
+ return response
660
+ except Exception as e:
661
+ return f"❌ Error: {str(e)}"
662
+
663
+ def reset_conversation():
664
+ """Reset conversation by returning new session ID"""
665
+ return str(uuid.uuid4())
666
+
667
+ # Create the Gradio interface
668
+ with gr.Blocks(
669
+ title="🧳 Multi-Agent Travel Planner",
670
+ theme=gr.themes.Soft(),
671
+ css="""
672
+ .gradio-container {
673
+ max-width: 900px !important;
674
+ }
675
+ .chat-message {
676
+ font-size: 14px !important;
677
+ }
678
+ """
679
+ ) as demo:
680
+
681
+ gr.Markdown("""
682
+ # 🧳 Multi-Agent Travel Planning System
683
+
684
+ **Your AI-powered travel assistant with specialized agents for:**
685
+ - ✈️ **Flight Search & Booking** - Find and compare flights
686
+ - 🏨 **Hotel Search & Booking** - Discover accommodations
687
+ - 🗺️ **Itinerary Planning** - Create detailed travel plans
688
+
689
+ Just type your travel question and let our agents help you plan your perfect trip!
690
+ """)
691
+
692
+ # Session state for maintaining conversation context
693
+ session_id = gr.State(value=str(uuid.uuid4()))
694
+
695
+ # Chat interface
696
+ chatbot = gr.Chatbot(
697
+ label="Travel Assistant",
698
+ height=500,
699
+ show_label=True,
700
+ container=True,
701
+ bubble_full_width=False
702
+ )
703
+
704
+ with gr.Row():
705
+ msg = gr.Textbox(
706
+ placeholder="Ask me about flights, hotels, or travel planning...",
707
+ label="Your Message",
708
+ scale=4,
709
+ container=False
710
+ )
711
+ send_btn = gr.Button("Send", scale=1, variant="primary")
712
+
713
+ with gr.Row():
714
+ clear_btn = gr.Button("Clear Chat", scale=1)
715
+ gr.Markdown("**Examples:** *Find flights from NYC to London*, *Hotels in Tokyo for 3 nights*, *Plan a 5-day trip to Italy*")
716
+
717
+ # Event handlers
718
+ def respond(message, history, session_id):
719
+ if not message.strip():
720
+ return history, ""
721
+
722
+ # Add user message to history
723
+ history.append([message, None])
724
+
725
+ # Get bot response
726
+ bot_response = chat_function(message, history, session_id)
727
+
728
+ # Add bot response to history
729
+ history[-1][1] = bot_response
730
+
731
+ return history, ""
732
+
733
+ def clear_chat():
734
+ return [], str(uuid.uuid4())
735
+
736
+ # Wire up the events
737
+ msg.submit(
738
+ respond,
739
+ inputs=[msg, chatbot, session_id],
740
+ outputs=[chatbot, msg]
741
+ )
742
+
743
+ send_btn.click(
744
+ respond,
745
+ inputs=[msg, chatbot, session_id],
746
+ outputs=[chatbot, msg]
747
+ )
748
+
749
+ clear_btn.click(
750
+ clear_chat,
751
+ outputs=[chatbot, session_id]
752
+ )
753
+
754
+ # Example buttons
755
+ gr.Examples(
756
+ examples=[
757
+ "Give me flight from delhi to dubai for 15 Aug 2025",
758
+ "any good 5 start hotel in dubai for my stay there from 15 Aug to 17 Aug 2025",
759
+ "Plan a 2 day itinerary for my dubai trip",
760
+ "Hey, I'm a foodie anything to try there"
761
+ ],
762
+ inputs=msg,
763
+ label="Example Queries"
764
+ )
765
+
766
+ gr.Markdown("""
767
+ ---
768
+ 💡 **Tips:**
769
+ - Be specific with dates, locations, and preferences
770
+ - The system remembers your conversation context
771
+ - Each agent specializes in their domain for better results
772
+ """)
773
+
774
+ return demo
775
+
776
+
777
+ def main():
778
+ """Main function to launch the Gradio interface"""
779
+ print("🚀 Starting Multi-Agent Travel Planning System...")
780
+
781
+ try:
782
+ # Create and launch the Gradio interface
783
+ demo = create_gradio_interface()
784
+
785
+ # Launch the interface
786
+ demo.launch(
787
+ share=False, # Set to True if you want to create a public link
788
+ #server_name="127.0.0.1", # Use localhost instead of 0.0.0.0
789
+ # server_port=7860,
790
+ # show_error=True,
791
+ # quiet=False,
792
+ # inbrowser=True # Automatically open browser
793
+ )
794
+
795
+ except Exception as e:
796
+ print(f"❌ Error launching interface: {str(e)}")
797
+ print("Please check your environment variables and dependencies.")
798
+
799
+
800
+ if __name__ == "__main__":
801
+ main()