Alecit1234 commited on
Commit
430a577
·
verified ·
1 Parent(s): 726a1e8

Upload 9 files

Browse files
Files changed (9) hide show
  1. app_whatsapp.py +33 -0
  2. config.py +31 -0
  3. core_pipeline.py +89 -0
  4. db_supabase.py +95 -0
  5. dockerfile +41 -0
  6. nlp_category.py +71 -0
  7. nlp_intent.py +62 -0
  8. nlp_ner.py +100 -0
  9. requirements.txt +17 -0
app_whatsapp.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app_whatsapp.py
2
+ from fastapi import FastAPI, Form
3
+ from fastapi.responses import PlainTextResponse
4
+ from core_pipeline import procesar_mensaje
5
+ from config import logger
6
+
7
+ app = FastAPI(title="Asistente Financiero WhatsApp")
8
+
9
+ # Twilio manda POST x-www-form-urlencoded a este endpoint
10
+ # Configura tu webhook en Twilio: https://TU-SERVIDOR/ngrok/etc/whatsapp
11
+
12
+
13
+ @app.post("/whatsapp", response_class=PlainTextResponse)
14
+ async def whatsapp_webhook(
15
+ Body: str = Form(...),
16
+ From: str = Form(None),
17
+ WaId: str = Form(None),
18
+ ):
19
+ """
20
+ Webhook de Twilio WhatsApp.
21
+ Body = mensaje de texto
22
+ From = número del usuario (whatsapp:+51...)
23
+ WaId = ID de WhatsApp del usuario
24
+ """
25
+ logger.info("===== WhatsApp WEBHOOK =====")
26
+ logger.info("From: %s | WaId: %s | Body: %s", From, WaId, Body)
27
+
28
+ resultado = procesar_mensaje(Body)
29
+ respuesta_texto = resultado["respuesta"]
30
+
31
+ # Respondemos en texto plano (Twilio lo acepta),
32
+ # si quieres TwiML puedes devolver XML.
33
+ return respuesta_texto
config.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # config.py
2
+ import os
3
+ from dotenv import load_dotenv
4
+ import logging
5
+
6
+ # Carga variables de entorno desde .env
7
+ load_dotenv()
8
+
9
+ # ========== LOGGING ==========
10
+ LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO").upper()
11
+ logging.basicConfig(
12
+ level=LOG_LEVEL,
13
+ format="%(asctime)s | %(name)s | %(levelname)s | %(message)s"
14
+ )
15
+
16
+ logger = logging.getLogger("finanzas_app")
17
+
18
+ # ========== TWILIO ==========
19
+ TWILIO_AUTH_TOKEN = os.getenv("TWILIO_AUTH_TOKEN") # para validar firma si quieres
20
+ TWILIO_WHATSAPP_NUMBER = os.getenv("TWILIO_WHATSAPP_NUMBER", "whatsapp:+14155238886")
21
+
22
+ # ========== SUPABASE ==========
23
+ SUPABASE_URL = os.getenv("SUPABASE_URL")
24
+ SUPABASE_ANON_KEY = os.getenv("SUPABASE_ANON_KEY")
25
+ SUPABASE_SERVICE_ROLE_KEY = os.getenv("SUPABASE_SERVICE_ROLE_KEY", SUPABASE_ANON_KEY)
26
+
27
+ # Usuario por defecto (tu UUID de la tabla usuarios)
28
+ DEFAULT_USER_ID = os.getenv(
29
+ "DEFAULT_USER_ID",
30
+ "c6f4a4b6-1234-45ab-b0a2-88ac4ed4d111"
31
+ )
core_pipeline.py ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # core_pipeline.py
2
+ from typing import Dict, Any
3
+ from config import logger
4
+ from nlp_intent import predecir_intencion
5
+ from nlp_ner import extraer_entidades
6
+ from nlp_category import predecir_categoria
7
+ from db_supabase import insertar_gasto, insertar_ingreso
8
+ import re
9
+
10
+
11
+ def _parse_monto_str(monto_str: str) -> float:
12
+ # Extrae número de algo como "50", "50.00", "S/ 50.90"
13
+ if not monto_str:
14
+ return 0.0
15
+ numeros = re.findall(r"\d+[.,]?\d*", monto_str)
16
+ if not numeros:
17
+ return 0.0
18
+ valor = numeros[0].replace(",", ".")
19
+ try:
20
+ return float(valor)
21
+ except ValueError:
22
+ return 0.0
23
+
24
+
25
+ def procesar_mensaje(texto: str) -> Dict[str, Any]:
26
+ """
27
+ Pipeline completo:
28
+ 1. Predice intención
29
+ 2. Extrae entidades (monto, fecha)
30
+ 3. Predice categoría usando SetFit con el TEXTO COMPLETO
31
+ 4. Aplica acción en Supabase según intención
32
+ 5. Devuelve dict con info + mensaje para usuario
33
+ """
34
+ logger.info("==== Procesando mensaje ====")
35
+ logger.info("Texto: %s", texto)
36
+
37
+ # 1. INTENCIÓN
38
+ intencion = predecir_intencion(texto)
39
+
40
+ # 2. ENTIDADES
41
+ ents = extraer_entidades(texto)
42
+ monto = _parse_monto_str(ents.get("monto"))
43
+ fecha = ents.get("fecha")
44
+
45
+ # 3. CATEGORÍA (texto completo, no la categoria_texto de NER)
46
+ categoria_final = predecir_categoria(texto)
47
+
48
+ # 4. LÓGICA DE NEGOCIO
49
+ respuesta = ""
50
+
51
+ if intencion == "agregar_gasto":
52
+ insertar_gasto(
53
+ monto=monto,
54
+ categoria_str=categoria_final,
55
+ fecha_str=fecha,
56
+ descripcion=texto
57
+ )
58
+ respuesta = f"Anoté un gasto de S/ {monto:.2f} en la categoría '{categoria_final}'."
59
+
60
+ elif intencion == "agregar_ingreso":
61
+ insertar_ingreso(
62
+ monto=monto,
63
+ categoria_ingreso=categoria_final,
64
+ fecha_str=fecha,
65
+ descripcion=texto
66
+ )
67
+ respuesta = f"Registré un ingreso de S/ {monto:.2f} como '{categoria_final}'."
68
+
69
+ elif intencion == "agregar_aporte":
70
+ # TODO: integrar con metas_ahorro
71
+ respuesta = "Detecté que quieres registrar un aporte a una meta de ahorro. Aún no he sido conectado a metas_ahorro 😅."
72
+
73
+ elif intencion == "puedo_gastar":
74
+ # TODO: leer presupuestos_mensuales y responder según límites
75
+ respuesta = "Según tu presupuesto, todavía no tengo conectada la lógica para validar si puedes gastar eso 😅, pero la intención está detectada."
76
+
77
+ else:
78
+ respuesta = f"Detecté intención '{intencion}' con categoría '{categoria_final}', pero aún no tengo lógica asociada."
79
+
80
+ logger.info("[PIPELINE] Resultado: intencion=%s, monto=%s, categoria=%s, fecha=%s",
81
+ intencion, monto, categoria_final, fecha)
82
+
83
+ return {
84
+ "intencion": intencion,
85
+ "monto": monto,
86
+ "categoria_final": categoria_final,
87
+ "fecha": fecha,
88
+ "respuesta": respuesta,
89
+ }
db_supabase.py ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # db_supabase.py
2
+ from supabase import create_client, Client
3
+ from datetime import date
4
+ from typing import Optional
5
+ from config import SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, DEFAULT_USER_ID, logger
6
+
7
+ supabase: Client = create_client(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY)
8
+
9
+
10
+ def get_or_create_categoria(nombre: str, tipo: str = "gasto") -> Optional[int]:
11
+ """
12
+ Busca una categoría por nombre (case insensitive).
13
+ Si no existe, la crea.
14
+ Devuelve id_categoria o None si algo falla.
15
+ """
16
+ logger.info("[DB] get_or_create_categoria: %s (%s)", nombre, tipo)
17
+
18
+ try:
19
+ res = (
20
+ supabase.table("categorias")
21
+ .select("id_categoria")
22
+ .eq("id_usuario", DEFAULT_USER_ID)
23
+ .ilike("nombre_categoria", nombre)
24
+ .execute()
25
+ )
26
+ data = res.data or []
27
+ if data:
28
+ return data[0]["id_categoria"]
29
+
30
+ # Crear nueva categoría
31
+ insert_res = (
32
+ supabase.table("categorias")
33
+ .insert(
34
+ {
35
+ "id_usuario": DEFAULT_USER_ID,
36
+ "nombre_categoria": nombre,
37
+ "tipo_categoria": "ingreso" if tipo.startswith("ingreso") else "gasto",
38
+ "descripcion": f"Creada automáticamente para {nombre}",
39
+ }
40
+ )
41
+ .execute()
42
+ )
43
+ return insert_res.data[0]["id_categoria"]
44
+ except Exception as e:
45
+ logger.error("[DB] Error get_or_create_categoria: %s", e, exc_info=True)
46
+ return None
47
+
48
+
49
+ def insertar_gasto(monto: float, categoria_str: str, fecha_str: Optional[str], descripcion: str):
50
+ logger.info("[DB] Insertar gasto: monto=%s, cat=%s, fecha=%s", monto, categoria_str, fecha_str)
51
+
52
+ id_categoria = get_or_create_categoria(categoria_str, tipo="gasto")
53
+
54
+ payload = {
55
+ "id_usuario": DEFAULT_USER_ID,
56
+ "id_categoria": id_categoria,
57
+ "id_tipo": 2, # por ahora, por defecto "Variable"
58
+ "monto": monto,
59
+ "descripcion": descripcion,
60
+ }
61
+
62
+ if fecha_str:
63
+ payload["fecha"] = fecha_str
64
+ else:
65
+ payload["fecha"] = str(date.today())
66
+
67
+ try:
68
+ supabase.table("gastos").insert(payload).execute()
69
+ logger.info("[DB] Gasto insertado correctamente.")
70
+ except Exception as e:
71
+ logger.error("[DB] Error al insertar gasto: %s", e, exc_info=True)
72
+
73
+
74
+ def insertar_ingreso(monto: float, categoria_ingreso: str, fecha_str: Optional[str], descripcion: str):
75
+ logger.info("[DB] Insertar ingreso: monto=%s, cat=%s, fecha=%s", monto, categoria_ingreso, fecha_str)
76
+
77
+ id_categoria = get_or_create_categoria(categoria_ingreso, tipo="ingreso")
78
+
79
+ payload = {
80
+ "id_usuario": DEFAULT_USER_ID,
81
+ "id_categoria": id_categoria,
82
+ "monto": monto,
83
+ "descripcion": descripcion,
84
+ }
85
+
86
+ if fecha_str:
87
+ payload["fecha"] = fecha_str
88
+ else:
89
+ payload["fecha"] = str(date.today())
90
+
91
+ try:
92
+ supabase.table("ingresos").insert(payload).execute()
93
+ logger.info("[DB] Ingreso insertado correctamente.")
94
+ except Exception as e:
95
+ logger.error("[DB] Error al insertar ingreso: %s", e, exc_info=True)
dockerfile ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ---------------------------------------------------
2
+ # Base image: Python optimizado para ML + CPU
3
+ # ---------------------------------------------------
4
+ FROM python:3.10-slim
5
+
6
+ # ---------------------------------------------------
7
+ # Instalar dependencias del sistema
8
+ # ---------------------------------------------------
9
+ RUN apt-get update && apt-get install -y \
10
+ git \
11
+ libsndfile1 \
12
+ && rm -rf /var/lib/apt/lists/*
13
+
14
+ # ---------------------------------------------------
15
+ # Crear directorio de la app
16
+ # ---------------------------------------------------
17
+ WORKDIR /app
18
+
19
+ # ---------------------------------------------------
20
+ # Copiar requirements.txt e instalar dependencias
21
+ # ---------------------------------------------------
22
+ COPY requirements.txt /app/requirements.txt
23
+
24
+ RUN pip install --upgrade pip
25
+ RUN pip install -r /app/requirements.txt
26
+
27
+ # ---------------------------------------------------
28
+ # Copiar TODO el proyecto dentro del contenedor
29
+ # ---------------------------------------------------
30
+ COPY . /app
31
+
32
+ # ---------------------------------------------------
33
+ # Puerto para HuggingFace Spaces
34
+ # ---------------------------------------------------
35
+ EXPOSE 7860
36
+
37
+ # ---------------------------------------------------
38
+ # Command para iniciar FastAPI en HF Spaces
39
+ # HuggingFace espera que corras en el puerto 7860
40
+ # ---------------------------------------------------
41
+ CMD ["uvicorn", "app_whatsapp:app", "--host", "0.0.0.0", "--port", "7860"]
nlp_category.py ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # nlp_category.py
2
+ import os
3
+ import logging
4
+ import unicodedata
5
+ from setfit import SetFitModel
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+ # ======================
10
+ # 1. Cargar modelo SetFit
11
+ # ======================
12
+
13
+ SETFIT_MODEL_PATH = "Alecit1234/modelo_finanzas_peru_v1"
14
+
15
+ logger.info("Cargando modelo SetFit desde HuggingFace...")
16
+
17
+ model_setfit = SetFitModel.from_pretrained(
18
+ SETFIT_MODEL_PATH,
19
+ use_auth_token=os.getenv("HUGGINGFACE_AUTH_TOKEN")
20
+ )
21
+
22
+ logger.info("Modelo SetFit cargado correctamente.")
23
+
24
+
25
+ # ===========================
26
+ # 2. Categorías (Label Map)
27
+ # ===========================
28
+
29
+ CATEGORY_LABEL_MAP = {
30
+ 0:"comida", 1:"supermercado", 2:"transporte", 3:"taxi", 4:"entretenimiento",
31
+ 5:"educacion", 6:"tecnologia", 7:"servicios", 8:"fitness", 9:"imprevistos",
32
+ 10:"delivery", 11:"mascotas", 12:"familia", 13:"salud",
33
+ 14:"ingreso_beca", 15:"ingreso_trabajo", 16:"ingreso_familia",
34
+ 17:"ingreso_venta", 18:"ingreso_freelance", 19:"ingreso_extra"
35
+ }
36
+
37
+
38
+ # ======================
39
+ # 3. Normalizar texto
40
+ # ======================
41
+
42
+ def _normalizar(texto: str) -> str:
43
+ texto = texto.lower()
44
+ texto = unicodedata.normalize("NFD", texto)
45
+ return "".join(c for c in texto if unicodedata.category(c) != "Mn")
46
+
47
+
48
+ # ======================
49
+ # 4. Predicción categoría
50
+ # ======================
51
+
52
+ def predecir_categoria(texto: str) -> str:
53
+ """
54
+ Predice categoría usando SOLO el texto completo.
55
+ NER aporta monto y fecha, pero NO categoría.
56
+ """
57
+
58
+ if not texto or texto.strip() == "":
59
+ logger.warning("Texto vacío recibido en predecir_categoria()")
60
+ return "otros"
61
+
62
+ texto_norm = _normalizar(texto)
63
+
64
+ logger.debug(f"[SETFIT] Texto normalizado: {texto_norm}")
65
+
66
+ pred_id = model_setfit.predict([texto_norm])[0].item()
67
+ categoria = CATEGORY_LABEL_MAP[pred_id]
68
+
69
+ logger.info(f"[SETFIT] Categoría predicha: {categoria} (id={pred_id})")
70
+
71
+ return categoria
nlp_intent.py ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # nlp_intent.py
2
+ import os
3
+ import json
4
+ import torch
5
+ import logging
6
+ from transformers import AutoTokenizer, AutoModelForSequenceClassification
7
+ from huggingface_hub import hf_hub_download
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+ # ========== RUTA DEL MODELO ==========
12
+ INTENT_MODEL_PATH = "Alecit1234/modelo_intenciones"
13
+
14
+ logger.info("Cargando modelo de intenciones desde HuggingFace Hub: %s", INTENT_MODEL_PATH)
15
+
16
+ # ========== CARGA DEL MODELO ==========
17
+ tokenizer_int = AutoTokenizer.from_pretrained(
18
+ INTENT_MODEL_PATH,
19
+ use_auth_token=os.getenv("HUGGINGFACE_AUTH_TOKEN")
20
+ )
21
+
22
+ model_int = AutoModelForSequenceClassification.from_pretrained(
23
+ INTENT_MODEL_PATH,
24
+ use_auth_token=os.getenv("HUGGINGFACE_AUTH_TOKEN")
25
+ )
26
+
27
+ # ========== LABEL MAP ==========
28
+ label_map_path = hf_hub_download(
29
+ repo_id=INTENT_MODEL_PATH,
30
+ filename="label_map.json",
31
+ use_auth_token=os.getenv("HUGGINGFACE_AUTH_TOKEN")
32
+ )
33
+
34
+ with open(label_map_path, "r", encoding="utf-8") as f:
35
+ INTENT_LABEL_MAP = json.load(f)
36
+
37
+
38
+ # ========== FUNCIÓN PRINCIPAL ==========
39
+ def predecir_intencion(texto: str) -> str:
40
+ """Predice intención de un texto usando modelo de clasificación."""
41
+ if not texto or texto.strip() == "":
42
+ logger.warning("[INTENT] Texto vacío recibido. Asignando intención 'otros'.")
43
+ return "otros"
44
+
45
+ logger.debug("[INTENT] Texto de entrada: %s", texto)
46
+
47
+ inputs = tokenizer_int(
48
+ texto,
49
+ return_tensors="pt",
50
+ truncation=True,
51
+ max_length=64,
52
+ padding="max_length"
53
+ )
54
+
55
+ with torch.no_grad():
56
+ logits = model_int(**inputs).logits
57
+ pred_id = torch.argmax(logits, dim=1).item()
58
+
59
+ intent = INTENT_LABEL_MAP[str(pred_id)]
60
+
61
+ logger.info("[INTENT] Predicción: %s (id=%s)", intent, pred_id)
62
+ return intent
nlp_ner.py ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # nlp_ner.py
2
+ import os
3
+ import json
4
+ import logging
5
+ import torch
6
+ from transformers import AutoTokenizer, AutoModelForTokenClassification
7
+ from huggingface_hub import hf_hub_download
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+ # =====================================
12
+ # 1. NOMBRE DEL MODELO EN HUGGINGFACE
13
+ # =====================================
14
+ NER_MODEL_PATH = "Alecit1234/modelo_ner"
15
+
16
+ logger.info("Cargando modelo NER desde HuggingFace Hub: %s", NER_MODEL_PATH)
17
+
18
+ # =====================================
19
+ # 2. CARGAR TOKENIZER & MODEL
20
+ # =====================================
21
+ tokenizer_ner = AutoTokenizer.from_pretrained(
22
+ NER_MODEL_PATH,
23
+ use_auth_token=os.getenv("HUGGINGFACE_AUTH_TOKEN")
24
+ )
25
+
26
+ model_ner = AutoModelForTokenClassification.from_pretrained(
27
+ NER_MODEL_PATH,
28
+ use_auth_token=os.getenv("HUGGINGFACE_AUTH_TOKEN")
29
+ )
30
+
31
+ # =====================================
32
+ # 3. CARGAR LABEL_MAP DESDE HF
33
+ # =====================================
34
+ label_map_path = hf_hub_download(
35
+ repo_id=NER_MODEL_PATH,
36
+ filename="label_map.json",
37
+ use_auth_token=os.getenv("HUGGINGFACE_AUTH_TOKEN")
38
+ )
39
+
40
+ with open(label_map_path, "r", encoding="utf-8") as f:
41
+ NER_LABELS = json.load(f)
42
+
43
+ logger.info("Etiquetas NER cargadas: %s", NER_LABELS)
44
+
45
+
46
+ # =====================================
47
+ # 4. FUNCIÓN: EXTRAER ENTIDADES
48
+ # =====================================
49
+ def extraer_entidades(texto: str) -> dict:
50
+ """
51
+ Extrae entidades: monto, fecha, categoria_texto (solo referencia)
52
+ """
53
+
54
+ logger.debug("[NER] Procesando texto: %s", texto)
55
+
56
+ inputs = tokenizer_ner(
57
+ texto,
58
+ return_tensors="pt",
59
+ truncation=True,
60
+ max_length=64
61
+ )
62
+
63
+ with torch.no_grad():
64
+ outputs = model_ner(**inputs)
65
+
66
+ preds = outputs.logits.argmax(dim=-1)[0].tolist()
67
+ tokens = tokenizer_ner.convert_ids_to_tokens(inputs["input_ids"][0])
68
+
69
+ entidades = {"monto": None, "categoria_texto": None, "fecha": None}
70
+
71
+ palabra = ""
72
+ tipo_actual = None
73
+
74
+ for tok, pred_id in zip(tokens, preds):
75
+ label = NER_LABELS[str(pred_id)]
76
+
77
+ # Cuando cambia la etiqueta
78
+ if label == "O":
79
+ if tipo_actual and palabra:
80
+ entidades[tipo_actual] = palabra
81
+ palabra = ""
82
+ tipo_actual = None
83
+ continue
84
+
85
+ # Asignación del tipo
86
+ if label == "MONEY":
87
+ tipo_actual = "monto"
88
+ elif label == "CATEGORY":
89
+ tipo_actual = "categoria_texto"
90
+ elif label == "DATE":
91
+ tipo_actual = "fecha"
92
+
93
+ palabra += tok.replace("▁", "")
94
+
95
+ # Último token acumulado
96
+ if tipo_actual and palabra:
97
+ entidades[tipo_actual] = palabra
98
+
99
+ logger.info("[NER] Resultado entidades: %s", entidades)
100
+ return entidades
requirements.txt ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi==0.110.0
2
+ uvicorn[standard]==0.29.0
3
+ python-dotenv==1.0.1
4
+
5
+ supabase==2.4.3
6
+
7
+ transformers==4.38.2
8
+ huggingface-hub==0.20.3
9
+ sentence-transformers==2.2.2
10
+ setfit==1.0.3
11
+
12
+ torch==2.0.1
13
+ numpy
14
+ pydantic>=1.10
15
+ requests
16
+ python-multipart
17
+ accelerate