taspol commited on
Commit
1bcc194
·
1 Parent(s): 0638730

fix: dependency

Browse files
Files changed (3) hide show
  1. app.py +1 -1
  2. interface.py +68 -32
  3. utils/llm_caller.py +236 -173
app.py CHANGED
@@ -62,7 +62,7 @@ def generate_trip_plan(request: PlanRequest):
62
  tripOverview=f"Error: {str(e)}",
63
  query_params=request,
64
  retrieved_data=[],
65
- trip_plan=TripPlan(overview="Error occurred", total_estimated_cost=0.0, steps=[]),
66
  meta={"status": "error", "error": str(e)}
67
  )
68
 
 
62
  tripOverview=f"Error: {str(e)}",
63
  query_params=request,
64
  retrieved_data=[],
65
+ trip_plan=TripPlan(overview="Error occurred"),
66
  meta={"status": "error", "error": str(e)}
67
  )
68
 
interface.py CHANGED
@@ -20,14 +20,26 @@ class YoutubeLinkResponse(BaseModel):
20
 
21
 
22
  class PlanRequest(BaseModel):
23
- start_place: str
24
- destination_place: str
 
 
 
 
 
 
 
 
 
 
 
 
25
  trip_price: Optional[float] = Field(None, description="Total budget in local currency")
26
- trip_context: Optional[str] = Field(None, description="e.g. adventure, rest, date")
27
- trip_duration_days: Optional[int] = 1
28
- group_size: Optional[int] = 1
29
- preferences: Optional[List[str]] = None
30
- top_k: Optional[int] = 3
31
 
32
 
33
  class RetrievedItem(BaseModel):
@@ -35,35 +47,59 @@ class RetrievedItem(BaseModel):
35
  place_name: str
36
  score: float
37
 
38
- class TransportInfo(BaseModel):
39
- mode: Optional[str]
40
- departure: Optional[str]
41
- arrival: Optional[str]
42
- duration_minutes: Optional[int]
43
- price: Optional[float]
44
- details: Optional[str]
45
-
46
- class PlanStep(BaseModel):
47
- day: Optional[int]
48
- title: Optional[str]
49
- description: Optional[str]
50
- transport: Optional[TransportInfo]
51
- booking_info: Optional[List[Dict[str, str]]]
52
- map_coordinates: Optional[Dict[str, float]]
53
- images: Optional[List[str]]
54
- tips: Optional[List[str]]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
 
56
  class TripPlan(BaseModel):
57
- overview: str
58
- total_estimated_cost: Optional[float]
59
- steps: List[PlanStep]
 
 
 
 
60
 
61
  class PlanResponse(BaseModel):
62
- tripOverview: str
63
- query_params: PlanRequest
64
- retrieved_data: List[RetrievedItem]
65
- trip_plan: TripPlan
66
- meta: Dict[str, Any]
 
67
 
68
  class ChatRequest(BaseModel):
69
  message: str
 
20
 
21
 
22
  class PlanRequest(BaseModel):
23
+ # Core location fields
24
+ start_place: str = Field(..., description="Starting location")
25
+ destination: str = Field(..., description="Destination location")
26
+
27
+ # Trip details
28
+ travelDates: Optional[str] = Field(None, description="Travel dates in format 'YYYY-MM-DD to YYYY-MM-DD'")
29
+ duration: int = Field(..., description="Duration in days")
30
+
31
+ # Group and preferences
32
+ groupSize: int = Field(4, description="Number of people in the group")
33
+ interests: List[str] = Field(default_factory=list, description="List of interests like 'Cultural immersion', 'Mountain views'")
34
+
35
+ # Budget and accommodation
36
+ budgetTier: Optional[str] = Field(None, description="Budget tier: 'Budget', 'Mid-range', 'Luxury'")
37
  trip_price: Optional[float] = Field(None, description="Total budget in local currency")
38
+ stayPref: Optional[str] = Field(None, description="Accommodation preference like 'Tea houses and lodges'")
39
+
40
+ # Transport and theme
41
+ transportPref: Optional[str] = Field(None, description="Transport preference like 'Local bus'")
42
+ theme: Optional[str] = Field(None, description="Trip theme like 'Adventure', 'Relaxation'")
43
 
