agroplan-ai-cli 1.0.21 → 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.
@@ -1,6 +1,6 @@
1
1
  {
2
- "cli_version": "1.0.21",
3
- "backend_template_version": "1.0.21",
2
+ "cli_version": "1.0.22",
3
+ "backend_template_version": "1.0.22",
4
4
  "zarc_index_version": "2025-2026-fast-index-v2",
5
5
  "features": [
6
6
  "zarc_fast_index",
@@ -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
- return {
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. {items_removidos} itens removidos.",
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") if zarc_data else "ZARC não consultado"
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 = normalizar_solo(solo)
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", "misto"]
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", "misto"]
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
- ) -> Optional[Dict[str, Any]]:
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 ou None se não encontrar
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
- return buscar_zarc_streaming(cultura, uf, municipio, solo, safra)
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 = normalizar_solo(solo) if solo else None
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 normalizar_solo(solo_registro) == solo_norm:
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
- ) -> Optional[Dict[str, Any]]:
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 = normalizar_solo(solo) if solo else None
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 normalizar_solo(registro.get("solo", "")) == solo_norm:
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
- return None
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agroplan-ai-cli",
3
- "version": "1.0.21",
3
+ "version": "1.0.22",
4
4
  "description": "CLI global para AgroPlan AI - modo local acelerado",
5
5
  "type": "module",
6
6
  "bin": {