agroplan-ai-cli 1.0.7 → 1.0.9
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/.env.example +7 -2
- package/backend-template/api.py +270 -76
- package/backend-template/core/climate_adapter.py +142 -0
- package/backend-template/providers/__init__.py +1 -0
- package/backend-template/providers/__pycache__/__init__.cpython-313.pyc +0 -0
- package/backend-template/providers/__pycache__/cache.cpython-313.pyc +0 -0
- package/backend-template/providers/__pycache__/types.cpython-313.pyc +0 -0
- package/backend-template/providers/__pycache__/weather_provider.cpython-313.pyc +0 -0
- package/backend-template/providers/cache.py +49 -0
- package/backend-template/providers/types.py +40 -0
- package/backend-template/providers/weather_provider.py +161 -0
- package/dist/index.js +1 -1
- package/package.json +1 -1
|
@@ -12,8 +12,13 @@ CORS_ORIGINS=http://localhost:3000,http://127.0.0.1:3000
|
|
|
12
12
|
|
|
13
13
|
# Data Mode
|
|
14
14
|
# simulated: Uses CSV files with simulated data
|
|
15
|
-
# real: Uses real APIs
|
|
16
|
-
|
|
15
|
+
# real: Uses real APIs only
|
|
16
|
+
# hybrid: Uses real APIs with fallback to simulated data
|
|
17
|
+
DATA_MODE=hybrid
|
|
18
|
+
|
|
19
|
+
# Weather Provider Configuration
|
|
20
|
+
WEATHER_PROVIDER=open-meteo
|
|
21
|
+
PROVIDER_CACHE_TTL=3600
|
|
17
22
|
|
|
18
23
|
# Cache Administration (optional)
|
|
19
24
|
# If set, /cache/limpar endpoint requires X-Cache-Token header
|
package/backend-template/api.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
FastAPI Backend para AgroPlan AI
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
-
from fastapi import FastAPI, HTTPException, Request
|
|
5
|
+
from fastapi import FastAPI, HTTPException, Request, Query
|
|
6
6
|
from fastapi.middleware.cors import CORSMiddleware
|
|
7
7
|
from pydantic import BaseModel
|
|
8
8
|
from typing import Optional
|
|
@@ -17,10 +17,17 @@ from core.planner import gerar_cenarios, gerar_plano_genetico
|
|
|
17
17
|
from core.bruteforce_validator import comparar_ag_com_forca_bruta, executar_multiplas_rodadas
|
|
18
18
|
from core.report_generator import gerar_relatorio_completo
|
|
19
19
|
|
|
20
|
+
# Importar provedores de dados reais
|
|
21
|
+
from providers.weather_provider import get_weather_summary
|
|
22
|
+
from providers.cache import clear_provider_cache, get_cache_stats
|
|
23
|
+
|
|
20
24
|
# Configurações de ambiente
|
|
21
25
|
HOST = os.getenv("HOST", "0.0.0.0")
|
|
22
26
|
PORT = int(os.getenv("PORT", "8000"))
|
|
23
27
|
CORS_ORIGINS = os.getenv("CORS_ORIGINS", "http://localhost:3000,http://127.0.0.1:3000").split(",")
|
|
28
|
+
DATA_MODE = os.getenv("DATA_MODE", "hybrid") # simulated, real, hybrid
|
|
29
|
+
WEATHER_PROVIDER = os.getenv("WEATHER_PROVIDER", "open-meteo")
|
|
30
|
+
PROVIDER_CACHE_TTL = int(os.getenv("PROVIDER_CACHE_TTL", "3600"))
|
|
24
31
|
|
|
25
32
|
# Cache em memória para resultados pesados
|
|
26
33
|
_resultados_cache = {}
|
|
@@ -63,6 +70,9 @@ class OtimizarRequest(BaseModel):
|
|
|
63
70
|
seed: Optional[int] = 42
|
|
64
71
|
geracoes: Optional[int] = 100
|
|
65
72
|
populacao: Optional[int] = 50
|
|
73
|
+
lat: Optional[float] = None
|
|
74
|
+
lon: Optional[float] = None
|
|
75
|
+
days: Optional[int] = 30
|
|
66
76
|
|
|
67
77
|
class ValidarRequest(BaseModel):
|
|
68
78
|
objetivo: str = "equilibrado"
|
|
@@ -71,6 +81,9 @@ class ValidarRequest(BaseModel):
|
|
|
71
81
|
class RelatorioRequest(BaseModel):
|
|
72
82
|
objetivo: str = "equilibrado"
|
|
73
83
|
formato: str = "md"
|
|
84
|
+
lat: Optional[float] = None
|
|
85
|
+
lon: Optional[float] = None
|
|
86
|
+
days: Optional[int] = 30
|
|
74
87
|
|
|
75
88
|
class RodadasRequest(BaseModel):
|
|
76
89
|
objetivo: str = "equilibrado"
|
|
@@ -126,32 +139,94 @@ def health():
|
|
|
126
139
|
"""Verifica saúde da API"""
|
|
127
140
|
try:
|
|
128
141
|
culturas, talhoes, regras = get_dados()
|
|
142
|
+
provider_cache_stats = get_cache_stats()
|
|
143
|
+
|
|
129
144
|
return {
|
|
130
145
|
"status": "healthy",
|
|
131
146
|
"culturas": len(culturas),
|
|
132
147
|
"talhoes": len(talhoes),
|
|
133
148
|
"regras": len(regras),
|
|
134
|
-
"cache_items": len(_resultados_cache)
|
|
149
|
+
"cache_items": len(_resultados_cache),
|
|
150
|
+
"data_mode": DATA_MODE,
|
|
151
|
+
"providers": {
|
|
152
|
+
"weather": "available" if WEATHER_PROVIDER else "disabled"
|
|
153
|
+
},
|
|
154
|
+
"provider_cache": provider_cache_stats
|
|
135
155
|
}
|
|
136
156
|
except Exception as e:
|
|
137
157
|
raise HTTPException(status_code=500, detail=str(e))
|
|
138
158
|
|
|
159
|
+
@app.get("/dados/clima")
|
|
160
|
+
def get_clima(
|
|
161
|
+
lat: Optional[float] = Query(None, description="Latitude"),
|
|
162
|
+
lon: Optional[float] = Query(None, description="Longitude"),
|
|
163
|
+
days: int = Query(30, description="Número de dias para análise")
|
|
164
|
+
):
|
|
165
|
+
"""Obtém dados climáticos reais ou simulados"""
|
|
166
|
+
try:
|
|
167
|
+
# Se lat ou lon não foram fornecidos, retornar mensagem amigável
|
|
168
|
+
if lat is None or lon is None:
|
|
169
|
+
return {
|
|
170
|
+
"message": "Informe latitude e longitude para consultar dados climáticos reais.",
|
|
171
|
+
"exemplo_sao_paulo": "/dados/clima?lat=-23.55&lon=-46.63&days=30",
|
|
172
|
+
"exemplo_brasilia": "/dados/clima?lat=-15.78&lon=-47.93&days=30",
|
|
173
|
+
"parametros": {
|
|
174
|
+
"lat": "Latitude da localização",
|
|
175
|
+
"lon": "Longitude da localização",
|
|
176
|
+
"days": "Número de dias analisados, padrão 30"
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if days < 1 or days > 365:
|
|
181
|
+
raise HTTPException(status_code=400, detail="Days deve estar entre 1 e 365")
|
|
182
|
+
|
|
183
|
+
weather_data = get_weather_summary(lat, lon, days)
|
|
184
|
+
return weather_data.to_dict()
|
|
185
|
+
|
|
186
|
+
except Exception as e:
|
|
187
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
188
|
+
|
|
139
189
|
@app.get("/dashboard")
|
|
140
|
-
def get_dashboard(
|
|
141
|
-
|
|
190
|
+
def get_dashboard(
|
|
191
|
+
lat: Optional[float] = None,
|
|
192
|
+
lon: Optional[float] = None,
|
|
193
|
+
days: int = Query(30, description="Número de dias para análise climática")
|
|
194
|
+
):
|
|
195
|
+
"""Retorna resumo do dashboard com contexto climático opcional"""
|
|
142
196
|
try:
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
197
|
+
culturas, talhoes, regras = get_dados()
|
|
198
|
+
|
|
199
|
+
# Obter contexto climático se coordenadas foram fornecidas
|
|
200
|
+
contexto_climatico = None
|
|
201
|
+
if lat is not None and lon is not None:
|
|
202
|
+
from core.climate_adapter import obter_contexto_climatico_por_coordenadas
|
|
203
|
+
contexto_climatico = obter_contexto_climatico_por_coordenadas(lat, lon, days)
|
|
204
|
+
|
|
205
|
+
# Gerar chave de cache considerando clima
|
|
206
|
+
cache_params = {"objetivo": "equilibrado", "seed": 42}
|
|
207
|
+
if lat is not None and lon is not None:
|
|
208
|
+
cache_params.update({"lat": lat, "lon": lon, "days": days})
|
|
209
|
+
|
|
210
|
+
cache_key = get_cache_key("dashboard", **cache_params)
|
|
211
|
+
|
|
212
|
+
def compute_dashboard():
|
|
213
|
+
# Usar AG com clima se disponível
|
|
214
|
+
if contexto_climatico:
|
|
215
|
+
from core.climate_adapter import gerar_plano_com_clima
|
|
216
|
+
resultado_ag = gerar_plano_com_clima(
|
|
217
|
+
culturas, talhoes, regras,
|
|
218
|
+
objetivo='equilibrado', seed=42,
|
|
219
|
+
lat=lat, lon=lon, days=days
|
|
220
|
+
)
|
|
221
|
+
else:
|
|
222
|
+
resultado_ag = get_ag_cacheado(objetivo='equilibrado', seed=42)
|
|
148
223
|
|
|
149
224
|
# Tenta validar
|
|
150
225
|
validacao = comparar_ag_com_forca_bruta(culturas, talhoes, regras, objetivo='equilibrado', seed=42)
|
|
151
226
|
|
|
152
|
-
#
|
|
227
|
+
# Preparar resultado base
|
|
153
228
|
if validacao.get('erro'):
|
|
154
|
-
|
|
229
|
+
resultado_base = {
|
|
155
230
|
"lucro_total": float(resultado_ag['lucro_total']),
|
|
156
231
|
"risco_medio": float(resultado_ag['risco_medio']),
|
|
157
232
|
"fitness": float(resultado_ag['fitness']),
|
|
@@ -179,47 +254,62 @@ def get_dashboard():
|
|
|
179
254
|
for p in resultado_ag['plano']
|
|
180
255
|
]
|
|
181
256
|
}
|
|
257
|
+
else:
|
|
258
|
+
resultado_base = {
|
|
259
|
+
"lucro_total": float(resultado_ag['lucro_total']),
|
|
260
|
+
"risco_medio": float(resultado_ag['risco_medio']),
|
|
261
|
+
"fitness": float(resultado_ag['fitness']),
|
|
262
|
+
"diversidade": int(resultado_ag['diversidade']),
|
|
263
|
+
"objetivo": str(resultado_ag['objetivo']),
|
|
264
|
+
"culturas_escolhidas": [str(p['cultura']) for p in resultado_ag['plano']],
|
|
265
|
+
"validacao": {
|
|
266
|
+
"otimo_global": bool(validacao.get('ag_encontrou_otimo_global', False)),
|
|
267
|
+
"total_combinacoes": int(validacao.get('forca_bruta', {}).get('total_combinacoes', 0))
|
|
268
|
+
},
|
|
269
|
+
"plano": [
|
|
270
|
+
{
|
|
271
|
+
"talhao": int(p['talhao']),
|
|
272
|
+
"area": float(p['area']),
|
|
273
|
+
"solo": str(p['solo']),
|
|
274
|
+
"clima": str(p['clima']),
|
|
275
|
+
"relevo": str(p['relevo']),
|
|
276
|
+
"agua": str(p['agua']),
|
|
277
|
+
"cultura": str(p['cultura']),
|
|
278
|
+
"lucro_estimado": float(p['lucro_estimado']),
|
|
279
|
+
"risco": float(p['risco']),
|
|
280
|
+
"nota": float(p['nota']),
|
|
281
|
+
"tempo": int(p['tempo'])
|
|
282
|
+
}
|
|
283
|
+
for p in resultado_ag['plano']
|
|
284
|
+
]
|
|
285
|
+
}
|
|
182
286
|
|
|
183
|
-
#
|
|
184
|
-
|
|
185
|
-
"
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
"
|
|
193
|
-
"
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
"relevo": str(p['relevo']),
|
|
202
|
-
"agua": str(p['agua']),
|
|
203
|
-
"cultura": str(p['cultura']),
|
|
204
|
-
"lucro_estimado": float(p['lucro_estimado']),
|
|
205
|
-
"risco": float(p['risco']),
|
|
206
|
-
"nota": float(p['nota']),
|
|
207
|
-
"tempo": int(p['tempo'])
|
|
208
|
-
}
|
|
209
|
-
for p in resultado_ag['plano']
|
|
210
|
-
]
|
|
211
|
-
}
|
|
287
|
+
# Adicionar informações de clima real
|
|
288
|
+
if contexto_climatico:
|
|
289
|
+
resultado_base["clima_real"] = {
|
|
290
|
+
"ativo": True,
|
|
291
|
+
"source": contexto_climatico.get("fonte", "unknown"),
|
|
292
|
+
"temperatura_media": contexto_climatico.get("temperatura_media"),
|
|
293
|
+
"precipitacao_total": contexto_climatico.get("precipitacao_total"),
|
|
294
|
+
"risco_climatico_estimado": contexto_climatico.get("risco_climatico_estimado"),
|
|
295
|
+
"clima_observado": contexto_climatico.get("clima_observado"),
|
|
296
|
+
"agua_observada": contexto_climatico.get("agua_observada"),
|
|
297
|
+
"ajuste_risco": contexto_climatico.get("ajuste_risco", 0),
|
|
298
|
+
"fallback": contexto_climatico.get("fallback", False),
|
|
299
|
+
"error": contexto_climatico.get("error")
|
|
300
|
+
}
|
|
301
|
+
else:
|
|
302
|
+
resultado_base["clima_real"] = {"ativo": False}
|
|
303
|
+
|
|
304
|
+
return resultado_base
|
|
212
305
|
|
|
213
|
-
# Usa cache para dashboard
|
|
214
|
-
|
|
215
|
-
resultado = get_or_compute_cache(key, montar_dashboard)
|
|
306
|
+
# Usa cache para dashboard com contexto climático
|
|
307
|
+
resultado = get_or_compute_cache(cache_key, compute_dashboard)
|
|
216
308
|
|
|
217
309
|
# Converte tipos Python (por segurança)
|
|
218
310
|
return converter_tipos_python(resultado)
|
|
219
311
|
except Exception as e:
|
|
220
312
|
raise HTTPException(status_code=500, detail=str(e))
|
|
221
|
-
except Exception as e:
|
|
222
|
-
raise HTTPException(status_code=500, detail=str(e))
|
|
223
313
|
|
|
224
314
|
@app.get("/talhoes")
|
|
225
315
|
def get_talhoes():
|
|
@@ -289,17 +379,42 @@ def get_culturas():
|
|
|
289
379
|
raise HTTPException(status_code=500, detail=str(e))
|
|
290
380
|
|
|
291
381
|
@app.get("/cenarios")
|
|
292
|
-
def get_cenarios(
|
|
293
|
-
|
|
382
|
+
def get_cenarios(
|
|
383
|
+
lat: Optional[float] = None,
|
|
384
|
+
lon: Optional[float] = None,
|
|
385
|
+
days: int = Query(30, description="Número de dias para análise climática")
|
|
386
|
+
):
|
|
387
|
+
"""Retorna comparação de cenários com contexto climático opcional"""
|
|
294
388
|
try:
|
|
389
|
+
# Obter contexto climático se coordenadas foram fornecidas
|
|
390
|
+
contexto_climatico = None
|
|
391
|
+
if lat is not None and lon is not None:
|
|
392
|
+
from core.climate_adapter import obter_contexto_climatico_por_coordenadas
|
|
393
|
+
contexto_climatico = obter_contexto_climatico_por_coordenadas(lat, lon, days)
|
|
394
|
+
|
|
395
|
+
# Gerar chave de cache considerando clima
|
|
396
|
+
cache_params = {"cenarios": True}
|
|
397
|
+
if lat is not None and lon is not None:
|
|
398
|
+
cache_params.update({"lat": lat, "lon": lon, "days": days})
|
|
399
|
+
|
|
400
|
+
cache_key = get_cache_key("cenarios", **cache_params)
|
|
401
|
+
|
|
295
402
|
def montar_cenarios():
|
|
296
403
|
culturas, talhoes, regras = get_dados()
|
|
297
404
|
|
|
298
405
|
# Gera todos os cenários
|
|
299
406
|
cenarios = gerar_cenarios(culturas, talhoes, regras)
|
|
300
407
|
|
|
301
|
-
#
|
|
302
|
-
|
|
408
|
+
# Usar AG com clima se disponível
|
|
409
|
+
if contexto_climatico:
|
|
410
|
+
from core.climate_adapter import gerar_plano_com_clima
|
|
411
|
+
resultado_ag = gerar_plano_com_clima(
|
|
412
|
+
culturas, talhoes, regras,
|
|
413
|
+
objetivo='equilibrado', seed=42,
|
|
414
|
+
lat=lat, lon=lon, days=days
|
|
415
|
+
)
|
|
416
|
+
else:
|
|
417
|
+
resultado_ag = get_ag_cacheado(objetivo='equilibrado', seed=42)
|
|
303
418
|
|
|
304
419
|
# Cria um mapa de talhões para facilitar o acesso
|
|
305
420
|
talhoes_dict = {int(row['id']): row for _, row in talhoes.iterrows()}
|
|
@@ -332,7 +447,7 @@ def get_cenarios():
|
|
|
332
447
|
]
|
|
333
448
|
}
|
|
334
449
|
|
|
335
|
-
# Adiciona AG
|
|
450
|
+
# Adiciona AG com contexto climático
|
|
336
451
|
cenarios_formatados['genetico'] = {
|
|
337
452
|
'nome': 'Algoritmo Genético',
|
|
338
453
|
'descricao': 'Solução otimizada automaticamente',
|
|
@@ -357,13 +472,28 @@ def get_cenarios():
|
|
|
357
472
|
]
|
|
358
473
|
}
|
|
359
474
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
475
|
+
# Adicionar informações de clima real se disponível
|
|
476
|
+
resultado_final = {"cenarios": cenarios_formatados}
|
|
477
|
+
if contexto_climatico:
|
|
478
|
+
resultado_final["clima_real"] = {
|
|
479
|
+
"ativo": True,
|
|
480
|
+
"source": contexto_climatico.get("fonte", "unknown"),
|
|
481
|
+
"temperatura_media": contexto_climatico.get("temperatura_media"),
|
|
482
|
+
"precipitacao_total": contexto_climatico.get("precipitacao_total"),
|
|
483
|
+
"risco_climatico_estimado": contexto_climatico.get("risco_climatico_estimado"),
|
|
484
|
+
"clima_observado": contexto_climatico.get("clima_observado"),
|
|
485
|
+
"agua_observada": contexto_climatico.get("agua_observada"),
|
|
486
|
+
"ajuste_risco": contexto_climatico.get("ajuste_risco", 0),
|
|
487
|
+
"fallback": contexto_climatico.get("fallback", False),
|
|
488
|
+
"error": contexto_climatico.get("error")
|
|
489
|
+
}
|
|
490
|
+
else:
|
|
491
|
+
resultado_final["clima_real"] = {"ativo": False}
|
|
492
|
+
|
|
493
|
+
return resultado_final
|
|
363
494
|
|
|
364
|
-
# Usa cache para cenários
|
|
365
|
-
|
|
366
|
-
resultado = get_or_compute_cache(key, montar_cenarios)
|
|
495
|
+
# Usa cache para cenários com contexto climático
|
|
496
|
+
resultado = get_or_compute_cache(cache_key, montar_cenarios)
|
|
367
497
|
|
|
368
498
|
return converter_tipos_python(resultado)
|
|
369
499
|
except Exception as e:
|
|
@@ -373,34 +503,54 @@ def get_cenarios():
|
|
|
373
503
|
|
|
374
504
|
@app.post("/otimizar")
|
|
375
505
|
def otimizar(request: OtimizarRequest):
|
|
376
|
-
"""Executa otimização com Algoritmo Genético"""
|
|
506
|
+
"""Executa otimização com Algoritmo Genético e contexto climático opcional"""
|
|
377
507
|
try:
|
|
378
508
|
# Valida objetivo
|
|
379
509
|
objetivos_validos = ['equilibrado', 'lucro', 'risco', 'sustentavel']
|
|
380
510
|
if request.objetivo not in objetivos_validos:
|
|
381
511
|
raise HTTPException(status_code=400, detail=f"Objetivo inválido. Use: {objetivos_validos}")
|
|
382
512
|
|
|
383
|
-
#
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
request.
|
|
388
|
-
|
|
513
|
+
# Obter contexto climático se coordenadas foram fornecidas
|
|
514
|
+
contexto_climatico = None
|
|
515
|
+
if request.lat is not None and request.lon is not None:
|
|
516
|
+
from core.climate_adapter import obter_contexto_climatico_por_coordenadas
|
|
517
|
+
contexto_climatico = obter_contexto_climatico_por_coordenadas(request.lat, request.lon, request.days)
|
|
518
|
+
|
|
519
|
+
# Usar AG com clima se disponível
|
|
520
|
+
if contexto_climatico:
|
|
521
|
+
from core.climate_adapter import gerar_plano_com_clima
|
|
522
|
+
resultado = gerar_plano_com_clima(
|
|
523
|
+
*get_dados(),
|
|
389
524
|
objetivo=request.objetivo,
|
|
390
525
|
seed=request.seed,
|
|
391
526
|
geracoes=request.geracoes,
|
|
392
|
-
populacao=request.populacao
|
|
393
|
-
)
|
|
394
|
-
else:
|
|
395
|
-
# Executa AG sem cache para parâmetros customizados
|
|
396
|
-
culturas, talhoes, regras = get_dados()
|
|
397
|
-
resultado = gerar_plano_genetico(
|
|
398
|
-
culturas, talhoes, regras,
|
|
399
|
-
objetivo=request.objetivo,
|
|
400
|
-
geracoes=request.geracoes,
|
|
401
527
|
populacao=request.populacao,
|
|
402
|
-
|
|
528
|
+
lat=request.lat,
|
|
529
|
+
lon=request.lon,
|
|
530
|
+
days=request.days
|
|
403
531
|
)
|
|
532
|
+
else:
|
|
533
|
+
# Usa AG cacheado se parâmetros forem padrão e sem clima
|
|
534
|
+
if (request.objetivo == "equilibrado" and
|
|
535
|
+
request.seed == 42 and
|
|
536
|
+
request.geracoes == 100 and
|
|
537
|
+
request.populacao == 50):
|
|
538
|
+
resultado = get_ag_cacheado(
|
|
539
|
+
objetivo=request.objetivo,
|
|
540
|
+
seed=request.seed,
|
|
541
|
+
geracoes=request.geracoes,
|
|
542
|
+
populacao=request.populacao
|
|
543
|
+
)
|
|
544
|
+
else:
|
|
545
|
+
# Executa AG sem cache para parâmetros customizados
|
|
546
|
+
culturas, talhoes, regras = get_dados()
|
|
547
|
+
resultado = gerar_plano_genetico(
|
|
548
|
+
culturas, talhoes, regras,
|
|
549
|
+
objetivo=request.objetivo,
|
|
550
|
+
geracoes=request.geracoes,
|
|
551
|
+
populacao=request.populacao,
|
|
552
|
+
seed=request.seed
|
|
553
|
+
)
|
|
404
554
|
|
|
405
555
|
# Converte tipos numpy para Python nativos
|
|
406
556
|
resultado_convertido = converter_tipos_python(resultado)
|
|
@@ -472,7 +622,7 @@ def rodadas(request: RodadasRequest):
|
|
|
472
622
|
|
|
473
623
|
@app.post("/relatorio")
|
|
474
624
|
def relatorio(request: RelatorioRequest):
|
|
475
|
-
"""Gera relatório"""
|
|
625
|
+
"""Gera relatório com contexto climático opcional"""
|
|
476
626
|
try:
|
|
477
627
|
culturas, talhoes, regras = get_dados()
|
|
478
628
|
|
|
@@ -486,7 +636,14 @@ def relatorio(request: RelatorioRequest):
|
|
|
486
636
|
if request.formato not in formatos_validos:
|
|
487
637
|
raise HTTPException(status_code=400, detail=f"Formato inválido. Use: {formatos_validos}")
|
|
488
638
|
|
|
489
|
-
#
|
|
639
|
+
# Obter contexto climático se coordenadas foram fornecidas
|
|
640
|
+
contexto_climatico = None
|
|
641
|
+
if request.lat is not None and request.lon is not None:
|
|
642
|
+
from core.climate_adapter import obter_contexto_climatico_por_coordenadas
|
|
643
|
+
contexto_climatico = obter_contexto_climatico_por_coordenadas(request.lat, request.lon, request.days)
|
|
644
|
+
|
|
645
|
+
# Gera relatório (por enquanto sem integração climática no gerador)
|
|
646
|
+
# TODO: Atualizar report_generator para aceitar contexto climático
|
|
490
647
|
caminho = gerar_relatorio_completo(
|
|
491
648
|
culturas, talhoes, regras,
|
|
492
649
|
objetivo=request.objetivo,
|
|
@@ -497,11 +654,48 @@ def relatorio(request: RelatorioRequest):
|
|
|
497
654
|
with open(caminho, 'r', encoding='utf-8') as f:
|
|
498
655
|
conteudo = f.read()
|
|
499
656
|
|
|
500
|
-
|
|
657
|
+
# Adiciona informações climáticas ao final se disponível
|
|
658
|
+
if contexto_climatico and not contexto_climatico.get("fallback", True):
|
|
659
|
+
clima_info = f"""
|
|
660
|
+
|
|
661
|
+
## Dados Climáticos Reais
|
|
662
|
+
|
|
663
|
+
**Fonte:** {contexto_climatico.get('fonte', 'N/A')}
|
|
664
|
+
**Temperatura Média:** {contexto_climatico.get('temperatura_media', 'N/A')}°C
|
|
665
|
+
**Precipitação Total:** {contexto_climatico.get('precipitacao_total', 'N/A')}mm
|
|
666
|
+
**Risco Climático:** {contexto_climatico.get('risco_climatico_estimado', 'N/A')}
|
|
667
|
+
**Clima Observado:** {contexto_climatico.get('clima_observado', 'N/A')}
|
|
668
|
+
**Água Observada:** {contexto_climatico.get('agua_observada', 'N/A')}
|
|
669
|
+
**Ajuste de Risco:** {contexto_climatico.get('ajuste_risco', 0):+.1%}
|
|
670
|
+
|
|
671
|
+
*Dados climáticos integrados ao planejamento para maior precisão.*
|
|
672
|
+
"""
|
|
673
|
+
conteudo += clima_info
|
|
674
|
+
|
|
675
|
+
resultado = {
|
|
501
676
|
"caminho": caminho,
|
|
502
677
|
"conteudo": conteudo,
|
|
503
678
|
"formato": request.formato
|
|
504
679
|
}
|
|
680
|
+
|
|
681
|
+
# Adicionar informações de clima real
|
|
682
|
+
if contexto_climatico:
|
|
683
|
+
resultado["clima_real"] = {
|
|
684
|
+
"ativo": True,
|
|
685
|
+
"source": contexto_climatico.get("fonte", "unknown"),
|
|
686
|
+
"temperatura_media": contexto_climatico.get("temperatura_media"),
|
|
687
|
+
"precipitacao_total": contexto_climatico.get("precipitacao_total"),
|
|
688
|
+
"risco_climatico_estimado": contexto_climatico.get("risco_climatico_estimado"),
|
|
689
|
+
"clima_observado": contexto_climatico.get("clima_observado"),
|
|
690
|
+
"agua_observada": contexto_climatico.get("agua_observada"),
|
|
691
|
+
"ajuste_risco": contexto_climatico.get("ajuste_risco", 0),
|
|
692
|
+
"fallback": contexto_climatico.get("fallback", False),
|
|
693
|
+
"error": contexto_climatico.get("error")
|
|
694
|
+
}
|
|
695
|
+
else:
|
|
696
|
+
resultado["clima_real"] = {"ativo": False}
|
|
697
|
+
|
|
698
|
+
return resultado
|
|
505
699
|
except HTTPException:
|
|
506
700
|
raise
|
|
507
701
|
except Exception as e:
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Adaptador para integrar dados climáticos reais no planejamento agrícola
|
|
3
|
+
"""
|
|
4
|
+
from typing import Optional, Dict, Any
|
|
5
|
+
from providers.weather_provider import get_weather_summary
|
|
6
|
+
from providers.types import WeatherSummary
|
|
7
|
+
|
|
8
|
+
def classificar_clima_por_temperatura(temp_media: Optional[float]) -> Optional[str]:
|
|
9
|
+
"""Classifica clima baseado na temperatura média"""
|
|
10
|
+
if temp_media is None:
|
|
11
|
+
return None
|
|
12
|
+
|
|
13
|
+
if temp_media >= 28:
|
|
14
|
+
return "quente"
|
|
15
|
+
if temp_media < 18:
|
|
16
|
+
return "frio"
|
|
17
|
+
return "ameno"
|
|
18
|
+
|
|
19
|
+
def classificar_agua_por_precipitacao(precipitacao_total: Optional[float]) -> Optional[str]:
|
|
20
|
+
"""Classifica disponibilidade de água baseada na precipitação"""
|
|
21
|
+
if precipitacao_total is None:
|
|
22
|
+
return None
|
|
23
|
+
|
|
24
|
+
if precipitacao_total < 50:
|
|
25
|
+
return "baixa"
|
|
26
|
+
if precipitacao_total > 120:
|
|
27
|
+
return "alta"
|
|
28
|
+
return "media"
|
|
29
|
+
|
|
30
|
+
def calcular_ajuste_risco_climatico(risco_climatico: str) -> float:
|
|
31
|
+
"""Calcula ajuste de risco baseado no risco climático estimado"""
|
|
32
|
+
ajustes = {
|
|
33
|
+
"alto": 0.15, # +15% risco
|
|
34
|
+
"medio": 0.05, # +5% risco
|
|
35
|
+
"baixo": -0.03, # -3% risco (leve benefício)
|
|
36
|
+
"indeterminado": 0
|
|
37
|
+
}
|
|
38
|
+
return ajustes.get(risco_climatico, 0)
|
|
39
|
+
|
|
40
|
+
def criar_contexto_climatico(weather_summary: WeatherSummary) -> Dict[str, Any]:
|
|
41
|
+
"""Cria contexto climático para uso no planejamento"""
|
|
42
|
+
|
|
43
|
+
clima_observado = classificar_clima_por_temperatura(weather_summary.temperatura_media)
|
|
44
|
+
agua_observada = classificar_agua_por_precipitacao(weather_summary.precipitacao_total)
|
|
45
|
+
ajuste_risco = calcular_ajuste_risco_climatico(weather_summary.risco_climatico_estimado)
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
"clima_observado": clima_observado,
|
|
49
|
+
"agua_observada": agua_observada,
|
|
50
|
+
"ajuste_risco": ajuste_risco,
|
|
51
|
+
"fonte": weather_summary.source,
|
|
52
|
+
"fallback": weather_summary.fallback,
|
|
53
|
+
"temperatura_media": weather_summary.temperatura_media,
|
|
54
|
+
"temperatura_maxima": weather_summary.temperatura_maxima,
|
|
55
|
+
"temperatura_minima": weather_summary.temperatura_minima,
|
|
56
|
+
"precipitacao_total": weather_summary.precipitacao_total,
|
|
57
|
+
"evapotranspiracao": weather_summary.evapotranspiracao,
|
|
58
|
+
"umidade_media": weather_summary.umidade_media,
|
|
59
|
+
"radiacao_solar": weather_summary.radiacao_solar,
|
|
60
|
+
"risco_climatico_estimado": weather_summary.risco_climatico_estimado,
|
|
61
|
+
"error": weather_summary.error
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
def aplicar_contexto_climatico_no_plano(resultado_ag: Dict[str, Any], contexto_climatico: Dict[str, Any]) -> Dict[str, Any]:
|
|
65
|
+
"""Aplica ajustes climáticos no resultado do algoritmo genético"""
|
|
66
|
+
|
|
67
|
+
if not contexto_climatico or contexto_climatico.get("ajuste_risco", 0) == 0:
|
|
68
|
+
# Sem ajuste necessário
|
|
69
|
+
resultado_ag["ajuste_climatico_aplicado"] = False
|
|
70
|
+
resultado_ag["contexto_climatico"] = contexto_climatico
|
|
71
|
+
return resultado_ag
|
|
72
|
+
|
|
73
|
+
ajuste_risco = contexto_climatico["ajuste_risco"]
|
|
74
|
+
|
|
75
|
+
# Aplicar ajuste no plano otimizado
|
|
76
|
+
if "plano_otimizado" in resultado_ag:
|
|
77
|
+
for item in resultado_ag["plano_otimizado"]:
|
|
78
|
+
if "risco" in item:
|
|
79
|
+
risco_original = item["risco"]
|
|
80
|
+
# Aplicar ajuste mantendo risco entre 0.05 e 0.95
|
|
81
|
+
novo_risco = min(0.95, max(0.05, risco_original + ajuste_risco))
|
|
82
|
+
item["risco"] = round(novo_risco, 3)
|
|
83
|
+
item["risco_original"] = risco_original
|
|
84
|
+
item["ajuste_aplicado"] = ajuste_risco
|
|
85
|
+
|
|
86
|
+
# Recalcular métricas gerais se existirem
|
|
87
|
+
if "metricas" in resultado_ag and "risco_medio" in resultado_ag["metricas"]:
|
|
88
|
+
risco_original = resultado_ag["metricas"]["risco_medio"]
|
|
89
|
+
novo_risco = min(0.95, max(0.05, risco_original + ajuste_risco))
|
|
90
|
+
resultado_ag["metricas"]["risco_medio"] = round(novo_risco, 3)
|
|
91
|
+
resultado_ag["metricas"]["risco_medio_original"] = risco_original
|
|
92
|
+
|
|
93
|
+
# Marcar que ajuste foi aplicado
|
|
94
|
+
resultado_ag["ajuste_climatico_aplicado"] = True
|
|
95
|
+
resultado_ag["contexto_climatico"] = contexto_climatico
|
|
96
|
+
|
|
97
|
+
return resultado_ag
|
|
98
|
+
|
|
99
|
+
def obter_contexto_climatico_por_coordenadas(lat: float, lon: float, days: int = 30) -> Optional[Dict[str, Any]]:
|
|
100
|
+
"""Obtém contexto climático para coordenadas específicas"""
|
|
101
|
+
try:
|
|
102
|
+
weather_summary = get_weather_summary(lat, lon, days)
|
|
103
|
+
return criar_contexto_climatico(weather_summary)
|
|
104
|
+
except Exception as e:
|
|
105
|
+
# Em caso de erro, retornar contexto vazio
|
|
106
|
+
return {
|
|
107
|
+
"clima_observado": None,
|
|
108
|
+
"agua_observada": None,
|
|
109
|
+
"ajuste_risco": 0,
|
|
110
|
+
"fonte": "erro",
|
|
111
|
+
"fallback": True,
|
|
112
|
+
"error": str(e),
|
|
113
|
+
"temperatura_media": None,
|
|
114
|
+
"precipitacao_total": None,
|
|
115
|
+
"risco_climatico_estimado": "indeterminado"
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
def gerar_plano_com_clima(culturas, talhoes, regras, objetivo="equilibrado", seed=42,
|
|
119
|
+
geracoes=100, populacao=50, lat=None, lon=None, days=30):
|
|
120
|
+
"""
|
|
121
|
+
Wrapper para gerar plano genético com contexto climático opcional
|
|
122
|
+
"""
|
|
123
|
+
from .planner import gerar_plano_genetico
|
|
124
|
+
|
|
125
|
+
# Gerar plano normalmente
|
|
126
|
+
resultado = gerar_plano_genetico(
|
|
127
|
+
culturas, talhoes, regras,
|
|
128
|
+
objetivo=objetivo, seed=seed,
|
|
129
|
+
geracoes=geracoes, populacao=populacao
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# Se coordenadas foram fornecidas, aplicar contexto climático
|
|
133
|
+
if lat is not None and lon is not None:
|
|
134
|
+
contexto_climatico = obter_contexto_climatico_por_coordenadas(lat, lon, days)
|
|
135
|
+
if contexto_climatico:
|
|
136
|
+
resultado = aplicar_contexto_climatico_no_plano(resultado, contexto_climatico)
|
|
137
|
+
else:
|
|
138
|
+
# Marcar que não há clima real aplicado
|
|
139
|
+
resultado["ajuste_climatico_aplicado"] = False
|
|
140
|
+
resultado["contexto_climatico"] = None
|
|
141
|
+
|
|
142
|
+
return resultado
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Provedores de dados reais para AgroPlan AI
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Cache simples em memória para chamadas de APIs externas
|
|
3
|
+
"""
|
|
4
|
+
import time
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
# Cache global em memória
|
|
8
|
+
_cache = {}
|
|
9
|
+
|
|
10
|
+
def get_cache(key: str) -> Optional[Any]:
|
|
11
|
+
"""Recupera valor do cache se ainda válido"""
|
|
12
|
+
if key not in _cache:
|
|
13
|
+
return None
|
|
14
|
+
|
|
15
|
+
value, expiry = _cache[key]
|
|
16
|
+
if time.time() > expiry:
|
|
17
|
+
# Cache expirado, remove
|
|
18
|
+
del _cache[key]
|
|
19
|
+
return None
|
|
20
|
+
|
|
21
|
+
return value
|
|
22
|
+
|
|
23
|
+
def set_cache(key: str, value: Any, ttl_seconds: int = 3600) -> None:
|
|
24
|
+
"""Armazena valor no cache com TTL"""
|
|
25
|
+
expiry = time.time() + ttl_seconds
|
|
26
|
+
_cache[key] = (value, expiry)
|
|
27
|
+
|
|
28
|
+
def clear_provider_cache() -> None:
|
|
29
|
+
"""Limpa todo o cache de provedores"""
|
|
30
|
+
global _cache
|
|
31
|
+
_cache = {}
|
|
32
|
+
|
|
33
|
+
def get_cache_stats() -> dict:
|
|
34
|
+
"""Retorna estatísticas do cache"""
|
|
35
|
+
current_time = time.time()
|
|
36
|
+
valid_items = 0
|
|
37
|
+
expired_items = 0
|
|
38
|
+
|
|
39
|
+
for key, (value, expiry) in _cache.items():
|
|
40
|
+
if current_time <= expiry:
|
|
41
|
+
valid_items += 1
|
|
42
|
+
else:
|
|
43
|
+
expired_items += 1
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
"total_items": len(_cache),
|
|
47
|
+
"valid_items": valid_items,
|
|
48
|
+
"expired_items": expired_items
|
|
49
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tipos de dados para provedores externos
|
|
3
|
+
"""
|
|
4
|
+
from typing import Optional
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class WeatherSummary:
|
|
9
|
+
"""Resumo de dados climáticos"""
|
|
10
|
+
source: str
|
|
11
|
+
latitude: float
|
|
12
|
+
longitude: float
|
|
13
|
+
temperatura_media: Optional[float] = None
|
|
14
|
+
temperatura_maxima: Optional[float] = None
|
|
15
|
+
temperatura_minima: Optional[float] = None
|
|
16
|
+
precipitacao_total: Optional[float] = None
|
|
17
|
+
evapotranspiracao: Optional[float] = None
|
|
18
|
+
umidade_media: Optional[float] = None
|
|
19
|
+
radiacao_solar: Optional[float] = None
|
|
20
|
+
risco_climatico_estimado: str = "indeterminado"
|
|
21
|
+
fallback: bool = False
|
|
22
|
+
error: Optional[str] = None
|
|
23
|
+
|
|
24
|
+
def to_dict(self) -> dict:
|
|
25
|
+
"""Converte para dicionário para JSON"""
|
|
26
|
+
return {
|
|
27
|
+
"source": self.source,
|
|
28
|
+
"latitude": self.latitude,
|
|
29
|
+
"longitude": self.longitude,
|
|
30
|
+
"temperatura_media": self.temperatura_media,
|
|
31
|
+
"temperatura_maxima": self.temperatura_maxima,
|
|
32
|
+
"temperatura_minima": self.temperatura_minima,
|
|
33
|
+
"precipitacao_total": self.precipitacao_total,
|
|
34
|
+
"evapotranspiracao": self.evapotranspiracao,
|
|
35
|
+
"umidade_media": self.umidade_media,
|
|
36
|
+
"radiacao_solar": self.radiacao_solar,
|
|
37
|
+
"risco_climatico_estimado": self.risco_climatico_estimado,
|
|
38
|
+
"fallback": self.fallback,
|
|
39
|
+
"error": self.error
|
|
40
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Provedor de dados climáticos usando Open-Meteo
|
|
3
|
+
"""
|
|
4
|
+
import urllib.request
|
|
5
|
+
import urllib.parse
|
|
6
|
+
import json
|
|
7
|
+
from datetime import datetime, timedelta
|
|
8
|
+
from typing import Optional
|
|
9
|
+
from .types import WeatherSummary
|
|
10
|
+
from .cache import get_cache, set_cache
|
|
11
|
+
|
|
12
|
+
def estimar_risco_climatico(temp_media: Optional[float], precipitacao_total: Optional[float]) -> str:
|
|
13
|
+
"""Estima risco climático baseado em temperatura e precipitação"""
|
|
14
|
+
if temp_media is None or precipitacao_total is None:
|
|
15
|
+
return "indeterminado"
|
|
16
|
+
|
|
17
|
+
# Heurística simples inicial
|
|
18
|
+
if precipitacao_total < 30:
|
|
19
|
+
return "alto"
|
|
20
|
+
if temp_media > 34:
|
|
21
|
+
return "alto"
|
|
22
|
+
if precipitacao_total < 70:
|
|
23
|
+
return "medio"
|
|
24
|
+
return "baixo"
|
|
25
|
+
|
|
26
|
+
def get_weather_summary(lat: float, lon: float, days: int = 30) -> WeatherSummary:
|
|
27
|
+
"""
|
|
28
|
+
Obtém resumo climático usando Open-Meteo
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
lat: Latitude
|
|
32
|
+
lon: Longitude
|
|
33
|
+
days: Número de dias para análise (padrão 30)
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
WeatherSummary com dados climáticos ou fallback
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
# Chave do cache
|
|
40
|
+
cache_key = f"weather_{lat}_{lon}_{days}"
|
|
41
|
+
|
|
42
|
+
# Verificar cache primeiro
|
|
43
|
+
cached = get_cache(cache_key)
|
|
44
|
+
if cached:
|
|
45
|
+
return WeatherSummary(**cached)
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
# Calcular datas (últimos N dias)
|
|
49
|
+
end_date = datetime.now().date()
|
|
50
|
+
start_date = end_date - timedelta(days=days)
|
|
51
|
+
|
|
52
|
+
# Parâmetros da API Open-Meteo Archive
|
|
53
|
+
params = {
|
|
54
|
+
"latitude": str(lat),
|
|
55
|
+
"longitude": str(lon),
|
|
56
|
+
"start_date": start_date.strftime("%Y-%m-%d"),
|
|
57
|
+
"end_date": end_date.strftime("%Y-%m-%d"),
|
|
58
|
+
"daily": ",".join([
|
|
59
|
+
"temperature_2m_mean",
|
|
60
|
+
"temperature_2m_max",
|
|
61
|
+
"temperature_2m_min",
|
|
62
|
+
"precipitation_sum",
|
|
63
|
+
"et0_fao_evapotranspiration",
|
|
64
|
+
"relative_humidity_2m_mean",
|
|
65
|
+
"shortwave_radiation_sum"
|
|
66
|
+
]),
|
|
67
|
+
"timezone": "America/Sao_Paulo"
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
# Construir URL
|
|
71
|
+
base_url = "https://archive-api.open-meteo.com/v1/archive"
|
|
72
|
+
query_string = urllib.parse.urlencode(params)
|
|
73
|
+
url = f"{base_url}?{query_string}"
|
|
74
|
+
|
|
75
|
+
# Fazer requisição
|
|
76
|
+
with urllib.request.urlopen(url, timeout=10) as response:
|
|
77
|
+
data = json.loads(response.read().decode())
|
|
78
|
+
|
|
79
|
+
daily = data.get("daily", {})
|
|
80
|
+
|
|
81
|
+
# Extrair dados
|
|
82
|
+
temps_mean = daily.get("temperature_2m_mean", [])
|
|
83
|
+
temps_max = daily.get("temperature_2m_max", [])
|
|
84
|
+
temps_min = daily.get("temperature_2m_min", [])
|
|
85
|
+
precipitation = daily.get("precipitation_sum", [])
|
|
86
|
+
evapotranspiration = daily.get("et0_fao_evapotranspiration", [])
|
|
87
|
+
humidity = daily.get("relative_humidity_2m_mean", [])
|
|
88
|
+
radiation = daily.get("shortwave_radiation_sum", [])
|
|
89
|
+
|
|
90
|
+
# Calcular médias (ignorando valores None)
|
|
91
|
+
def safe_mean(values):
|
|
92
|
+
valid_values = [v for v in values if v is not None]
|
|
93
|
+
return sum(valid_values) / len(valid_values) if valid_values else None
|
|
94
|
+
|
|
95
|
+
def safe_sum(values):
|
|
96
|
+
valid_values = [v for v in values if v is not None]
|
|
97
|
+
return sum(valid_values) if valid_values else None
|
|
98
|
+
|
|
99
|
+
temp_media = safe_mean(temps_mean)
|
|
100
|
+
temp_maxima = safe_mean(temps_max)
|
|
101
|
+
temp_minima = safe_mean(temps_min)
|
|
102
|
+
precip_total = safe_sum(precipitation)
|
|
103
|
+
evap_media = safe_mean(evapotranspiration)
|
|
104
|
+
umid_media = safe_mean(humidity)
|
|
105
|
+
rad_total = safe_sum(radiation)
|
|
106
|
+
|
|
107
|
+
# Estimar risco climático
|
|
108
|
+
risco = estimar_risco_climatico(temp_media, precip_total)
|
|
109
|
+
|
|
110
|
+
# Criar resultado
|
|
111
|
+
result = WeatherSummary(
|
|
112
|
+
source="open-meteo",
|
|
113
|
+
latitude=lat,
|
|
114
|
+
longitude=lon,
|
|
115
|
+
temperatura_media=round(temp_media, 1) if temp_media else None,
|
|
116
|
+
temperatura_maxima=round(temp_maxima, 1) if temp_maxima else None,
|
|
117
|
+
temperatura_minima=round(temp_minima, 1) if temp_minima else None,
|
|
118
|
+
precipitacao_total=round(precip_total, 1) if precip_total else None,
|
|
119
|
+
evapotranspiracao=round(evap_media, 2) if evap_media else None,
|
|
120
|
+
umidade_media=round(umid_media, 1) if umid_media else None,
|
|
121
|
+
radiacao_solar=round(rad_total, 1) if rad_total else None,
|
|
122
|
+
risco_climatico_estimado=risco,
|
|
123
|
+
fallback=False,
|
|
124
|
+
error=None
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# Cachear por 1 hora (dados históricos mudam pouco)
|
|
128
|
+
set_cache(cache_key, result.to_dict(), ttl_seconds=3600)
|
|
129
|
+
|
|
130
|
+
return result
|
|
131
|
+
|
|
132
|
+
except Exception as e:
|
|
133
|
+
# Fallback para dados simulados
|
|
134
|
+
return _get_fallback_weather(lat, lon, str(e))
|
|
135
|
+
|
|
136
|
+
def _get_fallback_weather(lat: float, lon: float, error: str) -> WeatherSummary:
|
|
137
|
+
"""Retorna dados climáticos simulados como fallback"""
|
|
138
|
+
|
|
139
|
+
# Dados simulados baseados na localização (heurística simples)
|
|
140
|
+
# Região Sudeste do Brasil como referência
|
|
141
|
+
temp_base = 22.0
|
|
142
|
+
if lat < -25: # Mais ao sul, mais frio
|
|
143
|
+
temp_base = 18.0
|
|
144
|
+
elif lat > -20: # Mais ao norte, mais quente
|
|
145
|
+
temp_base = 26.0
|
|
146
|
+
|
|
147
|
+
return WeatherSummary(
|
|
148
|
+
source="simulado",
|
|
149
|
+
latitude=lat,
|
|
150
|
+
longitude=lon,
|
|
151
|
+
temperatura_media=temp_base,
|
|
152
|
+
temperatura_maxima=temp_base + 8,
|
|
153
|
+
temperatura_minima=temp_base - 6,
|
|
154
|
+
precipitacao_total=120.0, # mm/mês típico
|
|
155
|
+
evapotranspiracao=4.5, # mm/dia típico
|
|
156
|
+
umidade_media=65.0, # % típico
|
|
157
|
+
radiacao_solar=180.0, # MJ/m² típico mensal
|
|
158
|
+
risco_climatico_estimado="medio",
|
|
159
|
+
fallback=True,
|
|
160
|
+
error=f"Open-Meteo indisponível: {error}"
|
|
161
|
+
)
|
package/dist/index.js
CHANGED
|
@@ -779,7 +779,7 @@ var COMMANDS = {
|
|
|
779
779
|
open: "Abre o AgroPlan AI no navegador"
|
|
780
780
|
};
|
|
781
781
|
function showHelp() {
|
|
782
|
-
console.log("\uD83C\uDF31 AgroPlan AI - CLI Local v1.0.
|
|
782
|
+
console.log("\uD83C\uDF31 AgroPlan AI - CLI Local v1.0.8");
|
|
783
783
|
console.log(` Launcher para modo local acelerado
|
|
784
784
|
`);
|
|
785
785
|
console.log("\uD83D\uDCCB Comandos dispon\xEDveis:");
|