Spaces:
Running
Running
File size: 15,163 Bytes
2f0c918 453b59a 2f0c918 ee7b1c4 c3f1d68 cf3a5d0 a5f79fe ee7b1c4 cf3a5d0 ee7b1c4 c3f1d68 2552c59 c3f1d68 ee7b1c4 5395e60 2552c59 5395e60 ee7b1c4 c3f1d68 2552c59 c3f1d68 ee7b1c4 2552c59 ee7b1c4 1dc0654 ee7b1c4 23a63d3 ee7b1c4 cf3a5d0 ee7b1c4 c3f1d68 cf3a5d0 c3f1d68 cf3a5d0 7516019 cf3a5d0 2f0c918 23a63d3 cf3a5d0 2f0c918 cf3a5d0 2f0c918 cf3a5d0 698e671 cf3a5d0 453b59a cf3a5d0 453b59a cf3a5d0 23a63d3 7516019 cf3a5d0 1dc0654 ee7b1c4 23a63d3 ee7b1c4 cf3a5d0 ee7b1c4 cf3a5d0 ee7b1c4 8d98892 cf3a5d0 23a63d3 cf3a5d0 7516019 cf3a5d0 7516019 2552c59 cf3a5d0 7516019 cf3a5d0 7516019 cf3a5d0 7516019 cf3a5d0 23a63d3 cf3a5d0 23a63d3 cf3a5d0 23a63d3 ee7b1c4 23a63d3 cf3a5d0 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 |
import os
import time
import requests
from fastapi import FastAPI, Query, HTTPException
from fastapi.middleware.cors import CORSMiddleware
import openai
# ==============================
# CONFIGURACIÓN DE CLASIFICADORES
# ==============================
CLASIFICADORES = {
"abaco": {
"candidate_labels": [
'Otros No Alimentos', 'Alimentos de hoteles, restaurantes o casinos', 'Juguetes', 'Muebles',
'Pequeño Aparato', 'Textiles', 'Bebidas Alcohólicas', 'Dulces y Postres', 'Papelería',
'Bebidas Azucaradas', 'Carnes', 'Panadería de sal', 'Materiales de construcción', 'Hogar',
'Otras bebidas', 'Grasas tipo 3 (saturadas)', 'Otros Alimentos', 'Cereales', 'Frutas', 'Personal',
'Gama Blanca', 'Gama Marron', 'Frutos secos y semillas', 'Paquetes/snacks', 'Agua',
'Productos lácteos grasa entera', 'Dulces y postres', 'Otras fórmulas especiales',
'Leches Enteras', 'Fórmulas infantiles', 'Leguminosas Secas', 'Tuberculos', 'Raíz',
'Plátano', 'Verduras', 'Panadería dulce', 'Huevos', 'Azúcares Simples',
'Medicamentos sin fórmula médica', 'Suplementos nutricionales', 'Alimentos para Mascotas',
'Grasas tipo 2 (poliinsaturadas)', 'Grasas tipo 1 (monoinsaturadas)'
],
"categoria_tipo": {
'Sin Categoria': 'Sin Categoria',
'Otros No Alimentos': 'other', 'Alimentos de hoteles, restaurantes o casinos': 'food',
'Juguetes': 'other', 'Muebles': 'other', 'Pequeño Aparato': 'other', 'Textiles': 'other',
'Bebidas Alcohólicas': 'food', 'Dulces y Postres': 'food', 'Papelería': 'other',
'Bebidas Azucaradas': 'food', 'Carnes': 'food', 'Panadería de sal': 'food',
'Materiales de construcción': 'other', 'Hogar': 'other', 'Otras bebidas': 'food',
'Grasas tipo 3 (saturadas)': 'food', 'Otros Alimentos': 'food', 'Cereales': 'food',
'Frutas': 'food', 'Personal': 'other', 'Gama Blanca': 'other', 'Gama Marron': 'other',
'Frutos secos y semillas': 'food', 'Paquetes/snacks': 'food', 'Agua': 'food',
'Productos lácteos grasa entera': 'food', 'Dulces y postres': 'food',
'Otras fórmulas especiales': 'food', 'Leches Enteras': 'food',
'Fórmulas infantiles': 'food', 'Leguminosas Secas': 'food', 'Tuberculos': 'food',
'Raíz': 'food', 'Plátano': 'food', 'Verduras': 'food', 'Panadería dulce': 'food',
'Huevos': 'food', 'Azúcares Simples': 'food',
'Medicamentos sin fórmula médica': 'other', 'Suplementos nutricionales': 'food',
'Alimentos para Mascotas': 'other', 'Grasas tipo 2 (poliinsaturadas)': 'food',
'Grasas tipo 1 (monoinsaturadas)': 'food'
}
},
"mexico": {
"candidate_labels": [
'No Comestible', 'Fruta y verdura', 'Pan', 'Cereales y Leguminosas', 'Bebidas Saborizadas',
'Azucares', 'Abarrotes', 'Proteina animal', 'Alimento Preparado',
'Refrigerados y Congelados', 'Lactéos', 'Agua', 'Jugo y Néctar',
'Leguminosa', 'Grasas Animales', 'Confiteria', 'Suplementos',
'Salsas', 'Café y Té', 'Sabores listos para comer', 'Aceites Vegetales',
'Raices, Tuberculos', 'Semillas y Nueces', 'Productos veganos'
],
"categoria_tipo": {
'No Comestible': 'other', 'Fruta y verdura': 'food', 'Pan': 'food',
'Cereales y Leguminosas': 'food', 'Bebidas Saborizadas': 'food', 'Azucares': 'food',
'Abarrotes': 'food', 'Proteina animal': 'food', 'Alimento Preparado': 'food',
'Refrigerados y Congelados': 'food', 'Lactéos': 'food', 'Agua': 'food',
'Jugo y Néctar': 'food', 'Leguminosa': 'food', 'Grasas Animales': 'food',
'Confiteria': 'food', 'Suplementos': 'food', 'Salsas': 'food', 'Café y Té': 'food',
'Sabores listos para comer': 'food', 'Aceites Vegetales': 'food',
'Raices, Tuberculos': 'food', 'Semillas y Nueces': 'food', 'Productos veganos': 'food'
}
}
}
# ==============================
# CONFIGURACIÓN APP Y API
# ==============================
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
HF_TOKEN = os.getenv("HG_TOKEN")
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
openai.api_key = OPENAI_API_KEY
MODEL_1_URL = "https://api-inference.huggingface.co/models/facebook/bart-large-mnli?wait_for_model=true"
MODEL_2_URL = "https://api-inference.huggingface.co/models/joeddav/xlm-roberta-large-xnli?wait_for_model=true"
HEADERS = {"Authorization": f"Bearer {HF_TOKEN}"}
# ==============================
# FUNCIONES DE UTILIDAD
# ==============================
def query_huggingface_api(sequence, labels, api_url):
payload = {
"inputs": sequence,
"parameters": {"candidate_labels": labels}
}
response = requests.post(api_url, headers=HEADERS, json=payload)
if response.status_code != 200:
raise Exception(f"Error al invocar Hugging Face API: {response.status_code}, {response.text}")
return response.json()
def query_openai_classification(sequence, labels):
prompt = (
f"Dado el siguiente texto: '{sequence}', selecciona la categoría más adecuada entre: {', '.join(labels)}. "
f"Devuelve únicamente el nombre exacto de la categoría seleccionada."
)
try:
# Usar la nueva API de openai >= 1.0.0
response = openai.chat.completions.create(
model="gpt-3.5-turbo",
messages=[
{"role": "system", "content": "Eres un clasificador experto."},
{"role": "user", "content": prompt}
],
temperature=0.0,
max_tokens=20
)
choice = response.choices[0].message.content.strip()
# Validar que la respuesta esté entre las etiquetas
if choice in labels:
return {"labels": [choice], "scores": [1.0]}
else:
# A veces el modelo puede devolver texto adicional, intentar limpiar
for label in labels:
if label.lower() in choice.lower():
return {"labels": [label], "scores": [1.0]}
raise Exception(f"Respuesta inválida del modelo OpenAI: '{choice}'")
except Exception as e:
raise Exception(f"Error en modelo OpenAI: {str(e)}")
def clasificar_con_modelos(sequence, labels):
errores = []
modelos = [
(MODEL_1_URL, "Modelo 1"),
(MODEL_2_URL, "Modelo 2"),
("openai", "Modelo 3 OpenAI")
]
for url, nombre in modelos:
for intento in range(2):
try:
if url == "openai":
resultado = query_openai_classification(sequence, labels)
else:
resultado = query_huggingface_api(sequence, labels, url)
return resultado, nombre
except Exception as e:
error_text = f"{nombre} intento {intento+1}: {str(e)}"
if ("Model too busy" in error_text or "Resource temporarily unavailable" in error_text or "Model" in error_text and "is currently loading" in error_text) and intento == 0: # Añadido "is currently loading"
time.sleep(5)
continue
errores.append(error_text)
break # Salir del bucle de intentos para este modelo si falla y no es reintentable
# Si todos los modelos fallan para este chunk de etiquetas
raise Exception("Fallo en todos los modelos para este chunk de etiquetas:\n" + "\n".join(errores))
def split_list(lst, max_size):
for i in range(0, len(lst), max_size):
yield lst[i:i + max_size]
# ==============================
# ENDPOINT PRINCIPAL
# ==============================
@app.get("/categorize")
def categorizar_producto(
cua_master: str = Query(..., description="Selección de clasificador (abaco/mexico)"),
typology: str = Query(..., description="Valor de typology a clasificar")
):
cua_master = cua_master.lower()
if cua_master not in CLASIFICADORES:
raise HTTPException(
status_code=400,
detail=f"Clasificador no válido. Opciones: {', '.join(CLASIFICADORES.keys())}"
)
config = CLASIFICADORES[cua_master]
best_score = -1.0 # Inicializar a un flotante para consistencia
best_label = "Sin Categoria"
modelo_utilizado = "Ninguno" # Un default más informativo
# Validar que los tokens estén presentes si se van a usar
if not HF_TOKEN and (MODEL_1_URL or MODEL_2_URL): # Si se usan modelos HF
# Podrías lanzar un error aquí o loguear una advertencia severa
# Por ahora, permitiremos que falle en query_huggingface_api si es el caso
print("Advertencia: HG_TOKEN no está configurado, los modelos de Hugging Face pueden fallar.")
# raise HTTPException(status_code=503, detail="Configuración del servidor incompleta: HG_TOKEN faltante.")
if not OPENAI_API_KEY: # Si se usa OpenAI
# Similar al anterior
print("Advertencia: OPENAI_API_KEY no está configurado, el modelo de OpenAI puede fallar.")
# raise HTTPException(status_code=503, detail="Configuración del servidor incompleta: OPENAI_API_KEY faltante.")
typology_stripped = typology.strip()
if not typology_stripped:
raise HTTPException(status_code=400, detail="El parámetro 'typology' no puede estar vacío.")
# Lógica para intentar clasificar con los modelos disponibles
# Si un chunk de etiquetas da un resultado, ese se usa.
# Si un modelo falla en un chunk, se intenta el siguiente modelo para ESE MISMO CHUNK.
# Si todos los modelos fallan para un chunk, se lanza una excepción y la clasificación general falla.
# Si un chunk es exitoso, se actualiza best_score y best_label.
# El objetivo es encontrar la mejor clasificación entre todos los chunks.
# Almacenar los resultados de cada chunk si es necesario, o simplemente el mejor hasta ahora.
# La lógica original actualiza best_score y best_label iterativamente.
try:
# Itera sobre los chunks de etiquetas candidatas
for sublist_labels in split_list(config["candidate_labels"], 10):
if not sublist_labels: # En caso de que split_list devuelva una lista vacía
continue
try:
# Intenta clasificar este chunk con la cascada de modelos
result, modelo = clasificar_con_modelos(typology_stripped, sublist_labels)
# Asegurarse que el resultado tiene la estructura esperada
if result and "scores" in result and result["scores"] and \
"labels" in result and result["labels"]:
current_score = result["scores"][0]
current_label = result["labels"][0]
if current_score > best_score:
best_score = current_score
best_label = current_label
modelo_utilizado = modelo
else:
# Esto no debería ocurrir si las funciones query_* devuelven el formato correcto
# o lanzan una excepción. Pero es una salvaguarda.
print(f"Advertencia: Resultado malformado o vacío del clasificador para el chunk: {sublist_labels} con tipología '{typology_stripped}'. Resultado: {result}")
# Continuar al siguiente chunk o manejar como un error si es crítico
# Por ahora, lo ignoramos y no actualizamos best_score
except Exception as e_chunk:
# Si clasificar_con_modelos lanza una excepción (todos los modelos fallaron para este chunk)
# Se podría decidir si continuar con el siguiente chunk o fallar toda la clasificación.
# La lógica original parece implicar que un fallo en un chunk no detiene los demás,
# sino que se busca el "best_score" global.
# Sin embargo, clasificar_con_modelos lanzará una excepción que detendría este bucle.
# Para que continúe y pruebe otros chunks (y tome el mejor de los exitosos),
# necesitaríamos capturar la excepción aquí y decidir.
print(f"Error procesando chunk de etiquetas {sublist_labels} para '{typology_stripped}': {e_chunk}. Continuando con el siguiente chunk si hay.")
# Si queremos que la clasificación general falle si un chunk falla, entonces no necesitamos este try-except interno.
# La lógica original tenía el try-except principal más afuera.
# Vamos a mantener la lógica de que si un chunk falla completamente (todos los modelos),
# la clasificación general para ese typology podría fallar o continuar.
# Si se quiere que falle toda la llamada a /categorize:
# raise HTTPException(status_code=500, detail=f"Error clasificando chunk: {str(e_chunk)}")
# Si se quiere continuar y ver si otros chunks dan resultado:
continue # Pasa al siguiente sublist_labels
# Después de probar todos los chunks:
if best_score == -1.0 and best_label == "Sin Categoria":
# Esto significa que o no hubo chunks, o todos los chunks fallaron de tal manera
# que no se actualizó best_score (por ejemplo, si capturamos excepciones por chunk y continuamos)
# o si la lista de candidate_labels estaba vacía.
# En este punto, si no hay una mejor etiqueta, se queda "Sin Categoria".
# Si se quiere lanzar un error si no se clasificó:
# raise HTTPException(status_code=404, detail=f"No se pudo clasificar la tipología '{typology_stripped}'.")
pass # Se devolverá "Sin Categoria"
etiqueta_a = config["categoria_tipo"].get(best_label, "Sin Categoria")
# Asegurar que probabilidad sea un número antes de redondear
probabilidad_final = 0.0
if isinstance(best_score, (int, float)) and best_score > -1.0 : # Solo si es un score válido
probabilidad_final = round(best_score * 100, 2)
return {
"eatc-odd_typology_b": best_label,
"eatc-odd_typology_a": etiqueta_a,
"probabilidad": probabilidad_final,
"modelo_usado": modelo_utilizado
}
except Exception as e:
# Este es el try-except principal que estaba en tu código original.
# Si `clasificar_con_modelos` lanza una excepción (todos los modelos fallaron para un chunk),
# y no la capturamos dentro del bucle de chunks, llegará aquí.
print(f"Error general en la clasificación para typology '{typology_stripped}': {str(e)}") # Loguear el error
raise HTTPException(status_code=500, detail=f"Error en clasificación: {str(e)}") |