agroplan-ai-cli 1.0.30 → 1.0.32

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.30",
3
- "backend_template_version": "1.0.30",
2
+ "cli_version": "1.0.32",
3
+ "backend_template_version": "1.0.32",
4
4
  "zarc_index_version": "2025-2026-fast-index-v2",
5
5
  "price_index_version": "2025-05-reference-v1",
6
6
  "features": [
@@ -18,7 +18,10 @@
18
18
  "market_profit_confidence_refinement",
19
19
  "market_profit_comparative_evaluation",
20
20
  "market_profit_experimental_optimizer",
21
- "smart_crop_calendar_engine"
21
+ "smart_crop_calendar_engine",
22
+ "manual_field_registration",
23
+ "crop_calendar_from_manual_field",
24
+ "expanded_crop_calendar_10_cultures"
22
25
  ],
23
- "generated_at": "2026-05-10T15:30:00Z"
26
+ "generated_at": "2026-05-10T18:30:00Z"
24
27
  }
@@ -1417,6 +1417,189 @@ def obter_cultura_info(cultura: str):
1417
1417
  except Exception as e:
1418
1418
  raise HTTPException(status_code=500, detail=str(e))
1419
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
+
1420
1603
  @app.post("/cache/limpar")
1421
1604
  def limpar_cache(request: Request):
1422
1605
  """Limpa o cache de resultados pesados (protegido por token)"""