44
 
45
  class RetrievedItem(BaseModel):
 
47
  place_name: str
48
  score: float
49
 
50
+ class Timeline(BaseModel):
51
+ t: str = Field(..., description="Time in HH:MM format")
52
+ detail: str = Field(..., description="Activity description")
53
+
54
+ class Spot(BaseModel):
55
+ name: str = Field(..., description="Location name")
56
+ time: str = Field(..., description="Time range like '09:30 – 11:45'")
57
+ notes: str = Field(..., description="Description and tips")
58
+
59
+ class Budget(BaseModel):
60
+ transport: Optional[float] = Field(None, description="Transport cost")
61
+ entrance: Optional[float] = Field(None, description="Entrance fees")
62
+ meals: Optional[float] = Field(None, description="Meal costs")
63
+ accommodation: Optional[float] = Field(None, description="Accommodation costs")
64
+ activities: Optional[float] = Field(None, description="Activity costs")
65
+ total: Optional[float] = Field(None, description="Total estimated cost")
66
+
67
+ class Permits(BaseModel):
68
+ needed: bool = Field(False, description="Whether permits are required")
69
+ notes: str = Field("", description="Permit requirements")
70
+ seasonal: str = Field("", description="Seasonal considerations")
71
+
72
+ class Contact(BaseModel):
73
+ name: str = Field(..., description="Contact name")
74
+ phone: str = Field(..., description="Phone number")
75
+
76
+ class SafetyContacts(BaseModel):
77
+ ranger: Optional[Contact] = Field(None, description="Ranger station contact")
78
+ hospital: Optional[Contact] = Field(None, description="Hospital contact")
79
+ police: Optional[Contact] = Field(None, description="Police contact")
80
+
81
+ class Safety(BaseModel):
82
+ registration: str = Field("", description="Safety registration info")
83
+ checkins: str = Field("", description="Check-in procedures")
84
+ sos: str = Field("", description="Emergency procedures")
85
+ contacts: Optional[SafetyContacts] = Field(None, description="Emergency contacts")
86
 
87
  class TripPlan(BaseModel):
88
+ title: str = Field(..., description="Descriptive title for the trip")
89
+ date: str = Field(..., description="Suggested date/timing")
90
+ timeline: List[Timeline] = Field(default_factory=list, description="Daily timeline")
91
+ spots: List[Spot] = Field(default_factory=list, description="Points of interest")
92
+ budget: Budget = Field(..., description="Budget breakdown")
93
+ permits: Optional[Permits] = Field(None, description="Permit information")
94
+ safety: Optional[Safety] = Field(None, description="Safety information")
95
 
96
  class PlanResponse(BaseModel):
97
+ tripOverview: str = Field(..., description="Overview of the trip")
98
+ query_params: PlanRequest = Field(..., description="Original request parameters")
99
+ retrieved_data: List[RetrievedItem] = Field(default_factory=list, description="Retrieved context data")
100
+ trip_plan: TripPlan = Field(..., description="Detailed trip plan")
101
+ meta: Dict[str, Any] = Field(default_factory=dict, description="Metadata")
102
+
103
 
104
  class ChatRequest(BaseModel):
105
  message: str
utils/llm_caller.py CHANGED
@@ -7,7 +7,8 @@ from dataclasses import dataclass
7
  from qdrant_client import QdrantClient
8
  from openai import OpenAI
9
  from sentence_transformers import SentenceTransformer
10
- from interface import PlanResponse, TripPlan, PlanStep, TransportInfo, RetrievedItem, PlanRequest
 
11
  from class_mod.rest_qdrant import RestQdrantClient
12
  import json
13
 
@@ -54,192 +55,254 @@ class LLMCaller:
54
  print(f"Error calling LLM: {e}")
55
  return f"Error: Unable to get LLM response - {str(e)}"
56
 
