diff --git a/backend/app/main.py b/backend/app/main.py
index f9440f6..06615ff 100644
--- a/backend/app/main.py
+++ b/backend/app/main.py
@@ -276,7 +276,7 @@ def extract_pdf_text_smart(pdf_content: bytes, max_chars: int = None) -> dict:
}
-BACKEND_VERSION = "1.0.98"
+BACKEND_VERSION = "1.1.0"
app = FastAPI(title="Checklist Inteligente API", version=BACKEND_VERSION)
# S3/MinIO configuration
@@ -1450,6 +1450,118 @@ def update_inspection(
return db_inspection
+async def generate_chat_summary(chat_history: list, question_text: str) -> dict:
+ """
+ Genera un resumen estructurado de una conversación de chat con el asistente IA.
+ Retorna un dict con: problema_identificado, hallazgos, diagnostico, recomendaciones
+ """
+ import asyncio
+ import json
+ import openai
+ import google.generativeai as genai
+
+ if not chat_history or len(chat_history) == 0:
+ return {
+ "problema_identificado": "Sin conversación registrada",
+ "hallazgos": [],
+ "diagnostico": "N/A",
+ "recomendaciones": []
+ }
+
+ # Obtener configuración de IA
+ db = next(get_db())
+ config = db.query(models.AIConfiguration).filter(models.AIConfiguration.is_active == True).first()
+
+ if not config:
+ # Fallback: devolver resumen simple
+ return {
+ "problema_identificado": f"Consulta sobre: {question_text}",
+ "hallazgos": ["Conversación completada con el asistente"],
+ "diagnostico": "Ver conversación completa en el sistema",
+ "recomendaciones": ["Revisar historial de chat para detalles"]
+ }
+
+ # Construir contexto de la conversación
+ conversation_text = ""
+ for msg in chat_history:
+ role = "Mecánico" if msg.get("role") == "user" else "Asistente"
+ content = msg.get("content", "")
+ conversation_text += f"{role}: {content}\n\n"
+
+ # Prompt para generar resumen estructurado
+ summary_prompt = f"""Analiza la siguiente conversación entre un mecánico y un asistente de diagnóstico automotriz, y genera un resumen ejecutivo estructurado para incluir en un informe PDF.
+
+CONVERSACIÓN:
+{conversation_text}
+
+INSTRUCCIONES:
+Genera un resumen profesional en formato JSON con esta estructura exacta:
+{{
+ "problema_identificado": "Descripción breve del problema o consulta principal (máximo 2 líneas)",
+ "hallazgos": ["Hallazgo 1", "Hallazgo 2", "Hallazgo 3"],
+ "diagnostico": "Conclusión técnica del diagnóstico (máximo 3 líneas)",
+ "recomendaciones": ["Recomendación 1", "Recomendación 2"]
+}}
+
+REGLAS:
+- Usa lenguaje técnico pero claro
+- Sé conciso y directo
+- Si no hay información suficiente para algún campo, usa "N/A" o lista vacía []
+- NO incluyas información que no esté en la conversación
+- El JSON debe ser válido y parseable
+"""
+
+ try:
+ # Usar OpenAI o Gemini según configuración
+ if config.provider == "openai" and config.openai_api_key:
+ client = openai.OpenAI(api_key=config.openai_api_key)
+ response = await asyncio.to_thread(
+ client.chat.completions.create,
+ model=config.openai_model or "gpt-4o",
+ messages=[{"role": "user", "content": summary_prompt}],
+ temperature=0.3,
+ max_tokens=800,
+ response_format={"type": "json_object"}
+ )
+ summary_json = response.choices[0].message.content
+
+ elif config.provider == "gemini" and config.gemini_api_key:
+ genai.configure(api_key=config.gemini_api_key)
+ model = genai.GenerativeModel(
+ model_name=config.gemini_model or "gemini-2.0-flash-exp",
+ generation_config={
+ "temperature": 0.3,
+ "max_output_tokens": 800,
+ "response_mime_type": "application/json"
+ }
+ )
+ response = await asyncio.to_thread(model.generate_content, summary_prompt)
+ summary_json = response.text
+ else:
+ raise Exception("No hay proveedor de IA configurado")
+
+ # Parsear JSON
+ summary = json.loads(summary_json)
+
+ # Validar estructura
+ required_keys = ["problema_identificado", "hallazgos", "diagnostico", "recomendaciones"]
+ for key in required_keys:
+ if key not in summary:
+ summary[key] = "N/A" if key in ["problema_identificado", "diagnostico"] else []
+
+ return summary
+
+ except Exception as e:
+ print(f"❌ Error generando resumen de chat: {e}")
+ # Fallback
+ return {
+ "problema_identificado": f"Consulta sobre: {question_text}",
+ "hallazgos": ["Error al generar resumen automático"],
+ "diagnostico": "Ver conversación completa en el sistema",
+ "recomendaciones": ["Revisar historial de chat para detalles completos"]
+ }
+
+
def generate_inspection_pdf(inspection_id: int, db: Session) -> str:
"""
Genera el PDF de una inspección y lo sube a S3.
@@ -1896,6 +2008,9 @@ def generate_inspection_pdf(inspection_id: int, db: Session) -> str:
elements.append(Paragraph(f"▶ {question.section or 'General'}", section_header_style))
elements.append(Spacer(1, 3*mm))
+ # Detectar si es pregunta con chat assistant
+ is_ai_assistant = question.options and question.options.get('type') == 'ai_assistant'
+
# Estado visual
status_colors = {
'ok': colors.HexColor('#22c55e'),
@@ -1918,38 +2033,108 @@ def generate_inspection_pdf(inspection_id: int, db: Session) -> str:
Paragraph(f"{status_icon} {question.text}", question_style),
])
- # Fila 2: Respuesta y estado - Convertir valor técnico a etiqueta legible
- answer_text = get_readable_answer(ans.answer_value, question.options)
- question_data.append([
- Table([
- [
- Paragraph(f"Respuesta: {answer_text}", answer_style),
- Paragraph(f"Estado: {ans.status.upper()}", ParagraphStyle('status', parent=answer_style, textColor=status_color, fontName='Helvetica-Bold'))
- ]
- ], colWidths=[120*mm, 50*mm])
- ])
+ # ===== LÓGICA ESPECIAL PARA AI_ASSISTANT =====
+ if is_ai_assistant and ans.chat_history:
+ # Generar resumen estructurado del chat
+ import asyncio
+ try:
+ # Ejecutar función async de forma sincrónica
+ loop = asyncio.new_event_loop()
+ asyncio.set_event_loop(loop)
+ chat_summary = loop.run_until_complete(
+ generate_chat_summary(ans.chat_history, question.text)
+ )
+ loop.close()
+
+ # Renderizar informe narrativo
+ question_data.append([
+ Paragraph(f"💬 INFORME DE DIAGNÓSTICO ASISTIDO",
+ ParagraphStyle('chat_title', parent=answer_style, fontSize=11,
+ textColor=colors.HexColor('#2563eb'), fontName='Helvetica-Bold'))
+ ])
+
+ # Problema identificado
+ question_data.append([
+ Paragraph(f"🔍 Problema Identificado:
{chat_summary.get('problema_identificado', 'N/A')}",
+ comment_style)
+ ])
+
+ # Hallazgos
+ if chat_summary.get('hallazgos') and len(chat_summary['hallazgos']) > 0:
+ hallazgos_text = "
".join([f"• {h}" for h in chat_summary['hallazgos']])
+ question_data.append([
+ Paragraph(f"📋 Hallazgos:
{hallazgos_text}", comment_style)
+ ])
+
+ # Diagnóstico
+ question_data.append([
+ Paragraph(f"🔧 Diagnóstico:
{chat_summary.get('diagnostico', 'N/A')}",
+ comment_style)
+ ])
+
+ # Recomendaciones
+ if chat_summary.get('recomendaciones') and len(chat_summary['recomendaciones']) > 0:
+ recomendaciones_text = "
".join([f"• {r}" for r in chat_summary['recomendaciones']])
+ question_data.append([
+ Paragraph(f"✅ Recomendaciones:
{recomendaciones_text}",
+ ParagraphStyle('recommendations', parent=comment_style,
+ textColor=colors.HexColor('#16a34a')))
+ ])
+
+ except Exception as e:
+ print(f"❌ Error generando resumen de chat en PDF: {e}")
+ # Fallback: mostrar que hubo conversación
+ question_data.append([
+ Table([
+ [
+ Paragraph(f"Respuesta: Diagnóstico asistido completado", answer_style),
+ Paragraph(f"Estado: {ans.status.upper()}",
+ ParagraphStyle('status', parent=answer_style,
+ textColor=status_color, fontName='Helvetica-Bold'))
+ ]
+ ], colWidths=[120*mm, 50*mm])
+ ])
+ question_data.append([
+ Paragraph(f"ℹ️ Nota: Ver historial de conversación completo en el sistema",
+ comment_style)
+ ])
- # Fila 3: Comentario mejorado (si existe)
- if ans.comment:
- comment_text = ans.comment
-
- # Limpiar prefijo de análisis automático/IA si existe (con cualquier porcentaje)
- import re
- # Patrón para detectar "Análisis Automático (XX% confianza): " o "Análisis IA (XX% confianza): "
- comment_text = re.sub(r'^(Análisis Automático|Análisis IA)\s*\(\d+%\s*confianza\):\s*', '', comment_text)
- # También remover variantes sin emoji
- comment_text = re.sub(r'^🤖\s*(Análisis Automático|Análisis IA)\s*\(\d+%\s*confianza\):\s*', '', comment_text)
-
- # Separar análisis y recomendaciones con salto de línea
- if "Recomendaciones:" in comment_text or "Recomendación:" in comment_text:
- comment_text = comment_text.replace("Recomendaciones:", "
Recomendaciones:")
- comment_text = comment_text.replace("Recomendación:", "
Recomendación:")
-
+ # ===== LÓGICA NORMAL PARA OTROS TIPOS =====
+ else:
+ # Fila 2: Respuesta y estado - Convertir valor técnico a etiqueta legible
+ answer_text = get_readable_answer(ans.answer_value, question.options)
question_data.append([
- Paragraph(f"Comentario: {comment_text}", comment_style)
+ Table([
+ [
+ Paragraph(f"Respuesta: {answer_text}", answer_style),
+ Paragraph(f"Estado: {ans.status.upper()}",
+ ParagraphStyle('status', parent=answer_style,
+ textColor=status_color, fontName='Helvetica-Bold'))
+ ]
+ ], colWidths=[120*mm, 50*mm])
])
+
+ # Fila 3: Comentario mejorado (si existe)
+ if ans.comment:
+ comment_text = ans.comment
+
+ # Limpiar prefijo de análisis automático/IA si existe (con cualquier porcentaje)
+ import re
+ # Patrón para detectar "Análisis Automático (XX% confianza): " o "Análisis IA (XX% confianza): "
+ comment_text = re.sub(r'^(Análisis Automático|Análisis IA)\s*\(\d+%\s*confianza\):\s*', '', comment_text)
+ # También remover variantes sin emoji
+ comment_text = re.sub(r'^🤖\s*(Análisis Automático|Análisis IA)\s*\(\d+%\s*confianza\):\s*', '', comment_text)
+
+ # Separar análisis y recomendaciones con salto de línea
+ if "Recomendaciones:" in comment_text or "Recomendación:" in comment_text:
+ comment_text = comment_text.replace("Recomendaciones:", "
Recomendaciones:")
+ comment_text = comment_text.replace("Recomendación:", "
Recomendación:")
+
+ question_data.append([
+ Paragraph(f"Comentario: {comment_text}", comment_style)
+ ])
- # Fila 4: Imágenes (si existen)
+ # Fila 4: Imágenes (si existen) - COMÚN PARA TODOS LOS TIPOS
if ans.media_files:
media_imgs = []
for media in ans.media_files:
@@ -3236,7 +3421,17 @@ INFORMACIÓN DEL VEHÍCULO:
# Construir el system prompt
base_prompt = assistant_prompt or "Eres un experto mecánico automotriz que ayuda a diagnosticar problemas."
- system_prompt = f"""{base_prompt}
+ system_prompt = f"""INSTRUCCIONES CRÍTICAS ANTI-ALUCINACIÓN (MÁXIMA PRIORIDAD):
+Estas reglas SIEMPRE tienen prioridad sobre cualquier otra instrucción:
+
+1. PRIMERO mira la imagen/documento y DESCRIBE LITERALMENTE lo que ves
+2. VERIFICA si hay texto/logos/marcas visibles (ej: "TEXA", "Bosch", "ESI[tronic]")
+3. Si la imagen muestra un analizador de gases (con mediciones CO, CO₂, HC, NOₓ, O₂, Lambda), NO ES un informe de códigos DTC
+4. Si la imagen muestra una pantalla con códigos tipo "P0XXX" o "1XXXX", SÍ ES un informe de diagnóstico DTC
+5. NUNCA inventes información que no esté visible en la imagen
+6. Si lo que ves NO coincide con lo que el usuario pregunta, DÍSELO INMEDIATAMENTE
+
+{base_prompt}
{vehicle_context}
@@ -3247,12 +3442,13 @@ INFORMACIÓN DEL VEHÍCULO:
INSTRUCCIONES ADICIONALES:
{assistant_instructions if assistant_instructions else "Sé técnico, claro y directo en tus respuestas."}
-FORMATO DE RESPUESTA:
-- Sé {response_length} en tus respuestas
-- Usa lenguaje técnico pero comprensible
-- Si ves algo preocupante en las fotos analizadas, menciónalo
-- Proporciona recomendaciones específicas cuando sea relevante
-- Si no tienes suficiente información, pide más detalles
+FORMATO DE RESPUESTA OBLIGATORIO:
+1. [IDENTIFICACIÓN] Qué tipo de documento/imagen es esto (describe lo que VES, no lo que asumes)
+2. [VERIFICACIÓN] ¿Coincide con lo que el usuario pregunta? Si NO, indícalo
+3. [ANÁLISIS] Basado ÚNICAMENTE en información visible
+4. [RECOMENDACIÓN] Pasos siguientes o información que necesitas
+
+Longitud: {response_length}
"""
# Construir el historial de mensajes para la IA
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index 00af35d..a06b595 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -5521,11 +5521,6 @@ function AIAssistantChatModal({ question, inspection, allAnswers, messages, setM
formData.append('files', file)
})
- // Adjuntar archivos
- currentFiles.forEach((file, index) => {
- formData.append('files', file)
- })
-
// Recopilar fotos de preguntas anteriores según configuración
const contextPhotos = []
const contextQuestionIds = config.context_questions