agroplan-ai-cli 1.0.23 → 1.0.24

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,14 +1,17 @@
1
1
  {
2
- "cli_version": "1.0.23",
3
- "backend_template_version": "1.0.23",
2
+ "cli_version": "1.0.24",
3
+ "backend_template_version": "1.0.24",
4
4
  "zarc_index_version": "2025-2026-fast-index-v2",
5
+ "price_index_version": "2025-05-reference-v1",
5
6
  "features": [
6
7
  "zarc_fast_index",
7
8
  "zarc_fallback_sorgo_mandioca",
8
9
  "soil_normalization_misto_siltoso",
9
10
  "climate_real_data",
10
11
  "hybrid_mode",
11
- "report_generator_zarc_support"
12
+ "report_generator_zarc_support",
13
+ "price_provider_index_fallback",
14
+ "price_display_only"
12
15
  ],
13
- "generated_at": "2026-05-09T19:00:00Z"
16
+ "generated_at": "2026-05-09T20:00:00Z"
14
17
  }
@@ -151,6 +151,10 @@ def health():
151
151
  from providers.zarc_provider import get_zarc_status
152
152
  zarc_status = get_zarc_status()
153
153
 
154
+ # Verificar status de preços
155
+ from providers.price_provider import get_price_status
156
+ price_status = get_price_status()
157
+
154
158
  # Carregar VERSION.json se existir
155
159
  version_info = {}
156
160
  version_path = os.path.join(os.path.dirname(__file__), "VERSION.json")
@@ -168,7 +172,8 @@ def health():
168
172
  "data_mode": DATA_MODE,
169
173
  "providers": {
170
174
  "weather": "available" if WEATHER_PROVIDER else "disabled",
171
- "zarc": zarc_status
175
+ "zarc": zarc_status,
176
+ "prices": price_status
172
177
  },
173
178
  "provider_cache": provider_cache_stats
174
179
  }
