pythonprincess commited on
Commit
d423254
Β·
verified Β·
1 Parent(s): a018fa1

Upload tool_agent.py

Browse files
Files changed (1) hide show
  1. app/tool_agent.py +726 -0
app/tool_agent.py ADDED
@@ -0,0 +1,726 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ )
57
+ LOCATION_UTILS_AVAILABLE = True
58
+ except ImportError as e:
59
+ logging.getLogger(__name__).warning(f"Location utils not available: {e}")
60
+ LOCATION_UTILS_AVAILABLE = False
61
+
62
+ # --- LOGGING SETUP ---
63
+ logger = logging.getLogger(__name__)
64
+
65
+ # --- TRACKING COUNTERS ---
66
+ _tool_request_count = 0
67
+ _weather_request_count = 0
68
+ _event_request_count = 0
69
+ _resource_request_count = 0
70
+
71
+
72
+ # ============================================================
73
+ # CITY DETECTION FROM CONTEXT (NEW FUNCTION)
74
+ # ============================================================
75
+
76
+ def get_city_from_context(context: Dict[str, Any], user_input: str) -> tuple[str, str]:
77
+ """
78
+ πŸ™οΈ Extracts city name and tenant_id from context or message.
79
+
80
+ Priority order:
81
+ 1. tenant_id from context (from UI dropdown)
82
+ 2. location from context
83
+ 3. Extract from user message text
84
+
85
+ Args:
86
+ context: Request context dictionary
87
+ user_input: User's message text
88
+
89
+ Returns:
90
+ Tuple of (city_name, tenant_id)
91
+ Example: ("Atlanta", "atlanta_ga")
92
+ """
93
+ # Priority 1: Check tenant_id in context (from dropdown)
94
+ tenant_id = context.get("tenant_id", "").lower()
95
+ if tenant_id and tenant_id != "unknown":
96
+ # Convert tenant_id back to city name
97
+ # "atlanta" -> "Atlanta"
98
+ city_name = tenant_id.replace("_", " ").title()
99
+ # Standardize tenant_id format
100
+ standardized_tenant_id = f"{tenant_id}_ga" if "_" not in tenant_id else tenant_id
101
+ logger.info(f"βœ… City from context tenant_id: {city_name} ({standardized_tenant_id})")
102
+ return city_name, standardized_tenant_id
103
+
104
+ # Priority 2: Check location in context
105
+ location = context.get("location")
106
+ if location:
107
+ city_name = location.title()
108
+ tenant_id = f"{location.lower().replace(' ', '_')}_ga"
109
+ logger.info(f"βœ… City from context location: {city_name} ({tenant_id})")
110
+ return city_name, tenant_id
111
+
112
+ # Priority 3: Fall back to extracting from message
113
+ city_name = extract_city_name(user_input)
114
+ tenant_id = f"{city_name.lower().replace(' ', '_')}_ga"
115
+ logger.info(f"⚠️ City extracted from message: {city_name} ({tenant_id})")
116
+ return city_name, tenant_id
117
+
118
+
119
+ # ============================================================
120
+ # MAIN TOOL REQUEST HANDLER (ENHANCED)
121
+ # ============================================================
122
+
123
+ async def handle_tool_request(
124
+ user_input: str,
125
+ role: str = "unknown",
126
+ lat: Optional[float] = None,
127
+ lon: Optional[float] = None,
128
+ context: Optional[Dict[str, Any]] = None
129
+ ) -> Dict[str, Any]:
130
+ """
131
+ πŸ› οΈ Handles tool-based actions for civic services.
132
+
133
+ Routes user requests to appropriate civic data sources and real-time
134
+ services, including weather, events, transit, trash, and emergency info.
135
+
136
+ Args:
137
+ user_input: User's request text
138
+ role: User's role (resident, official, etc.)
139
+ lat: Latitude coordinate (optional)
140
+ lon: Longitude coordinate (optional)
141
+ context: Request context with tenant_id, location, etc. (optional)
142
+
143
+ Returns:
144
+ Dictionary containing:
145
+ - tool: str (which tool was used)
146
+ - city: str (detected city name)
147
+ - response: str or dict (user-facing response)
148
+ - data: dict (optional, raw data)
149
+ - tenant_id: str (optional, standardized city identifier)
150
+
151
+ Example:
152
+ result = await handle_tool_request(
153
+ user_input="What's the weather in Atlanta?",
154
+ role="resident",
155
+ lat=33.7490,
156
+ lon=-84.3880,
157
+ context={"tenant_id": "atlanta"}
158
+ )
159
+ """
160
+ global _tool_request_count
161
+ _tool_request_count += 1
162
+
163
+ start_time = time.time()
164
+
165
+ # Initialize context if not provided
166
+ if context is None:
167
+ context = {}
168
+
169
+ # Sanitize input for logging (PII protection)
170
+ safe_input = sanitize_for_logging(user_input)
171
+ logger.info(f"πŸ› οΈ Tool request #{_tool_request_count}: '{safe_input[:50]}...'")
172
+ logger.info(f"πŸ“ Context: {context}")
173
+
174
+ try:
175
+ # Check if location utilities are available
176
+ if not LOCATION_UTILS_AVAILABLE:
177
+ logger.error("Location utilities not available")
178
+ return {
179
+ "tool": "error",
180
+ "response": (
181
+ "I'm having trouble accessing city data right now. "
182
+ "Try again in a moment! πŸ’›"
183
+ ),
184
+ "error": "Location utilities not loaded"
185
+ }
186
+
187
+ lowered = user_input.lower()
188
+
189
+ # πŸ”₯ NEW: Get city from context first, then fall back to message
190
+ city_name, tenant_id = get_city_from_context(context, user_input)
191
+
192
+ logger.info(f"πŸ™οΈ Detected city: {city_name} (tenant_id: {tenant_id})")
193
+
194
+ # Route to appropriate handler
195
+ result = None
196
+
197
+ # Weather queries
198
+ if any(keyword in lowered for keyword in ["weather", "forecast", "temperature", "rain", "sunny"]):
199
+ result = await _handle_weather_query(
200
+ user_input=user_input,
201
+ city_name=city_name,
202
+ tenant_id=tenant_id,
203
+ lat=lat,
204
+ lon=lon
205
+ )
206
+
207
+ # Event queries
208
+ elif any(keyword in lowered for keyword in ["events", "meetings", "city hall", "happening", "activities"]):
209
+ result = await _handle_events_query(
210
+ user_input=user_input,
211
+ city_name=city_name,
212
+ tenant_id=tenant_id,
213
+ lat=lat,
214
+ lon=lon
215
+ )
216
+
217
+ # Resource queries (trash, transit, emergency)
218
+ elif any(keyword in lowered for keyword in ["trash", "recycling", "garbage", "bus", "train", "schedule", "alert", "warning", "non emergency"]):
219
+ result = await _handle_resource_query(
220
+ user_input=user_input,
221
+ city_name=city_name,
222
+ tenant_id=tenant_id,
223
+ lowered=lowered
224
+ )
225
+
226
+ # Unknown/fallback
227
+ else:
228
+ result = _handle_unknown_query(city_name)
229
+
230
+ # Add metadata and log interaction
231
+ response_time = (time.time() - start_time) * 1000
232
+ result["response_time_ms"] = round(response_time, 2)
233
+ result["role"] = role
234
+
235
+ log_interaction(
236
+ tenant_id=tenant_id,
237
+ interaction_type="tool_request",
238
+ intent=result.get("tool", "unknown"),
239
+ response_time_ms=response_time,
240
+ success=result.get("error") is None,
241
+ metadata={
242
+ "city": city_name,
243
+ "tool": result.get("tool"),
244
+ "role": role,
245
+ "has_location": lat is not None and lon is not None
246
+ }
247
+ )
248
+
249
+ logger.info(
250
+ f"βœ… Tool request complete: {result.get('tool')} "
251
+ f"({response_time:.0f}ms)"
252
+ )
253
+
254
+ return result
255
+
256
+ except Exception as e:
257
+ response_time = (time.time() - start_time) * 1000
258
+ logger.error(f"❌ Tool agent error: {e}", exc_info=True)
259
+
260
+ log_interaction(
261
+ tenant_id="unknown",
262
+ interaction_type="tool_error",
263
+ intent="error",
264
+ response_time_ms=response_time,
265
+ success=False,
266
+ metadata={
267
+ "error": str(e),
268
+ "error_type": type(e).__name__
269
+ }
270
+ )
271
+
272
+ return {
273
+ "tool": "error",
274
+ "response": (
275
+ "I ran into trouble processing that request. "
276
+ "Could you try rephrasing? πŸ’›"
277
+ ),
278
+ "error": str(e),
279
+ "response_time_ms": round(response_time, 2)
280
+ }
281
+
282
+
283
+ # ============================================================
284
+ # WEATHER QUERY HANDLER (ENHANCED)
285
+ # ============================================================
286
+
287
+ async def _handle_weather_query(
288
+ user_input: str,
289
+ city_name: str,
290
+ tenant_id: str,
291
+ lat: Optional[float],
292
+ lon: Optional[float]
293
+ ) -> Dict[str, Any]:
294
+ """
295
+ 🌀️ Handles weather-related queries with outfit recommendations.
296
+ """
297
+ global _weather_request_count
298
+ _weather_request_count += 1
299
+
300
+ logger.info(f"🌀️ Weather query #{_weather_request_count} for {city_name}")
301
+
302
+ # Check weather agent availability
303
+ if not WEATHER_AGENT_AVAILABLE:
304
+ logger.warning("Weather agent not available")
305
+ return {
306
+ "tool": "weather",
307
+ "city": city_name,
308
+ "response": "Weather service isn't available right now. Try again soon! 🌀️"
309
+ }
310
+
311
+ # Get coordinates if not provided
312
+ if lat is None or lon is None:
313
+ coords = get_city_coordinates(tenant_id)
314
+ if coords:
315
+ lat, lon = coords["lat"], coords["lon"]
316
+ logger.info(f"Using city coordinates: {lat}, {lon}")
317
+
318
+ if lat is None or lon is None:
319
+ return {
320
+ "tool": "weather",
321
+ "city": city_name,
322
+ "response": (
323
+ f"To get weather for {city_name}, I need location coordinates. "
324
+ f"Can you share your location? πŸ“"
325
+ )
326
+ }
327
+
328
+ try:
329
+ # Fetch weather data
330
+ weather = await get_weather_for_location(lat, lon)
331
+
332
+ # Get weather-based event recommendations
333
+ recommendations = weather_to_event_recommendations(weather)
334
+
335
+ # Get outfit recommendation
336
+ temp = weather.get("temperature", {}).get("value", 70)
337
+ phrase = weather.get("phrase", "Clear")
338
+ outfit = recommend_outfit(temp, phrase)
339
+
340
+ # Format weather summary
341
+ weather_summary = format_weather_summary(weather)
342
+
343
+ # Build user-friendly response
344
+ response_text = (
345
+ f"🌀️ **Weather for {city_name}:**\n"
346
+ f"{weather_summary}\n\n"
347
+ f"πŸ‘• **What to wear:** {outfit}"
348
+ )
349
+
350
+ # Add event recommendations if available
351
+ if recommendations:
352
+ rec = recommendations[0] # Get top recommendation
353
+ response_text += f"\n\nπŸ“… **Activity suggestion:** {rec['reason']}"
354
+
355
+ return {
356
+ "tool": "weather",
357
+ "city": city_name,
358
+ "tenant_id": tenant_id,
359
+ "response": response_text,
360
+ "data": {
361
+ "weather": weather,
362
+ "recommendations": recommendations,
363
+ "outfit": outfit
364
+ }
365
+ }
366
+
367
+ except Exception as e:
368
+ logger.error(f"Weather query error: {e}", exc_info=True)
369
+ return {
370
+ "tool": "weather",
371
+ "city": city_name,
372
+ "response": (
373
+ f"I couldn't get the weather for {city_name} right now. "
374
+ f"Try again in a moment! 🌀️"
375
+ ),
376
+ "error": str(e)
377
+ }
378
+
379
+
380
+ # ============================================================
381
+ # EVENTS QUERY HANDLER (ENHANCED)
382
+ # ============================================================
383
+
384
+ async def _handle_events_query(
385
+ user_input: str,
386
+ city_name: str,
387
+ tenant_id: str,
388
+ lat: Optional[float],
389
+ lon: Optional[float]
390
+ ) -> Dict[str, Any]:
391
+ """
392
+ πŸ“… Handles event discovery queries.
393
+ """
394
+ global _event_request_count
395
+ _event_request_count += 1
396
+
397
+ logger.info(f"πŸ“… Event query #{_event_request_count} for {city_name}")
398
+
399
+ try:
400
+ # Load structured event data
401
+ event_data = load_city_events(tenant_id)
402
+ events = event_data.get("events", [])
403
+ num_events = len(events)
404
+
405
+ if num_events == 0:
406
+ return {
407
+ "tool": "civic_events",
408
+ "city": city_name,
409
+ "tenant_id": tenant_id,
410
+ "response": (
411
+ f"I don't have any upcoming events for {city_name} right now. "
412
+ f"Check back soon! πŸ“…"
413
+ )
414
+ }
415
+
416
+ # Get top event
417
+ top_event = events[0]
418
+ top_event_name = top_event.get("name", "Upcoming event")
419
+
420
+ # Build response
421
+ if num_events == 1:
422
+ response_text = (
423
+ f"πŸ“… **Upcoming event in {city_name}:**\n"
424
+ f"β€’ {top_event_name}\n\n"
425
+ f"Check the full details in the attached data!"
426
+ )
427
+ else:
428
+ response_text = (
429
+ f"πŸ“… **Found {num_events} upcoming events in {city_name}!**\n"
430
+ f"Top event: {top_event_name}\n\n"
431
+ f"Check the full list in the attached data!"
432
+ )
433
+
434
+ return {
435
+ "tool": "civic_events",
436
+ "city": city_name,
437
+ "tenant_id": tenant_id,
438
+ "response": response_text,
439
+ "data": event_data
440
+ }
441
+
442
+ except FileNotFoundError:
443
+ logger.warning(f"Event data file not found for {tenant_id}")
444
+ return {
445
+ "tool": "civic_events",
446
+ "city": city_name,
447
+ "response": (
448
+ f"Event data for {city_name} isn't available yet. "
449
+ f"I'm still learning about events in your area! πŸ“…"
450
+ ),
451
+ "error": "Event data file not found"
452
+ }
453
+
454
+ except Exception as e:
455
+ logger.error(f"Events query error: {e}", exc_info=True)
456
+ return {
457
+ "tool": "civic_events",
458
+ "city": city_name,
459
+ "response": (
460
+ f"I had trouble loading events for {city_name}. "
461
+ f"Try again soon! πŸ“…"
462
+ ),
463
+ "error": str(e)
464
+ }
465
+
466
+
467
+ # ============================================================
468
+ # RESOURCE QUERY HANDLER (ENHANCED)
469
+ # ============================================================
470
+
471
+ async def _handle_resource_query(
472
+ user_input: str,
473
+ city_name: str,
474
+ tenant_id: str,
475
+ lowered: str
476
+ ) -> Dict[str, Any]:
477
+ """
478
+ ♻️ Handles resource queries (trash, transit, emergency).
479
+ """
480
+ global _resource_request_count
481
+ _resource_request_count += 1
482
+
483
+ logger.info(f"♻️ Resource query #{_resource_request_count} for {city_name}")
484
+
485
+ # Map keywords to resource types
486
+ resource_query_map = {
487
+ "trash": "trash_and_recycling",
488
+ "recycling": "trash_and_recycling",
489
+ "garbage": "trash_and_recycling",
490
+ "bus": "transit",
491
+ "train": "transit",
492
+ "schedule": "transit",
493
+ "alert": "emergency",
494
+ "warning": "emergency",
495
+ "non emergency": "emergency"
496
+ }
497
+
498
+ # Find matching resource type
499
+ resource_key = next(
500
+ (resource_query_map[key] for key in resource_query_map if key in lowered),
501
+ None
502
+ )
503
+
504
+ if not resource_key:
505
+ return {
506
+ "tool": "unknown",
507
+ "city": city_name,
508
+ "response": (
509
+ "I'm not sure which resource you're asking about. "
510
+ "Try asking about trash, transit, or emergency services! πŸ’¬"
511
+ )
512
+ }
513
+
514
+ try:
515
+ # Load structured resource data
516
+ resource_data = load_city_resources(tenant_id)
517
+ service_info = resource_data["services"].get(resource_key, {})
518
+
519
+ if not service_info:
520
+ return {
521
+ "tool": resource_key,
522
+ "city": city_name,
523
+ "response": (
524
+ f"I don't have {resource_key.replace('_', ' ')} information "
525
+ f"for {city_name} yet. Check the city's official website! πŸ›οΈ"
526
+ )
527
+ }
528
+
529
+ # Build resource-specific response
530
+ if resource_key == "trash_and_recycling":
531
+ pickup_days = service_info.get('pickup_days', 'Varies by address')
532
+ response_text = (
533
+ f"♻️ **Trash & Recycling for {city_name}:**\n"
534
+ f"Pickup days: {pickup_days}\n\n"
535
+ f"Check the official link for your specific schedule!"
536
+ )
537
+
538
+ elif resource_key == "transit":
539
+ provider = service_info.get('provider', 'The local transit authority')
540
+ response_text = (
541
+ f"🚌 **Transit for {city_name}:**\n"
542
+ f"Provider: {provider}\n\n"
543
+ f"Use the provided links to find routes and schedules!"
544
+ )
545
+
546
+ elif resource_key == "emergency":
547
+ non_emergency = service_info.get('non_emergency_phone', 'N/A')
548
+ response_text = (
549
+ f"🚨 **Emergency Info for {city_name}:**\n"
550
+ f"Non-emergency: {non_emergency}\n\n"
551
+ f"**For life-threatening emergencies, always call 911.**"
552
+ )
553
+
554
+ else:
555
+ response_text = f"Information found for {resource_key.replace('_', ' ')}, but details aren't available yet."
556
+
557
+ return {
558
+ "tool": resource_key,
559
+ "city": city_name,
560
+ "tenant_id": tenant_id,
561
+ "response": response_text,
562
+ "data": service_info
563
+ }
564
+
565
+ except FileNotFoundError:
566
+ logger.warning(f"Resource data file not found for {tenant_id}")
567
+ return {
568
+ "tool": "resource_loader",
569
+ "city": city_name,
570
+ "response": (
571
+ f"Resource data for {city_name} isn't available yet. "
572
+ f"Check back soon! πŸ›οΈ"
573
+ ),
574
+ "error": "Resource data file not found"
575
+ }
576
+
577
+ except Exception as e:
578
+ logger.error(f"Resource query error: {e}", exc_info=True)
579
+ return {
580
+ "tool": "resource_loader",
581
+ "city": city_name,
582
+ "response": (
583
+ f"I had trouble loading resource data for {city_name}. "
584
+ f"Try again soon! πŸ›οΈ"
585
+ ),
586
+ "error": str(e)
587
+ }
588
+
589
+
590
+ # ============================================================
591
+ # UNKNOWN QUERY HANDLER
592
+ # ============================================================
593
+
594
+ def _handle_unknown_query(city_name: str) -> Dict[str, Any]:
595
+ """
596
+ ❓ Fallback for queries that don't match any tool.
597
+ """
598
+ logger.info(f"❓ Unknown query for {city_name}")
599
+
600
+ return {
601
+ "tool": "unknown",
602
+ "city": city_name,
603
+ "response": (
604
+ "I'm not sure which civic service you're asking about. "
605
+ "Try asking about weather, events, trash, or transit! πŸ’¬"
606
+ )
607
+ }
608
+
609
+
610
+ # ============================================================
611
+ # HEALTH CHECK & DIAGNOSTICS
612
+ # ============================================================
613
+
614
+ def get_tool_agent_health() -> Dict[str, Any]:
615
+ """
616
+ πŸ“Š Returns tool agent health status.
617
+
618
+ Used by the main application health check endpoint.
619
+ """
620
+ return {
621
+ "status": "operational",
622
+ "service_availability": {
623
+ "weather_agent": WEATHER_AGENT_AVAILABLE,
624
+ "location_utils": LOCATION_UTILS_AVAILABLE
625
+ },
626
+ "statistics": {
627
+ "total_requests": _tool_request_count,
628
+ "weather_requests": _weather_request_count,
629
+ "event_requests": _event_request_count,
630
+ "resource_requests": _resource_request_count
631
+ },
632
+ "supported_queries": [
633
+ "weather",
634
+ "events",
635
+ "trash_and_recycling",
636
+ "transit",
637
+ "emergency"
638
+ ]
639
+ }
640
+
641
+
642
+ # ============================================================
643
+ # TESTING
644
+ # ============================================================
645
+
646
+ if __name__ == "__main__":
647
+ """πŸ§ͺ Test tool agent functionality"""
648
+ import asyncio
649
+
650
+ print("=" * 60)
651
+ print("πŸ§ͺ Testing Tool Agent")
652
+ print("=" * 60)
653
+
654
+ # Display service availability
655
+ print("\nπŸ“Š Service Availability:")
656
+ print(f" Weather Agent: {'βœ…' if WEATHER_AGENT_AVAILABLE else '❌'}")
657
+ print(f" Location Utils: {'βœ…' if LOCATION_UTILS_AVAILABLE else '❌'}")
658
+
659
+ print("\n" + "=" * 60)
660
+
661
+ test_queries = [
662
+ {
663
+ "name": "Weather query with context",
664
+ "input": "What's the weather?",
665
+ "lat": 33.7490,
666
+ "lon": -84.3880,
667
+ "context": {"tenant_id": "atlanta"}
668
+ },
669
+ {
670
+ "name": "Events query with context",
671
+ "input": "show me local events",
672
+ "lat": None,
673
+ "lon": None,
674
+ "context": {"tenant_id": "atlanta"}
675
+ },
676
+ {
677
+ "name": "Trash query with context",
678
+ "input": "When is trash pickup?",
679
+ "lat": None,
680
+ "lon": None,
681
+ "context": {"tenant_id": "atlanta"}
682
+ }
683
+ ]
684
+
685
+ async def run_tests():
686
+ for i, query in enumerate(test_queries, 1):
687
+ print(f"\n--- Test {i}: {query['name']} ---")
688
+ print(f"Query: {query['input']}")
689
+ print(f"Context: {query.get('context', {})}")
690
+
691
+ try:
692
+ result = await handle_tool_request(
693
+ user_input=query["input"],
694
+ role="test_user",
695
+ lat=query.get("lat"),
696
+ lon=query.get("lon"),
697
+ context=query.get("context", {})
698
+ )
699
+
700
+ print(f"Tool: {result.get('tool')}")
701
+ print(f"City: {result.get('city')}")
702
+ print(f"Tenant ID: {result.get('tenant_id')}")
703
+
704
+ response = result.get('response')
705
+ if isinstance(response, str):
706
+ print(f"Response: {response[:150]}...")
707
+ else:
708
+ print(f"Response: [Dict with {len(response)} keys]")
709
+
710
+ if result.get('response_time_ms'):
711
+ print(f"Response time: {result['response_time_ms']:.0f}ms")
712
+
713
+ except Exception as e:
714
+ print(f"❌ Error: {e}")
715
+
716
+ asyncio.run(run_tests())
717
+
718
+ print("\n" + "=" * 60)
719
+ print("πŸ“Š Final Statistics:")
720
+ health = get_tool_agent_health()
721
+ for key, value in health["statistics"].items():
722
+ print(f" {key}: {value}")
723
+
724
+ print("\n" + "=" * 60)
725
+ print("βœ… Tests complete")
726
+ print("=" * 60)