pythonprincess commited on
Commit
5f5b682
·
verified ·
1 Parent(s): 9db7fa1

Delete weather_agent.py

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