@@ -207,6 +207,517 @@ CROP_CYCLES: Dict[str, Dict] = {
207
207
  "optimal_temp_max": 29,
208
208
  "critical_water_phases": ["germinacao", "florescimento", "enchimento_graos"],
209
209
  "harvest_window_days": 10
210
+ },
211
+ "cafe": {
212
+ "cycle_days": 730,
213
+ "phases": [
214
+ {
215
+ "name": "preparo",
216
+ "days": 30,
217
+ "description": "Preparo do solo e coveamento",
218
+ "critical_water": False,
219
+ "tasks": [
220
+ {"type": "prepare_soil", "title": "Preparar solo e covas", "priority": "high", "weather_sensitive": False},
221
+ {"type": "fertilize", "title": "Aplicar calcário e adubação de base", "priority": "high", "weather_sensitive": True}
222
+ ]
223
+ },
224
+ {
225
+ "name": "plantio",
226
+ "days": 60,
227
+ "description": "Plantio de mudas e estabelecimento",
228
+ "critical_water": True,
229
+ "tasks": [
230
+ {"type": "plant", "title": "Plantar mudas de café", "priority": "critical", "weather_sensitive": True},
231
+ {"type": "irrigate", "title": "Irrigar mudas - fase crítica", "priority": "critical", "weather_sensitive": True},
232
+ {"type": "inspect_pests", "title": "Monitorar pegamento das mudas", "priority": "high", "weather_sensitive": False}
233
+ ]
234
+ },
235
+ {
236
+ "name": "conducao",
237
+ "days": 365,
238
+ "description": "Condução e formação da lavoura",
239
+ "critical_water": False,
240
+ "tasks": [
241
+ {"type": "fertilize", "title": "Aplicar adubação de crescimento", "priority": "high", "weather_sensitive": True},
242
+ {"type": "inspect_pests", "title": "Monitorar pragas e doenças", "priority": "high", "weather_sensitive": False},
243
+ {"type": "monitor_growth", "title": "Avaliar desenvolvimento vegetativo", "priority": "medium", "weather_sensitive": False},
244
+ {"type": "irrigate", "title": "Irrigar conforme necessidade", "priority": "medium", "weather_sensitive": True}
245
+ ]
246
+ },
247
+ {
248
+ "name": "pre_producao",
249
+ "days": 180,
250
+ "description": "Preparação para primeira produção",
251
+ "critical_water": False,
252
+ "tasks": [
253
+ {"type": "fertilize", "title": "Aplicar adubação de produção", "priority": "high", "weather_sensitive": True},
254
+ {"type": "inspect_diseases", "title": "Monitorar sanidade da lavoura", "priority": "high", "weather_sensitive": False},
255
+ {"type": "monitor_growth", "title": "Avaliar floração inicial", "priority": "medium", "weather_sensitive": False}
256
+ ]
257
+ },
258
+ {
259
+ "name": "colheita",
260
+ "days": 95,
261
+ "description": "Primeira colheita",
262
+ "critical_water": False,
263
+ "tasks": [
264
+ {"type": "monitor_growth", "title": "Monitorar maturação dos frutos", "priority": "high", "weather_sensitive": False},
265
+ {"type": "harvest", "title": "Preparar colheita", "priority": "high", "weather_sensitive": True}
266
+ ]
267
+ }
268
+ ],
269
+ "optimal_temp_min": 18,
270
+ "optimal_temp_max": 28,
271
+ "critical_water_phases": ["plantio"],
272
+ "harvest_window_days": 60,
273
+ "category": "perene",
274
+ "water_need": "media",
275
+ "risk_notes": "Sensível a geadas e déficit hídrico em fases críticas.",
276
+ "calendar_notes": "Calendário simplificado para implantação e primeiros manejos. Cultura perene com ciclo longo."
277
+ },
278
+ "cana": {
279
+ "cycle_days": 365,
280
+ "phases": [
281
+ {
282
+ "name": "preparo",
283
+ "days": 15,
284
+ "description": "Preparo do solo",
285
+ "critical_water": False,
286
+ "tasks": [
287
+ {"type": "prepare_soil", "title": "Preparar solo para plantio", "priority": "high", "weather_sensitive": False},
288
+ {"type": "fertilize", "title": "Aplicar adubação de base", "priority": "high", "weather_sensitive": True}
289
+ ]
290
+ },
291
+ {
292
+ "name": "plantio",
293
+ "days": 30,
294
+ "description": "Plantio de mudas e brotação",
295
+ "critical_water": True,
296
+ "tasks": [
297
+ {"type": "plant", "title": "Plantar mudas de cana", "priority": "critical", "weather_sensitive": True},
298
+ {"type": "irrigate", "title": "Irrigar para brotação", "priority": "high", "weather_sensitive": True},
299
+ {"type": "inspect_pests", "title": "Monitorar brotação", "priority": "medium", "weather_sensitive": False}
300
+ ]
301
+ },
302
+ {
303
+ "name": "perfilhamento",
304
+ "days": 60,
305
+ "description": "Perfilhamento e estabelecimento",
306
+ "critical_water": False,
307
+ "tasks": [
308
+ {"type": "fertilize", "title": "Aplicar adubação de cobertura", "priority": "high", "weather_sensitive": True},
309
+ {"type": "inspect_pests", "title": "Monitorar plantas daninhas", "priority": "high", "weather_sensitive": False},
310
+ {"type": "irrigate", "title": "Irrigar moderadamente", "priority": "medium", "weather_sensitive": True}
311
+ ]
312
+ },
313
+ {
314
+ "name": "crescimento",
315
+ "days": 180,
316
+ "description": "Crescimento vegetativo intenso",
317
+ "critical_water": False,
318
+ "tasks": [
319
+ {"type": "inspect_pests", "title": "Monitorar pragas (broca, cigarrinha)", "priority": "high", "weather_sensitive": False},
320
+ {"type": "monitor_growth", "title": "Avaliar desenvolvimento", "priority": "medium", "weather_sensitive": False},
321
+ {"type": "irrigate", "title": "Irrigar conforme necessidade", "priority": "medium", "weather_sensitive": True}
322
+ ]
323
+ },
324
+ {
325
+ "name": "maturacao",
326
+ "days": 60,
327
+ "description": "Maturação e acúmulo de sacarose",
328
+ "critical_water": False,
329
+ "tasks": [
330
+ {"type": "monitor_growth", "title": "Monitorar maturação", "priority": "high", "weather_sensitive": False},
331
+ {"type": "harvest", "title": "Preparar colheita", "priority": "high", "weather_sensitive": True}
332
+ ]
333
+ },
334
+ {
335
+ "name": "colheita",
336
+ "days": 20,
337
+ "description": "Colheita",
338
+ "critical_water": False,
339
+ "tasks": [
340
+ {"type": "harvest", "title": "Colher cana", "priority": "critical", "weather_sensitive": True}
341
+ ]
342
+ }
343
+ ],
344
+ "optimal_temp_min": 20,
345
+ "optimal_temp_max": 35,
346
+ "critical_water_phases": ["plantio"],
347
+ "harvest_window_days": 30,
348
+ "category": "semi-perene",
349
+ "water_need": "alta",
350
+ "risk_notes": "Sensível a geadas. Requer manejo adequado de plantas daninhas.",
351
+ "calendar_notes": "Calendário para cana-planta (primeiro ciclo). Soqueiras têm ciclo diferente."
352
+ },
353
+ "arroz": {
354
+ "cycle_days": 120,
355
+ "phases": [
356
+ {
357
+ "name": "preparo",
358
+ "days": 10,
359
+ "description": "Preparo do solo e sistematização",
360
+ "critical_water": False,
361
+ "tasks": [
362
+ {"type": "prepare_soil", "title": "Preparar solo para plantio", "priority": "high", "weather_sensitive": False},
363
+ {"type": "fertilize", "title": "Aplicar adubação de base", "priority": "high", "weather_sensitive": True}
364
+ ]
365
+ },
366
+ {
367
+ "name": "germinacao",
368
+ "days": 15,
369
+ "description": "Germinação e emergência",
370
+ "critical_water": True,
371
+ "tasks": [
372
+ {"type": "plant", "title": "Semear arroz", "priority": "critical", "weather_sensitive": True},
373
+ {"type": "irrigate", "title": "Manter lâmina d'água", "priority": "critical", "weather_sensitive": True},
374
+ {"type": "inspect_pests", "title": "Monitorar emergência", "priority": "medium", "weather_sensitive": False}
375
+ ]
376
+ },
377
+ {
378
+ "name": "vegetativa",
379
+ "days": 40,
380
+ "description": "Crescimento vegetativo e perfilhamento",
381
+ "critical_water": True,
382
+ "tasks": [
383
+ {"type": "fertilize", "title": "Aplicar adubação nitrogenada", "priority": "high", "weather_sensitive": True},
384
+ {"type": "irrigate", "title": "Manter manejo hídrico", "priority": "high", "weather_sensitive": True},
385
+ {"type": "inspect_pests", "title": "Monitorar pragas e plantas daninhas", "priority": "high", "weather_sensitive": False}
386
+ ]
387
+ },
388
+ {
389
+ "name": "reproducao",
390
+ "days": 30,
391
+ "description": "Floração e formação de grãos",
392
+ "critical_water": True,
393
+ "tasks": [
394
+ {"type": "irrigate", "title": "Manter lâmina d'água - fase crítica", "priority": "critical", "weather_sensitive": True},
395
+ {"type": "inspect_diseases", "title": "Monitorar doenças (brusone)", "priority": "high", "weather_sensitive": False},
396
+ {"type": "monitor_growth", "title": "Avaliar floração", "priority": "medium", "weather_sensitive": False}
397
+ ]
398
+ },
399
+ {
400
+ "name": "maturacao",
401
+ "days": 25,
402
+ "description": "Maturação dos grãos",
403
+ "critical_water": False,
404
+ "tasks": [
405
+ {"type": "monitor_growth", "title": "Monitorar maturação", "priority": "high", "weather_sensitive": False},
406
+ {"type": "harvest", "title": "Preparar colheita", "priority": "high", "weather_sensitive": True}
407
+ ]
408
+ }
409
+ ],
410
+ "optimal_temp_min": 20,
411
+ "optimal_temp_max": 35,
412
+ "critical_water_phases": ["germinacao", "vegetativa", "reproducao"],
413
+ "harvest_window_days": 15,
414
+ "category": "anual",
415
+ "water_need": "muito_alta",
416
+ "risk_notes": "Requer manejo hídrico intensivo. Sensível a déficit hídrico.",
417
+ "calendar_notes": "Calendário para arroz irrigado. Arroz de sequeiro tem manejo diferente."
418
+ },
419
+ "trigo": {
420
+ "cycle_days": 120,
421
+ "phases": [
422
+ {
423
+ "name": "preparo",
424
+ "days": 10,
425
+ "description": "Preparo do solo",
426
+ "critical_water": False,
427
+ "tasks": [
428
+ {"type": "prepare_soil", "title": "Preparar solo para plantio", "priority": "high", "weather_sensitive": False},
429
+ {"type": "fertilize", "title": "Aplicar adubação de base", "priority": "high", "weather_sensitive": True}
430
+ ]
431
+ },
432
+ {
433
+ "name": "germinacao",
434
+ "days": 12,
435
+ "description": "Germinação e emergência",
436
+ "critical_water": True,
437
+ "tasks": [
438
+ {"type": "plant", "title": "Semear trigo", "priority": "critical", "weather_sensitive": True},
439
+ {"type": "irrigate", "title": "Irrigar se necessário", "priority": "high", "weather_sensitive": True},
440
+ {"type": "inspect_pests", "title": "Monitorar emergência", "priority": "medium", "weather_sensitive": False}
441
+ ]
442
+ },
443
+ {
444
+ "name": "perfilhamento",
445
+ "days": 35,
446
+ "description": "Perfilhamento",
447
+ "critical_water": False,
448
+ "tasks": [
449
+ {"type": "fertilize", "title": "Aplicar adubação nitrogenada", "priority": "high", "weather_sensitive": True},
450
+ {"type": "inspect_pests", "title": "Monitorar pragas (pulgão)", "priority": "high", "weather_sensitive": False},
451
+ {"type": "irrigate", "title": "Irrigar moderadamente", "priority": "medium", "weather_sensitive": True}
452
+ ]
453
+ },
454
+ {
455
+ "name": "espigamento",
456
+ "days": 28,
457
+ "description": "Espigamento e floração",
458
+ "critical_water": True,
459
+ "tasks": [
460
+ {"type": "irrigate", "title": "Irrigar - fase crítica", "priority": "critical", "weather_sensitive": True},
461
+ {"type": "inspect_diseases", "title": "Monitorar doenças foliares", "priority": "high", "weather_sensitive": False},
462
+ {"type": "monitor_growth", "title": "Avaliar floração", "priority": "medium", "weather_sensitive": False}
463
+ ]
464
+ },
465
+ {
466
+ "name": "enchimento_graos",
467
+ "days": 25,
468
+ "description": "Enchimento de grãos",
469
+ "critical_water": True,
470
+ "tasks": [
471
+ {"type": "irrigate", "title": "Irrigar - fase crítica", "priority": "critical", "weather_sensitive": True},
472
+ {"type": "monitor_growth", "title": "Monitorar desenvolvimento dos grãos", "priority": "high", "weather_sensitive": False}
473
+ ]
474
+ },
475
+ {
476
+ "name": "maturacao",
477
+ "days": 10,
478
+ "description": "Maturação",
479
+ "critical_water": False,
480
+ "tasks": [
481
+ {"type": "harvest", "title": "Preparar colheita", "priority": "high", "weather_sensitive": True},
482
+ {"type": "monitor_growth", "title": "Monitorar umidade dos grãos", "priority": "medium", "weather_sensitive": False}
483
+ ]
484
+ }
485
+ ],
486
+ "optimal_temp_min": 10,
487
+ "optimal_temp_max": 24,
488
+ "critical_water_phases": ["germinacao", "espigamento", "enchimento_graos"],
489
+ "harvest_window_days": 12,
490
+ "category": "anual",
491
+ "water_need": "media",
492
+ "risk_notes": "Sensível a chuvas excessivas na colheita. Requer clima ameno.",
493
+ "calendar_notes": "Calendário para trigo de inverno. Adaptar conforme região e cultivar."
494
+ },
495
+ "sorgo": {
496
+ "cycle_days": 110,
497
+ "phases": [
498
+ {
499
+ "name": "preparo",
500
+ "days": 8,
501
+ "description": "Preparo do solo",
502
+ "critical_water": False,
503
+ "tasks": [
504
+ {"type": "prepare_soil", "title": "Preparar solo para plantio", "priority": "high", "weather_sensitive": False},
505
+ {"type": "fertilize", "title": "Aplicar adubação de base", "priority": "high", "weather_sensitive": True}
506
+ ]
507
+ },
508
+ {
509
+ "name": "germinacao",
510
+ "days": 10,
511
+ "description": "Germinação e emergência",
512
+ "critical_water": True,
513
+ "tasks": [
514
+ {"type": "plant", "title": "Semear sorgo", "priority": "critical", "weather_sensitive": True},
515
+ {"type": "irrigate", "title": "Irrigar se necessário", "priority": "high", "weather_sensitive": True},
516
+ {"type": "inspect_pests", "title": "Monitorar emergência", "priority": "medium", "weather_sensitive": False}
517
+ ]
518
+ },
519
+ {
520
+ "name": "vegetativa",
521
+ "days": 40,
522
+ "description": "Crescimento vegetativo",
523
+ "critical_water": False,
524
+ "tasks": [
525
+ {"type": "fertilize", "title": "Aplicar adubação nitrogenada", "priority": "high", "weather_sensitive": True},
526
+ {"type": "inspect_pests", "title": "Monitorar pragas (pulgão, lagarta)", "priority": "high", "weather_sensitive": False},
527
+ {"type": "irrigate", "title": "Irrigar moderadamente", "priority": "medium", "weather_sensitive": True}
528
+ ]
529
+ },
530
+ {
531
+ "name": "florescimento",
532
+ "days": 22,
533
+ "description": "Florescimento e polinização",
534
+ "critical_water": True,
535
+ "tasks": [
536
+ {"type": "irrigate", "title": "Irrigar - fase crítica", "priority": "critical", "weather_sensitive": True},
537
+ {"type": "monitor_growth", "title": "Avaliar floração", "priority": "medium", "weather_sensitive": False}
538
+ ]
539
+ },
540
+ {
541
+ "name": "enchimento_graos",
542
+ "days": 20,
543
+ "description": "Enchimento de grãos",
544
+ "critical_water": True,
545
+ "tasks": [
546
+ {"type": "irrigate", "title": "Irrigar - fase crítica", "priority": "critical", "weather_sensitive": True},
547
+ {"type": "monitor_growth", "title": "Monitorar desenvolvimento dos grãos", "priority": "high", "weather_sensitive": False}
548
+ ]
549
+ },
550
+ {
551
+ "name": "maturacao",
552
+ "days": 10,
553
+ "description": "Maturação",
554
+ "critical_water": False,
555
+ "tasks": [
556
+ {"type": "harvest", "title": "Preparar colheita", "priority": "high", "weather_sensitive": True},
557
+ {"type": "monitor_growth", "title": "Monitorar umidade dos grãos", "priority": "medium", "weather_sensitive": False}
558
+ ]
559
+ }
560
+ ],
561
+ "optimal_temp_min": 21,
562
+ "optimal_temp_max": 35,
563
+ "critical_water_phases": ["germinacao", "florescimento", "enchimento_graos"],
564
+ "harvest_window_days": 15,
565
+ "category": "anual",
566
+ "water_need": "baixa",
567
+ "risk_notes": "Tolerante à seca. Boa opção para regiões com déficit hídrico.",
568
+ "calendar_notes": "Calendário para sorgo granífero. Sorgo forrageiro tem manejo diferente."
569
+ },
570
+ "mandioca": {
571
+ "cycle_days": 300,
572
+ "phases": [
573
+ {
574
+ "name": "preparo",
575
+ "days": 15,
576
+ "description": "Preparo do solo e seleção de manivas",
577
+ "critical_water": False,
578
+ "tasks": [
579
+ {"type": "prepare_soil", "title": "Preparar solo para plantio", "priority": "high", "weather_sensitive": False},
580
+ {"type": "fertilize", "title": "Aplicar adubação de base", "priority": "high", "weather_sensitive": True}
581
+ ]
582
+ },
583
+ {
584
+ "name": "plantio",
585
+ "days": 30,
586
+ "description": "Plantio de manivas e brotação",
587
+ "critical_water": True,
588
+ "tasks": [
589
+ {"type": "plant", "title": "Plantar manivas", "priority": "critical", "weather_sensitive": True},
590
+ {"type": "irrigate", "title": "Irrigar para brotação", "priority": "high", "weather_sensitive": True},
591
+ {"type": "inspect_pests", "title": "Monitorar brotação", "priority": "medium", "weather_sensitive": False}
592
+ ]
593
+ },
594
+ {
595
+ "name": "estabelecimento",
596
+ "days": 60,
597
+ "description": "Estabelecimento e crescimento inicial",
598
+ "critical_water": False,
599
+ "tasks": [
600
+ {"type": "inspect_pests", "title": "Controlar plantas daninhas", "priority": "high", "weather_sensitive": False},
601
+ {"type": "fertilize", "title": "Aplicar adubação de cobertura", "priority": "medium", "weather_sensitive": True},
602
+ {"type": "irrigate", "title": "Irrigar se necessário", "priority": "medium", "weather_sensitive": True}
603
+ ]
604
+ },
605
+ {
606
+ "name": "desenvolvimento",
607
+ "days": 120,
608
+ "description": "Desenvolvimento vegetativo e formação de raízes",
609
+ "critical_water": False,
610
+ "tasks": [
611
+ {"type": "inspect_pests", "title": "Monitorar pragas (mandarová, ácaros)", "priority": "high", "weather_sensitive": False},
612
+ {"type": "monitor_growth", "title": "Avaliar desenvolvimento", "priority": "medium", "weather_sensitive": False},
613
+ {"type": "irrigate", "title": "Irrigar conforme necessidade", "priority": "low", "weather_sensitive": True}
614
+ ]
615
+ },
616
+ {
617
+ "name": "engrossamento",
618
+ "days": 60,
619
+ "description": "Engrossamento das raízes",
620
+ "critical_water": False,
621
+ "tasks": [
622
+ {"type": "monitor_growth", "title": "Avaliar desenvolvimento das raízes", "priority": "high", "weather_sensitive": False},
623
+ {"type": "inspect_pests", "title": "Monitorar sanidade", "priority": "medium", "weather_sensitive": False}
624
+ ]
625
+ },
626
+ {
627
+ "name": "colheita",
628
+ "days": 15,
629
+ "description": "Colheita",
630
+ "critical_water": False,
631
+ "tasks": [
632
+ {"type": "harvest", "title": "Colher mandioca", "priority": "high", "weather_sensitive": True}
633
+ ]
634
+ }
635
+ ],
636
+ "optimal_temp_min": 20,
637
+ "optimal_temp_max": 35,
638
+ "critical_water_phases": ["plantio"],
639
+ "harvest_window_days": 60,
640
+ "category": "anual",
641
+ "water_need": "baixa",
642
+ "risk_notes": "Tolerante à seca após estabelecimento. Sensível a encharcamento.",
643
+ "calendar_notes": "Calendário para mandioca de mesa. Mandioca industrial pode ter ciclo mais longo."
644
+ },
645
+ "algodao": {
646
+ "cycle_days": 180,
647
+ "phases": [
648
+ {
649
+ "name": "preparo",
650
+ "days": 10,
651
+ "description": "Preparo do solo",
652
+ "critical_water": False,
653
+ "tasks": [
654
+ {"type": "prepare_soil", "title": "Preparar solo para plantio", "priority": "high", "weather_sensitive": False},
655
+ {"type": "fertilize", "title": "Aplicar adubação de base", "priority": "high", "weather_sensitive": True}
656
+ ]
657
+ },
658
+ {
659
+ "name": "germinacao",
660
+ "days": 12,
661
+ "description": "Germinação e emergência",
662
+ "critical_water": True,
663
+ "tasks": [
664
+ {"type": "plant", "title": "Semear algodão", "priority": "critical", "weather_sensitive": True},
665
+ {"type": "irrigate", "title": "Irrigar para emergência", "priority": "high", "weather_sensitive": True},
666
+ {"type": "inspect_pests", "title": "Monitorar emergência", "priority": "medium", "weather_sensitive": False}
667
+ ]
668
+ },
669
+ {
670
+ "name": "vegetativa",
671
+ "days": 50,
672
+ "description": "Crescimento vegetativo",
673
+ "critical_water": False,
674
+ "tasks": [
675
+ {"type": "fertilize", "title": "Aplicar adubação nitrogenada", "priority": "high", "weather_sensitive": True},
676
+ {"type": "inspect_pests", "title": "Monitorar pragas (bicudo, lagarta)", "priority": "high", "weather_sensitive": False},
677
+ {"type": "irrigate", "title": "Irrigar moderadamente", "priority": "medium", "weather_sensitive": True}
678
+ ]
679
+ },
680
+ {
681
+ "name": "florescimento",
682
+ "days": 40,
683
+ "description": "Florescimento",
684
+ "critical_water": True,
685
+ "tasks": [
686
+ {"type": "irrigate", "title": "Irrigar - fase crítica", "priority": "critical", "weather_sensitive": True},
687
+ {"type": "inspect_diseases", "title": "Monitorar doenças (ramulária)", "priority": "high", "weather_sensitive": False},
688
+ {"type": "monitor_growth", "title": "Avaliar floração", "priority": "medium", "weather_sensitive": False}
689
+ ]
690
+ },
691
+ {
692
+ "name": "formacao_macas",
693
+ "days": 48,
694
+ "description": "Formação e abertura de maçãs",
695
+ "critical_water": True,
696
+ "tasks": [
697
+ {"type": "irrigate", "title": "Irrigar - fase crítica", "priority": "critical", "weather_sensitive": True},
698
+ {"type": "inspect_pests", "title": "Monitorar pragas nas maçãs", "priority": "high", "weather_sensitive": False},
699
+ {"type": "monitor_growth", "title": "Avaliar desenvolvimento das maçãs", "priority": "high", "weather_sensitive": False}
700
+ ]
701
+ },
702
+ {
703
+ "name": "maturacao",
704
+ "days": 20,
705
+ "description": "Maturação e abertura dos capulhos",
706
+ "critical_water": False,
707
+ "tasks": [
708
+ {"type": "monitor_growth", "title": "Monitorar abertura dos capulhos", "priority": "high", "weather_sensitive": False},
709
+ {"type": "harvest", "title": "Preparar colheita", "priority": "high", "weather_sensitive": True}
710
+ ]
711
+ }
712
+ ],
713
+ "optimal_temp_min": 20,
714
+ "optimal_temp_max": 30,
715
+ "critical_water_phases": ["germinacao", "florescimento", "formacao_macas"],
716
+ "harvest_window_days": 30,
717
+ "category": "anual",
718
+ "water_need": "media",
719
+ "risk_notes": "Sensível a pragas. Requer manejo fitossanitário intensivo.",
720
+ "calendar_notes": "Calendário para algodão herbáceo. Requer monitoramento constante de pragas."
210
721
  }
