first commit

This commit is contained in:
2025-11-19 01:09:25 -03:00
parent e7a380f36e
commit be10a888fb
28 changed files with 2481 additions and 464 deletions

View File

@@ -9,7 +9,6 @@ from datetime import datetime, timedelta
from app.core.database import engine, get_db, Base
from app.core.security import verify_password, get_password_hash, create_access_token, decode_access_token
from app.core.config import settings
from app import models, schemas
# Crear tablas
@@ -17,18 +16,15 @@ Base.metadata.create_all(bind=engine)
app = FastAPI(title="Checklist Inteligente API", version="1.0.0")
# CORS - Usar configuración de settings
# CORS
app.add_middleware(
CORSMiddleware,
allow_origins=settings.cors_origins,
allow_origins=["http://localhost:5173", "http://localhost:3000"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Log para debug
print(f"🌐 CORS configured for origins: {settings.cors_origins}")
security = HTTPBearer()
# Dependency para obtener usuario actual
@@ -451,6 +447,428 @@ async def upload_photo(
return media_file
# ============= AI ANALYSIS =============
@app.get("/api/ai/models", response_model=List[schemas.AIModelInfo])
def get_available_ai_models(current_user: models.User = Depends(get_current_user)):
"""Obtener lista de modelos de IA disponibles"""
if current_user.role != "admin":
raise HTTPException(status_code=403, detail="Solo administradores pueden ver modelos de IA")
models_list = [
# OpenAI Models
{
"id": "gpt-4o",
"name": "GPT-4o (Recomendado)",
"provider": "openai",
"description": "Modelo multimodal más avanzado de OpenAI, rápido y preciso para análisis de imágenes"
},
{
"id": "gpt-4o-mini",
"name": "GPT-4o Mini",
"provider": "openai",
"description": "Versión compacta y económica de GPT-4o, ideal para análisis rápidos"
},
{
"id": "gpt-4-turbo",
"name": "GPT-4 Turbo",
"provider": "openai",
"description": "Modelo potente con capacidades de visión y contexto amplio"
},
{
"id": "gpt-4-vision-preview",
"name": "GPT-4 Vision (Preview)",
"provider": "openai",
"description": "Modelo específico para análisis de imágenes (versión previa)"
},
# Gemini Models - Actualizados a versiones 2.0, 2.5 y 3.0
{
"id": "gemini-3-pro-preview",
"name": "Gemini 3 Pro Preview (Último)",
"provider": "gemini",
"description": "Modelo de próxima generación en preview, máxima capacidad de análisis"
},
{
"id": "gemini-2.5-pro",
"name": "Gemini 2.5 Pro (Recomendado)",
"provider": "gemini",
"description": "Último modelo estable con excelente análisis visual y razonamiento avanzado"
},
{
"id": "gemini-2.5-flash",
"name": "Gemini 2.5 Flash",
"provider": "gemini",
"description": "Versión rápida del 2.5, ideal para inspecciones en tiempo real"
},
{
"id": "gemini-2.0-flash",
"name": "Gemini 2.0 Flash",
"provider": "gemini",
"description": "Modelo rápido y eficiente de la generación 2.0"
},
{
"id": "gemini-1.5-pro-latest",
"name": "Gemini 1.5 Pro Latest",
"provider": "gemini",
"description": "Versión estable 1.5 con contexto de 2M tokens"
},
{
"id": "gemini-1.5-flash-latest",
"name": "Gemini 1.5 Flash Latest",
"provider": "gemini",
"description": "Modelo 1.5 rápido para análisis básicos"
}
]
return models_list
@app.get("/api/ai/configuration", response_model=schemas.AIConfiguration)
def get_ai_configuration(
db: Session = Depends(get_db),
current_user: models.User = Depends(get_current_user)
):
"""Obtener configuración de IA actual"""
if current_user.role != "admin":
raise HTTPException(status_code=403, detail="Solo administradores pueden ver configuración de IA")
config = db.query(models.AIConfiguration).filter(
models.AIConfiguration.is_active == True
).first()
if not config:
raise HTTPException(status_code=404, detail="No hay configuración de IA activa")
return config
@app.post("/api/ai/configuration", response_model=schemas.AIConfiguration)
def create_ai_configuration(
config: schemas.AIConfigurationCreate,
db: Session = Depends(get_db),
current_user: models.User = Depends(get_current_user)
):
"""Crear o actualizar configuración de IA"""
if current_user.role != "admin":
raise HTTPException(status_code=403, detail="Solo administradores pueden configurar IA")
# Desactivar configuraciones anteriores
db.query(models.AIConfiguration).update({"is_active": False})
# Crear nueva configuración
new_config = models.AIConfiguration(
provider=config.provider,
api_key=config.api_key,
model_name=config.model_name,
is_active=True
)
db.add(new_config)
db.commit()
db.refresh(new_config)
return new_config
@app.put("/api/ai/configuration/{config_id}", response_model=schemas.AIConfiguration)
def update_ai_configuration(
config_id: int,
config_update: schemas.AIConfigurationUpdate,
db: Session = Depends(get_db),
current_user: models.User = Depends(get_current_user)
):
"""Actualizar configuración de IA existente"""
if current_user.role != "admin":
raise HTTPException(status_code=403, detail="Solo administradores pueden actualizar configuración de IA")
config = db.query(models.AIConfiguration).filter(
models.AIConfiguration.id == config_id
).first()
if not config:
raise HTTPException(status_code=404, detail="Configuración no encontrada")
# Actualizar campos
for key, value in config_update.dict(exclude_unset=True).items():
setattr(config, key, value)
db.commit()
db.refresh(config)
return config
@app.delete("/api/ai/configuration/{config_id}")
def delete_ai_configuration(
config_id: int,
db: Session = Depends(get_db),
current_user: models.User = Depends(get_current_user)
):
"""Eliminar configuración de IA"""
if current_user.role != "admin":
raise HTTPException(status_code=403, detail="Solo administradores pueden eliminar configuración de IA")
config = db.query(models.AIConfiguration).filter(
models.AIConfiguration.id == config_id
).first()
if not config:
raise HTTPException(status_code=404, detail="Configuración no encontrada")
db.delete(config)
db.commit()
return {"message": "Configuración eliminada correctamente"}
@app.post("/api/analyze-image")
async def analyze_image(
file: UploadFile = File(...),
question_id: int = None,
db: Session = Depends(get_db),
current_user: models.User = Depends(get_current_user)
):
"""
Analiza una imagen usando IA para sugerir respuestas
Usa la configuración de IA activa (OpenAI o Gemini)
"""
# Obtener configuración de IA activa
ai_config = db.query(models.AIConfiguration).filter(
models.AIConfiguration.is_active == True
).first()
if not ai_config:
return {
"status": "disabled",
"message": "No hay configuración de IA activa. Configure en Settings."
}
# Guardar imagen temporalmente
import base64
contents = await file.read()
image_b64 = base64.b64encode(contents).decode('utf-8')
# Obtener contexto de la pregunta si se proporciona
question_obj = None
if question_id:
question_obj = db.query(models.Question).filter(models.Question.id == question_id).first()
try:
# Construir prompt dinámico basado en la pregunta específica
if question_obj:
# Prompt altamente específico para la pregunta
question_text = question_obj.text
question_type = question_obj.type
section = question_obj.section
system_prompt = f"""Eres un mecánico experto realizando una inspección vehicular.
PREGUNTA ESPECÍFICA A RESPONDER: "{question_text}"
Sección: {section}
Analiza la imagen ÚNICAMENTE para responder esta pregunta específica.
Sé directo y enfócate solo en lo que la pregunta solicita.
Responde en formato JSON:
{{
"status": "ok|minor|critical",
"observations": "Respuesta específica a: {question_text}",
"recommendation": "Acción si aplica",
"confidence": 0.0-1.0
}}
IMPORTANTE:
- Responde SOLO lo que la pregunta pide
- No des información genérica del vehículo
- Sé específico y técnico
- Si la pregunta es pass/fail, indica claramente si pasa o falla
- Si la pregunta es bueno/regular/malo, indica el estado específico del componente"""
user_message = f"Inspecciona la imagen y responde específicamente: {question_text}"
else:
# Fallback para análisis general
system_prompt = """Eres un experto mecánico automotriz. Analiza la imagen y proporciona:
1. Estado del componente (bueno/regular/malo)
2. Nivel de criticidad (ok/minor/critical)
3. Observaciones técnicas breves
4. Recomendación de acción
Responde en formato JSON:
{
"status": "ok|minor|critical",
"observations": "descripción técnica",
"recommendation": "acción sugerida",
"confidence": 0.0-1.0
}"""
user_message = "Analiza este componente del vehículo para la inspección general."
if ai_config.provider == "openai":
import openai
openai.api_key = ai_config.api_key
response = openai.ChatCompletion.create(
model=ai_config.model_name,
messages=[
{"role": "system", "content": system_prompt},
{
"role": "user",
"content": [
{
"type": "text",
"text": user_message
},
{
"type": "image_url",
"image_url": {"url": f"data:image/jpeg;base64,{image_b64}"}
}
]
}
],
max_tokens=500
)
ai_response = response.choices[0].message.content
elif ai_config.provider == "gemini":
import google.generativeai as genai
from PIL import Image
from io import BytesIO
genai.configure(api_key=ai_config.api_key)
model = genai.GenerativeModel(ai_config.model_name)
# Convertir base64 a imagen PIL
image = Image.open(BytesIO(contents))
prompt = f"{system_prompt}\n\n{user_message}"
response = model.generate_content([prompt, image])
ai_response = response.text
else:
return {
"success": False,
"error": f"Provider {ai_config.provider} no soportado"
}
# Intentar parsear como JSON, si falla, usar texto plano
try:
import json
import re
# Limpiar markdown code blocks si existen
cleaned_response = ai_response.strip()
# Remover ```json ... ``` si existe
if cleaned_response.startswith('```'):
# Extraer contenido entre ``` markers
match = re.search(r'```(?:json)?\s*\n?(.*?)\n?```', cleaned_response, re.DOTALL)
if match:
cleaned_response = match.group(1).strip()
analysis = json.loads(cleaned_response)
except:
# Si no es JSON válido, crear estructura básica
analysis = {
"status": "ok",
"observations": ai_response,
"recommendation": "Revisar manualmente",
"confidence": 0.7
}
return {
"success": True,
"analysis": analysis,
"raw_response": ai_response,
"model": ai_config.model_name,
"provider": ai_config.provider
}
except Exception as e:
print(f"Error en análisis AI: {e}")
import traceback
traceback.print_exc()
return {
"success": False,
"error": str(e),
"message": "Error analyzing image with AI. Please check AI configuration in Settings."
}
try:
import openai
openai.api_key = settings.OPENAI_API_KEY
# Prompt especializado para inspección vehicular
system_prompt = """Eres un experto mecánico automotriz. Analiza la imagen y proporciona:
1. Estado del componente (bueno/regular/malo)
2. Nivel de criticidad (ok/minor/critical)
3. Observaciones técnicas breves
4. Recomendación de acción
Responde en formato JSON:
{
"status": "ok|minor|critical",
"observations": "descripción técnica",
"recommendation": "acción sugerida",
"confidence": 0.0-1.0
}"""
response = openai.ChatCompletion.create(
model="gpt-4-vision-preview" if "gpt-4" in str(settings.OPENAI_API_KEY) else "gpt-4o",
messages=[
{
"role": "system",
"content": system_prompt
},
{
"role": "user",
"content": [
{
"type": "text",
"text": f"Analiza este componente del vehículo.\n{question_context}"
},
{
"type": "image_url",
"image_url": {
"url": f"data:image/jpeg;base64,{image_b64}"
}
}
]
}
],
max_tokens=500
)
ai_response = response.choices[0].message.content
# Intentar parsear como JSON, si falla, usar texto plano
try:
import json
analysis = json.loads(ai_response)
except:
# Si no es JSON válido, crear estructura básica
analysis = {
"status": "ok",
"observations": ai_response,
"recommendation": "Revisar manualmente",
"confidence": 0.7
}
return {
"success": True,
"analysis": analysis,
"raw_response": ai_response,
"model": "gpt-4-vision"
}
except Exception as e:
print(f"Error en análisis AI: {e}")
return {
"success": False,
"error": str(e),
"message": "Error analyzing image with AI"
}
# ============= HEALTH CHECK =============
@app.get("/")
def root():