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.
- package/backend-template/VERSION.json +7 -4
- package/backend-template/api.py +292 -0
- package/backend-template/core/crop_calendar_engine.py +426 -0
- package/backend-template/core/field_storage.py +175 -0
- package/backend-template/core/planning_models.py +484 -0
- package/backend-template/data/user_fields/fields.json +0 -0
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"cli_version": "1.0.
|
|
3
|
-
"backend_template_version": "1.0.
|
|
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-
|
|
25
|
+
"generated_at": "2026-05-10T16:15:00Z"
|
|
23
26
|
}
|
package/backend-template/api.py
CHANGED
|
@@ -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)"""
|