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