agroplan-ai-cli 1.0.29 → 1.0.31

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.29",
3
- "backend_template_version": "1.0.29",
2
+ "cli_version": "1.0.31",
3
+ "backend_template_version": "1.0.31",
4
4
  "zarc_index_version": "2025-2026-fast-index-v2",
5
5
  "price_index_version": "2025-05-reference-v1",
6
6
  "features": [
@@ -17,7 +17,10 @@
17
17
  "market_profit_validation",
18
18
  "market_profit_confidence_refinement",
19
19
  "market_profit_comparative_evaluation",
20
- "market_profit_experimental_optimizer"
20
+ "market_profit_experimental_optimizer",
21
+ "smart_crop_calendar_engine",
22
+ "manual_field_registration",
23
+ "crop_calendar_from_manual_field"
21
24
  ],
22
- "generated_at": "2026-05-09T23:45:00Z"
25
+ "generated_at": "2026-05-10T16:15:00Z"
23
26
  }
@@ -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__)))
@@ -1308,6 +1309,297 @@ def otimizar_lucro_mercado_experimental(
1308
1309
  detail="Erro ao gerar otimização experimental de lucro de mercado."
1309
1310
  )
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
+
1420
+ # Endpoints de Talhões Manuais
1421
+
1422
+ @app.get("/planejamento/talhoes")
1423
+ def listar_talhoes():
1424
+ """Lista todos os talhões cadastrados pelo usuário"""
1425
+ try:
1426
+ from core.field_storage import listar_talhoes_usuario
1427
+
1428
+ fields = listar_talhoes_usuario()
1429
+
1430
+ return {
1431
+ "total": len(fields),
1432
+ "talhoes": fields
1433
+ }
1434
+ except Exception as e:
1435
+ raise HTTPException(status_code=500, detail=str(e))
1436
+
1437
+ @app.post("/planejamento/talhoes")
1438
+ def criar_talhao(field_data: dict):
1439
+ """Cria um novo talhão"""
1440
+ try:
1441
+ from core.field_storage import criar_talhao_usuario
1442
+ from core.planning_models import ManualFieldCreate
1443
+
1444
+ # Validar dados
1445
+ validated = ManualFieldCreate(**field_data)
1446
+
1447
+ # Criar talhão
1448
+ new_field = criar_talhao_usuario(validated.model_dump())
1449
+
1450
+ return new_field
1451
+ except ValueError as e:
1452
+ raise HTTPException(status_code=400, detail=str(e))
1453
+ except Exception as e:
1454
+ raise HTTPException(status_code=500, detail=str(e))
1455
+
1456
+ @app.get("/planejamento/talhoes/{field_id}")
1457
+ def obter_talhao(field_id: str):
1458
+ """Obtém um talhão pelo ID"""
1459
+ try:
1460
+ from core.field_storage import obter_talhao_usuario
1461
+
1462
+ field = obter_talhao_usuario(field_id)
1463
+
1464
+ if not field:
1465
+ raise HTTPException(
1466
+ status_code=404,
1467
+ detail=f"Talhão '{field_id}' não encontrado"
1468
+ )
1469
+
1470
+ return field
1471
+ except HTTPException:
1472
+ raise
1473
+ except Exception as e:
1474
+ raise HTTPException(status_code=500, detail=str(e))
1475
+
1476
+ @app.put("/planejamento/talhoes/{field_id}")
1477
+ def atualizar_talhao(field_id: str, field_data: dict):
1478
+ """Atualiza um talhão existente"""
1479
+ try:
1480
+ from core.field_storage import atualizar_talhao_usuario
1481
+ from core.planning_models import ManualFieldUpdate
1482
+
1483
+ # Validar dados
1484
+ validated = ManualFieldUpdate(**field_data)
1485
+
1486
+ # Atualizar apenas campos fornecidos
1487
+ update_data = {k: v for k, v in validated.model_dump().items() if v is not None}
1488
+
1489
+ # Atualizar talhão
1490
+ updated_field = atualizar_talhao_usuario(field_id, update_data)
1491
+
1492
+ if not updated_field:
1493
+ raise HTTPException(
1494
+ status_code=404,
1495
+ detail=f"Talhão '{field_id}' não encontrado"
1496
+ )
1497
+
1498
+ return updated_field
1499
+ except HTTPException:
1500
+ raise
1501
+ except ValueError as e:
1502
+ raise HTTPException(status_code=400, detail=str(e))
1503
+ except Exception as e:
1504
+ raise HTTPException(status_code=500, detail=str(e))
1505
+
1506
+ @app.delete("/planejamento/talhoes/{field_id}")
1507
+ def remover_talhao(field_id: str):
1508
+ """Remove um talhão"""
1509
+ try:
1510
+ from core.field_storage import remover_talhao_usuario
1511
+
1512
+ removed = remover_talhao_usuario(field_id)
1513
+
1514
+ if not removed:
1515
+ raise HTTPException(
1516
+ status_code=404,
1517
+ detail=f"Talhão '{field_id}' não encontrado"
1518
+ )
1519
+
1520
+ return {
1521
+ "message": "Talhão removido com sucesso",
1522
+ "id": field_id
1523
+ }
1524
+ except HTTPException:
1525
+ raise
1526
+ except Exception as e:
1527
+ raise HTTPException(status_code=500, detail=str(e))
1528
+
1529
+ @app.post("/planejamento/talhoes/{field_id}/calendario")
1530
+ def gerar_calendario_talhao(field_id: str, request: dict):
1531
+ """Gera calendário agrícola para um talhão cadastrado"""
1532
+ try:
1533
+ from core.field_storage import obter_talhao_usuario
1534
+ from core.crop_calendar_engine import gerar_calendario_cultura
1535
+ from core.planning_models import Field, SoilType, Slope, WaterAvailability, GenerateCalendarRequest
1536
+ from datetime import datetime
1537
+
1538
+ # Validar request
1539
+ validated = GenerateCalendarRequest(**request)
1540
+
1541
+ # Buscar talhão
1542
+ field_data = obter_talhao_usuario(field_id)
1543
+
1544
+ if not field_data:
1545
+ raise HTTPException(
1546
+ status_code=404,
1547
+ detail=f"Talhão '{field_id}' não encontrado"
1548
+ )
1549
+
1550
+ # Parsear data de plantio
1551
+ try:
1552
+ planting_date = datetime.fromisoformat(validated.planting_date).date()
1553
+ except Exception:
1554
+ raise HTTPException(
1555
+ status_code=400,
1556
+ detail="Formato de data inválido. Use ISO 8601 (YYYY-MM-DD)"
1557
+ )
1558
+
1559
+ # Criar objeto Field
1560
+ field = Field(
1561
+ id=field_data["id"],
1562
+ property_id="user",
1563
+ name=field_data["name"],
1564
+ area_ha=field_data["area_ha"],
1565
+ soil_type=SoilType(field_data["soil_type"]),
1566
+ slope=Slope(field_data["slope"]),
1567
+ water_availability=WaterAvailability(field_data["water_availability"])
1568
+ )
1569
+
1570
+ # Gerar calendário
1571
+ resultado = gerar_calendario_cultura(
1572
+ cultura=validated.cultura,
1573
+ planting_date=planting_date,
1574
+ field=field,
1575
+ crop_plan_id=None
1576
+ )
1577
+
1578
+ # Adicionar dados do talhão na resposta
1579
+ resultado["field_data"] = field_data
1580
+
1581
+ return converter_tipos_python(resultado)
1582
+
1583
+ except HTTPException:
1584
+ raise
1585
+ except ValueError as e:
1586
+ raise HTTPException(status_code=400, detail=str(e))
1587
+ except Exception as e:
1588
+ import traceback
1589
+ if DEBUG_ERRORS:
1590
+ raise HTTPException(
1591
+ status_code=500,
1592
+ detail={
1593
+ "error": str(e),
1594
+ "traceback": traceback.format_exc()
1595
+ }
1596
+ )
1597
+ else:
1598
+ raise HTTPException(
1599
+ status_code=500,
1600
+ detail="Erro ao gerar calendário para o talhão."
1601
+ )
1602
+
1311
1603
  @app.post("/cache/limpar")
1312
1604
  def limpar_cache(request: Request):
1313
1605
  """Limpa o cache de resultados pesados (protegido por token)"""