agroplan-ai-cli 1.0.20 → 1.0.22
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.
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"cli_version": "1.0.22",
|
|
3
|
+
"backend_template_version": "1.0.22",
|
|
4
|
+
"zarc_index_version": "2025-2026-fast-index-v2",
|
|
5
|
+
"features": [
|
|
6
|
+
"zarc_fast_index",
|
|
7
|
+
"zarc_fallback_sorgo_mandioca",
|
|
8
|
+
"soil_normalization_misto_siltoso",
|
|
9
|
+
"climate_real_data",
|
|
10
|
+
"hybrid_mode"
|
|
11
|
+
],
|
|
12
|
+
"generated_at": "2026-05-09T18:30:00Z"
|
|
13
|
+
}
|
package/backend-template/api.py
CHANGED
|
@@ -151,7 +151,15 @@ def health():
|
|
|
151
151
|
from providers.zarc_provider import get_zarc_status
|
|
152
152
|
zarc_status = get_zarc_status()
|
|
153
153
|
|
|
154
|
-
|
|
154
|
+
# Carregar VERSION.json se existir
|
|
155
|
+
version_info = {}
|
|
156
|
+
version_path = os.path.join(os.path.dirname(__file__), "VERSION.json")
|
|
157
|
+
if os.path.exists(version_path):
|
|
158
|
+
import json
|
|
159
|
+
with open(version_path, 'r') as f:
|
|
160
|
+
version_info = json.load(f)
|
|
161
|
+
|
|
162
|
+
response = {
|
|
155
163
|
"status": "healthy",
|
|
156
164
|
"culturas": len(culturas),
|
|
157
165
|
"talhoes": len(talhoes),
|
|
@@ -164,9 +172,169 @@ def health():
|
|
|
164
172
|
},
|
|
165
173
|
"provider_cache": provider_cache_stats
|
|
166
174
|
}
|
|
175
|
+
|
|
176
|
+
# Adicionar info de versão se disponível
|
|
177
|
+
if version_info:
|
|
178
|
+
response["backend_template_version"] = version_info.get("backend_template_version")
|
|
179
|
+
response["zarc_index_version"] = version_info.get("zarc_index_version")
|
|
180
|
+
|
|
181
|
+
return response
|
|
167
182
|
except Exception as e:
|
|
168
183
|
raise HTTPException(status_code=500, detail=str(e))
|
|
169
184
|
|
|
185
|
+
@app.get("/debug/version")
|
|
186
|
+
def debug_version():
|
|
187
|
+
"""Retorna informações detalhadas de versão e configuração do backend"""
|
|
188
|
+
try:
|
|
189
|
+
import json
|
|
190
|
+
from providers.zarc_provider import (
|
|
191
|
+
ZARC_FAST_INDEX_ENABLED,
|
|
192
|
+
ZARC_ALLOW_FULL_SCAN,
|
|
193
|
+
load_zarc_index,
|
|
194
|
+
get_zarc_fallback
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
# Carregar VERSION.json
|
|
198
|
+
version_info = {}
|
|
199
|
+
version_path = os.path.join(os.path.dirname(__file__), "VERSION.json")
|
|
200
|
+
if os.path.exists(version_path):
|
|
201
|
+
with open(version_path, 'r') as f:
|
|
202
|
+
version_info = json.load(f)
|
|
203
|
+
|
|
204
|
+
# Verificar índice ZARC
|
|
205
|
+
zarc_index = load_zarc_index()
|
|
206
|
+
zarc_index_info = {}
|
|
207
|
+
zarc_index_keys_sample = []
|
|
208
|
+
|
|
209
|
+
if zarc_index:
|
|
210
|
+
records = zarc_index.get("records", {})
|
|
211
|
+
zarc_index_info = {
|
|
212
|
+
"exists": True,
|
|
213
|
+
"total_records": len(records),
|
|
214
|
+
"generated_at": zarc_index.get("generated_at"),
|
|
215
|
+
"source": zarc_index.get("source")
|
|
216
|
+
}
|
|
217
|
+
# Sample de 10 primeiras chaves
|
|
218
|
+
zarc_index_keys_sample = list(records.keys())[:10]
|
|
219
|
+
else:
|
|
220
|
+
zarc_index_info = {"exists": False}
|
|
221
|
+
|
|
222
|
+
# Verificar fallbacks
|
|
223
|
+
fallback_data = get_zarc_fallback()
|
|
224
|
+
culturas_fallback = set(item.get("cultura") for item in fallback_data)
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
"api_version": "5.0.0",
|
|
228
|
+
"backend_file": __file__,
|
|
229
|
+
"backend_template_version": version_info.get("backend_template_version", "unknown"),
|
|
230
|
+
"cli_version": version_info.get("cli_version", "unknown"),
|
|
231
|
+
"zarc_index_version": version_info.get("zarc_index_version", "unknown"),
|
|
232
|
+
"features": version_info.get("features", []),
|
|
233
|
+
"generated_at": version_info.get("generated_at"),
|
|
234
|
+
"zarc_config": {
|
|
235
|
+
"fast_index_enabled": ZARC_FAST_INDEX_ENABLED,
|
|
236
|
+
"allow_full_scan": ZARC_ALLOW_FULL_SCAN,
|
|
237
|
+
"index": zarc_index_info,
|
|
238
|
+
"index_keys_sample": zarc_index_keys_sample
|
|
239
|
+
},
|
|
240
|
+
"zarc_fallback": {
|
|
241
|
+
"total_records": len(fallback_data),
|
|
242
|
+
"culturas": sorted(list(culturas_fallback)),
|
|
243
|
+
"has_sorgo": "sorgo" in culturas_fallback,
|
|
244
|
+
"has_mandioca": "mandioca" in culturas_fallback
|
|
245
|
+
},
|
|
246
|
+
"data_mode": DATA_MODE,
|
|
247
|
+
"weather_provider": WEATHER_PROVIDER
|
|
248
|
+
}
|
|
249
|
+
except Exception as e:
|
|
250
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
251
|
+
|
|
252
|
+
@app.get("/debug/zarc-coverage")
|
|
253
|
+
def debug_zarc_coverage(
|
|
254
|
+
uf: Optional[str] = Query(None, description="UF"),
|
|
255
|
+
municipio: Optional[str] = Query(None, description="Município"),
|
|
256
|
+
safra: str = Query("2025/2026", description="Safra")
|
|
257
|
+
):
|
|
258
|
+
"""Retorna diagnóstico detalhado de cobertura ZARC"""
|
|
259
|
+
try:
|
|
260
|
+
import json
|
|
261
|
+
from core.zarc_adapter import enriquecer_plano_com_zarc
|
|
262
|
+
|
|
263
|
+
# Usar o mesmo plano do dashboard
|
|
264
|
+
resultado_ag = get_ag_cacheado(objetivo="equilibrado")
|
|
265
|
+
|
|
266
|
+
# Enriquecer com ZARC
|
|
267
|
+
resultado_enriquecido = enriquecer_plano_com_zarc(
|
|
268
|
+
resultado_ag,
|
|
269
|
+
uf=uf,
|
|
270
|
+
municipio=municipio,
|
|
271
|
+
safra=safra
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
# Analisar cobertura
|
|
275
|
+
detalhes = []
|
|
276
|
+
culturas_com_zarc = 0
|
|
277
|
+
culturas_fallback = 0
|
|
278
|
+
culturas_unavailable = 0
|
|
279
|
+
|
|
280
|
+
for item in resultado_enriquecido["plano"]:
|
|
281
|
+
zarc = item.get("zarc", {})
|
|
282
|
+
|
|
283
|
+
detalhes.append({
|
|
284
|
+
"talhao": item.get("talhao"),
|
|
285
|
+
"cultura": item.get("cultura"),
|
|
286
|
+
"solo_original": item.get("solo"),
|
|
287
|
+
"zarc_ativo": zarc.get("ativo", False),
|
|
288
|
+
"zarc_source": zarc.get("source"),
|
|
289
|
+
"zarc_fallback": zarc.get("fallback", False),
|
|
290
|
+
"zarc_message": zarc.get("message"),
|
|
291
|
+
"zarc_janela": zarc.get("janela_plantio")
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
if zarc.get("ativo"):
|
|
295
|
+
culturas_com_zarc += 1
|
|
296
|
+
if zarc.get("fallback"):
|
|
297
|
+
culturas_fallback += 1
|
|
298
|
+
else:
|
|
299
|
+
culturas_unavailable += 1
|
|
300
|
+
|
|
301
|
+
total_culturas = len(resultado_enriquecido["plano"])
|
|
302
|
+
coverage_percent = (culturas_com_zarc / total_culturas * 100) if total_culturas > 0 else 0
|
|
303
|
+
|
|
304
|
+
# Carregar VERSION.json
|
|
305
|
+
version_info = {}
|
|
306
|
+
version_path = os.path.join(os.path.dirname(__file__), "VERSION.json")
|
|
307
|
+
if os.path.exists(version_path):
|
|
308
|
+
with open(version_path, 'r') as f:
|
|
309
|
+
version_info = json.load(f)
|
|
310
|
+
|
|
311
|
+
# Verificar índice ZARC
|
|
312
|
+
from providers.zarc_provider import load_zarc_index
|
|
313
|
+
zarc_index = load_zarc_index()
|
|
314
|
+
zarc_index_total = len(zarc_index.get("records", {})) if zarc_index else 0
|
|
315
|
+
|
|
316
|
+
return {
|
|
317
|
+
"uf": uf,
|
|
318
|
+
"municipio": municipio,
|
|
319
|
+
"safra": safra,
|
|
320
|
+
"summary": {
|
|
321
|
+
"culturas_com_zarc": culturas_com_zarc,
|
|
322
|
+
"culturas_fallback": culturas_fallback,
|
|
323
|
+
"culturas_unavailable": culturas_unavailable,
|
|
324
|
+
"total_culturas": total_culturas,
|
|
325
|
+
"coverage_percent": round(coverage_percent, 1)
|
|
326
|
+
},
|
|
327
|
+
"backend_info": {
|
|
328
|
+
"backend_template_version": version_info.get("backend_template_version", "unknown"),
|
|
329
|
+
"zarc_index_version": version_info.get("zarc_index_version", "unknown"),
|
|
330
|
+
"zarc_index_total_records": zarc_index_total
|
|
331
|
+
},
|
|
332
|
+
"details": detalhes
|
|
333
|
+
}
|
|
334
|
+
except Exception as e:
|
|
335
|
+
import traceback
|
|
336
|
+
raise HTTPException(status_code=500, detail=f"{str(e)}\n{traceback.format_exc()}")
|
|
337
|
+
|
|
170
338
|
@app.get("/dados/clima")
|
|
171
339
|
def get_clima(
|
|
172
340
|
lat: Optional[float] = Query(None, description="Latitude"),
|
|
@@ -850,14 +1018,37 @@ def limpar_cache(request: Request):
|
|
|
850
1018
|
detail="Token de administração inválido ou ausente. Use header X-Cache-Token."
|
|
851
1019
|
)
|
|
852
1020
|
|
|
853
|
-
# Limpa cache
|
|
1021
|
+
# Limpa cache de resultados
|
|
854
1022
|
global _resultados_cache
|
|
855
1023
|
items_removidos = len(_resultados_cache)
|
|
856
1024
|
_resultados_cache.clear()
|
|
857
1025
|
|
|
1026
|
+
# Limpa cache de provedores (weather, etc)
|
|
1027
|
+
provider_items_removidos = 0
|
|
1028
|
+
try:
|
|
1029
|
+
from providers.cache import clear_provider_cache
|
|
1030
|
+
provider_items_removidos = clear_provider_cache()
|
|
1031
|
+
except Exception:
|
|
1032
|
+
pass
|
|
1033
|
+
|
|
1034
|
+
# Limpa cache do índice ZARC em memória
|
|
1035
|
+
zarc_cache_cleared = False
|
|
1036
|
+
try:
|
|
1037
|
+
from providers import zarc_provider
|
|
1038
|
+
if hasattr(zarc_provider, '_zarc_index_cache'):
|
|
1039
|
+
zarc_provider._zarc_index_cache.clear()
|
|
1040
|
+
zarc_cache_cleared = True
|
|
1041
|
+
except Exception:
|
|
1042
|
+
pass
|
|
1043
|
+
|
|
858
1044
|
return {
|
|
859
1045
|
"status": "ok",
|
|
860
|
-
"message": f"Cache limpo
|
|
1046
|
+
"message": f"Cache limpo completamente.",
|
|
1047
|
+
"details": {
|
|
1048
|
+
"resultados_cache": items_removidos,
|
|
1049
|
+
"provider_cache": provider_items_removidos,
|
|
1050
|
+
"zarc_index_cache": "cleared" if zarc_cache_cleared else "not_found"
|
|
1051
|
+
},
|
|
861
1052
|
"protected": bool(cache_admin_token)
|
|
862
1053
|
}
|
|
863
1054
|
|
|
@@ -78,10 +78,10 @@ def enriquecer_plano_com_zarc(
|
|
|
78
78
|
if zarc_data.get("fallback"):
|
|
79
79
|
tem_fallback = True
|
|
80
80
|
else:
|
|
81
|
-
# ZARC não encontrado
|
|
81
|
+
# ZARC não encontrado - usar mensagem honesta
|
|
82
82
|
item["zarc"] = {
|
|
83
83
|
"ativo": False,
|
|
84
|
-
"message": zarc_data.get("message"
|
|
84
|
+
"message": zarc_data.get("message", "ZARC consultado, mas sem recomendação disponível para esta cultura/região.")
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
# Determinar source geral
|
|
@@ -304,6 +304,36 @@ def normalizar_solo(solo: str) -> str:
|
|
|
304
304
|
"""Normaliza tipo de solo"""
|
|
305
305
|
return normalizar_texto(solo)
|
|
306
306
|
|
|
307
|
+
def normalizar_solo_zarc(solo: str) -> str:
|
|
308
|
+
"""
|
|
309
|
+
Normaliza tipo de solo para busca ZARC
|
|
310
|
+
|
|
311
|
+
Mapeia variações de solo para os tipos reconhecidos pelo ZARC:
|
|
312
|
+
- misto -> medio
|
|
313
|
+
- siltoso -> medio
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
solo: Tipo de solo original
|
|
317
|
+
|
|
318
|
+
Returns:
|
|
319
|
+
Tipo de solo normalizado para ZARC
|
|
320
|
+
"""
|
|
321
|
+
if not solo:
|
|
322
|
+
return ""
|
|
323
|
+
|
|
324
|
+
solo_norm = normalizar_solo(solo)
|
|
325
|
+
|
|
326
|
+
# Mapeamento de solos para ZARC
|
|
327
|
+
mapa_zarc = {
|
|
328
|
+
"arenoso": "arenoso",
|
|
329
|
+
"medio": "medio",
|
|
330
|
+
"misto": "medio", # misto -> medio
|
|
331
|
+
"siltoso": "medio", # siltoso -> medio
|
|
332
|
+
"argiloso": "argiloso"
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return mapa_zarc.get(solo_norm, solo_norm)
|
|
336
|
+
|
|
307
337
|
def get_cache_path(safra: str) -> str:
|
|
308
338
|
"""Retorna caminho do arquivo de cache para a safra"""
|
|
309
339
|
os.makedirs(ZARC_CACHE_DIR, exist_ok=True)
|
|
@@ -427,15 +457,15 @@ def buscar_zarc_indexado(
|
|
|
427
457
|
# Tentar diferentes combinações de solo
|
|
428
458
|
solos_tentar = []
|
|
429
459
|
if solo:
|
|
430
|
-
solo_norm =
|
|
460
|
+
solo_norm = normalizar_solo_zarc(solo) # Usa normalização ZARC (misto->medio, siltoso->medio)
|
|
431
461
|
# Tentar o solo especificado primeiro, depois outros como fallback
|
|
432
|
-
solos_tentar = [solo_norm, "medio", "arenoso", "argiloso"
|
|
462
|
+
solos_tentar = [solo_norm, "medio", "arenoso", "argiloso"]
|
|
433
463
|
# Remover duplicatas mantendo ordem
|
|
434
464
|
seen = set()
|
|
435
465
|
solos_tentar = [s for s in solos_tentar if not (s in seen or seen.add(s))]
|
|
436
466
|
else:
|
|
437
467
|
# Se não especificou solo, tentar todos (preferir medio/argiloso)
|
|
438
|
-
solos_tentar = ["medio", "argiloso", "arenoso"
|
|
468
|
+
solos_tentar = ["medio", "argiloso", "arenoso"]
|
|
439
469
|
|
|
440
470
|
# Buscar no índice
|
|
441
471
|
for solo_test in solos_tentar:
|
|
@@ -680,6 +710,48 @@ def get_zarc_fallback() -> List[Dict[str, Any]]:
|
|
|
680
710
|
"janela_fim": "31/03",
|
|
681
711
|
"risco": "baixo",
|
|
682
712
|
"safra": "2025/2026"
|
|
713
|
+
},
|
|
714
|
+
# Sorgo
|
|
715
|
+
{
|
|
716
|
+
"cultura": "sorgo",
|
|
717
|
+
"uf": "SP",
|
|
718
|
+
"municipio": "ribeirao preto",
|
|
719
|
+
"solo": "medio",
|
|
720
|
+
"janela_inicio": "15/10",
|
|
721
|
+
"janela_fim": "15/12",
|
|
722
|
+
"risco": "medio",
|
|
723
|
+
"safra": "2025/2026"
|
|
724
|
+
},
|
|
725
|
+
{
|
|
726
|
+
"cultura": "sorgo",
|
|
727
|
+
"uf": "MG",
|
|
728
|
+
"municipio": "uberlandia",
|
|
729
|
+
"solo": "medio",
|
|
730
|
+
"janela_inicio": "01/10",
|
|
731
|
+
"janela_fim": "30/11",
|
|
732
|
+
"risco": "medio",
|
|
733
|
+
"safra": "2025/2026"
|
|
734
|
+
},
|
|
735
|
+
# Mandioca
|
|
736
|
+
{
|
|
737
|
+
"cultura": "mandioca",
|
|
738
|
+
"uf": "SP",
|
|
739
|
+
"municipio": "sao paulo",
|
|
740
|
+
"solo": "medio",
|
|
741
|
+
"janela_inicio": "01/09",
|
|
742
|
+
"janela_fim": "31/03",
|
|
743
|
+
"risco": "baixo",
|
|
744
|
+
"safra": "2025/2026"
|
|
745
|
+
},
|
|
746
|
+
{
|
|
747
|
+
"cultura": "mandioca",
|
|
748
|
+
"uf": "PR",
|
|
749
|
+
"municipio": "londrina",
|
|
750
|
+
"solo": "medio",
|
|
751
|
+
"janela_inicio": "15/08",
|
|
752
|
+
"janela_fim": "31/03",
|
|
753
|
+
"risco": "baixo",
|
|
754
|
+
"safra": "2025/2026"
|
|
683
755
|
}
|
|
684
756
|
]
|
|
685
757
|
|
|
@@ -689,12 +761,14 @@ def buscar_zarc(
|
|
|
689
761
|
municipio: Optional[str] = None,
|
|
690
762
|
solo: Optional[str] = None,
|
|
691
763
|
safra: str = ZARC_SAFRA_DEFAULT
|
|
692
|
-
) ->
|
|
764
|
+
) -> Dict[str, Any]:
|
|
693
765
|
"""
|
|
694
766
|
Busca dados ZARC para cultura/região específica
|
|
695
767
|
|
|
696
768
|
PERFORMANCE: Tenta índice primeiro (rápido), depois streaming (lento)
|
|
697
769
|
|
|
770
|
+
SEMPRE retorna um dicionário, nunca None
|
|
771
|
+
|
|
698
772
|
Args:
|
|
699
773
|
cultura: Nome da cultura
|
|
700
774
|
uf: Unidade Federativa (opcional)
|
|
@@ -703,7 +777,7 @@ def buscar_zarc(
|
|
|
703
777
|
safra: Safra (padrão: 2025/2026)
|
|
704
778
|
|
|
705
779
|
Returns:
|
|
706
|
-
Dicionário com dados ZARC
|
|
780
|
+
Dicionário com dados ZARC (sempre retorna, nunca None)
|
|
707
781
|
"""
|
|
708
782
|
# FAST PATH: Tentar índice primeiro (O(1) lookup)
|
|
709
783
|
if ZARC_FAST_INDEX_ENABLED:
|
|
@@ -718,7 +792,14 @@ def buscar_zarc(
|
|
|
718
792
|
return buscar_zarc_fallback(cultura, uf, municipio, solo, safra)
|
|
719
793
|
|
|
720
794
|
# Full scan permitido (desenvolvimento local)
|
|
721
|
-
|
|
795
|
+
resultado_streaming = buscar_zarc_streaming(cultura, uf, municipio, solo, safra)
|
|
796
|
+
|
|
797
|
+
# buscar_zarc_streaming pode retornar None se arquivo não disponível
|
|
798
|
+
# Nesse caso, usar fallback
|
|
799
|
+
if resultado_streaming is None:
|
|
800
|
+
return buscar_zarc_fallback(cultura, uf, municipio, solo, safra)
|
|
801
|
+
|
|
802
|
+
return resultado_streaming
|
|
722
803
|
|
|
723
804
|
def buscar_zarc_streaming(
|
|
724
805
|
cultura: str,
|
|
@@ -737,7 +818,7 @@ def buscar_zarc_streaming(
|
|
|
737
818
|
cultura_norm = normalizar_cultura(cultura)
|
|
738
819
|
uf_norm = normalizar_uf(uf) if uf else None
|
|
739
820
|
municipio_norm = normalizar_municipio(municipio) if municipio else None
|
|
740
|
-
solo_norm =
|
|
821
|
+
solo_norm = normalizar_solo_zarc(solo) if solo else None # Usa normalização ZARC
|
|
741
822
|
|
|
742
823
|
# Tentar obter arquivo ZARC
|
|
743
824
|
file_info = ensure_zarc_file(safra)
|
|
@@ -768,7 +849,7 @@ def buscar_zarc_streaming(
|
|
|
768
849
|
# Solo (se fornecido)
|
|
769
850
|
if solo_norm:
|
|
770
851
|
solo_registro = mapear_codigo_solo(registro.get("Cod_Solo", ""))
|
|
771
|
-
if
|
|
852
|
+
if normalizar_solo_zarc(solo_registro) == solo_norm: # Usa normalização ZARC
|
|
772
853
|
score += 2
|
|
773
854
|
|
|
774
855
|
# Manter apenas o melhor match (não acumula lista)
|
|
@@ -837,15 +918,17 @@ def buscar_zarc_fallback(
|
|
|
837
918
|
municipio: Optional[str] = None,
|
|
838
919
|
solo: Optional[str] = None,
|
|
839
920
|
safra: str = ZARC_SAFRA_DEFAULT
|
|
840
|
-
) ->
|
|
921
|
+
) -> Dict[str, Any]:
|
|
841
922
|
"""
|
|
842
923
|
Busca ZARC em dados simplificados (fallback)
|
|
924
|
+
|
|
925
|
+
SEMPRE retorna um dicionário, nunca None
|
|
843
926
|
"""
|
|
844
927
|
# Normalizar parâmetros
|
|
845
928
|
cultura_norm = normalizar_cultura(cultura)
|
|
846
929
|
uf_norm = normalizar_uf(uf) if uf else None
|
|
847
930
|
municipio_norm = normalizar_municipio(municipio) if municipio else None
|
|
848
|
-
solo_norm =
|
|
931
|
+
solo_norm = normalizar_solo_zarc(solo) if solo else None # Usa normalização ZARC
|
|
849
932
|
|
|
850
933
|
# Fallback: usar dados simplificados (lista pequena em memória)
|
|
851
934
|
fallback_data = get_zarc_fallback()
|
|
@@ -869,7 +952,7 @@ def buscar_zarc_fallback(
|
|
|
869
952
|
score += 3
|
|
870
953
|
|
|
871
954
|
# Solo (se fornecido)
|
|
872
|
-
if solo_norm and
|
|
955
|
+
if solo_norm and normalizar_solo_zarc(registro.get("solo", "")) == solo_norm: # Usa normalização ZARC
|
|
873
956
|
score += 2
|
|
874
957
|
|
|
875
958
|
if score > melhor_score:
|
|
@@ -894,4 +977,11 @@ def buscar_zarc_fallback(
|
|
|
894
977
|
"observacao": "Dados simplificados locais usados porque o CSV oficial não estava disponível."
|
|
895
978
|
}
|
|
896
979
|
|
|
897
|
-
|
|
980
|
+
# Nenhum match encontrado - retornar estado "unavailable" em vez de None
|
|
981
|
+
return {
|
|
982
|
+
"encontrado": False,
|
|
983
|
+
"source": "zarc-unavailable",
|
|
984
|
+
"fallback": False,
|
|
985
|
+
"message": "ZARC consultado, mas nenhuma recomendação foi encontrada para esta cultura, solo e região.",
|
|
986
|
+
"observacao": "A cultura pode não estar disponível no índice ZARC compacto para a região selecionada."
|
|
987
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -41,6 +41,13 @@ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
|
41
41
|
var __require = import.meta.require;
|
|
42
42
|
|
|
43
43
|
// src/utils/paths.ts
|
|
44
|
+
var exports_paths = {};
|
|
45
|
+
__export(exports_paths, {
|
|
46
|
+
getProjectPaths: () => getProjectPaths,
|
|
47
|
+
getHomeAgroplanDir: () => getHomeAgroplanDir,
|
|
48
|
+
findProjectRoot: () => findProjectRoot,
|
|
49
|
+
ensureAgroplanDir: () => ensureAgroplanDir
|
|
50
|
+
});
|
|
44
51
|
import { join, dirname } from "path";
|
|
45
52
|
import { existsSync, mkdirSync } from "fs";
|
|
46
53
|
import { homedir } from "os";
|
|
@@ -113,6 +120,77 @@ function ensureAgroplanDir() {
|
|
|
113
120
|
}
|
|
114
121
|
var init_paths = () => {};
|
|
115
122
|
|
|
123
|
+
// src/utils/process.ts
|
|
124
|
+
var exports_process = {};
|
|
125
|
+
__export(exports_process, {
|
|
126
|
+
savePid: () => savePid,
|
|
127
|
+
removePidFile: () => removePidFile,
|
|
128
|
+
readPid: () => readPid,
|
|
129
|
+
killProcess: () => killProcess,
|
|
130
|
+
isProcessRunning: () => isProcessRunning,
|
|
131
|
+
checkPort: () => checkPort
|
|
132
|
+
});
|
|
133
|
+
import { existsSync as existsSync2, readFileSync, writeFileSync, unlinkSync } from "fs";
|
|
134
|
+
function savePid(pid) {
|
|
135
|
+
const paths = getProjectPaths();
|
|
136
|
+
writeFileSync(paths.pidFile, pid.toString());
|
|
137
|
+
}
|
|
138
|
+
function readPid() {
|
|
139
|
+
const paths = getProjectPaths();
|
|
140
|
+
if (!existsSync2(paths.pidFile)) {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
try {
|
|
144
|
+
const pidStr = readFileSync(paths.pidFile, "utf-8").trim();
|
|
145
|
+
return parseInt(pidStr, 10);
|
|
146
|
+
} catch {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function removePidFile() {
|
|
151
|
+
const paths = getProjectPaths();
|
|
152
|
+
if (existsSync2(paths.pidFile)) {
|
|
153
|
+
unlinkSync(paths.pidFile);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function isProcessRunning(pid) {
|
|
157
|
+
try {
|
|
158
|
+
process.kill(pid, 0);
|
|
159
|
+
return true;
|
|
160
|
+
} catch {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function killProcess(pid) {
|
|
165
|
+
try {
|
|
166
|
+
process.kill(pid, "SIGTERM");
|
|
167
|
+
setTimeout(() => {
|
|
168
|
+
if (isProcessRunning(pid)) {
|
|
169
|
+
try {
|
|
170
|
+
process.kill(pid, "SIGKILL");
|
|
171
|
+
} catch {}
|
|
172
|
+
}
|
|
173
|
+
}, 2000);
|
|
174
|
+
return true;
|
|
175
|
+
} catch {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function checkPort(port) {
|
|
180
|
+
return new Promise((resolve) => {
|
|
181
|
+
try {
|
|
182
|
+
fetch(`http://localhost:${port}/health`, {
|
|
183
|
+
signal: AbortSignal.timeout(1000)
|
|
184
|
+
}).then(() => resolve(true)).catch(() => resolve(false));
|
|
185
|
+
} catch {
|
|
186
|
+
resolve(false);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
var init_process = __esm(() => {
|
|
191
|
+
init_paths();
|
|
192
|
+
});
|
|
193
|
+
|
|
116
194
|
// src/utils/setup-state.ts
|
|
117
195
|
var exports_setup_state = {};
|
|
118
196
|
__export(exports_setup_state, {
|
|
@@ -213,68 +291,7 @@ function checkPip() {
|
|
|
213
291
|
|
|
214
292
|
// src/commands/doctor.ts
|
|
215
293
|
init_paths();
|
|
216
|
-
|
|
217
|
-
// src/utils/process.ts
|
|
218
|
-
init_paths();
|
|
219
|
-
import { existsSync as existsSync2, readFileSync, writeFileSync, unlinkSync } from "fs";
|
|
220
|
-
function savePid(pid) {
|
|
221
|
-
const paths = getProjectPaths();
|
|
222
|
-
writeFileSync(paths.pidFile, pid.toString());
|
|
223
|
-
}
|
|
224
|
-
function readPid() {
|
|
225
|
-
const paths = getProjectPaths();
|
|
226
|
-
if (!existsSync2(paths.pidFile)) {
|
|
227
|
-
return null;
|
|
228
|
-
}
|
|
229
|
-
try {
|
|
230
|
-
const pidStr = readFileSync(paths.pidFile, "utf-8").trim();
|
|
231
|
-
return parseInt(pidStr, 10);
|
|
232
|
-
} catch {
|
|
233
|
-
return null;
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
function removePidFile() {
|
|
237
|
-
const paths = getProjectPaths();
|
|
238
|
-
if (existsSync2(paths.pidFile)) {
|
|
239
|
-
unlinkSync(paths.pidFile);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
function isProcessRunning(pid) {
|
|
243
|
-
try {
|
|
244
|
-
process.kill(pid, 0);
|
|
245
|
-
return true;
|
|
246
|
-
} catch {
|
|
247
|
-
return false;
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
function killProcess(pid) {
|
|
251
|
-
try {
|
|
252
|
-
process.kill(pid, "SIGTERM");
|
|
253
|
-
setTimeout(() => {
|
|
254
|
-
if (isProcessRunning(pid)) {
|
|
255
|
-
try {
|
|
256
|
-
process.kill(pid, "SIGKILL");
|
|
257
|
-
} catch {}
|
|
258
|
-
}
|
|
259
|
-
}, 2000);
|
|
260
|
-
return true;
|
|
261
|
-
} catch {
|
|
262
|
-
return false;
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
function checkPort(port) {
|
|
266
|
-
return new Promise((resolve) => {
|
|
267
|
-
try {
|
|
268
|
-
fetch(`http://localhost:${port}/health`, {
|
|
269
|
-
signal: AbortSignal.timeout(1000)
|
|
270
|
-
}).then(() => resolve(true)).catch(() => resolve(false));
|
|
271
|
-
} catch {
|
|
272
|
-
resolve(false);
|
|
273
|
-
}
|
|
274
|
-
});
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// src/commands/doctor.ts
|
|
294
|
+
init_process();
|
|
278
295
|
init_setup_state();
|
|
279
296
|
async function doctorCommand() {
|
|
280
297
|
console.log(`\uD83D\uDD0D AgroPlan AI - Diagn\xF3stico do Sistema
|
|
@@ -305,20 +322,37 @@ async function doctorCommand() {
|
|
|
305
322
|
\uD83D\uDEE0\uFE0F Setup Local:`);
|
|
306
323
|
const setupComplete = isSetupComplete();
|
|
307
324
|
const setupState = readSetupState();
|
|
308
|
-
let currentVersion = "1.0.
|
|
325
|
+
let currentVersion = "1.0.21";
|
|
309
326
|
try {
|
|
310
327
|
const packagePath = __require.resolve("agroplan-ai-cli/package.json");
|
|
311
328
|
const packageJson = __require(packagePath);
|
|
312
|
-
currentVersion = packageJson.version || "1.0.
|
|
329
|
+
currentVersion = packageJson.version || "1.0.21";
|
|
313
330
|
} catch {
|
|
314
|
-
currentVersion = "1.0.
|
|
331
|
+
currentVersion = "1.0.21";
|
|
315
332
|
}
|
|
316
333
|
if (setupComplete && setupState) {
|
|
317
334
|
const installedVersion = setupState.version || "unknown";
|
|
318
335
|
console.log(" \u2705 Setup conclu\xEDdo");
|
|
319
336
|
console.log(` \uD83D\uDCC5 Instalado em: ${new Date(setupState.installedAt).toLocaleString()}`);
|
|
320
|
-
console.log(` \uD83D\uDCE6 Vers\xE3o: ${installedVersion}`);
|
|
337
|
+
console.log(` \uD83D\uDCE6 Vers\xE3o CLI: ${installedVersion}`);
|
|
321
338
|
console.log(` \uD83D\uDC0D Python: ${setupState.python}`);
|
|
339
|
+
try {
|
|
340
|
+
const { getHomeAgroplanDir: getHomeAgroplanDir2 } = await Promise.resolve().then(() => (init_paths(), exports_paths));
|
|
341
|
+
const homeDir = getHomeAgroplanDir2();
|
|
342
|
+
const versionPath = `${homeDir}/backend/VERSION.json`;
|
|
343
|
+
const { existsSync: existsSync5, readFileSync: readFileSync3 } = await import("fs");
|
|
344
|
+
if (existsSync5(versionPath)) {
|
|
345
|
+
const versionContent = readFileSync3(versionPath, "utf-8");
|
|
346
|
+
const versionInfo = JSON.parse(versionContent);
|
|
347
|
+
console.log(` \uD83D\uDCE6 Backend template: ${versionInfo.backend_template_version || "unknown"}`);
|
|
348
|
+
console.log(` \uD83D\uDDC2\uFE0F ZARC index: ${versionInfo.zarc_index_version || "unknown"}`);
|
|
349
|
+
if (versionInfo.features && versionInfo.features.length > 0) {
|
|
350
|
+
console.log(` \u2728 Features: ${versionInfo.features.slice(0, 3).join(", ")}${versionInfo.features.length > 3 ? "..." : ""}`);
|
|
351
|
+
}
|
|
352
|
+
} else {
|
|
353
|
+
console.log(" \u26A0\uFE0F VERSION.json n\xE3o encontrado no backend local");
|
|
354
|
+
}
|
|
355
|
+
} catch (error) {}
|
|
322
356
|
if (installedVersion !== currentVersion) {
|
|
323
357
|
console.log(`
|
|
324
358
|
\u26A0\uFE0F API local desatualizada!`);
|
|
@@ -452,6 +486,7 @@ Correja os problemas acima:`);
|
|
|
452
486
|
|
|
453
487
|
// src/commands/serve.ts
|
|
454
488
|
init_paths();
|
|
489
|
+
init_process();
|
|
455
490
|
import { existsSync as existsSync5, readFileSync as readFileSync3, openSync, closeSync } from "fs";
|
|
456
491
|
import { spawn } from "child_process";
|
|
457
492
|
async function serveOnCommand() {
|
|
@@ -471,13 +506,13 @@ async function serveOnCommand() {
|
|
|
471
506
|
const { readSetupState: readSetupState2 } = await Promise.resolve().then(() => (init_setup_state(), exports_setup_state));
|
|
472
507
|
const setupState = readSetupState2();
|
|
473
508
|
if (setupState) {
|
|
474
|
-
let currentVersion = "1.0.
|
|
509
|
+
let currentVersion = "1.0.21";
|
|
475
510
|
try {
|
|
476
511
|
const packagePath = __require.resolve("agroplan-ai-cli/package.json");
|
|
477
512
|
const packageJson = __require(packagePath);
|
|
478
|
-
currentVersion = packageJson.version || "1.0.
|
|
513
|
+
currentVersion = packageJson.version || "1.0.21";
|
|
479
514
|
} catch {
|
|
480
|
-
currentVersion = "1.0.
|
|
515
|
+
currentVersion = "1.0.21";
|
|
481
516
|
}
|
|
482
517
|
const installedVersion = setupState.version || "unknown";
|
|
483
518
|
if (installedVersion !== currentVersion) {
|
|
@@ -738,13 +773,13 @@ async function setupCommand(force = false, pythonPath) {
|
|
|
738
773
|
const homeDir = getHomeAgroplanDir();
|
|
739
774
|
const backendDir = join3(homeDir, "backend");
|
|
740
775
|
console.log(`\uD83D\uDCC1 Diret\xF3rio de instala\xE7\xE3o: ${homeDir}`);
|
|
741
|
-
let currentVersion = "1.0.
|
|
776
|
+
let currentVersion = "1.0.21";
|
|
742
777
|
try {
|
|
743
778
|
const packagePath = __require.resolve("agroplan-ai-cli/package.json");
|
|
744
779
|
const packageJson = __require(packagePath);
|
|
745
|
-
currentVersion = packageJson.version || "1.0.
|
|
780
|
+
currentVersion = packageJson.version || "1.0.21";
|
|
746
781
|
} catch {
|
|
747
|
-
currentVersion = "1.0.
|
|
782
|
+
currentVersion = "1.0.21";
|
|
748
783
|
}
|
|
749
784
|
if (!force && isSetupComplete()) {
|
|
750
785
|
const setupState = (init_setup_state(), __toCommonJS(exports_setup_state)).readSetupState();
|
|
@@ -885,13 +920,13 @@ async function setupCommand(force = false, pythonPath) {
|
|
|
885
920
|
return;
|
|
886
921
|
}
|
|
887
922
|
console.log(" \u2705 Servidor web configurado");
|
|
888
|
-
let version = "1.0.
|
|
923
|
+
let version = "1.0.21";
|
|
889
924
|
try {
|
|
890
925
|
const packagePath = __require.resolve("agroplan-ai-cli/package.json");
|
|
891
926
|
const packageJson = __require(packagePath);
|
|
892
|
-
version = packageJson.version || "1.0.
|
|
927
|
+
version = packageJson.version || "1.0.21";
|
|
893
928
|
} catch {
|
|
894
|
-
version = "1.0.
|
|
929
|
+
version = "1.0.21";
|
|
895
930
|
}
|
|
896
931
|
saveSetupState({
|
|
897
932
|
version,
|
|
@@ -914,6 +949,7 @@ async function setupCommand(force = false, pythonPath) {
|
|
|
914
949
|
|
|
915
950
|
// src/commands/update.ts
|
|
916
951
|
init_setup_state();
|
|
952
|
+
init_process();
|
|
917
953
|
async function updateCommand() {
|
|
918
954
|
console.log(`\uD83D\uDD04 Atualizando API local do AgroPlan AI...
|
|
919
955
|
`);
|
|
@@ -932,13 +968,13 @@ async function updateCommand() {
|
|
|
932
968
|
console.log(" agroplan setup --force");
|
|
933
969
|
return;
|
|
934
970
|
}
|
|
935
|
-
let currentVersion = "1.0.
|
|
971
|
+
let currentVersion = "1.0.21";
|
|
936
972
|
try {
|
|
937
973
|
const packagePath = __require.resolve("agroplan-ai-cli/package.json");
|
|
938
974
|
const packageJson = __require(packagePath);
|
|
939
|
-
currentVersion = packageJson.version || "1.0.
|
|
975
|
+
currentVersion = packageJson.version || "1.0.21";
|
|
940
976
|
} catch {
|
|
941
|
-
currentVersion = "1.0.
|
|
977
|
+
currentVersion = "1.0.21";
|
|
942
978
|
}
|
|
943
979
|
const installedVersion = setupState.version || "unknown";
|
|
944
980
|
console.log(`\uD83D\uDCE6 Vers\xE3o instalada: ${installedVersion}`);
|
|
@@ -962,12 +998,62 @@ async function updateCommand() {
|
|
|
962
998
|
} else {
|
|
963
999
|
console.log("1\uFE0F\u20E3 API local n\xE3o est\xE1 rodando");
|
|
964
1000
|
}
|
|
965
|
-
|
|
1001
|
+
const { checkPort: checkPort2 } = await Promise.resolve().then(() => (init_process(), exports_process));
|
|
1002
|
+
const portInUse = await checkPort2(8000);
|
|
1003
|
+
if (portInUse) {
|
|
1004
|
+
console.log(`
|
|
1005
|
+
\u26A0\uFE0F Porta 8000 ainda est\xE1 ocupada por outro processo!`);
|
|
1006
|
+
console.log(`
|
|
1007
|
+
\uD83D\uDCA1 Para identificar e encerrar o processo:`);
|
|
1008
|
+
console.log(" netstat -ano | findstr :8000");
|
|
1009
|
+
console.log(" taskkill /PID <PID> /F");
|
|
1010
|
+
console.log(`
|
|
1011
|
+
Depois rode novamente: agroplan update`);
|
|
1012
|
+
return;
|
|
1013
|
+
}
|
|
1014
|
+
console.log("2\uFE0F\u20E3 Removendo backend antigo...");
|
|
1015
|
+
try {
|
|
1016
|
+
const { getHomeAgroplanDir: getHomeAgroplanDir2 } = await Promise.resolve().then(() => (init_paths(), exports_paths));
|
|
1017
|
+
const { existsSync: existsSync7, rmSync: rmSync2 } = await import("fs");
|
|
1018
|
+
const { join: join4 } = await import("path");
|
|
1019
|
+
const homeDir = getHomeAgroplanDir2();
|
|
1020
|
+
const backendDir = join4(homeDir, "backend");
|
|
1021
|
+
if (existsSync7(backendDir)) {
|
|
1022
|
+
rmSync2(backendDir, { recursive: true, force: true });
|
|
1023
|
+
console.log(" \u2705 Backend antigo removido");
|
|
1024
|
+
} else {
|
|
1025
|
+
console.log(" \u2139\uFE0F Nenhum backend antigo encontrado");
|
|
1026
|
+
}
|
|
1027
|
+
} catch (error) {
|
|
1028
|
+
console.log(` \u26A0\uFE0F Erro ao remover backend: ${error}`);
|
|
1029
|
+
}
|
|
1030
|
+
console.log("");
|
|
1031
|
+
console.log(`3\uFE0F\u20E3 Instalando backend atualizado...
|
|
966
1032
|
`);
|
|
967
1033
|
const pythonPath = setupState.pythonPath || undefined;
|
|
968
1034
|
await setupCommand(true, pythonPath);
|
|
969
1035
|
console.log(`
|
|
970
|
-
|
|
1036
|
+
4\uFE0F\u20E3 Verificando instala\xE7\xE3o...`);
|
|
1037
|
+
try {
|
|
1038
|
+
const { getHomeAgroplanDir: getHomeAgroplanDir2 } = await Promise.resolve().then(() => (init_paths(), exports_paths));
|
|
1039
|
+
const { existsSync: existsSync7, readFileSync: readFileSync4 } = await import("fs");
|
|
1040
|
+
const { join: join4 } = await import("path");
|
|
1041
|
+
const homeDir = getHomeAgroplanDir2();
|
|
1042
|
+
const versionPath = join4(homeDir, "backend", "VERSION.json");
|
|
1043
|
+
if (existsSync7(versionPath)) {
|
|
1044
|
+
const versionContent = readFileSync4(versionPath, "utf-8");
|
|
1045
|
+
const versionInfo = JSON.parse(versionContent);
|
|
1046
|
+
console.log(` \u2705 Backend template: ${versionInfo.backend_template_version}`);
|
|
1047
|
+
console.log(` \u2705 ZARC index: ${versionInfo.zarc_index_version}`);
|
|
1048
|
+
console.log(` \u2705 Features: ${versionInfo.features.length} ativas`);
|
|
1049
|
+
} else {
|
|
1050
|
+
console.log(" \u26A0\uFE0F VERSION.json n\xE3o encontrado");
|
|
1051
|
+
}
|
|
1052
|
+
} catch (error) {
|
|
1053
|
+
console.log(` \u26A0\uFE0F Erro ao verificar vers\xE3o: ${error}`);
|
|
1054
|
+
}
|
|
1055
|
+
console.log(`
|
|
1056
|
+
5\uFE0F\u20E3 Atualiza\xE7\xE3o conclu\xEDda!`);
|
|
971
1057
|
if (isRunning) {
|
|
972
1058
|
console.log(`
|
|
973
1059
|
\uD83D\uDCA1 Para reiniciar a API:`);
|
|
@@ -977,6 +1063,9 @@ async function updateCommand() {
|
|
|
977
1063
|
\uD83D\uDCA1 Para iniciar a API:`);
|
|
978
1064
|
console.log(" agroplan serve on");
|
|
979
1065
|
}
|
|
1066
|
+
console.log(`
|
|
1067
|
+
\uD83D\uDD0D Para verificar a vers\xE3o da API rodando:`);
|
|
1068
|
+
console.log(" http://localhost:8000/debug/version");
|
|
980
1069
|
}
|
|
981
1070
|
|
|
982
1071
|
// src/index.ts
|
|
@@ -993,7 +1082,7 @@ var COMMANDS = {
|
|
|
993
1082
|
open: "Abre o AgroPlan AI no navegador"
|
|
994
1083
|
};
|
|
995
1084
|
function showHelp() {
|
|
996
|
-
console.log("\uD83C\uDF31 AgroPlan AI - CLI Local v1.0.
|
|
1085
|
+
console.log("\uD83C\uDF31 AgroPlan AI - CLI Local v1.0.21");
|
|
997
1086
|
console.log(` Launcher para modo local acelerado
|
|
998
1087
|
`);
|
|
999
1088
|
console.log("\uD83D\uDCCB Comandos dispon\xEDveis:");
|