@@ -420,6 +425,71 @@ def get_zarc(
420
425
  except Exception as e:
421
426
  raise HTTPException(status_code=500, detail=str(e))
422
427
 
428
+ @app.get("/dados/precos")
429
+ def get_precos(
430
+ cultura: Optional[str] = Query(None, description="Nome da cultura"),
431
+ uf: Optional[str] = Query(None, description="Unidade Federativa (ex: SP, PR)")
432
+ ):
433
+ """Obtém preços agrícolas"""
434
+ try:
435
+ # Importar provider de preços
436
+ from providers.price_provider import buscar_preco
437
+
438
+ # Se cultura não foi fornecida, retornar mensagem amigável
439
+ if not cultura:
440
+ return {
441
+ "message": "Informe a cultura para consultar preços agrícolas.",
442
+ "exemplo_soja_sp": "/dados/precos?cultura=soja&uf=SP",
443
+ "exemplo_milho_pr": "/dados/precos?cultura=milho&uf=PR",
444
+ "parametros": {
445
+ "cultura": "Nome da cultura (obrigatório)",
446
+ "uf": "Unidade Federativa (opcional)"
447
+ },
448
+ "culturas_disponiveis": [
449
+ "soja", "milho", "feijao", "trigo", "algodao",
450
+ "cafe", "cana", "arroz", "sorgo", "mandioca"
451
+ ]
452
+ }
453
+
454
+ # Buscar preço
455
+ preco_data = buscar_preco(cultura=cultura, uf=uf)
456
+
457
+ return preco_data
458
+
459
+ except Exception as e:
460
+ raise HTTPException(status_code=500, detail=str(e))
461
+
462
+ @app.get("/dados/precos/lote")
463
+ def get_precos_lote(
464
+ uf: Optional[str] = Query(None, description="Unidade Federativa (ex: SP, PR)")
465
+ ):
466
+ """Obtém preços agrícolas para todas as culturas do AgroPlan"""
467
+ try:
468
+ # Importar provider de preços
469
+ from providers.price_provider import buscar_precos_lote
470
+
471
+ # Culturas do AgroPlan
472
+ culturas = ["soja", "milho", "feijao", "trigo", "algodao", "cafe", "cana", "arroz", "sorgo", "mandioca"]
473
+
474
+ # Buscar preços em lote
475
+ precos_data = buscar_precos_lote(culturas=culturas, uf=uf)
476
+
477
+ # Estatísticas
478
+ total = len(precos_data)
479
+ com_preco = sum(1 for p in precos_data if p.get("ativo"))
480
+ fallback = sum(1 for p in precos_data if p.get("fallback"))
481
+
482
+ return {
483
+ "uf": uf,
484
+ "total_culturas": total,
485
+ "culturas_com_preco": com_preco,
486
+ "culturas_fallback": fallback,
487
+ "precos": precos_data
488
+ }
489
+
490
+ except Exception as e:
491
+ raise HTTPException(status_code=500, detail=str(e))
492
+
423
493
  @app.get("/dashboard")
424
494
  def get_dashboard(
425
495
  lat: Optional[float] = None,
@@ -552,6 +622,10 @@ def get_dashboard(
552
622
  else:
553
623
  resultado_base["zarc"] = {"ativo": False}
554
624
 
625
+ # Enriquecer com preços agrícolas
626
+ from core.price_adapter import aplicar_precos_no_plano
627
+ resultado_base = aplicar_precos_no_plano(resultado_base, uf=uf)
628
+
555
629
  return resultado_base
556
630
 
557
631
  # Usa cache para dashboard com contexto climático
@@ -619,6 +693,13 @@ def get_recomendacoes(
619
693
  else:
620
694
  resultado["zarc"] = {"ativo": False}
621
695
 
696
+ # Enriquecer com preços agrícolas
697
+ from core.price_adapter import aplicar_precos_no_plano
698
+ resultado_temp = {"plano": resultado["recomendacoes"]}
699
+ resultado_temp = aplicar_precos_no_plano(resultado_temp, uf=uf)
700
+ resultado["recomendacoes"] = resultado_temp["plano"]
701
+ resultado["precos"] = resultado_temp.get("precos", {"ativo": False})
702
+
622
703
  return resultado
623
704
  except Exception as e:
624
705
  raise HTTPException(status_code=500, detail=str(e))
@@ -872,6 +953,10 @@ def otimizar(request: OtimizarRequest):
872
953
  else:
873
954
  resultado_convertido["zarc"] = {"ativo": False}
874
955
 
956
+ # Enriquecer com preços agrícolas
957
+ from core.price_adapter import aplicar_precos_no_plano
958
+ resultado_convertido = aplicar_precos_no_plano(resultado_convertido, uf=request.uf)
959
+
875
960
  return resultado_convertido
876
961
  except HTTPException:
877
962
  raise
@@ -1041,13 +1126,23 @@ def limpar_cache(request: Request):
1041
1126
  except Exception:
1042
1127
  pass
1043
1128
 
1129
+ # Limpa cache de preços em memória
1130
+ price_cache_cleared = False
1131
+ try:
1132
+ from providers.price_provider import clear_price_cache
1133
+ clear_price_cache()
1134
+ price_cache_cleared = True
1135
+ except Exception:
1136
+ pass
1137
+
1044
1138
  return {
1045
1139
  "status": "ok",
1046
1140
  "message": f"Cache limpo completamente.",
1047
1141
  "details": {
1048
1142
  "resultados_cache": items_removidos,
1049
1143
  "provider_cache": provider_items_removidos,
1050
- "zarc_index_cache": "cleared" if zarc_cache_cleared else "not_found"
1144
+ "zarc_index_cache": "cleared" if zarc_cache_cleared else "not_found",
1145
+ "price_cache": "cleared" if price_cache_cleared else "not_found"
1051
1146
  },
1052
1147
  "protected": bool(cache_admin_token)
1053
1148
  }
@@ -0,0 +1,191 @@
1
+ """
2
+ Adaptador de Preços Agrícolas
3
+ Integra preços reais no planejamento
4
+ """
5
+
6
+ import os
7
+ from typing import Dict, List, Optional
8
+ from providers.price_provider import buscar_preco, buscar_precos_lote
9
+
10
+ # Configuração
11
+ PRICE_APPLY_TO_PROFIT = os.getenv("PRICE_APPLY_TO_PROFIT", "false").lower() == "true"
12
+
13
+
14
+ def aplicar_precos_no_plano(resultado: Dict, uf: Optional[str] = None) -> Dict:
15
+ """
16
+ Aplica informações de preços no plano
17
+
18
+ Args:
19
+ resultado: Resultado do AG ou cenário
20
+ uf: Unidade Federativa (opcional)
21
+
22
+ Returns:
23
+ Resultado enriquecido com informações de preços
24
+ """
25
+ if "plano" not in resultado:
26
+ return resultado
27
+
28
+ # Coletar culturas únicas
29
+ culturas = list(set([item.get("cultura") for item in resultado["plano"]]))
30
+
31
+ # Buscar preços em lote
32
+ precos_data = buscar_precos_lote(culturas, uf)
33
+
34
+ # Criar mapa de preços por cultura
35
+ precos_map = {p["cultura"]: p for p in precos_data}
36
+
37
+ # Estatísticas
38
+ culturas_com_preco = 0
39
+ culturas_fallback = 0
40
+ culturas_sem_preco = 0
41
+
42
+ # Aplicar preços no plano
43
+ for item in resultado["plano"]:
44
+ cultura = item.get("cultura")
45
+ preco_info = precos_map.get(cultura, {})
46
+
47
+ # Adicionar informações de preço
48
+ item["preco_real"] = preco_info
49
+
50
+ # Contabilizar estatísticas
51
+ if preco_info.get("ativo"):
52
+ if preco_info.get("fallback"):
53
+ culturas_fallback += 1
54
+ else:
55
+ culturas_com_preco += 1
56
+ else:
57
+ culturas_sem_preco += 1
58
+
59
+ # Se PRICE_APPLY_TO_PROFIT estiver ativo, recalcular lucro
60
+ # NOTA: Por enquanto, apenas adiciona informação sem recalcular
61
+ # Recálculo será implementado após validação de unidades
62
+
63
+ # Adicionar resumo de preços ao resultado
64
+ resultado["precos"] = {
65
+ "ativo": True,
66
+ "source": "price-local-index",
67
+ "fallback_count": culturas_fallback,
68
+ "culturas_com_preco": culturas_com_preco,
69
+ "culturas_sem_preco": culturas_sem_preco,
70
+ "total_culturas": len(culturas),
71
+ "aplicado_no_lucro": PRICE_APPLY_TO_PROFIT,
72
+ "uf": uf
73
+ }
74
+
75
+ return resultado
76
+
77
+
78
+ def gerar_secao_precos_relatorio(plano: List[Dict], uf: Optional[str], formato: str = "md") -> str:
79
+ """
80
+ Gera seção de preços para o relatório
81
+
82
+ Args:
83
+ plano: Lista de itens do plano com informações de preços
84
+ uf: Unidade Federativa
85
+ formato: 'md' ou 'txt'
86
+
87
+ Returns:
88
+ String com seção formatada
89
+ """
90
+ if formato == "md":
91
+ secao = "## 💰 Preços Agrícolas Utilizados\n\n"
92
+
93
+ if uf:
94
+ secao += f"**Região:** {uf}\n\n"
95
+
96
+ secao += "### Preços por Cultura\n\n"
97
+ secao += "| Cultura | Preço | Unidade | Fonte | Observação |\n"
98
+ secao += "|---------|-------|---------|-------|------------|\n"
99
+
100
+ # Coletar culturas únicas
101
+ culturas_vistas = set()
102
+
103
+ for item in plano:
104
+ cultura = item.get("cultura", "").upper()
105
+
106
+ if cultura in culturas_vistas:
107
+ continue
108
+
109
+ culturas_vistas.add(cultura)
110
+
111
+ preco_info = item.get("preco_real", {})
112
+
113
+ if preco_info.get("ativo"):
114
+ preco = preco_info.get("preco")
115
+ unidade = preco_info.get("unidade", "N/A")
116
+ fonte = preco_info.get("source", "N/A")
117
+ fallback_icon = "⚠️ " if preco_info.get("fallback") else ""
118
+ observacao = preco_info.get("observacao", "")
119
+
120
+ preco_fmt = f"R$ {preco:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".") if preco else "N/A"
121
+
122
+ secao += f"| {cultura} | {preco_fmt} | {unidade} | {fallback_icon}{fonte} | {observacao} |\n"
123
+ else:
124
+ secao += f"| {cultura} | N/A | N/A | price-unavailable | Preço não disponível |\n"
125
+
126
+ secao += "\n"
127
+ secao += "### Observações\n\n"
128
+
129
+ if PRICE_APPLY_TO_PROFIT:
130
+ secao += "✅ **Os preços foram aplicados ao cálculo de lucro estimado.**\n\n"
131
+ else:
132
+ secao += "ℹ️ **Os preços são exibidos como referência, mas o cálculo de lucro ainda utiliza a base interna simulada.**\n\n"
133
+
134
+ secao += "**Fontes:**\n"
135
+ secao += "- `price-local-index`: Índice local de preços\n"
136
+ secao += "- `price-fallback`: Preço de referência (fallback)\n"
137
+ secao += "- `price-unavailable`: Preço não disponível\n"
138
+
139
+ else: # txt
140
+ secao = "PREÇOS AGRÍCOLAS UTILIZADOS\n\n"
141
+
142
+ if uf:
143
+ secao += f"Região: {uf}\n\n"
144
+
145
+ secao += "Preços por Cultura:\n\n"
146
+
147
+ # Coletar culturas únicas
148
+ culturas_vistas = set()
149
+
150
+ for item in plano:
151
+ cultura = item.get("cultura", "").upper()
152
+
153
+ if cultura in culturas_vistas:
154
+ continue
155
+
156
+ culturas_vistas.add(cultura)
157
+
158
+ preco_info = item.get("preco_real", {})
159
+
160
+ if preco_info.get("ativo"):
161
+ preco = preco_info.get("preco")
162
+ unidade = preco_info.get("unidade", "N/A")
163
+ fonte = preco_info.get("source", "N/A")
164
+ fallback_text = " (fallback)" if preco_info.get("fallback") else ""
165
+ observacao = preco_info.get("observacao", "")
166
+
167
+ preco_fmt = f"R$ {preco:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".") if preco else "N/A"
168
+
169
+ secao += f" {cultura}:\n"
170
+ secao += f" Preço: {preco_fmt}\n"
171
+ secao += f" Unidade: {unidade}\n"
172
+ secao += f" Fonte: {fonte}{fallback_text}\n"
173
+ secao += f" Observação: {observacao}\n\n"
174
+ else:
175
+ secao += f" {cultura}:\n"
176
+ secao += f" Preço: N/A\n"
177
+ secao += f" Observação: Preço não disponível\n\n"
178
+
179
+ secao += "Observações:\n\n"
180
+
181
+ if PRICE_APPLY_TO_PROFIT:
182
+ secao += "Os preços foram aplicados ao cálculo de lucro estimado.\n\n"
183
+ else:
184
+ secao += "Os preços são exibidos como referência, mas o cálculo de lucro ainda utiliza a base interna simulada.\n\n"
185
+
186
+ secao += "Fontes:\n"
187
+ secao += "- price-local-index: Índice local de preços\n"
188
+ secao += "- price-fallback: Preço de referência (fallback)\n"
189
+ secao += "- price-unavailable: Preço não disponível\n"
190
+
191
+ return secao
@@ -162,6 +162,13 @@ def gerar_relatorio_completo(culturas, talhoes, regras, objetivo='equilibrado',
162
162
  secao_zarc = gerar_secao_zarc_relatorio(resultado_temp["plano"], uf, municipio, safra, formato)
163
163
  conteudo += "\n\n" + secao_zarc
164
164
 
165
+ # Adicionar seção de preços agrícolas
166
+ from core.price_adapter import aplicar_precos_no_plano, gerar_secao_precos_relatorio
167
+ resultado_temp = {"plano": resultado_ag["plano"]}
168
+ resultado_temp = aplicar_precos_no_plano(resultado_temp, uf=uf)
169
+ secao_precos = gerar_secao_precos_relatorio(resultado_temp["plano"], uf, formato)
170
+ conteudo += "\n\n" + secao_precos
171
+
165
172
  # Salva arquivo
166
173
  os.makedirs('reports', exist_ok=True)
167
174
  timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
@@ -0,0 +1,87 @@
1
+ {
2
+ "generated_at": "2026-05-09T19:30:00Z",
3
+ "source": "price-fallback",
4
+ "description": "Preços de referência para fallback quando dados reais não estão disponíveis",
5
+ "precos": [
6
+ {
7
+ "cultura": "soja",
8
+ "preco": 128.50,
9
+ "unidade": "saca_60kg",
10
+ "fonte": "fallback",
11
+ "fallback": true,
12
+ "observacao": "Preço de referência baseado em média histórica"
13
+ },
14
+ {
15
+ "cultura": "milho",
16
+ "preco": 65.00,
17
+ "unidade": "saca_60kg",
18
+ "fonte": "fallback",
19
+ "fallback": true,
20
+ "observacao": "Preço de referência baseado em média histórica"
21
+ },
22
+ {
23
+ "cultura": "feijao",
24
+ "preco": 180.00,
25
+ "unidade": "saca_60kg",
26
+ "fonte": "fallback",
27
+ "fallback": true,
28
+ "observacao": "Preço de referência baseado em média histórica"
29
+ },
30
+ {
31
+ "cultura": "trigo",
32
+ "preco": 75.00,
33
+ "unidade": "saca_60kg",
34
+ "fonte": "fallback",
35
+ "fallback": true,
36
+ "observacao": "Preço de referência baseado em média histórica"
37
+ },
38
+ {
39
+ "cultura": "algodao",
40
+ "preco": 3200.00,
41
+ "unidade": "arroba_15kg",
42
+ "fonte": "fallback",
43
+ "fallback": true,
44
+ "observacao": "Preço de referência baseado em média histórica"
45
+ },
46
+ {
47
+ "cultura": "cafe",
48
+ "preco": 1250.00,
49
+ "unidade": "saca_60kg",
50
+ "fonte": "fallback",
51
+ "fallback": true,
52
+ "observacao": "Preço de referência baseado em média histórica"
53
+ },
54
+ {
55
+ "cultura": "cana",
56
+ "preco": 95.00,
57
+ "unidade": "tonelada",
58
+ "fonte": "fallback",
59
+ "fallback": true,
60
+ "observacao": "Preço de referência baseado em média histórica"
61
+ },
62
+ {
63
+ "cultura": "arroz",
64
+ "preco": 85.00,
65
+ "unidade": "saca_50kg",
66
+ "fonte": "fallback",
67
+ "fallback": true,
68
+ "observacao": "Preço de referência baseado em média histórica"
69
+ },
70
+ {
71
+ "cultura": "sorgo",
72
+ "preco": 55.00,
73
+ "unidade": "saca_60kg",
74
+ "fonte": "fallback",
75
+ "fallback": true,
76
+ "observacao": "Preço de referência baseado em média histórica"
77
+ },
78
+ {
79
+ "cultura": "mandioca",
80
+ "preco": 450.00,
81
+ "unidade": "tonelada",
82
+ "fonte": "fallback",
83
+ "fallback": true,
84
+ "observacao": "Preço de referência baseado em média histórica"
85
+ }
86
+ ]
87
+ }
@@ -0,0 +1,58 @@
1
+ {
2
+ "generated_at": "2026-05-09T19:30:00Z",
3
+ "source": "price-local-index",
4
+ "description": "Índice local de preços agrícolas - será substituído por dados reais futuramente",
5
+ "version": "1.0.0",
6
+ "records": {
7
+ "soja_SP": {
8
+ "cultura": "soja",
9
+ "uf": "SP",
10
+ "preco": 130.00,
11
+ "unidade": "saca_60kg",
12
+ "fonte": "price-local-index",
13
+ "fallback": false,
14
+ "data_referencia": "2026-05-01",
15
+ "observacao": "Preço de referência para SP"
16
+ },
17
+ "milho_SP": {
18
+ "cultura": "milho",
19
+ "uf": "SP",
20
+ "preco": 67.00,
21
+ "unidade": "saca_60kg",
22
+ "fonte": "price-local-index",
23
+ "fallback": false,
24
+ "data_referencia": "2026-05-01",
25
+ "observacao": "Preço de referência para SP"
26
+ },
27
+ "feijao_SP": {
28
+ "cultura": "feijao",
29
+ "uf": "SP",
30
+ "preco": 185.00,
31
+ "unidade": "saca_60kg",
32
+ "fonte": "price-local-index",
33
+ "fallback": false,
34
+ "data_referencia": "2026-05-01",
35
+ "observacao": "Preço de referência para SP"
36
+ },
37
+ "cafe_SP": {
38
+ "cultura": "cafe",
39
+ "uf": "SP",
40
+ "preco": 1280.00,
41
+ "unidade": "saca_60kg",
42
+ "fonte": "price-local-index",
43
+ "fallback": false,
44
+ "data_referencia": "2026-05-01",
45
+ "observacao": "Preço de referência para SP"
46
+ },
47
+ "cana_SP": {
48
+ "cultura": "cana",
49
+ "uf": "SP",
50
+ "preco": 98.00,
51
+ "unidade": "tonelada",
52
+ "fonte": "price-local-index",
53
+ "fallback": false,
54
+ "data_referencia": "2026-05-01",
55
+ "observacao": "Preço de referência para SP"
56
+ }
57
+ }
58
+ }
@@ -0,0 +1,219 @@
1
+ """
2
+ Provider de Preços Agrícolas
3
+ Fornece preços reais ou de referência para culturas
4
+ """
5
+
6
+ import os
7
+ import json
8
+ from typing import Optional, Dict, List
9
+
10
+ # Configurações
11
+ PRICE_PROVIDER = os.getenv("PRICE_PROVIDER", "local") # local, conab, disabled
12
+ PRICE_INDEX_PATH = os.path.join(os.path.dirname(__file__), "..", "data", "precos", "precos_index.json")
13
+ PRICE_FALLBACK_PATH = os.path.join(os.path.dirname(__file__), "..", "data", "precos", "precos_fallback.json")
14
+
15
+ # Cache em memória para índice de preços
16
+ _price_index_cache = None
17
+ _price_fallback_cache = None
18
+
19
+
20
+ def normalizar_cultura_preco(cultura: str) -> str:
21
+ """Normaliza nome da cultura para busca de preços"""
22
+ mapping = {
23
+ "café": "cafe",
24
+ "feijão": "feijao",
25
+ "algodão": "algodao",
26
+ "CAFÉ": "cafe",
27
+ "FEIJÃO": "feijao",
28
+ "ALGODÃO": "algodao",
29
+ "Café": "cafe",
30
+ "Feijão": "feijao",
31
+ "Algodão": "algodao"
32
+ }
33
+ cultura_normalizada = mapping.get(cultura, cultura)
34
+ return cultura_normalizada.lower().strip()
35
+
36
+
37
+ def load_price_index() -> Optional[Dict]:
38
+ """Carrega índice de preços do arquivo JSON"""
39
+ global _price_index_cache
40
+
41
+ if _price_index_cache is not None:
42
+ return _price_index_cache
43
+
44
+ if not os.path.exists(PRICE_INDEX_PATH):
45
+ return None
46
+
47
+ try:
48
+ with open(PRICE_INDEX_PATH, 'r', encoding='utf-8') as f:
49
+ _price_index_cache = json.load(f)
50
+ return _price_index_cache
51
+ except Exception as e:
52
+ print(f"Erro ao carregar índice de preços: {e}")
53
+ return None
54
+
55
+
56
+ def load_price_fallback() -> List[Dict]:
57
+ """Carrega preços de fallback do arquivo JSON"""
58
+ global _price_fallback_cache
59
+
60
+ if _price_fallback_cache is not None:
61
+ return _price_fallback_cache
62
+
63
+ if not os.path.exists(PRICE_FALLBACK_PATH):
64
+ return []
65
+
66
+ try:
67
+ with open(PRICE_FALLBACK_PATH, 'r', encoding='utf-8') as f:
68
+ data = json.load(f)
69
+ _price_fallback_cache = data.get("precos", [])
70
+ return _price_fallback_cache
71
+ except Exception as e:
72
+ print(f"Erro ao carregar preços de fallback: {e}")
73
+ return []
74
+
75
+
76
+ def buscar_preco_no_indice(cultura: str, uf: Optional[str] = None) -> Optional[Dict]:
77
+ """Busca preço no índice local"""
78
+ price_index = load_price_index()
79
+
80
+ if not price_index:
81
+ return None
82
+
83
+ records = price_index.get("records", {})
84
+
85
+ # Tentar buscar com UF específica
86
+ if uf:
87
+ key = f"{cultura}_{uf.upper()}"
88
+ if key in records:
89
+ return records[key]
90
+
91
+ # Tentar buscar sem UF (genérico)
92
+ for key, record in records.items():
93
+ if record.get("cultura") == cultura and not uf:
94
+ return record
95
+
96
+ return None
97
+
98
+
99
+ def buscar_preco_no_fallback(cultura: str) -> Optional[Dict]:
100
+ """Busca preço no fallback"""
101
+ fallback_data = load_price_fallback()
102
+
103
+ for item in fallback_data:
104
+ if item.get("cultura") == cultura:
105
+ return item
106
+
107
+ return None
108
+
109
+
110
+ def buscar_preco(cultura: str, uf: Optional[str] = None) -> Dict:
111
+ """
112
+ Busca preço de uma cultura
113
+
114
+ Args:
115
+ cultura: Nome da cultura
116
+ uf: Unidade Federativa (opcional)
117
+
118
+ Returns:
119
+ Dicionário com informações de preço (sempre retorna, nunca None)
120
+ """
121
+ # Normalizar cultura
122
+ cultura_normalizada = normalizar_cultura_preco(cultura)
123
+
124
+ # Se provider desabilitado, retornar inativo
125
+ if PRICE_PROVIDER == "disabled":
126
+ return {
127
+ "ativo": False,
128
+ "source": "price-disabled",
129
+ "fallback": False,
130
+ "cultura": cultura,
131
+ "uf": uf,
132
+ "preco": None,
133
+ "unidade": None,
134
+ "data_referencia": None,
135
+ "observacao": "Provider de preços desabilitado"
136
+ }
137
+
138
+ # Tentar buscar no índice local
139
+ preco_data = buscar_preco_no_indice(cultura_normalizada, uf)
140
+
141
+ if preco_data:
142
+ return {
143
+ "ativo": True,
144
+ "source": preco_data.get("fonte", "price-local-index"),
145
+ "fallback": preco_data.get("fallback", False),
146
+ "cultura": cultura,
147
+ "uf": uf or preco_data.get("uf"),
148
+ "preco": preco_data.get("preco"),
149
+ "unidade": preco_data.get("unidade"),
150
+ "data_referencia": preco_data.get("data_referencia"),
151
+ "observacao": preco_data.get("observacao", "Preço obtido do índice local")
152
+ }
153
+
154
+ # Tentar fallback
155
+ preco_data = buscar_preco_no_fallback(cultura_normalizada)
156
+
157
+ if preco_data:
158
+ return {
159
+ "ativo": True,
160
+ "source": "price-fallback",
161
+ "fallback": True,
162
+ "cultura": cultura,
163
+ "uf": uf,
164
+ "preco": preco_data.get("preco"),
165
+ "unidade": preco_data.get("unidade"),
166
+ "data_referencia": None,
167
+ "observacao": preco_data.get("observacao", "Preço de referência (fallback)")
168
+ }
169
+
170
+ # Nenhum preço encontrado
171
+ return {
172
+ "ativo": False,
173
+ "source": "price-unavailable",
174
+ "fallback": False,
175
+ "cultura": cultura,
176
+ "uf": uf,
177
+ "preco": None,
178
+ "unidade": None,
179
+ "data_referencia": None,
180
+ "observacao": "Preço não disponível para esta cultura"
181
+ }
182
+
183
+
184
+ def buscar_precos_lote(culturas: List[str], uf: Optional[str] = None) -> List[Dict]:
185
+ """
186
+ Busca preços para múltiplas culturas
187
+
188
+ Args:
189
+ culturas: Lista de nomes de culturas
190
+ uf: Unidade Federativa (opcional)
191
+
192
+ Returns:
193
+ Lista de dicionários com informações de preço
194
+ """
195
+ return [buscar_preco(cultura, uf) for cultura in culturas]
196
+
197
+
198
+ def get_price_status() -> Dict:
199
+ """Retorna status do provider de preços"""
200
+ price_index = load_price_index()
201
+ fallback_data = load_price_fallback()
202
+
203
+ index_count = len(price_index.get("records", {})) if price_index else 0
204
+ fallback_count = len(fallback_data)
205
+
206
+ return {
207
+ "provider": PRICE_PROVIDER,
208
+ "index_available": price_index is not None,
209
+ "index_records": index_count,
210
+ "fallback_records": fallback_count,
211
+ "total_culturas_cobertas": index_count + fallback_count
212
+ }
213
+
214
+
215
+ def clear_price_cache():
216
+ """Limpa cache de preços em memória"""
217
+ global _price_index_cache, _price_fallback_cache
218
+ _price_index_cache = None
219
+ _price_fallback_cache = None
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agroplan-ai-cli",
3
- "version": "1.0.23",
3
+ "version": "1.0.24",
4
4
  "description": "CLI global para AgroPlan AI - modo local acelerado",
5
5
  "type": "module",
6
6
  "bin": {