Files changed (1) hide show
  1. app.py +704 -291
app.py CHANGED
@@ -1,4 +1,5 @@
1
- from fastapi import FastAPI, HTTPException, Header, Request
 
2
  from pydantic import BaseModel
3
  import requests
4
  import json
@@ -8,77 +9,60 @@ import pandas as pd
8
  import os
9
  import re
10
  import statistics
11
- from datetime import datetime
12
- from typing import Optional, Dict, Any, List
13
-
14
- app = FastAPI()
15
 
16
- # Your Retell.ai secret key (get from Retell.ai dashboard)
17
- RETELL_SECRET_KEY = "key_bdb05277a4587c7441bdad4a2c1b"
18
 
19
- # --- WEATHER CONFIG ---
20
- WEATHER_API_KEY = "ee75ffd59875aa5ca6c207e594336b30"
 
 
 
21
 
22
- # Load CSV data on startup
 
 
23
  def load_csv_data():
24
- """Load all CSV files into memory"""
25
  data = {}
26
  csv_files = {
27
- 'contact_info': '/app/data/contact_info.csv',
28
- 'crop_advisory': '/app/data/crop_advisory.csv',
29
- 'government_schemes': '/app/data/government_schemes.csv'
 
30
  }
31
 
32
  for key, file_path in csv_files.items():
33
  try:
34
  if os.path.exists(file_path):
35
- data[key] = pd.read_csv(file_path)
36
- # Strip whitespace from column names and string values
37
- data[key].columns = data[key].columns.str.strip()
38
- for col in data[key].select_dtypes(include=['object']).columns:
39
- data[key][col] = data[key][col].astype(str).str.strip()
40
- print(f"Loaded {key}: {len(data[key])} records")
 
 
41
  else:
42
- print(f"Warning: {file_path} not found")
43
  data[key] = pd.DataFrame()
44
  except Exception as e:
45
- print(f"Error loading {key}: {str(e)}")
46
  data[key] = pd.DataFrame()
47
-
48
  return data
49
 
50
- # Load CSV data
51
  csv_data = load_csv_data()
52
 
53
- def get_weather(city: str):
54
- """Fetches weather data from OpenWeatherMap API."""
55
- if not city:
56
- return None, None, None, None
57
- url = f"https://api.openweathermap.org/data/2.5/weather?q={city}&appid={WEATHER_API_KEY}&units=metric"
58
- try:
59
- response = requests.get(url, timeout=5)
60
- response.raise_for_status()
61
- data = response.json()
62
-
63
- # OpenWeather returns cod as int or string depending on response
64
- if str(data.get("cod")) == "200":
65
- weather_description = data['weather'][0]['description']
66
- temperature = data['main']['temp']
67
- humidity = data['main']['humidity']
68
- pressure = data['main']['pressure']
69
- return temperature, humidity, weather_description, pressure
70
- except Exception as e:
71
- print(f"Error fetching weather data: {e}")
72
-
73
- return None, None, None, None
74
-
75
- class RetellRequest(BaseModel):
76
- name: str # Function name
77
- call: Dict[str, Any] # Call object with transcript and context
78
- args: Dict[str, Any] # Function arguments
79
-
80
- def verify_retell_signature(request_body: bytes, signature: str) -> bool:
81
- """Verify the request is from Retell.ai"""
82
  expected_signature = hmac.new(
83
  RETELL_SECRET_KEY.encode(),
84
  request_body,
@@ -86,55 +70,32 @@ def verify_retell_signature(request_body: bytes, signature: str) -> bool:
86
  ).hexdigest()
87
  return hmac.compare_digest(signature, expected_signature)
88
 
89
- def search_csv_data(df: pd.DataFrame, search_terms: Dict[str, str]) -> pd.DataFrame:
90
- """Search dataframe based on multiple criteria"""
91
- if df.empty:
92
- return df
93
-
94
- result = df.copy()
95
- for column, value in search_terms.items():
96
- if column in df.columns and value:
97
- # Case-insensitive partial matching
98
- result = result[result[column].astype(str).str.contains(value, case=False, na=False)]
99
-
100
- return result
101
-
102
- # -------------------------
103
- # Helper utilities
104
- # -------------------------
105
  def find_column(df: pd.DataFrame, candidates: List[str]) -> Optional[str]:
106
  """Return first matching column name from candidates (case-insensitive) or None."""
107
  cols = {c.lower(): c for c in df.columns}
108
  for cand in candidates:
109
- if cand.lower() in cols:
110
  return cols[cand.lower()]
111
  return None
112
 
113
  def extract_number_from_price(val: Any) -> Optional[float]:
114
- """
115
- Try to extract numeric value from price strings like "₹2,180 per quintal" or "2180".
116
- Returns float or None if not parseable.
117
- """
118
  if pd.isna(val):
119
  return None
120
  if isinstance(val, (int, float)):
121
  return float(val)
122
  s = str(val)
123
- # remove currency symbols and non-digit characters except dot and minus
124
- # first try to find first numeric group
125
- # remove common words like per, quintal
126
- # Use regex to capture numbers like 2,180.50 or 2180.5
127
- match = re.search(r"(-?\d{1,3}(?:[,]\d{3})*(?:\.\d+)?|-?\d+(?:\.\d+)?)", s.replace('₹','').replace('Rs','').replace('INR',''))
128
  if match:
129
- num = match.group(0).replace(',', '')
130
  try:
131
- return float(num)
132
  except:
133
  return None
134
  return None
135
 
136
  def format_scheme_row(row: pd.Series, mapping: Dict[str,str]) -> Dict[str,str]:
137
- """Build a consistent scheme dict from a CSV row using mapping of fields to column names."""
138
  return {
139
  "scheme": row.get(mapping.get("name", ""), "N/A"),
140
  "introduction": row.get(mapping.get("introduction", ""), ""),
@@ -143,21 +104,16 @@ def format_scheme_row(row: pd.Series, mapping: Dict[str,str]) -> Dict[str,str]:
143
  "eligibility": row.get(mapping.get("eligibility", ""), ""),
144
  "process": row.get(mapping.get("process", ""), "Contact local agriculture office"),
145
  "contact": row.get(mapping.get("contact", ""), ""),
146
- "extra": row.get(mapping.get("extra", ""), ""),
147
  }
148
 
149
  def get_schemes_from_csv(farmer_category: str, land_size: float, state: str, crop_type: str) -> List[Dict[str,str]]:
150
- """
151
- Read government_schemes dataframe and return a list of scheme dicts.
152
- This function attempts to surface the most relevant schemes first but will
153
- return all schemes if filtering doesn't match.
154
- """
155
  schemes_out = []
156
  df = csv_data.get('government_schemes', pd.DataFrame())
157
  if df.empty:
158
  return []
159
 
160
- # build mapping for column names (supports different CSV header variants)
161
  mapping = {
162
  "name": find_column(df, ["Name", "scheme_name", "Scheme", "Scheme Name"]),
163
  "introduction": find_column(df, ["Introduction", "introduction", "Description"]),
@@ -169,18 +125,16 @@ def get_schemes_from_csv(farmer_category: str, land_size: float, state: str, cro
169
  "extra": find_column(df, ["Extra Details", "extra_details", "Extra"])
170
  }
171
 
172
- # Build list of all schemes with formatting
173
  all_schemes = []
174
  for _, r in df.iterrows():
175
  all_schemes.append(format_scheme_row(r, mapping))
176
 
177
- # Try to filter schemes based on simple heuristics:
178
  prioritized = []
179
  others = []
180
 
181
- state_lower = state.lower() if state else ""
182
- farmer_cat_lower = farmer_category.lower() if farmer_category else ""
183
- crop_lower = crop_type.lower() if crop_type else ""
184
 
185
  for s in all_schemes:
186
  elig = str(s.get("eligibility", "")).lower()
@@ -194,137 +148,438 @@ def get_schemes_from_csv(farmer_category: str, land_size: float, state: str, cro
194
  ]).lower()
195
 
196
  score = 0
197
- # If scheme mentions the state explicitly -> higher relevance
198
  if state_lower and state_lower in text_blob:
