agroplan-ai-cli 1.0.15 → 1.0.18
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 +19 -0
- package/backend-template/api.py +128 -65
- package/backend-template/core/zarc_adapter.py +290 -0
- package/backend-template/data/zarc/zarc_index_2025-2026.json +1612 -0
- package/backend-template/providers/zarc_provider.py +294 -130
- package/backend-template/scripts/build_zarc_index.py +256 -0
- package/package.json +1 -1
|
@@ -24,3 +24,22 @@ PROVIDER_CACHE_TTL=3600
|
|
|
24
24
|
# If set, /cache/limpar endpoint requires X-Cache-Token header
|
|
25
25
|
# Leave empty to disable protection (not recommended for production)
|
|
26
26
|
CACHE_ADMIN_TOKEN=your-secret-token-here
|
|
27
|
+
|
|
28
|
+
# ZARC Configuration
|
|
29
|
+
# Source: official (download from gov portal) or fallback (use local simplified data)
|
|
30
|
+
ZARC_SOURCE=official
|
|
31
|
+
|
|
32
|
+
# Safra padrão
|
|
33
|
+
ZARC_SAFRA=2025/2026
|
|
34
|
+
|
|
35
|
+
# Cache TTL em segundos (padrão: 86400 = 24 horas)
|
|
36
|
+
ZARC_CACHE_TTL=86400
|
|
37
|
+
|
|
38
|
+
# Fast Index: usa índice compacto pré-processado (recomendado: true)
|
|
39
|
+
# O índice contém apenas regiões/culturas de interesse (~35KB vs 214MB)
|
|
40
|
+
ZARC_FAST_INDEX_ENABLED=true
|
|
41
|
+
|
|
42
|
+
# Allow Full Scan: permite varrer CSV completo se não encontrar no índice
|
|
43
|
+
# Produção (Render): false - evita timeout e uso excessivo de CPU
|
|
44
|
+
# Desenvolvimento local: true - permite consultar qualquer região
|
|
45
|
+
ZARC_ALLOW_FULL_SCAN=false
|
package/backend-template/api.py
CHANGED
|
@@ -73,6 +73,9 @@ class OtimizarRequest(BaseModel):
|
|
|
73
73
|
lat: Optional[float] = None
|
|
74
74
|
lon: Optional[float] = None
|
|
75
75
|
days: Optional[int] = 30
|
|
76
|
+
uf: Optional[str] = None
|
|
77
|
+
municipio: Optional[str] = None
|
|
78
|
+
safra: Optional[str] = "2025/2026"
|
|
76
79
|
|
|
77
80
|
class ValidarRequest(BaseModel):
|
|
78
81
|
objetivo: str = "equilibrado"
|
|
@@ -84,6 +87,9 @@ class RelatorioRequest(BaseModel):
|
|
|
84
87
|
lat: Optional[float] = None
|
|
85
88
|
lon: Optional[float] = None
|
|
86
89
|
days: Optional[int] = 30
|
|
90
|
+
uf: Optional[str] = None
|
|
91
|
+
municipio: Optional[str] = None
|
|
92
|
+
safra: Optional[str] = "2025/2026"
|
|
87
93
|
|
|
88
94
|
class RodadasRequest(BaseModel):
|
|
89
95
|
objetivo: str = "equilibrado"
|
|
@@ -141,6 +147,10 @@ def health():
|
|
|
141
147
|
culturas, talhoes, regras = get_dados()
|
|
142
148
|
provider_cache_stats = get_cache_stats()
|
|
143
149
|
|
|
150
|
+
# Verificar status ZARC (memory safe - não carrega CSV)
|
|
151
|
+
from providers.zarc_provider import get_zarc_status
|
|
152
|
+
zarc_status = get_zarc_status()
|
|
153
|
+
|
|
144
154
|
return {
|
|
145
155
|
"status": "healthy",
|
|
146
156
|
"culturas": len(culturas),
|
|
@@ -149,7 +159,8 @@ def health():
|
|
|
149
159
|
"cache_items": len(_resultados_cache),
|
|
150
160
|
"data_mode": DATA_MODE,
|
|
151
161
|
"providers": {
|
|
152
|
-
"weather": "available" if WEATHER_PROVIDER else "disabled"
|
|
162
|
+
"weather": "available" if WEATHER_PROVIDER else "disabled",
|
|
163
|
+
"zarc": zarc_status
|
|
153
164
|
},
|
|
154
165
|
"provider_cache": provider_cache_stats
|
|
155
166
|
}
|
|
@@ -186,13 +197,71 @@ def get_clima(
|
|
|
186
197
|
except Exception as e:
|
|
187
198
|
raise HTTPException(status_code=500, detail=str(e))
|
|
188
199
|
|
|
200
|
+
@app.get("/dados/zarc")
|
|
201
|
+
def get_zarc(
|
|
202
|
+
cultura: Optional[str] = Query(None, description="Nome da cultura"),
|
|
203
|
+
uf: Optional[str] = Query(None, description="Unidade Federativa (ex: SP, PR)"),
|
|
204
|
+
municipio: Optional[str] = Query(None, description="Nome do município"),
|
|
205
|
+
solo: Optional[str] = Query(None, description="Tipo de solo"),
|
|
206
|
+
safra: str = Query("2025/2026", description="Safra (ex: 2025/2026)")
|
|
207
|
+
):
|
|
208
|
+
"""Obtém dados ZARC (Zoneamento Agrícola de Risco Climático)"""
|
|
209
|
+
try:
|
|
210
|
+
# Importar provider ZARC
|
|
211
|
+
from providers.zarc_provider import buscar_zarc
|
|
212
|
+
|
|
213
|
+
# Se cultura não foi fornecida, retornar mensagem amigável
|
|
214
|
+
if not cultura:
|
|
215
|
+
return {
|
|
216
|
+
"message": "Informe a cultura para consultar dados ZARC.",
|
|
217
|
+
"exemplo_soja_sp": "/dados/zarc?cultura=soja&uf=SP&municipio=Sao%20Paulo&solo=argiloso",
|
|
218
|
+
"exemplo_milho_pr": "/dados/zarc?cultura=milho&uf=PR&municipio=Londrina&solo=argiloso",
|
|
219
|
+
"parametros": {
|
|
220
|
+
"cultura": "Nome da cultura (obrigatório)",
|
|
221
|
+
"uf": "Unidade Federativa (opcional)",
|
|
222
|
+
"municipio": "Nome do município (opcional)",
|
|
223
|
+
"solo": "Tipo de solo (opcional)",
|
|
224
|
+
"safra": "Safra, padrão 2025/2026"
|
|
225
|
+
},
|
|
226
|
+
"culturas_disponiveis": ["soja", "milho", "feijao", "cafe", "cana", "trigo", "algodao"],
|
|
227
|
+
"safras_disponiveis": ["2025/2026", "2026/2027"]
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
# Buscar dados ZARC
|
|
231
|
+
zarc_data = buscar_zarc(
|
|
232
|
+
cultura=cultura,
|
|
233
|
+
uf=uf,
|
|
234
|
+
municipio=municipio,
|
|
235
|
+
solo=solo,
|
|
236
|
+
safra=safra
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
if zarc_data:
|
|
240
|
+
return zarc_data
|
|
241
|
+
else:
|
|
242
|
+
return {
|
|
243
|
+
"message": "Dados ZARC não encontrados para os parâmetros fornecidos.",
|
|
244
|
+
"cultura": cultura,
|
|
245
|
+
"uf": uf,
|
|
246
|
+
"municipio": municipio,
|
|
247
|
+
"solo": solo,
|
|
248
|
+
"safra": safra,
|
|
249
|
+
"sugestao": "Tente com parâmetros mais genéricos (apenas cultura e UF)"
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
except Exception as e:
|
|
253
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
254
|
+
|
|
189
255
|
@app.get("/dashboard")
|
|
190
256
|
def get_dashboard(
|
|
191
257
|
lat: Optional[float] = None,
|
|
192
258
|
lon: Optional[float] = None,
|
|
193
|
-
days: int = Query(30, description="Número de dias para análise climática")
|
|
259
|
+
days: int = Query(30, description="Número de dias para análise climática"),
|
|
260
|
+
uf: Optional[str] = None,
|
|
261
|
+
municipio: Optional[str] = None,
|
|
262
|
+
safra: str = Query("2025/2026", description="Safra ZARC")
|
|
194
263
|
):
|
|
195
|
-
"""Retorna resumo do dashboard com contexto climático opcional"""
|
|
264
|
+
"""Retorna resumo do dashboard com contexto climático e ZARC opcional"""
|
|
196
265
|
try:
|
|
197
266
|
culturas, talhoes, regras = get_dados()
|
|
198
267
|
|
|
@@ -202,10 +271,12 @@ def get_dashboard(
|
|
|
202
271
|
from core.climate_adapter import obter_contexto_climatico_por_coordenadas
|
|
203
272
|
contexto_climatico = obter_contexto_climatico_por_coordenadas(lat, lon, days)
|
|
204
273
|
|
|
205
|
-
# Gerar chave de cache considerando clima
|
|
274
|
+
# Gerar chave de cache considerando clima e ZARC
|
|
206
275
|
cache_params = {"objetivo": "equilibrado", "seed": 42}
|
|
207
276
|
if lat is not None and lon is not None:
|
|
208
277
|
cache_params.update({"lat": lat, "lon": lon, "days": days})
|
|
278
|
+
if uf:
|
|
279
|
+
cache_params.update({"uf": uf, "municipio": municipio or "", "safra": safra})
|
|
209
280
|
|
|
210
281
|
cache_key = get_cache_key("dashboard", **cache_params)
|
|
211
282
|
|
|
@@ -301,6 +372,18 @@ def get_dashboard(
|
|
|
301
372
|
else:
|
|
302
373
|
resultado_base["clima_real"] = {"ativo": False}
|
|
303
374
|
|
|
375
|
+
# Enriquecer com ZARC se UF foi fornecida
|
|
376
|
+
if uf:
|
|
377
|
+
from core.zarc_adapter import enriquecer_plano_com_zarc
|
|
378
|
+
resultado_base = enriquecer_plano_com_zarc(
|
|
379
|
+
resultado_base,
|
|
380
|
+
uf=uf,
|
|
381
|
+
municipio=municipio,
|
|
382
|
+
safra=safra
|
|
383
|
+
)
|
|
384
|
+
else:
|
|
385
|
+
resultado_base["zarc"] = {"ativo": False}
|
|
386
|
+
|
|
304
387
|
return resultado_base
|
|
305
388
|
|
|
306
389
|
# Usa cache para dashboard com contexto climático
|
|
@@ -323,8 +406,12 @@ def get_talhoes():
|
|
|
323
406
|
raise HTTPException(status_code=500, detail=str(e))
|
|
324
407
|
|
|
325
408
|
@app.get("/recomendacoes")
|
|
326
|
-
def get_recomendacoes(
|
|
327
|
-
|
|
409
|
+
def get_recomendacoes(
|
|
410
|
+
uf: Optional[str] = None,
|
|
411
|
+
municipio: Optional[str] = None,
|
|
412
|
+
safra: str = Query("2025/2026", description="Safra ZARC")
|
|
413
|
+
):
|
|
414
|
+
"""Retorna recomendações de culturas por talhão com ZARC opcional"""
|
|
328
415
|
try:
|
|
329
416
|
culturas, talhoes, regras = get_dados()
|
|
330
417
|
|
|
@@ -347,9 +434,24 @@ def get_recomendacoes():
|
|
|
347
434
|
"agua": str(p['agua'])
|
|
348
435
|
})
|
|
349
436
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
437
|
+
resultado = {"recomendacoes": recomendacoes}
|
|
438
|
+
|
|
439
|
+
# Enriquecer com ZARC se UF foi fornecida
|
|
440
|
+
if uf:
|
|
441
|
+
from core.zarc_adapter import enriquecer_plano_com_zarc
|
|
442
|
+
resultado_temp = {"plano": recomendacoes}
|
|
443
|
+
resultado_temp = enriquecer_plano_com_zarc(
|
|
444
|
+
resultado_temp,
|
|
445
|
+
uf=uf,
|
|
446
|
+
municipio=municipio,
|
|
447
|
+
safra=safra
|
|
448
|
+
)
|
|
449
|
+
resultado["recomendacoes"] = resultado_temp["plano"]
|
|
450
|
+
resultado["zarc"] = resultado_temp.get("zarc", {"ativo": False})
|
|
451
|
+
else:
|
|
452
|
+
resultado["zarc"] = {"ativo": False}
|
|
453
|
+
|
|
454
|
+
return resultado
|
|
353
455
|
except Exception as e:
|
|
354
456
|
raise HTTPException(status_code=500, detail=str(e))
|
|
355
457
|
|
|
@@ -590,6 +692,18 @@ def otimizar(request: OtimizarRequest):
|
|
|
590
692
|
# Converte tipos numpy para Python nativos
|
|
591
693
|
resultado_convertido = converter_tipos_python(resultado)
|
|
592
694
|
|
|
695
|
+
# Enriquecer com ZARC se UF foi fornecida
|
|
696
|
+
if request.uf:
|
|
697
|
+
from core.zarc_adapter import enriquecer_plano_com_zarc
|
|
698
|
+
resultado_convertido = enriquecer_plano_com_zarc(
|
|
699
|
+
resultado_convertido,
|
|
700
|
+
uf=request.uf,
|
|
701
|
+
municipio=request.municipio,
|
|
702
|
+
safra=request.safra
|
|
703
|
+
)
|
|
704
|
+
else:
|
|
705
|
+
resultado_convertido["zarc"] = {"ativo": False}
|
|
706
|
+
|
|
593
707
|
return resultado_convertido
|
|
594
708
|
except HTTPException:
|
|
595
709
|
raise
|
|
@@ -677,12 +791,15 @@ def relatorio(request: RelatorioRequest):
|
|
|
677
791
|
from core.climate_adapter import obter_contexto_climatico_por_coordenadas
|
|
678
792
|
contexto_climatico = obter_contexto_climatico_por_coordenadas(request.lat, request.lon, request.days)
|
|
679
793
|
|
|
680
|
-
# Gera relatório com contexto climático integrado
|
|
794
|
+
# Gera relatório com contexto climático e ZARC integrado
|
|
681
795
|
caminho = gerar_relatorio_completo(
|
|
682
796
|
culturas, talhoes, regras,
|
|
683
797
|
objetivo=request.objetivo,
|
|
684
798
|
formato=request.formato,
|
|
685
|
-
contexto_climatico=contexto_climatico
|
|
799
|
+
contexto_climatico=contexto_climatico,
|
|
800
|
+
uf=request.uf,
|
|
801
|
+
municipio=request.municipio,
|
|
802
|
+
safra=request.safra
|
|
686
803
|
)
|
|
687
804
|
|
|
688
805
|
# Lê conteúdo
|
|
@@ -747,57 +864,3 @@ def limpar_cache(request: Request):
|
|
|
747
864
|
if __name__ == "__main__":
|
|
748
865
|
import uvicorn
|
|
749
866
|
uvicorn.run(app, host=HOST, port=PORT)
|
|
750
|
-
|
|
751
|
-
@app.get("/dados/zarc")
|
|
752
|
-
def get_zarc(
|
|
753
|
-
cultura: Optional[str] = Query(None, description="Nome da cultura"),
|
|
754
|
-
uf: Optional[str] = Query(None, description="Unidade Federativa (ex: SP, PR)"),
|
|
755
|
-
municipio: Optional[str] = Query(None, description="Nome do município"),
|
|
756
|
-
solo: Optional[str] = Query(None, description="Tipo de solo"),
|
|
757
|
-
safra: str = Query("2025/2026", description="Safra (ex: 2025/2026)")
|
|
758
|
-
):
|
|
759
|
-
"""Obtém dados ZARC (Zoneamento Agrícola de Risco Climático)"""
|
|
760
|
-
try:
|
|
761
|
-
# Importar provider ZARC
|
|
762
|
-
from providers.zarc_provider import buscar_zarc
|
|
763
|
-
|
|
764
|
-
# Se cultura não foi fornecida, retornar mensagem amigável
|
|
765
|
-
if not cultura:
|
|
766
|
-
return {
|
|
767
|
-
"message": "Informe a cultura para consultar dados ZARC.",
|
|
768
|
-
"exemplo_soja_sp": "/dados/zarc?cultura=soja&uf=SP&municipio=Sao%20Paulo&solo=argiloso",
|
|
769
|
-
"exemplo_milho_pr": "/dados/zarc?cultura=milho&uf=PR&municipio=Londrina&solo=argiloso",
|
|
770
|
-
"parametros": {
|
|
771
|
-
"cultura": "Nome da cultura (obrigatório)",
|
|
772
|
-
"uf": "Unidade Federativa (opcional)",
|
|
773
|
-
"municipio": "Nome do município (opcional)",
|
|
774
|
-
"solo": "Tipo de solo (opcional)",
|
|
775
|
-
"safra": "Safra, padrão 2025/2026"
|
|
776
|
-
},
|
|
777
|
-
"culturas_disponiveis": ["soja", "milho", "feijao", "cafe", "cana", "trigo", "algodao"],
|
|
778
|
-
"safras_disponiveis": ["2025/2026", "2026/2027"]
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
# Buscar dados ZARC
|
|
782
|
-
zarc_data = buscar_zarc(
|
|
783
|
-
cultura=cultura,
|
|
784
|
-
uf=uf,
|
|
785
|
-
municipio=municipio,
|
|
786
|
-
solo=solo,
|
|
787
|
-
safra=safra
|
|
788
|
-
)
|
|
789
|
-
|
|
790
|
-
if zarc_data:
|
|
791
|
-
return zarc_data
|
|
792
|
-
else:
|
|
793
|
-
return {
|
|
794
|
-
"message": "Dados ZARC não encontrados para os parâmetros fornecidos.",
|
|
795
|
-
"cultura": cultura,
|
|
796
|
-
"uf": uf,
|
|
797
|
-
"municipio": municipio,
|
|
798
|
-
"solo": solo,
|
|
799
|
-
"safra": safra,
|
|
800
|
-
"sugestao": "Tente com parâmetros mais genéricos (apenas cultura e UF)"
|
|
801
|
-
}
|
|
802
|
-
except Exception as e:
|
|
803
|
-
raise HTTPException(status_code=500, detail=str(e))
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Adaptador ZARC - Integra dados do ZARC no planejamento
|
|
3
|
+
"""
|
|
4
|
+
from typing import Dict, Any, Optional, List
|
|
5
|
+
from providers.zarc_provider import buscar_zarc
|
|
6
|
+
|
|
7
|
+
def enriquecer_plano_com_zarc(
|
|
8
|
+
resultado: Dict[str, Any],
|
|
9
|
+
uf: Optional[str] = None,
|
|
10
|
+
municipio: Optional[str] = None,
|
|
11
|
+
safra: str = "2025/2026"
|
|
12
|
+
) -> Dict[str, Any]:
|
|
13
|
+
"""
|
|
14
|
+
Enriquece resultado do planejamento com dados ZARC
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
resultado: Resultado do planejamento (AG ou cenário)
|
|
18
|
+
uf: Unidade Federativa
|
|
19
|
+
municipio: Nome do município
|
|
20
|
+
safra: Safra (padrão: 2025/2026)
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
Resultado enriquecido com dados ZARC
|
|
24
|
+
"""
|
|
25
|
+
# Se não tiver UF, não adiciona ZARC
|
|
26
|
+
if not uf:
|
|
27
|
+
resultado["zarc"] = {"ativo": False}
|
|
28
|
+
return resultado
|
|
29
|
+
|
|
30
|
+
# Cache local por requisição para evitar lookups repetidos
|
|
31
|
+
lookup_cache = {}
|
|
32
|
+
|
|
33
|
+
# Processar cada item do plano
|
|
34
|
+
culturas_com_zarc = 0
|
|
35
|
+
total_culturas = 0
|
|
36
|
+
sources = set()
|
|
37
|
+
tem_fallback = False
|
|
38
|
+
|
|
39
|
+
for item in resultado.get("plano", []):
|
|
40
|
+
total_culturas += 1
|
|
41
|
+
cultura = item.get("cultura")
|
|
42
|
+
solo = item.get("solo")
|
|
43
|
+
|
|
44
|
+
# Chave de cache: cultura|uf|municipio|solo|safra
|
|
45
|
+
cache_key = f"{cultura}|{uf}|{municipio}|{solo}|{safra}"
|
|
46
|
+
|
|
47
|
+
# Verificar cache local
|
|
48
|
+
if cache_key in lookup_cache:
|
|
49
|
+
zarc_data = lookup_cache[cache_key]
|
|
50
|
+
else:
|
|
51
|
+
# Buscar ZARC para esta cultura/solo
|
|
52
|
+
zarc_data = buscar_zarc(
|
|
53
|
+
cultura=cultura,
|
|
54
|
+
uf=uf,
|
|
55
|
+
municipio=municipio,
|
|
56
|
+
solo=solo,
|
|
57
|
+
safra=safra
|
|
58
|
+
)
|
|
59
|
+
# Cachear resultado
|
|
60
|
+
lookup_cache[cache_key] = zarc_data
|
|
61
|
+
|
|
62
|
+
if zarc_data and zarc_data.get("encontrado"):
|
|
63
|
+
# ZARC encontrado
|
|
64
|
+
item["zarc"] = {
|
|
65
|
+
"ativo": True,
|
|
66
|
+
"source": zarc_data.get("source"),
|
|
67
|
+
"fallback": zarc_data.get("fallback", False),
|
|
68
|
+
"janela_plantio": zarc_data.get("janela_plantio"),
|
|
69
|
+
"risco": zarc_data.get("risco"),
|
|
70
|
+
"safra": zarc_data.get("safra"),
|
|
71
|
+
"observacao": zarc_data.get("observacao"),
|
|
72
|
+
"decendios_recomendados": zarc_data.get("decendios_recomendados"),
|
|
73
|
+
"municipio_zarc": zarc_data.get("municipio"),
|
|
74
|
+
"geocodigo": zarc_data.get("geocodigo")
|
|
75
|
+
}
|
|
76
|
+
culturas_com_zarc += 1
|
|
77
|
+
sources.add(zarc_data.get("source"))
|
|
78
|
+
if zarc_data.get("fallback"):
|
|
79
|
+
tem_fallback = True
|
|
80
|
+
else:
|
|
81
|
+
# ZARC não encontrado
|
|
82
|
+
item["zarc"] = {
|
|
83
|
+
"ativo": False,
|
|
84
|
+
"message": zarc_data.get("message") if zarc_data else "ZARC não consultado"
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
# Determinar source geral
|
|
88
|
+
if len(sources) == 0:
|
|
89
|
+
source_geral = "unavailable"
|
|
90
|
+
elif len(sources) == 1:
|
|
91
|
+
source_geral = list(sources)[0]
|
|
92
|
+
else:
|
|
93
|
+
source_geral = "mixed"
|
|
94
|
+
|
|
95
|
+
# Adicionar resumo ZARC no resultado
|
|
96
|
+
resultado["zarc"] = {
|
|
97
|
+
"ativo": True,
|
|
98
|
+
"uf": uf,
|
|
99
|
+
"municipio": municipio,
|
|
100
|
+
"safra": safra,
|
|
101
|
+
"source": source_geral,
|
|
102
|
+
"fallback": tem_fallback,
|
|
103
|
+
"culturas_com_zarc": culturas_com_zarc,
|
|
104
|
+
"total_culturas": total_culturas
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return resultado
|
|
108
|
+
|
|
109
|
+
def aplicar_ajuste_zarc(
|
|
110
|
+
risco_base: float,
|
|
111
|
+
risco_zarc: str,
|
|
112
|
+
aplicar: bool = False
|
|
113
|
+
) -> Dict[str, Any]:
|
|
114
|
+
"""
|
|
115
|
+
Calcula ajuste de risco baseado no ZARC
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
risco_base: Risco base em pontos percentuais
|
|
119
|
+
risco_zarc: Risco ZARC (baixo, medio, alto, indeterminado)
|
|
120
|
+
aplicar: Se deve aplicar o ajuste (padrão: False)
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
Dicionário com risco ajustado e informações do ajuste
|
|
124
|
+
"""
|
|
125
|
+
# Mapeamento de ajustes ZARC (conservadores)
|
|
126
|
+
ajustes = {
|
|
127
|
+
"baixo": -2, # Reduz 2 pontos percentuais
|
|
128
|
+
"medio": +4, # Aumenta 4 pontos percentuais
|
|
129
|
+
"alto": +10, # Aumenta 10 pontos percentuais
|
|
130
|
+
"indeterminado": 0
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
ajuste = ajustes.get(risco_zarc, 0)
|
|
134
|
+
|
|
135
|
+
if aplicar and ajuste != 0:
|
|
136
|
+
risco_ajustado = min(95, max(5, risco_base + ajuste))
|
|
137
|
+
else:
|
|
138
|
+
risco_ajustado = risco_base
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
"risco_original": risco_base,
|
|
142
|
+
"risco_ajustado": risco_ajustado,
|
|
143
|
+
"ajuste_zarc": ajuste,
|
|
144
|
+
"ajuste_aplicado": aplicar,
|
|
145
|
+
"risco_zarc": risco_zarc
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
def gerar_secao_zarc_relatorio(
|
|
149
|
+
plano: List[Dict[str, Any]],
|
|
150
|
+
uf: Optional[str] = None,
|
|
151
|
+
municipio: Optional[str] = None,
|
|
152
|
+
safra: str = "2025/2026",
|
|
153
|
+
formato: str = "md"
|
|
154
|
+
) -> str:
|
|
155
|
+
"""
|
|
156
|
+
Gera seção ZARC para relatório
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
plano: Lista de itens do plano
|
|
160
|
+
uf: Unidade Federativa
|
|
161
|
+
municipio: Nome do município
|
|
162
|
+
safra: Safra
|
|
163
|
+
formato: Formato do relatório (md ou txt)
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
Texto da seção ZARC
|
|
167
|
+
"""
|
|
168
|
+
if not uf:
|
|
169
|
+
return ""
|
|
170
|
+
|
|
171
|
+
if formato == "md":
|
|
172
|
+
secao = "\n## 🌾 Zoneamento Agrícola de Risco Climático (ZARC)\n\n"
|
|
173
|
+
secao += f"**Região:** {municipio or 'Não especificado'}/{uf}\n"
|
|
174
|
+
secao += f"**Safra:** {safra}\n\n"
|
|
175
|
+
|
|
176
|
+
# Contar culturas com ZARC
|
|
177
|
+
com_zarc = sum(1 for item in plano if item.get("zarc", {}).get("ativo"))
|
|
178
|
+
total = len(plano)
|
|
179
|
+
|
|
180
|
+
secao += f"**Cobertura:** {com_zarc}/{total} culturas com recomendação ZARC\n\n"
|
|
181
|
+
|
|
182
|
+
# Tabela por talhão
|
|
183
|
+
secao += "| Talhão | Cultura | Solo | Janela de Plantio | Risco ZARC | Fonte |\n"
|
|
184
|
+
secao += "|--------|---------|------|-------------------|------------|-------|\n"
|
|
185
|
+
|
|
186
|
+
for item in plano:
|
|
187
|
+
talhao = item.get("talhao")
|
|
188
|
+
cultura = item.get("cultura")
|
|
189
|
+
solo = item.get("solo")
|
|
190
|
+
zarc = item.get("zarc", {})
|
|
191
|
+
|
|
192
|
+
if zarc.get("ativo"):
|
|
193
|
+
janela = zarc.get("janela_plantio", {})
|
|
194
|
+
inicio = janela.get("inicio", "N/A")
|
|
195
|
+
fim = janela.get("fim", "N/A")
|
|
196
|
+
risco = zarc.get("risco", "N/A")
|
|
197
|
+
source = zarc.get("source", "N/A")
|
|
198
|
+
|
|
199
|
+
# Emoji por fonte
|
|
200
|
+
if source == "zarc-oficial":
|
|
201
|
+
fonte_emoji = "✅ Oficial"
|
|
202
|
+
elif source == "zarc-cache":
|
|
203
|
+
fonte_emoji = "💾 Cache"
|
|
204
|
+
elif source == "zarc-fallback":
|
|
205
|
+
fonte_emoji = "⚠️ Fallback"
|
|
206
|
+
else:
|
|
207
|
+
fonte_emoji = source
|
|
208
|
+
|
|
209
|
+
secao += f"| {talhao} | {cultura} | {solo} | {inicio} a {fim} | {risco} | {fonte_emoji} |\n"
|
|
210
|
+
else:
|
|
211
|
+
message = zarc.get("message", "Não encontrado")
|
|
212
|
+
secao += f"| {talhao} | {cultura} | {solo} | - | - | ⚠️ {message} |\n"
|
|
213
|
+
|
|
214
|
+
secao += "\n"
|
|
215
|
+
|
|
216
|
+
# Observações
|
|
217
|
+
secao += "### Observações ZARC\n\n"
|
|
218
|
+
|
|
219
|
+
observacoes_unicas = set()
|
|
220
|
+
for item in plano:
|
|
221
|
+
zarc = item.get("zarc", {})
|
|
222
|
+
if zarc.get("ativo") and zarc.get("observacao"):
|
|
223
|
+
observacoes_unicas.add(zarc.get("observacao"))
|
|
224
|
+
|
|
225
|
+
if observacoes_unicas:
|
|
226
|
+
for obs in observacoes_unicas:
|
|
227
|
+
secao += f"- {obs}\n"
|
|
228
|
+
else:
|
|
229
|
+
secao += "- Nenhuma recomendação ZARC encontrada para os parâmetros informados.\n"
|
|
230
|
+
|
|
231
|
+
secao += "\n"
|
|
232
|
+
|
|
233
|
+
else: # txt
|
|
234
|
+
secao = "\n" + "="*80 + "\n"
|
|
235
|
+
secao += "ZONEAMENTO AGRÍCOLA DE RISCO CLIMÁTICO (ZARC)\n"
|
|
236
|
+
secao += "="*80 + "\n\n"
|
|
237
|
+
secao += f"Região: {municipio or 'Não especificado'}/{uf}\n"
|
|
238
|
+
secao += f"Safra: {safra}\n\n"
|
|
239
|
+
|
|
240
|
+
# Contar culturas com ZARC
|
|
241
|
+
com_zarc = sum(1 for item in plano if item.get("zarc", {}).get("ativo"))
|
|
242
|
+
total = len(plano)
|
|
243
|
+
|
|
244
|
+
secao += f"Cobertura: {com_zarc}/{total} culturas com recomendação ZARC\n\n"
|
|
245
|
+
|
|
246
|
+
# Lista por talhão
|
|
247
|
+
for item in plano:
|
|
248
|
+
talhao = item.get("talhao")
|
|
249
|
+
cultura = item.get("cultura")
|
|
250
|
+
solo = item.get("solo")
|
|
251
|
+
zarc = item.get("zarc", {})
|
|
252
|
+
|
|
253
|
+
secao += f"Talhão {talhao} - {cultura} ({solo})\n"
|
|
254
|
+
|
|
255
|
+
if zarc.get("ativo"):
|
|
256
|
+
janela = zarc.get("janela_plantio", {})
|
|
257
|
+
inicio = janela.get("inicio", "N/A")
|
|
258
|
+
fim = janela.get("fim", "N/A")
|
|
259
|
+
risco = zarc.get("risco", "N/A")
|
|
260
|
+
source = zarc.get("source", "N/A")
|
|
261
|
+
|
|
262
|
+
secao += f" Janela de Plantio: {inicio} a {fim}\n"
|
|
263
|
+
secao += f" Risco ZARC: {risco}\n"
|
|
264
|
+
secao += f" Fonte: {source}\n"
|
|
265
|
+
else:
|
|
266
|
+
message = zarc.get("message", "Não encontrado")
|
|
267
|
+
secao += f" Status: {message}\n"
|
|
268
|
+
|
|
269
|
+
secao += "\n"
|
|
270
|
+
|
|
271
|
+
# Observações
|
|
272
|
+
secao += "-"*80 + "\n"
|
|
273
|
+
secao += "OBSERVAÇÕES ZARC\n"
|
|
274
|
+
secao += "-"*80 + "\n\n"
|
|
275
|
+
|
|
276
|
+
observacoes_unicas = set()
|
|
277
|
+
for item in plano:
|
|
278
|
+
zarc = item.get("zarc", {})
|
|
279
|
+
if zarc.get("ativo") and zarc.get("observacao"):
|
|
280
|
+
observacoes_unicas.add(zarc.get("observacao"))
|
|
281
|
+
|
|
282
|
+
if observacoes_unicas:
|
|
283
|
+
for obs in observacoes_unicas:
|
|
284
|
+
secao += f"- {obs}\n"
|
|
285
|
+
else:
|
|
286
|
+
secao += "- Nenhuma recomendação ZARC encontrada para os parâmetros informados.\n"
|
|
287
|
+
|
|
288
|
+
secao += "\n"
|
|
289
|
+
|
|
290
|
+
return secao
|