57
- async def query_with_rag(self, plan_request: PlanRequest, collection_name: Optional[str] = None) -> 'PlanResponse':
58
- """
59
- Perform RAG query using PlanRequest, embed query, search Qdrant, and generate complete PlanResponse via LLM
60
- """
61
- print(plan_request)
62
- try:
63
- # 1. Create query string from PlanRequest
64
- query_text = f"Trip from {plan_request.start_place} to {plan_request.destination_place}"
65
- if plan_request.trip_context:
66
- query_text += f" for {plan_request.trip_context}"
67
- if plan_request.trip_duration_days:
68
- query_text += f" for {plan_request.trip_duration_days} days"
69
- if plan_request.trip_price:
70
- query_text += f" with budget {plan_request.trip_price}"
71
-
72
- # 2. Generate embedding for the query
73
- query_embedding = self.embedding_model.encode(query_text, normalize_embeddings=True).tolist()
74
-
75
- # 3. Search Qdrant for similar content
76
- collection = collection_name or self.collection_name
77
- top_k = plan_request.top_k or self.top_k
78
-
79
- search_results = self.qdrant.search(
80
- collection_name=collection,
81
- query_vector=query_embedding,
82
- limit=top_k,
83
- with_payload=True
84
- )
85
-
86
- # 4. Convert search results to RetrievedItem format
87
- retrieved_data = []
88
- context_text = ""
 
 
 
 
 
 
 
 
 
 
 
 
89
 
90
- print(f"Search results: {search_results['result']}")
91
- for result in search_results['result']:
92
- retrieved_item = RetrievedItem(
93
- place_id="Unkown",
94
- place_name=result['payload'].get("place_name", "Unknown"),
95
- score=result['score'],
96
- )
97
- retrieved_data.append(retrieved_item)
98
- context_text += f"\n{result['payload'].get('text', '')}"
99
 
100
- # 5. Create detailed prompt for LLM to generate structured response
101
- llm_prompt = f"""
102
- You are a travel planning assistant. Based on the trip request and travel context provided, generate a comprehensive trip plan in the exact JSON format specified below.
103
 
104
- Trip Request:
105
- - From: {plan_request.start_place}
106
- - To: {plan_request.destination_place}
107
- - Duration: {plan_request.trip_duration_days} days
108
- - Budget: {plan_request.trip_price}
109
- - Context: {plan_request.trip_context}
110
- - Group Size: {plan_request.group_size}
111
- - Preferences: {plan_request.preferences}
 
 
 
 
112
 
113
- Relevant Travel Context:
114
- {context_text}
115
- ***Don't Explain or add any additional text outside the JSON format***.
116
- Generate a response in this EXACT JSON format (no additional text before or after):
117
- {{
118
- "tripOverview": "A comprehensive 2-3 paragraph overview of the entire trip",
119
- "trip_plan": {{
120
- "overview": "Brief summary of the trip plan",
121
- "total_estimated_cost": estimated_total_cost_as_number,
122
- "steps": [
123
- {{
124
- "day": 1,
125
- "title": "Day 1 title",
126
- "description": "Detailed description of day 1 activities",
127
- "transport": {{
128
- "mode": "transportation method",
129
- "departure": "departure location",
130
- "arrival": "arrival location",
131
- "duration_minutes": estimated_duration_in_minutes,
132
- "price": estimated_price,
133
- "details": "additional transport details"
134
- }},
135
- "booking_info": [{{"name": "name of place or transport that need to be booked before", "description": "description to explain user that this place need to be booked or not or can walk in"}}],
136
- "map_coordinates": {{"lat": latitude_number, "lon": longitude_number}},
137
- "images": ["url1", "url2"],
138
- "tips": ["tip1", "tip2", "tip3"],
139
- }}
140
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
  }}
142
  }}
143
- Rules:
144
- - Use the provided context to fill in details.
145
- - Ensure all fields are filled with realistic and practical information.
146
- - Use local currency for prices.
147
- - DONT ADD ANY INLINE NOTE IN JSON
148
- - In booking_info field, include a list of places in the day that need to be booked.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
 
 
 
150
 
151
- ***Don't Explain or add any additional text outside the JSON format***.
152
- Ensure the JSON is valid and well-structured.
153
 
154
- Create {plan_request.trip_duration_days or 1} days of detailed activities. Include realistic prices, coordinates, and practical tips. Make it specific to the destinations and context provided.
155
- """
156
-
157
- # 6. Call LLM to generate structured trip plan
158
- llm_response = await self.basic_query(user_prompt=llm_prompt, max_tokens=24048)
159
- print(f"LLM Response: {llm_response}")
160
- # 7. Parse LLM response as JSON
161
- try:
162
- # Clean the response and parse JSON
163
- json_str = llm_response.strip()
164
- if json_str.startswith("```json"):
165
- json_str = json_str[7:]
166
- if json_str.endswith("```"):
167
- json_str = json_str[:-3]
168
-
169
- llm_data = json.loads(json_str)
170
- print(f"LLM Data: {llm_data}")
171
- # Convert to PlanResponse structure
172
- trip_plan_data = llm_data.get("trip_plan", {})
173
- steps_data = trip_plan_data.get("steps", [])
174
-
175
- # Convert steps to PlanStep objects
176
- plan_steps = []
177
- for step in steps_data:
178
- transport_data = step.get("transport", {})
179
- transport = TransportInfo(
180
- mode=transport_data.get("mode"),
181
- departure=transport_data.get("departure"),
182
- arrival=transport_data.get("arrival"),
183
- duration_minutes=transport_data.get("duration_minutes"),
184
- price=transport_data.get("price"),
185
- details=transport_data.get("details")
186
- )
187
-
188
- plan_step = PlanStep(
189
- day=step.get("day"),
190
- title=step.get("title"),
191
- description=step.get("description"),
192
- transport=transport,
193
- booking_info=step.get("booking_info", []),
194
- map_coordinates=step.get("map_coordinates", {}),
195
- images=step.get("images", []),
196
- tips=step.get("tips", [])
197
- )
198
- plan_steps.append(plan_step)
199
-
200
- trip_plan = TripPlan(
201
- overview=trip_plan_data.get("overview", ""),
202
- total_estimated_cost=trip_plan_data.get("total_estimated_cost"),
203
- steps=plan_steps
204
  )
