Spaces:
Running
Running
| # -*- 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() | |