FajarHidaa commited on
Commit
9bc554e
·
verified ·
1 Parent(s): 87dd0fd

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +578 -6
app.py CHANGED
@@ -119,15 +119,36 @@ VALID_SEVERITY = {"low", "medium", "high"}
119
  VALID_CONFIDENCE = {"low", "medium", "high"}
120
  VALID_PRIORITY = {"low", "medium", "high"}
121
 
 
122
  WEATHER_VARIABLES = [
123
- "temperature_2m_max", "temperature_2m_min", "rain_sum", "precipitation_sum",
124
- "wind_gusts_10m_max", "shortwave_radiation_sum", "temperature_2m_mean",
125
- "cloud_cover_mean", "et0_fao_evapotranspiration",
126
- "growing_degree_days_base_0_limit_50", "leaf_wetness_probability_mean",
127
- "vapour_pressure_deficit_max"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
  ]
129
 
130
 
 
131
  # ------------------------------------------------------------------
132
  # DATA CLASSES
133
  # ------------------------------------------------------------------
@@ -780,6 +801,7 @@ def handle_weather_and_analyze(weather_json_str: str, lat: float, lon: float, cr
780
  "current_concern": current_concern,
781
  },
782
  "raw_weather": raw_weather,
 
783
  "analysis": llm_output,
784
  "created_at": datetime.now(timezone.utc).isoformat()
785
  }
@@ -829,6 +851,480 @@ def handle_weather_and_analyze(weather_json_str: str, lat: float, lon: float, cr
829
  # ------------------------------------------------------------------
830
  # 4. ANALYTICS ENGINE
831
  # ------------------------------------------------------------------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
832
  def normalize_weather_data(raw_data: Dict, lat: float, lon: float, client_date: str) -> Tuple[Optional[Dict], str]:
833
  try:
834
  timezone_str = raw_data.get("timezone", "UTC")
@@ -880,6 +1376,12 @@ def normalize_weather_data(raw_data: Dict, lat: float, lon: float, client_date:
880
  "today": today,
881
  "forecast": forecast
882
  }
 
 
 
 
 
 
883
  return normalized, ""
884
 
885
  except Exception as e:
@@ -914,6 +1416,49 @@ def build_llm_payload(normalized_data: Dict, lat: float, lon: float, client_date
914
  "location_timezone": location.get("timezone", "UTC")
915
  },
916
  "units": normalized_data.get("units", {}),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
917
  "historical": {"daily": normalized_data.get("historical", {})},
918
  "forecast": {"daily": normalized_data.get("forecast", {})}
919
  }
@@ -1026,12 +1571,38 @@ def build_export_file(cache_entry: Optional[Dict]) -> Optional[str]:
1026
  lines.append(f"- Current Concern : {field_context.get('current_concern') or '-'}")
1027
  lines.append("")
1028
  lines.append("--- DATA CUACA MENTAH (Ecwf, historical 7 hari + forecast 7 hari) ---")
1029
- lines.append(json.dumps(cache_entry.get("raw_weather", {}), ensure_ascii=False, indent=2))
 
 
 
 
 
 
 
1030
  lines.append("")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1031
  lines.append("--- HASIL ANALISIS GEMINI AI ---")
1032
  lines.append(f"Ringkasan: {analysis.get('summary', '-')}")
1033
  lines.append("")
1034
 
 
 
 
 
 
1035
  hist = analysis.get("historical", {}) or {}
1036
  lines.append("Historical (7 hari terakhir):")
1037
  lines.append(f" Kondisi : {hist.get('condition', '-')}")
@@ -1455,6 +2026,7 @@ def handle_analyze(lat: float, lon: float, state_dict: Dict):
1455
  "current_concern": current_concern,
1456
  },
1457
  "raw_weather": raw_weather,
 
1458
  "analysis": llm_output,
1459
  "created_at": datetime.now(timezone.utc).isoformat()
1460
  }
 
119
  VALID_CONFIDENCE = {"low", "medium", "high"}
