✅ 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)
|
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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user