File size: 13,726 Bytes
3687fa1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d4dc142
3687fa1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d4dc142
3687fa1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
from fastapi import Depends, FastAPI, HTTPException, status, Request, File, UploadFile
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from pydantic import BaseModel
import jwt
from datetime import datetime, timedelta, timezone
import uvicorn
import modelsProyecto
from databaseProyecto import SessionLocal, engine
from sqlalchemy.orm import Session
import SeguridadProyecto
from SeguridadProyecto import generar_hash 
from typing import Annotated
import os
import shutil
import io
import numpy as np
import onnxruntime as ort
from PIL import Image
from ultralytics import YOLO
from fastapi.responses import StreamingResponse
from dotenv import load_dotenv
from groq import Groq
from fastapi.middleware.cors import CORSMiddleware
load_dotenv()


model = YOLO("best.onnx", task="detect")

client_groq = Groq(api_key=os.getenv("GROQ_API_KEY"))

SECRET_KEY = os.getenv("SECRET_KEY")
ALGORITHM = os.getenv("ALGORITHM")

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login")
app = FastAPI()

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)


modelsProyecto.Base.metadata.create_all(bind=engine)

def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

class Token(BaseModel):
    access_token: str
    token_type: str

registros_mensajes= {}

@app.post("/login",summary="Login de usuario", description="Comprueba las credenciales y genera un token JWT de acceso", response_model=Token)
async def login(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)):


    verificar_frecuencia(form_data.username, segundos=10)

    usuario_db = db.query(modelsProyecto.Usuario).filter(modelsProyecto.Usuario.username == form_data.username).first()

    if not usuario_db or not SeguridadProyecto.verificar_password(form_data.password, usuario_db.password_hash):
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="ID de usuario o contraseña incorrectos",
            headers={"WWW-Authenticate": "Bearer"},
        )

    expire = datetime.now(timezone.utc) + timedelta(minutes=30)
    to_encode = {
        "sub": usuario_db.username,
        "exp": expire,
        "rol": usuario_db.es_admin
    }
    
    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    return {"access_token": encoded_jwt, "token_type": "bearer"}

async def verificar_admin(token: str = Depends(oauth2_scheme)):
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username: str = payload.get("sub")
        es_admin: bool = payload.get("rol") 
        
    except jwt.PyJWTError:
        raise HTTPException(status_code=401, detail="Token inválido o expirado")

    if es_admin is not True:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN, 
            detail="No tienes permisos de administrador"
        )
    return {"username": username, "rol": es_admin}



@app.get("/revisarUsuarios", 
    summary="Listar usuarios (Solo Admin)",
    description="Retorna una lista de todos los usuarios registrados. Requiere privilegios de administrador.")
async def revisar_Usuarios(admin_info: dict = Depends(verificar_admin), db: Session = Depends(get_db)):
    usuarios = db.query(modelsProyecto.Usuario).all()
    return usuarios

def verificar_frecuencia(usuario_actual: str, segundos: int = 5):
    ahora = datetime.now()
    if usuario_actual in registros_mensajes:
        ultima_solicitud = registros_mensajes[usuario_actual]
        tiempo_transcurrido = ahora - ultima_solicitud

        if tiempo_transcurrido < timedelta(seconds=segundos):
            segundos_restantes = segundos - int(tiempo_transcurrido.total_seconds())
            raise HTTPException(
                status_code=status.HTTP_429_TOO_MANY_REQUESTS,
                detail=f"Por favor, espera {segundos_restantes} segundos antes de volver a intentarlo"
            )
    registros_mensajes[usuario_actual] = ahora