120
  VALID_PRIORITY = {"low", "medium", "high"}
121
 
122
+
123
  WEATHER_VARIABLES = [
124
+ # Temperature and plant development
125
+ "temperature_2m_max",
126
+ "temperature_2m_min",
127
+ "temperature_2m_mean",
128
+ "growing_degree_days_base_0_limit_50",
129
+
130
+ # Rainfall and water balance
131
+ "precipitation_sum",
132
+ "precipitation_hours",
133
+ "precipitation_probability_max",
134
+ "et0_fao_evapotranspiration",
135
+
136
+ # Humidity and disease-related conditions
137
+ "relative_humidity_2m_mean",
138
+ "relative_humidity_2m_max",
139
+ "dew_point_2m_mean",
140
+ "leaf_wetness_probability_mean",
141
+ "vapour_pressure_deficit_max",
142
+
143
+ # Radiation, clouds, and wind
144
+ "shortwave_radiation_sum",
145
+ "cloud_cover_mean",
146
+ "wind_speed_10m_max",
147
+ "wind_gusts_10m_max",
148
  ]
149
 
150
 
151
+
152
  # ------------------------------------------------------------------
153
  # DATA CLASSES
154
  # ------------------------------------------------------------------
 
801
  "current_concern": current_concern,
802
  },
803
  "raw_weather": raw_weather,
804
+ "derived_features": normalized.get("derived_features", {}),
805
  "analysis": llm_output,
806
  "created_at": datetime.now(timezone.utc).isoformat()
807
  }
 
851
  # ------------------------------------------------------------------
852
  # 4. ANALYTICS ENGINE
853
  # ------------------------------------------------------------------