205
-
206
- return PlanResponse(
207
- tripOverview=llm_data.get("tripOverview", ""),
208
- query_params=plan_request,
209
- retrieved_data=retrieved_data,
210
- trip_plan=trip_plan,
211
- meta={
212
- "status": "success",
213
- "query_text": query_text,
214
- "results_count": len(retrieved_data)
215
- }
216
- )
217
-
218
- except json.JSONDecodeError as e:
219
- print(f"Error parsing LLM JSON response: {e}")
220
- print(f"LLM Response: {llm_response}")
221
-
222
- # Fallback: create basic response with LLM text
223
- return PlanResponse(
224
- tripOverview=llm_response,
225
- query_params=plan_request,
226
- retrieved_data=retrieved_data,
227
- trip_plan=TripPlan(
228
- overview="Generated plan (parsing error)",
229
- total_estimated_cost=plan_request.trip_price,
230
- steps=[]
231
- ),
232
- meta={"status": "json_parse_error", "error": str(e)}
233
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
234
 
235
- except Exception as e:
236
- print(f"Error in RAG query: {e}")
 
 
 
237
  return PlanResponse(
238
- tripOverview=f"Error generating trip plan: {str(e)}",
239
  query_params=plan_request,
240
- retrieved_data=[],
241
- trip_plan=TripPlan(overview="Error occurred", total_estimated_cost=0.0, steps=[]),
242
- meta={"status": "error", "error": str(e)}
 
 
 
 
243
  )
244
 
 
 
 
 
 
 
 
 
 
 
245
 
 
7
  from qdrant_client import QdrantClient
8
  from openai import OpenAI
9
  from sentence_transformers import SentenceTransformer
10
+ from interface import PlanResponse, TripPlan, RetrievedItem, PlanRequest
11
+ from interface import Timeline, Spot, Budget, Permits, Safety, Contact, SafetyContacts
12
  from class_mod.rest_qdrant import RestQdrantClient
13
  import json
14
 
 
55
  print(f"Error calling LLM: {e}")
56
  return f"Error: Unable to get LLM response - {str(e)}"
57
 
58
+ async def query_with_rag(self, plan_request: PlanRequest, collection_name: Optional[str] = None) -> 'PlanResponse':
59
+ """
60
+ Perform RAG query using PlanRequest, embed query, search Qdrant, and generate complete PlanResponse via LLM
61
+ """
62
+ print(plan_request)
63
+ try:
64
+ # 1. Create query string from PlanRequest - updated for new fields
65
+ destination = plan_request.destination or plan_request.destination_place or "unknown destination"
66
+ duration = plan_request.duration or plan_request.trip_duration_days or 1
67
+ budget = plan_request.trip_price or 0
68
+
69
+ query_text = f"Trip from {plan_request.start_place} to {destination}"
70
+
71
+ # Add new fields to query
72
+ if plan_request.travelDates:
73
+ query_text += f" on {plan_request.travelDates}"
74
+ if duration:
75
+ query_text += f" for {duration} days"
76
+ if budget:
77
+ query_text += f" with budget {budget}"
78
+ if plan_request.theme:
79
+ query_text += f" {plan_request.theme} themed trip"
80
+ if plan_request.interests:
81
+ query_text += f" interested in {', '.join(plan_request.interests)}"
82
+ if plan_request.budgetTier:
83
+ query_text += f" {plan_request.budgetTier} budget tier"
84
+
85
+ # 2. Generate embedding for the query
86
+ query_embedding = self.embedding_model.encode(query_text, normalize_embeddings=True).tolist()
87
+
88
+ # 3. Search Qdrant for similar content
89
+ collection = collection_name or self.collection_name
90
+ top_k = plan_request.top_k or self.top_k
91
+
92
+ search_results = self.qdrant.search(
93
+ collection_name=collection,
94
+ query_vector=query_embedding,
95
+ limit=top_k,
96
+ with_payload=True
97
+ )
98
+
99
+ # 4. Convert search results to RetrievedItem format
100
+ retrieved_data = []
101
+ context_text = ""
102
 
103
+ print(f"Search results: {search_results['result']}")
104
+ for result in search_results['result']:
105
+ retrieved_item = RetrievedItem(
106
+ place_id=result.get('id', 'Unknown'),
107
+ place_name=result['payload'].get("place_name", "Unknown"),
108
+ score=result['score'],
109
+ )
110
+ retrieved_data.append(retrieved_item)
111
+ context_text += f"\n{result['payload'].get('text', '')}"
112
 
113
+ # 5. Create detailed prompt for LLM - updated with new fields
114
+ llm_prompt = f"""
115
+ You are a travel planning assistant. Based on the trip request and travel context provided, generate a comprehensive trip plan in the exact JSON format specified below.
116
 
117
+ Trip Request:
118
+ - From: {plan_request.start_place}
119
+ - To: {destination}
120
+ - Travel Dates: {plan_request.travelDates or 'Flexible'}
121
+ - Duration: {duration} days
122
+ - Budget: {budget} ({plan_request.budgetTier or 'Not specified'} tier)
123
+ - Group Size: {plan_request.groupSize} people
124
+ - Interests: {', '.join(plan_request.interests) if plan_request.interests else 'Not specified'}
125
+ - Theme: {plan_request.theme or 'General travel'}
126
+ - Transport Preference: {plan_request.transportPref or 'Any'}
127
+ - Stay Preference: {plan_request.stayPref or 'Any'}
128
+ - Legacy Context: {getattr(plan_request, 'trip_context', None) or 'None'}
129
 
130
+ Relevant Travel Context:
131
+ {context_text}
132
+
133
+ ***Don't Explain or add any additional text outside the JSON format***.
134
+ Generate a response in this EXACT JSON format (no additional text before or after):
135
+ {{
136
+ "tripOverview": "A comprehensive 2-3 paragraph overview of the entire {duration}-day trip from {plan_request.start_place} to {destination}, highlighting the {plan_request.theme or 'travel'} theme and {', '.join(plan_request.interests) if plan_request.interests else 'general sightseeing'} interests",
137
+ "trip_plan": {{
138
+ "title": "Descriptive title for the {duration}-day {plan_request.theme or 'travel'} trip to {destination}",
139
+ "date": "{plan_request.travelDates or 'Flexible dates'}",
140
+ "timeline": [
141
+ {{"t": "08:30", "detail": "Start of day activity"}},
142
+ {{"t": "12:00", "detail": "Lunch break"}},
143
+ {{"t": "14:00", "detail": "Afternoon activity"}},
144
+ {{"t": "18:00", "detail": "Evening activity"}}
145
+ ],
146
+ "spots": [
147
+ {{"name": "Location name", "time": "09:30 – 11:45", "notes": "Description and tips for this location"}},
148
+ {{"name": "Another location", "time": "14:00 – 16:30", "notes": "More details and recommendations"}}
149
+ ],
150
+ "budget": {{
151
+ "transport": estimated_transport_cost,
152
+ "entrance": estimated_entrance_fees,
153
+ "meals": estimated_meal_costs,
154
+ "accommodation": estimated_accommodation_costs,
155
+ "activities": estimated_activity_costs,
156
+ "total": total_estimated_cost
157
+ }},
158
+ "permits": {{
159
+ "needed": true_or_false,
160
+ "notes": "Permit requirements and how to obtain them",
161
+ "seasonal": "Seasonal considerations and restrictions"
162
+ }},
163
+ "safety": {{
164
+ "registration": "Safety registration procedures",
165
+ "checkins": "Check-in procedures and requirements",
166
+ "sos": "Emergency procedures and protocols",
167
+ "contacts": {{
168
+ "ranger": {{"name": "Local ranger station name", "phone": "+66-XX-XXX-XXXX"}},
169
+ "hospital": {{"name": "Nearest hospital name", "phone": "+66-XX-XXX-XXXX"}},
170
+ "police": {{"name": "Local police station", "phone": "1155"}}
171
+ }}
172
  }}
173
  }}
174
+ }}
175
+ Rules:
176
+ - Use the provided context to fill in details.
177
+ - Ensure all fields are filled with realistic and practical information.
178
+ - Use local currency for prices and consider the {plan_request.budgetTier or 'moderate'} budget tier.
179
+ - Accommodate {plan_request.groupSize} people in all recommendations.
180
+ - Focus on {plan_request.theme or 'general'} themed activities.
181
+ - Prioritize interests: {', '.join(plan_request.interests) if plan_request.interests else 'general sightseeing'}.
182
+ - Consider transport preference: {plan_request.transportPref or 'any'}.
183
+ - Consider accommodation preference: {plan_request.stayPref or 'any'}.
184
+ - DONT ADD ANY INLINE NOTE IN JSON
185
+
186
+ ***Don't Explain or add any additional text outside the JSON format***.
187
+ Ensure the JSON is valid and well-structured.
188
+
189
+ Create {duration} days of detailed activities. Include realistic prices, coordinates, and practical tips. Make it specific to the destinations, theme, interests, and budget tier provided.
190
+ """
191
+
192
+ # 6. Call LLM to generate structured trip plan
193
+ llm_response = await self.basic_query(user_prompt=llm_prompt, max_tokens=24048)
194
+ print(f"LLM Response: {llm_response}")
195
+
196
+ # 7. Parse LLM response as JSON
197
+ try:
198
+ # Clean the response and parse JSON
199
+ json_str = llm_response.strip()
200
+ if json_str.startswith("```json"):
201
+ json_str = json_str[7:]
202
+ if json_str.endswith("```"):
203
+ json_str = json_str[:-3]
204
 
205
+ llm_data = json.loads(json_str)
206
+ print(f"LLM Data: {llm_data}")
207
 
208
+ # Convert to PlanResponse structure
209
+ trip_plan_data = llm_data.get("trip_plan", {})
210
 
211
+ # Parse timeline
212
+ timeline_data = trip_plan_data.get("timeline", [])
213
+ timeline = [Timeline(t=item["t"], detail=item["detail"]) for item in timeline_data]
214
+
215
+ # Parse spots
216
+ spots_data = trip_plan_data.get("spots", [])
217
+ spots = [Spot(name=item["name"], time=item["time"], notes=item["notes"]) for item in spots_data]
218
+
219
+ # Parse budget
220
+ budget_data = trip_plan_data.get("budget", {})
221
+ budget = Budget(
222
+ transport=budget_data.get("transport"),
223
+ entrance=budget_data.get("entrance"),
224
+ meals=budget_data.get("meals"),
225
+ accommodation=budget_data.get("accommodation"),
226
+ activities=budget_data.get("activities"),
227
+ total=budget_data.get("total")
228
+ )
229
+
230
+ # Parse permits
231
+ permits_data = trip_plan_data.get("permits", {})
232
+ permits = Permits(
233
+ needed=permits_data.get("needed", False),
234
+ notes=permits_data.get("notes", ""),
235
+ seasonal=permits_data.get("seasonal", "")
236
+ ) if permits_data else None
237
+
238
+ # Parse safety
239
+ safety_data = trip_plan_data.get("safety", {})
240
+ safety = None
241
+ if safety_data:
242
+ contacts_data = safety_data.get("contacts", {})
243
+ contacts = SafetyContacts(
244
+ ranger=Contact(**contacts_data["ranger"]) if contacts_data.get("ranger") else None,
245
+ hospital=Contact(**contacts_data["hospital"]) if contacts_data.get("hospital") else None,
246
+ police=Contact(**contacts_data["police"]) if contacts_data.get("police") else None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
247
  )
248
+ safety = Safety(
249
+ registration=safety_data.get("registration", ""),
250
+ checkins=safety_data.get("checkins", ""),
251
+ sos=safety_data.get("sos", ""),
252
+ contacts=contacts
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
253
  )
254
+
255
+ trip_plan = TripPlan(
256
+ title=trip_plan_data.get("title", ""),
257
+ date=trip_plan_data.get("date", ""),
258
+ timeline=timeline,
259
+ spots=spots,
260
+ budget=budget,
261
+ permits=permits,
262
+ safety=safety
263
+ )
264
+
265
+ return PlanResponse(
266
+ tripOverview=llm_data.get("tripOverview", ""),
267
+ query_params=plan_request,
268
+ retrieved_data=retrieved_data,
269
+ trip_plan=trip_plan,
270
+ meta={
271
+ "status": "success",
272
+ "query_text": query_text,
273
+ "results_count": len(retrieved_data),
274
+ "theme": plan_request.theme,
275
+ "interests": plan_request.interests,
276
+ "budget_tier": plan_request.budgetTier,
277
+ "group_size": plan_request.groupSize
278
+ }
279
+ )
280
 
281
+ except json.JSONDecodeError as e:
282
+ print(f"Error parsing LLM JSON response: {e}")
283
+ print(f"LLM Response: {llm_response}")
284
+
285
+ # Fallback: create basic response with LLM text
286
  return PlanResponse(
287
+ tripOverview=llm_response,
288
  query_params=plan_request,
289
+ retrieved_data=retrieved_data,
290
+ trip_plan=TripPlan(
291
+ overview="Generated plan (parsing error)",
292
+ total_estimated_cost=budget,
293
+ steps=[]
294
+ ),
295
+ meta={"status": "json_parse_error", "error": str(e)}
296
  )
297
 
298
+ except Exception as e:
299
+ print(f"Error in RAG query: {e}")
300
+ return PlanResponse(
301
+ tripOverview=f"Error generating trip plan: {str(e)}",
302
+ query_params=plan_request,
303
+ retrieved_data=[],
304
+ trip_plan=TripPlan(overview="Error occurred", total_estimated_cost=0.0, steps=[]),
305
+ meta={"status": "error", "error": str(e)}
306
+ )
307
+
308