agroplan-ai-cli 1.0.7 → 1.0.8

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.
@@ -12,8 +12,13 @@ CORS_ORIGINS=http://localhost:3000,http://127.0.0.1:3000
12
12
 
13
13
  # Data Mode
14
14
  # simulated: Uses CSV files with simulated data
15
- # real: Uses real APIs (future implementation)
16
- DATA_MODE=simulated
15
+ # real: Uses real APIs only
16
+ # hybrid: Uses real APIs with fallback to simulated data
17
+ DATA_MODE=hybrid
18
+
19
+ # Weather Provider Configuration
20
+ WEATHER_PROVIDER=open-meteo
21
+ PROVIDER_CACHE_TTL=3600
17
22
 
18
23
  # Cache Administration (optional)
19
24
  # If set, /cache/limpar endpoint requires X-Cache-Token header
@@ -2,7 +2,7 @@
2
2
  FastAPI Backend para AgroPlan AI
3
3
  """
4
4
 
5
- from fastapi import FastAPI, HTTPException, Request
5
+ from fastapi import FastAPI, HTTPException, Request, Query
6
6
  from fastapi.middleware.cors import CORSMiddleware
7
7
  from pydantic import BaseModel
8
8
  from typing import Optional
@@ -17,10 +17,17 @@ from core.planner import gerar_cenarios, gerar_plano_genetico
17
17
  from core.bruteforce_validator import comparar_ag_com_forca_bruta, executar_multiplas_rodadas
18
18
  from core.report_generator import gerar_relatorio_completo
19
19
 
20
+ # Importar provedores de dados reais
21
+ from providers.weather_provider import get_weather_summary
22
+ from providers.cache import clear_provider_cache, get_cache_stats
23
+
20
24
  # Configurações de ambiente
21
25
  HOST = os.getenv("HOST", "0.0.0.0")
22
26
  PORT = int(os.getenv("PORT", "8000"))
23
27
  CORS_ORIGINS = os.getenv("CORS_ORIGINS", "http://localhost:3000,http://127.0.0.1:3000").split(",")
28
+ DATA_MODE = os.getenv("DATA_MODE", "hybrid") # simulated, real, hybrid
29
+ WEATHER_PROVIDER = os.getenv("WEATHER_PROVIDER", "open-meteo")
30
+ PROVIDER_CACHE_TTL = int(os.getenv("PROVIDER_CACHE_TTL", "3600"))
24
31
 
25
32
  # Cache em memória para resultados pesados
26
33
  _resultados_cache = {}
@@ -126,16 +133,53 @@ def health():
126
133
  """Verifica saúde da API"""
127
134
  try:
128
135
  culturas, talhoes, regras = get_dados()
136
+ provider_cache_stats = get_cache_stats()
137
+
129
138
  return {
130
139
  "status": "healthy",
131
140
  "culturas": len(culturas),
132
141
  "talhoes": len(talhoes),
133
142
  "regras": len(regras),
134
- "cache_items": len(_resultados_cache)
143
+ "cache_items": len(_resultados_cache),
144
+ "data_mode": DATA_MODE,
145
+ "providers": {
146
+ "weather": "available" if WEATHER_PROVIDER else "disabled"
147
+ },
148
+ "provider_cache": provider_cache_stats
135
149
  }
136
150
  except Exception as e:
137
151
  raise HTTPException(status_code=500, detail=str(e))
138
152
 
153
+ @app.get("/dados/clima")
154
+ def get_clima(
155
+ lat: Optional[float] = Query(None, description="Latitude"),
156
+ lon: Optional[float] = Query(None, description="Longitude"),
157
+ days: int = Query(30, description="Número de dias para análise")
158
+ ):
159
+ """Obtém dados climáticos reais ou simulados"""
160
+ try:
161
+ # Se lat ou lon não foram fornecidos, retornar mensagem amigável
162
+ if lat is None or lon is None:
163
+ return {
164
+ "message": "Informe latitude e longitude para consultar dados climáticos reais.",
165
+ "exemplo_sao_paulo": "/dados/clima?lat=-23.55&lon=-46.63&days=30",
166
+ "exemplo_brasilia": "/dados/clima?lat=-15.78&lon=-47.93&days=30",
167
+ "parametros": {
168
+ "lat": "Latitude da localização",
169
+ "lon": "Longitude da localização",
170
+ "days": "Número de dias analisados, padrão 30"
171
+ }
172
+ }
173
+
174
+ if days < 1 or days > 365:
175
+ raise HTTPException(status_code=400, detail="Days deve estar entre 1 e 365")
176
+
177
+ weather_data = get_weather_summary(lat, lon, days)
178
+ return weather_data.to_dict()
179
+
180
+ except Exception as e:
181
+ raise HTTPException(status_code=500, detail=str(e))
182
+
139
183
  @app.get("/dashboard")
140
184
  def get_dashboard():
141
185
  """Retorna resumo do dashboard"""
@@ -0,0 +1 @@
1
+ # Provedores de dados reais para AgroPlan AI
@@ -0,0 +1,49 @@
1
+ """
2
+ Cache simples em memória para chamadas de APIs externas
3
+ """
4
+ import time
5
+ from typing import Any, Optional
6
+
7
+ # Cache global em memória
8
+ _cache = {}
9
+
10
+ def get_cache(key: str) -> Optional[Any]:
11
+ """Recupera valor do cache se ainda válido"""
12
+ if key not in _cache:
13
+ return None
14
+
15
+ value, expiry = _cache[key]
16
+ if time.time() > expiry:
17
+ # Cache expirado, remove
18
+ del _cache[key]
19
+ return None
20
+
21
+ return value
22
+
23
+ def set_cache(key: str, value: Any, ttl_seconds: int = 3600) -> None:
24
+ """Armazena valor no cache com TTL"""
25
+ expiry = time.time() + ttl_seconds
26
+ _cache[key] = (value, expiry)
27
+
28
+ def clear_provider_cache() -> None:
29
+ """Limpa todo o cache de provedores"""
30
+ global _cache
31
+ _cache = {}
32
+
33
+ def get_cache_stats() -> dict:
34
+ """Retorna estatísticas do cache"""
35
+ current_time = time.time()
36
+ valid_items = 0
37
+ expired_items = 0
38
+
39
+ for key, (value, expiry) in _cache.items():
40
+ if current_time <= expiry:
41
+ valid_items += 1
42
+ else:
43
+ expired_items += 1
44
+
45
+ return {
46
+ "total_items": len(_cache),
47
+ "valid_items": valid_items,
48
+ "expired_items": expired_items
49
+ }
@@ -0,0 +1,40 @@
1
+ """
2
+ Tipos de dados para provedores externos
3
+ """
4
+ from typing import Optional
5
+ from dataclasses import dataclass
6
+
7
+ @dataclass
8
+ class WeatherSummary:
9
+ """Resumo de dados climáticos"""
10
+ source: str
11
+ latitude: float
12
+ longitude: float
13
+ temperatura_media: Optional[float] = None
14
+ temperatura_maxima: Optional[float] = None
15
+ temperatura_minima: Optional[float] = None
16
+ precipitacao_total: Optional[float] = None
17
+ evapotranspiracao: Optional[float] = None
18
+ umidade_media: Optional[float] = None
19
+ radiacao_solar: Optional[float] = None
20
+ risco_climatico_estimado: str = "indeterminado"
21
+ fallback: bool = False
22
+ error: Optional[str] = None
23
+
24
+ def to_dict(self) -> dict:
25
+ """Converte para dicionário para JSON"""
26
+ return {
27
+ "source": self.source,
28
+ "latitude": self.latitude,
29
+ "longitude": self.longitude,
30
+ "temperatura_media": self.temperatura_media,
31
+ "temperatura_maxima": self.temperatura_maxima,
32
+ "temperatura_minima": self.temperatura_minima,
33
+ "precipitacao_total": self.precipitacao_total,
34
+ "evapotranspiracao": self.evapotranspiracao,
35
+ "umidade_media": self.umidade_media,
36
+ "radiacao_solar": self.radiacao_solar,
37
+ "risco_climatico_estimado": self.risco_climatico_estimado,
38
+ "fallback": self.fallback,
39
+ "error": self.error
40
+ }
@@ -0,0 +1,161 @@
1
+ """
2
+ Provedor de dados climáticos usando Open-Meteo
3
+ """
4
+ import urllib.request
5
+ import urllib.parse
6
+ import json
7
+ from datetime import datetime, timedelta
8
+ from typing import Optional
9
+ from .types import WeatherSummary
10
+ from .cache import get_cache, set_cache
11
+
12
+ def estimar_risco_climatico(temp_media: Optional[float], precipitacao_total: Optional[float]) -> str:
13
+ """Estima risco climático baseado em temperatura e precipitação"""
14
+ if temp_media is None or precipitacao_total is None:
15
+ return "indeterminado"
16
+
17
+ # Heurística simples inicial
18
+ if precipitacao_total < 30:
19
+ return "alto"
20
+ if temp_media > 34:
21
+ return "alto"
22
+ if precipitacao_total < 70:
23
+ return "medio"
24
+ return "baixo"
25
+
26
+ def get_weather_summary(lat: float, lon: float, days: int = 30) -> WeatherSummary:
27
+ """
28
+ Obtém resumo climático usando Open-Meteo
29
+
30
+ Args:
31
+ lat: Latitude
32
+ lon: Longitude
33
+ days: Número de dias para análise (padrão 30)
34
+
35
+ Returns:
36
+ WeatherSummary com dados climáticos ou fallback
37
+ """
38
+
39
+ # Chave do cache
40
+ cache_key = f"weather_{lat}_{lon}_{days}"
41
+
42
+ # Verificar cache primeiro
43
+ cached = get_cache(cache_key)
44
+ if cached:
45
+ return WeatherSummary(**cached)
46
+
47
+ try:
48
+ # Calcular datas (últimos N dias)
49
+ end_date = datetime.now().date()
50
+ start_date = end_date - timedelta(days=days)
51
+
52
+ # Parâmetros da API Open-Meteo Archive
53
+ params = {
54
+ "latitude": str(lat),
55
+ "longitude": str(lon),
56
+ "start_date": start_date.strftime("%Y-%m-%d"),
57
+ "end_date": end_date.strftime("%Y-%m-%d"),
58
+ "daily": ",".join([
59
+ "temperature_2m_mean",
60
+ "temperature_2m_max",
61
+ "temperature_2m_min",
62
+ "precipitation_sum",
63
+ "et0_fao_evapotranspiration",
64
+ "relative_humidity_2m_mean",
65
+ "shortwave_radiation_sum"
66
+ ]),
67
+ "timezone": "America/Sao_Paulo"
68
+ }
69
+
70
+ # Construir URL
71
+ base_url = "https://archive-api.open-meteo.com/v1/archive"
72
+ query_string = urllib.parse.urlencode(params)
73
+ url = f"{base_url}?{query_string}"
74
+
75
+ # Fazer requisição
76
+ with urllib.request.urlopen(url, timeout=10) as response:
77
+ data = json.loads(response.read().decode())
78
+
79
+ daily = data.get("daily", {})
80
+
81
+ # Extrair dados
82
+ temps_mean = daily.get("temperature_2m_mean", [])
83
+ temps_max = daily.get("temperature_2m_max", [])
84
+ temps_min = daily.get("temperature_2m_min", [])
85
+ precipitation = daily.get("precipitation_sum", [])
86
+ evapotranspiration = daily.get("et0_fao_evapotranspiration", [])
87
+ humidity = daily.get("relative_humidity_2m_mean", [])
88
+ radiation = daily.get("shortwave_radiation_sum", [])
89
+
90
+ # Calcular médias (ignorando valores None)
91
+ def safe_mean(values):
92
+ valid_values = [v for v in values if v is not None]
93
+ return sum(valid_values) / len(valid_values) if valid_values else None
94
+
95
+ def safe_sum(values):
96
+ valid_values = [v for v in values if v is not None]
97
+ return sum(valid_values) if valid_values else None
98
+
99
+ temp_media = safe_mean(temps_mean)
100
+ temp_maxima = safe_mean(temps_max)
101
+ temp_minima = safe_mean(temps_min)
102
+ precip_total = safe_sum(precipitation)
103
+ evap_media = safe_mean(evapotranspiration)
104
+ umid_media = safe_mean(humidity)
105
+ rad_total = safe_sum(radiation)
106
+
107
+ # Estimar risco climático
108
+ risco = estimar_risco_climatico(temp_media, precip_total)
109
+
110
+ # Criar resultado
111
+ result = WeatherSummary(
112
+ source="open-meteo",
113
+ latitude=lat,
114
+ longitude=lon,
115
+ temperatura_media=round(temp_media, 1) if temp_media else None,
116
+ temperatura_maxima=round(temp_maxima, 1) if temp_maxima else None,
117
+ temperatura_minima=round(temp_minima, 1) if temp_minima else None,
118
+ precipitacao_total=round(precip_total, 1) if precip_total else None,
119
+ evapotranspiracao=round(evap_media, 2) if evap_media else None,
120
+ umidade_media=round(umid_media, 1) if umid_media else None,
121
+ radiacao_solar=round(rad_total, 1) if rad_total else None,
122
+ risco_climatico_estimado=risco,
123
+ fallback=False,
124
+ error=None
125
+ )
126
+
127
+ # Cachear por 1 hora (dados históricos mudam pouco)
128
+ set_cache(cache_key, result.to_dict(), ttl_seconds=3600)
129
+
130
+ return result
131
+
132
+ except Exception as e:
133
+ # Fallback para dados simulados
134
+ return _get_fallback_weather(lat, lon, str(e))
135
+
136
+ def _get_fallback_weather(lat: float, lon: float, error: str) -> WeatherSummary:
137
+ """Retorna dados climáticos simulados como fallback"""
138
+
139
+ # Dados simulados baseados na localização (heurística simples)
140
+ # Região Sudeste do Brasil como referência
141
+ temp_base = 22.0
142
+ if lat < -25: # Mais ao sul, mais frio
143
+ temp_base = 18.0
144
+ elif lat > -20: # Mais ao norte, mais quente
145
+ temp_base = 26.0
146
+
147
+ return WeatherSummary(
148
+ source="simulado",
149
+ latitude=lat,
150
+ longitude=lon,
151
+ temperatura_media=temp_base,
152
+ temperatura_maxima=temp_base + 8,
153
+ temperatura_minima=temp_base - 6,
154
+ precipitacao_total=120.0, # mm/mês típico
155
+ evapotranspiracao=4.5, # mm/dia típico
156
+ umidade_media=65.0, # % típico
157
+ radiacao_solar=180.0, # MJ/m² típico mensal
158
+ risco_climatico_estimado="medio",
159
+ fallback=True,
160
+ error=f"Open-Meteo indisponível: {error}"
161
+ )
package/dist/index.js CHANGED
@@ -779,7 +779,7 @@ var COMMANDS = {
779
779
  open: "Abre o AgroPlan AI no navegador"
780
780
  };
781
781
  function showHelp() {
782
- console.log("\uD83C\uDF31 AgroPlan AI - CLI Local v1.0.7");
782
+ console.log("\uD83C\uDF31 AgroPlan AI - CLI Local v1.0.8");
783
783
  console.log(` Launcher para modo local acelerado
784
784
  `);
785
785
  console.log("\uD83D\uDCCB Comandos dispon\xEDveis:");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agroplan-ai-cli",
3
- "version": "1.0.7",
3
+ "version": "1.0.8",
4
4
  "description": "CLI global para AgroPlan AI - modo local acelerado",
5
5
  "type": "module",
6
6
  "bin": {