✅ Validación de Coherencia IA Implementada
Cambios en el Backend (v1.0.96) Nuevo campo expected_answer en el análisis de IA: La IA ahora retorna cuál debería ser la respuesta correcta según lo que observa en la imagen Se incluyen las opciones de respuesta disponibles en el prompt para que la IA elija la correcta Extracción de opciones de pregunta: El sistema extrae las opciones disponibles (Buen Estado, Mal Estado, etc.) Las envía a la IA para que determine cuál es la respuesta esperada Cambios en el Frontend Validación antes de continuar: Cuando el mecánico intenta avanzar a la siguiente pregunta o firmar El sistema compara su respuesta con expected_answer del análisis de IA Si NO coinciden, aparece un popup con:
This commit is contained in:
@@ -276,7 +276,7 @@ def extract_pdf_text_smart(pdf_content: bytes, max_chars: int = None) -> dict:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
BACKEND_VERSION = "1.0.95"
|
BACKEND_VERSION = "1.0.96"
|
||||||
app = FastAPI(title="Checklist Inteligente API", version=BACKEND_VERSION)
|
app = FastAPI(title="Checklist Inteligente API", version=BACKEND_VERSION)
|
||||||
|
|
||||||
# S3/MinIO configuration
|
# S3/MinIO configuration
|
||||||
@@ -1807,8 +1807,8 @@ def generate_inspection_pdf(inspection_id: int, db: Session) -> str:
|
|||||||
elements.append(Spacer(1, 3*mm))
|
elements.append(Spacer(1, 3*mm))
|
||||||
|
|
||||||
# Cuadro de métricas con diseño moderno
|
# Cuadro de métricas con diseño moderno
|
||||||
metric_label = ParagraphStyle('metric_label', parent=small_style, fontSize=9, textColor=colors.HexColor('#64748b'), alignment=TA_CENTER)
|
metric_label = ParagraphStyle('metric_label', parent=small_style, fontSize=10, textColor=colors.HexColor('#64748b'), alignment=TA_CENTER)
|
||||||
metric_value = ParagraphStyle('metric_value', parent=info_style, fontSize=16, fontName='Helvetica-Bold', alignment=TA_CENTER)
|
metric_value = ParagraphStyle('metric_value', parent=info_style, fontSize=18, fontName='Helvetica-Bold', alignment=TA_CENTER)
|
||||||
|
|
||||||
metrics_data = [
|
metrics_data = [
|
||||||
[Paragraph("Puntuación", metric_label), Paragraph("Ítems Críticos", metric_label)],
|
[Paragraph("Puntuación", metric_label), Paragraph("Ítems Críticos", metric_label)],
|
||||||
@@ -1818,13 +1818,13 @@ def generate_inspection_pdf(inspection_id: int, db: Session) -> str:
|
|||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
score_table = Table(metrics_data, colWidths=[90*mm, 90*mm])
|
score_table = Table(metrics_data, colWidths=[90*mm, 90*mm], rowHeights=[12*mm, 18*mm])
|
||||||
score_table.setStyle(TableStyle([
|
score_table.setStyle(TableStyle([
|
||||||
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#f8fafc')),
|
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#f8fafc')),
|
||||||
('BACKGROUND', (0, 1), (-1, -1), colors.white),
|
('BACKGROUND', (0, 1), (-1, -1), colors.white),
|
||||||
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
|
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
|
||||||
('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
|
('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
|
||||||
('PADDING', (0, 0), (-1, -1), 12),
|
('PADDING', (0, 0), (-1, -1), 16),
|
||||||
('BOX', (0, 0), (-1, -1), 2, score_color),
|
('BOX', (0, 0), (-1, -1), 2, score_color),
|
||||||
('LINEABOVE', (0, 1), (-1, 1), 1.5, score_color),
|
('LINEABOVE', (0, 1), (-1, 1), 1.5, score_color),
|
||||||
('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor('#e2e8f0')),
|
('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor('#e2e8f0')),
|
||||||
@@ -2733,13 +2733,20 @@ async def analyze_image(
|
|||||||
|
|
||||||
# Obtener contexto de la pregunta si se proporciona
|
# Obtener contexto de la pregunta si se proporciona
|
||||||
question_obj = None
|
question_obj = None
|
||||||
|
question_options = []
|
||||||
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"📋 Pregunta encontrada:")
|
||||||
print(f" - ID: {question_obj.id}")
|
print(f" - ID: {question_obj.id}")
|
||||||
print(f" - Texto: {question_obj.text}")
|
print(f" - Texto: {question_obj.text}")
|
||||||
|
print(f" - Tipo: {question_obj.options.get('type') if question_obj.options else 'N/A'}")
|
||||||
print(f" - ai_prompt en DB: {question_obj.ai_prompt[:100] if question_obj.ai_prompt else 'NO TIENE'}")
|
print(f" - ai_prompt en DB: {question_obj.ai_prompt[:100] if question_obj.ai_prompt else 'NO TIENE'}")
|
||||||
|
|
||||||
|
# Extraer opciones de respuesta si existen
|
||||||
|
if question_obj.options and 'options' in question_obj.options:
|
||||||
|
question_options = question_obj.options['options']
|
||||||
|
print(f" - Opciones disponibles: {question_options}")
|
||||||
|
|
||||||
# Si no se proporciona custom_prompt en el Form, usar el de la pregunta
|
# 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:
|
if not custom_prompt and question_obj and question_obj.ai_prompt:
|
||||||
custom_prompt = question_obj.ai_prompt
|
custom_prompt = question_obj.ai_prompt
|
||||||
@@ -2774,6 +2781,11 @@ INFORMACIÓN DEL VEHÍCULO INSPECCIONADO:
|
|||||||
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:
|
||||||
|
# Agregar información de opciones de respuesta al prompt
|
||||||
|
options_context = ""
|
||||||
|
if question_options:
|
||||||
|
options_context = f"\n\nOPCIONES DE RESPUESTA DISPONIBLES:\n{', '.join(question_options)}\n\nEn el campo 'expected_answer', indica cuál de estas opciones es la más apropiada según lo que observas en la imagen."
|
||||||
|
|
||||||
# Usar prompt personalizado si está disponible
|
# Usar prompt personalizado si está disponible
|
||||||
if custom_prompt:
|
if custom_prompt:
|
||||||
# Prompt personalizado - DIRECTO Y SIMPLE
|
# Prompt personalizado - DIRECTO Y SIMPLE
|
||||||
@@ -2782,13 +2794,14 @@ INFORMACIÓN DEL VEHÍCULO INSPECCIONADO:
|
|||||||
{vehicle_context}
|
{vehicle_context}
|
||||||
|
|
||||||
TAREA ESPECÍFICA:
|
TAREA ESPECÍFICA:
|
||||||
{custom_prompt}
|
{custom_prompt}{options_context}
|
||||||
|
|
||||||
Responde SOLO en formato JSON válido (sin markdown, sin ```json):
|
Responde SOLO en formato JSON válido (sin markdown, sin ```json):
|
||||||
{{
|
{{
|
||||||
"status": "ok",
|
"status": "ok",
|
||||||
"observations": "Describe lo que observas en la imagen en relación a la tarea solicitada",
|
"observations": "Describe lo que observas en la imagen en relación a la tarea solicitada",
|
||||||
"recommendation": "Acción sugerida basada en lo observado",
|
"recommendation": "Acción sugerida basada en lo observado",
|
||||||
|
"expected_answer": "La respuesta que debería seleccionar el mecánico según lo observado (si hay opciones disponibles)",
|
||||||
"confidence": 0.85,
|
"confidence": 0.85,
|
||||||
"context_match": true
|
"context_match": true
|
||||||
}}
|
}}
|
||||||
@@ -2818,7 +2831,7 @@ IMPORTANTE:
|
|||||||
{vehicle_context}
|
{vehicle_context}
|
||||||
|
|
||||||
PREGUNTA ESPECÍFICA A RESPONDER: "{question_text}"
|
PREGUNTA ESPECÍFICA A RESPONDER: "{question_text}"
|
||||||
Sección: {section}
|
Sección: {section}{options_context}
|
||||||
|
|
||||||
Analiza la imagen ÚNICAMENTE para responder esta pregunta específica.
|
Analiza la imagen ÚNICAMENTE para responder esta pregunta específica.
|
||||||
Sé directo y enfócate solo en lo que la pregunta solicita.
|
Sé directo y enfócate solo en lo que la pregunta solicita.
|
||||||
@@ -2833,6 +2846,7 @@ Responde SOLO en formato JSON válido (sin markdown, sin ```json):
|
|||||||
"status": "ok",
|
"status": "ok",
|
||||||
"observations": "Respuesta técnica específica a: {question_text}",
|
"observations": "Respuesta técnica específica a: {question_text}",
|
||||||
"recommendation": "Acción técnica recomendada o mensaje si la foto no es apropiada",
|
"recommendation": "Acción técnica recomendada o mensaje si la foto no es apropiada",
|
||||||
|
"expected_answer": "La respuesta correcta que debería seleccionar según lo observado",
|
||||||
"confidence": 0.85,
|
"confidence": 0.85,
|
||||||
"context_match": true
|
"context_match": true
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -5273,6 +5273,29 @@ function InspectionModal({ checklist, existingInspection, user, onClose, onCompl
|
|||||||
alert('⚠️ Debes presionar "Cargar Documentos" antes de continuar')
|
alert('⚠️ Debes presionar "Cargar Documentos" antes de continuar')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NUEVA VALIDACIÓN: Verificar coherencia entre respuesta del mecánico y análisis de IA
|
||||||
|
const answer = answers[currentQuestion.id]
|
||||||
|
if (answer?.aiAnalysis && answer.aiAnalysis.length > 0 && answer.value) {
|
||||||
|
const aiData = answer.aiAnalysis[0]?.analysis
|
||||||
|
if (aiData?.expected_answer && aiData.expected_answer.toLowerCase() !== answer.value.toLowerCase()) {
|
||||||
|
const confirmChange = window.confirm(
|
||||||
|
`⚠️ ADVERTENCIA DE COHERENCIA\n\n` +
|
||||||
|
`Tu respuesta: "${answer.value}"\n` +
|
||||||
|
`Análisis de IA sugiere: "${aiData.expected_answer}"\n\n` +
|
||||||
|
`La imagen muestra:\n${aiData.observations || 'Sin detalles'}\n\n` +
|
||||||
|
`¿Deseas cambiar tu respuesta antes de continuar?\n\n` +
|
||||||
|
`• Presiona CANCELAR para revisar y cambiar tu respuesta\n` +
|
||||||
|
`• Presiona ACEPTAR para continuar con tu respuesta actual`
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!confirmChange) {
|
||||||
|
// El mecánico quiere cambiar su respuesta
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
saveAnswer(currentQuestion.id)
|
saveAnswer(currentQuestion.id)
|
||||||
goToQuestion(currentQuestionIndex + 1)
|
goToQuestion(currentQuestionIndex + 1)
|
||||||
}}
|
}}
|
||||||
@@ -5296,6 +5319,29 @@ function InspectionModal({ checklist, existingInspection, user, onClose, onCompl
|
|||||||
alert('⚠️ Debes presionar "Cargar Documentos" antes de continuar')
|
alert('⚠️ Debes presionar "Cargar Documentos" antes de continuar')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NUEVA VALIDACIÓN: Verificar coherencia entre respuesta del mecánico y análisis de IA
|
||||||
|
const answer = answers[currentQuestion.id]
|
||||||
|
if (answer?.aiAnalysis && answer.aiAnalysis.length > 0 && answer.value) {
|
||||||
|
const aiData = answer.aiAnalysis[0]?.analysis
|
||||||
|
if (aiData?.expected_answer && aiData.expected_answer.toLowerCase() !== answer.value.toLowerCase()) {
|
||||||
|
const confirmChange = window.confirm(
|
||||||
|
`⚠️ ADVERTENCIA DE COHERENCIA\n\n` +
|
||||||
|
`Tu respuesta: "${answer.value}"\n` +
|
||||||
|
`Análisis de IA sugiere: "${aiData.expected_answer}"\n\n` +
|
||||||
|
`La imagen muestra:\n${aiData.observations || 'Sin detalles'}\n\n` +
|
||||||
|
`¿Deseas cambiar tu respuesta antes de completar?\n\n` +
|
||||||
|
`• Presiona CANCELAR para revisar y cambiar tu respuesta\n` +
|
||||||
|
`• Presiona ACEPTAR para continuar con tu respuesta actual`
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!confirmChange) {
|
||||||
|
// El mecánico quiere cambiar su respuesta
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
saveAnswer(currentQuestion.id)
|
saveAnswer(currentQuestion.id)
|
||||||
proceedToSignatures()
|
proceedToSignatures()
|
||||||
}}
|
}}
|
||||||
|
|||||||
Reference in New Issue
Block a user