chirfort commited on
Commit
7570791
·
0 Parent(s):

Initial commit of Weather App

Browse files
.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