Aniket2006 commited on
Commit
fdcc2cb
·
1 Parent(s): 07c8371

Add /take-action-reasoning endpoint with stress clusters + timeseries + farmer profile + weather context

Browse files
Files changed (1) hide show
  1. app.py +262 -0
app.py CHANGED
@@ -734,6 +734,268 @@ async def get_heatmap_image(
734
  return Response(content=base64.b64decode(response.image_base64), media_type="image/png")
735
 
736
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
737
  if __name__ == "__main__":
738
  import uvicorn
739
  uvicorn.run(app, host="0.0.0.0", port=7860)
 
734
  return Response(content=base64.b64decode(response.image_base64), media_type="image/png")
735
 
736
 
737
+ # ============================================================================
738
+ # TAKE ACTION REASONING ENDPOINT
739
+ # ============================================================================
740
+
741
+ class TakeActionRequest(BaseModel):
742
+ """Request model for take-action reasoning."""
743
+ center_lat: float
744
+ center_lon: float
745
+ field_size_hectares: float
746
+ category: str # e.g., "field_variability", "irrigation", "pest_risk"
747
+ # Context data
748
+ stress_clusters: Optional[List[Dict[str, Any]]] = None # From CNN+LSTM model
749
+ indices_timeseries: Optional[Dict[str, Any]] = None # Historical + forecast for all indices
750
+ farmer_profile: Optional[Dict[str, Any]] = None # Questionnaire data
751
+ weather_data: Optional[Dict[str, Any]] = None # Current + forecast weather
752
+
753
+
754
+ class TakeActionResponse(BaseModel):
755
+ """Response model for take-action reasoning."""
756
+ success: bool
757
+ category: str
758
+ high_zones: List[Dict[str, Any]] # High performing/stress zones with coordinates
759
+ low_zones: List[Dict[str, Any]] # Low performing/stress zones with coordinates
760
+ recommendations: str # Main recommendation text
761
+ risk_suggestions: List[str] # List of risk suggestions
762
+ detailed_analysis: str # Detailed LLM analysis
763
+ stress_score: float
764
+ cluster_distribution: Dict[str, int]
765
+
766
+
767
+ def run_take_action_llm(category: str, stress_clusters: list, indices_data: dict,
768
+ farmer_profile: dict, weather_data: dict) -> dict:
769
+ """Run LLM analysis for Take Action reasoning with comprehensive context."""
770
+ from groq import Groq
771
+ import json
772
+
773
+ GROQ_MODEL = "llama-3.3-70b-versatile"
774
+
775
+ # Format stress clusters
776
+ cluster_text = "\n\nSTRESS CLUSTER DATA (CNN+LSTM Analysis):\n"
777
+ cluster_text += "=" * 50 + "\n"
778
+ if stress_clusters:
779
+ for i, cluster in enumerate(stress_clusters):
780
+ cluster_text += f"\nCluster {i+1}:\n"
781
+ cluster_text += f" - Location: ({cluster.get('lat', 0):.6f}, {cluster.get('lon', 0):.6f})\n"
782
+ cluster_text += f" - Stress Score: {cluster.get('stress_score', 0):.3f}\n"
783
+ cluster_text += f" - Category: {cluster.get('category', 'Unknown')}\n"
784
+ cluster_text += f" - Severity: {cluster.get('severity', 'Moderate')}\n"
785
+ else:
786
+ cluster_text += "No stress clusters detected - field appears healthy.\n"
787
+
788
+ # Format indices timeseries
789
+ ts_text = "\n\nINDICES TIME SERIES (Historical + Forecast):\n"
790
+ ts_text += "=" * 50 + "\n"
791
+ if indices_data:
792
+ for index_name, data in indices_data.items():
793
+ ts_text += f"\n{index_name}:\n"
794
+ if data.get('historical'):
795
+ hist = data['historical']
796
+ if len(hist) > 0:
797
+ first_val = hist[0].get('value', 0) if isinstance(hist[0], dict) else 0
798
+ last_val = hist[-1].get('value', 0) if isinstance(hist[-1], dict) else 0
799
+ ts_text += f" Historical: {first_val:.3f} → {last_val:.3f} (change: {last_val-first_val:+.3f})\n"
800
+ if data.get('forecast'):
801
+ fcast = data['forecast']
802
+ if len(fcast) > 0:
803
+ first_val = fcast[0].get('value', 0) if isinstance(fcast[0], dict) else 0
804
+ last_val = fcast[-1].get('value', 0) if isinstance(fcast[-1], dict) else 0
805
+ ts_text += f" Forecast: {first_val:.3f} → {last_val:.3f} (predicted: {last_val-first_val:+.3f})\n"
806
+
807
+ # Format farmer profile
808
+ farmer_text = "\n\nFARMER PROFILE (Questionnaire Data):\n"
809
+ farmer_text += "=" * 40 + "\n"
810
+ if farmer_profile:
811
+ farmer_text += f"- Crop Type: {farmer_profile.get('crop_type', 'Unknown')}\n"
812
+ farmer_text += f"- Field Size: {farmer_profile.get('field_size', 'Unknown')} hectares\n"
813
+ farmer_text += f"- Irrigation Method: {farmer_profile.get('irrigation_method', 'Unknown')}\n"
814
+ farmer_text += f"- Experience Level: {farmer_profile.get('experience', 'Unknown')}\n"
815
+ farmer_text += f"- Primary Goal: {farmer_profile.get('primary_goal', 'Maximize yield')}\n"
816
+ farmer_text += f"- Budget Constraints: {farmer_profile.get('budget', 'Moderate')}\n"
817
+ else:
818
+ farmer_text += "No farmer profile data available.\n"
819
+
820
+ # Format weather data
821
+ weather_text = "\n\nWEATHER CONDITIONS:\n"
822
+ weather_text += "=" * 30 + "\n"
823
+ if weather_data:
824
+ weather_text += f"- Temperature: {weather_data.get('temperature', 'N/A')}°C\n"
825
+ weather_text += f"- Humidity: {weather_data.get('humidity', 'N/A')}%\n"
826
+ weather_text += f"- Precipitation: {weather_data.get('precipitation', 'N/A')} mm\n"
827
+ weather_text += f"- Conditions: {weather_data.get('conditions', 'N/A')}\n"
828
+ weather_text += f"- Forecast: {weather_data.get('forecast', 'N/A')}\n"
829
+ else:
830
+ weather_text += "No weather data available.\n"
831
+
832
+ # Category-specific prompts
833
+ category_prompts = {
834
+ 'field_variability': "high and low performing zones, zonal management recommendations",
835
+ 'yield_stability': "yield stability patterns, management priority zones",
836
+ 'irrigation': "irrigation scheduling, water stress zones, optimal watering times",
837
+ 'vegetation_health': "vegetation health patterns, chlorophyll status, growth anomalies",
838
+ 'nutrient': "nutrient deficiency zones, chlorophyll patterns, fertilization recommendations",
839
+ 'pest_damage': "pest risk zones, damage detection areas, treatment priorities"
840
+ }
841
+
842
+ focus = category_prompts.get(category, "comprehensive field analysis")
843
+
844
+ prompt = f"""TAKE ACTION ANALYSIS REQUEST
845
+
846
+ {cluster_text}
847
+ {ts_text}
848
+ {farmer_text}
849
+ {weather_text}
850
+
851
+ CATEGORY: {category.upper().replace('_', ' ')}
852
+ FOCUS: {focus}
853
+
854
+ Based on the stress cluster data, indices trends, farmer profile, and weather conditions, provide actionable recommendations.
855
+
856
+ Respond with ONLY a valid JSON object:
857
+ {{
858
+ "high_zones": [
859
+ {{"lat": 0.0, "lon": 0.0, "score": 0.0, "label": "Zone description"}}
860
+ ],
861
+ "low_zones": [
862
+ {{"lat": 0.0, "lon": 0.0, "score": 0.0, "label": "Zone description"}}
863
+ ],
864
+ "recommendations": "2-3 sentences of main recommendation based on data",
865
+ "risk_suggestions": ["Risk 1 with action", "Risk 2 with action", "Risk 3 with action"],
866
+ "detailed_analysis": "4-5 sentences explaining the stress patterns, their causes based on indices trends and weather, and specific actions to take considering the farmer's goals and constraints."
867
+ }}
868
+ """
869
+
870
+ # Try each API key with cascading fallback
871
+ last_error = None
872
+ for i, api_key in enumerate(GROQ_API_KEYS):
873
+ try:
874
+ logger.info(f"[TakeAction] Trying Groq API key {i+1}/{len(GROQ_API_KEYS)}")
875
+ client = Groq(api_key=api_key)
876
+
877
+ chat_completion = client.chat.completions.create(
878
+ messages=[
879
+ {"role": "system", "content": "You are an expert agricultural advisor. Provide data-driven, actionable recommendations. Respond with valid JSON only."},
880
+ {"role": "user", "content": prompt}
881
+ ],
882
+ model=GROQ_MODEL,
883
+ temperature=0.7,
884
+ max_tokens=2000,
885
+ )
886
+
887
+ response_text = chat_completion.choices[0].message.content.strip()
888
+
889
+ # Clean markdown if present
890
+ if response_text.startswith("```"):
891
+ lines = response_text.split("\n")
892
+ response_text = "\n".join(lines[1:-1])
893
+ if response_text.startswith("json"):
894
+ response_text = response_text[4:].strip()
895
+
896
+ result = json.loads(response_text)
897
+ logger.info(f"[TakeAction] Groq API key {i+1} succeeded")
898
+ return result
899
+
900
+ except Exception as e:
901
+ last_error = e
902
+ logger.warning(f"[TakeAction] Groq API key {i+1} failed: {e}")
903
+ continue
904
+
905
+ # All keys failed - return fallback
906
+ logger.error(f"[TakeAction] All API keys failed. Last error: {last_error}")
907
+ return {
908
+ "high_zones": [],
909
+ "low_zones": [],
910
+ "recommendations": "Unable to generate recommendations. Please try again.",
911
+ "risk_suggestions": ["Manual field inspection recommended"],
912
+ "detailed_analysis": "Analysis unavailable due to API errors. Please refresh to try again."
913
+ }
914
+
915
+
916
+ @app.post("/take-action-reasoning", response_model=TakeActionResponse)
917
+ async def take_action_reasoning(request: TakeActionRequest):
918
+ """Generate comprehensive LLM reasoning for Take Action pages."""
919
+
920
+ try:
921
+ logger.info(f"[TakeAction] Processing {request.category} for ({request.center_lat}, {request.center_lon})")
922
+
923
+ # If no stress clusters provided, generate them using stress detection
924
+ stress_clusters = request.stress_clusters or []
925
+
926
+ if not stress_clusters:
927
+ # Run stress detection to get clusters
928
+ try:
929
+ config = get_sh_config()
930
+ bbox_coords = calculate_bbox(request.center_lat, request.center_lon, request.field_size_hectares)
931
+ end_date = datetime.now()
932
+ start_date = end_date - timedelta(days=30)
933
+
934
+ bands = fetch_sentinel_data(config, bbox_coords, start_date, end_date, ['B04', 'B08', 'B03', 'B02', 'B05'])
935
+
936
+ if bands:
937
+ stress_results = detect_stress_zones(bands)
938
+ # Convert stress map to cluster points
939
+ stress_map = stress_results['stress_map']
940
+ for cluster_id, count in stress_results['cluster_distribution'].items():
941
+ if 'severe' in cluster_id.lower() or 'high' in cluster_id.lower():
942
+ # Add as high stress zone
943
+ stress_clusters.append({
944
+ 'lat': request.center_lat + np.random.uniform(-0.001, 0.001),
945
+ 'lon': request.center_lon + np.random.uniform(-0.001, 0.001),
946
+ 'stress_score': 0.8,
947
+ 'category': cluster_id,
948
+ 'severity': 'High'
949
+ })
950
+ elif 'moderate' in cluster_id.lower():
951
+ stress_clusters.append({
952
+ 'lat': request.center_lat + np.random.uniform(-0.001, 0.001),
953
+ 'lon': request.center_lon + np.random.uniform(-0.001, 0.001),
954
+ 'stress_score': 0.5,
955
+ 'category': cluster_id,
956
+ 'severity': 'Moderate'
957
+ })
958
+ except Exception as e:
959
+ logger.warning(f"[TakeAction] Stress detection failed: {e}")
960
+
961
+ # Run LLM analysis
962
+ llm_result = run_take_action_llm(
963
+ category=request.category,
964
+ stress_clusters=stress_clusters,
965
+ indices_data=request.indices_timeseries or {},
966
+ farmer_profile=request.farmer_profile or {},
967
+ weather_data=request.weather_data or {}
968
+ )
969
+
970
+ # Calculate overall stress score
971
+ stress_score = 0.0
972
+ if stress_clusters:
973
+ stress_score = sum(c.get('stress_score', 0) for c in stress_clusters) / len(stress_clusters)
974
+
975
+ # Cluster distribution
976
+ cluster_dist = {}
977
+ for cluster in stress_clusters:
978
+ cat = cluster.get('severity', 'Unknown')
979
+ cluster_dist[cat] = cluster_dist.get(cat, 0) + 1
980
+
981
+ return TakeActionResponse(
982
+ success=True,
983
+ category=request.category,
984
+ high_zones=llm_result.get('high_zones', []),
985
+ low_zones=llm_result.get('low_zones', []),
986
+ recommendations=llm_result.get('recommendations', ''),
987
+ risk_suggestions=llm_result.get('risk_suggestions', []),
988
+ detailed_analysis=llm_result.get('detailed_analysis', ''),
989
+ stress_score=stress_score,
990
+ cluster_distribution=cluster_dist
991
+ )
992
+
993
+ except Exception as e:
994
+ logger.error(f"[TakeAction] Error: {e}")
995
+ logger.error(traceback.format_exc())
996
+ raise HTTPException(500, str(e))
997
+
998
+
999
  if __name__ == "__main__":
1000
  import uvicorn
1001
  uvicorn.run(app, host="0.0.0.0", port=7860)