emmamair commited on
Commit
408deb2
·
verified ·
1 Parent(s): 9f7174f

Upload 3 files

Browse files
Files changed (3) hide show
  1. app.py +994 -0
  2. readme.md +311 -0
  3. requirements.txt +4 -0
app.py ADDED
@@ -0,0 +1,994 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import requests
3
+ from openai import OpenAI
4
+ import json
5
+ from datetime import datetime
6
+ import re
7
+
8
+ # Your MCP server URLs
9
+ WEATHER_MCP_URL = "https://emma-ctrl--scotland-weather-mcp-fastapi-app.modal.run/mcp"
10
+ DAYLIGHT_MCP_URL = "https://emma-ctrl--scotland-daylight-mcp-fastapi-app.modal.run/mcp"
11
+ DRIVING_MCP_URL = "https://emma-ctrl--scottish-driving-mcp-fastapi-app.modal.run/mcp"
12
+
13
+ # Initialize Nebius AI Studio client
14
+ client = OpenAI(
15
+ api_key="NEBIUS_API_KEY",
16
+ base_url="https://api.studio.nebius.ai/v1"
17
+ )
18
+
19
+ def call_mcp_server(server_url, tool_name, arguments):
20
+ """Call any MCP server with Scottish location validation"""
21
+
22
+ # Define Scottish clarifications dictionary once for all uses
23
+ scottish_clarifications = {
24
+ # Major cities that exist elsewhere
25
+ "aberdeen": "Aberdeen, Scotland, UK",
26
+ "dundee": "Dundee, Scotland, UK",
27
+ "perth": "Perth, Scotland, UK",
28
+ "hamilton": "Hamilton, Scotland, UK",
29
+ "glasgow": "Glasgow, Scotland, UK",
30
+ "edinburgh": "Edinburgh, Scotland, UK",
31
+ "stirling": "Stirling, Scotland, UK",
32
+ "inverness": "Inverness, Scotland, UK",
33
+ "paisley": "Paisley, Scotland, UK",
34
+ "greenock": "Greenock, Scotland, UK",
35
+ "ayr": "Ayr, Scotland, UK",
36
+ "kilmarnock": "Kilmarnock, Scotland, UK",
37
+ "dumfries": "Dumfries, Scotland, UK",
38
+ "falkirk": "Falkirk, Scotland, UK",
39
+ "livingston": "Livingston, Scotland, UK",
40
+ "kirkcaldy": "Kirkcaldy, Scotland, UK",
41
+ "dunfermline": "Dunfermline, Scotland, UK",
42
+
43
+ # Islands (very commonly confused)
44
+ "mull": "Isle of Mull, Scotland, UK",
45
+ "isle of mull": "Isle of Mull, Scotland, UK",
46
+ "skye": "Isle of Skye, Scotland, UK",
47
+ "isle of skye": "Isle of Skye, Scotland, UK",
48
+ "arran": "Isle of Arran, Scotland, UK",
49
+ "isle of arran": "Isle of Arran, Scotland, UK",
50
+ "harris": "Isle of Harris, Scotland, UK",
51
+ "isle of harris": "Isle of Harris, Scotland, UK",
52
+ "lewis": "Isle of Lewis, Scotland, UK",
53
+ "isle of lewis": "Isle of Lewis, Scotland, UK",
54
+ "orkney": "Orkney Islands, Scotland, UK",
55
+ "orkney islands": "Orkney Islands, Scotland, UK",
56
+ "shetland": "Shetland Islands, Scotland, UK",
57
+ "shetland islands": "Shetland Islands, Scotland, UK",
58
+ "islay": "Isle of Islay, Scotland, UK",
59
+ "isle of islay": "Isle of Islay, Scotland, UK",
60
+ "jura": "Isle of Jura, Scotland, UK",
61
+ "isle of jura": "Isle of Jura, Scotland, UK",
62
+ "bute": "Isle of Bute, Scotland, UK",
63
+ "isle of bute": "Isle of Bute, Scotland, UK",
64
+ "iona": "Isle of Iona, Scotland, UK",
65
+ "isle of iona": "Isle of Iona, Scotland, UK",
66
+ "tiree": "Isle of Tiree, Scotland, UK",
67
+ "isle of tiree": "Isle of Tiree, Scotland, UK",
68
+ "coll": "Isle of Coll, Scotland, UK",
69
+ "isle of coll": "Isle of Coll, Scotland, UK",
70
+ "muck": "Isle of Muck, Scotland, UK",
71
+ "eigg": "Isle of Eigg, Scotland, UK",
72
+ "rum": "Isle of Rum, Scotland, UK",
73
+ "rhum": "Isle of Rum, Scotland, UK",
74
+ "canna": "Isle of Canna, Scotland, UK",
75
+ "staffa": "Isle of Staffa, Scotland, UK",
76
+ "ulva": "Isle of Ulva, Scotland, UK",
77
+
78
+ # Highland towns/villages that could be confused
79
+ "fort william": "Fort William, Scotland, UK",
80
+ "aviemore": "Aviemore, Scotland, UK",
81
+ "oban": "Oban, Scotland, UK",
82
+ "pitlochry": "Pitlochry, Scotland, UK",
83
+ "callander": "Callander, Scotland, UK",
84
+ "balloch": "Balloch, Scotland, UK",
85
+ "helensburgh": "Helensburgh, Scotland, UK",
86
+ "mallaig": "Mallaig, Scotland, UK",
87
+ "kyle": "Kyle of Lochalsh, Scotland, UK",
88
+ "kyle of lochalsh": "Kyle of Lochalsh, Scotland, UK",
89
+ "portree": "Portree, Scotland, UK",
90
+ "tobermory": "Tobermory, Scotland, UK",
91
+ "tarbert": "Tarbert, Scotland, UK",
92
+ "campbeltown": "Campbeltown, Scotland, UK",
93
+ "stranraer": "Stranraer, Scotland, UK",
94
+ "thurso": "Thurso, Scotland, UK",
95
+ "wick": "Wick, Scotland, UK",
96
+ "dornoch": "Dornoch, Scotland, UK",
97
+ "golspie": "Golspie, Scotland, UK",
98
+ "brora": "Brora, Scotland, UK",
99
+ "ullapool": "Ullapool, Scotland, UK",
100
+ "gairloch": "Gairloch, Scotland, UK",
101
+ "kinlochewe": "Kinlochewe, Scotland, UK",
102
+ "torridon": "Torridon, Scotland, UK",
103
+ "applecross": "Applecross, Scotland, UK",
104
+ "plockton": "Plockton, Scotland, UK",
105
+ "lochinver": "Lochinver, Scotland, UK",
106
+ "durness": "Durness, Scotland, UK",
107
+ "tongue": "Tongue, Scotland, UK",
108
+ "bettyhill": "Bettyhill, Scotland, UK",
109
+ "john o groats": "John O'Groats, Scotland, UK",
110
+ "john o'groats": "John O'Groats, Scotland, UK",
111
+
112
+ # Border towns that exist elsewhere
113
+ "kelso": "Kelso, Scotland, UK",
114
+ "jedburgh": "Jedburgh, Scotland, UK",
115
+ "hawick": "Hawick, Scotland, UK",
116
+ "galashiels": "Galashiels, Scotland, UK",
117
+ "selkirk": "Selkirk, Scotland, UK",
118
+ "melrose": "Melrose, Scotland, UK",
119
+ "peebles": "Peebles, Scotland, UK",
120
+ "biggar": "Biggar, Scotland, UK",
121
+ "moffat": "Moffat, Scotland, UK",
122
+ "sanquhar": "Sanquhar, Scotland, UK",
123
+ "langholm": "Langholm, Scotland, UK",
124
+ "annan": "Annan, Scotland, UK",
125
+ "gretna": "Gretna, Scotland, UK",
126
+ "gretna green": "Gretna Green, Scotland, UK",
127
+ "lockerbie": "Lockerbie, Scotland, UK",
128
+
129
+ # Eastern Scotland towns
130
+ "st andrews": "St Andrews, Scotland, UK",
131
+ "saint andrews": "St Andrews, Scotland, UK",
132
+ "cupar": "Cupar, Scotland, UK",
133
+ "anstruther": "Anstruther, Scotland, UK",
134
+ "crail": "Crail, Scotland, UK",
135
+ "elie": "Elie, Scotland, UK",
136
+ "pittenweem": "Pittenweem, Scotland, UK",
137
+ "north berwick": "North Berwick, Scotland, UK",
138
+ "dunbar": "Dunbar, Scotland, UK",
139
+ "haddington": "Haddington, Scotland, UK",
140
+ "linlithgow": "Linlithgow, Scotland, UK",
141
+ "bathgate": "Bathgate, Scotland, UK",
142
+ "armadale": "Armadale, Scotland, UK",
143
+ "stonehaven": "Stonehaven, Scotland, UK",
144
+ "montrose": "Montrose, Scotland, UK",
145
+ "arbroath": "Arbroath, Scotland, UK",
146
+ "carnoustie": "Carnoustie, Scotland, UK",
147
+ "forfar": "Forfar, Scotland, UK",
148
+ "brechin": "Brechin, Scotland, UK",
149
+ "kirriemuir": "Kirriemuir, Scotland, UK",
150
+ "blairgowrie": "Blairgowrie, Scotland, UK",
151
+ "crieff": "Crieff, Scotland, UK",
152
+ "aberfeldy": "Aberfeldy, Scotland, UK",
153
+ "dunkeld": "Dunkeld, Scotland, UK",
154
+ "birnam": "Birnam, Scotland, UK",
155
+
156
+ # Western Scotland and Argyll
157
+ "rothesay": "Rothesay, Scotland, UK",
158
+ "dunoon": "Dunoon, Scotland, UK",
159
+ "inveraray": "Inveraray, Scotland, UK",
160
+ "lochgilphead": "Lochgilphead, Scotland, UK",
161
+ "ardrishaig": "Ardrishaig, Scotland, UK",
162
+ "crinan": "Crinan, Scotland, UK",
163
+ "kilmartin": "Kilmartin, Scotland, UK",
164
+ "dalmally": "Dalmally, Scotland, UK",
165
+ "tyndrum": "Tyndrum, Scotland, UK",
166
+ "crianlarich": "Crianlarich, Scotland, UK",
167
+ "killin": "Killin, Scotland, UK",
168
+ "lochearnhead": "Lochearnhead, Scotland, UK",
169
+ "st fillans": "St Fillans, Scotland, UK",
170
+ "comrie": "Comrie, Scotland, UK",
171
+ "auchterarder": "Auchterarder, Scotland, UK",
172
+ "gleneagles": "Gleneagles, Scotland, UK",
173
+
174
+ # Central Scotland
175
+ "bridge of allan": "Bridge of Allan, Scotland, UK",
176
+ "alloa": "Alloa, Scotland, UK",
177
+ "clackmannan": "Clackmannan, Scotland, UK",
178
+ "tillicoultry": "Tillicoultry, Scotland, UK",
179
+ "dollar": "Dollar, Scotland, UK",
180
+ "alva": "Alva, Scotland, UK",
181
+ "menstrie": "Menstrie, Scotland, UK",
182
+ "denny": "Denny, Scotland, UK",
183
+ "bonnybridge": "Bonnybridge, Scotland, UK",
184
+ "larbert": "Larbert, Scotland, UK",
185
+ "stenhousemuir": "Stenhousemuir, Scotland, UK",
186
+ "grangemouth": "Grangemouth, Scotland, UK",
187
+ "bo'ness": "Bo'ness, Scotland, UK",
188
+ "blackness": "Blackness, Scotland, UK",
189
+ "queensferry": "South Queensferry, Scotland, UK",
190
+ "south queensferry": "South Queensferry, Scotland, UK",
191
+
192
+ # Famous landmarks and areas
193
+ "ben nevis": "Ben Nevis, Scotland, UK",
194
+ "ben lomond": "Ben Lomond, Scotland, UK",
195
+ "ben more": "Ben More, Scotland, UK",
196
+ "cairngorms": "Cairngorms, Scotland, UK",
197
+ "glencoe": "Glencoe, Scotland, UK",
198
+ "glen coe": "Glencoe, Scotland, UK",
199
+ "loch lomond": "Loch Lomond, Scotland, UK",
200
+ "loch ness": "Loch Ness, Scotland, UK",
201
+ "loch katrine": "Loch Katrine, Scotland, UK",
202
+ "loch earn": "Loch Earn, Scotland, UK",
203
+ "loch tay": "Loch Tay, Scotland, UK",
204
+ "loch tummel": "Loch Tummel, Scotland, UK",
205
+ "loch rannoch": "Loch Rannoch, Scotland, UK",
206
+ "loch awe": "Loch Awe, Scotland, UK",
207
+ "loch fyne": "Loch Fyne, Scotland, UK",
208
+ "loch long": "Loch Long, Scotland, UK",
209
+ "loch goil": "Loch Goil, Scotland, UK",
210
+ "the trossachs": "The Trossachs, Scotland, UK",
211
+ "trossachs": "The Trossachs, Scotland, UK",
212
+ "queen elizabeth forest park": "Queen Elizabeth Forest Park, Scotland, UK",
213
+ "cairngorms national park": "Cairngorms National Park, Scotland, UK",
214
+ "loch lomond and trossachs national park": "Loch Lomond and Trossachs National Park, Scotland, UK",
215
+
216
+ # Northern Scotland - abbreviated for space
217
+ "elgin": "Elgin, Scotland, UK",
218
+ "forres": "Forres, Scotland, UK",
219
+ "nairn": "Nairn, Scotland, UK",
220
+ "grantown": "Grantown-on-Spey, Scotland, UK",
221
+ "kingussie": "Kingussie, Scotland, UK",
222
+ "newtonmore": "Newtonmore, Scotland, UK",
223
+ "dalwhinnie": "Dalwhinnie, Scotland, UK",
224
+ "carrbridge": "Carrbridge, Scotland, UK",
225
+ "boat of garten": "Boat of Garten, Scotland, UK",
226
+ "tomintoul": "Tomintoul, Scotland, UK",
227
+ "aberlour": "Aberlour, Scotland, UK",
228
+ "dufftown": "Dufftown, Scotland, UK",
229
+ "keith": "Keith, Scotland, UK",
230
+ "huntly": "Huntly, Scotland, UK",
231
+ "inverurie": "Inverurie, Scotland, UK",
232
+ "banff": "Banff, Scotland, UK",
233
+ "fraserburgh": "Fraserburgh, Scotland, UK",
234
+ "peterhead": "Peterhead, Scotland, UK",
235
+ "lairg": "Lairg, Scotland, UK",
236
+ "dornoch": "Dornoch, Scotland, UK",
237
+ "helmsdale": "Helmsdale, Scotland, UK",
238
+ "bettyhill": "Bettyhill, Scotland, UK",
239
+ "tongue": "Tongue, Scotland, UK",
240
+
241
+ # Common abbreviations and variations
242
+ "fort bill": "Fort William, Scotland, UK",
243
+ "the fort": "Fort William, Scotland, UK",
244
+ "malky": "Mallaig, Scotland, UK",
245
+ "toby": "Tobermory, Scotland, UK",
246
+ }
247
+
248
+ # Handle single location field
249
+ if "location" in arguments:
250
+ location = arguments["location"].lower().strip()
251
+ if location in scottish_clarifications:
252
+ arguments["location"] = scottish_clarifications[location]
253
+ elif not any(keyword in location for keyword in ["scotland", "uk", "united kingdom"]):
254
+ arguments["location"] = f"{arguments['location']}, Scotland, UK"
255
+
256
+ # Handle multiple location fields for driving
257
+ for field in ["from_location", "to_location", "start_location"]:
258
+ if field in arguments:
259
+ location = arguments[field].lower().strip()
260
+ if location in scottish_clarifications:
261
+ arguments[field] = scottish_clarifications[location]
262
+ elif not any(keyword in location for keyword in ["scotland", "uk", "united kingdom"]):
263
+ arguments[field] = f"{arguments[field]}, Scotland, UK"
264
+
265
+ # Handle locations array for road trips
266
+ if "locations" in arguments and isinstance(arguments["locations"], list):
267
+ clarified_locations = []
268
+ for location in arguments["locations"]:
269
+ location_lower = location.lower().strip()
270
+ if location_lower in scottish_clarifications:
271
+ clarified_locations.append(scottish_clarifications[location_lower])
272
+ elif not any(keyword in location_lower for keyword in ["scotland", "uk", "united kingdom"]):
273
+ clarified_locations.append(f"{location}, Scotland, UK")
274
+ else:
275
+ clarified_locations.append(location)
276
+ arguments["locations"] = clarified_locations
277
+
278
+ payload = {
279
+ "method": "tools/call",
280
+ "params": {
281
+ "name": tool_name,
282
+ "arguments": arguments
283
+ }
284
+ }
285
+
286
+ try:
287
+ response = requests.post(server_url, json=payload, timeout=30)
288
+ response.raise_for_status()
289
+ return response.json()
290
+ except Exception as e:
291
+ return {"error": f"Failed to get data from {tool_name}: {str(e)}"}
292
+
293
+ def format_response(response, data_type="data"):
294
+ """Format the response nicely"""
295
+ if "error" in response:
296
+ return f"❌ {response['error']}"
297
+
298
+ if "content" in response and response["content"]:
299
+ return response["content"][0]["text"]
300
+
301
+ return f"❌ No {data_type} data received"
302
+
303
+ # Replace the extract_locations_from_text function with this enhanced version:
304
+
305
+ def extract_locations_from_text(text):
306
+ """Extract Scottish location names with better journey order detection"""
307
+ scottish_places = [
308
+ "Edinburgh", "Glasgow", "Aberdeen", "Dundee", "Stirling", "Inverness",
309
+ "Fort William", "Aviemore", "Perth", "Paisley", "Greenock", "Dunfermline",
310
+ "Kirkcaldy", "Ayr", "Kilmarnock", "Dumfries", "Oban", "Pitlochry",
311
+ "Callander", "Balloch", "Helensburgh", "Falkirk", "Livingston",
312
+ "Isle of Skye", "Skye", "Isle of Mull", "Mull", "Isle of Arran", "Arran",
313
+ "Isle of Islay", "Islay", "Isle of Jura", "Jura", "Harris", "Lewis",
314
+ "Orkney", "Shetland", "Orkney Islands", "Shetland Islands",
315
+ "Ben Nevis", "Loch Lomond", "Loch Ness", "Cairngorms", "Glencoe",
316
+ "St Andrews", "Melrose", "Jedburgh", "Galashiels", "Hawick",
317
+ "Mallaig", "Kyle of Lochalsh", "Kyle", "Portree", "Tobermory",
318
+ "Tarbert", "Campbeltown", "Stranraer", "Thurso", "Wick",
319
+ "Ullapool", "Durness", "John O'Groats", "Lochinver", "Tongue",
320
+ "North Berwick", "Dunbar", "Stonehaven", "Montrose", "Arbroath",
321
+ "Dunkeld", "Crieff", "Aberfeldy", "Rothesay", "Dunoon", "Inveraray",
322
+ "Tyndrum", "Crianlarich", "Killin", "The Trossachs", "Trossachs"
323
+ ]
324
+
325
+ # ENHANCED: Look for journey order keywords
326
+ text_lower = text.lower()
327
+
328
+ # Check for journey order patterns
329
+ journey_patterns = [
330
+ r'start in (.*?) and then (?:go to |visit )(.*?) and then (.*?)(?:\.|$)',
331
+ r'from (.*?) to (.*?) (?:via |through |and then )(.*?)(?:\.|$)',
332
+ r'(.*?) to (.*?) to (.*?)(?:\.|$)',
333
+ r'(.*?) → (.*?) → (.*?)(?:\.|$)',
334
+ r'(.*?) then (.*?) then (.*?)(?:\.|$)'
335
+ ]
336
+
337
+ # Try to extract ordered journey
338
+ for pattern in journey_patterns:
339
+ match = re.search(pattern, text_lower)
340
+ if match:
341
+ ordered_locations = []
342
+ for group in match.groups():
343
+ group_clean = group.strip().replace(' and', '').replace(',', '')
344
+ # Find matching Scottish place
345
+ for place in scottish_places:
346
+ if place.lower() in group_clean:
347
+ if place not in ordered_locations:
348
+ ordered_locations.append(place)
349
+ break
350
+
351
+ if len(ordered_locations) >= 2:
352
+ print(f"DEBUG: Found journey order: {ordered_locations}")
353
+ return ordered_locations
354
+
355
+ # Fallback to original method if no journey pattern found
356
+ found_locations = []
357
+ text_upper = text.title()
358
+
359
+ # Sort by length (descending) to match longer names first
360
+ sorted_places = sorted(scottish_places, key=len, reverse=True)
361
+
362
+ for place in sorted_places:
363
+ if place in text_upper and place not in found_locations:
364
+ found_locations.append(place)
365
+
366
+ # Try to reorder based on position in text for common journey words
367
+ if len(found_locations) >= 2:
368
+ journey_indicators = ['start', 'begin', 'first', 'then', 'next', 'finally', 'end']
369
+
370
+ # Look for starting location
371
+ for indicator in ['start in', 'begin in', 'from']:
372
+ if indicator in text_lower:
373
+ for location in found_locations:
374
+ location_pos = text_lower.find(location.lower())
375
+ indicator_pos = text_lower.find(indicator)
376
+ if location_pos > indicator_pos and location_pos - indicator_pos < 50:
377
+ # Move this location to the front
378
+ if location in found_locations:
379
+ found_locations.remove(location)
380
+ found_locations.insert(0, location)
381
+ break
382
+
383
+ print(f"DEBUG: Extracted locations (fallback): {found_locations}")
384
+ return found_locations
385
+
386
+ def extract_date_from_text(text):
387
+ """Enhanced date extraction"""
388
+ import re
389
+ from datetime import datetime, timedelta
390
+
391
+ # Look for YYYY-MM-DD format
392
+ date_match = re.search(r'\b(\d{4}-\d{2}-\d{2})\b', text)
393
+ if date_match:
394
+ return date_match.group(1)
395
+
396
+ # Handle relative dates
397
+ text_lower = text.lower()
398
+ if 'tomorrow' in text_lower:
399
+ return (datetime.now() + timedelta(days=1)).strftime('%Y-%m-%d')
400
+ elif 'yesterday' in text_lower:
401
+ return (datetime.now() - timedelta(days=1)).strftime('%Y-%m-%d')
402
+ elif 'next week' in text_lower:
403
+ return (datetime.now() + timedelta(weeks=1)).strftime('%Y-%m-%d')
404
+ elif 'this weekend' in text_lower:
405
+ days_until_saturday = (5 - datetime.now().weekday()) % 7
406
+ return (datetime.now() + timedelta(days=days_until_saturday)).strftime('%Y-%m-%d')
407
+
408
+ return None # Default to today
409
+
410
+ def should_get_daylight_data(message):
411
+ """Determine if the user is specifically asking about daylight/sunrise/sunset times"""
412
+ specific_daylight_keywords = [
413
+ 'sunrise', 'sunset', 'golden hour', 'photography', 'dawn', 'dusk',
414
+ 'sun up', 'sun down', 'light for photos', 'early morning light',
415
+ 'evening light', 'when does it get dark', 'when does sun rise',
416
+ 'best time for photos', 'photo timing', 'blue hour', 'magic hour'
417
+ ]
418
+
419
+ message_lower = message.lower()
420
+ return any(keyword in message_lower for keyword in specific_daylight_keywords)
421
+
422
+ def should_get_weather_data(message):
423
+ """Determine if the user is asking about weather"""
424
+ weather_keywords = [
425
+ 'weather', 'forecast', 'rain', 'sunny', 'temperature', 'wind',
426
+ 'cloudy', 'snow', 'storm', 'humid', 'cold', 'warm', 'hot',
427
+ 'precipitation', 'degrees', 'celsius', 'fahrenheit', 'conditions',
428
+ 'next week', 'this week', 'tomorrow', 'weekend', '7 day', 'weekly',
429
+ 'camping', 'hiking', 'outdoor', 'adventure'
430
+ ]
431
+
432
+ message_lower = message.lower()
433
+ return any(keyword in message_lower for keyword in weather_keywords)
434
+
435
+ def should_get_driving_data(message):
436
+ """Determine if user is asking about distances/routes"""
437
+ driving_keywords = [
438
+ 'drive', 'driving', 'distance', 'how far', 'route', 'directions',
439
+ 'road trip', 'travel time', 'journey', 'miles', 'km', 'kilometers',
440
+ 'how long to drive', 'car journey', 'road', 'travel to', 'get to',
441
+ 'from', 'to', 'via', 'through', 'stop at', 'visit', 'tour'
442
+ ]
443
+
444
+ message_lower = message.lower()
445
+ return any(keyword in message_lower for keyword in driving_keywords)
446
+
447
+ def decode_polyline(polyline_str):
448
+ """Decode Google polyline string into lat/lon coordinates"""
449
+ try:
450
+ index = 0
451
+ lat = 0
452
+ lng = 0
453
+ coordinates = []
454
+
455
+ while index < len(polyline_str):
456
+ # Decode latitude
457
+ shift = 0
458
+ result = 0
459
+ while True:
460
+ byte = ord(polyline_str[index]) - 63
461
+ index += 1
462
+ result |= (byte & 0x1f) << shift
463
+ shift += 5
464
+ if byte < 0x20:
465
+ break
466
+
467
+ dlat = ~(result >> 1) if result & 1 else result >> 1
468
+ lat += dlat
469
+
470
+ # Decode longitude
471
+ shift = 0
472
+ result = 0
473
+ while True:
474
+ byte = ord(polyline_str[index]) - 63
475
+ index += 1
476
+ result |= (byte & 0x1f) << shift
477
+ shift += 5
478
+ if byte < 0x20:
479
+ break
480
+
481
+ dlng = ~(result >> 1) if result & 1 else result >> 1
482
+ lng += dlng
483
+
484
+ coordinates.append([lat / 1e5, lng / 1e5])
485
+
486
+ return coordinates
487
+ except Exception as e:
488
+ print(f"DEBUG: Polyline decode error: {e}")
489
+ return []
490
+
491
+ def extract_route_geometry_from_mcp(mcp_response, locations):
492
+ """Extract real driving route coordinates from OpenRouteService API"""
493
+ try:
494
+ if len(locations) >= 2:
495
+ start_lat = float(locations[0][1])
496
+ start_lon = float(locations[0][2])
497
+ end_lat = float(locations[1][1])
498
+ end_lon = float(locations[1][2])
499
+
500
+ # Use your actual API key here
501
+ api_key = "OPEN_ROUTE_SERVICE_API_KEY" # ← Your real key
502
+
503
+ url = "https://api.openrouteservice.org/v2/directions/driving-car"
504
+ headers = {
505
+ 'Accept': 'application/json',
506
+ 'Authorization': api_key
507
+ }
508
+
509
+ body = {
510
+ "coordinates": [[start_lon, start_lat], [end_lon, end_lat]],
511
+ "format": "geojson",
512
+ "radiuses": [5000, 5000]
513
+ }
514
+
515
+ response = requests.post(url, headers=headers, json=body, timeout=10)
516
+ print(f"DEBUG: Got response status: {response.status_code}")
517
+
518
+ if response.status_code == 200:
519
+ data = response.json()
520
+
521
+ if 'routes' in data and len(data['routes']) > 0:
522
+ route = data['routes'][0]
523
+
524
+ if isinstance(route, dict) and 'geometry' in route:
525
+ geometry = route['geometry']
526
+
527
+ if isinstance(geometry, str):
528
+ # It's an encoded polyline - decode it!
529
+ print(f"DEBUG: Decoding polyline of length: {len(geometry)}")
530
+ decoded_coords = decode_polyline(geometry)
531
+ print(f"DEBUG: SUCCESS! Decoded {len(decoded_coords)} route points")
532
+ return decoded_coords
533
+ else:
534
+ print(f"DEBUG: Geometry is not a string: {type(geometry)}")
535
+ else:
536
+ print(f"DEBUG: Route structure issue: {route}")
537
+ else:
538
+ print(f"DEBUG: No routes in response")
539
+
540
+ except Exception as e:
541
+ print(f"DEBUG: Exception: {e}")
542
+ import traceback
543
+ traceback.print_exc()
544
+
545
+ # Fallback to straight line
546
+ return [[locations[0][1], locations[0][2]], [locations[1][1], locations[1][2]]]
547
+
548
+ def get_scottish_coordinates():
549
+ """Return coordinates for Scottish locations"""
550
+ return {
551
+ "edinburgh": [55.9533, -3.1883],
552
+ "glasgow": [55.8642, -4.2518],
553
+ "aberdeen": [57.1497, -2.0943],
554
+ "dundee": [56.4620, -2.9707],
555
+ "stirling": [56.1165, -3.9369],
556
+ "inverness": [57.4778, -4.2247],
557
+ "fort william": [56.8198, -5.1052],
558
+ "aviemore": [57.1952, -3.8263],
559
+ "perth": [56.3956, -3.4309],
560
+ "oban": [56.4154, -5.4713],
561
+ "pitlochry": [56.7028, -3.7340],
562
+ "isle of skye": [57.2740, -6.2149],
563
+ "skye": [57.2740, -6.2149],
564
+ "isle of mull": [56.4504, -5.8037],
565
+ "mull": [56.4504, -5.8037],
566
+ "isle of arran": [55.5836, -5.2489],
567
+ "arran": [55.5836, -5.2489],
568
+ "mallaig": [57.0067, -5.8283],
569
+ "portree": [57.4123, -6.1956],
570
+ "tobermory": [56.6229, -6.0679],
571
+ "glencoe": [56.6756, -5.1019],
572
+ "glen coe": [56.6756, -5.1019],
573
+ "ben nevis": [56.7969, -5.0037],
574
+ "st andrews": [56.3398, -2.7967],
575
+ "cairngorms": [57.0833, -3.6667],
576
+ "loch lomond": [56.1000, -4.6000],
577
+ "loch ness": [57.3229, -4.4244],
578
+ "kyle of lochalsh": [57.2785, -5.7127],
579
+ "kyle": [57.2785, -5.7127],
580
+ "thurso": [58.5944, -3.5267],
581
+ "wick": [58.4394, -3.0956],
582
+ "ullapool": [57.8952, -5.1587],
583
+ "durness": [58.5667, -4.7167],
584
+ "cairngorms": [57.0833, -3.6667],
585
+ "cairngorms national park": [57.1952, -3.8263]
586
+ }
587
+
588
+ def create_map_html(locations=[], routes=[], center_lat=56.8, center_lon=-4.2, zoom=6):
589
+ """Generate interactive map using Folium with real driving routes"""
590
+ try:
591
+ import folium
592
+
593
+ if locations:
594
+ center_lat = locations[0][1]
595
+ center_lon = locations[0][2]
596
+ zoom = 8
597
+
598
+ m = folium.Map(
599
+ location=[center_lat, center_lon],
600
+ zoom_start=zoom,
601
+ tiles='OpenStreetMap'
602
+ )
603
+
604
+ # Add markers
605
+ colors = ['red', 'blue', 'green', 'purple', 'orange']
606
+ for i, (name, lat, lon) in enumerate(locations):
607
+ folium.Marker(
608
+ [lat, lon],
609
+ popup=f"<b>{name}</b>",
610
+ tooltip=name,
611
+ icon=folium.Icon(color=colors[i % len(colors)], icon='info-sign')
612
+ ).add_to(m)
613
+
614
+ # Add actual driving route if available
615
+ if routes and len(routes) > 1:
616
+ folium.PolyLine(
617
+ routes,
618
+ color='red',
619
+ weight=4,
620
+ opacity=0.8,
621
+ popup="🚗 Driving Route"
622
+ ).add_to(m)
623
+
624
+ # Fit bounds to show entire route
625
+ m.fit_bounds(routes, padding=(20, 20))
626
+
627
+ return m._repr_html_()
628
+
629
+ except ImportError:
630
+ return "<div>Install folium for interactive maps</div>"
631
+
632
+ def extract_locations_and_routes_from_conversation(message, locations_mentioned):
633
+ """Extract locations and potential routes from current message and conversation context"""
634
+ coords_db = get_scottish_coordinates()
635
+
636
+ # Get coordinates for mentioned locations
637
+ location_coords = []
638
+ for location in locations_mentioned:
639
+ location_key = location.lower().strip()
640
+ if location_key in coords_db:
641
+ lat, lon = coords_db[location_key]
642
+ location_coords.append((location, lat, lon))
643
+ else:
644
+ print(f"DEBUG: Location '{location}' not found in coordinates database")
645
+
646
+ # Detect route patterns
647
+ routes = []
648
+ message_lower = message.lower()
649
+
650
+ route_patterns = ["from", "to", "drive", "route", "road trip", "journey", "travel", "start in", "then go", "then"]
651
+
652
+ if any(pattern in message_lower for pattern in route_patterns) and len(location_coords) >= 2:
653
+ routes.append(location_coords)
654
+
655
+ return location_coords, routes
656
+
657
+ # Replace your intelligent_weather_chat function with this stabilized version
658
+
659
+ def intelligent_weather_chat(message, history):
660
+ """Comprehensive chat with weather + daylight + driving data - STABILIZED VERSION"""
661
+ try:
662
+ # MAKE SURE THESE VARIABLES ARE INITIALIZED AT THE TOP
663
+ locations = extract_locations_from_text(message)
664
+ date = extract_date_from_text(message)
665
+ route_geometry = []
666
+ location_coords = [] # ← ADD THIS LINE
667
+
668
+ print(f"DEBUG: Extracted locations: {locations}")
669
+
670
+ # Determine what data to fetch based on the user's question
671
+ get_weather = should_get_weather_data(message)
672
+ get_daylight = should_get_daylight_data(message)
673
+ get_driving = should_get_driving_data(message)
674
+
675
+ # Smart defaults based on number of locations
676
+ if locations and not get_weather and not get_daylight and not get_driving:
677
+ if len(locations) >= 2:
678
+ get_weather = True
679
+ get_driving = True
680
+ else:
681
+ get_weather = True
682
+
683
+ weather_data = {}
684
+ daylight_data = {}
685
+ driving_data = {}
686
+
687
+ # Fetch weather data for up to 2 locations (reduced from 3)
688
+ if get_weather:
689
+ for location in locations[:2]:
690
+ current_weather = call_mcp_server(WEATHER_MCP_URL, "get_weather", {"location": location})
691
+ if "content" in current_weather:
692
+ weather_data[location] = format_response(current_weather, "weather")
693
+
694
+ # Fetch daylight data for up to 2 locations
695
+ if get_daylight:
696
+ for location in locations[:2]:
697
+ daylight_args = {"location": location}
698
+ if date:
699
+ daylight_args["date"] = date
700
+
701
+ daylight_times = call_mcp_server(DAYLIGHT_MCP_URL, "get_daylight_times", daylight_args)
702
+ if "content" in daylight_times:
703
+ daylight_data[location] = format_response(daylight_times, "daylight")
704
+
705
+ # Fetch driving data for 2+ locations
706
+ if get_driving and len(locations) >= 2:
707
+ try:
708
+ # GET LOCATION COORDINATES FIRST
709
+ location_coords, _ = extract_locations_and_routes_from_conversation(message, locations)
710
+ print(f"DEBUG: location_coords for route: {location_coords}")
711
+
712
+ if len(locations) == 2:
713
+ print(f"DEBUG: About to call driving MCP for {locations}")
714
+ driving_result = call_mcp_server(
715
+ DRIVING_MCP_URL,
716
+ "get_driving_distance",
717
+ {
718
+ "from_location": locations[0],
719
+ "to_location": locations[1]
720
+ }
721
+ )
722
+ if "content" in driving_result:
723
+ driving_data[f"{locations[0]} → {locations[1]}"] = format_response(driving_result, "driving")
724
+ # Extract route geometry
725
+ route_geometry = extract_route_geometry_from_mcp(driving_result, location_coords)
726
+ print(f"DEBUG: Final route_geometry: {len(route_geometry)} points")
727
+ else:
728
+ print(f"DEBUG: No content in driving result: {driving_result}")
729
+
730
+ elif len(locations) >= 3:
731
+ # ENHANCED: Get wiggly routes for 3+ locations by creating segments
732
+ print(f"DEBUG: Multi-location route with {len(locations)} stops")
733
+ all_route_points = []
734
+ driving_segments = []
735
+
736
+ # Create route segments between consecutive locations
737
+ for i in range(len(locations) - 1):
738
+ from_loc = locations[i]
739
+ to_loc = locations[i + 1]
740
+
741
+ print(f"DEBUG: Getting segment {from_loc} → {to_loc}")
742
+
743
+ driving_result = call_mcp_server(
744
+ DRIVING_MCP_URL,
745
+ "get_driving_distance",
746
+ {
747
+ "from_location": from_loc,
748
+ "to_location": to_loc
749
+ }
750
+ )
751
+
752
+ if "content" in driving_result:
753
+ segment_info = format_response(driving_result, "driving")
754
+ driving_segments.append(f"**{from_loc} → {to_loc}:** {segment_info}")
755
+
756
+ # Get wiggly route for this segment
757
+ if i < len(location_coords) - 1:
758
+ segment_coords = [location_coords[i], location_coords[i + 1]]
759
+ segment_route = extract_route_geometry_from_mcp(driving_result, segment_coords)
760
+
761
+ if len(segment_route) > 2: # We got actual route data
762
+ print(f"DEBUG: Segment {i+1} has {len(segment_route)} route points")
763
+ all_route_points.extend(segment_route)
764
+ else:
765
+ print(f"DEBUG: Segment {i+1} using straight line fallback")
766
+ # Add straight line for this segment
767
+ all_route_points.extend([
768
+ [location_coords[i][1], location_coords[i][2]],
769
+ [location_coords[i+1][1], location_coords[i+1][2]]
770
+ ])
771
+
772
+ # Combine all segments into one route
773
+ if all_route_points:
774
+ route_geometry = all_route_points
775
+ print(f"DEBUG: Combined route has {len(route_geometry)} total points")
776
+
777
+ # Combine driving info
778
+ if driving_segments:
779
+ driving_data["Multi-Stop Route"] = "\n\n".join(driving_segments)
780
+ else:
781
+ # Fallback to road trip planner
782
+ driving_result = call_mcp_server(
783
+ DRIVING_MCP_URL,
784
+ "plan_road_trip",
785
+ {"locations": locations[:4]}
786
+ )
787
+ if "content" in driving_result:
788
+ driving_data["Road Trip Plan"] = format_response(driving_result, "driving")
789
+ # Use straight lines as last resort
790
+ if location_coords:
791
+ route_geometry = [[lat, lon] for _, lat, lon in location_coords]
792
+ except Exception as e:
793
+ print(f"Driving data error: {e}")
794
+ route_geometry = []
795
+
796
+ # SIMPLIFIED SYSTEM PROMPT - much shorter to prevent token issues
797
+ system_prompt = """You are a helpful Scottish adventure assistant.
798
+
799
+ Be conversational, practical, and enthusiastic about Scottish adventures.
800
+
801
+ If you have weather data, focus on that first - interpret conditions for their activity and give gear advice.
802
+ If you have daylight data, mention it for photography or camping timing.
803
+ If you have driving data, include route advice and Highland driving tips.
804
+
805
+ Keep responses natural and under 200 words. Focus on practical advice for their Scottish adventure."""
806
+
807
+ # Build MUCH SHORTER context
808
+ context_parts = []
809
+ if weather_data:
810
+ context_parts.append("WEATHER:")
811
+ for location, weather in weather_data.items():
812
+ # Truncate weather data to prevent token overflow
813
+ short_weather = weather[:300] + "..." if len(weather) > 300 else weather
814
+ context_parts.append(f"• {location}: {short_weather}")
815
+
816
+ if daylight_data:
817
+ context_parts.append("\nDAYLIGHT:")
818
+ for location, daylight in daylight_data.items():
819
+ short_daylight = daylight[:200] + "..." if len(daylight) > 200 else daylight
820
+ context_parts.append(f"• {location}: {short_daylight}")
821
+
822
+ if driving_data:
823
+ context_parts.append("\nDRIVING:")
824
+ for route, info in driving_data.items():
825
+ short_driving = info[:300] + "..." if len(info) > 300 else info
826
+ context_parts.append(f"• {route}: {short_driving}")
827
+
828
+ if context_parts:
829
+ comprehensive_context = "\n".join(context_parts)
830
+ user_message = f"""User: "{message}"
831
+
832
+ {comprehensive_context}
833
+
834
+ Give a helpful, natural response under 200 words focusing on their Scottish adventure needs."""
835
+ else:
836
+ user_message = message
837
+
838
+ print(f"DEBUG: Context length: {len(user_message)} chars")
839
+
840
+ # SEVERELY LIMIT conversation history to prevent token overflow
841
+ recent_history = history[-2:] if len(history) > 2 else history
842
+
843
+ messages = [{"role": "system", "content": system_prompt}]
844
+
845
+ for user_msg, bot_msg in recent_history:
846
+ # Truncate long messages
847
+ truncated_user = user_msg[:100] + "..." if len(user_msg) > 100 else user_msg
848
+ truncated_bot = bot_msg[:200] + "..." if len(bot_msg) > 200 else bot_msg
849
+ messages.append({"role": "user", "content": truncated_user})
850
+ messages.append({"role": "assistant", "content": truncated_bot})
851
+
852
+ messages.append({"role": "user", "content": user_message})
853
+
854
+ # STABILIZED AI PARAMETERS
855
+ response = client.chat.completions.create(
856
+ model="deepseek-ai/DeepSeek-V3",
857
+ messages=messages,
858
+ max_tokens=300, # Severely reduced
859
+ temperature=0.1, # Much more conservative
860
+ top_p=0.9, # Add top_p for stability
861
+ frequency_penalty=0.3, # Prevent repetition
862
+ presence_penalty=0.1
863
+ )
864
+
865
+ bot_response = response.choices[0].message.content
866
+
867
+ # RESPONSE VALIDATION - catch broken responses
868
+ if (
869
+ "correct answer" in bot_response.lower() or
870
+ len(bot_response.split()) < 5 or
871
+ len(set(bot_response.split()[-10:])) < 3 or # Detect repetition
872
+ bot_response.count("16°C") > 5 # Detect specific repetition
873
+ ):
874
+ print("DEBUG: Detected broken AI response, using fallback")
875
+
876
+ # FALLBACK: Simple data summary
877
+ fallback_parts = []
878
+ if weather_data:
879
+ for location, weather in weather_data.items():
880
+ # Extract key info manually
881
+ lines = weather.split('\n')
882
+ temp_line = next((line for line in lines if '°C' in line), "")
883
+ fallback_parts.append(f"**{location}:** {temp_line}")
884
+
885
+ if driving_data:
886
+ for route, info in driving_data.items():
887
+ lines = info.split('\n')
888
+ distance_line = next((line for line in lines if 'km' in line or 'Distance' in line), "")
889
+ time_line = next((line for line in lines if 'Time' in line or 'hour' in line), "")
890
+ fallback_parts.append(f"**{route}:** {distance_line} {time_line}")
891
+
892
+ if fallback_parts:
893
+ bot_response = "Here's your Scottish adventure info:\n\n" + "\n".join(fallback_parts)
894
+ bot_response += "\n\nFor detailed planning, try asking about specific aspects like weather or routes separately!"
895
+ else:
896
+ bot_response = "I can help you plan your Scottish adventure! Try asking about specific locations like 'weather in Edinburgh' or 'drive from Glasgow to Skye'."
897
+
898
+ print(f"DEBUG: Final response length: {len(bot_response)} chars")
899
+
900
+ # ========== MAP UPDATE LOGIC ==========
901
+ # Extract locations and routes for map
902
+ if not location_coords and locations:
903
+ location_coords, routes = extract_locations_and_routes_from_conversation(message, locations)
904
+
905
+ # Create updated map HTML
906
+ if location_coords:
907
+ updated_map_html = create_map_html(
908
+ locations=location_coords,
909
+ routes=route_geometry,
910
+ center_lat=location_coords[0][1] if location_coords else 56.8,
911
+ center_lon=location_coords[0][2] if location_coords else -4.2,
912
+ zoom=8 if len(location_coords) <= 2 else 7
913
+ )
914
+ else:
915
+ # Default Scotland overview map
916
+ updated_map_html = create_map_html(
917
+ locations=[],
918
+ routes=[],
919
+ center_lat=56.8,
920
+ center_lon=-4.2,
921
+ zoom=6
922
+ )
923
+
924
+ print(f"DEBUG: Map updated with {len(location_coords)} locations")
925
+
926
+ except Exception as e:
927
+ print(f"ERROR: {e}")
928
+ bot_response = "I'm having technical difficulties. Please try a simpler question like 'weather in Edinburgh' or let me know specific Scottish locations you're interested in!"
929
+ # Default map for error case
930
+ updated_map_html = create_map_html()
931
+ location_coords = [] # ← ADD THIS LINE
932
+
933
+ history.append([message, bot_response])
934
+ return history, "", updated_map_html
935
+
936
+ # Create the ultimate Scottish adventure planning interface
937
+ with gr.Blocks(title="🏴󠁧󠁢󠁳󠁣󠁴󠁿 Scotland Adventure Planner", theme=gr.themes.Soft()) as app:
938
+ gr.Markdown("# 🏴󠁧󠁢󠁳󠁣󠁴󠁿 Scotland Adventure Planner")
939
+ gr.Markdown("**Your complete Scottish adventure assistant!** Get weather, driving distances, recommendations.")
940
+
941
+ with gr.Row():
942
+ with gr.Column(scale=3):
943
+ chatbot = gr.Chatbot(height=400, type='tuples')
944
+ msg = gr.Textbox(
945
+ label="Plan your Scottish adventure!",
946
+ placeholder="Try: 'Road trip from Edinburgh to Skye' or 'Photography spots near Fort William'",
947
+ lines=2
948
+ )
949
+
950
+ with gr.Column(scale=2):
951
+ # Simplified map display
952
+ map_display = gr.HTML(
953
+ value="""
954
+ <div style="width: 100%; height: 400px; border: 2px solid #ddd; background: #f5f5f5; display: flex; align-items: center; justify-content: center;">
955
+ <div style="text-align: center;">
956
+ <h3>🗺️ Interactive Map</h3>
957
+ <p>Map will show locations from your conversation</p>
958
+ </div>
959
+ </div>
960
+ """,
961
+ label="📍 Interactive Map"
962
+ )
963
+
964
+ # Compact example buttons
965
+ gr.Markdown("### 🎯 Quick Examples")
966
+ with gr.Row():
967
+ example1 = gr.Button("☀️ Weather Edinburgh", size="sm")
968
+ example2 = gr.Button("🚗 Drive Edinburgh→Skye", size="sm")
969
+ example3 = gr.Button("📸 Golden hour Glencoe", size="sm")
970
+ example4 = gr.Button("🏕️ Camping Cairngorms", size="sm")
971
+
972
+ with gr.Row():
973
+ example5 = gr.Button("🗺️ Road trip Glasgow→Skye", size="sm")
974
+ example6 = gr.Button("🌄 Photography Isle of Mull", size="sm")
975
+ example7 = gr.Button("⛅ Weather + route Perth→Fort William", size="sm")
976
+ example8 = gr.Button("🥾 Hiking weather Ben Nevis", size="sm")
977
+
978
+ # IMPORTANT: Update the submit function to also update the map
979
+ msg.submit(intelligent_weather_chat, [msg, chatbot], [chatbot, msg, map_display])
980
+
981
+ # Button actions
982
+ example1.click(lambda: "What's the weather like in Edinburgh?", outputs=msg)
983
+ example2.click(lambda: "How long to drive from Edinburgh to Skye?", outputs=msg)
984
+ example3.click(lambda: "Golden hour photography times in Glencoe?", outputs=msg)
985
+ example4.click(lambda: "Good camping weather in Cairngorms?", outputs=msg)
986
+ example5.click(lambda: "Road trip from Glasgow to Skye with stops", outputs=msg)
987
+ example6.click(lambda: "Best photography spots on Isle of Mull", outputs=msg)
988
+ example7.click(lambda: "Weather and driving route from Perth to Fort William", outputs=msg)
989
+ example8.click(lambda: "Hiking weather around Ben Nevis area", outputs=msg)
990
+
991
+ gr.Markdown("*Powered by Open-Meteo weather data, Sunrise-Sunset API, OpenRouteService routing, custom MCP servers, and Nebius AI Studio*")
992
+
993
+ if __name__ == "__main__":
994
+ app.launch(share=True)
readme.md ADDED
@@ -0,0 +1,311 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🏴󠁧󠁢󠁳󠁣󠁴󠁿 Scotland Adventure Weather Planner
2
+ tags:
3
+ - agent-demo-track
4
+
5
+
6
+ https://github.com/user-attachments/assets/5adf9e57-7087-4523-9198-8134f13cff7c
7
+
8
+
9
+ A comprehensive Scottish adventure planning app that combines three custom MCP (Model Context Protocol) servers with an intelligent Gradio interface. Get weather forecasts, driving routes, daylight times, and AI-powered recommendations for your Scottish adventures.
10
+
11
+ ## 🎯 Features
12
+
13
+ ### 🌦️ Weather Intelligence
14
+ - **Real-time weather data** - Current conditions and 7-day forecasts for any Scottish location
15
+ - **Adventure-focused recommendations** - Activity-specific advice for hiking, photography, camping
16
+ - **Geographic disambiguation** - Automatically finds Scottish locations (not US namesakes!)
17
+ - **Weather safety alerts** - Wind warnings, precipitation alerts, visibility conditions
18
+
19
+ ### 🌅 Daylight Planning
20
+ - **Sunrise/sunset times** - Perfect for photography and outdoor activity planning
21
+ - **Golden hour calculations** - Optimal lighting times for photographers
22
+ - **Seasonal daylight tracking** - Essential for Highland adventures where daylight varies dramatically
23
+
24
+ ### 🚗 Route Planning
25
+ - **Driving distances and times** - Between any Scottish locations
26
+ - **Multi-stop road trip planning** - Optimized routes with Highland driving considerations
27
+ - **Interactive route visualization** - Real driving routes displayed on maps
28
+ - **Scottish driving tips** - Single-track roads, ferry times, fuel stops
29
+
30
+ ### 🤖 AI Chat Interface
31
+ - **Natural language queries** - Ask questions like "Road trip from Edinburgh to Skye"
32
+ - **Intelligent data synthesis** - Combines weather, driving, and daylight data
33
+ - **Adventure recommendations** - Personalized suggestions based on conditions
34
+ - **Interactive maps** - Visual route and location display
35
+
36
+ ## 🏗️ Architecture
37
+
38
+ ### Three Custom MCP Servers
39
+ 1. **Weather MCP** (`scotland-weather-mcp`) - Open-Meteo API integration
40
+ 2. **Daylight MCP** (`scotland-daylight-mcp`) - Sunrise-Sunset API integration
41
+ 3. **Driving MCP** (`scottish-driving-mcp`) - OpenRouteService integration
42
+
43
+ ### Gradio Frontend
44
+ - **Multi-functional interface** - Chat, quick examples, interactive maps
45
+ - **Real-time data integration** - Fetches from all three MCP servers
46
+ - **AI-powered responses** - Uses Nebius AI Studio for intelligent synthesis
47
+
48
+ ## 🚀 Quick Start
49
+
50
+ ### Prerequisites
51
+ ```bash
52
+ Python 3.8+
53
+ Modal account (for MCP server deployment)
54
+ OpenRouteService API key (free tier: 2000 requests/day)
55
+ Nebius AI Studio API key
56
+ ```
57
+
58
+ ### 1. Deploy MCP Servers
59
+
60
+ #### Weather Server
61
+ ```bash
62
+ # Clone and setup
63
+ git clone <repository>
64
+ cd scotland-weather-adventure
65
+
66
+ # Deploy weather MCP
67
+ modal deploy weather_server.py
68
+ # Creates: https://your-username--scotland-weather-mcp-fastapi-app.modal.run
69
+ ```
70
+
71
+ #### Daylight Server
72
+ ```bash
73
+ # Deploy daylight MCP
74
+ modal deploy daylight_server.py
75
+ # Creates: https://your-username--scotland-daylight-mcp-fastapi-app.modal.run
76
+ ```
77
+
78
+ #### Driving Server
79
+ ```bash
80
+ # Get free API key from: https://openrouteservice.org/dev/#/signup
81
+ modal secret create openrouteservice OPENROUTESERVICE_API_KEY=your_key_here
82
+
83
+ # Deploy driving MCP
84
+ modal deploy driving_server.py
85
+ # Creates: https://your-username--scottish-driving-mcp-fastapi-app.modal.run
86
+ ```
87
+
88
+ ### 2. Setup Gradio Frontend
89
+ ```bash
90
+ # Update MCP server URLs in app.py
91
+ WEATHER_MCP_URL = "https://your-username--scotland-weather-mcp-fastapi-app.modal.run/mcp"
92
+ DAYLIGHT_MCP_URL = "https://your-username--scotland-daylight-mcp-fastapi-app.modal.run/mcp"
93
+ DRIVING_MCP_URL = "https://your-username--scottish-driving-mcp-fastapi-app.modal.run/mcp"
94
+
95
+ # Add your Nebius AI Studio API key
96
+ client = OpenAI(api_key="your_nebius_key_here", base_url="https://api.studio.nebius.ai/v1")
97
+
98
+ # Install dependencies and run
99
+ pip install gradio requests openai folium
100
+ python app.py
101
+ ```
102
+
103
+ ## 📁 Project Structure
104
+
105
+ ```
106
+ scotland-weather-adventure/
107
+ ├── README.md # This file
108
+ ├── app.py # Main Gradio web interface
109
+ ├── weather_server.py # Weather MCP server (Modal deployment)
110
+ ├── daylight_server.py # Daylight MCP server (Modal deployment)
111
+ ├── driving_server.py # Driving MCP server (Modal deployment)
112
+ └── requirements.txt # Dependencies
113
+ ```
114
+
115
+ ## 🛠️ MCP Server APIs
116
+
117
+ ### Weather MCP Tools
118
+
119
+ #### `get_weather`
120
+ Get current weather conditions for any Scottish location.
121
+ ```json
122
+ {
123
+ "method": "tools/call",
124
+ "params": {
125
+ "name": "get_weather",
126
+ "arguments": {"location": "Edinburgh"}
127
+ }
128
+ }
129
+ ```
130
+
131
+ #### `get_forecast`
132
+ Get 1-7 day weather forecast with adventure planning insights.
133
+ ```json
134
+ {
135
+ "method": "tools/call",
136
+ "params": {
137
+ "name": "get_forecast",
138
+ "arguments": {"location": "Fort William", "days": 5}
139
+ }
140
+ }
141
+ ```
142
+
143
+ ### Daylight MCP Tools
144
+
145
+ #### `get_daylight_times`
146
+ Get sunrise, sunset, and golden hour times for photography planning.
147
+ ```json
148
+ {
149
+ "method": "tools/call",
150
+ "params": {
151
+ "name": "get_daylight_times",
152
+ "arguments": {"location": "Glencoe", "date": "2024-07-15"}
153
+ }
154
+ }
155
+ ```
156
+
157
+ ### Driving MCP Tools
158
+
159
+ #### `get_driving_distance`
160
+ Calculate driving distance and time between locations.
161
+ ```json
162
+ {
163
+ "method": "tools/call",
164
+ "params": {
165
+ "name": "get_driving_distance",
166
+ "arguments": {
167
+ "from_location": "Edinburgh",
168
+ "to_location": "Isle of Skye"
169
+ }
170
+ }
171
+ }
172
+ ```
173
+
174
+ #### `plan_road_trip`
175
+ Plan multi-stop road trips with optimized Scottish routes.
176
+ ```json
177
+ {
178
+ "method": "tools/call",
179
+ "params": {
180
+ "name": "plan_road_trip",
181
+ "arguments": {
182
+ "locations": ["Glasgow", "Fort William", "Isle of Skye", "Inverness"]
183
+ }
184
+ }
185
+ }
186
+ ```
187
+
188
+ ## 🎮 Gradio Interface Features
189
+
190
+ ### 💬 Intelligent Chat
191
+ - Natural language adventure planning
192
+ - Combines weather, driving, and daylight data automatically
193
+ - Scottish location recognition and disambiguation
194
+ - Activity-specific recommendations
195
+
196
+ ### 📍 Interactive Maps
197
+ - Real driving route visualization using OpenRouteService
198
+ - Multiple location support with markers
199
+ - Route geometry display (not just straight lines!)
200
+ - Automatic map centering and zoom
201
+
202
+ ### 🎯 Quick Examples
203
+ Pre-built example queries:
204
+ - "☀️ Weather Edinburgh"
205
+ - "🚗 Drive Edinburgh→Skye"
206
+ - "📸 Golden hour Glencoe"
207
+ - "🗺️ Road trip Glasgow→Skye"
208
+ - And more...
209
+
210
+ ## 🌦️ Intelligent Features
211
+
212
+ ### Scottish Geographic Intelligence
213
+ The system automatically handles location disambiguation:
214
+ - **"Perth"** → Finds Perth, Scotland (not Australia)
215
+ - **"Hamilton"** → Finds Hamilton, Scotland (not Ontario)
216
+ - **"Arran"** → Finds Isle of Arran, Scotland (not Ireland)
217
+
218
+ ### Adventure-Specific Recommendations
219
+ - **Hiking**: Wind warnings, precipitation alerts, visibility
220
+ - **Photography**: Golden hour times, clear sky recommendations
221
+ - **Driving**: Highland road conditions, single-track warnings
222
+ - **Camping**: Daylight hours, weather suitability
223
+
224
+ ### Scottish Driving Considerations
225
+ - Single-track Highland roads (allow extra time)
226
+ - Ferry schedules for island destinations
227
+ - Remote area fuel stop planning
228
+ - Highland weather driving safety
229
+
230
+ ## 🚢 Deployment Options
231
+
232
+ ### MCP Servers (Modal - Recommended)
233
+ ```bash
234
+ # All three servers deploy to Modal's serverless platform
235
+ modal deploy weather_server.py
236
+ modal deploy daylight_server.py
237
+ modal deploy driving_server.py
238
+ ```
239
+
240
+ ### Gradio Frontend
241
+ - **Local Development**: `python app.py`
242
+ - **Gradio Sharing**: Built-in public demo links (`share=True`)
243
+ - **Production**: Deploy to Hugging Face Spaces, Modal, Railway, etc.
244
+
245
+ ## 🔧 Configuration
246
+
247
+ ### Environment Variables
248
+ ```bash
249
+ # Required for driving server
250
+ OPENROUTESERVICE_API_KEY=your_openroute_key
251
+
252
+ # Required for AI chat
253
+ NEBIUS_API_KEY=your_nebius_key
254
+ ```
255
+
256
+ ### API Keys Needed
257
+ 1. **OpenRouteService** (Free: 2000 requests/day) - For driving routes
258
+ 2. **Nebius AI Studio** - For intelligent chat responses
259
+ 3. **No API keys needed** for weather (Open-Meteo) or daylight (Sunrise-Sunset API)
260
+
261
+ ## 🏔️ Example Use Cases
262
+
263
+ ### Weekend Trip Planning
264
+ - **Query**: "Should I go to Aviemore or Cairngorms this weekend?"
265
+ - **Response**: Weather comparison, driving times, daylight hours, activity recommendations
266
+
267
+ ### Photography Expeditions
268
+ - **Query**: "Golden hour photography spots near Fort William"
269
+ - **Response**: Sunrise/sunset times, weather conditions, recommended locations
270
+
271
+ ### Multi-Day Adventures
272
+ - **Query**: "5-day road trip from Edinburgh to Skye with camping"
273
+ - **Response**: Route planning, weather forecast, camping suitability, daily recommendations
274
+
275
+ ### Safety Planning
276
+ - **Query**: "Are there wind warnings for climbing in Glencoe?"
277
+ - **Response**: Wind speed alerts, weather safety assessment, alternative suggestions
278
+
279
+ ## 🤝 Contributing
280
+
281
+ This project was built for adventure planning and learning! Contributions welcome:
282
+
283
+ 1. Fork the repository
284
+ 2. Create a feature branch (`git checkout -b feature/amazing-feature`)
285
+ 3. Make your changes
286
+ 4. Submit a pull request
287
+
288
+ ### Ideas for Enhancement
289
+ - [ ] Add tide times for coastal adventures
290
+ - [ ] Include mountain weather conditions (snow, ice)
291
+ - [ ] Ferry schedule integration
292
+ - [ ] Accommodation booking suggestions
293
+ - [ ] Trail condition reports
294
+
295
+ ## 📄 License
296
+
297
+ Open source - built for Scottish adventure enthusiasts and outdoor learning!
298
+
299
+ ## 🙏 Credits
300
+
301
+ - **Weather Data**: [Open-Meteo](https://open-meteo.com/) (free weather API)
302
+ - **Daylight Data**: [Sunrise-Sunset.org](https://sunrise-sunset.org/) API
303
+ - **Routing**: [OpenRouteService](https://openrouteservice.org/)
304
+ - **Deployment**: [Modal](https://modal.com/) serverless platform
305
+ - **AI**: [Nebius AI Studio](https://studio.nebius.ai/)
306
+ - **Interface**: [Gradio](https://gradio.app/) web framework
307
+ - **Maps**: [Folium](https://python-visualization.github.io/folium/) Python mapping
308
+
309
+ ---
310
+
311
+ *Built with ❤️ for Scottish outdoor enthusiasts and powered by multiple free APIs for maximum accessibility*ß
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ gradio>=4.0.0
2
+ requests>=2.31.0
3
+ openai>=1.0.0
4
+ folium>=0.14.0