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.
@@ -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,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