File size: 7,501 Bytes
2f39e98
6f9f2ac
 
 
 
def034a
2f39e98
 
 
 
6f9f2ac
 
 
 
 
def034a
2f39e98
6f9f2ac
 
 
 
3d28307
 
6f9f2ac
 
 
f528de8
 
3d28307
6f9f2ac
2f39e98
6f9f2ac
 
def034a
6f9f2ac
 
 
2f39e98
6f9f2ac
2f39e98
 
6f9f2ac
81d47a9
 
6f9f2ac
2f39e98
81d47a9
2f39e98
 
6f9f2ac
2f39e98
2dbbc07
2f39e98
6f9f2ac
 
 
2f39e98
 
 
 
 
 
 
 
def034a
2f39e98
 
 
 
 
 
 
 
 
 
 
 
 
f528de8
2f39e98
3d28307
2f39e98
 
 
 
 
 
 
 
 
 
 
 
 
f528de8
 
 
 
2f39e98
 
f528de8
b0ae086
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f528de8
 
 
 
2f39e98
 
 
6f9f2ac
 
3d28307
2f39e98
 
 
 
 
6f9f2ac
2f39e98
31f5215
2f39e98
 
6f9f2ac
2f39e98
6f9f2ac
2f39e98
 
 
 
 
6f9f2ac
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2f39e98
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
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"