Spaces:
Sleeping
Sleeping
| 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" | |