Agridatalog / app /core /context_engine.py
Agridatalogia's picture
Rebrand: AllTech → Agridatalogia (UI, AI identity, logo layout)
31f5215
Raw
History Blame Contribute Delete
7.5 kB
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"