agroplan-ai-cli 1.0.28 → 1.0.30

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.28",
3
- "backend_template_version": "1.0.28",
2
+ "cli_version": "1.0.30",
3
+ "backend_template_version": "1.0.30",
4
4
  "zarc_index_version": "2025-2026-fast-index-v2",
5
5
  "price_index_version": "2025-05-reference-v1",
6
6
  "features": [
@@ -16,7 +16,9 @@
16
16
  "market_profit_estimate",
17
17
  "market_profit_validation",
18
18
  "market_profit_confidence_refinement",
19
- "market_profit_comparative_evaluation"
19
+ "market_profit_comparative_evaluation",
20
+ "market_profit_experimental_optimizer",
21
+ "smart_crop_calendar_engine"
20
22
  ],
21
- "generated_at": "2026-05-09T23:30:00Z"
23
+ "generated_at": "2026-05-10T15:30:00Z"
22
24
  }
@@ -8,6 +8,7 @@ from pydantic import BaseModel
8
8
  from typing import Optional
9
9
  import sys
10
10
  import os
11
+ import uuid
11
12
 
12
13
  # Adiciona o diretório backend ao path para imports
13
14
  sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
@@ -1242,6 +1243,180 @@ def relatorio(request: RelatorioRequest):
1242
1243
  except Exception as e:
1243
1244
  raise HTTPException(status_code=500, detail=str(e))
1244
1245
 
