Spaces:
Sleeping
Sleeping
File size: 8,028 Bytes
b395f50 041af83 b395f50 041af83 b395f50 041af83 b395f50 5b0bb4b b395f50 5b0bb4b b395f50 5b0bb4b b395f50 041af83 b395f50 041af83 b395f50 041af83 b395f50 041af83 b395f50 041af83 b395f50 041af83 b395f50 041af83 b395f50 | 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 |
# main.py
# Orquestador principal de FastAPI: Endpoints, WebSockets y ciclo de vida de la aplicación.
# VERSIÓN 2.0: ARQUITECTURA SUPERIOR
import os
import asyncio
from typing import List, Optional
from datetime import datetime
from fastapi import FastAPI, WebSocket, Depends, HTTPException, status, Request, WebSocketDisconnect
from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.security import OAuth2PasswordRequestForm
from fastapi.staticfiles import StaticFiles
from sqlalchemy.orm import Session
from pydantic import BaseModel, EmailStr
import auth
import core
import database
import ui
from database import User, Memory
# --- Modelos de Datos Pydantic (Abstracción y Auto-Validación) ---
class UserCreate(BaseModel):
email: EmailStr
password: str
class Token(BaseModel):
access_token: str
token_type: str
class InitiationAnswer(BaseModel):
question: str
answer: str
class MemoryResponse(BaseModel):
content: str
timestamp: datetime
class Config:
orm_mode = True
# --- Inicialización de la Aplicación ---
app = FastAPI(
title="Samuel v2.0 - El Confidente Digital Argentino",
description="Una arquitectura cognitiva y de sistemas de nivel superior.",
version="2.0.0"
)
if not os.path.exists("static"):
os.makedirs("static")
app.mount("/static", StaticFiles(directory="static"), name="static")
# --- Ciclo de Vida de la Aplicación y Chequeos de Salud ---
@app.on_event("startup")
def on_startup():
"""Tareas a ejecutar al iniciar la aplicación con validaciones críticas."""
print("Iniciando Samuel v2.1 (Hardened)...")
# Principio Fail-Fast: si faltan secretos, la aplicación no debe iniciar.
if not os.getenv("JWT_SECRET_KEY"):
raise RuntimeError("FATAL: JWT_SECRET_KEY no configurada.")
if not os.getenv("GEMINI_API_KEY"):
raise RuntimeError("FATAL: GEMINI_API_KEY no configurada.")
database.create_db_and_tables()
print("Base de datos verificada y lista.")
core.load_tts_model()
print("Samuel v2.1 está listo para conversar.")
@app.get("/health", status_code=status.HTTP_200_OK)
def health_check():
"""Endpoint para el Health Check del orquestador de contenedores."""
return {"status": "ok"}
# --- Endpoints de Autenticación y Registro (Actualizados con Pydantic) ---
# --- Endpoints de Autenticación y Registro (Actualizados con Pydantic) ---
@app.post("/register", status_code=status.HTTP_201_CREATED)
def register_user(user: UserCreate, db: Session = Depends(database.get_db)):
db_user = db.query(User).filter(User.email == user.email).first()
if db_user:
raise HTTPException(status_code=400, detail="El email ya está registrado.")
hashed_password = auth.get_password_hash(user.password)
new_user = User(email=user.email, hashed_password=hashed_password)
db.add(new_user)
db.commit()
return {"message": f"Usuario {user.email} creado."}
@app.post("/token", response_class=JSONResponse)
def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(database.get_db)):
user = db.query(User).filter(User.email == form_data.username).first()
if not user or not auth.verify_password(form_data.password, user.hashed_password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Email o contraseña incorrectos",
)
access_token = auth.create_access_token(data={"sub": user.email})
response = JSONResponse(content={"message": "Autenticación exitosa"})
response.set_cookie(key="access_token", value=f"Bearer {access_token}", httponly=True, samesite='lax')
return response
@app.post("/logout")
def logout():
response = JSONResponse(content={"message": "Sesión cerrada"})
response.delete_cookie("access_token")
return response
# --- Endpoints de la Interfaz de Usuario (HTML) ---
@app.get("/", response_class=HTMLResponse)
async def read_root(request: Request, db: Session = Depends(database.get_db)):
token = request.cookies.get("access_token")
if not token:
return ui.get_login_page()
try:
user = await auth.get_current_user(token.replace("Bearer ", ""), db)
if not user.has_completed_initiation:
return ui.get_initiation_page()
return ui.get_main_app_page()
except HTTPException:
# Token inválido, expira la cookie y muestra el login
response = HTMLResponse(content=ui.get_login_page())
response.delete_cookie("access_token")
return response
# --- API para el Ritual de Iniciación y Memorias (Actualizados con Pydantic) ---
@app.post("/initiation/answer", status_code=status.HTTP_200_OK)
async def save_initiation_answer(answer_data: InitiationAnswer, current_user: User = Depends(auth.get_current_user), db: Session = Depends(database.get_db)):
memoria_fundacional = f"Memoria Fundacional. Pregunta: '{answer_data.question}' Respuesta: '{answer_data.answer}'"
new_memory = Memory(content=memoria_fundacional, owner=current_user)
db.add(new_memory)
user_memories_count = db.query(Memory).filter(Memory.user_id == current_user.id).count()
if user_memories_count >= 5:
current_user.has_completed_initiation = True
db.commit()
return {"message": "Respuesta guardada."}
@app.get("/api/memories", response_model=List[MemoryResponse])
async def get_user_memories(current_user: User = Depends(auth.get_current_user), db: Session = Depends(database.get_db)):
return db.query(Memory).filter(Memory.user_id == current_user.id).order_by(Memory.timestamp.desc()).all()
# --- WebSocket con Hiper-Paralelismo ---
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket, db: Session = Depends(database.get_db)):
await websocket.accept()
user = None
try:
token = websocket.cookies.get("access_token")
if not token:
await websocket.close(code=status.WS_1008_POLICY_VIOLATION, reason="Token no encontrado")
return
user = await auth.get_current_user(token.replace("Bearer ", ""), db)
if not user:
await websocket.close(code=status.WS_1008_POLICY_VIOLATION, reason="Usuario inválido")
return
history = [] # El historial se construye sobre la marcha
while True:
data = await websocket.receive_json()
user_message = data['content']
history.append({"role": "user", "parts": [user_message]})
samuel_response_text = core.get_samuel_response(history)
history.append({"role": "model", "parts": [samuel_response_text]})
# 1. Enviar texto INMEDIATAMENTE
await websocket.send_json({"type": "text", "content": samuel_response_text})
# 2. Crear tarea en paralelo para generar y enviar audio SIN BLOQUEAR
async def generate_and_send_audio(text: str):
loop = asyncio.get_running_loop()
# Ejecutar la función síncrona de TTS en un hilo separado
audio_base64 = await loop.run_in_executor(None, core.generate_audio_base64, text)
if audio_base64:
await websocket.send_json({"type": "audio", "content": audio_base64})
asyncio.create_task(generate_and_send_audio(samuel_response_text))
# 3. Procesar y guardar memoria en la base de datos
if samuel_response_text.startswith("MEMORIA_GENERADA:"):
mem_content = samuel_response_text.replace("MEMORIA_GENERADA:", "").strip()
new_memory = Memory(content=mem_content, owner=user)
db.add(new_memory)
db.commit()
except WebSocketDisconnect:
email = user.email if user else "Cliente desconocido"
print(f"{email} desconectado.")
except Exception as e:
print(f"Error fatal en WebSocket: {e}")
await websocket.close(code=status.WS_1011_INTERNAL_ERROR)
|