agroplan-ai-cli 1.0.0
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/README.md +95 -0
- package/backend-template/.env.example +21 -0
- package/backend-template/Dockerfile +21 -0
- package/backend-template/README.md +274 -0
- package/backend-template/api.py +538 -0
- package/backend-template/core/bruteforce_validator.py +248 -0
- package/backend-template/core/genetic_optimizer.py +328 -0
- package/backend-template/core/loader.py +8 -0
- package/backend-template/core/planner.py +79 -0
- package/backend-template/core/report_generator.py +785 -0
- package/backend-template/core/scenario_simulator.py +286 -0
- package/backend-template/core/scorer.py +101 -0
- package/backend-template/core/terrain_analyzer.py +123 -0
- package/backend-template/data/culturas.csv +11 -0
- package/backend-template/data/regras_culturas.csv +11 -0
- package/backend-template/data/talhoes.csv +11 -0
- package/backend-template/main.py +487 -0
- package/backend-template/reports/.gitkeep +1 -0
- package/backend-template/requirements.txt +6 -0
- package/dist/index.js +719 -0
- package/package.json +51 -0
|
@@ -0,0 +1,538 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FastAPI Backend para AgroPlan AI
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from fastapi import FastAPI, HTTPException, Request
|
|
6
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
from typing import Optional
|
|
9
|
+
import sys
|
|
10
|
+
import os
|
|
11
|
+
|
|
12
|
+
# Adiciona o diretório backend ao path para imports
|
|
13
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
14
|
+
|
|
15
|
+
from core.loader import carregar_dados
|
|
16
|
+
from core.planner import gerar_cenarios, gerar_plano_genetico
|
|
17
|
+
from core.bruteforce_validator import comparar_ag_com_forca_bruta, executar_multiplas_rodadas
|
|
18
|
+
from core.report_generator import gerar_relatorio_completo
|
|
19
|
+
|
|
20
|
+
# Configurações de ambiente
|
|
21
|
+
HOST = os.getenv("HOST", "0.0.0.0")
|
|
22
|
+
PORT = int(os.getenv("PORT", "8000"))
|
|
23
|
+
CORS_ORIGINS = os.getenv("CORS_ORIGINS", "http://localhost:3000,http://127.0.0.1:3000").split(",")
|
|
24
|
+
|
|
25
|
+
# Cache em memória para resultados pesados
|
|
26
|
+
_resultados_cache = {}
|
|
27
|
+
|
|
28
|
+
def get_cache_key(nome, **params):
|
|
29
|
+
"""Gera chave única para cache baseada no nome e parâmetros"""
|
|
30
|
+
return f"{nome}:" + ":".join(f"{k}={v}" for k, v in sorted(params.items()))
|
|
31
|
+
|
|
32
|
+
def get_or_compute_cache(key, compute_fn):
|
|
33
|
+
"""Retorna valor do cache ou computa e armazena se não existir"""
|
|
34
|
+
if key not in _resultados_cache:
|
|
35
|
+
_resultados_cache[key] = compute_fn()
|
|
36
|
+
return _resultados_cache[key]
|
|
37
|
+
|
|
38
|
+
def get_ag_cacheado(objetivo="equilibrado", seed=42, geracoes=100, populacao=50):
|
|
39
|
+
"""Retorna resultado do AG cacheado"""
|
|
40
|
+
culturas, talhoes, regras = get_dados()
|
|
41
|
+
key = get_cache_key("ag", objetivo=objetivo, seed=seed, geracoes=geracoes, populacao=populacao)
|
|
42
|
+
return get_or_compute_cache(key, lambda: gerar_plano_genetico(culturas, talhoes, regras, objetivo=objetivo, seed=seed, geracoes=geracoes, populacao=populacao))
|
|
43
|
+
|
|
44
|
+
# Inicializa FastAPI
|
|
45
|
+
app = FastAPI(
|
|
46
|
+
title="AgroPlan AI API",
|
|
47
|
+
description="API para Sistema Inteligente de Planejamento de Plantio",
|
|
48
|
+
version="5.0.0"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# Configura CORS
|
|
52
|
+
app.add_middleware(
|
|
53
|
+
CORSMiddleware,
|
|
54
|
+
allow_origins=CORS_ORIGINS,
|
|
55
|
+
allow_credentials=True,
|
|
56
|
+
allow_methods=["*"],
|
|
57
|
+
allow_headers=["*"],
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# Modelos Pydantic
|
|
61
|
+
class OtimizarRequest(BaseModel):
|
|
62
|
+
objetivo: str = "equilibrado"
|
|
63
|
+
seed: Optional[int] = 42
|
|
64
|
+
geracoes: Optional[int] = 100
|
|
65
|
+
populacao: Optional[int] = 50
|
|
66
|
+
|
|
67
|
+
class ValidarRequest(BaseModel):
|
|
68
|
+
objetivo: str = "equilibrado"
|
|
69
|
+
seed: Optional[int] = 42
|
|
70
|
+
|
|
71
|
+
class RelatorioRequest(BaseModel):
|
|
72
|
+
objetivo: str = "equilibrado"
|
|
73
|
+
formato: str = "md"
|
|
74
|
+
|
|
75
|
+
class RodadasRequest(BaseModel):
|
|
76
|
+
objetivo: str = "equilibrado"
|
|
77
|
+
rodadas: int = 5
|
|
78
|
+
|
|
79
|
+
# Cache de dados (carrega uma vez)
|
|
80
|
+
_dados_cache = None
|
|
81
|
+
|
|
82
|
+
def get_dados():
|
|
83
|
+
"""Carrega dados com cache"""
|
|
84
|
+
global _dados_cache
|
|
85
|
+
if _dados_cache is None:
|
|
86
|
+
culturas, talhoes, regras = carregar_dados()
|
|
87
|
+
_dados_cache = {
|
|
88
|
+
'culturas': culturas,
|
|
89
|
+
'talhoes': talhoes,
|
|
90
|
+
'regras': regras
|
|
91
|
+
}
|
|
92
|
+
return _dados_cache['culturas'], _dados_cache['talhoes'], _dados_cache['regras']
|
|
93
|
+
|
|
94
|
+
def converter_tipos_python(obj):
|
|
95
|
+
"""Converte tipos numpy para tipos Python nativos recursivamente"""
|
|
96
|
+
import numpy as np
|
|
97
|
+
|
|
98
|
+
if isinstance(obj, dict):
|
|
99
|
+
return {k: converter_tipos_python(v) for k, v in obj.items()}
|
|
100
|
+
elif isinstance(obj, list):
|
|
101
|
+
return [converter_tipos_python(item) for item in obj]
|
|
102
|
+
elif isinstance(obj, (np.integer, np.int64, np.int32)):
|
|
103
|
+
return int(obj)
|
|
104
|
+
elif isinstance(obj, (np.floating, np.float64, np.float32)):
|
|
105
|
+
return float(obj)
|
|
106
|
+
elif isinstance(obj, (np.bool_, bool)):
|
|
107
|
+
return bool(obj)
|
|
108
|
+
elif isinstance(obj, str):
|
|
109
|
+
return str(obj)
|
|
110
|
+
else:
|
|
111
|
+
return obj
|
|
112
|
+
|
|
113
|
+
# Endpoints
|
|
114
|
+
|
|
115
|
+
@app.get("/")
|
|
116
|
+
def root():
|
|
117
|
+
"""Endpoint raiz"""
|
|
118
|
+
return {
|
|
119
|
+
"message": "AgroPlan AI API",
|
|
120
|
+
"version": "5.0.0",
|
|
121
|
+
"docs": "/docs"
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
@app.get("/health")
|
|
125
|
+
def health():
|
|
126
|
+
"""Verifica saúde da API"""
|
|
127
|
+
try:
|
|
128
|
+
culturas, talhoes, regras = get_dados()
|
|
129
|
+
return {
|
|
130
|
+
"status": "healthy",
|
|
131
|
+
"culturas": len(culturas),
|
|
132
|
+
"talhoes": len(talhoes),
|
|
133
|
+
"regras": len(regras),
|
|
134
|
+
"cache_items": len(_resultados_cache)
|
|
135
|
+
}
|
|
136
|
+
except Exception as e:
|
|
137
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
138
|
+
|
|
139
|
+
@app.get("/dashboard")
|
|
140
|
+
def get_dashboard():
|
|
141
|
+
"""Retorna resumo do dashboard"""
|
|
142
|
+
try:
|
|
143
|
+
def montar_dashboard():
|
|
144
|
+
culturas, talhoes, regras = get_dados()
|
|
145
|
+
|
|
146
|
+
# Usa AG cacheado
|
|
147
|
+
resultado_ag = get_ag_cacheado(objetivo='equilibrado', seed=42)
|
|
148
|
+
|
|
149
|
+
# Tenta validar
|
|
150
|
+
validacao = comparar_ag_com_forca_bruta(culturas, talhoes, regras, objetivo='equilibrado', seed=42)
|
|
151
|
+
|
|
152
|
+
# Se força bruta é inviável, retorna dados especiais
|
|
153
|
+
if validacao.get('erro'):
|
|
154
|
+
return {
|
|
155
|
+
"lucro_total": float(resultado_ag['lucro_total']),
|
|
156
|
+
"risco_medio": float(resultado_ag['risco_medio']),
|
|
157
|
+
"fitness": float(resultado_ag['fitness']),
|
|
158
|
+
"diversidade": int(resultado_ag['diversidade']),
|
|
159
|
+
"objetivo": str(resultado_ag['objetivo']),
|
|
160
|
+
"culturas_escolhidas": [str(p['cultura']) for p in resultado_ag['plano']],
|
|
161
|
+
"validacao": {
|
|
162
|
+
"otimo_global": False,
|
|
163
|
+
"total_combinacoes": int(validacao.get('total_combinacoes', 0))
|
|
164
|
+
},
|
|
165
|
+
"plano": [
|
|
166
|
+
{
|
|
167
|
+
"talhao": int(p['talhao']),
|
|
168
|
+
"area": float(p['area']),
|
|
169
|
+
"solo": str(p['solo']),
|
|
170
|
+
"clima": str(p['clima']),
|
|
171
|
+
"relevo": str(p['relevo']),
|
|
172
|
+
"agua": str(p['agua']),
|
|
173
|
+
"cultura": str(p['cultura']),
|
|
174
|
+
"lucro_estimado": float(p['lucro_estimado']),
|
|
175
|
+
"risco": float(p['risco']),
|
|
176
|
+
"nota": float(p['nota']),
|
|
177
|
+
"tempo": int(p['tempo'])
|
|
178
|
+
}
|
|
179
|
+
for p in resultado_ag['plano']
|
|
180
|
+
]
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
# Converte tipos numpy para Python nativos
|
|
184
|
+
return {
|
|
185
|
+
"lucro_total": float(resultado_ag['lucro_total']),
|
|
186
|
+
"risco_medio": float(resultado_ag['risco_medio']),
|
|
187
|
+
"fitness": float(resultado_ag['fitness']),
|
|
188
|
+
"diversidade": int(resultado_ag['diversidade']),
|
|
189
|
+
"objetivo": str(resultado_ag['objetivo']),
|
|
190
|
+
"culturas_escolhidas": [str(p['cultura']) for p in resultado_ag['plano']],
|
|
191
|
+
"validacao": {
|
|
192
|
+
"otimo_global": bool(validacao.get('ag_encontrou_otimo_global', False)),
|
|
193
|
+
"total_combinacoes": int(validacao.get('forca_bruta', {}).get('total_combinacoes', 0))
|
|
194
|
+
},
|
|
195
|
+
"plano": [
|
|
196
|
+
{
|
|
197
|
+
"talhao": int(p['talhao']),
|
|
198
|
+
"area": float(p['area']),
|
|
199
|
+
"solo": str(p['solo']),
|
|
200
|
+
"clima": str(p['clima']),
|
|
201
|
+
"relevo": str(p['relevo']),
|
|
202
|
+
"agua": str(p['agua']),
|
|
203
|
+
"cultura": str(p['cultura']),
|
|
204
|
+
"lucro_estimado": float(p['lucro_estimado']),
|
|
205
|
+
"risco": float(p['risco']),
|
|
206
|
+
"nota": float(p['nota']),
|
|
207
|
+
"tempo": int(p['tempo'])
|
|
208
|
+
}
|
|
209
|
+
for p in resultado_ag['plano']
|
|
210
|
+
]
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
# Usa cache para dashboard
|
|
214
|
+
key = get_cache_key("dashboard", objetivo="equilibrado", seed=42)
|
|
215
|
+
resultado = get_or_compute_cache(key, montar_dashboard)
|
|
216
|
+
|
|
217
|
+
# Converte tipos Python (por segurança)
|
|
218
|
+
return converter_tipos_python(resultado)
|
|
219
|
+
except Exception as e:
|
|
220
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
221
|
+
except Exception as e:
|
|
222
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
223
|
+
|
|
224
|
+
@app.get("/talhoes")
|
|
225
|
+
def get_talhoes():
|
|
226
|
+
"""Retorna dados dos talhões"""
|
|
227
|
+
try:
|
|
228
|
+
culturas, talhoes, regras = get_dados()
|
|
229
|
+
return {
|
|
230
|
+
"talhoes": talhoes.to_dict('records')
|
|
231
|
+
}
|
|
232
|
+
except Exception as e:
|
|
233
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
234
|
+
|
|
235
|
+
@app.get("/recomendacoes")
|
|
236
|
+
def get_recomendacoes():
|
|
237
|
+
"""Retorna recomendações de culturas por talhão"""
|
|
238
|
+
try:
|
|
239
|
+
culturas, talhoes, regras = get_dados()
|
|
240
|
+
|
|
241
|
+
# Usa AG cacheado para obter recomendações
|
|
242
|
+
resultado_ag = get_ag_cacheado(objetivo='equilibrado', seed=42)
|
|
243
|
+
|
|
244
|
+
# Formata recomendações
|
|
245
|
+
recomendacoes = []
|
|
246
|
+
for p in resultado_ag['plano']:
|
|
247
|
+
recomendacoes.append({
|
|
248
|
+
"talhao": int(p['talhao']),
|
|
249
|
+
"cultura": str(p['cultura']),
|
|
250
|
+
"lucro_estimado": float(p['lucro_estimado']),
|
|
251
|
+
"risco": float(p['risco']),
|
|
252
|
+
"nota": float(p['nota']),
|
|
253
|
+
"area": float(p['area']),
|
|
254
|
+
"solo": str(p['solo']),
|
|
255
|
+
"clima": str(p['clima']),
|
|
256
|
+
"relevo": str(p['relevo']),
|
|
257
|
+
"agua": str(p['agua'])
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
"recomendacoes": recomendacoes
|
|
262
|
+
}
|
|
263
|
+
except Exception as e:
|
|
264
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
265
|
+
|
|
266
|
+
@app.get("/culturas")
|
|
267
|
+
def get_culturas():
|
|
268
|
+
"""Retorna dados das culturas"""
|
|
269
|
+
try:
|
|
270
|
+
culturas, talhoes, regras = get_dados()
|
|
271
|
+
|
|
272
|
+
# Combina culturas com regras
|
|
273
|
+
culturas_dict = culturas.to_dict('records')
|
|
274
|
+
regras_dict = regras.to_dict('records')
|
|
275
|
+
|
|
276
|
+
# Cria dicionário de regras por cultura
|
|
277
|
+
regras_map = {r['cultura']: r for r in regras_dict}
|
|
278
|
+
|
|
279
|
+
# Adiciona regras às culturas
|
|
280
|
+
for cultura in culturas_dict:
|
|
281
|
+
nome = cultura['nome']
|
|
282
|
+
if nome in regras_map:
|
|
283
|
+
cultura['regras'] = regras_map[nome]
|
|
284
|
+
|
|
285
|
+
return {
|
|
286
|
+
"culturas": culturas_dict
|
|
287
|
+
}
|
|
288
|
+
except Exception as e:
|
|
289
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
290
|
+
|
|
291
|
+
@app.get("/cenarios")
|
|
292
|
+
def get_cenarios():
|
|
293
|
+
"""Retorna comparação de cenários"""
|
|
294
|
+
try:
|
|
295
|
+
def montar_cenarios():
|
|
296
|
+
culturas, talhoes, regras = get_dados()
|
|
297
|
+
|
|
298
|
+
# Gera todos os cenários
|
|
299
|
+
cenarios = gerar_cenarios(culturas, talhoes, regras)
|
|
300
|
+
|
|
301
|
+
# Usa AG cacheado
|
|
302
|
+
resultado_ag = get_ag_cacheado(objetivo='equilibrado', seed=42)
|
|
303
|
+
|
|
304
|
+
# Cria um mapa de talhões para facilitar o acesso
|
|
305
|
+
talhoes_dict = {int(row['id']): row for _, row in talhoes.iterrows()}
|
|
306
|
+
|
|
307
|
+
# Formata resposta
|
|
308
|
+
cenarios_formatados = {}
|
|
309
|
+
|
|
310
|
+
for key, cenario in cenarios.items():
|
|
311
|
+
cenarios_formatados[key] = {
|
|
312
|
+
'nome': str(cenario['nome']),
|
|
313
|
+
'descricao': str(cenario['descricao']),
|
|
314
|
+
'lucro_total': float(cenario['lucro_total']),
|
|
315
|
+
'risco_medio': float(cenario['risco_medio']),
|
|
316
|
+
'area_total': float(cenario['area_total']),
|
|
317
|
+
'plano': [
|
|
318
|
+
{
|
|
319
|
+
"talhao": int(p['talhao']),
|
|
320
|
+
"area": float(p['area']),
|
|
321
|
+
"solo": str(talhoes_dict[int(p['talhao'])]['solo']),
|
|
322
|
+
"clima": str(talhoes_dict[int(p['talhao'])]['clima']),
|
|
323
|
+
"relevo": str(talhoes_dict[int(p['talhao'])]['relevo']),
|
|
324
|
+
"agua": str(talhoes_dict[int(p['talhao'])]['agua']),
|
|
325
|
+
"cultura": str(p['cultura']),
|
|
326
|
+
"lucro_estimado": float(p['lucro_estimado']),
|
|
327
|
+
"risco": float(p['risco']),
|
|
328
|
+
"nota": float(p['nota']),
|
|
329
|
+
"tempo": 0 # Não disponível nos cenários simples
|
|
330
|
+
}
|
|
331
|
+
for p in cenario['plano']
|
|
332
|
+
]
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
# Adiciona AG
|
|
336
|
+
cenarios_formatados['genetico'] = {
|
|
337
|
+
'nome': 'Algoritmo Genético',
|
|
338
|
+
'descricao': 'Solução otimizada automaticamente',
|
|
339
|
+
'lucro_total': float(resultado_ag['lucro_total']),
|
|
340
|
+
'risco_medio': float(resultado_ag['risco_medio']),
|
|
341
|
+
'area_total': float(resultado_ag['area_total']),
|
|
342
|
+
'plano': [
|
|
343
|
+
{
|
|
344
|
+
"talhao": int(p['talhao']),
|
|
345
|
+
"area": float(p['area']),
|
|
346
|
+
"solo": str(p['solo']),
|
|
347
|
+
"clima": str(p['clima']),
|
|
348
|
+
"relevo": str(p['relevo']),
|
|
349
|
+
"agua": str(p['agua']),
|
|
350
|
+
"cultura": str(p['cultura']),
|
|
351
|
+
"lucro_estimado": float(p['lucro_estimado']),
|
|
352
|
+
"risco": float(p['risco']),
|
|
353
|
+
"nota": float(p['nota']),
|
|
354
|
+
"tempo": int(p['tempo'])
|
|
355
|
+
}
|
|
356
|
+
for p in resultado_ag['plano']
|
|
357
|
+
]
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return {
|
|
361
|
+
"cenarios": cenarios_formatados
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
# Usa cache para cenários
|
|
365
|
+
key = get_cache_key("cenarios")
|
|
366
|
+
resultado = get_or_compute_cache(key, montar_cenarios)
|
|
367
|
+
|
|
368
|
+
return converter_tipos_python(resultado)
|
|
369
|
+
except Exception as e:
|
|
370
|
+
import traceback
|
|
371
|
+
traceback.print_exc()
|
|
372
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
373
|
+
|
|
374
|
+
@app.post("/otimizar")
|
|
375
|
+
def otimizar(request: OtimizarRequest):
|
|
376
|
+
"""Executa otimização com Algoritmo Genético"""
|
|
377
|
+
try:
|
|
378
|
+
# Valida objetivo
|
|
379
|
+
objetivos_validos = ['equilibrado', 'lucro', 'risco', 'sustentavel']
|
|
380
|
+
if request.objetivo not in objetivos_validos:
|
|
381
|
+
raise HTTPException(status_code=400, detail=f"Objetivo inválido. Use: {objetivos_validos}")
|
|
382
|
+
|
|
383
|
+
# Usa AG cacheado se parâmetros forem padrão
|
|
384
|
+
if (request.objetivo == "equilibrado" and
|
|
385
|
+
request.seed == 42 and
|
|
386
|
+
request.geracoes == 100 and
|
|
387
|
+
request.populacao == 50):
|
|
388
|
+
resultado = get_ag_cacheado(
|
|
389
|
+
objetivo=request.objetivo,
|
|
390
|
+
seed=request.seed,
|
|
391
|
+
geracoes=request.geracoes,
|
|
392
|
+
populacao=request.populacao
|
|
393
|
+
)
|
|
394
|
+
else:
|
|
395
|
+
# Executa AG sem cache para parâmetros customizados
|
|
396
|
+
culturas, talhoes, regras = get_dados()
|
|
397
|
+
resultado = gerar_plano_genetico(
|
|
398
|
+
culturas, talhoes, regras,
|
|
399
|
+
objetivo=request.objetivo,
|
|
400
|
+
geracoes=request.geracoes,
|
|
401
|
+
populacao=request.populacao,
|
|
402
|
+
seed=request.seed
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
# Converte tipos numpy para Python nativos
|
|
406
|
+
resultado_convertido = converter_tipos_python(resultado)
|
|
407
|
+
|
|
408
|
+
return resultado_convertido
|
|
409
|
+
except HTTPException:
|
|
410
|
+
raise
|
|
411
|
+
except Exception as e:
|
|
412
|
+
import traceback
|
|
413
|
+
traceback.print_exc()
|
|
414
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
415
|
+
|
|
416
|
+
@app.post("/validar")
|
|
417
|
+
def validar(request: ValidarRequest):
|
|
418
|
+
"""Valida AG com força bruta"""
|
|
419
|
+
try:
|
|
420
|
+
culturas, talhoes, regras = get_dados()
|
|
421
|
+
|
|
422
|
+
# Valida objetivo
|
|
423
|
+
objetivos_validos = ['equilibrado', 'lucro', 'risco', 'sustentavel']
|
|
424
|
+
if request.objetivo not in objetivos_validos:
|
|
425
|
+
raise HTTPException(status_code=400, detail=f"Objetivo inválido. Use: {objetivos_validos}")
|
|
426
|
+
|
|
427
|
+
# Executa validação
|
|
428
|
+
resultado = comparar_ag_com_forca_bruta(
|
|
429
|
+
culturas, talhoes, regras,
|
|
430
|
+
objetivo=request.objetivo,
|
|
431
|
+
seed=request.seed
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
if resultado.get('erro'):
|
|
435
|
+
raise HTTPException(status_code=400, detail=resultado.get('mensagem'))
|
|
436
|
+
|
|
437
|
+
# Converte tipos numpy para Python nativos
|
|
438
|
+
resultado_convertido = converter_tipos_python(resultado)
|
|
439
|
+
|
|
440
|
+
return resultado_convertido
|
|
441
|
+
except HTTPException:
|
|
442
|
+
raise
|
|
443
|
+
except Exception as e:
|
|
444
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
445
|
+
|
|
446
|
+
@app.post("/rodadas")
|
|
447
|
+
def rodadas(request: RodadasRequest):
|
|
448
|
+
"""Executa múltiplas rodadas do AG"""
|
|
449
|
+
try:
|
|
450
|
+
culturas, talhoes, regras = get_dados()
|
|
451
|
+
|
|
452
|
+
# Valida objetivo
|
|
453
|
+
objetivos_validos = ['equilibrado', 'lucro', 'risco', 'sustentavel']
|
|
454
|
+
if request.objetivo not in objetivos_validos:
|
|
455
|
+
raise HTTPException(status_code=400, detail=f"Objetivo inválido. Use: {objetivos_validos}")
|
|
456
|
+
|
|
457
|
+
# Executa rodadas
|
|
458
|
+
resultado = executar_multiplas_rodadas(
|
|
459
|
+
culturas, talhoes, regras,
|
|
460
|
+
objetivo=request.objetivo,
|
|
461
|
+
rodadas=request.rodadas
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
# Converte tipos numpy para Python nativos
|
|
465
|
+
resultado_convertido = converter_tipos_python(resultado)
|
|
466
|
+
|
|
467
|
+
return resultado_convertido
|
|
468
|
+
except HTTPException:
|
|
469
|
+
raise
|
|
470
|
+
except Exception as e:
|
|
471
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
472
|
+
|
|
473
|
+
@app.post("/relatorio")
|
|
474
|
+
def relatorio(request: RelatorioRequest):
|
|
475
|
+
"""Gera relatório"""
|
|
476
|
+
try:
|
|
477
|
+
culturas, talhoes, regras = get_dados()
|
|
478
|
+
|
|
479
|
+
# Valida objetivo
|
|
480
|
+
objetivos_validos = ['equilibrado', 'lucro', 'risco', 'sustentavel']
|
|
481
|
+
if request.objetivo not in objetivos_validos:
|
|
482
|
+
raise HTTPException(status_code=400, detail=f"Objetivo inválido. Use: {objetivos_validos}")
|
|
483
|
+
|
|
484
|
+
# Valida formato
|
|
485
|
+
formatos_validos = ['md', 'txt']
|
|
486
|
+
if request.formato not in formatos_validos:
|
|
487
|
+
raise HTTPException(status_code=400, detail=f"Formato inválido. Use: {formatos_validos}")
|
|
488
|
+
|
|
489
|
+
# Gera relatório
|
|
490
|
+
caminho = gerar_relatorio_completo(
|
|
491
|
+
culturas, talhoes, regras,
|
|
492
|
+
objetivo=request.objetivo,
|
|
493
|
+
formato=request.formato
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
# Lê conteúdo
|
|
497
|
+
with open(caminho, 'r', encoding='utf-8') as f:
|
|
498
|
+
conteudo = f.read()
|
|
499
|
+
|
|
500
|
+
return {
|
|
501
|
+
"caminho": caminho,
|
|
502
|
+
"conteudo": conteudo,
|
|
503
|
+
"formato": request.formato
|
|
504
|
+
}
|
|
505
|
+
except HTTPException:
|
|
506
|
+
raise
|
|
507
|
+
except Exception as e:
|
|
508
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
509
|
+
|
|
510
|
+
@app.post("/cache/limpar")
|
|
511
|
+
def limpar_cache(request: Request):
|
|
512
|
+
"""Limpa o cache de resultados pesados (protegido por token)"""
|
|
513
|
+
# Verifica token de administração
|
|
514
|
+
cache_admin_token = os.getenv("CACHE_ADMIN_TOKEN")
|
|
515
|
+
|
|
516
|
+
if cache_admin_token:
|
|
517
|
+
# Se token está configurado, verifica header
|
|
518
|
+
provided_token = request.headers.get("X-Cache-Token")
|
|
519
|
+
if not provided_token or provided_token != cache_admin_token:
|
|
520
|
+
raise HTTPException(
|
|
521
|
+
status_code=403,
|
|
522
|
+
detail="Token de administração inválido ou ausente. Use header X-Cache-Token."
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
# Limpa cache
|
|
526
|
+
global _resultados_cache
|
|
527
|
+
items_removidos = len(_resultados_cache)
|
|
528
|
+
_resultados_cache.clear()
|
|
529
|
+
|
|
530
|
+
return {
|
|
531
|
+
"status": "ok",
|
|
532
|
+
"message": f"Cache limpo. {items_removidos} itens removidos.",
|
|
533
|
+
"protected": bool(cache_admin_token)
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
if __name__ == "__main__":
|
|
537
|
+
import uvicorn
|
|
538
|
+
uvicorn.run(app, host=HOST, port=PORT)
|