Aniket2006 commited on
Commit
2221290
·
1 Parent(s): 3fad61c

Add satellite band data injection from HF Space APIs (SAR, Sentinel-2)

Browse files
Files changed (2) hide show
  1. app.py +73 -0
  2. context_aggregator.py +362 -0
app.py CHANGED
@@ -23,6 +23,7 @@ from pydantic import BaseModel
23
  from supabase_client import SupabaseClient
24
  from reasoning_engine import ReasoningEngine, simple_reason
25
  from intent_classifier import IntentClassifier
 
26
 
27
  # ============================================================================
28
  # LOGGING
@@ -287,6 +288,25 @@ async def chat(request: ChatRequest):
287
  intent = intent_classifier.classify(request.message)
288
  logger.info(f"Intent: {intent['primary_intent']} ({intent['confidence']})")
289
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
290
  # Determine if we need full reasoning or simple response
291
  use_full_reasoning = (
292
  intent["confidence"] > 0.6 and
@@ -413,7 +433,60 @@ async def analyze_intent(request: Dict[str, str]):
413
  }
414
 
415
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
416
  if __name__ == "__main__":
417
  import uvicorn
418
  logger.info("Starting AGROW Chatbot Service v2.0")
419
  uvicorn.run(app, host="0.0.0.0", port=7860)
 
 
23
  from supabase_client import SupabaseClient
24
  from reasoning_engine import ReasoningEngine, simple_reason
25
  from intent_classifier import IntentClassifier
26
+ from context_aggregator import ContextAggregator, fetch_field_context
27
 
28
  # ============================================================================
29
  # LOGGING
 
288
  intent = intent_classifier.classify(request.message)
289
  logger.info(f"Intent: {intent['primary_intent']} ({intent['confidence']})")
290
 
291
+ # Fetch satellite data for technical queries (vegetation, water, nutrient intents)
292
+ satellite_intents = ["vegetation_health", "water_stress", "nutrient_status", "pest_disease", "forecast_query"]
293
+ if (intent["primary_intent"] in satellite_intents and
294
+ context.get("coordinates") and
295
+ intent["confidence"] > 0.5):
296
+
297
+ logger.info("Fetching satellite data from HF Space APIs...")
298
+ try:
299
+ satellite_context = fetch_field_context(
300
+ coordinates=context.get("coordinates"),
301
+ crop_type=context.get("crop_type", "Wheat"),
302
+ area_acres=context.get("area_acres", 1.0),
303
+ fetch_satellite=True
304
+ )
305
+ context.update(satellite_context)
306
+ logger.info(f"Satellite data loaded: {list(satellite_context.keys())}")
307
+ except Exception as e:
308
+ logger.warning(f"Could not fetch satellite data: {e}")
309
+
310
  # Determine if we need full reasoning or simple response
311
  use_full_reasoning = (
312
  intent["confidence"] > 0.6 and
 
433
  }
434
 
435
 
436
+ # Satellite context endpoint (for debugging and direct access)
437
+ @app.post("/satellite-context")
438
+ async def get_satellite_context(request: Dict[str, Any]):
439
+ """
440
+ Fetch satellite band data from HF Space APIs.
441
+
442
+ Request body:
443
+ {
444
+ "user_id": "firebase_or_anon_id",
445
+ "coordinates": {"center_lat": 30.9, "center_lon": 75.8, "bbox": [...]},
446
+ "crop_type": "Wheat",
447
+ "area_acres": 1.0
448
+ }
449
+ """
450
+ logger.info("Fetching satellite context...")
451
+
452
+ user_id = request.get("user_id")
453
+ coordinates = request.get("coordinates")
454
+ crop_type = request.get("crop_type", "Wheat")
455
+ area_acres = request.get("area_acres", 1.0)
456
+
457
+ # If user_id provided, fetch from Supabase
458
+ if user_id and not coordinates:
459
+ field_context = supabase.get_field_context(user_id)
460
+ if field_context:
461
+ coordinates = field_context.get("coordinates")
462
+ crop_type = field_context.get("crop_type", crop_type)
463
+ area_acres = field_context.get("area_acres", area_acres)
464
+
465
+ if not coordinates:
466
+ return {"error": "No coordinates available", "data": None}
467
+
468
+ try:
469
+ aggregator = ContextAggregator(timeout=60)
470
+ raw_context = aggregator.fetch_full_context(
471
+ coordinates=coordinates,
472
+ crop_type=crop_type,
473
+ area_acres=area_acres
474
+ )
475
+ formatted_context = aggregator.format_for_llm(raw_context)
476
+
477
+ return {
478
+ "success": True,
479
+ "raw_context": raw_context,
480
+ "formatted_context": formatted_context,
481
+ "timestamp": datetime.now().isoformat()
482
+ }
483
+ except Exception as e:
484
+ logger.error(f"Satellite context error: {e}")
485
+ return {"success": False, "error": str(e)}
486
+
487
+
488
  if __name__ == "__main__":