854
+
855
+
856
+
857
+ # ------------------------------------------------------------------
858
+ # AGRICULTURAL WEATHER FEATURE ENGINEERING
859
+ # ------------------------------------------------------------------
860
+
861
+ def _weather_num(value):
862
+ """Convert valid finite numeric values to float; otherwise None."""
863
+ try:
864
+ if value is None:
865
+ return None
866
+ number = float(value)
867
+ return number if math.isfinite(number) else None
868
+ except (TypeError, ValueError):
869
+ return None
870
+
871
+
872
+ def _weather_probability_fraction(value, unit=None):
873
+ """
874
+ Normalize a probability to 0-1.
875
+ Open-Meteo probability variables commonly use percent units.
876
+ """
877
+ number = _weather_num(value)
878
+ if number is None:
879
+ return None
880
+
881
+ if unit and "%" in str(unit):
882
+ number /= 100.0
883
+ elif number > 1.0:
884
+ number /= 100.0
885
+
886
+ return max(0.0, min(1.0, number))
887
+
888
+
889
+ def engineer_weather_features(normalized):
890
+ """
891
+ Build derived agricultural weather features from the existing
892
+ daily arrays. No crop, soil, or sensor input is required.
893
+
894
+ Input:
895
+ normalized["historical"], normalized["today"],
896
+ normalized["forecast"], normalized["units"]
897
+
898
+ Output:
899
+ Separate daily feature arrays plus period summaries.
900
+ """
901
+
902
+ units = normalized.get("units", {}) or {}
903
+ periods = ("historical", "today", "forecast")
904
+ records = []
905
+
906
+ # 1. Combine the three time periods chronologically.
907
+ for period in periods:
908
+ block = normalized.get(period, {}) or {}
909
+ dates = block.get("time", []) or []
910
+
911
+ for i, date in enumerate(dates):
912
+ raw = {}
913
+
914
+ for variable in WEATHER_VARIABLES:
915
+ values = block.get(variable, [])
916
+ value = (
917
+ values[i]
918
+ if isinstance(values, list) and i < len(values)
919
+ else None
920
+ )
921
+ raw[variable] = _weather_num(value)
922
+
923
+ rh = raw.get("relative_humidity_2m_mean")
924
+ leaf_wetness = _weather_probability_fraction(
925
+ raw.get("leaf_wetness_probability_mean"),
926
+ units.get("leaf_wetness_probability_mean"),
927
+ )
928
+ precip_probability = _weather_probability_fraction(
929
+ raw.get("precipitation_probability_max"),
930
+ units.get("precipitation_probability_max"),
931
+ )
932
+
933
+ temperature_mean = raw.get("temperature_2m_mean")
934
+ temperature_min = raw.get("temperature_2m_min")
935
+ temperature_max = raw.get("temperature_2m_max")
936
+ dewpoint_mean = raw.get("dew_point_2m_mean")
937
+ precipitation = raw.get("precipitation_sum")
938
+ precipitation_hours = raw.get("precipitation_hours")
939
+ et0 = raw.get("et0_fao_evapotranspiration")
940
+
941
+ water_balance = (
942
+ precipitation - et0
943
+ if precipitation is not None and et0 is not None
944
+ else None
945
+ )
946
+
947
+ features = {
948
+ # Temperature
949
+ "temperature_range_c": (
950
+ temperature_max - temperature_min
951
+ if temperature_max is not None
952
+ and temperature_min is not None
953
+ else None
954
+ ),
955
+
956
+ "dewpoint_depression_proxy_c": (
957
+ temperature_mean - dewpoint_mean
958
+ if temperature_mean is not None
959
+ and dewpoint_mean is not None
960
+ else None
961
+ ),
962
+
963
+ # Water balance proxy
964
+ "water_balance_proxy_mm": water_balance,
965
+
966
+ "precipitation_per_wet_hour_proxy": (
967
+ precipitation / precipitation_hours
968
+ if precipitation is not None
969
+ and precipitation_hours is not None
970
+ and precipitation_hours > 0
971
+ else None
972
+ ),
973
+
974
+ # Normalized probabilities
975
+ "leaf_wetness_fraction": leaf_wetness,
976
+ "precip_probability_fraction": precip_probability,
977
+
978
+ # Humidity and leaf-wetness interactions.
979
+ # These are environmental proxies, NOT disease probabilities.
980
+ "humidity_wetness_interaction": (
981
+ (rh / 100.0) * leaf_wetness
982
+ if rh is not None and leaf_wetness is not None
983
+ else None
984
+ ),
985
+
986
+ "wetness_duration_interaction": (
987
+ leaf_wetness
988
+ * max(0.0, min(1.0, precipitation_hours / 24.0))
989
+ if leaf_wetness is not None
990
+ and precipitation_hours is not None
991
+ else None
992
+ ),
993
+ }
994
+
995
+ records.append({
996
+ "date": date,
997
+ "period": period,
998
+ "raw": raw,
999
+ "features": features,
1000
+ })
1001
+
1002
+ # 2. Consecutive days with nearly zero recorded precipitation.
1003
+ # 0.1 mm is an operational definition of a nearly rain-free day,
1004
+ # not a universal crop drought threshold.
1005
+ dry_streak = 0
1006
+
1007
+ for record in records:
1008
+ precipitation = record["raw"].get("precipitation_sum")
1009
+
1010
+ if precipitation is None:
1011
+ dry_streak = 0
1012
+ record["features"]["dry_spell_days"] = None
1013
+ elif precipitation <= 0.1:
1014
+ dry_streak += 1
1015
+ record["features"]["dry_spell_days"] = dry_streak
1016
+ else:
1017
+ dry_streak = 0
1018
+ record["features"]["dry_spell_days"] = 0
1019
+
1020
+ # 3. Trailing rolling sums and means across the continuous timeline.
1021
+ # Missing values are not replaced with zero.
1022
+ def rolling_value(index, source, key, window, operation):
1023
+ if index + 1 < window:
1024
+ return None
1025
+
1026
+ subset = records[index - window + 1:index + 1]
1027
+
1028
+ if source == "raw":
1029
+ values = [r["raw"].get(key) for r in subset]
1030
+ else:
1031
+ values = [r["features"].get(key) for r in subset]
1032
+
1033
+ if any(v is None for v in values):
1034
+ return None
1035
+
1036
+ if operation == "sum":
1037
+ return sum(values)
1038
+
1039
+ return sum(values) / len(values)
1040
+
1041
+ rolling_specs = {
1042
+ "precipitation_sum": ("raw", "precipitation_sum", "sum", "precipitation"),
1043
+ "et0_fao_evapotranspiration": (
1044
+ "raw", "et0_fao_evapotranspiration", "sum", "et0"
1045
+ ),
1046
+ "water_balance_proxy_mm": (
1047
+ "feature", "water_balance_proxy_mm", "sum", "water_balance"
1048
+ ),
1049
+ "growing_degree_days_base_0_limit_50": (
1050
+ "raw", "growing_degree_days_base_0_limit_50", "sum", "gdd"
1051
+ ),
1052
+ "temperature_2m_mean": (
1053
+ "raw", "temperature_2m_mean", "mean", "temperature_mean"
1054
+ ),
1055
+ "vapour_pressure_deficit_max": (
1056
+ "raw", "vapour_pressure_deficit_max", "mean", "vpd"
1057
+ ),
1058
+ "relative_humidity_2m_mean": (
1059
+ "raw", "relative_humidity_2m_mean", "mean", "rh_mean_pct"
1060
+ ),
1061
+ "leaf_wetness_fraction": (
1062
+ "feature", "leaf_wetness_fraction", "mean", "leaf_wetness"
1063
+ ),
1064
+ }
1065
+
1066
+ for i, record in enumerate(records):
1067
+ for window in (3, 7):
1068
+ for _, (source, key, operation, prefix) in rolling_specs.items():
1069
+ result_key = f"{prefix}_{window}d_{operation}"
1070
+ record["features"][result_key] = rolling_value(
1071
+ i, source, key, window, operation
1072
+ )
1073
+
1074
+ # 4. One-day changes; values compare against the immediately
1075
+ # preceding calendar day, including across history/today/forecast.
1076
+ delta_specs = {
1077
+ "temperature_mean_delta_1d": (
1078
+ "raw", "temperature_2m_mean"
1079
+ ),
1080
+ "vpd_delta_1d": (
1081
+ "raw", "vapour_pressure_deficit_max"
1082
+ ),
1083
+ "rh_mean_delta_1d": (
1084
+ "raw", "relative_humidity_2m_mean"
1085
+ ),
1086
+ "precipitation_delta_1d": (
1087
+ "raw", "precipitation_sum"
1088
+ ),
1089
+ "leaf_wetness_delta_1d": (
1090
+ "feature", "leaf_wetness_fraction"
1091
+ ),
1092
+ }
1093
+
1094
+ for i, record in enumerate(records):
1095
+ for feature_name in delta_specs:
1096
+ record["features"][feature_name] = None
1097
+
1098
+ if i == 0:
1099
+ continue
1100
+
1101
+ previous = records[i - 1]
1102
+
1103
+ for feature_name, (source, key) in delta_specs.items():
1104
+ current_value = (
1105
+ record["raw"].get(key)
1106
+ if source == "raw"
1107
+ else record["features"].get(key)
1108
+ )
1109
+ previous_value = (
1110
+ previous["raw"].get(key)
1111
+ if source == "raw"
1112
+ else previous["features"].get(key)
1113
+ )
1114
+
1115
+ if current_value is not None and previous_value is not None:
1116
+ record["features"][feature_name] = (
1117
+ current_value - previous_value
1118
+ )
1119
+
1120
+ # 5. Forecast daily values relative to the historical 7-day mean.
1121
+ anomaly_specs = {
1122
+ "temperature_mean_delta_vs_history7d": (
1123
+ "raw", "temperature_2m_mean"
1124
+ ),
1125
+ "vpd_delta_vs_history7d": (
1126
+ "raw", "vapour_pressure_deficit_max"
1127
+ ),
1128
+ "rh_mean_delta_vs_history7d": (
1129
+ "raw", "relative_humidity_2m_mean"
1130
+ ),
1131
+ "precipitation_delta_vs_history7d": (
1132
+ "raw", "precipitation_sum"
1133
+ ),
1134
+ "et0_delta_vs_history7d": (
1135
+ "raw", "et0_fao_evapotranspiration"
1136
+ ),
1137
+ "leaf_wetness_delta_vs_history7d": (
1138
+ "feature", "leaf_wetness_fraction"
1139
+ ),
1140
+ }
1141
+
1142
+ historical_records = [
1143
+ r for r in records if r["period"] == "historical"
1144
+ ]
1145
+
1146
+ baselines = {}
1147
+
1148
+ for feature_name, (source, key) in anomaly_specs.items():
1149
+ values = [
1150
+ r["raw"].get(key) if source == "raw"
1151
+ else r["features"].get(key)
1152
+ for r in historical_records
1153
+ ]
1154
+ values = [v for v in values if v is not None]
1155
+
1156
+ baselines[feature_name] = (
1157
+ sum(values) / len(values) if values else None
1158
+ )
1159
+
1160
+ for record in records:
1161
+ for feature_name, (source, key) in anomaly_specs.items():
1162
+ record["features"][feature_name] = None
1163
+
1164
+ if record["period"] != "forecast":
1165
+ continue
1166
+
1167
+ value = (
1168
+ record["raw"].get(key) if source == "raw"
1169
+ else record["features"].get(key)
1170
+ )
1171
+ baseline = baselines[feature_name]
1172
+
1173
+ if value is not None and baseline is not None:
1174
+ record["features"][feature_name] = value - baseline
1175
+
1176
+ # 6. Return feature arrays grouped by the original time windows.
1177
+ all_feature_names = sorted({
1178
+ key
1179
+ for record in records
1180
+ for key in record["features"]
1181
+ })
1182
+
1183
+ derived_blocks = {}
1184
+
1185
+ for period in periods:
1186
+ period_records = [
1187
+ r for r in records if r["period"] == period
1188
+ ]
1189
+
1190
+ derived_blocks[period] = {
1191
+ "time": [r["date"] for r in period_records],
1192
+ "features": {
1193
+ name: [
1194
+ r["features"].get(name)
1195
+ for r in period_records
1196
+ ]
1197
+ for name in all_feature_names
1198
+ },
1199
+ }
1200
+
1201
+ # 7. Compact period-level summary for the LLM.
1202
+ def mean_values(values):
1203
+ values = [v for v in values if v is not None]
1204
+ return sum(values) / len(values) if values else None
1205
+
1206
+ def total_values(values):
1207
+ # If any daily value is missing, avoid presenting a partial
1208
+ # sum as the complete period total.
1209
+ if not values or any(v is None for v in values):
1210
+ return None
1211
+ return sum(values)
1212
+
1213
+ def summarize_period(period):
1214
+ subset = [r for r in records if r["period"] == period]
1215
+
1216
+ def raw_values(key):
1217
+ return [r["raw"].get(key) for r in subset]
1218
+
1219
+ def feature_values(key):
1220
+ return [r["features"].get(key) for r in subset]
1221
+
1222
+ return {
1223
+ "days": len(subset),
1224
+ "temperature_mean_c": mean_values(
1225
+ raw_values("temperature_2m_mean")
1226
+ ),
1227
+ "temperature_max_peak_c": (
1228
+ max([v for v in raw_values("temperature_2m_max")
1229
+ if v is not None], default=None)
1230
+ ),
1231
+ "temperature_min_low_c": (
1232
+ min([v for v in raw_values("temperature_2m_min")
1233
+ if v is not None], default=None)
1234
+ ),
1235
+ "temperature_range_mean_c": mean_values(
1236
+ feature_values("temperature_range_c")
1237
+ ),
1238
+ "precipitation_total_mm": total_values(
1239
+ raw_values("precipitation_sum")
1240
+ ),
1241
+ "precipitation_hours_total": total_values(
1242
+ raw_values("precipitation_hours")
1243
+ ),
1244
+ "et0_total_mm": total_values(
1245
+ raw_values("et0_fao_evapotranspiration")
1246
+ ),
1247
+ "water_balance_proxy_total_mm": total_values(
1248
+ feature_values("water_balance_proxy_mm")
1249
+ ),
1250
+ "gdd_total": total_values(
1251
+ raw_values("growing_degree_days_base_0_limit_50")
1252
+ ),
1253
+ "vpd_mean": mean_values(
1254
+ raw_values("vapour_pressure_deficit_max")
1255
+ ),
1256
+ "relative_humidity_mean_pct": mean_values(
1257
+ raw_values("relative_humidity_2m_mean")
1258
+ ),
1259
+ "leaf_wetness_mean_fraction": mean_values(
1260
+ feature_values("leaf_wetness_fraction")
1261
+ ),
1262
+ "humidity_wetness_interaction_mean": mean_values(
1263
+ feature_values("humidity_wetness_interaction")
1264
+ ),
1265
+ "longest_dry_spell_days": max(
1266
+ [v for v in feature_values("dry_spell_days")
1267
+ if v is not None],
1268
+ default=None
1269
+ ),
1270
+ "max_wind_speed": max(
1271
+ [v for v in raw_values("wind_speed_10m_max")
1272
+ if v is not None],
1273
+ default=None
1274
+ ),
1275
+ "max_wind_gust": max(
1276
+ [v for v in raw_values("wind_gusts_10m_max")
1277
+ if v is not None],
1278
+ default=None
1279
+ ),
1280
+ "precip_probability_mean_fraction": mean_values(
1281
+ feature_values("precip_probability_fraction")
1282
+ ),
1283
+ }
1284
+
1285
+ history_summary = summarize_period("historical")
1286
+ forecast_summary = summarize_period("forecast")
1287
+
1288
+ comparison_keys = [
1289
+ "temperature_mean_c",
1290
+ "precipitation_total_mm",
1291
+ "et0_total_mm",
1292
+ "water_balance_proxy_total_mm",
1293
+ "gdd_total",
1294
+ "vpd_mean",
1295
+ "relative_humidity_mean_pct",
1296
+ "leaf_wetness_mean_fraction",
1297
+ ]
1298
+
1299
+ comparison = {}
1300
+
1301
+ for key in comparison_keys:
1302
+ h = history_summary.get(key)
1303
+ f = forecast_summary.get(key)
1304
+
1305
+ comparison[key + "_forecast_minus_history"] = (
1306
+ f - h if h is not None and f is not None else None
1307
+ )
1308
+
1309
+ return {
1310
+ "historical": derived_blocks["historical"],
1311
+ "today": derived_blocks["today"],
1312
+ "forecast": derived_blocks["forecast"],
1313
+ "summary": {
1314
+ "history_7d": history_summary,
1315
+ "forecast_7d": forecast_summary,
1316
+ "forecast_minus_history": comparison,
1317
+ },
1318
+ }
1319
+
1320
+
1321
+
1322
+
1323
+
1324
+
1325
+
1326
+
1327
+
1328
  def normalize_weather_data(raw_data: Dict, lat: float, lon: float, client_date: str) -> Tuple[Optional[Dict], str]:
