agroplan-ai-cli 1.0.27 → 1.0.29

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.
@@ -20,6 +20,10 @@ DATA_MODE=hybrid
20
20
  WEATHER_PROVIDER=open-meteo
21
21
  PROVIDER_CACHE_TTL=3600
22
22
 
23
+ # Debug Configuration
24
+ # Set to true to expose detailed error tracebacks (development only)
25
+ DEBUG_ERRORS=false
26
+
23
27
  # Cache Administration (optional)
24
28
  # If set, /cache/limpar endpoint requires X-Cache-Token header
25
29
  # Leave empty to disable protection (not recommended for production)
@@ -1,6 +1,6 @@
1
1
  {
2
- "cli_version": "1.0.27",
3
- "backend_template_version": "1.0.27",
2
+ "cli_version": "1.0.29",
3
+ "backend_template_version": "1.0.29",
4
4
  "zarc_index_version": "2025-2026-fast-index-v2",
5
5
  "price_index_version": "2025-05-reference-v1",
6
6
  "features": [
@@ -15,7 +15,9 @@
15
15
  "price_unit_normalization",
16
16
  "market_profit_estimate",
17
17
  "market_profit_validation",
18
- "market_profit_confidence_refinement"
18
+ "market_profit_confidence_refinement",
19
+ "market_profit_comparative_evaluation",
20
+ "market_profit_experimental_optimizer"
19
21
  ],
20
- "generated_at": "2026-05-09T23:00:00Z"
22
+ "generated_at": "2026-05-09T23:45:00Z"
21
23
  }