199
  score += 2
200
- # If eligibility explicitly mentions landholding and user has land_size > 0
201
  if land_size and ("land" in elig or "landholding" in elig or "land" in text_blob):
202
  score += 2
203
- # If eligibility says "all farmers" or similar, raise modestly
204
- if "all" in elig or "all farmers" in elig or "all landholding" in elig:
205
  score += 1
206
- # crop-specific mention
207
  if crop_lower and crop_lower in text_blob:
208
  score += 2
209
- # farmer category mention
210
  if farmer_cat_lower and farmer_cat_lower in text_blob:
211
  score += 1
212
 
213
- # Put high-scored into prioritized list
214
  if score >= 2:
215
  prioritized.append((score, s))
216
  else:
217
  others.append((score, s))
218
 
219
- # sort priority by score desc
220
  prioritized.sort(key=lambda x: x[0], reverse=True)
221
  others.sort(key=lambda x: x[0], reverse=True)
222
 
223
- # return only scheme dicts, prioritized first
224
  schemes_out = [s for _, s in prioritized] + [s for _, s in others]
225
  return schemes_out
226
 
227
  # -------------------------
228
- # End helpers
229
  # -------------------------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
230
 
231
- @app.post("/api/market-prices")
232
- async def market_prices(request: dict):
233
- # Keep your request shape usage intact
234
- crop_name = request.get("query", {}).get("crop_name", "").strip()
235
- state = request.get("query", {}).get("state", "").strip()
236
- district = request.get("query", {}).get("district", "").strip()
237
-
238
- # Safely handle missing CSV or missing expected columns
239
- if "crop_advisory" in csv_data and not csv_data["crop_advisory"].empty:
240
- df = csv_data["crop_advisory"].copy()
241
-
242
- # find likely column names for crop, state, district, price
243
- crop_col = find_column(df, ["crop_name", "crop", "Crop", "Crop Name"])
244
- state_col = find_column(df, ["state", "State", "state_name"])
245
- district_col = find_column(df, ["district", "District", "district_name"])
246
- price_col = find_column(df, ["price", "Price", "market_price", "market price", "price_per_quintal"])
247
-
248
- # build mask progressively (use contains if exact match column not present)
249
- mask = pd.Series([True] * len(df))
250
- if crop_col and crop_name:
251
- mask = mask & df[crop_col].astype(str).str.contains(crop_name, case=False, na=False)
252
- if state_col and state:
253
- mask = mask & df[state_col].astype(str).str.contains(state, case=False, na=False)
254
- if district_col and district:
255
- mask = mask & df[district_col].astype(str).str.contains(district, case=False, na=False)
256
-
257
- matches = df[mask]
258
-
259
- if not matches.empty:
260
- # compute average over numeric-parsable values in price_col if exists
261
- avg_price = None
262
- parsed_prices = []
263
- if price_col:
264
- for v in matches[price_col].tolist():
265
- num = extract_number_from_price(v)
266
- if num is not None:
267
- parsed_prices.append(num)
268
- if parsed_prices:
269
- try:
270
- avg_price = statistics.mean(parsed_prices)
271
- except Exception:
272
- avg_price = None
273
-
274
- if avg_price is not None:
275
- result = f"The average market price of {crop_name} in {district}, {state} is ₹{avg_price:.2f} per quintal."
276
  else:
