Romanes's picture
Update app.py
1e1e079 verified
# -*- coding: utf-8 -*-
import re, unicodedata
from pathlib import Path
from typing import Tuple
import gradio as gr
import joblib
import pandas as pd
from scipy import sparse
from sklearn.metrics.pairwise import cosine_similarity
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.utils.validation import check_is_fitted
# =========================
# Config
# =========================
ROOT = Path(__file__).parent
ART = ROOT / "artifacts"
VEC_PATH = ART / "tfidf_vectorizer.joblib"
MAT_PATH = ART / "tfidf_matrix.npz"
IDX_PATH = ART / "doc_index.csv"
CATALOGOS_PATH = ROOT / "CATALOGOS.xlsx"
# =========================
# Utils de texto
# =========================
def strip_accents(s: str) -> str:
return "".join(c for c in unicodedata.normalize("NFKD", s) if not unicodedata.combining(c))
STOPWORDS = {
"a","al","algo","algunas","algunos","ante","antes","aquel","aquella","aquellas","aquellos","aqui","asi","aun","aunque",
"bajo","bien","cada","casi","cierta","ciertas","cierto","ciertos","como","con","contra","cual","cuales","cualquier",
"cualesquiera","cuyo","cuya","cuyas","cuyos","de","del","desde","donde","dos","el","ella","ellas","ellos","en","entre",
"era","eran","eres","es","esa","esas","ese","eso","esos","esta","estaba","estaban","estamos","estan","estar","estas",
"este","esto","estos","fue","fueron","ha","habia","habian","haber","hay","hasta","la","las","le","les","lo","los",
"mas","mas","me","mi","mis","mucha","muchas","mucho","muchos","muy","nada","ni","no","nos","nosotras","nosotros",
"nuestra","nuestras","nuestro","nuestros","o","otra","otras","otro","otros","para","pero","poco","por","porque",
"que","quien","quienes","se","sea","sean","ser","si","si","sido","sin","sobre","su","sus","tal","tambien","tambien",
"tampoco","tan","tanta","tantas","tanto","te","tenia","tenian","tendra","tendran","tenemos","tengo","ti","tiene",
"tienen","todo","todos","tu","tus","un","una","unas","uno","unos","usted","ustedes","y","ya"
}
STOPWORDS = {strip_accents(w.lower()) for w in STOPWORDS} | {"aun"}
def clean_text(s: str) -> str:
if not isinstance(s, str): s = "" if s is None else str(s)
s = strip_accents(s.lower())
s = re.sub(r"[“”„‟‹›«»—–‐‒–—―\-]", " ", s)
s = re.sub(r"[^\w\s]", " ", s)
s = re.sub(r"\s+", " ", s).strip()
toks = [t for t in s.split() if t not in STOPWORDS and not t.isdigit()]
return " ".join(toks)
def _kw_pattern(kw_norm: str) -> str:
# "medidor ph" -> r"\bmedidor\b.*\bph\b"
parts = [re.escape(p) for p in kw_norm.split()]
if not parts: return ""
return r"\b" + r".*".join(parts) + r"\b"
def catalog_tag(source_file: str) -> str:
s = (source_file or "").lower()
if "cicp" in s: return "CICP"
if "cpc" in s: return "CPC"
if "unspsc" in s: return "UNSPSC"
return "OTRO"
# =========================
# Reglas
# =========================
REGLAS = [
# [1] REGLA: OPS (Orden de Prestación de Servicios)
{
"id": 1,
"keywords": ["ops", "orden de prestacion de servicios", "contrato ops", "prestación de servicios", "prestacion", "prestación"],
"respuesta": {
"CICP": ("2.3.2.02.02.008", "Servicios prestados a las empresas y servicios de producción"),
"CPC": ("8", "Servicios prestados a las empresas y servicios de producción"),
"UNSPSC":("80111600", "Servicios de personal temporal"),
},
"motivo": "Coincidencia con palabra clave OPS",
},
# [2] REGLA: Tiquetes aéreos (viajes)
{
"id": 2,
"keywords": ["tiquete", "tiquetes", "pasajes", "aereos", "aereo", "aéreo", "aéreos"],
"respuesta": {
"CICP": ("2.3.2.02.02.006", "Comercio y Distribución, alojamiento, servicio de suministros de comidas y bebidas, servicios de transporte y servicios de distribución de electricidad, gas y agua"),
"CPC": ("6", "Comercio y distribución; alojamiento; servicios de suministro de comidas y bebidas; servicios de transporte; y servicios de distribución de electricidad, gas y agua"),
"UNSPSC": ("78111500", "Servicios de transporte aéreo"),
},
"motivo": "Auto (tiquetes). 50 ejemplos en Excel",
},
# [3] REGLA: Viáticos (alojamiento, alimentación, transporte local)
{
"id": 3,
"keywords": ["viatico", "viaticos", "viático", "viáticos"],
"respuesta": {
"CICP": ("2.3.2.02.02.010", "Viáticos de los funcionarios en comisión"),
"CPC": ("901", "Gastos directos de la administración pública"),
"UNSPSC": ("N/A", "N/A"),
},
"motivo": "Auto (viaticos). 37 ejemplos en Excel",
},
# [4] REGLA: Inscripción a eventos/cursos
{
"id": 4,
"keywords": ["inscripcion", "inscripciones", "registro", "inscripción", "inscricion", "inscrición"],
"respuesta": {
"CICP": ("2.3.2.02.02.009", "Servicios para la comunidad, sociales y personales"),
"CPC": ("901", "Gastos directos de la administración pública"),
"UNSPSC": ("N/A", "N/A"),
},
"motivo": "Auto (inscripción). 39 ejemplos en Excel",
},
# [5] REGLA: Participación/Asistencia/Ponencia en eventos
{
"id": 5,
"keywords": ["participacion", "participación", "asistencia", "ponente", "conferencista"],
"respuesta": {
"CICP": ("2.3.2.02.02.006", "Servicios prestados a las empresas y servicios de producción"),
"CPC": ("6", "Servicios de transporte de pasajeros"),
"UNSPSC": ("78111500", "Servicios de transporte aéreo"),
},
"motivo": "Auto (participación). 35 ejemplos en Excel",
},
# [6] REGLA: Impresión y material impreso (SERVICIO)
{
"id": 6,
"keywords": ["impresion", "impresión", "imprimir", "impresiones", "material impreso", "afiches", "posters", "póster", "pósters", "folletos", "volantes", "brochure", "bróchure"],
"respuesta": {
"CICP": ("2.3.2.02.02.008", "Servicios prestados a las empresas y servicios de producción"),
"CPC": ("8", "Servicios prestados a las empresas y servicios de producción"),
"UNSPSC": ("82121500", "Servicios de impresión"),
},
"motivo": "Auto (impresión). 16 ejemplos en Excel",
},
# [7] REGLA: Publicación científica (APC/Open Access)
{
"id": 7,
"keywords": ["publicacion", "publicación", "article processing charge", "apc", "cuota publicacion", "edicion articulo", "edición artículo", "open access"],
"respuesta": {
"CICP": ("2.3.2.02.02.008", "Servicios prestados a las empresas y servicios de producción"),
"CPC": ("8", "Servicios editoriales"),
"UNSPSC": ("82121800", "Servicios editoriales y de publicación"),
},
"motivo": "Auto (publicación). 37 ejemplos en Excel",
},
# [8] REGLA: Servicios técnicos / Ensayos / Caracterización (Laboratorio)
{
"id": 8,
"keywords": ["servicios tecnicos", "servicios técnicos", "servicio tecnico", "análisis de laboratorio", "analisis de laboratorio", "caracterizacion", "caracterización", "ensayo", "ensáyo"],
"respuesta": {
"CICP": ("2.3.2.02.02.008", "Servicios prestados a las empresas y servicios de producción"),
"CPC": ("8", "Servicios técnicos y de apoyo"),
"UNSPSC": ("81101703", "Servicios de análisis y ensayo de laboratorio"),
},
"motivo": "Auto (servicios técnicos). 7 ejemplos en Excel",
},
# [9] REGLA: Mantenimiento / Calibración
{
"id": 9,
"keywords": ["mantenimiento", "mantenimientos", "preventivo", "correctivo", "calibracion", "calibración", "calibraciones"],
"respuesta": {
"CICP": ("2.3.2.02.02.008", "Servicios de mantenimiento y reparación"),
"CPC": ("8", "Servicios de mantenimiento preventivo y correctivo"),
"UNSPSC": ("80111600", "Servicios de soporte técnico o mantenimiento"),
},
"motivo": "Auto (mantenimiento). 2 ejemplos en Excel",
},
# [10] REGLA: Software / Suscripciones (SERVICIO)
{
"id": 10,
"keywords": ["software", "licenciamiento", "suscripcion", "suscripciones", "suscripción", "sistemas", "suite"],
"respuesta": {
"CICP": ("2.3.2.02.02.008", "Servicios prestados a las empresas y servicios de producción"),
"CPC": ("8", "Servicios de desarrollo y licencias de software"),
"UNSPSC": ("81112500", "Servicios de software o licencias informáticas"),
},
"motivo": "Auto (software). 17 ejemplos en Excel",
},
# [11] REGLA: Licencia (genérico) (SERVICIO)
{
"id": 11,
"keywords": ["licencia", "licencias", "licenciamiento"],
"respuesta": {
"CICP": ("2.3.2.02.02.008", "Servicios prestados a las empresas y servicios de producción"),
"CPC": ("8", "Servicios de licenciamiento"),
"UNSPSC": ("80111600", "Servicios de personal temporal"),
},
"motivo": "Auto (licencia). 4 ejemplos en Excel",
},
# [12] REGLA: Reactivos / Insumos de laboratorio (BIEN)
{
"id": 12,
"keywords": ["reactivos", "insumos de laboratorio", "quimicos", "químicos", "quimicas", "químicas", "repuestos laboratorio"],
"respuesta": {
"CICP": ("2.3.2.02.01.003", "Productos químicos y reactivos"),
"CPC": ("3", "Productos químicos y reactivos de laboratorio"),
"UNSPSC": ("41000000", "Equipos y suministros de laboratorio"),
},
"motivo": "Auto (reactivos). 14 ejemplos en Excel",
},
# [13] REGLA: Espectrometría / HPLC / RMN (SERVICIO)
{
"id": 13,
"keywords": ["espectrometria", "espectrometría", "hplc", "q tof", "qtof", "lc ms", "gc ms", "nmr", "rmn", "tiempo de vuelo", "masas"],
"respuesta": {
"CICP": ("2.3.2.02.02.008", "Servicios prestados a las empresas y servicios de producción"),
"CPC": ("8", "Servicios técnicos o de laboratorio"),
"UNSPSC": ("81101703", "Servicios de análisis espectrométrico o químico"),
},
"motivo": "Auto (espectrometría). 6 ejemplos en Excel",
},
# [14] REGLA: Equipo de cómputo (BIEN)
{
"id": 14,
"keywords": ["equipo de computo", "equipos de computo", "computo", "computador", "computadora", "pc", "desktop", "torre", "cpu", "hardware", "all in one", "aio", "portatil", "portátil", "laptop", "notebook", "ultrabook", "adquisic", "compra"],
"respuesta": {
"CICP": ("2.3.2.02.01.003", "Otros bienes transportables (excepto productos metálicos, maquinaria y equipo)"),
"CPC": ("3", "Equipo de cómputo y partes (bienes)"),
"UNSPSC": ("43211500", "Computadoras personales"),
},
"motivo": "Auto (equipo de cómputo). ~54 ejemplos en Excel; excluye impresoras y periféricos",
},
# [15] REGLA: Honorarios / Servicios profesionales (sin OPS) (SERVICIO)
{
"id": 15,
"keywords": ["honorarios", "contratar profesional", "profesional independiente", "asesoria", "asesoría", "consultoria", "consultoría", "servicios profesionales"],
"respuesta": {
"CICP": ("2.3.2.02.02.008", "Servicios prestados a las empresas y servicios de producción"),
"CPC": ("8", "Servicios técnicos y profesionales"),
"UNSPSC": ("80111600", "Servicios de personal temporal"),
},
"motivo": "Honorarios/servicios profesionales sin mención explícita de OPS",
},
# [16] REGLA: Transporte TERRESTRE de pasajeros (SERVICIO)
{
"id": 16,
"keywords": ["transporte terrestre", "terrestre", "bus", "autobus", "autobús"],
"respuesta": {
"CICP": ("2.3.2.02.02.006", "Servicios prestados a las empresas y servicios de producción"),
"CPC": ("6", "Servicios de transporte de pasajeros"),
"UNSPSC": ("78111800", "Servicios de transporte terrestre"),
},
"motivo": "Traslados terrestres a eventos/misiones",
},
# [17] REGLA: Alojamiento / Hospedaje (CPC 901 -> UNSPSC N/A)
{
"id": 17,
"keywords": ["hospedaje", "alojamiento", "hotel"],
"respuesta": {
"CICP": ("2.3.2.02.02.010", "Servicios administrativos de apoyo"),
"CPC": ("901", "Gastos directos de la administración pública"),
"UNSPSC": ("N/A", "N/A"),
},
"motivo": "Hospedaje asociado a comisiones y eventos",
},
# [18] REGLA: Papelería y útiles (BIEN)
{
"id": 18,
"keywords": ["papeleria", "papelería", "papel", "resma", "resmas", "cuaderno", "cuadernos"],
"respuesta": {
"CICP": ("2.3.2.02.01.003", "Otros bienes transportables"),
"CPC": ("3", "Suministros y papelería (bienes)"),
"UNSPSC": ("14111500", "Papel de oficina"),
},
"motivo": "Papelería/consumibles generales",
},
# [19] REGLA: Tóner y consumibles de impresión (BIEN)
{
"id": 19,
"keywords": ["toner", "tóner", "cartucho", "cartuchos"],
"respuesta": {
"CICP": ("2.3.2.02.01.003", "Otros bienes transportables"),
"CPC": ("3", "Consumibles de impresión (bienes)"),
"UNSPSC": ("44103100", "Consumibles para impresoras"),
},
"motivo": "Consumibles para impresión",
},
# [20] REGLA: Impresoras / Multifuncionales (BIEN)
{
"id": 20,
"keywords": ["impresora", "impresoras", "multifuncional", "multifuncionales"],
"respuesta": {
"CICP": ("2.3.2.02.01.004", "Productos metálicos, maquinaria y equipo"),
"CPC": ("4", "Maquinaria y equipo (bienes)"),
"UNSPSC": ("43212100", "Impresoras y periféricos de impresión"),
},
"motivo": "Adquisición de impresoras como bienes",
},
# [21] REGLA: Periféricos de cómputo (BIEN)
{
"id": 21,
"keywords": ["monitor", "monitores", "teclado", "teclados", "mouse", "periferico", "periférico", "perifericos", "periféricos"],
"respuesta": {
"CICP": ("2.3.2.02.01.004", "Productos metálicos, maquinaria y equipo"),
"CPC": ("4", "Equipo y periféricos de cómputo (bienes)"),
"UNSPSC": ("43211600", "Accesorios/periféricos de computadora"),
},
"motivo": "Periféricos y accesorios de TI como bienes",
},
# [22] REGLA: Equipo de laboratorio (BIEN)
{
"id": 22,
"keywords": ["equipo de laboratorio", "equipos de laboratorio", "microscopio", "balanza", "centrifuga", "centrífuga", "centrifugo", "espectrofotometro", "espectrofotómetro", "espectrofotometros", "espectrofotómetros"],
"respuesta": {
"CICP": ("2.3.2.02.01.004", "Productos metálicos, maquinaria y equipo"),
"CPC": ("4", "Equipo científico/laboratorio (bienes)"),
"UNSPSC": ("41110000", "Equipos científicos y de laboratorio"),
},
"motivo": "Adquisición de equipo científico como bien",
},
# [23] REGLA: Mensajería y envíos (courier) (SERVICIO)
{
"id": 23,
"keywords": ["envio", "envíos", "envio", "mensajeria", "mensajería", "paqueteria", "paquetería", "courier"],
"respuesta": {
"CICP": ("2.3.2.02.02.006", "Servicios prestados a las empresas y servicios de producción"),
"CPC": ("6", "Servicios de mensajería y correo"),
"UNSPSC": ("80131500", "Servicios de mensajería/courier"),
},
"motivo": "Logística de envíos de documentos/paquetes",
},
# [24] REGLA: Capacitación / Formación (CPC 901 -> UNSPSC N/A)
{
"id": 24,
"keywords": ["capacitacion", "capacitación", "formacion", "formación", "curso", "cursos", "seminario", "seminarios", "taller", "talleres"],
"respuesta": {
"CICP": ("2.3.2.02.02.009", "Servicios para la comunidad"),
"CPC": ("901", "Gastos directos de la administración pública"),
"UNSPSC": ("N/A", "N/A"),
},
"motivo": "Servicios de formación/capacitación distintos al ítem de inscripción",
},
# [25] REGLA: Mobiliario de oficina (BIEN)
{
"id": 25,
"keywords": ["mobiliario", "mueble", "muebles", "silla", "sillas", "mesa", "mesas", "escritorio", "escritorios"],
"respuesta": {
"CICP": ("2.3.2.02.01.003", "Otros bienes transportables"),
"CPC": ("3", "Mobiliario de oficina (bienes)"),
"UNSPSC": ("56100000", "Mobiliario de oficina"),
},
"motivo": "Compra de muebles y dotación",
},
# [26] REGLA: Producción audiovisual / video (SERVICIO)
{
"id": 26,
"keywords": ["video", "vídeo", "produccion audiovisual", "producción audiovisual", "grabacion", "grabación", "edicion de video", "edición de video"],
"respuesta": {
"CICP": ("2.3.2.02.02.008", "Servicios prestados a las empresas y servicios de producción"),
"CPC": ("8", "Servicios creativos y de medios"),
"UNSPSC": ("82111600", "Servicios de producción de video"),
},
"motivo": "Servicios de registro/edición/producción de contenidos",
},
# [27] REGLA: Apoyos / Auxiliares / Monitores (SERVICIO)
{
"id": 27,
"keywords": ["apoyo", "auxiliar", "auxiliares", "monitor", "monitores", "estudiante", "estudiantes"],
"respuesta": {
"CICP": ("2.3.2.02.02.008", "Servicios prestados a las empresas y servicios de producción"),
"CPC": ("8", "Servicios de apoyo y personal"),
"UNSPSC": ("80111600", "Servicios de personal temporal"),
},
"motivo": "Apoyos operativos/auxiliares vinculados por servicios",
},
# [28] REGLA: GPS portátil / navegadores GPS (BIEN)
{
"id": 28,
"keywords": ["gps", "gps portatil", "gps portátil", "navegador gps"],
"respuesta": {
"CICP": ("2.3.2.02.01.004", "Productos metálicos, maquinaria y equipo"),
"CPC": ("4", "Maquinaria y equipo (bienes)"),
"UNSPSC": ("52161500", "Sistemas de posicionamiento global (GPS)"),
},
"motivo": "Adquisición de GPS portátiles para trabajo de campo",
},
# [29] REGLA: Cámara fotográfica/digital (BIEN)
{
"id": 29,
"keywords": ["camara", "cámara", "camara digital", "cámara digital", "camara fotografica", "cámara fotográfica"],
"respuesta": {
"CICP": ("2.3.2.02.01.004", "Productos metálicos, maquinaria y equipo"),
"CPC": ("4", "Maquinaria y equipo (bienes)"),
"UNSPSC": ("45121504", "Cámaras digitales"),
},
"motivo": "Compra de cámaras para registro/producción",
},
# [30] REGLA: Sensores y módulos/placas electrónicas (BIEN)
{
"id": 30,
"keywords": ["sensor", "sensores", "modulo", "módulo", "modulos", "módulos", "arduino", "raspberry", "raspberry pi", "componentes electronicos", "componentes electrónicos", "accesorios electronicos", "accesorios electrónicos"],
"respuesta": {
"CICP": ("2.3.2.02.01.004", "Productos metálicos, maquinaria y equipo"),
"CPC": ("4", "Maquinaria y equipo (bienes)"),
"UNSPSC": ("32101700", "Módulos y conjuntos electrónicos"),
},
"motivo": "Adquisición de sensores/módulos electrónicos para prototipos e I+D",
},
# [31] REGLA: Herramientas y equipos menores de medición (BIEN)
{
"id": 31,
"keywords": ["herramienta", "herramientas", "multimetro", "multímetro", "osciloscopio", "tester"],
"respuesta": {
"CICP": ("2.3.2.02.01.003", "Otros bienes transportables"),
"CPC": ("3", "Suministros y herramientas (bienes)"),
"UNSPSC": ("27110000", "Herramientas manuales"),
},
"motivo": "Dotación de herramientas y equipos menores para laboratorios y campo",
},
# [32] REGLA: Proceso editorial/libros (SERVICIO editorial)
{
"id": 32,
"keywords": ["proceso editorial", "publicacion de libro", "publicación de libro", "edicion de libro", "edición de libro", "libros resultado de investigacion", "libros resultado de investigación"],
"respuesta": {
"CICP": ("2.3.2.02.02.008", "Servicios prestados a las empresas y servicios de producción"),
"CPC": ("8", "Servicios editoriales"),
"UNSPSC": ("82121800", "Servicios editoriales y de publicación"),
},
"motivo": "Gastos de gestión editorial distintos de la impresión física",
},
# [33] REGLA: Suministros / Materiales e insumos (genérico BIEN)
{
"id": 33,
"keywords": ["suministros", "materiales", "insumos", "dotacion", "dotación"],
"respuesta": {
"CICP": ("2.3.2.02.01.003", "Otros bienes transportables"),
"CPC": ("3", "Suministros y materiales (bienes)"),
"UNSPSC": ("11111500", "Suministros y materiales varios"),
},
"motivo": "Cobertura genérica para 'materiales/insumos/suministros'",
},
# [34] REGLA: Unidades de disco / almacenamiento (BIEN)
{
"id": 34,
"keywords": ["disco duro", "unidad de estado solido", "unidad estado solido", "ssd", "hdd", "discos duros", "unidades de disco"],
"respuesta": {
"CICP": ("2.3.2.02.01.003", "Otros bienes transportables"),
"CPC": ("3", "Equipo de cómputo y partes (bienes)"),
"UNSPSC": ("43201800", "Unidades y almacenamiento de datos"),
},
"motivo": "Adquisición de HDD/SSD/almacenamiento",
},
# [35] REGLA: UPS / Reguladores de voltaje (BIEN)
{
"id": 35,
"keywords": ["ups", "no break", "nobreak", "regulador de voltaje", "reguladores de voltaje"],
"respuesta": {
"CICP": ("2.3.2.02.01.004", "Productos metálicos, maquinaria y equipo"),
"CPC": ("4", "Maquinaria y equipo (bienes)"),
"UNSPSC": ("39121000", "Acondicionadores de energía y UPS"),
},
"motivo": "Protección eléctrica para equipos/laboratorio",
},
# [36] REGLA: Cables eléctricos (BIEN)
{
"id": 36,
"keywords": ["cable electrico", "cable eléctrico", "cobre calibre", "cable de cobre", "conductor electrico", "conductor eléctrico"],
"respuesta": {
"CICP": ("2.3.2.02.01.003", "Otros bienes transportables"),
"CPC": ("3", "Suministros eléctricos (bienes)"),
"UNSPSC": ("26121600", "Cables eléctricos"),
},
"motivo": "Compra de cableado/conductores eléctricos",
},
# [37] REGLA: Instrumentos de medición portátiles (pH-metro, etc.) (BIEN)
{
"id": 37,
"keywords": ["medidor de ph", "phmetro", "ph-metro", "potenciómetro", "medidor portatil", "medidor portátil"],
"respuesta": {
"CICP": ("2.3.2.02.01.004", "Productos metálicos, maquinaria y equipo"),
"CPC": ("4", "Equipo científico/laboratorio (bienes)"),
"UNSPSC": ("41115600", "Instrumentos de medición y análisis"),
},
"motivo": "Instrumentos portátiles de laboratorio (pH, etc.)",
},
# [38] REGLA: Estación meteorológica / agroclimática (BIEN)
{
"id": 38,
"keywords": ["estacion meteorologica", "estación meteorológica", "estacion agroclimatica", "estación agroclimática", "datalogger meteorologico"],
"respuesta": {
"CICP": ("2.3.2.02.01.004", "Productos metálicos, maquinaria y equipo"),
"CPC": ("4", "Equipo científico/laboratorio (bienes)"),
"UNSPSC": ("41111931", "Estaciones meteorológicas"),
},
"motivo": "Monitoreo ambiental/agroclimático",
},
# [39] REGLA: Maletín/estuche protector para equipos (BIEN)
{
"id": 39,
"keywords": ["maletin", "maletín", "estuche protector", "case rigido", "maletin rigido", "estuche rigido"],
"respuesta": {
"CICP": ("2.3.2.02.01.003", "Otros bienes transportables"),
"CPC": ("3", "Accesorios y estuches (bienes)"),
"UNSPSC": ("24112400", "Estuches/maletines protectores"),
},
"motivo": "Transporte/almacenamiento seguro de equipos",
},
# [40] REGLA: Camisas/camisetas corporativas (BIEN)
{
"id": 40,
"keywords": ["camisas corporativas","dotación", "camisas","camisetas corporativas", "uniformes", "camisas bordadas", "camisetas bordadas"],
"respuesta": {
"CICP": ("2.3.2.02.01.003", "Otros bienes transportables"),
"CPC": ("3", "Textiles y prendas (bienes)"),
"UNSPSC": ("80111603", "Necesidades de dotación de personal de producción temporal"),
},
"motivo": "Dotación/identidad visual para eventos y labores de campo",
},
# [41] REGLA: Material bibliográfico / Libros impresos (BIEN)
{
"id": 41,
"keywords": ["material bibliografico", "material bibliográfico", "libros", "libro impreso", "material bibliografico impresos"],
"respuesta": {
"CICP": ("2.3.2.02.01.003", "Otros bienes transportables"),
"CPC": ("3", "Material bibliográfico (bienes)"),
"UNSPSC": ("55101504", "Libros"),
},
"motivo": "Compra de libros/material bibliográfico impreso",
},
# [42] REGLA: Transporte local / genérico (TERRESTRE) (SERVICIO)
{
"id": 42,
"keywords": ["transporte", "desplazamientos", "movilizacion", "movilización", "taxis", "taxi", "transportes"],
"respuesta": {
"CICP": ("2.3.2.02.02.006", "Servicios prestados a las empresas y servicios de producción"),
"CPC": ("6", "Servicios de transporte de pasajeros"),
"UNSPSC": ("78111800", "Servicios de transporte terrestre"),
},
"motivo": "Traslados locales (taxi/transporte terrestre genérico)",
},
# [43] REGLA: Servicios profesionales (alias amplio, sin OPS)
{
"id": 43,
"keywords": ["servicios profesionales", "contratacion de personal", "contratación de personal", "personal capacitado", "apoyo profesional", "apoyar la redaccion", "apoyar la redacción", "supervision", "supervisión", "profesional investigador", "prestación"],
"respuesta": {
"CICP": ("2.3.2.02.02.008", "Servicios prestados a las empresas y servicios de producción"),
"CPC": ("8", "Servicios técnicos y profesionales"),
"UNSPSC": ("80111600", "Servicios de personal temporal"),
},
"motivo": "Cobertura de textos de contratación de profesionales sin mención 'OPS'",
},
# [44] REGLA: Análisis de datos / Ciencia de datos / ML (SERVICIO)
{
"id": 44,
"keywords": ["analisis de datos", "análisis de datos", "mineria de datos", "minería de datos", "machine learning", "aprendizaje automatico", "aprendizaje automático"],
"respuesta": {
"CICP": ("2.3.2.02.02.008", "Servicios prestados a las empresas y servicios de producción"),
"CPC": ("8", "Servicios técnicos y de apoyo"),
"UNSPSC": ("81161500", "Servicios de análisis y procesamiento de datos"),
},
"motivo": "Servicios especializados de analítica/ML",
},
# [45] REGLA: Alimento para animales / Pollos (BIEN)
# CUÁNDO: "alimento para pollos", "concentrado", "alimento para animales", "balanceado", "gallinas ponedoras", "engorde"
{
"id": 45,
"keywords": ["alimento para pollos", "alimento para animales", "concentrado", "concentrado para pollos", "balanceado", "concentrado avicola", "concentrado avícola", "gallinas ponedoras", "pollo engorde", "alimento avicola", "alimento avícola"],
"respuesta": {
"CICP": ("2.3.2.02.01.002", "Productos alimenticios, bebidas y tabaco; textiles, prendas de vestir y productos de cuero"),
"CPC": ("2", "Productos alimenticios, bebidas y tabaco; textiles, prendas de vestir y productos de cuero"),
"UNSPSC": ("10120000", "Comida de animales"),
},
"motivo": "Compra de alimento/concentrado para pollos u otros animales",
},
# [46] REGLA: Combustible y lubricantes (BIEN)
# CUÁNDO: gasolina, diésel/ACPM, combustible, lubricantes
{
"id": 46,
"keywords": ["combustible", "gasolina", "diesel", "diésel", "acpm", "lubricante", "lubricantes"],
"respuesta": {
"CICP": ("2.3.2.02.01.003", "Otros bienes transportables"),
"CPC": ("3", "Suministros (bienes)"),
"UNSPSC": ("15101500", "Combustibles"),
},
"motivo": "Abastecimiento de combustible y lubricantes para misiones/equipos",
},
# [47] REGLA: Refrigerios / Catering para eventos (SERVICIO)
# CUÁNDO: refrigerios, alimentación de evento, coffee break, catering
{
"id": 47,
"keywords": ["refrigerios", "refrigerio", "coffee break", "catering", "alimentacion evento", "alimentación evento", "servicio de alimentación"],
"respuesta": {
"CICP": ("2.3.2.02.02.008", "Servicios prestados a las empresas y servicios de producción"),
"CPC": ("8", "Servicios de alimentos para eventos"),
"UNSPSC": ("90101600", "Servicios de catering"),
},
"motivo": "Atención alimentaria en eventos/capacitaciones",
},
# [48] REGLA: Avances
# CUÁNDO: avances, progreso, desarrollo
{
"id": 48,
"keywords": ["avances", "progreso", "desarrollo"],
"respuesta": {
"CICP": ("2.3.2.02.02.008", "Servicios prestados a las empresas y servicios de producción"),
"CPC": ("901", "Gastos directos de la administración pública"),
"UNSPSC": ("N/A", "N/A"),
},
"motivo": "Atención alimentaria en eventos/capacitaciones",
},
]
def aplicar_reglas(query: str):
texto = clean_text(query)
for r in REGLAS:
for kw in r["keywords"]:
kw_norm = clean_text(kw)
if not kw_norm: continue
pat = _kw_pattern(kw_norm)
if re.search(pat, texto):
tmp = pd.DataFrame(
[{"Catálogo": k, "Código": v[0], "Nombre": v[1], "Similaridad": 1.0}
for k, v in r["respuesta"].items()]
)
return tmp, f"⚙️ Regla activada: {r['motivo']}"
return None, None
# =========================
# Parsing de códigos (robusto, mismo que search_tfidf.py)
# =========================
ORDER_CATS = ["CICP", "CPC", "UNSPSC"]
def _s(x) -> str:
"""string seguro ('' si None/NaN)"""
try:
if x is None: return ""
if isinstance(x, float) and x != x: # NaN
return ""
return str(x)
except Exception:
return "" if x is None else str(x)
def parse_code_name(catalogo: str, codes_raw, text_original) -> Tuple[str,str]:
cat = _s(catalogo).strip().upper()
cr = _s(codes_raw)
to = _s(text_original)
if cat == "UNSPSC":
m = re.search(r"UNSPSC:\s*([^;]+)\s*;\s*(.+)", cr, flags=re.I)
if m: return m.group(1).strip(), m.group(2).strip()
if cat == "CPC":
m = re.search(r"CPC:\s*([^;]+)\s*;\s*(.+)", cr, flags=re.I)
if m: return m.group(1).strip(), m.group(2).strip()
if cat == "CICP":
code = None
m1 = re.search(r"CODIGO:\s*([^\s\|;]+)", cr, flags=re.I)
if m1: code = m1.group(1).strip()
name = None
m2 = re.search(r"CICP:\s*([^|]+)$", to, flags=re.I)
if m2: name = m2.group(1).strip()
if code or name:
return _s(code).strip(), _s(name).strip()
# Fallback genérico
if ";" in cr:
parts = [p.strip() for p in cr.split(";", 2)]
if len(parts) >= 2:
return parts[-2], parts[-1]
return cr.strip(), (to if to else cr).strip()
def normalize_unspsc_if_cpc_901(rows):
"""Si el CPC seleccionado es 901, fuerza UNSPSC=N/A."""
out = []
cpc_is_901 = any(r["Catálogo"]=="CPC" and str(r["Código"]).strip()=="901" for r in rows)
for r in rows:
if r["Catálogo"]=="UNSPSC" and cpc_is_901:
out.append({"Catálogo":"UNSPSC","Código":"N/A","Nombre":"N/A","Similaridad":1.0})
else:
out.append(r)
return out
def order_and_one_per_catalog(df_like):
"""Top-1 por catálogo + orden CICP→CPC→UNSPSC + normalización 901."""
df = pd.DataFrame(df_like)
best = (df.sort_values("Similaridad", ascending=False)
.groupby("Catálogo", as_index=False)
.head(1))
rows = [{"Catálogo": r["Catálogo"], "Código": r["Código"], "Nombre": r["Nombre"],
"Similaridad": r["Similaridad"]} for _, r in best.iterrows()]
rows = normalize_unspsc_if_cpc_901(rows)
have = {r["Catálogo"] for r in rows}
for cat in ORDER_CATS:
if cat not in have:
rows.append({"Catálogo":cat,"Código":"", "Nombre":"", "Similaridad":0.0})
rows.sort(key=lambda r: ORDER_CATS.index(r["Catálogo"]))
return pd.DataFrame(rows, columns=["Catálogo","Código","Nombre","Similaridad"])
# =========================
# Carga/entrenamiento TF-IDF (como app (2).py)
# =========================
VECTOR = None
MATRIX = None
INDEX = None
def _is_fitted_vectorizer(vec) -> bool:
try:
check_is_fitted(vec, attributes=["vocabulary_"])
check_is_fitted(vec._tfidf, attributes=["idf_"])
return True
except Exception:
return False
def _train_and_persist_from_index(index_df: pd.DataFrame):
corpus = (index_df["tokens_lemmatized"]
if "tokens_lemmatized" in index_df.columns
else index_df["text_original"].fillna("").astype(str).map(clean_text))
vec = TfidfVectorizer(analyzer="word", token_pattern=r"(?u)\b\w+\b",
min_df=1, max_df=0.9, ngram_range=(1,2),
sublinear_tf=True, norm="l2")
X = vec.fit_transform(list(corpus))
ART.mkdir(exist_ok=True, parents=True)
joblib.dump(vec, VEC_PATH); sparse.save_npz(MAT_PATH, X)
return vec, X
def ensure_loaded():
global VECTOR, MATRIX, INDEX
if INDEX is None:
INDEX = pd.read_csv(IDX_PATH)
vec = joblib.load(VEC_PATH) if VEC_PATH.exists() else None
X = sparse.load_npz(MAT_PATH) if MAT_PATH.exists() else None
if vec is None or not _is_fitted_vectorizer(vec):
vec, X = _train_and_persist_from_index(INDEX)
elif X is None:
corpus = (INDEX["tokens_lemmatized"]
if "tokens_lemmatized" in INDEX.columns
else INDEX["text_original"].fillna("").astype(str).map(clean_text))
X = vec.transform(list(corpus))
sparse.save_npz(MAT_PATH, X)
VECTOR, MATRIX = vec, X
# =========================
# Búsqueda
# =========================
def recomendar(query: str):
# 1) Reglas
df_regla, motivo = aplicar_reglas(query)
if df_regla is not None:
df_out = order_and_one_per_catalog(df_regla)
return df_out, motivo
# 2) Modelo
ensure_loaded()
q = clean_text(query)
if not q:
return pd.DataFrame(), "La consulta quedó vacía tras limpieza."
xq = VECTOR.transform([q])
sims = cosine_similarity(xq, MATRIX).flatten()
df = INDEX.copy()
df["Similaridad"] = sims
df["Catálogo"] = df["source_file"].apply(catalog_tag)
# Evitar NaN antes del parser
if "codes_raw" in df.columns: df["codes_raw"] = df["codes_raw"].fillna("")
if "text_original" in df.columns: df["text_original"] = df["text_original"].fillna("")
parsed = df.apply(lambda r: parse_code_name(r["Catálogo"], r.get("codes_raw",""), r.get("text_original","")), axis=1)
df["Código"] = [c for c,_ in parsed]
df["Nombre"] = [n for _,n in parsed]
df = df[["Catálogo","Código","Nombre","Similaridad"]]
df_out = order_and_one_per_catalog(df)
return df_out, "OK"
# =========================
# Exportar (xlsx con fallback a csv)
# =========================
def exportar(query: str):
df, _ = recomendar(query)
if df is None or df.empty:
df = pd.DataFrame(columns=["Catálogo","Código","Nombre","Similaridad"])
try:
path = "/tmp/busqueda.xlsx"
with pd.ExcelWriter(path, engine="openpyxl") as w:
df.to_excel(w, index=False, sheet_name="Resultados")
return path, "Archivo Excel (.xlsx) generado."
except Exception:
try:
import xlsxwriter # noqa: F401
path = "/tmp/busqueda.xlsx"
with pd.ExcelWriter(path, engine="xlsxwriter") as w:
df.to_excel(w, index=False, sheet_name="Resultados")
return path, "Archivo Excel (.xlsx) generado (xlsxwriter)."
except Exception:
path = "/tmp/busqueda.csv"
df.to_csv(path, index=False)
return path, "openpyxl/xlsxwriter no disponibles: se generó CSV."
# =========================
# UI (Gradio)
# =========================
with gr.Blocks(title="Recomendador de Códigos (CICP / CPC / UNSPSC)") as demo:
gr.Markdown("# FinCode - Recomendador de Códigos (CICP / CPC / UNSPSC)")
query = gr.Textbox(
label="Descripción técnica",
placeholder="reactivos de laboratorio para cromatografía hplc",
lines=3
)
with gr.Row():
btn = gr.Button("Buscar", variant="primary")
btn_xlsx = gr.Button("Descargar búsqueda")
out = gr.Dataframe(headers=["Catálogo","Código","Nombre","Similaridad"], label="Resultados", wrap=True)
status = gr.Markdown()
file_out = gr.File(label="Archivo generado", interactive=False)
# --- NUEVO: descarga del catálogo oficial + texto descriptivo ---
gr.Markdown("**Para más información, consultar los catálogos.**")
gr.DownloadButton(label="📥 Descargar CATALOGOS.xlsx", value=str(CATALOGOS_PATH), variant="secondary")
# ----------------------------------------------------------------
def _on_search(q):
df, msg = recomendar(q)
return df, (f"**Estado:** {msg}" if msg else "")
def _on_download(q):
path, info = exportar(q)
return path, f"**Descarga:** {info}"
btn.click(_on_search, inputs=[query], outputs=[out, status])
query.submit(_on_search, inputs=[query], outputs=[out, status])
btn_xlsx.click(_on_download, inputs=[query], outputs=[file_out, status])
if __name__ == "__main__":
demo.launch()