Aniket2006 commited on
Commit
e2d9873
·
1 Parent(s): f895e11

Upgrade to Hybrid Architecture v3 (Fast Lane + Deep Dive)

Browse files
Files changed (4) hide show
  1. app.py +129 -415
  2. context_aggregator.py +107 -0
  3. prompts.py +183 -86
  4. reasoning_engine.py +109 -13
app.py CHANGED
@@ -1,13 +1,11 @@
1
  """
2
- AGROW Agricultural Chatbot Service - Comprehensive Context
3
- ============================================================
4
- AI-powered agricultural advisor with COMPLETE context from:
5
- - SAR analysis (crop health, stress scores, recommendations)
6
- - Sentinel-2 vegetation indices (all from vegetation_indices_summary)
7
- - Sentinel-2 stress detection (cluster-wise patterns)
8
- - Weather data (current + forecast)
9
- - Farmer profile from questionnaire
10
- - Field data from coordinates_quad
11
  """
12
 
13
  import os
@@ -27,7 +25,9 @@ import asyncio
27
  from groq import Groq
28
 
29
  from supabase_client import SupabaseClient
30
- from prompts import PERSONA_DEFINITIONS, EXPERIENCE_MAP, TECH_COMFORT_MAP, INNOVATION_MAP, FARMING_GOAL_MAP
 
 
31
 
32
  # ============================================================================
33
  # LOGGING
@@ -40,35 +40,61 @@ logging.basicConfig(
40
  logger = logging.getLogger("ChatbotService")
41
 
42
  print("=" * 50)
43
- print(f"===== Application Startup at {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} =====")
44
  print("=" * 50)
45
 
46
  # ============================================================================
47
- # API ENDPOINTS
48
- # ============================================================================
49
- SAR_API_URL = os.getenv("SAR_API_URL", "https://aniket2006-agrow-backend-v2.hf.space")
50
- SENTINEL2_API_URL = os.getenv("SENTINEL2_API_URL", "https://aniket2006-agrow-sentinel2.hf.space")
51
-
52
  # ============================================================================
53
- # GROQ SETUP (Centralized)
54
- # ============================================================================
55
- import sys
56
- sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)) + "/..")
57
- from groq_client import GROQ_API_KEYS, GROQ_MODEL, call_groq
58
 
59
- # Verify keys loaded
60
  logger.info(f"Loaded {len(GROQ_API_KEYS)} Groq API keys")
61
 
62
- # Supabase
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  supabase = SupabaseClient()
 
 
64
 
65
  # ============================================================================
66
  # FASTAPI
67
  # ============================================================================
68
  app = FastAPI(
69
  title="AGROW Chatbot Service",
70
- description="AI agricultural advisor with comprehensive satellite context",
71
- version="2.1.0"
72
  )
73
 