277
- # If price_col absent or non-numeric, fallback to your previous text but mention CSV found
278
- result = f"Market data found for {crop_name} in {district}, {state} but numeric price values were not available."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
279
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
280
  return {
281
  "success": True,
282
- "result": result,
283
- "data": matches.to_dict(orient="records")
 
284
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
285
 
286
- # fallback to previous mock behavior (keeps your logic)
287
- return {
288
- "success": False,
289
- "result": f"No market price data found for {crop_name} in {district}, {state}."
290
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
291
 
 
 
 
292
  @app.post("/api/scheme-eligibility")
293
  async def scheme_eligibility_endpoint(
294
  request: Request,
295
- x_retell_signature: str = Header(None, alias="X-Retell-Signature")
296
  ):
297
- """Handle scheme eligibility function call from Retell.ai"""
298
  request_body = await request.body()
299
- retell_request = json.loads(request_body.decode('utf-8'))
 
 
 
 
 
 
 
300
 
301
- # Extract arguments
302
- farmer_category = retell_request["args"].get("farmer_category", "")
303
- land_size = retell_request["args"].get("land_size", 0)
304
- state = retell_request["args"].get("state", "")
305
- crop_type = retell_request["args"].get("crop_type", "")
306
 
307
  try:
308
  eligible_schemes = []
309
-
310
- # Search government schemes CSV and apply simple relevance heuristics
311
  if not csv_data['government_schemes'].empty:
312
- eligible_schemes = get_schemes_from_csv(farmer_category, land_size, state, crop_type)
313
-
314
- # Add default schemes if no CSV data or as fallback
 
 
 
 
 
315
  if not eligible_schemes:
316
- # PM-KISAN eligibility
317
- if land_size and float(land_size) > 0:
 
 
 
318
  eligible_schemes.append({
319
  "scheme": "PM-KISAN",
320
  "benefit": "₹6,000 per year in 3 installments",
321
  "description": "Direct income support to landholding farmer families.",
322
  "eligibility": "All landholding farmer families.",
323
- "process": "Apply online at pmkisan.gov.in or visit nearest CSC",
324
  "contact": "https://pmkisan.gov.in/"
325
  })
326
-
327
- # Crop Insurance
328
  eligible_schemes.append({
329
  "scheme": "Pradhan Mantri Fasal Bima Yojana",
330
  "benefit": "Comprehensive crop insurance coverage",
@@ -333,9 +588,7 @@ async def scheme_eligibility_endpoint(
333
  "process": "Contact your nearest bank, CSC or PMFBY portal",
334
  "contact": "https://pmfby.gov.in/"
335
  })
336
-
337
- # State-specific schemes
338
- if state and state.lower() == "punjab":
339
  eligible_schemes.append({
340
  "scheme": "Punjab Crop Diversification Scheme",
341
  "benefit": "₹17,500 per hectare for diversification",
@@ -343,15 +596,14 @@ async def scheme_eligibility_endpoint(
343
  "contact": ""
344
  })
345
 
346
- # Format response for voice (limit to first 3 items, keep original style)
347
  if eligible_schemes:
348
  schemes_text = f"You are eligible for {len(eligible_schemes)} government schemes: "
349
- for i, scheme in enumerate(eligible_schemes[:3]): # Limit to first 3 for voice response
350
- contact_info = f" Apply through {scheme.get('process','Contact local agriculture office')}" if scheme.get('process') else ""
351
  if scheme.get('contact'):
352
  contact_info += f" or contact {scheme.get('contact')}"
353
  schemes_text += f"{i+1}. {scheme.get('scheme','N/A')} - {scheme.get('benefit', scheme.get('description','N/A'))}.{contact_info}. "
354
-
355
  if len(eligible_schemes) > 3:
356
  schemes_text += f"And {len(eligible_schemes)-3} more schemes available."
357
  else:
@@ -364,22 +616,82 @@ async def scheme_eligibility_endpoint(
364
 
365
  except Exception as e:
366
  return {
367
- "result": "I'm having trouble accessing scheme information right now. Please contact your local agriculture officer or visit the nearest CSC for scheme details.",
368
  "error": str(e)
369
  }
370
 
371
- @app.post("/api/weather-advisory")
372
- async def weather_advisory(request: dict):
373
- # Keep your request shape usage intact
374
- city = request.get("query", {}).get("location", "").strip()
 
 
 
 
 
 
 
375
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
376
  temperature, humidity, description, pressure = get_weather(city)
377
  if temperature is None:
378
- # Fallback values
379
  temperature, humidity, description, pressure = 32.0, 60, "Not Available", 1012
380
  weather_condition = "NORMAL"
381
  else:
382
- desc_lower = description.lower()
383
  if "clear" in desc_lower:
384
  weather_condition = "SUNNY"
385
  elif "rain" in desc_lower:
@@ -390,7 +702,7 @@ async def weather_advisory(request: dict):
390
  weather_condition = "NORMAL"
391
 
392
  result = (
393
- f"Weather in {city}: {description}. "
394
  f"Temperature {temperature}°C, Humidity {humidity}%, Pressure {pressure} hPa. "
395
  f"Condition classified as {weather_condition}."
396
  )
@@ -408,89 +720,132 @@ async def weather_advisory(request: dict):
408
  }
409
  }
410
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
411
  @app.post("/api/crop-advisory")
412
  async def crop_advisory_endpoint(
413
  request: Request,
414
- x_retell_signature: str = Header(None, alias="X-Retell-Signature")
415
  ):
416
- """Handle crop advisory function call from Retell.ai"""
417
  request_body = await request.body()
418
- retell_request = json.loads(request_body.decode('utf-8'))
 
 
 
 
 
 
419
 
420
- crop_name = retell_request["args"].get("crop_name", "")
421
- growth_stage = retell_request["args"].get("growth_stage", "")
422
- issue_type = retell_request["args"].get("issue_type", "general")
423
- state = retell_request["args"].get("state", "")
424
 
425
  try:
426
  advisory = None
427
  contact_info = ""
428
 
429
- # Search crop advisory CSV
430
- if not csv_data['crop_advisory'].empty:
431
- search_terms = {}
432
- if crop_name:
433
- # Search by crop name
434
- crop_matches = csv_data['crop_advisory'][
435
- csv_data['crop_advisory']['crop'].str.contains(crop_name, case=False, na=False)
436
- ]
437
-
438
- if not crop_matches.empty:
439
- crop_info = crop_matches.iloc[0]
440
-
441
- # Build advisory based on available data
442
- advisory_parts = []
443
-
444
- if issue_type == "general":
445
- if pd.notna(crop_info.get('sowing_time')):
446
- advisory_parts.append(f"Sowing time: {crop_info['sowing_time']}")
447
- if pd.notna(crop_info.get('fertilizer')):
448
- advisory_parts.append(f"Recommended fertilizer: {crop_info['fertilizer']}")
449
- if pd.notna(crop_info.get('season')):
450
- advisory_parts.append(f"Best season: {crop_info['season']}")
451
-
452
- # Check for specific issues
453
- if pd.notna(crop_info.get('common_issues')) and pd.notna(crop_info.get('solution')):
454
- if issue_type in ['pest', 'disease'] or issue_type == 'general':
455
- advisory_parts.append(f"For {crop_info['common_issues']}: {crop_info['solution']}")
456
-
457
- if advisory_parts:
458
- advisory = f"For {crop_name}: " + ". ".join(advisory_parts)
459
-
460
- # Search contact info CSV
461
- if not csv_data['contact_info'].empty and state:
462
- # Search by state
463
- contact_matches = csv_data['contact_info'][
464
- csv_data['contact_info']['state'].str.contains(state, case=False, na=False)
465
- ]
466
-
467
- if not contact_matches.empty:
468
- contact_match = contact_matches.iloc[0]
469
- contact_parts = []
470
-
471
- if pd.notna(contact_match.get('agriculture_officer')):
472
- contact_parts.append(f"Agriculture Officer at {contact_match['agriculture_officer']}")
473
-
474
- if pd.notna(contact_match.get('kvk_contact')):
475
- contact_parts.append(f"KVK at {contact_match['kvk_contact']}")
476
-
477
- if pd.notna(contact_match.get('kisan_call_center')):
478
- contact_parts.append(f"Kisan Call Center at {contact_match['kisan_call_center']}")
479
-
480
- if contact_parts:
481
- contact_info = f"For detailed advice in {state}, contact: " + " or ".join(contact_parts) + "."
482
-
483
- # Fallback advisory
484
  if not advisory:
485
- if crop_name.lower() == "wheat" and issue_type == "pest":
486
- advisory = "For wheat pest control: If you see aphids, spray Imidacloprid 200 SL at 0.3ml per liter of water. Spray during evening hours. Avoid over-irrigation which attracts pests."
487
- elif crop_name.lower() == "rice" and issue_type == "disease":
488
  advisory = "For rice disease management: If you see brown spots on leaves, it might be blast disease. Apply Tricyclazole 75% WP at 0.6g per liter. Ensure proper drainage."
489
  else:
490
- advisory = f"For {crop_name} at {growth_stage} stage: Monitor crop regularly, maintain proper spacing, apply fertilizers as per soil test recommendations."
491
 
492
  if not contact_info:
493
- contact_info = f"For detailed advice, contact your local Krishi Vigyan Kendra or Agriculture Officer in {state}. You can also call the Kisan Call Centre at 1800-1801-551."
494
 
495
  result_text = f"{advisory} {contact_info}"
496
 
@@ -506,41 +861,99 @@ async def crop_advisory_endpoint(
506
  "error": str(e)
507
  }
508
 
509
- @app.get("/api/csv-status")
510
- async def csv_status():
511
- """Check status of loaded CSV files"""
512
- status = {}
513
- for key, df in csv_data.items():
514
- status[key] = {
515
- "loaded": not df.empty,
516
- "records": len(df),
517
- "columns": list(df.columns) if not df.empty else []
518
- }
519
- return status
 
520
 
521
- # Health check endpoint
522
- @app.get("/health")
523
- async def health_check():
524
- return {
525
- "status": "healthy",
526
- "service": "Krishi Mitra API",
527
- "csv_files_loaded": {key: len(df) for key, df in csv_data.items()}
528
- }
529
 
530
- @app.get("/")
531
- async def root():
532
- return {
533
- "message": "Krishi Mitra API is running!",
534
- "endpoints": [
535
- "/api/market-prices",
536
- "/api/scheme-eligibility",
537
- "/api/weather-advisory",
538
- "/api/crop-advisory",
539
- "/api/csv-status",
540
- "/health"
541
- ]
542
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
543
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
544
  if __name__ == "__main__":
545
  import uvicorn
546
- uvicorn.run(app, host="0.0.0.0", port=7860)
 
1
+ # app.py
2
+ from fastapi import FastAPI, HTTPException, Header, Request, Query
3
  from pydantic import BaseModel
4
  import requests
5
  import json
 
9
  import os
10
  import re
11
  import statistics
12
+ from datetime import datetime, timedelta
13
+ from typing import Optional, Dict, Any, List, Union
14
+ import requests
15
+ import urllib.parse
16
 
17
+ app = FastAPI(title="Krishi Mitra API")
 
18
 
19
+ # -------------------------
20
+ # Configuration (update with env vars in production)
21
+ # -------------------------
22
+ RETELL_SECRET_KEY = os.getenv("RETELL_SECRET_KEY", "key_bdb05277a4587c7441bdad4a2c1b")
23
+ WEATHER_API_KEY = os.getenv("WEATHER_API_KEY", "ee75ffd59875aa5ca6c207e594336b30")
24
 
25
+ # -------------------------
26
+ # CSV loader
27
+ # -------------------------
28
  def load_csv_data():
29
+ """Load all CSV files into memory; trim whitespace from columns and string cells."""
30
  data = {}
31
  csv_files = {
32
+ 'contact_info': './data/contact_info.csv',
33
+ 'crop_advisory': './data/crop_advisory.csv',
34
+ 'government_schemes': './data/government_schemes.csv',
35
+ 'market_prices': './data/market_prices.csv'
36
  }
37
 
38
  for key, file_path in csv_files.items():
39
  try:
40
  if os.path.exists(file_path):
41
+ df = pd.read_csv(file_path)
42
+ # strip whitespace from column names
43
+ df.columns = df.columns.str.strip()
44
+ # strip whitespace from string columns
45
+ for col in df.select_dtypes(include=['object']).columns:
46
+ df[col] = df[col].astype(str).str.strip()
47
+ data[key] = df
48
+ print(f"Loaded {key} ({file_path}): {len(df)} records")
49
  else:
50
+ print(f"Warning: {file_path} not found - {key} will be empty")
51
  data[key] = pd.DataFrame()
52
  except Exception as e:
53
+ print(f"Error loading {file_path}: {e}")
54
  data[key] = pd.DataFrame()
 
55
  return data
56
 
 
57
  csv_data = load_csv_data()
58
 
59
+ # -------------------------
60
+ # Helpers
61
+ # -------------------------
62
+ def verify_retell_signature(request_body: bytes, signature: Optional[str]) -> bool:
63
+ """Verify the request is from Retell.ai if signature provided. If no signature, treat as allowed (for local testing)."""
64
+ if not signature:
65
+ return True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  expected_signature = hmac.new(
67
  RETELL_SECRET_KEY.encode(),
68
  request_body,
 
70
  ).hexdigest()
71
  return hmac.compare_digest(signature, expected_signature)
72
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  def find_column(df: pd.DataFrame, candidates: List[str]) -> Optional[str]:
74
  """Return first matching column name from candidates (case-insensitive) or None."""
75
  cols = {c.lower(): c for c in df.columns}
76
  for cand in candidates:
77
+ if cand and cand.lower() in cols:
78
  return cols[cand.lower()]
79
  return None
80
 
81
  def extract_number_from_price(val: Any) -> Optional[float]:
82
+ """Extract numeric value from messy price strings like '₹2,180 per quintal'."""
 
 
 
83
  if pd.isna(val):
84
  return None
85
  if isinstance(val, (int, float)):
86
  return float(val)
87
  s = str(val)
88
+ s = s.replace('₹', '').replace('Rs', '').replace('INR', '')
89
+ match = re.search(r"(-?\d{1,3}(?:[,]\d{3})*(?:\.\d+)?|-?\d+(?:\.\d+)?)", s)
 
 
 
90
  if match:
 
91
  try:
92
+ return float(match.group(0).replace(',', ''))
93
  except:
94
  return None
95
  return None
96
 
97
  def format_scheme_row(row: pd.Series, mapping: Dict[str,str]) -> Dict[str,str]:
98
+ """Normalize scheme row into dict keys used in responses."""
99
  return {
100
  "scheme": row.get(mapping.get("name", ""), "N/A"),
101
  "introduction": row.get(mapping.get("introduction", ""), ""),
 
104
  "eligibility": row.get(mapping.get("eligibility", ""), ""),
105
  "process": row.get(mapping.get("process", ""), "Contact local agriculture office"),
106
  "contact": row.get(mapping.get("contact", ""), ""),
107
+ "extra": row.get(mapping.get("extra", ""), "")
108
  }
109
 
110
  def get_schemes_from_csv(farmer_category: str, land_size: float, state: str, crop_type: str) -> List[Dict[str,str]]:
111
+ """Return list of scheme dicts from government_schemes CSV (with simple heuristics)."""
 
 
 
 
112
  schemes_out = []
113
  df = csv_data.get('government_schemes', pd.DataFrame())
114
  if df.empty:
115
  return []
116
 
 
117
  mapping = {
118
  "name": find_column(df, ["Name", "scheme_name", "Scheme", "Scheme Name"]),
119
  "introduction": find_column(df, ["Introduction", "introduction", "Description"]),
 
125
  "extra": find_column(df, ["Extra Details", "extra_details", "Extra"])
126
  }
127
 
 
128
  all_schemes = []
129
  for _, r in df.iterrows():
130
  all_schemes.append(format_scheme_row(r, mapping))
131
 
 
132
  prioritized = []
133
  others = []
134
 
135
+ state_lower = (state or "").lower()
136
+ farmer_cat_lower = (farmer_category or "").lower()
137
+ crop_lower = (crop_type or "").lower()
138
 
139
  for s in all_schemes:
140
  elig = str(s.get("eligibility", "")).lower()
 
148
  ]).lower()
149
 
150
  score = 0
 
151
  if state_lower and state_lower in text_blob:
152
  score += 2
 
153
  if land_size and ("land" in elig or "landholding" in elig or "land" in text_blob):
154
  score += 2
155
+ if "all" in elig or "all farmers" in elig:
 
156
  score += 1
 
157
  if crop_lower and crop_lower in text_blob:
158
  score += 2
 
159
  if farmer_cat_lower and farmer_cat_lower in text_blob:
160
  score += 1
161
 
 
162
  if score >= 2:
163
  prioritized.append((score, s))
164
  else:
165
  others.append((score, s))
166
 
 
167
  prioritized.sort(key=lambda x: x[0], reverse=True)
168
  others.sort(key=lambda x: x[0], reverse=True)
169
 
 
170
  schemes_out = [s for _, s in prioritized] + [s for _, s in others]
171
  return schemes_out
172
 
173
  # -------------------------
174
+ # Weather helper (simple)
175
  # -------------------------
176
+ def get_weather(city: str):
177
+ """Fetch weather data from OpenWeatherMap API. Returns (temperature, humidity, description, pressure) or (None,...)."""
178
+ if not city:
179
+ return None, None, None, None
180
+ url = f"https://api.openweathermap.org/data/2.5/weather?q={city}&appid={WEATHER_API_KEY}&units=metric"
181
+ try:
182
+ resp = requests.get(url, timeout=5)
183
+ resp.raise_for_status()
184
+ data = resp.json()
185
+ if str(data.get("cod")) == "200":
186
+ weather_description = data['weather'][0]['description']
187
+ temperature = data['main']['temp']
188
+ humidity = data['main']['humidity']
189
+ pressure = data['main']['pressure']
190
+ return temperature, humidity, weather_description, pressure
191
+ except Exception as e:
192
+ print(f"Weather fetch error: {e}")
193
+ return None, None, None, None
194
 
195
+ # -------------------------
196
+ # Market Prices Helper Functions (Updated for CSV)
197
+ # -------------------------
198
+ def get_market_prices_from_csv(state: str, district: Optional[str] = None, crop_name: Optional[str] = None):
199
+ """
200
+ Fetch market price data from local CSV file
201
+ Returns (success: bool, data: list, message: str)
202
+ """
203
+ try:
204
+ # Load market prices CSV
205
+ market_df = csv_data.get('market_prices', pd.DataFrame())
206
+
207
+ # If market_prices not loaded, try to load it directly
208
+ if market_df.empty:
209
+ market_csv_path = './data/market_prices.csv'
210
+ if os.path.exists(market_csv_path):
211
+ market_df = pd.read_csv(market_csv_path)
212
+ # Clean column names and string data
213
+ market_df.columns = market_df.columns.str.strip()
214
+ for col in market_df.select_dtypes(include=['object']).columns:
215
+ market_df[col] = market_df[col].astype(str).str.strip()
216
+ # Update the global csv_data
217
+ csv_data['market_prices'] = market_df
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
218
  else:
219
+ return False, [], f"Market prices CSV file not found at {market_csv_path}"
220
+
221
+ if market_df.empty:
222
+ return False, [], "No market price data available"
223
+
224
+ # Find relevant columns (case-insensitive matching)
225
+ state_col = find_column(market_df, ["State", "state"])
226
+ district_col = find_column(market_df, ["District", "district"])
227
+ commodity_col = find_column(market_df, ["Commodity", "commodity", "Crop", "crop"])
228
+ market_col = find_column(market_df, ["Market", "market"])
229
+ variety_col = find_column(market_df, ["Variety", "variety"])
230
+ date_col = find_column(market_df, ["Arrival_Date", "arrival_date", "Date", "date"])
231
+ min_price_col = find_column(market_df, ["Min_x0020_Price", "min_price", "Min_Price", "Minimum_Price"])
232
+ max_price_col = find_column(market_df, ["Max_x0020_Price", "max_price", "Max_Price", "Maximum_Price"])
233
+ modal_price_col = find_column(market_df, ["Modal_x0020_Price", "modal_price", "Modal_Price", "Average_Price"])
234
+
235
+ if not state_col:
236
+ return False, [], "State column not found in market prices data"
237
+
238
+ # Filter by state (case-insensitive)
239
+ filtered_df = market_df[market_df[state_col].astype(str).str.contains(state, case=False, na=False)]
240
+
241
+ # Filter by district if provided
242
+ if district and district_col:
243
+ filtered_df = filtered_df[filtered_df[district_col].astype(str).str.contains(district, case=False, na=False)]
244
+
245
+ # Filter by crop/commodity if provided
246
+ if crop_name and commodity_col:
247
+ filtered_df = filtered_df[filtered_df[commodity_col].astype(str).str.contains(crop_name, case=False, na=False)]
248
+
249
+ if filtered_df.empty:
250
+ return False, [], f"No market price data found for the specified criteria"
251
+
252
+ # Convert to list of dictionaries
253
+ processed_data = []
254
+ for _, record in filtered_df.iterrows():
255
+ processed_record = {
256
+ "state": record.get(state_col, "") if state_col else "",
257
+ "district": record.get(district_col, "") if district_col else "",
258
+ "market": record.get(market_col, "") if market_col else "",
259
+ "commodity": record.get(commodity_col, "") if commodity_col else "",
260
+ "variety": record.get(variety_col, "") if variety_col else "",
261
+ "arrival_date": record.get(date_col, "") if date_col else "",
262
+ "min_price": record.get(min_price_col, "") if min_price_col else "",
263
+ "max_price": record.get(max_price_col, "") if max_price_col else "",
264
+ "modal_price": record.get(modal_price_col, "") if modal_price_col else ""
265
+ }
266
+ processed_data.append(processed_record)
267
+
268
+ return True, processed_data, f"Found {len(processed_data)} market price records"
269
+
270
+ except Exception as e:
271
+ return False, [], f"Error processing market data: {str(e)}"
272
 
273
+ def format_market_prices_response(data: List[Dict], state: str, district: Optional[str] = None, crop_name: Optional[str] = None):
274
+ """
275
+ Format market price data into a voice-friendly response
276
+ """
277
+ if not data:
278
+ location_text = f"{district}, {state}" if district else state
279
+ return f"No current market price data available for {location_text}. Please contact your local market or agriculture office for current rates."
280
+
281
+ # Group data by commodity for better presentation
282
+ commodity_data = {}
283
+ for record in data:
284
+ commodity = record.get("commodity", "Unknown")
285
+ if commodity not in commodity_data:
286
+ commodity_data[commodity] = []
287
+ commodity_data[commodity].append(record)
288
+
289
+ # Build response text
290
+ location_text = f"{district}, {state}" if district else state
291
+
292
+ if crop_name and crop_name.lower() in [c.lower() for c in commodity_data.keys()]:
293
+ # Specific crop requested
294
+ matching_commodity = next(c for c in commodity_data.keys() if c.lower() == crop_name.lower())
295
+ crop_records = commodity_data[matching_commodity]
296
+
297
+ if len(crop_records) == 1:
298
+ record = crop_records[0]
299
+ response_text = f"Market price for {matching_commodity} in {record.get('market', location_text)}: "
300
+
301
+ # Clean and format prices
302
+ min_price = extract_number_from_price(record.get('min_price', ''))
303
+ max_price = extract_number_from_price(record.get('max_price', ''))
304
+ modal_price = extract_number_from_price(record.get('modal_price', ''))
305
+
306
+ if min_price is not None:
307
+ response_text += f"Minimum ₹{min_price:.0f}, "
308
+ if max_price is not None:
309
+ response_text += f"Maximum ₹{max_price:.0f}, "
310
+ if modal_price is not None:
311
+ response_text += f"Modal price ₹{modal_price:.0f} per quintal. "
312
+ else:
313
+ response_text += "per quintal. "
314
+
315
+ if record.get('arrival_date'):
316
+ response_text += f"Data from {record.get('arrival_date')}."
317
+ else:
318
+ # Multiple records for the same commodity
319
+ min_prices = []
320
+ max_prices = []
321
+ modal_prices = []
322
+
323
+ for r in crop_records:
324
+ min_p = extract_number_from_price(r.get('min_price', ''))
325
+ max_p = extract_number_from_price(r.get('max_price', ''))
326
+ modal_p = extract_number_from_price(r.get('modal_price', ''))
327
+
328
+ if min_p is not None:
329
+ min_prices.append(min_p)
330
+ if max_p is not None:
331
+ max_prices.append(max_p)
332
+ if modal_p is not None:
333
+ modal_prices.append(modal_p)
334
+
335
+ response_text = f"Market prices for {matching_commodity} in {location_text}: "
336
+ if min_prices and max_prices:
337
+ response_text += f"Price range ₹{min(min_prices):.0f} to ₹{max(max_prices):.0f} per quintal. "
338
+ if modal_prices:
339
+ avg_modal = sum(modal_prices) / len(modal_prices)
340
+ response_text += f"Average modal price ₹{avg_modal:.0f} per quintal. "
341
+ response_text += f"Data from {len(crop_records)} markets."
342
+ else:
343
+ # General market overview or multiple commodities
344
+ response_text = f"Current market prices in {location_text}: "
345
+
346
+ commodity_summaries = []
347
+ for commodity, records in list(commodity_data.items())[:5]: # Limit to 5 commodities for voice
348
+ if records:
349
+ modal_prices = []
350
+ for r in records:
351
+ modal_p = extract_number_from_price(r.get('modal_price', ''))
352
+ if modal_p is not None:
353
+ modal_prices.append(modal_p)
354
+
355
+ if modal_prices:
356
+ avg_price = sum(modal_prices) / len(modal_prices)
357
+ commodity_summaries.append(f"{commodity} at ₹{avg_price:.0f}")
358
+ else:
359
+ commodity_summaries.append(f"{commodity} (price varies)")
360
+
361
+ if commodity_summaries:
362
+ response_text += ", ".join(commodity_summaries) + " per quintal. "
363
+
364
+ if len(commodity_data) > 5:
365
+ response_text += f"And {len(commodity_data) - 5} more commodities available."
366
+
367
+ return response_text
368
+
369
+ # -------------------------
370
+ # Request models (if needed)
371
+ # -------------------------
372
+ class RetellRequest(BaseModel):
373
+ name: str
374
+ call: Dict[str, Any]
375
+ args: Dict[str, Any]
376
+
377
+ # -------------------------
378
+ # Endpoints
379
+ # -------------------------
380
+
381
+ # Root and health
382
+ @app.get("/")
383
+ async def root():
384
+ return {
385
+ "message": "Krishi Mitra API is running!",
386
+ "endpoints": [
387
+ "/api/market-prices (GET|POST)",
388
+ "/api/scheme-eligibility (GET|POST)",
389
+ "/api/weather-advisory (GET|POST)",
390
+ "/api/crop-advisory (GET|POST)",
391
+ "/api/csv-status (GET)",
392
+ "/health (GET)"
393
+ ]
394
+ }
395
+
396
+ @app.get("/health")
397
+ async def health_check():
398
+ return {
399
+ "status": "healthy",
400
+ "service": "Krishi Mitra API",
401
+ "csv_files_loaded": {key: len(df) for key, df in csv_data.items()}
402
+ }
403
+
404
+ @app.get("/api/csv-status")
405
+ async def csv_status():
406
+ """Check status of loaded CSV files"""
407
+ status = {}
408
+ for key, df in csv_data.items():
409
+ status[key] = {
410
+ "loaded": not df.empty,
411
+ "records": len(df),
412
+ "columns": list(df.columns) if not df.empty else []
413
+ }
414
+ return status
415
+
416
+ # -------------------------
417
+ # Market prices (Updated for CSV)
418
+ # -------------------------
419
+ @app.post("/api/market-prices")
420
+ async def market_prices_post(request: Request):
421
+ """
422
+ Get market prices from local CSV data
423
+ """
424
+ try:
425
+ body = await request.json() if (await request.body()) else {}
426
+
427
+ # Extract parameters from different possible locations in payload
428
+ query_params = body.get("query", {})
429
+ args_params = body.get("args", {})
430
+
431
+ crop_name = (
432
+ query_params.get("crop_name", "") or
433
+ args_params.get("crop_name", "") or
434
+ body.get("crop_name", "")
435
+ ).strip()
436
+
437
+ state = (
438
+ query_params.get("state", "") or
439
+ args_params.get("state", "") or
440
+ body.get("state", "")
441
+ ).strip()
442
+
443
+ district = (
444
+ query_params.get("district", "") or
445
+ args_params.get("district", "") or
446
+ body.get("district", "")
447
+ ).strip()
448
+
449
+ if not state:
450
+ return {
451
+ "success": False,
452
+ "result": "Please provide state name to get market prices.",
453
+ "data": []
454
+ }
455
+
456
+ # Fetch market data from CSV
457
+ success, data, message = get_market_prices_from_csv(state, district or None, crop_name or None)
458
+
459
+ if success:
460
+ response_text = format_market_prices_response(data, state, district or None, crop_name or None)
461
  return {
462
  "success": True,
463
+ "result": response_text,
464
+ "data": data[:10], # Limit response data for voice interface
465
+ "total_records": len(data)
466
  }
467
+ else:
468
+ # Fallback message
469
+ location_text = f"{district}, {state}" if district else state
470
+ fallback_message = f"Current market price data for {location_text} is not available right now. Please contact your local mandi or agriculture market committee for current rates."
471
+
472
+ return {
473
+ "success": False,
474
+ "result": fallback_message,
475
+ "data": [],
476
+ "error": message
477
+ }
478
+
479
+ except Exception as e:
480
+ return {
481
+ "success": False,
482
+ "result": "I'm having trouble accessing market price data right now. Please contact your local mandi for current rates.",
483
+ "data": [],
484
+ "error": str(e)
485
+ }
486
 
487
+ @app.get("/api/market-prices")
488
+ async def market_prices_get(
489
+ crop_name: Optional[str] = Query("", alias="crop_name"),
490
+ state: Optional[str] = Query("", alias="state"),
491
+ district: Optional[str] = Query("", alias="district")
492
+ ):
493
+ """
494
+ Get market prices via GET request from local CSV
495
+ """
496
+ if not state:
497
+ return {
498
+ "success": False,
499
+ "result": "Please provide state parameter to get market prices.",
500
+ "data": []
501
+ }
502
+
503
+ try:
504
+ # Fetch market data from CSV
505
+ success, data, message = get_market_prices_from_csv(state.strip(), district.strip() if district else None, crop_name.strip() if crop_name else None)
506
+
507
+ if success:
508
+ response_text = format_market_prices_response(data, state.strip(), district.strip() if district else None, crop_name.strip() if crop_name else None)
509
+ return {
510
+ "success": True,
511
+ "result": response_text,
512
+ "data": data[:10], # Limit response data
513
+ "total_records": len(data)
514
+ }
515
+ else:
516
+ # Fallback message
517
+ location_text = f"{district}, {state}" if district else state
518
+ fallback_message = f"Current market price data for {location_text} is not available right now. Please contact your local mandi or agriculture market committee for current rates."
519
+
520
+ return {
521
+ "success": False,
522
+ "result": fallback_message,
523
+ "data": [],
524
+ "error": message
525
+ }
526
+
527
+ except Exception as e:
528
+ return {
529
+ "success": False,
530
+ "result": "I'm having trouble accessing market price data right now. Please contact your local mandi for current rates.",
531
+ "data": [],
532
+ "error": str(e)
533
+ }
534
 
535
+ # -------------------------
536
+ # Scheme eligibility (POST for Retell style, GET for easy testing)
537
+ # -------------------------
538
  @app.post("/api/scheme-eligibility")
539
  async def scheme_eligibility_endpoint(
540
  request: Request,
541
+ x_retell_signature: Optional[str] = Header(None, alias="X-Retell-Signature")
542
  ):
 
543
  request_body = await request.body()
544
+ # verify signature if header present
545
+ if x_retell_signature and not verify_retell_signature(request_body, x_retell_signature):
546
+ raise HTTPException(status_code=401, detail="Invalid Retell signature")
547
+
548
+ try:
549
+ payload = json.loads(request_body.decode('utf-8')) if request_body else {}
550
+ except Exception:
551
+ payload = {}
552
 
553
+ farmer_category = payload.get("args", {}).get("farmer_category", "") or payload.get("farmer_category", "")
554
+ land_size = payload.get("args", {}).get("land_size", 0) or payload.get("land_size", 0)
555
+ state = payload.get("args", {}).get("state", "") or payload.get("state", "")
556
+ crop_type = payload.get("args", {}).get("crop_type", "") or payload.get("crop_type", "")
 
557
 
558
  try:
559
  eligible_schemes = []
 
 
560
  if not csv_data['government_schemes'].empty:
561
+ # ensure land_size numeric
562
+ try:
563
+ land_size_f = float(land_size) if land_size not in [None, ""] else 0.0
564
+ except:
565
+ land_size_f = 0.0
566
+ eligible_schemes = get_schemes_from_csv(farmer_category or "", land_size_f, state or "", crop_type or "")
567
+
568
+ # Fallback defaults
569
  if not eligible_schemes:
570
+ try:
571
+ ls_f = float(land_size) if land_size not in [None, ""] else 0.0
572
+ except:
573
+ ls_f = 0.0
574
+ if ls_f > 0:
575
  eligible_schemes.append({
576
  "scheme": "PM-KISAN",
577
  "benefit": "₹6,000 per year in 3 installments",
578
  "description": "Direct income support to landholding farmer families.",
579
  "eligibility": "All landholding farmer families.",
580
+ "process": "Apply via pmkisan.gov.in or your nearest CSC",
581
  "contact": "https://pmkisan.gov.in/"
582
  })
 
 
583
  eligible_schemes.append({
584
  "scheme": "Pradhan Mantri Fasal Bima Yojana",
585
  "benefit": "Comprehensive crop insurance coverage",
 
588
  "process": "Contact your nearest bank, CSC or PMFBY portal",
589
  "contact": "https://pmfby.gov.in/"
590
  })
591
+ if state and state.strip().lower() == "punjab":
 
 
592
  eligible_schemes.append({
593
  "scheme": "Punjab Crop Diversification Scheme",
594
  "benefit": "₹17,500 per hectare for diversification",
 
596
  "contact": ""
597
  })
598
 
599
+ # Build voice-friendly text (limit first 3)
600
  if eligible_schemes:
601
  schemes_text = f"You are eligible for {len(eligible_schemes)} government schemes: "
602
+ for i, scheme in enumerate(eligible_schemes[:3]):
603
+ contact_info = f" Apply through {scheme.get('process','Contact local agriculture office')}"
604
  if scheme.get('contact'):
605
  contact_info += f" or contact {scheme.get('contact')}"
606
  schemes_text += f"{i+1}. {scheme.get('scheme','N/A')} - {scheme.get('benefit', scheme.get('description','N/A'))}.{contact_info}. "
 
607
  if len(eligible_schemes) > 3:
608
  schemes_text += f"And {len(eligible_schemes)-3} more schemes available."
609
  else:
 
616
 
617
  except Exception as e:
618
  return {
619
+ "result": "I'm having trouble accessing scheme information right now. Please contact your local agriculture officer.",
620
  "error": str(e)
621
  }
622
 
623
+ @app.get("/api/scheme-eligibility")
624
+ async def scheme_eligibility_get(
625
+ farmer_category: Optional[str] = Query("", alias="farmer_category"),
626
+ land_size: Optional[float] = Query(0.0, alias="land_size"),
627
+ state: Optional[str] = Query("", alias="state"),
628
+ crop_type: Optional[str] = Query("", alias="crop_type")
629
+ ):
630
+ try:
631
+ eligible_schemes = []
632
+ if not csv_data['government_schemes'].empty:
633
+ eligible_schemes = get_schemes_from_csv(farmer_category or "", float(land_size or 0.0), state or "", crop_type or "")
634
 
635
+ if not eligible_schemes:
636
+ if float(land_size or 0.0) > 0:
637
+ eligible_schemes.append({
638
+ "scheme": "PM-KISAN",
639
+ "benefit": "₹6,000 per year in 3 installments",
640
+ "description": "Direct income support to landholding farmer families.",
641
+ "eligibility": "All landholding farmer families.",
642
+ "process": "Apply via pmkisan.gov.in or your nearest CSC",
643
+ "contact": "https://pmkisan.gov.in/"
644
+ })
645
+ eligible_schemes.append({
646
+ "scheme": "Pradhan Mantri Fasal Bima Yojana",
647
+ "benefit": "Comprehensive crop insurance coverage",
648
+ "description": "Crop insurance against natural calamities, pests, and diseases.",
649
+ "eligibility": "All farmers in notified crops/areas",
650
+ "process": "Contact your nearest bank, CSC or PMFBY portal",
651
+ "contact": "https://pmfby.gov.in/"
652
+ })
653
+ if state and state.strip().lower() == "punjab":
654
+ eligible_schemes.append({
655
+ "scheme": "Punjab Crop Diversification Scheme",
656
+ "benefit": "₹17,500 per hectare for diversification",
657
+ "process": "Contact District Agriculture Officer",
658
+ "contact": ""
659
+ })
660
+
661
+ # Build text
662
+ if eligible_schemes:
663
+ schemes_text = f"You are eligible for {len(eligible_schemes)} government schemes: "
664
+ for i, scheme in enumerate(eligible_schemes[:3]):
665
+ contact_info = f" Apply through {scheme.get('process','Contact local agriculture office')}"
666
+ if scheme.get('contact'):
667
+ contact_info += f" or contact {scheme.get('contact')}"
668
+ schemes_text += f"{i+1}. {scheme.get('scheme','N/A')} - {scheme.get('benefit', scheme.get('description','N/A'))}.{contact_info}. "
669
+ if len(eligible_schemes) > 3:
670
+ schemes_text += f"And {len(eligible_schemes)-3} more schemes available."
671
+ else:
672
+ schemes_text = "I couldn't find specific schemes for your profile. Please contact your local agriculture department for personalized advice."
673
+
674
+ return {
675
+ "result": schemes_text,
676
+ "eligible_schemes": eligible_schemes
677
+ }
678
+
679
+ except Exception as e:
680
+ return {"result": "Error computing schemes", "error": str(e)}
681
+
682
+ # -------------------------
683
+ # Weather advisory (POST and GET)
684
+ # -------------------------
685
+ @app.post("/api/weather-advisory")
686
+ async def weather_advisory_post(request: Request):
687
+ body = await request.json() if (await request.body()) else {}
688
+ city = body.get("query", {}).get("location", "").strip() if body else ""
689
  temperature, humidity, description, pressure = get_weather(city)
690
  if temperature is None:
 
691
  temperature, humidity, description, pressure = 32.0, 60, "Not Available", 1012
692
  weather_condition = "NORMAL"
693
  else:
694
+ desc_lower = (description or "").lower()
695
  if "clear" in desc_lower:
696
  weather_condition = "SUNNY"
697
  elif "rain" in desc_lower:
 
702
  weather_condition = "NORMAL"
703
 
704
  result = (
705
+ f"Weather in {city or 'your location'}: {description}. "
706
  f"Temperature {temperature}°C, Humidity {humidity}%, Pressure {pressure} hPa. "
707
  f"Condition classified as {weather_condition}."
708
  )
 
720
  }
721
  }