def agente_inteligente_llm(deteccion: str, confianza: float):

    saludo = "¡Bienvenido! Soy ReciclIA. Estoy aquí para ayudarte a transformar tus hábitos y cuidar el planeta de forma experta. Vamos a hacer que cada residuo cuente."
    
    prompt = f"""
    Eres un ReciclIA, un Agente experto en sostenibilidad y reciclaje. 
    Se ha detectado un objeto mediante visión artificial:
    - Objeto: {deteccion}
    - Confianza: {confianza:.2f}

    INSTRUCCIÓN DE SALUDO:
    Comienza SIEMPRE tu respuesta con esta frase exacta: "{saludo}"


    BASE DE CONOCIMIENTO (ESTRICTA):
    1. CONTENEDOR MARRÓN (Orgánico): Restos de comida, cáscaras de fruta, posos de café, tapones de corcho, servilletas sucias y cualquier residuo de origen BIOLÓGICO.
    2. CONTENEDOR VERDE (Vidrio): Botellas de vidrio, frascos de conservas, tarros de perfume. (¡NUNCA para orgánicos!).
    3. CONTENEDOR AMARILLO (Envases): Plásticos, latas, briks y envases metálicos.
    4. CONTENEDOR AZUL (Papel/Cartón): Cajas de cartón, periódicos, folletos.
    5. CONTENEDOR GRIS (Resto): Pañales, colillas, objetos rotos que no se reciclan.
    6. PUNTO LIMPIO: Pilas, electrónica, muebles, ropa, aceite usado.

    
    Explica al usuario de forma breve y amable:
    1. En qué contenedor debe tirarlo, añade el color especifico del contenedor utilizando la base de conocimientos.
    2. Un dato curioso sobre el reciclaje de ese material.
    
    REGLA CRÍTICA PARA EL PUNTO 3:
    - SI la confianza es menor a 0.5: Añade una advertencia breve diciendo que podrías estar equivocado debido a la baja precisión.
    - SI la confianza es 0.5 o mayor: NO menciones nada sobre la confianza, ni sobre umbrales, ni des explicaciones de por qué estás seguro. Omite este punto totalmente.

    Termina la explicación siempre con esta frase:
    -Recuerda, cada pequeño gesto cuenta, y reciclar correctamente es un paso importante hacia un futuro más sostenible. ¡Gracias por tu contribución!
    """

    chat_completion = client_groq.chat.completions.create(
        messages=[{"role": "user", "content": prompt}],
        model="llama-3.3-70b-versatile", # Un modelo rápido y capaz
    )
    return chat_completion.choices[0].message.content



#CRUD Usuarios

@app.post("/registrar", 
    summary="Registrar nuevo usuario",
    description="Crea la cuenta de un usuario y lo guarda en la base de datos.",
    status_code=status.HTTP_201_CREATED)
async def registrar_usuario(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)):
    existe = db.query(modelsProyecto.Usuario).filter(modelsProyecto.Usuario.username == form_data.username).first()
    if existe:
        raise HTTPException(status_code=400, detail="El nombre de usuario ya existe")

    password_con_hash = generar_hash(form_data.password)

    nuevo_usuario = modelsProyecto.Usuario(
        username=form_data.username,
        password_hash=password_con_hash,
        es_admin=False
    )
    db.add(nuevo_usuario)
    db.commit()
    return {"mensaje": "Usuario creado con éxito usando HASH"}


@app.put("/usuarios/actualizarUsuario", 
    summary="Actualizar nombre de usuario",
    description="Actualiza el nombre de usuario del perfil activo.",
    responses={400: {"description": "Nombre en uso"}, 404: {"description": "No encontrado"}})
async def actualizar_username(
    nuevo_username: str,
    token: str = Depends(oauth2_scheme), 
    db: Session = Depends(get_db)
):
    payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
    username_actual = payload.get("sub")
    
    usuario = db.query(modelsProyecto.Usuario).filter(modelsProyecto.Usuario.username == username_actual).first()
    
    if not usuario:
        raise HTTPException(status_code=404, detail="Usuario no encontrado")

    # 3. Verificar si el nuevo nombre ya está pillado por OTRO usuario
    existe = db.query(modelsProyecto.Usuario).filter(modelsProyecto.Usuario.username == nuevo_username).first()
    if existe and existe.username != username_actual:
        raise HTTPException(status_code=400, detail="Ese nombre de usuario ya está en uso")

    usuario.username = nuevo_username
    db.commit()
    db.refresh(usuario)
    
    return {"message": "Nombre actualizado", "nuevo_username": nuevo_username}

@app.delete("/usuarios/{user}", 
    summary="Eliminar cuenta",
    description="Borra un usuario. Los administradores pueden borrar cualquier cuenta; usuarios normales solo la suya.",
    status_code=status.HTTP_204_NO_CONTENT)
async def eliminar_usuario(
    user: str, 
    token: str = Depends(oauth2_scheme), 
    db: Session = Depends(get_db),
):

    payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
    username_token = payload.get("sub")
    es_admin: bool = payload.get("rol") 

    usuario = db.query(modelsProyecto.Usuario).filter(modelsProyecto.Usuario.username == user).first()
    
    if not usuario:
        raise HTTPException(status_code=404, detail="Usuario no encontrado")

    if usuario.username != username_token and not es_admin:
        raise HTTPException(status_code=403, detail="No tienes permiso para eliminar esta cuenta")

    db.delete(usuario)
    db.commit()
    
    return None 