211
722
  }
212
723
 
@@ -377,6 +888,10 @@ def gerar_calendario_cultura(
377
888
  "optimal_temp_max": cycle_data["optimal_temp_max"],
378
889
  "critical_water_phases": cycle_data["critical_water_phases"],
379
890
  "harvest_window_days": cycle_data["harvest_window_days"],
891
+ "category": cycle_data.get("category", "anual"),
892
+ "water_need": cycle_data.get("water_need", "media"),
893
+ "risk_notes": cycle_data.get("risk_notes", ""),
894
+ "calendar_notes": cycle_data.get("calendar_notes", ""),
380
895
  "phases": [
381
896
  {
382
897
  "name": phase["name"],
@@ -390,7 +905,8 @@ def gerar_calendario_cultura(
390
905
  "tasks": [task.to_dict() for task in tasks],
391
906
  "total_tasks": len(tasks),
392
907
  "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)
908
+ "critical_tasks": sum(1 for task in tasks if task.priority == TaskPriority.CRITICAL),
909
+ "cautela": "Este calendário é uma base inicial de planejamento. As datas e tarefas devem ser ajustadas conforme clima, solo, cultivar, manejo e orientação técnica."
394
910
  }
395
911
 
396
912
 
@@ -0,0 +1,175 @@
1
+ """
2
+ Storage simples para talhões do usuário
3
+
4
+ Persistência em JSON local:
5
+ - API Local: dados persistem no PC do usuário (~/.agroplan/backend/data/user_fields/)
6
+ - API Render: dados temporários/voláteis (perdem-se ao reiniciar)
7
+
8
+ Fase futura: Migrar para banco de dados PostgreSQL
9
+ """
10
+
11
+ import json
12
+ import os
13
+ import uuid
14
+ from datetime import datetime
15
+ from typing import List, Dict, Optional
16
+ from pathlib import Path
17
+
18
+
19
+ # Diretório de dados
20
+ DATA_DIR = Path(__file__).parent.parent / "data" / "user_fields"
21
+ FIELDS_FILE = DATA_DIR / "fields.json"
22
+
23
+
24
+ def _ensure_data_dir():
25
+ """Garante que o diretório de dados existe"""
26
+ DATA_DIR.mkdir(parents=True, exist_ok=True)
27
+
28
+ # Criar arquivo vazio se não existir
29
+ if not FIELDS_FILE.exists():
30
+ with open(FIELDS_FILE, 'w', encoding='utf-8') as f:
31
+ json.dump([], f)
32
+
33
+
34
+ def _load_fields() -> List[Dict]:
35
+ """Carrega todos os talhões do arquivo JSON"""
36
+ _ensure_data_dir()
37
+
38
+ try:
39
+ with open(FIELDS_FILE, 'r', encoding='utf-8') as f:
40
+ return json.load(f)
41
+ except (json.JSONDecodeError, FileNotFoundError):
42
+ return []
43
+
44
+
45
+ def _save_fields(fields: List[Dict]):
46
+ """
47
+ Salva talhões no arquivo JSON com escrita segura.
48
+
49
+ Usa arquivo temporário + rename para evitar corrupção.
50
+ """
51
+ _ensure_data_dir()
52
+
53
+ # Escrever em arquivo temporário
54
+ temp_file = FIELDS_FILE.with_suffix('.tmp')
55
+
56
+ try:
57
+ with open(temp_file, 'w', encoding='utf-8') as f:
58
+ json.dump(fields, f, indent=2, ensure_ascii=False)
59
+
60
+ # Renomear atomicamente
61
+ temp_file.replace(FIELDS_FILE)
62
+ except Exception as e:
63
+ # Limpar arquivo temporário em caso de erro
64
+ if temp_file.exists():
65
+ temp_file.unlink()
66
+ raise e
67
+
68
+
69
+ def listar_talhoes_usuario() -> List[Dict]:
70
+ """
71
+ Lista todos os talhões do usuário.
72
+
73
+ Returns:
74
+ Lista de talhões
75
+ """
76
+ return _load_fields()
77
+
78
+
79
+ def criar_talhao_usuario(data: Dict) -> Dict:
80
+ """
81
+ Cria um novo talhão.
82
+
83
+ Args:
84
+ data: Dados do talhão (sem id, created_at, updated_at)
85
+
86
+ Returns:
87
+ Talhão criado com id e timestamps
88
+ """
89
+ fields = _load_fields()
90
+
91
+ # Gerar novo talhão
92
+ now = datetime.now().isoformat()
93
+ new_field = {
94
+ "id": str(uuid.uuid4()),
95
+ **data,
96
+ "created_at": now,
97
+ "updated_at": now
98
+ }
99
+
100
+ fields.append(new_field)
101
+ _save_fields(fields)
102
+
103
+ return new_field
104
+
105
+
106
+ def obter_talhao_usuario(field_id: str) -> Optional[Dict]:
107
+ """
108
+ Obtém um talhão pelo ID.
109
+
110
+ Args:
111
+ field_id: ID do talhão
112
+
113
+ Returns:
114
+ Talhão ou None se não encontrado
115
+ """
116
+ fields = _load_fields()
117
+
118
+ for field in fields:
119
+ if field.get("id") == field_id:
120
+ return field
121
+
122
+ return None
123
+
124
+
125
+ def atualizar_talhao_usuario(field_id: str, data: Dict) -> Optional[Dict]:
126
+ """
127
+ Atualiza um talhão existente.
128
+
129
+ Args:
130
+ field_id: ID do talhão
131
+ data: Novos dados (sem id, created_at, updated_at)
132
+
133
+ Returns:
134
+ Talhão atualizado ou None se não encontrado
135
+ """
136
+ fields = _load_fields()
137
+
138
+ for i, field in enumerate(fields):
139
+ if field.get("id") == field_id:
140
+ # Preservar id e created_at, atualizar updated_at
141
+ updated_field = {
142
+ "id": field["id"],
143
+ **data,
144
+ "created_at": field["created_at"],
145
+ "updated_at": datetime.now().isoformat()
146
+ }
147
+
148
+ fields[i] = updated_field
149
+ _save_fields(fields)
150
+
151
+ return updated_field
152
+
153
+ return None
154
+
155
+
156
+ def remover_talhao_usuario(field_id: str) -> bool:
157
+ """
158
+ Remove um talhão.
159
+
160
+ Args:
161
+ field_id: ID do talhão
162
+
163
+ Returns:
164
+ True se removido, False se não encontrado
165
+ """
166
+ fields = _load_fields()
167
+
168
+ original_length = len(fields)
169
+ fields = [f for f in fields if f.get("id") != field_id]
170
+
171
+ if len(fields) < original_length:
172
+ _save_fields(fields)
173
+ return True
174
+
175
+ return False
@@ -11,12 +11,18 @@ Define as entidades principais do sistema de planejamento agrícola:
11
11
  - UserObservation: Observação do usuário
12
12
  - Intervention: Intervenção/replanejamento
13
13
  - PlanningSession: Sessão de planejamento
14
+
15
+ Modelos Pydantic para API:
16
+ - ManualFieldCreate: Criação de talhão manual
17
+ - ManualFieldUpdate: Atualização de talhão manual
18
+ - ManualFieldResponse: Resposta de talhão manual
14
19
  """
15
20
 
16
21
  from dataclasses import dataclass, field
17
22
  from datetime import date, datetime
18
23
  from typing import Optional, List, Dict
19
24
  from enum import Enum
25
+ from pydantic import BaseModel, Field as PydanticField, field_validator
20
26
 
21
27
 
22
28
  # Enums para tipos padronizados
@@ -364,3 +370,115 @@ class PlanningSession:
364
370
  "cultures_recommended": self.cultures_recommended,
365
371
  "created_at": self.created_at.isoformat()
366
372
  }
373
+
374
+
375
+ # Modelos Pydantic para API
376
+
377
+ class ManualFieldCreate(BaseModel):
378
+ """Modelo para criação de talhão manual"""
379
+ name: str = PydanticField(..., min_length=1, max_length=100, description="Nome do talhão")
380
+ area_ha: float = PydanticField(..., gt=0, description="Área em hectares")
381
+ soil_type: str = PydanticField(..., description="Tipo de solo")
382
+ slope: str = PydanticField(..., description="Tipo de relevo")
383
+ water_availability: str = PydanticField(..., description="Disponibilidade de água")
384
+ uf: Optional[str] = PydanticField(None, min_length=2, max_length=2, description="UF")
385
+ municipio: Optional[str] = PydanticField(None, max_length=100, description="Município")
386
+ lat: Optional[float] = PydanticField(None, ge=-90, le=90, description="Latitude")
387
+ lon: Optional[float] = PydanticField(None, ge=-180, le=180, description="Longitude")
388
+
389
+ @field_validator('soil_type')
390
+ @classmethod
391
+ def validate_soil_type(cls, v: str) -> str:
392
+ allowed = ['argiloso', 'arenoso', 'misto', 'siltoso']
393
+ if v not in allowed:
394
+ raise ValueError(f'soil_type deve ser um de: {", ".join(allowed)}')
395
+ return v
396
+
397
+ @field_validator('slope')
398
+ @classmethod
399
+ def validate_slope(cls, v: str) -> str:
400
+ allowed = ['plano', 'suave', 'moderado', 'ingreme']
401
+ if v not in allowed:
402
+ raise ValueError(f'slope deve ser um de: {", ".join(allowed)}')
403
+ return v
404
+
405
+ @field_validator('water_availability')
406
+ @classmethod
407
+ def validate_water_availability(cls, v: str) -> str:
408
+ allowed = ['baixa', 'media', 'alta']
409
+ if v not in allowed:
410
+ raise ValueError(f'water_availability deve ser um de: {", ".join(allowed)}')
411
+ return v
412
+
413
+
414
+ class ManualFieldUpdate(BaseModel):
415
+ """Modelo para atualização de talhão manual"""
416
+ name: Optional[str] = PydanticField(None, min_length=1, max_length=100)
417
+ area_ha: Optional[float] = PydanticField(None, gt=0)
418
+ soil_type: Optional[str] = None
419
+ slope: Optional[str] = None
420
+ water_availability: Optional[str] = None
421
+ uf: Optional[str] = PydanticField(None, min_length=2, max_length=2)
422
+ municipio: Optional[str] = PydanticField(None, max_length=100)
423
+ lat: Optional[float] = PydanticField(None, ge=-90, le=90)
424
+ lon: Optional[float] = PydanticField(None, ge=-180, le=180)
425
+
426
+ @field_validator('soil_type')
427
+ @classmethod
428
+ def validate_soil_type(cls, v: Optional[str]) -> Optional[str]:
429
+ if v is not None:
430
+ allowed = ['argiloso', 'arenoso', 'misto', 'siltoso']
431
+ if v not in allowed:
432
+ raise ValueError(f'soil_type deve ser um de: {", ".join(allowed)}')
433
+ return v
434
+
435
+ @field_validator('slope')
436
+ @classmethod
437
+ def validate_slope(cls, v: Optional[str]) -> Optional[str]:
438
+ if v is not None:
439
+ allowed = ['plano', 'suave', 'moderado', 'ingreme']
440
+ if v not in allowed:
441
+ raise ValueError(f'slope deve ser um de: {", ".join(allowed)}')
442
+ return v
443
+
444
+ @field_validator('water_availability')
445
+ @classmethod
446
+ def validate_water_availability(cls, v: Optional[str]) -> Optional[str]:
447
+ if v is not None:
448
+ allowed = ['baixa', 'media', 'alta']
449
+ if v not in allowed:
450
+ raise ValueError(f'water_availability deve ser um de: {", ".join(allowed)}')
451
+ return v
452
+
453
+
454
+ class ManualFieldResponse(BaseModel):
455
+ """Modelo de resposta de talhão manual"""
456
+ id: str
457
+ name: str
458
+ area_ha: float
459
+ soil_type: str
460
+ slope: str
461
+ water_availability: str
462
+ uf: Optional[str] = None
463
+ municipio: Optional[str] = None
464
+ lat: Optional[float] = None
465
+ lon: Optional[float] = None
466
+ created_at: str
467
+ updated_at: str
468
+
469
+ class Config:
470
+ from_attributes = True
471
+
472
+
473
+ class GenerateCalendarRequest(BaseModel):
474
+ """Modelo para geração de calendário"""
475
+ cultura: str = PydanticField(..., description="Nome da cultura")
476
+ planting_date: str = PydanticField(..., description="Data de plantio (YYYY-MM-DD)")
477
+
478
+ @field_validator('cultura')
479
+ @classmethod
480
+ def validate_cultura(cls, v: str) -> str:
481
+ allowed = ['soja', 'milho', 'feijao']
482
+ if v not in allowed:
483
+ raise ValueError(f'cultura deve ser uma de: {", ".join(allowed)}')
484
+ return v
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agroplan-ai-cli",
3
- "version": "1.0.30",
3
+ "version": "1.0.32",
4
4
  "description": "CLI global para AgroPlan AI - modo local acelerado",
5
5
  "type": "module",
6
6
  "bin": {