Actualziacion de analisis de ia con las imagenes y se agrega el campo de cod operario en el front y en el back

This commit is contained in:
2025-11-26 01:20:26 -03:00
parent cbfab59222
commit 822ab5a1cb
5 changed files with 129 additions and 38 deletions

View File

@@ -325,6 +325,7 @@ def create_user(
username=user.username, username=user.username,
email=user.email, email=user.email,
full_name=user.full_name, full_name=user.full_name,
employee_code=user.employee_code,
role=user.role, role=user.role,
password_hash=hashed_password, password_hash=hashed_password,
is_active=True is_active=True
@@ -374,6 +375,9 @@ def update_user(
if user_update.full_name is not None: if user_update.full_name is not None:
db_user.full_name = user_update.full_name db_user.full_name = user_update.full_name
if user_update.employee_code is not None:
db_user.employee_code = user_update.employee_code
# Solo admin puede cambiar roles # Solo admin puede cambiar roles
if user_update.role is not None: if user_update.role is not None:
if current_user.role != "admin": if current_user.role != "admin":
@@ -1519,9 +1523,9 @@ def delete_ai_configuration(
@app.post("/api/analyze-image") @app.post("/api/analyze-image")
async def analyze_image( async def analyze_image(
file: UploadFile = File(...), file: UploadFile = File(...),
question_id: int = None, question_id: int = Form(None),
inspection_id: int = None, inspection_id: int = Form(None),
custom_prompt: str = None, custom_prompt: str = Form(None),
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: models.User = Depends(get_current_user) current_user: models.User = Depends(get_current_user)
): ):
@@ -1530,6 +1534,15 @@ async def analyze_image(
Usa la configuración de IA activa (OpenAI o Gemini) Usa la configuración de IA activa (OpenAI o Gemini)
Incluye contexto del vehículo si se proporciona inspection_id Incluye contexto del vehículo si se proporciona inspection_id
""" """
print("\n" + "="*80)
print("🔍 ANALYZE IMAGE - DEBUG")
print("="*80)
print(f"📥 Parámetros recibidos:")
print(f" - file: {file.filename}")
print(f" - question_id: {question_id}")
print(f" - inspection_id: {inspection_id}")
print(f" - custom_prompt (del Form): {custom_prompt[:100] if custom_prompt else 'NO RECIBIDO'}")
# Obtener configuración de IA activa # Obtener configuración de IA activa
ai_config = db.query(models.AIConfiguration).filter( ai_config = db.query(models.AIConfiguration).filter(
models.AIConfiguration.is_active == True models.AIConfiguration.is_active == True
@@ -1551,71 +1564,71 @@ async def analyze_image(
question_obj = None question_obj = None
if question_id: if question_id:
question_obj = db.query(models.Question).filter(models.Question.id == question_id).first() question_obj = db.query(models.Question).filter(models.Question.id == question_id).first()
print(f"📋 Pregunta encontrada:")
print(f" - ID: {question_obj.id}")
print(f" - Texto: {question_obj.text}")
print(f" - ai_prompt en DB: {question_obj.ai_prompt[:100] if question_obj.ai_prompt else 'NO TIENE'}")
# Si no se proporciona custom_prompt en el Form, usar el de la pregunta
if not custom_prompt and question_obj and question_obj.ai_prompt:
custom_prompt = question_obj.ai_prompt
print(f"✅ Usando ai_prompt de la pregunta de la DB")
elif custom_prompt:
print(f"✅ Usando custom_prompt del Form")
else:
print(f"⚠️ NO HAY custom_prompt (ni del Form ni de la DB)")
print(f"📝 Custom prompt FINAL a usar: {custom_prompt[:150] if custom_prompt else 'NINGUNO'}...")
# Obtener contexto del vehículo si se proporciona inspection_id # Obtener contexto del vehículo si se proporciona inspection_id
vehicle_context = "" vehicle_context = ""
if inspection_id: if inspection_id:
inspection = db.query(models.Inspection).filter(models.Inspection.id == inspection_id).first() inspection = db.query(models.Inspection).filter(models.Inspection.id == inspection_id).first()
if inspection: if inspection:
print(f"🚗 Contexto del vehículo agregado: {inspection.vehicle_brand} {inspection.vehicle_model}")
vehicle_context = f""" vehicle_context = f"""
INFORMACIÓN DEL VEHÍCULO INSPECCIONADO: INFORMACIÓN DEL VEHÍCULO INSPECCIONADO:
- Marca: {inspection.vehicle_brand} - Marca: {inspection.vehicle_brand}
- Modelo: {inspection.vehicle_model} - Modelo: {inspection.vehicle_model}
- Año: {inspection.vehicle_year or 'No especificado'}
- Placa: {inspection.vehicle_plate} - Placa: {inspection.vehicle_plate}
- Kilometraje: {inspection.mileage} km - Kilometraje: {inspection.vehicle_km} km
- Cliente: {inspection.client_name} - Cliente: {inspection.client_name}
- OR/Orden: {inspection.or_number} - OR/Orden: {inspection.or_number}
""" """
else:
print(f"⚠️ inspection_id {inspection_id} no encontrado en DB")
else:
print(f"⚠️ NO se proporcionó inspection_id, sin contexto de vehículo")
try: try:
# Construir prompt dinámico basado en la pregunta específica # Construir prompt dinámico basado en la pregunta específica
if question_obj: if question_obj:
# Usar prompt personalizado si está disponible # Usar prompt personalizado si está disponible
if custom_prompt: if custom_prompt:
# Prompt 100% personalizado por el administrador # Prompt personalizado - DIRECTO Y SIMPLE
system_prompt = f"""Eres un mecánico experto realizando una inspección vehicular. system_prompt = f"""Eres un mecánico experto realizando una inspección vehicular.
{vehicle_context} {vehicle_context}
INSTRUCCIONES ESPECÍFICAS DEL ADMINISTRADOR PARA ESTA PREGUNTA: TAREA ESPECÍFICA:
{custom_prompt} {custom_prompt}
PREGUNTA A RESPONDER: "{question_obj.text}"
Sección: {question_obj.section}
IMPORTANTE - VALIDACIÓN ESTRICTA:
1. Lee CUIDADOSAMENTE las instrucciones específicas del administrador arriba
2. Verifica si la imagen proporcionada PERMITE responder lo que se pide
3. Si las instrucciones piden verificar algo dinámico (como "si prende", "si funciona", "si enciende"):
- Y la imagen es estática (foto), indica en "recommendation" que NO se puede verificar con una foto estática
- Sugiere que se necesita una prueba en vivo o un video
4. Si la imagen NO corresponde a lo que piden las instrucciones, indica claramente en "recommendation" qué foto necesitan tomar
VALIDACIÓN DE IMAGEN:
- Si piden verificar funcionamiento (prende, enciende, funciona) pero solo hay una foto → Indica "No se puede verificar funcionamiento con foto estática. Se requiere prueba en vivo."
- Si la imagen es borrosa o no permite análisis → Indica que tomen otra foto más clara
- Si la imagen muestra un componente diferente al solicitado → Indica qué foto necesitan tomar
Responde SOLO en formato JSON válido (sin markdown, sin ```json): Responde SOLO en formato JSON válido (sin markdown, sin ```json):
{{ {{
"status": "minor", "status": "ok",
"observations": "Describe lo que SÍ puedes ver en la imagen y explica por qué no puedes responder completamente la pregunta si aplica", "observations": "Describe lo que observas en la imagen en relación a la tarea solicitada",
"recommendation": "Si no puedes verificar lo solicitado con la imagen, explica claramente QUÉ se necesita (prueba en vivo, video, foto diferente, etc.)", "recommendation": "Acción sugerida basada en lo observado",
"confidence": 0.85 "confidence": 0.85
}} }}
VALORES DE STATUS: VALORES DE STATUS:
- "ok": Solo si puedes CONFIRMAR que todo está bien según las instrucciones - "ok": Cumple con lo esperado según la tarea
- "minor": Si hay limitaciones en la imagen o no puedes verificar completamente lo solicitado - "minor": Presenta observaciones menores o advertencias
- "critical": Si hay problemas graves visibles o la imagen es completamente inadecuada - "critical": Presenta problemas graves o no cumple con lo esperado
RECORDATORIO: En tus observaciones, menciona si el estado es apropiado para el kilometraje y marca/modelo del vehículo cuando sea relevante.""" IMPORTANTE: Si la tarea requiere verificar funcionamiento (algo encendido, prendido, activo) pero la imagen muestra el componente apagado o en reposo, usa status "critical" e indica en "recommendation" que se necesita una foto con el componente funcionando o un video."""
if vehicle_context: user_message = f"Pregunta de inspección: {question_obj.text}\n\nAnaliza esta imagen según la tarea especificada."
user_message = f"Inspecciona esta imagen del vehículo. Las instrucciones específicas requieren: '{custom_prompt}'. Verifica si con esta imagen puedes responder completamente esa solicitud."
else:
user_message = f"Inspecciona la imagen. Las instrucciones requieren: '{custom_prompt}'. Verifica si puedes responder esa solicitud con esta imagen."
else: else:
# Prompt altamente específico para la pregunta # Prompt altamente específico para la pregunta
question_text = question_obj.text question_text = question_obj.text
@@ -1682,6 +1695,13 @@ Responde SOLO en formato JSON válido (sin markdown, sin ```json):
NOTA: "status" debe ser "ok" (bueno), "minor" (problemas leves) o "critical" (problemas graves).""" NOTA: "status" debe ser "ok" (bueno), "minor" (problemas leves) o "critical" (problemas graves)."""
user_message = "Analiza este componente del vehículo para la inspección general." user_message = "Analiza este componente del vehículo para la inspección general."
print(f"\n🤖 PROMPT ENVIADO AL AI:")
print(f"Provider: {ai_config.provider}")
print(f"Model: {ai_config.model_name}")
print(f"System prompt (primeros 200 chars): {system_prompt[:200]}...")
print(f"User message: {user_message}")
print("="*80 + "\n")
if ai_config.provider == "openai": if ai_config.provider == "openai":
import openai import openai
openai.api_key = ai_config.api_key openai.api_key = ai_config.api_key

View File

@@ -12,6 +12,7 @@ class User(Base):
password_hash = Column(String(255), nullable=False) password_hash = Column(String(255), nullable=False)
role = Column(String(20), nullable=False) # admin, mechanic, asesor role = Column(String(20), nullable=False) # admin, mechanic, asesor
full_name = Column(String(100)) full_name = Column(String(100))
employee_code = Column(String(50)) # Nro Operario - código de otro sistema
is_active = Column(Boolean, default=True) is_active = Column(Boolean, default=True)
created_at = Column(DateTime(timezone=True), server_default=func.now()) created_at = Column(DateTime(timezone=True), server_default=func.now())

View File

@@ -7,6 +7,7 @@ class UserBase(BaseModel):
username: str username: str
email: Optional[EmailStr] = None email: Optional[EmailStr] = None
full_name: Optional[str] = None full_name: Optional[str] = None
employee_code: Optional[str] = None # Nro Operario - código de otro sistema
role: str = "mechanic" role: str = "mechanic"
class UserCreate(UserBase): class UserCreate(UserBase):
@@ -16,6 +17,7 @@ class UserUpdate(BaseModel):
username: Optional[str] = None username: Optional[str] = None
email: Optional[EmailStr] = None email: Optional[EmailStr] = None
full_name: Optional[str] = None full_name: Optional[str] = None
employee_code: Optional[str] = None
role: Optional[str] = None role: Optional[str] = None
class UserPasswordUpdate(BaseModel): class UserPasswordUpdate(BaseModel):
@@ -31,6 +33,7 @@ class UserLogin(BaseModel):
class User(UserBase): class User(UserBase):
id: int id: int
employee_code: Optional[str] = None
is_active: bool is_active: bool
created_at: datetime created_at: datetime

View File

@@ -3088,14 +3088,25 @@ function InspectionModal({ checklist, user, onClose, onComplete }) {
formData.append('file', file) formData.append('file', file)
formData.append('question_id', question.id.toString()) formData.append('question_id', question.id.toString())
console.log('📤 DATOS ENVIADOS AL BACKEND:')
console.log(' - question_id:', question.id)
console.log(' - question.text:', question.text)
console.log(' - question.ai_prompt:', question.ai_prompt || 'NO TIENE')
// Include inspection_id for vehicle context // Include inspection_id for vehicle context
if (inspectionId) { if (inspectionId) {
formData.append('inspection_id', inspectionId.toString()) formData.append('inspection_id', inspectionId.toString())
console.log(' - inspection_id:', inspectionId)
} else {
console.log(' - inspection_id: NO ENVIADO')
} }
// Include custom prompt if available // Include custom prompt if available
if (question.ai_prompt) { if (question.ai_prompt) {
formData.append('custom_prompt', question.ai_prompt) formData.append('custom_prompt', question.ai_prompt)
console.log(' - custom_prompt ENVIADO:', question.ai_prompt)
} else {
console.log(' - custom_prompt: NO ENVIADO (pregunta no tiene ai_prompt)')
} }
const response = await fetch(`${API_URL}/api/analyze-image`, { const response = await fetch(`${API_URL}/api/analyze-image`, {
@@ -3104,13 +3115,21 @@ function InspectionModal({ checklist, user, onClose, onComplete }) {
body: formData body: formData
}) })
console.log('📥 RESPUESTA DEL BACKEND:')
console.log(' - Status:', response.status)
if (response.ok) { if (response.ok) {
const result = await response.json() const result = await response.json()
console.log(' - Result completo:', JSON.stringify(result, null, 2))
// Check if AI analysis was successful // Check if AI analysis was successful
if (result.success && result.analysis) { if (result.success && result.analysis) {
analyses.push(result) analyses.push(result)
console.log('✅ Análisis IA:', result) console.log('✅ Análisis IA exitoso')
console.log(' - Provider:', result.provider)
console.log(' - Model:', result.model)
console.log(' - Status:', result.analysis.status)
console.log(' - Observations:', result.analysis.observations)
} else { } else {
console.warn('⚠️ Error en análisis IA:', result.error || result.message) console.warn('⚠️ Error en análisis IA:', result.error || result.message)
// Show user-friendly error // Show user-friendly error
@@ -3119,7 +3138,8 @@ function InspectionModal({ checklist, user, onClose, onComplete }) {
} }
} }
} else { } else {
console.warn('⚠️ Error HTTP en análisis IA:', response.status, await response.text()) const errorText = await response.text()
console.warn('⚠️ Error HTTP en análisis IA:', response.status, errorText)
} }
} }
@@ -3703,6 +3723,7 @@ function UsersTab({ user }) {
username: '', username: '',
email: '', email: '',
password: '', password: '',
employee_code: '',
role: 'mechanic' role: 'mechanic'
}) })
@@ -3746,7 +3767,7 @@ function UsersTab({ user }) {
if (response.ok) { if (response.ok) {
setShowCreateForm(false) setShowCreateForm(false)
setFormData({ username: '', email: '', password: '', role: 'mechanic' }) setFormData({ username: '', email: '', password: '', employee_code: '', role: 'mechanic' })
loadUsers() loadUsers()
} else { } else {
const error = await response.json() const error = await response.json()
@@ -3872,6 +3893,9 @@ function UsersTab({ user }) {
<div> <div>
<h3 className="font-semibold text-gray-800">{u.username}</h3> <h3 className="font-semibold text-gray-800">{u.username}</h3>
<p className="text-sm text-gray-500">{u.email}</p> <p className="text-sm text-gray-500">{u.email}</p>
{u.employee_code && (
<p className="text-xs text-gray-400 mt-0.5">Nro Operario: {u.employee_code}</p>
)}
<div className="flex gap-2 mt-1"> <div className="flex gap-2 mt-1">
<span className={`text-xs px-2 py-1 rounded ${ <span className={`text-xs px-2 py-1 rounded ${
u.role === 'admin' u.role === 'admin'
@@ -3960,6 +3984,19 @@ function UsersTab({ user }) {
/> />
</div> </div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Nro Operario <span className="text-gray-400 text-xs">(opcional)</span>
</label>
<input
type="text"
value={formData.employee_code || ''}
onChange={(e) => setFormData({...formData, employee_code: e.target.value})}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
placeholder="Código de operario"
/>
</div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-1">
Contraseña Contraseña
@@ -3993,7 +4030,7 @@ function UsersTab({ user }) {
type="button" type="button"
onClick={() => { onClick={() => {
setShowCreateForm(false) setShowCreateForm(false)
setFormData({ username: '', email: '', password: '', role: 'mechanic' }) setFormData({ username: '', email: '', password: '', employee_code: '', role: 'mechanic' })
}} }}
className="flex-1 px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition" className="flex-1 px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition"
> >
@@ -4021,6 +4058,7 @@ function UsersTab({ user }) {
const updates = { const updates = {
username: editingUser.username, username: editingUser.username,
email: editingUser.email, email: editingUser.email,
employee_code: editingUser.employee_code,
role: editingUser.role role: editingUser.role
} }
handleUpdateUser(editingUser.id, updates) handleUpdateUser(editingUser.id, updates)
@@ -4051,6 +4089,19 @@ function UsersTab({ user }) {
/> />
</div> </div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Nro Operario <span className="text-gray-400 text-xs">(opcional)</span>
</label>
<input
type="text"
value={editingUser.employee_code || ''}
onChange={(e) => setEditingUser({...editingUser, employee_code: e.target.value})}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
placeholder="Código de operario"
/>
</div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-1">
Rol Rol

View File

@@ -0,0 +1,16 @@
-- Migración: Agregar campo employee_code (Nro Operario) a la tabla users
-- Fecha: 2025-11-26
-- Descripción: Agrega un campo opcional para almacenar el código de operario de otro sistema
-- Agregar columna employee_code a la tabla users
ALTER TABLE users
ADD COLUMN IF NOT EXISTS employee_code VARCHAR(50);
-- Comentario descriptivo
COMMENT ON COLUMN users.employee_code IS 'Número de operario - código de identificación de otro sistema';
-- Verificar que la columna se agregó correctamente
SELECT column_name, data_type, character_maximum_length
FROM information_schema.columns
WHERE table_name = 'users'
AND column_name = 'employee_code';