agroplan-ai-cli 1.0.32 → 1.0.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/backend-template/VERSION.json +6 -4
- package/backend-template/api.py +20 -0
- package/backend-template/core/calendar_weather_adapter.py +176 -0
- package/backend-template/core/crop_calendar_engine.py +45 -0
- package/backend-template/core/planning_models.py +10 -2
- package/backend-template/providers/__pycache__/price_provider.cpython-313.pyc +0 -0
- package/backend-template/providers/__pycache__/zarc_provider.cpython-313.pyc +0 -0
- package/backend-template/providers/calendar_weather_provider.py +272 -0
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"cli_version": "1.0.
|
|
3
|
-
"backend_template_version": "1.0.
|
|
2
|
+
"cli_version": "1.0.34",
|
|
3
|
+
"backend_template_version": "1.0.34",
|
|
4
4
|
"zarc_index_version": "2025-2026-fast-index-v2",
|
|
5
5
|
"price_index_version": "2025-05-reference-v1",
|
|
6
6
|
"features": [
|
|
@@ -21,7 +21,9 @@
|
|
|
21
21
|
"smart_crop_calendar_engine",
|
|
22
22
|
"manual_field_registration",
|
|
23
23
|
"crop_calendar_from_manual_field",
|
|
24
|
-
"expanded_crop_calendar_10_cultures"
|
|
24
|
+
"expanded_crop_calendar_10_cultures",
|
|
25
|
+
"calendar_date_safety",
|
|
26
|
+
"calendar_weather_integration"
|
|
25
27
|
],
|
|
26
|
-
"generated_at": "2026-05-
|
|
28
|
+
"generated_at": "2026-05-10T21:00:00Z"
|
|
27
29
|
}
|
package/backend-template/api.py
CHANGED
|
@@ -1358,6 +1358,16 @@ def gerar_calendario(request: dict):
|
|
|
1358
1358
|
zarc_context=request.get("zarc_context")
|
|
1359
1359
|
)
|
|
1360
1360
|
|
|
1361
|
+
# Enriquecer com clima se solicitado
|
|
1362
|
+
usar_clima = request.get("usar_clima", False)
|
|
1363
|
+
if usar_clima:
|
|
1364
|
+
from core.calendar_weather_adapter import enriquecer_calendario_com_clima
|
|
1365
|
+
lat = field_data.get("lat")
|
|
1366
|
+
lon = field_data.get("lon")
|
|
1367
|
+
resultado = enriquecer_calendario_com_clima(resultado, lat=lat, lon=lon)
|
|
1368
|
+
else:
|
|
1369
|
+
resultado["weather_enabled"] = False
|
|
1370
|
+
|
|
1361
1371
|
return converter_tipos_python(resultado)
|
|
1362
1372
|
|
|
1363
1373
|
except HTTPException:
|
|
@@ -1532,6 +1542,7 @@ def gerar_calendario_talhao(field_id: str, request: dict):
|
|
|
1532
1542
|
try:
|
|
1533
1543
|
from core.field_storage import obter_talhao_usuario
|
|
1534
1544
|
from core.crop_calendar_engine import gerar_calendario_cultura
|
|
1545
|
+
from core.calendar_weather_adapter import enriquecer_calendario_com_clima
|
|
1535
1546
|
from core.planning_models import Field, SoilType, Slope, WaterAvailability, GenerateCalendarRequest
|
|
1536
1547
|
from datetime import datetime
|
|
1537
1548
|
|
|
@@ -1575,6 +1586,15 @@ def gerar_calendario_talhao(field_id: str, request: dict):
|
|
|
1575
1586
|
crop_plan_id=None
|
|
1576
1587
|
)
|
|
1577
1588
|
|
|
1589
|
+
# Enriquecer com clima se solicitado
|
|
1590
|
+
usar_clima = request.get("usar_clima", False)
|
|
1591
|
+
if usar_clima:
|
|
1592
|
+
lat = field_data.get("lat")
|
|
1593
|
+
lon = field_data.get("lon")
|
|
1594
|
+
resultado = enriquecer_calendario_com_clima(resultado, lat=lat, lon=lon)
|
|
1595
|
+
else:
|
|
1596
|
+
resultado["weather_enabled"] = False
|
|
1597
|
+
|
|
1578
1598
|
# Adicionar dados do talhão na resposta
|
|
1579
1599
|
resultado["field_data"] = field_data
|
|
1580
1600
|
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Adaptador de Clima para Calendário Agrícola
|
|
3
|
+
|
|
4
|
+
Enriquece tarefas do calendário com contexto climático:
|
|
5
|
+
- Previsão real (0-16 dias)
|
|
6
|
+
- Climatologia (17+ dias)
|
|
7
|
+
- Recomendações situacionais
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from datetime import date, timedelta
|
|
11
|
+
from typing import Dict, List, Optional
|
|
12
|
+
from providers.calendar_weather_provider import (
|
|
13
|
+
buscar_previsao_curto_prazo,
|
|
14
|
+
buscar_climatologia_longo_prazo,
|
|
15
|
+
gerar_recomendacao_climatica
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def enriquecer_calendario_com_clima(
|
|
20
|
+
calendar: Dict,
|
|
21
|
+
lat: Optional[float] = None,
|
|
22
|
+
lon: Optional[float] = None
|
|
23
|
+
) -> Dict:
|
|
24
|
+
"""
|
|
25
|
+
Enriquece calendário com contexto climático.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
calendar: Calendário gerado
|
|
29
|
+
lat: Latitude do talhão
|
|
30
|
+
lon: Longitude do talhão
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Calendário enriquecido com weather_context em cada tarefa
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
# Verificar se coordenadas foram fornecidas
|
|
37
|
+
if lat is None or lon is None:
|
|
38
|
+
calendar["weather_enabled"] = False
|
|
39
|
+
calendar["weather_warnings"] = [
|
|
40
|
+
"Para usar clima integrado, informe latitude e longitude do talhão."
|
|
41
|
+
]
|
|
42
|
+
return calendar
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
today = date.today()
|
|
46
|
+
planting_date = date.fromisoformat(calendar["planting_date"])
|
|
47
|
+
estimated_harvest = date.fromisoformat(calendar["estimated_harvest_date"])
|
|
48
|
+
|
|
49
|
+
# Buscar previsão de curto prazo (0-16 dias)
|
|
50
|
+
forecast_end = today + timedelta(days=16)
|
|
51
|
+
forecast_data = buscar_previsao_curto_prazo(lat, lon, today, days=16)
|
|
52
|
+
|
|
53
|
+
# Criar mapa de previsão por data
|
|
54
|
+
forecast_map = {item["date"]: item for item in forecast_data}
|
|
55
|
+
|
|
56
|
+
# Buscar climatologia de longo prazo (17+ dias até colheita)
|
|
57
|
+
climatology_data = []
|
|
58
|
+
if estimated_harvest > forecast_end:
|
|
59
|
+
climatology_start = forecast_end + timedelta(days=1)
|
|
60
|
+
climatology_data = buscar_climatologia_longo_prazo(
|
|
61
|
+
lat, lon, climatology_start, estimated_harvest
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# Criar mapa de climatologia por data
|
|
65
|
+
climatology_map = {item["date"]: item for item in climatology_data}
|
|
66
|
+
|
|
67
|
+
# Contadores
|
|
68
|
+
forecast_tasks = 0
|
|
69
|
+
climatology_tasks = 0
|
|
70
|
+
no_weather_tasks = 0
|
|
71
|
+
sources_used = set()
|
|
72
|
+
|
|
73
|
+
# Enriquecer cada tarefa
|
|
74
|
+
for task in calendar.get("tasks", []):
|
|
75
|
+
task_date_str = task["date"]
|
|
76
|
+
task_date = date.fromisoformat(task_date_str)
|
|
77
|
+
|
|
78
|
+
# Apenas enriquecer tarefas sensíveis ao clima
|
|
79
|
+
if not task.get("weather_sensitive", False):
|
|
80
|
+
task["weather_context"] = {
|
|
81
|
+
"active": False,
|
|
82
|
+
"reason": "Tarefa não sensível ao clima"
|
|
83
|
+
}
|
|
84
|
+
no_weather_tasks += 1
|
|
85
|
+
continue
|
|
86
|
+
|
|
87
|
+
# Verificar se está no passado
|
|
88
|
+
if task_date < today:
|
|
89
|
+
task["weather_context"] = {
|
|
90
|
+
"active": False,
|
|
91
|
+
"reason": "Tarefa no passado"
|
|
92
|
+
}
|
|
93
|
+
no_weather_tasks += 1
|
|
94
|
+
continue
|
|
95
|
+
|
|
96
|
+
# Tentar usar previsão real (0-16 dias)
|
|
97
|
+
if task_date_str in forecast_map:
|
|
98
|
+
weather_data = forecast_map[task_date_str]
|
|
99
|
+
forecast_tasks += 1
|
|
100
|
+
sources_used.add("open-meteo")
|
|
101
|
+
# Usar climatologia (17+ dias)
|
|
102
|
+
elif task_date_str in climatology_map:
|
|
103
|
+
weather_data = climatology_map[task_date_str]
|
|
104
|
+
climatology_tasks += 1
|
|
105
|
+
sources_used.add(weather_data["source"])
|
|
106
|
+
else:
|
|
107
|
+
# Sem dados disponíveis
|
|
108
|
+
task["weather_context"] = {
|
|
109
|
+
"active": False,
|
|
110
|
+
"reason": "Dados climáticos não disponíveis para esta data"
|
|
111
|
+
}
|
|
112
|
+
no_weather_tasks += 1
|
|
113
|
+
continue
|
|
114
|
+
|
|
115
|
+
# Gerar recomendação
|
|
116
|
+
recommendation = gerar_recomendacao_climatica(
|
|
117
|
+
task.get("type", ""),
|
|
118
|
+
task.get("title", ""),
|
|
119
|
+
weather_data
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# Gerar resumo
|
|
123
|
+
forecast_type = weather_data.get("forecast_type", "climatology")
|
|
124
|
+
if forecast_type == "forecast":
|
|
125
|
+
summary = f"Previsão: {weather_data.get('precipitation_sum', 0):.1f}mm de chuva, {weather_data.get('temperature_min', 0):.0f}°C a {weather_data.get('temperature_max', 0):.0f}°C"
|
|
126
|
+
else:
|
|
127
|
+
summary = f"Climatologia: {weather_data.get('precipitation_expected', 'Condições típicas')}"
|
|
128
|
+
|
|
129
|
+
# Adicionar contexto climático à tarefa
|
|
130
|
+
task["weather_context"] = {
|
|
131
|
+
"active": True,
|
|
132
|
+
"source": weather_data.get("source", "unknown"),
|
|
133
|
+
"forecast_type": forecast_type,
|
|
134
|
+
"summary": summary,
|
|
135
|
+
"precipitation_mm": weather_data.get("precipitation_sum") or weather_data.get("precipitation_mm_avg"),
|
|
136
|
+
"precipitation_probability": weather_data.get("precipitation_probability"),
|
|
137
|
+
"temperature_min": weather_data.get("temperature_min"),
|
|
138
|
+
"temperature_max": weather_data.get("temperature_max"),
|
|
139
|
+
"recommendation": recommendation,
|
|
140
|
+
"confidence": weather_data.get("confidence", "media")
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
# Adicionar resumo geral
|
|
144
|
+
calendar["weather_enabled"] = True
|
|
145
|
+
calendar["weather_summary"] = {
|
|
146
|
+
"forecast_tasks": forecast_tasks,
|
|
147
|
+
"climatology_tasks": climatology_tasks,
|
|
148
|
+
"no_weather_tasks": no_weather_tasks,
|
|
149
|
+
"sources": list(sources_used)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
# Adicionar avisos
|
|
153
|
+
weather_warnings = []
|
|
154
|
+
|
|
155
|
+
if climatology_tasks > 0:
|
|
156
|
+
weather_warnings.append(
|
|
157
|
+
f"{climatology_tasks} tarefa(s) usam climatologia/histórico (17+ dias). "
|
|
158
|
+
"Não é previsão exata, apenas condições típicas do período."
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
if forecast_tasks > 0:
|
|
162
|
+
weather_warnings.append(
|
|
163
|
+
f"{forecast_tasks} tarefa(s) usam previsão meteorológica real (0-16 dias)."
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
calendar["weather_warnings"] = weather_warnings
|
|
167
|
+
|
|
168
|
+
return calendar
|
|
169
|
+
|
|
170
|
+
except Exception as e:
|
|
171
|
+
print(f"Erro ao enriquecer calendário com clima: {e}")
|
|
172
|
+
calendar["weather_enabled"] = False
|
|
173
|
+
calendar["weather_warnings"] = [
|
|
174
|
+
f"Erro ao buscar dados climáticos: {str(e)}"
|
|
175
|
+
]
|
|
176
|
+
return calendar
|
|
@@ -875,6 +875,48 @@ def gerar_calendario_cultura(
|
|
|
875
875
|
# Ordenar tarefas por data
|
|
876
876
|
tasks.sort(key=lambda t: t.date)
|
|
877
877
|
|
|
878
|
+
# Detectar e ajustar tarefas no passado
|
|
879
|
+
today = date.today()
|
|
880
|
+
adjusted_tasks_count = 0
|
|
881
|
+
calendar_warnings = []
|
|
882
|
+
|
|
883
|
+
for task in tasks:
|
|
884
|
+
if task.date < today:
|
|
885
|
+
# Marcar tarefa como ajustada
|
|
886
|
+
task.original_date = task.date
|
|
887
|
+
task.date = today
|
|
888
|
+
task.adjusted = True
|
|
889
|
+
|
|
890
|
+
# Aumentar prioridade se não for crítica
|
|
891
|
+
if task.priority != TaskPriority.CRITICAL:
|
|
892
|
+
task.priority = TaskPriority.HIGH
|
|
893
|
+
|
|
894
|
+
# Adicionar observação à descrição
|
|
895
|
+
task.description += f" [AJUSTADA: Data original era {task.original_date.isoformat()}, mas já passou. Tarefa reagendada para hoje.]"
|
|
896
|
+
|
|
897
|
+
adjusted_tasks_count += 1
|
|
898
|
+
|
|
899
|
+
# Adicionar avisos se houver tarefas ajustadas
|
|
900
|
+
if adjusted_tasks_count > 0:
|
|
901
|
+
calendar_warnings.append(
|
|
902
|
+
f"{adjusted_tasks_count} tarefa(s) foram ajustadas porque a data original já havia passado. "
|
|
903
|
+
"Considere escolher uma data de plantio mais distante no futuro."
|
|
904
|
+
)
|
|
905
|
+
|
|
906
|
+
# Verificar se data de plantio está muito próxima
|
|
907
|
+
days_until_planting = (planting_date - today).days
|
|
908
|
+
if days_until_planting <= 7 and days_until_planting >= 0:
|
|
909
|
+
calendar_warnings.append(
|
|
910
|
+
f"Sua data de plantio está em {days_until_planting} dia(s). "
|
|
911
|
+
"Tarefas preparatórias podem ter sido ajustadas. "
|
|
912
|
+
"Recomendamos planejar com pelo menos 2 semanas de antecedência."
|
|
913
|
+
)
|
|
914
|
+
elif days_until_planting < 0:
|
|
915
|
+
calendar_warnings.append(
|
|
916
|
+
"A data de plantio escolhida já passou. "
|
|
917
|
+
"O calendário foi ajustado, mas recomendamos escolher uma data futura."
|
|
918
|
+
)
|
|
919
|
+
|
|
878
920
|
# Montar resposta
|
|
879
921
|
return {
|
|
880
922
|
"cultura": cultura,
|
|
@@ -906,6 +948,9 @@ def gerar_calendario_cultura(
|
|
|
906
948
|
"total_tasks": len(tasks),
|
|
907
949
|
"weather_sensitive_tasks": sum(1 for task in tasks if task.weather_sensitive),
|
|
908
950
|
"critical_tasks": sum(1 for task in tasks if task.priority == TaskPriority.CRITICAL),
|
|
951
|
+
"has_adjusted_tasks": adjusted_tasks_count > 0,
|
|
952
|
+
"adjusted_tasks_count": adjusted_tasks_count,
|
|
953
|
+
"calendar_warnings": calendar_warnings,
|
|
909
954
|
"cautela": "Este calendário é uma base inicial de planejamento. As datas e tarefas devem ser ajustadas conforme clima, solo, cultivar, manejo e orientação técnica."
|
|
910
955
|
}
|
|
911
956
|
|
|
@@ -259,9 +259,11 @@ class CalendarTask:
|
|
|
259
259
|
status: TaskStatus = TaskStatus.PENDING
|
|
260
260
|
weather_sensitive: bool = False
|
|
261
261
|
completed_at: Optional[datetime] = None
|
|
262
|
+
adjusted: bool = False # Se a tarefa foi ajustada por estar no passado
|
|
263
|
+
original_date: Optional[date] = None # Data original antes do ajuste
|
|
262
264
|
|
|
263
265
|
def to_dict(self) -> Dict:
|
|
264
|
-
|
|
266
|
+
result = {
|
|
265
267
|
"id": self.id,
|
|
266
268
|
"crop_plan_id": self.crop_plan_id,
|
|
267
269
|
"date": self.date.isoformat(),
|
|
@@ -272,8 +274,14 @@ class CalendarTask:
|
|
|
272
274
|
"source": self.source,
|
|
273
275
|
"status": self.status.value,
|
|
274
276
|
"weather_sensitive": self.weather_sensitive,
|
|
275
|
-
"completed_at": self.completed_at.isoformat() if self.completed_at else None
|
|
277
|
+
"completed_at": self.completed_at.isoformat() if self.completed_at else None,
|
|
278
|
+
"adjusted": self.adjusted
|
|
276
279
|
}
|
|
280
|
+
|
|
281
|
+
if self.original_date:
|
|
282
|
+
result["original_date"] = self.original_date.isoformat()
|
|
283
|
+
|
|
284
|
+
return result
|
|
277
285
|
|
|
278
286
|
|
|
279
287
|
@dataclass
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Provider de Clima para Calendário Agrícola
|
|
3
|
+
|
|
4
|
+
Estratégia honesta sobre previsão climática:
|
|
5
|
+
- 0-16 dias: Previsão meteorológica real (Open-Meteo)
|
|
6
|
+
- 17+ dias: Climatologia/histórico (NASA POWER ou fallback local)
|
|
7
|
+
|
|
8
|
+
Não fingimos ter previsão exata para ciclos longos (120+ dias).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from datetime import date, timedelta
|
|
12
|
+
from typing import Dict, List, Optional
|
|
13
|
+
import requests
|
|
14
|
+
from .cache import get_cache, set_cache
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def buscar_previsao_curto_prazo(
|
|
18
|
+
lat: float,
|
|
19
|
+
lon: float,
|
|
20
|
+
start_date: date,
|
|
21
|
+
days: int = 16
|
|
22
|
+
) -> List[Dict]:
|
|
23
|
+
"""
|
|
24
|
+
Busca previsão meteorológica real para os próximos 16 dias.
|
|
25
|
+
|
|
26
|
+
Usa Open-Meteo Forecast API.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
lat: Latitude
|
|
30
|
+
lon: Longitude
|
|
31
|
+
start_date: Data inicial
|
|
32
|
+
days: Número de dias (máximo 16)
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
Lista de dicionários com previsão por data
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
# Limitar a 16 dias (limite confiável do Open-Meteo)
|
|
39
|
+
days = min(days, 16)
|
|
40
|
+
|
|
41
|
+
# Cache key
|
|
42
|
+
cache_key = f"calendar_forecast:{lat}:{lon}:{start_date.isoformat()}:{days}"
|
|
43
|
+
cached = get_cache(cache_key)
|
|
44
|
+
if cached:
|
|
45
|
+
return cached
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
# Open-Meteo Forecast API
|
|
49
|
+
url = "https://api.open-meteo.com/v1/forecast"
|
|
50
|
+
params = {
|
|
51
|
+
"latitude": lat,
|
|
52
|
+
"longitude": lon,
|
|
53
|
+
"daily": "temperature_2m_max,temperature_2m_min,precipitation_sum,precipitation_probability_max",
|
|
54
|
+
"timezone": "auto",
|
|
55
|
+
"forecast_days": days
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
response = requests.get(url, params=params, timeout=10)
|
|
59
|
+
response.raise_for_status()
|
|
60
|
+
data = response.json()
|
|
61
|
+
|
|
62
|
+
# Processar resposta
|
|
63
|
+
daily = data.get("daily", {})
|
|
64
|
+
dates = daily.get("time", [])
|
|
65
|
+
temp_max = daily.get("temperature_2m_max", [])
|
|
66
|
+
temp_min = daily.get("temperature_2m_min", [])
|
|
67
|
+
precip_sum = daily.get("precipitation_sum", [])
|
|
68
|
+
precip_prob = daily.get("precipitation_probability_max", [])
|
|
69
|
+
|
|
70
|
+
result = []
|
|
71
|
+
for i, date_str in enumerate(dates):
|
|
72
|
+
result.append({
|
|
73
|
+
"date": date_str,
|
|
74
|
+
"source": "open-meteo",
|
|
75
|
+
"forecast_type": "forecast",
|
|
76
|
+
"temperature_max": temp_max[i] if i < len(temp_max) else None,
|
|
77
|
+
"temperature_min": temp_min[i] if i < len(temp_min) else None,
|
|
78
|
+
"precipitation_sum": precip_sum[i] if i < len(precip_sum) else None,
|
|
79
|
+
"precipitation_probability": precip_prob[i] if i < len(precip_prob) else None,
|
|
80
|
+
"confidence": "alta"
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
# Cache por 6 horas
|
|
84
|
+
set_cache(cache_key, result, ttl_seconds=21600)
|
|
85
|
+
|
|
86
|
+
return result
|
|
87
|
+
|
|
88
|
+
except Exception as e:
|
|
89
|
+
print(f"Erro ao buscar previsão Open-Meteo: {e}")
|
|
90
|
+
return []
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def buscar_climatologia_longo_prazo(
|
|
94
|
+
lat: float,
|
|
95
|
+
lon: float,
|
|
96
|
+
start_date: date,
|
|
97
|
+
end_date: date
|
|
98
|
+
) -> List[Dict]:
|
|
99
|
+
"""
|
|
100
|
+
Busca climatologia/histórico para períodos longos (17+ dias).
|
|
101
|
+
|
|
102
|
+
Inicialmente usa fallback local mensal.
|
|
103
|
+
Futuramente pode integrar NASA POWER.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
lat: Latitude
|
|
107
|
+
lon: Longitude
|
|
108
|
+
start_date: Data inicial
|
|
109
|
+
end_date: Data final
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
Lista de dicionários com climatologia por data/mês
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
# Por enquanto, usar fallback local baseado em médias mensais
|
|
116
|
+
# Futuramente: integrar NASA POWER Climatology API
|
|
117
|
+
|
|
118
|
+
result = []
|
|
119
|
+
current = start_date
|
|
120
|
+
|
|
121
|
+
while current <= end_date:
|
|
122
|
+
month = current.month
|
|
123
|
+
|
|
124
|
+
# Climatologia simplificada por mês (Brasil)
|
|
125
|
+
climatology = _get_monthly_climatology(month, lat, lon)
|
|
126
|
+
|
|
127
|
+
result.append({
|
|
128
|
+
"date": current.isoformat(),
|
|
129
|
+
"source": "climate-fallback",
|
|
130
|
+
"forecast_type": "climatology",
|
|
131
|
+
"temperature_max": climatology["temp_max"],
|
|
132
|
+
"temperature_min": climatology["temp_min"],
|
|
133
|
+
"precipitation_expected": climatology["precip_desc"],
|
|
134
|
+
"precipitation_mm_avg": climatology["precip_mm"],
|
|
135
|
+
"confidence": "media"
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
current += timedelta(days=1)
|
|
139
|
+
|
|
140
|
+
return result
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _get_monthly_climatology(month: int, lat: float, lon: float) -> Dict:
|
|
144
|
+
"""
|
|
145
|
+
Retorna climatologia simplificada por mês.
|
|
146
|
+
|
|
147
|
+
Baseado em médias históricas do Brasil.
|
|
148
|
+
Futuramente: usar NASA POWER ou dados regionais.
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
# Determinar região aproximada (simplificado)
|
|
152
|
+
if lat < -23: # Sul
|
|
153
|
+
region = "sul"
|
|
154
|
+
elif lat < -15: # Sudeste/Centro-Oeste
|
|
155
|
+
region = "sudeste"
|
|
156
|
+
else: # Norte/Nordeste
|
|
157
|
+
region = "norte"
|
|
158
|
+
|
|
159
|
+
# Climatologia simplificada (valores aproximados)
|
|
160
|
+
climatology_data = {
|
|
161
|
+
"sul": {
|
|
162
|
+
1: {"temp_max": 28, "temp_min": 18, "precip_mm": 150, "precip_desc": "Chuvas frequentes"},
|
|
163
|
+
2: {"temp_max": 28, "temp_min": 18, "precip_mm": 140, "precip_desc": "Chuvas frequentes"},
|
|
164
|
+
3: {"temp_max": 26, "temp_min": 16, "precip_mm": 120, "precip_desc": "Chuvas moderadas"},
|
|
165
|
+
4: {"temp_max": 23, "temp_min": 13, "precip_mm": 100, "precip_desc": "Chuvas moderadas"},
|
|
166
|
+
5: {"temp_max": 20, "temp_min": 10, "precip_mm": 90, "precip_desc": "Chuvas ocasionais"},
|
|
167
|
+
6: {"temp_max": 18, "temp_min": 8, "precip_mm": 80, "precip_desc": "Chuvas ocasionais"},
|
|
168
|
+
7: {"temp_max": 18, "temp_min": 8, "precip_mm": 70, "precip_desc": "Período seco"},
|
|
169
|
+
8: {"temp_max": 20, "temp_min": 10, "precip_mm": 80, "precip_desc": "Período seco"},
|
|
170
|
+
9: {"temp_max": 22, "temp_min": 12, "precip_mm": 110, "precip_desc": "Chuvas aumentando"},
|
|
171
|
+
10: {"temp_max": 24, "temp_min": 14, "precip_mm": 130, "precip_desc": "Chuvas frequentes"},
|
|
172
|
+
11: {"temp_max": 26, "temp_min": 16, "precip_mm": 120, "precip_desc": "Chuvas frequentes"},
|
|
173
|
+
12: {"temp_max": 28, "temp_min": 18, "precip_mm": 140, "precip_desc": "Chuvas frequentes"},
|
|
174
|
+
},
|
|
175
|
+
"sudeste": {
|
|
176
|
+
1: {"temp_max": 30, "temp_min": 20, "precip_mm": 220, "precip_desc": "Estação chuvosa"},
|
|
177
|
+
2: {"temp_max": 30, "temp_min": 20, "precip_mm": 200, "precip_desc": "Estação chuvosa"},
|
|
178
|
+
3: {"temp_max": 29, "temp_min": 19, "precip_mm": 160, "precip_desc": "Chuvas frequentes"},
|
|
179
|
+
4: {"temp_max": 27, "temp_min": 17, "precip_mm": 80, "precip_desc": "Chuvas diminuindo"},
|
|
180
|
+
5: {"temp_max": 25, "temp_min": 15, "precip_mm": 50, "precip_desc": "Período seco"},
|
|
181
|
+
6: {"temp_max": 24, "temp_min": 13, "precip_mm": 40, "precip_desc": "Período seco"},
|
|
182
|
+
7: {"temp_max": 24, "temp_min": 13, "precip_mm": 30, "precip_desc": "Período seco"},
|
|
183
|
+
8: {"temp_max": 26, "temp_min": 15, "precip_mm": 40, "precip_desc": "Período seco"},
|
|
184
|
+
9: {"temp_max": 27, "temp_min": 16, "precip_mm": 70, "precip_desc": "Chuvas aumentando"},
|
|
185
|
+
10: {"temp_max": 28, "temp_min": 18, "precip_mm": 130, "precip_desc": "Chuvas frequentes"},
|
|
186
|
+
11: {"temp_max": 29, "temp_min": 19, "precip_mm": 170, "precip_desc": "Chuvas frequentes"},
|
|
187
|
+
12: {"temp_max": 29, "temp_min": 20, "precip_mm": 200, "precip_desc": "Estação chuvosa"},
|
|
188
|
+
},
|
|
189
|
+
"norte": {
|
|
190
|
+
1: {"temp_max": 31, "temp_min": 23, "precip_mm": 280, "precip_desc": "Estação chuvosa"},
|
|
191
|
+
2: {"temp_max": 31, "temp_min": 23, "precip_mm": 300, "precip_desc": "Estação chuvosa"},
|
|
192
|
+
3: {"temp_max": 31, "temp_min": 23, "precip_mm": 320, "precip_desc": "Estação chuvosa"},
|
|
193
|
+
4: {"temp_max": 31, "temp_min": 23, "precip_mm": 280, "precip_desc": "Chuvas frequentes"},
|
|
194
|
+
5: {"temp_max": 31, "temp_min": 23, "precip_mm": 200, "precip_desc": "Chuvas moderadas"},
|
|
195
|
+
6: {"temp_max": 32, "temp_min": 23, "precip_mm": 100, "precip_desc": "Chuvas ocasionais"},
|
|
196
|
+
7: {"temp_max": 33, "temp_min": 23, "precip_mm": 60, "precip_desc": "Período seco"},
|
|
197
|
+
8: {"temp_max": 34, "temp_min": 24, "precip_mm": 50, "precip_desc": "Período seco"},
|
|
198
|
+
9: {"temp_max": 34, "temp_min": 24, "precip_mm": 80, "precip_desc": "Chuvas aumentando"},
|
|
199
|
+
10: {"temp_max": 33, "temp_min": 24, "precip_mm": 130, "precip_desc": "Chuvas aumentando"},
|
|
200
|
+
11: {"temp_max": 32, "temp_min": 23, "precip_mm": 180, "precip_desc": "Chuvas frequentes"},
|
|
201
|
+
12: {"temp_max": 31, "temp_min": 23, "precip_mm": 250, "precip_desc": "Estação chuvosa"},
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return climatology_data.get(region, climatology_data["sudeste"]).get(month, {
|
|
206
|
+
"temp_max": 28,
|
|
207
|
+
"temp_min": 18,
|
|
208
|
+
"precip_mm": 100,
|
|
209
|
+
"precip_desc": "Chuvas moderadas"
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def gerar_recomendacao_climatica(
|
|
214
|
+
task_type: str,
|
|
215
|
+
task_title: str,
|
|
216
|
+
weather_data: Dict
|
|
217
|
+
) -> str:
|
|
218
|
+
"""
|
|
219
|
+
Gera recomendação baseada no contexto climático.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
task_type: Tipo da tarefa (irrigate, plant, etc)
|
|
223
|
+
task_title: Título da tarefa
|
|
224
|
+
weather_data: Dados climáticos
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
Recomendação textual
|
|
228
|
+
"""
|
|
229
|
+
|
|
230
|
+
forecast_type = weather_data.get("forecast_type", "climatology")
|
|
231
|
+
precip = weather_data.get("precipitation_sum") or weather_data.get("precipitation_mm_avg", 0)
|
|
232
|
+
precip_prob = weather_data.get("precipitation_probability", 0)
|
|
233
|
+
temp_max = weather_data.get("temperature_max", 25)
|
|
234
|
+
temp_min = weather_data.get("temperature_min", 15)
|
|
235
|
+
|
|
236
|
+
# Recomendações para irrigação
|
|
237
|
+
if "irrigar" in task_title.lower() or task_type == "irrigate":
|
|
238
|
+
if forecast_type == "forecast":
|
|
239
|
+
if precip >= 8:
|
|
240
|
+
return f"Chuva prevista suficiente ({precip:.1f}mm). Verifique o solo antes de irrigar."
|
|
241
|
+
elif precip >= 3:
|
|
242
|
+
return f"Chuva moderada prevista ({precip:.1f}mm). Irrigação pode ser parcial."
|
|
243
|
+
else:
|
|
244
|
+
return f"Pouca chuva prevista ({precip:.1f}mm). Irrigação provavelmente necessária."
|
|
245
|
+
else:
|
|
246
|
+
return f"Climatologia: {weather_data.get('precipitation_expected', 'Chuvas moderadas')}. Monitore o solo."
|
|
247
|
+
|
|
248
|
+
# Recomendações para plantio
|
|
249
|
+
if "plantar" in task_title.lower() or task_type == "plant":
|
|
250
|
+
if forecast_type == "forecast":
|
|
251
|
+
if precip > 50:
|
|
252
|
+
return f"Chuva elevada prevista ({precip:.1f}mm). Avalie adiar o plantio para evitar solo encharcado."
|
|
253
|
+
elif precip < 5 and precip_prob < 30:
|
|
254
|
+
return f"Tempo seco previsto. Bom para plantio, mas prepare irrigação."
|
|
255
|
+
else:
|
|
256
|
+
return f"Condições adequadas para plantio. Chuva moderada prevista ({precip:.1f}mm)."
|
|
257
|
+
else:
|
|
258
|
+
return f"Climatologia: {weather_data.get('precipitation_expected', 'Chuvas moderadas')}. Planeje conforme histórico."
|
|
259
|
+
|
|
260
|
+
# Recomendações para temperatura
|
|
261
|
+
if temp_max > 35:
|
|
262
|
+
return f"Calor elevado previsto ({temp_max:.1f}°C). Monitorar estresse hídrico da cultura."
|
|
263
|
+
elif temp_min < 5:
|
|
264
|
+
return f"Frio intenso previsto ({temp_min:.1f}°C). Avaliar risco para a cultura."
|
|
265
|
+
elif temp_min < 10:
|
|
266
|
+
return f"Temperatura baixa prevista ({temp_min:.1f}°C). Monitorar desenvolvimento da cultura."
|
|
267
|
+
|
|
268
|
+
# Recomendação genérica
|
|
269
|
+
if forecast_type == "forecast":
|
|
270
|
+
return f"Temperatura: {temp_min:.1f}°C a {temp_max:.1f}°C. Chuva: {precip:.1f}mm."
|
|
271
|
+
else:
|
|
272
|
+
return f"Climatologia: {weather_data.get('precipitation_expected', 'Condições típicas do período')}."
|