74
  app.add_middleware(
@@ -93,6 +119,7 @@ class ChatResponse(BaseModel):
93
  session_id: str
94
  message_id: str
95
  context_used: List[str]
 
96
  timestamp: str
97
 
98
  class SessionRequest(BaseModel):
@@ -105,40 +132,7 @@ class SessionResponse(BaseModel):
105
  created_at: str
106
 
107
  # ============================================================================
108
- # PERSONA DETECTION
109
- # ============================================================================
110
- def detect_persona(questionnaire: Dict) -> str:
111
- """Detect user persona from questionnaire answers."""
112
- if not questionnaire:
113
- return "experienced_farmer_traditional"
114
-
115
- experience = questionnaire.get("experience", "2 - 5 years")
116
- tech = questionnaire.get("tech_comfort", "I can use basic features")
117
- innovation = questionnaire.get("innovation", "I try new methods occasionally")
118
- goal = questionnaire.get("farming_goal", "Earn Income / Livelihood")
119
- role = questionnaire.get("role", "Farmer")
120
-
121
- years = EXPERIENCE_MAP.get(experience, 3)
122
- tech_level = TECH_COMFORT_MAP.get(tech, "moderate")
123
- innovation_level = INNOVATION_MAP.get(innovation, "moderate")
124
-
125
- if role == "Agricultural Officer":
126
- return "agricultural_officer"
127
- elif role in ["Agronomist", "Researcher"]:
128
- return "agronomist_researcher"
129
-
130
- if years < 3:
131
- return "new_farmer_tech_savvy" if tech_level == "advanced" else "new_farmer_basic_tech"
132
- elif FARMING_GOAL_MAP.get(goal) == "commercial":
133
- return "commercial_farmer"
134
- elif innovation_level == "innovative":
135
- return "experienced_farmer_innovative"
136
- else:
137
- return "experienced_farmer_traditional"
138
-
139
-
140
- # ============================================================================
141
- # FETCH DATA DIRECTLY FROM APIs
142
  # ============================================================================
143
  def fetch_field_data(user_id: str, field_id: Optional[str] = None) -> Optional[Dict]:
144
  """Fetch field data from Supabase coordinates_quad."""
@@ -152,16 +146,13 @@ def fetch_field_data(user_id: str, field_id: Optional[str] = None) -> Optional[D
152
  field = query.data[0]
153
  lats = [field.get(f"lat{i}", 0) for i in range(1, 5)]
154
  lons = [field.get(f"lon{i}", 0) for i in range(1, 5)]
155
- center_lat = sum(lats) / 4
156
- center_lon = sum(lons) / 4
157
-
158
  return {
159
  "id": field.get("id"),
160
  "name": field.get("name", "My Field"),
161
  "crop_type": field.get("crop_type", "Wheat"),
162
  "area_acres": field.get("area_acres", 1.0),
163
- "center_lat": center_lat,
164
- "center_lon": center_lon,
165
  "bbox": [min(lons), min(lats), max(lons), max(lats)]
166
  }
167
  except Exception as e:
@@ -179,370 +170,88 @@ def fetch_user_profile(user_id: str) -> Dict:
179
  if query.data and len(query.data) > 0:
180
  profile = query.data[0]
181
  return {
182
- "name": profile.get("full_name", ""),
183
- "location": profile.get("address", ""),
184
- "questionnaire": profile.get("questionnaire_data", {}) or {}
185
  }
186
  except Exception as e:
187
  logger.error(f"Error fetching user profile: {e}")
188
- return {"name": "", "location": "", "questionnaire": {}}
189
-
190
-
191
- def fetch_sar_data(bbox: List, crop_type: str) -> Optional[Dict]:
192
- """Fetch SAR data directly from API."""
193
- try:
194
- response = requests.post(
195
- f"{SAR_API_URL}/analyze",
196
- json={
197
- "coordinates": bbox,
198
- "date": datetime.now().strftime("%Y-%m-%d"),
199
- "crop_type": crop_type,
200
- "farmer_context": {}
201
- },
202
- timeout=60
203
- )
204
- if response.status_code == 200:
205
- data = response.json()
206
- logger.info(f"SAR raw data keys: {list(data.keys())}")
207
- return data
208
- except Exception as e:
209
- logger.error(f"SAR fetch error: {e}")
210
- return None
211
-
212
-
213
- def fetch_sentinel2_data(center_lat: float, center_lon: float, crop_type: str, field_hectares: float) -> Optional[Dict]:
214
- """Fetch Sentinel-2 data directly from API."""
215
- try:
216
- response = requests.post(
217
- f"{SENTINEL2_API_URL}/analyze",
218
- json={
219
- "center_lat": center_lat,
220
- "center_lon": center_lon,
221
- "crop_type": crop_type,
222
- "analysis_date": datetime.now().strftime("%Y-%m-%d"),
223
- "field_size_hectares": field_hectares,
224
- "farmer_context": {},
225
- "skip_llm": True
226
- },
227
- timeout=90
228
- )
229
- if response.status_code == 200:
230
- data = response.json()
231
- logger.info(f"Sentinel-2 raw data keys: {list(data.keys())}")
232
- return data
233
- except Exception as e:
234
- logger.error(f"Sentinel-2 fetch error: {e}")
235
- return None
236
-
237
-
238
- def fetch_weather(lat: float, lon: float) -> Optional[Dict]:
239
- """Fetch weather from Open-Meteo."""
240
- try:
241
- response = requests.get(
242
- "https://api.open-meteo.com/v1/forecast",
243
- params={
244
- "latitude": lat,
245
- "longitude": lon,
246
- "current": "temperature_2m,relative_humidity_2m,precipitation,wind_speed_10m",
247
- "daily": "temperature_2m_max,temperature_2m_min,precipitation_sum,precipitation_probability_max",
248
- "timezone": "auto",
249
- "forecast_days": 7
250
- },
251
- timeout=15
252
- )
253
- if response.status_code == 200:
254
- data = response.json()
255
- current = data.get("current", {})
256
- daily = data.get("daily", {})
257
- return {
258
- "current_temp": current.get("temperature_2m"),
259
- "current_humidity": current.get("relative_humidity_2m"),
260
- "current_precipitation": current.get("precipitation"),
261
- "forecast_max_temps": daily.get("temperature_2m_max", [])[:3],
262
- "forecast_rain_prob": daily.get("precipitation_probability_max", [])[:3],
263
- "forecast_precipitation": daily.get("precipitation_sum", [])[:3]
264
- }
265
- except Exception as e:
266
- logger.error(f"Weather fetch error: {e}")
267
- return None
268
 
269
 
270
  # ============================================================================
271
- # BUILD COMPREHENSIVE CONTEXT - DIRECT EXTRACTION
272
  # ============================================================================
273
- def build_comprehensive_context(user_id: str, field_id: Optional[str] = None) -> Dict:
274
- """Build comprehensive context with DIRECT API data extraction."""
275
- context = {
276
- "fetch_timestamp": datetime.now().isoformat(),
277
- "data_sources": []
278
- }
279
 
280
- # 1. Fetch field data
281
  field = fetch_field_data(user_id, field_id)
282
  if not field:
283
  logger.warning("No field data found")
284
  return context
285
 
286
- context["field"] = field
287
- context["data_sources"].append("coordinates_quad")
288
- logger.info(f"✓ Field: {field['name']} ({field['crop_type']})")
 
 
289
 
290
- # 2. Fetch user profile
291
  profile = fetch_user_profile(user_id)
292
- questionnaire = profile.get("questionnaire", {})
293
- persona = detect_persona(questionnaire)
294
- context["farmer"] = {
295
- "name": profile.get("name", ""),
296
- "persona": persona,
297
- "experience": questionnaire.get("experience", ""),
298
- "tech_comfort": questionnaire.get("tech_comfort", ""),
299
- "farming_goal": questionnaire.get("farming_goal", "")
300
- }
301
- context["data_sources"].append("user_profiles")
302
- logger.info(f"✓ Farmer persona: {persona}")
303
 
304
- # 3. Fetch SAR data
305
- sar_data = fetch_sar_data(field["bbox"], field["crop_type"])
306
- if sar_data:
307
- context["sar"] = {
308
- "crop_health": sar_data.get("crop_health", "Unknown"),
309
- "confidence": sar_data.get("confidence_score", 0),
310
- "stress_score": sar_data.get("average_stress_score", 0),
311
- "summary": sar_data.get("summary", ""),
312
- "recommendations": sar_data.get("recommendations", []),
313
- "health_summary": sar_data.get("health_summary", {}),
314
- "stressed_patches": sar_data.get("stressed_patches", [])
315
- }
316
- context["data_sources"].append("sar_api")
317
- logger.info(f"✓ SAR: health={sar_data.get('crop_health')}, stress={sar_data.get('average_stress_score')}")
318
 
319
- # 4. Fetch Sentinel-2 data - EXTRACT DIRECTLY
320
- s2_data = fetch_sentinel2_data(field["center_lat"], field["center_lon"], field["crop_type"], field["area_acres"] * 0.404686)
321
- if s2_data:
322
- # Extract vegetation_indices_summary -> contains 'indices' dict
323
- vi_summary = s2_data.get("vegetation_indices_summary", {})
324
- indices = vi_summary.get("indices", {})
325
-
326
- # Extract all vegetation indices
327
- vi_extracted = {}
328
- for name, stats in indices.items():
329
- if isinstance(stats, dict):
330
- latest = stats.get("latest", {})
331
- vi_extracted[name.upper()] = {
332
- "mean": latest.get("mean"),
333
- "min": stats.get("min_in_field"),
334
- "max": stats.get("max_in_field"),
335
- "change": stats.get("change")
336
- }
337
-
338
- if vi_extracted:
339
- context["vegetation_indices"] = vi_extracted
340
- logger.info(f"✓ Vegetation indices: {list(vi_extracted.keys())}")
341
-
342
- # Extract stress_detection - cluster-wise patterns
343
- stress_det = s2_data.get("stress_detection", {})
344
- if stress_det:
345
- context["stress_detection"] = {
346
- "overall_stress": stress_det.get("overall_stress_score"),
347
- "stress_category": stress_det.get("stress_category"),
348
- "cluster_summary": stress_det.get("cluster_summary", []),
349
- "high_stress_zones": stress_det.get("high_stress_zones", []),
350
- "recommendations": stress_det.get("recommendations", [])
351
- }
352
- logger.info(f"✓ Stress detection: {stress_det.get('stress_category')}")
353
-
354
- context["data_sources"].append("sentinel2_api")
355
 
356
- # 5. Fetch weather
357
- weather = fetch_weather(field["center_lat"], field["center_lon"])
358
- if weather:
359
- context["weather"] = weather
360
- context["data_sources"].append("weather_api")
361
- logger.info(f"✓ Weather: {weather.get('current_temp')}°C")
 
 
 
 
 
362
 
363
- logger.info(f"Context complete: {context['data_sources']}")
364
  return context
365
 
366
 
367
  # ============================================================================
368
- # BUILD LLM PROMPT - COMPREHENSIVE
369
  # ============================================================================
370
- def build_llm_prompt(query: str, context: Dict, history: List[Dict] = None) -> str:
371
- """Build comprehensive prompt for LLM with ALL context data."""
372
-
373
- # Get persona
374
- farmer = context.get("farmer", {})
375
- persona_key = farmer.get("persona", "experienced_farmer_traditional")
376
- persona = PERSONA_DEFINITIONS.get(persona_key, PERSONA_DEFINITIONS["experienced_farmer_traditional"])
377
-
378
- # Build context sections
379
- sections = []
380
-
381
- # 1. Field Information
382
- field = context.get("field", {})
383
- if field:
384
- sections.append(f"""## Field Information
385
- - Name: {field.get('name', 'Unknown')}
386
- - Crop: {field.get('crop_type', 'Unknown')}
387
- - Area: {field.get('area_acres', 0):.2f} acres
388
- - Location: {field.get('center_lat', 0):.4f}°N, {field.get('center_lon', 0):.4f}°E""")
389
-
390
- # 2. Vegetation Indices (ALL extracted)
391
- vi = context.get("vegetation_indices", {})
392
- if vi:
393
- vi_lines = ["## Vegetation Indices (Sentinel-2 Analysis)"]
394
- for name, data in vi.items():
395
- if isinstance(data, dict) and data.get("mean") is not None:
396
- mean = data.get("mean", 0)
397
- change = data.get("change", 0)
398
- change_str = f" (Δ{change:+.4f})" if isinstance(change, (int, float)) else ""
399
- vi_lines.append(f"- {name}: {mean:.4f}{change_str}")
400
- if len(vi_lines) > 1:
401
- sections.append("\n".join(vi_lines))
402
-
403
- # 3. SAR Analysis
404
- sar = context.get("sar", {})
405
- if sar:
406
- stress = sar.get("stress_score", 0)
407
- stress_str = f"{stress:.2f}" if isinstance(stress, (int, float)) else str(stress)
408
- recommendations = sar.get("recommendations", [])
409
- rec_list = "\n".join([f" • {r}" for r in recommendations[:3]]) if recommendations else " • No specific recommendations"
410
 
411
- sections.append(f"""## SAR Crop Health Analysis
412
- - Overall Health: {sar.get('crop_health', 'Unknown')}
413
- - Stress Score: {stress_str}
414
- - Confidence: {sar.get('confidence', 0)}%
415
- - Summary: {sar.get('summary', 'N/A')[:300]}
416
- - Recommendations:
417
- {rec_list}""")
418
-
419
- # 4. Stress Detection (Cluster-wise)
420
- stress = context.get("stress_detection", {})
421
- if stress:
422
- clusters = stress.get("cluster_summary", [])
423
- cluster_lines = []
424
- for c in clusters[:5]:
425
- if isinstance(c, dict):
426
- cluster_lines.append(f" • Cluster {c.get('id', '?')}: {c.get('stress_level', 'Unknown')} stress, {c.get('area_percent', 0):.1f}% of field")
427
 
428
- high_stress_zones = stress.get("high_stress_zones", [])
429
- zone_lines = []
430
- for z in high_stress_zones[:3]:
431
- if isinstance(z, dict):
432
- zone_lines.append(f" • {z.get('location', 'Unknown')}: {z.get('severity', 'Unknown')}")
433
 
434
- sections.append(f"""## Stress Detection (Cluster Analysis)
435
- - Overall Stress: {stress.get('overall_stress', 'N/A')}
436
- - Category: {stress.get('stress_category', 'N/A')}
437
- - Clusters:
438
- {chr(10).join(cluster_lines) if cluster_lines else ' • No cluster data'}
439
- - High Stress Zones:
440
- {chr(10).join(zone_lines) if zone_lines else ' • No high stress zones detected'}""")
441
-
442
- # 5. Weather
443
- weather = context.get("weather", {})
444
- if weather:
445
- sections.append(f"""## Weather Data
446
- - Current Temperature: {weather.get('current_temp', 'N/A')}°C
447
- - Humidity: {weather.get('current_humidity', 'N/A')}%
448
- - Current Precipitation: {weather.get('current_precipitation', 0)} mm
449
- - 3-Day Forecast Max Temps: {weather.get('forecast_max_temps', [])}
450
- - Rain Probability (next 3 days): {weather.get('forecast_rain_prob', [])}%""")
451
-
452
- # 6. Farmer Profile
453
- sections.append(f"""## Farmer Profile
454
- - Experience: {farmer.get('experience', 'Unknown')}
455
- - Farming Goal: {farmer.get('farming_goal', 'Unknown')}
456
- - Tech Comfort: {farmer.get('tech_comfort', 'Unknown')}""")
457
-
458
- # Combine context
459
- context_str = "\n\n".join(sections)
460
-
461
- # Log what we're passing
462
- logger.info(f"Prompt sections: {len(sections)}")
463
- for i, s in enumerate(sections[:3]):
464
- logger.info(f"Section[{i}]: {s[:100]}...")
465
-
466
- prompt = f"""You are AGROW AI, an expert agricultural advisor analyzing REAL satellite data for an Indian farmer's field.
467
-
468
- # CRITICAL: USE THE ACTUAL DATA BELOW
469
- You MUST analyze and cite the specific values provided. DO NOT give generic advice.
470
- Reference exact numbers like "Your NDVI of 0.65..." or "The 0.10 stress score indicates..."
471
-
472
- # FARMER CONTEXT
473
- {persona.get('description', 'Experienced farmer')}
474
- Style: {persona.get('style', 'Practical')} | Tone: {persona.get('tone', 'Friendly')}
475
-
476
- # SATELLITE ANALYSIS DATA (TODAY'S READINGS)
477
- {context_str}
478
-
479
- # USER'S QUESTION
480
- {query}
481
-
482
- # RESPONSE REQUIREMENTS
483
- 1. START with the field name and crop type
484
- 2. CITE at least 3 specific data values from above
485
- 3. If stress detected, address it with locations
486
- 4. Give 2-3 actionable recommendations based on the data
487
- 5. Keep response under 250 words
488
- 6. Use simple farmer-friendly language
489
-
490
- Your analysis:"""
491
-
492
- logger.info(f"Total prompt length: {len(prompt)} chars")
493
- return prompt
494
-
495
-
496
- # ============================================================================
497
- # GENERATE RESPONSE
498
- # ============================================================================
499
- def generate_response(user_message: str, history: List[Dict], context: Dict) -> tuple[str, List[str]]:
500
- """Generate AI response using comprehensive context with API Key Rotation."""
501
- context = context or {}
502
- history = history or []
503
- context_used = context.get("data_sources", [])
504
-
505
- prompt = build_llm_prompt(user_message, context, history)
506
- last_error = None
507
-
508
- # Try keys sequentially with fallback on failure
509
- for i, api_key in enumerate(GROQ_API_KEYS):
510
- try:
511
- logger.info(f"[Chatbot] Trying Groq API key {i+1}/{len(GROQ_API_KEYS)}")
512
- client = Groq(api_key=api_key)
513
-
514
- chat_completion = client.chat.completions.create(
515
- messages=[
516
- {
517
- "role": "system",
518
- "content": "You are AGROW AI, an expert agricultural advisor. Provide helpful, data-driven advice."
519
- },
520
- {
521
- "role": "user",
522
- "content": prompt
523
- }
524
- ],
525
- model=GROQ_MODEL,
526
- temperature=0.7,
527
- max_tokens=4096,
528
- )
529
-
530
- return chat_completion.choices[0].message.content, context_used
531
-
532
- except Exception as e:
533
- last_error = str(e)
534
- logger.warning(f"[Chatbot] Key {i+1} failed: {e}")
535
- # If it's not a rate limit issue, maybe we shouldn't retry?
536
- # But for robustness, we'll assume any error warrants trying another key.
537
- continue
538
-
539
- # All keys failed
540
- logger.error(f"[Chatbot] All {len(GROQ_API_KEYS)} keys failed. Last error: {last_error}")
541
- traceback.print_exc()
542
- fallback_msg = "I apologize, but I'm currently experiencing high traffic. Please try again in a moment."
543
- if context.get('weather'): # Simple fallback if we have context
544
- fallback_msg += f" (Weather: {context['weather'].get('current_temp')}°C)"
545
- return fallback_msg, []
546
 
547
 
548
  # ============================================================================
@@ -552,18 +261,19 @@ def generate_response(user_message: str, history: List[Dict], context: Dict) ->
552
  async def root():
553
  return {
554
  "service": "AGROW Chatbot Service",
555
- "version": "2.1.0",
556
- "features": ["comprehensive_context", "cluster_stress", "persona_based"]
 
557
  }
558
 
559
  @app.get("/health")
560
  async def health():
561
- return {"status": "healthy", "groq_configured": len(GROQ_API_KEYS) > 0}
562
 
563
 
564
  @app.post("/session/new", response_model=SessionResponse)
565
  async def create_session(request: SessionRequest):
566
- logger.info(f"Creating new session for user: {request.user_id}")
567
  try:
568
  session = supabase.create_session(
569
  user_id=request.user_id,
@@ -581,7 +291,7 @@ async def create_session(request: SessionRequest):
581
 
582
  @app.post("/chat", response_model=ChatResponse)
583
  async def chat(request: ChatRequest):
584
- logger.info(f"Chat request - Session: {request.session_id}")
585
 
586
  try:
587
  history = supabase.get_messages(request.session_id)
@@ -592,12 +302,14 @@ async def chat(request: ChatRequest):
592
  content=request.message
593
  )
594
 
595
- # Build comprehensive context
596
  context = {}
597
  if request.user_id:
598
- context = build_comprehensive_context(request.user_id, request.field_id)
599
 
600
- response_text, context_used = generate_response(request.message, history, context)
 
 
601
 
602
  assistant_msg_id = supabase.add_message(
603
  session_id=request.session_id,
@@ -613,6 +325,7 @@ async def chat(request: ChatRequest):
613
  session_id=request.session_id,
614
  message_id=assistant_msg_id,
615
  context_used=context_used,
 
616
  timestamp=datetime.now().isoformat()
617
  )
618
 
@@ -635,12 +348,13 @@ async def chat_stream(request: ChatRequest):
635
  content=request.message
636
  )
637
 
638
- # Build comprehensive context
639
  context = {}
640
  if request.user_id:
641
- context = build_comprehensive_context(request.user_id, request.field_id)
642
 
643
- response_text, context_used = generate_response(request.message, history, context)
 
 
644
 
645
  assistant_msg_id = supabase.add_message(
646
  session_id=request.session_id,
@@ -652,7 +366,7 @@ async def chat_stream(request: ChatRequest):
652
  supabase.update_session_timestamp(request.session_id)
653
 
654
  async def stream_response():
655
- yield f"data: {json.dumps({'type': 'metadata', 'session_id': request.session_id, 'message_id': assistant_msg_id, 'context_sources': context_used})}\n\n"
656
 
657
  for i in range(0, len(response_text), 15):
658
  yield f"data: {json.dumps({'type': 'chunk', 'text': response_text[i:i+15]})}\n\n"
@@ -670,7 +384,7 @@ async def chat_stream(request: ChatRequest):
670
  @app.get("/context/{user_id}")
671
  async def get_context(user_id: str, field_id: Optional[str] = None):
672
  """Debug endpoint - returns full context JSON."""
673
- return build_comprehensive_context(user_id, field_id)
674
 
675
 
676
  @app.get("/session/{session_id}/history")
@@ -702,5 +416,5 @@ async def delete_session(session_id: str):
702
 
703
  if __name__ == "__main__":
704
  import uvicorn
705
- logger.info("Starting AGROW Chatbot Service v2.1")
706
  uvicorn.run(app, host="0.0.0.0", port=7860)
 
1
  """
2
+ AGROW Agricultural Chatbot Service - Hybrid Architecture v3
3
+ =============================================================
4
+ AI-powered agricultural advisor with:
5
+ - Hybrid Routing (Fast Lane vs Deep Dive)
6
+ - Fast Lane: 1-call for simple queries (<2s latency)
7
+ - Deep Dive: 3-call for complex diagnosis (Hypothesis → Adversary → Judge)
8
+ - Comprehensive satellite context from SAR, Sentinel-2, Weather
 
 
9
  """
10
 
11
  import os
 
25
  from groq import Groq
26
 
27
  from supabase_client import SupabaseClient
28
+ from reasoning_engine import ReasoningEngine
29
+ from context_aggregator import ContextAggregator
30
+ from prompts import create_user_persona, PERSONA_DEFINITIONS
31
 
32
  # ============================================================================
33
  # LOGGING
 
40
  logger = logging.getLogger("ChatbotService")
41
 
42
  print("=" * 50)
43
+ print(f"===== AGROW Chatbot v3.0 (Hybrid) - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} =====")
44
  print("=" * 50)
45
 
46
  # ============================================================================
47
+ # GROQ SETUP
 
 
 
 
48
  # ============================================================================
49
+ from groq_client import GROQ_API_KEYS, GROQ_MODEL
 
 
 
 
50
 
 
51
  logger.info(f"Loaded {len(GROQ_API_KEYS)} Groq API keys")
52
 
53
+ # Global key index for round-robin
54
+ current_key_idx = 0
55
+
56
+ def get_llm_caller():
57
+ """Create LLM caller function with key rotation."""
58
+ global current_key_idx
59
+
60
+ def call_llm(prompt: str) -> str:
61
+ global current_key_idx
62
+ last_error = None
63
+
64
+ for attempt in range(len(GROQ_API_KEYS)):
65
+ key_idx = (current_key_idx + attempt) % len(GROQ_API_KEYS)
66
+ try:
67
+ client = Groq(api_key=GROQ_API_KEYS[key_idx])
68
+ response = client.chat.completions.create(
69
+ messages=[{"role": "user", "content": prompt}],
70
+ model=GROQ_MODEL,
71
+ temperature=0.7,
72
+ max_tokens=4096,
73
+ )
74
+ # Rotate to next key for next call
75
+ current_key_idx = (key_idx + 1) % len(GROQ_API_KEYS)
76
+ return response.choices[0].message.content
77
+ except Exception as e:
78
+ last_error = e
79
+ logger.warning(f"Key {key_idx+1} failed: {str(e)[:50]}")
80
+ continue
81
+
82
+ raise Exception(f"All {len(GROQ_API_KEYS)} keys failed. Last: {last_error}")
83
+
84
+ return call_llm
85
+
86
+ # Initialize components
87
  supabase = SupabaseClient()
88
+ aggregator = ContextAggregator()
89
+ reasoning_engine = ReasoningEngine(llm_caller=get_llm_caller())
90
 
91
  # ============================================================================
92
  # FASTAPI
93
  # ============================================================================
94
  app = FastAPI(
95
  title="AGROW Chatbot Service",
96
+ description="AI agricultural advisor with Hybrid Reasoning (Fast Lane + Deep Dive)",
97
+ version="3.0.0"
98
  )
99
 
100
  app.add_middleware(
 
119
  session_id: str
120
  message_id: str
121
  context_used: List[str]
122
+ routing_mode: str # NEW: "FAST_LANE" or "DEEP_DIVE"
123
  timestamp: str
124
 
125
  class SessionRequest(BaseModel):
 
132
  created_at: str
133
 
134
  # ============================================================================
135
+ # FETCH FIELD & PROFILE DATA FROM SUPABASE
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
  # ============================================================================
137
  def fetch_field_data(user_id: str, field_id: Optional[str] = None) -> Optional[Dict]:
138
  """Fetch field data from Supabase coordinates_quad."""
 
146
  field = query.data[0]
147
  lats = [field.get(f"lat{i}", 0) for i in range(1, 5)]
148
  lons = [field.get(f"lon{i}", 0) for i in range(1, 5)]
 
 
 
149
  return {
150
  "id": field.get("id"),
151
  "name": field.get("name", "My Field"),
152
  "crop_type": field.get("crop_type", "Wheat"),
153
  "area_acres": field.get("area_acres", 1.0),
154
+ "center_lat": sum(lats) / 4,
155
+ "center_lon": sum(lons) / 4,
156
  "bbox": [min(lons), min(lats), max(lons), max(lats)]
157
  }
158
  except Exception as e:
 
170
  if query.data and len(query.data) > 0:
171
  profile = query.data[0]
172
  return {
173
+ "full_name": profile.get("full_name", ""),
174
+ "address": profile.get("address", ""),
175
+ "questionnaire_data": profile.get("questionnaire_data", {}) or {}
176
  }
177
  except Exception as e:
178
  logger.error(f"Error fetching user profile: {e}")
179
+ return {"full_name": "", "address": "", "questionnaire_data": {}}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
 
181
 
182
  # ============================================================================
183
+ # BUILD CONTEXT FOR REASONING ENGINE
184
  # ============================================================================
185
+ def build_context_for_reasoning(user_id: str, field_id: Optional[str] = None) -> Dict:
186
+ """Build context dict for reasoning engine using Supabase + APIs."""
187
+ context = {"fetch_timestamp": datetime.now().isoformat()}
 
 
 
188
 
189
+ # 1. Field data
190
  field = fetch_field_data(user_id, field_id)
191
  if not field:
192
  logger.warning("No field data found")
193
  return context
194
 
195
+ context["field_info"] = {
196
+ "name": field["name"],
197
+ "crop_type": field["crop_type"],
198
+ "area_acres": field["area_acres"]
199
+ }
200
 
201
+ # 2. Profile + Persona
202
  profile = fetch_user_profile(user_id)
203
+ persona = create_user_persona(profile)
204
+ context["persona"] = persona
 
 
 
 
 
 
 
 
 
205
 
206
+ # 3. Fetch satellite data via aggregator
207
+ coordinates = {
208
+ "center_lat": field["center_lat"],
209
+ "center_lon": field["center_lon"],
210
+ "bbox": field["bbox"]
211
+ }
 
 
 
 
 
 
 
 
212
 
213
+ farmer_context = {
214
+ "profile": persona,
215
+ "actions": {}
216
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
217
 
218
+ try:
219
+ satellite_context = aggregator.fetch_full_context(
220
+ coordinates=coordinates,
221
+ crop_type=field["crop_type"],
222
+ area_acres=field["area_acres"],
223
+ farmer_context=farmer_context
224
+ )
225
+ context.update(satellite_context)
226
+ logger.info(f"Context built with keys: {list(context.keys())}")
227
+ except Exception as e:
228
+ logger.error(f"Error fetching satellite context: {e}")
229
 
 
230
  return context
231
 
232
 
233
  # ============================================================================
234
+ # GENERATE RESPONSE USING HYBRID REASONING
235
  # ============================================================================
236
+ def generate_response(user_message: str, history: List[Dict], context: Dict) -> tuple[str, List[str], str]:
237
+ """Generate AI response using Hybrid Reasoning Engine."""
238
+ try:
239
+ response_text, trace = reasoning_engine.process_query(
240
+ query=user_message,
241
+ context=context
242
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
243
 
244
+ routing_mode = trace.get("routing_mode", "UNKNOWN")
245
+ context_used = list(trace.get("context_priority_used", {}).get("priority_1", {}).keys())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
246
 
247
+ logger.info(f"[Hybrid] Mode: {routing_mode}, Diagnosis: {trace.get('stages', {}).get('confirmation', {}).get('final', 'N/A')[:50]}")
 
 
 
 
248
 
249
+ return response_text, context_used, routing_mode
250
+
251
+ except Exception as e:
252
+ logger.error(f"Reasoning error: {e}")
253
+ traceback.print_exc()
254
+ return "I apologize, but I encountered an error. Please try again.", [], "ERROR"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
255
 
256
 
257
  # ============================================================================
 
261
  async def root():
262
  return {
263
  "service": "AGROW Chatbot Service",
264
+ "version": "3.0.0",
265
+ "architecture": "Hybrid (Fast Lane + Deep Dive)",
266
+ "features": ["hybrid_routing", "3_stage_reasoning", "persona_based"]
267
  }
268
 
269
  @app.get("/health")
270
  async def health():
271
+ return {"status": "healthy", "groq_keys": len(GROQ_API_KEYS), "version": "3.0.0"}
272
 
273
 
274
  @app.post("/session/new", response_model=SessionResponse)
275
  async def create_session(request: SessionRequest):
276
+ logger.info(f"Creating session for user: {request.user_id}")
277
  try:
278
  session = supabase.create_session(
279
  user_id=request.user_id,
 
291
 
292
  @app.post("/chat", response_model=ChatResponse)
293
  async def chat(request: ChatRequest):
294
+ logger.info(f"Chat request - Session: {request.session_id}, Mode: Hybrid")
295
 
296
  try:
297
  history = supabase.get_messages(request.session_id)
 
302
  content=request.message
303
  )
304
 
305
+ # Build context and generate response
306
  context = {}
307
  if request.user_id:
308
+ context = build_context_for_reasoning(request.user_id, request.field_id)
309
 
310
+ response_text, context_used, routing_mode = generate_response(
311
+ request.message, history, context
312
+ )
313
 
314
  assistant_msg_id = supabase.add_message(
315
  session_id=request.session_id,
 
325
  session_id=request.session_id,
326
  message_id=assistant_msg_id,
327
  context_used=context_used,
328
+ routing_mode=routing_mode,
329
  timestamp=datetime.now().isoformat()
330
  )
331
 
 
348
  content=request.message
349
  )
350
 
 
351
  context = {}
352
  if request.user_id:
353
+ context = build_context_for_reasoning(request.user_id, request.field_id)
354
 
355
+ response_text, context_used, routing_mode = generate_response(
356
+ request.message, history, context
357
+ )
358
 
359
  assistant_msg_id = supabase.add_message(
360
  session_id=request.session_id,
 
366
  supabase.update_session_timestamp(request.session_id)
367
 
368
  async def stream_response():
369
+ yield f"data: {json.dumps({'type': 'metadata', 'session_id': request.session_id, 'message_id': assistant_msg_id, 'routing_mode': routing_mode})}\n\n"
370
 
371
  for i in range(0, len(response_text), 15):
372
  yield f"data: {json.dumps({'type': 'chunk', 'text': response_text[i:i+15]})}\n\n"
 
384
  @app.get("/context/{user_id}")
385
  async def get_context(user_id: str, field_id: Optional[str] = None):
386
  """Debug endpoint - returns full context JSON."""
387
+ return build_context_for_reasoning(user_id, field_id)
388
 
389
 
390
  @app.get("/session/{session_id}/history")
 
416
 
417
  if __name__ == "__main__":
418
  import uvicorn
419
+ logger.info("Starting AGROW Chatbot Service v3.0 (Hybrid Architecture)")
420
  uvicorn.run(app, host="0.0.0.0", port=7860)
context_aggregator.py CHANGED
@@ -778,6 +778,113 @@ class ContextAggregator:
778
  }
779
 
780
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
781
  # =============================================================================
782
  # QUICK FUNCTIONS
783
  # =============================================================================
 
778
  }
779
 
780
 
781
+ def build_ultra_compact_context(self, context: Dict) -> str:
782
+ """
783
+ Build ULTRA-COMPACT context for Fast Lane (1-Call).
784
+
785
+ Strictly filters for:
786
+ 1. Primary Health Indices (NDVI, NDRE) + Interpretation
787
+ 2. Soil Moisture (SMI) + Interpretation
788
+ 3. Weather Summary + Alerts
789
+ 4. Interpretation Strings (Crucial for 1-shot)
790
+
791
+ Excludes:
792
+ - SAR data (Too verbose)
793
+ - Historical trends (Unless critical)
794
+ - Raw bands
795
+ - Patch lists (Summary only)
796
+ """
797
+ lines = []
798
+
799
+ # 1. Primary Indicators (Health)
800
+ veg = context.get("vegetation_indices", {})
801
+ health_parts = []
802
+ for k in ["NDVI", "NDRE", "EVI"]:
803
+ val = veg.get(k)
804
+ if val and isinstance(val, dict):
805
+ curr = val.get("current")
806
+ interp = val.get("interpretation", "")
807
+ if curr is not None:
808
+ health_parts.append(f"{k}:{curr:.2f}({interp})")
809
+
810
+ if health_parts:
811
+ lines.append(f"[HEALTH_SIGNALS] " + " | ".join(health_parts))
812
+
813
+ # 2. Secondary Indicators (Water/Stress)
814
+ water_parts = []
815
+ for k in ["SMI", "NDWI"]:
816
+ val = veg.get(k)
817
+ if val and isinstance(val, dict):
818
+ curr = val.get("current")
819
+ interp = val.get("interpretation", "")
820
+ if curr is not None:
821
+ water_parts.append(f"{k}:{curr:.2f}({interp})")
822
+
823
+ if water_parts:
824
+ lines.append(f"[WATER_SIGNALS] " + " | ".join(water_parts))
825
+
826
+ # 3. Weather Snapshot (Current + Alert)
827
+ weather = context.get("weather", {})
828
+ if weather:
829
+ curr = weather.get("current", {})
830
+ lines.append(f"[WEATHER] {curr.get('temp', '?')}°C, Rain: {curr.get('precip', '?')}mm")
831
+
832
+ # Critical Alerts Only
833
+ alerts = []
834
+ stress = weather.get("stress_indicators", {})
835
+ if stress.get("drought_risk"): alerts.append("DROUGHT_RISK")
836
+ if stress.get("current_heat_stress"): alerts.append("HEAT_STRESS")
837
+ if alerts:
838
+ lines.append(f"[ALERTS] " + ", ".join(alerts))
839
+
840
+ # 4. Stress Pattern
841
+ analysis = context.get("stress_analysis", {})
842
+ pct = analysis.get("impaired_percentage", 0)
843
+ if pct > 10:
844
+ lines.append(f"[PATTERN] {pct:.0f}% of field affected. Widespread stress.")
845
+
846
+ return "\n".join(lines)
847
+
848
+ def build_deep_dive_context(self, context: Dict, stage: str = "hypothesis") -> str:
849
+ """
850
+ Build specialized context for Deep Dive stages.
851
+ """
852
+ lines = []
853
+
854
+ # Common Data (Always needed)
855
+ lines.append(self.build_ultra_compact_context(context))
856
+
857
+ if stage == "hypothesis":
858
+ # Add History + Trends for robust hypothesis generation
859
+ trends = context.get("historical_trends", {})
860
+ if trends:
861
+ summary = trends.get("summary", "")
862
+ lines.append(f"[HISTORY] {summary}")
863
+
864
+ elif stage == "adversary":
865
+ # Add SAR + Soil + Detailed Weather for contradiction checking
866
+ # This is data that was HIDDEN in the Fast Lane
867
+ sar = context.get("sar_bands", {})
868
+ if sar:
869
+ lines.append(f"[SAR_DATA] VV:{sar.get('vv', '?')} VH:{sar.get('vh', '?')} Structure:{sar.get('interpretation', 'stable')}")
870
+
871
+ soil = context.get("soil_indicators", {})
872
+ if soil:
873
+ lines.append(f"[SOIL_LAB] Salinity:{soil.get('salinity', {}).get('level')} Organic:{soil.get('organic_matter', {}).get('level')}")
874
+
875
+ elif stage == "judge":
876
+ # Add Farmer Context + Constraints
877
+ farmer = context.get("farmer_profile", {})
878
+ actions = context.get("farmer_actions", {})
879
+
880
+ if farmer:
881
+ lines.append(f"[FARMER] Goal:{farmer.get('farming_goal')} Budget:{farmer.get('budget_level', 'medium')}")
882
+ if actions:
883
+ lines.append(f"[ACTIONS] Irrigated:{actions.get('days_since_irrigation')} days ago. Fertilized:{actions.get('days_since_fertilizer')} days ago.")
884
+
885
+ return "\n".join(lines)
886
+
887
+
888
  # =============================================================================
889
  # QUICK FUNCTIONS
890
  # =============================================================================
prompts.py CHANGED
@@ -15,59 +15,59 @@ from typing import Dict, List, Any, Optional
15
  PERSONA_DEFINITIONS = {
16
  "new_farmer_basic_tech": {
17
  "description": "New farmer (0-3 years), limited smartphone familiarity",
18
- "style": "Very simple, step-by-step guidance with visual cues",
19
- "format": "Short bullet points, numbered steps, clear YES/NO answers",
20
- "focus": "Immediate actions today, what to look for visually, when to seek help",
21
  "tone": "Warm, patient, encouraging, supportive",
22
- "recommendations": "Simple low-cost solutions, avoid complex equipment suggestions"
23
  },
24
  "new_farmer_tech_savvy": {
25
  "description": "New farmer (0-3 years), comfortable with technology",
26
- "style": "Clear explanations with some context, can reference app features",
27
- "format": "Organized lists, basic metrics explained, graphs mentioned",
28
- "focus": "Learning-oriented, explain the 'why', build understanding",
29
- "tone": "Friendly, educational, encouraging experimentation",
30
- "recommendations": "Modern approaches welcome, mention monitoring features"
31
  },
32
  "experienced_farmer_traditional": {
33
  "description": "Experienced farmer (5+ years), prefers traditional methods",
34
- "style": "Practical, reference seasonal patterns they would recognize",
35
- "format": "Direct recommendations, compare to normal conditions",
36
- "focus": "Cost-effective proven solutions, risk assessment, timing",
37
- "tone": "Respectful of expertise, collaborative, peer-to-peer",
38
- "recommendations": "Proven methods first, new tech only if clearly beneficial"
39
  },
40
  "experienced_farmer_innovative": {
41
  "description": "Experienced farmer (5+ years), open to modern methods",
42
- "style": "Practical with technical context, efficiency-focused",
43
- "format": "Data-backed recommendations, optimization suggestions",
44
- "focus": "Maximize yield/profit, precision timing, resource efficiency",
45
- "tone": "Professional, partner-like, respects their judgment",
46
- "recommendations": "Innovation welcomed, precision agriculture approaches"
47
  },
48
  "commercial_farmer": {
49
  "description": "Commercial farming focus, business-oriented",
50
- "style": "Business-focused, ROI considerations, scalable solutions",
51
- "format": "Clear priorities, cost-benefit mentioned, zone-wise breakdown",
52
- "focus": "Profit optimization, risk management, market timing",
53
  "tone": "Professional, efficient, results-oriented",
54
- "recommendations": "Commercial-grade solutions, labor/resource optimization"
55
  },
56
  "agricultural_officer": {
57
  "description": "Extension officer or field advisor",
58
- "style": "Professional, documentation-ready, multi-farm applicable",
59
- "format": "Structured findings, zone-wise data, statistical summary",
60
- "focus": "Regional patterns, farmer communication tips, policy relevance",
61
- "tone": "Formal, comprehensive, shareable insights",
62
- "recommendations": "Scalable solutions, training opportunities"
63
  },
64
  "agronomist_researcher": {
65
  "description": "Technical researcher or scientist",
66
- "style": "Full technical precision, spectral indices, statistical rigor",
67
- "format": "Numerical tables, temporal correlations, spatial patterns, confidence intervals",
68
- "focus": "Causal mechanisms, data quality notes, methodology",
69
  "tone": "Scientific, analytical, evidence-driven",
70
- "recommendations": "Research-backed interventions, experimental approaches"
71
  }
72
  }
73
 
@@ -215,7 +215,8 @@ IMPORTANT: Generate a response that this specific user will find most helpful an
215
  # SYSTEM PROMPT (Base context)
216
  # =============================================================================
217
 
218
- SYSTEM_PROMPT = """You are AGROW AI, an expert agricultural advisor for Indian farmers.
 
219
 
220
  SPECIALIZATIONS:
221
  - Satellite imagery interpretation (Sentinel-1 SAR, Sentinel-2 optical bands)
@@ -225,18 +226,19 @@ SPECIALIZATIONS:
225
  - Regional crop knowledge (wheat, rice, cotton, sugarcane, pulses, mustard, etc.)
226
 
227
  COMMUNICATION STYLE:
228
- - Use simple, practical language farmers understand
229
- - Cite specific values from satellite data when available
230
- - Distinguish ROOT CAUSE from SYMPTOMS
231
- - Give actionable, prioritized recommendations
232
- - Reference local conditions and seasonal context
233
- - Be concise but thorough
 
234
 
235
  ANALYSIS APPROACH:
236
- - Always consider multiple hypotheses before concluding
237
- - Seek contradicting evidence actively
238
- - Build causal chains: Event A → Effect B → Symptom C
239
- - Confidence scores reflect evidence strength
240
 
241
  When you lack specific data, acknowledge it honestly and provide general guidance based on described symptoms."""
242
 
@@ -367,7 +369,8 @@ RESPONSE_PROMPT = """
367
  CONVERSATION HISTORY (for follow-up awareness):
368
  {conversation_history}
369
 
370
- Based on the diagnostic analysis, generate a FOCUSED, TO-THE-POINT response.
 
371
 
372
  USER QUERY: {query}
373
 
@@ -383,47 +386,46 @@ ZONE ANALYSIS:
383
  HISTORICAL TRENDS:
384
  {trend_context}
385
 
386
- ═══════════════════════════════════════════════════════════════
387
- CRITICAL: RESPONSE STRUCTURE (MUST FOLLOW THIS ORDER)
388
- ═══════════════════════════════════════════════════════════════
389
-
390
- **SECTION 1: DIRECT ANSWER (2-3 sentences MAX)**
391
- - Answer the user's EXACT question immediately
392
- - No preamble, no explanation - just the answer
393
- - If it's a yes/no question, start with "Yes" or "No"
394
- - Include the key data point that supports your answer
395
-
396
- **SECTION 2: BRIEF EVIDENCE (1-2 bullets)**
397
- - Only cite 1-2 most relevant data points
398
- - Format: "📊 NDVI: 0.45 (below healthy threshold of 0.6)"
399
-
400
- **SECTION 3: PRIORITY ACTION (1 main action)**
401
- - ONE clear, immediate action they should take
402
- - Include timing: "Do this TODAY/this week/before rain"
403
-
404
- ---
405
-
406
- **SECTION 4: AUXILIARY INFORMATION** (only if relevant)
407
- After the main answer, you MAY add:
408
- - 🌤️ Weather timing (if relevant to action)
409
- - 📍 Zone priority (if spatial variation exists)
410
- - 📈 Trend context (if historical data shows pattern)
411
- - 💡 Additional tips (max 2 bullets)
412
-
413
- ═══════════════════════════════════════════════════════════════
414
- WRONG ❌: "Based on the satellite analysis of your field, I can see that there are several factors to consider. The NDVI values show..."
415
- RIGHT ✅: "Your crop needs water immediately. NDVI is 0.42, down 15% from last week, indicating water stress."
416
- ═══════════════════════════════════════════════════════════════
417
-
418
- FOLLOW-UP AWARENESS:
419
- - If user asks "what about X" after previous discussion, connect to earlier context
420
- - Reference previous diagnosis if relevant: "Building on the water stress we discussed..."
421
-
422
- ZONE-SPECIFIC (if zone data available):
423
- - Identify the PRIORITY ZONE needing immediate attention
424
- - Give zone-specific dosage/quantity if applicable
425
-
426
- Keep total response under 150 words for basic farmers, up to 250 for researchers."""
427
 
428
 
429
  # =============================================================================
@@ -972,4 +974,99 @@ def format_minimal_diagnosis(result) -> str:
972
  """Format diagnosis in minimal tokens."""
973
  if isinstance(result, dict):
974
  return f"diag:{result.get('final_diagnosis','')} conf:{result.get('final_confidence',0):.2f} cause:{result.get('root_cause','?')}"
975
- return str(result)[:100]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  PERSONA_DEFINITIONS = {
16
  "new_farmer_basic_tech": {
17
  "description": "New farmer (0-3 years), limited smartphone familiarity",
18
+ "style": "Very simple, step-by-step guidance. NO NUMBERS OR INDICES.",
19
+ "format": "Short bullet points, numbered steps. Use terms like 'Low', 'High', 'Good', 'Bad'.",
20
+ "focus": "Immediate actions. Explain strictly based on their irrigation method.",
21
  "tone": "Warm, patient, encouraging, supportive",
22
+ "recommendations": "Simple low-cost solutions. Tailor to their specific farming technique."
23
  },
24
  "new_farmer_tech_savvy": {
25
  "description": "New farmer (0-3 years), comfortable with technology",
26
+ "style": "Clear explanations without complex numbers. NO RAW INDICES.",
27
+ "format": "Use 'High'/'Moderate'/'Low' for status. Visual cues.",
28
+ "focus": "Learning-oriented. Explain 'why' using their specific crop context.",
29
+ "tone": "Friendly, educational, encouraging.",
30
+ "recommendations": "Modern approaches. Consider their specific irrigation setup."
31
  },
32
  "experienced_farmer_traditional": {
33
  "description": "Experienced farmer (5+ years), prefers traditional methods",
34
+ "style": "Practical. NO RAW DATA/NUMBERS. Use traditional terms.",
35
+ "format": "Direct recommendations. Use 'Adequate', 'Stressed', 'Severe'.",
36
+ "focus": "Risk assessment. Relate to their traditional techniques.",
37
+ "tone": "Respectful of expertise, peer-to-peer.",
38
+ "recommendations": "Proven methods first. Match their existing irrigation habits."
39
  },
40
  "experienced_farmer_innovative": {
41
  "description": "Experienced farmer (5+ years), open to modern methods",
42
+ "style": "Practical but simplified metrics. NO COMPLEX DECIMALS.",
43
+ "format": "Use 'High Efficiency' vs 'Low'.",
44
+ "focus": "Efficiency. Optimize their specific machinery/irrigation inputs.",
45
+ "tone": "Professional, partner-like.",
46
+ "recommendations": "Innovation welcomed. Precision approaches for their equipment."
47
  },
48
  "commercial_farmer": {
49
  "description": "Commercial farming focus, business-oriented",
50
+ "style": "Business-focused. Summary stats only (High/Low risks).",
51
+ "format": "Clear priorities. Zone breakdowns by 'Severity' (not index).",
52
+ "focus": "ROI and scalable solutions for their infrastructure.",
53
  "tone": "Professional, efficient, results-oriented",
54
+ "recommendations": "Commercial-grade solutions tailored to their scale/inputs."
55
  },
56
  "agricultural_officer": {
57
  "description": "Extension officer or field advisor",
58
+ "style": "Professional summary. Minimal raw numbers, focus on status.",
59
+ "format": "Zone-wise 'Affected' vs 'Healthy'. trend direction.",
60
+ "focus": "Regional patterns. Farmer communication tips.",
61
+ "tone": "Formal, shareable insights",
62
+ "recommendations": "Scalable solutions for the region's common techniques."
63
  },
64
  "agronomist_researcher": {
65
  "description": "Technical researcher or scientist",
66
+ "style": "Full technical precision. HEAVY USE OF NUMERICAL DATA & INDICES.",
67
+ "format": "Tables with exact NDVI/NDRE values. Statistical confidence intervals.",
68
+ "focus": "Causal mechanisms, data quality, methodology.",
69
  "tone": "Scientific, analytical, evidence-driven",
70
+ "recommendations": "Experimental approaches. Cite specific spectral bands/thresholds."
71
  }
72
  }
73
 
 
215
  # SYSTEM PROMPT (Base context)
216
  # =============================================================================
217
 
218
+ SYSTEM_PROMPT = """You are AGROW AI, a dedicated PERSONAL agricultural advisor for Indian farmers.
219
+ Your goal is to be a trusted partner in their farming journey, not just a data analyzer.
220
 
221
  SPECIALIZATIONS:
222
  - Satellite imagery interpretation (Sentinel-1 SAR, Sentinel-2 optical bands)
 
226
  - Regional crop knowledge (wheat, rice, cotton, sugarcane, pulses, mustard, etc.)
227
 
228
  COMMUNICATION STYLE:
229
+ - Use a PERSONAL, RELATABLE tone. Use "I", "We", and refer to "Your field".
230
+ - Avoid neutral, robotic assertions. Show empathy and understanding.
231
+ - Use simple, practical language farmers understand.
232
+ - Cite specific values but explain what they mean for *their* specific field.
233
+ - Distinguish ROOT CAUSE from SYMPTOMS.
234
+ - Give actionable, prioritized recommendations.
235
+ - Reference local conditions and seasonal context.
236
 
237
  ANALYSIS APPROACH:
238
+ - Always consider multiple hypotheses before concluding.
239
+ - Seek contradicting evidence actively.
240
+ - Build causal chains: Event A → Effect B → Symptom C.
241
+ - Confidence scores reflect evidence strength.
242
 
243
  When you lack specific data, acknowledge it honestly and provide general guidance based on described symptoms."""
244
 
 
369
  CONVERSATION HISTORY (for follow-up awareness):
370
  {conversation_history}
371
 
372
+ Based on the diagnostic analysis, generate a COMPREHENSIVE and DETAILED response.
373
+ The user wants a full explanation, not just a summary.
374
 
375
  USER QUERY: {query}
376
 
 
386
  HISTORICAL TRENDS:
387
  {trend_context}
388
 
389
+ ---------------------------------------------------------------
390
+ CRITICAL: RESPONSE STRUCTURE (MUST FOLLOW THIS EXACT FORMAT)
391
+ ---------------------------------------------------------------
392
+
393
+ **DIAGNOSIS & STATUS**
394
+ * Clearly state the primary issue identified (or confirmation of health).
395
+ * Mention the severity level (Mild/Moderate/Severe) based on the data.
396
+ * State the confidence level in this diagnosis.
397
+
398
+ **DETAILED REASONING**
399
+ * Explain *WHY* this is the diagnosis, connecting the dots between different data points.
400
+ * Cite specific metrics (NDVI, SMI, NDRE) and explain what they mean in this context.
401
+ * Explicitly mention if the 3-stage analysis (Hypothesis -> Adversary -> Judge) ruled out other causes.
402
+ * Reference historical trends or weather patterns that support this conclusion.
403
+
404
+ **FUTURE RISKS**
405
+ * Explain what will happen if this issue is ignored for 3-5 days.
406
+ * Mention potential yield impact or long-term damage.
407
+ * Flag any upcoming weather risks (e.g., "Forecast rain might worsen fungal spread").
408
+
409
+ **RECOMMENDATIONS**
410
+ * **Immediate Action**: What needs to be done TODAY? (be specific: amounts, methods).
411
+ * **Follow-up**: What to check in 3 days.
412
+ * **Long-term**: Preventative measures for next season.
413
+
414
+ **NEXT STEPS**
415
+ * End with a specific question to keep the conversation going.
416
+ * Examples:
417
+ * "Should I help you calculate the fertilizer dosage?"
418
+ * "Would you like to analyze the historical trends for this field?"
419
+ * "Shall I monitor this area for you over the next week?"
420
+
421
+ ---------------------------------------------------------------
422
+ GUIDELINES:
423
+ * NO EMOJIS in the output.
424
+ * Use asterisk (*) for bullet points, do NOT use hyphens (-).
425
+ * Bold the section headings.
426
+ * Tone: Professional, authoritative, but helpful (Agro-Expert).
427
+ * Length: Comprehensive (300-500 words is acceptable for Deep Dive).
428
+ """
 
429
 
430
 
431
  # =============================================================================
 
974
  """Format diagnosis in minimal tokens."""
975
  if isinstance(result, dict):
976
  return f"diag:{result.get('final_diagnosis','')} conf:{result.get('final_confidence',0):.2f} cause:{result.get('root_cause','?')}"
977
+
978
+ # =============================================================================
979
+ # HYBRID ARCHITECTURE PROMPTS
980
+ # =============================================================================
981
+
982
+ FAST_LANE_PROMPT = """You are Agrow-AI.
983
+ TASK: Diagnose the crop issue based on the provided context.
984
+ PRIORITY: SPEED & ACCURACY.
985
+
986
+ CONTEXT:
987
+ {context}
988
+
989
+ INSTRUCTIONS:
990
+ 1. [Hypothesis]: Briefly state what the primary signals (NDVI, NDRE, etc.) suggest.
991
+ 2. [Check]: Verify if supporting data (Moisture, Weather) aligns or contradicts.
992
+ 3. [Diagnosis]: State the final conclusion.
993
+ 4. [Action]: One specific corrective action.
994
+
995
+ OUTPUT JSON ONLY:
996
+ {{
997
+ "reasoning_trace": "Hypothesis... Check... Conclusion...",
998
+ "diagnosis": "Final Diagnosis",
999
+ "confidence": 0.0-1.0,
1000
+ "action": "Corrective Action"
1001
+ }}
1002
+ """
1003
+
1004
+ DEEP_DIVE_HYPOTHESIS_PROMPT = """You are Agrow-AI, conducting a DEEP DIVE diagnosis.
1005
+ STAGE A: HYPOTHESIS GENERATION
1006
+
1007
+ CONTEXT:
1008
+ {context}
1009
+
1010
+ TASK:
1011
+ Identify top 3 possible causes for the observed issues. Do not conclude yet.
1012
+ Think broadly (Nutrients, Pests, Water, Soil, Disease).
1013
+
1014
+ OUTPUT JSON ONLY:
1015
+ {{
1016
+ "hypotheses": [
1017
+ {{"cause": "Cause 1", "likelihood": "High/Med", "reason": "why"}},
1018
+ {{"cause": "Cause 2", "likelihood": "High/Med", "reason": "why"}},
1019
+ {{"cause": "Cause 3", "likelihood": "High/Med", "reason": "why"}}
1020
+ ]
1021
+ }}
1022
+ """
1023
+
1024
+ DEEP_DIVE_ADVERSARY_PROMPT = """You are Agrow-AI.
1025
+ STAGE B: ADVERSARIAL CHECK
1026
+
1027
+ HYPOTHESES:
1028
+ {hypotheses}
1029
+
1030
+ NEW EVIDENCE (Adversarial Data):
1031
+ {context}
1032
+
1033
+ TASK:
1034
+ Actively try to DISPROVE each hypothesis using the new evidence (SAR, Soil, Pests).
1035
+ If evidence contradicts a hypothesis, mark it as INVALID.
1036
+
1037
+ OUTPUT JSON ONLY:
1038
+ {{
1039
+ "analysis": [
1040
+ {{"cause": "Cause 1", "status": "Valid/Invalid", "reason": "Support/Contradiction from new evidence"}},
1041
+ ...
1042
+ ],
1043
+ "surviving_hypothesis": "The strongest remaining cause",
1044
+ "confidence": 0.0-1.0
1045
+ }}
1046
+ """
1047
+
1048
+ DEEP_DIVE_JUDGE_PROMPT = """You are Agrow-AI.
1049
+ STAGE C: FINAL VERDICT
1050
+
1051
+ WINNING HYPOTHESIS:
1052
+ {hypothesis}
1053
+
1054
+ CONSTRAINTS & HISTORY:
1055
+ {context}
1056
+
1057
+ TASK:
1058
+ Provide the final diagnostic report and a detailed action plan.
1059
+ Consider farmer constraints (budget, machinery) and historical trends.
1060
+
1061
+ OUTPUT JSON ONLY:
1062
+ {{
1063
+ "final_diagnosis": "Diagnosis",
1064
+ "root_cause": "Root Cause",
1065
+ "detailed_reasoning": "Explanation of why this is the verdict",
1066
+ "action_plan": {{
1067
+ "immediate": "Action 1",
1068
+ "long_term": "Action 2"
1069
+ }}
1070
+ }}
1071
+ """
1072
+
reasoning_engine.py CHANGED
@@ -19,9 +19,14 @@ from prompts import (
19
  # Compact prompts for token reduction
20
  COMPACT_CLAIM_PROMPT, COMPACT_VALIDATE_PROMPT, COMPACT_CONTRADICT_PROMPT,
21
  COMPACT_CONFIRM_PROMPT, COMPACT_RESPONSE_PROMPT,
22
- build_compact_context, get_compact_prompt, format_minimal_diagnosis
 
 
 
23
  )
24
 
 
 
25
  logger = logging.getLogger("ReasoningEngine")
26
 
27
  # Toggle compact prompts to reduce token usage (saves ~50% tokens)
@@ -81,6 +86,7 @@ class ReasoningEngine:
81
  self.llm = llm_caller
82
  self.intent_classifier = IntentClassifier()
83
  self.priority_mapper = PriorityContextMapper()
 
84
 
85
  def process_query(
86
  self,
@@ -88,10 +94,7 @@ class ReasoningEngine:
88
  context: Optional[Dict[str, Any]] = None
89
  ) -> Tuple[str, Dict[str, Any]]:
90
  """
91
- Process user query through full reasoning pipeline.
92
-
93
- Returns:
94
- (response_text, reasoning_trace)
95
  """
96
  logger.info(f"Processing query: {query[:50]}...")
97
 
@@ -99,16 +102,18 @@ class ReasoningEngine:
99
  intent = self.intent_classifier.classify(query)
100
  logger.info(f"Intent: {intent['primary_intent']} ({intent['confidence']})")
101
 
102
- # Stage 2: Get prioritized context (NOT all context at once!)
103
- staged_context = self.priority_mapper.build_staged_context(
104
- intent=intent["primary_intent"],
105
- full_context=context or {}
106
- )
107
 
108
- # Stage 3: Multi-stage reasoning
109
- reasoning_result = self._reason(query, intent, staged_context)
 
 
 
110
 
111
  # Stage 4: Generate response (pass full context for persona/weather/zone)
 
112
  response = self._generate_response(query, reasoning_result, context)
113
 
114
  # Stage 5: Generate followups
@@ -118,9 +123,100 @@ class ReasoningEngine:
118
  )
119
 
120
  # Build complete trace
121
- trace = self._build_trace(intent, reasoning_result, staged_context, followups)
 
122
 
123
  return response, trace
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
 
125
  def _reason(
126
  self,
 
19
  # Compact prompts for token reduction
20
  COMPACT_CLAIM_PROMPT, COMPACT_VALIDATE_PROMPT, COMPACT_CONTRADICT_PROMPT,
21
  COMPACT_CONFIRM_PROMPT, COMPACT_RESPONSE_PROMPT,
22
+ build_compact_context, get_compact_prompt, format_minimal_diagnosis,
23
+ # Hybrid Prompts
24
+ FAST_LANE_PROMPT, DEEP_DIVE_HYPOTHESIS_PROMPT,
25
+ DEEP_DIVE_ADVERSARY_PROMPT, DEEP_DIVE_JUDGE_PROMPT
26
  )
27
 
28
+ from context_aggregator import ContextAggregator
29
+
30
  logger = logging.getLogger("ReasoningEngine")
31
 
32
  # Toggle compact prompts to reduce token usage (saves ~50% tokens)
 
86
  self.llm = llm_caller
87
  self.intent_classifier = IntentClassifier()
88
  self.priority_mapper = PriorityContextMapper()
89
+ self.aggregator = ContextAggregator()
90
 
91
  def process_query(
92
  self,
 
94
  context: Optional[Dict[str, Any]] = None
95
  ) -> Tuple[str, Dict[str, Any]]:
96
  """
97
+ Process user query through Hybrid Architecture (Fast Lane vs Deep Dive).
 
 
 
98
  """
99
  logger.info(f"Processing query: {query[:50]}...")
100
 
 
102
  intent = self.intent_classifier.classify(query)
103
  logger.info(f"Intent: {intent['primary_intent']} ({intent['confidence']})")
104
 
105
+ # Stage 2: Route Query
106
+ mode = self.route_query(query, intent)
107
+ logger.info(f"Routing mode: {mode}")
 
 
108
 
109
+ # Stage 3: Execute Logic
110
+ if mode == "FAST_LANE":
111
+ reasoning_result = self._execute_fast_lane(query, intent, context or {})
112
+ else:
113
+ reasoning_result = self._execute_deep_dive(query, intent, context or {})
114
 
115
  # Stage 4: Generate response (pass full context for persona/weather/zone)
116
+ # Note: Fast Lane already generates action/diagnosis, but we standardize output format
117
  response = self._generate_response(query, reasoning_result, context)
118
 
119
  # Stage 5: Generate followups
 
123
  )
124
 
125
  # Build complete trace
126
+ trace = self._build_trace(intent, reasoning_result, {}, followups)
127
+ trace["routing_mode"] = mode
128
 
129
  return response, trace
130
+
131
+ def route_query(self, query: str, intent: Dict) -> str:
132
+ """Decide between Fast Lane and Deep Dive."""
133
+ # Intention-based routing
134
+ fast_intents = ["vegetation_health", "water_stress", "nutrient_status"]
135
+ if intent["primary_intent"] in fast_intents and intent["confidence"] > 0.7:
136
+ return "FAST_LANE"
137
+
138
+ # "Why" questions or Comparisons usually need Deep Dive
139
+ if "compare" in query.lower() or "difference" in query.lower():
140
+ return "DEEP_DIVE"
141
+
142
+ return "DEEP_DIVE" # Default to robust mode for safety
143
+
144
+ def _execute_fast_lane(self, query: str, intent: Dict, context: Dict) -> ReasoningResult:
145
+ """Execute 1-Shot Reasoning."""
146
+ logger.info("Executing FAST LANE (1-Call)...")
147
+
148
+ # Build ultra-compact context
149
+ compact_ctx = self.aggregator.build_ultra_compact_context(context)
150
+
151
+ prompt = FAST_LANE_PROMPT.format(context=compact_ctx)
152
+ full_prompt = f"{SYSTEM_PROMPT}\n\n{prompt}"
153
+
154
+ response = self.llm(full_prompt)
155
+
156
+ output = self._parse_json_safe(response, {
157
+ "reasoning_trace": "Analysis failed",
158
+ "diagnosis": "Unknown",
159
+ "confidence": 0.0,
160
+ "action": "Consult expert"
161
+ })
162
+
163
+ # Create Dummy StageResult for compatibility
164
+ dummy_stage = StageResult("fast_lane", output, [], output.get("confidence", 0.0))
165
+
166
+ return ReasoningResult(
167
+ claim=dummy_stage, # Fill for struct compatibility
168
+ validation=dummy_stage,
169
+ contradiction=dummy_stage,
170
+ confirmation=dummy_stage,
171
+ final_diagnosis=output.get("diagnosis", "Unknown"),
172
+ final_confidence=output.get("confidence", 0.0),
173
+ causal_chain=output.get("reasoning_trace", ""),
174
+ root_cause=output.get("diagnosis", "Unknown"),
175
+ symptoms=[],
176
+ recommendation=output.get("action", ""),
177
+ evidence_summary={"method": ["fast_lane_optimization"]}
178
+ )
179
+
180
+ def _execute_deep_dive(self, query: str, intent: Dict, context: Dict) -> ReasoningResult:
181
+ """Execute 3-Stage Deep Dive."""
182
+ logger.info("Executing DEEP DIVE (3-Call)...")
183
+
184
+ # 1. Hypothesis Generation
185
+ ctx_hyp = self.aggregator.build_deep_dive_context(context, "hypothesis")
186
+ resp_hyp = self.llm(f"{SYSTEM_PROMPT}\n{DEEP_DIVE_HYPOTHESIS_PROMPT.format(context=ctx_hyp)}")
187
+ out_hyp = self._parse_json_safe(resp_hyp, {"hypotheses": []})
188
+
189
+ # 2. Adversarial Check
190
+ ctx_adv = self.aggregator.build_deep_dive_context(context, "adversary")
191
+ hyp_str = json.dumps(out_hyp, indent=2)
192
+ resp_adv = self.llm(f"{SYSTEM_PROMPT}\n{DEEP_DIVE_ADVERSARY_PROMPT.format(hypotheses=hyp_str, context=ctx_adv)}")
193
+ out_adv = self._parse_json_safe(resp_adv, {"surviving_hypothesis": "Unknown"})
194
+
195
+ # 3. Final Verdict
196
+ ctx_judge = self.aggregator.build_deep_dive_context(context, "judge")
197
+ winner = out_adv.get("surviving_hypothesis", "Unknown")
198
+ resp_judge = self.llm(f"{SYSTEM_PROMPT}\n{DEEP_DIVE_JUDGE_PROMPT.format(hypothesis=winner, context=ctx_judge)}")
199
+ out_judge = self._parse_json_safe(resp_judge, {"final_diagnosis": winner, "action_plan": {}})
200
+
201
+ # Map to ReasoningResult
202
+ # We map stages roughly to Maintain compatibility
203
+ result_hyp = StageResult("hypothesis", out_hyp, [], 0.0)
204
+ result_adv = StageResult("adversary", out_adv, [], 0.0)
205
+ result_judge = StageResult("judge", out_judge, [], 0.0)
206
+
207
+ return ReasoningResult(
208
+ claim=result_hyp,
209
+ validation=result_adv,
210
+ contradiction=result_adv,
211
+ confirmation=result_judge,
212
+ final_diagnosis=out_judge.get("final_diagnosis", "Unknown"),
213
+ final_confidence=0.9, # Deep dive implies high confidence
214
+ causal_chain=out_judge.get("detailed_reasoning", ""),
215
+ root_cause=out_judge.get("root_cause", ""),
216
+ symptoms=[],
217
+ recommendation=str(out_judge.get("action_plan", "")),
218
+ evidence_summary={"method": ["deep_dive_3_stage"]}
219
+ )
220
 
221
  def _reason(
222
  self,