NSamson1 commited on
Commit
be93ed8
·
verified ·
1 Parent(s): ae30f3b

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +296 -0
app.py ADDED
@@ -0,0 +1,296 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import requests
4
+ from functools import lru_cache
5
+ import gradio as gr
6
+ import openai
7
+
8
+ # -------------------- CONFIGURATION --------------------
9
+ # Use environment variables for security (set them before running)
10
+ OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "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")
11
+ OPENWEATHER_API_KEY = os.getenv("OPENWEATHER_API_KEY", "") # Optional, for live weather
12
+
13
+ client = openai.OpenAI(api_key=OPENAI_API_KEY)
14
+
15
+ # Load attractions database
16
+ with open("attractions_data.json", "r") as f:
17
+ ATTRACTIONS_DB = json.load(f)
18
+
19
+ # -------------------- TOOL IMPLEMENTATIONS --------------------
20
+
21
+ def get_weather(city: str) -> dict:
22
+ """Get current weather for a city (mock if no API key)."""
23
+ if not OPENWEATHER_API_KEY:
24
+ # Mock response
25
+ return {
26
+ "city": city,
27
+ "temperature": 22,
28
+ "condition": "sunny",
29
+ "humidity": 60,
30
+ "wind_speed": 10,
31
+ "precipitation": 0,
32
+ "note": "Mock data (no API key)"
33
+ }
34
+ try:
35
+ url = f"http://api.openweathermap.org/data/2.5/weather?q={city}&appid={OPENWEATHER_API_KEY}&units=metric"
36
+ resp = requests.get(url)
37
+ data = resp.json()
38
+ if resp.status_code != 200:
39
+ return {"error": f"Weather API error: {data.get('message', 'unknown')}"}
40
+ # Extract relevant fields
41
+ weather_info = {
42
+ "city": city,
43
+ "temperature": data["main"]["temp"],
44
+ "condition": data["weather"][0]["description"],
45
+ "humidity": data["main"]["humidity"],
46
+ "wind_speed": data["wind"]["speed"],
47
+ "precipitation": data.get("rain", {}).get("1h", 0)
48
+ }
49
+ return weather_info
50
+ except Exception as e:
51
+ return {"error": f"Weather service unavailable: {str(e)}"}
52
+
53
+ @lru_cache(maxsize=1)
54
+ def get_exchange_rates(base="USD"):
55
+ """Fetch latest exchange rates (cached). Fallback to mock."""
56
+ try:
57
+ url = f"https://api.exchangerate-api.com/v4/latest/{base}"
58
+ resp = requests.get(url)
59
+ return resp.json().get("rates", {})
60
+ except:
61
+ # Mock rates for common currencies
62
+ return {"USD": 1, "EUR": 0.85, "GBP": 0.75, "JPY": 110, "UGX": 3700, "KES": 130, "RWF": 1100}
63
+
64
+ def convert_currency(amount: float, from_currency: str, to_currency: str) -> dict:
65
+ """Convert amount from one currency to another."""
66
+ rates = get_exchange_rates(from_currency.upper())
67
+ if to_currency.upper() not in rates:
68
+ return {"error": f"Currency {to_currency} not supported."}
69
+ converted = amount * rates[to_currency.upper()]
70
+ return {
71
+ "amount": amount,
72
+ "from": from_currency.upper(),
73
+ "to": to_currency.upper(),
74
+ "converted": round(converted, 2),
75
+ "rate": rates[to_currency.upper()]
76
+ }
77
+
78
+ def get_attractions(city: str) -> dict:
79
+ """Return list of attractions for a given city."""
80
+ city_key = city.title()
81
+ if city_key in ATTRACTIONS_DB:
82
+ return {"city": city_key, "attractions": ATTRACTIONS_DB[city_key]}
83
+ else:
84
+ return {"error": f"No attractions data available for {city}.", "city": city}
85
+
86
+ def compose_itinerary(city: str, weather: dict, attractions: list, budget: dict = None) -> str:
87
+ """
88
+ Generate a day trip itinerary using the tool outputs.
89
+ Calls OpenAI to format the plan based on verified data.
90
+ """
91
+ # Build context string
92
+ weather_text = f"{weather['condition'].capitalize()}, {weather['temperature']}°C"
93
+ if weather.get('precipitation', 0) > 0:
94
+ weather_text += f", chance of rain {weather['precipitation']}mm"
95
+ if "note" in weather:
96
+ weather_text += f" ({weather['note']})"
97
+
98
+ attractions_text = "\n".join([
99
+ f"- {a['name']} ({a['type']}, ~{a['duration_hours']} hrs, entry: {a['entry_fee']} {a.get('currency', '')})"
100
+ for a in attractions
101
+ ])
102
+
103
+ budget_text = ""
104
+ if budget and "error" not in budget:
105
+ budget_text = f"Budget: {budget['amount']} {budget['from']} (approx. {budget['converted']} {budget['to']} in local currency)"
106
+
107
+ prompt = f"""
108
+ You are a travel assistant. Create a one-day itinerary for {city} based on the following verified information.
109
+
110
+ Weather: {weather_text}
111
+ Attractions available:
112
+ {attractions_text}
113
+ {budget_text}
114
+
115
+ The itinerary should:
116
+ - Be realistic given the weather (e.g., outdoor activities if sunny, indoor if rain).
117
+ - Sequence attractions logically (considering location and opening hours if known).
118
+ - Include estimated times for each activity.
119
+ - Suggest meal times and local food options (use common knowledge, do not invent specific restaurants).
120
+ - If a budget is provided, mention if attractions have entry fees and stay within budget.
121
+ - Use markdown for readability (headings, bullet points, emojis).
122
+
123
+ Return only the itinerary.
124
+ """
125
+ response = client.chat.completions.create(
126
+ model="gpt-3.5-turbo",
127
+ messages=[{"role": "user", "content": prompt}],
128
+ temperature=0.7
129
+ )
130
+ return response.choices[0].message.content
131
+
132
+ # -------------------- TOOL SCHEMAS FOR OPENAI --------------------
133
+ tools = [
134
+ {
135
+ "type": "function",
136
+ "function": {
137
+ "name": "get_weather",
138
+ "description": "Get current weather for a city",
139
+ "parameters": {
140
+ "type": "object",
141
+ "properties": {
142
+ "city": {"type": "string", "description": "City name"}
143
+ },
144
+ "required": ["city"]
145
+ }
146
+ }
147
+ },
148
+ {
149
+ "type": "function",
150
+ "function": {
151
+ "name": "convert_currency",
152
+ "description": "Convert an amount from one currency to another",
153
+ "parameters": {
154
+ "type": "object",
155
+ "properties": {
156
+ "amount": {"type": "number", "description": "Amount to convert"},
157
+ "from_currency": {"type": "string", "description": "Source currency code (e.g., USD)"},
158
+ "to_currency": {"type": "string", "description": "Target currency code (e.g., UGX)"}
159
+ },
160
+ "required": ["amount", "from_currency", "to_currency"]
161
+ }
162
+ }
163
+ },
164
+ {
165
+ "type": "function",
166
+ "function": {
167
+ "name": "get_attractions",
168
+ "description": "Get list of tourist attractions for a city",
169
+ "parameters": {
170
+ "type": "object",
171
+ "properties": {
172
+ "city": {"type": "string", "description": "City name"}
173
+ },
174
+ "required": ["city"]
175
+ }
176
+ }
177
+ }
178
+ # Note: compose_itinerary is not exposed as a tool; it's called by the agent after gathering data.
179
+ ]
180
+
181
+ # -------------------- AGENT ORCHESTRATION --------------------
182
+ def run_agent(user_message, history):
183
+ """
184
+ Main agent loop: maintains conversation, calls tools, and finally generates itinerary.
185
+ """
186
+ # System prompt that guides the agent
187
+ system_prompt = """You are a helpful travel planning assistant. You have access to tools that can:
188
+ - Get current weather for a city
189
+ - Convert currency
190
+ - Retrieve tourist attractions for a city
191
+
192
+ Your goal is to help the user plan a day trip. If the user's request is missing information (e.g., city, budget, currency), ask clarifying questions.
193
+ When you have gathered all necessary data (city, weather, attractions, and optionally budget), call the appropriate tools to fetch the data, then use that information to compose a final itinerary.
194
+ Do not invent any data; only use the tool outputs.
195
+ Be friendly, concise, and helpful."""
196
+
197
+ messages = [{"role": "system", "content": system_prompt}]
198
+ # Add conversation history (Gradio passes history as list of [user, assistant] pairs)
199
+ for user_msg, asst_msg in history:
200
+ messages.append({"role": "user", "content": user_msg})
201
+ if asst_msg:
202
+ messages.append({"role": "assistant", "content": asst_msg})
203
+ messages.append({"role": "user", "content": user_message})
204
+
205
+ # We'll collect tool outputs in a dict for later use
206
+ tool_outputs = {}
207
+
208
+ # Max iterations to prevent infinite loops
209
+ for _ in range(5):
210
+ response = client.chat.completions.create(
211
+ model="gpt-3.5-turbo",
212
+ messages=messages,
213
+ tools=tools,
214
+ tool_choice="auto"
215
+ )
216
+ msg = response.choices[0].message
217
+
218
+ # If the model wants to call tools
219
+ if msg.tool_calls:
220
+ # Add assistant message with tool calls to history
221
+ messages.append(msg)
222
+ for tool_call in msg.tool_calls:
223
+ func_name = tool_call.function.name
224
+ args = json.loads(tool_call.function.arguments)
225
+ # Execute the tool
226
+ if func_name == "get_weather":
227
+ result = get_weather(args["city"])
228
+ tool_outputs["weather"] = result
229
+ elif func_name == "convert_currency":
230
+ result = convert_currency(args["amount"], args["from_currency"], args["to_currency"])
231
+ tool_outputs["budget"] = result
232
+ elif func_name == "get_attractions":
233
+ result = get_attractions(args["city"])
234
+ tool_outputs["attractions"] = result
235
+ else:
236
+ result = {"error": "Unknown tool"}
237
+
238
+ # Append tool response
239
+ messages.append({
240
+ "role": "tool",
241
+ "tool_call_id": tool_call.id,
242
+ "content": json.dumps(result)
243
+ })
244
+ # Continue loop to let model process tool responses
245
+ continue
246
+ else:
247
+ # No tool calls: either final answer or clarification request
248
+ final_text = msg.content
249
+ # If we have all necessary data, we might want to generate the itinerary via compose_itinerary
250
+ # But the model may already have done it if we instructed it to.
251
+ # To be safe, we can check if we have weather and attractions, and then call compose_itinerary.
252
+ if "weather" in tool_outputs and "attractions" in tool_outputs and "error" not in tool_outputs["attractions"]:
253
+ # We have the data, generate itinerary
254
+ city = tool_outputs["attractions"]["city"]
255
+ weather = tool_outputs["weather"]
256
+ attractions = tool_outputs["attractions"]["attractions"]
257
+ budget = tool_outputs.get("budget")
258
+ itinerary = compose_itinerary(city, weather, attractions, budget)
259
+ return itinerary
260
+ else:
261
+ # Model's response is a clarification or intermediate answer
262
+ return final_text
263
+
264
+ return "I'm having trouble processing your request. Please try again."
265
+
266
+ # -------------------- GRADIO INTERFACE --------------------
267
+ def chat_interface(message, history):
268
+ response = run_agent(message, history)
269
+ return response
270
+
271
+ # Custom CSS for a nicer look
272
+ css = """
273
+ .gradio-container { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; }
274
+ .chatbot { border-radius: 10px; }
275
+ """
276
+
277
+ with gr.Blocks(css=css, title="TouristGuide AI Agent") as demo:
278
+ gr.Markdown("""
279
+ # 🌍 TouristGuide AI Agent
280
+ Your personal travel planner powered by AI and real-time tools.
281
+ Ask for a day trip itinerary in any supported city (Kigali, Kampala, Nairobi, etc.) with optional budget and currency conversion.
282
+ """)
283
+ chatbot = gr.Chatbot(label="Conversation", bubble_full_width=False, height=500)
284
+ msg = gr.Textbox(label="Your message", placeholder="e.g., Plan a day trip in Kigali with a budget of 150 USD", lines=2)
285
+ clear = gr.Button("Clear conversation")
286
+
287
+ def respond(message, chat_history):
288
+ bot_message = run_agent(message, chat_history)
289
+ chat_history.append((message, bot_message))
290
+ return "", chat_history
291
+
292
+ msg.submit(respond, [msg, chatbot], [msg, chatbot])
293
+ clear.click(lambda: None, None, chatbot, queue=False)
294
+
295
+ if __name__ == "__main__":
296
+ demo.launch(share=False, server_name="0.0.0.0")