pythonprincess commited on
Commit
9db7fa1
·
verified ·
1 Parent(s): 389406b

Delete tool_agent.py

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