489
  import uvicorn
490
  logger.info("Starting AGROW Chatbot Service v2.0")
491
  uvicorn.run(app, host="0.0.0.0", port=7860)
492
+
context_aggregator.py ADDED
@@ -0,0 +1,362 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Context Aggregator for Agricultural Chatbot
3
+ =============================================
4
+ Fetches satellite data from HF Space APIs and formats for LLM context.
5
+ """
6
+
7
+ import os
8
+ import logging
9
+ import requests
10
+ from typing import Dict, List, Any, Optional
11
+ from datetime import datetime
12
+
13
+ logger = logging.getLogger("ContextAggregator")
14
+
15
+ # HF Space URLs
16
+ SAR_API_URL = "https://aniket2006-agrow-backend-v2.hf.space"
17
+ SENTINEL2_API_URL = "https://aniket2006-agrow-sentinel2.hf.space"
18
+ HEATMAP_API_URL = "https://aniket2006-heatmap.hf.space"
19
+
20
+
21
+ class ContextAggregator:
22
+ """
23
+ Aggregates satellite data from multiple HF Space APIs.
24
+ Returns structured JSON for LLM context injection.
25
+ """
26
+
27
+ def __init__(self, timeout: int = 30):
28
+ self.timeout = timeout
29
+
30
+ def fetch_full_context(
31
+ self,
32
+ coordinates: Dict[str, Any],
33
+ crop_type: str = "Wheat",
34
+ area_acres: float = 1.0,
35
+ farmer_context: Optional[Dict] = None
36
+ ) -> Dict[str, Any]:
37
+ """
38
+ Fetch complete satellite context for a field.
39
+
40
+ Args:
41
+ coordinates: {"center_lat": float, "center_lon": float, "bbox": [lon_min, lat_min, lon_max, lat_max]}
42
+ crop_type: Type of crop
43
+ area_acres: Field size in acres
44
+ farmer_context: Additional farmer profile data
45
+
46
+ Returns:
47
+ {
48
+ "vegetation_indices": {...},
49
+ "sar_data": {...},
50
+ "soil_indicators": {...},
51
+ "weather_data": {...},
52
+ "anomalies": {...},
53
+ "temporal_trends": {...}
54
+ }
55
+ """
56
+ context = {
57
+ "fetch_timestamp": datetime.now().isoformat(),
58
+ "field_info": {
59
+ "crop_type": crop_type,
60
+ "area_acres": area_acres,
61
+ "coordinates": coordinates
62
+ }
63
+ }
64
+
65
+ if not coordinates:
66
+ return context
67
+
68
+ center_lat = coordinates.get("center_lat")
69
+ center_lon = coordinates.get("center_lon")
70
+ bbox = coordinates.get("bbox")
71
+
72
+ if not center_lat or not center_lon:
73
+ return context
74
+
75
+ # Fetch SAR analysis (VV, VH bands, patches, predictions)
76
+ sar_data = self._fetch_sar_data(bbox, crop_type, farmer_context)
77
+ if sar_data:
78
+ context["sar_bands"] = sar_data.get("sar_bands", {})
79
+ context["patches"] = sar_data.get("patches", [])
80
+ context["stressed_patches"] = sar_data.get("stressed_patches", [])
81
+ context["health_summary"] = sar_data.get("health_summary", {})
82
+ context["temporal_trends"] = sar_data.get("temporal_trends", {})
83
+ context["weather_data"] = sar_data.get("weather_data", [])
84
+
85
+ # Fetch Sentinel-2 analysis (vegetation indices)
86
+ s2_data = self._fetch_sentinel2_data(center_lat, center_lon, crop_type, area_acres, farmer_context)
87
+ if s2_data:
88
+ context["vegetation_indices"] = s2_data.get("vegetation_indices", {})
89
+ context["soil_indicators"] = s2_data.get("soil_indicators", {})
90
+ context["llm_analysis"] = s2_data.get("llm_analysis", {})
91
+ context["sentinel2_bands"] = s2_data.get("band_values", {})
92
+
93
+ return context
94
+
95
+ def _fetch_sar_data(
96
+ self,
97
+ bbox: List[float],
98
+ crop_type: str,
99
+ farmer_context: Optional[Dict]
100
+ ) -> Optional[Dict]:
101
+ """Fetch SAR analysis from HF Space."""
102
+ if not bbox or len(bbox) < 4:
103
+ return None
104
+
105
+ try:
106
+ response = requests.post(
107
+ f"{SAR_API_URL}/analyze",
108
+ json={
109
+ "coordinates": bbox,
110
+ "date": datetime.now().strftime("%Y-%m-%d"),
111
+ "crop_type": crop_type,
112
+ "farmer_context": farmer_context
113
+ },
114
+ timeout=self.timeout
115
+ )
116
+
117
+ if response.status_code == 200:
118
+ data = response.json()
119
+ logger.info(f"SAR data fetched: {list(data.keys())}")
120
+ return data
121
+ else:
122
+ logger.warning(f"SAR API error: {response.status_code}")
123
+ return None
124
+
125
+ except Exception as e:
126
+ logger.error(f"SAR fetch error: {e}")
127
+ return None
128
+
129
+ def _fetch_sentinel2_data(
130
+ self,
131
+ center_lat: float,
132
+ center_lon: float,
133
+ crop_type: str,
134
+ area_acres: float,
135
+ farmer_context: Optional[Dict]
136
+ ) -> Optional[Dict]:
137
+ """Fetch Sentinel-2 analysis from HF Space."""
138
+ try:
139
+ field_hectares = area_acres * 0.404686 # Convert acres to hectares
140
+
141
+ response = requests.post(
142
+ f"{SENTINEL2_API_URL}/analyze",
143
+ json={
144
+ "center_lat": center_lat,
145
+ "center_lon": center_lon,
146
+ "crop_type": crop_type,
147
+ "analysis_date": datetime.now().strftime("%Y-%m-%d"),
148
+ "field_size_hectares": field_hectares,
149
+ "farmer_context": farmer_context or {}
150
+ },
151
+ timeout=self.timeout
152
+ )
153
+
154
+ if response.status_code == 200:
155
+ data = response.json()
156
+ logger.info(f"Sentinel-2 data fetched: {list(data.keys())}")
157
+ return data
158
+ else:
159
+ logger.warning(f"Sentinel-2 API error: {response.status_code}")
160
+ return None
161
+
162
+ except Exception as e:
163
+ logger.error(f"Sentinel-2 fetch error: {e}")
164
+ return None
165
+
166
+ def fetch_specific_metrics(
167
+ self,
168
+ center_lat: float,
169
+ center_lon: float,
170
+ area_acres: float,
171
+ metrics: List[str]
172
+ ) -> Dict[str, Any]:
173
+ """
174
+ Fetch specific heatmap metrics.
175
+
176
+ Metrics: soil_moisture, soil_organic_matter, soil_fertility,
177
+ soil_salinity, greenness, nitrogen_level,
178
+ photosynthetic_capacity, pest_risk, disease_risk
179
+ """
180
+ results = {}
181
+ field_hectares = area_acres * 0.404686
182
+
183
+ for metric in metrics:
184
+ try:
185
+ response = requests.post(
186
+ f"{HEATMAP_API_URL}/generate-heatmap",
187
+ json={
188
+ "center_lat": center_lat,
189
+ "center_lon": center_lon,
190
+ "field_size_hectares": field_hectares,
191
+ "metric": metric,
192
+ "gaussian_sigma": 1.5,
193
+ "show_field_boundary": False
194
+ },
195
+ timeout=60
196
+ )
197
+
198
+ if response.status_code == 200:
199
+ data = response.json()
200
+ results[metric] = {
201
+ "min_value": data.get("min_value"),
202
+ "max_value": data.get("max_value"),
203
+ "mean_value": data.get("mean_value"),
204
+ "level": data.get("level"),
205
+ "analysis": data.get("analysis"),
206
+ "stress_score": data.get("stress_score"),
207
+ "recommendations": data.get("recommendations", [])
208
+ }
209
+
210
+ except Exception as e:
211
+ logger.error(f"Heatmap fetch error for {metric}: {e}")
212
+
213
+ return results
214
+
215
+ def format_for_llm(self, context: Dict[str, Any]) -> Dict[str, Any]:
216
+ """
217
+ Format aggregated context for LLM consumption.
218
+ Extracts key values and interpretations.
219
+ """
220
+ formatted = {
221
+ "field_info": context.get("field_info", {}),
222
+ }
223
+
224
+ # Vegetation indices
225
+ veg = context.get("vegetation_indices", {})
226
+ if veg:
227
+ formatted["vegetation_indices"] = {
228
+ "NDVI": {
229
+ "current": veg.get("ndvi", {}).get("mean"),
230
+ "min": veg.get("ndvi", {}).get("min"),
231
+ "max": veg.get("ndvi", {}).get("max"),
232
+ "interpretation": self._interpret_ndvi(veg.get("ndvi", {}).get("mean", 0))
233
+ },
234
+ "NDRE": {
235
+ "current": veg.get("ndre", {}).get("mean"),
236
+ "interpretation": self._interpret_ndre(veg.get("ndre", {}).get("mean", 0))
237
+ },
238
+ "EVI": {
239
+ "current": veg.get("evi", {}).get("mean"),
240
+ },
241
+ "SMI": {
242
+ "current": veg.get("smi", {}).get("mean"),
243
+ "interpretation": self._interpret_smi(veg.get("smi", {}).get("mean", 0))
244
+ }
245
+ }
246
+
247
+ # SAR bands
248
+ sar = context.get("sar_bands", {})
249
+ if sar:
250
+ formatted["sar_bands"] = {
251
+ "VV": sar.get("vv"),
252
+ "VH": sar.get("vh"),
253
+ "VV_VH_ratio": sar.get("ratio")
254
+ }
255
+
256
+ # Health summary
257
+ health = context.get("health_summary", {})
258
+ if health:
259
+ formatted["health_summary"] = health
260
+
261
+ # Stressed patches
262
+ stressed = context.get("stressed_patches", [])
263
+ if stressed:
264
+ formatted["stress_analysis"] = {
265
+ "stressed_patch_count": len(stressed),
266
+ "high_stress_patches": [p for p in stressed if p.get("stress_score", 0) > 0.7],
267
+ "affected_area_percent": sum(p.get("percentage", 0) for p in stressed)
268
+ }
269
+
270
+ # Weather
271
+ weather = context.get("weather_data", [])
272
+ if weather:
273
+ latest = weather[0] if weather else {}
274
+ formatted["weather"] = {
275
+ "current": latest,
276
+ "recent_days": len(weather)
277
+ }
278
+
279
+ # LLM analysis from Sentinel-2
280
+ llm = context.get("llm_analysis", {})
281
+ if llm:
282
+ formatted["previous_analysis"] = {
283
+ "soil_moisture": llm.get("soil_moisture"),
284
+ "soil_fertility": llm.get("soil_fertility"),
285
+ "nitrogen_status": llm.get("nitrogen_status"),
286
+ "overall_health": llm.get("overall_health")
287
+ }
288
+
289
+ return formatted
290
+
291
+ def _interpret_ndvi(self, value: float) -> str:
292
+ if value is None:
293
+ return "unknown"
294
+ if value > 0.7:
295
+ return "excellent_vegetation"
296
+ elif value > 0.5:
297
+ return "healthy_vegetation"
298
+ elif value > 0.3:
299
+ return "moderate_stress"
300
+ elif value > 0.1:
301
+ return "severe_stress"
302
+ else:
303
+ return "bare_soil_or_water"
304
+
305
+ def _interpret_ndre(self, value: float) -> str:
306
+ if value is None:
307
+ return "unknown"
308
+ if value > 0.5:
309
+ return "high_chlorophyll"
310
+ elif value > 0.3:
311
+ return "adequate_chlorophyll"
312
+ elif value > 0.1:
313
+ return "low_chlorophyll"
314
+ else:
315
+ return "chlorophyll_deficiency"
316
+
317
+ def _interpret_smi(self, value: float) -> str:
318
+ if value is None:
319
+ return "unknown"
320
+ if value > 0.6:
321
+ return "adequate_moisture"
322
+ elif value > 0.4:
323
+ return "moderate_moisture"
324
+ elif value > 0.2:
325
+ return "low_moisture"
326
+ else:
327
+ return "critical_moisture_deficit"
328
+
329
+
330
+ # Quick context fetch function
331
+ def fetch_field_context(
332
+ coordinates: Dict[str, Any],
333
+ crop_type: str = "Wheat",
334
+ area_acres: float = 1.0,
335
+ fetch_satellite: bool = True
336
+ ) -> Dict[str, Any]:
337
+ """
338
+ Quick function to fetch and format field context.
339
+
340
+ Args:
341
+ coordinates: {"center_lat": float, "center_lon": float, "bbox": [...]}
342
+ crop_type: Crop type string
343
+ area_acres: Field size
344
+ fetch_satellite: Whether to fetch from HF APIs (set False for quick response)
345
+ """
346
+ aggregator = ContextAggregator()
347
+
348
+ if fetch_satellite:
349
+ raw_context = aggregator.fetch_full_context(
350
+ coordinates=coordinates,
351
+ crop_type=crop_type,
352
+ area_acres=area_acres
353
+ )
354
+ return aggregator.format_for_llm(raw_context)
355
+ else:
356
+ return {
357
+ "field_info": {
358
+ "crop_type": crop_type,
359
+ "area_acres": area_acres,
360
+ "coordinates": coordinates
361
+ }
362
+ }