722
 
723
+ @app.get("/api/weather-advisory")
724
+ async def weather_advisory_get(location: Optional[str] = Query("", alias="location")):
725
+ # delegate to same logic above
726
+ temperature, humidity, description, pressure = get_weather(location)
727
+ if temperature is None:
728
+ temperature, humidity, description, pressure = 32.0, 60, "Not Available", 1012
729
+ weather_condition = "NORMAL"
730
+ else:
731
+ desc_lower = (description or "").lower()
732
+ if "clear" in desc_lower:
733
+ weather_condition = "SUNNY"
734
+ elif "rain" in desc_lower:
735
+ weather_condition = "RAINY"
736
+ elif "wind" in desc_lower:
737
+ weather_condition = "WINDY"
738
+ else:
739
+ weather_condition = "NORMAL"
740
+
741
+ result = (
742
+ f"Weather in {location or 'your location'}: {description}. "
743
+ f"Temperature {temperature}°C, Humidity {humidity}%, Pressure {pressure} hPa. "
744
+ f"Condition classified as {weather_condition}."
745
+ )
746
+
747
+ return {
748
+ "success": True,
749
+ "result": result,
750
+ "data": {
751
+ "city": location,
752
+ "temperature": temperature,
753
+ "humidity": humidity,
754
+ "pressure": pressure,
755
+ "description": description,
756
+ "condition": weather_condition
757
+ }
758
+ }
759
+
760
+ # -------------------------
761
+ # Crop advisory (POST - Retell style; GET - query params)
762
+ # -------------------------
763
  @app.post("/api/crop-advisory")
