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.
- package/backend-template/.env.example +7 -2
- package/backend-template/api.py +46 -2
- package/backend-template/providers/__init__.py +1 -0
- package/backend-template/providers/__pycache__/__init__.cpython-313.pyc +0 -0
- package/backend-template/providers/__pycache__/cache.cpython-313.pyc +0 -0
- package/backend-template/providers/__pycache__/types.cpython-313.pyc +0 -0
- package/backend-template/providers/__pycache__/weather_provider.cpython-313.pyc +0 -0
- package/backend-template/providers/cache.py +49 -0
- package/backend-template/providers/types.py +40 -0
- package/backend-template/providers/weather_provider.py +161 -0
- package/dist/index.js +1 -1
- package/package.json +1 -1
|
@@ -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
|
|
16
|
-
|
|
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
|
package/backend-template/api.py
CHANGED
|
@@ -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
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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.
|
|
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:");
|