IMPLEMENTACIÓN COMPLETADA - Informes Personalizados para Chat Assistant

📊 Backend actualizado a v1.1.0
He implementado un sistema inteligente de generación de informes para preguntas con chat assistant:

🔧 Cambios Implementados:
1. Nueva función generate_chat_summary() (líneas ~1450)
Funcionalidad:

Recibe el chat_history completo de una conversación
Usa OpenAI o Gemini (según configuración activa) para analizar la conversación
Genera un resumen estructurado en JSON con:
problema_identificado: Descripción del problema principal
hallazgos: Lista de observaciones técnicas
diagnostico: Conclusión del diagnóstico
recomendaciones: Pasos sugeridos
Características:

Temperature: 0.3 (respuestas consistentes)
Max tokens: 800
Response format: JSON
Manejo robusto de errores con fallback
This commit is contained in:
2025-12-04 10:56:00 -03:00
parent 3bf8b44581
commit 59a0f56b99
2 changed files with 232 additions and 41 deletions

View File

@@ -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) app = FastAPI(title="Checklist Inteligente API", version=BACKEND_VERSION)
# S3/MinIO configuration # S3/MinIO configuration
@@ -1450,6 +1450,118 @@ def update_inspection(
return db_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: def generate_inspection_pdf(inspection_id: int, db: Session) -> str:
""" """
Genera el PDF de una inspección y lo sube a S3. 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(Paragraph(f"{question.section or 'General'}", section_header_style))
elements.append(Spacer(1, 3*mm)) 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 # Estado visual
status_colors = { status_colors = {
'ok': colors.HexColor('#22c55e'), 'ok': colors.HexColor('#22c55e'),
@@ -1918,13 +2033,83 @@ def generate_inspection_pdf(inspection_id: int, db: Session) -> str:
Paragraph(f"<b>{status_icon} {question.text}</b>", question_style), Paragraph(f"<b>{status_icon} {question.text}</b>", question_style),
]) ])
# ===== 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"<b>💬 INFORME DE DIAGNÓSTICO ASISTIDO</b>",
ParagraphStyle('chat_title', parent=answer_style, fontSize=11,
textColor=colors.HexColor('#2563eb'), fontName='Helvetica-Bold'))
])
# Problema identificado
question_data.append([
Paragraph(f"<b>🔍 Problema Identificado:</b><br/>{chat_summary.get('problema_identificado', 'N/A')}",
comment_style)
])
# Hallazgos
if chat_summary.get('hallazgos') and len(chat_summary['hallazgos']) > 0:
hallazgos_text = "<br/>".join([f"{h}" for h in chat_summary['hallazgos']])
question_data.append([
Paragraph(f"<b>📋 Hallazgos:</b><br/>{hallazgos_text}", comment_style)
])
# Diagnóstico
question_data.append([
Paragraph(f"<b>🔧 Diagnóstico:</b><br/>{chat_summary.get('diagnostico', 'N/A')}",
comment_style)
])
# Recomendaciones
if chat_summary.get('recomendaciones') and len(chat_summary['recomendaciones']) > 0:
recomendaciones_text = "<br/>".join([f"{r}" for r in chat_summary['recomendaciones']])
question_data.append([
Paragraph(f"<b>✅ Recomendaciones:</b><br/>{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"<b>Respuesta:</b> Diagnóstico asistido completado", answer_style),
Paragraph(f"<b>Estado:</b> {ans.status.upper()}",
ParagraphStyle('status', parent=answer_style,
textColor=status_color, fontName='Helvetica-Bold'))
]
], colWidths=[120*mm, 50*mm])
])
question_data.append([
Paragraph(f"<b> Nota:</b> Ver historial de conversación completo en el sistema",
comment_style)
])
# ===== LÓGICA NORMAL PARA OTROS TIPOS =====
else:
# Fila 2: Respuesta y estado - Convertir valor técnico a etiqueta legible # Fila 2: Respuesta y estado - Convertir valor técnico a etiqueta legible
answer_text = get_readable_answer(ans.answer_value, question.options) answer_text = get_readable_answer(ans.answer_value, question.options)
question_data.append([ question_data.append([
Table([ Table([
[ [
Paragraph(f"<b>Respuesta:</b> {answer_text}", answer_style), Paragraph(f"<b>Respuesta:</b> {answer_text}", answer_style),
Paragraph(f"<b>Estado:</b> {ans.status.upper()}", ParagraphStyle('status', parent=answer_style, textColor=status_color, fontName='Helvetica-Bold')) Paragraph(f"<b>Estado:</b> {ans.status.upper()}",
ParagraphStyle('status', parent=answer_style,
textColor=status_color, fontName='Helvetica-Bold'))
] ]
], colWidths=[120*mm, 50*mm]) ], colWidths=[120*mm, 50*mm])
]) ])
@@ -1949,7 +2134,7 @@ def generate_inspection_pdf(inspection_id: int, db: Session) -> str:
Paragraph(f"<b>Comentario:</b> {comment_text}", comment_style) Paragraph(f"<b>Comentario:</b> {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: if ans.media_files:
media_imgs = [] media_imgs = []
for media in ans.media_files: for media in ans.media_files:
@@ -3236,7 +3421,17 @@ INFORMACIÓN DEL VEHÍCULO:
# Construir el system prompt # Construir el system prompt
base_prompt = assistant_prompt or "Eres un experto mecánico automotriz que ayuda a diagnosticar problemas." 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} {vehicle_context}
@@ -3247,12 +3442,13 @@ INFORMACIÓN DEL VEHÍCULO:
INSTRUCCIONES ADICIONALES: INSTRUCCIONES ADICIONALES:
{assistant_instructions if assistant_instructions else "Sé técnico, claro y directo en tus respuestas."} {assistant_instructions if assistant_instructions else "Sé técnico, claro y directo en tus respuestas."}
FORMATO DE RESPUESTA: FORMATO DE RESPUESTA OBLIGATORIO:
- Sé {response_length} en tus respuestas 1. [IDENTIFICACIÓN] Qué tipo de documento/imagen es esto (describe lo que VES, no lo que asumes)
- Usa lenguaje técnico pero comprensible 2. [VERIFICACIÓN] ¿Coincide con lo que el usuario pregunta? Si NO, indícalo
- Si ves algo preocupante en las fotos analizadas, menciónalo 3. [ANÁLISIS] Basado ÚNICAMENTE en información visible
- Proporciona recomendaciones específicas cuando sea relevante 4. [RECOMENDACIÓN] Pasos siguientes o información que necesitas
- Si no tienes suficiente información, pide más detalles
Longitud: {response_length}
""" """
# Construir el historial de mensajes para la IA # Construir el historial de mensajes para la IA

View File

@@ -5521,11 +5521,6 @@ function AIAssistantChatModal({ question, inspection, allAnswers, messages, setM
formData.append('files', file) formData.append('files', file)
}) })
// Adjuntar archivos
currentFiles.forEach((file, index) => {
formData.append('files', file)
})
// Recopilar fotos de preguntas anteriores según configuración // Recopilar fotos de preguntas anteriores según configuración
const contextPhotos = [] const contextPhotos = []
const contextQuestionIds = config.context_questions const contextQuestionIds = config.context_questions