Files
checklist/backend/app/schemas.py
ronalds c374909fa8 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
2025-12-02 11:22:21 -03:00

366 lines
9.2 KiB
Python

from pydantic import BaseModel, EmailStr, Field
from typing import Optional, List
from datetime import datetime
# User Schemas
class UserBase(BaseModel):
username: str
email: Optional[EmailStr] = None
full_name: Optional[str] = None
employee_code: Optional[str] = None # Nro Operario - código de otro sistema
role: str = "mechanic"
class UserCreate(UserBase):
password: str
class UserUpdate(BaseModel):
username: Optional[str] = None
email: Optional[EmailStr] = None
full_name: Optional[str] = None
employee_code: Optional[str] = None
role: Optional[str] = None
class UserPasswordUpdate(BaseModel):
current_password: str
new_password: str
class AdminPasswordUpdate(BaseModel):
new_password: str
class UserLogin(BaseModel):
username: str
password: str
class User(UserBase):
id: int
employee_code: Optional[str] = None
is_active: bool
created_at: datetime
class Config:
from_attributes = True
class Token(BaseModel):
access_token: str
token_type: str
user: User
# API Token Schemas
class APITokenCreate(BaseModel):
description: Optional[str] = None
class APIToken(BaseModel):
id: int
description: Optional[str] = None
is_active: bool
last_used_at: Optional[datetime] = None
created_at: datetime
class Config:
from_attributes = True
class APITokenWithValue(APIToken):
token: str
# Checklist Schemas
class ChecklistBase(BaseModel):
name: str
description: Optional[str] = None
ai_mode: str = "off"
scoring_enabled: bool = True
logo_url: Optional[str] = None
class ChecklistCreate(ChecklistBase):
mechanic_ids: Optional[List[int]] = [] # IDs de mecánicos autorizados
class ChecklistUpdate(BaseModel):
name: Optional[str] = None
description: Optional[str] = None
ai_mode: Optional[str] = None
scoring_enabled: Optional[bool] = None
logo_url: Optional[str] = None
is_active: Optional[bool] = None
mechanic_ids: Optional[List[int]] = None # IDs de mecánicos autorizados
class Checklist(ChecklistBase):
id: int
max_score: int
is_active: bool
created_by: int
created_at: datetime
allowed_mechanics: Optional[List[int]] = [] # IDs de mecánicos permitidos
class Config:
from_attributes = True
# Question Schemas
# Tipos de preguntas soportados:
# - boolean: Dos opciones personalizables (ej: Sí/No, Pasa/Falla)
# - single_choice: Selección única con N opciones
# - multiple_choice: Selección múltiple
# - scale: Escala numérica (1-5, 1-10, etc.)
# - text: Texto libre
# - number: Valor numérico
# - date: Fecha
# - time: Hora
class QuestionBase(BaseModel):
section: Optional[str] = None
text: str
type: str # boolean, single_choice, multiple_choice, scale, text, number, date, time
points: int = 1
options: Optional[dict] = None # Configuración flexible según tipo
# Estructura de options:
# Boolean: {"type": "boolean", "choices": [{"value": "yes", "label": "Sí", "points": 1, "status": "ok"}, ...]}
# Single/Multiple Choice: {"type": "single_choice", "choices": [{"value": "opt1", "label": "Opción 1", "points": 2}, ...]}
# Scale: {"type": "scale", "min": 1, "max": 5, "step": 1, "labels": {"min": "Muy malo", "max": "Excelente"}}
# Text: {"type": "text", "multiline": true, "max_length": 500}
order: int = 0
allow_photos: bool = True
max_photos: int = 3
requires_comment_on_fail: bool = False
send_notification: bool = False
parent_question_id: Optional[int] = None
show_if_answer: Optional[str] = None
ai_prompt: Optional[str] = None
is_deleted: bool = False
class QuestionCreate(QuestionBase):
checklist_id: int
class QuestionUpdate(QuestionBase):
pass
class QuestionReorder(BaseModel):
question_id: int
new_order: int
class Question(QuestionBase):
id: int
checklist_id: int
created_at: datetime
updated_at: Optional[datetime] = None
class Config:
from_attributes = True
# Question Audit Schemas
class QuestionAuditLog(BaseModel):
id: int
question_id: int
checklist_id: int
user_id: int
action: str
field_name: Optional[str] = None
old_value: Optional[str] = None
new_value: Optional[str] = None
comment: Optional[str] = None
created_at: datetime
user: Optional['User'] = None
class Config:
from_attributes = True
# Inspection Schemas
class InspectionBase(BaseModel):
or_number: Optional[str] = None
work_order_number: Optional[str] = None
vehicle_plate: str
vehicle_brand: Optional[str] = None
vehicle_model: Optional[str] = None
vehicle_km: Optional[int] = None
order_number: Optional[str] = None # Nº de Pedido
mechanic_employee_code: Optional[str] = None
class InspectionCreate(InspectionBase):
checklist_id: int
class InspectionUpdate(BaseModel):
vehicle_brand: Optional[str] = None
vehicle_model: Optional[str] = None
vehicle_km: Optional[int] = None
signature_data: Optional[str] = None
status: Optional[str] = None
class Inspection(InspectionBase):
id: int
checklist_id: int
mechanic_id: int
mechanic_employee_code: Optional[str] = None
score: int
max_score: int
percentage: float
flagged_items_count: int
status: str
started_at: datetime
completed_at: Optional[datetime] = None
class Config:
from_attributes = True
# Answer Schemas
class AnswerBase(BaseModel):
answer_value: Optional[str] = None # Opcional para permitir guardar solo análisis IA
status: str = "ok"
comment: Optional[str] = None
is_flagged: bool = False
class AnswerCreate(AnswerBase):
inspection_id: int
question_id: int
ai_analysis: Optional[list] = None # Lista de análisis de IA (soporta múltiples imágenes)
chat_history: Optional[list] = None # Historial de chat con AI Assistant
class AnswerUpdate(AnswerBase):
pass
class Answer(AnswerBase):
id: int
inspection_id: int
question_id: int
points_earned: int
ai_analysis: Optional[list] = None # Lista de análisis de IA
chat_history: Optional[list] = None # Historial de chat con AI Assistant
created_at: datetime
class Config:
from_attributes = True
# MediaFile Schemas
class MediaFileBase(BaseModel):
caption: Optional[str] = None
order: int = 0
class MediaFileCreate(MediaFileBase):
file_type: str = "image"
class MediaFile(MediaFileBase):
id: int
answer_id: int
file_path: str
file_type: str
uploaded_at: datetime
class Config:
from_attributes = True
# Response Schemas
class ChecklistWithQuestions(Checklist):
questions: List[Question] = []
class AnswerWithMedia(Answer):
media_files: List[MediaFile] = []
question: Question
class InspectionDetail(Inspection):
checklist: ChecklistWithQuestions
mechanic: User
answers: List[AnswerWithMedia] = []
# AI Configuration Schemas
class AIConfigurationBase(BaseModel):
provider: str # openai, gemini
api_key: str
model_name: Optional[str] = None
class AIConfigurationCreate(AIConfigurationBase):
pass
class AIConfigurationUpdate(BaseModel):
provider: Optional[str] = None
api_key: Optional[str] = None
model_name: Optional[str] = None
is_active: Optional[bool] = None
class AIConfiguration(AIConfigurationBase):
id: int
is_active: bool
created_at: datetime
class Config:
from_attributes = True
class AIModelInfo(BaseModel):
id: str
name: str
provider: str
description: Optional[str] = None
# Reports Schemas
class InspectionStats(BaseModel):
total_inspections: int
completed_inspections: int
pending_inspections: int
completion_rate: float
avg_score: float
total_flagged_items: int
class MechanicRanking(BaseModel):
mechanic_id: int
mechanic_name: str
total_inspections: int
avg_score: float
completion_rate: float
class ChecklistStats(BaseModel):
checklist_id: int
checklist_name: str
total_inspections: int
avg_score: float
class DashboardData(BaseModel):
stats: InspectionStats
mechanic_ranking: List[MechanicRanking]
checklist_stats: List[ChecklistStats]
inspections_by_date: dict
pass_fail_ratio: dict
class InspectionListItem(BaseModel):
id: int
vehicle_plate: str
checklist_name: str
mechanic_name: str
status: str
score: Optional[int]
max_score: Optional[int]
flagged_items: int
started_at: Optional[datetime]
completed_at: Optional[datetime]
# Audit Log Schemas
class AuditLogBase(BaseModel):
action: str
entity_type: str
field_name: Optional[str] = None
old_value: Optional[str] = None
new_value: Optional[str] = None
comment: Optional[str] = None
class AuditLog(AuditLogBase):
id: int
inspection_id: int
answer_id: Optional[int] = None
user_id: int
user_name: Optional[str] = None
created_at: datetime
class Config:
from_attributes = True
class AnswerEdit(BaseModel):
answer_value: Optional[str] = None
status: Optional[str] = None
comment: Optional[str] = None
is_flagged: Optional[bool] = None
edit_comment: Optional[str] = None # Comentario del admin sobre por qué editó