Spaces:
Sleeping
Sleeping
Commit
·
7570791
0
Parent(s):
Initial commit of Weather App
Browse files- .gitignore +79 -0
- CONVERSATIONAL_FEATURES.md +148 -0
- INSTALLATION.md +128 -0
- README.md +239 -0
- README_HF.md +145 -0
- UI_ENHANCEMENT_SUMMARY.md +170 -0
- app.py +1394 -0
- demo_features.py +100 -0
- hf_app.py +69 -0
- requirements.txt +17 -0
- run.bat +61 -0
- run.sh +62 -0
- run_enhanced.sh +72 -0
- src/__init__.py +0 -0
- src/ai/__init__.py +0 -0
- src/ai/multi_provider.py +170 -0
- src/analysis/__init__.py +0 -0
- src/analysis/climate_analyzer.py +380 -0
- src/api/__init__.py +0 -0
- src/api/weather_client.py +442 -0
- src/chatbot/__init__.py +0 -0
- src/chatbot/climate_expert_nlp.py +608 -0
- src/chatbot/enhanced_chatbot.py +962 -0
- src/chatbot/nlp_processor.py +515 -0
- src/chatbot/weather_knowledge_base.py +587 -0
- src/geovisor/__init__.py +0 -0
- src/geovisor/map_manager.py +667 -0
- src/mcp_server/__init__.py +0 -0
- src/utils/__init__.py +0 -0
- wind_test_interface.html +0 -0
.gitignore
ADDED
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Python
|
2 |
+
__pycache__/
|
3 |
+
*.py[cod]
|
4 |
+
*$py.class
|
5 |
+
*.so
|
6 |
+
.Python
|
7 |
+
build/
|
8 |
+
develop-eggs/
|
9 |
+
dist/
|
10 |
+
downloads/
|
11 |
+
eggs/
|
12 |
+
.eggs/
|
13 |
+
lib/
|
14 |
+
lib64/
|
15 |
+
parts/
|
16 |
+
sdist/
|
17 |
+
var/
|
18 |
+
wheels/
|
19 |
+
*.egg-info/
|
20 |
+
.installed.cfg
|
21 |
+
*.egg
|
22 |
+
MANIFEST
|
23 |
+
|
24 |
+
# Environment variables
|
25 |
+
.env
|
26 |
+
.venv
|
27 |
+
env/
|
28 |
+
venv/
|
29 |
+
ENV/
|
30 |
+
env.bak/
|
31 |
+
venv.bak/
|
32 |
+
|
33 |
+
# Logs
|
34 |
+
*.log
|
35 |
+
logs/
|
36 |
+
data/
|
37 |
+
|
38 |
+
# IDE
|
39 |
+
.vscode/
|
40 |
+
.idea/
|
41 |
+
*.swp
|
42 |
+
*.swo
|
43 |
+
*~
|
44 |
+
|
45 |
+
# OS
|
46 |
+
.DS_Store
|
47 |
+
.DS_Store?
|
48 |
+
._*
|
49 |
+
.Spotlight-V100
|
50 |
+
.Trashes
|
51 |
+
ehthumbs.db
|
52 |
+
Thumbs.db
|
53 |
+
|
54 |
+
# Temporary files
|
55 |
+
*.tmp
|
56 |
+
*.temp
|
57 |
+
.cache/
|
58 |
+
|
59 |
+
# API Keys (should be in HF Spaces secrets)
|
60 |
+
*.key
|
61 |
+
api_keys.txt
|
62 |
+
credentials.json
|
63 |
+
|
64 |
+
# Local development
|
65 |
+
test_*
|
66 |
+
debug_*
|
67 |
+
scratch_*
|
68 |
+
|
69 |
+
# Node modules (if any)
|
70 |
+
node_modules/
|
71 |
+
|
72 |
+
# Coverage reports
|
73 |
+
htmlcov/
|
74 |
+
.coverage
|
75 |
+
.coverage.*
|
76 |
+
coverage.xml
|
77 |
+
*.cover
|
78 |
+
.hypothesis/
|
79 |
+
.pytest_cache/
|
CONVERSATIONAL_FEATURES.md
ADDED
@@ -0,0 +1,148 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Enhanced Conversational Weather Features
|
2 |
+
|
3 |
+
## Overview
|
4 |
+
The enhanced weather app now supports natural, conversational queries including activity advice, forecast requests, and historical comparisons. You can ask questions just like you would to a weather-savvy friend!
|
5 |
+
|
6 |
+
## New Query Types
|
7 |
+
|
8 |
+
### 🚴♂️ Activity-Based Queries
|
9 |
+
Ask for weather advice for specific activities:
|
10 |
+
|
11 |
+
**Examples:**
|
12 |
+
- "Should I go biking in Seattle today?"
|
13 |
+
- "Is it good weather for walking in Central Park?"
|
14 |
+
- "Can I have a picnic in Austin this weekend?"
|
15 |
+
- "Is it safe to drive to Chicago tomorrow?"
|
16 |
+
- "Good conditions for running in Miami?"
|
17 |
+
- "Should I plan an outdoor BBQ in Denver?"
|
18 |
+
|
19 |
+
**Supported Activities:**
|
20 |
+
- **Biking/Cycling**: Considers wind speed, precipitation, temperature comfort, visibility
|
21 |
+
- **Walking/Hiking**: Focuses on precipitation, temperature, air quality
|
22 |
+
- **Outdoor Events**: Considers all weather factors, especially precipitation probability
|
23 |
+
- **Driving/Travel**: Emphasizes visibility, precipitation, wind, road conditions
|
24 |
+
- **Water Activities**: Considers temperature, thunderstorm risk, UV considerations
|
25 |
+
- **Sports**: Factors in wind for ball sports, temperature for comfort
|
26 |
+
|
27 |
+
### 📅 Forecast & Timeline Queries
|
28 |
+
Get weather information for different time horizons:
|
29 |
+
|
30 |
+
**Examples:**
|
31 |
+
- "What's the forecast for New York this week?"
|
32 |
+
- "Will it rain in Portland tomorrow?"
|
33 |
+
- "Extended forecast for Miami"
|
34 |
+
- "Hourly weather breakdown for today in Chicago"
|
35 |
+
- "What's the weather trend for Los Angeles?"
|
36 |
+
- "Next few days outlook for Boston"
|
37 |
+
|
38 |
+
**Timeline Options:**
|
39 |
+
- **Short-term**: Today, tonight, tomorrow, next few hours
|
40 |
+
- **Medium-term**: This week, next week, weekend, next few days
|
41 |
+
- **Long-term**: Next month, seasonal outlook, extended forecast
|
42 |
+
|
43 |
+
### 📊 Historical & Comparative Queries
|
44 |
+
Compare current conditions to historical patterns:
|
45 |
+
|
46 |
+
**Examples:**
|
47 |
+
- "How does today's weather in Phoenix compare to normal?"
|
48 |
+
- "Is this typical weather for Seattle in June?"
|
49 |
+
- "Temperature compared to last year in New York"
|
50 |
+
- "Has it been rainier than usual in Portland?"
|
51 |
+
- "Climate data for Denver this season"
|
52 |
+
- "Weather records for Chicago"
|
53 |
+
|
54 |
+
**Historical Context:**
|
55 |
+
- **Recent**: Yesterday, last week, past week
|
56 |
+
- **Seasonal**: Last month, past season, seasonal averages
|
57 |
+
- **Yearly**: Last year, annual comparisons
|
58 |
+
- **Long-term**: Historical records, climate data, trends
|
59 |
+
|
60 |
+
## Smart Features
|
61 |
+
|
62 |
+
### 🧠 Context-Aware Responses
|
63 |
+
The AI understands context and provides relevant advice:
|
64 |
+
|
65 |
+
```
|
66 |
+
User: "Should I go biking in Seattle?"
|
67 |
+
AI: "Based on current conditions in Seattle - 65°F with light winds
|
68 |
+
and only 10% chance of rain - it's excellent weather for biking!
|
69 |
+
The temperature is comfortable and conditions are dry. Perfect
|
70 |
+
time to get out on the trails."
|
71 |
+
```
|
72 |
+
|
73 |
+
### 🌍 Multi-City Comparisons
|
74 |
+
Compare weather and activity conditions across cities:
|
75 |
+
|
76 |
+
```
|
77 |
+
User: "Compare biking conditions between Portland and Seattle"
|
78 |
+
AI: "For biking today: Portland has 72°F with calm winds and 0%
|
79 |
+
rain chance - ideal conditions! Seattle is 65°F with 5-10 mph
|
80 |
+
winds and 15% rain chance - still good but slightly windier.
|
81 |
+
Both cities offer great biking weather today."
|
82 |
+
```
|
83 |
+
|
84 |
+
### ⚠️ Safety Considerations
|
85 |
+
The AI prioritizes safety in activity recommendations:
|
86 |
+
|
87 |
+
- **High wind warnings** for outdoor activities
|
88 |
+
- **Precipitation alerts** for outdoor events
|
89 |
+
- **Temperature extremes** for physical activities
|
90 |
+
- **Visibility concerns** for travel
|
91 |
+
- **Storm warnings** for all outdoor activities
|
92 |
+
|
93 |
+
### 🎯 Personalized Advice
|
94 |
+
Get specific recommendations based on your activity:
|
95 |
+
|
96 |
+
- **Timing suggestions**: "Better conditions expected this afternoon"
|
97 |
+
- **Alternative activities**: "Not ideal for biking, but great for indoor activities"
|
98 |
+
- **Preparation tips**: "Bring layers for temperature changes"
|
99 |
+
- **Safety reminders**: "High winds - secure outdoor items"
|
100 |
+
|
101 |
+
## Sample Conversations
|
102 |
+
|
103 |
+
### Conversation 1: Activity Planning
|
104 |
+
```
|
105 |
+
User: "Should I go on a bike ride in Austin?"
|
106 |
+
AI: "Great question! In Austin right now it's 78°F with partly cloudy
|
107 |
+
skies and light 5 mph winds. Only 5% chance of rain - these are
|
108 |
+
excellent biking conditions! The temperature is perfect for cycling,
|
109 |
+
winds are calm, and no precipitation expected. Perfect time to hit
|
110 |
+
the trails! 🚴♂️"
|
111 |
+
```
|
112 |
+
|
113 |
+
### Conversation 2: Forecast Planning
|
114 |
+
```
|
115 |
+
User: "What's the forecast for outdoor events in Miami this weekend?"
|
116 |
+
AI: "For outdoor events in Miami this weekend: Saturday looks perfect
|
117 |
+
with 82°F, sunny skies, and only 10% rain chance. Sunday has a 40%
|
118 |
+
chance of afternoon showers with temps around 85°F. I'd recommend
|
119 |
+
planning outdoor activities for Saturday morning/afternoon, and have
|
120 |
+
backup indoor plans for Sunday afternoon. 🌞"
|
121 |
+
```
|
122 |
+
|
123 |
+
### Conversation 3: Travel Advice
|
124 |
+
```
|
125 |
+
User: "Is it safe to drive from Chicago to Detroit tomorrow?"
|
126 |
+
AI: "Travel conditions from Chicago to Detroit tomorrow look good!
|
127 |
+
Clear skies with 45°F temperatures and light winds. No precipitation
|
128 |
+
expected and good visibility. Road conditions should be excellent
|
129 |
+
for your drive. Safe travels! 🚗"
|
130 |
+
```
|
131 |
+
|
132 |
+
## Pro Tips
|
133 |
+
|
134 |
+
1. **Be specific about activities**: The more specific you are, the better advice you'll get
|
135 |
+
2. **Mention timeframes**: "today", "tomorrow", "this weekend" help focus the response
|
136 |
+
3. **Ask follow-up questions**: The AI remembers context within the conversation
|
137 |
+
4. **Combine queries**: "Should I bike to the park or drive due to weather?"
|
138 |
+
5. **Safety first**: The AI will always prioritize safety in recommendations
|
139 |
+
|
140 |
+
## Technical Features
|
141 |
+
|
142 |
+
- **Real-time data**: All recommendations based on current NWS data
|
143 |
+
- **AI-powered**: Gemini 2.0 Flash provides intelligent, contextual responses
|
144 |
+
- **Natural language**: Ask questions in plain English
|
145 |
+
- **Memory**: Conversation context is maintained for follow-up questions
|
146 |
+
- **Comprehensive**: Considers multiple weather factors for each activity
|
147 |
+
|
148 |
+
Start chatting with your enhanced weather assistant today! 🌤️
|
INSTALLATION.md
ADDED
@@ -0,0 +1,128 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# 🌤️ Weather App Pro - Complete Package
|
2 |
+
|
3 |
+
## 📦 What's Included
|
4 |
+
|
5 |
+
This package contains everything you need to run the Weather App Pro:
|
6 |
+
|
7 |
+
### 📁 Files:
|
8 |
+
- **`app.py`** - Main application (self-contained)
|
9 |
+
- **`requirements.txt`** - Python dependencies
|
10 |
+
- **`README.md`** - Documentation
|
11 |
+
- **`run.sh`** - Quick start script (Linux/Mac)
|
12 |
+
- **`run.bat`** - Quick start script (Windows)
|
13 |
+
|
14 |
+
## 🚀 Quick Start
|
15 |
+
|
16 |
+
### Option 1: Use Quick Start Scripts
|
17 |
+
|
18 |
+
**Linux/Mac:**
|
19 |
+
```bash
|
20 |
+
# Extract the archive
|
21 |
+
tar -xzf weather_app_pro_final.tar.gz
|
22 |
+
cd weather_app_final
|
23 |
+
|
24 |
+
# Run the quick start script
|
25 |
+
./run.sh
|
26 |
+
```
|
27 |
+
|
28 |
+
**Windows:**
|
29 |
+
```cmd
|
30 |
+
REM Extract the archive
|
31 |
+
REM Open the weather_app_final folder
|
32 |
+
|
33 |
+
REM Double-click run.bat or run in command prompt:
|
34 |
+
run.bat
|
35 |
+
```
|
36 |
+
|
37 |
+
### Option 2: Manual Installation
|
38 |
+
|
39 |
+
```bash
|
40 |
+
# Extract and navigate
|
41 |
+
tar -xzf weather_app_pro_final.tar.gz
|
42 |
+
cd weather_app_final
|
43 |
+
|
44 |
+
# Install dependencies
|
45 |
+
pip install -r requirements.txt
|
46 |
+
|
47 |
+
# Run the application
|
48 |
+
python app.py
|
49 |
+
```
|
50 |
+
|
51 |
+
### Option 3: Deploy to Hugging Face Spaces
|
52 |
+
|
53 |
+
1. Create new Space on Hugging Face
|
54 |
+
2. Select **Gradio** SDK
|
55 |
+
3. Upload `app.py`, `requirements.txt`, and `README.md`
|
56 |
+
4. Space auto-deploys!
|
57 |
+
|
58 |
+
## 🌟 Features
|
59 |
+
|
60 |
+
### 🤖 Smart Weather Chat
|
61 |
+
- Natural language processing
|
62 |
+
- Auto-zoom maps to mentioned cities
|
63 |
+
- Smart city comparisons
|
64 |
+
- Real-time weather data
|
65 |
+
|
66 |
+
### 🗺️ Dynamic Interactive Maps
|
67 |
+
- Dark theme optimized
|
68 |
+
- Auto-zoom to queried locations
|
69 |
+
- Comparison lines between cities
|
70 |
+
- Detailed weather popups
|
71 |
+
|
72 |
+
### 📊 Multi-Tab Interface
|
73 |
+
- **Chat + Map**: Main interactive experience
|
74 |
+
- **Forecasts**: Detailed 7-day predictions
|
75 |
+
- **Alerts**: Real-time weather alerts
|
76 |
+
- **About**: Documentation and tips
|
77 |
+
|
78 |
+
### 🌙 Beautiful Dark Mode
|
79 |
+
- Professional gradient theme
|
80 |
+
- Dark maps and charts
|
81 |
+
- Comfortable viewing
|
82 |
+
|
83 |
+
## 💬 Example Queries
|
84 |
+
|
85 |
+
Try these natural language questions:
|
86 |
+
|
87 |
+
- "What's the temperature in Wichita?"
|
88 |
+
- "Compare rain in New York and Miami"
|
89 |
+
- "Show me weather in Los Angeles"
|
90 |
+
- "Wind conditions in Chicago"
|
91 |
+
|
92 |
+
## 📍 Coverage
|
93 |
+
|
94 |
+
100+ US cities supported including:
|
95 |
+
- All major metropolitan areas
|
96 |
+
- State capitals
|
97 |
+
- Popular tourist destinations
|
98 |
+
|
99 |
+
## 🔧 System Requirements
|
100 |
+
|
101 |
+
- Python 3.7+
|
102 |
+
- Internet connection (for weather data)
|
103 |
+
- Modern web browser
|
104 |
+
|
105 |
+
## 📱 Access
|
106 |
+
|
107 |
+
Once running, open your browser to:
|
108 |
+
**http://localhost:7860**
|
109 |
+
|
110 |
+
## 🆘 Troubleshooting
|
111 |
+
|
112 |
+
### Common Issues:
|
113 |
+
|
114 |
+
1. **Port already in use**: Change port in app.py (line with `server_port=7860`)
|
115 |
+
2. **Dependencies fail**: Try upgrading pip: `pip install --upgrade pip`
|
116 |
+
3. **Python not found**: Ensure Python 3.7+ is installed and in PATH
|
117 |
+
|
118 |
+
### Support:
|
119 |
+
- Check the console output for error messages
|
120 |
+
- Ensure all dependencies are installed
|
121 |
+
- Verify internet connection for weather data
|
122 |
+
|
123 |
+
---
|
124 |
+
|
125 |
+
**🌟 Enjoy your intelligent weather assistant!**
|
126 |
+
|
127 |
+
*Powered by the National Weather Service API*
|
128 |
+
|
README.md
ADDED
@@ -0,0 +1,239 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# 🌤️ Weather App Pro Enhanced - Complete Package
|
2 |
+
|
3 |
+
## 🚀 **NEW: Full AI Integration with LlamaIndex & Gemini!**
|
4 |
+
|
5 |
+
This is the **COMPLETE** Weather App Pro package with **full LLM integration** for intelligent weather conversations.
|
6 |
+
|
7 |
+
### 🤖 **Enhanced AI Features**
|
8 |
+
|
9 |
+
#### **LlamaIndex Integration**
|
10 |
+
- **Document Understanding**: Advanced context processing
|
11 |
+
- **Memory Management**: Maintains conversation context
|
12 |
+
- **Vector Search**: Intelligent information retrieval
|
13 |
+
- **Chat Engine**: Sophisticated conversation handling
|
14 |
+
|
15 |
+
#### **Gemini API Integration**
|
16 |
+
- **Google's Gemini Pro**: State-of-the-art language model
|
17 |
+
- **Natural Conversations**: Human-like weather discussions
|
18 |
+
- **Smart Responses**: Context-aware and informative
|
19 |
+
- **Fallback System**: Works even without API key
|
20 |
+
|
21 |
+
#### **🆕 Conversational Weather Intelligence**
|
22 |
+
- **Activity Advice**: "Should I go biking in Seattle?" - Get weather-based recommendations
|
23 |
+
- **Forecast Queries**: Natural language forecast requests with timeline awareness
|
24 |
+
- **Historical Comparisons**: "How does today compare to normal?" context
|
25 |
+
- **Safety Considerations**: Intelligent warnings for outdoor activities
|
26 |
+
- **Multi-City Analysis**: Smart comparisons between locations
|
27 |
+
- **Personalized Responses**: Context-aware advice for your specific needs
|
28 |
+
|
29 |
+
> 📖 **See [CONVERSATIONAL_FEATURES.md](CONVERSATIONAL_FEATURES.md) for detailed examples and usage guide**
|
30 |
+
|
31 |
+
### 📁 **Complete Project Structure**
|
32 |
+
|
33 |
+
```
|
34 |
+
weather_app_complete/
|
35 |
+
├── enhanced_main.py # 🆕 Enhanced app with full AI
|
36 |
+
├── app.py # Standalone version (Hugging Face ready)
|
37 |
+
├── main.py # Basic modular version
|
38 |
+
├── requirements.txt # Updated with LlamaIndex & Gemini
|
39 |
+
├── run_enhanced.sh # 🆕 Enhanced quick start script
|
40 |
+
├── run.sh # Basic quick start (Linux/Mac)
|
41 |
+
├── run.bat # Basic quick start (Windows)
|
42 |
+
├── README.md # This comprehensive guide
|
43 |
+
├── INSTALLATION.md # Detailed setup instructions
|
44 |
+
├── src/ # Complete modular architecture
|
45 |
+
│ ├── __init__.py
|
46 |
+
│ ├── ai/ # Multi-AI provider system
|
47 |
+
│ │ ├── __init__.py
|
48 |
+
│ │ └── multi_provider.py
|
49 |
+
│ ├── analysis/ # Climate analysis & ML
|
50 |
+
│ │ ├── __init__.py
|
51 |
+
│ │ └── climate_analyzer.py
|
52 |
+
│ ├── api/ # Advanced weather API client
|
53 |
+
│ │ ├── __init__.py
|
54 |
+
│ │ └── weather_client.py
|
55 |
+
│ ├── chatbot/ # 🆕 Enhanced chatbot system
|
56 |
+
│ │ ├── __init__.py
|
57 |
+
│ │ ├── nlp_processor.py
|
58 |
+
│ │ └── enhanced_chatbot.py # 🆕 Full LLM integration
|
59 |
+
│ ├── geovisor/ # Interactive maps & visualization
|
60 |
+
│ │ ├── __init__.py
|
61 |
+
│ │ └── map_manager.py
|
62 |
+
│ ├── mcp_server/ # MCP protocol support
|
63 |
+
│ │ └── __init__.py
|
64 |
+
│ └── utils/ # Utility functions
|
65 |
+
│ └── __init__.py
|
66 |
+
├── static/ # Static assets
|
67 |
+
├── templates/ # HTML templates
|
68 |
+
├── data/ # Data storage
|
69 |
+
└── logs/ # Application logs
|
70 |
+
```
|
71 |
+
|
72 |
+
## 🚀 **Quick Start Options**
|
73 |
+
|
74 |
+
### **Option 1: Enhanced AI Version (Recommended)**
|
75 |
+
```bash
|
76 |
+
# Extract the package
|
77 |
+
tar -xzf weather_app_complete_ENHANCED.tar.gz
|
78 |
+
cd weather_app_complete
|
79 |
+
|
80 |
+
# Set Gemini API key for full AI features
|
81 |
+
export GEMINI_API_KEY="your-gemini-api-key"
|
82 |
+
|
83 |
+
# Run enhanced version
|
84 |
+
./run_enhanced.sh
|
85 |
+
```
|
86 |
+
|
87 |
+
### **Option 2: Standalone Version (Hugging Face)**
|
88 |
+
```bash
|
89 |
+
# For Hugging Face Spaces deployment
|
90 |
+
python app.py
|
91 |
+
```
|
92 |
+
|
93 |
+
### **Option 3: Basic Modular Version**
|
94 |
+
```bash
|
95 |
+
# Basic version without advanced AI
|
96 |
+
./run.sh
|
97 |
+
```
|
98 |
+
|
99 |
+
## 🤖 **AI Configuration**
|
100 |
+
|
101 |
+
### **Gemini API Key (Recommended)**
|
102 |
+
```bash
|
103 |
+
# Get your free API key from: https://makersuite.google.com/app/apikey
|
104 |
+
export GEMINI_API_KEY="your-gemini-api-key-here"
|
105 |
+
```
|
106 |
+
|
107 |
+
### **Optional: Additional AI Providers**
|
108 |
+
```bash
|
109 |
+
export OPENAI_API_KEY="your-openai-key"
|
110 |
+
export CLAUDE_API_KEY="your-claude-key"
|
111 |
+
```
|
112 |
+
|
113 |
+
## 💬 **Enhanced Conversation Examples**
|
114 |
+
|
115 |
+
The AI now provides **intelligent, contextual responses**:
|
116 |
+
|
117 |
+
### **Simple Query**
|
118 |
+
**User:** "What's the temperature in Wichita?"
|
119 |
+
**AI:** "The current temperature in Wichita is 72°F with partly cloudy skies. It's a pleasant day with light winds from the southwest at 8 mph. The humidity is comfortable at 45%, making it feel quite nice outside. Perfect weather for outdoor activities!"
|
120 |
+
|
121 |
+
### **Comparison Query**
|
122 |
+
**User:** "Compare rain in New York and Miami"
|
123 |
+
**AI:** "Great question! Looking at the current conditions:
|
124 |
+
|
125 |
+
🗽 **New York**: 15% chance of rain, mostly sunny with 68°F
|
126 |
+
🏖️ **Miami**: 65% chance of rain, scattered thunderstorms with 82°F
|
127 |
+
|
128 |
+
Miami has a significantly higher chance of precipitation today - about 50% more likely than New York. This is typical for Miami's subtropical climate, especially during summer months when afternoon thunderstorms are common. New York is experiencing a drier weather pattern today with high pressure dominating the region."
|
129 |
+
|
130 |
+
### **Complex Analysis**
|
131 |
+
**User:** "Should I plan outdoor activities in Denver this weekend?"
|
132 |
+
**AI:** "Based on Denver's weekend forecast, here's my recommendation:
|
133 |
+
|
134 |
+
**Saturday**: Excellent for outdoor activities! 75°F, sunny skies, light winds. Perfect hiking or biking weather.
|
135 |
+
|
136 |
+
**Sunday**: Plan indoor alternatives. 30% chance of afternoon thunderstorms, temperatures dropping to 65°F with gusty winds up to 25 mph.
|
137 |
+
|
138 |
+
**Recommendation**: Schedule your outdoor activities for Saturday morning through early afternoon. If you must be outside Sunday, plan for earlier in the day before the storm system moves in around 2 PM."
|
139 |
+
|
140 |
+
## 🌟 **Key Enhancements**
|
141 |
+
|
142 |
+
### **🧠 Intelligent Responses**
|
143 |
+
- **Context Awareness**: Remembers conversation history
|
144 |
+
- **Weather Expertise**: Trained on meteorological knowledge
|
145 |
+
- **Practical Advice**: Offers actionable recommendations
|
146 |
+
- **Natural Language**: Human-like conversation flow
|
147 |
+
|
148 |
+
### **🗺️ Dynamic Map Integration**
|
149 |
+
- **Auto-Focus**: Map zooms to mentioned cities automatically
|
150 |
+
- **Smart Markers**: Rich weather information in popups
|
151 |
+
- **Comparison Lines**: Visual connections between compared cities
|
152 |
+
- **Weather Layers**: Precipitation radar and overlays
|
153 |
+
|
154 |
+
### **📊 Advanced Analytics**
|
155 |
+
- **Real-time Processing**: Instant weather data analysis
|
156 |
+
- **Trend Recognition**: Identifies weather patterns
|
157 |
+
- **Anomaly Detection**: Highlights unusual conditions
|
158 |
+
- **Predictive Insights**: Future weather implications
|
159 |
+
|
160 |
+
## 🔧 **Technical Features**
|
161 |
+
|
162 |
+
### **LlamaIndex Integration**
|
163 |
+
- **Vector Store**: Efficient weather knowledge retrieval
|
164 |
+
- **Chat Memory**: Maintains conversation context
|
165 |
+
- **Document Processing**: Weather data understanding
|
166 |
+
- **Query Engine**: Intelligent information synthesis
|
167 |
+
|
168 |
+
### **Gemini API Features**
|
169 |
+
- **Advanced NLP**: Superior language understanding
|
170 |
+
- **Context Retention**: Multi-turn conversations
|
171 |
+
- **Weather Expertise**: Domain-specific knowledge
|
172 |
+
- **Error Handling**: Graceful fallback mechanisms
|
173 |
+
|
174 |
+
### **Robust Architecture**
|
175 |
+
- **Modular Design**: Clean, maintainable code
|
176 |
+
- **Error Recovery**: Handles API failures gracefully
|
177 |
+
- **Performance Optimized**: Fast response times
|
178 |
+
- **Scalable**: Ready for production deployment
|
179 |
+
|
180 |
+
## 📱 **Deployment Options**
|
181 |
+
|
182 |
+
### **Hugging Face Spaces**
|
183 |
+
1. Upload `app.py` and `requirements.txt`
|
184 |
+
2. Add `GEMINI_API_KEY` in Space settings
|
185 |
+
3. Select Gradio SDK
|
186 |
+
4. Auto-deploys with full AI features!
|
187 |
+
|
188 |
+
### **Local Development**
|
189 |
+
```bash
|
190 |
+
pip install -r requirements.txt
|
191 |
+
export GEMINI_API_KEY="your-key"
|
192 |
+
python enhanced_main.py
|
193 |
+
```
|
194 |
+
|
195 |
+
### **Docker Deployment**
|
196 |
+
```dockerfile
|
197 |
+
FROM python:3.9
|
198 |
+
COPY . /app
|
199 |
+
WORKDIR /app
|
200 |
+
RUN pip install -r requirements.txt
|
201 |
+
ENV GEMINI_API_KEY="your-key"
|
202 |
+
CMD ["python", "enhanced_main.py"]
|
203 |
+
```
|
204 |
+
|
205 |
+
## 🆘 **Troubleshooting**
|
206 |
+
|
207 |
+
### **AI Features Not Working**
|
208 |
+
1. Verify Gemini API key is set correctly
|
209 |
+
2. Check internet connection
|
210 |
+
3. Ensure API key has proper permissions
|
211 |
+
4. App works in basic mode without API key
|
212 |
+
|
213 |
+
### **Import Errors**
|
214 |
+
1. Install all requirements: `pip install -r requirements.txt`
|
215 |
+
2. Check Python version (3.7+ required)
|
216 |
+
3. Verify all files are in correct structure
|
217 |
+
|
218 |
+
### **Performance Issues**
|
219 |
+
1. Ensure stable internet connection
|
220 |
+
2. Check system resources (512MB RAM minimum)
|
221 |
+
3. Try basic version if enhanced is slow
|
222 |
+
|
223 |
+
## 🌟 **What's New in Enhanced Version**
|
224 |
+
|
225 |
+
✅ **Full LlamaIndex Integration** - Advanced AI framework
|
226 |
+
✅ **Gemini API Integration** - Google's powerful LLM
|
227 |
+
✅ **Intelligent Conversations** - Context-aware responses
|
228 |
+
✅ **Enhanced NLP** - Better query understanding
|
229 |
+
✅ **Memory Management** - Conversation history retention
|
230 |
+
✅ **Weather Expertise** - Domain-specific knowledge
|
231 |
+
✅ **Fallback Systems** - Works without API keys
|
232 |
+
✅ **Performance Optimized** - Fast, efficient processing
|
233 |
+
|
234 |
+
---
|
235 |
+
|
236 |
+
**🌟 The Most Advanced Weather Intelligence Platform**
|
237 |
+
|
238 |
+
*Complete AI integration with LlamaIndex & Gemini for truly intelligent weather conversations!*
|
239 |
+
|
README_HF.md
ADDED
@@ -0,0 +1,145 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
title: Weather App Pro Enhanced
|
3 |
+
emoji: 🌤️
|
4 |
+
colorFrom: blue
|
5 |
+
colorTo: purple
|
6 |
+
sdk: gradio
|
7 |
+
sdk_version: 4.0.0
|
8 |
+
app_file: hf_app.py
|
9 |
+
pinned: false
|
10 |
+
license: mit
|
11 |
+
short_description: AI-powered weather assistant with LlamaIndex & Gemini integration
|
12 |
+
---
|
13 |
+
|
14 |
+
# 🌤️ Weather App Pro Enhanced
|
15 |
+
|
16 |
+
## 🚀 Overview
|
17 |
+
|
18 |
+
An intelligent weather application powered by **LlamaIndex** and **Google Gemini 2.0 Flash** that provides natural language weather interactions, real-time forecasts, and interactive maps.
|
19 |
+
|
20 |
+
## ✨ Features
|
21 |
+
|
22 |
+
### 🤖 AI-Powered Chat Assistant
|
23 |
+
- **Natural Language Processing**: Ask questions in plain English
|
24 |
+
- **Smart City Recognition**: Understands 100+ US cities and variations
|
25 |
+
- **Intelligent Comparisons**: Compare weather between multiple cities
|
26 |
+
- **Contextual Responses**: Powered by Google Gemini 2.0 Flash
|
27 |
+
- **LlamaIndex Integration**: Advanced document understanding
|
28 |
+
|
29 |
+
### 🗺️ Interactive Weather Maps
|
30 |
+
- **Auto-Zoom**: Automatically focuses on mentioned cities
|
31 |
+
- **Real-time Data**: Official weather.gov API integration
|
32 |
+
- **Dark Theme**: Professional appearance optimized for viewing
|
33 |
+
- **Comparison Mode**: Visual connections between cities
|
34 |
+
|
35 |
+
### 📊 Advanced Analytics
|
36 |
+
- **7-Day Forecasts**: Detailed predictions with interactive charts
|
37 |
+
- **Weather Alerts**: Real-time severe weather monitoring
|
38 |
+
- **Temperature Trends**: Visual analysis with Plotly charts
|
39 |
+
- **Precipitation Tracking**: Hourly probability forecasts
|
40 |
+
|
41 |
+
## 💬 How to Use
|
42 |
+
|
43 |
+
1. **Natural Questions**: "What's the weather in Boston?"
|
44 |
+
2. **City Comparisons**: "Compare temperature in Miami and Seattle"
|
45 |
+
3. **Specific Queries**: "Is it rainy in Wichita?"
|
46 |
+
4. **Weather Patterns**: "Show me the forecast for New York"
|
47 |
+
|
48 |
+
## 🛠️ Technology Stack
|
49 |
+
|
50 |
+
- **Frontend**: Gradio with custom CSS themes
|
51 |
+
- **AI**: Google Gemini 2.0 Flash + LlamaIndex framework
|
52 |
+
- **Data**: National Weather Service (weather.gov) API
|
53 |
+
- **Maps**: Folium with OpenStreetMap integration
|
54 |
+
- **Charts**: Plotly for interactive visualizations
|
55 |
+
- **NLP**: Custom natural language processing engine
|
56 |
+
|
57 |
+
## ⚙️ Configuration
|
58 |
+
|
59 |
+
### For Full AI Features
|
60 |
+
Set your **Gemini API key** as a Hugging Face Space secret:
|
61 |
+
- Secret Name: `GEMINI_API_KEY`
|
62 |
+
- Secret Value: Your Google AI Studio API key
|
63 |
+
|
64 |
+
### API Key Setup
|
65 |
+
1. Visit [Google AI Studio](https://makersuite.google.com/app/apikey)
|
66 |
+
2. Create a new API key
|
67 |
+
3. Add it as a Space secret in your Hugging Face repository
|
68 |
+
|
69 |
+
## 📍 City Coverage
|
70 |
+
|
71 |
+
**Supported US cities include:**
|
72 |
+
- All major metropolitan areas (NYC, LA, Chicago, etc.)
|
73 |
+
- State capitals and major cities
|
74 |
+
- Popular tourist destinations
|
75 |
+
- 100+ cities with intelligent disambiguation
|
76 |
+
|
77 |
+
**City Disambiguation Examples:**
|
78 |
+
- "Wichita" → Defaults to Wichita, KS
|
79 |
+
- "Portland" → Defaults to Portland, OR
|
80 |
+
- "Springfield" → Defaults to Springfield, IL
|
81 |
+
|
82 |
+
## 🌟 Example Queries
|
83 |
+
|
84 |
+
```
|
85 |
+
"What's the temperature in Wichita?"
|
86 |
+
"Compare rain in New York and Miami"
|
87 |
+
"Show me weather in Los Angeles"
|
88 |
+
"Is it going to be windy in Chicago tomorrow?"
|
89 |
+
"What's the difference between Dallas and Austin weather?"
|
90 |
+
```
|
91 |
+
|
92 |
+
## 🔧 Technical Details
|
93 |
+
|
94 |
+
### AI Integration
|
95 |
+
- **Model**: Google Gemini 2.0 Flash (latest experimental)
|
96 |
+
- **Framework**: LlamaIndex for document understanding
|
97 |
+
- **Context Window**: 3000 tokens for conversation memory
|
98 |
+
- **Response Generation**: Contextual with real weather data
|
99 |
+
|
100 |
+
### Data Sources
|
101 |
+
- **Weather API**: National Weather Service (weather.gov)
|
102 |
+
- **Geocoding**: Built-in US city database with coordinates
|
103 |
+
- **Maps**: OpenStreetMap with custom weather overlays
|
104 |
+
- **Alerts**: Real-time weather warning system
|
105 |
+
|
106 |
+
### Performance
|
107 |
+
- **Response Time**: < 3 seconds for AI responses
|
108 |
+
- **Data Freshness**: Real-time weather updates
|
109 |
+
- **Caching**: Intelligent forecast caching
|
110 |
+
- **Fallback**: Graceful degradation without API keys
|
111 |
+
|
112 |
+
## 🐛 Troubleshooting
|
113 |
+
|
114 |
+
### Limited Features?
|
115 |
+
- Check that `GEMINI_API_KEY` is set as a Space secret
|
116 |
+
- Verify API key is valid and has quota remaining
|
117 |
+
- App works in basic mode without AI features
|
118 |
+
|
119 |
+
### City Not Found?
|
120 |
+
- Try full city name with state (e.g., "Portland, Oregon")
|
121 |
+
- Check spelling of city name
|
122 |
+
- Currently supports US cities only
|
123 |
+
|
124 |
+
### Map Not Loading?
|
125 |
+
- Check internet connection for map tiles
|
126 |
+
- Allow some time for initial map rendering
|
127 |
+
- Try refreshing the page
|
128 |
+
|
129 |
+
## 📄 License
|
130 |
+
|
131 |
+
MIT License - Feel free to use and modify!
|
132 |
+
|
133 |
+
## 🙏 Acknowledgments
|
134 |
+
|
135 |
+
- **Data**: National Weather Service for weather data
|
136 |
+
- **AI**: Google for Gemini API access
|
137 |
+
- **Framework**: LlamaIndex team for document AI
|
138 |
+
- **Interface**: Gradio team for the web framework
|
139 |
+
- **Maps**: OpenStreetMap contributors
|
140 |
+
|
141 |
+
---
|
142 |
+
|
143 |
+
**Built with ❤️ for weather enthusiasts and AI developers**
|
144 |
+
|
145 |
+
*Powered by Google Gemini 2.0 Flash, LlamaIndex, and the National Weather Service*
|
UI_ENHANCEMENT_SUMMARY.md
ADDED
@@ -0,0 +1,170 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# 🌤️ Weather App Pro Enhanced - UI Enhancement Summary
|
2 |
+
|
3 |
+
## ✨ Complete Interface Redesign Completed
|
4 |
+
|
5 |
+
The weather app has been successfully enhanced with a comprehensive premium UI redesign that improves visual appeal, user experience, and overall functionality while maintaining all core features.
|
6 |
+
|
7 |
+
## 🎨 Major UI Improvements
|
8 |
+
|
9 |
+
### 1. **Premium Dark Theme & Modern Styling**
|
10 |
+
- **Enhanced Color Palette**: Switched from basic blue gradients to sophisticated slate/blue professional theme
|
11 |
+
- **Modern Gradient Backgrounds**: `linear-gradient(135deg, #0f172a, #1e293b, #334155)`
|
12 |
+
- **Improved Typography**: Enhanced font weights, sizing, and spacing with Segoe UI family
|
13 |
+
- **Professional Shadows**: Added depth with layered box-shadows and backdrop blur effects
|
14 |
+
|
15 |
+
### 2. **Advanced Button Styling**
|
16 |
+
- **3D Effect Buttons**: Gradient backgrounds with hover transformations
|
17 |
+
- **Interactive Animations**: Scale and translateY effects on hover
|
18 |
+
- **Enhanced Visual Feedback**: Improved shadow depth and color transitions
|
19 |
+
- **Consistent Sizing**: Standardized padding and border-radius across all buttons
|
20 |
+
|
21 |
+
### 3. **Input Field Enhancements**
|
22 |
+
- **Glassmorphism Design**: Backdrop blur effects with semi-transparent backgrounds
|
23 |
+
- **Focus States**: Enhanced border colors and glow effects on focus
|
24 |
+
- **Better Contrast**: Improved text color and background contrast for readability
|
25 |
+
- **Smooth Transitions**: All input interactions have smooth CSS transitions
|
26 |
+
|
27 |
+
### 4. **Section Organization & Layout**
|
28 |
+
- **Container System**: New `.section-container` class with consistent styling
|
29 |
+
- **Hover Effects**: Subtle lift animations on section hover
|
30 |
+
- **Visual Hierarchy**: Clear section titles with consistent spacing
|
31 |
+
- **Feature Grid Layout**: Organized information in responsive card grids
|
32 |
+
|
33 |
+
### 5. **Enhanced Header Design**
|
34 |
+
- **Premium Header**: New `.premium-header` with sophisticated styling
|
35 |
+
- **Status Badges**: Color-coded badges for AI status, sync status, and real-time features
|
36 |
+
- **Better Typography**: Improved font sizing and weight hierarchy
|
37 |
+
- **Visual Accents**: Top border gradient for premium feel
|
38 |
+
|
39 |
+
## 🔧 Functional Improvements
|
40 |
+
|
41 |
+
### 1. **Auto-Sync Status Indicators**
|
42 |
+
- Visual feedback showing when chat and forecast sections are synchronized
|
43 |
+
- Real-time status badges indicating AI and sync capabilities
|
44 |
+
- Enhanced user awareness of app state
|
45 |
+
|
46 |
+
### 2. **Improved Chart Containers**
|
47 |
+
- Better background styling for charts with backdrop blur
|
48 |
+
- Consistent styling across temperature and precipitation charts
|
49 |
+
- Enhanced visibility on dark theme
|
50 |
+
|
51 |
+
### 3. **Feature Cards & Information Display**
|
52 |
+
- Interactive feature cards with hover effects
|
53 |
+
- Better organization of app capabilities and examples
|
54 |
+
- Responsive grid layout that adapts to screen size
|
55 |
+
|
56 |
+
### 4. **Enhanced Visual Feedback**
|
57 |
+
- Loading states and interactive feedback
|
58 |
+
- Improved button states and hover effects
|
59 |
+
- Better visual hierarchy for information display
|
60 |
+
|
61 |
+
## 📱 Mobile Responsiveness
|
62 |
+
|
63 |
+
### 1. **Responsive Design Elements**
|
64 |
+
- CSS media queries for mobile optimization
|
65 |
+
- Adaptive grid layouts that stack on smaller screens
|
66 |
+
- Consistent padding and margin adjustments for mobile
|
67 |
+
|
68 |
+
### 2. **Touch-Friendly Interface**
|
69 |
+
- Larger button sizes for better touch interaction
|
70 |
+
- Improved spacing for mobile interaction
|
71 |
+
- Consistent styling across different screen sizes
|
72 |
+
|
73 |
+
## 🎯 User Experience Enhancements
|
74 |
+
|
75 |
+
### 1. **Visual Hierarchy**
|
76 |
+
- Clear section titles with emoji icons and consistent styling
|
77 |
+
- Better contrast and readability across all elements
|
78 |
+
- Logical flow and organization of information
|
79 |
+
|
80 |
+
### 2. **Interactive Elements**
|
81 |
+
- Smooth animations and transitions throughout the interface
|
82 |
+
- Consistent hover states and interactive feedback
|
83 |
+
- Professional styling that feels modern and polished
|
84 |
+
|
85 |
+
### 3. **Information Architecture**
|
86 |
+
- Organized feature cards showing app capabilities
|
87 |
+
- Clear separation between different functional areas
|
88 |
+
- Better use of visual space and information density
|
89 |
+
|
90 |
+
## 🔄 Maintained Core Functionality
|
91 |
+
|
92 |
+
### ✅ All Original Features Preserved
|
93 |
+
- **AI-Powered Chat**: Full conversational AI with Google Gemini integration
|
94 |
+
- **City Synchronization**: Auto-sync between chat and forecast sections
|
95 |
+
- **Auto-Clear Functionality**: Forecast input clears after processing
|
96 |
+
- **Interactive Maps**: Dynamic weather map with city markers
|
97 |
+
- **Professional Charts**: Temperature and precipitation visualizations
|
98 |
+
- **Weather Alerts**: Real-time alert monitoring
|
99 |
+
- **7-Day Forecasts**: Comprehensive weather forecasts with emojis
|
100 |
+
|
101 |
+
### ✅ Enhanced Existing Features
|
102 |
+
- Better visual presentation of all features
|
103 |
+
- Improved user guidance and examples
|
104 |
+
- Enhanced accessibility and usability
|
105 |
+
- Professional styling that maintains functionality
|
106 |
+
|
107 |
+
## 🚀 Technical Implementation
|
108 |
+
|
109 |
+
### 1. **CSS Architecture**
|
110 |
+
- Modular CSS class system with consistent naming
|
111 |
+
- Advanced CSS features: backdrop-filter, gradients, transforms
|
112 |
+
- Mobile-first responsive design approach
|
113 |
+
- Performance-optimized animations and transitions
|
114 |
+
|
115 |
+
### 2. **Component Organization**
|
116 |
+
- Structured HTML with semantic class names
|
117 |
+
- Consistent styling patterns across all components
|
118 |
+
- Reusable design elements and patterns
|
119 |
+
|
120 |
+
### 3. **Theme Integration**
|
121 |
+
- Dark theme optimization throughout the interface
|
122 |
+
- Consistent color palette and styling
|
123 |
+
- Professional gradient and shadow systems
|
124 |
+
|
125 |
+
## 🎨 Visual Design Elements
|
126 |
+
|
127 |
+
### 1. **Color System**
|
128 |
+
- **Primary**: Blue gradients (#3b82f6, #6366f1, #8b5cf6)
|
129 |
+
- **Background**: Slate gradients (#0f172a, #1e293b, #334155)
|
130 |
+
- **Accent**: Green for sync indicators (#10b981)
|
131 |
+
- **Status**: Orange/Purple for various badges
|
132 |
+
|
133 |
+
### 2. **Typography**
|
134 |
+
- **Font Family**: Segoe UI, Tahoma, Geneva, Verdana, sans-serif
|
135 |
+
- **Hierarchy**: Clear font weight and size progression
|
136 |
+
- **Readability**: Optimized contrast and spacing
|
137 |
+
|
138 |
+
### 3. **Spacing & Layout**
|
139 |
+
- **Consistent Padding**: 20-40px for sections, 10-25px for elements
|
140 |
+
- **Border Radius**: 12-20px for modern rounded corners
|
141 |
+
- **Grid System**: Responsive feature card grids
|
142 |
+
|
143 |
+
## 📊 Results Summary
|
144 |
+
|
145 |
+
### ✅ **Completed Successfully**
|
146 |
+
1. **Premium UI Redesign**: Complete visual overhaul with modern styling
|
147 |
+
2. **Enhanced User Experience**: Better organization and visual hierarchy
|
148 |
+
3. **Maintained Functionality**: All original features preserved and enhanced
|
149 |
+
4. **Mobile Optimization**: Responsive design for all screen sizes
|
150 |
+
5. **Professional Appearance**: Enterprise-grade visual design
|
151 |
+
6. **Improved Accessibility**: Better contrast and interaction feedback
|
152 |
+
|
153 |
+
### 🌟 **Key Benefits**
|
154 |
+
- **Professional Appearance**: Enterprise-grade visual design
|
155 |
+
- **Better Usability**: Improved user guidance and interaction
|
156 |
+
- **Modern Design**: Contemporary UI/UX patterns and styling
|
157 |
+
- **Enhanced Accessibility**: Better contrast and readability
|
158 |
+
- **Responsive Layout**: Works great on all devices
|
159 |
+
- **Visual Feedback**: Clear interactive states and animations
|
160 |
+
|
161 |
+
## 🔗 Access Information
|
162 |
+
|
163 |
+
**Application URL**: http://localhost:7860
|
164 |
+
**Status**: ✅ Running successfully with all enhancements
|
165 |
+
**AI Features**: ✅ Fully enabled with Gemini API integration
|
166 |
+
**All Features**: ✅ Tested and working correctly
|
167 |
+
|
168 |
+
---
|
169 |
+
|
170 |
+
*The weather app now provides a premium user experience with professional styling while maintaining all the powerful AI and weather functionality that users expect.*
|
app.py
ADDED
@@ -0,0 +1,1394 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
Complete Weather App with AI Integration
|
3 |
+
Full LlamaIndex and Gemini API integration for intelligent conversations
|
4 |
+
"""
|
5 |
+
|
6 |
+
import gradio as gr
|
7 |
+
import asyncio
|
8 |
+
import logging
|
9 |
+
import os
|
10 |
+
import sys
|
11 |
+
from datetime import datetime
|
12 |
+
import json
|
13 |
+
import plotly.graph_objects as go
|
14 |
+
|
15 |
+
# Load environment variables from .env file
|
16 |
+
try:
|
17 |
+
from dotenv import load_dotenv
|
18 |
+
load_dotenv()
|
19 |
+
print("✅ Loaded .env file successfully")
|
20 |
+
except ImportError:
|
21 |
+
print("⚠️ python-dotenv not installed. Install with: pip install python-dotenv")
|
22 |
+
print("🔄 Trying to read environment variables directly...")
|
23 |
+
|
24 |
+
# Add src to path
|
25 |
+
sys.path.append(os.path.join(os.path.dirname(__file__), 'src'))
|
26 |
+
|
27 |
+
# Import enhanced modules
|
28 |
+
try:
|
29 |
+
from api.weather_client import create_weather_client
|
30 |
+
from chatbot.nlp_processor import create_nlp_processor
|
31 |
+
from chatbot.enhanced_chatbot import create_enhanced_chatbot
|
32 |
+
from geovisor.map_manager import create_map_manager
|
33 |
+
from analysis.climate_analyzer import create_climate_analyzer
|
34 |
+
except ImportError as e:
|
35 |
+
print(f"Import error: {e}")
|
36 |
+
print("Falling back to standalone mode...")
|
37 |
+
# Fallback to standalone app
|
38 |
+
exec(open('app.py').read())
|
39 |
+
exit()
|
40 |
+
|
41 |
+
# Configure logging
|
42 |
+
logging.basicConfig(level=logging.INFO)
|
43 |
+
logger = logging.getLogger(__name__)
|
44 |
+
|
45 |
+
class WeatherAppProEnhanced:
|
46 |
+
"""Enhanced Weather App with full AI integration"""
|
47 |
+
|
48 |
+
def __init__(self):
|
49 |
+
"""Initialize the enhanced weather app"""
|
50 |
+
# Get Gemini API key from environment (now supports .env files)
|
51 |
+
self.gemini_api_key = os.getenv("GEMINI_API_KEY")
|
52 |
+
|
53 |
+
if self.gemini_api_key:
|
54 |
+
print("🤖 Gemini API key found - AI features enabled!")
|
55 |
+
print(f"🔑 API key starts with: {self.gemini_api_key[:10]}...")
|
56 |
+
else:
|
57 |
+
print("⚠️ GEMINI_API_KEY not found in environment variables or .env file.")
|
58 |
+
print("💡 Create a .env file with: GEMINI_API_KEY=your-api-key")
|
59 |
+
print("🔄 App will work in basic mode without AI features.")
|
60 |
+
|
61 |
+
# Initialize components
|
62 |
+
self.weather_client = create_weather_client()
|
63 |
+
self.nlp_processor = create_nlp_processor()
|
64 |
+
self.enhanced_chatbot = create_enhanced_chatbot(
|
65 |
+
self.weather_client,
|
66 |
+
self.nlp_processor,
|
67 |
+
self.gemini_api_key
|
68 |
+
)
|
69 |
+
self.map_manager = create_map_manager()
|
70 |
+
self.climate_analyzer = create_climate_analyzer(self.weather_client)
|
71 |
+
|
72 |
+
# App state
|
73 |
+
self.current_cities = []
|
74 |
+
self.chat_history = []
|
75 |
+
self.last_weather_data = {}
|
76 |
+
|
77 |
+
logger.info("Enhanced Weather App initialized successfully")
|
78 |
+
|
79 |
+
async def process_chat_message(self, message: str, history: list) -> tuple:
|
80 |
+
"""Process chat message with enhanced AI"""
|
81 |
+
try:
|
82 |
+
if not message.strip():
|
83 |
+
return history, "", self._create_default_map(), "Please enter a weather question!"
|
84 |
+
|
85 |
+
# DEBUG: Check if LLM is enabled
|
86 |
+
llm_status = "🤖 LLM Enabled" if self.gemini_api_key else "⚠️ Basic mode (no LLM)"
|
87 |
+
print(f"Processing: '{message}' | {llm_status}")
|
88 |
+
|
89 |
+
# Add user message to history in messages format
|
90 |
+
history.append({"role": "user", "content": message})
|
91 |
+
|
92 |
+
# Process with appropriate method based on AI availability
|
93 |
+
if self.gemini_api_key:
|
94 |
+
result = await self._process_with_ai(message, history)
|
95 |
+
else:
|
96 |
+
result = self._process_basic(message, history)
|
97 |
+
|
98 |
+
# DEBUG: Show what the processing returned
|
99 |
+
print(f"Processing result keys: {list(result.keys()) if isinstance(result, dict) else 'Not a dict'}")
|
100 |
+
|
101 |
+
ai_response = result.get('response', 'Sorry, I could not process your request.')
|
102 |
+
cities = result.get('cities', [])
|
103 |
+
weather_data = result.get('weather_data', {})
|
104 |
+
map_data = result.get('map_data', [])
|
105 |
+
comparison_mode = result.get('comparison_mode', False)
|
106 |
+
|
107 |
+
# Update app state
|
108 |
+
self.current_cities = cities
|
109 |
+
self.last_weather_data = weather_data
|
110 |
+
|
111 |
+
# Add AI response to history in messages format
|
112 |
+
history.append({"role": "assistant", "content": ai_response})
|
113 |
+
|
114 |
+
# Create updated map
|
115 |
+
if map_data:
|
116 |
+
map_html = self.map_manager.create_weather_map(
|
117 |
+
map_data,
|
118 |
+
comparison_mode=comparison_mode,
|
119 |
+
show_weather_layers=True
|
120 |
+
)
|
121 |
+
else:
|
122 |
+
map_html = self._create_default_map()
|
123 |
+
|
124 |
+
# Create status message
|
125 |
+
if cities:
|
126 |
+
status = f"🎯 Found weather data for: {', '.join([c.title() for c in cities])}"
|
127 |
+
if comparison_mode:
|
128 |
+
status += " (Comparison mode active)"
|
129 |
+
else:
|
130 |
+
status = "💬 General weather assistance"
|
131 |
+
|
132 |
+
return history, "", map_html, status
|
133 |
+
|
134 |
+
except Exception as e:
|
135 |
+
logger.error(f"Error processing chat message: {e}")
|
136 |
+
error_response = f"I apologize, but I encountered an error: {str(e)}"
|
137 |
+
history.append({"role": "assistant", "content": error_response})
|
138 |
+
return history, "", self._create_default_map(), "❌ Error processing request"
|
139 |
+
|
140 |
+
def get_detailed_forecast(self, city_input: str) -> str:
|
141 |
+
"""Get detailed forecast text for a city"""
|
142 |
+
try:
|
143 |
+
if not city_input.strip():
|
144 |
+
return "Please enter a city name"
|
145 |
+
|
146 |
+
coords = self.weather_client.geocode_location(city_input)
|
147 |
+
if not coords:
|
148 |
+
return f"City '{city_input}' not found"
|
149 |
+
|
150 |
+
lat, lon = coords
|
151 |
+
forecast = self.weather_client.get_forecast(lat, lon)
|
152 |
+
|
153 |
+
if not forecast:
|
154 |
+
return f"No forecast data available for {city_input}"
|
155 |
+
|
156 |
+
# Create detailed forecast text with emojis
|
157 |
+
forecast_text = f"# 📊 7-Day Forecast for {city_input.title()}\n\n"
|
158 |
+
|
159 |
+
for i, period in enumerate(forecast[:7]): # 7-day forecast
|
160 |
+
condition = period.get('shortForecast', 'N/A')
|
161 |
+
weather_emoji = self._get_weather_emoji(condition)
|
162 |
+
temp = period.get('temperature', 'N/A')
|
163 |
+
temp_unit = period.get('temperatureUnit', 'F')
|
164 |
+
wind_speed = period.get('windSpeed', 'N/A')
|
165 |
+
wind_dir = period.get('windDirection', '')
|
166 |
+
precip = period.get('precipitationProbability', 0)
|
167 |
+
|
168 |
+
# Temperature emoji based on value
|
169 |
+
if isinstance(temp, (int, float)):
|
170 |
+
if temp >= 85:
|
171 |
+
temp_emoji = "🔥"
|
172 |
+
elif temp >= 75:
|
173 |
+
temp_emoji = "🌡️"
|
174 |
+
elif temp >= 60:
|
175 |
+
temp_emoji = "🌡️"
|
176 |
+
elif temp >= 40:
|
177 |
+
temp_emoji = "🧊"
|
178 |
+
else:
|
179 |
+
temp_emoji = "❄️"
|
180 |
+
else:
|
181 |
+
temp_emoji = "🌡️"
|
182 |
+
|
183 |
+
# Day/Night emoji
|
184 |
+
day_night_emoji = "☀️" if period.get('isDaytime', True) else "🌙"
|
185 |
+
|
186 |
+
forecast_text += f"## {day_night_emoji} {period.get('name', f'Period {i+1}')}\n"
|
187 |
+
forecast_text += f"**{temp_emoji} Temperature:** {temp}°{temp_unit}\n"
|
188 |
+
forecast_text += f"**{weather_emoji} Conditions:** {condition}\n"
|
189 |
+
forecast_text += f"**💨 Wind:** {wind_speed} {wind_dir}\n"
|
190 |
+
forecast_text += f"**🌧️ Rain Chance:** {precip}%\n"
|
191 |
+
|
192 |
+
# Add detailed forecast with line breaks for readability
|
193 |
+
details = period.get('detailedForecast', 'No details available')
|
194 |
+
if len(details) > 100:
|
195 |
+
details = details[:100] + "..."
|
196 |
+
forecast_text += f"**📝 Details:** {details}\n\n"
|
197 |
+
forecast_text += "---\n\n"
|
198 |
+
|
199 |
+
return forecast_text
|
200 |
+
|
201 |
+
except Exception as e:
|
202 |
+
logger.error(f"Error getting detailed forecast: {e}")
|
203 |
+
return f"Error getting forecast: {str(e)}"
|
204 |
+
|
205 |
+
def get_weather_alerts(self) -> str:
|
206 |
+
"""Get current weather alerts"""
|
207 |
+
try:
|
208 |
+
alerts = self.weather_client.get_alerts()
|
209 |
+
|
210 |
+
if not alerts:
|
211 |
+
return "# 🟢 No Active Weather Alerts\n\nThere are currently no active weather alerts in the system."
|
212 |
+
|
213 |
+
alerts_text = f"# 🚨 Active Weather Alerts ({len(alerts)} alerts)\n\n"
|
214 |
+
|
215 |
+
for alert in alerts[:10]: # Limit to 10 alerts
|
216 |
+
severity = alert.get('severity', 'Unknown')
|
217 |
+
event = alert.get('event', 'Weather Alert')
|
218 |
+
headline = alert.get('headline', 'No headline available')
|
219 |
+
areas = alert.get('areas', 'Unknown areas')
|
220 |
+
expires = alert.get('expires', 'Unknown expiration')
|
221 |
+
|
222 |
+
# Color code by severity
|
223 |
+
if severity.lower() == 'severe':
|
224 |
+
icon = "🔴"
|
225 |
+
elif severity.lower() == 'moderate':
|
226 |
+
icon = "🟡"
|
227 |
+
else:
|
228 |
+
icon = "🟠"
|
229 |
+
|
230 |
+
alerts_text += f"## {icon} {event}\n"
|
231 |
+
alerts_text += f"**Severity:** {severity}\n"
|
232 |
+
alerts_text += f"**Areas:** {areas}\n"
|
233 |
+
alerts_text += f"**Expires:** {expires}\n"
|
234 |
+
alerts_text += f"**Details:** {headline}\n\n"
|
235 |
+
alerts_text += "---\n\n"
|
236 |
+
|
237 |
+
return alerts_text
|
238 |
+
|
239 |
+
except Exception as e:
|
240 |
+
logger.error(f"Error getting weather alerts: {e}")
|
241 |
+
return f"# ❌ Error Getting Alerts\n\nError retrieving weather alerts: {str(e)}"
|
242 |
+
|
243 |
+
def _create_default_map(self) -> str:
|
244 |
+
"""Create default map view"""
|
245 |
+
try:
|
246 |
+
return self.map_manager.create_weather_map([])
|
247 |
+
except Exception as e:
|
248 |
+
logger.error(f"Error creating default map: {e}")
|
249 |
+
return """
|
250 |
+
<div style="width: 100%; height: 400px; background: #2c3e50; color: white;
|
251 |
+
display: flex; align-items: center; justify-content: center;
|
252 |
+
font-family: Arial, sans-serif; border-radius: 10px;">
|
253 |
+
<div style="text-align: center;">
|
254 |
+
<h3>🗺️ Weather Map</h3>
|
255 |
+
<p>Ask about weather in a city to see it on the map!</p>
|
256 |
+
</div>
|
257 |
+
</div>
|
258 |
+
"""
|
259 |
+
|
260 |
+
def _create_default_chart(self) -> go.Figure:
|
261 |
+
"""Create default empty chart with professional dark styling"""
|
262 |
+
fig = go.Figure()
|
263 |
+
|
264 |
+
# Add placeholder data
|
265 |
+
fig.add_trace(go.Scatter(
|
266 |
+
x=['Day 1', 'Day 2', 'Day 3', 'Day 4', 'Day 5', 'Day 6', 'Day 7'],
|
267 |
+
y=[72, 75, 71, 68, 73, 76, 74],
|
268 |
+
mode='lines+markers',
|
269 |
+
name='Temperature (°F)',
|
270 |
+
line=dict(color='#ff6b6b', width=3),
|
271 |
+
marker=dict(size=8, color='#ff6b6b')
|
272 |
+
))
|
273 |
+
|
274 |
+
fig.add_trace(go.Bar(
|
275 |
+
x=['Day 1', 'Day 2', 'Day 3', 'Day 4', 'Day 5', 'Day 6', 'Day 7'],
|
276 |
+
y=[20, 10, 40, 60, 30, 15, 25],
|
277 |
+
name='Precipitation (%)',
|
278 |
+
yaxis='y2',
|
279 |
+
opacity=0.7,
|
280 |
+
marker_color='#4ecdc4'
|
281 |
+
))
|
282 |
+
|
283 |
+
fig.update_layout(
|
284 |
+
title="📈 7-Day Weather Forecast",
|
285 |
+
xaxis_title="Days",
|
286 |
+
yaxis_title="Temperature (°F)",
|
287 |
+
yaxis2=dict(
|
288 |
+
title="Precipitation (%)",
|
289 |
+
overlaying='y',
|
290 |
+
side='right',
|
291 |
+
range=[0, 100]
|
292 |
+
),
|
293 |
+
template='plotly_dark',
|
294 |
+
height=400,
|
295 |
+
showlegend=True,
|
296 |
+
paper_bgcolor='rgba(0,0,0,0)',
|
297 |
+
plot_bgcolor='rgba(0,0,0,0)',
|
298 |
+
font=dict(color='white'),
|
299 |
+
title_font=dict(size=16, color='white')
|
300 |
+
)
|
301 |
+
|
302 |
+
return fig
|
303 |
+
|
304 |
+
def _get_weather_emoji(self, condition: str) -> str:
|
305 |
+
"""Get appropriate emoji for weather condition"""
|
306 |
+
condition_lower = condition.lower()
|
307 |
+
|
308 |
+
if any(word in condition_lower for word in ['sunny', 'clear', 'fair']):
|
309 |
+
return '☀️'
|
310 |
+
elif any(word in condition_lower for word in ['partly cloudy', 'partly sunny']):
|
311 |
+
return '⛅'
|
312 |
+
elif any(word in condition_lower for word in ['cloudy', 'overcast']):
|
313 |
+
return '☁️'
|
314 |
+
elif any(word in condition_lower for word in ['rain', 'shower', 'drizzle']):
|
315 |
+
return '🌧️'
|
316 |
+
elif any(word in condition_lower for word in ['thunderstorm', 'storm', 'lightning']):
|
317 |
+
return '⛈️'
|
318 |
+
elif any(word in condition_lower for word in ['snow', 'snowy', 'blizzard']):
|
319 |
+
return '❄️'
|
320 |
+
elif any(word in condition_lower for word in ['fog', 'mist', 'haze']):
|
321 |
+
return '🌫️'
|
322 |
+
elif any(word in condition_lower for word in ['wind', 'breezy', 'gusty']):
|
323 |
+
return '💨'
|
324 |
+
else:
|
325 |
+
return '🌤️'
|
326 |
+
|
327 |
+
# ...existing code...
|
328 |
+
|
329 |
+
async def _process_with_ai(self, message: str, history: list) -> dict:
|
330 |
+
"""Process message with full AI integration and weather context"""
|
331 |
+
try:
|
332 |
+
# Check if query lacks cities but might reference previous context
|
333 |
+
enhanced_message = await self._inject_context_if_needed(message, history)
|
334 |
+
|
335 |
+
# Use the enhanced chatbot's process_weather_query method for full AI integration
|
336 |
+
result = await self.enhanced_chatbot.process_weather_query(
|
337 |
+
enhanced_message,
|
338 |
+
chat_history=history
|
339 |
+
)
|
340 |
+
|
341 |
+
return result
|
342 |
+
|
343 |
+
except Exception as e:
|
344 |
+
logger.error(f"Error in AI processing: {e}")
|
345 |
+
return {
|
346 |
+
'response': f"I encountered an error processing your request: {str(e)}",
|
347 |
+
'cities': [],
|
348 |
+
'weather_data': {},
|
349 |
+
'map_data': [],
|
350 |
+
'query_analysis': {},
|
351 |
+
'map_update_needed': False,
|
352 |
+
'comparison_mode': False
|
353 |
+
}
|
354 |
+
|
355 |
+
def _process_basic(self, message: str, history: list) -> dict:
|
356 |
+
"""Process message with basic functionality (no AI)"""
|
357 |
+
try:
|
358 |
+
# Parse the query using NLP
|
359 |
+
query_analysis = self.nlp_processor.process_query(message)
|
360 |
+
cities = query_analysis.get('cities', [])
|
361 |
+
query_type = query_analysis.get('query_type', 'general')
|
362 |
+
is_comparison = query_analysis.get('comparison_info', {}).get('is_comparison', False)
|
363 |
+
|
364 |
+
# Basic city geocoding
|
365 |
+
geocoded_cities = []
|
366 |
+
weather_data = {}
|
367 |
+
|
368 |
+
for city in cities:
|
369 |
+
coords = self.weather_client.geocode_location(city)
|
370 |
+
if coords:
|
371 |
+
lat, lon = coords
|
372 |
+
forecast = self.weather_client.get_forecast(lat, lon)
|
373 |
+
current_obs = self.weather_client.get_current_observations(lat, lon)
|
374 |
+
|
375 |
+
weather_data[city] = {
|
376 |
+
'name': city,
|
377 |
+
'coordinates': coords,
|
378 |
+
'forecast': forecast,
|
379 |
+
'current': current_obs
|
380 |
+
}
|
381 |
+
geocoded_cities.append(city)
|
382 |
+
|
383 |
+
# Generate basic response
|
384 |
+
basic_response = self._generate_basic_response(
|
385 |
+
message, weather_data, query_analysis
|
386 |
+
)
|
387 |
+
|
388 |
+
return {
|
389 |
+
'response': basic_response,
|
390 |
+
'cities': geocoded_cities,
|
391 |
+
'weather_data': weather_data,
|
392 |
+
'map_data': self._prepare_map_data(geocoded_cities, weather_data),
|
393 |
+
'query_analysis': query_analysis,
|
394 |
+
'map_update_needed': len(geocoded_cities) > 0,
|
395 |
+
'comparison_mode': is_comparison
|
396 |
+
}
|
397 |
+
|
398 |
+
except Exception as e:
|
399 |
+
logger.error(f"Error in basic processing: {e}")
|
400 |
+
return {
|
401 |
+
'response': f"I encountered an error: {str(e)}",
|
402 |
+
'cities': [],
|
403 |
+
'weather_data': {},
|
404 |
+
'map_data': [],
|
405 |
+
'query_analysis': {},
|
406 |
+
'map_update_needed': False,
|
407 |
+
'comparison_mode': False
|
408 |
+
}
|
409 |
+
|
410 |
+
async def _geocode_with_disambiguation(self, city: str) -> dict:
|
411 |
+
"""Enhanced geocoding with disambiguation for multiple matches"""
|
412 |
+
try:
|
413 |
+
# Common city disambiguation patterns
|
414 |
+
city_mappings = {
|
415 |
+
'wichita': 'Wichita, KS', # Default to Kansas
|
416 |
+
'portland': 'Portland, OR', # Default to Oregon
|
417 |
+
'springfield': 'Springfield, IL', # Default to Illinois
|
418 |
+
'columbia': 'Columbia, SC', # Default to South Carolina
|
419 |
+
'franklin': 'Franklin, TN', # Default to Tennessee
|
420 |
+
'manchester': 'Manchester, NH', # Default to New Hampshire
|
421 |
+
'canton': 'Canton, OH', # Default to Ohio
|
422 |
+
'auburn': 'Auburn, AL', # Default to Alabama
|
423 |
+
}
|
424 |
+
|
425 |
+
# Check for disambiguation
|
426 |
+
city_lower = city.lower().strip()
|
427 |
+
if city_lower in city_mappings:
|
428 |
+
disambiguated_city = city_mappings[city_lower]
|
429 |
+
coords = self.weather_client.geocode_location(disambiguated_city)
|
430 |
+
else:
|
431 |
+
coords = self.weather_client.geocode_location(city)
|
432 |
+
|
433 |
+
if not coords:
|
434 |
+
return None
|
435 |
+
|
436 |
+
lat, lon = coords
|
437 |
+
|
438 |
+
# Get comprehensive weather data
|
439 |
+
forecast = self.weather_client.get_forecast(lat, lon)
|
440 |
+
current_obs = self.weather_client.get_current_observations(lat, lon)
|
441 |
+
|
442 |
+
return {
|
443 |
+
'name': city,
|
444 |
+
'original_name': city,
|
445 |
+
'disambiguated_name': city_mappings.get(city_lower, city),
|
446 |
+
'coordinates': coords,
|
447 |
+
'lat': lat,
|
448 |
+
'lon': lon,
|
449 |
+
'forecast': forecast,
|
450 |
+
'current': current_obs
|
451 |
+
}
|
452 |
+
|
453 |
+
except Exception as e:
|
454 |
+
logger.error(f"Error geocoding {city}: {e}")
|
455 |
+
return None
|
456 |
+
|
457 |
+
async def _generate_contextual_response(self, message: str, weather_data: dict, query_analysis: dict) -> str:
|
458 |
+
"""Generate AI-powered contextual response with weather data"""
|
459 |
+
try:
|
460 |
+
# Format weather context for AI
|
461 |
+
weather_context = self._format_weather_context_for_ai(weather_data, query_analysis)
|
462 |
+
|
463 |
+
# Enhanced prompt with detailed weather data
|
464 |
+
enhanced_prompt = f"""
|
465 |
+
User Query: {message}
|
466 |
+
|
467 |
+
Current Weather Data:
|
468 |
+
{weather_context}
|
469 |
+
|
470 |
+
Query Analysis:
|
471 |
+
- Cities mentioned: {query_analysis.get('cities', [])}
|
472 |
+
- Query type: {query_analysis.get('query_type', 'general')}
|
473 |
+
- Is comparison: {query_analysis.get('comparison_info', {}).get('is_comparison', False)}
|
474 |
+
|
475 |
+
Please provide a helpful, accurate, and engaging response about the weather.
|
476 |
+
Include specific data from the weather information provided.
|
477 |
+
If comparing cities, highlight key differences with specific numbers.
|
478 |
+
Offer practical advice or insights when relevant.
|
479 |
+
Be conversational and friendly.
|
480 |
+
"""
|
481 |
+
|
482 |
+
# Use the enhanced chatbot to generate response
|
483 |
+
if self.enhanced_chatbot.chat_engine:
|
484 |
+
response = await self.enhanced_chatbot._get_llamaindex_response(enhanced_prompt)
|
485 |
+
elif self.enhanced_chatbot.llm:
|
486 |
+
response = await self.enhanced_chatbot._get_direct_llm_response(enhanced_prompt)
|
487 |
+
elif self.gemini_api_key:
|
488 |
+
response = await self.enhanced_chatbot._get_gemini_response(enhanced_prompt)
|
489 |
+
else:
|
490 |
+
response = self._generate_basic_response(message, weather_data, query_analysis)
|
491 |
+
|
492 |
+
return response
|
493 |
+
|
494 |
+
except Exception as e:
|
495 |
+
logger.error(f"Error generating contextual response: {e}")
|
496 |
+
return self._generate_basic_response(message, weather_data, query_analysis)
|
497 |
+
|
498 |
+
def _format_weather_context_for_ai(self, weather_data: dict, query_analysis: dict) -> str:
|
499 |
+
"""Format weather data for AI context with rich details"""
|
500 |
+
if not weather_data:
|
501 |
+
return "No weather data available."
|
502 |
+
|
503 |
+
context_parts = []
|
504 |
+
|
505 |
+
for city, data in weather_data.items():
|
506 |
+
forecast = data.get('forecast', [])
|
507 |
+
current = data.get('current', {})
|
508 |
+
|
509 |
+
city_context = f"\n{city.title()}:"
|
510 |
+
|
511 |
+
if forecast:
|
512 |
+
current_period = forecast[0]
|
513 |
+
temp = current_period.get('temperature', 'N/A')
|
514 |
+
temp_unit = current_period.get('temperatureUnit', 'F')
|
515 |
+
conditions = current_period.get('shortForecast', 'N/A')
|
516 |
+
wind_speed = current_period.get('windSpeed', 'N/A')
|
517 |
+
wind_dir = current_period.get('windDirection', '')
|
518 |
+
precip = current_period.get('precipitationProbability', 0)
|
519 |
+
|
520 |
+
city_context += f"""
|
521 |
+
- Current Temperature: {temp}°{temp_unit}
|
522 |
+
- Conditions: {conditions}
|
523 |
+
- Wind: {wind_speed} {wind_dir}
|
524 |
+
- Precipitation Chance: {precip}%
|
525 |
+
- Detailed Forecast: {current_period.get('detailedForecast', 'N/A')[:200]}...
|
526 |
+
"""
|
527 |
+
|
528 |
+
# Add next few periods for context
|
529 |
+
if len(forecast) > 1:
|
530 |
+
city_context += "\n Next periods:"
|
531 |
+
for i, period in enumerate(forecast[1:4], 1):
|
532 |
+
name = period.get('name', f'Period {i+1}')
|
533 |
+
temp = period.get('temperature', 'N/A')
|
534 |
+
conditions = period.get('shortForecast', 'N/A')
|
535 |
+
city_context += f"\n - {name}: {temp}°F, {conditions}"
|
536 |
+
|
537 |
+
if current:
|
538 |
+
temp_c = current.get('temperature')
|
539 |
+
if temp_c:
|
540 |
+
temp_f = (temp_c * 9/5) + 32
|
541 |
+
city_context += f"\n- Observed Temperature: {temp_f:.1f}°F"
|
542 |
+
|
543 |
+
humidity = current.get('relativeHumidity', {})
|
544 |
+
if isinstance(humidity, dict):
|
545 |
+
humidity_val = humidity.get('value')
|
546 |
+
if humidity_val:
|
547 |
+
city_context += f"\n- Humidity: {humidity_val}%"
|
548 |
+
|
549 |
+
context_parts.append(city_context)
|
550 |
+
|
551 |
+
return "\n".join(context_parts)
|
552 |
+
|
553 |
+
def _generate_basic_response(self, message: str, weather_data: dict, query_analysis: dict) -> str:
|
554 |
+
"""Generate basic response without AI"""
|
555 |
+
cities = query_analysis.get('cities', [])
|
556 |
+
query_type = query_analysis.get('query_type', 'general')
|
557 |
+
is_comparison = query_analysis.get('comparison_info', {}).get('is_comparison', False)
|
558 |
+
|
559 |
+
if not cities:
|
560 |
+
return "I'd be happy to help with weather information! Please specify a city you're interested in."
|
561 |
+
|
562 |
+
if len(cities) == 1:
|
563 |
+
city = cities[0]
|
564 |
+
if city in weather_data:
|
565 |
+
return self._generate_single_city_response(city, weather_data[city], query_type)
|
566 |
+
else:
|
567 |
+
return f"I couldn't find weather data for {city.title()}. Please check the city name."
|
568 |
+
|
569 |
+
elif is_comparison and len(cities) >= 2:
|
570 |
+
return self._generate_comparison_response(cities, weather_data, query_type)
|
571 |
+
|
572 |
+
return "I can help you with weather information for US cities. Try asking about temperature, conditions, or comparing cities!"
|
573 |
+
|
574 |
+
def _generate_single_city_response(self, city: str, city_data: dict, query_type: str) -> str:
|
575 |
+
"""Generate response for single city"""
|
576 |
+
forecast = city_data.get('forecast', [])
|
577 |
+
if not forecast:
|
578 |
+
return f"Sorry, I couldn't get weather data for {city.title()}."
|
579 |
+
|
580 |
+
current = forecast[0]
|
581 |
+
temp = current.get('temperature', 'N/A')
|
582 |
+
temp_unit = current.get('temperatureUnit', 'F')
|
583 |
+
conditions = current.get('shortForecast', 'N/A')
|
584 |
+
wind = current.get('windSpeed', 'N/A')
|
585 |
+
wind_dir = current.get('windDirection', '')
|
586 |
+
precip = current.get('precipitationProbability', 0)
|
587 |
+
|
588 |
+
if query_type == 'temperature':
|
589 |
+
return f"🌡️ The current temperature in **{city.title()}** is **{temp}°{temp_unit}**. Conditions are {conditions.lower()}."
|
590 |
+
elif query_type == 'precipitation':
|
591 |
+
return f"🌧️ In **{city.title()}**, there's a **{precip}% chance of precipitation**. Current conditions: {conditions}."
|
592 |
+
elif query_type == 'wind':
|
593 |
+
return f"💨 Wind in **{city.title()}** is **{wind} {wind_dir}**. Current conditions: {conditions}."
|
594 |
+
else: # general
|
595 |
+
return f"""
|
596 |
+
🌤️ **Weather in {city.title()}:**
|
597 |
+
- **Temperature:** {temp}°{temp_unit}
|
598 |
+
- **Conditions:** {conditions}
|
599 |
+
- **Wind:** {wind} {wind_dir}
|
600 |
+
- **Rain Chance:** {precip}%
|
601 |
+
|
602 |
+
*I've updated the map to show {city.title()}. Click the marker for more details!*
|
603 |
+
"""
|
604 |
+
|
605 |
+
def _generate_comparison_response(self, cities: list, weather_data: dict, query_type: str) -> str:
|
606 |
+
"""Generate response for city comparison"""
|
607 |
+
if len(cities) < 2:
|
608 |
+
return "I need at least two cities to make a comparison."
|
609 |
+
|
610 |
+
comparison_text = f"Weather comparison between {' and '.join([c.title() for c in cities])}:\n\n"
|
611 |
+
|
612 |
+
for city in cities:
|
613 |
+
if city in weather_data:
|
614 |
+
forecast = weather_data[city].get('forecast', [])
|
615 |
+
if forecast:
|
616 |
+
current = forecast[0]
|
617 |
+
temp = current.get('temperature', 'N/A')
|
618 |
+
conditions = current.get('shortForecast', 'N/A')
|
619 |
+
precip = current.get('precipitationProbability', 0)
|
620 |
+
|
621 |
+
comparison_text += f"📍 **{city.title()}**: {temp}°F, {conditions}, {precip}% rain chance\n"
|
622 |
+
|
623 |
+
comparison_text += "\n*Check the map to see both locations with detailed weather data!*"
|
624 |
+
return comparison_text
|
625 |
+
|
626 |
+
def _generate_fallback_response(self, message: str, query_analysis: dict) -> str:
|
627 |
+
"""Generate fallback response when no weather data is available"""
|
628 |
+
cities = query_analysis.get('cities', [])
|
629 |
+
|
630 |
+
if cities:
|
631 |
+
return f"I couldn't find weather data for {', '.join([c.title() for c in cities])}. Please check the city names and try again."
|
632 |
+
else:
|
633 |
+
return "I can help you with weather information for US cities. Please mention a city you're interested in!"
|
634 |
+
|
635 |
+
def _prepare_map_data(self, cities: list, weather_data: dict) -> list:
|
636 |
+
"""Prepare data for map visualization"""
|
637 |
+
map_data = []
|
638 |
+
|
639 |
+
for city in cities:
|
640 |
+
if city in weather_data:
|
641 |
+
city_data = weather_data[city]
|
642 |
+
coords = city_data.get('coordinates')
|
643 |
+
forecast = city_data.get('forecast', [])
|
644 |
+
|
645 |
+
if coords and forecast:
|
646 |
+
lat, lon = coords
|
647 |
+
map_data.append({
|
648 |
+
'name': city,
|
649 |
+
'lat': lat,
|
650 |
+
'lon': lon,
|
651 |
+
'forecast': forecast
|
652 |
+
})
|
653 |
+
|
654 |
+
return map_data
|
655 |
+
|
656 |
+
async def _inject_context_if_needed(self, message: str, history: list) -> str:
|
657 |
+
"""Inject previous city context if the query lacks explicit cities but seems weather-related"""
|
658 |
+
try:
|
659 |
+
# Quick NLP analysis to see if message has cities
|
660 |
+
query_analysis = self.nlp_processor.process_query(message)
|
661 |
+
cities_in_query = query_analysis.get('cities', [])
|
662 |
+
|
663 |
+
# If query already has cities, no context injection needed
|
664 |
+
if cities_in_query:
|
665 |
+
return message
|
666 |
+
|
667 |
+
# Check if this looks like a follow-up weather query
|
668 |
+
weather_related_keywords = [
|
669 |
+
'weather', 'temperature', 'rain', 'snow', 'wind', 'forecast',
|
670 |
+
'conditions', 'humidity', 'precipitation', 'cloudy', 'sunny',
|
671 |
+
'hot', 'cold', 'warm', 'cool', 'transport', 'transportation',
|
672 |
+
'recommended', 'should i', 'can i', 'good for', 'advice',
|
673 |
+
'biking', 'walking', 'driving', 'outdoor', 'activity'
|
674 |
+
]
|
675 |
+
|
676 |
+
message_lower = message.lower()
|
677 |
+
is_weather_related = any(keyword in message_lower for keyword in weather_related_keywords)
|
678 |
+
|
679 |
+
if not is_weather_related:
|
680 |
+
return message
|
681 |
+
|
682 |
+
# Look for the most recent cities mentioned in conversation history
|
683 |
+
recent_cities = []
|
684 |
+
|
685 |
+
# Search through recent conversation history (last 10 messages)
|
686 |
+
for entry in reversed(history[-10:]):
|
687 |
+
if isinstance(entry, dict) and entry.get('role') == 'user':
|
688 |
+
content = entry.get('content', '')
|
689 |
+
elif isinstance(entry, list) and len(entry) >= 1:
|
690 |
+
content = entry[0] # User message in [user, assistant] format
|
691 |
+
else:
|
692 |
+
continue
|
693 |
+
|
694 |
+
# Extract cities from this historical message
|
695 |
+
historical_analysis = self.nlp_processor.process_query(content)
|
696 |
+
historical_cities = historical_analysis.get('cities', [])
|
697 |
+
|
698 |
+
if historical_cities:
|
699 |
+
recent_cities.extend(historical_cities)
|
700 |
+
break # Use the most recent cities found
|
701 |
+
|
702 |
+
# If we found recent cities, inject them into the current query
|
703 |
+
if recent_cities:
|
704 |
+
# Remove duplicates while preserving order
|
705 |
+
unique_cities = []
|
706 |
+
for city in recent_cities:
|
707 |
+
if city not in unique_cities:
|
708 |
+
unique_cities.append(city)
|
709 |
+
|
710 |
+
# Inject context into the message
|
711 |
+
cities_context = ", ".join(unique_cities[:2]) # Limit to 2 most recent cities
|
712 |
+
enhanced_message = f"{message} (referring to {cities_context})"
|
713 |
+
|
714 |
+
logger.info(f"Context injection: '{message}' -> '{enhanced_message}'")
|
715 |
+
return enhanced_message
|
716 |
+
|
717 |
+
return message
|
718 |
+
|
719 |
+
except Exception as e:
|
720 |
+
logger.error(f"Error in context injection: {e}")
|
721 |
+
return message
|
722 |
+
|
723 |
+
def create_interface(self):
|
724 |
+
"""Create the enhanced Gradio interface with integrated forecast functionality"""
|
725 |
+
|
726 |
+
# Enhanced custom CSS for premium dark theme and modern UI
|
727 |
+
custom_css = """
|
728 |
+
.gradio-container {
|
729 |
+
background: linear-gradient(135deg, #0f172a, #1e293b, #334155) !important;
|
730 |
+
min-height: 100vh;
|
731 |
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif !important;
|
732 |
+
}
|
733 |
+
|
734 |
+
.gr-button {
|
735 |
+
background: linear-gradient(45deg, #3b82f6, #6366f1, #8b5cf6) !important;
|
736 |
+
border: none !important;
|
737 |
+
border-radius: 12px !important;
|
738 |
+
color: white !important;
|
739 |
+
font-weight: 600 !important;
|
740 |
+
box-shadow: 0 4px 15px rgba(59, 130, 246, 0.3) !important;
|
741 |
+
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
|
742 |
+
padding: 12px 24px !important;
|
743 |
+
font-size: 14px !important;
|
744 |
+
}
|
745 |
+
|
746 |
+
.gr-button:hover {
|
747 |
+
transform: translateY(-2px) scale(1.02) !important;
|
748 |
+
box-shadow: 0 8px 25px rgba(59, 130, 246, 0.4) !important;
|
749 |
+
background: linear-gradient(45deg, #2563eb, #4f46e5, #7c3aed) !important;
|
750 |
+
}
|
751 |
+
|
752 |
+
.gr-textbox {
|
753 |
+
background: rgba(15, 23, 42, 0.8) !important;
|
754 |
+
border: 2px solid rgba(59, 130, 246, 0.3) !important;
|
755 |
+
color: #e2e8f0 !important;
|
756 |
+
border-radius: 12px !important;
|
757 |
+
backdrop-filter: blur(10px) !important;
|
758 |
+
transition: all 0.3s ease !important;
|
759 |
+
}
|
760 |
+
|
761 |
+
.gr-textbox:focus {
|
762 |
+
border-color: rgba(59, 130, 246, 0.6) !important;
|
763 |
+
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1) !important;
|
764 |
+
}
|
765 |
+
|
766 |
+
.gr-chatbot {
|
767 |
+
background: rgba(15, 23, 42, 0.6) !important;
|
768 |
+
border-radius: 16px !important;
|
769 |
+
border: 2px solid rgba(59, 130, 246, 0.2) !important;
|
770 |
+
backdrop-filter: blur(20px) !important;
|
771 |
+
box-shadow: inset 0 2px 10px rgba(0, 0, 0, 0.3) !important;
|
772 |
+
}
|
773 |
+
|
774 |
+
.premium-header {
|
775 |
+
background: linear-gradient(135deg, #1e293b, #334155, #475569);
|
776 |
+
color: white;
|
777 |
+
padding: 40px 30px;
|
778 |
+
border-radius: 20px;
|
779 |
+
margin-bottom: 30px;
|
780 |
+
text-align: center;
|
781 |
+
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4);
|
782 |
+
border: 1px solid rgba(59, 130, 246, 0.2);
|
783 |
+
position: relative;
|
784 |
+
overflow: hidden;
|
785 |
+
}
|
786 |
+
|
787 |
+
.premium-header::before {
|
788 |
+
content: '';
|
789 |
+
position: absolute;
|
790 |
+
top: 0;
|
791 |
+
left: 0;
|
792 |
+
right: 0;
|
793 |
+
height: 2px;
|
794 |
+
background: linear-gradient(90deg, #3b82f6, #6366f1, #8b5cf6);
|
795 |
+
}
|
796 |
+
|
797 |
+
.section-container {
|
798 |
+
background: rgba(15, 23, 42, 0.4) !important;
|
799 |
+
border-radius: 16px !important;
|
800 |
+
padding: 25px !important;
|
801 |
+
margin: 20px 0 !important;
|
802 |
+
border: 2px solid rgba(59, 130, 246, 0.15) !important;
|
803 |
+
backdrop-filter: blur(20px) !important;
|
804 |
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3) !important;
|
805 |
+
transition: all 0.3s ease !important;
|
806 |
+
}
|
807 |
+
|
808 |
+
.section-container:hover {
|
809 |
+
border-color: rgba(59, 130, 246, 0.3) !important;
|
810 |
+
transform: translateY(-2px) !important;
|
811 |
+
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.4) !important;
|
812 |
+
}
|
813 |
+
|
814 |
+
.chart-container {
|
815 |
+
background: rgba(15, 23, 42, 0.6) !important;
|
816 |
+
border-radius: 12px !important;
|
817 |
+
padding: 15px !important;
|
818 |
+
border: 1px solid rgba(59, 130, 246, 0.2) !important;
|
819 |
+
backdrop-filter: blur(10px) !important;
|
820 |
+
}
|
821 |
+
|
822 |
+
.feature-grid {
|
823 |
+
display: grid;
|
824 |
+
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
825 |
+
gap: 20px;
|
826 |
+
margin: 20px 0;
|
827 |
+
}
|
828 |
+
|
829 |
+
.feature-card {
|
830 |
+
background: rgba(15, 23, 42, 0.6);
|
831 |
+
border-radius: 12px;
|
832 |
+
padding: 20px;
|
833 |
+
border: 1px solid rgba(59, 130, 246, 0.2);
|
834 |
+
backdrop-filter: blur(10px);
|
835 |
+
transition: all 0.3s ease;
|
836 |
+
}
|
837 |
+
|
838 |
+
.feature-card:hover {
|
839 |
+
transform: translateY(-4px);
|
840 |
+
border-color: rgba(59, 130, 246, 0.4);
|
841 |
+
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
|
842 |
+
}
|
843 |
+
|
844 |
+
.sync-indicator {
|
845 |
+
background: linear-gradient(45deg, #10b981, #059669);
|
846 |
+
color: white;
|
847 |
+
padding: 10px 20px;
|
848 |
+
border-radius: 25px;
|
849 |
+
font-size: 12px;
|
850 |
+
font-weight: 600;
|
851 |
+
text-align: center;
|
852 |
+
margin: 15px 0;
|
853 |
+
box-shadow: 0 4px 15px rgba(16, 185, 129, 0.3);
|
854 |
+
display: inline-block;
|
855 |
+
}
|
856 |
+
|
857 |
+
.section-title {
|
858 |
+
font-size: 20px;
|
859 |
+
font-weight: 700;
|
860 |
+
color: #60a5fa;
|
861 |
+
margin-bottom: 20px;
|
862 |
+
display: flex;
|
863 |
+
align-items: center;
|
864 |
+
gap: 10px;
|
865 |
+
}
|
866 |
+
|
867 |
+
.status-badge {
|
868 |
+
background: linear-gradient(45deg, #f59e0b, #d97706);
|
869 |
+
color: white;
|
870 |
+
padding: 6px 12px;
|
871 |
+
border-radius: 20px;
|
872 |
+
font-size: 11px;
|
873 |
+
font-weight: 600;
|
874 |
+
text-transform: uppercase;
|
875 |
+
letter-spacing: 0.5px;
|
876 |
+
}
|
877 |
+
|
878 |
+
.ai-badge {
|
879 |
+
background: linear-gradient(45deg, #8b5cf6, #7c3aed);
|
880 |
+
color: white;
|
881 |
+
padding: 6px 12px;
|
882 |
+
border-radius: 20px;
|
883 |
+
font-size: 11px;
|
884 |
+
font-weight: 600;
|
885 |
+
text-transform: uppercase;
|
886 |
+
letter-spacing: 0.5px;
|
887 |
+
}
|
888 |
+
|
889 |
+
/* Mobile responsiveness */
|
890 |
+
@media (max-width: 768px) {
|
891 |
+
.premium-header {
|
892 |
+
padding: 25px 20px;
|
893 |
+
}
|
894 |
+
|
895 |
+
.section-container {
|
896 |
+
padding: 20px;
|
897 |
+
margin: 15px 0;
|
898 |
+
}
|
899 |
+
|
900 |
+
.feature-grid {
|
901 |
+
grid-template-columns: 1fr;
|
902 |
+
gap: 15px;
|
903 |
+
}
|
904 |
+
}
|
905 |
+
"""
|
906 |
+
|
907 |
+
with gr.Blocks(css=custom_css, title="🌤️ Weather App", theme=gr.themes.Base()) as app:
|
908 |
+
|
909 |
+
# Premium Header
|
910 |
+
gr.HTML("""
|
911 |
+
<div class="premium-header">
|
912 |
+
<h1 style="font-size: 2.5rem; margin-bottom: 15px; font-weight: 800;">
|
913 |
+
🌤️ Weather App
|
914 |
+
</h1>
|
915 |
+
<p style="font-size: 1.2rem; margin-bottom: 10px; opacity: 0.9;">
|
916 |
+
🤖 AI-Powered Weather Intelligence Platform
|
917 |
+
</p>
|
918 |
+
<div style="display: flex; justify-content: center; gap: 10px; margin: 20px 0; flex-wrap: wrap;">
|
919 |
+
<span class="ai-badge">🧠 AI ENABLED</span>
|
920 |
+
<span class="status-badge">🔗 SYNC ACTIVE</span>
|
921 |
+
<span class="status-badge">📊 REAL-TIME</span>
|
922 |
+
</div>
|
923 |
+
<p style="font-size: 1rem; opacity: 0.8; max-width: 600px; margin: 0 auto;">
|
924 |
+
<strong>✨ Experience intelligent weather conversations ✨</strong><br>
|
925 |
+
Ask natural questions, compare cities, get forecasts, and explore interactive maps
|
926 |
+
</p>
|
927 |
+
</div>
|
928 |
+
""")
|
929 |
+
|
930 |
+
# Main AI Weather Assistant Section
|
931 |
+
with gr.Group(elem_classes=["section-container"]):
|
932 |
+
with gr.Row():
|
933 |
+
with gr.Column(scale=1):
|
934 |
+
gr.HTML('<div class="section-title">💬 AI Weather Assistant</div>')
|
935 |
+
chatbot = gr.Chatbot(
|
936 |
+
label="Enhanced Weather Assistant",
|
937 |
+
height=400,
|
938 |
+
placeholder="👋 Hi! I'm your AI weather assistant. Ask me about weather in any city, compare locations, or use the forecast section below!",
|
939 |
+
elem_classes=["dark"],
|
940 |
+
type="messages"
|
941 |
+
)
|
942 |
+
|
943 |
+
with gr.Row():
|
944 |
+
msg_input = gr.Textbox(
|
945 |
+
placeholder="Ask about weather: 'Compare rain in Seattle and Portland' or 'What's the forecast for Miami?'",
|
946 |
+
label="Your Question",
|
947 |
+
scale=4,
|
948 |
+
elem_classes=["dark"]
|
949 |
+
)
|
950 |
+
send_btn = gr.Button("🚀 Send", scale=1)
|
951 |
+
|
952 |
+
clear_btn = gr.Button("🗑️ Clear Chat")
|
953 |
+
|
954 |
+
gr.HTML("""
|
955 |
+
<div class="feature-grid">
|
956 |
+
<div class="feature-card">
|
957 |
+
<h4>🧠 Smart Conversations</h4>
|
958 |
+
<p>"What's the weather like in Denver today?"</p>
|
959 |
+
</div>
|
960 |
+
<div class="feature-card">
|
961 |
+
<h4>🔍 City Comparisons</h4>
|
962 |
+
<p>"Compare temperature between Austin and Houston"</p>
|
963 |
+
</div>
|
964 |
+
<div class="feature-card">
|
965 |
+
<h4>🌂 Activity Advice</h4>
|
966 |
+
<p>"Should I bring an umbrella in Portland?"</p>
|
967 |
+
</div>
|
968 |
+
<div class="feature-card">
|
969 |
+
<h4>💨 Specialized Queries</h4>
|
970 |
+
<p>"Wind conditions for sailing in San Francisco"</p>
|
971 |
+
</div>
|
972 |
+
</div>
|
973 |
+
""")
|
974 |
+
|
975 |
+
with gr.Column(scale=1):
|
976 |
+
gr.HTML('<div class="section-title">🗺️ Dynamic Weather Map</div>')
|
977 |
+
weather_map = gr.HTML(
|
978 |
+
value=self._get_current_map(),
|
979 |
+
label="Interactive Map"
|
980 |
+
)
|
981 |
+
|
982 |
+
gr.HTML("""
|
983 |
+
<div class="feature-grid">
|
984 |
+
<div class="feature-card">
|
985 |
+
<h4>🔍 Auto-Zoom</h4>
|
986 |
+
<p>Automatically focus on mentioned cities</p>
|
987 |
+
</div>
|
988 |
+
<div class="feature-card">
|
989 |
+
<h4>📍 Rich Markers</h4>
|
990 |
+
<p>Click for detailed weather information</p>
|
991 |
+
</div>
|
992 |
+
<div class="feature-card">
|
993 |
+
<h4>🔗 Comparison Lines</h4>
|
994 |
+
<p>Visual connections between cities</p>
|
995 |
+
</div>
|
996 |
+
<div class="feature-card">
|
997 |
+
<h4>🌙 Dark Theme</h4>
|
998 |
+
<p>Optimized for comfortable viewing</p>
|
999 |
+
</div>
|
1000 |
+
</div>
|
1001 |
+
""")
|
1002 |
+
|
1003 |
+
# Premium Integrated Forecast Section
|
1004 |
+
with gr.Group(elem_classes=["section-container"]):
|
1005 |
+
gr.HTML('<div class="section-title">📊 Professional Weather Forecasts</div>')
|
1006 |
+
gr.HTML('<div class="sync-indicator">🔄 Auto-synced with AI Assistant</div>')
|
1007 |
+
gr.Markdown("Get comprehensive 7-day forecasts with professional charts, detailed analysis, and weather emojis")
|
1008 |
+
|
1009 |
+
with gr.Row():
|
1010 |
+
forecast_city_input = gr.Textbox(
|
1011 |
+
placeholder="Enter city name (e.g., New York, Los Angeles, Chicago)",
|
1012 |
+
label="🏙️ City for Detailed Forecast",
|
1013 |
+
scale=3
|
1014 |
+
)
|
1015 |
+
get_forecast_btn = gr.Button("📈 Get Professional Forecast", scale=1)
|
1016 |
+
|
1017 |
+
forecast_output = gr.Markdown("Enter a city name above to see detailed forecast with weather emojis! 🌤️")
|
1018 |
+
|
1019 |
+
with gr.Row():
|
1020 |
+
with gr.Column(elem_classes=["chart-container"]):
|
1021 |
+
temp_chart = gr.Plot(label="🌡️ Temperature Analysis")
|
1022 |
+
with gr.Column(elem_classes=["chart-container"]):
|
1023 |
+
precip_chart = gr.Plot(label="🌧️ Precipitation Forecast")
|
1024 |
+
|
1025 |
+
# Weather Alerts Section
|
1026 |
+
with gr.Group(elem_classes=["section-container"]):
|
1027 |
+
gr.HTML('<div class="section-title">🚨 Live Weather Alerts</div>')
|
1028 |
+
with gr.Row():
|
1029 |
+
refresh_alerts_btn = gr.Button("🔄 Refresh Alerts")
|
1030 |
+
alerts_output = gr.Markdown("Click 'Refresh Alerts' to see current weather alerts.")
|
1031 |
+
|
1032 |
+
# Enhanced About Section
|
1033 |
+
with gr.Group(elem_classes=["section-container"]):
|
1034 |
+
gr.HTML('<div class="section-title">ℹ️ About Weather App</div>')
|
1035 |
+
|
1036 |
+
gr.HTML("""
|
1037 |
+
<div class="feature-grid">
|
1038 |
+
<div class="feature-card">
|
1039 |
+
<h3>🤖 AI-Powered Chat</h3>
|
1040 |
+
<p>Natural language processing with Google Gemini for intelligent weather conversations</p>
|
1041 |
+
</div>
|
1042 |
+
<div class="feature-card">
|
1043 |
+
<h3>📊 Professional Charts</h3>
|
1044 |
+
<p>Interactive visualizations with temperature trends and precipitation forecasts</p>
|
1045 |
+
</div>
|
1046 |
+
<div class="feature-card">
|
1047 |
+
<h3>🗺️ Dynamic Maps</h3>
|
1048 |
+
<p>Real-time weather visualization with auto-zoom and comparison features</p>
|
1049 |
+
</div>
|
1050 |
+
<div class="feature-card">
|
1051 |
+
<h3>🔄 Smart Sync</h3>
|
1052 |
+
<p>Automatic synchronization between chat and forecast sections</p>
|
1053 |
+
</div>
|
1054 |
+
<div class="feature-card">
|
1055 |
+
<h3>⚠️ Live Alerts</h3>
|
1056 |
+
<p>Real-time weather alert monitoring and notifications</p>
|
1057 |
+
</div>
|
1058 |
+
<div class="feature-card">
|
1059 |
+
<h3>🌡️ Detailed Forecasts</h3>
|
1060 |
+
<p>Comprehensive 7-day forecasts with weather emojis and analysis</p>
|
1061 |
+
</div>
|
1062 |
+
</div>
|
1063 |
+
""")
|
1064 |
+
|
1065 |
+
gr.HTML("""
|
1066 |
+
<div style="text-align: center; margin-top: 30px; padding: 20px; background: rgba(59, 130, 246, 0.1); border-radius: 12px; border: 1px solid rgba(59, 130, 246, 0.2);">
|
1067 |
+
<h3 style="color: #60a5fa; margin-bottom: 15px;">🌟 Built with ❤️ for weather enthusiasts</h3>
|
1068 |
+
<p style="color: #cbd5e1; margin-bottom: 10px;">
|
1069 |
+
<strong>Powered by:</strong> National Weather Service API & Google Gemini AI
|
1070 |
+
</p>
|
1071 |
+
<p style="color: #94a3b8; font-size: 0.9rem;">
|
1072 |
+
Experience the future of weather intelligence with seamless AI integration
|
1073 |
+
</p>
|
1074 |
+
</div>
|
1075 |
+
""")
|
1076 |
+
|
1077 |
+
# Event handlers
|
1078 |
+
def handle_chat(message, history):
|
1079 |
+
"""Handle chat messages with enhanced AI processing and auto-sync cities"""
|
1080 |
+
try:
|
1081 |
+
if self.gemini_api_key:
|
1082 |
+
# Use enhanced AI processing
|
1083 |
+
loop = asyncio.new_event_loop()
|
1084 |
+
asyncio.set_event_loop(loop)
|
1085 |
+
result = loop.run_until_complete(
|
1086 |
+
self._process_with_ai(message, history)
|
1087 |
+
)
|
1088 |
+
loop.close()
|
1089 |
+
else:
|
1090 |
+
# Fallback to basic processing
|
1091 |
+
result = self._process_basic(message, history)
|
1092 |
+
|
1093 |
+
# Update chat history in messages format
|
1094 |
+
new_history = history + [{"role": "user", "content": message}, {"role": "assistant", "content": result['response']}]
|
1095 |
+
|
1096 |
+
# Update map if needed
|
1097 |
+
new_map = self._get_current_map()
|
1098 |
+
if result.get('map_update_needed', False):
|
1099 |
+
new_map = self._update_map_with_cities(result.get('cities', []), result.get('weather_data', {}))
|
1100 |
+
|
1101 |
+
# Auto-sync cities to forecast input (get the first city mentioned)
|
1102 |
+
cities = result.get('cities', [])
|
1103 |
+
forecast_city_sync = cities[0] if cities else ""
|
1104 |
+
|
1105 |
+
return new_history, "", new_map, forecast_city_sync
|
1106 |
+
|
1107 |
+
except Exception as e:
|
1108 |
+
logger.error(f"Error in chat handler: {e}")
|
1109 |
+
error_history = history + [{"role": "user", "content": message}, {"role": "assistant", "content": f"Sorry, I encountered an error: {str(e)}"}]
|
1110 |
+
return error_history, "", self._get_current_map(), ""
|
1111 |
+
|
1112 |
+
def handle_forecast(city_name):
|
1113 |
+
"""Handle detailed forecast requests with auto-clear and enhanced styling"""
|
1114 |
+
try:
|
1115 |
+
if not city_name.strip():
|
1116 |
+
return (
|
1117 |
+
"Please enter a city name to get the forecast! 🏙️",
|
1118 |
+
self._create_default_chart(),
|
1119 |
+
self._create_default_chart(),
|
1120 |
+
"" # Clear input
|
1121 |
+
)
|
1122 |
+
|
1123 |
+
# Get enhanced forecast text with emojis
|
1124 |
+
forecast_text = self.get_detailed_forecast(city_name)
|
1125 |
+
|
1126 |
+
# Get coordinates for charts
|
1127 |
+
coords = self.weather_client.geocode_location(city_name)
|
1128 |
+
if not coords:
|
1129 |
+
return (
|
1130 |
+
f"❌ Could not find weather data for '{city_name}'. Please check the city name and try again.",
|
1131 |
+
self._create_default_chart(),
|
1132 |
+
self._create_default_chart(),
|
1133 |
+
"" # Clear input even on error
|
1134 |
+
)
|
1135 |
+
|
1136 |
+
lat, lon = coords
|
1137 |
+
|
1138 |
+
# Get forecast data for charts
|
1139 |
+
forecast_data = self.weather_client.get_forecast(lat, lon)
|
1140 |
+
hourly_data = self.weather_client.get_hourly_forecast(lat, lon, hours=24)
|
1141 |
+
|
1142 |
+
# Create professional charts
|
1143 |
+
temp_chart = self._create_temperature_chart(forecast_data) if forecast_data else self._create_default_chart()
|
1144 |
+
precip_chart = self._create_precipitation_chart(hourly_data) if hourly_data else self._create_default_chart()
|
1145 |
+
|
1146 |
+
return forecast_text, temp_chart, precip_chart, "" # Clear input after successful processing
|
1147 |
+
|
1148 |
+
except Exception as e:
|
1149 |
+
logger.error(f"Error in forecast handler: {e}")
|
1150 |
+
return (
|
1151 |
+
f"❌ Error getting forecast for '{city_name}': {str(e)}",
|
1152 |
+
self._create_default_chart(),
|
1153 |
+
self._create_default_chart(),
|
1154 |
+
"" # Clear input on error
|
1155 |
+
)
|
1156 |
+
|
1157 |
+
# Wire up event handlers with enhanced synchronization
|
1158 |
+
send_btn.click(
|
1159 |
+
fn=handle_chat,
|
1160 |
+
inputs=[msg_input, chatbot],
|
1161 |
+
outputs=[chatbot, msg_input, weather_map, forecast_city_input]
|
1162 |
+
)
|
1163 |
+
|
1164 |
+
msg_input.submit(
|
1165 |
+
fn=handle_chat,
|
1166 |
+
inputs=[msg_input, chatbot],
|
1167 |
+
outputs=[chatbot, msg_input, weather_map, forecast_city_input]
|
1168 |
+
)
|
1169 |
+
|
1170 |
+
clear_btn.click(
|
1171 |
+
fn=lambda: ([], "", ""),
|
1172 |
+
outputs=[chatbot, msg_input, forecast_city_input]
|
1173 |
+
)
|
1174 |
+
|
1175 |
+
get_forecast_btn.click(
|
1176 |
+
fn=handle_forecast,
|
1177 |
+
inputs=[forecast_city_input],
|
1178 |
+
outputs=[forecast_output, temp_chart, precip_chart, forecast_city_input]
|
1179 |
+
)
|
1180 |
+
|
1181 |
+
forecast_city_input.submit(
|
1182 |
+
fn=handle_forecast,
|
1183 |
+
inputs=[forecast_city_input],
|
1184 |
+
outputs=[forecast_output, temp_chart, precip_chart, forecast_city_input]
|
1185 |
+
)
|
1186 |
+
|
1187 |
+
refresh_alerts_btn.click(
|
1188 |
+
fn=self.get_weather_alerts,
|
1189 |
+
outputs=[alerts_output]
|
1190 |
+
)
|
1191 |
+
|
1192 |
+
return app
|
1193 |
+
|
1194 |
+
def _get_current_map(self) -> str:
|
1195 |
+
"""Get the current map HTML"""
|
1196 |
+
try:
|
1197 |
+
return self.map_manager.create_weather_map([])
|
1198 |
+
except Exception as e:
|
1199 |
+
logger.error(f"Error getting current map: {e}")
|
1200 |
+
return "<div>Map temporarily unavailable</div>"
|
1201 |
+
|
1202 |
+
def _update_map_with_cities(self, cities: list, weather_data: dict) -> str:
|
1203 |
+
"""Update map with city markers"""
|
1204 |
+
try:
|
1205 |
+
# Transform weather_data to the format expected by map manager
|
1206 |
+
cities_data = []
|
1207 |
+
for city in cities:
|
1208 |
+
if city in weather_data:
|
1209 |
+
city_data = weather_data[city]
|
1210 |
+
coords = city_data.get('coordinates')
|
1211 |
+
if coords:
|
1212 |
+
lat, lon = coords
|
1213 |
+
cities_data.append({
|
1214 |
+
'name': city,
|
1215 |
+
'lat': lat,
|
1216 |
+
'lon': lon,
|
1217 |
+
'forecast': city_data.get('forecast', []),
|
1218 |
+
'current': city_data.get('current', {})
|
1219 |
+
})
|
1220 |
+
|
1221 |
+
return self.map_manager.create_weather_map(cities_data)
|
1222 |
+
except Exception as e:
|
1223 |
+
logger.error(f"Error updating map: {e}")
|
1224 |
+
return self._get_current_map()
|
1225 |
+
|
1226 |
+
def _create_temperature_chart(self, forecast: list) -> go.Figure:
|
1227 |
+
"""Create professional temperature chart with dark theme and emojis"""
|
1228 |
+
if not forecast:
|
1229 |
+
return self._create_default_chart()
|
1230 |
+
|
1231 |
+
try:
|
1232 |
+
dates = []
|
1233 |
+
temps_high = []
|
1234 |
+
temps_low = []
|
1235 |
+
|
1236 |
+
for day in forecast[:7]: # 7-day forecast
|
1237 |
+
dates.append(day.get('name', 'Unknown'))
|
1238 |
+
temps_high.append(day.get('temperature', 0))
|
1239 |
+
# For low temps, we'll use a simple estimation
|
1240 |
+
high_temp = day.get('temperature', 0)
|
1241 |
+
temps_low.append(max(high_temp - 15, high_temp * 0.7))
|
1242 |
+
|
1243 |
+
fig = go.Figure()
|
1244 |
+
|
1245 |
+
# High temperatures with gradient
|
1246 |
+
fig.add_trace(go.Scatter(
|
1247 |
+
x=dates,
|
1248 |
+
y=temps_high,
|
1249 |
+
mode='lines+markers',
|
1250 |
+
name='🌡️ High Temp',
|
1251 |
+
line=dict(color='#ff6b6b', width=3),
|
1252 |
+
marker=dict(size=8, symbol='circle'),
|
1253 |
+
fill=None
|
1254 |
+
))
|
1255 |
+
|
1256 |
+
# Low temperatures with gradient
|
1257 |
+
fig.add_trace(go.Scatter(
|
1258 |
+
x=dates,
|
1259 |
+
y=temps_low,
|
1260 |
+
mode='lines+markers',
|
1261 |
+
name='🧊 Low Temp',
|
1262 |
+
line=dict(color='#4ecdc4', width=3),
|
1263 |
+
marker=dict(size=8, symbol='circle'),
|
1264 |
+
fill='tonexty',
|
1265 |
+
fillcolor='rgba(78, 205, 196, 0.1)'
|
1266 |
+
))
|
1267 |
+
|
1268 |
+
# Professional dark theme styling
|
1269 |
+
fig.update_layout(
|
1270 |
+
title=dict(
|
1271 |
+
text="🌡️ 7-Day Temperature Forecast",
|
1272 |
+
font=dict(size=16, color='white')
|
1273 |
+
),
|
1274 |
+
paper_bgcolor='rgba(0,0,0,0)',
|
1275 |
+
plot_bgcolor='rgba(0,0,0,0.1)',
|
1276 |
+
font=dict(color='white'),
|
1277 |
+
xaxis=dict(
|
1278 |
+
gridcolor='rgba(255,255,255,0.1)',
|
1279 |
+
zerolinecolor='rgba(255,255,255,0.2)'
|
1280 |
+
),
|
1281 |
+
yaxis=dict(
|
1282 |
+
title="Temperature (°F)",
|
1283 |
+
gridcolor='rgba(255,255,255,0.1)',
|
1284 |
+
zerolinecolor='rgba(255,255,255,0.2)'
|
1285 |
+
),
|
1286 |
+
legend=dict(
|
1287 |
+
bgcolor='rgba(0,0,0,0.5)',
|
1288 |
+
bordercolor='rgba(255,255,255,0.2)'
|
1289 |
+
),
|
1290 |
+
hovermode='x unified'
|
1291 |
+
)
|
1292 |
+
|
1293 |
+
return fig
|
1294 |
+
|
1295 |
+
except Exception as e:
|
1296 |
+
logger.error(f"Error creating temperature chart: {e}")
|
1297 |
+
return self._create_default_chart()
|
1298 |
+
|
1299 |
+
def _create_precipitation_chart(self, hourly: list) -> go.Figure:
|
1300 |
+
"""Create professional precipitation chart with dark theme"""
|
1301 |
+
if not hourly:
|
1302 |
+
return self._create_default_chart()
|
1303 |
+
|
1304 |
+
try:
|
1305 |
+
hours = []
|
1306 |
+
precip_prob = []
|
1307 |
+
|
1308 |
+
for hour in hourly[:24]: # 24-hour forecast
|
1309 |
+
time_str = hour.get('startTime', '')
|
1310 |
+
if time_str:
|
1311 |
+
# Extract hour from ISO format
|
1312 |
+
try:
|
1313 |
+
from datetime import datetime
|
1314 |
+
dt = datetime.fromisoformat(time_str.replace('Z', '+00:00'))
|
1315 |
+
hours.append(dt.strftime('%I %p'))
|
1316 |
+
except:
|
1317 |
+
hours.append(f"Hour {len(hours)}")
|
1318 |
+
else:
|
1319 |
+
hours.append(f"Hour {len(hours)}")
|
1320 |
+
|
1321 |
+
# Get precipitation probability
|
1322 |
+
prob = hour.get('probabilityOfPrecipitation', {}).get('value', 0) or 0
|
1323 |
+
precip_prob.append(prob)
|
1324 |
+
|
1325 |
+
fig = go.Figure()
|
1326 |
+
|
1327 |
+
# Precipitation probability bars with gradient
|
1328 |
+
fig.add_trace(go.Bar(
|
1329 |
+
x=hours,
|
1330 |
+
y=precip_prob,
|
1331 |
+
name='🌧️ Rain Probability',
|
1332 |
+
marker=dict(
|
1333 |
+
color=precip_prob,
|
1334 |
+
colorscale='Blues',
|
1335 |
+
colorbar=dict(title="Probability %")
|
1336 |
+
),
|
1337 |
+
hovertemplate='%{x}: %{y}% chance of rain<extra></extra>'
|
1338 |
+
))
|
1339 |
+
|
1340 |
+
# Professional dark theme styling
|
1341 |
+
fig.update_layout(
|
1342 |
+
title=dict(
|
1343 |
+
text="🌧️ 24-Hour Precipitation Forecast",
|
1344 |
+
font=dict(size=16, color='white')
|
1345 |
+
),
|
1346 |
+
paper_bgcolor='rgba(0,0,0,0)',
|
1347 |
+
plot_bgcolor='rgba(0,0,0,0.1)',
|
1348 |
+
font=dict(color='white'),
|
1349 |
+
xaxis=dict(
|
1350 |
+
title="Time",
|
1351 |
+
gridcolor='rgba(255,255,255,0.1)',
|
1352 |
+
zerolinecolor='rgba(255,255,255,0.2)',
|
1353 |
+
tickangle=45
|
1354 |
+
),
|
1355 |
+
yaxis=dict(
|
1356 |
+
title="Precipitation Probability (%)",
|
1357 |
+
gridcolor='rgba(255,255,255,0.1)',
|
1358 |
+
zerolinecolor='rgba(255,255,255,0.2)',
|
1359 |
+
range=[0, 100]
|
1360 |
+
),
|
1361 |
+
showlegend=False
|
1362 |
+
)
|
1363 |
+
|
1364 |
+
return fig
|
1365 |
+
|
1366 |
+
except Exception as e:
|
1367 |
+
logger.error(f"Error creating precipitation chart: {e}")
|
1368 |
+
return self._create_default_chart()
|
1369 |
+
|
1370 |
+
# ...existing code...
|
1371 |
+
def main():
|
1372 |
+
"""Main application entry point"""
|
1373 |
+
print("🌤️ Starting Weather App")
|
1374 |
+
print("🤖 AI Features:", "✅ Enabled" if os.getenv("GEMINI_API_KEY") else "⚠️ Limited (no API key)")
|
1375 |
+
print("📱 App will be available at: http://localhost:7860")
|
1376 |
+
|
1377 |
+
try:
|
1378 |
+
app_instance = WeatherAppProEnhanced()
|
1379 |
+
app = app_instance.create_interface()
|
1380 |
+
|
1381 |
+
app.launch(
|
1382 |
+
server_name="0.0.0.0",
|
1383 |
+
server_port=7860,
|
1384 |
+
share=False,
|
1385 |
+
show_error=True
|
1386 |
+
)
|
1387 |
+
|
1388 |
+
except Exception as e:
|
1389 |
+
logger.error(f"Error starting application: {e}")
|
1390 |
+
print(f"❌ Error starting app: {e}")
|
1391 |
+
|
1392 |
+
if __name__ == "__main__":
|
1393 |
+
main()
|
1394 |
+
|
demo_features.py
ADDED
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#!/usr/bin/env python3
|
2 |
+
"""
|
3 |
+
Enhanced Weather App Demo Script
|
4 |
+
Demonstrates the new conversational features
|
5 |
+
"""
|
6 |
+
|
7 |
+
import asyncio
|
8 |
+
from src.chatbot.nlp_processor import WeatherNLPProcessor
|
9 |
+
from src.api.weather_client import WeatherClient
|
10 |
+
from src.chatbot.enhanced_chatbot import EnhancedWeatherChatbot
|
11 |
+
import os
|
12 |
+
from dotenv import load_dotenv
|
13 |
+
|
14 |
+
load_dotenv()
|
15 |
+
|
16 |
+
async def demo_conversational_features():
|
17 |
+
"""Demo the enhanced conversational features"""
|
18 |
+
|
19 |
+
print("🌤️ Enhanced Weather App Demo - Conversational Features\n")
|
20 |
+
print("=" * 60)
|
21 |
+
|
22 |
+
# Initialize components
|
23 |
+
weather_client = WeatherClient()
|
24 |
+
nlp_processor = WeatherNLPProcessor()
|
25 |
+
gemini_api_key = os.getenv("GEMINI_API_KEY")
|
26 |
+
chatbot = EnhancedWeatherChatbot(weather_client, nlp_processor, gemini_api_key)
|
27 |
+
|
28 |
+
# Demo queries to test
|
29 |
+
demo_queries = [
|
30 |
+
{
|
31 |
+
"category": "🚴♂️ Activity Advice",
|
32 |
+
"queries": [
|
33 |
+
"Should I go biking in Seattle?",
|
34 |
+
"Is it good weather for walking in New York?",
|
35 |
+
"Can I have a picnic in Austin today?"
|
36 |
+
]
|
37 |
+
},
|
38 |
+
{
|
39 |
+
"category": "📅 Forecast Queries",
|
40 |
+
"queries": [
|
41 |
+
"What's the forecast for Miami this week?",
|
42 |
+
"Will it rain in Portland tomorrow?",
|
43 |
+
"Hourly weather for Chicago today"
|
44 |
+
]
|
45 |
+
},
|
46 |
+
{
|
47 |
+
"category": "📊 Historical Comparisons",
|
48 |
+
"queries": [
|
49 |
+
"How does today's weather in Phoenix compare to normal?",
|
50 |
+
"Is this typical weather for Boston?",
|
51 |
+
"Temperature trends for Denver"
|
52 |
+
]
|
53 |
+
},
|
54 |
+
{
|
55 |
+
"category": "🌍 Multi-City Comparisons",
|
56 |
+
"queries": [
|
57 |
+
"Compare biking conditions between Portland and Seattle",
|
58 |
+
"Weather differences between Miami and Chicago",
|
59 |
+
"Which city is better for outdoor events: Austin or Dallas?"
|
60 |
+
]
|
61 |
+
}
|
62 |
+
]
|
63 |
+
|
64 |
+
for category_info in demo_queries:
|
65 |
+
print(f"\n{category_info['category']}")
|
66 |
+
print("-" * 40)
|
67 |
+
|
68 |
+
for query in category_info['queries']:
|
69 |
+
print(f"\n💬 Query: \"{query}\"")
|
70 |
+
print("🔍 Analyzing...")
|
71 |
+
|
72 |
+
# Process the query with NLP analysis
|
73 |
+
analysis = nlp_processor.process_query(query)
|
74 |
+
|
75 |
+
print(f" 📍 Cities detected: {analysis.get('cities', [])}")
|
76 |
+
print(f" 🎯 Query type: {analysis.get('query_type', 'general')}")
|
77 |
+
print(f" 🏃♂️ Activity context: {analysis.get('activity_context', {}).get('has_activity', False)}")
|
78 |
+
print(f" 📅 Forecast intent: {analysis.get('forecast_context', {}).get('has_forecast_intent', False)}")
|
79 |
+
print(f" 📊 Historical intent: {analysis.get('historical_context', {}).get('has_historical_intent', False)}")
|
80 |
+
|
81 |
+
if analysis.get('activity_context', {}).get('has_activity'):
|
82 |
+
activity_type = analysis.get('activity_context', {}).get('activity_type')
|
83 |
+
print(f" 🎪 Activity type: {activity_type}")
|
84 |
+
|
85 |
+
if gemini_api_key:
|
86 |
+
try:
|
87 |
+
# Get AI response (would need actual weather data in real demo)
|
88 |
+
print(" 🤖 AI response would provide contextual advice based on current weather data")
|
89 |
+
except Exception as e:
|
90 |
+
print(f" ⚠️ AI processing: {str(e)}")
|
91 |
+
else:
|
92 |
+
print(" ℹ️ Add GEMINI_API_KEY to .env for full AI responses")
|
93 |
+
|
94 |
+
print("\n" + "=" * 60)
|
95 |
+
print("✅ Demo completed! Run the app with 'python enhanced_main.py' to try these features interactively.")
|
96 |
+
print("🌐 App will be available at: http://localhost:7860")
|
97 |
+
print("\n📖 For more examples, see CONVERSATIONAL_FEATURES.md")
|
98 |
+
|
99 |
+
if __name__ == "__main__":
|
100 |
+
asyncio.run(demo_conversational_features())
|
hf_app.py
ADDED
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
Hugging Face Spaces Entry Point for Weather App Pro Enhanced
|
3 |
+
"""
|
4 |
+
|
5 |
+
import os
|
6 |
+
import sys
|
7 |
+
import logging
|
8 |
+
|
9 |
+
# Set up environment for Hugging Face Spaces
|
10 |
+
os.environ['GRADIO_SERVER_NAME'] = '0.0.0.0'
|
11 |
+
os.environ['GRADIO_SERVER_PORT'] = '7860'
|
12 |
+
|
13 |
+
# Add src to path for imports
|
14 |
+
sys.path.append(os.path.join(os.path.dirname(__file__), 'src'))
|
15 |
+
|
16 |
+
# Configure logging for Hugging Face Spaces
|
17 |
+
logging.basicConfig(
|
18 |
+
level=logging.INFO,
|
19 |
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
20 |
+
)
|
21 |
+
|
22 |
+
def main():
|
23 |
+
"""Main entry point for Hugging Face Spaces"""
|
24 |
+
try:
|
25 |
+
# Import and run the enhanced weather app
|
26 |
+
from app import WeatherAppProEnhanced
|
27 |
+
|
28 |
+
print("🌤️ Starting Weather App Pro Enhanced for Hugging Face Spaces...")
|
29 |
+
print("🤖 AI Features:", "✅ Enabled" if os.getenv("GEMINI_API_KEY") else "⚠️ Limited (set GEMINI_API_KEY secret)")
|
30 |
+
|
31 |
+
# Create app instance
|
32 |
+
app_instance = WeatherAppProEnhanced()
|
33 |
+
app = app_instance.create_interface()
|
34 |
+
|
35 |
+
# Launch for Hugging Face Spaces
|
36 |
+
app.launch(
|
37 |
+
server_name="0.0.0.0",
|
38 |
+
server_port=7860,
|
39 |
+
share=False,
|
40 |
+
show_error=True,
|
41 |
+
enable_queue=True
|
42 |
+
)
|
43 |
+
|
44 |
+
except Exception as e:
|
45 |
+
logging.error(f"Error starting Weather App: {e}")
|
46 |
+
print(f"❌ Error: {e}")
|
47 |
+
|
48 |
+
# Fallback to basic app if enhanced fails
|
49 |
+
try:
|
50 |
+
print("🔄 Falling back to basic weather app...")
|
51 |
+
from app import WeatherAppPro
|
52 |
+
|
53 |
+
basic_app = WeatherAppPro()
|
54 |
+
basic_interface = basic_app.create_interface()
|
55 |
+
|
56 |
+
basic_interface.launch(
|
57 |
+
server_name="0.0.0.0",
|
58 |
+
server_port=7860,
|
59 |
+
share=False,
|
60 |
+
show_error=True,
|
61 |
+
enable_queue=True
|
62 |
+
)
|
63 |
+
|
64 |
+
except Exception as fallback_error:
|
65 |
+
logging.error(f"Fallback app also failed: {fallback_error}")
|
66 |
+
print(f"❌ Fallback error: {fallback_error}")
|
67 |
+
|
68 |
+
if __name__ == "__main__":
|
69 |
+
main()
|
requirements.txt
ADDED
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
gradio>=4.0.0
|
2 |
+
requests>=2.25.0
|
3 |
+
folium>=0.14.0
|
4 |
+
plotly>=5.0.0
|
5 |
+
pandas>=1.3.0
|
6 |
+
numpy>=1.21.0
|
7 |
+
python-dateutil>=2.8.0
|
8 |
+
scikit-learn>=1.0.0
|
9 |
+
google-generativeai>=0.8.0
|
10 |
+
anthropic>=0.52.0
|
11 |
+
openai>=1.0.0
|
12 |
+
llama-index>=0.12.0
|
13 |
+
llama-index-llms-gemini>=0.3.0
|
14 |
+
llama-index-embeddings-gemini>=0.3.0
|
15 |
+
python-dotenv>=1.0.0
|
16 |
+
asyncio
|
17 |
+
|
run.bat
ADDED
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
@echo off
|
2 |
+
REM Weather App Pro - Quick Start Script for Windows
|
3 |
+
REM This script sets up and runs the Weather App Pro
|
4 |
+
|
5 |
+
echo 🌤️ Weather App Pro - Quick Start
|
6 |
+
echo ==================================
|
7 |
+
echo.
|
8 |
+
|
9 |
+
REM Check if Python is installed
|
10 |
+
python --version >nul 2>&1
|
11 |
+
if errorlevel 1 (
|
12 |
+
echo ❌ Python is required but not installed.
|
13 |
+
echo Please install Python and try again.
|
14 |
+
pause
|
15 |
+
exit /b 1
|
16 |
+
)
|
17 |
+
|
18 |
+
echo ✅ Python found
|
19 |
+
|
20 |
+
REM Check if pip is installed
|
21 |
+
pip --version >nul 2>&1
|
22 |
+
if errorlevel 1 (
|
23 |
+
echo ❌ pip is required but not installed.
|
24 |
+
echo Please install pip and try again.
|
25 |
+
pause
|
26 |
+
exit /b 1
|
27 |
+
)
|
28 |
+
|
29 |
+
echo ✅ pip found
|
30 |
+
|
31 |
+
REM Create virtual environment (optional but recommended)
|
32 |
+
echo 🔧 Setting up virtual environment...
|
33 |
+
python -m venv weather_env
|
34 |
+
|
35 |
+
REM Activate virtual environment
|
36 |
+
call weather_env\Scripts\activate.bat
|
37 |
+
|
38 |
+
echo ✅ Virtual environment activated
|
39 |
+
|
40 |
+
REM Install dependencies
|
41 |
+
echo 📦 Installing dependencies...
|
42 |
+
pip install -r requirements.txt
|
43 |
+
|
44 |
+
if errorlevel 1 (
|
45 |
+
echo ❌ Failed to install dependencies
|
46 |
+
pause
|
47 |
+
exit /b 1
|
48 |
+
)
|
49 |
+
|
50 |
+
echo ✅ Dependencies installed successfully
|
51 |
+
echo.
|
52 |
+
echo 🚀 Starting Weather App Pro...
|
53 |
+
echo 📱 The app will be available at: http://localhost:7860
|
54 |
+
echo 🌟 Press Ctrl+C to stop the application
|
55 |
+
echo.
|
56 |
+
|
57 |
+
REM Run the application
|
58 |
+
python app.py
|
59 |
+
|
60 |
+
pause
|
61 |
+
|
run.sh
ADDED
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#!/bin/bash
|
2 |
+
|
3 |
+
# Weather App Pro - Quick Start Script
|
4 |
+
# This script sets up and runs the Weather App Pro
|
5 |
+
|
6 |
+
echo "🌤️ Weather App Pro - Quick Start"
|
7 |
+
echo "=================================="
|
8 |
+
echo ""
|
9 |
+
|
10 |
+
# Check if Python is installed
|
11 |
+
if ! command -v python3 &> /dev/null; then
|
12 |
+
echo "❌ Python 3 is required but not installed."
|
13 |
+
echo "Please install Python 3 and try again."
|
14 |
+
exit 1
|
15 |
+
fi
|
16 |
+
|
17 |
+
echo "✅ Python 3 found"
|
18 |
+
|
19 |
+
# Check if pip is installed
|
20 |
+
if ! command -v pip3 &> /dev/null; then
|
21 |
+
echo "❌ pip3 is required but not installed."
|
22 |
+
echo "Please install pip3 and try again."
|
23 |
+
exit 1
|
24 |
+
fi
|
25 |
+
|
26 |
+
echo "✅ pip3 found"
|
27 |
+
|
28 |
+
# Create virtual environment (optional but recommended)
|
29 |
+
echo "🔧 Setting up virtual environment..."
|
30 |
+
python3 -m venv weather_env
|
31 |
+
|
32 |
+
# Activate virtual environment
|
33 |
+
if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "win32" ]]; then
|
34 |
+
# Windows
|
35 |
+
source weather_env/Scripts/activate
|
36 |
+
else
|
37 |
+
# Linux/Mac
|
38 |
+
source weather_env/bin/activate
|
39 |
+
fi
|
40 |
+
|
41 |
+
echo "✅ Virtual environment activated"
|
42 |
+
|
43 |
+
# Install dependencies
|
44 |
+
echo "📦 Installing dependencies..."
|
45 |
+
pip3 install -r requirements.txt
|
46 |
+
|
47 |
+
if [ $? -eq 0 ]; then
|
48 |
+
echo "✅ Dependencies installed successfully"
|
49 |
+
else
|
50 |
+
echo "❌ Failed to install dependencies"
|
51 |
+
exit 1
|
52 |
+
fi
|
53 |
+
|
54 |
+
echo ""
|
55 |
+
echo "🚀 Starting Weather App Pro..."
|
56 |
+
echo "📱 The app will be available at: http://localhost:7860"
|
57 |
+
echo "🌟 Press Ctrl+C to stop the application"
|
58 |
+
echo ""
|
59 |
+
|
60 |
+
# Run the application
|
61 |
+
python3 app.py
|
62 |
+
|
run_enhanced.sh
ADDED
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#!/bin/bash
|
2 |
+
|
3 |
+
# Weather App Pro Enhanced - Quick Start Script
|
4 |
+
# This script sets up and runs the Enhanced Weather App Pro with AI
|
5 |
+
|
6 |
+
echo "🌤️ Weather App Pro Enhanced - Quick Start"
|
7 |
+
echo "=========================================="
|
8 |
+
echo ""
|
9 |
+
|
10 |
+
# Check if Python is installed
|
11 |
+
if ! command -v python3 &> /dev/null; then
|
12 |
+
echo "❌ Python 3 is required but not installed."
|
13 |
+
echo "Please install Python 3 and try again."
|
14 |
+
exit 1
|
15 |
+
fi
|
16 |
+
|
17 |
+
echo "✅ Python 3 found"
|
18 |
+
|
19 |
+
# Check if pip is installed
|
20 |
+
if ! command -v pip3 &> /dev/null; then
|
21 |
+
echo "❌ pip3 is required but not installed."
|
22 |
+
echo "Please install pip3 and try again."
|
23 |
+
exit 1
|
24 |
+
fi
|
25 |
+
|
26 |
+
echo "✅ pip3 found"
|
27 |
+
|
28 |
+
# Check for Gemini API key
|
29 |
+
if [ -z "$GEMINI_API_KEY" ]; then
|
30 |
+
echo "⚠️ GEMINI_API_KEY not found in environment variables."
|
31 |
+
echo "💡 For full AI features, set it with:"
|
32 |
+
echo " export GEMINI_API_KEY='your-api-key'"
|
33 |
+
echo "🔄 App will work in basic mode without AI features."
|
34 |
+
echo ""
|
35 |
+
fi
|
36 |
+
|
37 |
+
# Create virtual environment (optional but recommended)
|
38 |
+
echo "🔧 Setting up virtual environment..."
|
39 |
+
python3 -m venv weather_env
|
40 |
+
|
41 |
+
# Activate virtual environment
|
42 |
+
if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "win32" ]]; then
|
43 |
+
# Windows
|
44 |
+
source weather_env/Scripts/activate
|
45 |
+
else
|
46 |
+
# Linux/Mac
|
47 |
+
source weather_env/bin/activate
|
48 |
+
fi
|
49 |
+
|
50 |
+
echo "✅ Virtual environment activated"
|
51 |
+
|
52 |
+
# Install dependencies
|
53 |
+
echo "📦 Installing dependencies (this may take a few minutes)..."
|
54 |
+
pip3 install -r requirements.txt
|
55 |
+
|
56 |
+
if [ $? -eq 0 ]; then
|
57 |
+
echo "✅ Dependencies installed successfully"
|
58 |
+
else
|
59 |
+
echo "❌ Failed to install dependencies"
|
60 |
+
exit 1
|
61 |
+
fi
|
62 |
+
|
63 |
+
echo ""
|
64 |
+
echo "🚀 Starting Weather App Pro Enhanced..."
|
65 |
+
echo "🤖 AI Features: LlamaIndex + Gemini integration"
|
66 |
+
echo "📱 The app will be available at: http://localhost:7860"
|
67 |
+
echo "🌟 Press Ctrl+C to stop the application"
|
68 |
+
echo ""
|
69 |
+
|
70 |
+
# Run the enhanced application
|
71 |
+
python3 enhanced_main.py
|
72 |
+
|
src/__init__.py
ADDED
File without changes
|
src/ai/__init__.py
ADDED
File without changes
|
src/ai/multi_provider.py
ADDED
@@ -0,0 +1,170 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
Multi-Provider AI System for Weather App
|
3 |
+
Supports OpenAI, Google Gemini, and Anthropic Claude
|
4 |
+
"""
|
5 |
+
|
6 |
+
import os
|
7 |
+
import logging
|
8 |
+
from typing import Dict, List, Optional, Any
|
9 |
+
from enum import Enum
|
10 |
+
import asyncio
|
11 |
+
|
12 |
+
# Import AI clients
|
13 |
+
try:
|
14 |
+
import openai
|
15 |
+
OPENAI_AVAILABLE = True
|
16 |
+
except ImportError:
|
17 |
+
OPENAI_AVAILABLE = False
|
18 |
+
|
19 |
+
try:
|
20 |
+
import google.generativeai as genai
|
21 |
+
GEMINI_AVAILABLE = True
|
22 |
+
except ImportError:
|
23 |
+
GEMINI_AVAILABLE = False
|
24 |
+
|
25 |
+
try:
|
26 |
+
import anthropic
|
27 |
+
CLAUDE_AVAILABLE = True
|
28 |
+
except ImportError:
|
29 |
+
CLAUDE_AVAILABLE = False
|
30 |
+
|
31 |
+
logger = logging.getLogger(__name__)
|
32 |
+
|
33 |
+
class AIProvider(Enum):
|
34 |
+
"""Available AI providers"""
|
35 |
+
OPENAI = "openai"
|
36 |
+
GEMINI = "gemini"
|
37 |
+
CLAUDE = "claude"
|
38 |
+
|
39 |
+
class AIProviderManager:
|
40 |
+
"""Manages multiple AI providers with fallback"""
|
41 |
+
|
42 |
+
def __init__(self,
|
43 |
+
openai_api_key: str = None,
|
44 |
+
gemini_api_key: str = None,
|
45 |
+
claude_api_key: str = None,
|
46 |
+
default_provider: AIProvider = AIProvider.OPENAI):
|
47 |
+
"""Initialize AI provider manager"""
|
48 |
+
self.providers = {}
|
49 |
+
self.available_providers = []
|
50 |
+
self.default_provider = default_provider
|
51 |
+
|
52 |
+
# Configure OpenAI
|
53 |
+
if OPENAI_AVAILABLE and (openai_api_key or os.getenv("OPENAI_API_KEY")):
|
54 |
+
try:
|
55 |
+
self.providers[AIProvider.OPENAI] = openai.OpenAI(
|
56 |
+
api_key=openai_api_key or os.getenv("OPENAI_API_KEY")
|
57 |
+
)
|
58 |
+
self.available_providers.append(AIProvider.OPENAI)
|
59 |
+
logger.info("OpenAI configured successfully")
|
60 |
+
except Exception as e:
|
61 |
+
logger.error(f"Error configuring OpenAI: {e}")
|
62 |
+
|
63 |
+
# Configure Gemini
|
64 |
+
if GEMINI_AVAILABLE and (gemini_api_key or os.getenv("GEMINI_API_KEY")):
|
65 |
+
try:
|
66 |
+
genai.configure(api_key=gemini_api_key or os.getenv("GEMINI_API_KEY"))
|
67 |
+
self.providers[AIProvider.GEMINI] = genai.GenerativeModel('gemini-2.0-flash-exp')
|
68 |
+
self.available_providers.append(AIProvider.GEMINI)
|
69 |
+
logger.info("Gemini 2.0 Flash configured successfully")
|
70 |
+
except Exception as e:
|
71 |
+
logger.error(f"Error configuring Gemini: {e}")
|
72 |
+
|
73 |
+
# Configure Claude
|
74 |
+
if CLAUDE_AVAILABLE and (claude_api_key or os.getenv("CLAUDE_API_KEY")):
|
75 |
+
try:
|
76 |
+
self.providers[AIProvider.CLAUDE] = anthropic.Anthropic(
|
77 |
+
api_key=claude_api_key or os.getenv("CLAUDE_API_KEY")
|
78 |
+
)
|
79 |
+
self.available_providers.append(AIProvider.CLAUDE)
|
80 |
+
logger.info("Claude configured successfully")
|
81 |
+
except Exception as e:
|
82 |
+
logger.error(f"Error configuring Claude: {e}")
|
83 |
+
|
84 |
+
if not self.available_providers:
|
85 |
+
logger.warning("No AI providers configured. Using basic mode.")
|
86 |
+
else:
|
87 |
+
if self.default_provider not in self.available_providers:
|
88 |
+
self.default_provider = self.available_providers[0]
|
89 |
+
|
90 |
+
def get_available_providers(self) -> List[AIProvider]:
|
91 |
+
"""Get list of available providers"""
|
92 |
+
return self.available_providers
|
93 |
+
|
94 |
+
async def generate_response(self, prompt: str, provider: AIProvider = None) -> str:
|
95 |
+
"""Generate response using specified or default provider"""
|
96 |
+
if not self.available_providers:
|
97 |
+
return self._generate_basic_response(prompt)
|
98 |
+
|
99 |
+
provider = provider or self.default_provider
|
100 |
+
|
101 |
+
try:
|
102 |
+
if provider == AIProvider.OPENAI and provider in self.providers:
|
103 |
+
return await self._generate_openai_response(prompt)
|
104 |
+
elif provider == AIProvider.GEMINI and provider in self.providers:
|
105 |
+
return await self._generate_gemini_response(prompt)
|
106 |
+
elif provider == AIProvider.CLAUDE and provider in self.providers:
|
107 |
+
return await self._generate_claude_response(prompt)
|
108 |
+
else:
|
109 |
+
# Fallback to first available provider
|
110 |
+
if self.available_providers:
|
111 |
+
return await self.generate_response(prompt, self.available_providers[0])
|
112 |
+
else:
|
113 |
+
return self._generate_basic_response(prompt)
|
114 |
+
except Exception as e:
|
115 |
+
logger.error(f"Error generating response with {provider.value}: {e}")
|
116 |
+
# Try fallback providers
|
117 |
+
for fallback_provider in self.available_providers:
|
118 |
+
if fallback_provider != provider:
|
119 |
+
try:
|
120 |
+
return await self.generate_response(prompt, fallback_provider)
|
121 |
+
except:
|
122 |
+
continue
|
123 |
+
return self._generate_basic_response(prompt)
|
124 |
+
|
125 |
+
async def _generate_openai_response(self, prompt: str) -> str:
|
126 |
+
"""Generate response using OpenAI"""
|
127 |
+
client = self.providers[AIProvider.OPENAI]
|
128 |
+
response = await client.chat.completions.acreate(
|
129 |
+
model="gpt-3.5-turbo",
|
130 |
+
messages=[{"role": "user", "content": prompt}],
|
131 |
+
max_tokens=500,
|
132 |
+
temperature=0.7
|
133 |
+
)
|
134 |
+
return response.choices[0].message.content
|
135 |
+
|
136 |
+
async def _generate_gemini_response(self, prompt: str) -> str:
|
137 |
+
"""Generate response using Gemini"""
|
138 |
+
model = self.providers[AIProvider.GEMINI]
|
139 |
+
response = await model.generate_content_async(prompt)
|
140 |
+
return response.text
|
141 |
+
|
142 |
+
async def _generate_claude_response(self, prompt: str) -> str:
|
143 |
+
"""Generate response using Claude"""
|
144 |
+
client = self.providers[AIProvider.CLAUDE]
|
145 |
+
response = await client.messages.acreate(
|
146 |
+
model="claude-3-sonnet-20240229",
|
147 |
+
max_tokens=500,
|
148 |
+
messages=[{"role": "user", "content": prompt}]
|
149 |
+
)
|
150 |
+
return response.content[0].text
|
151 |
+
|
152 |
+
def _generate_basic_response(self, prompt: str) -> str:
|
153 |
+
"""Generate basic response when no AI providers available"""
|
154 |
+
prompt_lower = prompt.lower()
|
155 |
+
|
156 |
+
if "temperature" in prompt_lower:
|
157 |
+
return "I can help you find temperature information. Please specify a city."
|
158 |
+
elif "rain" in prompt_lower or "precipitation" in prompt_lower:
|
159 |
+
return "I can provide precipitation data. Please specify a location."
|
160 |
+
elif "wind" in prompt_lower:
|
161 |
+
return "I can show wind information. Please specify a city."
|
162 |
+
elif "compare" in prompt_lower:
|
163 |
+
return "I can compare weather between cities. Please specify the cities you'd like to compare."
|
164 |
+
else:
|
165 |
+
return "I'm a weather assistant. Ask me about temperature, precipitation, wind, or weather comparisons for US cities."
|
166 |
+
|
167 |
+
def create_ai_provider_manager(**kwargs) -> AIProviderManager:
|
168 |
+
"""Factory function to create AI provider manager"""
|
169 |
+
return AIProviderManager(**kwargs)
|
170 |
+
|
src/analysis/__init__.py
ADDED
File without changes
|
src/analysis/climate_analyzer.py
ADDED
@@ -0,0 +1,380 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
Advanced Climate Analysis System
|
3 |
+
Historical data analysis, trend detection, and anomaly identification
|
4 |
+
"""
|
5 |
+
|
6 |
+
import pandas as pd
|
7 |
+
import numpy as np
|
8 |
+
from datetime import datetime, timedelta
|
9 |
+
from typing import List, Dict, Tuple, Optional
|
10 |
+
import logging
|
11 |
+
from sklearn.linear_model import LinearRegression
|
12 |
+
from sklearn.preprocessing import StandardScaler
|
13 |
+
from sklearn.cluster import KMeans
|
14 |
+
import json
|
15 |
+
|
16 |
+
logger = logging.getLogger(__name__)
|
17 |
+
|
18 |
+
class ClimateAnalyzer:
|
19 |
+
"""Advanced climate analysis with machine learning capabilities"""
|
20 |
+
|
21 |
+
def __init__(self, weather_client):
|
22 |
+
self.weather_client = weather_client
|
23 |
+
self.scaler = StandardScaler()
|
24 |
+
|
25 |
+
def analyze_temperature_trends(self, city: str, days: int = 30) -> Dict:
|
26 |
+
"""Analyze temperature trends for a city"""
|
27 |
+
try:
|
28 |
+
coords = self.weather_client.geocode_location(city)
|
29 |
+
if not coords:
|
30 |
+
return {'error': f'City {city} not found'}
|
31 |
+
|
32 |
+
lat, lon = coords
|
33 |
+
|
34 |
+
# Get historical data (simulated for demo)
|
35 |
+
historical_data = self._generate_historical_data(lat, lon, days)
|
36 |
+
|
37 |
+
if not historical_data:
|
38 |
+
return {'error': 'No historical data available'}
|
39 |
+
|
40 |
+
# Convert to DataFrame
|
41 |
+
df = pd.DataFrame(historical_data)
|
42 |
+
df['date'] = pd.to_datetime(df['date'])
|
43 |
+
df = df.sort_values('date')
|
44 |
+
|
45 |
+
# Calculate trends
|
46 |
+
X = np.arange(len(df)).reshape(-1, 1)
|
47 |
+
y = df['temperature'].values
|
48 |
+
|
49 |
+
model = LinearRegression()
|
50 |
+
model.fit(X, y)
|
51 |
+
|
52 |
+
trend_slope = model.coef_[0]
|
53 |
+
trend_direction = 'increasing' if trend_slope > 0 else 'decreasing'
|
54 |
+
|
55 |
+
# Calculate statistics
|
56 |
+
stats = {
|
57 |
+
'mean_temp': float(np.mean(y)),
|
58 |
+
'max_temp': float(np.max(y)),
|
59 |
+
'min_temp': float(np.min(y)),
|
60 |
+
'std_temp': float(np.std(y)),
|
61 |
+
'trend_slope': float(trend_slope),
|
62 |
+
'trend_direction': trend_direction,
|
63 |
+
'data_points': len(df)
|
64 |
+
}
|
65 |
+
|
66 |
+
# Detect anomalies
|
67 |
+
anomalies = self._detect_temperature_anomalies(df)
|
68 |
+
|
69 |
+
return {
|
70 |
+
'city': city,
|
71 |
+
'analysis_period': f'{days} days',
|
72 |
+
'statistics': stats,
|
73 |
+
'anomalies': anomalies,
|
74 |
+
'trend_analysis': {
|
75 |
+
'slope': trend_slope,
|
76 |
+
'direction': trend_direction,
|
77 |
+
'significance': 'significant' if abs(trend_slope) > 0.1 else 'minimal'
|
78 |
+
}
|
79 |
+
}
|
80 |
+
|
81 |
+
except Exception as e:
|
82 |
+
logger.error(f"Error analyzing temperature trends: {e}")
|
83 |
+
return {'error': str(e)}
|
84 |
+
|
85 |
+
def compare_cities_climate(self, cities: List[str], metric: str = 'temperature') -> Dict:
|
86 |
+
"""Compare climate metrics between multiple cities"""
|
87 |
+
try:
|
88 |
+
results = {}
|
89 |
+
|
90 |
+
for city in cities:
|
91 |
+
coords = self.weather_client.geocode_location(city)
|
92 |
+
if coords:
|
93 |
+
lat, lon = coords
|
94 |
+
forecast = self.weather_client.get_forecast(lat, lon)
|
95 |
+
|
96 |
+
if forecast:
|
97 |
+
current = forecast[0]
|
98 |
+
results[city] = {
|
99 |
+
'temperature': current.get('temperature', 0),
|
100 |
+
'precipitation_prob': current.get('precipitationProbability', 0),
|
101 |
+
'wind_speed': self._extract_wind_speed(current.get('windSpeed', '0 mph')),
|
102 |
+
'conditions': current.get('shortForecast', 'Unknown')
|
103 |
+
}
|
104 |
+
|
105 |
+
if not results:
|
106 |
+
return {'error': 'No data available for comparison'}
|
107 |
+
|
108 |
+
# Perform comparison analysis
|
109 |
+
comparison = self._analyze_city_differences(results, metric)
|
110 |
+
|
111 |
+
return {
|
112 |
+
'cities': list(results.keys()),
|
113 |
+
'metric': metric,
|
114 |
+
'data': results,
|
115 |
+
'comparison': comparison
|
116 |
+
}
|
117 |
+
|
118 |
+
except Exception as e:
|
119 |
+
logger.error(f"Error comparing cities: {e}")
|
120 |
+
return {'error': str(e)}
|
121 |
+
|
122 |
+
def detect_weather_patterns(self, city: str, pattern_type: str = 'seasonal') -> Dict:
|
123 |
+
"""Detect weather patterns and cycles"""
|
124 |
+
try:
|
125 |
+
coords = self.weather_client.geocode_location(city)
|
126 |
+
if not coords:
|
127 |
+
return {'error': f'City {city} not found'}
|
128 |
+
|
129 |
+
lat, lon = coords
|
130 |
+
|
131 |
+
# Get extended forecast for pattern analysis
|
132 |
+
forecast = self.weather_client.get_forecast(lat, lon)
|
133 |
+
hourly = self.weather_client.get_hourly_forecast(lat, lon, 168) # 7 days
|
134 |
+
|
135 |
+
if not forecast or not hourly:
|
136 |
+
return {'error': 'Insufficient data for pattern analysis'}
|
137 |
+
|
138 |
+
patterns = {}
|
139 |
+
|
140 |
+
if pattern_type == 'seasonal':
|
141 |
+
patterns = self._analyze_seasonal_patterns(forecast, hourly)
|
142 |
+
elif pattern_type == 'daily':
|
143 |
+
patterns = self._analyze_daily_patterns(hourly)
|
144 |
+
elif pattern_type == 'precipitation':
|
145 |
+
patterns = self._analyze_precipitation_patterns(forecast, hourly)
|
146 |
+
|
147 |
+
return {
|
148 |
+
'city': city,
|
149 |
+
'pattern_type': pattern_type,
|
150 |
+
'patterns': patterns,
|
151 |
+
'confidence': self._calculate_pattern_confidence(patterns)
|
152 |
+
}
|
153 |
+
|
154 |
+
except Exception as e:
|
155 |
+
logger.error(f"Error detecting weather patterns: {e}")
|
156 |
+
return {'error': str(e)}
|
157 |
+
|
158 |
+
def predict_weather_anomalies(self, city: str, days_ahead: int = 7) -> Dict:
|
159 |
+
"""Predict potential weather anomalies"""
|
160 |
+
try:
|
161 |
+
coords = self.weather_client.geocode_location(city)
|
162 |
+
if not coords:
|
163 |
+
return {'error': f'City {city} not found'}
|
164 |
+
|
165 |
+
lat, lon = coords
|
166 |
+
forecast = self.weather_client.get_forecast(lat, lon)
|
167 |
+
|
168 |
+
if not forecast:
|
169 |
+
return {'error': 'No forecast data available'}
|
170 |
+
|
171 |
+
# Analyze forecast for anomalies
|
172 |
+
anomalies = []
|
173 |
+
|
174 |
+
for i, period in enumerate(forecast[:days_ahead]):
|
175 |
+
temp = period.get('temperature', 0)
|
176 |
+
precip = period.get('precipitationProbability', 0)
|
177 |
+
wind_speed = self._extract_wind_speed(period.get('windSpeed', '0 mph'))
|
178 |
+
|
179 |
+
# Check for temperature anomalies
|
180 |
+
if temp > 100 or temp < -20: # Extreme temperatures
|
181 |
+
anomalies.append({
|
182 |
+
'type': 'extreme_temperature',
|
183 |
+
'period': period.get('name'),
|
184 |
+
'value': temp,
|
185 |
+
'severity': 'high' if temp > 110 or temp < -30 else 'moderate'
|
186 |
+
})
|
187 |
+
|
188 |
+
# Check for precipitation anomalies
|
189 |
+
if precip > 80: # High precipitation probability
|
190 |
+
anomalies.append({
|
191 |
+
'type': 'high_precipitation',
|
192 |
+
'period': period.get('name'),
|
193 |
+
'value': precip,
|
194 |
+
'severity': 'high' if precip > 90 else 'moderate'
|
195 |
+
})
|
196 |
+
|
197 |
+
# Check for wind anomalies
|
198 |
+
if wind_speed > 25: # High wind speeds
|
199 |
+
anomalies.append({
|
200 |
+
'type': 'high_wind',
|
201 |
+
'period': period.get('name'),
|
202 |
+
'value': wind_speed,
|
203 |
+
'severity': 'high' if wind_speed > 40 else 'moderate'
|
204 |
+
})
|
205 |
+
|
206 |
+
return {
|
207 |
+
'city': city,
|
208 |
+
'prediction_period': f'{days_ahead} days',
|
209 |
+
'anomalies_detected': len(anomalies),
|
210 |
+
'anomalies': anomalies,
|
211 |
+
'risk_level': self._calculate_risk_level(anomalies)
|
212 |
+
}
|
213 |
+
|
214 |
+
except Exception as e:
|
215 |
+
logger.error(f"Error predicting anomalies: {e}")
|
216 |
+
return {'error': str(e)}
|
217 |
+
|
218 |
+
def _generate_historical_data(self, lat: float, lon: float, days: int) -> List[Dict]:
|
219 |
+
"""Generate simulated historical data for analysis"""
|
220 |
+
# In a real implementation, this would fetch actual historical data
|
221 |
+
data = []
|
222 |
+
base_temp = 70 # Base temperature
|
223 |
+
|
224 |
+
for i in range(days):
|
225 |
+
date = datetime.now() - timedelta(days=days-i)
|
226 |
+
# Add seasonal variation and random noise
|
227 |
+
seasonal_factor = 10 * np.sin(2 * np.pi * date.timetuple().tm_yday / 365)
|
228 |
+
noise = np.random.normal(0, 5)
|
229 |
+
temp = base_temp + seasonal_factor + noise
|
230 |
+
|
231 |
+
data.append({
|
232 |
+
'date': date.strftime('%Y-%m-%d'),
|
233 |
+
'temperature': round(temp, 1),
|
234 |
+
'humidity': round(50 + np.random.normal(0, 15), 1),
|
235 |
+
'precipitation': round(max(0, np.random.exponential(0.1)), 2)
|
236 |
+
})
|
237 |
+
|
238 |
+
return data
|
239 |
+
|
240 |
+
def _detect_temperature_anomalies(self, df: pd.DataFrame) -> List[Dict]:
|
241 |
+
"""Detect temperature anomalies using statistical methods"""
|
242 |
+
temps = df['temperature'].values
|
243 |
+
mean_temp = np.mean(temps)
|
244 |
+
std_temp = np.std(temps)
|
245 |
+
|
246 |
+
anomalies = []
|
247 |
+
threshold = 2 * std_temp # 2 standard deviations
|
248 |
+
|
249 |
+
for idx, temp in enumerate(temps):
|
250 |
+
if abs(temp - mean_temp) > threshold:
|
251 |
+
anomalies.append({
|
252 |
+
'date': df.iloc[idx]['date'].strftime('%Y-%m-%d'),
|
253 |
+
'temperature': temp,
|
254 |
+
'deviation': abs(temp - mean_temp),
|
255 |
+
'type': 'hot' if temp > mean_temp else 'cold'
|
256 |
+
})
|
257 |
+
|
258 |
+
return anomalies
|
259 |
+
|
260 |
+
def _analyze_city_differences(self, results: Dict, metric: str) -> Dict:
|
261 |
+
"""Analyze differences between cities for a specific metric"""
|
262 |
+
values = [data[metric] for data in results.values()]
|
263 |
+
cities = list(results.keys())
|
264 |
+
|
265 |
+
max_val = max(values)
|
266 |
+
min_val = min(values)
|
267 |
+
avg_val = np.mean(values)
|
268 |
+
|
269 |
+
max_city = cities[values.index(max_val)]
|
270 |
+
min_city = cities[values.index(min_val)]
|
271 |
+
|
272 |
+
return {
|
273 |
+
'highest': {'city': max_city, 'value': max_val},
|
274 |
+
'lowest': {'city': min_city, 'value': min_val},
|
275 |
+
'average': avg_val,
|
276 |
+
'range': max_val - min_val,
|
277 |
+
'std_deviation': np.std(values)
|
278 |
+
}
|
279 |
+
|
280 |
+
def _analyze_seasonal_patterns(self, forecast: List[Dict], hourly: List[Dict]) -> Dict:
|
281 |
+
"""Analyze seasonal weather patterns"""
|
282 |
+
# Simplified seasonal analysis
|
283 |
+
day_temps = []
|
284 |
+
night_temps = []
|
285 |
+
|
286 |
+
for period in forecast[:14]: # 2 weeks
|
287 |
+
if period.get('isDaytime'):
|
288 |
+
day_temps.append(period.get('temperature', 0))
|
289 |
+
else:
|
290 |
+
night_temps.append(period.get('temperature', 0))
|
291 |
+
|
292 |
+
return {
|
293 |
+
'day_night_difference': np.mean(day_temps) - np.mean(night_temps) if day_temps and night_temps else 0,
|
294 |
+
'temperature_stability': np.std(day_temps + night_temps),
|
295 |
+
'pattern_type': 'stable' if np.std(day_temps + night_temps) < 10 else 'variable'
|
296 |
+
}
|
297 |
+
|
298 |
+
def _analyze_daily_patterns(self, hourly: List[Dict]) -> Dict:
|
299 |
+
"""Analyze daily weather patterns"""
|
300 |
+
hourly_temps = []
|
301 |
+
hourly_precip = []
|
302 |
+
|
303 |
+
for hour in hourly[:24]: # 24 hours
|
304 |
+
hourly_temps.append(hour.get('temperature', 0))
|
305 |
+
precip_prob = hour.get('probabilityOfPrecipitation', {})
|
306 |
+
if isinstance(precip_prob, dict):
|
307 |
+
hourly_precip.append(precip_prob.get('value', 0))
|
308 |
+
else:
|
309 |
+
hourly_precip.append(precip_prob or 0)
|
310 |
+
|
311 |
+
return {
|
312 |
+
'temperature_range': max(hourly_temps) - min(hourly_temps) if hourly_temps else 0,
|
313 |
+
'peak_precipitation_hour': hourly_precip.index(max(hourly_precip)) if hourly_precip else 0,
|
314 |
+
'temperature_trend': 'warming' if hourly_temps[-1] > hourly_temps[0] else 'cooling'
|
315 |
+
}
|
316 |
+
|
317 |
+
def _analyze_precipitation_patterns(self, forecast: List[Dict], hourly: List[Dict]) -> Dict:
|
318 |
+
"""Analyze precipitation patterns"""
|
319 |
+
precip_periods = []
|
320 |
+
|
321 |
+
for period in forecast[:7]: # 7 days
|
322 |
+
precip_periods.append(period.get('precipitationProbability', 0))
|
323 |
+
|
324 |
+
return {
|
325 |
+
'average_precipitation_probability': np.mean(precip_periods),
|
326 |
+
'max_precipitation_probability': max(precip_periods),
|
327 |
+
'precipitation_days': sum(1 for p in precip_periods if p > 30),
|
328 |
+
'pattern': 'wet' if np.mean(precip_periods) > 50 else 'dry'
|
329 |
+
}
|
330 |
+
|
331 |
+
def _calculate_pattern_confidence(self, patterns: Dict) -> float:
|
332 |
+
"""Calculate confidence level for detected patterns"""
|
333 |
+
# Simplified confidence calculation
|
334 |
+
if not patterns:
|
335 |
+
return 0.0
|
336 |
+
|
337 |
+
# Base confidence on data consistency
|
338 |
+
confidence = 0.7 # Base confidence
|
339 |
+
|
340 |
+
# Adjust based on pattern strength
|
341 |
+
if 'temperature_stability' in patterns:
|
342 |
+
stability = patterns['temperature_stability']
|
343 |
+
if stability < 5:
|
344 |
+
confidence += 0.2
|
345 |
+
elif stability > 15:
|
346 |
+
confidence -= 0.2
|
347 |
+
|
348 |
+
return min(1.0, max(0.0, confidence))
|
349 |
+
|
350 |
+
def _calculate_risk_level(self, anomalies: List[Dict]) -> str:
|
351 |
+
"""Calculate overall risk level based on anomalies"""
|
352 |
+
if not anomalies:
|
353 |
+
return 'low'
|
354 |
+
|
355 |
+
high_severity_count = sum(1 for a in anomalies if a.get('severity') == 'high')
|
356 |
+
total_count = len(anomalies)
|
357 |
+
|
358 |
+
if high_severity_count > 0:
|
359 |
+
return 'high'
|
360 |
+
elif total_count > 3:
|
361 |
+
return 'moderate'
|
362 |
+
else:
|
363 |
+
return 'low'
|
364 |
+
|
365 |
+
def _extract_wind_speed(self, wind_str: str) -> float:
|
366 |
+
"""Extract numeric wind speed from string"""
|
367 |
+
try:
|
368 |
+
# Extract number from string like "10 mph" or "5 to 10 mph"
|
369 |
+
import re
|
370 |
+
numbers = re.findall(r'\d+', wind_str)
|
371 |
+
if numbers:
|
372 |
+
return float(numbers[0])
|
373 |
+
return 0.0
|
374 |
+
except:
|
375 |
+
return 0.0
|
376 |
+
|
377 |
+
def create_climate_analyzer(weather_client) -> ClimateAnalyzer:
|
378 |
+
"""Factory function to create climate analyzer"""
|
379 |
+
return ClimateAnalyzer(weather_client)
|
380 |
+
|
src/api/__init__.py
ADDED
File without changes
|
src/api/weather_client.py
ADDED
@@ -0,0 +1,442 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
Advanced Weather API Client
|
3 |
+
Comprehensive interface to weather.gov API with enhanced features
|
4 |
+
"""
|
5 |
+
|
6 |
+
import requests
|
7 |
+
import logging
|
8 |
+
from typing import List, Dict, Tuple, Optional
|
9 |
+
from datetime import datetime, timedelta
|
10 |
+
import json
|
11 |
+
import time
|
12 |
+
from urllib.parse import quote
|
13 |
+
|
14 |
+
logger = logging.getLogger(__name__)
|
15 |
+
|
16 |
+
class WeatherClient:
|
17 |
+
"""Advanced weather client for weather.gov API"""
|
18 |
+
|
19 |
+
def __init__(self):
|
20 |
+
self.base_url = "https://api.weather.gov"
|
21 |
+
self.geocoding_url = "https://nominatim.openstreetmap.org/search"
|
22 |
+
self.session = requests.Session()
|
23 |
+
self.session.headers.update({
|
24 |
+
'User-Agent': 'WeatherAppPro/1.0 ([email protected])'
|
25 |
+
})
|
26 |
+
|
27 |
+
# Cache for geocoding results to avoid repeated API calls
|
28 |
+
self.geocoding_cache = {}
|
29 |
+
|
30 |
+
# Common city aliases and abbreviations for better user experience
|
31 |
+
self.city_aliases = {
|
32 |
+
'nyc': 'New York City, NY',
|
33 |
+
'la': 'Los Angeles, CA',
|
34 |
+
'sf': 'San Francisco, CA',
|
35 |
+
'dc': 'Washington, DC',
|
36 |
+
'philly': 'Philadelphia, PA',
|
37 |
+
'vegas': 'Las Vegas, NV',
|
38 |
+
'chi': 'Chicago, IL',
|
39 |
+
'atx': 'Austin, TX',
|
40 |
+
'h-town': 'Houston, TX',
|
41 |
+
'pdx': 'Portland, OR',
|
42 |
+
'nola': 'New Orleans, LA',
|
43 |
+
'the bay': 'San Francisco Bay Area, CA',
|
44 |
+
'south beach': 'Miami Beach, FL',
|
45 |
+
'motor city': 'Detroit, MI',
|
46 |
+
'space city': 'Houston, TX',
|
47 |
+
'city of angels': 'Los Angeles, CA',
|
48 |
+
'windy city': 'Chicago, IL',
|
49 |
+
'big apple': 'New York City, NY',
|
50 |
+
'music city': 'Nashville, TN',
|
51 |
+
'silicon valley': 'San Jose, CA',
|
52 |
+
}
|
53 |
+
|
54 |
+
# Fallback coordinate database for common cities when external geocoding fails
|
55 |
+
# This ensures the app works reliably even if external APIs are down
|
56 |
+
self.fallback_coordinates = {
|
57 |
+
# Major US cities with commonly ambiguous names
|
58 |
+
'lincoln': (40.8136, -96.7026), # Lincoln, NE (most populous Lincoln)
|
59 |
+
'columbia': (34.0007, -81.0348), # Columbia, SC (state capital)
|
60 |
+
'springfield': (39.7817, -89.6501), # Springfield, IL (state capital)
|
61 |
+
'portland': (45.5152, -122.6784), # Portland, OR (larger than Portland, ME)
|
62 |
+
'manchester': (42.9956, -71.4548), # Manchester, NH
|
63 |
+
'franklin': (35.9251, -86.8689), # Franklin, TN
|
64 |
+
'canton': (40.7990, -81.3785), # Canton, OH
|
65 |
+
'auburn': (32.6010, -85.4808), # Auburn, AL
|
66 |
+
'athens': (33.9519, -83.3576), # Athens, GA
|
67 |
+
'madison': (43.0731, -89.4012), # Madison, WI (state capital)
|
68 |
+
'richmond': (37.5407, -77.4360), # Richmond, VA (state capital)
|
69 |
+
'charleston': (32.7765, -79.9311), # Charleston, SC
|
70 |
+
'fayetteville': (36.0625, -94.1574), # Fayetteville, AR
|
71 |
+
'burlington': (44.4759, -73.2121), # Burlington, VT
|
72 |
+
'dover': (39.1581, -75.5244), # Dover, DE (state capital)
|
73 |
+
'concord': (43.2081, -71.5376), # Concord, NH (state capital)
|
74 |
+
'albany': (42.6526, -73.7562), # Albany, NY (state capital)
|
75 |
+
'jackson': (32.2988, -90.1848), # Jackson, MS (state capital)
|
76 |
+
'montgomery': (32.3617, -86.2792), # Montgomery, AL (state capital)
|
77 |
+
'pierre': (44.3683, -100.3510), # Pierre, SD (state capital)
|
78 |
+
'helena': (46.5956, -112.0362), # Helena, MT (state capital)
|
79 |
+
'juneau': (58.3019, -134.4197), # Juneau, AK (state capital)
|
80 |
+
'olympia': (47.0379, -122.9015), # Olympia, WA (state capital)
|
81 |
+
'salem': (44.9429, -123.0351), # Salem, OR (state capital)
|
82 |
+
'cheyenne': (41.1400, -104.8197), # Cheyenne, WY (state capital)
|
83 |
+
'bismarck': (46.8083, -100.7837), # Bismarck, ND (state capital)
|
84 |
+
'topeka': (39.0473, -95.6890), # Topeka, KS (state capital)
|
85 |
+
'jefferson city': (38.5767, -92.1735), # Jefferson City, MO (state capital)
|
86 |
+
'little rock': (34.7465, -92.2896), # Little Rock, AR (state capital)
|
87 |
+
'harrisburg': (40.2732, -76.8839), # Harrisburg, PA (state capital)
|
88 |
+
'annapolis': (38.9784, -76.5021), # Annapolis, MD (state capital)
|
89 |
+
'trenton': (40.2206, -74.7562), # Trenton, NJ (state capital)
|
90 |
+
'hartford': (41.7658, -72.6734), # Hartford, CT (state capital)
|
91 |
+
'providence': (41.8240, -71.4128), # Providence, RI (state capital)
|
92 |
+
'montpelier': (44.2601, -72.5806), # Montpelier, VT (state capital)
|
93 |
+
'augusta': (44.3106, -69.7795), # Augusta, ME (state capital)
|
94 |
+
|
95 |
+
# Additional commonly requested cities
|
96 |
+
'wichita': (37.6872, -97.3301), # Wichita, KS
|
97 |
+
'lubbock': (33.5779, -101.8552), # Lubbock, TX
|
98 |
+
'shreveport': (32.5252, -93.7502), # Shreveport, LA
|
99 |
+
'mobile': (30.6944, -88.0431), # Mobile, AL
|
100 |
+
'pensacola': (30.4213, -87.2169), # Pensacola, FL
|
101 |
+
'tallahassee': (30.4518, -84.2807), # Tallahassee, FL
|
102 |
+
'gainesville': (29.6516, -82.3248), # Gainesville, FL
|
103 |
+
'eugene': (44.0521, -123.0868), # Eugene, OR
|
104 |
+
'spokane': (47.6588, -117.4260), # Spokane, WA
|
105 |
+
'tacoma': (47.2529, -122.4443), # Tacoma, WA
|
106 |
+
'anchorage': (61.2181, -149.9003), # Anchorage, AK
|
107 |
+
'honolulu': (21.3099, -157.8581), # Honolulu, HI
|
108 |
+
|
109 |
+
# Texas cities (commonly requested)
|
110 |
+
'midland': (31.9973, -102.0779), # Midland, TX
|
111 |
+
'odessa': (31.8457, -102.3676), # Odessa, TX
|
112 |
+
}
|
113 |
+
|
114 |
+
def geocode_location(self, location: str) -> Optional[Tuple[float, float]]:
|
115 |
+
"""
|
116 |
+
Advanced geocoding using external API with fallback mechanisms.
|
117 |
+
Supports any city worldwide via OpenStreetMap/Nominatim geocoding.
|
118 |
+
"""
|
119 |
+
location_clean = location.strip()
|
120 |
+
location_lower = location_clean.lower()
|
121 |
+
|
122 |
+
# Check cache first to avoid repeated API calls
|
123 |
+
if location_lower in self.geocoding_cache:
|
124 |
+
logger.info(f"Using cached coordinates for {location}")
|
125 |
+
return self.geocoding_cache[location_lower]
|
126 |
+
|
127 |
+
# Handle common aliases and abbreviations
|
128 |
+
if location_lower in self.city_aliases:
|
129 |
+
search_location = self.city_aliases[location_lower]
|
130 |
+
logger.info(f"Using alias: {location} -> {search_location}")
|
131 |
+
else:
|
132 |
+
search_location = location_clean
|
133 |
+
|
134 |
+
# Add country bias for US if not specified
|
135 |
+
if ',' not in search_location and 'usa' not in search_location.lower() and 'united states' not in search_location.lower():
|
136 |
+
search_location += ', USA'
|
137 |
+
|
138 |
+
try:
|
139 |
+
# Use Nominatim (OpenStreetMap) geocoding service
|
140 |
+
params = {
|
141 |
+
'q': search_location,
|
142 |
+
'format': 'json',
|
143 |
+
'limit': 1,
|
144 |
+
'countrycodes': 'us', # Prefer US results for weather.gov compatibility
|
145 |
+
'addressdetails': 1,
|
146 |
+
'bounded': 1,
|
147 |
+
'viewbox': '-125,50,-66,25' # US bounding box
|
148 |
+
}
|
149 |
+
|
150 |
+
logger.info(f"Geocoding '{search_location}' via Nominatim API")
|
151 |
+
response = self.session.get(
|
152 |
+
self.geocoding_url,
|
153 |
+
params=params,
|
154 |
+
timeout=10,
|
155 |
+
headers={'User-Agent': 'WeatherAppPro/1.0 ([email protected])'}
|
156 |
+
)
|
157 |
+
response.raise_for_status()
|
158 |
+
|
159 |
+
# Rate limiting - Nominatim requests max 1 request per second
|
160 |
+
time.sleep(1)
|
161 |
+
|
162 |
+
data = response.json()
|
163 |
+
|
164 |
+
if data and len(data) > 0:
|
165 |
+
result = data[0]
|
166 |
+
lat = float(result['lat'])
|
167 |
+
lon = float(result['lon'])
|
168 |
+
|
169 |
+
# Validate coordinates are within reasonable US bounds
|
170 |
+
if self._is_valid_us_coordinates(lat, lon):
|
171 |
+
coords = (lat, lon)
|
172 |
+
# Cache the result
|
173 |
+
self.geocoding_cache[location_lower] = coords
|
174 |
+
|
175 |
+
# Log the successful geocoding
|
176 |
+
display_name = result.get('display_name', search_location)
|
177 |
+
logger.info(f"Successfully geocoded '{location}' to {lat:.4f}, {lon:.4f} ({display_name})")
|
178 |
+
|
179 |
+
return coords
|
180 |
+
else:
|
181 |
+
logger.warning(f"Coordinates {lat}, {lon} for '{location}' are outside US bounds")
|
182 |
+
|
183 |
+
else:
|
184 |
+
logger.warning(f"No geocoding results found for '{location}'")
|
185 |
+
|
186 |
+
except requests.exceptions.RequestException as e:
|
187 |
+
logger.error(f"Geocoding API request failed for '{location}': {e}")
|
188 |
+
except (ValueError, KeyError) as e:
|
189 |
+
logger.error(f"Error parsing geocoding response for '{location}': {e}")
|
190 |
+
except Exception as e:
|
191 |
+
logger.error(f"Unexpected error during geocoding for '{location}': {e}")
|
192 |
+
|
193 |
+
# If geocoding fails, try fallback search without country constraint
|
194 |
+
try:
|
195 |
+
logger.info(f"Trying fallback geocoding for '{location}' without country constraint")
|
196 |
+
params = {
|
197 |
+
'q': location_clean,
|
198 |
+
'format': 'json',
|
199 |
+
'limit': 1,
|
200 |
+
'addressdetails': 1
|
201 |
+
}
|
202 |
+
|
203 |
+
response = self.session.get(
|
204 |
+
self.geocoding_url,
|
205 |
+
params=params,
|
206 |
+
timeout=10,
|
207 |
+
headers={'User-Agent': 'WeatherAppPro/1.0 ([email protected])'}
|
208 |
+
)
|
209 |
+
response.raise_for_status()
|
210 |
+
time.sleep(1)
|
211 |
+
|
212 |
+
data = response.json()
|
213 |
+
if data and len(data) > 0:
|
214 |
+
result = data[0]
|
215 |
+
lat = float(result['lat'])
|
216 |
+
lon = float(result['lon'])
|
217 |
+
|
218 |
+
# Check if it's at least in North America for weather.gov compatibility
|
219 |
+
if -170 <= lon <= -50 and 15 <= lat <= 75:
|
220 |
+
coords = (lat, lon)
|
221 |
+
self.geocoding_cache[location_lower] = coords
|
222 |
+
|
223 |
+
display_name = result.get('display_name', location_clean)
|
224 |
+
logger.info(f"Fallback geocoded '{location}' to {lat:.4f}, {lon:.4f} ({display_name})")
|
225 |
+
|
226 |
+
return coords
|
227 |
+
|
228 |
+
except Exception as e:
|
229 |
+
logger.error(f"Fallback geocoding also failed for '{location}': {e}")
|
230 |
+
|
231 |
+
# Final fallback: check our local coordinate database
|
232 |
+
if location_lower in self.fallback_coordinates:
|
233 |
+
coords = self.fallback_coordinates[location_lower]
|
234 |
+
self.geocoding_cache[location_lower] = coords
|
235 |
+
logger.info(f"Using fallback coordinates for '{location}': {coords[0]:.4f}, {coords[1]:.4f}")
|
236 |
+
return coords
|
237 |
+
|
238 |
+
# Try partial matching in fallback database for more flexible city name matching
|
239 |
+
for city_key, coords in self.fallback_coordinates.items():
|
240 |
+
if city_key in location_lower or location_lower in city_key:
|
241 |
+
self.geocoding_cache[location_lower] = coords
|
242 |
+
logger.info(f"Using partial match fallback coordinates for '{location}' (matched '{city_key}'): {coords[0]:.4f}, {coords[1]:.4f}")
|
243 |
+
return coords
|
244 |
+
|
245 |
+
logger.error(f"Unable to geocode location: '{location}' - not found in external APIs or fallback database")
|
246 |
+
return None
|
247 |
+
|
248 |
+
def _is_valid_us_coordinates(self, lat: float, lon: float) -> bool:
|
249 |
+
"""Check if coordinates are within reasonable US bounds"""
|
250 |
+
# Continental US, Alaska, Hawaii, and territories
|
251 |
+
return (
|
252 |
+
(25 <= lat <= 49 and -125 <= lon <= -66) or # Continental US
|
253 |
+
(54 <= lat <= 71 and -179 <= lon <= -130) or # Alaska
|
254 |
+
(18 <= lat <= 23 and -161 <= lon <= -154) or # Hawaii
|
255 |
+
(17 <= lat <= 19 and -68 <= lon <= -64) or # Puerto Rico
|
256 |
+
(14 <= lat <= 15 and -65 <= lon <= -64) # US Virgin Islands
|
257 |
+
)
|
258 |
+
|
259 |
+
def get_point_info(self, lat: float, lon: float) -> Dict:
|
260 |
+
"""Get point information from weather.gov"""
|
261 |
+
try:
|
262 |
+
url = f"{self.base_url}/points/{lat},{lon}"
|
263 |
+
response = self.session.get(url, timeout=10)
|
264 |
+
response.raise_for_status()
|
265 |
+
data = response.json()
|
266 |
+
props = data.get('properties', {})
|
267 |
+
return {
|
268 |
+
'office': props.get('gridId'),
|
269 |
+
'grid_x': props.get('gridX'),
|
270 |
+
'grid_y': props.get('gridY'),
|
271 |
+
'forecast_url': props.get('forecast'),
|
272 |
+
'forecast_hourly_url': props.get('forecastHourly'),
|
273 |
+
'timezone': props.get('timeZone'),
|
274 |
+
'radar_station': props.get('radarStation'),
|
275 |
+
'fire_weather_zone': props.get('fireWeatherZone'),
|
276 |
+
'county': props.get('county')
|
277 |
+
}
|
278 |
+
except Exception as e:
|
279 |
+
logger.error(f"Error getting point info: {e}")
|
280 |
+
return {}
|
281 |
+
|
282 |
+
def get_forecast(self, lat: float, lon: float) -> List[Dict]:
|
283 |
+
"""Get weather forecast"""
|
284 |
+
try:
|
285 |
+
point_info = self.get_point_info(lat, lon)
|
286 |
+
if not point_info.get('office'):
|
287 |
+
return []
|
288 |
+
|
289 |
+
office = point_info['office']
|
290 |
+
grid_x = point_info['grid_x']
|
291 |
+
grid_y = point_info['grid_y']
|
292 |
+
|
293 |
+
url = f"{self.base_url}/gridpoints/{office}/{grid_x},{grid_y}/forecast"
|
294 |
+
response = self.session.get(url, timeout=10)
|
295 |
+
response.raise_for_status()
|
296 |
+
|
297 |
+
data = response.json()
|
298 |
+
periods = data.get('properties', {}).get('periods', [])
|
299 |
+
|
300 |
+
forecast = []
|
301 |
+
for period in periods:
|
302 |
+
forecast.append({
|
303 |
+
'name': period.get('name'),
|
304 |
+
'temperature': period.get('temperature'),
|
305 |
+
'temperatureUnit': period.get('temperatureUnit'),
|
306 |
+
'windSpeed': period.get('windSpeed'),
|
307 |
+
'windDirection': period.get('windDirection'),
|
308 |
+
'shortForecast': period.get('shortForecast'),
|
309 |
+
'detailedForecast': period.get('detailedForecast'),
|
310 |
+
'precipitationProbability': period.get('probabilityOfPrecipitation', {}).get('value', 0),
|
311 |
+
'startTime': period.get('startTime'),
|
312 |
+
'endTime': period.get('endTime'),
|
313 |
+
'isDaytime': period.get('isDaytime'),
|
314 |
+
'temperatureTrend': period.get('temperatureTrend')
|
315 |
+
})
|
316 |
+
|
317 |
+
return forecast
|
318 |
+
except Exception as e:
|
319 |
+
logger.error(f"Error getting forecast: {e}")
|
320 |
+
return []
|
321 |
+
|
322 |
+
def get_hourly_forecast(self, lat: float, lon: float, hours: int = 48) -> List[Dict]:
|
323 |
+
"""Get hourly forecast"""
|
324 |
+
try:
|
325 |
+
point_info = self.get_point_info(lat, lon)
|
326 |
+
if not point_info.get('office'):
|
327 |
+
return []
|
328 |
+
|
329 |
+
office = point_info['office']
|
330 |
+
grid_x = point_info['grid_x']
|
331 |
+
grid_y = point_info['grid_y']
|
332 |
+
|
333 |
+
url = f"{self.base_url}/gridpoints/{office}/{grid_x},{grid_y}/forecast/hourly"
|
334 |
+
response = self.session.get(url, timeout=10)
|
335 |
+
response.raise_for_status()
|
336 |
+
|
337 |
+
data = response.json()
|
338 |
+
periods = data.get('properties', {}).get('periods', [])
|
339 |
+
|
340 |
+
return periods[:hours]
|
341 |
+
except Exception as e:
|
342 |
+
logger.error(f"Error getting hourly forecast: {e}")
|
343 |
+
return []
|
344 |
+
|
345 |
+
def get_current_observations(self, lat: float, lon: float) -> Dict:
|
346 |
+
"""Get current weather observations"""
|
347 |
+
try:
|
348 |
+
# Find nearest observation station
|
349 |
+
stations_url = f"{self.base_url}/points/{lat},{lon}/stations"
|
350 |
+
response = self.session.get(stations_url, timeout=10)
|
351 |
+
response.raise_for_status()
|
352 |
+
|
353 |
+
stations_data = response.json()
|
354 |
+
stations = stations_data.get('features', [])
|
355 |
+
|
356 |
+
if not stations:
|
357 |
+
return {}
|
358 |
+
|
359 |
+
# Get observations from first station
|
360 |
+
station_id = stations[0]['properties']['stationIdentifier']
|
361 |
+
obs_url = f"{self.base_url}/stations/{station_id}/observations/latest"
|
362 |
+
|
363 |
+
response = self.session.get(obs_url, timeout=10)
|
364 |
+
response.raise_for_status()
|
365 |
+
|
366 |
+
obs_data = response.json()
|
367 |
+
props = obs_data.get('properties', {})
|
368 |
+
|
369 |
+
return {
|
370 |
+
'timestamp': props.get('timestamp'),
|
371 |
+
'temperature': props.get('temperature', {}).get('value'),
|
372 |
+
'dewpoint': props.get('dewpoint', {}).get('value'),
|
373 |
+
'windDirection': props.get('windDirection', {}).get('value'),
|
374 |
+
'windSpeed': props.get('windSpeed', {}).get('value'),
|
375 |
+
'windGust': props.get('windGust', {}).get('value'),
|
376 |
+
'barometricPressure': props.get('barometricPressure', {}).get('value'),
|
377 |
+
'visibility': props.get('visibility', {}).get('value'),
|
378 |
+
'relativeHumidity': props.get('relativeHumidity', {}).get('value'),
|
379 |
+
'heatIndex': props.get('heatIndex', {}).get('value'),
|
380 |
+
'windChill': props.get('windChill', {}).get('value')
|
381 |
+
}
|
382 |
+
except Exception as e:
|
383 |
+
logger.error(f"Error getting current observations: {e}")
|
384 |
+
return {}
|
385 |
+
|
386 |
+
def get_alerts(self, lat: float = None, lon: float = None) -> List[Dict]:
|
387 |
+
"""Get weather alerts"""
|
388 |
+
try:
|
389 |
+
if lat and lon:
|
390 |
+
url = f"{self.base_url}/alerts/active?point={lat},{lon}"
|
391 |
+
else:
|
392 |
+
url = f"{self.base_url}/alerts/active"
|
393 |
+
|
394 |
+
response = self.session.get(url, timeout=10)
|
395 |
+
response.raise_for_status()
|
396 |
+
|
397 |
+
data = response.json()
|
398 |
+
alerts = []
|
399 |
+
|
400 |
+
for feature in data.get('features', [])[:15]:
|
401 |
+
props = feature.get('properties', {})
|
402 |
+
alerts.append({
|
403 |
+
'id': props.get('id'),
|
404 |
+
'event': props.get('event'),
|
405 |
+
'headline': props.get('headline'),
|
406 |
+
'description': props.get('description'),
|
407 |
+
'severity': props.get('severity'),
|
408 |
+
'urgency': props.get('urgency'),
|
409 |
+
'certainty': props.get('certainty'),
|
410 |
+
'areas': props.get('areaDesc'),
|
411 |
+
'effective': props.get('effective'),
|
412 |
+
'expires': props.get('expires'),
|
413 |
+
'senderName': props.get('senderName'),
|
414 |
+
'category': props.get('category')
|
415 |
+
})
|
416 |
+
|
417 |
+
return alerts
|
418 |
+
except Exception as e:
|
419 |
+
logger.error(f"Error getting alerts: {e}")
|
420 |
+
return []
|
421 |
+
|
422 |
+
def get_radar_data(self, lat: float, lon: float) -> Dict:
|
423 |
+
"""Get radar station information"""
|
424 |
+
try:
|
425 |
+
point_info = self.get_point_info(lat, lon)
|
426 |
+
radar_station = point_info.get('radar_station')
|
427 |
+
|
428 |
+
if not radar_station:
|
429 |
+
return {}
|
430 |
+
|
431 |
+
return {
|
432 |
+
'station': radar_station,
|
433 |
+
'base_url': f"https://radar.weather.gov/ridge/lite/{radar_station}_loop.gif"
|
434 |
+
}
|
435 |
+
except Exception as e:
|
436 |
+
logger.error(f"Error getting radar data: {e}")
|
437 |
+
return {}
|
438 |
+
|
439 |
+
def create_weather_client() -> WeatherClient:
|
440 |
+
"""Factory function to create weather client"""
|
441 |
+
return WeatherClient()
|
442 |
+
|
src/chatbot/__init__.py
ADDED
File without changes
|
src/chatbot/climate_expert_nlp.py
ADDED
@@ -0,0 +1,608 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
Enhanced Climate Expert NLP Processor
|
3 |
+
Advanced natural language processing for climate expertise and intelligent weather analysis
|
4 |
+
"""
|
5 |
+
|
6 |
+
import re
|
7 |
+
import logging
|
8 |
+
from typing import List, Dict, Set, Tuple, Optional
|
9 |
+
from datetime import datetime, date
|
10 |
+
import calendar
|
11 |
+
|
12 |
+
logger = logging.getLogger(__name__)
|
13 |
+
|
14 |
+
class ClimateExpertNLP:
|
15 |
+
"""Advanced NLP processor with climate expertise and meteorological intelligence"""
|
16 |
+
|
17 |
+
def __init__(self):
|
18 |
+
# Enhanced weather patterns with meteorological expertise
|
19 |
+
self.climate_keywords = {
|
20 |
+
'temperature_analysis': {
|
21 |
+
'primary': ['temperature', 'temp', 'thermal', 'heat', 'warmth', 'cold', 'chill', 'frost', 'freeze'],
|
22 |
+
'patterns': ['heat wave', 'cold snap', 'thermal gradient', 'temperature inversion', 'diurnal range'],
|
23 |
+
'expert': ['heat index', 'wind chill', 'feels like', 'apparent temperature', 'dewpoint'],
|
24 |
+
'anomalies': ['record', 'unusual', 'extreme', 'abnormal', 'unprecedented', 'rare']
|
25 |
+
},
|
26 |
+
'precipitation_analysis': {
|
27 |
+
'primary': ['rain', 'precipitation', 'moisture', 'wet', 'dry', 'storm', 'shower', 'drizzle'],
|
28 |
+
'patterns': ['drought', 'flooding', 'monsoon', 'rainy season', 'dry spell', 'accumulation'],
|
29 |
+
'expert': ['precipitation rate', 'intensity', 'convective', 'stratiform', 'orographic'],
|
30 |
+
'types': ['thunderstorm', 'snow', 'sleet', 'hail', 'freezing rain', 'virga']
|
31 |
+
},
|
32 |
+
'atmospheric_dynamics': {
|
33 |
+
'primary': ['pressure', 'atmospheric', 'barometric', 'high pressure', 'low pressure'],
|
34 |
+
'patterns': ['front', 'cold front', 'warm front', 'occluded front', 'cyclone', 'anticyclone'],
|
35 |
+
'expert': ['isobar', 'gradient', 'ridge', 'trough', 'convergence', 'divergence'],
|
36 |
+
'phenomena': ['inversion', 'subsidence', 'convection', 'advection']
|
37 |
+
},
|
38 |
+
'wind_analysis': {
|
39 |
+
'primary': ['wind', 'breeze', 'gust', 'air movement', 'circulation'],
|
40 |
+
'patterns': ['jet stream', 'trade winds', 'westerlies', 'polar easterlies', 'monsoon'],
|
41 |
+
'expert': ['wind shear', 'turbulence', 'downdraft', 'updraft', 'vorticity'],
|
42 |
+
'local': ['sea breeze', 'land breeze', 'mountain breeze', 'valley breeze', 'chinook']
|
43 |
+
},
|
44 |
+
'climate_patterns': {
|
45 |
+
'primary': ['climate', 'pattern', 'cycle', 'oscillation', 'trend', 'variability'],
|
46 |
+
'global': ['el nino', 'la nina', 'enso', 'pdo', 'amo', 'nao', 'ao'],
|
47 |
+
'regional': ['monsoon', 'mediterranean', 'continental', 'maritime', 'subtropical'],
|
48 |
+
'temporal': ['seasonal', 'annual', 'decadal', 'interannual', 'long-term']
|
49 |
+
},
|
50 |
+
'extreme_weather': {
|
51 |
+
'primary': ['extreme', 'severe', 'dangerous', 'hazardous', 'warning', 'watch'],
|
52 |
+
'types': ['tornado', 'hurricane', 'typhoon', 'blizzard', 'derecho', 'microburst'],
|
53 |
+
'conditions': ['severe thunderstorm', 'flash flood', 'ice storm', 'heat dome'],
|
54 |
+
'impacts': ['damage', 'destruction', 'flooding', 'power outage', 'travel disruption']
|
55 |
+
},
|
56 |
+
'seasonal_intelligence': {
|
57 |
+
'primary': ['season', 'seasonal', 'spring', 'summer', 'fall', 'autumn', 'winter'],
|
58 |
+
'transitions': ['equinox', 'solstice', 'onset', 'retreat', 'shift'],
|
59 |
+
'phenomena': ['indian summer', 'polar vortex', 'heat dome', 'arctic blast'],
|
60 |
+
'agriculture': ['growing season', 'frost date', 'planting', 'harvest']
|
61 |
+
},
|
62 |
+
'climate_change': {
|
63 |
+
'primary': ['climate change', 'warming', 'trend', 'shift', 'change'],
|
64 |
+
'indicators': ['rising temperatures', 'sea level', 'ice melt', 'precipitation change'],
|
65 |
+
'impacts': ['drought frequency', 'storm intensity', 'heat events', 'cold events'],
|
66 |
+
'adaptation': ['resilience', 'mitigation', 'adaptation', 'sustainability']
|
67 |
+
},
|
68 |
+
'meteorological_analysis': {
|
69 |
+
'primary': ['analysis', 'forecast', 'model', 'prediction', 'simulation'],
|
70 |
+
'models': ['gfs', 'ecmwf', 'nam', 'ensemble', 'numerical model'],
|
71 |
+
'data': ['satellite', 'radar', 'observation', 'sounding', 'mesonet'],
|
72 |
+
'uncertainty': ['confidence', 'uncertainty', 'probability', 'likelihood', 'chance']
|
73 |
+
}
|
74 |
+
}
|
75 |
+
|
76 |
+
# Expert-level weather relationships and correlations
|
77 |
+
self.weather_relationships = {
|
78 |
+
'temperature_pressure': {
|
79 |
+
'high_pressure': 'generally associated with clear skies and stable temperatures',
|
80 |
+
'low_pressure': 'often brings clouds, precipitation, and temperature changes',
|
81 |
+
'pressure_gradient': 'strong gradients indicate windy conditions'
|
82 |
+
},
|
83 |
+
'moisture_temperature': {
|
84 |
+
'dewpoint_spread': 'narrow spread indicates high humidity or fog potential',
|
85 |
+
'relative_humidity': 'temperature-dependent moisture content',
|
86 |
+
'heat_index': 'combines temperature and humidity for apparent temperature'
|
87 |
+
},
|
88 |
+
'wind_patterns': {
|
89 |
+
'thermal_circulation': 'temperature differences drive local wind patterns',
|
90 |
+
'pressure_gradient': 'wind flows from high to low pressure areas',
|
91 |
+
'coriolis_effect': 'Earth\'s rotation affects wind direction'
|
92 |
+
}
|
93 |
+
}
|
94 |
+
|
95 |
+
# Regional climate knowledge
|
96 |
+
self.regional_climate_patterns = {
|
97 |
+
'pacific_northwest': {
|
98 |
+
'characteristics': ['marine west coast', 'wet winters', 'dry summers', 'moderate temperatures'],
|
99 |
+
'phenomena': ['atmospheric rivers', 'rain shadow', 'marine layer'],
|
100 |
+
'seasons': ['wet season (oct-apr)', 'dry season (may-sep)']
|
101 |
+
},
|
102 |
+
'southwest_desert': {
|
103 |
+
'characteristics': ['arid', 'hot summers', 'mild winters', 'low humidity'],
|
104 |
+
'phenomena': ['monsoon season', 'heat island effect', 'dust storms'],
|
105 |
+
'extremes': ['extreme heat', 'flash flooding', 'drought']
|
106 |
+
},
|
107 |
+
'great_plains': {
|
108 |
+
'characteristics': ['continental', 'temperature extremes', 'variable precipitation'],
|
109 |
+
'phenomena': ['severe thunderstorms', 'tornadoes', 'chinook winds'],
|
110 |
+
'seasons': ['severe weather season (spring-summer)']
|
111 |
+
},
|
112 |
+
'southeast': {
|
113 |
+
'characteristics': ['humid subtropical', 'hot summers', 'mild winters'],
|
114 |
+
'phenomena': ['hurricanes', 'afternoon thunderstorms', 'heat index'],
|
115 |
+
'seasons': ['hurricane season (jun-nov)']
|
116 |
+
},
|
117 |
+
'northeast': {
|
118 |
+
'characteristics': ['four-season continental', 'nor\'easters', 'lake effect'],
|
119 |
+
'phenomena': ['coastal storms', 'lake effect snow', 'heat waves'],
|
120 |
+
'seasons': ['distinct four seasons', 'winter storms']
|
121 |
+
}
|
122 |
+
}
|
123 |
+
|
124 |
+
# Enhanced cities database with regional climate context
|
125 |
+
self.cities_with_climate_context = self._build_climate_aware_cities()
|
126 |
+
|
127 |
+
# Meteorological expertise patterns
|
128 |
+
self.expert_analysis_patterns = {
|
129 |
+
'synoptic_scale': ['large scale', 'continental', 'hemispheric', 'global pattern'],
|
130 |
+
'mesoscale': ['regional', 'local', 'thunderstorm scale', 'sea breeze'],
|
131 |
+
'microscale': ['turbulence', 'surface layer', 'boundary layer'],
|
132 |
+
'forecast_confidence': ['high confidence', 'low confidence', 'uncertain', 'model disagreement'],
|
133 |
+
'climatology': ['normal', 'average', 'typical', 'anomaly', 'departure from normal']
|
134 |
+
}
|
135 |
+
|
136 |
+
def _build_climate_aware_cities(self) -> Dict[str, Dict]:
|
137 |
+
"""Build cities database with climate context"""
|
138 |
+
cities = {
|
139 |
+
# Pacific Northwest
|
140 |
+
'seattle': {'region': 'pacific_northwest', 'climate': 'oceanic', 'elevation': 56},
|
141 |
+
'portland': {'region': 'pacific_northwest', 'climate': 'oceanic', 'elevation': 50},
|
142 |
+
'spokane': {'region': 'pacific_northwest', 'climate': 'continental', 'elevation': 1843},
|
143 |
+
|
144 |
+
# Southwest Desert
|
145 |
+
'phoenix': {'region': 'southwest_desert', 'climate': 'desert', 'elevation': 1086},
|
146 |
+
'tucson': {'region': 'southwest_desert', 'climate': 'desert', 'elevation': 2389},
|
147 |
+
'las vegas': {'region': 'southwest_desert', 'climate': 'desert', 'elevation': 2001},
|
148 |
+
'albuquerque': {'region': 'southwest_desert', 'climate': 'high_desert', 'elevation': 5312},
|
149 |
+
|
150 |
+
# Great Plains
|
151 |
+
'kansas city': {'region': 'great_plains', 'climate': 'continental', 'elevation': 910},
|
152 |
+
'omaha': {'region': 'great_plains', 'climate': 'continental', 'elevation': 1090},
|
153 |
+
'oklahoma city': {'region': 'great_plains', 'climate': 'continental', 'elevation': 1201},
|
154 |
+
'denver': {'region': 'great_plains', 'climate': 'semi_arid', 'elevation': 5280},
|
155 |
+
|
156 |
+
# Southeast
|
157 |
+
'miami': {'region': 'southeast', 'climate': 'tropical', 'elevation': 6},
|
158 |
+
'atlanta': {'region': 'southeast', 'climate': 'humid_subtropical', 'elevation': 1050},
|
159 |
+
'new orleans': {'region': 'southeast', 'climate': 'humid_subtropical', 'elevation': -6},
|
160 |
+
'charlotte': {'region': 'southeast', 'climate': 'humid_subtropical', 'elevation': 751},
|
161 |
+
|
162 |
+
# Northeast
|
163 |
+
'new york': {'region': 'northeast', 'climate': 'humid_continental', 'elevation': 33},
|
164 |
+
'boston': {'region': 'northeast', 'climate': 'humid_continental', 'elevation': 141},
|
165 |
+
'philadelphia': {'region': 'northeast', 'climate': 'humid_subtropical', 'elevation': 39},
|
166 |
+
'buffalo': {'region': 'northeast', 'climate': 'humid_continental', 'elevation': 585},
|
167 |
+
|
168 |
+
# Additional major cities
|
169 |
+
'chicago': {'region': 'great_lakes', 'climate': 'humid_continental', 'elevation': 594},
|
170 |
+
'detroit': {'region': 'great_lakes', 'climate': 'humid_continental', 'elevation': 574},
|
171 |
+
'minneapolis': {'region': 'upper_midwest', 'climate': 'humid_continental', 'elevation': 830},
|
172 |
+
'milwaukee': {'region': 'great_lakes', 'climate': 'humid_continental', 'elevation': 634},
|
173 |
+
|
174 |
+
# Mountain West
|
175 |
+
'salt lake city': {'region': 'mountain_west', 'climate': 'semi_arid', 'elevation': 4226},
|
176 |
+
'boise': {'region': 'mountain_west', 'climate': 'semi_arid', 'elevation': 2730},
|
177 |
+
'billings': {'region': 'mountain_west', 'climate': 'semi_arid', 'elevation': 3123},
|
178 |
+
|
179 |
+
# California - diverse climates
|
180 |
+
'los angeles': {'region': 'california', 'climate': 'mediterranean', 'elevation': 285},
|
181 |
+
'san francisco': {'region': 'california', 'climate': 'mediterranean', 'elevation': 52},
|
182 |
+
'san diego': {'region': 'california', 'climate': 'mediterranean', 'elevation': 62},
|
183 |
+
'sacramento': {'region': 'california', 'climate': 'mediterranean', 'elevation': 30},
|
184 |
+
'fresno': {'region': 'california', 'climate': 'semi_arid', 'elevation': 335},
|
185 |
+
|
186 |
+
# Texas - varied climates
|
187 |
+
'houston': {'region': 'texas_gulf', 'climate': 'humid_subtropical', 'elevation': 80},
|
188 |
+
'dallas': {'region': 'texas_plains', 'climate': 'humid_subtropical', 'elevation': 430},
|
189 |
+
'san antonio': {'region': 'texas_hill_country', 'climate': 'humid_subtropical', 'elevation': 650},
|
190 |
+
'austin': {'region': 'texas_hill_country', 'climate': 'humid_subtropical', 'elevation': 489},
|
191 |
+
'el paso': {'region': 'texas_desert', 'climate': 'desert', 'elevation': 3740},
|
192 |
+
|
193 |
+
# Florida
|
194 |
+
'orlando': {'region': 'florida_central', 'climate': 'humid_subtropical', 'elevation': 82},
|
195 |
+
'tampa': {'region': 'florida_gulf', 'climate': 'humid_subtropical', 'elevation': 48},
|
196 |
+
'jacksonville': {'region': 'florida_northeast', 'climate': 'humid_subtropical', 'elevation': 16}
|
197 |
+
}
|
198 |
+
|
199 |
+
return cities
|
200 |
+
|
201 |
+
def extract_climate_intelligence(self, text: str) -> Dict:
|
202 |
+
"""Extract advanced climate and meteorological intelligence from query"""
|
203 |
+
text_lower = text.lower()
|
204 |
+
|
205 |
+
intelligence = {
|
206 |
+
'expertise_level': self._assess_expertise_level(text),
|
207 |
+
'climate_phenomena': self._detect_climate_phenomena(text),
|
208 |
+
'meteorological_concepts': self._detect_meteorological_concepts(text),
|
209 |
+
'seasonal_context': self._extract_seasonal_intelligence(text),
|
210 |
+
'regional_patterns': self._detect_regional_patterns(text),
|
211 |
+
'expert_analysis_needed': self._requires_expert_analysis(text),
|
212 |
+
'climate_relationships': self._identify_climate_relationships(text),
|
213 |
+
'forecast_complexity': self._assess_forecast_complexity(text)
|
214 |
+
}
|
215 |
+
|
216 |
+
return intelligence
|
217 |
+
|
218 |
+
def _assess_expertise_level(self, text: str) -> str:
|
219 |
+
"""Assess the expertise level required for the query"""
|
220 |
+
text_lower = text.lower()
|
221 |
+
|
222 |
+
expert_indicators = [
|
223 |
+
'meteorological', 'atmospheric', 'synoptic', 'mesoscale', 'climatology',
|
224 |
+
'pressure gradient', 'thermal gradient', 'convective', 'advection',
|
225 |
+
'el nino', 'la nina', 'jet stream', 'vorticity', 'geostrophic'
|
226 |
+
]
|
227 |
+
|
228 |
+
intermediate_indicators = [
|
229 |
+
'heat index', 'dewpoint', 'wind chill', 'pressure system', 'front',
|
230 |
+
'high pressure', 'low pressure', 'atmospheric river', 'heat dome'
|
231 |
+
]
|
232 |
+
|
233 |
+
expert_count = sum(1 for term in expert_indicators if term in text_lower)
|
234 |
+
intermediate_count = sum(1 for term in intermediate_indicators if term in text_lower)
|
235 |
+
|
236 |
+
if expert_count > 0:
|
237 |
+
return 'expert'
|
238 |
+
elif intermediate_count > 0:
|
239 |
+
return 'intermediate'
|
240 |
+
else:
|
241 |
+
return 'basic'
|
242 |
+
|
243 |
+
def _detect_climate_phenomena(self, text: str) -> List[str]:
|
244 |
+
"""Detect mention of specific climate phenomena"""
|
245 |
+
text_lower = text.lower()
|
246 |
+
|
247 |
+
phenomena = {
|
248 |
+
'el_nino': ['el nino', 'el niño', 'enso warm phase'],
|
249 |
+
'la_nina': ['la nina', 'la niña', 'enso cold phase'],
|
250 |
+
'heat_dome': ['heat dome', 'high pressure ridge', 'persistent high'],
|
251 |
+
'polar_vortex': ['polar vortex', 'arctic blast', 'polar air mass'],
|
252 |
+
'atmospheric_river': ['atmospheric river', 'pineapple express', 'moisture plume'],
|
253 |
+
'monsoon': ['monsoon', 'monsoonal', 'monsoon season'],
|
254 |
+
'drought': ['drought', 'dry conditions', 'precipitation deficit'],
|
255 |
+
'heat_wave': ['heat wave', 'extreme heat', 'prolonged heat'],
|
256 |
+
'cold_snap': ['cold snap', 'arctic outbreak', 'extreme cold']
|
257 |
+
}
|
258 |
+
|
259 |
+
detected = []
|
260 |
+
for phenomenon, keywords in phenomena.items():
|
261 |
+
if any(keyword in text_lower for keyword in keywords):
|
262 |
+
detected.append(phenomenon)
|
263 |
+
|
264 |
+
return detected
|
265 |
+
|
266 |
+
def _detect_meteorological_concepts(self, text: str) -> List[str]:
|
267 |
+
"""Detect meteorological concepts and terminology"""
|
268 |
+
text_lower = text.lower()
|
269 |
+
|
270 |
+
concepts = {
|
271 |
+
'pressure_systems': ['high pressure', 'low pressure', 'pressure system', 'anticyclone', 'cyclone'],
|
272 |
+
'frontal_systems': ['cold front', 'warm front', 'occluded front', 'stationary front'],
|
273 |
+
'atmospheric_layers': ['troposphere', 'stratosphere', 'boundary layer', 'inversion layer'],
|
274 |
+
'circulation_patterns': ['jet stream', 'trade winds', 'westerlies', 'polar easterlies'],
|
275 |
+
'thermodynamics': ['adiabatic', 'latent heat', 'sensible heat', 'convection', 'radiation'],
|
276 |
+
'moisture_processes': ['evaporation', 'condensation', 'precipitation', 'sublimation'],
|
277 |
+
'wind_phenomena': ['wind shear', 'turbulence', 'downdraft', 'updraft', 'convergence']
|
278 |
+
}
|
279 |
+
|
280 |
+
detected = []
|
281 |
+
for concept_group, keywords in concepts.items():
|
282 |
+
if any(keyword in text_lower for keyword in keywords):
|
283 |
+
detected.append(concept_group)
|
284 |
+
|
285 |
+
return detected
|
286 |
+
|
287 |
+
def _extract_seasonal_intelligence(self, text: str) -> Dict:
|
288 |
+
"""Extract sophisticated seasonal weather intelligence"""
|
289 |
+
text_lower = text.lower()
|
290 |
+
current_date = datetime.now()
|
291 |
+
|
292 |
+
# Determine current season and seasonal transitions
|
293 |
+
month = current_date.month
|
294 |
+
if month in [12, 1, 2]:
|
295 |
+
current_season = 'winter'
|
296 |
+
transition_info = self._get_winter_transition_info(month)
|
297 |
+
elif month in [3, 4, 5]:
|
298 |
+
current_season = 'spring'
|
299 |
+
transition_info = self._get_spring_transition_info(month)
|
300 |
+
elif month in [6, 7, 8]:
|
301 |
+
current_season = 'summer'
|
302 |
+
transition_info = self._get_summer_transition_info(month)
|
303 |
+
else:
|
304 |
+
current_season = 'fall'
|
305 |
+
transition_info = self._get_fall_transition_info(month)
|
306 |
+
|
307 |
+
# Detect seasonal phenomena mentions
|
308 |
+
seasonal_phenomena = {
|
309 |
+
'winter': ['polar vortex', 'blizzard', 'ice storm', 'lake effect', 'arctic blast'],
|
310 |
+
'spring': ['severe weather', 'tornado season', 'flooding', 'rapid warming'],
|
311 |
+
'summer': ['heat wave', 'monsoon', 'hurricane season', 'heat dome', 'drought'],
|
312 |
+
'fall': ['hurricane season', 'leaf change', 'first frost', 'indian summer']
|
313 |
+
}
|
314 |
+
|
315 |
+
detected_phenomena = []
|
316 |
+
for season, phenomena in seasonal_phenomena.items():
|
317 |
+
for phenomenon in phenomena:
|
318 |
+
if phenomenon in text_lower:
|
319 |
+
detected_phenomena.append((season, phenomenon))
|
320 |
+
|
321 |
+
return {
|
322 |
+
'current_season': current_season,
|
323 |
+
'transition_info': transition_info,
|
324 |
+
'seasonal_phenomena': detected_phenomena,
|
325 |
+
'climatological_context': self._get_climatological_context(current_season, month)
|
326 |
+
}
|
327 |
+
|
328 |
+
def _get_winter_transition_info(self, month: int) -> Dict:
|
329 |
+
"""Get winter season transition information"""
|
330 |
+
if month == 12:
|
331 |
+
return {'phase': 'early_winter', 'characteristics': ['winter solstice approaching', 'temperature decline']}
|
332 |
+
elif month == 1:
|
333 |
+
return {'phase': 'mid_winter', 'characteristics': ['coldest period', 'arctic air masses']}
|
334 |
+
else: # February
|
335 |
+
return {'phase': 'late_winter', 'characteristics': ['spring transition beginning', 'daylight increasing']}
|
336 |
+
|
337 |
+
def _get_spring_transition_info(self, month: int) -> Dict:
|
338 |
+
"""Get spring season transition information"""
|
339 |
+
if month == 3:
|
340 |
+
return {'phase': 'early_spring', 'characteristics': ['spring equinox', 'warming trend']}
|
341 |
+
elif month == 4:
|
342 |
+
return {'phase': 'mid_spring', 'characteristics': ['severe weather season', 'rapid warming']}
|
343 |
+
else: # May
|
344 |
+
return {'phase': 'late_spring', 'characteristics': ['summer transition', 'thunderstorm activity']}
|
345 |
+
|
346 |
+
def _get_summer_transition_info(self, month: int) -> Dict:
|
347 |
+
"""Get summer season transition information"""
|
348 |
+
if month == 6:
|
349 |
+
return {'phase': 'early_summer', 'characteristics': ['summer solstice', 'heat building']}
|
350 |
+
elif month == 7:
|
351 |
+
return {'phase': 'mid_summer', 'characteristics': ['peak heat', 'monsoon activity']}
|
352 |
+
else: # August
|
353 |
+
return {'phase': 'late_summer', 'characteristics': ['hurricane peak', 'heat dome potential']}
|
354 |
+
|
355 |
+
def _get_fall_transition_info(self, month: int) -> Dict:
|
356 |
+
"""Get fall season transition information"""
|
357 |
+
if month == 9:
|
358 |
+
return {'phase': 'early_fall', 'characteristics': ['autumn equinox', 'cooling trend']}
|
359 |
+
elif month == 10:
|
360 |
+
return {'phase': 'mid_fall', 'characteristics': ['temperature drop', 'first frost potential']}
|
361 |
+
else: # November
|
362 |
+
return {'phase': 'late_fall', 'characteristics': ['winter transition', 'storm systems']}
|
363 |
+
|
364 |
+
def _get_climatological_context(self, season: str, month: int) -> Dict:
|
365 |
+
"""Get climatological context for current conditions"""
|
366 |
+
climatology = {
|
367 |
+
'winter': {
|
368 |
+
'temperature_patterns': 'coldest period with arctic air masses',
|
369 |
+
'precipitation_patterns': 'snow in northern regions, rain in south',
|
370 |
+
'storm_systems': 'nor\'easters, alberta clippers, arctic fronts'
|
371 |
+
},
|
372 |
+
'spring': {
|
373 |
+
'temperature_patterns': 'rapid warming with daily temperature swings',
|
374 |
+
'precipitation_patterns': 'increased thunderstorm activity',
|
375 |
+
'storm_systems': 'severe thunderstorms, tornadoes, flooding'
|
376 |
+
},
|
377 |
+
'summer': {
|
378 |
+
'temperature_patterns': 'peak heat with heat dome potential',
|
379 |
+
'precipitation_patterns': 'monsoons, afternoon thunderstorms',
|
380 |
+
'storm_systems': 'hurricanes, derechos, heat waves'
|
381 |
+
},
|
382 |
+
'fall': {
|
383 |
+
'temperature_patterns': 'cooling trend with first frost',
|
384 |
+
'precipitation_patterns': 'transitional weather patterns',
|
385 |
+
'storm_systems': 'late hurricanes, early winter storms'
|
386 |
+
}
|
387 |
+
}
|
388 |
+
|
389 |
+
return climatology.get(season, {})
|
390 |
+
|
391 |
+
def _detect_regional_patterns(self, text: str) -> List[str]:
|
392 |
+
"""Detect regional climate pattern references"""
|
393 |
+
text_lower = text.lower()
|
394 |
+
|
395 |
+
regional_patterns = [
|
396 |
+
'marine layer', 'lake effect', 'orographic lift', 'rain shadow',
|
397 |
+
'urban heat island', 'sea breeze', 'land breeze', 'valley breeze',
|
398 |
+
'mountain breeze', 'chinook winds', 'santa ana winds',
|
399 |
+
'gulf stream', 'jet stream', 'bermuda high', 'pacific high'
|
400 |
+
]
|
401 |
+
|
402 |
+
detected = []
|
403 |
+
for pattern in regional_patterns:
|
404 |
+
if pattern in text_lower:
|
405 |
+
detected.append(pattern)
|
406 |
+
|
407 |
+
return detected
|
408 |
+
|
409 |
+
def _requires_expert_analysis(self, text: str) -> bool:
|
410 |
+
"""Determine if query requires expert meteorological analysis"""
|
411 |
+
text_lower = text.lower()
|
412 |
+
|
413 |
+
expert_indicators = [
|
414 |
+
'why', 'how', 'explain', 'analysis', 'pattern', 'trend', 'anomaly',
|
415 |
+
'unusual', 'abnormal', 'record', 'extreme', 'unprecedented',
|
416 |
+
'climate change', 'long term', 'statistical', 'probability'
|
417 |
+
]
|
418 |
+
|
419 |
+
return any(indicator in text_lower for indicator in expert_indicators)
|
420 |
+
|
421 |
+
def _identify_climate_relationships(self, text: str) -> List[str]:
|
422 |
+
"""Identify climate variable relationships mentioned"""
|
423 |
+
text_lower = text.lower()
|
424 |
+
|
425 |
+
relationships = {
|
426 |
+
'temperature_humidity': ['heat index', 'feels like', 'apparent temperature'],
|
427 |
+
'temperature_wind': ['wind chill', 'cooling effect'],
|
428 |
+
'pressure_weather': ['high pressure clear', 'low pressure storms'],
|
429 |
+
'elevation_temperature': ['altitude effect', 'elevation cooling'],
|
430 |
+
'ocean_climate': ['sea surface temperature', 'coastal climate'],
|
431 |
+
'latitude_climate': ['polar', 'tropical', 'temperate zones']
|
432 |
+
}
|
433 |
+
|
434 |
+
detected = []
|
435 |
+
for relationship, keywords in relationships.items():
|
436 |
+
if any(keyword in text_lower for keyword in keywords):
|
437 |
+
detected.append(relationship)
|
438 |
+
|
439 |
+
return detected
|
440 |
+
|
441 |
+
def _assess_forecast_complexity(self, text: str) -> str:
|
442 |
+
"""Assess the complexity level needed for forecast response"""
|
443 |
+
text_lower = text.lower()
|
444 |
+
|
445 |
+
# Complex forecast indicators
|
446 |
+
complex_indicators = [
|
447 |
+
'detailed', 'hourly', 'hour by hour', 'breakdown', 'analysis',
|
448 |
+
'trend', 'pattern', 'model', 'ensemble', 'uncertainty'
|
449 |
+
]
|
450 |
+
|
451 |
+
# Extended forecast indicators
|
452 |
+
extended_indicators = [
|
453 |
+
'week', 'month', 'season', 'long term', 'extended', 'outlook'
|
454 |
+
]
|
455 |
+
|
456 |
+
if any(indicator in text_lower for indicator in complex_indicators):
|
457 |
+
return 'complex'
|
458 |
+
elif any(indicator in text_lower for indicator in extended_indicators):
|
459 |
+
return 'extended'
|
460 |
+
else:
|
461 |
+
return 'standard'
|
462 |
+
|
463 |
+
def enhance_query_processing(self, basic_analysis: Dict, text: str) -> Dict:
|
464 |
+
"""Enhance basic query processing with climate expertise"""
|
465 |
+
climate_intelligence = self.extract_climate_intelligence(text)
|
466 |
+
|
467 |
+
# Add climate expertise to basic analysis
|
468 |
+
enhanced_analysis = basic_analysis.copy()
|
469 |
+
enhanced_analysis.update({
|
470 |
+
'climate_intelligence': climate_intelligence,
|
471 |
+
'expert_context': self._build_expert_context(basic_analysis, climate_intelligence),
|
472 |
+
'response_sophistication': self._determine_response_sophistication(climate_intelligence),
|
473 |
+
'regional_climate_context': self._add_regional_context(basic_analysis.get('cities', [])),
|
474 |
+
'meteorological_insights': self._generate_meteorological_insights(basic_analysis, climate_intelligence)
|
475 |
+
})
|
476 |
+
|
477 |
+
return enhanced_analysis
|
478 |
+
|
479 |
+
def _build_expert_context(self, basic_analysis: Dict, climate_intelligence: Dict) -> Dict:
|
480 |
+
"""Build expert context for response generation"""
|
481 |
+
cities = basic_analysis.get('cities', [])
|
482 |
+
query_type = basic_analysis.get('query_type', 'general')
|
483 |
+
|
484 |
+
expert_context = {
|
485 |
+
'requires_technical_explanation': climate_intelligence.get('expertise_level') in ['intermediate', 'expert'],
|
486 |
+
'include_meteorological_background': climate_intelligence.get('expert_analysis_needed', False),
|
487 |
+
'seasonal_considerations': climate_intelligence.get('seasonal_context', {}),
|
488 |
+
'regional_climate_factors': self._get_regional_factors(cities),
|
489 |
+
'phenomenon_explanations': climate_intelligence.get('climate_phenomena', []),
|
490 |
+
'forecast_confidence_level': self._assess_forecast_confidence(basic_analysis, climate_intelligence)
|
491 |
+
}
|
492 |
+
|
493 |
+
return expert_context
|
494 |
+
|
495 |
+
def _determine_response_sophistication(self, climate_intelligence: Dict) -> str:
|
496 |
+
"""Determine the sophistication level needed for response"""
|
497 |
+
expertise_level = climate_intelligence.get('expertise_level', 'basic')
|
498 |
+
phenomena_count = len(climate_intelligence.get('climate_phenomena', []))
|
499 |
+
concepts_count = len(climate_intelligence.get('meteorological_concepts', []))
|
500 |
+
|
501 |
+
if expertise_level == 'expert' or phenomena_count > 2 or concepts_count > 2:
|
502 |
+
return 'expert'
|
503 |
+
elif expertise_level == 'intermediate' or phenomena_count > 0 or concepts_count > 0:
|
504 |
+
return 'intermediate'
|
505 |
+
else:
|
506 |
+
return 'basic'
|
507 |
+
|
508 |
+
def _add_regional_context(self, cities: List[str]) -> Dict:
|
509 |
+
"""Add regional climate context for cities"""
|
510 |
+
regional_context = {}
|
511 |
+
|
512 |
+
for city in cities:
|
513 |
+
city_lower = city.lower()
|
514 |
+
if city_lower in self.cities_with_climate_context:
|
515 |
+
city_info = self.cities_with_climate_context[city_lower]
|
516 |
+
regional_context[city] = {
|
517 |
+
'climate_type': city_info.get('climate', 'unknown'),
|
518 |
+
'regional_patterns': self.regional_climate_patterns.get(city_info.get('region', ''), {}),
|
519 |
+
'elevation_effects': self._get_elevation_effects(city_info.get('elevation', 0)),
|
520 |
+
'seasonal_characteristics': self._get_city_seasonal_characteristics(city_info)
|
521 |
+
}
|
522 |
+
|
523 |
+
return regional_context
|
524 |
+
|
525 |
+
def _get_elevation_effects(self, elevation: int) -> Dict:
|
526 |
+
"""Get elevation effects on weather"""
|
527 |
+
if elevation > 5000:
|
528 |
+
return {
|
529 |
+
'temperature_effect': 'significantly cooler due to elevation',
|
530 |
+
'precipitation_effect': 'orographic enhancement possible',
|
531 |
+
'pressure_effect': 'lower atmospheric pressure'
|
532 |
+
}
|
533 |
+
elif elevation > 2000:
|
534 |
+
return {
|
535 |
+
'temperature_effect': 'moderately cooler due to elevation',
|
536 |
+
'precipitation_effect': 'some orographic effects',
|
537 |
+
'pressure_effect': 'slightly lower pressure'
|
538 |
+
}
|
539 |
+
else:
|
540 |
+
return {
|
541 |
+
'temperature_effect': 'minimal elevation effects',
|
542 |
+
'precipitation_effect': 'standard precipitation patterns',
|
543 |
+
'pressure_effect': 'near sea level pressure'
|
544 |
+
}
|
545 |
+
|
546 |
+
def _get_city_seasonal_characteristics(self, city_info: Dict) -> Dict:
|
547 |
+
"""Get seasonal characteristics for a city"""
|
548 |
+
region = city_info.get('region', '')
|
549 |
+
climate = city_info.get('climate', '')
|
550 |
+
|
551 |
+
if region in self.regional_climate_patterns:
|
552 |
+
return self.regional_climate_patterns[region]
|
553 |
+
else:
|
554 |
+
return {'characteristics': [climate], 'phenomena': [], 'seasons': []}
|
555 |
+
|
556 |
+
def _generate_meteorological_insights(self, basic_analysis: Dict, climate_intelligence: Dict) -> List[str]:
|
557 |
+
"""Generate meteorological insights for expert responses"""
|
558 |
+
insights = []
|
559 |
+
|
560 |
+
# Add insights based on query type and climate intelligence
|
561 |
+
query_type = basic_analysis.get('query_type', 'general')
|
562 |
+
phenomena = climate_intelligence.get('climate_phenomena', [])
|
563 |
+
concepts = climate_intelligence.get('meteorological_concepts', [])
|
564 |
+
|
565 |
+
if 'temperature_analysis' in query_type:
|
566 |
+
insights.append('Consider thermal gradient effects and heat/cold transport mechanisms')
|
567 |
+
|
568 |
+
if 'el_nino' in phenomena or 'la_nina' in phenomena:
|
569 |
+
insights.append('ENSO phase impacts global weather patterns and regional climate')
|
570 |
+
|
571 |
+
if 'pressure_systems' in concepts:
|
572 |
+
insights.append('Pressure gradient forces drive wind patterns and weather system movement')
|
573 |
+
|
574 |
+
if climate_intelligence.get('expertise_level') == 'expert':
|
575 |
+
insights.append('Provide detailed meteorological explanation with atmospheric dynamics')
|
576 |
+
|
577 |
+
return insights
|
578 |
+
|
579 |
+
def _get_regional_factors(self, cities: List[str]) -> Dict:
|
580 |
+
"""Get regional climate factors for cities"""
|
581 |
+
factors = {}
|
582 |
+
|
583 |
+
for city in cities:
|
584 |
+
city_lower = city.lower()
|
585 |
+
if city_lower in self.cities_with_climate_context:
|
586 |
+
city_info = self.cities_with_climate_context[city_lower]
|
587 |
+
region = city_info.get('region', '')
|
588 |
+
|
589 |
+
if region in self.regional_climate_patterns:
|
590 |
+
factors[city] = self.regional_climate_patterns[region]
|
591 |
+
|
592 |
+
return factors
|
593 |
+
|
594 |
+
def _assess_forecast_confidence(self, basic_analysis: Dict, climate_intelligence: Dict) -> str:
|
595 |
+
"""Assess forecast confidence based on complexity"""
|
596 |
+
forecast_context = basic_analysis.get('forecast_context', {})
|
597 |
+
complexity = climate_intelligence.get('forecast_complexity', 'standard')
|
598 |
+
|
599 |
+
if complexity == 'complex' or len(climate_intelligence.get('climate_phenomena', [])) > 1:
|
600 |
+
return 'moderate' # Complex phenomena reduce confidence
|
601 |
+
elif forecast_context.get('timeline') == 'long_term':
|
602 |
+
return 'low' # Long-term forecasts have lower confidence
|
603 |
+
else:
|
604 |
+
return 'high' # Standard forecasts have high confidence
|
605 |
+
|
606 |
+
def create_climate_expert_nlp() -> ClimateExpertNLP:
|
607 |
+
"""Factory function to create climate expert NLP processor"""
|
608 |
+
return ClimateExpertNLP()
|
src/chatbot/enhanced_chatbot.py
ADDED
@@ -0,0 +1,962 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
Enhanced AI-Powered Chatbot with LlamaIndex and Gemini Integration
|
3 |
+
Full LLM integration for intelligent weather conversations
|
4 |
+
"""
|
5 |
+
|
6 |
+
import os
|
7 |
+
import logging
|
8 |
+
import asyncio
|
9 |
+
from typing import Dict, List, Optional, Any
|
10 |
+
from datetime import datetime
|
11 |
+
|
12 |
+
# Import enhanced climate expert components
|
13 |
+
from .climate_expert_nlp import ClimateExpertNLP, create_climate_expert_nlp
|
14 |
+
from .weather_knowledge_base import WeatherKnowledgeBase, create_weather_knowledge_base
|
15 |
+
|
16 |
+
# LlamaIndex imports
|
17 |
+
try:
|
18 |
+
from llama_index.core import VectorStoreIndex, Document, Settings
|
19 |
+
from llama_index.core.memory import ChatMemoryBuffer
|
20 |
+
from llama_index.core.chat_engine import SimpleChatEngine
|
21 |
+
from llama_index.llms.gemini import Gemini
|
22 |
+
from llama_index.embeddings.gemini import GeminiEmbedding
|
23 |
+
LLAMA_INDEX_AVAILABLE = True
|
24 |
+
except ImportError:
|
25 |
+
LLAMA_INDEX_AVAILABLE = False
|
26 |
+
|
27 |
+
# Google Gemini imports
|
28 |
+
try:
|
29 |
+
import google.generativeai as genai
|
30 |
+
GEMINI_AVAILABLE = True
|
31 |
+
except ImportError:
|
32 |
+
GEMINI_AVAILABLE = False
|
33 |
+
|
34 |
+
logger = logging.getLogger(__name__)
|
35 |
+
|
36 |
+
class EnhancedWeatherChatbot:
|
37 |
+
"""AI-powered weather chatbot with LlamaIndex and Gemini integration"""
|
38 |
+
|
39 |
+
def __init__(self, weather_client, nlp_processor, gemini_api_key: str = None):
|
40 |
+
"""Initialize the enhanced chatbot with climate expertise"""
|
41 |
+
self.weather_client = weather_client
|
42 |
+
self.nlp_processor = nlp_processor
|
43 |
+
self.gemini_api_key = gemini_api_key or os.getenv("GEMINI_API_KEY")
|
44 |
+
|
45 |
+
# Initialize climate expert components
|
46 |
+
self.climate_expert_nlp = create_climate_expert_nlp()
|
47 |
+
self.weather_knowledge_base = create_weather_knowledge_base()
|
48 |
+
|
49 |
+
# Initialize LlamaIndex components
|
50 |
+
self.llm = None
|
51 |
+
self.chat_engine = None
|
52 |
+
self.memory = ChatMemoryBuffer.from_defaults(token_limit=3000)
|
53 |
+
self.weather_knowledge_base_docs = []
|
54 |
+
|
55 |
+
# Enhanced weather context for AI with climate expertise
|
56 |
+
self.weather_context = """
|
57 |
+
You are an expert climate scientist and meteorologist with comprehensive knowledge of weather patterns,
|
58 |
+
atmospheric dynamics, and climate science. You have access to real-time weather data from the National Weather Service
|
59 |
+
and extensive meteorological expertise.
|
60 |
+
|
61 |
+
Your capabilities include:
|
62 |
+
- Advanced weather analysis and interpretation of atmospheric phenomena
|
63 |
+
- Expert-level explanations of meteorological processes and climate patterns
|
64 |
+
- Regional climate expertise and seasonal pattern recognition
|
65 |
+
- Climate change impacts and long-term trend analysis
|
66 |
+
- Severe weather forecasting and risk assessment
|
67 |
+
- Agricultural and seasonal weather impacts
|
68 |
+
- Historical weather pattern analysis and climatological context
|
69 |
+
- ENSO (El Niño/La Niña) and other climate oscillation impacts
|
70 |
+
- Activity-specific weather advice with safety considerations
|
71 |
+
|
72 |
+
When responding to weather queries:
|
73 |
+
1. Provide accurate, real-time weather information
|
74 |
+
2. Include relevant meteorological explanations and context
|
75 |
+
3. Explain atmospheric processes and weather phenomena
|
76 |
+
4. Consider regional climate patterns and seasonal effects
|
77 |
+
5. Relate current conditions to historical norms and climate patterns
|
78 |
+
6. Provide expert insights on weather relationships and causes
|
79 |
+
7. Include seasonal and climatological context when relevant
|
80 |
+
8. Assess and communicate weather-related risks and safety considerations
|
81 |
+
9. For complex queries, provide detailed meteorological analysis
|
82 |
+
10. Connect local weather to larger-scale atmospheric patterns
|
83 |
+
|
84 |
+
Expertise Areas:
|
85 |
+
- Synoptic and mesoscale meteorology
|
86 |
+
- Climate patterns and oscillations (ENSO, PDO, NAO, etc.)
|
87 |
+
- Extreme weather events and their formation
|
88 |
+
- Regional climatology and seasonal patterns
|
89 |
+
- Weather forecasting uncertainty and model interpretation
|
90 |
+
- Climate change impacts on weather patterns
|
91 |
+
- Agricultural meteorology and seasonal planning
|
92 |
+
- Weather safety and preparedness
|
93 |
+
|
94 |
+
Always maintain scientific accuracy while making complex concepts accessible to users.
|
95 |
+
"""
|
96 |
+
|
97 |
+
# Setup AI components
|
98 |
+
self._setup_ai_components()
|
99 |
+
|
100 |
+
def _setup_ai_components(self):
|
101 |
+
"""Setup LlamaIndex and Gemini components"""
|
102 |
+
try:
|
103 |
+
if not self.gemini_api_key:
|
104 |
+
logger.warning("No Gemini API key provided. Using basic mode.")
|
105 |
+
return
|
106 |
+
|
107 |
+
if GEMINI_AVAILABLE:
|
108 |
+
# Configure Gemini
|
109 |
+
genai.configure(api_key=self.gemini_api_key)
|
110 |
+
|
111 |
+
if LLAMA_INDEX_AVAILABLE and self.gemini_api_key:
|
112 |
+
# Setup LlamaIndex with Gemini 2.0 Flash
|
113 |
+
self.llm = Gemini(
|
114 |
+
api_key=self.gemini_api_key,
|
115 |
+
model="models/gemini-2.0-flash-exp",
|
116 |
+
temperature=0.7,
|
117 |
+
max_tokens=1000
|
118 |
+
)
|
119 |
+
|
120 |
+
# Configure global settings
|
121 |
+
Settings.llm = self.llm
|
122 |
+
Settings.embed_model = GeminiEmbedding(
|
123 |
+
api_key=self.gemini_api_key,
|
124 |
+
model_name="models/embedding-001"
|
125 |
+
)
|
126 |
+
|
127 |
+
# Create weather knowledge base
|
128 |
+
self._build_weather_knowledge_base()
|
129 |
+
|
130 |
+
logger.info("LlamaIndex with Gemini configured successfully")
|
131 |
+
else:
|
132 |
+
logger.warning("LlamaIndex not available. Using direct Gemini API.")
|
133 |
+
|
134 |
+
except Exception as e:
|
135 |
+
logger.error(f"Error setting up AI components: {e}")
|
136 |
+
|
137 |
+
def _build_weather_knowledge_base(self):
|
138 |
+
"""Build comprehensive weather knowledge base for LlamaIndex"""
|
139 |
+
try:
|
140 |
+
weather_docs = [
|
141 |
+
Document(text=self.weather_context),
|
142 |
+
Document(text="""
|
143 |
+
Advanced Weather Forecast Interpretation:
|
144 |
+
- Temperature: Measured in Fahrenheit for US locations, consider heat index and wind chill
|
145 |
+
- Precipitation Probability: Percentage chance of measurable precipitation (≥0.01 inches)
|
146 |
+
- Wind Speed: Measured in mph, includes sustained winds and gusts
|
147 |
+
- Wind Direction: From direction (e.g., SW means wind coming from southwest)
|
148 |
+
- Conditions: Detailed forecast including cloud cover, precipitation type, visibility
|
149 |
+
- Pressure: Atmospheric pressure in inches of mercury, indicates weather system movement
|
150 |
+
|
151 |
+
Meteorological Analysis Guidelines:
|
152 |
+
- Temperature differences of 5°F+ are noticeable, 15°F+ are significant
|
153 |
+
- Precipitation probability >30% indicates likely precipitation
|
154 |
+
- Wind speeds >15 mph are noticeable, >25 mph can affect activities
|
155 |
+
- Pressure falling indicates approaching weather systems
|
156 |
+
- Always consider seasonal and regional climatological context
|
157 |
+
"""),
|
158 |
+
Document(text="""
|
159 |
+
Regional Climate Patterns and Characteristics:
|
160 |
+
|
161 |
+
Pacific Northwest: Marine west coast climate
|
162 |
+
- Wet winters (Oct-Apr) with frequent Pacific storms
|
163 |
+
- Dry summers (May-Sep) with pleasant temperatures
|
164 |
+
- Orographic precipitation enhancement in mountains
|
165 |
+
- Rain shadow effects east of Cascades
|
166 |
+
- Marine influence moderates temperatures
|
167 |
+
|
168 |
+
Southwest Desert: Hot arid climate
|
169 |
+
- Extreme heat in summer with low humidity
|
170 |
+
- Mild winters with minimal precipitation
|
171 |
+
- Summer monsoon season (Jul-Sep) brings thunderstorms
|
172 |
+
- Large diurnal temperature ranges
|
173 |
+
- Flash flood risks during monsoon
|
174 |
+
|
175 |
+
Great Plains: Continental climate
|
176 |
+
- Large seasonal temperature ranges
|
177 |
+
- Severe weather season (Mar-Jun) with tornadoes
|
178 |
+
- Variable precipitation with drought potential
|
179 |
+
- Chinook winds in northern areas
|
180 |
+
- Strong pressure gradients drive weather systems
|
181 |
+
|
182 |
+
Southeast: Humid subtropical climate
|
183 |
+
- Hot, humid summers with afternoon thunderstorms
|
184 |
+
- Mild winters with occasional cold snaps
|
185 |
+
- Hurricane season (Jun-Nov) peak activity Aug-Oct
|
186 |
+
- High humidity increases heat index values
|
187 |
+
- Frontal systems bring weather changes
|
188 |
+
|
189 |
+
Northeast: Humid continental climate
|
190 |
+
- Four distinct seasons with variable weather
|
191 |
+
- Nor'easter storms in winter bring heavy snow
|
192 |
+
- Lake effect snow in Great Lakes region
|
193 |
+
- Tropical systems can affect area in late summer
|
194 |
+
- Strong seasonal temperature contrasts
|
195 |
+
"""),
|
196 |
+
Document(text="""
|
197 |
+
Climate Oscillations and Their Weather Impacts:
|
198 |
+
|
199 |
+
El Niño Southern Oscillation (ENSO):
|
200 |
+
El Niño Phase:
|
201 |
+
- Warmer, wetter winters in southern US
|
202 |
+
- Reduced Atlantic hurricane activity
|
203 |
+
- Milder winters in northern US
|
204 |
+
- Increased precipitation in California
|
205 |
+
- Warmer global temperatures
|
206 |
+
|
207 |
+
La Niña Phase:
|
208 |
+
- Cooler, drier winters in southern US
|
209 |
+
- Increased Atlantic hurricane activity
|
210 |
+
- Harsher winters in northern US
|
211 |
+
- Drought conditions in Southwest
|
212 |
+
- Cooler global temperatures
|
213 |
+
|
214 |
+
Pacific Decadal Oscillation (PDO):
|
215 |
+
- 20-30 year cycles affecting Pacific Basin
|
216 |
+
- Influences long-term precipitation patterns
|
217 |
+
- Affects marine ecosystems and weather
|
218 |
+
|
219 |
+
North Atlantic Oscillation (NAO):
|
220 |
+
- Affects European and eastern US weather
|
221 |
+
- Positive phase: mild, wet European winters
|
222 |
+
- Negative phase: cold European winters, eastern US storms
|
223 |
+
|
224 |
+
These oscillations interact to create complex weather patterns.
|
225 |
+
Always consider ENSO phase when analyzing seasonal forecasts.
|
226 |
+
"""),
|
227 |
+
Document(text="""
|
228 |
+
Severe Weather Formation and Safety:
|
229 |
+
|
230 |
+
Thunderstorms:
|
231 |
+
Formation: Instability + moisture + lifting mechanism
|
232 |
+
Types: Single-cell, multi-cell, supercell
|
233 |
+
Hazards: Lightning, hail, damaging winds, flooding, tornadoes
|
234 |
+
Safety: Seek substantial shelter, avoid windows, wait 30 minutes after last thunder
|
235 |
+
|
236 |
+
Tornadoes:
|
237 |
+
Formation: Wind shear + instability + low-level rotation
|
238 |
+
Scale: EF0 (light damage) to EF5 (incredible destruction)
|
239 |
+
Peak season: April-June in late afternoon/early evening
|
240 |
+
Safety: Lowest floor, interior room, cover yourself
|
241 |
+
|
242 |
+
Hurricanes:
|
243 |
+
Formation: Warm ocean water >80°F + low wind shear + disturbance
|
244 |
+
Hazards: Storm surge (primary killer), winds, flooding, tornadoes
|
245 |
+
Categories: 1 (minimal) to 5 (catastrophic) based on wind speed
|
246 |
+
Safety: Evacuate if ordered, never drive through flooded roads
|
247 |
+
|
248 |
+
Winter Storms:
|
249 |
+
Types: Blizzards, ice storms, nor'easters
|
250 |
+
Hazards: Heavy snow, ice accumulation, wind chill, power outages
|
251 |
+
Formation: Cold air + moisture + storm system
|
252 |
+
Safety: Emergency supplies, avoid travel, carbon monoxide awareness
|
253 |
+
"""),
|
254 |
+
Document(text="""
|
255 |
+
Seasonal Weather Patterns and Climate Context:
|
256 |
+
|
257 |
+
Spring (March-May):
|
258 |
+
- Rapid warming with large temperature swings
|
259 |
+
- Peak severe weather season due to strong temperature contrasts
|
260 |
+
- Increasing thunderstorm activity
|
261 |
+
- Transition from winter to summer circulation patterns
|
262 |
+
- Flooding potential from snowmelt and spring rains
|
263 |
+
|
264 |
+
Summer (June-August):
|
265 |
+
- Peak heat with minimal daily temperature variation
|
266 |
+
- Convective afternoon/evening thunderstorms
|
267 |
+
- Hurricane season development
|
268 |
+
- Monsoon activity in Southwest
|
269 |
+
- Heat dome and heat wave potential
|
270 |
+
|
271 |
+
Fall (September-November):
|
272 |
+
- Gradual cooling with increasing day-to-day variability
|
273 |
+
- Peak hurricane season (August-October)
|
274 |
+
- First frost and end of growing season
|
275 |
+
- Beautiful foliage season in deciduous regions
|
276 |
+
- Transition to winter storm patterns
|
277 |
+
|
278 |
+
Winter (December-February):
|
279 |
+
- Coldest temperatures with arctic air mass intrusions
|
280 |
+
- Snow season in northern regions
|
281 |
+
- Nor'easters and other winter storm systems
|
282 |
+
- Polar vortex displacement events
|
283 |
+
- Shortest days with limited solar heating
|
284 |
+
|
285 |
+
Each season has distinct weather patterns influenced by solar angle,
|
286 |
+
jet stream position, and large-scale circulation patterns.
|
287 |
+
""")
|
288 |
+
]
|
289 |
+
|
290 |
+
# Add knowledge base content as documents
|
291 |
+
phenomenal_knowledge = self.weather_knowledge_base.meteorological_knowledge
|
292 |
+
climate_patterns = self.weather_knowledge_base.climate_patterns
|
293 |
+
|
294 |
+
# Convert knowledge base content to documents
|
295 |
+
for category, content in phenomenal_knowledge.items():
|
296 |
+
weather_docs.append(Document(text=f"Meteorological Knowledge - {category}: {str(content)}"))
|
297 |
+
|
298 |
+
for pattern_type, patterns in climate_patterns.items():
|
299 |
+
weather_docs.append(Document(text=f"Climate Patterns - {pattern_type}: {str(patterns)}"))
|
300 |
+
|
301 |
+
# Create vector index
|
302 |
+
if Settings.embed_model:
|
303 |
+
self.index = VectorStoreIndex.from_documents(weather_docs)
|
304 |
+
self.chat_engine = self.index.as_chat_engine(
|
305 |
+
chat_mode="context",
|
306 |
+
memory=self.memory,
|
307 |
+
system_prompt=self.weather_context
|
308 |
+
)
|
309 |
+
logger.info("Enhanced weather knowledge base created successfully")
|
310 |
+
|
311 |
+
except Exception as e:
|
312 |
+
logger.error(f"Error building enhanced knowledge base: {e}")
|
313 |
+
|
314 |
+
async def process_weather_query(self, user_message: str, chat_history: List = None) -> Dict:
|
315 |
+
"""Process weather query with enhanced climate expertise and AI integration"""
|
316 |
+
try:
|
317 |
+
# Update conversation memory with chat history if provided
|
318 |
+
if chat_history and self.memory:
|
319 |
+
await self._update_conversation_memory(chat_history)
|
320 |
+
|
321 |
+
# Parse the query using basic NLP
|
322 |
+
basic_analysis = self.nlp_processor.process_query(user_message)
|
323 |
+
|
324 |
+
# Enhance with climate expert analysis
|
325 |
+
enhanced_analysis = self.climate_expert_nlp.enhance_query_processing(basic_analysis, user_message)
|
326 |
+
|
327 |
+
cities = enhanced_analysis.get('cities', [])
|
328 |
+
query_type = enhanced_analysis.get('query_type', 'general')
|
329 |
+
is_comparison = enhanced_analysis.get('comparison_info', {}).get('is_comparison', False)
|
330 |
+
climate_intelligence = enhanced_analysis.get('climate_intelligence', {})
|
331 |
+
|
332 |
+
# Get weather data for mentioned cities
|
333 |
+
weather_data = {}
|
334 |
+
climate_analysis_data = {}
|
335 |
+
|
336 |
+
for city in cities:
|
337 |
+
coords = self.weather_client.geocode_location(city)
|
338 |
+
if coords:
|
339 |
+
lat, lon = coords
|
340 |
+
forecast = self.weather_client.get_forecast(lat, lon)
|
341 |
+
current_obs = self.weather_client.get_current_observations(lat, lon)
|
342 |
+
|
343 |
+
weather_data[city] = {
|
344 |
+
'coordinates': coords,
|
345 |
+
'forecast': forecast,
|
346 |
+
'current': current_obs
|
347 |
+
}
|
348 |
+
|
349 |
+
# Add climate analysis if needed
|
350 |
+
if enhanced_analysis.get('expert_context', {}).get('requires_technical_explanation'):
|
351 |
+
climate_analysis_data[city] = await self._get_climate_analysis(city, enhanced_analysis)
|
352 |
+
|
353 |
+
# Generate enhanced AI response with climate expertise
|
354 |
+
ai_response = await self._generate_enhanced_ai_response(
|
355 |
+
user_message, weather_data, enhanced_analysis, climate_analysis_data, chat_history
|
356 |
+
)
|
357 |
+
|
358 |
+
# Prepare map data
|
359 |
+
map_data = self._prepare_map_data(cities, weather_data)
|
360 |
+
|
361 |
+
return {
|
362 |
+
'response': ai_response,
|
363 |
+
'cities': cities,
|
364 |
+
'weather_data': weather_data,
|
365 |
+
'climate_analysis': climate_analysis_data,
|
366 |
+
'map_data': map_data,
|
367 |
+
'query_analysis': enhanced_analysis,
|
368 |
+
'climate_intelligence': climate_intelligence,
|
369 |
+
'map_update_needed': len(cities) > 0,
|
370 |
+
'comparison_mode': is_comparison,
|
371 |
+
'expertise_level': climate_intelligence.get('expertise_level', 'basic'),
|
372 |
+
'requires_expert_explanation': enhanced_analysis.get('expert_context', {}).get('requires_technical_explanation', False)
|
373 |
+
}
|
374 |
+
|
375 |
+
except Exception as e:
|
376 |
+
logger.error(f"Error processing weather query: {e}")
|
377 |
+
return {
|
378 |
+
'response': f"I apologize, but I encountered an error processing your weather query: {str(e)}",
|
379 |
+
'cities': [],
|
380 |
+
'weather_data': {},
|
381 |
+
'climate_analysis': {},
|
382 |
+
'map_data': [],
|
383 |
+
'query_analysis': {},
|
384 |
+
'climate_intelligence': {},
|
385 |
+
'map_update_needed': False,
|
386 |
+
'comparison_mode': False,
|
387 |
+
'expertise_level': 'basic',
|
388 |
+
'requires_expert_explanation': False
|
389 |
+
}
|
390 |
+
|
391 |
+
async def _update_conversation_memory(self, chat_history: List) -> None:
|
392 |
+
"""Update LlamaIndex memory with recent chat history"""
|
393 |
+
try:
|
394 |
+
if not self.memory or not chat_history:
|
395 |
+
return
|
396 |
+
|
397 |
+
# Process recent conversation history (last 6 messages to avoid token limits)
|
398 |
+
recent_history = chat_history[-6:]
|
399 |
+
|
400 |
+
for entry in recent_history:
|
401 |
+
if isinstance(entry, dict):
|
402 |
+
# Handle messages format: {"role": "user/assistant", "content": "..."}
|
403 |
+
role = entry.get('role', '')
|
404 |
+
content = entry.get('content', '')
|
405 |
+
|
406 |
+
if role == 'user' and content:
|
407 |
+
# Add user message to memory
|
408 |
+
self.memory.put(f"User: {content}")
|
409 |
+
elif role == 'assistant' and content:
|
410 |
+
# Add assistant message to memory
|
411 |
+
self.memory.put(f"Assistant: {content}")
|
412 |
+
|
413 |
+
elif isinstance(entry, list) and len(entry) >= 2:
|
414 |
+
# Handle legacy format: [user_message, assistant_response]
|
415 |
+
user_msg, assistant_msg = entry[0], entry[1]
|
416 |
+
if user_msg:
|
417 |
+
self.memory.put(f"User: {user_msg}")
|
418 |
+
if assistant_msg:
|
419 |
+
self.memory.put(f"Assistant: {assistant_msg}")
|
420 |
+
|
421 |
+
logger.debug(f"Updated conversation memory with {len(recent_history)} recent messages")
|
422 |
+
|
423 |
+
except Exception as e:
|
424 |
+
logger.error(f"Error updating conversation memory: {e}")
|
425 |
+
|
426 |
+
def _extract_conversation_context(self, chat_history: List) -> str:
|
427 |
+
"""Extract relevant context from conversation history for continuity"""
|
428 |
+
try:
|
429 |
+
if not chat_history:
|
430 |
+
return ""
|
431 |
+
|
432 |
+
# Look for recent cities and topics in conversation
|
433 |
+
recent_cities = []
|
434 |
+
recent_topics = []
|
435 |
+
|
436 |
+
# Check last few messages for context
|
437 |
+
for entry in reversed(chat_history[-4:]):
|
438 |
+
if isinstance(entry, dict):
|
439 |
+
content = entry.get('content', '')
|
440 |
+
elif isinstance(entry, list) and len(entry) >= 1:
|
441 |
+
content = entry[0] # User message
|
442 |
+
else:
|
443 |
+
continue
|
444 |
+
|
445 |
+
# Extract cities mentioned
|
446 |
+
analysis = self.nlp_processor.process_query(content)
|
447 |
+
cities = analysis.get('cities', [])
|
448 |
+
if cities:
|
449 |
+
recent_cities.extend(cities)
|
450 |
+
|
451 |
+
# Extract weather topics
|
452 |
+
content_lower = content.lower()
|
453 |
+
if any(topic in content_lower for topic in ['temperature', 'rain', 'weather', 'forecast', 'conditions']):
|
454 |
+
recent_topics.append(content[:100]) # First 100 chars
|
455 |
+
|
456 |
+
# Build context summary
|
457 |
+
context_parts = []
|
458 |
+
|
459 |
+
if recent_cities:
|
460 |
+
unique_cities = []
|
461 |
+
for city in recent_cities:
|
462 |
+
if city not in unique_cities:
|
463 |
+
unique_cities.append(city)
|
464 |
+
context_parts.append(f"Recently discussed cities: {', '.join(unique_cities[:3])}")
|
465 |
+
|
466 |
+
if recent_topics:
|
467 |
+
context_parts.append(f"Recent conversation topics: {'; '.join(recent_topics[-2:])}")
|
468 |
+
|
469 |
+
return '; '.join(context_parts) if context_parts else ""
|
470 |
+
|
471 |
+
except Exception as e:
|
472 |
+
logger.error(f"Error extracting conversation context: {e}")
|
473 |
+
return ""
|
474 |
+
|
475 |
+
def _format_weather_context(self, weather_data: Dict, query_analysis: Dict) -> str:
|
476 |
+
"""Format weather data for AI context with rich details"""
|
477 |
+
if not weather_data:
|
478 |
+
return "No weather data available."
|
479 |
+
|
480 |
+
context_parts = []
|
481 |
+
|
482 |
+
for city, data in weather_data.items():
|
483 |
+
forecast = data.get('forecast', [])
|
484 |
+
current = data.get('current', {})
|
485 |
+
|
486 |
+
city_context = f"\n{city.title()}:"
|
487 |
+
|
488 |
+
if forecast:
|
489 |
+
current_period = forecast[0]
|
490 |
+
temp = current_period.get('temperature', 'N/A')
|
491 |
+
temp_unit = current_period.get('temperatureUnit', 'F')
|
492 |
+
conditions = current_period.get('shortForecast', 'N/A')
|
493 |
+
wind_speed = current_period.get('windSpeed', 'N/A')
|
494 |
+
wind_dir = current_period.get('windDirection', '')
|
495 |
+
precip = current_period.get('precipitationProbability', 0)
|
496 |
+
|
497 |
+
city_context += f"""
|
498 |
+
- Current Temperature: {temp}°{temp_unit}
|
499 |
+
- Conditions: {conditions}
|
500 |
+
- Wind: {wind_speed} {wind_dir}
|
501 |
+
- Precipitation Chance: {precip}%
|
502 |
+
- Detailed Forecast: {current_period.get('detailedForecast', 'N/A')[:200]}...
|
503 |
+
"""
|
504 |
+
|
505 |
+
# Add next few periods for context
|
506 |
+
if len(forecast) > 1:
|
507 |
+
city_context += "\n Next periods:"
|
508 |
+
for i, period in enumerate(forecast[1:4], 1):
|
509 |
+
name = period.get('name', f'Period {i+1}')
|
510 |
+
temp = period.get('temperature', 'N/A')
|
511 |
+
conditions = period.get('shortForecast', 'N/A')
|
512 |
+
city_context += f"\n - {name}: {temp}°F, {conditions}"
|
513 |
+
|
514 |
+
if current:
|
515 |
+
temp_c = current.get('temperature')
|
516 |
+
if temp_c:
|
517 |
+
temp_f = (temp_c * 9/5) + 32
|
518 |
+
city_context += f"\n- Observed Temperature: {temp_f:.1f}°F"
|
519 |
+
|
520 |
+
humidity = current.get('relativeHumidity', {})
|
521 |
+
if isinstance(humidity, dict):
|
522 |
+
humidity_val = humidity.get('value')
|
523 |
+
if humidity_val:
|
524 |
+
city_context += f"\n- Humidity: {humidity_val}%"
|
525 |
+
|
526 |
+
context_parts.append(city_context)
|
527 |
+
|
528 |
+
return "\n".join(context_parts)
|
529 |
+
|
530 |
+
async def _generate_ai_response(self, user_message: str, weather_data: Dict, query_analysis: Dict, chat_history: List = None) -> str:
|
531 |
+
"""Generate intelligent AI response using LlamaIndex and Gemini"""
|
532 |
+
try:
|
533 |
+
# Prepare context with weather data
|
534 |
+
weather_context = self._format_weather_context(weather_data, query_analysis)
|
535 |
+
|
536 |
+
# Extract enhanced context
|
537 |
+
activity_context = query_analysis.get('activity_context', {})
|
538 |
+
forecast_context = query_analysis.get('forecast_context', {})
|
539 |
+
historical_context = query_analysis.get('historical_context', {})
|
540 |
+
response_hints = query_analysis.get('response_hints', {})
|
541 |
+
|
542 |
+
# Build enhanced prompt based on query type
|
543 |
+
enhanced_prompt = f"""
|
544 |
+
User Query: {user_message}
|
545 |
+
|
546 |
+
Weather Data Available:
|
547 |
+
{weather_context}
|
548 |
+
|
549 |
+
Query Analysis:
|
550 |
+
- Cities mentioned: {query_analysis.get('cities', [])}
|
551 |
+
- Query type: {query_analysis.get('query_type', 'general')}
|
552 |
+
- Is comparison: {query_analysis.get('comparison_info', {}).get('is_comparison', False)}
|
553 |
+
|
554 |
+
"""
|
555 |
+
|
556 |
+
# Add conversation context if available
|
557 |
+
if chat_history:
|
558 |
+
context_summary = self._extract_conversation_context(chat_history)
|
559 |
+
if context_summary:
|
560 |
+
enhanced_prompt += f"""
|
561 |
+
Conversation Context:
|
562 |
+
{context_summary}
|
563 |
+
|
564 |
+
"""
|
565 |
+
|
566 |
+
# Add activity-specific guidance
|
567 |
+
if activity_context.get('has_activity'):
|
568 |
+
enhanced_prompt += f"""
|
569 |
+
Activity Context:
|
570 |
+
- Activity type: {activity_context.get('activity_type', 'general')}
|
571 |
+
- Specific activity: {activity_context.get('specific_activity', 'N/A')}
|
572 |
+
- Advice intent: {activity_context.get('advice_intent', False)}
|
573 |
+
|
574 |
+
Please provide weather-based advice for this activity, considering:
|
575 |
+
- Safety factors (precipitation, wind, visibility, temperature extremes)
|
576 |
+
- Comfort factors (temperature, humidity, wind chill/heat index)
|
577 |
+
- Timing recommendations if conditions vary throughout the day
|
578 |
+
- Alternative suggestions if conditions are unfavorable
|
579 |
+
"""
|
580 |
+
|
581 |
+
# Add forecast-specific guidance
|
582 |
+
if forecast_context.get('has_forecast_intent'):
|
583 |
+
enhanced_prompt += f"""
|
584 |
+
Forecast Context:
|
585 |
+
- Timeline: {forecast_context.get('timeline', 'short_term')}
|
586 |
+
- Forecast type: {forecast_context.get('forecast_type', 'summary')}
|
587 |
+
- Detailed request: {forecast_context.get('detailed_request', False)}
|
588 |
+
|
589 |
+
Please provide forecast information focusing on the requested timeline.
|
590 |
+
Include trends, changes, and key weather events expected.
|
591 |
+
"""
|
592 |
+
|
593 |
+
# Add historical context guidance
|
594 |
+
if historical_context.get('has_historical_intent'):
|
595 |
+
enhanced_prompt += f"""
|
596 |
+
Historical Context:
|
597 |
+
- Period: {historical_context.get('period', 'recent')}
|
598 |
+
- Comparison intent: {historical_context.get('comparison_intent', False)}
|
599 |
+
- Data request: {historical_context.get('data_request', False)}
|
600 |
+
|
601 |
+
Please provide context about how current conditions compare to historical patterns.
|
602 |
+
Mention if conditions are typical, unusual, or record-setting for this time of year.
|
603 |
+
"""
|
604 |
+
|
605 |
+
enhanced_prompt += """
|
606 |
+
|
607 |
+
Response Guidelines:
|
608 |
+
- Be conversational and engaging
|
609 |
+
- Include specific data from the weather information provided
|
610 |
+
- If comparing cities, highlight key differences
|
611 |
+
- Offer practical advice or insights when relevant
|
612 |
+
- For activity queries, prioritize safety and comfort recommendations
|
613 |
+
- Use clear, actionable language
|
614 |
+
- Consider conversation history for context and continuity
|
615 |
+
"""
|
616 |
+
|
617 |
+
# Use LlamaIndex chat engine if available (it has conversation memory built-in)
|
618 |
+
if self.chat_engine and LLAMA_INDEX_AVAILABLE:
|
619 |
+
response = await self._get_llamaindex_response(enhanced_prompt)
|
620 |
+
elif self.llm and LLAMA_INDEX_AVAILABLE:
|
621 |
+
response = await self._get_direct_llm_response(enhanced_prompt)
|
622 |
+
elif GEMINI_AVAILABLE and self.gemini_api_key:
|
623 |
+
response = await self._get_gemini_response(enhanced_prompt)
|
624 |
+
else:
|
625 |
+
response = self._generate_basic_expert_response(user_message, weather_data, query_analysis)
|
626 |
+
|
627 |
+
return response
|
628 |
+
|
629 |
+
except Exception as e:
|
630 |
+
logger.error(f"Error generating AI response: {e}")
|
631 |
+
return self._generate_basic_expert_response(user_message, weather_data, query_analysis)
|
632 |
+
|
633 |
+
async def _get_llamaindex_response(self, prompt: str) -> str:
|
634 |
+
"""Get response using LlamaIndex chat engine"""
|
635 |
+
try:
|
636 |
+
response = await self.chat_engine.achat(prompt)
|
637 |
+
return str(response)
|
638 |
+
except Exception as e:
|
639 |
+
logger.error(f"LlamaIndex chat error: {e}")
|
640 |
+
raise
|
641 |
+
|
642 |
+
async def _get_direct_llm_response(self, prompt: str) -> str:
|
643 |
+
"""Get response using direct LLM call"""
|
644 |
+
try:
|
645 |
+
response = await self.llm.acomplete(prompt)
|
646 |
+
return str(response)
|
647 |
+
except Exception as e:
|
648 |
+
logger.error(f"Direct LLM error: {e}")
|
649 |
+
raise
|
650 |
+
|
651 |
+
async def _get_gemini_response(self, prompt: str) -> str:
|
652 |
+
"""Get response using direct Gemini API"""
|
653 |
+
try:
|
654 |
+
model = genai.GenerativeModel('gemini-2.0-flash-exp')
|
655 |
+
response = await model.generate_content_async(prompt)
|
656 |
+
return response.text
|
657 |
+
except Exception as e:
|
658 |
+
logger.error(f"Gemini API error: {e}")
|
659 |
+
raise
|
660 |
+
|
661 |
+
async def _get_climate_analysis(self, city: str, enhanced_analysis: Dict) -> Dict:
|
662 |
+
"""Get advanced climate analysis for a city"""
|
663 |
+
try:
|
664 |
+
from ..analysis.climate_analyzer import create_climate_analyzer
|
665 |
+
|
666 |
+
climate_analyzer = create_climate_analyzer(self.weather_client)
|
667 |
+
analysis_results = {}
|
668 |
+
|
669 |
+
# Get the analysis type based on query complexity
|
670 |
+
climate_intelligence = enhanced_analysis.get('climate_intelligence', {})
|
671 |
+
query_type = enhanced_analysis.get('query_type', 'general')
|
672 |
+
|
673 |
+
# Temperature trend analysis
|
674 |
+
if 'temperature' in query_type.lower():
|
675 |
+
analysis_results['temperature_trends'] = climate_analyzer.analyze_temperature_trends(city, days=30)
|
676 |
+
|
677 |
+
# Pattern detection for complex queries
|
678 |
+
if climate_intelligence.get('expertise_level') in ['intermediate', 'expert']:
|
679 |
+
analysis_results['weather_patterns'] = climate_analyzer.detect_weather_patterns(city, 'seasonal')
|
680 |
+
|
681 |
+
# Anomaly prediction for expert queries
|
682 |
+
if climate_intelligence.get('expert_analysis_needed'):
|
683 |
+
analysis_results['anomaly_prediction'] = climate_analyzer.predict_weather_anomalies(city, days_ahead=7)
|
684 |
+
|
685 |
+
# City comparison if multiple cities
|
686 |
+
cities = enhanced_analysis.get('cities', [])
|
687 |
+
if len(cities) > 1:
|
688 |
+
analysis_results['city_comparison'] = climate_analyzer.compare_cities_climate(cities[:3]) # Limit to 3 cities
|
689 |
+
|
690 |
+
return analysis_results
|
691 |
+
|
692 |
+
except Exception as e:
|
693 |
+
logger.error(f"Error getting climate analysis for {city}: {e}")
|
694 |
+
return {}
|
695 |
+
|
696 |
+
async def _generate_enhanced_ai_response(self, user_message: str, weather_data: Dict,
|
697 |
+
enhanced_analysis: Dict, climate_analysis_data: Dict,
|
698 |
+
chat_history: List = None) -> str:
|
699 |
+
"""Generate enhanced AI response with climate expertise"""
|
700 |
+
try:
|
701 |
+
# Get climate intelligence and expert context
|
702 |
+
climate_intelligence = enhanced_analysis.get('climate_intelligence', {})
|
703 |
+
expert_context = enhanced_analysis.get('expert_context', {})
|
704 |
+
regional_context = enhanced_analysis.get('regional_climate_context', {})
|
705 |
+
|
706 |
+
# Prepare comprehensive weather context
|
707 |
+
weather_context = self._format_enhanced_weather_context(
|
708 |
+
weather_data, enhanced_analysis, climate_analysis_data, regional_context
|
709 |
+
)
|
710 |
+
|
711 |
+
# Add climate phenomenon explanations if needed
|
712 |
+
phenomena_explanations = self._get_phenomenon_explanations(climate_intelligence)
|
713 |
+
|
714 |
+
# Build expert prompt based on sophistication level
|
715 |
+
expert_prompt = self._build_expert_prompt(
|
716 |
+
enhanced_analysis, climate_intelligence, phenomena_explanations
|
717 |
+
)
|
718 |
+
|
719 |
+
# Extract conversation context
|
720 |
+
conversation_context = ""
|
721 |
+
if chat_history:
|
722 |
+
conversation_context = self._extract_conversation_context(chat_history)
|
723 |
+
|
724 |
+
# Use LlamaIndex chat engine if available
|
725 |
+
if self.chat_engine and LLAMA_INDEX_AVAILABLE:
|
726 |
+
full_query = f"""
|
727 |
+
{expert_prompt}
|
728 |
+
|
729 |
+
Weather Data Context:
|
730 |
+
{weather_context}
|
731 |
+
|
732 |
+
{conversation_context}
|
733 |
+
|
734 |
+
User Query: {user_message}
|
735 |
+
|
736 |
+
Please provide a comprehensive response that includes:
|
737 |
+
1. Direct answer to the user's question
|
738 |
+
2. Relevant meteorological explanation
|
739 |
+
3. Regional climate context where applicable
|
740 |
+
4. Safety considerations if relevant
|
741 |
+
5. Expert insights based on the data provided
|
742 |
+
"""
|
743 |
+
|
744 |
+
response = await asyncio.to_thread(self.chat_engine.chat, full_query)
|
745 |
+
|
746 |
+
# Handle different response types from LlamaIndex
|
747 |
+
if hasattr(response, 'response'):
|
748 |
+
return str(response.response)
|
749 |
+
elif hasattr(response, 'content'):
|
750 |
+
return str(response.content)
|
751 |
+
else:
|
752 |
+
return str(response)
|
753 |
+
|
754 |
+
# Fallback to direct Gemini API
|
755 |
+
elif GEMINI_AVAILABLE and self.gemini_api_key:
|
756 |
+
return await self._generate_direct_gemini_response(
|
757 |
+
user_message, weather_context, expert_prompt, conversation_context
|
758 |
+
)
|
759 |
+
|
760 |
+
# Basic fallback response
|
761 |
+
return self._generate_basic_expert_response(
|
762 |
+
user_message, weather_data, enhanced_analysis
|
763 |
+
)
|
764 |
+
|
765 |
+
except Exception as e:
|
766 |
+
logger.error(f"Error generating enhanced AI response: {e}")
|
767 |
+
return self._generate_basic_expert_response(user_message, weather_data, enhanced_analysis)
|
768 |
+
|
769 |
+
def _format_enhanced_weather_context(self, weather_data: Dict, enhanced_analysis: Dict,
|
770 |
+
climate_analysis_data: Dict, regional_context: Dict) -> str:
|
771 |
+
"""Format comprehensive weather context with climate expertise"""
|
772 |
+
context_parts = []
|
773 |
+
|
774 |
+
# Current weather data
|
775 |
+
for city, data in weather_data.items():
|
776 |
+
forecast = data.get('forecast', [])
|
777 |
+
if forecast:
|
778 |
+
current = forecast[0]
|
779 |
+
context_parts.append(f"""
|
780 |
+
📍 {city.title()} Current Conditions:
|
781 |
+
🌡️ Temperature: {current.get('temperature', 'N/A')}°F
|
782 |
+
🌤️ Conditions: {current.get('shortForecast', 'N/A')}
|
783 |
+
🌧️ Precipitation Chance: {current.get('precipitationProbability', 0)}%
|
784 |
+
💨 Wind: {current.get('windSpeed', 'N/A')} {current.get('windDirection', '')}
|
785 |
+
💧 Humidity: {current.get('relativeHumidity', {}).get('value', 'N/A')}%
|
786 |
+
""")
|
787 |
+
|
788 |
+
# Climate analysis data
|
789 |
+
for city, analysis in climate_analysis_data.items():
|
790 |
+
if analysis:
|
791 |
+
context_parts.append(f"\n🔬 Climate Analysis for {city.title()}:")
|
792 |
+
|
793 |
+
if 'temperature_trends' in analysis:
|
794 |
+
trends = analysis['temperature_trends']
|
795 |
+
if 'statistics' in trends:
|
796 |
+
stats = trends['statistics']
|
797 |
+
context_parts.append(f"📈 30-day temperature trend: {stats.get('trend_direction', 'stable')}")
|
798 |
+
context_parts.append(f"📊 Average: {stats.get('mean_temp', 'N/A'):.1f}°F")
|
799 |
+
|
800 |
+
if 'weather_patterns' in analysis:
|
801 |
+
patterns = analysis['weather_patterns']
|
802 |
+
confidence = patterns.get('confidence', 0)
|
803 |
+
context_parts.append(f"🔍 Weather pattern confidence: {confidence:.2f}")
|
804 |
+
|
805 |
+
if 'anomaly_prediction' in analysis:
|
806 |
+
anomalies = analysis['anomaly_prediction']
|
807 |
+
risk_level = anomalies.get('risk_level', 'low')
|
808 |
+
context_parts.append(f"⚠️ Weather anomaly risk: {risk_level}")
|
809 |
+
|
810 |
+
# Regional climate context
|
811 |
+
for city, region_info in regional_context.items():
|
812 |
+
if region_info:
|
813 |
+
climate_type = region_info.get('climate_type', 'unknown')
|
814 |
+
context_parts.append(f"\n🌍 {city.title()} Regional Climate: {climate_type}")
|
815 |
+
|
816 |
+
seasonal_chars = region_info.get('seasonal_characteristics', {})
|
817 |
+
if seasonal_chars:
|
818 |
+
context_parts.append(f"🗓️ Seasonal characteristics: {seasonal_chars.get('characteristics', [])}")
|
819 |
+
|
820 |
+
return "\n".join(context_parts)
|
821 |
+
|
822 |
+
def _get_phenomenon_explanations(self, climate_intelligence: Dict) -> str:
|
823 |
+
"""Get explanations for detected climate phenomena"""
|
824 |
+
explanations = []
|
825 |
+
|
826 |
+
phenomena = climate_intelligence.get('climate_phenomena', [])
|
827 |
+
for phenomenon in phenomena:
|
828 |
+
explanation = self.weather_knowledge_base.get_phenomenon_explanation(phenomenon)
|
829 |
+
if explanation:
|
830 |
+
explanations.append(f"🌡️ {phenomenon.replace('_', ' ').title()}: {explanation}")
|
831 |
+
|
832 |
+
return "\n".join(explanations) if explanations else ""
|
833 |
+
|
834 |
+
def _build_expert_prompt(self, enhanced_analysis: Dict, climate_intelligence: Dict,
|
835 |
+
phenomena_explanations: str) -> str:
|
836 |
+
"""Build expert-level prompt based on query sophistication"""
|
837 |
+
expertise_level = climate_intelligence.get('expertise_level', 'basic')
|
838 |
+
seasonal_context = climate_intelligence.get('seasonal_context', {})
|
839 |
+
|
840 |
+
base_prompt = f"""
|
841 |
+
You are responding as an expert meteorologist and climate scientist with access to comprehensive weather data
|
842 |
+
and advanced climate analysis. The user's query requires a {expertise_level}-level response.
|
843 |
+
"""
|
844 |
+
|
845 |
+
if expertise_level == 'expert':
|
846 |
+
base_prompt += """
|
847 |
+
Provide detailed meteorological explanations including:
|
848 |
+
- Atmospheric dynamics and physical processes
|
849 |
+
- Synoptic and mesoscale weather patterns
|
850 |
+
- Climate oscillations and their impacts
|
851 |
+
- Statistical analysis and confidence levels
|
852 |
+
- Regional climatology and seasonal patterns
|
853 |
+
"""
|
854 |
+
elif expertise_level == 'intermediate':
|
855 |
+
base_prompt += """
|
856 |
+
Include moderate technical detail with:
|
857 |
+
- Weather system explanations
|
858 |
+
- Regional climate factors
|
859 |
+
- Seasonal considerations
|
860 |
+
- Safety and impact information
|
861 |
+
"""
|
862 |
+
else:
|
863 |
+
base_prompt += """
|
864 |
+
Provide clear, accessible explanations with:
|
865 |
+
- Current conditions and forecasts
|
866 |
+
- Basic weather reasoning
|
867 |
+
- Practical implications
|
868 |
+
- Safety considerations
|
869 |
+
"""
|
870 |
+
|
871 |
+
if phenomena_explanations:
|
872 |
+
base_prompt += f"\n\nRelevant Climate Phenomena:\n{phenomena_explanations}"
|
873 |
+
|
874 |
+
if seasonal_context:
|
875 |
+
current_season = seasonal_context.get('current_season', '')
|
876 |
+
base_prompt += f"\n\nSeasonal Context: Currently in {current_season} season."
|
877 |
+
|
878 |
+
return base_prompt
|
879 |
+
|
880 |
+
async def _generate_direct_gemini_response(self, user_message: str, weather_context: str,
|
881 |
+
expert_prompt: str, conversation_context: str) -> str:
|
882 |
+
"""Generate response using direct Gemini API"""
|
883 |
+
try:
|
884 |
+
model = genai.GenerativeModel('gemini-2.0-flash-exp')
|
885 |
+
|
886 |
+
full_prompt = f"""
|
887 |
+
{expert_prompt}
|
888 |
+
|
889 |
+
Weather Data:
|
890 |
+
{weather_context}
|
891 |
+
|
892 |
+
{conversation_context}
|
893 |
+
|
894 |
+
User Question: {user_message}
|
895 |
+
|
896 |
+
Provide a comprehensive, expert-level weather response.
|
897 |
+
"""
|
898 |
+
|
899 |
+
response = await asyncio.to_thread(model.generate_content, full_prompt)
|
900 |
+
return response.text
|
901 |
+
|
902 |
+
except Exception as e:
|
903 |
+
logger.error(f"Error with direct Gemini API: {e}")
|
904 |
+
return f"I can provide the weather information, but I'm having trouble with the detailed analysis right now. {weather_context}"
|
905 |
+
|
906 |
+
def _generate_basic_expert_response(self, user_message: str, weather_data: Dict,
|
907 |
+
enhanced_analysis: Dict) -> str:
|
908 |
+
"""Generate basic expert response as fallback"""
|
909 |
+
cities = enhanced_analysis.get('cities', [])
|
910 |
+
query_type = enhanced_analysis.get('query_type', 'general')
|
911 |
+
|
912 |
+
if not cities:
|
913 |
+
return "I'd be happy to help with weather information! Please specify a city or location for detailed analysis."
|
914 |
+
|
915 |
+
response_parts = []
|
916 |
+
|
917 |
+
for city in cities:
|
918 |
+
if city in weather_data:
|
919 |
+
forecast = weather_data[city].get('forecast', [])
|
920 |
+
if forecast:
|
921 |
+
current = forecast[0]
|
922 |
+
temp = current.get('temperature', 'N/A')
|
923 |
+
conditions = current.get('shortForecast', 'N/A')
|
924 |
+
|
925 |
+
response_parts.append(f"""
|
926 |
+
📍 **{city.title()} Weather Analysis:**
|
927 |
+
🌡️ Current: {temp}°F
|
928 |
+
🌤️ Conditions: {conditions}
|
929 |
+
|
930 |
+
**Meteorological Context:** The current conditions in {city.title()} are typical for this time of year.
|
931 |
+
{conditions.lower()} conditions are influenced by regional climate patterns and seasonal atmospheric circulation.
|
932 |
+
""")
|
933 |
+
|
934 |
+
if len(cities) > 1:
|
935 |
+
response_parts.append("\n**Regional Comparison:** These cities show the typical climate diversity across different regions, influenced by latitude, elevation, and local geographic features.")
|
936 |
+
|
937 |
+
return "\n".join(response_parts)
|
938 |
+
|
939 |
+
def _prepare_map_data(self, cities: List[str], weather_data: Dict) -> List[Dict]:
|
940 |
+
"""Prepare data for map visualization"""
|
941 |
+
map_data = []
|
942 |
+
|
943 |
+
for city in cities:
|
944 |
+
if city in weather_data:
|
945 |
+
coords = weather_data[city].get('coordinates')
|
946 |
+
forecast = weather_data[city].get('forecast', [])
|
947 |
+
|
948 |
+
if coords and forecast:
|
949 |
+
lat, lon = coords
|
950 |
+
map_data.append({
|
951 |
+
'name': city,
|
952 |
+
'lat': lat,
|
953 |
+
'lon': lon,
|
954 |
+
'forecast': forecast
|
955 |
+
})
|
956 |
+
|
957 |
+
return map_data
|
958 |
+
|
959 |
+
def create_enhanced_chatbot(weather_client, nlp_processor, gemini_api_key: str = None) -> EnhancedWeatherChatbot:
|
960 |
+
"""Factory function to create enhanced chatbot"""
|
961 |
+
return EnhancedWeatherChatbot(weather_client, nlp_processor, gemini_api_key)
|
962 |
+
|
src/chatbot/nlp_processor.py
ADDED
@@ -0,0 +1,515 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
Natural Language Processing for Weather Queries
|
3 |
+
Advanced query understanding and city extraction
|
4 |
+
"""
|
5 |
+
|
6 |
+
import re
|
7 |
+
import logging
|
8 |
+
from typing import List, Dict, Set, Tuple
|
9 |
+
from datetime import datetime
|
10 |
+
|
11 |
+
logger = logging.getLogger(__name__)
|
12 |
+
|
13 |
+
class WeatherNLPProcessor:
|
14 |
+
"""Advanced NLP processor for weather queries"""
|
15 |
+
|
16 |
+
def __init__(self):
|
17 |
+
self.weather_keywords = {
|
18 |
+
'temperature': {
|
19 |
+
'primary': ['temperature', 'temp', 'degrees', 'hot', 'cold', 'warm', 'cool', 'heat', 'chill'],
|
20 |
+
'modifiers': ['high', 'low', 'maximum', 'minimum', 'average', 'current']
|
21 |
+
},
|
22 |
+
'precipitation': {
|
23 |
+
'primary': ['rain', 'precipitation', 'shower', 'drizzle', 'downpour', 'wet', 'storm', 'snow'],
|
24 |
+
'modifiers': ['chance', 'probability', 'amount', 'heavy', 'light']
|
25 |
+
},
|
26 |
+
'wind': {
|
27 |
+
'primary': ['wind', 'windy', 'breeze', 'gust', 'mph', 'speed'],
|
28 |
+
'modifiers': ['strong', 'light', 'direction', 'gusts']
|
29 |
+
},
|
30 |
+
'conditions': {
|
31 |
+
'primary': ['weather', 'conditions', 'forecast', 'outlook', 'sunny', 'cloudy', 'clear', 'overcast'],
|
32 |
+
'modifiers': ['current', 'today', 'tomorrow', 'this week']
|
33 |
+
},
|
34 |
+
'comparison': {
|
35 |
+
'primary': ['difference', 'compare', 'versus', 'vs', 'between', 'than', 'against'],
|
36 |
+
'modifiers': ['warmer', 'colder', 'wetter', 'drier', 'windier']
|
37 |
+
},
|
38 |
+
'time': {
|
39 |
+
'primary': ['today', 'tomorrow', 'yesterday', 'week', 'weekend', 'now', 'currently'],
|
40 |
+
'modifiers': ['this', 'next', 'last', 'morning', 'afternoon', 'evening', 'night']
|
41 |
+
},
|
42 |
+
'activity_advice': {
|
43 |
+
'primary': ['should', 'can', 'good', 'bad', 'safe', 'bike', 'biking', 'cycling', 'walk', 'walking',
|
44 |
+
'run', 'running', 'jog', 'jogging', 'picnic', 'bbq', 'barbecue', 'outdoor', 'outside',
|
45 |
+
'drive', 'driving', 'travel', 'trip', 'swim', 'swimming', 'beach', 'park', 'hike', 'hiking'],
|
46 |
+
'modifiers': ['go', 'plan', 'planning', 'advisable', 'recommended', 'suitable', 'ideal']
|
47 |
+
},
|
48 |
+
'forecast': {
|
49 |
+
'primary': ['forecast', 'prediction', 'future', 'upcoming', 'next', 'later', 'will', 'going to',
|
50 |
+
'expect', 'expected', 'outlook', 'trend', 'pattern'],
|
51 |
+
'modifiers': ['week', 'month', 'days', 'hours', 'extended', 'long term', 'short term']
|
52 |
+
},
|
53 |
+
'historical': {
|
54 |
+
'primary': ['past', 'previous', 'history', 'historical', 'ago', 'before', 'last', 'earlier',
|
55 |
+
'trend', 'pattern', 'average', 'normal', 'typical', 'record'],
|
56 |
+
'modifiers': ['year', 'month', 'week', 'data', 'records', 'climate']
|
57 |
+
}
|
58 |
+
}
|
59 |
+
|
60 |
+
# Keep a core set of major cities for pattern recognition and aliases
|
61 |
+
# This is used for pattern matching, not limiting geocoding
|
62 |
+
self.major_cities_patterns = {
|
63 |
+
'new york', 'los angeles', 'chicago', 'houston', 'phoenix', 'philadelphia',
|
64 |
+
'san antonio', 'san diego', 'dallas', 'san jose', 'austin', 'jacksonville',
|
65 |
+
'fort worth', 'columbus', 'charlotte', 'san francisco', 'indianapolis',
|
66 |
+
'seattle', 'denver', 'washington', 'boston', 'el paso', 'detroit',
|
67 |
+
'nashville', 'portland', 'memphis', 'oklahoma city', 'las vegas',
|
68 |
+
'louisville', 'baltimore', 'milwaukee', 'albuquerque', 'tucson',
|
69 |
+
'atlanta', 'miami', 'tampa', 'orlando', 'cleveland', 'pittsburgh',
|
70 |
+
'cincinnati', 'kansas city', 'raleigh', 'omaha', 'virginia beach',
|
71 |
+
'oakland', 'minneapolis', 'tulsa', 'new orleans', 'buffalo',
|
72 |
+
'lincoln', 'madison', 'boise', 'birmingham', 'spokane', 'columbia'
|
73 |
+
}
|
74 |
+
|
75 |
+
# Common city abbreviations and aliases for better user experience
|
76 |
+
self.city_aliases = {
|
77 |
+
'nyc': 'New York City',
|
78 |
+
'la': 'Los Angeles',
|
79 |
+
'sf': 'San Francisco',
|
80 |
+
'dc': 'Washington DC',
|
81 |
+
'philly': 'Philadelphia',
|
82 |
+
'chi': 'Chicago',
|
83 |
+
'vegas': 'Las Vegas',
|
84 |
+
'nola': 'New Orleans',
|
85 |
+
'atx': 'Austin',
|
86 |
+
'h-town': 'Houston',
|
87 |
+
'pdx': 'Portland',
|
88 |
+
'the bay': 'San Francisco Bay Area',
|
89 |
+
'south beach': 'Miami Beach',
|
90 |
+
'motor city': 'Detroit',
|
91 |
+
'space city': 'Houston',
|
92 |
+
'city of angels': 'Los Angeles',
|
93 |
+
'windy city': 'Chicago',
|
94 |
+
'big apple': 'New York City',
|
95 |
+
'music city': 'Nashville',
|
96 |
+
'silicon valley': 'San Jose',
|
97 |
+
'bean town': 'Boston',
|
98 |
+
'mile high city': 'Denver',
|
99 |
+
'emerald city': 'Seattle',
|
100 |
+
'magic city': 'Miami',
|
101 |
+
'charm city': 'Baltimore',
|
102 |
+
}
|
103 |
+
|
104 |
+
def _extract_potential_locations(self, text: str) -> List[str]:
|
105 |
+
"""
|
106 |
+
Extract potential location names from text using multiple strategies.
|
107 |
+
This method identifies location candidates that will be validated via geocoding.
|
108 |
+
"""
|
109 |
+
text_lower = text.lower().strip()
|
110 |
+
potential_locations = []
|
111 |
+
|
112 |
+
# Strategy 1: Check for known aliases first
|
113 |
+
for alias, full_name in self.city_aliases.items():
|
114 |
+
if re.search(r'\b' + re.escape(alias) + r'\b', text_lower):
|
115 |
+
potential_locations.append(full_name)
|
116 |
+
logger.info(f"Found city alias: {alias} -> {full_name}")
|
117 |
+
|
118 |
+
# Strategy 2: Look for known major city patterns
|
119 |
+
for city in self.major_cities_patterns:
|
120 |
+
pattern = r'\b' + re.escape(city) + r'\b'
|
121 |
+
if re.search(pattern, text_lower):
|
122 |
+
potential_locations.append(city.title())
|
123 |
+
logger.info(f"Found major city pattern: {city}")
|
124 |
+
|
125 |
+
# Strategy 3: Extract capitalized words that could be city names
|
126 |
+
# Look for capitalized words (potential proper nouns)
|
127 |
+
words = re.findall(r'\b[A-Z][a-z]+(?:\s+[A-Z][a-z]+)*\b', text)
|
128 |
+
for word in words:
|
129 |
+
word_lower = word.lower()
|
130 |
+
# Skip common non-location words
|
131 |
+
if word_lower not in {'weather', 'today', 'tomorrow', 'forecast', 'temperature',
|
132 |
+
'monday', 'tuesday', 'wednesday', 'thursday', 'friday',
|
133 |
+
'saturday', 'sunday', 'morning', 'afternoon', 'evening',
|
134 |
+
'night', 'celsius', 'fahrenheit', 'america', 'united', 'states'}:
|
135 |
+
if len(word) >= 3: # City names are usually at least 3 characters
|
136 |
+
potential_locations.append(word)
|
137 |
+
logger.info(f"Found potential location from capitalization: {word}")
|
138 |
+
|
139 |
+
# Strategy 4: Look for patterns like "in [location]", "at [location]", "[location] weather"
|
140 |
+
location_patterns = [
|
141 |
+
r'(?:in|at|for|from)\s+([A-Za-z\s]+?)(?:\s+weather|\s+forecast|\s+temperature|$|[,.])',
|
142 |
+
r'([A-Za-z\s]+?)\s+(?:weather|forecast|temperature|rain|snow|wind)',
|
143 |
+
r'weather\s+(?:in|at|for)\s+([A-Za-z\s]+?)(?:$|[,.])',
|
144 |
+
r'how.*(?:in|at)\s+([A-Za-z\s]+?)(?:$|[,.\?])'
|
145 |
+
]
|
146 |
+
|
147 |
+
for pattern in location_patterns:
|
148 |
+
matches = re.finditer(pattern, text, re.IGNORECASE)
|
149 |
+
for match in matches:
|
150 |
+
location = match.group(1).strip()
|
151 |
+
if len(location) >= 3 and location.lower() not in {'the', 'and', 'for', 'with'}:
|
152 |
+
potential_locations.append(location.title())
|
153 |
+
logger.info(f"Found location from pattern: {location}")
|
154 |
+
|
155 |
+
# Remove duplicates while preserving order
|
156 |
+
unique_locations = []
|
157 |
+
seen = set()
|
158 |
+
for location in potential_locations:
|
159 |
+
location_clean = location.strip()
|
160 |
+
if location_clean and location_clean.lower() not in seen:
|
161 |
+
unique_locations.append(location_clean)
|
162 |
+
seen.add(location_clean.lower())
|
163 |
+
|
164 |
+
return unique_locations
|
165 |
+
|
166 |
+
def extract_cities(self, text: str) -> List[str]:
|
167 |
+
"""
|
168 |
+
Extract and validate city names from text using dynamic approach.
|
169 |
+
This method extracts potential locations and returns them for geocoding validation.
|
170 |
+
"""
|
171 |
+
logger.info(f"Extracting cities from: '{text}'")
|
172 |
+
|
173 |
+
# Extract potential locations using multiple strategies
|
174 |
+
potential_locations = self._extract_potential_locations(text)
|
175 |
+
|
176 |
+
if not potential_locations:
|
177 |
+
logger.info("No potential locations found in text")
|
178 |
+
return []
|
179 |
+
|
180 |
+
logger.info(f"Found potential locations: {potential_locations}")
|
181 |
+
|
182 |
+
# Return all potential locations - the geocoding will validate them
|
183 |
+
# This allows for any city to be processed, not just those in predefined lists
|
184 |
+
return potential_locations
|
185 |
+
|
186 |
+
def identify_query_type(self, text: str) -> str:
|
187 |
+
"""Identify the primary intent of the weather query"""
|
188 |
+
text_lower = text.lower()
|
189 |
+
|
190 |
+
# Score each category
|
191 |
+
scores = {}
|
192 |
+
|
193 |
+
for category, keywords in self.weather_keywords.items():
|
194 |
+
score = 0
|
195 |
+
|
196 |
+
# Primary keywords get higher weight
|
197 |
+
for keyword in keywords['primary']:
|
198 |
+
if keyword in text_lower:
|
199 |
+
score += 3
|
200 |
+
|
201 |
+
# Modifier keywords get lower weight
|
202 |
+
for keyword in keywords['modifiers']:
|
203 |
+
if keyword in text_lower:
|
204 |
+
score += 1
|
205 |
+
|
206 |
+
scores[category] = score
|
207 |
+
|
208 |
+
# Return category with highest score
|
209 |
+
if not scores or max(scores.values()) == 0:
|
210 |
+
return 'general'
|
211 |
+
|
212 |
+
return max(scores, key=scores.get)
|
213 |
+
|
214 |
+
def extract_time_context(self, text: str) -> Dict:
|
215 |
+
"""Extract temporal context from query"""
|
216 |
+
text_lower = text.lower()
|
217 |
+
|
218 |
+
time_indicators = {
|
219 |
+
'now': ['now', 'current', 'currently', 'right now', 'at the moment'],
|
220 |
+
'today': ['today', 'this day'],
|
221 |
+
'tomorrow': ['tomorrow', 'next day'],
|
222 |
+
'this_week': ['this week', 'week', 'weekly'],
|
223 |
+
'weekend': ['weekend', 'saturday', 'sunday'],
|
224 |
+
'future': ['forecast', 'prediction', 'will be', 'going to be']
|
225 |
+
}
|
226 |
+
|
227 |
+
detected_times = []
|
228 |
+
for time_type, indicators in time_indicators.items():
|
229 |
+
for indicator in indicators:
|
230 |
+
if indicator in text_lower:
|
231 |
+
detected_times.append(time_type)
|
232 |
+
break
|
233 |
+
|
234 |
+
return {
|
235 |
+
'time_context': detected_times[0] if detected_times else 'general',
|
236 |
+
'all_contexts': detected_times
|
237 |
+
}
|
238 |
+
|
239 |
+
def extract_comparison_intent(self, text: str) -> Dict:
|
240 |
+
"""Extract comparison intent and structure"""
|
241 |
+
text_lower = text.lower()
|
242 |
+
|
243 |
+
comparison_patterns = [
|
244 |
+
r'compare\s+(.+?)\s+(?:and|with|to|vs)\s+(.+)',
|
245 |
+
r'difference\s+between\s+(.+?)\s+and\s+(.+)',
|
246 |
+
r'(.+?)\s+(?:vs|versus)\s+(.+)',
|
247 |
+
r'(.+?)\s+compared\s+to\s+(.+)',
|
248 |
+
r'how\s+(?:different|similar)\s+(?:is|are)\s+(.+?)\s+(?:and|from)\s+(.+)'
|
249 |
+
]
|
250 |
+
|
251 |
+
for pattern in comparison_patterns:
|
252 |
+
match = re.search(pattern, text_lower)
|
253 |
+
if match:
|
254 |
+
entity1 = match.group(1).strip()
|
255 |
+
entity2 = match.group(2).strip()
|
256 |
+
|
257 |
+
# Extract cities from each entity
|
258 |
+
cities1 = self.extract_cities(entity1)
|
259 |
+
cities2 = self.extract_cities(entity2)
|
260 |
+
|
261 |
+
return {
|
262 |
+
'is_comparison': True,
|
263 |
+
'entities': [entity1, entity2],
|
264 |
+
'cities': cities1 + cities2,
|
265 |
+
'comparison_type': self.identify_query_type(text)
|
266 |
+
}
|
267 |
+
|
268 |
+
return {'is_comparison': False}
|
269 |
+
|
270 |
+
def extract_weather_attributes(self, text: str) -> List[str]:
|
271 |
+
"""Extract specific weather attributes mentioned"""
|
272 |
+
text_lower = text.lower()
|
273 |
+
attributes = []
|
274 |
+
|
275 |
+
attribute_patterns = {
|
276 |
+
'temperature': r'\b(?:temp|temperature|degrees?|hot|cold|warm|cool)\b',
|
277 |
+
'precipitation': r'\b(?:rain|precipitation|shower|storm|wet|dry)\b',
|
278 |
+
'wind': r'\b(?:wind|windy|breeze|gust)\b',
|
279 |
+
'humidity': r'\b(?:humid|humidity|moisture)\b',
|
280 |
+
'pressure': r'\b(?:pressure|barometric)\b',
|
281 |
+
'visibility': r'\b(?:visibility|fog|clear)\b',
|
282 |
+
'conditions': r'\b(?:sunny|cloudy|overcast|clear|stormy)\b'
|
283 |
+
}
|
284 |
+
|
285 |
+
for attribute, pattern in attribute_patterns.items():
|
286 |
+
if re.search(pattern, text_lower):
|
287 |
+
attributes.append(attribute)
|
288 |
+
|
289 |
+
return attributes
|
290 |
+
|
291 |
+
def extract_activity_context(self, text: str) -> Dict:
|
292 |
+
"""Extract activity and advice context from query"""
|
293 |
+
text_lower = text.lower()
|
294 |
+
|
295 |
+
# Activity patterns
|
296 |
+
activity_patterns = {
|
297 |
+
'biking': ['bike', 'biking', 'cycling', 'bicycle', 'cycle'],
|
298 |
+
'walking': ['walk', 'walking', 'stroll', 'hike', 'hiking'],
|
299 |
+
'running': ['run', 'running', 'jog', 'jogging', 'exercise'],
|
300 |
+
'outdoor_events': ['picnic', 'bbq', 'barbecue', 'outdoor', 'outside', 'park'],
|
301 |
+
'travel': ['drive', 'driving', 'travel', 'trip', 'road trip'],
|
302 |
+
'water_activities': ['swim', 'swimming', 'beach', 'pool', 'lake'],
|
303 |
+
'sports': ['game', 'match', 'tournament', 'sports', 'baseball', 'football', 'soccer', 'tennis']
|
304 |
+
}
|
305 |
+
|
306 |
+
# Advice patterns
|
307 |
+
advice_patterns = [
|
308 |
+
'should i', 'can i', 'is it good', 'is it safe', 'would it be',
|
309 |
+
'advisable', 'recommended', 'good idea', 'bad idea', 'suitable',
|
310 |
+
'ideal for', 'perfect for', 'okay to', 'fine to'
|
311 |
+
]
|
312 |
+
|
313 |
+
detected_activities = []
|
314 |
+
for activity_type, keywords in activity_patterns.items():
|
315 |
+
for keyword in keywords:
|
316 |
+
if keyword in text_lower:
|
317 |
+
detected_activities.append(activity_type)
|
318 |
+
break
|
319 |
+
|
320 |
+
# Check for advice intent
|
321 |
+
advice_intent = any(pattern in text_lower for pattern in advice_patterns)
|
322 |
+
|
323 |
+
# Extract specific activity mentioned
|
324 |
+
specific_activity = None
|
325 |
+
for activity_type, keywords in activity_patterns.items():
|
326 |
+
for keyword in keywords:
|
327 |
+
if keyword in text_lower:
|
328 |
+
specific_activity = keyword
|
329 |
+
break
|
330 |
+
if specific_activity:
|
331 |
+
break
|
332 |
+
|
333 |
+
return {
|
334 |
+
'has_activity': bool(detected_activities),
|
335 |
+
'activities': detected_activities,
|
336 |
+
'specific_activity': specific_activity,
|
337 |
+
'advice_intent': advice_intent,
|
338 |
+
'activity_type': detected_activities[0] if detected_activities else None
|
339 |
+
}
|
340 |
+
|
341 |
+
def extract_forecast_context(self, text: str) -> Dict:
|
342 |
+
"""Extract forecast and timeline context"""
|
343 |
+
text_lower = text.lower()
|
344 |
+
|
345 |
+
# Timeline patterns
|
346 |
+
timeline_patterns = {
|
347 |
+
'short_term': ['today', 'tonight', 'tomorrow', 'next few hours', 'this afternoon', 'this evening'],
|
348 |
+
'medium_term': ['this week', 'next week', 'weekend', 'next few days', 'coming days'],
|
349 |
+
'long_term': ['next month', 'next season', 'long term', 'extended forecast', 'monthly outlook']
|
350 |
+
}
|
351 |
+
|
352 |
+
# Forecast types
|
353 |
+
forecast_types = {
|
354 |
+
'detailed': ['detailed', 'hourly', 'hour by hour', 'breakdown'],
|
355 |
+
'summary': ['summary', 'overview', 'general', 'brief'],
|
356 |
+
'trends': ['trend', 'pattern', 'outlook', 'change', 'changing']
|
357 |
+
}
|
358 |
+
|
359 |
+
detected_timeline = []
|
360 |
+
detected_types = []
|
361 |
+
|
362 |
+
for timeline, keywords in timeline_patterns.items():
|
363 |
+
for keyword in keywords:
|
364 |
+
if keyword in text_lower:
|
365 |
+
detected_timeline.append(timeline)
|
366 |
+
break
|
367 |
+
|
368 |
+
for forecast_type, keywords in forecast_types.items():
|
369 |
+
for keyword in keywords:
|
370 |
+
if keyword in text_lower:
|
371 |
+
detected_types.append(forecast_type)
|
372 |
+
break
|
373 |
+
|
374 |
+
return {
|
375 |
+
'has_forecast_intent': any(word in text_lower for word in ['forecast', 'future', 'will be', 'going to', 'expect', 'prediction']),
|
376 |
+
'timeline': detected_timeline[0] if detected_timeline else 'short_term',
|
377 |
+
'all_timelines': detected_timeline,
|
378 |
+
'forecast_type': detected_types[0] if detected_types else 'summary',
|
379 |
+
'detailed_request': 'detailed' in detected_types or 'hourly' in text_lower
|
380 |
+
}
|
381 |
+
|
382 |
+
def extract_historical_context(self, text: str) -> Dict:
|
383 |
+
"""Extract historical data context"""
|
384 |
+
text_lower = text.lower()
|
385 |
+
|
386 |
+
# Historical patterns
|
387 |
+
historical_patterns = {
|
388 |
+
'recent': ['yesterday', 'last week', 'past week', 'recent', 'lately'],
|
389 |
+
'seasonal': ['last month', 'past month', 'last season', 'past season'],
|
390 |
+
'yearly': ['last year', 'past year', 'previous year', 'annual'],
|
391 |
+
'long_term': ['historical', 'history', 'records', 'archive', 'past data', 'climate data']
|
392 |
+
}
|
393 |
+
|
394 |
+
# Comparison with historical data
|
395 |
+
historical_comparison = any(word in text_lower for word in [
|
396 |
+
'compared to', 'versus last', 'than usual', 'than normal', 'average',
|
397 |
+
'typical', 'record', 'above normal', 'below normal'
|
398 |
+
])
|
399 |
+
|
400 |
+
detected_periods = []
|
401 |
+
for period, keywords in historical_patterns.items():
|
402 |
+
for keyword in keywords:
|
403 |
+
if keyword in text_lower:
|
404 |
+
detected_periods.append(period)
|
405 |
+
break
|
406 |
+
|
407 |
+
return {
|
408 |
+
'has_historical_intent': any(word in text_lower for word in ['past', 'history', 'historical', 'ago', 'before', 'last', 'previous']),
|
409 |
+
'period': detected_periods[0] if detected_periods else 'recent',
|
410 |
+
'all_periods': detected_periods,
|
411 |
+
'comparison_intent': historical_comparison,
|
412 |
+
'data_request': any(word in text_lower for word in ['data', 'records', 'statistics', 'stats'])
|
413 |
+
}
|
414 |
+
|
415 |
+
def process_query(self, text: str) -> Dict:
|
416 |
+
"""Comprehensive query processing"""
|
417 |
+
# Basic extraction
|
418 |
+
cities = self.extract_cities(text)
|
419 |
+
query_type = self.identify_query_type(text)
|
420 |
+
time_context = self.extract_time_context(text)
|
421 |
+
comparison_info = self.extract_comparison_intent(text)
|
422 |
+
weather_attributes = self.extract_weather_attributes(text)
|
423 |
+
activity_context = self.extract_activity_context(text)
|
424 |
+
forecast_context = self.extract_forecast_context(text)
|
425 |
+
historical_context = self.extract_historical_context(text)
|
426 |
+
|
427 |
+
# Determine complexity
|
428 |
+
complexity = 'simple'
|
429 |
+
if len(cities) > 1 or comparison_info['is_comparison']:
|
430 |
+
complexity = 'comparison'
|
431 |
+
elif len(weather_attributes) > 2:
|
432 |
+
complexity = 'complex'
|
433 |
+
|
434 |
+
# Generate response hints
|
435 |
+
response_hints = self._generate_response_hints(
|
436 |
+
query_type, cities, comparison_info, weather_attributes,
|
437 |
+
activity_context, forecast_context, historical_context
|
438 |
+
)
|
439 |
+
|
440 |
+
return {
|
441 |
+
'original_text': text,
|
442 |
+
'cities': cities,
|
443 |
+
'query_type': query_type,
|
444 |
+
'time_context': time_context,
|
445 |
+
'comparison_info': comparison_info,
|
446 |
+
'weather_attributes': weather_attributes,
|
447 |
+
'activity_context': activity_context,
|
448 |
+
'forecast_context': forecast_context,
|
449 |
+
'historical_context': historical_context,
|
450 |
+
'complexity': complexity,
|
451 |
+
'response_hints': response_hints,
|
452 |
+
'has_multiple_cities': len(cities) > 1,
|
453 |
+
'confidence': self._calculate_confidence(cities, query_type, weather_attributes)
|
454 |
+
}
|
455 |
+
|
456 |
+
def _generate_response_hints(self, query_type: str, cities: List[str],
|
457 |
+
comparison_info: Dict, attributes: List[str],
|
458 |
+
activity_context: Dict, forecast_context: Dict,
|
459 |
+
historical_context: Dict) -> Dict:
|
460 |
+
"""Generate hints for response generation"""
|
461 |
+
hints = {
|
462 |
+
'focus_on': query_type,
|
463 |
+
'include_map_update': len(cities) > 0,
|
464 |
+
'comparison_mode': comparison_info['is_comparison'],
|
465 |
+
'detailed_attributes': attributes,
|
466 |
+
'response_style': 'detailed' if len(attributes) > 1 else 'concise',
|
467 |
+
'activity_advice': activity_context.get('advice_intent', False),
|
468 |
+
'specific_activity': activity_context.get('specific_activity'),
|
469 |
+
'forecast_timeline': forecast_context.get('timeline', 'short_term'),
|
470 |
+
'historical_period': historical_context.get('period', 'recent')
|
471 |
+
}
|
472 |
+
|
473 |
+
if comparison_info['is_comparison']:
|
474 |
+
hints['comparison_type'] = comparison_info.get('comparison_type', 'general')
|
475 |
+
|
476 |
+
# Add activity-specific hints
|
477 |
+
if activity_context.get('has_activity'):
|
478 |
+
hints['activity_type'] = activity_context.get('activity_type')
|
479 |
+
hints['needs_weather_advice'] = True
|
480 |
+
|
481 |
+
# Add forecast-specific hints
|
482 |
+
if forecast_context.get('has_forecast_intent'):
|
483 |
+
hints['forecast_type'] = forecast_context.get('forecast_type', 'summary')
|
484 |
+
hints['detailed_forecast'] = forecast_context.get('detailed_request', False)
|
485 |
+
|
486 |
+
# Add historical-specific hints
|
487 |
+
if historical_context.get('has_historical_intent'):
|
488 |
+
hints['historical_comparison'] = historical_context.get('comparison_intent', False)
|
489 |
+
hints['data_focus'] = historical_context.get('data_request', False)
|
490 |
+
|
491 |
+
return hints
|
492 |
+
|
493 |
+
def _calculate_confidence(self, cities: List[str], query_type: str,
|
494 |
+
attributes: List[str]) -> float:
|
495 |
+
"""Calculate confidence in query understanding"""
|
496 |
+
confidence = 0.5 # Base confidence
|
497 |
+
|
498 |
+
# Boost confidence for recognized cities
|
499 |
+
if cities:
|
500 |
+
confidence += 0.3
|
501 |
+
|
502 |
+
# Boost confidence for clear query type
|
503 |
+
if query_type != 'general':
|
504 |
+
confidence += 0.2
|
505 |
+
|
506 |
+
# Boost confidence for specific attributes
|
507 |
+
if attributes:
|
508 |
+
confidence += 0.1 * min(len(attributes), 3)
|
509 |
+
|
510 |
+
return min(1.0, confidence)
|
511 |
+
|
512 |
+
def create_nlp_processor() -> WeatherNLPProcessor:
|
513 |
+
"""Factory function to create NLP processor"""
|
514 |
+
return WeatherNLPProcessor()
|
515 |
+
|
src/chatbot/weather_knowledge_base.py
ADDED
@@ -0,0 +1,587 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
Enhanced Weather Knowledge Base
|
3 |
+
Comprehensive meteorological and climate expertise for intelligent weather analysis
|
4 |
+
"""
|
5 |
+
|
6 |
+
import json
|
7 |
+
import logging
|
8 |
+
from typing import Dict, List, Optional, Any
|
9 |
+
from datetime import datetime, timedelta
|
10 |
+
|
11 |
+
logger = logging.getLogger(__name__)
|
12 |
+
|
13 |
+
class WeatherKnowledgeBase:
|
14 |
+
"""Comprehensive weather and climate knowledge base for expert-level responses"""
|
15 |
+
|
16 |
+
def __init__(self):
|
17 |
+
self.meteorological_knowledge = self._build_meteorological_knowledge()
|
18 |
+
self.climate_patterns = self._build_climate_patterns()
|
19 |
+
self.weather_phenomena = self._build_weather_phenomena()
|
20 |
+
self.seasonal_patterns = self._build_seasonal_patterns()
|
21 |
+
self.regional_climatology = self._build_regional_climatology()
|
22 |
+
self.extreme_weather_knowledge = self._build_extreme_weather_knowledge()
|
23 |
+
|
24 |
+
def _build_meteorological_knowledge(self) -> Dict:
|
25 |
+
"""Build comprehensive meteorological knowledge"""
|
26 |
+
return {
|
27 |
+
'atmospheric_dynamics': {
|
28 |
+
'pressure_systems': {
|
29 |
+
'high_pressure': {
|
30 |
+
'characteristics': 'Clockwise circulation, subsiding air, clear skies',
|
31 |
+
'weather_effects': 'Generally fair weather, light winds, stable conditions',
|
32 |
+
'formation': 'Cooling aloft, surface divergence, subsidence',
|
33 |
+
'movement': 'Generally eastward in mid-latitudes'
|
34 |
+
},
|
35 |
+
'low_pressure': {
|
36 |
+
'characteristics': 'Counterclockwise circulation, rising air, cloud formation',
|
37 |
+
'weather_effects': 'Clouds, precipitation, stronger winds, unstable conditions',
|
38 |
+
'formation': 'Surface convergence, lifting mechanisms, instability',
|
39 |
+
'movement': 'Generally eastward with steering flow'
|
40 |
+
}
|
41 |
+
},
|
42 |
+
'frontal_systems': {
|
43 |
+
'cold_front': {
|
44 |
+
'characteristics': 'Sharp temperature gradient, steep slope, rapid movement',
|
45 |
+
'weather_effects': 'Thunderstorms, brief heavy rain, temperature drop, wind shift',
|
46 |
+
'approach_signs': 'Towering cumulus, wind shift, pressure drop',
|
47 |
+
'passage_signs': 'Temperature drop, wind shift to northwest, clearing'
|
48 |
+
},
|
49 |
+
'warm_front': {
|
50 |
+
'characteristics': 'Gradual temperature rise, shallow slope, slow movement',
|
51 |
+
'weather_effects': 'Stratiform clouds, light precipitation, gradual warming',
|
52 |
+
'approach_signs': 'Cirrus clouds, gradual cloud thickening, pressure fall',
|
53 |
+
'passage_signs': 'Temperature rise, wind shift to southwest, clearing'
|
54 |
+
},
|
55 |
+
'occluded_front': {
|
56 |
+
'characteristics': 'Complex structure, cold air overtaking warm sector',
|
57 |
+
'weather_effects': 'Mixed precipitation, complex weather patterns',
|
58 |
+
'formation': 'Cold front catches up to warm front',
|
59 |
+
'evolution': 'Eventually weakens as temperature contrast decreases'
|
60 |
+
}
|
61 |
+
},
|
62 |
+
'wind_patterns': {
|
63 |
+
'jet_stream': {
|
64 |
+
'description': 'High-altitude river of fast-moving air',
|
65 |
+
'altitude': '30,000-40,000 feet (9-12 km)',
|
66 |
+
'speed': '80-200+ mph (130-320+ km/h)',
|
67 |
+
'weather_impact': 'Steers storm systems, influences temperature patterns',
|
68 |
+
'seasonal_variation': 'Stronger and farther south in winter'
|
69 |
+
},
|
70 |
+
'trade_winds': {
|
71 |
+
'description': 'Consistent easterly winds in tropical regions',
|
72 |
+
'location': '0-30 degrees latitude',
|
73 |
+
'characteristics': 'Steady, moderate speed, moisture transport',
|
74 |
+
'climate_impact': 'Tropical weather patterns, ocean currents'
|
75 |
+
},
|
76 |
+
'westerlies': {
|
77 |
+
'description': 'Prevailing westerly winds in mid-latitudes',
|
78 |
+
'location': '30-60 degrees latitude',
|
79 |
+
'characteristics': 'Variable strength, storm track guidance',
|
80 |
+
'weather_impact': 'Primary storm steering mechanism'
|
81 |
+
}
|
82 |
+
}
|
83 |
+
},
|
84 |
+
'thermodynamics': {
|
85 |
+
'temperature_processes': {
|
86 |
+
'radiative_heating': 'Solar radiation warming Earth\'s surface',
|
87 |
+
'advective_warming': 'Warm air transport from other regions',
|
88 |
+
'adiabatic_warming': 'Compression warming of descending air',
|
89 |
+
'latent_heat_release': 'Warming from water vapor condensation'
|
90 |
+
},
|
91 |
+
'cooling_processes': {
|
92 |
+
'radiative_cooling': 'Heat loss to space, especially at night',
|
93 |
+
'advective_cooling': 'Cold air transport from other regions',
|
94 |
+
'adiabatic_cooling': 'Expansion cooling of rising air',
|
95 |
+
'evaporative_cooling': 'Cooling from water evaporation'
|
96 |
+
},
|
97 |
+
'heat_index': {
|
98 |
+
'purpose': 'Apparent temperature combining temperature and humidity',
|
99 |
+
'calculation': 'Complex formula considering human heat stress',
|
100 |
+
'danger_levels': {
|
101 |
+
'80-90°F': 'Caution - fatigue possible',
|
102 |
+
'90-105°F': 'Extreme caution - heat exhaustion possible',
|
103 |
+
'105-130°F': 'Danger - heat stroke likely',
|
104 |
+
'130°F+': 'Extreme danger - heat stroke imminent'
|
105 |
+
}
|
106 |
+
},
|
107 |
+
'wind_chill': {
|
108 |
+
'purpose': 'Apparent temperature from wind cooling effect',
|
109 |
+
'mechanism': 'Increased heat loss from exposed skin',
|
110 |
+
'danger_levels': {
|
111 |
+
'32-16°F': 'Little danger for properly clothed individuals',
|
112 |
+
'15-(-5)°F': 'Increasing danger of frostbite',
|
113 |
+
'(-6)-(-25)°F': 'Great danger of frostbite',
|
114 |
+
'Below -25°F': 'Extreme danger of frostbite'
|
115 |
+
}
|
116 |
+
}
|
117 |
+
},
|
118 |
+
'moisture_processes': {
|
119 |
+
'evaporation': {
|
120 |
+
'process': 'Liquid water to water vapor',
|
121 |
+
'factors': 'Temperature, humidity, wind speed, surface area',
|
122 |
+
'energy': 'Requires latent heat, cooling effect'
|
123 |
+
},
|
124 |
+
'condensation': {
|
125 |
+
'process': 'Water vapor to liquid water',
|
126 |
+
'requirements': 'Saturation, condensation nuclei',
|
127 |
+
'energy': 'Releases latent heat, warming effect'
|
128 |
+
},
|
129 |
+
'sublimation': {
|
130 |
+
'process': 'Ice directly to water vapor',
|
131 |
+
'conditions': 'Low pressure, dry air',
|
132 |
+
'examples': 'Snow disappearing without melting'
|
133 |
+
},
|
134 |
+
'deposition': {
|
135 |
+
'process': 'Water vapor directly to ice',
|
136 |
+
'conditions': 'Below-freezing surfaces, high humidity',
|
137 |
+
'examples': 'Frost formation, ice crystal growth'
|
138 |
+
}
|
139 |
+
}
|
140 |
+
}
|
141 |
+
|
142 |
+
def _build_climate_patterns(self) -> Dict:
|
143 |
+
"""Build climate pattern knowledge"""
|
144 |
+
return {
|
145 |
+
'enso': {
|
146 |
+
'el_nino': {
|
147 |
+
'definition': 'Warming of eastern Pacific sea surface temperatures',
|
148 |
+
'frequency': 'Every 2-7 years',
|
149 |
+
'duration': '9-12 months typically',
|
150 |
+
'global_effects': {
|
151 |
+
'temperature': 'Generally warmer global temperatures',
|
152 |
+
'precipitation': 'Increased rainfall in southern US, drier in northern regions',
|
153 |
+
'storms': 'Reduced Atlantic hurricane activity, increased Pacific activity'
|
154 |
+
},
|
155 |
+
'regional_impacts': {
|
156 |
+
'southwest_us': 'Increased precipitation, cooler temperatures',
|
157 |
+
'southeast_us': 'Warmer, drier conditions',
|
158 |
+
'pacific_northwest': 'Warmer, drier winter conditions',
|
159 |
+
'great_plains': 'Variable impacts, potential for severe weather changes'
|
160 |
+
}
|
161 |
+
},
|
162 |
+
'la_nina': {
|
163 |
+
'definition': 'Cooling of eastern Pacific sea surface temperatures',
|
164 |
+
'frequency': 'Every 2-7 years',
|
165 |
+
'duration': '9-24 months typically',
|
166 |
+
'global_effects': {
|
167 |
+
'temperature': 'Generally cooler global temperatures',
|
168 |
+
'precipitation': 'Drier conditions in southern US, wetter in northern regions',
|
169 |
+
'storms': 'Increased Atlantic hurricane activity'
|
170 |
+
},
|
171 |
+
'regional_impacts': {
|
172 |
+
'southwest_us': 'Decreased precipitation, hotter temperatures',
|
173 |
+
'southeast_us': 'Increased hurricane risk',
|
174 |
+
'pacific_northwest': 'Cooler, wetter winter conditions',
|
175 |
+
'great_plains': 'Increased tornado activity, temperature extremes'
|
176 |
+
}
|
177 |
+
}
|
178 |
+
},
|
179 |
+
'oscillations': {
|
180 |
+
'pdo': {
|
181 |
+
'name': 'Pacific Decadal Oscillation',
|
182 |
+
'timescale': '20-30 year cycles',
|
183 |
+
'impacts': 'Long-term precipitation patterns, marine ecosystems'
|
184 |
+
},
|
185 |
+
'amo': {
|
186 |
+
'name': 'Atlantic Multidecadal Oscillation',
|
187 |
+
'timescale': '60-80 year cycles',
|
188 |
+
'impacts': 'Atlantic hurricane activity, drought patterns'
|
189 |
+
},
|
190 |
+
'nao': {
|
191 |
+
'name': 'North Atlantic Oscillation',
|
192 |
+
'timescale': 'Weekly to decadal',
|
193 |
+
'impacts': 'European and eastern US weather patterns'
|
194 |
+
},
|
195 |
+
'ao': {
|
196 |
+
'name': 'Arctic Oscillation',
|
197 |
+
'timescale': 'Weekly to seasonal',
|
198 |
+
'impacts': 'Northern hemisphere temperature patterns'
|
199 |
+
}
|
200 |
+
},
|
201 |
+
'climate_zones': {
|
202 |
+
'tropical': {
|
203 |
+
'characteristics': 'High temperatures year-round, distinct wet/dry seasons',
|
204 |
+
'precipitation': 'Heavy rainfall, monsoon patterns',
|
205 |
+
'temperature_range': 'Small annual variation, high daily variation'
|
206 |
+
},
|
207 |
+
'subtropical': {
|
208 |
+
'characteristics': 'Hot summers, mild winters, variable precipitation',
|
209 |
+
'precipitation': 'Summer-dominant or winter-dominant patterns',
|
210 |
+
'temperature_range': 'Moderate seasonal variation'
|
211 |
+
},
|
212 |
+
'temperate': {
|
213 |
+
'characteristics': 'Four distinct seasons, moderate temperatures',
|
214 |
+
'precipitation': 'Even distribution or winter-dominant',
|
215 |
+
'temperature_range': 'Large seasonal variation'
|
216 |
+
},
|
217 |
+
'continental': {
|
218 |
+
'characteristics': 'Large temperature ranges, dry conditions',
|
219 |
+
'precipitation': 'Summer-dominant, low total amounts',
|
220 |
+
'temperature_range': 'Extreme seasonal variation'
|
221 |
+
},
|
222 |
+
'polar': {
|
223 |
+
'characteristics': 'Cold year-round, short summers',
|
224 |
+
'precipitation': 'Low amounts, mostly snow',
|
225 |
+
'temperature_range': 'Extreme seasonal variation in daylight'
|
226 |
+
}
|
227 |
+
}
|
228 |
+
}
|
229 |
+
|
230 |
+
def _build_weather_phenomena(self) -> Dict:
|
231 |
+
"""Build weather phenomena knowledge"""
|
232 |
+
return {
|
233 |
+
'thunderstorms': {
|
234 |
+
'formation': {
|
235 |
+
'requirements': 'Instability, moisture, lifting mechanism',
|
236 |
+
'types': 'Single-cell, multi-cell, supercell',
|
237 |
+
'energy_source': 'Latent heat release from condensation'
|
238 |
+
},
|
239 |
+
'hazards': {
|
240 |
+
'lightning': 'Electrical discharge, fire and electrocution risk',
|
241 |
+
'hail': 'Ice stones, crop and property damage',
|
242 |
+
'straight_line_winds': 'Downburst winds, tree and structure damage',
|
243 |
+
'flooding': 'Heavy rainfall rates exceeding drainage capacity'
|
244 |
+
},
|
245 |
+
'severity_indicators': {
|
246 |
+
'weak': 'Light rain, occasional thunder, minimal wind',
|
247 |
+
'moderate': 'Heavy rain, frequent lightning, gusty winds',
|
248 |
+
'severe': 'Damaging winds >58 mph, hail >1 inch, tornadoes'
|
249 |
+
}
|
250 |
+
},
|
251 |
+
'tornadoes': {
|
252 |
+
'formation': {
|
253 |
+
'requirements': 'Wind shear, instability, low-level convergence',
|
254 |
+
'mechanism': 'Rotation from horizontal to vertical orientation',
|
255 |
+
'supercell_association': 'Most strong tornadoes from supercell thunderstorms'
|
256 |
+
},
|
257 |
+
'classification': {
|
258 |
+
'ef0': 'Light damage, 65-85 mph winds',
|
259 |
+
'ef1': 'Moderate damage, 86-110 mph winds',
|
260 |
+
'ef2': 'Considerable damage, 111-135 mph winds',
|
261 |
+
'ef3': 'Severe damage, 136-165 mph winds',
|
262 |
+
'ef4': 'Devastating damage, 166-200 mph winds',
|
263 |
+
'ef5': 'Incredible damage, >200 mph winds'
|
264 |
+
},
|
265 |
+
'safety': {
|
266 |
+
'warnings': 'Tornado warning issued when tornado spotted or indicated',
|
267 |
+
'shelter': 'Lowest floor, interior room, away from windows',
|
268 |
+
'mobile_homes': 'Evacuate to substantial structure if possible'
|
269 |
+
}
|
270 |
+
},
|
271 |
+
'hurricanes': {
|
272 |
+
'formation': {
|
273 |
+
'requirements': 'Warm ocean water >80°F, low wind shear, disturbance',
|
274 |
+
'seasons': 'Atlantic: June-November, Pacific: May-November',
|
275 |
+
'development_stages': 'Tropical disturbance → depression → storm → hurricane'
|
276 |
+
},
|
277 |
+
'classification': {
|
278 |
+
'category_1': '74-95 mph, minimal damage',
|
279 |
+
'category_2': '96-110 mph, moderate damage',
|
280 |
+
'category_3': '111-129 mph, extensive damage',
|
281 |
+
'category_4': '130-156 mph, extreme damage',
|
282 |
+
'category_5': '>157 mph, catastrophic damage'
|
283 |
+
},
|
284 |
+
'hazards': {
|
285 |
+
'storm_surge': 'Ocean water pushed ashore, primary killer',
|
286 |
+
'wind': 'Sustained high winds, structural damage',
|
287 |
+
'flooding': 'Heavy rainfall, inland flooding',
|
288 |
+
'tornadoes': 'Embedded tornadoes in rainbands'
|
289 |
+
}
|
290 |
+
},
|
291 |
+
'winter_storms': {
|
292 |
+
'types': {
|
293 |
+
'blizzard': 'Heavy snow, strong winds, low visibility',
|
294 |
+
'ice_storm': 'Freezing rain, ice accumulation',
|
295 |
+
'nor_easter': 'Coastal storm, heavy snow/rain, strong winds'
|
296 |
+
},
|
297 |
+
'formation': {
|
298 |
+
'requirements': 'Cold air, moisture, lifting mechanism',
|
299 |
+
'temperature_profile': 'Critical for precipitation type',
|
300 |
+
'storm_tracks': 'Determine snow amounts and areas affected'
|
301 |
+
},
|
302 |
+
'hazards': {
|
303 |
+
'heavy_snow': 'Transportation disruption, roof collapse',
|
304 |
+
'ice_accumulation': 'Power outages, tree damage',
|
305 |
+
'wind_chill': 'Dangerous cold, frostbite risk',
|
306 |
+
'visibility': 'Whiteout conditions, travel hazards'
|
307 |
+
}
|
308 |
+
}
|
309 |
+
}
|
310 |
+
|
311 |
+
def _build_seasonal_patterns(self) -> Dict:
|
312 |
+
"""Build seasonal weather pattern knowledge"""
|
313 |
+
return {
|
314 |
+
'spring': {
|
315 |
+
'temperature_patterns': {
|
316 |
+
'characteristics': 'Rapid warming, large daily temperature swings',
|
317 |
+
'processes': 'Increasing solar angle, longer days',
|
318 |
+
'variability': 'High day-to-day variability, late cold snaps possible'
|
319 |
+
},
|
320 |
+
'precipitation_patterns': {
|
321 |
+
'characteristics': 'Increasing thunderstorm activity',
|
322 |
+
'mechanisms': 'Strong temperature contrasts, jet stream position',
|
323 |
+
'distribution': 'Often wettest season in many regions'
|
324 |
+
},
|
325 |
+
'severe_weather': {
|
326 |
+
'peak_season': 'April-June for tornadoes',
|
327 |
+
'reasons': 'Strong temperature contrasts, wind shear',
|
328 |
+
'regions': 'Great Plains, Southeast most active'
|
329 |
+
}
|
330 |
+
},
|
331 |
+
'summer': {
|
332 |
+
'temperature_patterns': {
|
333 |
+
'characteristics': 'Peak heat, least daily variation',
|
334 |
+
'processes': 'Maximum solar angle, longest days',
|
335 |
+
'extremes': 'Heat waves, urban heat island effects'
|
336 |
+
},
|
337 |
+
'precipitation_patterns': {
|
338 |
+
'characteristics': 'Convective thunderstorms, monsoons',
|
339 |
+
'timing': 'Afternoon/evening thunderstorm peaks',
|
340 |
+
'distribution': 'Heavy rainfall in short periods'
|
341 |
+
},
|
342 |
+
'circulation_patterns': {
|
343 |
+
'bermuda_high': 'Dominant high pressure over Atlantic',
|
344 |
+
'monsoon': 'Seasonal wind reversal, moisture transport',
|
345 |
+
'heat_domes': 'Persistent high pressure, extreme heat'
|
346 |
+
}
|
347 |
+
},
|
348 |
+
'fall': {
|
349 |
+
'temperature_patterns': {
|
350 |
+
'characteristics': 'Gradual cooling, first frost',
|
351 |
+
'processes': 'Decreasing solar angle, shorter days',
|
352 |
+
'variability': 'Indian summer periods, early cold snaps'
|
353 |
+
},
|
354 |
+
'precipitation_patterns': {
|
355 |
+
'characteristics': 'Transitional weather patterns',
|
356 |
+
'systems': 'Early winter storms, tropical systems',
|
357 |
+
'distribution': 'More organized storm systems'
|
358 |
+
},
|
359 |
+
'transition_indicators': {
|
360 |
+
'first_frost': 'End of growing season',
|
361 |
+
'leaf_change': 'Temperature and daylight response',
|
362 |
+
'storm_patterns': 'Shift to winter-type systems'
|
363 |
+
}
|
364 |
+
},
|
365 |
+
'winter': {
|
366 |
+
'temperature_patterns': {
|
367 |
+
'characteristics': 'Coldest temperatures, arctic air masses',
|
368 |
+
'processes': 'Minimum solar angle, shortest days',
|
369 |
+
'extremes': 'Polar vortex events, arctic blasts'
|
370 |
+
},
|
371 |
+
'precipitation_patterns': {
|
372 |
+
'characteristics': 'Snow in northern regions, winter storms',
|
373 |
+
'storm_types': 'Nor\'easters, alberta clippers, pacific storms',
|
374 |
+
'temperature_dependence': 'Snow vs. rain based on temperature profile'
|
375 |
+
},
|
376 |
+
'circulation_patterns': {
|
377 |
+
'polar_vortex': 'Arctic circulation, occasional breakdowns',
|
378 |
+
'storm_tracks': 'More southern storm tracks',
|
379 |
+
'blocking_patterns': 'Persistent weather patterns'
|
380 |
+
}
|
381 |
+
}
|
382 |
+
}
|
383 |
+
|
384 |
+
def _build_regional_climatology(self) -> Dict:
|
385 |
+
"""Build regional climate knowledge"""
|
386 |
+
return {
|
387 |
+
'pacific_northwest': {
|
388 |
+
'climate_type': 'Marine West Coast',
|
389 |
+
'temperature_regime': 'Mild temperatures, small annual range',
|
390 |
+
'precipitation_regime': 'Wet winters, dry summers',
|
391 |
+
'dominant_features': {
|
392 |
+
'marine_influence': 'Pacific Ocean moderates temperatures',
|
393 |
+
'orographic_effects': 'Mountains enhance precipitation',
|
394 |
+
'rain_shadow': 'Eastern areas much drier'
|
395 |
+
},
|
396 |
+
'seasonal_characteristics': {
|
397 |
+
'winter': 'Frequent rain, mild temperatures, storms from Pacific',
|
398 |
+
'spring': 'Gradual drying, wildflower season',
|
399 |
+
'summer': 'Dry, pleasant, occasional heat waves',
|
400 |
+
'fall': 'Return of rain, beautiful foliage'
|
401 |
+
}
|
402 |
+
},
|
403 |
+
'southwest_desert': {
|
404 |
+
'climate_type': 'Hot Desert',
|
405 |
+
'temperature_regime': 'Hot summers, mild winters, large daily range',
|
406 |
+
'precipitation_regime': 'Very low precipitation, summer monsoon',
|
407 |
+
'dominant_features': {
|
408 |
+
'aridity': 'Low humidity, high evaporation rates',
|
409 |
+
'temperature_extremes': 'Very hot days, cool nights',
|
410 |
+
'monsoon_season': 'July-September moisture from Gulf of Mexico'
|
411 |
+
},
|
412 |
+
'seasonal_characteristics': {
|
413 |
+
'winter': 'Mild, pleasant, occasional rain',
|
414 |
+
'spring': 'Warm, dry, windy',
|
415 |
+
'summer': 'Extreme heat, monsoon thunderstorms',
|
416 |
+
'fall': 'Gradual cooling, pleasant'
|
417 |
+
}
|
418 |
+
},
|
419 |
+
'great_plains': {
|
420 |
+
'climate_type': 'Continental',
|
421 |
+
'temperature_regime': 'Large annual range, hot summers, cold winters',
|
422 |
+
'precipitation_regime': 'Moderate precipitation, summer maximum',
|
423 |
+
'dominant_features': {
|
424 |
+
'continental_influence': 'Large temperature swings',
|
425 |
+
'severe_weather': 'Tornado alley, severe thunderstorms',
|
426 |
+
'drought_prone': 'Variable precipitation, drought cycles'
|
427 |
+
},
|
428 |
+
'seasonal_characteristics': {
|
429 |
+
'winter': 'Cold, dry, occasional blizzards',
|
430 |
+
'spring': 'Severe weather season, rapid warming',
|
431 |
+
'summer': 'Hot, humid, thunderstorms',
|
432 |
+
'fall': 'Pleasant, variable'
|
433 |
+
}
|
434 |
+
}
|
435 |
+
}
|
436 |
+
|
437 |
+
def _build_extreme_weather_knowledge(self) -> Dict:
|
438 |
+
"""Build extreme weather event knowledge"""
|
439 |
+
return {
|
440 |
+
'heat_waves': {
|
441 |
+
'definition': 'Period of excessively hot weather',
|
442 |
+
'criteria': 'Temperature >90°F for 2+ consecutive days',
|
443 |
+
'formation': 'High pressure ridge, subsidence, clear skies',
|
444 |
+
'health_impacts': {
|
445 |
+
'heat_exhaustion': 'Heavy sweating, weakness, nausea',
|
446 |
+
'heat_stroke': 'Body temperature >103°F, organ failure risk',
|
447 |
+
'vulnerable_populations': 'Elderly, children, chronic health conditions'
|
448 |
+
},
|
449 |
+
'mitigation': {
|
450 |
+
'cooling_centers': 'Public facilities with air conditioning',
|
451 |
+
'hydration': 'Increased fluid intake, avoid alcohol',
|
452 |
+
'activity_modification': 'Limit outdoor activities during peak heat'
|
453 |
+
}
|
454 |
+
},
|
455 |
+
'cold_waves': {
|
456 |
+
'definition': 'Period of excessively cold weather',
|
457 |
+
'criteria': 'Temperature significantly below normal for 2+ days',
|
458 |
+
'formation': 'Arctic air mass, polar vortex displacement',
|
459 |
+
'health_impacts': {
|
460 |
+
'hypothermia': 'Body temperature <95°F, organ failure risk',
|
461 |
+
'frostbite': 'Tissue freezing, permanent damage possible',
|
462 |
+
'increased_mortality': 'Heart attacks, accidents, carbon monoxide'
|
463 |
+
},
|
464 |
+
'mitigation': {
|
465 |
+
'heating': 'Adequate home heating, generator safety',
|
466 |
+
'clothing': 'Layered clothing, wind protection',
|
467 |
+
'travel': 'Emergency kits, avoid unnecessary travel'
|
468 |
+
}
|
469 |
+
},
|
470 |
+
'drought': {
|
471 |
+
'types': {
|
472 |
+
'meteorological': 'Below-normal precipitation',
|
473 |
+
'agricultural': 'Soil moisture deficiency',
|
474 |
+
'hydrological': 'Reduced water supply',
|
475 |
+
'socioeconomic': 'Water shortage affecting human activities'
|
476 |
+
},
|
477 |
+
'impacts': {
|
478 |
+
'agriculture': 'Crop failure, livestock stress',
|
479 |
+
'water_supply': 'Reservoir depletion, well failure',
|
480 |
+
'environment': 'Wildfire risk, ecosystem stress',
|
481 |
+
'economy': 'Agricultural losses, increased costs'
|
482 |
+
}
|
483 |
+
},
|
484 |
+
'flooding': {
|
485 |
+
'types': {
|
486 |
+
'flash_flood': 'Rapid onset, small watersheds',
|
487 |
+
'river_flood': 'Gradual onset, large watersheds',
|
488 |
+
'coastal_flood': 'Storm surge, high tides',
|
489 |
+
'urban_flood': 'Overwhelmed drainage systems'
|
490 |
+
},
|
491 |
+
'causes': {
|
492 |
+
'heavy_rainfall': 'Intense precipitation rates',
|
493 |
+
'snowmelt': 'Rapid snow and ice melt',
|
494 |
+
'dam_failure': 'Infrastructure failure',
|
495 |
+
'storm_surge': 'Hurricane/storm-driven water'
|
496 |
+
},
|
497 |
+
'safety': {
|
498 |
+
'awareness': 'Never drive through flooded roads',
|
499 |
+
'evacuation': 'Move to higher ground immediately',
|
500 |
+
'preparation': 'Emergency kits, evacuation plans'
|
501 |
+
}
|
502 |
+
}
|
503 |
+
}
|
504 |
+
|
505 |
+
def get_phenomenon_explanation(self, phenomenon: str) -> Optional[Dict]:
|
506 |
+
"""Get detailed explanation of weather phenomenon"""
|
507 |
+
phenomenon_lower = phenomenon.lower().replace(' ', '_')
|
508 |
+
|
509 |
+
# Search through all knowledge bases
|
510 |
+
for knowledge_base in [self.meteorological_knowledge, self.climate_patterns,
|
511 |
+
self.weather_phenomena, self.extreme_weather_knowledge]:
|
512 |
+
explanation = self._search_knowledge_base(knowledge_base, phenomenon_lower)
|
513 |
+
if explanation:
|
514 |
+
return explanation
|
515 |
+
|
516 |
+
return None
|
517 |
+
|
518 |
+
def get_seasonal_context(self, season: str, month: Optional[int] = None) -> Dict:
|
519 |
+
"""Get seasonal weather context"""
|
520 |
+
if season.lower() in self.seasonal_patterns:
|
521 |
+
context = self.seasonal_patterns[season.lower()].copy()
|
522 |
+
|
523 |
+
if month:
|
524 |
+
# Add month-specific details
|
525 |
+
context['month_specific'] = self._get_month_specific_info(season, month)
|
526 |
+
|
527 |
+
return context
|
528 |
+
|
529 |
+
return {}
|
530 |
+
|
531 |
+
def get_regional_climatology(self, region: str) -> Dict:
|
532 |
+
"""Get regional climate information"""
|
533 |
+
region_lower = region.lower().replace(' ', '_')
|
534 |
+
return self.regional_climatology.get(region_lower, {})
|
535 |
+
|
536 |
+
def get_weather_relationship_explanation(self, variables: List[str]) -> Optional[str]:
|
537 |
+
"""Get explanation of weather variable relationships"""
|
538 |
+
# Check for known relationships
|
539 |
+
for relationship_key, explanation in self.meteorological_knowledge.get('thermodynamics', {}).items():
|
540 |
+
if any(var.lower() in relationship_key for var in variables):
|
541 |
+
return explanation
|
542 |
+
|
543 |
+
return None
|
544 |
+
|
545 |
+
def _search_knowledge_base(self, knowledge_base: Dict, search_term: str) -> Optional[Dict]:
|
546 |
+
"""Recursively search knowledge base for term"""
|
547 |
+
if isinstance(knowledge_base, dict):
|
548 |
+
for key, value in knowledge_base.items():
|
549 |
+
if search_term in key.lower():
|
550 |
+
return {key: value}
|
551 |
+
elif isinstance(value, dict):
|
552 |
+
result = self._search_knowledge_base(value, search_term)
|
553 |
+
if result:
|
554 |
+
return result
|
555 |
+
|
556 |
+
return None
|
557 |
+
|
558 |
+
def _get_month_specific_info(self, season: str, month: int) -> Dict:
|
559 |
+
"""Get month-specific seasonal information"""
|
560 |
+
month_info = {
|
561 |
+
'spring': {
|
562 |
+
3: {'phase': 'early', 'characteristics': 'Spring equinox, rapid warming begins'},
|
563 |
+
4: {'phase': 'mid', 'characteristics': 'Peak severe weather season'},
|
564 |
+
5: {'phase': 'late', 'characteristics': 'Transition to summer patterns'}
|
565 |
+
},
|
566 |
+
'summer': {
|
567 |
+
6: {'phase': 'early', 'characteristics': 'Summer solstice, heat building'},
|
568 |
+
7: {'phase': 'mid', 'characteristics': 'Peak heat, monsoon season'},
|
569 |
+
8: {'phase': 'late', 'characteristics': 'Hurricane season peak'}
|
570 |
+
},
|
571 |
+
'fall': {
|
572 |
+
9: {'phase': 'early', 'characteristics': 'Autumn equinox, cooling begins'},
|
573 |
+
10: {'phase': 'mid', 'characteristics': 'Peak fall foliage, first frost'},
|
574 |
+
11: {'phase': 'late', 'characteristics': 'Winter transition, storm season'}
|
575 |
+
},
|
576 |
+
'winter': {
|
577 |
+
12: {'phase': 'early', 'characteristics': 'Winter solstice, cold deepening'},
|
578 |
+
1: {'phase': 'mid', 'characteristics': 'Coldest period, arctic air masses'},
|
579 |
+
2: {'phase': 'late', 'characteristics': 'Daylight increasing, spring transition'}
|
580 |
+
}
|
581 |
+
}
|
582 |
+
|
583 |
+
return month_info.get(season.lower(), {}).get(month, {})
|
584 |
+
|
585 |
+
def create_weather_knowledge_base() -> WeatherKnowledgeBase:
|
586 |
+
"""Factory function to create weather knowledge base"""
|
587 |
+
return WeatherKnowledgeBase()
|
src/geovisor/__init__.py
ADDED
File without changes
|
src/geovisor/map_manager.py
ADDED
@@ -0,0 +1,667 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
Intelligent Map Management System
|
3 |
+
Dynamic zoom, markers, and weather layer visualization
|
4 |
+
"""
|
5 |
+
|
6 |
+
import folium
|
7 |
+
import json
|
8 |
+
from typing import List, Dict, Tuple, Optional
|
9 |
+
import logging
|
10 |
+
import math
|
11 |
+
import re
|
12 |
+
import re
|
13 |
+
|
14 |
+
logger = logging.getLogger(__name__)
|
15 |
+
|
16 |
+
class WeatherMapManager:
|
17 |
+
"""Advanced map management with intelligent features"""
|
18 |
+
|
19 |
+
def __init__(self):
|
20 |
+
self.default_center = (39.8283, -98.5795) # Geographic center of US
|
21 |
+
self.default_zoom = 4
|
22 |
+
|
23 |
+
# Weather condition to icon mapping
|
24 |
+
self.weather_icons = {
|
25 |
+
# Sunny/Clear conditions
|
26 |
+
'sunny': {'icon': 'sun', 'color': 'orange'},
|
27 |
+
'clear': {'icon': 'sun', 'color': 'orange'},
|
28 |
+
'fair': {'icon': 'sun', 'color': 'orange'},
|
29 |
+
'hot': {'icon': 'sun', 'color': 'red'},
|
30 |
+
'warm': {'icon': 'sun', 'color': 'orange'},
|
31 |
+
|
32 |
+
# Cloudy conditions
|
33 |
+
'cloudy': {'icon': 'cloud', 'color': 'gray'},
|
34 |
+
'partly cloudy': {'icon': 'cloud-sun', 'color': 'lightblue'},
|
35 |
+
'mostly cloudy': {'icon': 'cloud', 'color': 'gray'},
|
36 |
+
'overcast': {'icon': 'cloud', 'color': 'darkgray'},
|
37 |
+
'partly sunny': {'icon': 'cloud-sun', 'color': 'lightblue'},
|
38 |
+
'mostly sunny': {'icon': 'cloud-sun', 'color': 'orange'},
|
39 |
+
|
40 |
+
# Rainy conditions
|
41 |
+
'rainy': {'icon': 'cloud-rain', 'color': 'blue'},
|
42 |
+
'rain': {'icon': 'cloud-rain', 'color': 'blue'},
|
43 |
+
'light rain': {'icon': 'cloud-rain', 'color': 'lightblue'},
|
44 |
+
'heavy rain': {'icon': 'cloud-rain', 'color': 'darkblue'},
|
45 |
+
'drizzle': {'icon': 'cloud-drizzle', 'color': 'lightblue'},
|
46 |
+
'showers': {'icon': 'cloud-showers-heavy', 'color': 'blue'},
|
47 |
+
'scattered showers': {'icon': 'cloud-rain', 'color': 'blue'},
|
48 |
+
'isolated showers': {'icon': 'cloud-rain', 'color': 'lightblue'},
|
49 |
+
|
50 |
+
# Stormy conditions
|
51 |
+
'thunderstorm': {'icon': 'bolt', 'color': 'purple'},
|
52 |
+
'thunderstorms': {'icon': 'bolt', 'color': 'purple'},
|
53 |
+
'severe thunderstorms': {'icon': 'bolt', 'color': 'darkred'},
|
54 |
+
'storm': {'icon': 'bolt', 'color': 'purple'},
|
55 |
+
'storms': {'icon': 'bolt', 'color': 'purple'},
|
56 |
+
'lightning': {'icon': 'bolt', 'color': 'purple'},
|
57 |
+
|
58 |
+
# Snow conditions
|
59 |
+
'snow': {'icon': 'snowflake', 'color': 'white'},
|
60 |
+
'snowy': {'icon': 'snowflake', 'color': 'white'},
|
61 |
+
'light snow': {'icon': 'snowflake', 'color': 'lightgray'},
|
62 |
+
'heavy snow': {'icon': 'snowflake', 'color': 'gray'},
|
63 |
+
'snow showers': {'icon': 'snowflake', 'color': 'lightgray'},
|
64 |
+
'blizzard': {'icon': 'snowflake', 'color': 'darkgray'},
|
65 |
+
'flurries': {'icon': 'snowflake', 'color': 'lightgray'},
|
66 |
+
|
67 |
+
# Windy conditions
|
68 |
+
'windy': {'icon': 'wind', 'color': 'green'},
|
69 |
+
'breezy': {'icon': 'wind', 'color': 'lightgreen'},
|
70 |
+
'gusty': {'icon': 'wind', 'color': 'green'},
|
71 |
+
|
72 |
+
# Foggy/Misty conditions
|
73 |
+
'fog': {'icon': 'smog', 'color': 'gray'},
|
74 |
+
'foggy': {'icon': 'smog', 'color': 'gray'},
|
75 |
+
'mist': {'icon': 'smog', 'color': 'lightgray'},
|
76 |
+
'misty': {'icon': 'smog', 'color': 'lightgray'},
|
77 |
+
'haze': {'icon': 'smog', 'color': 'gray'},
|
78 |
+
'hazy': {'icon': 'smog', 'color': 'gray'},
|
79 |
+
|
80 |
+
# Mixed conditions
|
81 |
+
'rain and snow': {'icon': 'cloud-rain', 'color': 'blue'},
|
82 |
+
'wintry mix': {'icon': 'snowflake', 'color': 'lightblue'},
|
83 |
+
'sleet': {'icon': 'snowflake', 'color': 'lightblue'},
|
84 |
+
'freezing rain': {'icon': 'icicles', 'color': 'lightblue'},
|
85 |
+
'ice': {'icon': 'icicles', 'color': 'lightblue'},
|
86 |
+
'icy': {'icon': 'icicles', 'color': 'lightblue'},
|
87 |
+
|
88 |
+
# Temperature extremes
|
89 |
+
'cold': {'icon': 'thermometer-quarter', 'color': 'blue'},
|
90 |
+
'freezing': {'icon': 'thermometer-empty', 'color': 'lightblue'},
|
91 |
+
'cool': {'icon': 'thermometer-half', 'color': 'lightblue'},
|
92 |
+
|
93 |
+
# Default fallback
|
94 |
+
'default': {'icon': 'cloud', 'color': 'blue'}
|
95 |
+
}
|
96 |
+
|
97 |
+
def _get_weather_icon(self, weather_condition: str) -> Dict[str, str]:
|
98 |
+
"""Determine appropriate icon and color based on weather condition"""
|
99 |
+
if not weather_condition:
|
100 |
+
return self.weather_icons['default']
|
101 |
+
|
102 |
+
# Convert to lowercase for matching
|
103 |
+
condition_lower = weather_condition.lower()
|
104 |
+
|
105 |
+
# Direct match first
|
106 |
+
if condition_lower in self.weather_icons:
|
107 |
+
return self.weather_icons[condition_lower]
|
108 |
+
|
109 |
+
# Partial matching for complex descriptions
|
110 |
+
for key in self.weather_icons:
|
111 |
+
if key in condition_lower:
|
112 |
+
return self.weather_icons[key]
|
113 |
+
|
114 |
+
# Temperature-based fallback
|
115 |
+
if any(term in condition_lower for term in ['hot', 'warm', 'sunny', 'clear']):
|
116 |
+
return self.weather_icons['sunny']
|
117 |
+
elif any(term in condition_lower for term in ['rain', 'shower', 'drizzle']):
|
118 |
+
return self.weather_icons['rainy']
|
119 |
+
elif any(term in condition_lower for term in ['storm', 'thunder', 'lightning']):
|
120 |
+
return self.weather_icons['thunderstorm']
|
121 |
+
elif any(term in condition_lower for term in ['snow', 'blizzard', 'flurries']):
|
122 |
+
return self.weather_icons['snow']
|
123 |
+
elif any(term in condition_lower for term in ['cloud', 'overcast']):
|
124 |
+
return self.weather_icons['cloudy']
|
125 |
+
elif any(term in condition_lower for term in ['fog', 'mist', 'haze']):
|
126 |
+
return self.weather_icons['fog']
|
127 |
+
elif any(term in condition_lower for term in ['wind', 'breezy', 'gusty']):
|
128 |
+
return self.weather_icons['windy']
|
129 |
+
|
130 |
+
# Final fallback
|
131 |
+
return self.weather_icons['default']
|
132 |
+
|
133 |
+
def _get_temperature_icon_enhancement(self, temperature: Optional[int]) -> Dict[str, str]:
|
134 |
+
"""Get additional icon styling based on temperature"""
|
135 |
+
if temperature is None:
|
136 |
+
return {}
|
137 |
+
|
138 |
+
# Temperature-based color adjustments
|
139 |
+
if temperature >= 90:
|
140 |
+
return {'color': 'red'}
|
141 |
+
elif temperature >= 80:
|
142 |
+
return {'color': 'orange'}
|
143 |
+
elif temperature >= 70:
|
144 |
+
return {'color': 'green'}
|
145 |
+
elif temperature >= 60:
|
146 |
+
return {'color': 'lightgreen'}
|
147 |
+
elif temperature >= 50:
|
148 |
+
return {'color': 'lightblue'}
|
149 |
+
elif temperature >= 40:
|
150 |
+
return {'color': 'blue'}
|
151 |
+
elif temperature >= 32:
|
152 |
+
return {'color': 'purple'}
|
153 |
+
else:
|
154 |
+
return {'color': 'darkblue'}
|
155 |
+
|
156 |
+
def _get_weather_emoji(self, weather_condition: str) -> str:
|
157 |
+
"""Get appropriate emoji for weather condition"""
|
158 |
+
if not weather_condition:
|
159 |
+
return "🌤️"
|
160 |
+
|
161 |
+
condition_lower = weather_condition.lower()
|
162 |
+
|
163 |
+
# Direct emoji mapping
|
164 |
+
emoji_map = {
|
165 |
+
'sunny': '☀️', 'clear': '☀️', 'fair': '🌤️', 'hot': '🌡️',
|
166 |
+
'cloudy': '☁️', 'partly cloudy': '⛅', 'mostly cloudy': '☁️', 'overcast': '☁️',
|
167 |
+
'partly sunny': '⛅', 'mostly sunny': '🌤️',
|
168 |
+
'rainy': '🌧️', 'rain': '🌧️', 'light rain': '🌦️', 'heavy rain': '🌧️',
|
169 |
+
'drizzle': '🌦️', 'showers': '🌦️', 'scattered showers': '🌦️',
|
170 |
+
'thunderstorm': '⛈️', 'thunderstorms': '⛈️', 'storm': '⛈️', 'storms': '⛈️',
|
171 |
+
'snow': '❄️', 'snowy': '❄️', 'light snow': '🌨️', 'heavy snow': '❄️',
|
172 |
+
'snow showers': '🌨️', 'blizzard': '🌨️', 'flurries': '🌨️',
|
173 |
+
'windy': '💨', 'breezy': '🍃', 'gusty': '💨',
|
174 |
+
'fog': '🌫️', 'foggy': '🌫️', 'mist': '🌫️', 'misty': '🌫️', 'haze': '🌫️',
|
175 |
+
'freezing rain': '🧊', 'sleet': '🧊', 'ice': '🧊', 'icy': '🧊'
|
176 |
+
}
|
177 |
+
|
178 |
+
# Direct match
|
179 |
+
if condition_lower in emoji_map:
|
180 |
+
return emoji_map[condition_lower]
|
181 |
+
|
182 |
+
# Partial matching
|
183 |
+
for key, emoji in emoji_map.items():
|
184 |
+
if key in condition_lower:
|
185 |
+
return emoji
|
186 |
+
|
187 |
+
return "🌤️" # Default emoji
|
188 |
+
|
189 |
+
def calculate_optimal_bounds(self, coordinates: List[Tuple[float, float]]) -> Dict:
|
190 |
+
"""Calculate optimal map bounds for given coordinates"""
|
191 |
+
if not coordinates:
|
192 |
+
return {'center': self.default_center, 'zoom': self.default_zoom}
|
193 |
+
|
194 |
+
if len(coordinates) == 1:
|
195 |
+
return {'center': coordinates[0], 'zoom': 10}
|
196 |
+
|
197 |
+
# Calculate center point
|
198 |
+
lats = [coord[0] for coord in coordinates]
|
199 |
+
lons = [coord[1] for coord in coordinates]
|
200 |
+
|
201 |
+
center_lat = sum(lats) / len(lats)
|
202 |
+
center_lon = sum(lons) / len(lons)
|
203 |
+
|
204 |
+
# Calculate zoom based on coordinate spread
|
205 |
+
lat_range = max(lats) - min(lats)
|
206 |
+
lon_range = max(lons) - min(lons)
|
207 |
+
max_range = max(lat_range, lon_range)
|
208 |
+
|
209 |
+
# Determine appropriate zoom level
|
210 |
+
if max_range < 0.5:
|
211 |
+
zoom = 11
|
212 |
+
elif max_range < 1:
|
213 |
+
zoom = 9
|
214 |
+
elif max_range < 3:
|
215 |
+
zoom = 8
|
216 |
+
elif max_range < 5:
|
217 |
+
zoom = 7
|
218 |
+
elif max_range < 10:
|
219 |
+
zoom = 6
|
220 |
+
elif max_range < 20:
|
221 |
+
zoom = 5
|
222 |
+
else:
|
223 |
+
zoom = 4
|
224 |
+
|
225 |
+
return {'center': (center_lat, center_lon), 'zoom': zoom}
|
226 |
+
|
227 |
+
def create_weather_map(self, cities_data: List[Dict], comparison_mode: bool = False,
|
228 |
+
show_weather_layers: bool = True) -> str:
|
229 |
+
"""Create comprehensive weather map with all features"""
|
230 |
+
try:
|
231 |
+
# Calculate optimal view
|
232 |
+
if cities_data:
|
233 |
+
coordinates = [(city['lat'], city['lon']) for city in cities_data]
|
234 |
+
bounds = self.calculate_optimal_bounds(coordinates)
|
235 |
+
else:
|
236 |
+
bounds = {'center': self.default_center, 'zoom': self.default_zoom}
|
237 |
+
|
238 |
+
# Create base map with street map as default
|
239 |
+
m = folium.Map(
|
240 |
+
location=bounds['center'],
|
241 |
+
zoom_start=bounds['zoom'],
|
242 |
+
tiles='OpenStreetMap',
|
243 |
+
attr='OpenStreetMap'
|
244 |
+
)
|
245 |
+
|
246 |
+
# Add alternative tile layers
|
247 |
+
folium.TileLayer(
|
248 |
+
'CartoDB dark_matter',
|
249 |
+
name='Dark Theme',
|
250 |
+
overlay=False,
|
251 |
+
control=True
|
252 |
+
).add_to(m)
|
253 |
+
|
254 |
+
folium.TileLayer(
|
255 |
+
'CartoDB positron',
|
256 |
+
name='Light Theme',
|
257 |
+
overlay=False,
|
258 |
+
control=True
|
259 |
+
).add_to(m)
|
260 |
+
|
261 |
+
# Add weather layers if requested
|
262 |
+
if show_weather_layers:
|
263 |
+
self._add_weather_layers(m)
|
264 |
+
|
265 |
+
# Add city markers
|
266 |
+
if cities_data:
|
267 |
+
self._add_city_markers(m, cities_data)
|
268 |
+
|
269 |
+
# Add comparison features
|
270 |
+
if comparison_mode and len(cities_data) > 1:
|
271 |
+
self._add_comparison_features(m, cities_data)
|
272 |
+
|
273 |
+
# Add map controls
|
274 |
+
folium.LayerControl().add_to(m)
|
275 |
+
|
276 |
+
# Add fullscreen button
|
277 |
+
from folium.plugins import Fullscreen
|
278 |
+
Fullscreen().add_to(m)
|
279 |
+
|
280 |
+
return m._repr_html_()
|
281 |
+
|
282 |
+
except Exception as e:
|
283 |
+
logger.error(f"Error creating weather map: {e}")
|
284 |
+
return self._create_error_map(str(e))
|
285 |
+
|
286 |
+
def _add_weather_layers(self, map_obj: folium.Map):
|
287 |
+
"""Add weather overlay layers"""
|
288 |
+
try:
|
289 |
+
# Precipitation radar layer
|
290 |
+
precipitation_layer = folium.raster_layers.WmsTileLayer(
|
291 |
+
url='https://mesonet.agron.iastate.edu/cgi-bin/wms/nexrad/n0r.cgi',
|
292 |
+
layers='nexrad-n0r-900913',
|
293 |
+
name='Precipitation Radar',
|
294 |
+
overlay=True,
|
295 |
+
control=True,
|
296 |
+
transparent=True,
|
297 |
+
format='image/png',
|
298 |
+
opacity=0.6
|
299 |
+
)
|
300 |
+
precipitation_layer.add_to(map_obj)
|
301 |
+
|
302 |
+
# Temperature layer (simplified)
|
303 |
+
# Note: In production, you'd use actual weather service WMS layers
|
304 |
+
|
305 |
+
except Exception as e:
|
306 |
+
logger.warning(f"Could not add weather layers: {e}")
|
307 |
+
|
308 |
+
def _add_city_markers(self, map_obj: folium.Map, cities_data: List[Dict]):
|
309 |
+
"""Add markers for cities with weather information"""
|
310 |
+
for i, city_data in enumerate(cities_data):
|
311 |
+
# Get weather condition and temperature from forecast
|
312 |
+
forecast = city_data.get('forecast', [])
|
313 |
+
current_weather = forecast[0] if forecast else {}
|
314 |
+
weather_condition = current_weather.get('shortForecast', '')
|
315 |
+
temperature = current_weather.get('temperature')
|
316 |
+
|
317 |
+
# Determine weather-appropriate icon
|
318 |
+
weather_icon_info = self._get_weather_icon(weather_condition)
|
319 |
+
icon_name = weather_icon_info['icon']
|
320 |
+
icon_color = weather_icon_info['color']
|
321 |
+
|
322 |
+
# Apply temperature-based color enhancement if available
|
323 |
+
if temperature:
|
324 |
+
temp_enhancement = self._get_temperature_icon_enhancement(temperature)
|
325 |
+
if temp_enhancement.get('color'):
|
326 |
+
icon_color = temp_enhancement['color']
|
327 |
+
|
328 |
+
# Create weather condition tooltip
|
329 |
+
weather_tooltip = f"📍 {city_data['name'].title()}"
|
330 |
+
if weather_condition:
|
331 |
+
weather_tooltip += f" - {weather_condition}"
|
332 |
+
if temperature:
|
333 |
+
weather_tooltip += f" ({temperature}°F)"
|
334 |
+
weather_tooltip += " - Click for details"
|
335 |
+
|
336 |
+
# Create detailed popup
|
337 |
+
popup_html = self._create_popup_html(city_data)
|
338 |
+
|
339 |
+
# Add main marker with weather-appropriate icon
|
340 |
+
folium.Marker(
|
341 |
+
location=[city_data['lat'], city_data['lon']],
|
342 |
+
popup=folium.Popup(popup_html, max_width=350),
|
343 |
+
tooltip=weather_tooltip,
|
344 |
+
icon=folium.Icon(
|
345 |
+
color=icon_color,
|
346 |
+
icon=icon_name,
|
347 |
+
prefix='fa'
|
348 |
+
)
|
349 |
+
).add_to(map_obj)
|
350 |
+
|
351 |
+
# Add wind flow arrow if wind data is available
|
352 |
+
wind_speed = current_weather.get('windSpeed')
|
353 |
+
wind_direction = current_weather.get('windDirection')
|
354 |
+
if wind_speed and wind_direction:
|
355 |
+
self._add_wind_arrow(map_obj, city_data['lat'], city_data['lon'],
|
356 |
+
wind_speed, wind_direction)
|
357 |
+
|
358 |
+
# Add weather circle indicator
|
359 |
+
if city_data.get('forecast'):
|
360 |
+
current_temp = city_data['forecast'][0].get('temperature', 0)
|
361 |
+
|
362 |
+
# Color code temperature
|
363 |
+
if current_temp > 80:
|
364 |
+
circle_color = 'red'
|
365 |
+
elif current_temp > 60:
|
366 |
+
circle_color = 'orange'
|
367 |
+
elif current_temp > 40:
|
368 |
+
circle_color = 'yellow'
|
369 |
+
else:
|
370 |
+
circle_color = 'blue'
|
371 |
+
|
372 |
+
folium.Circle(
|
373 |
+
location=[city_data['lat'], city_data['lon']],
|
374 |
+
radius=30000, # 30km radius
|
375 |
+
popup=f"Temperature Zone: {current_temp}°F",
|
376 |
+
color=circle_color,
|
377 |
+
fill=True,
|
378 |
+
fillOpacity=0.2,
|
379 |
+
weight=2
|
380 |
+
).add_to(map_obj)
|
381 |
+
|
382 |
+
# Add wind arrow if wind data is available
|
383 |
+
if current_weather.get('windSpeed') and current_weather.get('windDirection'):
|
384 |
+
self._add_wind_arrow(map_obj, city_data['lat'], city_data['lon'],
|
385 |
+
current_weather['windSpeed'], current_weather['windDirection'])
|
386 |
+
|
387 |
+
def _add_comparison_features(self, map_obj: folium.Map, cities_data: List[Dict]):
|
388 |
+
"""Add comparison lines and features between cities"""
|
389 |
+
if len(cities_data) < 2:
|
390 |
+
return
|
391 |
+
|
392 |
+
# Add connection lines between all cities
|
393 |
+
coordinates = [[city['lat'], city['lon']] for city in cities_data]
|
394 |
+
|
395 |
+
# Create comparison route
|
396 |
+
folium.PolyLine(
|
397 |
+
coordinates,
|
398 |
+
color='yellow',
|
399 |
+
weight=4,
|
400 |
+
opacity=0.8,
|
401 |
+
popup="Weather Comparison Route",
|
402 |
+
tooltip="Cities being compared"
|
403 |
+
).add_to(map_obj)
|
404 |
+
|
405 |
+
# Add midpoint marker for comparison summary
|
406 |
+
if len(cities_data) == 2:
|
407 |
+
city1, city2 = cities_data[0], cities_data[1]
|
408 |
+
mid_lat = (city1['lat'] + city2['lat']) / 2
|
409 |
+
mid_lon = (city1['lon'] + city2['lon']) / 2
|
410 |
+
|
411 |
+
comparison_summary = self._create_comparison_summary(city1, city2)
|
412 |
+
|
413 |
+
folium.Marker(
|
414 |
+
location=[mid_lat, mid_lon],
|
415 |
+
popup=folium.Popup(comparison_summary, max_width=400),
|
416 |
+
tooltip="Comparison Summary",
|
417 |
+
icon=folium.Icon(
|
418 |
+
color='lightblue',
|
419 |
+
icon='balance-scale',
|
420 |
+
prefix='fa'
|
421 |
+
)
|
422 |
+
).add_to(map_obj)
|
423 |
+
|
424 |
+
def _create_popup_html(self, city_data: Dict) -> str:
|
425 |
+
"""Create detailed HTML popup for city marker"""
|
426 |
+
forecast = city_data.get('forecast', [])
|
427 |
+
if not forecast:
|
428 |
+
return f"""
|
429 |
+
<div style="width: 300px; font-family: Arial, sans-serif;">
|
430 |
+
<h3 style="margin: 0; color: #2c3e50;">📍 {city_data['name'].title()}</h3>
|
431 |
+
<p>No weather data available</p>
|
432 |
+
</div>
|
433 |
+
"""
|
434 |
+
|
435 |
+
current = forecast[0]
|
436 |
+
next_period = forecast[1] if len(forecast) > 1 else {}
|
437 |
+
|
438 |
+
html = f"""
|
439 |
+
<div style="width: 320px; font-family: Arial, sans-serif; line-height: 1.4;">
|
440 |
+
<h3 style="margin: 0 0 10px 0; color: #2c3e50; border-bottom: 2px solid #3498db; padding-bottom: 5px;">
|
441 |
+
📍 {city_data['name'].title()}
|
442 |
+
</h3>
|
443 |
+
|
444 |
+
<div style="background: #f8f9fa; padding: 10px; border-radius: 5px; margin-bottom: 10px;">
|
445 |
+
<h4 style="margin: 0 0 8px 0; color: #e74c3c;">🌡️ Current Conditions</h4>
|
446 |
+
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 5px; font-size: 13px;">
|
447 |
+
<div><strong>Temperature:</strong></div>
|
448 |
+
<div>{current.get('temperature', 'N/A')}°{current.get('temperatureUnit', 'F')}</div>
|
449 |
+
|
450 |
+
<div><strong>Conditions:</strong></div>
|
451 |
+
<div>{current.get('shortForecast', 'N/A')}</div>
|
452 |
+
|
453 |
+
<div><strong>Wind:</strong></div>
|
454 |
+
<div>{current.get('windSpeed', 'N/A')} {current.get('windDirection', '')}</div>
|
455 |
+
|
456 |
+
<div><strong>Rain Chance:</strong></div>
|
457 |
+
<div>{current.get('precipitationProbability', 0)}%</div>
|
458 |
+
</div>
|
459 |
+
</div>
|
460 |
+
|
461 |
+
<div style="background: #e8f4f8; padding: 10px; border-radius: 5px; margin-bottom: 10px;">
|
462 |
+
<h4 style="margin: 0 0 8px 0; color: #2980b9;">📅 Next Period</h4>
|
463 |
+
<div style="font-size: 13px;">
|
464 |
+
<strong>{next_period.get('name', 'N/A')}:</strong><br>
|
465 |
+
{next_period.get('temperature', 'N/A')}°{next_period.get('temperatureUnit', 'F')} -
|
466 |
+
{next_period.get('shortForecast', 'N/A')}
|
467 |
+
</div>
|
468 |
+
</div>
|
469 |
+
|
470 |
+
<div style="background: #fff3cd; padding: 8px; border-radius: 5px; font-size: 12px;">
|
471 |
+
<strong>📝 Details:</strong><br>
|
472 |
+
{current.get('detailedForecast', 'No detailed forecast available')[:150]}...
|
473 |
+
</div>
|
474 |
+
|
475 |
+
<div style="text-align: center; margin-top: 10px; font-size: 11px; color: #6c757d;">
|
476 |
+
📊 Coordinates: {city_data['lat']:.3f}°, {city_data['lon']:.3f}°
|
477 |
+
</div>
|
478 |
+
</div>
|
479 |
+
"""
|
480 |
+
|
481 |
+
return html
|
482 |
+
|
483 |
+
def _create_comparison_summary(self, city1: Dict, city2: Dict) -> str:
|
484 |
+
"""Create comparison summary popup"""
|
485 |
+
name1, name2 = city1['name'].title(), city2['name'].title()
|
486 |
+
forecast1 = city1.get('forecast', [{}])[0]
|
487 |
+
forecast2 = city2.get('forecast', [{}])[0]
|
488 |
+
|
489 |
+
temp1 = forecast1.get('temperature', 0)
|
490 |
+
temp2 = forecast2.get('temperature', 0)
|
491 |
+
temp_diff = abs(temp1 - temp2)
|
492 |
+
warmer_city = name1 if temp1 > temp2 else name2
|
493 |
+
|
494 |
+
rain1 = forecast1.get('precipitationProbability', 0)
|
495 |
+
rain2 = forecast2.get('precipitationProbability', 0)
|
496 |
+
rain_diff = abs(rain1 - rain2)
|
497 |
+
rainier_city = name1 if rain1 > rain2 else name2
|
498 |
+
|
499 |
+
html = f"""
|
500 |
+
<div style="width: 350px; font-family: Arial, sans-serif;">
|
501 |
+
<h3 style="margin: 0 0 15px 0; color: #2c3e50; text-align: center; border-bottom: 2px solid #f39c12; padding-bottom: 8px;">
|
502 |
+
⚖️ Weather Comparison
|
503 |
+
</h3>
|
504 |
+
|
505 |
+
<div style="background: linear-gradient(135deg, #667eea, #764ba2); color: white; padding: 12px; border-radius: 8px; margin-bottom: 15px;">
|
506 |
+
<h4 style="margin: 0 0 10px 0; text-align: center;">🌡️ Temperature Comparison</h4>
|
507 |
+
<div style="display: grid; grid-template-columns: 1fr auto 1fr; gap: 10px; align-items: center;">
|
508 |
+
<div style="text-align: center;">
|
509 |
+
<div style="font-weight: bold;">{name1}</div>
|
510 |
+
<div style="font-size: 18px;">{temp1}°F</div>
|
511 |
+
</div>
|
512 |
+
<div style="text-align: center; font-size: 20px;">VS</div>
|
513 |
+
<div style="text-align: center;">
|
514 |
+
<div style="font-weight: bold;">{name2}</div>
|
515 |
+
<div style="font-size: 18px;">{temp2}°F</div>
|
516 |
+
</div>
|
517 |
+
</div>
|
518 |
+
<div style="text-align: center; margin-top: 10px; font-size: 14px;">
|
519 |
+
<strong>{warmer_city}</strong> is {temp_diff}°F warmer
|
520 |
+
</div>
|
521 |
+
</div>
|
522 |
+
|
523 |
+
<div style="background: linear-gradient(135deg, #4ecdc4, #44a08d); color: white; padding: 12px; border-radius: 8px; margin-bottom: 15px;">
|
524 |
+
<h4 style="margin: 0 0 10px 0; text-align: center;">🌧️ Precipitation Comparison</h4>
|
525 |
+
<div style="display: grid; grid-template-columns: 1fr auto 1fr; gap: 10px; align-items: center;">
|
526 |
+
<div style="text-align: center;">
|
527 |
+
<div style="font-weight: bold;">{name1}</div>
|
528 |
+
<div style="font-size: 18px;">{rain1}%</div>
|
529 |
+
</div>
|
530 |
+
<div style="text-align: center; font-size: 20px;">VS</div>
|
531 |
+
<div style="text-align: center;">
|
532 |
+
<div style="font-weight: bold;">{name2}</div>
|
533 |
+
<div style="font-size: 18px;">{rain2}%</div>
|
534 |
+
</div>
|
535 |
+
</div>
|
536 |
+
<div style="text-align: center; margin-top: 10px; font-size: 14px;">
|
537 |
+
<strong>{rainier_city}</strong> has {rain_diff}% higher chance
|
538 |
+
</div>
|
539 |
+
</div>
|
540 |
+
|
541 |
+
<div style="background: #f8f9fa; padding: 10px; border-radius: 5px; font-size: 13px; text-align: center;">
|
542 |
+
<strong>📏 Distance:</strong> {self._calculate_distance(city1['lat'], city1['lon'], city2['lat'], city2['lon']):.0f} miles
|
543 |
+
</div>
|
544 |
+
</div>
|
545 |
+
"""
|
546 |
+
|
547 |
+
return html
|
548 |
+
|
549 |
+
def _calculate_distance(self, lat1: float, lon1: float, lat2: float, lon2: float) -> float:
|
550 |
+
"""Calculate distance between two points using Haversine formula"""
|
551 |
+
R = 3959 # Earth's radius in miles
|
552 |
+
|
553 |
+
lat1_rad = math.radians(lat1)
|
554 |
+
lat2_rad = math.radians(lat2)
|
555 |
+
delta_lat = math.radians(lat2 - lat1)
|
556 |
+
delta_lon = math.radians(lon2 - lon1)
|
557 |
+
|
558 |
+
a = (math.sin(delta_lat / 2) ** 2 +
|
559 |
+
math.cos(lat1_rad) * math.cos(lat2_rad) * math.sin(delta_lon / 2) ** 2)
|
560 |
+
c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
|
561 |
+
|
562 |
+
return R * c
|
563 |
+
|
564 |
+
def _create_error_map(self, error_message: str) -> str:
|
565 |
+
"""Create error map when main map creation fails"""
|
566 |
+
try:
|
567 |
+
m = folium.Map(
|
568 |
+
location=self.default_center,
|
569 |
+
zoom_start=self.default_zoom,
|
570 |
+
tiles='CartoDB dark_matter'
|
571 |
+
)
|
572 |
+
|
573 |
+
folium.Marker(
|
574 |
+
location=self.default_center,
|
575 |
+
popup=f"Error: {error_message}",
|
576 |
+
tooltip="Map Error",
|
577 |
+
icon=folium.Icon(color='red', icon='exclamation-triangle', prefix='fa')
|
578 |
+
).add_to(m)
|
579 |
+
|
580 |
+
return m._repr_html_()
|
581 |
+
except:
|
582 |
+
return f"""
|
583 |
+
<div style="width: 100%; height: 400px; background: #2c3e50; color: white;
|
584 |
+
display: flex; align-items: center; justify-content: center;
|
585 |
+
font-family: Arial, sans-serif; border-radius: 10px;">
|
586 |
+
<div style="text-align: center;">
|
587 |
+
<h3>🗺️ Map Error</h3>
|
588 |
+
<p>Unable to load map: {error_message}</p>
|
589 |
+
</div>
|
590 |
+
</div>
|
591 |
+
"""
|
592 |
+
|
593 |
+
def _add_wind_arrow(self, map_obj: folium.Map, lat: float, lon: float,
|
594 |
+
wind_speed: str, wind_direction: str) -> None:
|
595 |
+
"""Add wind flow arrow to the map"""
|
596 |
+
try:
|
597 |
+
# Parse wind speed and direction
|
598 |
+
if not wind_speed or not wind_direction or wind_speed == 'N/A':
|
599 |
+
return
|
600 |
+
|
601 |
+
# Extract numeric wind speed
|
602 |
+
speed_match = re.search(r'(\d+)', str(wind_speed))
|
603 |
+
if not speed_match:
|
604 |
+
return
|
605 |
+
speed = int(speed_match.group(1))
|
606 |
+
|
607 |
+
# Convert wind direction to degrees
|
608 |
+
direction_map = {
|
609 |
+
'N': 0, 'NNE': 22.5, 'NE': 45, 'ENE': 67.5,
|
610 |
+
'E': 90, 'ESE': 112.5, 'SE': 135, 'SSE': 157.5,
|
611 |
+
'S': 180, 'SSW': 202.5, 'SW': 225, 'WSW': 247.5,
|
612 |
+
'W': 270, 'WNW': 292.5, 'NW': 315, 'NNW': 337.5
|
613 |
+
}
|
614 |
+
|
615 |
+
direction_deg = direction_map.get(wind_direction.upper(), 0)
|
616 |
+
|
617 |
+
# Calculate wind arrow properties
|
618 |
+
arrow_length = min(max(speed * 0.001, 0.01), 0.05) # Scale arrow length
|
619 |
+
arrow_color = self._get_wind_arrow_color(speed)
|
620 |
+
|
621 |
+
# Calculate arrow endpoint
|
622 |
+
import math
|
623 |
+
# Convert to radians and adjust for map orientation (wind direction is "from")
|
624 |
+
rad = math.radians(direction_deg + 180) # +180 because wind direction is "from"
|
625 |
+
end_lat = lat + arrow_length * math.cos(rad)
|
626 |
+
end_lon = lon + arrow_length * math.sin(rad)
|
627 |
+
|
628 |
+
# Create wind arrow as a polyline with arrowhead
|
629 |
+
folium.PolyLine(
|
630 |
+
locations=[(lat, lon), (end_lat, end_lon)],
|
631 |
+
color=arrow_color,
|
632 |
+
weight=4,
|
633 |
+
opacity=0.8,
|
634 |
+
popup=f"Wind: {wind_speed} from {wind_direction}",
|
635 |
+
tooltip=f"💨 {wind_speed} {wind_direction}"
|
636 |
+
).add_to(map_obj)
|
637 |
+
|
638 |
+
# Add arrowhead marker
|
639 |
+
folium.Marker(
|
640 |
+
location=[end_lat, end_lon],
|
641 |
+
icon=folium.Icon(
|
642 |
+
icon='arrow-up',
|
643 |
+
prefix='fa',
|
644 |
+
color=arrow_color,
|
645 |
+
icon_size=(10, 10)
|
646 |
+
),
|
647 |
+
popup=f"Wind: {wind_speed} from {wind_direction}"
|
648 |
+
).add_to(map_obj)
|
649 |
+
|
650 |
+
except Exception as e:
|
651 |
+
logger.warning(f"Could not add wind arrow: {e}")
|
652 |
+
|
653 |
+
def _get_wind_arrow_color(self, speed: int) -> str:
|
654 |
+
"""Get color for wind arrow based on speed"""
|
655 |
+
if speed >= 30:
|
656 |
+
return 'red' # Strong winds
|
657 |
+
elif speed >= 20:
|
658 |
+
return 'orange' # Moderate winds
|
659 |
+
elif speed >= 10:
|
660 |
+
return 'yellow' # Light winds
|
661 |
+
else:
|
662 |
+
return 'green' # Calm winds
|
663 |
+
|
664 |
+
def create_map_manager() -> WeatherMapManager:
|
665 |
+
"""Factory function to create map manager"""
|
666 |
+
return WeatherMapManager()
|
667 |
+
|
src/mcp_server/__init__.py
ADDED
File without changes
|
src/utils/__init__.py
ADDED
File without changes
|
wind_test_interface.html
ADDED
File without changes
|