Chat AI Assistant con Archivos Adjuntos Implementado

🎯 Nueva Funcionalidad Completa
Se ha implementado un sistema de chat conversacional con IA que permite adjuntar archivos (imágenes y PDFs), similar a ChatGPT, con prompt personalizable y envío completo al webhook.

📋 Características Implementadas
1. Adjuntar Archivos en el Chat
 Botón 📎 para adjuntar archivos
 Soporte para imágenes (JPG, PNG, etc.) y PDFs
 Preview de archivos adjuntos antes de enviar
 Eliminación individual de archivos adjuntos
 Múltiples archivos por mensaje
 Validación de tipos de archivo
2. Procesamiento Backend de Archivos
 Endpoint modificado para recibir FormData con archivos
 PDFs: Extracción automática de texto con pypdf
 Imágenes: Conversión a base64 para Vision AI
 Análisis combinado de texto + imágenes
 Límite de 2000 caracteres por PDF para optimizar
3. Integración con IA
 OpenAI Vision: Soporte multimodal (texto + imágenes)
 Gemini: Soporte de imágenes y texto
 Contexto enriquecido con archivos adjuntos
 Prompts adaptados según tipo de archivo
4. Custom Prompt por Checklist
 Campo assistant_prompt configurable por pregunta
 Campo assistant_instructions para instrucciones adicionales
 Control de longitud de respuesta (short/medium/long)
 Contexto automático del vehículo en cada mensaje
5. Persistencia del Chat
 Nuevo campo chat_history en modelo Answer
 Migración SQL: add_chat_history_to_answers.sql
 Guardado automático del historial completo
 Restauración del chat al reabrir
6. Envío al Webhook (n8n)
 Todos los chats incluidos en send_completed_inspection_to_n8n()
 Campo chat_history en cada respuesta del webhook
 Incluye metadata de archivos adjuntos
 Tipo de pregunta identificado en webhook
 Datos completos para análisis posterior
This commit is contained in:
2025-12-02 11:22:21 -03:00
parent bf30b1a2bf
commit c374909fa8
8 changed files with 240 additions and 57 deletions

View File