764
  async def crop_advisory_endpoint(
765
  request: Request,
766
+ x_retell_signature: Optional[str] = Header(None, alias="X-Retell-Signature")
767
  ):
 
768
  request_body = await request.body()
769
+ if x_retell_signature and not verify_retell_signature(request_body, x_retell_signature):
770
+ raise HTTPException(status_code=401, detail="Invalid Retell signature")
771
+
772
+ try:
773
+ payload = json.loads(request_body.decode('utf-8')) if request_body else {}
774
+ except Exception:
775
+ payload = {}
776
 
777
+ crop_name = (payload.get("args", {}).get("crop_name", "") or payload.get("crop_name", "") or "").strip()
778
+ growth_stage = (payload.get("args", {}).get("growth_stage", "") or payload.get("growth_stage", "") or "").strip()
779
+ issue_type = (payload.get("args", {}).get("issue_type", "general") or payload.get("issue_type", "general") or "general").strip().lower()
780
+ state = (payload.get("args", {}).get("state", "") or payload.get("state", "") or "").strip()
781
 
782
  try:
783
  advisory = None
784
  contact_info = ""
785
 
786
+ # Search crop_advisory CSV robustly (support various column names)
787
+ df = csv_data.get('crop_advisory', pd.DataFrame())
788
+ if not df.empty:
789
+ # find likely columns
790
+ crop_col = find_column(df, ["crop", "Crop", "Crop Name", "crop_name"])
791
+ sowing_col = find_column(df, ["sowing_time", "Sowing_Time", "Sowing Time", "Sowing"])
792
+ fertilizer_col = find_column(df, ["fertilizer", "Fertilizer"])
793
+ season_col = find_column(df, ["season", "Season"])
794
+ issues_col = find_column(df, ["common_issues", "Common_Issues", "Common Issues", "Common_Issues"])
795
+ solution_col = find_column(df, ["solution", "Solution", "Solution"])
796
+
797
+ if crop_col and crop_name:
798
+ matches = df[df[crop_col].astype(str).str.contains(crop_name, case=False, na=False)]
799
+ elif crop_col:
800
+ matches = df.copy()
801
+ else:
802
+ matches = pd.DataFrame()
803
+
804
+ if not matches.empty:
805
+ crop_info = matches.iloc[0]
806
+ parts = []
807
+ if issue_type == "general":
808
+ if sowing_col and pd.notna(crop_info.get(sowing_col)):
809
+ parts.append(f"Sowing time: {crop_info[sowing_col]}")
810
+ if fertilizer_col and pd.notna(crop_info.get(fertilizer_col)):
811
+ parts.append(f"Recommended fertilizer: {crop_info[fertilizer_col]}")
812
+ if season_col and pd.notna(crop_info.get(season_col)):
813
+ parts.append(f"Best season: {crop_info[season_col]}")
814
+ if issues_col and solution_col and pd.notna(crop_info.get(issues_col)) and pd.notna(crop_info.get(solution_col)):
815
+ if issue_type in ['pest', 'disease'] or issue_type == 'general':
816
+ parts.append(f"For {crop_info[issues_col]}: {crop_info[solution_col]}")
817
+ if parts:
818
+ advisory = f"For {crop_name or crop_info.get(crop_col,'the crop')}: " + ". ".join(parts)
819
+
820
+ # contact info from contact_info CSV
821
+ df_contact = csv_data.get('contact_info', pd.DataFrame())
822
+ if not df_contact.empty and state:
823
+ state_col = find_column(df_contact, ["state", "State", "state_name"])
824
+ if state_col:
825
+ contact_matches = df_contact[df_contact[state_col].astype(str).str.contains(state, case=False, na=False)]
826
+ if not contact_matches.empty:
827
+ contact_match = contact_matches.iloc[0]
828
+ contact_parts = []
829
+ if 'agriculture_officer' in contact_match and pd.notna(contact_match.get('agriculture_officer')):
830
+ contact_parts.append(f"Agriculture Officer at {contact_match['agriculture_officer']}")
831
+ if 'kvk_contact' in contact_match and pd.notna(contact_match.get('kvk_contact')):
832
+ contact_parts.append(f"KVK at {contact_match['kvk_contact']}")
833
+ if 'kisan_call_center' in contact_match and pd.notna(contact_match.get('kisan_call_center')):
834
+ contact_parts.append(f"Kisan Call Center at {contact_match['kisan_call_center']}")
835
+ if contact_parts:
836
+ contact_info = f"For detailed advice in {state}, contact: " + " or ".join(contact_parts) + "."
837
+
838
+ # fallback advisory if none found
 
 
839
  if not advisory:
