elanuk commited on
Commit
e9dace8
·
verified ·
1 Parent(s): ac74c31

Update server.py

Browse files
Files changed (1) hide show
  1. server.py +554 -979
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
- from tools import open_meteo, tomorrow_io, google_weather, openweathermap, accuweather, openai_llm, geographic_tools, crop_calendar_tools, alert_generation_tools
 
 
 
 
 
 
 
 
 
 
 
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
- print("OPENAI_API_KEY exists?", os.getenv("OPENAI_API_KEY") is not None)
26
- openai.api_key = os.getenv("OPENAI_API_KEY")
 
 
 
 
27
 
28
- app = FastAPI()
29
 
30
- # CORS middleware for frontend
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
- def get_regional_crop_for_area(district: str, state: str):
52
- """Get typical crop for the region"""
53
- if state.lower() == 'bihar':
54
- district_crops = {
55
- 'patna': 'rice',
56
- 'gaya': 'wheat',
57
- 'bhagalpur': 'rice',
58
- 'muzaffarpur': 'sugarcane',
59
- 'darbhanga': 'rice',
60
- 'siwan': 'rice',
61
- 'begusarai': 'rice',
62
- 'katihar': 'maize',
63
- }
64
- return district_crops.get(district.lower(), 'rice')
65
- return 'rice'
66
-
67
- def get_current_crop_stage(crop: str):
68
- """Determine crop stage based on current date"""
69
- current_month = datetime.now().month
70
-
71
- if crop == 'rice':
72
- if current_month in [6, 7]:
73
- return 'planting'
74
- elif current_month in [8, 9]:
75
- return 'growing'
76
- elif current_month in [10, 11]:
77
- return 'flowering'
78
- else:
79
- return 'harvesting'
80
- elif crop == 'wheat':
81
- if current_month in [11, 12]:
82
- return 'planting'
83
- elif current_month in [1, 2]:
84
- return 'growing'
85
- elif current_month in [3, 4]:
86
- return 'flowering'
87
- else:
88
- return 'harvesting'
89
- elif crop == 'sugarcane':
90
- if current_month in [2, 3, 4]:
91
- return 'planting'
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
- # Get district preferences or use default
353
- district_prefs = district_crop_preferences.get(district.lower(), {
354
- 'primary': ['rice', 'wheat'],
355
- 'secondary': ['gram', 'mustard'],
356
- 'specialty': ['maize']
357
- })
358
-
359
- # Combine all possible crops for this district
360
- all_district_crops = (district_prefs.get('primary', []) +
361
- district_prefs.get('secondary', []) +
362
- district_prefs.get('specialty', []))
363
-
364
- # Find crops that are both seasonal AND grown in this district
365
- suitable_crops = []
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
- async def get_regional_crop_for_area(district: str, state: str):
461
- """Get typical crop for the region based on season and district - now fully dynamic"""
462
-
463
  if state.lower() != 'bihar':
464
- return 'rice' # fallback for other states
465
 
466
  current_month = datetime.now().month
467
  current_season = get_current_season(current_month)
468
 
469
- # Get crops that are currently in season using your crop calendar tools
470
- try:
471
- seasonal_crops_data = await crop_calendar_tools.get_prominent_crops('bihar', current_season)
472
- if "error" not in seasonal_crops_data:
473
- seasonal_crops = seasonal_crops_data.get('crops', [])
474
- else:
475
- seasonal_crops = []
476
- except Exception as e:
477
- logger.warning(f"Failed to get seasonal crops: {e}")
478
- seasonal_crops = []
479
 
480
- # District-specific crop preferences
481
- district_crop_preferences = {
482
- 'patna': {'primary': ['rice', 'wheat', 'potato'], 'secondary': ['mustard', 'gram', 'barley'], 'specialty': ['sugarcane']},
483
- 'gaya': {'primary': ['wheat', 'rice', 'gram'], 'secondary': ['barley', 'lentil', 'mustard'], 'specialty': ['arhar']},
484
- 'bhagalpur': {'primary': ['rice', 'maize', 'wheat'], 'secondary': ['jute', 'urd', 'moong'], 'specialty': ['groundnut']},
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
- district_prefs = district_crop_preferences.get(district.lower(), {'primary': ['rice', 'wheat'], 'secondary': ['gram', 'mustard'], 'specialty': ['maize']})
493
-
494
- all_district_crops = (district_prefs.get('primary', []) + district_prefs.get('secondary', []) + district_prefs.get('specialty', []))
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
- # If no seasonal match, use season-based fallback
502
- if not suitable_crops:
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
- # Weight selection based on crop category
517
  weighted_crops = []
518
  for crop in suitable_crops:
519
  if crop in district_prefs.get('primary', []):
520
- weighted_crops.extend([crop] * 5) # 5x weight for primary crops
521
  elif crop in district_prefs.get('secondary', []):
522
- weighted_crops.extend([crop] * 3) # 3x weight for secondary crops
523
  else:
524
- weighted_crops.extend([crop] * 1) # 1x weight for specialty crops
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
- async def get_current_crop_stage_dynamic(crop: str, district: str = None):
532
- """Determine crop stage based on current date and crop calendar"""
533
- try:
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, 5: 6, 6: 7, 7: 8},
602
- 'maize': {6: 0, 7: 1, 8: 2, 9: 3, 10: 4, 11: 5, 12: 6, 1: 7, 2: 7, 3: 0, 4: 1, 5: 2}
 
 
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 stage_index < len(stages) else stages[-1]
610
 
