✅ 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:
@@ -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"<b>{status_icon} {question.text}</b>", 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"<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'))
|
||||
]
|
||||
], 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"<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)
|
||||
])
|
||||
|
||||
# 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:", "<br/><br/><b>Recomendaciones:</b>")
|
||||
comment_text = comment_text.replace("Recomendación:", "<br/><br/><b>Recomendación:</b>")
|
||||
|
||||
# ===== 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"<b>Comentario:</b> {comment_text}", comment_style)
|
||||
Table([
|
||||
[
|
||||
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'))
|
||||
]
|
||||
], 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:", "<br/><br/><b>Recomendaciones:</b>")
|
||||
comment_text = comment_text.replace("Recomendación:", "<br/><br/><b>Recomendación:</b>")
|
||||
|
||||
question_data.append([
|
||||
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:
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user