@app.post("/detectar-visual", 
    summary="Detección de objetos y Reciclaje",
    description="Analiza una imagen, devuelve la predicción + respuesta del agente y guarda en historial.",
    responses={
        200: {"content": {"image/jpeg": {}}, "description": "Imagen procesada con cajas de detección."},
        429: {"description": "Límite de velocidad excedido"}
    })
async def detectar_visual(file: UploadFile = File(...), token: str = Depends(oauth2_scheme),db: Session = Depends(get_db)):
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username: str = payload.get("sub")
        usuario = db.query(modelsProyecto.Usuario).filter(modelsProyecto.Usuario.username == username).first()
        if not usuario:
            raise HTTPException(status_code=404, detail="Usuario no encontrado")
    except jwt.PyJWTError:
        raise HTTPException(status_code=401, detail="Token inválido")
    

    verificar_frecuencia(username, segundos=30)

    contents = await file.read()
    image = Image.open(io.BytesIO(contents)).convert("RGB")
    
    results = model.predict(image, imgsz=640, conf=0.25)[0]

    prediccion_nombre = "Sin detecciones"
    confianza_valor = 0.0

    if len(results.boxes) > 0:
        box = results.boxes[0]
        prediccion_nombre = results.names[int(box.cls)]
        confianza_valor = float(box.conf)
        respuesta_agente = agente_inteligente_llm(prediccion_nombre, confianza_valor)
    else:
        respuesta_agente = "No veo nada claro aquí. ¿Puedes acercar más la cámara?"


    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    
    ext = file.filename.split(".")[-1]
    nombre_unico = f"img_{timestamp}.{ext}"

    
    if not os.path.exists("imagenes_subidas"):
        os.makedirs("imagenes_subidas")
    
    ruta_archivo = f"imagenes_subidas/{nombre_unico}"
    with open(ruta_archivo, "wb") as buffer:
        buffer.write(contents)

    nueva_imagen = modelsProyecto.Imagen(
        name=nombre_unico,
        ruta=ruta_archivo,
        usuario_id=usuario.id,
        prediccion=prediccion_nombre,
        confianza=confianza_valor
    )
    db.add(nueva_imagen)
    
    nuevo_historial = modelsProyecto.HistorialChat(
        usuario_id=usuario.id,
        mensaje_usuario=f"Detección de {prediccion_nombre}",
        respuesta_agente=respuesta_agente
    )
    db.add(nuevo_historial)
    db.commit()
    
    im_array = results.plot() 
    im_rgb = Image.fromarray(im_array[..., ::-1])
    
    img_io = io.BytesIO()
    im_rgb.save(img_io, 'JPEG', quality=70)
    img_io.seek(0)
    return StreamingResponse(
        img_io, 
        media_type="image/jpeg",
        headers={
            "X-Agent-Reply": respuesta_agente.replace("\n", " | "), 
            "X-Detection": prediccion_nombre
        }
    )

@app.get("/historial", 
    summary="Obtener historial de chat",
    description="Recupera las últimas 10 consultas del usuario autenticado.")
async def obtener_mi_historial(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)):
    payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
    username = payload.get("sub")
    usuario = db.query(modelsProyecto.Usuario).filter(modelsProyecto.Usuario.username == username).first()
    
    # Trae los últimos 10 mensajes
    return db.query(modelsProyecto.HistorialChat).filter(
        modelsProyecto.HistorialChat.usuario_id == usuario.id
    ).order_by(modelsProyecto.HistorialChat.fecha.desc()).limit(10).all()


@app.delete("/historial", 
    summary="Vaciar historial",
    description="Borra permanentemente todo el historial del usuari.")
async def vaciar_todo_el_historial(
    token: str = Depends(oauth2_scheme), 
    db: Session = Depends(get_db)
):
    
    payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
    username = payload.get("sub")
    usuario = db.query(modelsProyecto.Usuario).filter(modelsProyecto.Usuario.username == username).first()

    registros = db.query(modelsProyecto.HistorialChat).filter(
        modelsProyecto.HistorialChat.usuario_id == usuario.id
    )
    
    cantidad = registros.count()
    registros.delete(synchronize_session=False)
    db.commit()
    
    return {"mensaje": f"Se han eliminado {cantidad} registros de tu historial"}


if __name__ == "__main__":
    uvicorn.run("ProyectoBasura:app", host="127.0.0.1", port=8000, reload=True)