agroplan-ai-cli 1.0.33 → 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 +5 -4
- package/backend-template/api.py +20 -0
- package/backend-template/core/calendar_weather_adapter.py +176 -0
- 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": [
|
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
"manual_field_registration",
|
|
23
23
|
"crop_calendar_from_manual_field",
|
|
24
24
|
"expanded_crop_calendar_10_cultures",
|
|
25
|
-
"calendar_date_safety"
|
|
25
|
+
"calendar_date_safety",
|
|
26
|
+
"calendar_weather_integration"
|
|
26
27
|
],
|
|
27
|
-
"generated_at": "2026-05-
|
|
28
|
+
"generated_at": "2026-05-10T21:00:00Z"
|
|
28
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
|
|
@@ -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')}."
|