agroplan-ai-cli 1.0.10 → 1.0.12

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.
@@ -422,30 +422,65 @@ def get_cenarios(
422
422
  # Formata resposta
423
423
  cenarios_formatados = {}
424
424
 
425
+ # Aplicar ajuste climático em todos os cenários se disponível
426
+ ajuste_risco = contexto_climatico.get("ajuste_risco", 0) if contexto_climatico else 0
427
+
425
428
  for key, cenario in cenarios.items():
429
+ # Aplicar ajuste climático no risco de cada item do plano
430
+ plano_ajustado = []
431
+ soma_risco_area = 0
432
+ soma_area = 0
433
+
434
+ for p in cenario['plano']:
435
+ risco_original = float(p['risco']) # Em pontos percentuais (30, 35, etc.)
436
+ area = float(p['area'])
437
+
438
+ # Aplicar ajuste se houver (ajuste também em pontos percentuais: -3, +5, +15)
439
+ if ajuste_risco != 0:
440
+ novo_risco = min(95, max(5, risco_original + ajuste_risco))
441
+ else:
442
+ novo_risco = risco_original
443
+
444
+ item_plano = {
445
+ "talhao": int(p['talhao']),
446
+ "area": area,
447
+ "solo": str(talhoes_dict[int(p['talhao'])]['solo']),
448
+ "clima": str(talhoes_dict[int(p['talhao'])]['clima']),
449
+ "relevo": str(talhoes_dict[int(p['talhao'])]['relevo']),
450
+ "agua": str(talhoes_dict[int(p['talhao'])]['agua']),
451
+ "cultura": str(p['cultura']),
452
+ "lucro_estimado": float(p['lucro_estimado']),
453
+ "risco": round(novo_risco, 1), # Em pontos percentuais
454
+ "nota": float(p['nota']),
455
+ "tempo": 0
456
+ }
457
+
458
+ # Adicionar campos de ajuste se aplicado
459
+ if ajuste_risco != 0:
460
+ item_plano["risco_original"] = round(risco_original, 1)
461
+ item_plano["ajuste_aplicado"] = round(ajuste_risco, 1)
462
+
463
+ plano_ajustado.append(item_plano)
464
+
465
+ # Acumular para recalcular risco médio
466
+ soma_risco_area += novo_risco * area
467
+ soma_area += area
468
+
469
+ # Recalcular risco médio ponderado
470
+ risco_medio_ajustado = soma_risco_area / soma_area if soma_area > 0 else cenario['risco_medio']
471
+
426
472
  cenarios_formatados[key] = {
427
473
  'nome': str(cenario['nome']),
428
474
  'descricao': str(cenario['descricao']),
429
475
  'lucro_total': float(cenario['lucro_total']),
430
- 'risco_medio': float(cenario['risco_medio']),
476
+ 'risco_medio': round(risco_medio_ajustado, 1), # Em pontos percentuais
431
477
  'area_total': float(cenario['area_total']),
432
- 'plano': [
433
- {
434
- "talhao": int(p['talhao']),
435
- "area": float(p['area']),
436
- "solo": str(talhoes_dict[int(p['talhao'])]['solo']),
437
- "clima": str(talhoes_dict[int(p['talhao'])]['clima']),
438
- "relevo": str(talhoes_dict[int(p['talhao'])]['relevo']),
439
- "agua": str(talhoes_dict[int(p['talhao'])]['agua']),
440
- "cultura": str(p['cultura']),
441
- "lucro_estimado": float(p['lucro_estimado']),
442
- "risco": float(p['risco']),
443
- "nota": float(p['nota']),
444
- "tempo": 0 # Não disponível nos cenários simples
445
- }
446
- for p in cenario['plano']
447
- ]
478
+ 'plano': plano_ajustado
448
479
  }
480
+
481
+ # Adicionar risco médio original se ajuste foi aplicado
482
+ if ajuste_risco != 0:
483
+ cenarios_formatados[key]['risco_medio_original'] = round(float(cenario['risco_medio']), 1)
449
484
 
450
485
  # Adiciona AG com contexto climático
451
486
  cenarios_formatados['genetico'] = {
@@ -642,36 +677,18 @@ def relatorio(request: RelatorioRequest):
642
677
  from core.climate_adapter import obter_contexto_climatico_por_coordenadas
643
678
  contexto_climatico = obter_contexto_climatico_por_coordenadas(request.lat, request.lon, request.days)
644
679
 
645
- # Gera relatório (por enquanto sem integração climática no gerador)
646
- # TODO: Atualizar report_generator para aceitar contexto climático
680
+ # Gera relatório com contexto climático integrado
647
681
  caminho = gerar_relatorio_completo(
648
682
  culturas, talhoes, regras,
649
683
  objetivo=request.objetivo,
650
- formato=request.formato
684
+ formato=request.formato,
685
+ contexto_climatico=contexto_climatico
651
686
  )
652
687
 
653
688
  # Lê conteúdo
654
689
  with open(caminho, 'r', encoding='utf-8') as f:
655
690
  conteudo = f.read()
656
691
 
657
- # Adiciona informações climáticas ao final se disponível
658
- if contexto_climatico and not contexto_climatico.get("fallback", True):
659
- clima_info = f"""
660
-
661
- ## Dados Climáticos Reais
662
-
663
- **Fonte:** {contexto_climatico.get('fonte', 'N/A')}
664
- **Temperatura Média:** {contexto_climatico.get('temperatura_media', 'N/A')}°C
665
- **Precipitação Total:** {contexto_climatico.get('precipitacao_total', 'N/A')}mm
666
- **Risco Climático:** {contexto_climatico.get('risco_climatico_estimado', 'N/A')}
667
- **Clima Observado:** {contexto_climatico.get('clima_observado', 'N/A')}
668
- **Água Observada:** {contexto_climatico.get('agua_observada', 'N/A')}
669
- **Ajuste de Risco:** {contexto_climatico.get('ajuste_risco', 0):+.1%}
670
-
671
- *Dados climáticos integrados ao planejamento para maior precisão.*
672
- """
673
- conteudo += clima_info
674
-
675
692
  resultado = {
676
693
  "caminho": caminho,
677
694
  "conteudo": conteudo,
@@ -28,11 +28,15 @@ def classificar_agua_por_precipitacao(precipitacao_total: Optional[float]) -> Op
28
28
  return "media"
29
29
 
30
30
  def calcular_ajuste_risco_climatico(risco_climatico: str) -> float:
31
- """Calcula ajuste de risco baseado no risco climático estimado"""
31
+ """Calcula ajuste de risco baseado no risco climático estimado
32
+
33
+ Retorna ajuste em pontos percentuais (não decimal)
34
+ Exemplo: retorna 15 para +15 pontos percentuais, não 0.15
35
+ """
32
36
  ajustes = {
33
- "alto": 0.15, # +15% risco
34
- "medio": 0.05, # +5% risco
35
- "baixo": -0.03, # -3% risco (leve benefício)
37
+ "alto": 15, # +15 pontos percentuais
38
+ "medio": 5, # +5 pontos percentuais
39
+ "baixo": -3, # -3 pontos percentuais (leve benefício)
36
40
  "indeterminado": 0
37
41
  }
38
42
  return ajustes.get(risco_climatico, 0)
@@ -62,33 +66,66 @@ def criar_contexto_climatico(weather_summary: WeatherSummary) -> Dict[str, Any]:
62
66
  }
63
67
 
64
68
  def aplicar_contexto_climatico_no_plano(resultado_ag: Dict[str, Any], contexto_climatico: Dict[str, Any]) -> Dict[str, Any]:
65
- """Aplica ajustes climáticos no resultado do algoritmo genético"""
69
+ """Aplica ajustes climáticos no resultado do algoritmo genético
70
+
71
+ O risco no sistema está em formato de pontos percentuais (30 = 30%, não 0.30)
72
+ """
66
73
 
67
74
  if not contexto_climatico or contexto_climatico.get("ajuste_risco", 0) == 0:
68
- # Sem ajuste necessário
75
+ # Sem ajuste necessário, mas ainda adiciona contexto
69
76
  resultado_ag["ajuste_climatico_aplicado"] = False
70
77
  resultado_ag["contexto_climatico"] = contexto_climatico
71
78
  return resultado_ag
72
79
 
73
- ajuste_risco = contexto_climatico["ajuste_risco"]
80
+ ajuste_risco = contexto_climatico["ajuste_risco"] # Em pontos percentuais (ex: -3, +5, +15)
81
+
82
+ # Salvar risco médio original antes de aplicar ajustes
83
+ risco_medio_original = resultado_ag.get("risco_medio", 0)
84
+
85
+ # Aplicar ajuste no plano (formato padrão do gerar_plano_genetico)
86
+ if "plano" in resultado_ag:
87
+ soma_risco_area = 0
88
+ soma_area = 0
89
+
90
+ for item in resultado_ag["plano"]:
91
+ if "risco" in item and "area" in item:
92
+ risco_original = item["risco"] # Em pontos percentuais (ex: 30, 35, 40)
93
+ area = item["area"]
94
+
95
+ # Aplicar ajuste mantendo risco entre 5 e 95 pontos percentuais
96
+ novo_risco = min(95, max(5, risco_original + ajuste_risco))
97
+
98
+ # Salvar valores
99
+ item["risco_original"] = round(risco_original, 1)
100
+ item["risco"] = round(novo_risco, 1)
101
+ item["ajuste_aplicado"] = round(ajuste_risco, 1)
102
+
103
+ # Acumular para recalcular risco médio
104
+ soma_risco_area += novo_risco * area
105
+ soma_area += area
106
+
107
+ # Recalcular risco médio ponderado por área
108
+ if soma_area > 0:
109
+ novo_risco_medio = soma_risco_area / soma_area
110
+ resultado_ag["risco_medio_original"] = round(risco_medio_original, 1)
111
+ resultado_ag["risco_medio"] = round(novo_risco_medio, 1)
74
112
 
75
- # Aplicar ajuste no plano otimizado
76
- if "plano_otimizado" in resultado_ag:
113
+ # Também suportar formato alternativo (plano_otimizado)
114
+ elif "plano_otimizado" in resultado_ag:
77
115
  for item in resultado_ag["plano_otimizado"]:
78
116
  if "risco" in item:
79
117
  risco_original = item["risco"]
80
- # Aplicar ajuste mantendo risco entre 0.05 e 0.95
81
- novo_risco = min(0.95, max(0.05, risco_original + ajuste_risco))
82
- item["risco"] = round(novo_risco, 3)
83
- item["risco_original"] = risco_original
84
- item["ajuste_aplicado"] = ajuste_risco
85
-
86
- # Recalcular métricas gerais se existirem
87
- if "metricas" in resultado_ag and "risco_medio" in resultado_ag["metricas"]:
88
- risco_original = resultado_ag["metricas"]["risco_medio"]
89
- novo_risco = min(0.95, max(0.05, risco_original + ajuste_risco))
90
- resultado_ag["metricas"]["risco_medio"] = round(novo_risco, 3)
91
- resultado_ag["metricas"]["risco_medio_original"] = risco_original
118
+ novo_risco = min(95, max(5, risco_original + ajuste_risco))
119
+ item["risco_original"] = round(risco_original, 1)
120
+ item["risco"] = round(novo_risco, 1)
121
+ item["ajuste_aplicado"] = round(ajuste_risco, 1)
122
+
123
+ # Recalcular métricas se existirem
124
+ if "metricas" in resultado_ag and "risco_medio" in resultado_ag["metricas"]:
125
+ risco_original = resultado_ag["metricas"]["risco_medio"]
126
+ novo_risco = min(95, max(5, risco_original + ajuste_risco))
127
+ resultado_ag["metricas"]["risco_medio_original"] = round(risco_original, 1)
128
+ resultado_ag["metricas"]["risco_medio"] = round(novo_risco, 1)
92
129
 
93
130
  # Marcar que ajuste foi aplicado
94
131
  resultado_ag["ajuste_climatico_aplicado"] = True
@@ -85,7 +85,7 @@ def get_objetivo_description(objetivo):
85
85
  return descriptions.get(objetivo, "otimizou múltiplos critérios")
86
86
 
87
87
 
88
- def gerar_relatorio_completo(culturas, talhoes, regras, objetivo='equilibrado', formato='md'):
88
+ def gerar_relatorio_completo(culturas, talhoes, regras, objetivo='equilibrado', formato='md', contexto_climatico=None):
89
89
  """
90
90
  Gera relatório completo do sistema
91
91
 
@@ -95,6 +95,7 @@ def gerar_relatorio_completo(culturas, talhoes, regras, objetivo='equilibrado',
95
95
  regras: DataFrame com regras
96
96
  objetivo: objetivo do AG
97
97
  formato: 'md' ou 'txt'
98
+ contexto_climatico: Dicionário com dados climáticos reais (opcional)
98
99
 
99
100
  Returns:
100
101
  Caminho do arquivo gerado
@@ -104,7 +105,13 @@ def gerar_relatorio_completo(culturas, talhoes, regras, objetivo='equilibrado',
104
105
  cenarios = gerar_cenarios(culturas, talhoes, regras)
105
106
 
106
107
  print(" 🧬 Executando Algoritmo Genético...")
107
- resultado_ag = gerar_plano_genetico(culturas, talhoes, regras, objetivo, seed=42)
108
+ # Aplicar contexto climático se disponível
109
+ if contexto_climatico:
110
+ from core.climate_adapter import aplicar_contexto_climatico_no_plano
111
+ resultado_ag = gerar_plano_genetico(culturas, talhoes, regras, objetivo, seed=42)
112
+ resultado_ag = aplicar_contexto_climatico_no_plano(resultado_ag, contexto_climatico)
113
+ else:
114
+ resultado_ag = gerar_plano_genetico(culturas, talhoes, regras, objetivo, seed=42)
108
115
 
109
116
  print(" 🔬 Validando com força bruta...")
110
117
  validacao = comparar_ag_com_forca_bruta(culturas, talhoes, regras, objetivo, seed=42)
@@ -126,6 +133,11 @@ def gerar_relatorio_completo(culturas, talhoes, regras, objetivo='equilibrado',
126
133
  )
127
134
  extensao = 'txt'
128
135
 
136
+ # Adicionar seção climática se disponível
137
+ if contexto_climatico:
138
+ secao_clima = gerar_secao_climatica(contexto_climatico, formato)
139
+ conteudo += "\n\n" + secao_clima
140
+
129
141
  # Salva arquivo
130
142
  os.makedirs('reports', exist_ok=True)
131
143
  timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
@@ -138,6 +150,74 @@ def gerar_relatorio_completo(culturas, talhoes, regras, objetivo='equilibrado',
138
150
  return caminho
139
151
 
140
152
 
153
+ def gerar_secao_climatica(contexto_climatico, formato='md'):
154
+ """Gera seção de dados climáticos para o relatório"""
155
+ if not contexto_climatico or contexto_climatico.get("fallback", True):
156
+ if formato == 'md':
157
+ return "## Dados Climáticos\n\nEste relatório foi gerado com base nos dados internos simulados."
158
+ else:
159
+ return "DADOS CLIMÁTICOS\n\nEste relatório foi gerado com base nos dados internos simulados."
160
+
161
+ if formato == 'md':
162
+ secao = "## Dados Climáticos Reais Utilizados\n\n"
163
+ secao += f"**Fonte:** {contexto_climatico.get('fonte', 'N/A')}\n\n"
164
+
165
+ if contexto_climatico.get('temperatura_media'):
166
+ secao += f"**Temperatura Média:** {contexto_climatico['temperatura_media']:.1f}°C\n\n"
167
+ if contexto_climatico.get('temperatura_maxima'):
168
+ secao += f"**Temperatura Máxima:** {contexto_climatico['temperatura_maxima']:.1f}°C\n\n"
169
+ if contexto_climatico.get('temperatura_minima'):
170
+ secao += f"**Temperatura Mínima:** {contexto_climatico['temperatura_minima']:.1f}°C\n\n"
171
+ if contexto_climatico.get('precipitacao_total'):
172
+ secao += f"**Precipitação Total (30 dias):** {contexto_climatico['precipitacao_total']:.1f}mm\n\n"
173
+ if contexto_climatico.get('risco_climatico_estimado'):
174
+ secao += f"**Risco Climático Estimado:** {contexto_climatico['risco_climatico_estimado']}\n\n"
175
+ if contexto_climatico.get('clima_observado'):
176
+ secao += f"**Clima Observado:** {contexto_climatico['clima_observado']}\n\n"
177
+ if contexto_climatico.get('agua_observada'):
178
+ secao += f"**Disponibilidade Hídrica:** {contexto_climatico['agua_observada']}\n\n"
179
+ if contexto_climatico.get('ajuste_risco') is not None:
180
+ ajuste = contexto_climatico['ajuste_risco']
181
+ sinal = '+' if ajuste > 0 else ''
182
+ secao += f"**Ajuste de Risco Aplicado:** {sinal}{ajuste} pontos percentuais\n\n"
183
+
184
+ secao += "**Impacto no Planejamento:**\n\n"
185
+ secao += "Os dados climáticos reais foram integrados ao algoritmo de otimização, "
186
+ secao += "ajustando automaticamente os níveis de risco de cada cultura com base nas "
187
+ secao += "condições meteorológicas observadas. Este ajuste proporciona maior precisão "
188
+ secao += "nas recomendações de plantio.\n"
189
+ else:
190
+ secao = "DADOS CLIMÁTICOS REAIS UTILIZADOS\n\n"
191
+ secao += f"Fonte: {contexto_climatico.get('fonte', 'N/A')}\n\n"
192
+
193
+ if contexto_climatico.get('temperatura_media'):
194
+ secao += f"Temperatura Média: {contexto_climatico['temperatura_media']:.1f}°C\n"
195
+ if contexto_climatico.get('temperatura_maxima'):
196
+ secao += f"Temperatura Máxima: {contexto_climatico['temperatura_maxima']:.1f}°C\n"
197
+ if contexto_climatico.get('temperatura_minima'):
198
+ secao += f"Temperatura Mínima: {contexto_climatico['temperatura_minima']:.1f}°C\n"
199
+ if contexto_climatico.get('precipitacao_total'):
200
+ secao += f"Precipitação Total (30 dias): {contexto_climatico['precipitacao_total']:.1f}mm\n"
201
+ if contexto_climatico.get('risco_climatico_estimado'):
202
+ secao += f"Risco Climático Estimado: {contexto_climatico['risco_climatico_estimado']}\n"
203
+ if contexto_climatico.get('clima_observado'):
204
+ secao += f"Clima Observado: {contexto_climatico['clima_observado']}\n"
205
+ if contexto_climatico.get('agua_observada'):
206
+ secao += f"Disponibilidade Hídrica: {contexto_climatico['agua_observada']}\n"
207
+ if contexto_climatico.get('ajuste_risco') is not None:
208
+ ajuste = contexto_climatico['ajuste_risco']
209
+ sinal = '+' if ajuste > 0 else ''
210
+ secao += f"Ajuste de Risco Aplicado: {sinal}{ajuste} pontos percentuais\n"
211
+
212
+ secao += "\nImpacto no Planejamento:\n\n"
213
+ secao += "Os dados climáticos reais foram integrados ao algoritmo de otimização, "
214
+ secao += "ajustando automaticamente os níveis de risco de cada cultura com base nas "
215
+ secao += "condições meteorológicas observadas. Este ajuste proporciona maior precisão "
216
+ secao += "nas recomendações de plantio.\n"
217
+
218
+ return secao
219
+
220
+
141
221
  def gerar_relatorio_markdown(culturas, talhoes, regras, objetivo, cenarios, resultado_ag, validacao, estabilidade):
142
222
  """Gera relatório em formato Markdown"""
143
223
 
@@ -417,8 +497,8 @@ def gerar_relatorio_markdown(culturas, talhoes, regras, objetivo, cenarios, resu
417
497
  md.append("1. **Dados Simulados:** Os dados atuais são estimativas, não medições reais de campo")
418
498
  md.append("2. **Modelo Simplificado:** Não considera todos os fatores agronômicos (pragas, doenças, mercado local)")
419
499
  md.append("3. **Sem Análise Laboratorial:** Não utiliza análise química e física do solo")
420
- md.append("4. **Sem Dados Climáticos Reais:** Não integra com estações meteorológicas ou previsões")
421
- md.append("5. **Sem Preços de Mercado Reais:** Usa valores estimados, não cotações atuais")
500
+ md.append("4. **Dados Climáticos Regionais:** O sistema utiliza dados climáticos reais via Open-Meteo, mas ainda trabalha em escala regional, sem análise individualizada por polígono/talhão real")
501
+ md.append("5. **Sem Preços de Mercado Reais:** Ainda utiliza preços simulados, sem integração com cotações oficiais como Conab/CEPEA")
422
502
  md.append("6. **Não Substitui Agrônomo:** As recomendações devem ser validadas por profissional qualificado")
423
503
  md.append("")
424
504
  md.append("**Recomendação:** Use este sistema como ferramenta de apoio à decisão, não como decisão final.")
@@ -429,25 +509,19 @@ def gerar_relatorio_markdown(culturas, talhoes, regras, objetivo, cenarios, resu
429
509
  # 9. Próximas Evoluções
430
510
  md.append("## 9. 🚀 Próximas Evoluções")
431
511
  md.append("")
432
- md.append("### Fase 5 - Interface Web")
433
- md.append("- Dashboard interativo")
434
- md.append("- Visualização de mapas")
435
- md.append("- Gráficos de evolução")
436
- md.append("- Comparação visual de cenários")
437
- md.append("")
438
- md.append("### Fase 6 - Integração com APIs Reais")
439
- md.append("- Dados climáticos (INMET, OpenWeather)")
440
- md.append("- Preços de mercado (CEPEA, CONAB)")
441
- md.append("- Análise de solo (laboratórios)")
442
- md.append("- Imagens de satélite")
512
+ md.append("### Próximas Integrações de Dados Reais")
513
+ md.append("- **ZARC:** Zoneamento Agrícola de Risco Climático (dados oficiais MAPA)")
514
+ md.append("- **Preços Agrícolas:** Integração com Conab/CEPEA para cotações oficiais")
515
+ md.append("- **Dados Agroclimáticos:** NASA POWER para radiação solar e evapotranspiração")
516
+ md.append("- **Análise Geográfica:** Análise individualizada por propriedade/talhão em fase futura")
443
517
  md.append("")
444
- md.append("### Fase 7 - Machine Learning")
518
+ md.append("### Machine Learning")
445
519
  md.append("- Previsão de produtividade")
446
520
  md.append("- Previsão de preços")
447
521
  md.append("- Detecção de anomalias")
448
522
  md.append("- Recomendação personalizada")
449
523
  md.append("")
450
- md.append("### Fase 8 - Sistema Completo")
524
+ md.append("### Sistema Completo")
451
525
  md.append("- Cadastro de propriedades")
452
526
  md.append("- Gestão de usuários")
453
527
  md.append("- Histórico de safras")
@@ -739,8 +813,11 @@ def gerar_relatorio_txt(culturas, talhoes, regras, objetivo, cenarios, resultado
739
813
  txt.append("1. Dados Simulados: Os dados atuais são estimativas, não medições reais")
740
814
  txt.append("2. Modelo Simplificado: Não considera todos os fatores agronômicos")
741
815
  txt.append("3. Sem Análise Laboratorial: Não utiliza análise química e física do solo")
742
- txt.append("4. Sem Dados Climáticos Reais: Não integra com estações meteorológicas")
743
- txt.append("5. Sem Preços de Mercado Reais: Usa valores estimados")
816
+ txt.append("4. Dados Climáticos Regionais: O sistema utiliza dados climáticos reais")
817
+ txt.append(" via Open-Meteo, mas ainda trabalha em escala regional, sem análise")
818
+ txt.append(" individualizada por polígono/talhão real")
819
+ txt.append("5. Sem Preços de Mercado Reais: Ainda utiliza preços simulados, sem")
820
+ txt.append(" integração com cotações oficiais como Conab/CEPEA")
744
821
  txt.append("6. Não Substitui Agrônomo: As recomendações devem ser validadas")
745
822
  txt.append("")
746
823
  txt.append("RECOMENDAÇÃO: Use este sistema como ferramenta de apoio à decisão,")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agroplan-ai-cli",
3
- "version": "1.0.10",
3
+ "version": "1.0.12",
4
4
  "description": "CLI global para AgroPlan AI - modo local acelerado",
5
5
  "type": "module",
6
6
  "bin": {