pythonprincess commited on
Commit
44588a9
·
verified ·
1 Parent(s): 6294a91

Delete app/weather_agent.py

Browse files
Files changed (1) hide show
  1. app/weather_agent.py +0 -535
app/weather_agent.py DELETED
@@ -1,535 +0,0 @@
1
- # app/weather_agent.py
2
- """
3
- 🌤️ PENNY Weather Agent - Azure Maps Integration
4
-
5
- Provides real-time weather information and weather-aware recommendations
6
- for civic engagement activities.
7
-
8
- MISSION: Help residents plan their day with accurate weather data and
9
- smart suggestions for indoor/outdoor activities based on conditions.
10
-
11
- ENHANCEMENTS (Phase 1 Complete):
12
- - ✅ Structured logging with performance tracking
13
- - ✅ Enhanced error handling with graceful degradation
14
- - ✅ Type hints for all functions
15
- - ✅ Health check integration
16
- - ✅ Response caching for performance
17
- - ✅ Detailed weather parsing with validation
18
- - ✅ Penny's friendly voice in all responses
19
-
20
- Production-ready for Azure ML deployment.
21
- """
22
-
23
- import os
24
- import logging
25
- import time
26
- from typing import Dict, Any, Optional, List, Tuple
27
- from datetime import datetime, timedelta
28
- import httpx
29
-
30
- # --- ENHANCED MODULE IMPORTS ---
31
- from app.logging_utils import log_interaction
32
-
33
- # --- LOGGING SETUP ---
34
- logger = logging.getLogger(__name__)
35
-
36
- # --- CONFIGURATION ---
37
- AZURE_WEATHER_URL = "https://atlas.microsoft.com/weather/currentConditions/json"
38
- DEFAULT_TIMEOUT = 10.0 # seconds
39
- CACHE_TTL_SECONDS = 300 # 5 minutes - weather doesn't change that fast
40
-
41
- # --- CHECK API KEY AVAILABILITY AT MODULE LOAD (NEW - PREVENTS IMPORT FAILURES) ---
42
- AZURE_MAPS_KEY = os.getenv("AZURE_MAPS_KEY")
43
-
44
- if not AZURE_MAPS_KEY:
45
- logger.warning("⚠️ AZURE_MAPS_KEY not configured - weather features will be limited")
46
- _WEATHER_SERVICE_AVAILABLE = False
47
- else:
48
- logger.info("✅ AZURE_MAPS_KEY configured")
49
- _WEATHER_SERVICE_AVAILABLE = True
50
-
51
- # --- WEATHER CACHE ---
52
- _weather_cache: Dict[str, Tuple[Dict[str, Any], datetime]] = {}
53
-
54
-
55
- # ============================================================
56
- # WEATHER DATA RETRIEVAL
57
- # ============================================================
58
-
59
- async def get_weather_for_location(
60
- lat: float,
61
- lon: float,
62
- use_cache: bool = True
63
- ) -> Dict[str, Any]:
64
- """
65
- 🌤️ Fetches real-time weather from Azure Maps.
66
-
67
- Retrieves current weather conditions for a specific location using
68
- Azure Maps Weather API. Includes caching to reduce API calls and
69
- improve response times.
70
-
71
- Args:
72
- lat: Latitude coordinate
73
- lon: Longitude coordinate
74
- use_cache: Whether to use cached data if available (default: True)
75
-
76
- Returns:
77
- Dictionary containing weather data with keys:
78
- - temperature: {value: float, unit: str}
79
- - phrase: str (weather description)
80
- - iconCode: int
81
- - hasPrecipitation: bool
82
- - isDayTime: bool
83
- - relativeHumidity: int
84
- - cloudCover: int
85
- - etc.
86
-
87
- Raises:
88
- RuntimeError: If AZURE_MAPS_KEY is not configured
89
- httpx.HTTPError: If API request fails
90
-
91
- Example:
92
- weather = await get_weather_for_location(33.7490, -84.3880)
93
- temp = weather.get("temperature", {}).get("value")
94
- condition = weather.get("phrase", "Unknown")
95
- """
96
- start_time = time.time()
97
-
98
- # Create cache key
99
- cache_key = f"{lat:.4f},{lon:.4f}"
100
-
101
- # Check cache first
102
- if use_cache and cache_key in _weather_cache:
103
- cached_data, cached_time = _weather_cache[cache_key]
104
- age = (datetime.now() - cached_time).total_seconds()
105
-
106
- if age < CACHE_TTL_SECONDS:
107
- logger.info(
108
- f"🌤️ Weather cache hit (age: {age:.0f}s, "
109
- f"location: {cache_key})"
110
- )
111
- return cached_data
112
-
113
- # Check if service is available (MODIFIED - USES FLAG INSTEAD OF CHECKING ENV VAR)
114
- if not _WEATHER_SERVICE_AVAILABLE:
115
- logger.error("❌ AZURE_MAPS_KEY not configured")
116
- raise RuntimeError(
117
- "AZURE_MAPS_KEY is required and not set in environment variables."
118
- )
119
-
120
- # Build request parameters
121
- params = {
122
- "api-version": "1.0",
123
- "query": f"{lat},{lon}",
124
- "subscription-key": AZURE_MAPS_KEY,
125
- "details": "true",
126
- "language": "en-US",
127
- }
128
-
129
- try:
130
- logger.info(f"🌤️ Fetching weather for location: {cache_key}")
131
-
132
- async with httpx.AsyncClient(timeout=DEFAULT_TIMEOUT) as client:
133
- response = await client.get(AZURE_WEATHER_URL, params=params)
134
- response.raise_for_status()
135
- data = response.json()
136
-
137
- # Parse response
138
- if "results" in data and len(data["results"]) > 0:
139
- weather_data = data["results"][0]
140
- else:
141
- weather_data = data # Fallback if structure changes
142
-
143
- # Validate essential fields
144
- weather_data = _validate_weather_data(weather_data)
145
-
146
- # Cache the result
147
- _weather_cache[cache_key] = (weather_data, datetime.now())
148
-
149
- # Calculate response time
150
- response_time = (time.time() - start_time) * 1000
151
-
152
- # Log successful retrieval
153
- log_interaction(
154
- tenant_id="weather_service",
155
- interaction_type="weather_fetch",
156
- intent="weather",
157
- response_time_ms=response_time,
158
- success=True,
159
- metadata={
160
- "location": cache_key,
161
- "cached": False,
162
- "temperature": weather_data.get("temperature", {}).get("value"),
163
- "condition": weather_data.get("phrase")
164
- }
165
- )
166
-
167
- logger.info(
168
- f"✅ Weather fetched successfully ({response_time:.0f}ms, "
169
- f"location: {cache_key})"
170
- )
171
-
172
- return weather_data
173
-
174
- except httpx.TimeoutException as e:
175
- logger.error(f"⏱️ Weather API timeout: {e}")
176
- raise
177
-
178
- except httpx.HTTPStatusError as e:
179
- logger.error(f"❌ Weather API HTTP error: {e.response.status_code}")
180
- raise
181
-
182
- except Exception as e:
183
- logger.error(f"❌ Weather API error: {e}", exc_info=True)
184
- raise
185
-
186
-
187
- def _validate_weather_data(data: Dict[str, Any]) -> Dict[str, Any]:
188
- """
189
- Validates and normalizes weather data from Azure Maps.
190
-
191
- Ensures essential fields are present with sensible defaults.
192
- """
193
- # Ensure temperature exists
194
- if "temperature" not in data:
195
- data["temperature"] = {"value": None, "unit": "F"}
196
- elif isinstance(data["temperature"], (int, float)):
197
- # Handle case where temperature is just a number
198
- data["temperature"] = {"value": data["temperature"], "unit": "F"}
199
-
200
- # Ensure phrase exists
201
- if "phrase" not in data or not data["phrase"]:
202
- data["phrase"] = "Conditions unavailable"
203
-
204
- # Ensure boolean flags exist
205
- data.setdefault("hasPrecipitation", False)
206
- data.setdefault("isDayTime", True)
207
-
208
- # Ensure numeric fields exist
209
- data.setdefault("relativeHumidity", None)
210
- data.setdefault("cloudCover", None)
211
- data.setdefault("iconCode", None)
212
-
213
- return data
214
-
215
-
216
- # ============================================================
217
- # OUTFIT RECOMMENDATIONS
218
- # ============================================================
219
-
220
- def recommend_outfit(high_temp: float, condition: str) -> str:
221
- """
222
- 👕 Recommends what to wear based on weather conditions.
223
-
224
- Provides friendly, practical clothing suggestions based on
225
- temperature and weather conditions.
226
-
227
- Args:
228
- high_temp: Expected high temperature in Fahrenheit
229
- condition: Weather condition description (e.g., "Sunny", "Rainy")
230
-
231
- Returns:
232
- Friendly outfit recommendation string
233
-
234
- Example:
235
- outfit = recommend_outfit(85, "Sunny")
236
- # Returns: "Light clothes, sunscreen, and stay hydrated! ☀️"
237
- """
238
- condition_lower = condition.lower()
239
-
240
- # Check for precipitation first
241
- if "rain" in condition_lower or "storm" in condition_lower:
242
- logger.debug(f"Outfit rec: Rain/Storm (temp: {high_temp}°F)")
243
- return "Bring an umbrella or rain jacket! ☔"
244
-
245
- # Temperature-based recommendations
246
- if high_temp >= 85:
247
- logger.debug(f"Outfit rec: Hot (temp: {high_temp}°F)")
248
- return "Light clothes, sunscreen, and stay hydrated! ☀️"
249
-
250
- if high_temp >= 72:
251
- logger.debug(f"Outfit rec: Warm (temp: {high_temp}°F)")
252
- return "T-shirt and jeans or a casual dress. 👕"
253
-
254
- if high_temp >= 60:
255
- logger.debug(f"Outfit rec: Mild (temp: {high_temp}°F)")
256
- return "A hoodie or light jacket should do! 🧥"
257
-
258
- logger.debug(f"Outfit rec: Cold (temp: {high_temp}°F)")
259
- return "Bundle up — sweater or coat recommended! 🧣"
260
-
261
-
262
- # ============================================================
263
- # EVENT RECOMMENDATIONS BASED ON WEATHER
264
- # ============================================================
265
-
266
- def weather_to_event_recommendations(
267
- weather: Dict[str, Any]
268
- ) -> List[Dict[str, Any]]:
269
- """
270
- 📅 Suggests activity types based on current weather conditions.
271
-
272
- Analyzes weather data to provide smart recommendations for
273
- indoor vs outdoor activities, helping residents plan their day.
274
-
275
- Args:
276
- weather: Weather data dictionary from get_weather_for_location()
277
-
278
- Returns:
279
- List of recommendation dictionaries with keys:
280
- - type: str ("indoor", "outdoor", "neutral")
281
- - suggestions: List[str] (specific activity ideas)
282
- - reason: str (explanation for recommendation)
283
- - priority: int (1-3, added for sorting)
284
-
285
- Example:
286
- weather = await get_weather_for_location(33.7490, -84.3880)
287
- recs = weather_to_event_recommendations(weather)
288
- for rec in recs:
289
- print(f"{rec['type']}: {rec['suggestions']}")
290
- """
291
- condition = (weather.get("phrase") or "").lower()
292
- temp = weather.get("temperature", {}).get("value")
293
- has_precipitation = weather.get("hasPrecipitation", False)
294
-
295
- recs = []
296
-
297
- # Check for rain or storms (highest priority)
298
- if "rain" in condition or "storm" in condition or has_precipitation:
299
- logger.debug("Event rec: Indoor (precipitation)")
300
- recs.append({
301
- "type": "indoor",
302
- "suggestions": [
303
- "Visit a library 📚",
304
- "Check out a community center event 🏛️",
305
- "Find an indoor workshop or class 🎨",
306
- "Explore a local museum 🖼️"
307
- ],
308
- "reason": "Rainy weather makes indoor events ideal!",
309
- "priority": 1
310
- })
311
-
312
- # Warm weather outdoor activities
313
- elif temp is not None and temp >= 75:
314
- logger.debug(f"Event rec: Outdoor (warm: {temp}°F)")
315
- recs.append({
316
- "type": "outdoor",
317
- "suggestions": [
318
- "Visit a park 🌳",
319
- "Check out a farmers market 🥕",
320
- "Look for outdoor concerts or festivals 🎵",
321
- "Enjoy a community picnic or BBQ 🍔"
322
- ],
323
- "reason": "Beautiful weather for outdoor activities!",
324
- "priority": 1
325
- })
326
-
327
- # Cold weather considerations
328
- elif temp is not None and temp < 50:
329
- logger.debug(f"Event rec: Indoor (cold: {temp}°F)")
330
- recs.append({
331
- "type": "indoor",
332
- "suggestions": [
333
- "Browse local events at community centers 🏛️",
334
- "Visit a museum or art gallery 🖼️",
335
- "Check out indoor markets or shopping 🛍️",
336
- "Warm up at a local café or restaurant ☕"
337
- ],
338
- "reason": "Chilly weather — indoor activities are cozy!",
339
- "priority": 1
340
- })
341
-
342
- # Mild/neutral weather
343
- else:
344
- logger.debug(f"Event rec: Neutral (mild: {temp}°F if temp else 'unknown')")
345
- recs.append({
346
- "type": "neutral",
347
- "suggestions": [
348
- "Browse local events 📅",
349
- "Visit a museum or cultural center 🏛️",
350
- "Walk around a local plaza or downtown 🚶",
351
- "Check out both indoor and outdoor activities 🌍"
352
- ],
353
- "reason": "Mild weather gives you flexible options!",
354
- "priority": 2
355
- })
356
-
357
- return recs
358
-
359
-
360
- # ============================================================
361
- # HELPER FUNCTIONS
362
- # ============================================================
363
-
364
- def format_weather_summary(weather: Dict[str, Any]) -> str:
365
- """
366
- 📝 Formats weather data into a human-readable summary.
367
-
368
- Args:
369
- weather: Weather data dictionary
370
-
371
- Returns:
372
- Formatted weather summary string with Penny's friendly voice
373
-
374
- Example:
375
- summary = format_weather_summary(weather_data)
376
- # "Currently 72°F and Partly Cloudy. Humidity: 65%"
377
- """
378
- temp_data = weather.get("temperature", {})
379
- temp = temp_data.get("value")
380
- unit = temp_data.get("unit", "F")
381
- phrase = weather.get("phrase", "Conditions unavailable")
382
- humidity = weather.get("relativeHumidity")
383
-
384
- # Build summary
385
- parts = []
386
-
387
- if temp is not None:
388
- parts.append(f"Currently {int(temp)}°{unit}")
389
-
390
- parts.append(phrase)
391
-
392
- if humidity is not None:
393
- parts.append(f"Humidity: {humidity}%")
394
-
395
- summary = " and ".join(parts[:2])
396
- if len(parts) > 2:
397
- summary += f". {parts[2]}"
398
-
399
- return summary
400
-
401
-
402
- def clear_weather_cache():
403
- """
404
- 🧹 Clears the weather cache.
405
-
406
- Useful for testing or if fresh data is needed immediately.
407
- """
408
- global _weather_cache
409
- cache_size = len(_weather_cache)
410
- _weather_cache.clear()
411
- logger.info(f"🧹 Weather cache cleared ({cache_size} entries removed)")
412
-
413
-
414
- def get_cache_stats() -> Dict[str, Any]:
415
- """
416
- 📊 Returns weather cache statistics.
417
-
418
- Returns:
419
- Dictionary with cache statistics:
420
- - entries: int (number of cached locations)
421
- - oldest_entry_age_seconds: float
422
- - newest_entry_age_seconds: float
423
- """
424
- if not _weather_cache:
425
- return {
426
- "entries": 0,
427
- "oldest_entry_age_seconds": None,
428
- "newest_entry_age_seconds": None
429
- }
430
-
431
- now = datetime.now()
432
- ages = [
433
- (now - cached_time).total_seconds()
434
- for _, cached_time in _weather_cache.values()
435
- ]
436
-
437
- return {
438
- "entries": len(_weather_cache),
439
- "oldest_entry_age_seconds": max(ages) if ages else None,
440
- "newest_entry_age_seconds": min(ages) if ages else None
441
- }
442
-
443
-
444
- # ============================================================
445
- # HEALTH CHECK
446
- # ============================================================
447
-
448
- def get_weather_agent_health() -> Dict[str, Any]:
449
- """
450
- 📊 Returns weather agent health status.
451
-
452
- Used by the main application health check endpoint to monitor
453
- the weather service availability and performance.
454
-
455
- Returns:
456
- Dictionary with health information
457
- """
458
- cache_stats = get_cache_stats()
459
-
460
- # MODIFIED - USES FLAG INSTEAD OF CHECKING ENV VAR
461
- return {
462
- "status": "operational" if _WEATHER_SERVICE_AVAILABLE else "degraded",
463
- "service": "azure_maps_weather",
464
- "api_key_configured": _WEATHER_SERVICE_AVAILABLE,
465
- "cache": cache_stats,
466
- "cache_ttl_seconds": CACHE_TTL_SECONDS,
467
- "default_timeout_seconds": DEFAULT_TIMEOUT,
468
- "features": {
469
- "real_time_weather": True,
470
- "outfit_recommendations": True,
471
- "event_recommendations": True,
472
- "response_caching": True
473
- }
474
- }
475
-
476
-
477
- # ============================================================
478
- # TESTING
479
- # ============================================================
480
-
481
- if __name__ == "__main__":
482
- """🧪 Test weather agent functionality"""
483
- import asyncio
484
-
485
- print("=" * 60)
486
- print("🧪 Testing Weather Agent")
487
- print("=" * 60)
488
-
489
- async def run_tests():
490
- # Test location: Atlanta, GA
491
- lat, lon = 33.7490, -84.3880
492
-
493
- print(f"\n--- Test 1: Fetch Weather ---")
494
- print(f"Location: {lat}, {lon} (Atlanta, GA)")
495
-
496
- try:
497
- weather = await get_weather_for_location(lat, lon)
498
- print(f"✅ Weather fetched successfully")
499
- print(f"Temperature: {weather.get('temperature', {}).get('value')}°F")
500
- print(f"Condition: {weather.get('phrase')}")
501
- print(f"Precipitation: {weather.get('hasPrecipitation')}")
502
-
503
- print(f"\n--- Test 2: Weather Summary ---")
504
- summary = format_weather_summary(weather)
505
- print(f"Summary: {summary}")
506
-
507
- print(f"\n--- Test 3: Outfit Recommendation ---")
508
- temp = weather.get('temperature', {}).get('value', 70)
509
- condition = weather.get('phrase', 'Clear')
510
- outfit = recommend_outfit(temp, condition)
511
- print(f"Outfit: {outfit}")
512
-
513
- print(f"\n--- Test 4: Event Recommendations ---")
514
- recs = weather_to_event_recommendations(weather)
515
- for rec in recs:
516
- print(f"Type: {rec['type']}")
517
- print(f"Reason: {rec['reason']}")
518
- print(f"Suggestions: {', '.join(rec['suggestions'][:2])}")
519
-
520
- print(f"\n--- Test 5: Cache Test ---")
521
- weather2 = await get_weather_for_location(lat, lon, use_cache=True)
522
- print(f"✅ Cache working (should be instant)")
523
-
524
- print(f"\n--- Test 6: Health Check ---")
525
- health = get_weather_agent_health()
526
- print(f"Status: {health['status']}")
527
- print(f"Cache entries: {health['cache']['entries']}")
528
-
529
- except Exception as e:
530
- print(f"❌ Error: {e}")
531
-
532
- asyncio.run(run_tests())
533
-
534
- print("\n" + "=" * 60)
535
- print("✅ Tests complete")