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