from typing import Optional, Dict, Any from app.services.bbch_service import get_stage_by_code from app.services.sensor_service import SensorService from app.core.time_engine import TimeEngine from app.schemas.sensor import SensorData from app.services.phenology_service import PhenologyService from app.utils.logger import setup_logger logger = setup_logger() class ContextEngine: def __init__(self): self.sensor_service = SensorService() self.time_engine = TimeEngine() self.phenology_service = PhenologyService() async def build_farming_context( self, crop_type: Optional[str] = None, bbch_stage: Optional[str] = None, crop_source: Optional[str] = None, bbch_source: Optional[str] = None, sensor_data: Optional[SensorData] = None, weather_data: Optional[Dict] = None, disease_detected: Optional[str] = None, disease_image_analyzed: bool = False, sensors_enabled: bool = True, weather_enabled: bool = True, ) -> Dict[str, Any]: context: Dict[str, Any] = { "time_context": {}, "crop_context": {}, "phenology_context": {}, "weather_context": {}, "sensor_context": {}, "disease_context": {}, "recommendations": [], } # Time context["time_context"] = { "date": self.time_engine.get_current_date(), "time": self.time_engine.get_current_time(), "time_of_day": self.time_engine.get_time_of_day(), "season": self.time_engine.get_season(), } # Crop + BBCH if crop_type: context["crop_context"]["crop"] = crop_type context["crop_context"]["crop_source"] = crop_source if bbch_stage: stage_info = get_stage_by_code(bbch_stage, crop_type) if stage_info: context["crop_context"]["bbch_stage"] = { "code": bbch_stage, "name": stage_info.name, "description": stage_info.description, } context["crop_context"]["growth_phase"] = self._get_growth_phase(bbch_stage) # Phenology if crop_type and bbch_stage: pheno = self.phenology_service.get_phenology(crop_type, bbch_stage) if pheno: context["phenology_context"] = pheno if pheno.get("needs"): context["recommendations"].append( f"Stage needs: {', '.join(pheno['needs'][:3])}" ) if pheno.get("risks"): context["recommendations"].append( f"Key risks: {', '.join(pheno['risks'][:3])}" ) # Weather if weather_enabled: if weather_data and weather_data.get("forecast"): context["weather_context"] = weather_data first = weather_data["forecast"][0] if first.get("rain_chance", 0) > 70: context["recommendations"].append( f"High rain ({first['rain_chance']}%) tomorrow — delay irrigation." ) if first.get("temp_max", 0) > 30: context["recommendations"].append( f"Heat ({first['temp_max']}°C) forecast — irrigate early morning." ) if first.get("temp_max", 0) < 5: context["recommendations"].append( f"Frost risk ({first['temp_max']}°C) — protect crops." ) else: context["weather_context"] = {"no_data": True} else: context["weather_context"] = {"disabled": True} # Sensors if sensors_enabled: if sensor_data: # Always inject raw sensor values — analysis requires crop+bbch but raw data does not raw = { "soil_moisture": sensor_data.soil_moisture, "soil_temperature": sensor_data.soil_temperature, "air_temperature": sensor_data.air_temperature, "humidity": sensor_data.humidity, } if crop_type and bbch_stage: try: analysis = self.sensor_service.analyze_sensor_data( sensor_data, crop_type, bbch_stage ) context["sensor_context"] = { **raw, "status": analysis.status, "soil_moisture_status": analysis.soil_moisture_status, "soil_temperature_status": analysis.soil_temperature_status, "air_temperature_status": analysis.air_temperature_status, "humidity_status": analysis.humidity_status, "message": analysis.message, } if analysis.status in ("warning", "critical"): context["recommendations"].append(f"Sensor alert: {analysis.message}") except Exception as e: logger.warning(f"Sensor analysis error: {e}") context["sensor_context"] = {**raw, "status": "unknown", "message": ""} else: # No crop/BBCH context — inject raw values without analysis context["sensor_context"] = {**raw, "status": "unknown", "message": ""} else: context["sensor_context"] = {"no_data": True} else: context["sensor_context"] = {"disabled": True} # Disease (from image analysis or explicit report) if disease_detected and disease_detected.lower() not in ("healthy", "none", ""): context["disease_context"] = { "detected": True, "disease": disease_detected, "natural_description": ( f"{disease_detected} (from image)" if disease_image_analyzed else f"{disease_detected} (reported)" ), "analyzed_from_image": disease_image_analyzed, } context["recommendations"].append( f"Disease detected: {disease_detected}. Check Agridatalogia products or active ingredient recommendations." ) return context def _get_growth_phase(self, bbch_code: str) -> str: try: code_num = int(bbch_code.split("-")[0]) if "-" in bbch_code else int(bbch_code) except (ValueError, AttributeError): return "Unknown" if code_num < 10: return "Germination" elif code_num < 20: return "Leaf development" elif code_num < 30: return "Tillering / Branching" elif code_num < 40: return "Stem elongation" elif code_num < 50: return "Booting / Heading" elif code_num < 60: return "Inflorescence emergence" elif code_num < 70: return "Flowering" elif code_num < 80: return "Fruit development" elif code_num < 90: return "Ripening" else: return "Senescence"