611
- def get_current_crop_stage_static(crop: str):
612
- """Original static crop stage function as fallback"""
613
- current_month = datetime.now().month
 
614
 
615
- if crop == 'rice':
616
- if current_month in [6, 7]:
617
- return 'Transplanting'
618
- elif current_month in [8, 9]:
619
- return 'Vegetative'
620
- elif current_month in [10, 11]:
621
- return 'Flowering'
622
- else:
623
- return 'Maturity'
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
- return 'Growing'
653
-
654
- #
655
-
656
- async def generate_dynamic_alert(district: str, state: str):
657
- """Generate AI-powered dynamic alert data using real weather and crop intelligence"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 for the selected village/district
675
- location_coords = None
676
- location_source = ""
677
 
678
- # Try village coordinates first
679
- try:
680
- village_location = await geographic_tools.reverse_geocode(selected_village)
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
- # Final fallback
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
- logger.info(f"Generating AI-powered alert for coordinates: {location_coords} (source: {location_source})")
712
-
713
- # Get the API key
714
- api_key = config.get("OPENAI_API_KEY")
715
- if not api_key:
716
- raise Exception("OpenAI API key not found")
717
 
718
- # Use your AI prediction tool
719
- ai_alert = await alert_generation_tools.predict_weather_alert(
720
- latitude=location_coords[0],
721
  longitude=location_coords[1],
722
- api_key=api_key
723
  )
724
 
725
- logger.info(f"AI alert generated successfully for {selected_village}, {district}")
726
 
727
- # Also get basic weather data for additional context
728
- try:
729
- current_weather_data = await open_meteo.get_current_weather(
730
- latitude=location_coords[0],
731
- longitude=location_coords[1]
732
- )
733
-
734
- forecast_data = await open_meteo.get_weather_forecast(
735
- latitude=location_coords[0],
736
- longitude=location_coords[1],
737
- days=7
738
- )
739
-
740
- current_weather = current_weather_data.get('current_weather', {})
741
- daily_forecast = forecast_data.get('daily', {})
742
-
743
- current_temp = current_weather.get('temperature', 25)
744
- current_windspeed = current_weather.get('windspeed', 10)
745
-
746
- precipitation_list = daily_forecast.get('precipitation_sum', [0, 0, 0])
747
- next_3_days_rain = sum(precipitation_list[:3]) if precipitation_list else 0
748
-
749
- rain_probability = min(90, max(10, int(next_3_days_rain * 10))) if next_3_days_rain > 0 else 10
750
- estimated_humidity = min(95, max(40, 60 + int(next_3_days_rain * 2)))
751
-
752
- weather_context = {
753
- "forecast_days": 7,
754
- "rain_probability": rain_probability,
755
- "expected_rainfall": f"{next_3_days_rain:.1f}mm",
756
- "temperature": f"{current_temp:.1f}°C",
757
- "humidity": f"{estimated_humidity}%",
758
- "wind_speed": f"{current_windspeed:.1f} km/h",
759
- "coordinates_source": location_source
760
- }
761
-
762
- except Exception as weather_error:
763
- logger.warning(f"Could not get basic weather data: {weather_error}")
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 with timestamp
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
- return {
 
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" # Could make this dynamic based on crop calendar
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": True
846
- },
847
- "ai_analysis": {
848
- "alert": alert_description,
849
- "impact": impact_description,
850
- "recommendations": recommendations
851
  },
852
  "weather": weather_context,
853
- "data_source": "ai_powered_openai_gpt4_with_open_meteo"
854
  }
855
-
 
 
 
 
 
 
 
 
 
856
  except Exception as e:
857
- logger.error(f"Error generating AI-powered alert for {district}, {state}: {e}")
858
- raise Exception(f"Failed to generate AI weather alert for {district}: {str(e)}")
859
 
 
860
  @app.get("/")
861
  async def root():
862
- return {"message": "MCP Weather Server is running"}
863
 
864
  @app.get("/api/health")
865
  async def health_check():
866
- return {"status": "healthy", "message": "API is working"}
 
 
 
 
 
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
- # Initialize variables
874
- sample_alert = None
875
- csv_content = ""
876
 
877
  try:
878
- # Create comprehensive workflow response
879
- workflow_results = []
880
-
881
- # Add workflow header
882
- workflow_results.append(f"Workflow for {request.district}, {request.state}")
883
- workflow_results.append("=" * 50)
884
-
885
- # weather data collection
886
- workflow_results.append("\n🌤️ Weather Data Collection")
887
- workflow_results.append("-" * 30)
888
- workflow_results.append("📡 Fetching real-time weather data...")
889
-
890
- try:
891
- sample_alert = await generate_dynamic_alert(request.district, request.state)
892
-
893
- workflow_results.append(" Current weather data retrieved from Open-Meteo API")
894
- workflow_results.append(" 7-day forecast collected")
895
- workflow_results.append(" Agricultural indices calculated")
896
-
897
- except Exception as weather_error:
898
- logger.error(f"Weather data error: {weather_error}")
899
- workflow_results.append(f"❌ Weather data collection failed: {str(weather_error)}")
900
- return {
901
- "message": "\n".join(workflow_results),
902
- "status": "error",
903
- "csv": "",
904
- "error": f"Unable to retrieve weather data: {str(weather_error)}"
905
- }
906
-
907
- if not sample_alert:
908
- return {
909
- "message": "Failed to generate alert data",
910
- "status": "error",
911
- "csv": "",
912
- "error": "Alert generation failed"
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
- ivr_script = ivr_agent.create_ivr_script(sample_alert)
1052
- ivr_data.append(str(ivr_script))
1053
- except Exception as e:
1054
- ivr_data.append(f"Error: {str(e)}")
1055
-
1056
- # Get Telegram message
1057
- try:
1058
- telegram_message = telegram_agent.create_telegram_message(sample_alert)
1059
- telegram_data.append(str(telegram_message))
 
 
1060
  except Exception as e:
1061
- telegram_data.append(f"Error: {str(e)}")
 
1062
 
1063
- # Find the maximum number of rows needed
1064
- max_rows = max(
1065
- len(weather_data),
1066
- len(whatsapp_data) if whatsapp_data else 1,
1067
- len(sms_data) if sms_data else 1,
1068
- len(ussd_data) if ussd_data else 1,
1069
- len(ivr_data) if ivr_data else 1,
1070
- len(telegram_data) if telegram_data else 1
1071
- )
1072
-
1073
- # Write data rows
1074
- for i in range(max_rows):
1075
- row = [
1076
- weather_data[i] if i < len(weather_data) else "",
1077
- whatsapp_data[i] if i < len(whatsapp_data) else "",
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": formatted_output,
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
- logger.exception(f"Error in workflow for {request.district}, {request.state}")
 
 
 
1106
  return {
1107
- "message": f"Error running workflow: {str(e)}",
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
- if tool_config["module"] == "open_meteo":
1125
- result = await getattr(open_meteo, request.tool)(**request.parameters)
1126
- elif tool_config["module"] == "tomorrow_io":
1127
- api_key = config.get("TOMORROW_IO_API_KEY")
1128
- result = await getattr(tomorrow_io, request.tool)(**request.parameters, api_key=api_key)
1129
- elif tool_config["module"] == "google_weather":
1130
- api_key = config.get("GOOGLE_WEATHER_API_KEY")
1131
- result = await getattr(google_weather, request.tool)(**request.parameters, api_key=api_key)
1132
- elif tool_config["module"] == "openweathermap":
1133
- api_key = config.get("OPENWEATHERMAP_API_KEY")
1134
- result = await getattr(openweathermap, request.tool)(**request.parameters, api_key=api_key)
1135
- elif tool_config["module"] == "accuweather":
1136
- api_key = config.get("ACCUWEATHER_API_KEY")
1137
- result = await getattr(accuweather, request.tool)(**request.parameters, api_key=api_key)
1138
- elif tool_config["module"] == "openai_llm":
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
- # for smithery + context7
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 via your tool config
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
- uvicorn.run(app, host="0.0.0.0", port=8000)
 
 
 
 
 
 
 
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
+ )