pythonprincess commited on
Commit
a9b19b6
·
verified ·
1 Parent(s): f0be0cf

Delete app/tool_agent.py

Browse files
Files changed (1) hide show
  1. app/tool_agent.py +0 -898
app/tool_agent.py DELETED
@@ -1,898 +0,0 @@
1
- # app/tool_agent.py
2
- """
3
- 🛠️ PENNY Tool Agent - Civic Data & Services Handler
4
-
5
- Routes requests to civic data sources (events, resources, transit, etc.)
6
- and integrates with real-time weather information.
7
-
8
- MISSION: Connect residents to local civic services by intelligently
9
- processing their requests and returning relevant, actionable information.
10
-
11
- FEATURES:
12
- - Real-time weather integration with outfit recommendations
13
- - Event discovery with weather-aware suggestions
14
- - Resource lookup (trash, transit, emergency services)
15
- - City-specific data routing
16
- - Graceful fallback for missing data
17
-
18
- ENHANCEMENTS (Phase 1):
19
- - ✅ Structured logging with performance tracking
20
- - ✅ Enhanced error handling with user-friendly messages
21
- - ✅ Type hints for all functions
22
- - ✅ Health check integration
23
- - ✅ Service availability tracking
24
- - ✅ Integration with enhanced modules
25
- - ✅ Penny's friendly voice throughout
26
- - ✅ Context-aware city detection (uses tenant_id from UI dropdown)
27
- """
28
-
29
- import logging
30
- import time
31
- from typing import Optional, Dict, Any
32
-
33
- # --- ENHANCED MODULE IMPORTS ---
34
- from app.logging_utils import log_interaction, sanitize_for_logging
35
-
36
- # --- AGENT IMPORTS (with availability tracking) ---
37
- try:
38
- from app.weather_agent import (
39
- get_weather_for_location,
40
- weather_to_event_recommendations,
41
- recommend_outfit,
42
- format_weather_summary
43
- )
44
- WEATHER_AGENT_AVAILABLE = True
45
- except ImportError as e:
46
- logging.getLogger(__name__).warning(f"Weather agent not available: {e}")
47
- WEATHER_AGENT_AVAILABLE = False
48
-
49
- # --- UTILITY IMPORTS (with availability tracking) ---
50
- try:
51
- from app.location_utils import (
52
- extract_city_name,
53
- load_city_events,
54
- load_city_resources,
55
- get_city_coordinates,
56
- extract_location_detailed,
57
- SupportedCities,
58
- LocationStatus
59
- )
60
- LOCATION_UTILS_AVAILABLE = True
61
- except ImportError as e:
62
- logging.getLogger(__name__).warning(f"Location utils not available: {e}")
63
- LOCATION_UTILS_AVAILABLE = False
64
-
65
- # --- LOGGING SETUP ---
66
- logger = logging.getLogger(__name__)
67
-
68
- # --- TRACKING COUNTERS ---
69
- _tool_request_count = 0
70
- _weather_request_count = 0
71
- _event_request_count = 0
72
- _resource_request_count = 0
73
-
74
-
75
- # ============================================================
76
- # CITY DETECTION FROM CONTEXT (NEW FUNCTION)
77
- # ============================================================
78
-
79
- def get_city_from_context(context: Dict[str, Any], user_input: str) -> tuple[str, str]:
80
- """
81
- 🏙️ Extracts city name and tenant_id from context or message.
82
-
83
- Priority order:
84
- 1. tenant_id from context (from UI dropdown)
85
- 2. location from context
86
- 3. Extract from user message text
87
-
88
- Args:
89
- context: Request context dictionary
90
- user_input: User's message text
91
-
92
- Returns:
93
- Tuple of (city_name, tenant_id)
94
- Example: ("Atlanta", "atlanta_ga")
95
- """
96
- def _normalize_tenant_id(tenant_id: str) -> tuple[str, str]:
97
- """
98
- Normalizes tenant_id to proper format using city registry.
99
- Returns (city_name, proper_tenant_id)
100
- """
101
- tenant_id_lower = tenant_id.lower()
102
-
103
- # If already in proper format (has underscore), verify it exists
104
- if "_" in tenant_id_lower:
105
- city_info = SupportedCities.get_city_by_tenant_id(tenant_id_lower)
106
- if city_info:
107
- return city_info.full_name.split(",")[0], city_info.tenant_id
108
-
109
- # Try to find city by partial match
110
- for city in SupportedCities.get_all_cities():
111
- # Check if tenant_id matches any alias
112
- if tenant_id_lower in [alias.lower() for alias in city.aliases]:
113
- return city.full_name.split(",")[0], city.tenant_id
114
- # Check if tenant_id matches city name part
115
- city_name_part = city.tenant_id.split("_")[0]
116
- if tenant_id_lower == city_name_part:
117
- return city.full_name.split(",")[0], city.tenant_id
118
-
119
- # Fallback: return as-is (will be handled by caller)
120
- return tenant_id.replace("_", " ").title(), tenant_id_lower
121
-
122
- # Priority 1: Check tenant_id in context (from dropdown)
123
- tenant_id = context.get("tenant_id", "").lower()
124
- if tenant_id and tenant_id != "unknown":
125
- city_name, proper_tenant_id = _normalize_tenant_id(tenant_id)
126
- logger.info(f"✅ City from context tenant_id: {city_name} ({proper_tenant_id})")
127
- return city_name, proper_tenant_id
128
-
129
- # Priority 2: Check location in context
130
- location = context.get("location")
131
- if location:
132
- # Try to extract proper tenant_id from location string
133
- location_result = extract_location_detailed(location)
134
- if location_result.status == LocationStatus.FOUND and location_result.tenant_id:
135
- city_name = location_result.city_info.full_name.split(",")[0] if location_result.city_info else location.title()
136
- proper_tenant_id = location_result.tenant_id
137
- logger.info(f"✅ City from context location: {city_name} ({proper_tenant_id})")
138
- return city_name, proper_tenant_id
139
- else:
140
- # Fallback to old method if extraction fails
141
- city_name = location.title()
142
- city_name, proper_tenant_id = _normalize_tenant_id(location.lower().replace(" ", "_"))
143
- logger.info(f"✅ City from context location (fallback): {city_name} ({proper_tenant_id})")
144
- return city_name, proper_tenant_id
145
-
146
- # Priority 3: Fall back to extracting from message
147
- location_result = extract_location_detailed(user_input)
148
- if location_result.status == LocationStatus.FOUND and location_result.tenant_id:
149
- city_name = location_result.city_info.full_name.split(",")[0] if location_result.city_info else extract_city_name(user_input)
150
- proper_tenant_id = location_result.tenant_id
151
- logger.info(f"⚠️ City extracted from message: {city_name} ({proper_tenant_id})")
152
- return city_name, proper_tenant_id
153
- else:
154
- # Fallback to old method
155
- city_name = extract_city_name(user_input)
156
- city_name, proper_tenant_id = _normalize_tenant_id(city_name.lower().replace(" ", "_"))
157
- logger.info(f"⚠️ City extracted from message (fallback): {city_name} ({proper_tenant_id})")
158
- return city_name, proper_tenant_id
159
-
160
-
161
- # ============================================================
162
- # MAIN TOOL REQUEST HANDLER (ENHANCED)
163
- # ============================================================
164
-
165
- async def handle_tool_request(
166
- user_input: str,
167
- role: str = "unknown",
168
- lat: Optional[float] = None,
169
- lon: Optional[float] = None,
170
- context: Optional[Dict[str, Any]] = None
171
- ) -> Dict[str, Any]:
172
- """
173
- 🛠️ Handles tool-based actions for civic services.
174
-
175
- Routes user requests to appropriate civic data sources and real-time
176
- services, including weather, events, transit, trash, and emergency info.
177
-
178
- Args:
179
- user_input: User's request text
180
- role: User's role (resident, official, etc.)
181
- lat: Latitude coordinate (optional)
182
- lon: Longitude coordinate (optional)
183
- context: Request context with tenant_id, location, etc. (optional)
184
-
185
- Returns:
186
- Dictionary containing:
187
- - tool: str (which tool was used)
188
- - city: str (detected city name)
189
- - response: str or dict (user-facing response)
190
- - data: dict (optional, raw data)
191
- - tenant_id: str (optional, standardized city identifier)
192
-
193
- Example:
194
- result = await handle_tool_request(
195
- user_input="What's the weather in Atlanta?",
196
- role="resident",
197
- lat=33.7490,
198
- lon=-84.3880,
199
- context={"tenant_id": "atlanta"}
200
- )
201
- """
202
- global _tool_request_count
203
- _tool_request_count += 1
204
-
205
- start_time = time.time()
206
-
207
- # Initialize context if not provided
208
- if context is None:
209
- context = {}
210
-
211
- # Sanitize input for logging (PII protection)
212
- safe_input = sanitize_for_logging(user_input)
213
- logger.info(f"🛠️ Tool request #{_tool_request_count}: '{safe_input[:50]}...'")
214
- logger.info(f"📍 Context: {context}")
215
-
216
- try:
217
- # Check if location utilities are available
218
- if not LOCATION_UTILS_AVAILABLE:
219
- logger.error("Location utilities not available")
220
- return {
221
- "tool": "error",
222
- "response": (
223
- "I'm having trouble accessing city data right now. "
224
- "Try again in a moment! 💛"
225
- ),
226
- "error": "Location utilities not loaded"
227
- }
228
-
229
- lowered = user_input.lower()
230
-
231
- # 🔥 NEW: Get city from context first, then fall back to message
232
- city_name, tenant_id = get_city_from_context(context, user_input)
233
-
234
- logger.info(f"🏙️ Detected city: {city_name} (tenant_id: {tenant_id})")
235
-
236
- # Route to appropriate handler
237
- result = None
238
-
239
- # Weather queries
240
- if any(keyword in lowered for keyword in ["weather", "forecast", "temperature", "rain", "sunny"]):
241
- result = await _handle_weather_query(
242
- user_input=user_input,
243
- city_name=city_name,
244
- tenant_id=tenant_id,
245
- lat=lat,
246
- lon=lon
247
- )
248
-
249
- # Event queries
250
- elif any(keyword in lowered for keyword in ["events", "meetings", "city hall", "happening", "activities"]):
251
- result = await _handle_events_query(
252
- user_input=user_input,
253
- city_name=city_name,
254
- tenant_id=tenant_id,
255
- lat=lat,
256
- lon=lon
257
- )
258
-
259
- # Resource queries (trash, transit, emergency, food banks, libraries, shelters, etc.)
260
- elif any(keyword in lowered for keyword in [
261
- "trash", "recycling", "garbage", "waste",
262
- "bus", "train", "transit", "transportation", "schedule",
263
- "alert", "warning", "non emergency", "emergency",
264
- "food bank", "foodbank", "food pantry", "pantry",
265
- "library", "libraries", "shelter", "shelters",
266
- "warming center", "cooling center", "warming", "cooling",
267
- "help center", "help center", "assistance", "resource",
268
- "clinic", "hospital", "pharmacy", "health",
269
- "housing", "utility", "water", "electric", "gas"
270
- ]):
271
- result = await _handle_resource_query(
272
- user_input=user_input,
273
- city_name=city_name,
274
- tenant_id=tenant_id,
275
- lowered=lowered
276
- )
277
-
278
- # Unknown/fallback
279
- else:
280
- result = _handle_unknown_query(city_name)
281
-
282
- # Add metadata and log interaction
283
- response_time = (time.time() - start_time) * 1000
284
- result["response_time_ms"] = round(response_time, 2)
285
- result["role"] = role
286
-
287
- log_interaction(
288
- tenant_id=tenant_id,
289
- interaction_type="tool_request",
290
- intent=result.get("tool", "unknown"),
291
- response_time_ms=response_time,
292
- success=result.get("error") is None,
293
- metadata={
294
- "city": city_name,
295
- "tool": result.get("tool"),
296
- "role": role,
297
- "has_location": lat is not None and lon is not None
298
- }
299
- )
300
-
301
- logger.info(
302
- f"✅ Tool request complete: {result.get('tool')} "
303
- f"({response_time:.0f}ms)"
304
- )
305
-
306
- return result
307
-
308
- except Exception as e:
309
- response_time = (time.time() - start_time) * 1000
310
- logger.error(f"❌ Tool agent error: {e}", exc_info=True)
311
-
312
- log_interaction(
313
- tenant_id="unknown",
314
- interaction_type="tool_error",
315
- intent="error",
316
- response_time_ms=response_time,
317
- success=False,
318
- metadata={
319
- "error": str(e),
320
- "error_type": type(e).__name__
321
- }
322
- )
323
-
324
- return {
325
- "tool": "error",
326
- "response": (
327
- "I ran into trouble processing that request. "
328
- "Could you try rephrasing? 💛"
329
- ),
330
- "error": str(e),
331
- "response_time_ms": round(response_time, 2)
332
- }
333
-
334
-
335
- # ============================================================
336
- # WEATHER QUERY HANDLER (ENHANCED)
337
- # ============================================================
338
-
339
- async def _handle_weather_query(
340
- user_input: str,
341
- city_name: str,
342
- tenant_id: str,
343
- lat: Optional[float],
344
- lon: Optional[float]
345
- ) -> Dict[str, Any]:
346
- """
347
- 🌤️ Handles weather-related queries with outfit recommendations.
348
- """
349
- global _weather_request_count
350
- _weather_request_count += 1
351
-
352
- logger.info(f"🌤️ Weather query #{_weather_request_count} for {city_name}")
353
-
354
- # Check weather agent availability
355
- if not WEATHER_AGENT_AVAILABLE:
356
- logger.warning("Weather agent not available")
357
- return {
358
- "tool": "weather",
359
- "city": city_name,
360
- "response": "Weather service isn't available right now. Try again soon! 🌤️"
361
- }
362
-
363
- # Get coordinates if not provided
364
- if lat is None or lon is None:
365
- coords = get_city_coordinates(tenant_id)
366
- if coords:
367
- lat, lon = coords["lat"], coords["lon"]
368
- logger.info(f"Using city coordinates: {lat}, {lon}")
369
-
370
- if lat is None or lon is None:
371
- return {
372
- "tool": "weather",
373
- "city": city_name,
374
- "response": (
375
- f"To get weather for {city_name}, I need location coordinates. "
376
- f"Can you share your location? 📍"
377
- )
378
- }
379
-
380
- try:
381
- # Fetch weather data
382
- weather = await get_weather_for_location(lat, lon)
383
-
384
- # Get weather-based event recommendations
385
- recommendations = weather_to_event_recommendations(weather)
386
-
387
- # Get outfit recommendation
388
- temp = weather.get("temperature", {}).get("value", 70)
389
- phrase = weather.get("phrase", "Clear")
390
- outfit = recommend_outfit(temp, phrase)
391
-
392
- # Format weather summary
393
- weather_summary = format_weather_summary(weather)
394
-
395
- # Build user-friendly response
396
- response_text = (
397
- f"🌤️ **Weather for {city_name}:**\n"
398
- f"{weather_summary}\n\n"
399
- f"👕 **What to wear:** {outfit}"
400
- )
401
-
402
- # Add event recommendations if available
403
- if recommendations:
404
- rec = recommendations[0] # Get top recommendation
405
- response_text += f"\n\n📅 **Activity suggestion:** {rec['reason']}"
406
-
407
- return {
408
- "tool": "weather",
409
- "city": city_name,
410
- "tenant_id": tenant_id,
411
- "response": response_text,
412
- "data": {
413
- "weather": weather,
414
- "recommendations": recommendations,
415
- "outfit": outfit
416
- }
417
- }
418
-
419
- except Exception as e:
420
- logger.error(f"Weather query error: {e}", exc_info=True)
421
- return {
422
- "tool": "weather",
423
- "city": city_name,
424
- "response": (
425
- f"I couldn't get the weather for {city_name} right now. "
426
- f"Try again in a moment! 🌤️"
427
- ),
428
- "error": str(e)
429
- }
430
-
431
-
432
- # ============================================================
433
- # EVENTS QUERY HANDLER (ENHANCED)
434
- # ============================================================
435
-
436
- async def _handle_events_query(
437
- user_input: str,
438
- city_name: str,
439
- tenant_id: str,
440
- lat: Optional[float],
441
- lon: Optional[float]
442
- ) -> Dict[str, Any]:
443
- """
444
- 📅 Handles event discovery queries.
445
- """
446
- global _event_request_count
447
- _event_request_count += 1
448
-
449
- logger.info(f"📅 Event query #{_event_request_count} for {city_name}")
450
-
451
- try:
452
- # Load structured event data
453
- event_data = load_city_events(tenant_id)
454
- events = event_data.get("events", [])
455
- num_events = len(events)
456
-
457
- if num_events == 0:
458
- return {
459
- "tool": "civic_events",
460
- "city": city_name,
461
- "tenant_id": tenant_id,
462
- "response": (
463
- f"I don't have any upcoming events for {city_name} right now. "
464
- f"Check back soon! 📅"
465
- )
466
- }
467
-
468
- # Get top event
469
- top_event = events[0]
470
- top_event_name = top_event.get("name", "Upcoming event")
471
-
472
- # Build response
473
- if num_events == 1:
474
- response_text = (
475
- f"📅 **Upcoming event in {city_name}:**\n"
476
- f"• {top_event_name}\n\n"
477
- f"Check the full details in the attached data!"
478
- )
479
- else:
480
- response_text = (
481
- f"📅 **Found {num_events} upcoming events in {city_name}!**\n"
482
- f"Top event: {top_event_name}\n\n"
483
- f"Check the full list in the attached data!"
484
- )
485
-
486
- return {
487
- "tool": "civic_events",
488
- "city": city_name,
489
- "tenant_id": tenant_id,
490
- "response": response_text,
491
- "data": event_data
492
- }
493
-
494
- except FileNotFoundError:
495
- logger.warning(f"Event data file not found for {tenant_id}")
496
- return {
497
- "tool": "civic_events",
498
- "city": city_name,
499
- "response": (
500
- f"Event data for {city_name} isn't available yet. "
501
- f"I'm still learning about events in your area! 📅"
502
- ),
503
- "error": "Event data file not found"
504
- }
505
-
506
- except Exception as e:
507
- logger.error(f"Events query error: {e}", exc_info=True)
508
- return {
509
- "tool": "civic_events",
510
- "city": city_name,
511
- "response": (
512
- f"I had trouble loading events for {city_name}. "
513
- f"Try again soon! 📅"
514
- ),
515
- "error": str(e)
516
- }
517
-
518
-
519
- # ============================================================
520
- # RESOURCE QUERY HANDLER (ENHANCED)
521
- # ============================================================
522
-
523
- async def _handle_resource_query(
524
- user_input: str,
525
- city_name: str,
526
- tenant_id: str,
527
- lowered: str
528
- ) -> Dict[str, Any]:
529
- """
530
- ♻️ Handles resource queries (trash, transit, emergency).
531
- """
532
- global _resource_request_count
533
- _resource_request_count += 1
534
-
535
- logger.info(f"♻️ Resource query #{_resource_request_count} for {city_name}")
536
-
537
- # Map keywords to resource types
538
- resource_query_map = {
539
- # Trash & Recycling
540
- "trash": "trash_and_recycling",
541
- "recycling": "trash_and_recycling",
542
- "garbage": "trash_and_recycling",
543
- "waste": "trash_and_recycling",
544
- # Transit
545
- "bus": "transit",
546
- "train": "transit",
547
- "transit": "transit",
548
- "transportation": "transit",
549
- "schedule": "transit",
550
- # Emergency
551
- "alert": "emergency",
552
- "warning": "emergency",
553
- "non emergency": "emergency",
554
- "emergency": "emergency",
555
- # Food Assistance
556
- "food bank": "food_assistance",
557
- "foodbank": "food_assistance",
558
- "food pantry": "food_assistance",
559
- "pantry": "food_assistance",
560
- # Libraries
561
- "library": "libraries",
562
- "libraries": "libraries",
563
- # Shelters
564
- "shelter": "shelters",
565
- "shelters": "shelters",
566
- "warming center": "shelters",
567
- "cooling center": "shelters",
568
- "warming": "shelters",
569
- "cooling": "shelters",
570
- # General Resources
571
- "help center": "community_resources",
572
- "help center": "community_resources",
573
- "assistance": "community_resources",
574
- "resource": "community_resources",
575
- "resources": "community_resources",
576
- # Health Services
577
- "clinic": "health_services",
578
- "hospital": "health_services",
579
- "pharmacy": "health_services",
580
- "health": "health_services",
581
- # Housing & Utilities
582
- "housing": "housing_utilities",
583
- "utility": "housing_utilities",
584
- "utilities": "housing_utilities",
585
- "water": "housing_utilities",
586
- "electric": "housing_utilities",
587
- "gas": "housing_utilities"
588
- }
589
-
590
- # Find matching resource type (check longer phrases first for better specificity)
591
- # Sort keys by length (longest first) to match "food bank" before "food" (if it existed)
592
- sorted_keys = sorted(resource_query_map.keys(), key=len, reverse=True)
593
- resource_key = next(
594
- (resource_query_map[key] for key in sorted_keys if key in lowered),
595
- None
596
- )
597
-
598
- if not resource_key:
599
- return {
600
- "tool": "unknown",
601
- "city": city_name,
602
- "response": (
603
- "I'm not sure which resource you're asking about. "
604
- "Try asking about trash, transit, food banks, libraries, shelters, "
605
- "health services, or emergency services! 💬"
606
- )
607
- }
608
-
609
- try:
610
- # Load structured resource data
611
- resource_data = load_city_resources(tenant_id)
612
- service_info = resource_data["services"].get(resource_key, {})
613
-
614
- if not service_info:
615
- return {
616
- "tool": resource_key,
617
- "city": city_name,
618
- "response": (
619
- f"I don't have {resource_key.replace('_', ' ')} information "
620
- f"for {city_name} yet. Check the city's official website! 🏛️"
621
- )
622
- }
623
-
624
- # Build resource-specific response
625
- if resource_key == "trash_and_recycling":
626
- pickup_days = service_info.get('pickup_days', 'Varies by address')
627
- response_text = (
628
- f"♻️ **Trash & Recycling for {city_name}:**\n"
629
- f"Pickup days: {pickup_days}\n\n"
630
- f"Check the official link for your specific schedule!"
631
- )
632
-
633
- elif resource_key == "transit":
634
- provider = service_info.get('provider', 'The local transit authority')
635
- response_text = (
636
- f"🚌 **Transit for {city_name}:**\n"
637
- f"Provider: {provider}\n\n"
638
- f"Use the provided links to find routes and schedules!"
639
- )
640
-
641
- elif resource_key == "emergency":
642
- non_emergency = service_info.get('non_emergency_phone', 'N/A')
643
- response_text = (
644
- f"🚨 **Emergency Info for {city_name}:**\n"
645
- f"Non-emergency: {non_emergency}\n\n"
646
- f"**For life-threatening emergencies, always call 911.**"
647
- )
648
-
649
- elif resource_key == "food_assistance":
650
- locations = service_info.get('locations', [])
651
- if locations:
652
- response_text = (
653
- f"🍽️ **Food Assistance in {city_name}:**\n"
654
- f"Found {len(locations)} food bank(s) or pantry(ies).\n\n"
655
- f"Check the attached data for locations, hours, and contact information!"
656
- )
657
- else:
658
- response_text = (
659
- f"🍽️ **Food Assistance in {city_name}:**\n"
660
- f"Food assistance information is available. Check the attached data for details!"
661
- )
662
-
663
- elif resource_key == "libraries":
664
- locations = service_info.get('locations', [])
665
- if locations:
666
- response_text = (
667
- f"📚 **Libraries in {city_name}:**\n"
668
- f"Found {len(locations)} library(ies).\n\n"
669
- f"Check the attached data for locations, hours, and services!"
670
- )
671
- else:
672
- response_text = (
673
- f"📚 **Libraries in {city_name}:**\n"
674
- f"Library information is available. Check the attached data for details!"
675
- )
676
-
677
- elif resource_key == "shelters":
678
- locations = service_info.get('locations', [])
679
- if locations:
680
- response_text = (
681
- f"🏠 **Shelters in {city_name}:**\n"
682
- f"Found {len(locations)} shelter(s).\n\n"
683
- f"Check the attached data for locations, availability, and contact information!"
684
- )
685
- else:
686
- response_text = (
687
- f"🏠 **Shelters in {city_name}:**\n"
688
- f"Shelter information is available. Check the attached data for details!"
689
- )
690
-
691
- elif resource_key == "community_resources":
692
- resources = service_info.get('resources', [])
693
- if resources:
694
- response_text = (
695
- f"🏛️ **Community Resources in {city_name}:**\n"
696
- f"Found {len(resources)} resource(s).\n\n"
697
- f"Check the attached data for available services and contact information!"
698
- )
699
- else:
700
- response_text = (
701
- f"🏛️ **Community Resources in {city_name}:**\n"
702
- f"Community resource information is available. Check the attached data for details!"
703
- )
704
-
705
- elif resource_key == "health_services":
706
- locations = service_info.get('locations', [])
707
- if locations:
708
- response_text = (
709
- f"🏥 **Health Services in {city_name}:**\n"
710
- f"Found {len(locations)} health service location(s).\n\n"
711
- f"Check the attached data for clinics, hospitals, and pharmacies!"
712
- )
713
- else:
714
- response_text = (
715
- f"🏥 **Health Services in {city_name}:**\n"
716
- f"Health service information is available. Check the attached data for details!"
717
- )
718
-
719
- elif resource_key == "housing_utilities":
720
- response_text = (
721
- f"🏘️ **Housing & Utilities in {city_name}:**\n"
722
- f"Housing and utility information is available.\n\n"
723
- f"Check the attached data for housing assistance and utility services!"
724
- )
725
-
726
- else:
727
- response_text = f"Information found for {resource_key.replace('_', ' ')}, but details aren't available yet."
728
-
729
- return {
730
- "tool": resource_key,
731
- "city": city_name,
732
- "tenant_id": tenant_id,
733
- "response": response_text,
734
- "data": service_info
735
- }
736
-
737
- except FileNotFoundError:
738
- logger.warning(f"Resource data file not found for {tenant_id}")
739
- return {
740
- "tool": "resource_loader",
741
- "city": city_name,
742
- "response": (
743
- f"Resource data for {city_name} isn't available yet. "
744
- f"Check back soon! 🏛️"
745
- ),
746
- "error": "Resource data file not found"
747
- }
748
-
749
- except Exception as e:
750
- logger.error(f"Resource query error: {e}", exc_info=True)
751
- return {
752
- "tool": "resource_loader",
753
- "city": city_name,
754
- "response": (
755
- f"I had trouble loading resource data for {city_name}. "
756
- f"Try again soon! 🏛️"
757
- ),
758
- "error": str(e)
759
- }
760
-
761
-
762
- # ============================================================
763
- # UNKNOWN QUERY HANDLER
764
- # ============================================================
765
-
766
- def _handle_unknown_query(city_name: str) -> Dict[str, Any]:
767
- """
768
- ❓ Fallback for queries that don't match any tool.
769
- """
770
- logger.info(f"❓ Unknown query for {city_name}")
771
-
772
- return {
773
- "tool": "unknown",
774
- "city": city_name,
775
- "response": (
776
- "I'm not sure which civic service you're asking about. "
777
- "Try asking about weather, events, trash, or transit! 💬"
778
- )
779
- }
780
-
781
-
782
- # ============================================================
783
- # HEALTH CHECK & DIAGNOSTICS
784
- # ============================================================
785
-
786
- def get_tool_agent_health() -> Dict[str, Any]:
787
- """
788
- 📊 Returns tool agent health status.
789
-
790
- Used by the main application health check endpoint.
791
- """
792
- return {
793
- "status": "operational",
794
- "service_availability": {
795
- "weather_agent": WEATHER_AGENT_AVAILABLE,
796
- "location_utils": LOCATION_UTILS_AVAILABLE
797
- },
798
- "statistics": {
799
- "total_requests": _tool_request_count,
800
- "weather_requests": _weather_request_count,
801
- "event_requests": _event_request_count,
802
- "resource_requests": _resource_request_count
803
- },
804
- "supported_queries": [
805
- "weather",
806
- "events",
807
- "trash_and_recycling",
808
- "transit",
809
- "emergency"
810
- ]
811
- }
812
-
813
-
814
- # ============================================================
815
- # TESTING
816
- # ============================================================
817
-
818
- if __name__ == "__main__":
819
- """🧪 Test tool agent functionality"""
820
- import asyncio
821
-
822
- print("=" * 60)
823
- print("🧪 Testing Tool Agent")
824
- print("=" * 60)
825
-
826
- # Display service availability
827
- print("\n📊 Service Availability:")
828
- print(f" Weather Agent: {'✅' if WEATHER_AGENT_AVAILABLE else '❌'}")
829
- print(f" Location Utils: {'✅' if LOCATION_UTILS_AVAILABLE else '❌'}")
830
-
831
- print("\n" + "=" * 60)
832
-
833
- test_queries = [
834
- {
835
- "name": "Weather query with context",
836
- "input": "What's the weather?",
837
- "lat": 33.7490,
838
- "lon": -84.3880,
839
- "context": {"tenant_id": "atlanta"}
840
- },
841
- {
842
- "name": "Events query with context",
843
- "input": "show me local events",
844
- "lat": None,
845
- "lon": None,
846
- "context": {"tenant_id": "atlanta"}
847
- },
848
- {
849
- "name": "Trash query with context",
850
- "input": "When is trash pickup?",
851
- "lat": None,
852
- "lon": None,
853
- "context": {"tenant_id": "atlanta"}
854
- }
855
- ]
856
-
857
- async def run_tests():
858
- for i, query in enumerate(test_queries, 1):
859
- print(f"\n--- Test {i}: {query['name']} ---")
860
- print(f"Query: {query['input']}")
861
- print(f"Context: {query.get('context', {})}")
862
-
863
- try:
864
- result = await handle_tool_request(
865
- user_input=query["input"],
866
- role="test_user",
867
- lat=query.get("lat"),
868
- lon=query.get("lon"),
869
- context=query.get("context", {})
870
- )
871
-
872
- print(f"Tool: {result.get('tool')}")
873
- print(f"City: {result.get('city')}")
874
- print(f"Tenant ID: {result.get('tenant_id')}")
875
-
876
- response = result.get('response')
877
- if isinstance(response, str):
878
- print(f"Response: {response[:150]}...")
879
- else:
880
- print(f"Response: [Dict with {len(response)} keys]")
881
-
882
- if result.get('response_time_ms'):
883
- print(f"Response time: {result['response_time_ms']:.0f}ms")
884
-
885
- except Exception as e:
886
- print(f"❌ Error: {e}")
887
-
888
- asyncio.run(run_tests())
889
-
890
- print("\n" + "=" * 60)
891
- print("📊 Final Statistics:")
892
- health = get_tool_agent_health()
893
- for key, value in health["statistics"].items():
894
- print(f" {key}: {value}")
895
-
896
- print("\n" + "=" * 60)
897
- print("✅ Tests complete")
898
- print("=" * 60)