1329
  try:
1330
  timezone_str = raw_data.get("timezone", "UTC")
 
1376
  "today": today,
1377
  "forecast": forecast
1378
  }
1379
+
1380
+
1381
+ normalized["derived_features"] = engineer_weather_features(
1382
+ normalized
1383
+ )
1384
+
1385
  return normalized, ""
1386
 
1387
  except Exception as e:
 
1416
  "location_timezone": location.get("timezone", "UTC")
1417
  },
1418
  "units": normalized_data.get("units", {}),
1419
+
1420
+
1421
+
1422
+ # NEW: engineered agricultural weather features
1423
+ "derived_features": normalized_data.get(
1424
+ "derived_features", {}
1425
+ ),
1426
+
1427
+ "derived_feature_guide": {
1428
+ "temperature_range_c":
1429
+ "Daily maximum temperature minus minimum temperature.",
1430
+ "dewpoint_depression_proxy_c":
1431
+ "Mean air temperature minus mean dewpoint.",
1432
+ "water_balance_proxy_mm":
1433
+ "Precipitation minus reference ET0; not soil water balance.",
1434
+ "precipitation_per_wet_hour_proxy":
1435
+ "Daily precipitation divided by precipitation hours; "
1436
+ "not peak rainfall intensity.",
1437
+ "humidity_wetness_interaction":
1438
+ "Relative humidity fraction multiplied by leaf-wetness "
1439
+ "probability fraction; not disease probability.",
1440
+ "wetness_duration_interaction":
1441
+ "Leaf-wetness probability multiplied by precipitation "
1442
+ "hours divided by 24; not measured leaf-wetness duration.",
1443
+ "dry_spell_days":
1444
+ "Consecutive days with precipitation at or below "
1445
+ "0.1 mm; not a crop-specific drought threshold.",
1446
+ "rolling_features":
1447
+ "Trailing 3-day and 7-day sums or means. Windows span "
1448
+ "the continuous history, today, and forecast timeline.",
1449
+ "delta_vs_history7d":
1450
+ "Forecast daily value minus the mean of available "
1451
+ "historical daily values over the previous 7 days.",
1452
+ "forecast_minus_history":
1453
+ "Difference between the 7-day forecast summary and "
1454
+ "the 7-day historical summary.",
1455
+ "probability_fraction":
1456
+ "Normalized to a 0-1 scale when the source unit is "
1457
+ "percent. Null means unavailable, not zero."
1458
+ },
1459
+
1460
+
1461
+
1462
  "historical": {"daily": normalized_data.get("historical", {})},
