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,484 @@
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
+ 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
19
+ """
20
+
21
+ from dataclasses import dataclass, field
22
+ from datetime import date, datetime
23
+ from typing import Optional, List, Dict
24
+ from enum import Enum
25
+ from pydantic import BaseModel, Field as PydanticField, field_validator
26
+
27
+
28
+ # Enums para tipos padronizados
29
+
30
+ class SoilType(str, Enum):
31
+ """Tipos de solo"""
32
+ ARGILOSO = "argiloso"
33
+ ARENOSO = "arenoso"
34
+ MISTO = "misto"
35
+ SILTOSO = "siltoso"
36
+
37
+
38
+ class Slope(str, Enum):
39
+ """Tipos de relevo"""
40
+ PLANO = "plano"
41
+ LEVE = "leve"
42
+ MEDIO = "medio"
43
+ INGREME = "ingreme"
44
+
45
+
46
+ class WaterAvailability(str, Enum):
47
+ """Disponibilidade de água"""
48
+ BAIXA = "baixa"
49
+ MEDIA = "media"
50
+ ALTA = "alta"
51
+
52
+
53
+ class Objective(str, Enum):
54
+ """Objetivos de otimização"""
55
+ EQUILIBRADO = "equilibrado"
56
+ LUCRO = "lucro"
57
+ RISCO = "risco"
58
+ SUSTENTAVEL = "sustentavel"
59
+
60
+
61
+ class PlanStatus(str, Enum):
62
+ """Status do plano de cultura"""
63
+ PLANNED = "planned"
64
+ ACTIVE = "active"
65
+ COMPLETED = "completed"
66
+ CANCELLED = "cancelled"
67
+
68
+
69
+ class TaskType(str, Enum):
70
+ """Tipos de tarefa"""
71
+ PREPARE_SOIL = "prepare_soil"
72
+ PLANT = "plant"
73
+ IRRIGATE = "irrigate"
74
+ FERTILIZE = "fertilize"
75
+ INSPECT_PESTS = "inspect_pests"
76
+ INSPECT_DISEASES = "inspect_diseases"
77
+ MONITOR_GROWTH = "monitor_growth"
78
+ HARVEST = "harvest"
79
+
80
+
81
+ class TaskPriority(str, Enum):
82
+ """Prioridade da tarefa"""
83
+ LOW = "low"
84
+ MEDIUM = "medium"
85
+ HIGH = "high"
86
+ CRITICAL = "critical"
87
+
88
+
89
+ class TaskStatus(str, Enum):
90
+ """Status da tarefa"""
91
+ PENDING = "pending"
92
+ COMPLETED = "completed"
93
+ SKIPPED = "skipped"
94
+ RESCHEDULED = "rescheduled"
95
+
96
+
97
+ class AlertType(str, Enum):
98
+ """Tipos de alerta climático"""
99
+ RAIN = "rain"
100
+ DROUGHT = "drought"
101
+ HEAT = "heat"
102
+ COLD = "cold"
103
+ FROST = "frost"
104
+
105
+
106
+ class AlertSeverity(str, Enum):
107
+ """Severidade do alerta"""
108
+ INFO = "info"
109
+ WARNING = "warning"
110
+ CRITICAL = "critical"
111
+
112
+
113
+ class InterventionReason(str, Enum):
114
+ """Razão da intervenção"""
115
+ MISSED_TASK = "missed_task"
116
+ WEATHER_EVENT = "weather_event"
117
+ SOIL_CONDITION = "soil_condition"
118
+ USER_REQUEST = "user_request"
119
+
120
+
121
+ class PlanningMode(str, Enum):
122
+ """Modo de planejamento"""
123
+ GUIDED = "guided"
124
+ ADVANCED = "advanced"
125
+
126
+
127
+ # Modelos de Domínio
128
+
129
+ @dataclass
130
+ class Property:
131
+ """Propriedade rural"""
132
+ id: str
133
+ name: str
134
+ uf: str
135
+ municipio: str
136
+ lat: Optional[float] = None
137
+ lon: Optional[float] = None
138
+ created_at: datetime = field(default_factory=datetime.now)
139
+ updated_at: datetime = field(default_factory=datetime.now)
140
+
141
+ def to_dict(self) -> Dict:
142
+ return {
143
+ "id": self.id,
144
+ "name": self.name,
145
+ "uf": self.uf,
146
+ "municipio": self.municipio,
147
+ "lat": self.lat,
148
+ "lon": self.lon,
149
+ "created_at": self.created_at.isoformat(),
150
+ "updated_at": self.updated_at.isoformat()
151
+ }
152
+
153
+
154
+ @dataclass
155
+ class Field:
156
+ """Talhão/campo"""
157
+ id: str
158
+ property_id: str
159
+ name: str
160
+ area_ha: float
161
+ soil_type: SoilType
162
+ slope: Slope
163
+ water_availability: WaterAvailability
164
+ geometry: Optional[Dict] = None # GeoJSON para mapa
165
+ created_at: datetime = field(default_factory=datetime.now)
166
+
167
+ def to_dict(self) -> Dict:
168
+ return {
169
+ "id": self.id,
170
+ "property_id": self.property_id,
171
+ "name": self.name,
172
+ "area_ha": self.area_ha,
173
+ "soil_type": self.soil_type.value,
174
+ "slope": self.slope.value,
175
+ "water_availability": self.water_availability.value,
176
+ "geometry": self.geometry,
177
+ "created_at": self.created_at.isoformat()
178
+ }
179
+
180
+
181
+ @dataclass
182
+ class CropPlan:
183
+ """Plano de cultura para um talhão"""
184
+ id: str
185
+ field_id: str
186
+ culture: str
187
+ planting_date: date
188
+ estimated_harvest_date: date
189
+ objective: Objective
190
+ status: PlanStatus = PlanStatus.PLANNED
191
+ created_at: datetime = field(default_factory=datetime.now)
192
+
193
+ def to_dict(self) -> Dict:
194
+ return {
195
+ "id": self.id,
196
+ "field_id": self.field_id,
197
+ "culture": self.culture,
198
+ "planting_date": self.planting_date.isoformat(),
199
+ "estimated_harvest_date": self.estimated_harvest_date.isoformat(),
200
+ "objective": self.objective.value,
201
+ "status": self.status.value,
202
+ "created_at": self.created_at.isoformat()
203
+ }
204
+
205
+
206
+ @dataclass
207
+ class CropPhase:
208
+ """Fase do ciclo da cultura"""
209
+ name: str
210
+ days: int
211
+ description: str
212
+ critical_water: bool
213
+ tasks: List[str]
214
+
215
+
216
+ @dataclass
217
+ class CropCycle:
218
+ """Ciclo completo de uma cultura"""
219
+ culture: str
220
+ cycle_days: int
221
+ phases: List[CropPhase]
222
+ critical_water_phases: List[str]
223
+ optimal_temp_min: float
224
+ optimal_temp_max: float
225
+ harvest_window_days: int
226
+
227
+ def to_dict(self) -> Dict:
228
+ return {
229
+ "culture": self.culture,
230
+ "cycle_days": self.cycle_days,
231
+ "phases": [
232
+ {
233
+ "name": phase.name,
234
+ "days": phase.days,
235
+ "description": phase.description,
236
+ "critical_water": phase.critical_water,
237
+ "tasks": phase.tasks
238
+ }
239
+ for phase in self.phases
240
+ ],
241
+ "critical_water_phases": self.critical_water_phases,
242
+ "optimal_temp_min": self.optimal_temp_min,
243
+ "optimal_temp_max": self.optimal_temp_max,
244
+ "harvest_window_days": self.harvest_window_days
245
+ }
246
+
247
+
248
+ @dataclass
249
+ class CalendarTask:
250
+ """Tarefa do calendário agrícola"""
251
+ id: str
252
+ crop_plan_id: str
253
+ date: date
254
+ type: TaskType
255
+ title: str
256
+ description: str
257
+ priority: TaskPriority
258
+ source: str # system, user, weather_alert
259
+ status: TaskStatus = TaskStatus.PENDING
260
+ weather_sensitive: bool = False
261
+ completed_at: Optional[datetime] = None
262
+
263
+ def to_dict(self) -> Dict:
264
+ return {
265
+ "id": self.id,
266
+ "crop_plan_id": self.crop_plan_id,
267
+ "date": self.date.isoformat(),
268
+ "type": self.type.value,
269
+ "title": self.title,
270
+ "description": self.description,
271
+ "priority": self.priority.value,
272
+ "source": self.source,
273
+ "status": self.status.value,
274
+ "weather_sensitive": self.weather_sensitive,
275
+ "completed_at": self.completed_at.isoformat() if self.completed_at else None
276
+ }
277
+
278
+
279
+ @dataclass
280
+ class WeatherAlert:
281
+ """Alerta climático"""
282
+ id: str
283
+ crop_plan_id: str
284
+ date: date
285
+ alert_type: AlertType
286
+ severity: AlertSeverity
287
+ message: str
288
+ action_suggested: Optional[str] = None
289
+ created_at: datetime = field(default_factory=datetime.now)
290
+
291
+ def to_dict(self) -> Dict:
292
+ return {
293
+ "id": self.id,
294
+ "crop_plan_id": self.crop_plan_id,
295
+ "date": self.date.isoformat(),
296
+ "alert_type": self.alert_type.value,
297
+ "severity": self.severity.value,
298
+ "message": self.message,
299
+ "action_suggested": self.action_suggested,
300
+ "created_at": self.created_at.isoformat()
301
+ }
302
+
303
+
304
+ @dataclass
305
+ class UserObservation:
306
+ """Observação do usuário sobre o cultivo"""
307
+ id: str
308
+ crop_plan_id: str
309
+ date: date
310
+ note: str
311
+ impact: str # positive, neutral, negative
312
+ created_at: datetime = field(default_factory=datetime.now)
313
+
314
+ def to_dict(self) -> Dict:
315
+ return {
316
+ "id": self.id,
317
+ "crop_plan_id": self.crop_plan_id,
318
+ "date": self.date.isoformat(),
319
+ "note": self.note,
320
+ "impact": self.impact,
321
+ "created_at": self.created_at.isoformat()
322
+ }
323
+
324
+
325
+ @dataclass
326
+ class Intervention:
327
+ """Intervenção/replanejamento"""
328
+ id: str
329
+ crop_plan_id: str
330
+ reason: InterventionReason
331
+ suggested_action: str
332
+ original_task_id: Optional[str] = None
333
+ new_date: Optional[date] = None
334
+ risk_adjustment: float = 0.0
335
+ created_at: datetime = field(default_factory=datetime.now)
336
+ applied: bool = False
337
+
338
+ def to_dict(self) -> Dict:
339
+ return {
340
+ "id": self.id,
341
+ "crop_plan_id": self.crop_plan_id,
342
+ "original_task_id": self.original_task_id,
343
+ "reason": self.reason.value,
344
+ "suggested_action": self.suggested_action,
345
+ "new_date": self.new_date.isoformat() if self.new_date else None,
346
+ "risk_adjustment": self.risk_adjustment,
347
+ "created_at": self.created_at.isoformat(),
348
+ "applied": self.applied
349
+ }
350
+
351
+
352
+ @dataclass
353
+ class PlanningSession:
354
+ """Sessão de planejamento"""
355
+ id: str
356
+ property_id: str
357
+ mode: PlanningMode
358
+ objective: Objective
359
+ fields_count: int
360
+ cultures_recommended: List[str]
361
+ created_at: datetime = field(default_factory=datetime.now)
362
+
363
+ def to_dict(self) -> Dict:
364
+ return {
365
+ "id": self.id,
366
+ "property_id": self.property_id,
367
+ "mode": self.mode.value,
368
+ "objective": self.objective.value,
369
+ "fields_count": self.fields_count,
370
+ "cultures_recommended": self.cultures_recommended,
371
+ "created_at": self.created_at.isoformat()
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.29",
3
+ "version": "1.0.31",
4
4
  "description": "CLI global para AgroPlan AI - modo local acelerado",
5
5
  "type": "module",
6
6
  "bin": {