gerardocabrera's picture
Se modifica la función procesar_tickets para un mejor procesamiento
cdcc94d
import os
import re
import time
import json
import requests
import gradio as gr
import pandas as pd
import torch
import logging
from dotenv import load_dotenv
from transformers import pipeline, AutoTokenizer
import sys
# Configurar logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Cargar variables de entorno
load_dotenv()
# 1. MODELO ESPECÍFICO PARA ESPAÑOL
MODEL_NAME = "Recognai/zeroshot_selectra_medium" # Modelo en español para zero-shot
CATEGORIAS = {
"logística": re.compile(r"pedido|entrega|env[íi]o|llegada|reparto|transporte|seguimiento", re.IGNORECASE),
"pagos": re.compile(r"pago|tarjeta|factura|cobro|d[eé]bito|cr[eé]dito|transacci[oó]n", re.IGNORECASE),
"producto defectuoso": re.compile(r"defectuoso|roto|dañado|mal estado|no funciona|averiado|falla", re.IGNORECASE),
"cuenta": re.compile(r"cuenta|login|registro|acceso|contraseña|usuario|perfil", re.IGNORECASE),
"facturación": re.compile(r"factura|recibo|impuesto|cargo|precio|valor|subtotal", re.IGNORECASE)
}
URGENCY_PATTERNS = [ # Patrones
r"\b(urgente|inmediato|cr[íi]tico|asap|necesito ayuda ya)\b",
r"\b(no funciona|error|fallo|roto|averiado|defectuoso|no sirve)\b",
r"!\s*!+",
r"\b(prioridad [1-3]|nivel [1-3])\b"
]
# 2. Clase para manejo de tickets
class TicketSystem:
def limpiar_historial(self, filename="tickets_db.json"):
"""Limpia el historial de tickets simulados."""
self.tickets = []
self.next_id = 1000
self.save_to_json(filename)
return True
def __init__(self):
self.mode = os.getenv("TICKET_API_MODE", "simulated")
self.tickets = []
self.next_id = 1000
def create_ticket(self, description: str, category: str, urgent: bool):
"""Crea un ticket en Zendesk o modo simulado"""
if self.mode == "zendesk":
return self._create_zendesk_ticket(description, category, urgent)
else:
return self._create_simulated_ticket(description, category, urgent)
def _create_simulated_ticket(self, description: str, category: str, urgent: bool):
ticket = {
"id": self.next_id,
"description": description,
"category": category,
"urgent": urgent,
"status": "open",
"assigned_to": "Agente Humano" if urgent else "Sistema Automático",
"created_at": time.strftime("%Y-%m-%d %H:%M:%S"),
"source": "Simulado"
}
self.tickets.append(ticket)
self.next_id += 1
self.save_to_json()
return ticket
def _create_zendesk_ticket(self, description: str, category: str, urgent: bool):
"""Crea un ticket real en Zendesk"""
subdomain = os.getenv("ZENDESK_SUBDOMAIN")
email = os.getenv("ZENDESK_EMAIL")
api_token = os.getenv("ZENDESK_API_TOKEN")
priority = "urgent" if urgent else "normal"
subject = f"[{category}] {'[URGENTE] ' if urgent else ''}Ticket Automático"
data = {
"ticket": {
"subject": subject,
"comment": {"body": description},
"priority": priority,
"tags": ["auto_classified", category],
"type": "problem"
}
}
try:
response = requests.post(
f"https://{subdomain}.zendesk.com/api/v2/tickets.json",
json=data,
auth=(f"{email}/token", api_token),
headers={"Content-Type": "application/json"}
)
if response.status_code == 201:
ticket_data = response.json().get("ticket", {})
ticket = {
"id": ticket_data["id"],
"description": description,
"category": category,
"urgent": urgent,
"status": ticket_data.get("status", "open"),
"assigned_to": "Agente Humano" if urgent else "Sistema Automático",
"created_at": ticket_data.get("created_at", time.strftime("%Y-%m-%d %H:%M:%S")),
"source": "Zendesk"
}
self.tickets.append(ticket)
self.save_to_json()
return ticket
else:
error_msg = f"Error {response.status_code}: {response.text}"
return {"error": error_msg}
except Exception as e:
return {"error": str(e)}
def get_tickets(self):
return self.tickets
def save_to_json(self, filename="tickets_db.json"):
with open(filename, 'w') as f:
json.dump(self.tickets, f, indent=2)
# 3. Cargar modelo de clasificación con manejo de errores
MODEL_LOADED = False
classifier = None
try:
# Modelo específico para español
tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
classifier = pipeline(
"zero-shot-classification",
model=MODEL_NAME,
device=0 if torch.cuda.is_available() else -1
)
MODEL_LOADED = True
logger.info("✅ Modelo en español cargado exitosamente")
except Exception as e:
logger.error(f"⚠️ Error cargando modelo principal: {e}")
logger.info("🔶 Usando modelo alternativo multilingüe...")
try:
MODEL_NAME = "vicgalle/xlm-roberta-large-xnli-anli"
tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
classifier = pipeline(
"zero-shot-classification",
model=MODEL_NAME,
device=0 if torch.cuda.is_available() else -1
)
MODEL_LOADED = True
logger.info("✅ Modelo multilingüe cargado exitosamente")
except Exception as alt_e:
logger.error(f"⚠️ Error cargando modelo alternativo: {alt_e}")
logger.info("🔶 Usando clasificador aleatorio como fallback")
# 4. Funciones de clasificación
def es_urgente(text: str) -> bool:
text = text.lower()
for pattern in URGENCY_PATTERNS:
if re.search(pattern, text, flags=re.IGNORECASE):
return True
return False
def clasificar_con_palabras_clave(text: str) -> str:
text_lower = text.lower()
for category, pattern in CATEGORIAS.items():
if pattern.search(text_lower):
return category
return "otros"
def clasificar_texto(text: str) -> str:
if not MODEL_LOADED or classifier is None:
return clasificar_con_palabras_clave(text)
try:
# Usar plantillas específicas por categoría para mejor precisión
hypothesis_templates = [
f"Este ticket trata sobre {cat}." for cat in CATEGORIAS
]
result = classifier(
text,
candidate_labels=CATEGORIAS,
hypothesis_template=hypothesis_templates,
multi_label=False
)
# Umbral de confianza ajustable por categoría
umbrales = {
"logística": 0.4, # Umbral más bajo por la ambigüedad natural
"otros": 0.3,
"default": 0.5
}
top_label = result['labels'][0]
top_score = result['scores'][0]
umbral = umbrales.get(top_label, umbrales["default"])
if top_score >= umbral:
return top_label
# Si confianza baja, usar sistema de palabras clave
return clasificar_con_palabras_clave(text)
except Exception as e:
logger.error(f"⚠️ Error en clasificación: {e}")
return clasificar_con_palabras_clave(text)
# 5. Función para procesar archivos CSV
def procesar_tickets(input_csv, output_csv=None):
"""
Procesa un archivo CSV con tickets y genera resultados clasificados.
- Permite nombres únicos para archivos de salida.
- Valida la existencia de la columna 'descripcion' (case-insensitive).
"""
try:
df = pd.read_csv(input_csv)
# Buscar columna 'descripcion' de forma flexible
desc_col = None
for col in df.columns:
if col.strip().lower() == 'descripcion':
desc_col = col
break
if not desc_col:
raise ValueError("El CSV debe contener una columna llamada 'descripcion' (no se encontró, revise el encabezado)")
# Validar duplicados
num_duplicados = df.duplicated(subset=[desc_col]).sum()
if num_duplicados > 0:
logger.warning(f"Se encontraron {num_duplicados} tickets duplicados (por descripción) en el archivo CSV.")
# Nombres únicos para archivos de salida
timestamp = time.strftime("%Y%m%d_%H%M%S")
if not output_csv:
output_csv = f"tickets_clasificados_{timestamp}.csv"
urgentes_csv = f"tickets_urgentes_{timestamp}.csv"
categorias_pred = []
urgencias = []
logger.info("Iniciando procesamiento de tickets...")
for i, descripcion in enumerate(df[desc_col]):
descripcion_str = str(descripcion)
categoria = clasificar_texto(descripcion_str)
categorias_pred.append(categoria)
urgencia = es_urgente(descripcion_str)
urgencias.append(urgencia)
logger.info(f"Ticket {i+1}: '{descripcion_str[:30]}...' -> Categoría: {categoria}, Urgente: {urgencia}")
df['categoria'] = categorias_pred
df['urgente'] = urgencias
df.to_csv(output_csv, index=False)
logger.info(f"Resultados guardados en {output_csv}")
urgentes = df[df['urgente']]
if not urgentes.empty:
urgentes.to_csv(urgentes_csv, index=False)
logger.info(f"⚠️ {len(urgentes)} tickets urgentes guardados en '{urgentes_csv}'")
return df, urgentes_csv, output_csv, len(df), len(urgentes), num_duplicados
else:
logger.info("No se encontraron tickets urgentes")
return df, None, output_csv, len(df), 0, num_duplicados
except Exception as e:
logger.error(f"❌ Error procesando CSV: {e}")
raise
# 6. Inicializar sistema de tickets para la interfaz web
ticket_system = TicketSystem()
# 7. Función para procesar tickets individuales
def procesar_ticket_individual(text):
if not text.strip():
return "", "", ""
categoria = clasificar_texto(text)
urgente = es_urgente(text)
# Crear ticket en el sistema
ticket = ticket_system.create_ticket(text, categoria, urgente)
status = "🔴 URGENTE - Asignado a Agente Humano" if urgente else "🟢 Enviado a Sistema Automático"
return categoria, "SÍ" if urgente else "NO", status
# 8. Interfaz de usuario con Gradio
with gr.Blocks(title="Sistema de Soporte Inteligente", theme=gr.themes.Soft()) as demo:
gr.Markdown("# 🚀 Sistema Clasificador de Tickets")
gr.Markdown(f"**Modo actual:** `{ticket_system.mode.upper()}` | **Modelo:** `{MODEL_NAME if MODEL_LOADED else 'ALEATORIO'}`")
# Pestañas para diferentes funcionalidades
"""
with gr.Tab("Clasificación Individual"):
with gr.Row():
with gr.Column():
input_text = gr.Textbox(
label="Descripción del problema",
placeholder="Escribe aquí el problema del cliente...",
lines=4
)
submit_btn = gr.Button("Procesar Ticket", variant="primary")
with gr.Accordion("Ejemplos Rápidos", open=False):
gr.Examples(
examples=[
["Mi paquete no llegó a tiempo, ¡es urgente!"],
["Error 500 al procesar mi tarjeta de crédito"],
["El producto llegó con la pantalla rota"],
["No puedo acceder a mi cuenta premium"],
["Factura con impuestos incorrectos"]
],
inputs=[input_text]
)
with gr.Column():
categoria_out = gr.Textbox(label="Categoría")
urgencia_out = gr.Textbox(label="¿Urgente?")
status_out = gr.Textbox(label="Estado del Ticket")
with gr.Accordion("Base de Tickets", open=False):
ticket_db = gr.JSON(label="Tickets Registrados")
update_btn = gr.Button("Actualizar Base de Datos")
"""
with gr.Tab("Procesar Archivo CSV"):
with gr.Row():
with gr.Column():
file_input = gr.File(label="Subir CSV de tickets", file_types=[".csv"])
process_btn = gr.Button("Procesar Archivo", variant="primary")
with gr.Column():
output_status = gr.Textbox(label="Estado de Procesamiento")
output_download = gr.File(label="Descargar Resultados")
urgent_download = gr.File(label="Descargar Tickets Urgentes", visible=False)
with gr.Accordion("Instrucciones", open=False):
gr.Markdown("""
**Formato CSV requerido:**
- Debe contener columna 'descripcion'
- Ejemplo:
```
id,descripcion
1,Mi pedido no llegó
2,Error en mi pago
```
""")
# Event handlers
"""
submit_btn.click(
fn=procesar_ticket_individual,
inputs=input_text,
outputs=[categoria_out, urgencia_out, status_out]
)
update_btn.click(
fn=lambda: ticket_system.get_tickets(),
inputs=[],
outputs=ticket_db
)
"""
# Función wrapper para procesar CSV
def procesar_csv_wrapper(archivo):
"""
Procesa el archivo CSV subido y retorna mensajes y archivos de salida únicos.
"""
if archivo is None:
return "❌ No se subió ningún archivo", None, None, gr.update(visible=False)
try:
file_path = archivo.name
result, urgentes_file, output_file, total, urgentes_count, duplicados = procesar_tickets(file_path)
resumen = f"Total tickets procesados: {total}. "
if duplicados > 0:
resumen += f"Duplicados detectados: {duplicados}. "
if urgentes_count > 0:
resumen += f"Tickets urgentes: {urgentes_count}. "
else:
resumen += "No se encontraron tickets urgentes. "
if result is not None:
if urgentes_file:
return (
f"✅ Procesamiento completado con éxito. {resumen}Resultados: {output_file}",
output_file,
urgentes_file,
gr.update(visible=True)
)
else:
return (
f"✅ Procesamiento completado. {resumen}Resultados: {output_file}",
output_file,
None,
gr.update(visible=False)
)
else:
return "❌ Error procesando el archivo", None, None, gr.update(visible=False)
except Exception as e:
return f"❌ Error: {str(e)}", None, None, gr.update(visible=False)
process_btn.click(
fn=procesar_csv_wrapper,
inputs=file_input,
outputs=[output_status, output_download, urgent_download, urgent_download]
)
# 9. Ejecutar la aplicación
if __name__ == "__main__":
# Si se pasa un archivo CSV como argumento, procesar en modo batch
if len(sys.argv) > 1:
input_csv = sys.argv[1]
logger.info(f"Procesando archivo: {input_csv}")
try:
result, urgentes, salida, total, urgentes_count, duplicados = procesar_tickets(input_csv)
logger.info(f"Total tickets procesados: {total}")
logger.info(f"Duplicados detectados: {duplicados}")
logger.info(f"Tickets urgentes: {urgentes_count}")
logger.info(f"Archivo de resultados: {salida}")
if urgentes:
logger.info(f"Archivo de tickets urgentes: {urgentes}")
else:
logger.info("No se encontraron tickets urgentes.")
except Exception as e:
logger.error(f"Error procesando el archivo: {e}")
sys.exit(1)
else:
# Modo interfaz web
demo.launch(
server_name="0.0.0.0",
server_port=7860,
show_error=True
)