Update server.py
Browse files
server.py
CHANGED
@@ -1,42 +1,57 @@
|
|
1 |
import os
|
2 |
import logging
|
3 |
import random
|
|
|
|
|
|
|
|
|
|
|
4 |
from fastapi import FastAPI, HTTPException
|
5 |
from fastapi.middleware.cors import CORSMiddleware
|
6 |
from pydantic import BaseModel
|
7 |
from dotenv import dotenv_values
|
8 |
-
import asyncio
|
9 |
-
from datetime import datetime, timedelta
|
10 |
-
import csv
|
11 |
-
from io import StringIO
|
12 |
-
import openai
|
13 |
|
14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
15 |
from a2a_agents import sms_agent, whatsapp_agent, ussd_agent, ivr_agent, telegram_agent
|
16 |
from utils.weather_utils import get_tool_config
|
17 |
|
18 |
-
|
19 |
config = dotenv_values(".env")
|
20 |
-
|
21 |
LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO")
|
22 |
logging.basicConfig(level=LOG_LEVEL)
|
23 |
logger = logging.getLogger(__name__)
|
24 |
|
25 |
-
|
26 |
-
|
|
|
|
|
|
|
|
|
27 |
|
28 |
-
app = FastAPI()
|
29 |
|
30 |
-
# CORS middleware
|
31 |
app.add_middleware(
|
32 |
CORSMiddleware,
|
33 |
-
allow_origins=["https://mcp-ui.vercel.app"],
|
34 |
allow_credentials=True,
|
35 |
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
36 |
allow_headers=["*"],
|
37 |
expose_headers=["*"]
|
38 |
)
|
39 |
|
|
|
40 |
class MCPRequest(BaseModel):
|
41 |
tool: str
|
42 |
parameters: dict
|
@@ -48,407 +63,63 @@ class WorkflowRequest(BaseModel):
|
|
48 |
state: str
|
49 |
district: str
|
50 |
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
-
|
67 |
-
|
68 |
-
|
69 |
-
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
|
78 |
-
|
79 |
-
|
80 |
-
|
81 |
-
|
82 |
-
|
83 |
-
|
84 |
-
|
85 |
-
|
86 |
-
|
87 |
-
|
88 |
-
|
89 |
-
|
90 |
-
|
91 |
-
|
92 |
-
elif current_month in [5, 6, 7, 8]:
|
93 |
-
return 'growing'
|
94 |
-
elif current_month in [9, 10, 11]:
|
95 |
-
return 'maturing'
|
96 |
-
else:
|
97 |
-
return 'harvesting'
|
98 |
-
elif crop == 'maize':
|
99 |
-
if current_month in [6, 7]:
|
100 |
-
return 'planting'
|
101 |
-
elif current_month in [8, 9]:
|
102 |
-
return 'growing'
|
103 |
-
elif current_month in [10, 11]:
|
104 |
-
return 'flowering'
|
105 |
-
else:
|
106 |
-
return 'harvesting'
|
107 |
-
|
108 |
-
return 'growing'
|
109 |
-
|
110 |
-
async def generate_dynamic_alert(district: str, state: str):
|
111 |
-
"""Generate dynamic alert data using geographic functions and REAL weather data"""
|
112 |
-
|
113 |
-
try:
|
114 |
-
# Step 1: Get villages for the district using your geographic tools
|
115 |
-
villages_data = await geographic_tools.list_villages(state, district)
|
116 |
-
|
117 |
-
if "error" in villages_data:
|
118 |
-
raise Exception(f"District '{district}' not found in {state}")
|
119 |
-
|
120 |
-
# Step 2: Pick a random village from the actual list
|
121 |
-
available_villages = villages_data.get("villages", [])
|
122 |
-
if not available_villages:
|
123 |
-
raise Exception(f"No villages found for {district}")
|
124 |
-
|
125 |
-
selected_village = random.choice(available_villages)
|
126 |
-
logger.info(f"Selected village: {selected_village} from {len(available_villages)} villages")
|
127 |
-
|
128 |
-
# Step 3: Try to get coordinates for the selected village first, then district
|
129 |
-
location_coords = None
|
130 |
-
location_source = ""
|
131 |
-
|
132 |
-
# Try village coordinates first
|
133 |
-
try:
|
134 |
-
village_location = await geographic_tools.reverse_geocode(selected_village)
|
135 |
-
if "error" not in village_location and "lat" in village_location:
|
136 |
-
location_coords = [village_location["lat"], village_location["lng"]]
|
137 |
-
location_source = f"village_{selected_village}"
|
138 |
-
logger.info(f"Using village coordinates for {selected_village}: {location_coords}")
|
139 |
-
except Exception as e:
|
140 |
-
logger.warning(f"Village geocoding failed for {selected_village}: {e}")
|
141 |
-
|
142 |
-
# Fallback to district coordinates if village lookup failed
|
143 |
-
if not location_coords:
|
144 |
-
try:
|
145 |
-
district_location = await geographic_tools.reverse_geocode(district)
|
146 |
-
if "error" not in district_location and "lat" in district_location:
|
147 |
-
location_coords = [district_location["lat"], district_location["lng"]]
|
148 |
-
location_source = f"district_{district}"
|
149 |
-
logger.info(f"Using district coordinates for {district}: {location_coords}")
|
150 |
-
except Exception as e:
|
151 |
-
logger.warning(f"District geocoding failed for {district}: {e}")
|
152 |
-
|
153 |
-
# Final fallback - but this should rarely happen now
|
154 |
-
if not location_coords:
|
155 |
-
logger.warning(f"No coordinates found for {selected_village} or {district}, using default")
|
156 |
-
location_coords = [25.5941, 85.1376] # Patna fallback
|
157 |
-
location_source = "fallback_patna"
|
158 |
-
|
159 |
-
# Step 4: Generate regional crop and stage using crop calendar data
|
160 |
-
regional_crop = await get_regional_crop_for_area(district, state)
|
161 |
-
crop_stage = await get_current_crop_stage_dynamic(regional_crop, district)
|
162 |
-
|
163 |
-
# Step 5: GET REAL WEATHER DATA using the actual coordinates
|
164 |
-
try:
|
165 |
-
logger.info(f"Fetching weather for coordinates: {location_coords} (source: {location_source})")
|
166 |
-
|
167 |
-
current_weather_data = await open_meteo.get_current_weather(
|
168 |
-
latitude=location_coords[0],
|
169 |
-
longitude=location_coords[1]
|
170 |
-
)
|
171 |
-
|
172 |
-
forecast_data = await open_meteo.get_weather_forecast(
|
173 |
-
latitude=location_coords[0],
|
174 |
-
longitude=location_coords[1],
|
175 |
-
days=7
|
176 |
-
)
|
177 |
-
|
178 |
-
current_weather = current_weather_data.get('current_weather', {})
|
179 |
-
daily_forecast = forecast_data.get('daily', {})
|
180 |
-
|
181 |
-
current_temp = current_weather.get('temperature', 25)
|
182 |
-
current_windspeed = current_weather.get('windspeed', 10)
|
183 |
-
|
184 |
-
precipitation_list = daily_forecast.get('precipitation_sum', [0, 0, 0])
|
185 |
-
next_3_days_rain = sum(precipitation_list[:3]) if precipitation_list else 0
|
186 |
-
|
187 |
-
rain_probability = min(90, max(10, int(next_3_days_rain * 10))) if next_3_days_rain > 0 else 10
|
188 |
-
|
189 |
-
# Higher precipitation = higher humidity estimate
|
190 |
-
estimated_humidity = min(95, max(40, 60 + int(next_3_days_rain * 2)))
|
191 |
-
|
192 |
-
real_weather = {
|
193 |
-
"forecast_days": 3,
|
194 |
-
"rain_probability": rain_probability,
|
195 |
-
"expected_rainfall": f"{next_3_days_rain:.1f}mm",
|
196 |
-
"temperature": f"{current_temp:.1f}°C",
|
197 |
-
"humidity": f"{estimated_humidity}%",
|
198 |
-
"wind_speed": f"{current_windspeed:.1f} km/h",
|
199 |
-
"coordinates_source": location_source # Track where coords came from
|
200 |
-
}
|
201 |
-
|
202 |
-
# Step 6: Generate alert message based on actual weather conditions
|
203 |
-
if next_3_days_rain > 25:
|
204 |
-
alert_type = "heavy_rain_warning"
|
205 |
-
urgency = "high"
|
206 |
-
alert_message = f"Heavy rainfall ({next_3_days_rain:.1f}mm) expected in next 3 days near {selected_village}, {district}. Delay fertilizer application. Ensure proper drainage."
|
207 |
-
action_items = ["delay_fertilizer", "check_drainage", "monitor_crops", "prepare_harvest_protection"]
|
208 |
-
elif next_3_days_rain > 10:
|
209 |
-
alert_type = "moderate_rain_warning"
|
210 |
-
urgency = "medium"
|
211 |
-
alert_message = f"Moderate rainfall ({next_3_days_rain:.1f}mm) expected in next 3 days near {selected_village}, {district}. Monitor soil moisture levels."
|
212 |
-
action_items = ["monitor_soil", "check_drainage", "adjust_irrigation"]
|
213 |
-
elif next_3_days_rain < 2 and current_temp > 35:
|
214 |
-
alert_type = "heat_drought_warning"
|
215 |
-
urgency = "high"
|
216 |
-
alert_message = f"High temperature ({current_temp:.1f}°C) with minimal rainfall expected near {selected_village}, {district}. Increase irrigation frequency."
|
217 |
-
action_items = ["increase_irrigation", "mulch_crops", "monitor_plant_stress"]
|
218 |
-
elif current_temp < 10:
|
219 |
-
alert_type = "cold_warning"
|
220 |
-
urgency = "medium"
|
221 |
-
alert_message = f"Low temperature ({current_temp:.1f}°C) expected near {selected_village}, {district}. Protect crops from cold damage."
|
222 |
-
action_items = ["protect_crops", "cover_seedlings", "adjust_irrigation_timing"]
|
223 |
-
elif current_windspeed > 30:
|
224 |
-
alert_type = "high_wind_warning"
|
225 |
-
urgency = "medium"
|
226 |
-
alert_message = f"High winds ({current_windspeed:.1f} km/h) expected near {selected_village}, {district}. Secure crop supports and structures."
|
227 |
-
action_items = ["secure_supports", "check_structures", "monitor_damage"]
|
228 |
-
else:
|
229 |
-
alert_type = "weather_update"
|
230 |
-
urgency = "low"
|
231 |
-
alert_message = f"Normal weather conditions expected near {selected_village}, {district}. Temperature {current_temp:.1f}°C, rainfall {next_3_days_rain:.1f}mm."
|
232 |
-
action_items = ["routine_monitoring", "maintain_irrigation"]
|
233 |
-
|
234 |
-
logger.info(f"Real weather data retrieved for {selected_village}, {district}: {current_temp}°C, {next_3_days_rain:.1f}mm rain (coords: {location_coords})")
|
235 |
-
|
236 |
-
except Exception as weather_error:
|
237 |
-
logger.error(f"Failed to get real weather data for {selected_village}, {district}: {weather_error}")
|
238 |
-
raise Exception(f"Unable to retrieve current weather conditions for {selected_village}, {district}")
|
239 |
-
|
240 |
-
return {
|
241 |
-
"alert_id": f"{state.upper()[:2]}_{district.upper()[:3]}_{selected_village.upper()[:3]}_{datetime.now().strftime('%Y%m%d_%H%M')}",
|
242 |
-
"timestamp": datetime.now().isoformat() + "Z",
|
243 |
-
"location": {
|
244 |
-
"village": selected_village,
|
245 |
-
"district": district,
|
246 |
-
"state": state.capitalize(),
|
247 |
-
"coordinates": location_coords,
|
248 |
-
"coordinates_source": location_source,
|
249 |
-
"total_villages_in_district": len(available_villages)
|
250 |
-
},
|
251 |
-
"crop": {
|
252 |
-
"name": regional_crop,
|
253 |
-
"stage": crop_stage,
|
254 |
-
"planted_estimate": "2025-06-15" # You could make this dynamic too
|
255 |
-
},
|
256 |
-
"alert": {
|
257 |
-
"type": alert_type,
|
258 |
-
"urgency": urgency,
|
259 |
-
"message": alert_message,
|
260 |
-
"action_items": action_items,
|
261 |
-
"valid_until": (datetime.now() + timedelta(days=3)).isoformat() + "Z"
|
262 |
-
},
|
263 |
-
"weather": real_weather,
|
264 |
-
"data_source": "open_meteo_api_with_dynamic_location"
|
265 |
-
}
|
266 |
-
|
267 |
-
except Exception as e:
|
268 |
-
logger.error(f"Error generating dynamic alert for {district}, {state}: {e}")
|
269 |
-
raise Exception(f"Failed to generate weather alert for {district}: {str(e)}")
|
270 |
-
|
271 |
-
|
272 |
-
|
273 |
-
|
274 |
-
import random
|
275 |
-
from datetime import datetime, date
|
276 |
-
|
277 |
-
# Enhanced crop selection function using your crop calendar data
|
278 |
-
async def get_regional_crop_for_area(district: str, state: str):
|
279 |
-
"""Get typical crop for the region based on season and district - now fully dynamic"""
|
280 |
-
|
281 |
-
if state.lower() != 'bihar':
|
282 |
-
return 'rice' # fallback for other states
|
283 |
-
|
284 |
-
current_month = datetime.now().month
|
285 |
-
current_season = get_current_season(current_month)
|
286 |
-
|
287 |
-
# Get crops that are currently in season using your crop calendar tools
|
288 |
-
try:
|
289 |
-
seasonal_crops_data = await crop_calendar_tools.get_prominent_crops('bihar', current_season)
|
290 |
-
if "error" not in seasonal_crops_data:
|
291 |
-
seasonal_crops = seasonal_crops_data.get('crops', [])
|
292 |
-
else:
|
293 |
-
seasonal_crops = []
|
294 |
-
except Exception as e:
|
295 |
-
logger.warning(f"Failed to get seasonal crops: {e}")
|
296 |
-
seasonal_crops = []
|
297 |
-
|
298 |
-
# District-specific crop preferences (what's commonly grown in each district)
|
299 |
-
district_crop_preferences = {
|
300 |
-
'patna': {
|
301 |
-
'primary': ['rice', 'wheat', 'potato'],
|
302 |
-
'secondary': ['mustard', 'gram', 'barley'],
|
303 |
-
'specialty': ['sugarcane']
|
304 |
-
},
|
305 |
-
'gaya': {
|
306 |
-
'primary': ['wheat', 'rice', 'gram'],
|
307 |
-
'secondary': ['barley', 'lentil', 'mustard'],
|
308 |
-
'specialty': ['arhar']
|
309 |
-
},
|
310 |
-
'bhagalpur': {
|
311 |
-
'primary': ['rice', 'maize', 'wheat'],
|
312 |
-
'secondary': ['jute', 'urd', 'moong'],
|
313 |
-
'specialty': ['groundnut']
|
314 |
-
},
|
315 |
-
'muzaffarpur': {
|
316 |
-
'primary': ['sugarcane', 'rice', 'wheat'],
|
317 |
-
'secondary': ['potato', 'mustard'],
|
318 |
-
'specialty': ['lentil']
|
319 |
-
},
|
320 |
-
'darbhanga': {
|
321 |
-
'primary': ['rice', 'wheat', 'maize'],
|
322 |
-
'secondary': ['gram', 'arhar'],
|
323 |
-
'specialty': ['bajra']
|
324 |
-
},
|
325 |
-
'siwan': {
|
326 |
-
'primary': ['rice', 'wheat'],
|
327 |
-
'secondary': ['gram', 'lentil', 'pea'],
|
328 |
-
'specialty': ['mustard']
|
329 |
-
},
|
330 |
-
'begusarai': {
|
331 |
-
'primary': ['rice', 'wheat'],
|
332 |
-
'secondary': ['jute', 'mustard'],
|
333 |
-
'specialty': ['moong', 'urd']
|
334 |
-
},
|
335 |
-
'katihar': {
|
336 |
-
'primary': ['maize', 'rice'],
|
337 |
-
'secondary': ['jute', 'urd', 'moong'],
|
338 |
-
'specialty': ['jowar', 'bajra']
|
339 |
-
},
|
340 |
-
'vaishali': {
|
341 |
-
'primary': ['rice', 'wheat', 'sugarcane'],
|
342 |
-
'secondary': ['potato', 'gram'],
|
343 |
-
'specialty': ['mustard']
|
344 |
-
},
|
345 |
-
'madhubani': {
|
346 |
-
'primary': ['rice', 'wheat', 'maize'],
|
347 |
-
'secondary': ['gram', 'lentil'],
|
348 |
-
'specialty': ['arhar']
|
349 |
-
}
|
350 |
}
|
351 |
-
|
352 |
-
|
353 |
-
|
354 |
-
|
355 |
-
|
356 |
-
|
357 |
-
}
|
358 |
-
|
359 |
-
|
360 |
-
|
361 |
-
|
362 |
-
|
363 |
-
|
364 |
-
|
365 |
-
|
366 |
-
if seasonal_crops:
|
367 |
-
suitable_crops = [crop for crop in all_district_crops if crop in seasonal_crops]
|
368 |
-
|
369 |
-
# If no seasonal match, use district preferences with seasonal weighting
|
370 |
-
if not suitable_crops:
|
371 |
-
if current_season == 'kharif':
|
372 |
-
# Monsoon crops preference
|
373 |
-
kharif_crops = ['rice', 'maize', 'arhar', 'moong', 'urd', 'jowar', 'bajra', 'groundnut', 'soybean']
|
374 |
-
suitable_crops = [crop for crop in all_district_crops if crop in kharif_crops]
|
375 |
-
elif current_season == 'rabi':
|
376 |
-
# Winter crops preference
|
377 |
-
rabi_crops = ['wheat', 'barley', 'gram', 'lentil', 'pea', 'mustard', 'linseed', 'potato']
|
378 |
-
suitable_crops = [crop for crop in all_district_crops if crop in rabi_crops]
|
379 |
-
elif current_season == 'zaid':
|
380 |
-
# Summer crops preference
|
381 |
-
zaid_crops = ['maize', 'moong', 'urd', 'watermelon', 'cucumber']
|
382 |
-
suitable_crops = [crop for crop in all_district_crops if crop in zaid_crops]
|
383 |
-
|
384 |
-
# If still no match, fall back to district primary crops
|
385 |
-
if not suitable_crops:
|
386 |
-
suitable_crops = district_prefs.get('primary', ['rice'])
|
387 |
-
|
388 |
-
# Weight selection based on crop category (primary crops more likely)
|
389 |
-
weighted_crops = []
|
390 |
-
for crop in suitable_crops:
|
391 |
-
if crop in district_prefs.get('primary', []):
|
392 |
-
weighted_crops.extend([crop] * 5) # 5x weight for primary crops
|
393 |
-
elif crop in district_prefs.get('secondary', []):
|
394 |
-
weighted_crops.extend([crop] * 3) # 3x weight for secondary crops
|
395 |
-
else:
|
396 |
-
weighted_crops.extend([crop] * 1) # 1x weight for specialty crops
|
397 |
-
|
398 |
-
selected_crop = random.choice(weighted_crops) if weighted_crops else 'rice'
|
399 |
-
|
400 |
-
logger.info(f"Selected crop: {selected_crop} for {district} in {current_season} season from options: {suitable_crops}")
|
401 |
-
|
402 |
-
return selected_crop
|
403 |
-
|
404 |
-
|
405 |
-
async def get_current_crop_stage_dynamic(crop: str, district: str = None):
|
406 |
-
"""Determine crop stage based on current date and crop calendar - now more accurate"""
|
407 |
-
|
408 |
-
try:
|
409 |
-
# Get crop calendar information
|
410 |
-
crop_info = await crop_calendar_tools.get_crop_calendar('bihar', crop)
|
411 |
-
|
412 |
-
if "error" in crop_info:
|
413 |
-
# Fallback to the old static method
|
414 |
-
return get_current_crop_stage_static(crop)
|
415 |
-
|
416 |
-
# Parse planting and harvesting periods
|
417 |
-
planting_period = crop_info.get('planting', '')
|
418 |
-
season = crop_info.get('season', '')
|
419 |
-
stages = crop_info.get('stages', [])
|
420 |
-
|
421 |
-
current_month = datetime.now().month
|
422 |
-
current_date = date.today()
|
423 |
-
|
424 |
-
# Estimate planting date based on season and current month
|
425 |
-
estimated_plant_date = estimate_planting_date(crop, season, planting_period, current_month)
|
426 |
-
|
427 |
-
if estimated_plant_date:
|
428 |
-
# Use the crop calendar function to estimate stage
|
429 |
-
try:
|
430 |
-
stage_data = await crop_calendar_tools.estimate_crop_stage(
|
431 |
-
crop,
|
432 |
-
estimated_plant_date.isoformat(),
|
433 |
-
current_date.isoformat()
|
434 |
-
)
|
435 |
-
|
436 |
-
if "error" not in stage_data:
|
437 |
-
stage = stage_data.get('stage', stages[0] if stages else 'Growing')
|
438 |
-
logger.info(f"Dynamic stage calculation for {crop}: {stage} (planted ~{estimated_plant_date})")
|
439 |
-
return stage
|
440 |
-
except Exception as e:
|
441 |
-
logger.warning(f"Error in dynamic stage calculation: {e}")
|
442 |
-
|
443 |
-
# Fallback to month-based estimation
|
444 |
-
return estimate_stage_by_month(crop, current_month, stages)
|
445 |
-
|
446 |
-
except Exception as e:
|
447 |
-
logger.error(f"Error in dynamic crop stage calculation: {e}")
|
448 |
-
return get_current_crop_stage_static(crop)
|
449 |
-
|
450 |
-
|
451 |
-
def get_current_season(month: int):
|
452 |
"""Determine current agricultural season"""
|
453 |
if month in [6, 7, 8, 9]: # June to September
|
454 |
return 'kharif'
|
@@ -457,205 +128,208 @@ def get_current_season(month: int):
|
|
457 |
else: # April, May
|
458 |
return 'zaid'
|
459 |
|
460 |
-
|
461 |
-
"""
|
462 |
-
|
463 |
if state.lower() != 'bihar':
|
464 |
-
return 'rice' # fallback
|
465 |
|
466 |
current_month = datetime.now().month
|
467 |
current_season = get_current_season(current_month)
|
468 |
|
469 |
-
# Get
|
470 |
-
|
471 |
-
|
472 |
-
|
473 |
-
|
474 |
-
|
475 |
-
seasonal_crops = []
|
476 |
-
except Exception as e:
|
477 |
-
logger.warning(f"Failed to get seasonal crops: {e}")
|
478 |
-
seasonal_crops = []
|
479 |
|
480 |
-
#
|
481 |
-
|
482 |
-
'
|
483 |
-
'
|
484 |
-
'
|
485 |
-
'muzaffarpur': {'primary': ['sugarcane', 'rice', 'wheat'], 'secondary': ['potato', 'mustard'], 'specialty': ['lentil']},
|
486 |
-
'darbhanga': {'primary': ['rice', 'wheat', 'maize'], 'secondary': ['gram', 'arhar'], 'specialty': ['bajra']},
|
487 |
-
'siwan': {'primary': ['rice', 'wheat'], 'secondary': ['gram', 'lentil', 'pea'], 'specialty': ['mustard']},
|
488 |
-
'begusarai': {'primary': ['rice', 'wheat'], 'secondary': ['jute', 'mustard'], 'specialty': ['moong', 'urd']},
|
489 |
-
'katihar': {'primary': ['maize', 'rice'], 'secondary': ['jute', 'urd', 'moong'], 'specialty': ['jowar', 'bajra']}
|
490 |
}
|
491 |
|
492 |
-
|
493 |
-
|
494 |
-
|
495 |
-
|
496 |
-
# Find crops that are both seasonal AND grown in this district
|
497 |
-
suitable_crops = []
|
498 |
-
if seasonal_crops:
|
499 |
-
suitable_crops = [crop for crop in all_district_crops if crop in seasonal_crops]
|
500 |
|
501 |
-
|
502 |
-
|
503 |
-
if current_season == 'kharif':
|
504 |
-
kharif_crops = ['rice', 'maize', 'arhar', 'moong', 'urd', 'jowar', 'bajra', 'groundnut', 'soybean']
|
505 |
-
suitable_crops = [crop for crop in all_district_crops if crop in kharif_crops]
|
506 |
-
elif current_season == 'rabi':
|
507 |
-
rabi_crops = ['wheat', 'barley', 'gram', 'lentil', 'pea', 'mustard', 'linseed', 'potato']
|
508 |
-
suitable_crops = [crop for crop in all_district_crops if crop in rabi_crops]
|
509 |
-
elif current_season == 'zaid':
|
510 |
-
zaid_crops = ['maize', 'moong', 'urd', 'watermelon', 'cucumber']
|
511 |
-
suitable_crops = [crop for crop in all_district_crops if crop in zaid_crops]
|
512 |
|
513 |
if not suitable_crops:
|
514 |
suitable_crops = district_prefs.get('primary', ['rice'])
|
515 |
|
516 |
-
#
|
517 |
weighted_crops = []
|
518 |
for crop in suitable_crops:
|
519 |
if crop in district_prefs.get('primary', []):
|
520 |
-
weighted_crops.extend([crop] * 5)
|
521 |
elif crop in district_prefs.get('secondary', []):
|
522 |
-
weighted_crops.extend([crop] * 3)
|
523 |
else:
|
524 |
-
weighted_crops.extend([crop] * 1)
|
525 |
|
526 |
selected_crop = random.choice(weighted_crops) if weighted_crops else 'rice'
|
527 |
logger.info(f"Selected crop: {selected_crop} for {district} in {current_season} season")
|
528 |
|
529 |
return selected_crop
|
530 |
|
531 |
-
|
532 |
-
"""
|
533 |
-
|
534 |
-
crop_info = await crop_calendar_tools.get_crop_calendar('bihar', crop)
|
535 |
-
|
536 |
-
if "error" in crop_info:
|
537 |
-
return get_current_crop_stage_static(crop)
|
538 |
-
|
539 |
-
stages = crop_info.get('stages', [])
|
540 |
-
planting_period = crop_info.get('planting', '')
|
541 |
-
current_month = datetime.now().month
|
542 |
-
current_date = date.today()
|
543 |
-
|
544 |
-
estimated_plant_date = estimate_planting_date(crop, planting_period, current_month)
|
545 |
-
|
546 |
-
if estimated_plant_date:
|
547 |
-
try:
|
548 |
-
stage_data = await crop_calendar_tools.estimate_crop_stage(
|
549 |
-
crop, estimated_plant_date.isoformat(), current_date.isoformat()
|
550 |
-
)
|
551 |
-
|
552 |
-
if "error" not in stage_data:
|
553 |
-
return stage_data.get('stage', stages[0] if stages else 'Growing')
|
554 |
-
except Exception as e:
|
555 |
-
logger.warning(f"Error in dynamic stage calculation: {e}")
|
556 |
-
|
557 |
-
return estimate_stage_by_month(crop, current_month, stages)
|
558 |
-
|
559 |
-
except Exception as e:
|
560 |
-
logger.error(f"Error in dynamic crop stage calculation: {e}")
|
561 |
-
return get_current_crop_stage_static(crop)
|
562 |
-
|
563 |
-
def estimate_planting_date(crop: str, planting_period: str, current_month: int):
|
564 |
-
"""Estimate when the crop was likely planted"""
|
565 |
-
current_year = datetime.now().year
|
566 |
-
|
567 |
-
try:
|
568 |
-
if 'june' in planting_period.lower():
|
569 |
-
return date(current_year, 6, 15) if current_month >= 6 else date(current_year - 1, 6, 15)
|
570 |
-
elif 'november' in planting_period.lower():
|
571 |
-
if current_month >= 11:
|
572 |
-
return date(current_year, 11, 15)
|
573 |
-
elif current_month <= 4:
|
574 |
-
return date(current_year - 1, 11, 15)
|
575 |
-
else:
|
576 |
-
return date(current_year, 11, 15)
|
577 |
-
elif 'october' in planting_period.lower():
|
578 |
-
if current_month >= 10:
|
579 |
-
return date(current_year, 10, 15)
|
580 |
-
elif current_month <= 4:
|
581 |
-
return date(current_year - 1, 10, 15)
|
582 |
-
else:
|
583 |
-
return date(current_year, 10, 15)
|
584 |
-
elif 'march' in planting_period.lower():
|
585 |
-
if current_month >= 3 and current_month <= 8:
|
586 |
-
return date(current_year, 3, 15)
|
587 |
-
else:
|
588 |
-
return date(current_year - 1, 3, 15)
|
589 |
-
except Exception as e:
|
590 |
-
logger.warning(f"Error estimating planting date: {e}")
|
591 |
-
|
592 |
-
return None
|
593 |
-
|
594 |
-
def estimate_stage_by_month(crop: str, current_month: int, stages: list):
|
595 |
-
"""Estimate crop stage based on current month"""
|
596 |
-
if not stages:
|
597 |
return 'Growing'
|
598 |
|
|
|
|
|
|
|
|
|
599 |
stage_mappings = {
|
600 |
'rice': {6: 0, 7: 1, 8: 2, 9: 3, 10: 4, 11: 5, 12: 6, 1: 7, 2: 8},
|
601 |
-
'wheat': {11: 0, 12: 1, 1: 2, 2: 3, 3: 4, 4: 5
|
602 |
-
'maize': {6: 0, 7: 1, 8: 2, 9: 3, 10: 4, 11: 5,
|
|
|
|
|
603 |
}
|
604 |
|
605 |
crop_mapping = stage_mappings.get(crop, {})
|
606 |
-
stage_index = crop_mapping.get(current_month, 2)
|
607 |
stage_index = min(stage_index, len(stages) - 1)
|
608 |
|
609 |
-
return stages[stage_index] if
|
610 |
|
611 |
-
def
|
612 |
-
"""
|
613 |
-
|
|
|
614 |
|
615 |
-
|
616 |
-
|
617 |
-
|
618 |
-
|
619 |
-
|
620 |
-
|
621 |
-
|
622 |
-
|
623 |
-
|
624 |
-
elif crop == 'wheat':
|
625 |
-
if current_month in [11, 12]:
|
626 |
-
return 'Sowing'
|
627 |
-
elif current_month in [1, 2]:
|
628 |
-
return 'Tillering'
|
629 |
-
elif current_month in [3, 4]:
|
630 |
-
return 'Flowering'
|
631 |
-
else:
|
632 |
-
return 'Harvesting'
|
633 |
-
elif crop == 'sugarcane':
|
634 |
-
if current_month in [2, 3, 4]:
|
635 |
-
return 'Planting'
|
636 |
-
elif current_month in [5, 6, 7, 8]:
|
637 |
-
return 'Vegetative'
|
638 |
-
elif current_month in [9, 10, 11]:
|
639 |
-
return 'Maturity'
|
640 |
-
else:
|
641 |
-
return 'Harvesting'
|
642 |
-
elif crop == 'maize':
|
643 |
-
if current_month in [6, 7]:
|
644 |
-
return 'Sowing'
|
645 |
-
elif current_month in [8, 9]:
|
646 |
-
return 'Vegetative'
|
647 |
-
elif current_month in [10, 11]:
|
648 |
-
return 'Grain Filling'
|
649 |
-
else:
|
650 |
-
return 'Harvesting'
|
651 |
|
652 |
-
|
653 |
-
|
654 |
-
|
655 |
-
|
656 |
-
|
657 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
658 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
659 |
try:
|
660 |
# Step 1: Get villages for the district
|
661 |
villages_data = await geographic_tools.list_villages(state, district)
|
@@ -663,163 +337,79 @@ async def generate_dynamic_alert(district: str, state: str):
|
|
663 |
if "error" in villages_data:
|
664 |
raise Exception(f"District '{district}' not found in {state}")
|
665 |
|
666 |
-
# Step 2: Pick a random village from the actual list
|
667 |
available_villages = villages_data.get("villages", [])
|
668 |
if not available_villages:
|
669 |
raise Exception(f"No villages found for {district}")
|
670 |
|
|
|
671 |
selected_village = random.choice(available_villages)
|
672 |
logger.info(f"Selected village: {selected_village} from {len(available_villages)} villages")
|
673 |
|
674 |
-
# Step 3: Get coordinates
|
675 |
-
location_coords =
|
676 |
-
location_source = ""
|
677 |
|
678 |
-
#
|
679 |
-
|
680 |
-
|
681 |
-
if "error" not in village_location and "lat" in village_location:
|
682 |
-
location_coords = [village_location["lat"], village_location["lng"]]
|
683 |
-
location_source = f"village_{selected_village}"
|
684 |
-
logger.info(f"Using village coordinates for {selected_village}: {location_coords}")
|
685 |
-
except Exception as e:
|
686 |
-
logger.warning(f"Village geocoding failed for {selected_village}: {e}")
|
687 |
-
|
688 |
-
# Fallback to district coordinates if village lookup failed
|
689 |
-
if not location_coords:
|
690 |
-
try:
|
691 |
-
district_location = await geographic_tools.reverse_geocode(district)
|
692 |
-
if "error" not in district_location and "lat" in district_location:
|
693 |
-
location_coords = [district_location["lat"], district_location["lng"]]
|
694 |
-
location_source = f"district_{district}"
|
695 |
-
logger.info(f"Using district coordinates for {district}: {location_coords}")
|
696 |
-
except Exception as e:
|
697 |
-
logger.warning(f"District geocoding failed for {district}: {e}")
|
698 |
|
699 |
-
#
|
700 |
-
if not location_coords:
|
701 |
-
logger.warning(f"No coordinates found for {selected_village} or {district}, using default")
|
702 |
-
location_coords = [25.5941, 85.1376] # Patna fallback
|
703 |
-
location_source = "fallback_patna"
|
704 |
-
|
705 |
-
# Step 4: Generate dynamic crop selection and stage
|
706 |
-
regional_crop = await get_regional_crop_for_area(district, state)
|
707 |
-
crop_stage = await get_current_crop_stage_dynamic(regional_crop, district)
|
708 |
-
|
709 |
-
# Step 5: GET AI-POWERED WEATHER ALERT using your alert_generation_tools
|
710 |
try:
|
711 |
-
|
712 |
-
|
713 |
-
|
714 |
-
|
715 |
-
if not api_key:
|
716 |
-
raise Exception("OpenAI API key not found")
|
717 |
|
718 |
-
|
719 |
-
|
720 |
-
latitude=location_coords[0],
|
721 |
longitude=location_coords[1],
|
722 |
-
|
723 |
)
|
724 |
|
725 |
-
logger.info(f"
|
726 |
|
727 |
-
|
728 |
-
|
729 |
-
|
730 |
-
|
731 |
-
|
732 |
-
|
733 |
-
|
734 |
-
|
735 |
-
|
736 |
-
|
737 |
-
|
738 |
-
|
739 |
-
|
740 |
-
|
741 |
-
|
742 |
-
|
743 |
-
|
744 |
-
|
745 |
-
|
746 |
-
|
747 |
-
|
748 |
-
|
749 |
-
|
750 |
-
|
751 |
-
|
752 |
-
|
753 |
-
|
754 |
-
|
755 |
-
|
756 |
-
|
757 |
-
|
758 |
-
|
759 |
-
|
760 |
-
|
761 |
-
|
762 |
-
|
763 |
-
|
764 |
-
weather_context = {
|
765 |
-
"forecast_days": 7,
|
766 |
-
"coordinates_source": location_source,
|
767 |
-
"note": "Weather context limited due to API error"
|
768 |
-
}
|
769 |
-
|
770 |
-
# Extract AI analysis
|
771 |
-
alert_description = ai_alert.get('alert', 'Weather update for agricultural activities')
|
772 |
-
impact_description = ai_alert.get('impact', 'Monitor crops regularly')
|
773 |
-
recommendations = ai_alert.get('recommendations', 'Continue routine farming activities')
|
774 |
-
|
775 |
-
# Create comprehensive alert message combining AI insights
|
776 |
-
alert_message = f"🤖 AI Weather Alert for {selected_village}, {district}: {alert_description}"
|
777 |
-
if impact_description and impact_description.lower() not in ['none', 'n/a', '']:
|
778 |
-
alert_message += f" 🌾 Crop Impact: {impact_description}"
|
779 |
-
|
780 |
-
# Determine urgency and type based on AI response content
|
781 |
-
urgency = "low"
|
782 |
-
alert_type = "weather_update"
|
783 |
-
|
784 |
-
alert_lower = alert_description.lower()
|
785 |
-
impact_lower = impact_description.lower()
|
786 |
-
recommendations_lower = recommendations.lower()
|
787 |
-
|
788 |
-
# High urgency keywords
|
789 |
-
if any(word in alert_lower + impact_lower for word in ['urgent', 'severe', 'critical', 'danger', 'emergency', 'immediate']):
|
790 |
-
urgency = "high"
|
791 |
-
alert_type = "severe_weather_warning"
|
792 |
-
# Medium urgency keywords
|
793 |
-
elif any(word in alert_lower + impact_lower for word in ['warning', 'caution', 'alert', 'risk', 'damage', 'loss', 'stress', 'threat']):
|
794 |
-
urgency = "medium"
|
795 |
-
alert_type = "weather_warning"
|
796 |
-
# Check recommendations for urgency indicators
|
797 |
-
elif any(word in recommendations_lower for word in ['immediate', 'urgent', 'quickly', 'soon', 'now']):
|
798 |
-
urgency = "medium"
|
799 |
-
alert_type = "crop_risk_alert"
|
800 |
-
|
801 |
-
# Parse recommendations into actionable items
|
802 |
-
action_items = []
|
803 |
-
if recommendations:
|
804 |
-
# Split recommendations by common delimiters and clean up
|
805 |
-
items = recommendations.replace('.', '|').replace(',', '|').replace(';', '|').replace(' and ', '|').split('|')
|
806 |
-
action_items = [item.strip().lower().replace(' ', '_') for item in items if item.strip() and len(item.strip()) > 3]
|
807 |
-
# Limit to 5 most important items and ensure they're actionable
|
808 |
-
action_items = [item for item in action_items[:5] if any(verb in item for verb in ['monitor', 'check', 'apply', 'water', 'harvest', 'plant', 'protect', 'cover', 'drain', 'spray', 'fertilize'])]
|
809 |
-
|
810 |
-
if not action_items:
|
811 |
-
action_items = ["monitor_crops", "follow_weather_updates", "maintain_irrigation"]
|
812 |
-
|
813 |
-
logger.info(f"AI-powered alert processed: Type={alert_type}, Urgency={urgency}, Actions={len(action_items)}")
|
814 |
-
|
815 |
-
except Exception as ai_error:
|
816 |
-
logger.error(f"Failed to get AI weather alert for {selected_village}, {district}: {ai_error}")
|
817 |
-
raise Exception(f"Unable to generate AI weather alert: {str(ai_error)}")
|
818 |
|
819 |
-
# Generate unique alert ID
|
820 |
alert_id = f"{state.upper()[:2]}_{district.upper()[:3]}_{selected_village.upper()[:3]}_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
|
821 |
|
822 |
-
|
|
|
823 |
"alert_id": alert_id,
|
824 |
"timestamp": datetime.now().isoformat() + "Z",
|
825 |
"location": {
|
@@ -834,285 +424,188 @@ async def generate_dynamic_alert(district: str, state: str):
|
|
834 |
"name": regional_crop,
|
835 |
"stage": crop_stage,
|
836 |
"season": get_current_season(datetime.now().month),
|
837 |
-
"planted_estimate": "2025-06-15"
|
838 |
},
|
839 |
"alert": {
|
840 |
"type": alert_type,
|
841 |
"urgency": urgency,
|
842 |
-
"message": alert_message,
|
843 |
"action_items": action_items,
|
844 |
"valid_until": (datetime.now() + timedelta(days=3)).isoformat() + "Z",
|
845 |
-
"ai_generated":
|
846 |
-
},
|
847 |
-
"ai_analysis": {
|
848 |
-
"alert": alert_description,
|
849 |
-
"impact": impact_description,
|
850 |
-
"recommendations": recommendations
|
851 |
},
|
852 |
"weather": weather_context,
|
853 |
-
"data_source": "
|
854 |
}
|
855 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
856 |
except Exception as e:
|
857 |
-
logger.error(f"Error generating
|
858 |
-
raise Exception(f"Failed to generate
|
859 |
|
|
|
860 |
@app.get("/")
|
861 |
async def root():
|
862 |
-
return {"message": "MCP Weather Server
|
863 |
|
864 |
@app.get("/api/health")
|
865 |
async def health_check():
|
866 |
-
return {
|
|
|
|
|
|
|
|
|
|
|
867 |
|
868 |
-
# workflow endpoint for frontend
|
869 |
@app.post("/api/run-workflow")
|
870 |
async def run_workflow(request: WorkflowRequest):
|
|
|
871 |
logger.info(f"Received workflow request: {request.state}, {request.district}")
|
872 |
|
873 |
-
|
874 |
-
sample_alert = None
|
875 |
-
csv_content = ""
|
876 |
|
877 |
try:
|
878 |
-
#
|
879 |
-
workflow_results
|
880 |
-
|
881 |
-
|
882 |
-
|
883 |
-
|
884 |
-
|
885 |
-
|
886 |
-
|
887 |
-
|
888 |
-
|
889 |
-
|
890 |
-
|
891 |
-
|
892 |
-
|
893 |
-
|
894 |
-
|
895 |
-
|
896 |
-
|
897 |
-
|
898 |
-
|
899 |
-
|
900 |
-
|
901 |
-
|
902 |
-
|
903 |
-
|
904 |
-
|
905 |
-
|
906 |
-
|
907 |
-
|
908 |
-
|
909 |
-
|
910 |
-
|
911 |
-
|
912 |
-
|
913 |
-
|
914 |
-
|
915 |
-
# Alert generation
|
916 |
-
workflow_results.append("\n🚨 Alert Generation")
|
917 |
-
workflow_results.append("-" * 30)
|
918 |
-
workflow_results.append("✅ Weather alerts generated")
|
919 |
-
workflow_results.append(f" - Data Source: {sample_alert.get('data_source', 'API')}")
|
920 |
-
workflow_results.append(f" - Alert Type: {sample_alert['alert']['type']}")
|
921 |
-
workflow_results.append(f" - Severity: {sample_alert['alert']['urgency']}")
|
922 |
-
workflow_results.append(f" - Village: {sample_alert['location']['village']}")
|
923 |
-
workflow_results.append(f" - Coordinates: {sample_alert['location']['coordinates']}")
|
924 |
-
workflow_results.append(f" - Crop: {sample_alert['crop']['name']} ({sample_alert['crop']['stage']})")
|
925 |
-
workflow_results.append(f" - Temperature: {sample_alert['weather']['temperature']}")
|
926 |
-
workflow_results.append(f" - Humidity: {sample_alert['weather']['humidity']}")
|
927 |
-
workflow_results.append(f" - Expected Rainfall: {sample_alert['weather']['expected_rainfall']}")
|
928 |
-
workflow_results.append(f" - Rain Probability: {sample_alert['weather']['rain_probability']}%")
|
929 |
-
|
930 |
-
# WhatsApp Agent Response
|
931 |
-
workflow_results.append("\n📱 WhatsApp Agent Response")
|
932 |
-
workflow_results.append("-" * 30)
|
933 |
-
try:
|
934 |
-
whatsapp_message = whatsapp_agent.create_whatsapp_message(sample_alert)
|
935 |
-
workflow_results.append(f"✅ Message created successfully")
|
936 |
-
workflow_results.append(f"Text: {whatsapp_message.get('text', 'N/A')}")
|
937 |
-
if 'buttons' in whatsapp_message:
|
938 |
-
workflow_results.append(f"Buttons: {len(whatsapp_message['buttons'])} button(s)")
|
939 |
-
except Exception as e:
|
940 |
-
workflow_results.append(f"❌ Error: {str(e)}")
|
941 |
-
|
942 |
-
# SMS Agent Response
|
943 |
-
workflow_results.append("\n📱 SMS Agent Response")
|
944 |
-
workflow_results.append("-" * 30)
|
945 |
-
try:
|
946 |
-
sms_message = sms_agent.create_sms_message(sample_alert)
|
947 |
-
workflow_results.append(f"✅ SMS created successfully")
|
948 |
-
workflow_results.append(f"Content: {str(sms_message)}")
|
949 |
-
except Exception as e:
|
950 |
-
workflow_results.append(f"❌ Error: {str(e)}")
|
951 |
-
|
952 |
-
# USSD Agent Response
|
953 |
-
workflow_results.append("\n📞 USSD Agent Response")
|
954 |
-
workflow_results.append("-" * 30)
|
955 |
-
try:
|
956 |
-
ussd_menu = ussd_agent.create_ussd_menu(sample_alert)
|
957 |
-
workflow_results.append(f"✅ USSD menu created successfully")
|
958 |
-
workflow_results.append(f"Menu: {str(ussd_menu)}")
|
959 |
-
except Exception as e:
|
960 |
-
workflow_results.append(f"❌ Error: {str(e)}")
|
961 |
-
|
962 |
-
# IVR Agent Response
|
963 |
-
workflow_results.append("\n🎙️ IVR Agent Response")
|
964 |
-
workflow_results.append("-" * 30)
|
965 |
-
try:
|
966 |
-
ivr_script = ivr_agent.create_ivr_script(sample_alert)
|
967 |
-
workflow_results.append(f"✅ IVR script created successfully")
|
968 |
-
workflow_results.append(f"Script: {str(ivr_script)}")
|
969 |
-
except Exception as e:
|
970 |
-
workflow_results.append(f"❌ Error: {str(e)}")
|
971 |
-
|
972 |
-
# Telegram Agent Response
|
973 |
-
workflow_results.append("\n🤖 Telegram Agent Response")
|
974 |
-
workflow_results.append("-" * 30)
|
975 |
-
try:
|
976 |
-
telegram_message = telegram_agent.create_telegram_message(sample_alert)
|
977 |
-
workflow_results.append(f"✅ Telegram message created successfully")
|
978 |
-
workflow_results.append(f"Content: {str(telegram_message)}")
|
979 |
-
except Exception as e:
|
980 |
-
workflow_results.append(f"❌ Error: {str(e)}")
|
981 |
-
|
982 |
-
# Summary
|
983 |
-
workflow_results.append("\n✅ Workflow Summary")
|
984 |
-
workflow_results.append("-" * 30)
|
985 |
-
workflow_results.append("Workflow execution completed with REAL weather data")
|
986 |
-
workflow_results.append(f"Location: {request.district}, {request.state}")
|
987 |
-
workflow_results.append(f"Weather Source: Open-Meteo API")
|
988 |
-
workflow_results.append(f"Timestamp: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
989 |
-
|
990 |
-
# Join all results into a single formatted string
|
991 |
-
formatted_output = "\n".join(workflow_results)
|
992 |
-
|
993 |
-
# Generate CSV
|
994 |
-
try:
|
995 |
-
csv_buffer = StringIO()
|
996 |
-
writer = csv.writer(csv_buffer)
|
997 |
-
|
998 |
-
# Write headers
|
999 |
-
headers = ["weather data", "whatsapp", "sms", "ussd", "ivr", "telegram"]
|
1000 |
-
writer.writerow(headers)
|
1001 |
-
|
1002 |
-
# Prepare weather data as a single string with line breaks
|
1003 |
-
weather_info = "\n".join([
|
1004 |
-
f" - Data Source: {sample_alert.get('data_source', 'API')}",
|
1005 |
-
f" - Alert Type: {sample_alert['alert']['type']}",
|
1006 |
-
f" - Severity: {sample_alert['alert']['urgency']}",
|
1007 |
-
f" - Village: {sample_alert['location']['village']}",
|
1008 |
-
f" - Coordinates: {sample_alert['location']['coordinates']}",
|
1009 |
-
f" - Crop: {sample_alert['crop']['name']} ({sample_alert['crop']['stage']})",
|
1010 |
-
f" - Temperature: {sample_alert['weather']['temperature']}",
|
1011 |
-
f" - Humidity: {sample_alert['weather']['humidity']}",
|
1012 |
-
f" - Expected Rainfall: {sample_alert['weather']['expected_rainfall']}",
|
1013 |
-
f" - Rain Probability: {sample_alert['weather']['rain_probability']}%"
|
1014 |
-
])
|
1015 |
-
|
1016 |
-
weather_data = [weather_info]
|
1017 |
-
|
1018 |
-
# Extract agent outputs only (no status messages)
|
1019 |
-
whatsapp_data = []
|
1020 |
-
sms_data = []
|
1021 |
-
ussd_data = []
|
1022 |
-
ivr_data = []
|
1023 |
-
telegram_data = []
|
1024 |
-
|
1025 |
-
# Get WhatsApp message
|
1026 |
-
try:
|
1027 |
-
whatsapp_message = whatsapp_agent.create_whatsapp_message(sample_alert)
|
1028 |
-
whatsapp_text = whatsapp_message.get('text', 'N/A')
|
1029 |
-
whatsapp_data.append(whatsapp_text)
|
1030 |
-
if 'buttons' in whatsapp_message and whatsapp_message['buttons']:
|
1031 |
-
whatsapp_data.append(f"Buttons: {whatsapp_message['buttons']}")
|
1032 |
-
except Exception as e:
|
1033 |
-
whatsapp_data.append(f"Error: {str(e)}")
|
1034 |
-
|
1035 |
-
# Get SMS message
|
1036 |
-
try:
|
1037 |
-
sms_message = sms_agent.create_sms_message(sample_alert)
|
1038 |
-
sms_data.append(str(sms_message))
|
1039 |
-
except Exception as e:
|
1040 |
-
sms_data.append(f"Error: {str(e)}")
|
1041 |
-
|
1042 |
-
# Get USSD menu
|
1043 |
-
try:
|
1044 |
-
ussd_menu = ussd_agent.create_ussd_menu(sample_alert)
|
1045 |
-
ussd_data.append(str(ussd_menu))
|
1046 |
-
except Exception as e:
|
1047 |
-
ussd_data.append(f"Error: {str(e)}")
|
1048 |
-
|
1049 |
-
# Get IVR script
|
1050 |
try:
|
1051 |
-
|
1052 |
-
|
1053 |
-
|
1054 |
-
|
1055 |
-
|
1056 |
-
|
1057 |
-
|
1058 |
-
|
1059 |
-
|
|
|
|
|
1060 |
except Exception as e:
|
1061 |
-
|
|
|
1062 |
|
1063 |
-
|
1064 |
-
|
1065 |
-
|
1066 |
-
|
1067 |
-
|
1068 |
-
|
1069 |
-
|
1070 |
-
|
1071 |
-
|
1072 |
-
|
1073 |
-
|
1074 |
-
|
1075 |
-
|
1076 |
-
|
1077 |
-
|
1078 |
-
sms_data[i] if i < len(sms_data) else "",
|
1079 |
-
ussd_data[i] if i < len(ussd_data) else "",
|
1080 |
-
ivr_data[i] if i < len(ivr_data) else "",
|
1081 |
-
telegram_data[i] if i < len(telegram_data) else ""
|
1082 |
-
]
|
1083 |
-
writer.writerow(row)
|
1084 |
-
|
1085 |
-
csv_content = csv_buffer.getvalue()
|
1086 |
-
logger.info("CSV content generated successfully")
|
1087 |
-
|
1088 |
-
except Exception as csv_error:
|
1089 |
-
logger.error(f"Error generating CSV: {csv_error}")
|
1090 |
-
csv_content = f"Error generating CSV: {str(csv_error)}"
|
1091 |
|
1092 |
-
logger.info(f"Successfully completed workflow for {request.district}, {request.state}")
|
1093 |
return {
|
1094 |
-
"message":
|
1095 |
"status": "success",
|
1096 |
"csv": csv_content,
|
1097 |
"raw_data": {
|
1098 |
"state": request.state,
|
1099 |
"district": request.district,
|
1100 |
-
"alert_data": sample_alert
|
|
|
1101 |
}
|
1102 |
}
|
1103 |
|
1104 |
except Exception as e:
|
1105 |
-
|
|
|
|
|
|
|
1106 |
return {
|
1107 |
-
"message":
|
1108 |
"status": "error",
|
1109 |
"csv": "",
|
1110 |
"error": str(e)
|
1111 |
}
|
1112 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1113 |
|
|
|
1114 |
@app.post("/mcp")
|
1115 |
async def mcp_endpoint(request: MCPRequest):
|
|
|
1116 |
logger.info(f"Received request for tool: {request.tool}")
|
1117 |
tool_config = get_tool_config(request.tool)
|
1118 |
|
@@ -1121,64 +614,77 @@ async def mcp_endpoint(request: MCPRequest):
|
|
1121 |
raise HTTPException(status_code=404, detail="Tool not found")
|
1122 |
|
1123 |
try:
|
1124 |
-
|
1125 |
-
|
1126 |
-
|
1127 |
-
|
1128 |
-
|
1129 |
-
|
1130 |
-
|
1131 |
-
|
1132 |
-
|
1133 |
-
|
1134 |
-
|
1135 |
-
|
1136 |
-
|
1137 |
-
|
1138 |
-
|
1139 |
-
api_key = config.get("OPENAI_API_KEY")
|
1140 |
-
result = await getattr(openai_llm, request.tool)(**request.parameters, api_key=api_key)
|
1141 |
-
elif tool_config["module"] == "geographic_tools":
|
1142 |
-
result = await getattr(geographic_tools, request.tool)(**request.parameters)
|
1143 |
-
elif tool_config["module"] == "crop_calendar_tools":
|
1144 |
-
result = await getattr(crop_calendar_tools, request.tool)(**request.parameters)
|
1145 |
-
elif tool_config["module"] == "alert_generation_tools":
|
1146 |
-
api_key = config.get("OPENAI_API_KEY")
|
1147 |
-
result = await getattr(alert_generation_tools, request.tool)(**request.parameters, api_key=api_key)
|
1148 |
-
else:
|
1149 |
raise HTTPException(status_code=500, detail="Invalid tool module")
|
1150 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1151 |
logger.info(f"Successfully executed tool: {request.tool}")
|
1152 |
return result
|
|
|
1153 |
except Exception as e:
|
1154 |
logger.exception(f"Error executing tool: {request.tool}")
|
1155 |
raise HTTPException(status_code=500, detail=str(e))
|
1156 |
|
|
|
1157 |
@app.post("/a2a/sms")
|
1158 |
async def a2a_sms_endpoint(request: AlertRequest):
|
|
|
1159 |
return {"message": sms_agent.create_sms_message(request.alert_json)}
|
1160 |
|
1161 |
@app.post("/a2a/whatsapp")
|
1162 |
async def a2a_whatsapp_endpoint(request: AlertRequest):
|
|
|
1163 |
return whatsapp_agent.create_whatsapp_message(request.alert_json)
|
1164 |
|
1165 |
@app.post("/a2a/ussd")
|
1166 |
async def a2a_ussd_endpoint(request: AlertRequest):
|
|
|
1167 |
return {"menu": ussd_agent.create_ussd_menu(request.alert_json)}
|
1168 |
|
1169 |
@app.post("/a2a/ivr")
|
1170 |
async def a2a_ivr_endpoint(request: AlertRequest):
|
|
|
1171 |
return {"script": ivr_agent.create_ivr_script(request.alert_json)}
|
1172 |
|
1173 |
@app.post("/a2a/telegram")
|
1174 |
async def a2a_telegram_endpoint(request: AlertRequest):
|
|
|
1175 |
return telegram_agent.create_telegram_message(request.alert_json)
|
1176 |
|
1177 |
-
|
1178 |
-
|
1179 |
-
|
1180 |
-
@app.post("/mcp")
|
1181 |
async def mcp_rpc_handler(request: dict):
|
|
|
1182 |
method = request.get("method")
|
1183 |
params = request.get("params", {})
|
1184 |
tool_name = params.get("tool_name")
|
@@ -1192,7 +698,7 @@ async def mcp_rpc_handler(request: dict):
|
|
1192 |
result = await run_workflow(WorkflowRequest(state=state, district=district))
|
1193 |
return {"jsonrpc": "2.0", "result": result, "id": req_id}
|
1194 |
|
1195 |
-
# Handle other tools dynamically
|
1196 |
if method == "call_tool":
|
1197 |
try:
|
1198 |
result = await mcp_endpoint(MCPRequest(tool=tool_name, parameters=arguments))
|
@@ -1202,7 +708,76 @@ async def mcp_rpc_handler(request: dict):
|
|
1202 |
|
1203 |
return {"jsonrpc": "2.0", "error": {"code": -32601, "message": "Unknown method"}, "id": req_id}
|
1204 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1205 |
|
1206 |
if __name__ == "__main__":
|
1207 |
import uvicorn
|
1208 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
import os
|
2 |
import logging
|
3 |
import random
|
4 |
+
from datetime import datetime, timedelta, date
|
5 |
+
from typing import Optional, Dict, List, Any
|
6 |
+
import csv
|
7 |
+
from io import StringIO
|
8 |
+
|
9 |
from fastapi import FastAPI, HTTPException
|
10 |
from fastapi.middleware.cors import CORSMiddleware
|
11 |
from pydantic import BaseModel
|
12 |
from dotenv import dotenv_values
|
|
|
|
|
|
|
|
|
|
|
13 |
|
14 |
+
# Import your tools
|
15 |
+
from tools import (
|
16 |
+
open_meteo,
|
17 |
+
tomorrow_io,
|
18 |
+
google_weather,
|
19 |
+
openweathermap,
|
20 |
+
accuweather,
|
21 |
+
openai_llm,
|
22 |
+
geographic_tools,
|
23 |
+
crop_calendar_tools,
|
24 |
+
alert_generation_tools
|
25 |
+
)
|
26 |
from a2a_agents import sms_agent, whatsapp_agent, ussd_agent, ivr_agent, telegram_agent
|
27 |
from utils.weather_utils import get_tool_config
|
28 |
|
29 |
+
# Configuration
|
30 |
config = dotenv_values(".env")
|
|
|
31 |
LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO")
|
32 |
logging.basicConfig(level=LOG_LEVEL)
|
33 |
logger = logging.getLogger(__name__)
|
34 |
|
35 |
+
# Verify API keys
|
36 |
+
openai_key = config.get("OPENAI_API_KEY") or os.getenv("OPENAI_API_KEY")
|
37 |
+
if not openai_key:
|
38 |
+
logger.warning("OpenAI API key not found - AI features will be limited")
|
39 |
+
else:
|
40 |
+
logger.info("OpenAI API key found")
|
41 |
|
42 |
+
app = FastAPI(title="MCP Weather Server", version="1.0.0")
|
43 |
|
44 |
+
# CORS middleware
|
45 |
app.add_middleware(
|
46 |
CORSMiddleware,
|
47 |
+
allow_origins=["https://mcp-ui.vercel.app", "*"], # Add * for development
|
48 |
allow_credentials=True,
|
49 |
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
50 |
allow_headers=["*"],
|
51 |
expose_headers=["*"]
|
52 |
)
|
53 |
|
54 |
+
# Pydantic models
|
55 |
class MCPRequest(BaseModel):
|
56 |
tool: str
|
57 |
parameters: dict
|
|
|
63 |
state: str
|
64 |
district: str
|
65 |
|
66 |
+
# Crop calendar constants
|
67 |
+
CROP_CALENDAR = {
|
68 |
+
"rice": {
|
69 |
+
"season": "Kharif",
|
70 |
+
"planting": "June-July",
|
71 |
+
"harvesting": "October-November",
|
72 |
+
"duration_days": 120,
|
73 |
+
"stages": ["Nursery/Seedling", "Transplanting", "Vegetative", "Tillering",
|
74 |
+
"Panicle Initiation", "Flowering", "Milk/Dough", "Maturity", "Harvesting"]
|
75 |
+
},
|
76 |
+
"wheat": {
|
77 |
+
"season": "Rabi",
|
78 |
+
"planting": "November-December",
|
79 |
+
"harvesting": "March-April",
|
80 |
+
"duration_days": 120,
|
81 |
+
"stages": ["Sowing", "Germination", "Tillering", "Jointing", "Booting",
|
82 |
+
"Heading", "Flowering", "Grain Filling", "Maturity", "Harvesting"]
|
83 |
+
},
|
84 |
+
"maize": {
|
85 |
+
"season": "Kharif/Zaid",
|
86 |
+
"planting": "June-July / March-April",
|
87 |
+
"harvesting": "September-October / June",
|
88 |
+
"duration_days": 110,
|
89 |
+
"stages": ["Sowing", "Emergence", "Vegetative", "Tasseling", "Silking",
|
90 |
+
"Grain Filling", "Maturity", "Harvesting"]
|
91 |
+
},
|
92 |
+
"sugarcane": {
|
93 |
+
"season": "Annual",
|
94 |
+
"planting": "February-March",
|
95 |
+
"harvesting": "December-January",
|
96 |
+
"duration_days": 300,
|
97 |
+
"stages": ["Planting", "Germination", "Tillering", "Grand Growth",
|
98 |
+
"Maturation", "Ripening", "Harvesting"]
|
99 |
+
},
|
100 |
+
"mustard": {
|
101 |
+
"season": "Rabi",
|
102 |
+
"planting": "October-November",
|
103 |
+
"harvesting": "February-March",
|
104 |
+
"duration_days": 110,
|
105 |
+
"stages": ["Sowing", "Germination", "Rosette", "Stem Elongation",
|
106 |
+
"Flowering", "Pod Formation", "Pod Filling", "Maturity", "Harvesting"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
107 |
}
|
108 |
+
}
|
109 |
+
|
110 |
+
# District-specific crop preferences for Bihar
|
111 |
+
DISTRICT_CROPS = {
|
112 |
+
'patna': {'primary': ['rice', 'wheat', 'potato'], 'secondary': ['mustard', 'gram'], 'specialty': ['sugarcane']},
|
113 |
+
'gaya': {'primary': ['wheat', 'rice'], 'secondary': ['barley', 'mustard'], 'specialty': ['gram']},
|
114 |
+
'bhagalpur': {'primary': ['rice', 'maize', 'wheat'], 'secondary': ['jute'], 'specialty': ['groundnut']},
|
115 |
+
'muzaffarpur': {'primary': ['sugarcane', 'rice', 'wheat'], 'secondary': ['potato', 'mustard'], 'specialty': ['lentil']},
|
116 |
+
'darbhanga': {'primary': ['rice', 'wheat', 'maize'], 'secondary': ['gram'], 'specialty': ['bajra']},
|
117 |
+
'siwan': {'primary': ['rice', 'wheat'], 'secondary': ['gram', 'lentil'], 'specialty': ['mustard']},
|
118 |
+
'begusarai': {'primary': ['rice', 'wheat'], 'secondary': ['jute', 'mustard'], 'specialty': ['moong']},
|
119 |
+
'katihar': {'primary': ['maize', 'rice'], 'secondary': ['jute'], 'specialty': ['jowar']}
|
120 |
+
}
|
121 |
+
|
122 |
+
def get_current_season(month: int) -> str:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
123 |
"""Determine current agricultural season"""
|
124 |
if month in [6, 7, 8, 9]: # June to September
|
125 |
return 'kharif'
|
|
|
128 |
else: # April, May
|
129 |
return 'zaid'
|
130 |
|
131 |
+
def select_regional_crop(district: str, state: str) -> str:
|
132 |
+
"""Select appropriate crop based on district, season, and preferences"""
|
|
|
133 |
if state.lower() != 'bihar':
|
134 |
+
return 'rice' # fallback
|
135 |
|
136 |
current_month = datetime.now().month
|
137 |
current_season = get_current_season(current_month)
|
138 |
|
139 |
+
# Get district preferences
|
140 |
+
district_prefs = DISTRICT_CROPS.get(district.lower(), {
|
141 |
+
'primary': ['rice', 'wheat'],
|
142 |
+
'secondary': ['gram'],
|
143 |
+
'specialty': ['maize']
|
144 |
+
})
|
|
|
|
|
|
|
|
|
145 |
|
146 |
+
# Season-specific crop filtering
|
147 |
+
seasonal_crops = {
|
148 |
+
'kharif': ['rice', 'maize', 'sugarcane', 'jowar', 'bajra', 'groundnut'],
|
149 |
+
'rabi': ['wheat', 'barley', 'gram', 'lentil', 'mustard', 'potato'],
|
150 |
+
'zaid': ['maize', 'moong', 'watermelon', 'cucumber']
|
|
|
|
|
|
|
|
|
|
|
151 |
}
|
152 |
|
153 |
+
# Combine district and seasonal preferences
|
154 |
+
all_district_crops = (district_prefs.get('primary', []) +
|
155 |
+
district_prefs.get('secondary', []) +
|
156 |
+
district_prefs.get('specialty', []))
|
|
|
|
|
|
|
|
|
157 |
|
158 |
+
suitable_crops = [crop for crop in all_district_crops
|
159 |
+
if crop in seasonal_crops.get(current_season, [])]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
160 |
|
161 |
if not suitable_crops:
|
162 |
suitable_crops = district_prefs.get('primary', ['rice'])
|
163 |
|
164 |
+
# Weighted selection (primary crops more likely)
|
165 |
weighted_crops = []
|
166 |
for crop in suitable_crops:
|
167 |
if crop in district_prefs.get('primary', []):
|
168 |
+
weighted_crops.extend([crop] * 5)
|
169 |
elif crop in district_prefs.get('secondary', []):
|
170 |
+
weighted_crops.extend([crop] * 3)
|
171 |
else:
|
172 |
+
weighted_crops.extend([crop] * 1)
|
173 |
|
174 |
selected_crop = random.choice(weighted_crops) if weighted_crops else 'rice'
|
175 |
logger.info(f"Selected crop: {selected_crop} for {district} in {current_season} season")
|
176 |
|
177 |
return selected_crop
|
178 |
|
179 |
+
def estimate_crop_stage(crop: str, current_month: int) -> str:
|
180 |
+
"""Estimate current crop stage based on crop type and month"""
|
181 |
+
if crop not in CROP_CALENDAR:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
182 |
return 'Growing'
|
183 |
|
184 |
+
crop_data = CROP_CALENDAR[crop]
|
185 |
+
stages = crop_data['stages']
|
186 |
+
|
187 |
+
# Month-based stage estimation
|
188 |
stage_mappings = {
|
189 |
'rice': {6: 0, 7: 1, 8: 2, 9: 3, 10: 4, 11: 5, 12: 6, 1: 7, 2: 8},
|
190 |
+
'wheat': {11: 0, 12: 1, 1: 2, 2: 3, 3: 4, 4: 5},
|
191 |
+
'maize': {6: 0, 7: 1, 8: 2, 9: 3, 10: 4, 11: 5, 3: 0, 4: 1, 5: 2},
|
192 |
+
'sugarcane': {2: 0, 3: 1, 4: 2, 5: 3, 6: 3, 7: 3, 8: 4, 9: 4, 10: 5, 11: 6, 12: 6, 1: 6},
|
193 |
+
'mustard': {10: 0, 11: 1, 12: 2, 1: 3, 2: 4, 3: 5}
|
194 |
}
|
195 |
|
196 |
crop_mapping = stage_mappings.get(crop, {})
|
197 |
+
stage_index = crop_mapping.get(current_month, len(stages) // 2) # Default to middle stage
|
198 |
stage_index = min(stage_index, len(stages) - 1)
|
199 |
|
200 |
+
return stages[stage_index] if stages else 'Growing'
|
201 |
|
202 |
+
async def get_location_coordinates(village: str, district: str) -> tuple[list, str]:
|
203 |
+
"""Get coordinates for village or district with fallback"""
|
204 |
+
location_coords = None
|
205 |
+
location_source = ""
|
206 |
|
207 |
+
# Try village coordinates first
|
208 |
+
try:
|
209 |
+
village_location = await geographic_tools.reverse_geocode(village)
|
210 |
+
if "error" not in village_location and "lat" in village_location:
|
211 |
+
location_coords = [village_location["lat"], village_location["lng"]]
|
212 |
+
location_source = f"village_{village}"
|
213 |
+
logger.info(f"Using village coordinates for {village}: {location_coords}")
|
214 |
+
except Exception as e:
|
215 |
+
logger.warning(f"Village geocoding failed for {village}: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
216 |
|
217 |
+
# Fallback to district coordinates
|
218 |
+
if not location_coords:
|
219 |
+
try:
|
220 |
+
district_location = await geographic_tools.reverse_geocode(district)
|
221 |
+
if "error" not in district_location and "lat" in district_location:
|
222 |
+
location_coords = [district_location["lat"], district_location["lng"]]
|
223 |
+
location_source = f"district_{district}"
|
224 |
+
logger.info(f"Using district coordinates for {district}: {location_coords}")
|
225 |
+
except Exception as e:
|
226 |
+
logger.warning(f"District geocoding failed for {district}: {e}")
|
227 |
+
|
228 |
+
# Final fallback
|
229 |
+
if not location_coords:
|
230 |
+
logger.warning(f"No coordinates found, using Patna fallback")
|
231 |
+
location_coords = [25.5941, 85.1376] # Patna coordinates
|
232 |
+
location_source = "fallback_patna"
|
233 |
+
|
234 |
+
return location_coords, location_source
|
235 |
+
|
236 |
+
async def generate_weather_based_alert(weather_data: dict, crop: str, crop_stage: str,
|
237 |
+
village: str, district: str) -> tuple[str, str, str, list]:
|
238 |
+
"""Generate alert based on weather conditions"""
|
239 |
+
current_weather = weather_data.get('current_weather', {})
|
240 |
+
daily_forecast = weather_data.get('daily', {})
|
241 |
+
|
242 |
+
current_temp = current_weather.get('temperature', 25)
|
243 |
+
current_windspeed = current_weather.get('windspeed', 10)
|
244 |
+
|
245 |
+
precipitation_list = daily_forecast.get('precipitation_sum', [0, 0, 0])
|
246 |
+
next_3_days_rain = sum(precipitation_list[:3]) if precipitation_list else 0
|
247 |
+
|
248 |
+
# Generate alert based on conditions
|
249 |
+
if next_3_days_rain > 25:
|
250 |
+
alert_type = "heavy_rain_warning"
|
251 |
+
urgency = "high"
|
252 |
+
alert_message = (f"Heavy rainfall ({next_3_days_rain:.1f}mm) expected in next 3 days "
|
253 |
+
f"near {village}, {district}. {crop} at {crop_stage} stage may be affected. "
|
254 |
+
f"Delay fertilizer application and ensure proper drainage.")
|
255 |
+
action_items = ["delay_fertilizer", "check_drainage", "monitor_crops", "prepare_harvest_protection"]
|
256 |
+
|
257 |
+
elif next_3_days_rain > 10:
|
258 |
+
alert_type = "moderate_rain_warning"
|
259 |
+
urgency = "medium"
|
260 |
+
alert_message = (f"Moderate rainfall ({next_3_days_rain:.1f}mm) expected in next 3 days "
|
261 |
+
f"near {village}, {district}. Monitor {crop} at {crop_stage} stage carefully.")
|
262 |
+
action_items = ["monitor_soil", "check_drainage", "adjust_irrigation"]
|
263 |
+
|
264 |
+
elif next_3_days_rain < 2 and current_temp > 35:
|
265 |
+
alert_type = "heat_drought_warning"
|
266 |
+
urgency = "high"
|
267 |
+
alert_message = (f"High temperature ({current_temp:.1f}°C) with minimal rainfall expected "
|
268 |
+
f"near {village}, {district}. {crop} at {crop_stage} stage needs extra care. "
|
269 |
+
f"Increase irrigation frequency.")
|
270 |
+
action_items = ["increase_irrigation", "mulch_crops", "monitor_plant_stress"]
|
271 |
+
|
272 |
+
elif current_temp < 10:
|
273 |
+
alert_type = "cold_warning"
|
274 |
+
urgency = "medium"
|
275 |
+
alert_message = (f"Low temperature ({current_temp:.1f}°C) expected near {village}, {district}. "
|
276 |
+
f"Protect {crop} crops from cold damage.")
|
277 |
+
action_items = ["protect_crops", "cover_seedlings", "adjust_irrigation_timing"]
|
278 |
+
|
279 |
+
elif current_windspeed > 30:
|
280 |
+
alert_type = "high_wind_warning"
|
281 |
+
urgency = "medium"
|
282 |
+
alert_message = (f"High winds ({current_windspeed:.1f} km/h) expected near {village}, {district}. "
|
283 |
+
f"Secure {crop} crop supports and structures.")
|
284 |
+
action_items = ["secure_supports", "check_structures", "monitor_damage"]
|
285 |
+
|
286 |
+
else:
|
287 |
+
alert_type = "weather_update"
|
288 |
+
urgency = "low"
|
289 |
+
alert_message = (f"Normal weather conditions expected near {village}, {district}. "
|
290 |
+
f"{crop} at {crop_stage} stage. Temperature {current_temp:.1f}°C, "
|
291 |
+
f"rainfall {next_3_days_rain:.1f}mm.")
|
292 |
+
action_items = ["routine_monitoring", "maintain_irrigation"]
|
293 |
+
|
294 |
+
return alert_type, urgency, alert_message, action_items
|
295 |
+
|
296 |
+
async def generate_ai_alert(latitude: float, longitude: float, crop: str,
|
297 |
+
crop_stage: str, village: str, district: str) -> Optional[dict]:
|
298 |
+
"""Generate AI-powered weather alert using OpenAI"""
|
299 |
+
if not openai_key:
|
300 |
+
logger.warning("No OpenAI API key - skipping AI alert generation")
|
301 |
+
return None
|
302 |
|
303 |
+
try:
|
304 |
+
ai_alert = await alert_generation_tools.predict_weather_alert(
|
305 |
+
latitude=latitude,
|
306 |
+
longitude=longitude,
|
307 |
+
api_key=openai_key
|
308 |
+
)
|
309 |
+
|
310 |
+
# Extract and enhance AI response
|
311 |
+
alert_description = ai_alert.get('alert', 'Weather update for agricultural activities')
|
312 |
+
impact_description = ai_alert.get('impact', 'Monitor crops regularly')
|
313 |
+
recommendations = ai_alert.get('recommendations', 'Continue routine farming activities')
|
314 |
+
|
315 |
+
# Create enhanced alert message
|
316 |
+
alert_message = f"🤖 AI Weather Alert for {village}, {district}: {alert_description}"
|
317 |
+
if impact_description and impact_description.lower() not in ['none', 'n/a', '']:
|
318 |
+
alert_message += f" 🌾 Crop Impact ({crop} - {crop_stage}): {impact_description}"
|
319 |
+
|
320 |
+
return {
|
321 |
+
'alert': alert_description,
|
322 |
+
'impact': impact_description,
|
323 |
+
'recommendations': recommendations,
|
324 |
+
'enhanced_message': alert_message
|
325 |
+
}
|
326 |
+
|
327 |
+
except Exception as e:
|
328 |
+
logger.error(f"AI alert generation failed: {e}")
|
329 |
+
return None
|
330 |
+
|
331 |
+
async def generate_dynamic_alert(district: str, state: str) -> dict:
|
332 |
+
"""Main function to generate comprehensive weather alert"""
|
333 |
try:
|
334 |
# Step 1: Get villages for the district
|
335 |
villages_data = await geographic_tools.list_villages(state, district)
|
|
|
337 |
if "error" in villages_data:
|
338 |
raise Exception(f"District '{district}' not found in {state}")
|
339 |
|
|
|
340 |
available_villages = villages_data.get("villages", [])
|
341 |
if not available_villages:
|
342 |
raise Exception(f"No villages found for {district}")
|
343 |
|
344 |
+
# Step 2: Select random village
|
345 |
selected_village = random.choice(available_villages)
|
346 |
logger.info(f"Selected village: {selected_village} from {len(available_villages)} villages")
|
347 |
|
348 |
+
# Step 3: Get coordinates
|
349 |
+
location_coords, location_source = await get_location_coordinates(selected_village, district)
|
|
|
350 |
|
351 |
+
# Step 4: Generate crop selection and stage
|
352 |
+
regional_crop = select_regional_crop(district, state)
|
353 |
+
crop_stage = estimate_crop_stage(regional_crop, datetime.now().month)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
354 |
|
355 |
+
# Step 5: Get weather data
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
356 |
try:
|
357 |
+
current_weather_data = await open_meteo.get_current_weather(
|
358 |
+
latitude=location_coords[0],
|
359 |
+
longitude=location_coords[1]
|
360 |
+
)
|
|
|
|
|
361 |
|
362 |
+
forecast_data = await open_meteo.get_weather_forecast(
|
363 |
+
latitude=location_coords[0],
|
|
|
364 |
longitude=location_coords[1],
|
365 |
+
days=7
|
366 |
)
|
367 |
|
368 |
+
logger.info(f"Weather data retrieved for {selected_village}, {district}")
|
369 |
|
370 |
+
except Exception as weather_error:
|
371 |
+
logger.error(f"Failed to get weather data: {weather_error}")
|
372 |
+
raise Exception(f"Unable to retrieve weather conditions for {selected_village}, {district}")
|
373 |
+
|
374 |
+
# Step 6: Generate alerts
|
375 |
+
alert_type, urgency, alert_message, action_items = await generate_weather_based_alert(
|
376 |
+
{'current_weather': current_weather_data.get('current_weather', {}),
|
377 |
+
'daily': forecast_data.get('daily', {})},
|
378 |
+
regional_crop, crop_stage, selected_village, district
|
379 |
+
)
|
380 |
+
|
381 |
+
# Step 7: Try to enhance with AI if available
|
382 |
+
ai_analysis = await generate_ai_alert(
|
383 |
+
location_coords[0], location_coords[1],
|
384 |
+
regional_crop, crop_stage, selected_village, district
|
385 |
+
)
|
386 |
+
|
387 |
+
# Step 8: Prepare weather context
|
388 |
+
current_weather = current_weather_data.get('current_weather', {})
|
389 |
+
daily_forecast = forecast_data.get('daily', {})
|
390 |
+
|
391 |
+
current_temp = current_weather.get('temperature', 25)
|
392 |
+
current_windspeed = current_weather.get('windspeed', 10)
|
393 |
+
precipitation_list = daily_forecast.get('precipitation_sum', [0, 0, 0])
|
394 |
+
next_3_days_rain = sum(precipitation_list[:3]) if precipitation_list else 0
|
395 |
+
rain_probability = min(90, max(10, int(next_3_days_rain * 10))) if next_3_days_rain > 0 else 10
|
396 |
+
estimated_humidity = min(95, max(40, 60 + int(next_3_days_rain * 2)))
|
397 |
+
|
398 |
+
weather_context = {
|
399 |
+
"forecast_days": 7,
|
400 |
+
"rain_probability": rain_probability,
|
401 |
+
"expected_rainfall": f"{next_3_days_rain:.1f}mm",
|
402 |
+
"temperature": f"{current_temp:.1f}°C",
|
403 |
+
"humidity": f"{estimated_humidity}%",
|
404 |
+
"wind_speed": f"{current_windspeed:.1f} km/h",
|
405 |
+
"coordinates_source": location_source
|
406 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
407 |
|
408 |
+
# Generate unique alert ID
|
409 |
alert_id = f"{state.upper()[:2]}_{district.upper()[:3]}_{selected_village.upper()[:3]}_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
|
410 |
|
411 |
+
# Build final response
|
412 |
+
result = {
|
413 |
"alert_id": alert_id,
|
414 |
"timestamp": datetime.now().isoformat() + "Z",
|
415 |
"location": {
|
|
|
424 |
"name": regional_crop,
|
425 |
"stage": crop_stage,
|
426 |
"season": get_current_season(datetime.now().month),
|
427 |
+
"planted_estimate": "2025-06-15"
|
428 |
},
|
429 |
"alert": {
|
430 |
"type": alert_type,
|
431 |
"urgency": urgency,
|
432 |
+
"message": ai_analysis['enhanced_message'] if ai_analysis else alert_message,
|
433 |
"action_items": action_items,
|
434 |
"valid_until": (datetime.now() + timedelta(days=3)).isoformat() + "Z",
|
435 |
+
"ai_generated": ai_analysis is not None
|
|
|
|
|
|
|
|
|
|
|
436 |
},
|
437 |
"weather": weather_context,
|
438 |
+
"data_source": "open_meteo_api_with_ai_enhancement" if ai_analysis else "open_meteo_api"
|
439 |
}
|
440 |
+
|
441 |
+
if ai_analysis:
|
442 |
+
result["ai_analysis"] = {
|
443 |
+
"alert": ai_analysis['alert'],
|
444 |
+
"impact": ai_analysis['impact'],
|
445 |
+
"recommendations": ai_analysis['recommendations']
|
446 |
+
}
|
447 |
+
|
448 |
+
return result
|
449 |
+
|
450 |
except Exception as e:
|
451 |
+
logger.error(f"Error generating alert for {district}, {state}: {e}")
|
452 |
+
raise Exception(f"Failed to generate weather alert for {district}: {str(e)}")
|
453 |
|
454 |
+
# API Routes
|
455 |
@app.get("/")
|
456 |
async def root():
|
457 |
+
return {"message": "MCP Weather Server v1.0 - AI-Powered Agricultural Alerts", "status": "running"}
|
458 |
|
459 |
@app.get("/api/health")
|
460 |
async def health_check():
|
461 |
+
return {
|
462 |
+
"status": "healthy",
|
463 |
+
"message": "API is working",
|
464 |
+
"openai_available": openai_key is not None,
|
465 |
+
"timestamp": datetime.now().isoformat()
|
466 |
+
}
|
467 |
|
|
|
468 |
@app.post("/api/run-workflow")
|
469 |
async def run_workflow(request: WorkflowRequest):
|
470 |
+
"""Main workflow endpoint for generating comprehensive agricultural alerts"""
|
471 |
logger.info(f"Received workflow request: {request.state}, {request.district}")
|
472 |
|
473 |
+
workflow_results = []
|
|
|
|
|
474 |
|
475 |
try:
|
476 |
+
# Workflow header
|
477 |
+
workflow_results.extend([
|
478 |
+
f"🌾 Agricultural Alert Workflow for {request.district.title()}, {request.state.title()}",
|
479 |
+
"=" * 70,
|
480 |
+
"",
|
481 |
+
"🌤️ Weather Data Collection",
|
482 |
+
"-" * 30
|
483 |
+
])
|
484 |
+
|
485 |
+
# Generate dynamic alert
|
486 |
+
sample_alert = await generate_dynamic_alert(request.district, request.state)
|
487 |
+
|
488 |
+
workflow_results.extend([
|
489 |
+
"✅ Weather data retrieved successfully",
|
490 |
+
f" 📍 Location: {sample_alert['location']['village']}, {sample_alert['location']['district']}",
|
491 |
+
f" 🌡️ Temperature: {sample_alert['weather']['temperature']}",
|
492 |
+
f" 🌧️ Expected Rainfall: {sample_alert['weather']['expected_rainfall']}",
|
493 |
+
f" 💨 Wind Speed: {sample_alert['weather']['wind_speed']}",
|
494 |
+
f" 🌾 Crop: {sample_alert['crop']['name']} ({sample_alert['crop']['stage']})",
|
495 |
+
f" 🚨 Alert Type: {sample_alert['alert']['type']} - {sample_alert['alert']['urgency'].upper()} priority",
|
496 |
+
""
|
497 |
+
])
|
498 |
+
|
499 |
+
# Generate agent responses
|
500 |
+
agents = [
|
501 |
+
("📱 WhatsApp Agent", whatsapp_agent.create_whatsapp_message),
|
502 |
+
("📱 SMS Agent", sms_agent.create_sms_message),
|
503 |
+
("📞 USSD Agent", ussd_agent.create_ussd_menu),
|
504 |
+
("🎙️ IVR Agent", ivr_agent.create_ivr_script),
|
505 |
+
("🤖 Telegram Agent", telegram_agent.create_telegram_message)
|
506 |
+
]
|
507 |
+
|
508 |
+
agent_responses = {}
|
509 |
+
|
510 |
+
for agent_name, agent_func in agents:
|
511 |
+
workflow_results.extend([agent_name, "-" * 30])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
512 |
try:
|
513 |
+
response = agent_func(sample_alert)
|
514 |
+
workflow_results.append("✅ Message generated successfully")
|
515 |
+
agent_responses[agent_name] = response
|
516 |
+
|
517 |
+
# Add preview for some agents
|
518 |
+
if "WhatsApp" in agent_name:
|
519 |
+
text = response.get('text', 'N/A')
|
520 |
+
workflow_results.append(f" Preview: {text[:100]}..." if len(text) > 100 else f" Preview: {text}")
|
521 |
+
elif "SMS" in agent_name:
|
522 |
+
workflow_results.append(f" Preview: {str(response)[:100]}...")
|
523 |
+
|
524 |
except Exception as e:
|
525 |
+
workflow_results.append(f"❌ Error: {str(e)}")
|
526 |
+
agent_responses[agent_name] = f"Error: {str(e)}"
|
527 |
|
528 |
+
workflow_results.append("")
|
529 |
+
|
530 |
+
# Summary
|
531 |
+
workflow_results.extend([
|
532 |
+
"✅ Workflow Summary",
|
533 |
+
"-" * 30,
|
534 |
+
f"🎯 Successfully generated alerts for {sample_alert['location']['village']}, {request.district}",
|
535 |
+
f"📊 Data Sources: {sample_alert['data_source']}",
|
536 |
+
f"🤖 AI Enhanced: {'Yes' if sample_alert['alert']['ai_generated'] else 'No'}",
|
537 |
+
f"⏰ Generated at: {datetime.now().strftime('%Y-%m-%d %H:%M:%S UTC')}",
|
538 |
+
f"📱 Agents Processed: {len([r for r in agent_responses.values() if not str(r).startswith('Error:')])}/{len(agents)}"
|
539 |
+
])
|
540 |
+
|
541 |
+
# Generate CSV
|
542 |
+
csv_content = generate_csv_export(sample_alert, agent_responses)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
543 |
|
|
|
544 |
return {
|
545 |
+
"message": "\n".join(workflow_results),
|
546 |
"status": "success",
|
547 |
"csv": csv_content,
|
548 |
"raw_data": {
|
549 |
"state": request.state,
|
550 |
"district": request.district,
|
551 |
+
"alert_data": sample_alert,
|
552 |
+
"agent_responses": agent_responses
|
553 |
}
|
554 |
}
|
555 |
|
556 |
except Exception as e:
|
557 |
+
error_msg = f"❌ Workflow failed: {str(e)}"
|
558 |
+
workflow_results.append(error_msg)
|
559 |
+
logger.exception(f"Workflow error for {request.district}, {request.state}")
|
560 |
+
|
561 |
return {
|
562 |
+
"message": "\n".join(workflow_results),
|
563 |
"status": "error",
|
564 |
"csv": "",
|
565 |
"error": str(e)
|
566 |
}
|
567 |
|
568 |
+
def generate_csv_export(alert_data: dict, agent_responses: dict) -> str:
|
569 |
+
"""Generate CSV export of workflow results"""
|
570 |
+
try:
|
571 |
+
csv_buffer = StringIO()
|
572 |
+
writer = csv.writer(csv_buffer)
|
573 |
+
|
574 |
+
# Headers
|
575 |
+
headers = ["Field", "Value"]
|
576 |
+
writer.writerow(headers)
|
577 |
+
|
578 |
+
# Alert data
|
579 |
+
writer.writerow(["Alert ID", alert_data["alert_id"]])
|
580 |
+
writer.writerow(["Village", alert_data["location"]["village"]])
|
581 |
+
writer.writerow(["District", alert_data["location"]["district"]])
|
582 |
+
writer.writerow(["State", alert_data["location"]["state"]])
|
583 |
+
writer.writerow(["Coordinates", str(alert_data["location"]["coordinates"])])
|
584 |
+
writer.writerow(["Crop", alert_data["crop"]["name"]])
|
585 |
+
writer.writerow(["Crop Stage", alert_data["crop"]["stage"]])
|
586 |
+
writer.writerow(["Temperature", alert_data["weather"]["temperature"]])
|
587 |
+
writer.writerow(["Rainfall", alert_data["weather"]["expected_rainfall"]])
|
588 |
+
writer.writerow(["Alert Type", alert_data["alert"]["type"]])
|
589 |
+
writer.writerow(["Urgency", alert_data["alert"]["urgency"]])
|
590 |
+
writer.writerow(["Alert Message", alert_data["alert"]["message"]])
|
591 |
+
|
592 |
+
# Agent responses
|
593 |
+
writer.writerow([]) # Empty row
|
594 |
+
writer.writerow(["Agent", "Response"])
|
595 |
+
for agent_name, response in agent_responses.items():
|
596 |
+
clean_agent_name = agent_name.replace("📱 ", "").replace("📞 ", "").replace("🎙️ ", "").replace("🤖 ", "")
|
597 |
+
writer.writerow([clean_agent_name, str(response)[:500]]) # Limit response length
|
598 |
+
|
599 |
+
return csv_buffer.getvalue()
|
600 |
+
|
601 |
+
except Exception as e:
|
602 |
+
logger.error(f"CSV generation error: {e}")
|
603 |
+
return f"Error generating CSV: {str(e)}"
|
604 |
|
605 |
+
# Other API endpoints
|
606 |
@app.post("/mcp")
|
607 |
async def mcp_endpoint(request: MCPRequest):
|
608 |
+
"""MCP tool execution endpoint"""
|
609 |
logger.info(f"Received request for tool: {request.tool}")
|
610 |
tool_config = get_tool_config(request.tool)
|
611 |
|
|
|
614 |
raise HTTPException(status_code=404, detail="Tool not found")
|
615 |
|
616 |
try:
|
617 |
+
# Route to appropriate module
|
618 |
+
module_map = {
|
619 |
+
"open_meteo": open_meteo,
|
620 |
+
"tomorrow_io": tomorrow_io,
|
621 |
+
"google_weather": google_weather,
|
622 |
+
"openweathermap": openweathermap,
|
623 |
+
"accuweather": accuweather,
|
624 |
+
"openai_llm": openai_llm,
|
625 |
+
"geographic_tools": geographic_tools,
|
626 |
+
"crop_calendar_tools": crop_calendar_tools,
|
627 |
+
"alert_generation_tools": alert_generation_tools
|
628 |
+
}
|
629 |
+
|
630 |
+
module = module_map.get(tool_config["module"])
|
631 |
+
if not module:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
632 |
raise HTTPException(status_code=500, detail="Invalid tool module")
|
633 |
+
|
634 |
+
# Add API key if needed
|
635 |
+
params = request.parameters.copy()
|
636 |
+
if tool_config["module"] in ["tomorrow_io", "google_weather", "openweathermap", "accuweather"]:
|
637 |
+
api_key_map = {
|
638 |
+
"tomorrow_io": "TOMORROW_IO_API_KEY",
|
639 |
+
"google_weather": "GOOGLE_WEATHER_API_KEY",
|
640 |
+
"openweathermap": "OPENWEATHERMAP_API_KEY",
|
641 |
+
"accuweather": "ACCUWEATHER_API_KEY"
|
642 |
+
}
|
643 |
+
key_name = api_key_map[tool_config["module"]]
|
644 |
+
params["api_key"] = config.get(key_name)
|
645 |
+
elif tool_config["module"] in ["openai_llm", "alert_generation_tools"]:
|
646 |
+
params["api_key"] = openai_key
|
647 |
+
|
648 |
+
# Execute tool function
|
649 |
+
result = await getattr(module, request.tool)(**params)
|
650 |
+
|
651 |
logger.info(f"Successfully executed tool: {request.tool}")
|
652 |
return result
|
653 |
+
|
654 |
except Exception as e:
|
655 |
logger.exception(f"Error executing tool: {request.tool}")
|
656 |
raise HTTPException(status_code=500, detail=str(e))
|
657 |
|
658 |
+
# A2A Agent endpoints
|
659 |
@app.post("/a2a/sms")
|
660 |
async def a2a_sms_endpoint(request: AlertRequest):
|
661 |
+
"""SMS agent endpoint"""
|
662 |
return {"message": sms_agent.create_sms_message(request.alert_json)}
|
663 |
|
664 |
@app.post("/a2a/whatsapp")
|
665 |
async def a2a_whatsapp_endpoint(request: AlertRequest):
|
666 |
+
"""WhatsApp agent endpoint"""
|
667 |
return whatsapp_agent.create_whatsapp_message(request.alert_json)
|
668 |
|
669 |
@app.post("/a2a/ussd")
|
670 |
async def a2a_ussd_endpoint(request: AlertRequest):
|
671 |
+
"""USSD agent endpoint"""
|
672 |
return {"menu": ussd_agent.create_ussd_menu(request.alert_json)}
|
673 |
|
674 |
@app.post("/a2a/ivr")
|
675 |
async def a2a_ivr_endpoint(request: AlertRequest):
|
676 |
+
"""IVR agent endpoint"""
|
677 |
return {"script": ivr_agent.create_ivr_script(request.alert_json)}
|
678 |
|
679 |
@app.post("/a2a/telegram")
|
680 |
async def a2a_telegram_endpoint(request: AlertRequest):
|
681 |
+
"""Telegram agent endpoint"""
|
682 |
return telegram_agent.create_telegram_message(request.alert_json)
|
683 |
|
684 |
+
# MCP RPC handler for external integration
|
685 |
+
@app.post("/mcp-rpc")
|
|
|
|
|
686 |
async def mcp_rpc_handler(request: dict):
|
687 |
+
"""JSON-RPC handler for MCP integration"""
|
688 |
method = request.get("method")
|
689 |
params = request.get("params", {})
|
690 |
tool_name = params.get("tool_name")
|
|
|
698 |
result = await run_workflow(WorkflowRequest(state=state, district=district))
|
699 |
return {"jsonrpc": "2.0", "result": result, "id": req_id}
|
700 |
|
701 |
+
# Handle other tools dynamically
|
702 |
if method == "call_tool":
|
703 |
try:
|
704 |
result = await mcp_endpoint(MCPRequest(tool=tool_name, parameters=arguments))
|
|
|
708 |
|
709 |
return {"jsonrpc": "2.0", "error": {"code": -32601, "message": "Unknown method"}, "id": req_id}
|
710 |
|
711 |
+
# Additional utility endpoints
|
712 |
+
@app.get("/api/districts/{state}")
|
713 |
+
async def get_districts(state: str):
|
714 |
+
"""Get list of districts for a state"""
|
715 |
+
try:
|
716 |
+
result = await geographic_tools.list_villages(state)
|
717 |
+
if "districts" in result:
|
718 |
+
return {"districts": result["districts"]}
|
719 |
+
return {"error": "State not found"}
|
720 |
+
except Exception as e:
|
721 |
+
raise HTTPException(status_code=500, detail=str(e))
|
722 |
+
|
723 |
+
@app.get("/api/villages/{state}/{district}")
|
724 |
+
async def get_villages(state: str, district: str):
|
725 |
+
"""Get list of villages for a district"""
|
726 |
+
try:
|
727 |
+
result = await geographic_tools.list_villages(state, district)
|
728 |
+
return result
|
729 |
+
except Exception as e:
|
730 |
+
raise HTTPException(status_code=500, detail=str(e))
|
731 |
+
|
732 |
+
@app.get("/api/crops/{region}")
|
733 |
+
async def get_crops(region: str):
|
734 |
+
"""Get list of crops for a region"""
|
735 |
+
try:
|
736 |
+
result = await crop_calendar_tools.get_crop_calendar(region)
|
737 |
+
return result
|
738 |
+
except Exception as e:
|
739 |
+
raise HTTPException(status_code=500, detail=str(e))
|
740 |
+
|
741 |
+
@app.get("/api/weather/{latitude}/{longitude}")
|
742 |
+
async def get_weather(latitude: float, longitude: float):
|
743 |
+
"""Get current weather for coordinates"""
|
744 |
+
try:
|
745 |
+
current_weather = await open_meteo.get_current_weather(latitude=latitude, longitude=longitude)
|
746 |
+
forecast = await open_meteo.get_weather_forecast(latitude=latitude, longitude=longitude, days=7)
|
747 |
+
|
748 |
+
return {
|
749 |
+
"current": current_weather,
|
750 |
+
"forecast": forecast,
|
751 |
+
"timestamp": datetime.now().isoformat()
|
752 |
+
}
|
753 |
+
except Exception as e:
|
754 |
+
raise HTTPException(status_code=500, detail=str(e))
|
755 |
+
|
756 |
+
# Error handlers
|
757 |
+
@app.exception_handler(HTTPException)
|
758 |
+
async def http_exception_handler(request, exc):
|
759 |
+
logger.error(f"HTTP {exc.status_code}: {exc.detail}")
|
760 |
+
return {
|
761 |
+
"error": exc.detail,
|
762 |
+
"status_code": exc.status_code,
|
763 |
+
"timestamp": datetime.now().isoformat()
|
764 |
+
}
|
765 |
+
|
766 |
+
@app.exception_handler(Exception)
|
767 |
+
async def general_exception_handler(request, exc):
|
768 |
+
logger.exception("Unhandled exception occurred")
|
769 |
+
return {
|
770 |
+
"error": "Internal server error",
|
771 |
+
"message": str(exc),
|
772 |
+
"timestamp": datetime.now().isoformat()
|
773 |
+
}
|
774 |
|
775 |
if __name__ == "__main__":
|
776 |
import uvicorn
|
777 |
+
logger.info("Starting MCP Weather Server...")
|
778 |
+
uvicorn.run(
|
779 |
+
app,
|
780 |
+
host="0.0.0.0",
|
781 |
+
port=int(os.getenv("PORT", 8000)),
|
782 |
+
log_level=LOG_LEVEL.lower()
|
783 |
+
)
|