pythonprincess commited on
Commit
50ba4f8
·
verified ·
1 Parent(s): 06be5b4

Delete event_weather.py

Browse files
Files changed (1) hide show
  1. event_weather.py +0 -761
event_weather.py DELETED
@@ -1,761 +0,0 @@
1
- # app/event_weather.py
2
- """
3
- 🌤️ Penny's Event + Weather Matchmaker
4
- Helps residents find the perfect community activity based on real-time weather.
5
- Penny always suggests what's actually enjoyable — not just what exists.
6
-
7
- Production-ready version with structured logging, performance tracking, and robust error handling.
8
- """
9
-
10
- import json
11
- import time
12
- from pathlib import Path
13
- from typing import Dict, Any, List, Optional, Tuple
14
- from datetime import datetime
15
- from enum import Enum
16
-
17
- from app.weather_agent import get_weather_for_location
18
- from app.location_utils import load_city_events
19
- from app.logging_utils import log_interaction, sanitize_for_logging
20
-
21
- # --- LOGGING SETUP (Structured, Azure-compatible) ---
22
- import logging
23
- logger = logging.getLogger(__name__)
24
-
25
-
26
- # --- CONFIGURATION CONSTANTS ---
27
- class EventWeatherConfig:
28
- """Configuration constants for event recommendation system."""
29
- MAX_FALLBACK_EVENTS = 10
30
- MAX_RECOMMENDATIONS = 20
31
- WEATHER_TIMEOUT_SECONDS = 5.0
32
- SLOW_OPERATION_THRESHOLD_MS = 2000
33
-
34
-
35
- # --- PENNY'S WEATHER WISDOM (Personality-Driven Thresholds) ---
36
- class WeatherThresholds:
37
- """
38
- Penny's practical weather rules for event recommendations.
39
- These are based on real resident comfort, not just data.
40
- """
41
- WARM_THRESHOLD = 70 # F° - Great for outdoor events
42
- HOT_THRESHOLD = 85 # F° - Maybe too hot for some activities
43
- COOL_THRESHOLD = 60 # F° - Bring a jacket
44
- COLD_THRESHOLD = 40 # F° - Indoor events preferred
45
-
46
- RAINY_KEYWORDS = ["rain", "shower", "storm", "drizzle", "thunderstorm"]
47
- SNOWY_KEYWORDS = ["snow", "flurries", "blizzard", "ice"]
48
- NICE_KEYWORDS = ["clear", "sunny", "fair", "partly cloudy"]
49
-
50
-
51
- class ErrorType(str, Enum):
52
- """Structured error types for event weather system."""
53
- NOT_FOUND = "event_data_not_found"
54
- PARSE_ERROR = "json_parse_error"
55
- WEATHER_ERROR = "weather_service_error"
56
- UNKNOWN = "unknown_error"
57
-
58
-
59
- class EventWeatherException(Exception):
60
- """Base exception for event weather system."""
61
- def __init__(self, error_type: ErrorType, message: str, original_error: Optional[Exception] = None):
62
- self.error_type = error_type
63
- self.message = message
64
- self.original_error = original_error
65
- super().__init__(message)
66
-
67
-
68
- # --- MAIN RECOMMENDATION FUNCTION ---
69
- async def get_event_recommendations_with_weather(
70
- tenant_id: str,
71
- lat: float,
72
- lon: float,
73
- include_all_events: bool = False,
74
- session_id: Optional[str] = None,
75
- user_id: Optional[str] = None
76
- ) -> Dict[str, Any]:
77
- """
78
- 🌤️ Penny's Event + Weather Intelligence System
79
-
80
- Combines real-time weather with community events to give residents
81
- smart, helpful suggestions about what to do today.
82
-
83
- Args:
84
- tenant_id: City identifier (e.g., 'atlanta_ga', 'seattle_wa')
85
- lat: Latitude for weather lookup
86
- lon: Longitude for weather lookup
87
- include_all_events: If True, returns all events regardless of weather fit
88
- session_id: Optional session identifier for logging
89
- user_id: Optional user identifier for logging
90
-
91
- Returns:
92
- Dict containing:
93
- - weather: Current conditions
94
- - suggestions: Penny's prioritized recommendations
95
- - all_events: Optional full event list
96
- - metadata: Useful context (timestamp, event count, etc.)
97
-
98
- Raises:
99
- EventWeatherException: When critical errors occur
100
-
101
- Example:
102
- >>> recommendations = await get_event_recommendations_with_weather(
103
- ... tenant_id="norfolk_va",
104
- ... lat=36.8508,
105
- ... lon=-76.2859
106
- ... )
107
- >>> print(recommendations["suggestions"][0])
108
- 🌟 **Outdoor Concert**at Town Point Park — Perfect outdoor weather! This is the one.
109
- """
110
- start_time = time.time()
111
-
112
- # Sanitize inputs for logging
113
- safe_tenant_id = sanitize_for_logging(tenant_id)
114
- safe_coords = f"({lat:.4f}, {lon:.4f})"
115
-
116
- logger.info(
117
- f"🌤️ Event weather recommendation request: tenant={safe_tenant_id}, coords={safe_coords}"
118
- )
119
-
120
- try:
121
- # --- STEP 1: Load City Events (Standardized) ---
122
- events, event_load_time = await _load_events_with_timing(tenant_id)
123
-
124
- if not events:
125
- response = _create_no_events_response(tenant_id)
126
- _log_operation(
127
- operation="event_weather_recommendations",
128
- tenant_id=tenant_id,
129
- session_id=session_id,
130
- user_id=user_id,
131
- success=True,
132
- event_count=0,
133
- response_time_ms=_calculate_response_time(start_time),
134
- fallback_used=False,
135
- weather_available=False
136
- )
137
- return response
138
-
139
- logger.info(f"✅ Loaded {len(events)} events for {safe_tenant_id} in {event_load_time:.2f}s")
140
-
141
- # --- STEP 2: Get Live Weather Data ---
142
- weather, weather_available = await _get_weather_with_fallback(lat, lon)
143
-
144
- # --- STEP 3: Generate Recommendations ---
145
- if weather_available:
146
- response = await _generate_weather_optimized_recommendations(
147
- tenant_id=tenant_id,
148
- events=events,
149
- weather=weather,
150
- include_all_events=include_all_events
151
- )
152
- else:
153
- # Graceful degradation: Still show events without weather optimization
154
- response = _create_fallback_response(tenant_id, events)
155
-
156
- # --- STEP 4: Calculate Performance Metrics ---
157
- response_time_ms = _calculate_response_time(start_time)
158
-
159
- # Add performance metadata
160
- response["performance"] = {
161
- "response_time_ms": response_time_ms,
162
- "event_load_time_ms": int(event_load_time * 1000),
163
- "weather_available": weather_available
164
- }
165
-
166
- # Warn if operation was slow
167
- if response_time_ms > EventWeatherConfig.SLOW_OPERATION_THRESHOLD_MS:
168
- logger.warning(
169
- f"⚠️ Slow event weather operation: {response_time_ms}ms for {safe_tenant_id}"
170
- )
171
-
172
- # --- STEP 5: Log Structured Interaction ---
173
- _log_operation(
174
- operation="event_weather_recommendations",
175
- tenant_id=tenant_id,
176
- session_id=session_id,
177
- user_id=user_id,
178
- success=True,
179
- event_count=len(events),
180
- response_time_ms=response_time_ms,
181
- fallback_used=not weather_available,
182
- weather_available=weather_available
183
- )
184
-
185
- logger.info(
186
- f"✅ Returning {len(response.get('suggestions', []))} recommendations "
187
- f"for {safe_tenant_id} in {response_time_ms}ms"
188
- )
189
-
190
- return response
191
-
192
- except EventWeatherException as e:
193
- # Known error with structured handling
194
- response_time_ms = _calculate_response_time(start_time)
195
-
196
- _log_operation(
197
- operation="event_weather_recommendations",
198
- tenant_id=tenant_id,
199
- session_id=session_id,
200
- user_id=user_id,
201
- success=False,
202
- event_count=0,
203
- response_time_ms=response_time_ms,
204
- fallback_used=False,
205
- weather_available=False,
206
- error_type=e.error_type.value,
207
- error_message=str(e)
208
- )
209
-
210
- return _create_error_response(
211
- tenant_id=tenant_id,
212
- error_type=e.error_type.value,
213
- message=e.message
214
- )
215
-
216
- except Exception as e:
217
- # Unexpected error
218
- response_time_ms = _calculate_response_time(start_time)
219
-
220
- logger.error(
221
- f"❌ Unexpected error in event weather recommendations: {str(e)}",
222
- exc_info=True
223
- )
224
-
225
- _log_operation(
226
- operation="event_weather_recommendations",
227
- tenant_id=tenant_id,
228
- session_id=session_id,
229
- user_id=user_id,
230
- success=False,
231
- event_count=0,
232
- response_time_ms=response_time_ms,
233
- fallback_used=False,
234
- weather_available=False,
235
- error_type=ErrorType.UNKNOWN.value,
236
- error_message="Unexpected system error"
237
- )
238
-
239
- return _create_error_response(
240
- tenant_id=tenant_id,
241
- error_type=ErrorType.UNKNOWN.value,
242
- message="Something unexpected happened. Please try again in a moment."
243
- )
244
-
245
-
246
- # --- EVENT LOADING WITH TIMING ---
247
- async def _load_events_with_timing(tenant_id: str) -> Tuple[List[Dict[str, Any]], float]:
248
- """
249
- Load city events with performance timing.
250
-
251
- Args:
252
- tenant_id: City identifier
253
-
254
- Returns:
255
- Tuple of (events list, load time in seconds)
256
-
257
- Raises:
258
- EventWeatherException: When event loading fails
259
- """
260
- load_start = time.time()
261
-
262
- try:
263
- loaded_data = load_city_events(tenant_id)
264
- events = loaded_data.get("events", [])
265
- load_time = time.time() - load_start
266
-
267
- return events, load_time
268
-
269
- except FileNotFoundError as e:
270
- logger.error(f"❌ Event data file not found for tenant: {tenant_id}")
271
- raise EventWeatherException(
272
- error_type=ErrorType.NOT_FOUND,
273
- message=f"I don't have event data for {tenant_id} yet. Let me know if you'd like me to add it!",
274
- original_error=e
275
- )
276
-
277
- except json.JSONDecodeError as e:
278
- logger.error(f"❌ Invalid JSON in event data for {tenant_id}: {e}")
279
- raise EventWeatherException(
280
- error_type=ErrorType.PARSE_ERROR,
281
- message="There's an issue with the event data format. Our team has been notified!",
282
- original_error=e
283
- )
284
-
285
- except Exception as e:
286
- logger.error(f"❌ Unexpected error loading events: {e}", exc_info=True)
287
- raise EventWeatherException(
288
- error_type=ErrorType.UNKNOWN,
289
- message="Something went wrong loading events. Please try again in a moment.",
290
- original_error=e
291
- )
292
-
293
-
294
- # --- WEATHER RETRIEVAL WITH FALLBACK ---
295
- async def _get_weather_with_fallback(
296
- lat: float,
297
- lon: float
298
- ) -> Tuple[Dict[str, Any], bool]:
299
- """
300
- Get weather data with graceful fallback if service is unavailable.
301
-
302
- Args:
303
- lat: Latitude
304
- lon: Longitude
305
-
306
- Returns:
307
- Tuple of (weather data dict, availability boolean)
308
- """
309
- try:
310
- weather = await get_weather_for_location(lat, lon)
311
-
312
- temp = weather.get("temperature", {}).get("value")
313
- phrase = weather.get("phrase", "N/A")
314
-
315
- logger.info(f"✅ Weather retrieved: {phrase} at {temp}°F")
316
-
317
- return weather, True
318
-
319
- except Exception as e:
320
- logger.warning(f"⚠️ Weather service unavailable: {str(e)}")
321
- return {"error": "Weather service unavailable"}, False
322
-
323
-
324
- # --- WEATHER-OPTIMIZED RECOMMENDATIONS ---
325
- async def _generate_weather_optimized_recommendations(
326
- tenant_id: str,
327
- events: List[Dict[str, Any]],
328
- weather: Dict[str, Any],
329
- include_all_events: bool
330
- ) -> Dict[str, Any]:
331
- """
332
- Generate event recommendations optimized for current weather conditions.
333
-
334
- Args:
335
- tenant_id: City identifier
336
- events: List of available events
337
- weather: Weather data dictionary
338
- include_all_events: Whether to include full event list in response
339
-
340
- Returns:
341
- Structured response with weather-optimized suggestions
342
- """
343
- temp = weather.get("temperature", {}).get("value")
344
- phrase = weather.get("phrase", "").lower()
345
-
346
- # Analyze weather conditions
347
- weather_analysis = _analyze_weather_conditions(temp, phrase)
348
-
349
- # Generate Penny's smart suggestions
350
- suggestions = _generate_recommendations(
351
- events=events,
352
- weather_analysis=weather_analysis,
353
- temp=temp,
354
- phrase=phrase
355
- )
356
-
357
- # Build response
358
- response = {
359
- "weather": weather,
360
- "weather_summary": _create_weather_summary(temp, phrase),
361
- "suggestions": suggestions[:EventWeatherConfig.MAX_RECOMMENDATIONS],
362
- "tenant_id": tenant_id,
363
- "event_count": len(events),
364
- "timestamp": datetime.utcnow().isoformat(),
365
- "weather_analysis": weather_analysis
366
- }
367
-
368
- # Optionally include full event list
369
- if include_all_events:
370
- response["all_events"] = events
371
-
372
- return response
373
-
374
-
375
- # --- HELPER FUNCTIONS (Penny's Intelligence Layer) ---
376
-
377
- def _analyze_weather_conditions(temp: Optional[float], phrase: str) -> Dict[str, Any]:
378
- """
379
- 🧠 Penny's weather interpretation logic.
380
- Returns structured analysis of current conditions.
381
-
382
- Args:
383
- temp: Temperature in Fahrenheit
384
- phrase: Weather description phrase
385
-
386
- Returns:
387
- Dictionary with weather analysis including outdoor suitability
388
- """
389
- analysis = {
390
- "is_rainy": any(keyword in phrase for keyword in WeatherThresholds.RAINY_KEYWORDS),
391
- "is_snowy": any(keyword in phrase for keyword in WeatherThresholds.SNOWY_KEYWORDS),
392
- "is_nice": any(keyword in phrase for keyword in WeatherThresholds.NICE_KEYWORDS),
393
- "temp_category": None,
394
- "outdoor_friendly": False,
395
- "indoor_preferred": False
396
- }
397
-
398
- if temp:
399
- if temp >= WeatherThresholds.HOT_THRESHOLD:
400
- analysis["temp_category"] = "hot"
401
- elif temp >= WeatherThresholds.WARM_THRESHOLD:
402
- analysis["temp_category"] = "warm"
403
- elif temp >= WeatherThresholds.COOL_THRESHOLD:
404
- analysis["temp_category"] = "mild"
405
- elif temp >= WeatherThresholds.COLD_THRESHOLD:
406
- analysis["temp_category"] = "cool"
407
- else:
408
- analysis["temp_category"] = "cold"
409
-
410
- # Outdoor-friendly = warm/mild + not rainy/snowy
411
- analysis["outdoor_friendly"] = (
412
- temp >= WeatherThresholds.COOL_THRESHOLD and
413
- not analysis["is_rainy"] and
414
- not analysis["is_snowy"]
415
- )
416
-
417
- # Indoor preferred = cold or rainy or snowy
418
- analysis["indoor_preferred"] = (
419
- temp < WeatherThresholds.COOL_THRESHOLD or
420
- analysis["is_rainy"] or
421
- analysis["is_snowy"]
422
- )
423
-
424
- return analysis
425
-
426
-
427
- def _generate_recommendations(
428
- events: List[Dict[str, Any]],
429
- weather_analysis: Dict[str, Any],
430
- temp: Optional[float],
431
- phrase: str
432
- ) -> List[str]:
433
- """
434
- 🎯 Penny's event recommendation engine.
435
- Prioritizes events based on weather + category fit.
436
- Keeps Penny's warm, helpful voice throughout.
437
-
438
- Args:
439
- events: List of available events
440
- weather_analysis: Weather condition analysis
441
- temp: Current temperature
442
- phrase: Weather description
443
-
444
- Returns:
445
- List of formatted event suggestions
446
- """
447
- suggestions = []
448
-
449
- # Sort events: Best weather fit first
450
- scored_events = []
451
- for event in events:
452
- score = _calculate_event_weather_score(event, weather_analysis)
453
- scored_events.append((score, event))
454
-
455
- scored_events.sort(reverse=True, key=lambda x: x[0])
456
-
457
- # Generate suggestions with Penny's personality
458
- for score, event in scored_events:
459
- event_name = event.get("name", "Unnamed Event")
460
- event_category = event.get("category", "").lower()
461
- event_location = event.get("location", "")
462
-
463
- # Build suggestion with appropriate emoji + messaging
464
- suggestion = _create_suggestion_message(
465
- event_name=event_name,
466
- event_category=event_category,
467
- event_location=event_location,
468
- score=score,
469
- weather_analysis=weather_analysis,
470
- temp=temp,
471
- phrase=phrase
472
- )
473
-
474
- suggestions.append(suggestion)
475
-
476
- return suggestions
477
-
478
-
479
- def _calculate_event_weather_score(
480
- event: Dict[str, Any],
481
- weather_analysis: Dict[str, Any]
482
- ) -> int:
483
- """
484
- 📊 Scores event suitability based on weather (0-100).
485
- Higher = better match for current conditions.
486
-
487
- Args:
488
- event: Event dictionary with category information
489
- weather_analysis: Weather condition analysis
490
-
491
- Returns:
492
- Integer score from 0-100
493
- """
494
- category = event.get("category", "").lower()
495
- score = 50 # Neutral baseline
496
-
497
- # Perfect matches
498
- if "outdoor" in category and weather_analysis["outdoor_friendly"]:
499
- score = 95
500
- elif "indoor" in category and weather_analysis["indoor_preferred"]:
501
- score = 90
502
-
503
- # Good matches
504
- elif "indoor" in category and not weather_analysis["outdoor_friendly"]:
505
- score = 75
506
- elif "outdoor" in category and weather_analysis["temp_category"] in ["warm", "mild"]:
507
- score = 70
508
-
509
- # Acceptable matches
510
- elif "civic" in category or "community" in category:
511
- score = 60 # Usually indoor, weather-neutral
512
-
513
- # Poor matches (but still list them)
514
- elif "outdoor" in category and weather_analysis["indoor_preferred"]:
515
- score = 30
516
-
517
- return score
518
-
519
-
520
- def _create_suggestion_message(
521
- event_name: str,
522
- event_category: str,
523
- event_location: str,
524
- score: int,
525
- weather_analysis: Dict[str, Any],
526
- temp: Optional[float],
527
- phrase: str
528
- ) -> str:
529
- """
530
- 💬 Penny's voice: Generates natural, helpful event suggestions.
531
- Adapts tone based on weather fit score.
532
-
533
- Args:
534
- event_name: Name of the event
535
- event_category: Event category (outdoor, indoor, etc.)
536
- event_location: Event location/venue
537
- score: Weather suitability score (0-100)
538
- weather_analysis: Weather condition analysis
539
- temp: Current temperature
540
- phrase: Weather description
541
-
542
- Returns:
543
- Formatted suggestion string with emoji and helpful context
544
- """
545
- location_text = f" at {event_location}" if event_location else ""
546
-
547
- # PERFECT MATCHES (90-100)
548
- if score >= 90:
549
- if "outdoor" in event_category:
550
- return f"🌟 **{event_name}**{location_text} — Perfect outdoor weather! This is the one."
551
- else:
552
- return f"🏛️ **{event_name}**{location_text} — Ideal indoor activity for today's weather!"
553
-
554
- # GOOD MATCHES (70-89)
555
- elif score >= 70:
556
- if "outdoor" in event_category:
557
- return f"☀️ **{event_name}**{location_text} — Great day for outdoor activities!"
558
- else:
559
- return f"🔵 **{event_name}**{location_text} — Solid indoor option!"
560
-
561
- # DECENT MATCHES (50-69)
562
- elif score >= 50:
563
- if "outdoor" in event_category:
564
- temp_text = f" (It's {int(temp)}°F)" if temp else ""
565
- return f"🌤️ **{event_name}**{location_text} — Weather's okay for outdoor events{temp_text}."
566
- else:
567
- return f"⚪ **{event_name}**{location_text} — Weather-neutral activity."
568
-
569
- # POOR MATCHES (Below 50)
570
- else:
571
- if "outdoor" in event_category and weather_analysis["is_rainy"]:
572
- return f"🌧️ **{event_name}**{location_text} — Outdoor event, but it's rainy. Bring an umbrella or check if it's postponed!"
573
- elif "outdoor" in event_category and weather_analysis.get("temp_category") == "cold":
574
- return f"❄️ **{event_name}**{location_text} — Outdoor event, but bundle up — it's chilly!"
575
- else:
576
- return f"⚪ **{event_name}**{location_text} — Check weather before heading out."
577
-
578
-
579
- def _create_weather_summary(temp: Optional[float], phrase: str) -> str:
580
- """
581
- 🌤️ Penny's plain-English weather summary.
582
-
583
- Args:
584
- temp: Temperature in Fahrenheit
585
- phrase: Weather description phrase
586
-
587
- Returns:
588
- Human-readable weather summary
589
- """
590
- if not temp:
591
- return f"Current conditions: {phrase.title()}"
592
-
593
- temp_desc = ""
594
- if temp >= 85:
595
- temp_desc = "hot"
596
- elif temp >= 70:
597
- temp_desc = "warm"
598
- elif temp >= 60:
599
- temp_desc = "mild"
600
- elif temp >= 40:
601
- temp_desc = "cool"
602
- else:
603
- temp_desc = "cold"
604
-
605
- return f"It's {temp_desc} at {int(temp)}°F — {phrase.lower()}."
606
-
607
-
608
- # --- ERROR RESPONSE HELPERS (Penny stays helpful even in failures) ---
609
-
610
- def _create_no_events_response(tenant_id: str) -> Dict[str, Any]:
611
- """
612
- Returns friendly response when no events are found.
613
-
614
- Args:
615
- tenant_id: City identifier
616
-
617
- Returns:
618
- Structured response with helpful message
619
- """
620
- return {
621
- "weather": {},
622
- "suggestions": [
623
- f"🤔 I don't have any events loaded for {tenant_id} right now. "
624
- "Let me know if you'd like me to check again or add some!"
625
- ],
626
- "tenant_id": tenant_id,
627
- "event_count": 0,
628
- "timestamp": datetime.utcnow().isoformat()
629
- }
630
-
631
-
632
- def _create_error_response(
633
- tenant_id: str,
634
- error_type: str,
635
- message: str
636
- ) -> Dict[str, Any]:
637
- """
638
- Returns structured error with Penny's helpful tone.
639
-
640
- Args:
641
- tenant_id: City identifier
642
- error_type: Structured error type code
643
- message: User-friendly error message
644
-
645
- Returns:
646
- Error response dictionary
647
- """
648
- logger.error(f"Error in event_weather: {error_type} - {message}")
649
- return {
650
- "weather": {},
651
- "suggestions": [f"⚠️ {message}"],
652
- "tenant_id": tenant_id,
653
- "event_count": 0,
654
- "error_type": error_type,
655
- "timestamp": datetime.utcnow().isoformat()
656
- }
657
-
658
-
659
- def _create_fallback_response(
660
- tenant_id: str,
661
- events: List[Dict[str, Any]]
662
- ) -> Dict[str, Any]:
663
- """
664
- Graceful degradation: Shows events even if weather service is down.
665
- Penny stays helpful!
666
-
667
- Args:
668
- tenant_id: City identifier
669
- events: List of available events
670
-
671
- Returns:
672
- Fallback response with events but no weather optimization
673
- """
674
- # Limit to configured maximum
675
- display_events = events[:EventWeatherConfig.MAX_FALLBACK_EVENTS]
676
-
677
- suggestions = [
678
- f"📅 **{event.get('name', 'Event')}** — {event.get('category', 'Community event')}"
679
- for event in display_events
680
- ]
681
-
682
- suggestions.insert(0, "⚠️ Weather service is temporarily unavailable, but here are today's events:")
683
-
684
- return {
685
- "weather": {"error": "Weather service unavailable"},
686
- "suggestions": suggestions,
687
- "tenant_id": tenant_id,
688
- "event_count": len(events),
689
- "timestamp": datetime.utcnow().isoformat(),
690
- "fallback_mode": True
691
- }
692
-
693
-
694
- # --- STRUCTURED LOGGING HELPER ---
695
-
696
- def _log_operation(
697
- operation: str,
698
- tenant_id: str,
699
- success: bool,
700
- event_count: int,
701
- response_time_ms: int,
702
- fallback_used: bool,
703
- weather_available: bool,
704
- session_id: Optional[str] = None,
705
- user_id: Optional[str] = None,
706
- error_type: Optional[str] = None,
707
- error_message: Optional[str] = None
708
- ) -> None:
709
- """
710
- Log event weather operation with structured data.
711
-
712
- Args:
713
- operation: Operation name
714
- tenant_id: City identifier
715
- success: Whether operation succeeded
716
- event_count: Number of events processed
717
- response_time_ms: Total response time in milliseconds
718
- fallback_used: Whether fallback mode was used
719
- weather_available: Whether weather data was available
720
- session_id: Optional session identifier
721
- user_id: Optional user identifier
722
- error_type: Optional error type if failed
723
- error_message: Optional error message if failed
724
- """
725
- log_data = {
726
- "operation": operation,
727
- "tenant_id": sanitize_for_logging(tenant_id),
728
- "success": success,
729
- "event_count": event_count,
730
- "response_time_ms": response_time_ms,
731
- "fallback_used": fallback_used,
732
- "weather_available": weather_available,
733
- "timestamp": datetime.utcnow().isoformat()
734
- }
735
-
736
- if session_id:
737
- log_data["session_id"] = sanitize_for_logging(session_id)
738
-
739
- if user_id:
740
- log_data["user_id"] = sanitize_for_logging(user_id)
741
-
742
- if error_type:
743
- log_data["error_type"] = error_type
744
-
745
- if error_message:
746
- log_data["error_message"] = sanitize_for_logging(error_message)
747
-
748
- log_interaction(log_data)
749
-
750
-
751
- def _calculate_response_time(start_time: float) -> int:
752
- """
753
- Calculate response time in milliseconds.
754
-
755
- Args:
756
- start_time: Operation start time from time.time()
757
-
758
- Returns:
759
- Response time in milliseconds
760
- """
761
- return int((time.time() - start_time) * 1000)