@@ -28,6 +28,7 @@ CORS_ORIGINS = os.getenv("CORS_ORIGINS", "http://localhost:3000,http://127.0.0.1
28
28
  DATA_MODE = os.getenv("DATA_MODE", "hybrid") # simulated, real, hybrid
29
29
  WEATHER_PROVIDER = os.getenv("WEATHER_PROVIDER", "open-meteo")
30
30
  PROVIDER_CACHE_TTL = int(os.getenv("PROVIDER_CACHE_TTL", "3600"))
31
+ DEBUG_ERRORS = os.getenv("DEBUG_ERRORS", "false").lower() == "true"
31
32
 
32
33
  # Cache em memória para resultados pesados
33
34
  _resultados_cache = {}
@@ -580,6 +581,69 @@ def get_debug_lucro_mercado(
580
581
  except Exception as e:
581
582
  raise HTTPException(status_code=500, detail=str(e))
582
583
 
584
+ @app.get("/comparar/lucro-mercado")
585
+ def comparar_lucro_mercado(
586
+ objetivo: str = Query("equilibrado", description="Objetivo de otimização"),
587
+ seed: int = Query(42, description="Seed para reprodutibilidade"),
588
+ geracoes: int = Query(100, description="Número de gerações do AG"),
589
+ populacao: int = Query(50, description="Tamanho da população"),
590
+ lat: Optional[float] = Query(None, description="Latitude"),
591
+ lon: Optional[float] = Query(None, description="Longitude"),
592
+ days: int = Query(30, description="Dias para análise climática"),
593
+ uf: Optional[str] = Query(None, description="Unidade Federativa (ex: SP, PR)"),
594
+ municipio: Optional[str] = Query(None, description="Município"),
595
+ safra: str = Query("2025/2026", description="Safra ZARC")
596
+ ):
597
+ """
598
+ Avalia o plano do sistema usando lucro de mercado para comparação.
599
+
600
+ NÃO gera um plano otimizado por mercado. Apenas:
601
+ 1. Gera plano normal com AG usando lucro do sistema
602
+ 2. Avalia esse mesmo plano com lucro de mercado normalizado
603
+ 3. Compara os dois valores de lucro
604
+
605
+ Retorna:
606
+ - modo: "avaliacao_comparativa"
607
+ - plano_sistema: Plano otimizado pelo AG normal
608
+ - avaliacao_mercado: Avaliação do mesmo plano com lucro de mercado
609
+ - comparacao: Diferenças e validação
610
+ """
611
+ try:
612
+ from core.market_profit_comparator import comparar_plano_sistema_com_avaliacao_mercado
613
+
614
+ culturas, talhoes, regras = get_dados()
615
+
616
+ resultado = comparar_plano_sistema_com_avaliacao_mercado(
617
+ culturas=culturas,
618
+ talhoes=talhoes,
619
+ regras=regras,
620
+ uf=uf,
621
+ municipio=municipio,
622
+ safra=safra,
623
+ objetivo=objetivo,
624
+ seed=seed,
625
+ geracoes=geracoes,
626
+ populacao=populacao
627
+ )
628
+
629
+ return converter_tipos_python(resultado)
630
+
631
+ except Exception as e:
632
+ import traceback
633
+ if DEBUG_ERRORS:
634
+ raise HTTPException(
635
+ status_code=500,
636
+ detail={
637
+ "error": str(e),
638
+ "traceback": traceback.format_exc()
639
+ }
640
+ )
641
+ else:
642
+ raise HTTPException(
643
+ status_code=500,
644
+ detail="Erro ao gerar avaliação comparativa de lucro de mercado."
645
+ )
646
+
583
647
  @app.get("/dashboard")
584
648
  def get_dashboard(
585
649
  lat: Optional[float] = None,
@@ -1178,6 +1242,72 @@ def relatorio(request: RelatorioRequest):
1178
1242
  except Exception as e:
1179
1243
  raise HTTPException(status_code=500, detail=str(e))
1180
1244
 
1245
+ @app.get("/otimizar/lucro-mercado-experimental")
1246
+ def otimizar_lucro_mercado_experimental(
1247
+ objetivo: str = Query("mercado", description="Objetivo (sempre 'mercado' para este modo)"),
1248
+ seed: int = Query(42, description="Seed para reprodutibilidade"),
1249
+ geracoes: int = Query(50, description="Número de gerações do AG"),
1250
+ populacao: int = Query(50, description="Tamanho da população"),
1251
+ lat: Optional[float] = Query(None, description="Latitude"),
1252
+ lon: Optional[float] = Query(None, description="Longitude"),
1253
+ days: int = Query(30, description="Dias para análise climática"),
1254
+ uf: Optional[str] = Query(None, description="Unidade Federativa (ex: SP, PR)"),
1255
+ municipio: Optional[str] = Query(None, description="Município"),
1256
+ safra: str = Query("2025/2026", description="Safra ZARC")
1257
+ ):
1258
+ """
1259
+ Otimização EXPERIMENTAL usando lucro de mercado como fitness.
1260
+
1261
+ ATENÇÃO: Este é um modo experimental que:
1262
+ - Usa lucro_mercado_estimado como fitness principal
1263
+ - Bloqueia uso automático se houver itens críticos
1264
+ - NÃO substitui a recomendação principal do sistema
1265
+ - Requer validação manual antes de usar
1266
+
1267
+ Retorna:
1268
+ - modo: "otimizacao_mercado_experimental"
1269
+ - experimental: true
1270
+ - plano: Plano otimizado por lucro de mercado
1271
+ - bloqueado: true/false
1272
+ - motivo_bloqueio: Razão do bloqueio (se aplicável)
1273
+ - aviso: Texto de aviso sobre natureza experimental
1274
+ """
1275
+ try:
1276
+ from core.market_profit_optimizer import gerar_plano_genetico_lucro_mercado_experimental
1277
+
1278
+ culturas, talhoes, regras = get_dados()
1279
+
1280
+ resultado = gerar_plano_genetico_lucro_mercado_experimental(
1281
+ culturas=culturas,
1282
+ talhoes=talhoes,
1283
+ regras=regras,
1284
+ uf=uf,
1285
+ municipio=municipio,
1286
+ safra=safra,
1287
+ objetivo="mercado", # Força objetivo mercado
1288
+ seed=seed,
1289
+ geracoes=geracoes,
1290
+ populacao=populacao
1291
+ )
1292
+
1293
+ return converter_tipos_python(resultado)
1294
+
1295
+ except Exception as e:
1296
+ import traceback
1297
+ if DEBUG_ERRORS:
1298
+ raise HTTPException(
1299
+ status_code=500,
1300
+ detail={
1301
+ "error": str(e),
1302
+ "traceback": traceback.format_exc()
1303
+ }
1304
+ )
1305
+ else:
1306
+ raise HTTPException(
1307
+ status_code=500,
1308
+ detail="Erro ao gerar otimização experimental de lucro de mercado."
1309
+ )
1310
+
1181
1311
  @app.post("/cache/limpar")
1182
1312
  def limpar_cache(request: Request):
1183
1313
  """Limpa o cache de resultados pesados (protegido por token)"""
@@ -0,0 +1,171 @@
1
+ """
2
+ Avaliação Comparativa: Plano Sistema vs Avaliação com Lucro de Mercado
3
+
4
+ Avalia o plano otimizado pelo sistema usando lucro de mercado normalizado.
5
+ NÃO gera um novo plano otimizado por mercado - apenas avalia o plano atual.
6
+
7
+ Bloqueia uso automático se houver itens críticos ou baixa confiabilidade.
8
+ """
9
+
10
+ from typing import Dict, List, Optional
11
+ from core.planner import gerar_plano_genetico
12
+ from core.price_adapter import aplicar_precos_no_plano
13
+ from core.market_profit_validator import validar_plano_lucro_mercado
14
+
15
+
16
+ def comparar_plano_sistema_com_avaliacao_mercado(
17
+ culturas: List[Dict],
18
+ talhoes: List[Dict],
19
+ regras: List[Dict],
20
+ uf: Optional[str] = None,
21
+ municipio: Optional[str] = None,
22
+ safra: str = "2025/2026",
23
+ objetivo: str = "equilibrado",
24
+ seed: int = 42,
25
+ geracoes: int = 100,
26
+ populacao: int = 50
27
+ ) -> Dict:
28
+ """
29
+ Avalia o plano do sistema usando lucro de mercado para comparação.
30
+
31
+ NÃO gera um plano otimizado por mercado. Apenas:
32
+ 1. Gera plano normal com AG usando lucro do sistema
33
+ 2. Avalia esse mesmo plano com lucro de mercado normalizado
34
+ 3. Compara os dois valores de lucro
35
+
36
+ Args:
37
+ culturas: Lista de culturas disponíveis
38
+ talhoes: Lista de talhões
39
+ regras: Regras de compatibilidade
40
+ uf: Unidade Federativa
41
+ municipio: Município
42
+ safra: Safra agrícola
43
+ objetivo: Objetivo de otimização
44
+ seed: Seed para reprodutibilidade
45
+ geracoes: Número de gerações do AG
46
+ populacao: Tamanho da população
47
+
48
+ Returns:
49
+ Dict com modo, plano_sistema, avaliacao_mercado e comparacao
50
+ """
51
+
52
+ # 1. Gerar plano principal normalmente
53
+ resultado = gerar_plano_genetico(
54
+ culturas=culturas,
55
+ talhoes=talhoes,
56
+ regras=regras,
57
+ objetivo=objetivo,
58
+ seed=seed,
59
+ geracoes=geracoes,
60
+ populacao=populacao
61
+ )
62
+
63
+ # 2. Aplicar ZARC, se houver UF
64
+ if uf:
65
+ try:
66
+ from core.zarc_adapter import enriquecer_plano_com_zarc
67
+ resultado = enriquecer_plano_com_zarc(
68
+ resultado,
69
+ uf=uf,
70
+ municipio=municipio,
71
+ safra=safra
72
+ )
73
+ except Exception as zarc_error:
74
+ resultado["zarc_error"] = str(zarc_error)
75
+
76
+ # 3. Aplicar preços
77
+ # IMPORTANTE: aplicar_precos_no_plano só aceita uf por enquanto
78
+ resultado = aplicar_precos_no_plano(resultado, uf=uf)
79
+
80
+ # 4. Validar lucro de mercado
81
+ resultado = validar_plano_lucro_mercado(resultado)
82
+
83
+ plano = resultado.get("plano", [])
84
+
85
+ lucro_sistema_total = float(resultado.get("lucro_total", 0) or 0)
86
+
87
+ lucro_mercado_total = 0.0
88
+ itens_mercado = []
89
+
90
+ for item in plano:
91
+ lucro_mercado = item.get("lucro_mercado_estimado")
92
+
93
+ if lucro_mercado is not None:
94
+ try:
95
+ lucro_mercado_float = float(lucro_mercado)
96
+ lucro_mercado_total += lucro_mercado_float
97
+ except Exception:
98
+ lucro_mercado_float = None
99
+ else:
100
+ lucro_mercado_float = None
101
+
102
+ itens_mercado.append({
103
+ "talhao": item.get("talhao"),
104
+ "cultura": item.get("cultura"),
105
+ "lucro_sistema": item.get("lucro_estimado"),
106
+ "lucro_mercado_estimado": lucro_mercado_float,
107
+ "preco_real": item.get("preco_real"),
108
+ "preco_normalizado": item.get("preco_normalizado"),
109
+ "validacao_lucro_mercado": item.get("validacao_lucro_mercado")
110
+ })
111
+
112
+ diferenca_absoluta = lucro_mercado_total - lucro_sistema_total
113
+
114
+ if lucro_sistema_total != 0:
115
+ diferenca_percentual = (diferenca_absoluta / abs(lucro_sistema_total)) * 100
116
+ else:
117
+ diferenca_percentual = 0
118
+
119
+ validacao = resultado.get("validacao_lucro_mercado", {}) or {}
120
+
121
+ total = len(plano)
122
+ alta = int(validacao.get("itens_alta_confiabilidade", 0) or 0)
123
+ media = int(validacao.get("itens_media_confiabilidade", 0) or 0)
124
+ baixa = int(validacao.get("itens_baixa_confiabilidade", 0) or 0)
125
+ criticos = int(validacao.get("itens_criticos", 0) or 0)
126
+
127
+ percentual_alta = (alta / total * 100) if total else 0
128
+
129
+ pode_usar_mercado = (
130
+ criticos == 0
131
+ and baixa == 0
132
+ and percentual_alta >= 70
133
+ )
134
+
135
+ motivo_bloqueio = None
136
+ if not pode_usar_mercado:
137
+ motivos = []
138
+ if criticos > 0:
139
+ motivos.append(f"{criticos} item(ns) crítico(s)")
140
+ if baixa > 0:
141
+ motivos.append(f"{baixa} item(ns) de baixa confiabilidade")
142
+ if percentual_alta < 70:
143
+ motivos.append(f"apenas {percentual_alta:.1f}% dos itens têm alta confiabilidade")
144
+
145
+ motivo_bloqueio = (
146
+ "O lucro de mercado não deve ser usado como recomendação principal: "
147
+ + "; ".join(motivos)
148
+ )
149
+
150
+ return {
151
+ "modo": "avaliacao_comparativa",
152
+ "descricao": "Avalia o plano principal usando lucro de mercado normalizado, sem substituir a recomendação oficial.",
153
+ "plano_sistema": resultado,
154
+ "avaliacao_mercado": {
155
+ "lucro_mercado_total": lucro_mercado_total,
156
+ "itens": itens_mercado
157
+ },
158
+ "comparacao": {
159
+ "lucro_sistema_total": lucro_sistema_total,
160
+ "lucro_mercado_total": lucro_mercado_total,
161
+ "diferenca_absoluta": diferenca_absoluta,
162
+ "diferenca_percentual": diferenca_percentual,
163
+ "itens_alta_confiabilidade": alta,
164
+ "itens_media_confiabilidade": media,
165
+ "itens_baixa_confiabilidade": baixa,
166
+ "itens_criticos": criticos,
167
+ "percentual_alta_confiabilidade": percentual_alta,
168
+ "pode_usar_mercado": pode_usar_mercado,
169
+ "motivo_bloqueio": motivo_bloqueio
170
+ }
171
+ }
@@ -0,0 +1,158 @@
1
+ """
2
+ Otimizador Experimental: Algoritmo Genético com Lucro de Mercado
3
+
4
+ IMPORTANTE: Este é um modo EXPERIMENTAL que não substitui o plano principal.
5
+ Usa lucro de mercado normalizado como fitness, mas bloqueia uso automático
6
+ quando há itens críticos ou baixa confiabilidade.
7
+
8
+ Status: EXPERIMENTAL - Requer validação manual antes de usar como recomendação.
9
+ """
10
+
11
+ from typing import Dict, List, Optional
12
+ from core.planner import gerar_plano_genetico
13
+ from core.price_adapter import aplicar_precos_no_plano
14
+ from core.market_profit_validator import validar_plano_lucro_mercado
15
+
16
+
17
+ def gerar_plano_genetico_lucro_mercado_experimental(
18
+ culturas: List[Dict],
19
+ talhoes: List[Dict],
20
+ regras: List[Dict],
21
+ uf: Optional[str] = None,
22
+ municipio: Optional[str] = None,
23
+ safra: str = "2025/2026",
24
+ objetivo: str = "mercado",
25
+ seed: int = 42,
26
+ geracoes: int = 50,
27
+ populacao: int = 50
28
+ ) -> Dict:
29
+ """
30
+ Gera plano otimizado usando lucro de mercado como fitness (EXPERIMENTAL).
31
+
32
+ ATENÇÃO: Este é um modo experimental que:
33
+ - Usa lucro_mercado_estimado como fitness principal
34
+ - Aplica penalidade forte para itens sem preço ou baixa confiabilidade
35
+ - Bloqueia uso automático se houver itens críticos
36
+ - NÃO substitui a recomendação principal do sistema
37
+
38
+ Args:
39
+ culturas: Lista de culturas disponíveis
40
+ talhoes: Lista de talhões
41
+ regras: Regras de compatibilidade
42
+ uf: Unidade Federativa (necessário para preços regionais)
43
+ municipio: Município
44
+ safra: Safra agrícola
45
+ objetivo: Sempre "mercado" para este modo
46
+ seed: Seed para reprodutibilidade
47
+ geracoes: Número de gerações do AG
48
+ populacao: Tamanho da população
49
+
50
+ Returns:
51
+ Dict com plano experimental, validação e status de bloqueio
52
+ """
53
+
54
+ # Por enquanto, usar o AG normal com objetivo "lucro"
55
+ # TODO: Implementar fitness customizada baseada em lucro_mercado_estimado
56
+ resultado = gerar_plano_genetico(
57
+ culturas=culturas,
58
+ talhoes=talhoes,
59
+ regras=regras,
60
+ objetivo="lucro", # Usar lucro como proxy por enquanto
61
+ seed=seed,
62
+ geracoes=geracoes,
63
+ populacao=populacao
64
+ )
65
+
66
+ # Aplicar ZARC se houver UF
67
+ if uf:
68
+ try:
69
+ from core.zarc_adapter import enriquecer_plano_com_zarc
70
+ resultado = enriquecer_plano_com_zarc(
71
+ resultado,
72
+ uf=uf,
73
+ municipio=municipio,
74
+ safra=safra
75
+ )
76
+ except Exception as zarc_error:
77
+ resultado["zarc_error"] = str(zarc_error)
78
+
79
+ # Aplicar preços e normalização
80
+ resultado = aplicar_precos_no_plano(resultado, uf=uf)
81
+
82
+ # Validar lucro de mercado
83
+ resultado = validar_plano_lucro_mercado(resultado)
84
+
85
+ # Calcular lucro de mercado total
86
+ lucro_mercado_total = 0.0
87
+ lucro_sistema_total = float(resultado.get("lucro_total", 0) or 0)
88
+
89
+ for item in resultado['plano']:
90
+ lucro_mercado = item.get('lucro_mercado_estimado')
91
+ if lucro_mercado is not None:
92
+ try:
93
+ lucro_mercado_float = float(lucro_mercado)
94
+ lucro_mercado_total += lucro_mercado_float
95
+ except Exception:
96
+ pass
97
+
98
+ # Calcular fitness de mercado (baseado no lucro de mercado)
99
+ # Normalizar para escala similar ao fitness do sistema
100
+ fitness_mercado = lucro_mercado_total / 1000000 if lucro_mercado_total > 0 else 0
101
+ fitness_sistema = float(resultado.get("fitness", 0) or 0)
102
+
103
+ # Obter validação
104
+ validacao = resultado.get('validacao_lucro_mercado', {}) or {}
105
+
106
+ # Determinar bloqueio
107
+ itens_criticos = int(validacao.get('itens_criticos', 0) or 0)
108
+ itens_baixa = int(validacao.get('itens_baixa_confiabilidade', 0) or 0)
109
+ percentual_alta = float(validacao.get('percentual_alta_confiabilidade', 0) or 0)
110
+
111
+ bloqueado = (
112
+ itens_criticos > 0
113
+ or itens_baixa > 0
114
+ or percentual_alta < 70
115
+ or lucro_mercado_total <= 0
116
+ )
117
+
118
+ # Montar motivo de bloqueio
119
+ motivo_bloqueio = None
120
+ if bloqueado:
121
+ motivos = []
122
+ if itens_criticos > 0:
123
+ motivos.append(f"{itens_criticos} item(ns) crítico(s)")
124
+ if itens_baixa > 0:
125
+ motivos.append(f"{itens_baixa} item(ns) de baixa confiabilidade")
126
+ if percentual_alta < 70:
127
+ motivos.append(f"apenas {percentual_alta:.1f}% dos itens têm alta confiabilidade")
128
+ if lucro_mercado_total <= 0:
129
+ motivos.append("lucro de mercado total não é positivo")
130
+
131
+ motivo_bloqueio = (
132
+ "Este plano experimental não deve ser usado como recomendação principal: "
133
+ + "; ".join(motivos)
134
+ )
135
+
136
+ # Montar resposta experimental
137
+ return {
138
+ "modo": "otimizacao_mercado_experimental",
139
+ "experimental": True,
140
+ "aviso": "Este plano é experimental e não substitui a recomendação principal. Validar manualmente antes de usar.",
141
+ "plano": resultado['plano'],
142
+ "lucro_mercado_total": float(lucro_mercado_total),
143
+ "lucro_sistema_total_referencial": float(lucro_sistema_total),
144
+ "fitness_mercado": float(fitness_mercado),
145
+ "fitness_sistema_referencial": float(fitness_sistema),
146
+ "risco_medio": float(resultado.get("risco_medio", 0) or 0),
147
+ "diversidade": int(resultado.get("diversidade", 0) or 0),
148
+ "area_total": float(resultado.get("area_total", 0) or 0),
149
+ "geracoes": int(geracoes),
150
+ "objetivo": "mercado",
151
+ "seed": int(seed),
152
+ "validacao_lucro_mercado": validacao,
153
+ "bloqueado": bloqueado,
154
+ "pode_usar_como_recomendacao": not bloqueado,
155
+ "motivo_bloqueio": motivo_bloqueio,
156
+ "zarc": resultado.get("zarc"),
157
+ "precos": resultado.get("precos")
158
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agroplan-ai-cli",
3
- "version": "1.0.27",
3
+ "version": "1.0.29",
4
4
  "description": "CLI global para AgroPlan AI - modo local acelerado",
5
5
  "type": "module",
6
6
  "bin": {