840
+ if crop_name and crop_name.lower() == "wheat" and issue_type == "pest":
841
+ advisory = "For wheat pest control: If you see aphids, spray Imidacloprid 200 SL at 0.3ml per liter of water. Spray during evening hours. Avoid over-irrigation."
842
+ elif crop_name and crop_name.lower() == "rice" and issue_type == "disease":
843
  advisory = "For rice disease management: If you see brown spots on leaves, it might be blast disease. Apply Tricyclazole 75% WP at 0.6g per liter. Ensure proper drainage."
844
  else:
845
+ advisory = f"For {crop_name or 'the crop'} at {growth_stage or 'current'} stage: Monitor crop regularly, maintain proper spacing, apply fertilizers as per soil test recommendations."
846
 
847
  if not contact_info:
848
+ contact_info = f"For detailed advice, contact your local Krishi Vigyan Kendra or Agriculture Officer in {state or 'your state'}. You can also call the Kisan Call Centre at 1800-1801-551."
849
 
850
  result_text = f"{advisory} {contact_info}"
851
 
 
861
  "error": str(e)
862
  }
863
 
864
+ @app.get("/api/crop-advisory")
865
+ async def crop_advisory_get(
866
+ crop_name: Optional[str] = Query("", alias="crop_name"),
867
+ growth_stage: Optional[str] = Query("", alias="growth_stage"),
868
+ issue_type: Optional[str] = Query("general", alias="issue_type"),
869
+ state: Optional[str] = Query("", alias="state")
870
+ ):
871
+ try:
872
+ crop_name = (crop_name or "").strip()
873
+ growth_stage = (growth_stage or "").strip()
874
+ issue_type = (issue_type or "general").strip().lower()
875
+ state = (state or "").strip()
876
 