@@ -131,7 +131,8 @@ def send_completed_inspection_to_n8n(inspection, db):
"id": answer.question.id,
"texto": answer.question.text,
"seccion": answer.question.section,
"orden": answer.question.order
"orden": answer.question.order,
"tipo": answer.question.type
},
"respuesta": answer.answer_value,
"estado": answer.status,
@@ -139,7 +140,8 @@ def send_completed_inspection_to_n8n(inspection, db):
"puntos_obtenidos": answer.points_earned,
"es_critico": answer.is_flagged,
"imagenes": imagenes,
"ai_analysis": answer.ai_analysis
"ai_analysis": answer.ai_analysis,
"chat_history": answer.chat_history # Incluir historial de chat si existe
})
# Preparar datos completos de la inspección
@@ -207,7 +209,7 @@ def send_completed_inspection_to_n8n(inspection, db):
# No lanzamos excepción para no interrumpir el flujo normal
BACKEND_VERSION = "1.0.90"
BACKEND_VERSION = "1.0.91"
app = FastAPI(title="Checklist Inteligente API", version=BACKEND_VERSION)
# S3/MinIO configuration
@@ -2959,33 +2961,80 @@ Responde en formato JSON:
@app.post("/api/ai/chat-assistant")
async def chat_with_ai_assistant(
request: dict,
question_id: int = Form(...),
inspection_id: int = Form(...),
user_message: str = Form(""),
chat_history: str = Form("[]"),
context_photos: str = Form("[]"),
vehicle_info: str = Form("{}"),
assistant_prompt: str = Form(""),
assistant_instructions: str = Form(""),
response_length: str = Form("medium"),
files: List[UploadFile] = File(default=[]),
db: Session = Depends(get_db),
current_user: models.User = Depends(get_current_user)
):
"""
Chat conversacional con IA usando contexto de fotos anteriores
El asistente tiene acceso a fotos de preguntas previas para dar mejor contexto
Ahora soporta archivos adjuntos (imágenes y PDFs)
"""
print("\n" + "="*80)
print("🤖 AI CHAT ASSISTANT")
print("="*80)
question_id = request.get('question_id')
inspection_id = request.get('inspection_id')
user_message = request.get('user_message')
chat_history = request.get('chat_history', [])
context_photos = request.get('context_photos', [])
assistant_prompt = request.get('assistant_prompt', '')
assistant_instructions = request.get('assistant_instructions', '')
response_length = request.get('response_length', 'medium')
vehicle_info = request.get('vehicle_info', {})
# Parsear JSON strings
import json
chat_history_list = json.loads(chat_history)
context_photos_list = json.loads(context_photos)
vehicle_info_dict = json.loads(vehicle_info)
print(f"📋 Question ID: {question_id}")
print(f"🚗 Inspection ID: {inspection_id}")
print(f"💬 User message: {user_message}")
print(f"📸 Context photos: {len(context_photos)} fotos")
print(f"💭 Chat history: {len(chat_history)} mensajes previos")
print(f"📎 Attached files: {len(files)}")
print(f"📸 Context photos: {len(context_photos_list)} fotos")
print(f"💭 Chat history: {len(chat_history_list)} mensajes previos")
# Procesar archivos adjuntos
attached_files_data = []
if files:
import base64
from pypdf import PdfReader
from io import BytesIO
for file in files:
file_content = await file.read()
file_type = file.content_type
file_info = {
'filename': file.filename,
'type': file_type,
'size': len(file_content)
}
# Si es PDF, extraer texto
if file_type == 'application/pdf' or file.filename.lower().endswith('.pdf'):
try:
pdf_file = BytesIO(file_content)
pdf_reader = PdfReader(pdf_file)
pdf_text = ""
for page in pdf_reader.pages:
pdf_text += page.extract_text()
file_info['content_type'] = 'pdf'
file_info['text'] = pdf_text[:2000] # Limitar texto
print(f"📄 PDF procesado: {file.filename} - {len(pdf_text)} caracteres")
except Exception as e:
print(f"❌ Error procesando PDF {file.filename}: {str(e)}")
file_info['error'] = str(e)
# Si es imagen, convertir a base64
elif file_type.startswith('image/'):
file_info['content_type'] = 'image'
file_info['base64'] = base64.b64encode(file_content).decode('utf-8')
print(f"🖼️ Imagen procesada: {file.filename}")
attached_files_data.append(file_info)
# Obtener configuración de IA
ai_config = db.query(models.AIConfiguration).filter(
@@ -3003,17 +3052,17 @@ async def chat_with_ai_assistant(
# Construir el contexto del vehículo
vehicle_context = f"""
INFORMACIÓN DEL VEHÍCULO:
- Marca: {vehicle_info.get('brand', 'N/A')}
- Modelo: {vehicle_info.get('model', 'N/A')}
- Placa: {vehicle_info.get('plate', 'N/A')}
- Kilometraje: {vehicle_info.get('km', 'N/A')} km
- Marca: {vehicle_info_dict.get('brand', 'N/A')}
- Modelo: {vehicle_info_dict.get('model', 'N/A')}
- Placa: {vehicle_info_dict.get('plate', 'N/A')}
- Kilometraje: {vehicle_info_dict.get('km', 'N/A')} km
"""
# Construir el contexto de las fotos anteriores
photos_context = ""
if context_photos:
photos_context = f"\n\nFOTOS ANALIZADAS PREVIAMENTE ({len(context_photos)} imágenes):\n"
for idx, photo in enumerate(context_photos[:10], 1): # Limitar a 10 fotos
if context_photos_list:
photos_context = f"\n\nFOTOS ANALIZADAS PREVIAMENTE ({len(context_photos_list)} imágenes):\n"
for idx, photo in enumerate(context_photos_list[:10], 1): # Limitar a 10 fotos
ai_analysis = photo.get('aiAnalysis', [])
if ai_analysis and len(ai_analysis) > 0:
analysis_text = ai_analysis[0].get('analysis', {})
@@ -3029,6 +3078,18 @@ INFORMACIÓN DEL VEHÍCULO:
}
max_tokens = max_tokens_map.get(response_length, 400)
# Construir contexto de archivos adjuntos
attached_context = ""
if attached_files_data:
attached_context = f"\n\nARCHIVOS ADJUNTOS EN ESTE MENSAJE ({len(attached_files_data)} archivos):\n"
for idx, file_info in enumerate(attached_files_data, 1):
if file_info.get('content_type') == 'pdf':
attached_context += f"\n{idx}. PDF: {file_info['filename']}\n"
if 'text' in file_info:
attached_context += f" Contenido: {file_info['text'][:500]}...\n"
elif file_info.get('content_type') == 'image':
attached_context += f"\n{idx}. Imagen: {file_info['filename']}\n"
# Construir el system prompt
base_prompt = assistant_prompt or "Eres un experto mecánico automotriz que ayuda a diagnosticar problemas."
@@ -3038,6 +3099,8 @@ INFORMACIÓN DEL VEHÍCULO:
{photos_context}
{attached_context}
INSTRUCCIONES ADICIONALES:
{assistant_instructions if assistant_instructions else "Sé técnico, claro y directo en tus respuestas."}
@@ -3053,17 +3116,39 @@ FORMATO DE RESPUESTA:
messages = [{"role": "system", "content": system_prompt}]
# Agregar historial previo (últimos 10 mensajes para no saturar)
for msg in chat_history[-10:]:
for msg in chat_history_list[-10:]:
messages.append({
"role": msg.get('role'),
"content": msg.get('content')
})
# Agregar el mensaje actual del usuario
messages.append({
"role": "user",
"content": user_message
})
# Agregar el mensaje actual del usuario con imágenes si hay
has_images = any(f.get('content_type') == 'image' for f in attached_files_data)
if has_images:
# Formato multimodal para OpenAI/Gemini
user_content = []
if user_message:
user_content.append({"type": "text", "text": user_message})
# Agregar imágenes
for file_info in attached_files_data:
if file_info.get('content_type') == 'image':
user_content.append({
"type": "image_url",
"image_url": {"url": f"data:image/jpeg;base64,{file_info['base64']}"}
})
messages.append({
"role": "user",
"content": user_content
})
else:
# Solo texto
messages.append({
"role": "user",
"content": user_message
})
print(f"🔧 Enviando a {ai_config.provider} con {len(messages)} mensajes")
@@ -3111,7 +3196,8 @@ FORMATO DE RESPUESTA:
"response": ai_response,
"confidence": confidence,
"provider": ai_config.provider,
"model": ai_config.model_name
"model": ai_config.model_name,
"attached_files": [{'filename': f['filename'], 'type': f['type']} for f in attached_files_data]
}
except Exception as e: