pythonprincess commited on
Commit
40bd7d6
·
verified ·
1 Parent(s): fa9458f

Delete app/tool_agent.py

Browse files
Files changed (1) hide show
  1. app/tool_agent.py +0 -726
app/tool_agent.py DELETED
@@ -1,726 +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
- )
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)