1463
  "forecast": {"daily": normalized_data.get("forecast", {})}
1464
  }
 
1571
  lines.append(f"- Current Concern : {field_context.get('current_concern') or '-'}")
1572
  lines.append("")
1573
  lines.append("--- DATA CUACA MENTAH (Ecwf, historical 7 hari + forecast 7 hari) ---")
1574
+
1575
+
1576
+ lines.append("--- DATA CUACA MENTAH (Open-Meteo, historical + forecast) ---")
1577
+ lines.append(json.dumps(
1578
+ cache_entry.get("raw_weather", {}),
1579
+ ensure_ascii=False,
1580
+ indent=2
1581
+ ))
1582
  lines.append("")
1583
+
1584
+ # FEATURE ENGINEERING: tambahkan tepat di sini
1585
+ lines.append("--- FITUR CUACA TURUNAN ---")
1586
+ lines.append(
1587
+ json.dumps(
1588
+ cache_entry.get("derived_features", {}),
1589
+ ensure_ascii=False,
1590
+ indent=2,
1591
+ allow_nan=False
1592
+ )
1593
+ )
1594
+ lines.append("")
1595
+
1596
+ # Bagian analisis Gemini tetap dilanjutkan seperti sebelumnya
1597
  lines.append("--- HASIL ANALISIS GEMINI AI ---")
1598
  lines.append(f"Ringkasan: {analysis.get('summary', '-')}")
1599
  lines.append("")
1600
 
1601
+
1602
+
1603
+ lines.append(f"Ringkasan: {analysis.get('summary', '-')}")
1604
+ lines.append("")
1605
+
1606
  hist = analysis.get("historical", {}) or {}
1607
  lines.append("Historical (7 hari terakhir):")
1608
  lines.append(f" Kondisi : {hist.get('condition', '-')}")
 
2026
  "current_concern": current_concern,
2027
  },
2028
  "raw_weather": raw_weather,
2029
+ "derived_features": normalized.get("derived_features", {}),
2030
  "analysis": llm_output,
2031
  "created_at": datetime.now(timezone.utc).isoformat()
2032
  }