877
+ advisory = None
878
+ contact_info = ""
 
 
 
 
 
 
879
 
880
+ df = csv_data.get('crop_advisory', pd.DataFrame())
881
+ if not df.empty:
882
+ crop_col = find_column(df, ["crop", "Crop", "Crop Name", "crop_name"])
883
+ sowing_col = find_column(df, ["sowing_time", "Sowing_Time", "Sowing Time", "Sowing"])
884
+ fertilizer_col = find_column(df, ["fertilizer", "Fertilizer"])
885
+ season_col = find_column(df, ["season", "Season"])
886
+ issues_col = find_column(df, ["common_issues", "Common_Issues", "Common Issues"])
887
+ solution_col = find_column(df, ["solution", "Solution", "Solution"])
888
+
889
+ if crop_col and crop_name:
890
+ matches = df[df[crop_col].astype(str).str.contains(crop_name, case=False, na=False)]
891
+ elif crop_col:
892
+ matches = df.copy()
893
+ else:
894
+ matches = pd.DataFrame()
895
+
896
+ if not matches.empty:
897
+ crop_info = matches.iloc[0]
898
+ parts = []
899
+ if issue_type == "general":
900
+ if sowing_col and pd.notna(crop_info.get(sowing_col)):
901
+ parts.append(f"Sowing time: {crop_info[sowing_col]}")
902
+ if fertilizer_col and pd.notna(crop_info.get(fertilizer_col)):
903
+ parts.append(f"Recommended fertilizer: {crop_info[fertilizer_col]}")
904
+ if season_col and pd.notna(crop_info.get(season_col)):
905
+ parts.append(f"Best season: {crop_info[season_col]}")
906
+ if issues_col and solution_col and pd.notna(crop_info.get(issues_col)) and pd.notna(crop_info.get(solution_col)):
907
+ if issue_type in ['pest', 'disease'] or issue_type == 'general':
908
+ parts.append(f"For {crop_info[issues_col]}: {crop_info[solution_col]}")
909
+ if parts:
910
+ advisory = f"For {crop_name or crop_info.get(crop_col,'the crop')}: " + ". ".join(parts)
911
+
912
+ # contact info
913
+ df_contact = csv_data.get('contact_info', pd.DataFrame())
914
+ if not df_contact.empty and state:
915
+ state_col = find_column(df_contact, ["state", "State", "state_name"])
916
+ if state_col:
917
+ contact_matches = df_contact[df_contact[state_col].astype(str).str.contains(state, case=False, na=False)]
918
+ if not contact_matches.empty:
919
+ contact_match = contact_matches.iloc[0]
920
+ contact_parts = []
921
+ if 'agriculture_officer' in contact_match and pd.notna(contact_match.get('agriculture_officer')):
922
+ contact_parts.append(f"Agriculture Officer at {contact_match['agriculture_officer']}")
923
+ if 'kvk_contact' in contact_match and pd.notna(contact_match.get('kvk_contact')):
924
+ contact_parts.append(f"KVK at {contact_match['kvk_contact']}")
925
+ if 'kisan_call_center' in contact_match and pd.notna(contact_match.get('kisan_call_center')):
926
+ contact_parts.append(f"Kisan Call Center at {contact_match['kisan_call_center']}")
927
+ if contact_parts:
928
+ contact_info = f"For detailed advice in {state}, contact: " + " or ".join(contact_parts) + "."
929
+
930
+ if not advisory:
931
+ if crop_name and crop_name.lower() == "wheat" and issue_type == "pest":
932
+ advisory = "For wheat pest control: If you see aphids, spray Imidacloprid 200 SL at 0.3ml per liter of water. Spray during evening hours. Avoid over-irrigation."
933
+ elif crop_name and crop_name.lower() == "rice" and issue_type == "disease":
934
+ advisory = "For rice disease management: If you see brown spots on leaves, it might be blast disease. Apply Tricyclazole 75% WP at 0.6g per liter. Ensure proper drainage."
935
+ else:
936
+ advisory = f"For {crop_name or 'the crop'} at {growth_stage or 'current'} stage: Monitor crop regularly, maintain proper spacing, apply fertilizers as per soil test recommendations."
937
+
938
+ if not contact_info:
939
+ contact_info = f"For detailed advice, contact your local Krishi Vigyan Kendra or Agriculture Officer in {state or 'your state'}. You can also call the Kisan Call Centre at 1800-1801-551."
940
 
941
+ result_text = f"{advisory} {contact_info}"
942
+ return {
943
+ "result": result_text,
944
+ "recommendations": advisory,
945
+ "contact_info": contact_info
946
+ }
947
+
948
+ except Exception as e:
949
+ return {
950
+ "result": "I couldn't provide specific advice right now. Please contact your local agriculture extension officer.",
951
+ "error": str(e)
952
+ }
953
+
954
+ # -------------------------
955
+ # Run server (for local dev)
956
+ # -------------------------
957
  if __name__ == "__main__":
958
  import uvicorn
959
+ uvicorn.run(app, host="0.0.0.0", port=int(os.getenv("PORT", 7860)))