1246
+ @app.get("/otimizar/lucro-mercado-experimental")
1247
+ def otimizar_lucro_mercado_experimental(
1248
+ objetivo: str = Query("mercado", description="Objetivo (sempre 'mercado' para este modo)"),
1249
+ seed: int = Query(42, description="Seed para reprodutibilidade"),
1250
+ geracoes: int = Query(50, description="Número de gerações do AG"),
1251
+ populacao: int = Query(50, description="Tamanho da população"),
1252
+ lat: Optional[float] = Query(None, description="Latitude"),
1253
+ lon: Optional[float] = Query(None, description="Longitude"),
1254
+ days: int = Query(30, description="Dias para análise climática"),
1255
+ uf: Optional[str] = Query(None, description="Unidade Federativa (ex: SP, PR)"),
1256
+ municipio: Optional[str] = Query(None, description="Município"),
1257
+ safra: str = Query("2025/2026", description="Safra ZARC")
1258
+ ):
1259
+ """
1260
+ Otimização EXPERIMENTAL usando lucro de mercado como fitness.
1261
+
1262
+ ATENÇÃO: Este é um modo experimental que:
1263
+ - Usa lucro_mercado_estimado como fitness principal
1264
+ - Bloqueia uso automático se houver itens críticos
1265
+ - NÃO substitui a recomendação principal do sistema
1266
+ - Requer validação manual antes de usar
1267
+
1268
+ Retorna:
1269
+ - modo: "otimizacao_mercado_experimental"
1270
+ - experimental: true
1271
+ - plano: Plano otimizado por lucro de mercado
1272
+ - bloqueado: true/false
1273
+ - motivo_bloqueio: Razão do bloqueio (se aplicável)
1274
+ - aviso: Texto de aviso sobre natureza experimental
1275
+ """
1276
+ try:
1277
+ from core.market_profit_optimizer import gerar_plano_genetico_lucro_mercado_experimental
1278
+
1279
+ culturas, talhoes, regras = get_dados()
1280
+
1281
+ resultado = gerar_plano_genetico_lucro_mercado_experimental(
1282
+ culturas=culturas,
1283
+ talhoes=talhoes,
1284
+ regras=regras,
1285
+ uf=uf,
1286
+ municipio=municipio,
1287
+ safra=safra,
1288
+ objetivo="mercado", # Força objetivo mercado
1289
+ seed=seed,
1290
+ geracoes=geracoes,
1291
+ populacao=populacao
1292
+ )
1293
+
1294
+ return converter_tipos_python(resultado)
1295
+
1296
+ except Exception as e:
1297
+ import traceback
1298
+ if DEBUG_ERRORS:
1299
+ raise HTTPException(
1300
+ status_code=500,
1301
+ detail={
1302
+ "error": str(e),
1303
+ "traceback": traceback.format_exc()
1304
+ }
1305
+ )
1306
+ else:
1307
+ raise HTTPException(
1308
+ status_code=500,
1309
+ detail="Erro ao gerar otimização experimental de lucro de mercado."
1310
+ )
1311
+
1312
+ @app.post("/planejamento/calendario")
1313
+ def gerar_calendario(request: dict):
1314
+ """
1315
+ Gera calendário agrícola para uma cultura.
1316
+
1317
+ Fase 10.1: Base local para soja, milho e feijão
1318
+ Fase futura: Integração com clima real e replanejamento
1319
+ """
1320
+ try:
1321
+ from core.crop_calendar_engine import gerar_calendario_cultura
1322
+ from core.planning_models import Field, SoilType, Slope, WaterAvailability
1323
+ from datetime import datetime
1324
+
1325
+ # Validar campos obrigatórios
1326
+ if "cultura" not in request:
1327
+ raise HTTPException(status_code=400, detail="Campo 'cultura' é obrigatório")
1328
+ if "planting_date" not in request:
1329
+ raise HTTPException(status_code=400, detail="Campo 'planting_date' é obrigatório")
1330
+ if "field" not in request:
1331
+ raise HTTPException(status_code=400, detail="Campo 'field' é obrigatório")
1332
+
1333
+ # Parsear data de plantio
1334
+ try:
1335
+ planting_date = datetime.fromisoformat(request["planting_date"]).date()
1336
+ except Exception:
1337
+ raise HTTPException(status_code=400, detail="Formato de data inválido. Use ISO 8601 (YYYY-MM-DD)")
1338
+
1339
+ # Criar objeto Field
1340
+ field_data = request["field"]
1341
+ field = Field(
1342
+ id=field_data.get("id", str(uuid.uuid4())),
1343
+ property_id=field_data.get("property_id", "temp"),
1344
+ name=field_data.get("name", "Talhão Temporário"),
1345
+ area_ha=float(field_data.get("area_ha", 10)),
1346
+ soil_type=SoilType(field_data.get("soil_type", "argiloso")),
1347
+ slope=Slope(field_data.get("slope", "plano")),
1348
+ water_availability=WaterAvailability(field_data.get("water_availability", "media"))
1349
+ )
1350
+
1351
+ # Gerar calendário
1352
+ resultado = gerar_calendario_cultura(
1353
+ cultura=request["cultura"],
1354
+ planting_date=planting_date,
1355
+ field=field,
1356
+ crop_plan_id=request.get("crop_plan_id"),
1357
+ weather_context=request.get("weather_context"),
1358
+ zarc_context=request.get("zarc_context")
1359
+ )
1360
+
1361
+ return converter_tipos_python(resultado)
1362
+
1363
+ except HTTPException:
1364
+ raise
1365
+ except Exception as e:
1366
+ import traceback
1367
+ if DEBUG_ERRORS:
1368
+ raise HTTPException(
1369
+ status_code=500,
1370
+ detail={
1371
+ "error": str(e),
1372
+ "traceback": traceback.format_exc()
1373
+ }
1374
+ )
1375
+ else:
1376
+ raise HTTPException(
1377
+ status_code=500,
1378
+ detail="Erro ao gerar calendário agrícola."
1379
+ )
1380
+
1381
+ @app.get("/planejamento/culturas")
1382
+ def listar_culturas():
1383
+ """Lista culturas disponíveis no sistema de planejamento"""
1384
+ try:
1385
+ from core.crop_calendar_engine import get_culturas_disponiveis, get_cultura_info
1386
+
1387
+ culturas = get_culturas_disponiveis()
1388
+
1389
+ return {
1390
+ "total": len(culturas),
1391
+ "culturas": culturas,
1392
+ "detalhes": {
1393
+ cultura: get_cultura_info(cultura)
1394
+ for cultura in culturas
1395
+ }
1396
+ }
1397
+ except Exception as e:
1398
+ raise HTTPException(status_code=500, detail=str(e))
1399
+
1400
+ @app.get("/planejamento/culturas/{cultura}")
1401
+ def obter_cultura_info(cultura: str):
1402
+ """Obtém informações detalhadas de uma cultura"""
1403
+ try:
1404
+ from core.crop_calendar_engine import get_cultura_info
1405
+
1406
+ info = get_cultura_info(cultura)
1407
+
1408
+ if not info:
1409
+ raise HTTPException(
1410
+ status_code=404,
1411
+ detail=f"Cultura '{cultura}' não encontrada"
1412
+ )
1413
+
1414
+ return info
1415
+ except HTTPException:
1416
+ raise
1417
+ except Exception as e:
1418
+ raise HTTPException(status_code=500, detail=str(e))
1419
+
1245
1420
  @app.post("/cache/limpar")
1246
1421
  def limpar_cache(request: Request):
1247
1422
  """Limpa o cache de resultados pesados (protegido por token)"""
@@ -0,0 +1,426 @@
1
+ """
2
+ Engine de Calendário Agrícola
3
+
4
+ Gera calendários de tarefas para culturas baseado em:
5
+ - Ciclo da cultura
6
+ - Data de plantio
7
+ - Características do talhão
8
+ - Contexto climático (opcional)
9
+ - Contexto ZARC (opcional)
10
+
11
+ Fase inicial: Base local para soja, milho e feijão
12
+ Fase futura: Integração com clima real e replanejamento
13
+ """
14
+
15
+ from datetime import date, timedelta
16
+ from typing import Dict, List, Optional
17
+ import uuid
18
+
19
+ from .planning_models import (
20
+ Field, CropCycle, CropPhase, CalendarTask,
21
+ TaskType, TaskPriority, TaskStatus
22
+ )
23
+
24
+
25
+ # Base de Conhecimento Local - Fase 10.1
26
+
27
+ CROP_CYCLES: Dict[str, Dict] = {
28
+ "soja": {
29
+ "cycle_days": 120,
30
+ "phases": [
31
+ {
32
+ "name": "germinacao",
33
+ "days": 10,
34
+ "description": "Emergência das plântulas",
35
+ "critical_water": True,
36
+ "tasks": [
37
+ {"type": "irrigate", "title": "Irrigar se não houver chuva", "priority": "high", "weather_sensitive": True},
38
+ {"type": "inspect_pests", "title": "Inspecionar germinação", "priority": "medium", "weather_sensitive": False}
39
+ ]
40
+ },
41
+ {
42
+ "name": "vegetativa",
43
+ "days": 40,
44
+ "description": "Crescimento vegetativo",
45
+ "critical_water": False,
46
+ "tasks": [
47
+ {"type": "fertilize", "title": "Aplicar fertilizante de cobertura", "priority": "high", "weather_sensitive": True},
48
+ {"type": "inspect_pests", "title": "Inspecionar pragas", "priority": "medium", "weather_sensitive": False},
49
+ {"type": "irrigate", "title": "Irrigar moderadamente", "priority": "medium", "weather_sensitive": True}
50
+ ]
51
+ },
52
+ {
53
+ "name": "florescimento",
54
+ "days": 30,
55
+ "description": "Floração e formação de vagens",
56
+ "critical_water": True,
57
+ "tasks": [
58
+ {"type": "irrigate", "title": "Irrigar - fase crítica", "priority": "critical", "weather_sensitive": True},
59
+ {"type": "inspect_diseases", "title": "Inspecionar doenças", "priority": "high", "weather_sensitive": False},
60
+ {"type": "monitor_growth", "title": "Monitorar temperatura", "priority": "medium", "weather_sensitive": False}
61
+ ]
62
+ },
63
+ {
64
+ "name": "enchimento_graos",
65
+ "days": 30,
66
+ "description": "Enchimento de grãos",
67
+ "critical_water": True,
68
+ "tasks": [
69
+ {"type": "irrigate", "title": "Irrigar - fase crítica", "priority": "critical", "weather_sensitive": True},
70
+ {"type": "monitor_growth", "title": "Monitorar maturação", "priority": "high", "weather_sensitive": False}
71
+ ]
72
+ },
73
+ {
74
+ "name": "maturacao",
75
+ "days": 10,
76
+ "description": "Maturação e secagem",
77
+ "critical_water": False,
78
+ "tasks": [
79
+ {"type": "harvest", "title": "Preparar colheita", "priority": "high", "weather_sensitive": True},
80
+ {"type": "monitor_growth", "title": "Monitorar umidade dos grãos", "priority": "medium", "weather_sensitive": False}
81
+ ]
82
+ }
83
+ ],
84
+ "optimal_temp_min": 20,
85
+ "optimal_temp_max": 30,
86
+ "critical_water_phases": ["germinacao", "florescimento", "enchimento_graos"],
87
+ "harvest_window_days": 15
88
+ },
89
+ "milho": {
90
+ "cycle_days": 140,
91
+ "phases": [
92
+ {
93
+ "name": "germinacao",
94
+ "days": 12,
95
+ "description": "Emergência das plântulas",
96
+ "critical_water": True,
97
+ "tasks": [
98
+ {"type": "irrigate", "title": "Irrigar se não houver chuva", "priority": "high", "weather_sensitive": True},
99
+ {"type": "inspect_pests", "title": "Inspecionar germinação", "priority": "medium", "weather_sensitive": False}
100
+ ]
101
+ },
102
+ {
103
+ "name": "vegetativa",
104
+ "days": 50,
105
+ "description": "Crescimento vegetativo",
106
+ "critical_water": False,
107
+ "tasks": [
108
+ {"type": "fertilize", "title": "Aplicar fertilizante nitrogenado", "priority": "high", "weather_sensitive": True},
109
+ {"type": "inspect_pests", "title": "Inspecionar pragas (lagarta)", "priority": "high", "weather_sensitive": False},
110
+ {"type": "irrigate", "title": "Irrigar moderadamente", "priority": "medium", "weather_sensitive": True}
111
+ ]
112
+ },
113
+ {
114
+ "name": "florescimento",
115
+ "days": 28,
116
+ "description": "Floração e polinização",
117
+ "critical_water": True,
118
+ "tasks": [
119
+ {"type": "irrigate", "title": "Irrigar - fase crítica", "priority": "critical", "weather_sensitive": True},
120
+ {"type": "monitor_growth", "title": "Monitorar polinização", "priority": "high", "weather_sensitive": False}
121
+ ]
122
+ },
123
+ {
124
+ "name": "enchimento_graos",
125
+ "days": 40,
126
+ "description": "Enchimento de grãos",
127
+ "critical_water": True,
128
+ "tasks": [
129
+ {"type": "irrigate", "title": "Irrigar - fase crítica", "priority": "critical", "weather_sensitive": True},
130
+ {"type": "inspect_diseases", "title": "Inspecionar doenças foliares", "priority": "high", "weather_sensitive": False},
131
+ {"type": "monitor_growth", "title": "Monitorar desenvolvimento das espigas", "priority": "medium", "weather_sensitive": False}
132
+ ]
133
+ },
134
+ {
135
+ "name": "maturacao",
136
+ "days": 10,
137
+ "description": "Maturação fisiológica",
138
+ "critical_water": False,
139
+ "tasks": [
140
+ {"type": "harvest", "title": "Preparar colheita", "priority": "high", "weather_sensitive": True},
141
+ {"type": "monitor_growth", "title": "Monitorar umidade dos grãos", "priority": "medium", "weather_sensitive": False}
142
+ ]
143
+ }
144
+ ],
145
+ "optimal_temp_min": 18,
146
+ "optimal_temp_max": 32,
147
+ "critical_water_phases": ["germinacao", "florescimento", "enchimento_graos"],
148
+ "harvest_window_days": 20
149
+ },
150
+ "feijao": {
151
+ "cycle_days": 90,
152
+ "phases": [
153
+ {
154
+ "name": "germinacao",
155
+ "days": 8,
156
+ "description": "Emergência das plântulas",
157
+ "critical_water": True,
158
+ "tasks": [
159
+ {"type": "irrigate", "title": "Irrigar se não houver chuva", "priority": "high", "weather_sensitive": True},
160
+ {"type": "inspect_pests", "title": "Inspecionar germinação", "priority": "medium", "weather_sensitive": False}
161
+ ]
162
+ },
163
+ {
164
+ "name": "vegetativa",
165
+ "days": 30,
166
+ "description": "Crescimento vegetativo",
167
+ "critical_water": False,
168
+ "tasks": [
169
+ {"type": "fertilize", "title": "Aplicar fertilizante de cobertura", "priority": "high", "weather_sensitive": True},
170
+ {"type": "inspect_pests", "title": "Inspecionar pragas (vaquinha, mosca-branca)", "priority": "high", "weather_sensitive": False},
171
+ {"type": "irrigate", "title": "Irrigar moderadamente", "priority": "medium", "weather_sensitive": True}
172
+ ]
173
+ },
174
+ {
175
+ "name": "florescimento",
176
+ "days": 22,
177
+ "description": "Floração e formação de vagens",
178
+ "critical_water": True,
179
+ "tasks": [
180
+ {"type": "irrigate", "title": "Irrigar - fase crítica", "priority": "critical", "weather_sensitive": True},
181
+ {"type": "inspect_diseases", "title": "Inspecionar doenças (antracnose, ferrugem)", "priority": "high", "weather_sensitive": False},
182
+ {"type": "monitor_growth", "title": "Monitorar formação de vagens", "priority": "medium", "weather_sensitive": False}
183
+ ]
184
+ },
185
+ {
186
+ "name": "enchimento_graos",
187
+ "days": 20,
188
+ "description": "Enchimento de grãos",
189
+ "critical_water": True,
190
+ "tasks": [
191
+ {"type": "irrigate", "title": "Irrigar - fase crítica", "priority": "critical", "weather_sensitive": True},
192
+ {"type": "monitor_growth", "title": "Monitorar desenvolvimento das vagens", "priority": "high", "weather_sensitive": False}
193
+ ]
194
+ },
195
+ {
196
+ "name": "maturacao",
197
+ "days": 10,
198
+ "description": "Maturação e secagem",
199
+ "critical_water": False,
200
+ "tasks": [
201
+ {"type": "harvest", "title": "Preparar colheita", "priority": "high", "weather_sensitive": True},
202
+ {"type": "monitor_growth", "title": "Monitorar umidade dos grãos", "priority": "medium", "weather_sensitive": False}
203
+ ]
204
+ }
205
+ ],
206
+ "optimal_temp_min": 18,
207
+ "optimal_temp_max": 29,
208
+ "critical_water_phases": ["germinacao", "florescimento", "enchimento_graos"],
209
+ "harvest_window_days": 10
210
+ }
211
+ }
212
+
213
+
214
+ def get_crop_cycle(cultura: str) -> Optional[CropCycle]:
215
+ """
216
+ Retorna o ciclo de uma cultura.
217
+
218
+ Args:
219
+ cultura: Nome da cultura
220
+
221
+ Returns:
222
+ CropCycle ou None se cultura não encontrada
223
+ """
224
+ if cultura not in CROP_CYCLES:
225
+ return None
226
+
227
+ cycle_data = CROP_CYCLES[cultura]
228
+
229
+ phases = [
230
+ CropPhase(
231
+ name=phase["name"],
232
+ days=phase["days"],
233
+ description=phase["description"],
234
+ critical_water=phase["critical_water"],
235
+ tasks=[task["type"] for task in phase["tasks"]]
236
+ )
237
+ for phase in cycle_data["phases"]
238
+ ]
239
+
240
+ return CropCycle(
241
+ culture=cultura,
242
+ cycle_days=cycle_data["cycle_days"],
243
+ phases=phases,
244
+ critical_water_phases=cycle_data["critical_water_phases"],
245
+ optimal_temp_min=cycle_data["optimal_temp_min"],
246
+ optimal_temp_max=cycle_data["optimal_temp_max"],
247
+ harvest_window_days=cycle_data["harvest_window_days"]
248
+ )
249
+
250
+
251
+ def gerar_calendario_cultura(
252
+ cultura: str,
253
+ planting_date: date,
254
+ field: Field,
255
+ crop_plan_id: Optional[str] = None,
256
+ weather_context: Optional[Dict] = None,
257
+ zarc_context: Optional[Dict] = None
258
+ ) -> Dict:
259
+ """
260
+ Gera calendário de tarefas para uma cultura.
261
+
262
+ Args:
263
+ cultura: Nome da cultura
264
+ planting_date: Data de plantio
265
+ field: Dados do talhão
266
+ crop_plan_id: ID do plano de cultura (opcional)
267
+ weather_context: Contexto climático (opcional, fase futura)
268
+ zarc_context: Contexto ZARC (opcional, fase futura)
269
+
270
+ Returns:
271
+ Dict com calendário e informações do ciclo
272
+ """
273
+
274
+ # Verificar se cultura existe
275
+ if cultura not in CROP_CYCLES:
276
+ return {
277
+ "error": f"Cultura '{cultura}' não encontrada",
278
+ "culturas_disponiveis": list(CROP_CYCLES.keys())
279
+ }
280
+
281
+ cycle_data = CROP_CYCLES[cultura]
282
+ crop_plan_id = crop_plan_id or str(uuid.uuid4())
283
+
284
+ # Calcular data estimada de colheita
285
+ estimated_harvest_date = planting_date + timedelta(days=cycle_data["cycle_days"])
286
+
287
+ # Gerar tarefas
288
+ tasks = []
289
+ current_date = planting_date
290
+
291
+ # Tarefa de preparação do solo (7 dias antes do plantio)
292
+ prepare_date = planting_date - timedelta(days=7)
293
+ tasks.append(
294
+ CalendarTask(
295
+ id=str(uuid.uuid4()),
296
+ crop_plan_id=crop_plan_id,
297
+ date=prepare_date,
298
+ type=TaskType.PREPARE_SOIL,
299
+ title="Preparar solo para plantio",
300
+ description=f"Preparar solo {field.soil_type.value} para plantio de {cultura}",
301
+ priority=TaskPriority.HIGH,
302
+ source="system",
303
+ weather_sensitive=False
304
+ )
305
+ )
306
+
307
+ # Tarefa de plantio
308
+ tasks.append(
309
+ CalendarTask(
310
+ id=str(uuid.uuid4()),
311
+ crop_plan_id=crop_plan_id,
312
+ date=planting_date,
313
+ type=TaskType.PLANT,
314
+ title=f"Plantar {cultura}",
315
+ description=f"Plantio de {cultura} em {field.area_ha} ha",
316
+ priority=TaskPriority.CRITICAL,
317
+ source="system",
318
+ weather_sensitive=True
319
+ )
320
+ )
321
+
322
+ # Tarefas por fase
323
+ for phase in cycle_data["phases"]:
324
+ phase_start = current_date
325
+ phase_end = current_date + timedelta(days=phase["days"])
326
+
327
+ # Distribuir tarefas ao longo da fase
328
+ for i, task_data in enumerate(phase["tasks"]):
329
+ # Espaçar tarefas uniformemente na fase
330
+ task_offset = (phase["days"] // (len(phase["tasks"]) + 1)) * (i + 1)
331
+ task_date = phase_start + timedelta(days=task_offset)
332
+
333
+ tasks.append(
334
+ CalendarTask(
335
+ id=str(uuid.uuid4()),
336
+ crop_plan_id=crop_plan_id,
337
+ date=task_date,
338
+ type=TaskType[task_data["type"].upper()],
339
+ title=task_data["title"],
340
+ description=f"{task_data['title']} - Fase: {phase['description']}",
341
+ priority=TaskPriority[task_data["priority"].upper()],
342
+ source="system",
343
+ weather_sensitive=task_data["weather_sensitive"]
344
+ )
345
+ )
346
+
347
+ current_date = phase_end
348
+
349
+ # Tarefa de colheita
350
+ tasks.append(
351
+ CalendarTask(
352
+ id=str(uuid.uuid4()),
353
+ crop_plan_id=crop_plan_id,
354
+ date=estimated_harvest_date,
355
+ type=TaskType.HARVEST,
356
+ title=f"Colher {cultura}",
357
+ description=f"Colheita de {cultura} - Janela de {cycle_data['harvest_window_days']} dias",
358
+ priority=TaskPriority.CRITICAL,
359
+ source="system",
360
+ weather_sensitive=True
361
+ )
362
+ )
363
+
364
+ # Ordenar tarefas por data
365
+ tasks.sort(key=lambda t: t.date)
366
+
367
+ # Montar resposta
368
+ return {
369
+ "cultura": cultura,
370
+ "planting_date": planting_date.isoformat(),
371
+ "estimated_harvest_date": estimated_harvest_date.isoformat(),
372
+ "cycle_days": cycle_data["cycle_days"],
373
+ "field": field.to_dict(),
374
+ "crop_plan_id": crop_plan_id,
375
+ "cycle_info": {
376
+ "optimal_temp_min": cycle_data["optimal_temp_min"],
377
+ "optimal_temp_max": cycle_data["optimal_temp_max"],
378
+ "critical_water_phases": cycle_data["critical_water_phases"],
379
+ "harvest_window_days": cycle_data["harvest_window_days"],
380
+ "phases": [
381
+ {
382
+ "name": phase["name"],
383
+ "days": phase["days"],
384
+ "description": phase["description"],
385
+ "critical_water": phase["critical_water"]
386
+ }
387
+ for phase in cycle_data["phases"]
388
+ ]
389
+ },
390
+ "tasks": [task.to_dict() for task in tasks],
391
+ "total_tasks": len(tasks),
392
+ "weather_sensitive_tasks": sum(1 for task in tasks if task.weather_sensitive),
393
+ "critical_tasks": sum(1 for task in tasks if task.priority == TaskPriority.CRITICAL)
394
+ }
395
+
396
+
397
+ def get_culturas_disponiveis() -> List[str]:
398
+ """Retorna lista de culturas disponíveis no sistema"""
399
+ return list(CROP_CYCLES.keys())
400
+
401
+
402
+ def get_cultura_info(cultura: str) -> Optional[Dict]:
403
+ """
404
+ Retorna informações resumidas de uma cultura.
405
+
406
+ Args:
407
+ cultura: Nome da cultura
408
+
409
+ Returns:
410
+ Dict com informações ou None se não encontrada
411
+ """
412
+ if cultura not in CROP_CYCLES:
413
+ return None
414
+
415
+ cycle_data = CROP_CYCLES[cultura]
416
+
417
+ return {
418
+ "cultura": cultura,
419
+ "cycle_days": cycle_data["cycle_days"],
420
+ "optimal_temp_min": cycle_data["optimal_temp_min"],
421
+ "optimal_temp_max": cycle_data["optimal_temp_max"],
422
+ "critical_water_phases": cycle_data["critical_water_phases"],
423
+ "harvest_window_days": cycle_data["harvest_window_days"],
424
+ "total_phases": len(cycle_data["phases"]),
425
+ "phases_names": [phase["name"] for phase in cycle_data["phases"]]
426
+ }
@@ -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
+ }
@@ -0,0 +1,366 @@
1
+ """
2
+ Modelos de Domínio para Planejador de Safra Inteligente
3
+
4
+ Define as entidades principais do sistema de planejamento agrícola:
5
+ - Property: Propriedade rural
6
+ - Field: Talhão/campo
7
+ - CropPlan: Plano de cultura
8
+ - CropCycle: Ciclo da cultura
9
+ - CalendarTask: Tarefa do calendário
10
+ - WeatherAlert: Alerta climático
11
+ - UserObservation: Observação do usuário
12
+ - Intervention: Intervenção/replanejamento
13
+ - PlanningSession: Sessão de planejamento
14
+ """
15
+
16
+ from dataclasses import dataclass, field
17
+ from datetime import date, datetime
18
+ from typing import Optional, List, Dict
19
+ from enum import Enum
20
+
21
+
22
+ # Enums para tipos padronizados
23
+
24
+ class SoilType(str, Enum):
25
+ """Tipos de solo"""
26
+ ARGILOSO = "argiloso"
27
+ ARENOSO = "arenoso"
28
+ MISTO = "misto"
29
+ SILTOSO = "siltoso"
30
+
31
+
32
+ class Slope(str, Enum):
33
+ """Tipos de relevo"""
34
+ PLANO = "plano"
35
+ LEVE = "leve"
36
+ MEDIO = "medio"
37
+ INGREME = "ingreme"
38
+
39
+
40
+ class WaterAvailability(str, Enum):
41
+ """Disponibilidade de água"""
42
+ BAIXA = "baixa"
43
+ MEDIA = "media"
44
+ ALTA = "alta"
45
+
46
+
47
+ class Objective(str, Enum):
48
+ """Objetivos de otimização"""
49
+ EQUILIBRADO = "equilibrado"
50
+ LUCRO = "lucro"
51
+ RISCO = "risco"
52
+ SUSTENTAVEL = "sustentavel"
53
+
54
+
55
+ class PlanStatus(str, Enum):
56
+ """Status do plano de cultura"""
57
+ PLANNED = "planned"
58
+ ACTIVE = "active"
59
+ COMPLETED = "completed"
60
+ CANCELLED = "cancelled"
61
+
62
+
63
+ class TaskType(str, Enum):
64
+ """Tipos de tarefa"""
65
+ PREPARE_SOIL = "prepare_soil"
66
+ PLANT = "plant"
67
+ IRRIGATE = "irrigate"
68
+ FERTILIZE = "fertilize"
69
+ INSPECT_PESTS = "inspect_pests"
70
+ INSPECT_DISEASES = "inspect_diseases"
71
+ MONITOR_GROWTH = "monitor_growth"
72
+ HARVEST = "harvest"
73
+
74
+
75
+ class TaskPriority(str, Enum):
76
+ """Prioridade da tarefa"""
77
+ LOW = "low"
78
+ MEDIUM = "medium"
79
+ HIGH = "high"
80
+ CRITICAL = "critical"
81
+
82
+
83
+ class TaskStatus(str, Enum):
84
+ """Status da tarefa"""
85
+ PENDING = "pending"
86
+ COMPLETED = "completed"
87
+ SKIPPED = "skipped"
88
+ RESCHEDULED = "rescheduled"
89
+
90
+
91
+ class AlertType(str, Enum):
92
+ """Tipos de alerta climático"""
93
+ RAIN = "rain"
94
+ DROUGHT = "drought"
95
+ HEAT = "heat"
96
+ COLD = "cold"
97
+ FROST = "frost"
98
+
99
+
100
+ class AlertSeverity(str, Enum):
101
+ """Severidade do alerta"""
102
+ INFO = "info"
103
+ WARNING = "warning"
104
+ CRITICAL = "critical"
105
+
106
+
107
+ class InterventionReason(str, Enum):
108
+ """Razão da intervenção"""
109
+ MISSED_TASK = "missed_task"
110
+ WEATHER_EVENT = "weather_event"
111
+ SOIL_CONDITION = "soil_condition"
112
+ USER_REQUEST = "user_request"
113
+
114
+
115
+ class PlanningMode(str, Enum):
116
+ """Modo de planejamento"""
117
+ GUIDED = "guided"
118
+ ADVANCED = "advanced"
119
+
120
+
121
+ # Modelos de Domínio
122
+
123
+ @dataclass
124
+ class Property:
125
+ """Propriedade rural"""
126
+ id: str
127
+ name: str
128
+ uf: str
129
+ municipio: str
130
+ lat: Optional[float] = None
131
+ lon: Optional[float] = None
132
+ created_at: datetime = field(default_factory=datetime.now)
133
+ updated_at: datetime = field(default_factory=datetime.now)
134
+
135
+ def to_dict(self) -> Dict:
136
+ return {
137
+ "id": self.id,
138
+ "name": self.name,
139
+ "uf": self.uf,
140
+ "municipio": self.municipio,
141
+ "lat": self.lat,
142
+ "lon": self.lon,
143
+ "created_at": self.created_at.isoformat(),
144
+ "updated_at": self.updated_at.isoformat()
145
+ }
146
+
147
+
148
+ @dataclass
149
+ class Field:
150
+ """Talhão/campo"""
151
+ id: str
152
+ property_id: str
153
+ name: str
154
+ area_ha: float
155
+ soil_type: SoilType
156
+ slope: Slope
157
+ water_availability: WaterAvailability
158
+ geometry: Optional[Dict] = None # GeoJSON para mapa
159
+ created_at: datetime = field(default_factory=datetime.now)
160
+
161
+ def to_dict(self) -> Dict:
162
+ return {
163
+ "id": self.id,
164
+ "property_id": self.property_id,
165
+ "name": self.name,
166
+ "area_ha": self.area_ha,
167
+ "soil_type": self.soil_type.value,
168
+ "slope": self.slope.value,
169
+ "water_availability": self.water_availability.value,
170
+ "geometry": self.geometry,
171
+ "created_at": self.created_at.isoformat()
172
+ }
173
+
174
+
175
+ @dataclass
176
+ class CropPlan:
177
+ """Plano de cultura para um talhão"""
178
+ id: str
179
+ field_id: str
180
+ culture: str
181
+ planting_date: date
182
+ estimated_harvest_date: date
183
+ objective: Objective
184
+ status: PlanStatus = PlanStatus.PLANNED
185
+ created_at: datetime = field(default_factory=datetime.now)
186
+
187
+ def to_dict(self) -> Dict:
188
+ return {
189
+ "id": self.id,
190
+ "field_id": self.field_id,
191
+ "culture": self.culture,
192
+ "planting_date": self.planting_date.isoformat(),
193
+ "estimated_harvest_date": self.estimated_harvest_date.isoformat(),
194
+ "objective": self.objective.value,
195
+ "status": self.status.value,
196
+ "created_at": self.created_at.isoformat()
197
+ }
198
+
199
+
200
+ @dataclass
201
+ class CropPhase:
202
+ """Fase do ciclo da cultura"""
203
+ name: str
204
+ days: int
205
+ description: str
206
+ critical_water: bool
207
+ tasks: List[str]
208
+
209
+
210
+ @dataclass
211
+ class CropCycle:
212
+ """Ciclo completo de uma cultura"""
213
+ culture: str
214
+ cycle_days: int
215
+ phases: List[CropPhase]
216
+ critical_water_phases: List[str]
217
+ optimal_temp_min: float
218
+ optimal_temp_max: float
219
+ harvest_window_days: int
220
+
221
+ def to_dict(self) -> Dict:
222
+ return {
223
+ "culture": self.culture,
224
+ "cycle_days": self.cycle_days,
225
+ "phases": [
226
+ {
227
+ "name": phase.name,
228
+ "days": phase.days,
229
+ "description": phase.description,
230
+ "critical_water": phase.critical_water,
231
+ "tasks": phase.tasks
232
+ }
233
+ for phase in self.phases
234
+ ],
235
+ "critical_water_phases": self.critical_water_phases,
236
+ "optimal_temp_min": self.optimal_temp_min,
237
+ "optimal_temp_max": self.optimal_temp_max,
238
+ "harvest_window_days": self.harvest_window_days
239
+ }
240
+
241
+
242
+ @dataclass
243
+ class CalendarTask:
244
+ """Tarefa do calendário agrícola"""
245
+ id: str
246
+ crop_plan_id: str
247
+ date: date
248
+ type: TaskType
249
+ title: str
250
+ description: str
251
+ priority: TaskPriority
252
+ source: str # system, user, weather_alert
253
+ status: TaskStatus = TaskStatus.PENDING
254
+ weather_sensitive: bool = False
255
+ completed_at: Optional[datetime] = None
256
+
257
+ def to_dict(self) -> Dict:
258
+ return {
259
+ "id": self.id,
260
+ "crop_plan_id": self.crop_plan_id,
261
+ "date": self.date.isoformat(),
262
+ "type": self.type.value,
263
+ "title": self.title,
264
+ "description": self.description,
265
+ "priority": self.priority.value,
266
+ "source": self.source,
267
+ "status": self.status.value,
268
+ "weather_sensitive": self.weather_sensitive,
269
+ "completed_at": self.completed_at.isoformat() if self.completed_at else None
270
+ }
271
+
272
+
273
+ @dataclass
274
+ class WeatherAlert:
275
+ """Alerta climático"""
276
+ id: str
277
+ crop_plan_id: str
278
+ date: date
279
+ alert_type: AlertType
280
+ severity: AlertSeverity
281
+ message: str
282
+ action_suggested: Optional[str] = None
283
+ created_at: datetime = field(default_factory=datetime.now)
284
+
285
+ def to_dict(self) -> Dict:
286
+ return {
287
+ "id": self.id,
288
+ "crop_plan_id": self.crop_plan_id,
289
+ "date": self.date.isoformat(),
290
+ "alert_type": self.alert_type.value,
291
+ "severity": self.severity.value,
292
+ "message": self.message,
293
+ "action_suggested": self.action_suggested,
294
+ "created_at": self.created_at.isoformat()
295
+ }
296
+
297
+
298
+ @dataclass
299
+ class UserObservation:
300
+ """Observação do usuário sobre o cultivo"""
301
+ id: str
302
+ crop_plan_id: str
303
+ date: date
304
+ note: str
305
+ impact: str # positive, neutral, negative
306
+ created_at: datetime = field(default_factory=datetime.now)
307
+
308
+ def to_dict(self) -> Dict:
309
+ return {
310
+ "id": self.id,
311
+ "crop_plan_id": self.crop_plan_id,
312
+ "date": self.date.isoformat(),
313
+ "note": self.note,
314
+ "impact": self.impact,
315
+ "created_at": self.created_at.isoformat()
316
+ }
317
+
318
+
319
+ @dataclass
320
+ class Intervention:
321
+ """Intervenção/replanejamento"""
322
+ id: str
323
+ crop_plan_id: str
324
+ reason: InterventionReason
325
+ suggested_action: str
326
+ original_task_id: Optional[str] = None
327
+ new_date: Optional[date] = None
328
+ risk_adjustment: float = 0.0
329
+ created_at: datetime = field(default_factory=datetime.now)
330
+ applied: bool = False
331
+
332
+ def to_dict(self) -> Dict:
333
+ return {
334
+ "id": self.id,
335
+ "crop_plan_id": self.crop_plan_id,
336
+ "original_task_id": self.original_task_id,
337
+ "reason": self.reason.value,
338
+ "suggested_action": self.suggested_action,
339
+ "new_date": self.new_date.isoformat() if self.new_date else None,
340
+ "risk_adjustment": self.risk_adjustment,
341
+ "created_at": self.created_at.isoformat(),
342
+ "applied": self.applied
343
+ }
344
+
345
+
346
+ @dataclass
347
+ class PlanningSession:
348
+ """Sessão de planejamento"""
349
+ id: str
350
+ property_id: str
351
+ mode: PlanningMode
352
+ objective: Objective
353
+ fields_count: int
354
+ cultures_recommended: List[str]
355
+ created_at: datetime = field(default_factory=datetime.now)
356
+
357
+ def to_dict(self) -> Dict:
358
+ return {
359
+ "id": self.id,
360
+ "property_id": self.property_id,
361
+ "mode": self.mode.value,
362
+ "objective": self.objective.value,
363
+ "fields_count": self.fields_count,
364
+ "cultures_recommended": self.cultures_recommended,
365
+ "created_at": self.created_at.isoformat()
366
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agroplan-ai-cli",
3
- "version": "1.0.28",
3
+ "version": "1.0.30",
4
4
  "description": "CLI global para AgroPlan AI - modo local acelerado",
5
5
  "type": "module",
6
6
  "bin": {